Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # wave_function_collapse.py
- # ZZZ? I did not verify
- from collections import Counter
- from itertools import chain, izip
- import math
- d = 20 # dimensions of output (array of dxd cells)
- N = 3 # dimensions of a pattern (NxN matrix)
- Output = [120 for i in xrange(d*d)] # array holding the color value for each cell in the output (at start each cell is grey = 120)
- def setup():
- size(800, 800, P2D)
- textSize(11)
- global W, H, A, freqs, patterns, directions, xs, ys, npat
- img = loadImage('Flowers.png') # path to the input image
- iw, ih = img.width, img.height # dimensions of input image
- xs, ys = width//d, height//d # dimensions of cells (squares) in output
- kernel = [[i + n*iw for i in xrange(N)] for n in xrange(N)] # NxN matrix to read every patterns contained in input image
- directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # (x, y) tuples to access the 4 neighboring cells of a collapsed cell
- all = [] # array list to store all the patterns found in input
- # Stores the different patterns found in input
- for y in xrange(ih):
- for x in xrange(iw):
- ''' The one-liner below (cmat) creates a NxN matrix with (x, y) being its top left corner.
- This matrix will wrap around the edges of the input image.
- The whole snippet reads every NxN part of the input image and store the associated colors.
- Each NxN part is called a 'pattern' (of colors). Each pattern can be rotated or flipped (not mandatory). '''
- cmat = [[img.pixels[((x+n)%iw)+(((a[0]+iw*y)/iw)%ih)*iw] for n in a] for a in kernel]
- '''
- # ???
- # Storing rotated patterns (90°, 180°, 270°, 360°)
- for r in xrange(4):
- cmat = zip(*cmat[::-1]) # +90° rotation
- all.append(cmat)
- # Storing reflected patterns (vertical/horizontal flip)
- all.append([a[::-1] for a in cmat])
- '''
- all.append(cmat[::-1])
- # Flatten pattern matrices + count occurences
- ''' Once every pattern has been stored,
- - we flatten them (convert to 1D) for convenience
- - count the number of occurences for each one of them (one pattern can be found multiple times in input)
- - select unique patterns only
- - store them from less common to most common (needed for weighted choice)'''
- all = [tuple(chain.from_iterable(p)) for p in all] # flattern pattern matrices (NxN --> [])
- c = Counter(all)
- freqs = sorted(c.values()) # number of occurences for each unique pattern, in sorted order
- npat = len(freqs) # number of unique patterns
- total = sum(freqs) # sum of frequencies of unique patterns
- patterns = [p[0] for p in c.most_common()[:-npat-1:-1]] # list of unique patterns sorted from less common to most common
- # Computes entropy
- ''' The entropy of a cell is correlated to the number of possible patterns that cell holds.
- The more a cell has valid patterns (set to 'True'), the higher its entropy is.
- At start, every pattern is set to 'True' for each cell. So each cell holds the same high entropy value'''
- ent = math.log(total) - sum(map(lambda x: x * math.log(x), freqs)) / total
- # Initializes the 'wave' (W), entropy (H) and adjacencies (A) array lists
- W = [[True for _ in xrange(npat)] for i in xrange(d*d)] # every pattern is set to 'True' at start, for each cell
- H = [ent for i in xrange(d*d)] # same entropy for each cell at start (every pattern is valid)
- A = [[set() for dir in xrange(len(directions))] for i in xrange(npat)] #see below for explanation
- # Compute patterns compatibilities (check if some patterns are adjacent, if so -> store them based on their location)
- ''' EXAMPLE:
- If pattern index 42 can placed to the right of pattern index 120,
- we will store this adjacency rule as follow:
- A[120][1].add(42)
- Here '1' stands for 'right' or 'East'/'E'
- 0 = left or West/W
- 1 = right or East/E
- 2 = up or North/N
- 3 = down or South/S '''
- # Comparing patterns to each other
- for i1 in xrange(npat):
- for i2 in xrange(npat):
- for dir in (0, 2):
- if compatible(patterns[i1], patterns[i2], dir):
- A[i1][dir].add(i2)
- A[i2][dir+1].add(i1)
- def compatible(p1, p2, dir):
- '''NOTE:
- what is refered as 'columns' and 'rows' here below is not really columns and rows
- since we are dealing with 1D patterns. Remember here N = 3'''
- # If the first two columns of pattern 1 == the last two columns of pattern 2
- # --> pattern 2 can be placed to the left (0) of pattern 1
- if dir == 0:
- return [n for i, n in enumerate(p1) if i%N!=2] == [n for i, n in enumerate(p2) if i%N!=0]
- # If the first two rows of pattern 1 == the last two rows of pattern 2
- # --> pattern 2 can be placed on top (2) of pattern 1
- if dir == 2:
- return p1[:6] == p2[-6:]
- def draw(): # Equivalent of a 'while' loop in Processing (all the code below will be looped over and over until all cells are collapsed)
- global H, W, grid
- ### OBSERVATION
- # Find cell with minimum non-zero entropy (not collapsed yet)
- '''Randomly select 1 cell at the first iteration (when all entropies are equal),
- otherwise select cell with minimum non-zero entropy'''
- emin = int(random(d*d)) if frameCount <= 1 else H.index(min(H))
- # Stoping mechanism
- ''' When 'H' array is full of 'collapsed' cells --> stop iteration '''
- if H[emin] == 'CONT' or H[emin] == 'collapsed':
- print 'stopped'
- noLoop()
- return
- ### COLLAPSE
- # Weighted choice of a pattern
- ''' Among the patterns available in the selected cell (the one with min entropy),
- select one pattern randomly, weighted by the frequency that pattern appears in the input image.
- With Python 2.7 no possibility to use random.choice(x, weight) so we have to hard code the weighted choice '''
- lfreqs = [b * freqs[i] for i, b in enumerate(W[emin])] # frequencies of the patterns available in the selected cell
- weights = [float(f) / sum(lfreqs) for f in lfreqs] # normalizing these frequencies
- cumsum = [sum(weights[:i]) for i in xrange(1, len(weights)+1)] # cumulative sums of normalized frequencies
- r = random(1)
- idP = sum([cs < r for cs in cumsum]) # index of selected pattern
- # Set all patterns to False except for the one that has been chosen
- W[emin] = [0 if i != idP else 1 for i, b in enumerate(W[emin])]
- # Marking selected cell as 'collapsed' in H (array of entropies)
- H[emin] = 'collapsed'
- # Storing first color (top left corner) of the selected pattern at the location of the collapsed cell
- Output[emin] = patterns[idP][0]
- ### PROPAGATION
- # For each neighbor (left, right, up, down) of the recently collapsed cell
- for dir, t in enumerate(directions):
- x = (emin%d + t[0])%d
- y = (emin/d + t[1])%d
- idN = x + y * d #index of neighbor
- # If that neighbor hasn't been collapsed yet
- if H[idN] != 'collapsed':
- # Check indices of all available patterns in that neighboring cell
- available = [i for i, b in enumerate(W[idN]) if b]
- # Among these indices, select indices of patterns that can be adjacent to the collapsed cell at this location
- intersection = A[idP][dir] & set(available)
- # If the neighboring cell contains indices of patterns that can be adjacent to the collapsed cell
- if intersection:
- # Remove indices of all other patterns that cannot be adjacent to the collapsed cell
- W[idN] = [True if i in list(intersection) else False for i in xrange(npat)]
- ### Update entropy of that neighboring cell accordingly (less patterns = lower entropy)
- # If only 1 pattern available left, no need to compute entropy because entropy is necessarily 0
- if len(intersection) == 1:
- H[idN] = '0' # Putting a str at this location in 'H' (array of entropies) so that it doesn't return 0 (float) when looking for minimum entropy (min(H)) at next iteration
- # If more than 1 pattern available left --> compute/update entropy + add noise (to prevent cells to share the same minimum entropy value)
- else:
- lfreqs = [b * f for b, f in izip(W[idN], freqs) if b]
- ent = math.log(sum(lfreqs)) - sum(map(lambda x: x * math.log(x), lfreqs)) / sum(lfreqs)
- H[idN] = ent + random(.001)
- # If no index of adjacent pattern in the list of pattern indices of the neighboring cell
- # --> mark cell as a 'contradiction'
- else:
- H[idN] = 'CONT'
- # Draw output
- ''' dxd grid of cells (squares) filled with their corresponding color.
- That color is the first (top-left) color of the pattern assigned to that cell '''
- for i, c in enumerate(Output):
- x, y = i%d, i/d
- fill(c)
- rect(x * xs, y * ys, xs, ys)
- # Displaying corresponding entropy value
- fill(0)
- text(H[i], x * xs + xs/2 - 12, y * ys + ys/2)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement