Проблемы с реализацией алгоритма «Функция коллапса волны» в Python

В двух словах:

Моя реализация Алгоритм Функция коллапса волны в Python 2.7 ошибочна, но я не могу определить, где находится проблема. Мне нужна помощь, чтобы выяснить, что я, возможно, упускаю или делаю неправильно.

Что такое алгоритм Функция коллапса волны?

Это алгоритм, написанный в 2016 году Максимом Гуминым, который может генерировать процедурные шаблоны из образца изображения. Вы можете увидеть это в действии здесь (2D модель с перекрытием) и здесь (3D мозаичная модель).

Цель этой реализации:

Чтобы свести алгоритм (двухмерная перекрывающаяся модель) к его сути и избежать повторений и неуклюжести оригинальный скрипт C# (на удивление длинного и трудного для чтения). Это попытка сделать более короткую, понятную и питоническую версию этого алгоритма.

Характеристики этой реализации:

Я использую Обработка (режим Python), программное обеспечение для визуального дизайна, которое упрощает работу с изображениями (без PIL, без Matplotlib,...). Основные недостатки заключаются в том, что я ограничен Python 2.7 и НЕ могу импортировать numpy.

В отличие от оригинальной версии, эта реализация:

  • не является объектно-ориентированным (в его текущем состоянии), что упрощает понимание/приближение к псевдокоду
  • использует 1D-массивы вместо 2D-массивов
  • использует нарезку массива для манипуляций с матрицами

Алгоритм (как я понимаю)

1/ Прочитать входное растровое изображение, сохранить все шаблоны NxN и подсчитать их появление. (необязательный: Дополнить данные шаблона поворотами и отражениями.)

Например, когда N = 3:

Проблемы с реализацией алгоритма «Функция коллапса волны» в Python

2/ Предварительно вычислить и сохранить все возможные отношения смежности между шаблонами. В приведенном ниже примере шаблоны 207, 242, 182 и 125 могут перекрывать правую часть шаблона 246.

Проблемы с реализацией алгоритма «Функция коллапса волны» в Python

3/ Создайте массив с размерами вывода (называемый W для волны). Каждый элемент этого массива представляет собой массив, содержащий состояние (True из False) каждого шаблона.

Например, предположим, что мы насчитали 326 уникальных шаблонов во входных данных и хотим, чтобы наши выходные данные имели размеры 20 на 20 (400 ячеек). Тогда массив "Wave" будет содержать 400 (20x20) массивов, каждый из которых содержит 326 логических значений.

В начале все логические значения установлены на True, потому что каждый паттерн разрешен в любой позиции волны.

W = [[True for pattern in xrange(len(patterns))] for cell in xrange(20*20)]

4/ Создайте еще один массив с размерами вывода (называемый H). Каждый элемент этого массива представляет собой число с плавающей запятой, содержащее значение «энтропии» соответствующей ячейки на выходе.

Энтропия здесь относится к Энтропия Шеннона и рассчитывается на основе количества действительных паттернов в определенном месте волны. Чем больше в ячейке допустимых паттернов (в волне установлено значение True), тем выше ее энтропия.

Например, чтобы вычислить энтропию ячейки 22, мы смотрим на соответствующий индекс в волне (W[22]) и подсчитываем количество логических значений, установленных на True. С этим подсчетом мы теперь можем вычислить энтропию по формуле Шеннона. Результат этого вычисления будет затем сохранен в H под тем же индексом H[22]

В начале все ячейки имеют одинаковое значение энтропии (одинаковое число с плавающей запятой в каждой позиции в H), поскольку для всех шаблонов установлено значение True для каждой ячейки.

H = [entropyValue for cell in xrange(20*20)]

Эти 4 шага являются вводными, они необходимы для инициализации алгоритма. Теперь запускается основной алгоритма:

5/ Наблюдение:

Найдите индекс ячейки с энтропией минимумотличный от нуля (Обратите внимание, что на самой первой итерации все энтропии равны, поэтому нам нужно выбрать индекс ячейки случайным образом.)

