Advertisement
here2share

# wave_function_collapse.py

May 3rd, 2022
935
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.33 KB | None | 0 0
  1. # wave_function_collapse.py
  2. # ZZZ? I did not verify
  3.  
  4. from collections import Counter
  5. from itertools import chain, izip
  6. import math
  7.  
  8. d = 20  # dimensions of output (array of dxd cells)
  9. N = 3 # dimensions of a pattern (NxN matrix)
  10.  
  11. 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)
  12.  
  13. def setup():
  14.     size(800, 800, P2D)
  15.     textSize(11)
  16.  
  17.     global W, H, A, freqs, patterns, directions, xs, ys, npat
  18.  
  19.     img = loadImage('Flowers.png') # path to the input image
  20.     iw, ih = img.width, img.height # dimensions of input image
  21.     xs, ys = width//d, height//d # dimensions of cells (squares) in output
  22.     kernel = [[i + n*iw for i in xrange(N)] for n in xrange(N)] # NxN matrix to read every patterns contained in input image
  23.     directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # (x, y) tuples to access the 4 neighboring cells of a collapsed cell
  24.     all = [] # array list to store all the patterns found in input
  25.  
  26.  
  27.  
  28.     # Stores the different patterns found in input
  29.     for y in xrange(ih):
  30.         for x in xrange(iw):
  31.  
  32.             ''' The one-liner below (cmat) creates a NxN matrix with (x, y) being its top left corner.
  33.                This matrix will wrap around the edges of the input image.
  34.                The whole snippet reads every NxN part of the input image and store the associated colors.
  35.                Each NxN part is called a 'pattern' (of colors). Each pattern can be rotated or flipped (not mandatory). '''
  36.  
  37.  
  38.             cmat = [[img.pixels[((x+n)%iw)+(((a[0]+iw*y)/iw)%ih)*iw] for n in a] for a in kernel]
  39.  
  40.             '''
  41.             # ???
  42.             # Storing rotated patterns (90°, 180°, 270°, 360°)
  43.            for r in xrange(4):
  44.                cmat = zip(*cmat[::-1]) # +90° rotation
  45.                all.append(cmat)
  46.  
  47.            # Storing reflected patterns (vertical/horizontal flip)
  48.            all.append([a[::-1] for a in cmat])
  49.             '''
  50.             all.append(cmat[::-1])
  51.  
  52.  
  53.  
  54.  
  55.     # Flatten pattern matrices + count occurences
  56.  
  57.     ''' Once every pattern has been stored,
  58.        - we flatten them (convert to 1D) for convenience
  59.        - count the number of occurences for each one of them (one pattern can be found multiple times in input)
  60.        - select unique patterns only
  61.        - store them from less common to most common (needed for weighted choice)'''
  62.  
  63.     all = [tuple(chain.from_iterable(p)) for p in all] # flattern pattern matrices (NxN --> [])
  64.     c = Counter(all)
  65.     freqs = sorted(c.values()) # number of occurences for each unique pattern, in sorted order
  66.     npat = len(freqs) # number of unique patterns
  67.     total = sum(freqs) # sum of frequencies of unique patterns
  68.     patterns = [p[0] for p in c.most_common()[:-npat-1:-1]] # list of unique patterns sorted from less common to most common
  69.  
  70.  
  71.  
  72.     # Computes entropy
  73.  
  74.     ''' The entropy of a cell is correlated to the number of possible patterns that cell holds.
  75.        The more a cell has valid patterns (set to 'True'), the higher its entropy is.
  76.        At start, every pattern is set to 'True' for each cell. So each cell holds the same high entropy value'''
  77.  
  78.     ent = math.log(total) - sum(map(lambda x: x * math.log(x), freqs)) / total
  79.  
  80.  
  81.  
  82.     # Initializes the 'wave' (W), entropy (H) and adjacencies (A) array lists
  83.  
  84.     W = [[True for _ in xrange(npat)] for i in xrange(d*d)] # every pattern is set to 'True' at start, for each cell
  85.     H = [ent for i in xrange(d*d)] # same entropy for each cell at start (every pattern is valid)
  86.     A = [[set() for dir in xrange(len(directions))] for i in xrange(npat)] #see below for explanation
  87.  
  88.  
  89.  
  90.  
  91.     # Compute patterns compatibilities (check if some patterns are adjacent, if so -> store them based on their location)
  92.  
  93.     ''' EXAMPLE:
  94.    If pattern index 42 can placed to the right of pattern index 120,
  95.    we will store this adjacency rule as follow:
  96.  
  97.                     A[120][1].add(42)
  98.  
  99.    Here '1' stands for 'right' or 'East'/'E'
  100.  
  101.    0 = left or West/W
  102.    1 = right or East/E
  103.    2 = up or North/N
  104.    3 = down or South/S '''
  105.  
  106.     # Comparing patterns to each other
  107.     for i1 in xrange(npat):
  108.         for i2 in xrange(npat):
  109.             for dir in (0, 2):
  110.                 if compatible(patterns[i1], patterns[i2], dir):
  111.                     A[i1][dir].add(i2)
  112.                     A[i2][dir+1].add(i1)
  113.  
  114.  
  115. def compatible(p1, p2, dir):
  116.  
  117.     '''NOTE:
  118.    what is refered as 'columns' and 'rows' here below is not really columns and rows
  119.    since we are dealing with 1D patterns. Remember here N = 3'''
  120.  
  121.     # If the first two columns of pattern 1 == the last two columns of pattern 2
  122.     # --> pattern 2 can be placed to the left (0) of pattern 1
  123.     if dir == 0:
  124.         return [n for i, n in enumerate(p1) if i%N!=2] == [n for i, n in enumerate(p2) if i%N!=0]
  125.  
  126.     # If the first two rows of pattern 1 == the last two rows of pattern 2
  127.     # --> pattern 2 can be placed on top (2) of pattern 1
  128.     if dir == 2:
  129.         return p1[:6] == p2[-6:]
  130.  
  131.  
  132.  
  133. def draw():    # Equivalent of a 'while' loop in Processing (all the code below will be looped over and over until all cells are collapsed)
  134.     global H, W, grid
  135.  
  136.     ### OBSERVATION
  137.     # Find cell with minimum non-zero entropy (not collapsed yet)
  138.  
  139.     '''Randomly select 1 cell at the first iteration (when all entropies are equal),
  140.       otherwise select cell with minimum non-zero entropy'''
  141.  
  142.     emin = int(random(d*d)) if frameCount <= 1 else H.index(min(H))
  143.  
  144.  
  145.  
  146.     # Stoping mechanism
  147.  
  148.     ''' When 'H' array is full of 'collapsed' cells --> stop iteration '''
  149.  
  150.     if H[emin] == 'CONT' or H[emin] == 'collapsed':
  151.         print 'stopped'
  152.         noLoop()
  153.         return
  154.  
  155.  
  156.  
  157.     ### COLLAPSE
  158.     # Weighted choice of a pattern
  159.  
  160.     ''' Among the patterns available in the selected cell (the one with min entropy),
  161.        select one pattern randomly, weighted by the frequency that pattern appears in the input image.
  162.        With Python 2.7 no possibility to use random.choice(x, weight) so we have to hard code the weighted choice '''
  163.  
  164.     lfreqs = [b * freqs[i] for i, b in enumerate(W[emin])] # frequencies of the patterns available in the selected cell
  165.     weights = [float(f) / sum(lfreqs) for f in lfreqs] # normalizing these frequencies
  166.     cumsum = [sum(weights[:i]) for i in xrange(1, len(weights)+1)] # cumulative sums of normalized frequencies
  167.     r = random(1)
  168.     idP = sum([cs < r for cs in cumsum])  # index of selected pattern
  169.  
  170.     # Set all patterns to False except for the one that has been chosen  
  171.     W[emin] = [0 if i != idP else 1 for i, b in enumerate(W[emin])]
  172.  
  173.     # Marking selected cell as 'collapsed' in H (array of entropies)
  174.     H[emin] = 'collapsed'
  175.  
  176.     # Storing first color (top left corner) of the selected pattern at the location of the collapsed cell
  177.     Output[emin] = patterns[idP][0]
  178.  
  179.  
  180.  
  181.     ### PROPAGATION
  182.     # For each neighbor (left, right, up, down) of the recently collapsed cell
  183.     for dir, t in enumerate(directions):
  184.         x = (emin%d + t[0])%d
  185.         y = (emin/d + t[1])%d
  186.         idN = x + y * d #index of neighbor
  187.  
  188.         # If that neighbor hasn't been collapsed yet
  189.         if H[idN] != 'collapsed':
  190.  
  191.             # Check indices of all available patterns in that neighboring cell
  192.             available = [i for i, b in enumerate(W[idN]) if b]
  193.  
  194.             # Among these indices, select indices of patterns that can be adjacent to the collapsed cell at this location
  195.             intersection = A[idP][dir] & set(available)
  196.  
  197.             # If the neighboring cell contains indices of patterns that can be adjacent to the collapsed cell
  198.             if intersection:
  199.  
  200.                 # Remove indices of all other patterns that cannot be adjacent to the collapsed cell
  201.                 W[idN] = [True if i in list(intersection) else False for i in xrange(npat)]
  202.  
  203.  
  204.                 ### Update entropy of that neighboring cell accordingly (less patterns = lower entropy)
  205.  
  206.                 # If only 1 pattern available left, no need to compute entropy because entropy is necessarily 0
  207.                 if len(intersection) == 1:
  208.                     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
  209.  
  210.  
  211.                 # If more than 1 pattern available left --> compute/update entropy + add noise (to prevent cells to share the same minimum entropy value)
  212.                 else:
  213.                     lfreqs = [b * f for b, f in izip(W[idN], freqs) if b]
  214.                     ent = math.log(sum(lfreqs)) - sum(map(lambda x: x * math.log(x), lfreqs)) / sum(lfreqs)
  215.                     H[idN] = ent + random(.001)
  216.  
  217.  
  218.             # If no index of adjacent pattern in the list of pattern indices of the neighboring cell
  219.             # --> mark cell as a 'contradiction'
  220.             else:
  221.                 H[idN] = 'CONT'
  222.  
  223.  
  224.  
  225.     # Draw output
  226.  
  227.     ''' dxd grid of cells (squares) filled with their corresponding color.      
  228.        That color is the first (top-left) color of the pattern assigned to that cell '''
  229.  
  230.     for i, c in enumerate(Output):
  231.         x, y = i%d, i/d
  232.         fill(c)
  233.         rect(x * xs, y * ys, xs, ys)
  234.  
  235.         # Displaying corresponding entropy value
  236.         fill(0)
  237.         text(H[i], x * xs + xs/2 - 12, y * ys + ys/2)
  238.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement