I'm using Python to generate images using dashed lines for stippling. The period of the dashing is constant, what changes is dash/space ratio. This produces something like this:
However in that image the dashing has a uniform origin and this creates unsightly vertical gutters. So I tried to randomize the origin to remove the gutters. This sort of works but there is an obvious pattern:
Wondering where this comes from I made a very simple test case with stacked dashed straight lines:
random.uniform(-10.,+10.)
(*) (after an initial random.seed()
And with added randomness:
So there is still pattern. What I don't understand is that to get a visible gutter you need to have 6 or 7 consecutive values falling in the same range (says, half the total range), which should be a 1/64 probability but seems to happen a lot more often in the 200 lines generated.
Am I misunderstanding something? Is it just our human brain which is seeing patterns where there is none? Could there be a better way to generate something more "visually random" (python 2.7, and preferably without installing anything)?
(*) partial pixels are valid in that context
Annex: the code I use (this is a Gimp script):
#!/usr/bin/env python # -*- coding: iso-8859-15 -*- # Python script for Gimp (requires Gimp 2.10) # Run on a 400x400 image to see something without having to wait too much # Menu entry is in "Test" submenu of image menubar import random,traceback from gimpfu import * def constant(minShift,maxShift): return 0 def triangle(minShift,maxShift): return random.triangular(minShift,maxShift) def uniform(minShift,maxShift): return random.uniform(minShift,maxShift) def gauss(minShift,maxShift): return random.gauss((minShift+maxShift)/2,(maxShift-minShift)/2) variants=[('Constant',constant),('Triangle',triangle),('Uniform',uniform),('Gauss',gauss)] def generate(image,name,generator): random.seed() layer=gimp.Layer(image, name, image.width, image.height, RGB_IMAGE,100, LAYER_MODE_NORMAL) image.add_layer(layer,0) layer.fill(FILL_WHITE) path=pdb.gimp_vectors_new(image,name) # Generate path, horizontal lines are 2px apart, # Start on left has a random offset, end is on the right edge right edge for i in range(1,image.height, 2): shift=generator(-10.,10.) points=[shift,i]*3+[image.width,i]*3 pdb.gimp_vectors_stroke_new_from_points(path,0, len(points),points,False) pdb.gimp_image_add_vectors(image, path, 0) # Stroke the path pdb.gimp_context_set_foreground(gimpcolor.RGB(0, 0, 0, 255)) pdb.gimp_context_set_stroke_method(STROKE_LINE) pdb.gimp_context_set_line_cap_style(0) pdb.gimp_context_set_line_join_style(0) pdb.gimp_context_set_line_miter_limit(0.) pdb.gimp_context_set_line_width(2) pdb.gimp_context_set_line_dash_pattern(2,[5,5]) pdb.gimp_drawable_edit_stroke_item(layer,path) def randomTest(image): image.undo_group_start() gimp.context_push() try: for name,generator in variants: generate(image,name,generator) except Exception as e: print e.args[0] pdb.gimp_message(e.args[0]) traceback.print_exc() gimp.context_pop() image.undo_group_end() return; ### Registration desc="Python random test" register( "randomize-test",desc,'','','','',desc,"*", [(PF_IMAGE, "image", "Input image", None),],[], randomTest,menu="<Image>/Test", ) main()
Python offers random module that can generate random numbers. These are pseudo-random number as the sequence of number generated depends on the seed. If the seeding value is same, the sequence will be the same. For example, if you use 2 as the seeding value, you will always see the following sequence.
Random integer values can be generated with the randint() function. This function takes two arguments: the start and the end of the range for the generated integer values. Random integers are generated within and including the start and end of range values, specifically in the interval [start, end].
Python has nothing that allows you to generate "truly random" or "authentic random" numbers, in the sense that they are uniformly distributed and independent of everything else (especially the latter). In any case, the distinction between "pseudorandom" and "truly random" numbers is not what applications care about.
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))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With