Затем посмотрите на все еще действующие паттерны по соответствующему индексу в Волне и выберите один из них случайным образом, взвешенный по частоте появления паттерна на входном изображении (взвешенный выбор).

Например, если наименьшее значение в H имеет индекс 22 (H[22]), мы смотрим на все шаблоны, установленные на True в W[22], и выбираем один случайным образом в зависимости от того, сколько раз он появляется во входных данных. (Помните, что на шаге 1 мы подсчитали количество вхождений для каждого шаблона). Это гарантирует, что шаблоны появятся на выходе с тем же распределением, что и на входе.

6/ Свернуть:

Теперь мы присваиваем индекс выбранного паттерна ячейке с минимальной энтропией. Это означает, что каждый паттерн в соответствующем месте в Волне установлен на False, кроме того, который был выбран.

Например, если шаблон 246 в W[22] был установлен на True и был выбран, то все остальные шаблоны будут установлены на False. Ячейке 22 присвоен шаблон 246. На выходе ячейка 22 будет заполнена первым цветом (верхний левый угол) узора 246. (синий в этом примере)

7/ Распространение:

Из-за ограничений смежности этот выбор шаблона имеет последствия для соседних ячеек в волне. Массивы логических значений, соответствующие ячейкам слева и справа, поверх и над недавно свернутой ячейкой, необходимо соответствующим образом обновить.

Например, если ячейка 22 была свернута и ей присвоен шаблон 246, то W[21] (слева), W[23] (справа), W[2] (вверху) и W[42] (внизу) необходимо изменить так, чтобы они сохраняли True только соседние шаблоны. выкройка 246.

Например, оглядываясь назад на изображение шага 2, мы видим, что только шаблоны 207, 242, 182 и 125 могут быть размещены на правильно шаблона 246. Это означает, что W[23] (справа от ячейки 22) должен сохранить шаблоны 207. , 242, 182 и 125 как True и установить все остальные шаблоны в массиве как False. Если эти шаблоны больше недействительны (уже установлены на False из-за предыдущего ограничения), то алгоритм сталкивается с противоречие.

8/ Обновление энтропии

Поскольку ячейка была свернута (выбран один шаблон, установлен на True) и окружающие ее ячейки обновлены соответствующим образом (настройка несмежных шаблонов на False), энтропия всех этих ячеек изменилась, и ее необходимо вычислить снова. (Помните, что энтропия ячейки коррелирует с количеством действительных паттернов, которые она содержит в Волне.)

В примере энтропия ячейки 22 теперь равна 0 (H[22] = 0, потому что только шаблон 246 установлен в True в W[22]), а энтропия соседних ячеек уменьшилась (шаблоны, которые не были смежными с шаблоном 246, были установлены в False ).

К настоящему времени алгоритм достигает конца первой итерации и будет выполнять шаги с 5 (найти ячейку с минимальной ненулевой энтропией) до 8 (обновить энтропию), пока все ячейки не будут свернуты.

Мой сценарий

Вам понадобится Обработка с установленным Режим Python для запуска этого скрипта. Он содержит около 80 строк кода (коротких по сравнению с ~ 1000 строк исходного сценария), которые полностью аннотированы, чтобы их можно было быстро понять. Вам также необходимо загрузить входное изображение и соответствующим образом изменить путь в строке 16.

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(cmat[::-1])
            all.append([a[::-1] for a in cmat])




    # 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)

Проблема

Несмотря на все мои усилия по аккуратному внедрению в код всех описанных выше шагов, эта реализация возвращает очень странные и разочаровывающие результаты:

Пример вывода 20x20

Проблемы с реализацией алгоритма «Функция коллапса волны» в Python

Необходимо соблюдать как распределение паттернов, так и ограничения смежности казаться (то же количество синего, зеленого, желтого и коричневого цветов, что и во входных данных, и те же паттерны Что-то вроде: горизонтальная земля, зеленые стебли).

