Randomness of Python's random
Think of it like this: a gutter is perceptible until it is obstructed (or almost so). This only happens when two successive lines are almost completely out of phase (with the black segments in the first line lying nearly above the white segments in the next). Such extreme situations only happens about one out of every 10 rows, hence the visible gutters which seem to extend around 10 rows before being obstructed.
Looked at another way -- if you print out the image, there really are longish white channels through which you can easily draw a line with a pen. Why should your mind not perceive them?
To get better visual randomness, find a way to make successive lines dependent rather than independent in such a way that the almost-out-of-phase behavior appears more often.
There's at least one obvious reason why we see a pattern in the "random" picture : the 400x400 pixels are just the same 20x400 pixels repeated 20 times.
So every apparent movement is repeated 20 times in parallel, which really helps the brain analyzing the picture.
Actually, the same 10px wide pattern is repeated 40 times, alternating between black and white:
You could randomize the dash period separately for each line (e.g. between 12 and 28):
Here's the corresponding code :
import numpy as np
import random
from matplotlib import pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = [13, 13]
N = 400
def random_pixels(width, height):
return np.random.rand(height, width) < 0.5
def display(table):
plt.imshow(table, cmap='Greys', interpolation='none')
plt.show()
display(random_pixels(N, N))
def stripes(width, height, stripe_width):
table = np.zeros((height, width))
cycles = width // (stripe_width * 2) + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
for i in range(height):
table[i] = np.tile(pattern, cycles)[:width]
return table
display(stripes(N, N, 10))
def shifted_stripes(width, height, stripe_width):
table = np.zeros((height, width))
period = stripe_width * 2
cycles = width // period + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
for i in range(height):
table[i] = np.roll(np.tile(pattern, cycles), random.randrange(0, period))[:width]
return table
display(shifted_stripes(N, N, 10))
def flexible_stripes(width, height, average_width, delta):
table = np.zeros((height, width))
for i in range(height):
stripe_width = random.randint(average_width - delta, average_width + delta)
period = stripe_width * 2
cycles = width // period + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
table[i] = np.roll(np.tile(pattern, cycles), random.randrange(0, period))[:width]
return table
display(flexible_stripes(N, N, 10, 4))
Posting my final solution as an answer, but please upvote others.
John Coleman has a point when he says:
To get better visual randomness, find a way to make successive lines dependent rather than independent in such a way that the almost-out-of-phase behavior appears more often.
So, finally, the best way to avoid gutters is to forego randomness and have a very fixed scheme of shifts, and one that works well is a 4-phase 0,25%,75%,50% cycle:
OK, there is still slight diamond pattern, but it is much less visible than the patterns introduced by the random schemes I tried.