在Python中实现“波浪折叠功能”算法的问题

时间:2019-07-16 02:08:39

标签: python algorithm markov-chains procedural-generation

简而言之:

我在Python 2.7中对Wave Collapse Function algorithm的实现存在缺陷,但是我无法确定问题所在。我需要帮助来找出我可能会丢失或做错的事情。

什么是波崩函数算法?

这是Maxim Gumin在2016年编写的一种算法,可以从样本图像生成程序模式。您可以在here(2D重叠模型)和here(3D切片模型)中看到它。

此实施的目标:

将算法(2D重叠模型)简化为本质,并避免original C# script的冗长和笨拙(令人惊讶的是,它很长且难以阅读)。这是为了使该算法更短,更清晰和pythonic版本。

此实现的特征:

我正在使用Processing(Python模式),这是一种用于视觉设计的软件,可简化图像处理(没有PIL,没有Matplotlib等)。主要缺点是我仅限于Python 2.7,并且无法导入numpy。

与原始版本不同,此实现:

  • 并非面向对象(处于当前状态),因此更易于理解/更接近伪代码
  • 使用1D数组而不是2D数组
  • 正在使用数组切片进行矩阵处理

算法(据我了解)

1 / 读取输入位图,存储每个NxN模式并计算它们的出现次数。   (可选:具有旋转和反射的增强图案数据。)

例如,当N = 3时:

enter image description here

2 / 预计算并存储模式之间的所有可能的邻接关系。 在下面的示例中,图案207、242、182和125可以与图案246的右侧重叠

enter image description here