Однако эти шаблоны:

  • часто отключаются
  • часто бывают неполными (отсутствие «головок», состоящих из 4-х желтых лепестков)
  • столкнуться со слишком большим количеством противоречивых состояний (серые ячейки, отмеченные как «ПРОДОЛЖЕНИЕ»)

Что касается последнего пункта, я должен уточнить, что противоречивые состояния нормальны, но должны возникать очень редко (как указано в середине страницы 6 документа это и в статье это).

Часы отладки убедили меня в правильности вводных шагов (с 1 по 5) (подсчет и сохранение паттернов, вычисления смежности и энтропии, инициализация массивов). Это навело меня на мысль, что что-то должен быть отключен в основной части алгоритма (шаги 6–8).. Либо я неправильно реализую один из этих шагов, либо мне не хватает ключевого элемента логики.

Таким образом, любая помощь в этом вопросе будет безмерно оценена!

Кроме того, приветствуется любой ответ, основанный на предоставленном сценарии (с использованием обработки или без)..

Полезные дополнительные ресурсы:

Это подробный статья от Стивена Шерратта и пояснительный бумага от Karth & Smith. Кроме того, для сравнения я бы предложил проверить этот другой Реализация Python (содержит механизм возврата, который не является обязательным).

Примечание. Я сделал все возможное, чтобы сделать этот вопрос максимально ясным (подробное объяснение с GIF-файлами и иллюстрациями, полностью аннотированный код с полезными ссылками и ресурсами), но если по каким-то причинам вы решите проголосовать против, пожалуйста, оставьте краткий комментарий, чтобы объяснить почему ты так поступаешь.

Посмотрите на другую реализацию, которая использует только PIL из вашего черного списка github.com/ikart/wfc_python/blob/master/model.py.

norok2 17.07.2019 22:21

хм очень интересный алгоритм. Спасибо за идею, чем заняться на выходных :D

Coderino Javarino 18.07.2019 12:57

Это может быть не то, что вы хотите услышать, но решение этой ошибки заключается в рефакторинге вашего кода на более мелкие функции и их модульном тестировании. В качестве дополнительного бонуса это устранит любые другие ошибки (а их будет несколько) в коде. Если вы не хотите делать это сами и хотите, чтобы S.O. пользователи в помощь, могу ли я предложить предоставить остальную часть вашего кода, поскольку нет функции main() и есть несколько необъявленных функций (fill, rect, text, random и т. д.).

FiddleStix 18.07.2019 15:51

Уважаемый @FiddleStix, спасибо за комментарий и предложение. Рефакторинг кода на более мелкие функции, имхо, здесь не актуален, поскольку я не смог бы определить «ошибку». Это больше касается понимания структурного дефекта алгоритма, чем отслеживания ошибочных данных. Обратите внимание, что это полный скрипт: 'fill', 'rect', 'loadImage', 'pixels', ... являются встроенными функциями обработки. Код необходимо вставить в Processing IDE. Любой ответ, который не зависит от среды Processsing, но основан на предоставленном мной сценарии, конечно, приветствуется/приемлем.

solub 18.07.2019 16:29

что делает random(1) и т.д.?

Antti Haapala -- Слава Україні 22.07.2019 05:15

random — это встроенная функция Processing. Здесь он вернет число с плавающей запятой от 0 до 1. Цель состоит в том, чтобы выбрать кумулятивную сумму от 0 до 1 для вычисления взвешенного выбора. Пожалуйста, смотрите обновленный сценарий в моем ответе для более простого подхода.

solub 22.07.2019 05:28
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
56
6
1 744
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Глядя на живая демонстрация, связанный в одном из ваших примеров, и основываясь на быстром просмотре исходного кода алгоритма, я считаю, что ваша ошибка заключается в шаге «Распространение».

Распространение — это не просто обновление соседних 4 ячеек до свернутой ячейки. Вы также должны рекурсивно обновить все соседи этих ячеек, а затем соседей этих ячеек и т. д. Ну, если быть точным, как только вы обновляете одну соседнюю ячейку, вы затем обновляете ее соседа (прежде чем добраться до других соседей первой ячейки), то есть сначала обновляете в глубину, а не в ширину. По крайней мере, это то, что я понял из живого демо.

Фактическая реализация исходного алгоритма в коде C# довольно сложна, и я не совсем понимаю ее, но ключевыми моментами, по-видимому, являются создание объекта «распространителя» здесь, а также самой функции распространения, здесь.

Привет @mbrig, спасибо за ответ. К сожалению, я думаю, что распространение на самом деле связано с обновлением 4 прямых соседей. Строка 123 вашей второй ссылки, цикл повторяется по 4 направлениям DX и DY. В середине первой статьи, на которую я дал ссылку, вы можете прочитать «в каждом из четырех ближайших соседей этой ячейки». Строка 230 реализации Python (также связанная) перебирает 4 соседа "pos"... Пожалуйста, постарайтесь не делать выводов, основанных исключительно на том, как алгоритм выглядит в видео, в этом есть чисто косметический аспект (слой цветов для каждой ячейки), что может ввести в заблуждение.

solub 19.07.2019 03:26

Кроме того, функция «распространителя», о которой вы говорите, на самом деле не связана с шагом распространения. Его цель — создать индексную структуру данных, описывающую способы размещения шаблонов рядом друг с другом (см. подробности на стр. 6 статьи Karth & Smith или вернитесь к шагу 2 алгоритма).

solub 19.07.2019 03:55

@solub Я не говорю о видео, я говорю о живой интерактивной демонстрации. Маловероятно, что в живом демо реализован какой-то похожий, но существенно отличающийся алгоритм, просто чтобы выглядеть круче. Я думаю, вы также пропустили ключевую часть функции Propagate, где она продолжает повторяться до тех пор, пока размер стека var не станет равным нулю, а вызов Ban() внутри нее будет продолжать добавлять в этот стек. Совершенно очевидно, что он делает больше, чем просто обновляет 4 соседние ячейки. Я бы рекомендовал запустить код C# в отладчике, чтобы полностью понять, что он делает.

mbrig 19.07.2019 06:42

Чтобы увидеть, что я имею в виду в живой демонстрации, вам нужно отрегулировать ползунок, чтобы существенно замедлить скорость. Кроме того, переменная «propagator» определенно связана с функцией Propagate. Он повторяется внутри него. Если у меня будет время на этих выходных, я посмотрю, смогу ли я запустить код С# самостоятельно.

mbrig 19.07.2019 06:46

Извините, неправильно прочитал вашу фразу. Мой вопрос касается перекрывающейся модели, а не мозаичной модели, которая демонстрируется в демонстрации. Хотя оба алгоритма, вероятно, имеют много общего, я бы не стал использовать последний в качестве эталона для понимания первого. Опять же, то, что вы видите, не обязательно является подходящим представлением лежащей в основе логики. Имейте в виду, что в этой демонстрации может быть реализована система возврата, которая может привести к неправильной интерпретации механизма распространения. В качестве меры предосторожности я бы посоветовал придерживаться закодированных примеров и сосредоточиться только на перекрывающейся модели.

solub 19.07.2019 14:30

Извините, что противоречу вам, мой пример реализует механизм потока управления, чтобы продолжать распространение до тех пор, пока все ячейки не будут свернуты (назначение переменной stacksize). Кроме того, пропагандист именно то, что я сказал. Позвольте мне разбить его: для каждого направления (d), для каждого шаблона1 (t), для каждого шаблона2 (t2), если шаблон2 перекрывает шаблон1 в этом направлении (согласуется) --> сохраните его как соседний шаблон в этом направлении. Он вызывается позже во время распространения, чтобы проверить, может ли шаблон быть размещен (или нет) в соседнем месте.

solub 19.07.2019 15:11

Пожалуйста, внимательно прочитайте пояснения/код/документацию перед публикацией. Если у вас есть интуиция, в чем может быть проблема, но вы не уверены, достаточно комментария. С другой стороны, ответ подразумевает, что вы протестировали свое решение с помощью кода и можете предложить соответствующий обходной путь.