3 / 创建一个具有输出尺寸的数组(对于wave称为W)。该数组的每个元素都是一个保存每个模式的状态(True的状态(False)的数组。

例如,假设我们在输入中计数了326个唯一的模式,并且希望输出尺寸为20 x 20(400个单元)。然后,“ Wave”数组将包含400个(20x20)数组,每个数组包含326个布尔值。

开始时,所有布尔值都设置为True,因为在Wave的任何位置都允许使用每个模式。

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

4 / 创建另一个具有输出尺寸的数组(称为H)。该数组的每个元素都是一个浮点数,在输出中保存其对应单元格的“熵”值。

此处的熵是指Shannon Entropy,它是根据Wave中特定位置的有效模式数量来计算的。有效格模式越多(在Wave中设置为True),其熵就越大。

例如,要计算单元格22的熵,我们查看其在波(W[22])中的对应索引,并对设置为True的布尔值进行计数。有了这个计数,我们现在可以使用香农公式来计算熵。然后,此计算结果将以相同的索引H[22]

存储在H中

开始时,所有单元格的熵值相同(H中每个位置的浮点数相同),因为每个单元格的所有模式都设置为True

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

这四个步骤是介绍性步骤,它们是初始化算法所必需的。现在启动算法的核心

5 / 观察:

使用最小 非零熵来查找单元格的索引(请注意,在第一次迭代时,所有熵都是相等的,因此我们需要选择一个单元格的索引

然后,查看Wave中相应索引处的仍然有效的模式,并随机选择其中一个模式,并根据模式在输入图像中出现的频率进行加权(加权选择)。

例如,如果H中的最小值位于索引22(H[22]),则我们查看在True处设置为W[22]的所有模式,然后随机选择一个基于它出现在输入中的次数。 (请记住,在第1步中,我们已经计算出每种模式的出现次数)。这样可以确保模式在输出中出现的分布与输入中的分布相似。

6 / 折叠:

我们现在将选定模式的索引分配给具有最小熵的单元。意味着将Wave中相应位置的每个模式都设置为False,除了已选择的模式。

例如,如果246中的模式W[22]被设置为True,并且已被选择,则所有其他模式都被设置为False。单元格22被分配了模式246在输出单元格22中,将填充图案246的第一个颜色(左上角)。(在此示例中为蓝色)

7 / 传播:

由于邻接约束,该模式选择会对Wave中的相邻单元产生影响。需要相应地更新与最近折叠的单元格左右上方,上方和上方的单元格相对应的布尔数组。

例如,如果单元格22已折叠并分配了模式246,则W[21](左),W[23](右),W[2](向上)和W[42](向下)必须进行修改,以使它们仅保持True与模式246相邻的模式。

例如,回顾一下步骤2的图片,我们可以看到只有模式207、242、182和125可以放置在模式246的 right 上。这意味着{{ 1}}(单元格W[23]的右侧)需要将模式207、242、182和125保持为22,并将数组中的所有其他模式设置为True。如果这些模式不再有效(由于先前的限制而已设置为False),则该算法将面临矛盾

8 / 更新熵

由于一个单元格已折叠(选择了一个模式,设置为False),并且其周围的单元格也进行了相应的更新(将非相邻模式设置为True),因此所有这些单元格的熵都发生了变化,需要再次计算。 (请记住,单元的熵与其在Wave中保存的有效模式的数量有关。)

在该示例中,单元格22的熵现在为0(False,因为只有模式H[22] = 0246设置为True),并且相邻单元格减少(与模式W[22]不相邻的模式已设置为246)。

现在,该算法在第一次迭代结束时到达,并将循环执行第5步(查找具有最小非零熵的单元格)到第8步(更新熵),直到所有单元格都崩溃为止。

我的脚本

您需要安装ProcessingPython mode才能运行此脚本。 它包含大约80行代码(与原始脚本的约1000行相比要短一些),这些代码已完全注释,因此可以快速理解。您还需要下载input image并相应地更改第16行的路径。

False

问题

尽管我竭尽全力将上述所有步骤仔细地编写到代码中,但此实现返回的结果非常奇怪和令人失望:

20x20输出示例

enter image description here

模式分布和邻接约束似乎都应得到尊重(与输入中相同的蓝色,绿色,黄色和棕色颜色,以及相同的 类型):水平地面,绿色茎)。

但是这些模式:

  • 经常断开连接
  • 通常不完整(缺少由4个黄色花瓣组成的“头”)
  • 遇到太多矛盾的状态(灰色单元格标记为“ CONT”)

关于最后一点,我应该澄清矛盾的状态是正常的,但是很少发生(如this论文第6页的中部和this文章中所述)

数小时的调试使我确信入门步骤(1至5)是正确的(计数和存储模式,邻接关系和熵计算,数组初始化)。 这使我认为某事必须与算法的核心部分不符(步骤6至8)。我可能没有正确执行这些步骤之一,或者我错过了逻辑的关键要素。

因此,非常感谢您对此事的任何帮助!

此外,任何基于提供的脚本(是否使用处理)的答案都将受到欢迎

有用的其他资源:

Stephen Sherratt的详细article和Karth&Smith的解释性paper。 另外,为了进行比较,我建议检查其他Python implementation(包含非强制性的回溯机制)。

注意:我已尽力使这个问题尽可能清晰(带有GIF和插图的全面解释,带有有用链接和资源的带有完整注释的代码),但是如果出于某些原因您决定拒绝它,请简要说明一下评论以解释您为什么这样做。

2 个答案:

答案 0 :(得分:4)

在查看示例中链接的live demo的同时,基于对原始算法代码的快速回顾,我相信您的错误在于“传播”步骤。

传播不仅将相邻的4个单元更新为折叠的单元。您还必须递归更新所有这些单元的邻居,然后更新这些单元的邻居,等等。好吧,具体地说,一旦您更新了一个相邻的小区,就可以更新它的邻居(在到达第一个小区的其他邻居之前),即深度优先更新,而不是广度优先更新。至少,这就是我从现场演示中收集的信息。

原始算法的实际C#代码实现非常复杂,我还不完全理解,但是关键点似乎在于创建“传播器”对象here以及Propagate函数本身here

答案 1 :(得分:4)

@mbrig和@Leon提出的假设是,传播步骤遍历整个细胞堆栈(而不是局限于4个直接邻居的集合)是正确的。以下是在回答我自己的问题时提供更多详细信息的尝试。

该问题在传播时发生在步骤7。原始算法确实更新了特定单元格BUT的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):

如果没有,我们看一下两组的交叉点->可以放置在该位置并且“幸运的”可以在同一位置使用的所有图案位置:

                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)

最后,最重要的是,我们将该相邻单元格的索引添加到堆栈中,从而使其依次成为下一个 current 单元格(其邻居将在下一个{{1 }}循环):

while

完整的脚本

                stack.add(idN)

enter image description here

总体改进

除了这些修复程序外,我还做了一些次要的代码优化,以加快观察和传播步骤,并缩短了加权选择计算。

  • “波动”现在由索引的Python 组成,其大小随着“折叠”单元而减小(替换固定的布尔大尺寸列表)

  • 熵存储在 defaultdict 中,该密钥的键将逐渐删除。

  • 起始熵值替换为随机整数(由于开始时存在相当高的不确定性,因此无需进行第一熵计算)

  • 单元格显示一次(避免将它们存储在数组中并在每帧重绘)

  • 现在,加权选择是单线的(避免使用列表理解的几行)