solub 19.07.2019 15:29

@solub Я согласен с этим ответом. Шаг 4.ii в исходном описании алгоритма читается как «Распространение: распространение информации, полученной на предыдущем шаге наблюдения.», что, несмотря на возможность различных интерпретаций, предполагает, что должны применяться все последствия выбора фиксированного значения в одной ячейке. Я могу привести конкретный пример того, почему, если этого не сделать, вероятность противоречий возрастает. Но это будет слишком много для комментария. Я думаю, что вместо этого этот ответ должен быть улучшен.

Leon 19.07.2019 15:58

@ Леон Что ты имеешь в виду под «всеми последствиями»? Обсуждаем ли мы количество соседних ячеек для обновления? Если это так, пожалуйста, прочитайте оригинальную функцию распространения. Вы увидите, что последствия распространяются только на 4 из них (E, W, N, S). Другими словами: 1 ячейка свернута, 4 ее непосредственных соседа обновлены.

solub 19.07.2019 16:40

@solub Обновление ячейки заключается в удалении шаблонов, которые она не может вместить. Но этот эффект может распространяться. (Возможное) присутствие шаблона А в ячейке Икс может быть единственным обоснованием присутствия шаблона Б в соседней ячейке Д (когда шаблон Б не может быть смежным с каким-либо другим шаблоном, все еще находящимся в Икс). Следовательно, если мы удаляем А из Икс, мы также должны удалять Б из Д. Процесс должен продолжаться рекурсивно до тех пор, пока ячейка, которую мы пытаемся обновить, не останется нетронутой.

Leon 19.07.2019 16:59

... и это именно то, что я описываю/реализую в первую очередь. Как это связано с приведенным выше «ответом»?

solub 19.07.2019 17:04

@solub (продолжая мой предыдущий комментарий) Если ограничить обновление только непосредственными соседями ячейки, где произошел коллапс волны, то можно в какой-то степени смягчить проблемы, которые вы сейчас наблюдаете, не выбирая вслепую паттерн из списка паттерны, сохранившиеся к моменту, когда ячейка стала той, у которой наименьшее значение энтропии. Сначала вы должны отбросить те, которые стали неосуществимыми. Но это меняет значение энтропии ячейки! Поэтому это лучше сделать до того, как значение энтропии будет использовано для выбора ячейки.

Leon 19.07.2019 17:06

@solub (теперь отвечая на ваш комментарий, появившийся между двумя частями моего разделенного комментария) Вы обновляете только непосредственных соседей наблюдаемой ячейки. Но вы также должны обновить всех соседей каждой обновленной ячейки и так далее.

Leon 19.07.2019 17:09

@solub Обратите внимание, что обновление удаленного соседа немного отличается от обновления непосредственного соседа. Последнее является частным случаем первого, если учесть удаление всех шаблонов, кроме одного. Общая процедура обновления должна работать для удаления произвольного подмножества шаблонов и может быть реализована как последовательное применение более простой процедуры, обрабатывающей удаление одного шаблона.

Leon 19.07.2019 17:12

@solub Повторение основного процесса (наблюдение-> распространение) до тех пор, пока все ячейки не будут свернуты, нет эквивалентно тому, что эталонная реализация делает с размером стека var. Эталонная реализация включает возможность добавления дополнительных ячеек в стек, пока еще на этапе распространения. Ваша реализация всегда будет делать только observe -> propagate 4 directly connected cells -> repeat.

mbrig 19.07.2019 20:13

@mbrig Пожалуйста, обновите свой ответ последовательным, подробным и поддающимся проверке решением.

solub 19.07.2019 20:42

Переполнение стека @solub не является службой написания кода. Когда у меня будет время, я смогу более подробно описать разницу между вашим кодом и эталоном, но внедрение такого существенного изменения — это работа твой.

mbrig 19.07.2019 21:41

@mbrig Не запрашивать конкретно код (хотя пример фрагмента для иллюстративных целей был бы полезен), а подробный и связный ответ (см. Руководство SO), который я и другие читатели могут проверить на своей стороне. Ваш текущий ответ + комментарии отсутствуют на данный момент. Конечно, вы можете больше не тратить время на эту награду и оставить ее другим.

solub 19.07.2019 22:38
Ответ принят как подходящий

Гипотеза, предложенная @mbrig и @Leon, о том, что шаг распространения повторяется по всему стеку ячеек (вместо того, чтобы ограничиваться набором из 4 прямых соседей), была верна. Следующее является попыткой предоставить более подробную информацию, отвечая на мои собственные вопросы.

Проблема возникла на шаге 7 при распространении. Исходный алгоритм обновляет 4 прямых соседа конкретной ячейки, НО:

  • индекс этой конкретной ячейки по очереди заменяются индексами ранее обновленных соседей.
  • этот каскадный процесс срабатывает каждый раз, когда ячейка схлопывается
  • и последний до тех пор, пока соседние шаблоны конкретной ячейки доступны в 1 соседней ячейке

Другими словами, как упоминалось в комментариях, это тип распространения рекурсивный, который обновляет не только соседей свернутой ячейки, но и соседей соседей... и так далее, пока возможны смежности.

Подробный алгоритм

Когда ячейка свернута, ее индекс помещается в стек. Этот стек позже предназначен для хранения временно индексов соседних ячеек.

stack = set([emin]) #emin = index of cell with minimum entropy that has been collapsed

Распространение будет продолжаться до тех пор, пока этот стек заполнен индексами:

while stack:

Первое, что мы делаем, это pop() последний индекс, содержащийся в стеке (пока единственный), и получаем индексы его 4-х соседних ячеек (E, W, N, S). Мы должны держать их в пределах границ и следить за тем, чтобы они заворачивались.

while stack:
    idC = stack.pop() # index of current cell
    for dir, t in enumerate(mat):
        x = (idC%w + t[0])%w
        y = (idC/w + t[1])%h
        idN = x + y * w  # index of neighboring cell

Прежде чем идти дальше, убедитесь, что соседняя ячейка еще не свернута (мы не хотим обновлять ячейку, в которой доступен только 1 шаблон):

        if H[idN] != 'c': 

Затем мы проверяем все шаблоны, чтобы мог располагались в этом месте. пример: если соседняя ячейка находится слева от текущей ячейки (восточная сторона), мы смотрим на все узоры, которые можно разместить слева от каждого узора, содержащегося в текущей ячейке.

            possible = set([n for idP in W[idC] for n in A[idP][dir]])

Также смотрим на шаблоны, которые являются доступны в соседней ячейке:

            available = W[idN]

Теперь убедимся, что соседняя ячейка В самом деле должна быть обновлена. Если все его доступные шаблоны уже есть в списке всех возможных шаблонов —> обновлять его не нужно (алгоритм пропускает этого соседа и переходит к следующему):

            if not available.issubset(possible):

Однако, если это нет подмножество списка possible —> мы смотрим на перекресток двух наборов (все шаблоны, которые можно разместить в этом месте и которые, «к счастью», доступны в этом же месте):

                intersection = possible & available

Если они не пересекаются (паттерны, которые могли быть туда помещены, но недоступны), значит, мы столкнулись с "противоречием". Мы должны остановить весь алгоритм WFC.

                if not intersection:
                    print 'contradiction'
                    noLoop()

Если, наоборот, пересекаются --> обновляем соседнюю ячейку этим уточненным списком индексов шаблона:

                W[idN] = intersection

Поскольку эта соседняя ячейка была обновлена, ее энтропия также должна быть обновлена:

                lfreqs = [freqs[i] for i in W[idN]]
                H[idN] = (log(sum(lfreqs)) - sum(map(lambda x: x * log(x), lfreqs)) / sum(lfreqs)) - random(.001)

Наконец, что наиболее важно, мы добавляем индекс этой соседней ячейки в стек, чтобы она по очереди становилась следующей ячейкой ток (той, чьи соседи будут обновлены во время следующего цикла while):

                stack.add(idN)

Полностью обновленный скрипт

from collections import Counter
from itertools import chain
from random import choice

w, h = 40, 25
N = 3

def setup():
    size(w*20, h*20, P2D)
    background('#FFFFFF')
    frameRate(1000)
    noStroke()

    global W, A, H, patterns, freqs, npat, mat, xs, ys

    img = loadImage('Flowers.png') 
    iw, ih = img.width, img.height
    xs, ys = width//w, height//h
    kernel = [[i + n*iw for i in xrange(N)] for n in xrange(N)]
    mat = ((-1, 0), (1, 0), (0, -1), (0, 1))
    all = []

    for y in xrange(ih):
        for x in xrange(iw):
            cmat = [[img.pixels[((x+n)%iw)+(((a[0]+iw*y)/iw)%ih)*iw] for n in a] for a in kernel]
            for r in xrange(4):
                cmat = zip(*cmat[::-1])
                all.append(cmat)
                all.append(cmat[::-1])
                all.append([a[::-1] for a in cmat])

    all = [tuple(chain.from_iterable(p)) for p in all] 
    c = Counter(all)
    patterns = c.keys()
    freqs = c.values()
    npat = len(freqs) 

    W = [set(range(npat)) for i in xrange(w*h)] 
    A = [[set() for dir in xrange(len(mat))] for i in xrange(npat)]
    H = [100 for i in xrange(w*h)] 

    for i1 in xrange(npat):
        for i2 in xrange(npat):
            if [n for i, n in enumerate(patterns[i1]) if i%N!=(N-1)] == [n for i, n in enumerate(patterns[i2]) if i%N!=0]:
                A[i1][0].add(i2)
                A[i2][1].add(i1)
            if patterns[i1][:(N*N)-N] == patterns[i2][N:]:
                A[i1][2].add(i2)
                A[i2][3].add(i1)


def draw():    
    global H, W

    emin = int(random(w*h)) if frameCount <= 1 else H.index(min(H)) 

    if H[emin] == 'c': 
        print 'finished'
        noLoop()

    id = choice([idP for idP in W[emin] for i in xrange(freqs[idP])])
    W[emin] = [id]
    H[emin] = 'c' 

    stack = set([emin])
    while stack:
        idC = stack.pop() 
        for dir, t in enumerate(mat):
            x = (idC%w + t[0])%w
            y = (idC/w + t[1])%h
            idN = x + y * w 
            if H[idN] != 'c': 
                possible = set([n for idP in W[idC] for n in A[idP][dir]])
                if not W[idN].issubset(possible):
                    intersection = possible & W[idN] 
                    if not intersection:
                        print 'contradiction'
                        noLoop()
                        return

                    W[idN] = intersection
                    lfreqs = [freqs[i] for i in W[idN]]
                    H[idN] = (log(sum(lfreqs)) - sum(map(lambda x: x * log(x), lfreqs)) / sum(lfreqs)) - random(.001)
                    stack.add(idN)

    fill(patterns[id][0])
    rect((emin%w) * xs, (emin/w) * ys, xs, ys)

Общие улучшения

В дополнение к этим исправлениям я также сделал небольшую оптимизацию кода, чтобы ускорить этапы наблюдения и распространения, а также сократить вычисление взвешенного выбора.

  • «Волна» теперь состоит из Python наборы из индексы, размер которых уменьшается по мере того, как ячейки «сворачиваются» (заменяя большие списки логических значений фиксированного размера).

  • Энтропии хранятся в defaultdict, чьи ключи постепенно удаляются.

  • Начальное значение энтропии заменяется случайным целым числом (первый расчет энтропии не требуется, так как при запуске существует равновероятный высокий уровень неопределенности)

  • Ячейки отображаются один раз (избегая хранения их в массиве и перерисовки в каждом кадре)

  • Взвешенный выбор теперь является однострочным (избегая нескольких необязательных строк понимания списка)

Другие вопросы по теме