简而言之:
我在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。
与原始版本不同,此实现:
算法(据我了解)
1 / 读取输入位图,存储每个NxN模式并计算它们的出现次数。 (可选:具有旋转和反射的增强图案数据。)
例如,当N = 3时:
2 / 预计算并存储模式之间的所有可能的邻接关系。 在下面的示例中,图案207、242、182和125可以与图案246的右侧重叠
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
中每个位置的浮点数相同),因为每个单元格的所有模式都设置为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] = 0
在246
设置为True
),并且相邻单元格减少(与模式W[22]
不相邻的模式已设置为246
)。
现在,该算法在第一次迭代结束时到达,并将循环执行第5步(查找具有最小非零熵的单元格)到第8步(更新熵),直到所有单元格都崩溃为止。
我的脚本
您需要安装Processing的Python mode才能运行此脚本。 它包含大约80行代码(与原始脚本的约1000行相比要短一些),这些代码已完全注释,因此可以快速理解。您还需要下载input image并相应地更改第16行的路径。
False
问题
尽管我竭尽全力将上述所有步骤仔细地编写到代码中,但此实现返回的结果非常奇怪和令人失望:
20x20输出示例
模式分布和邻接约束似乎都应得到尊重(与输入中相同的蓝色,绿色,黄色和棕色颜色,以及相同的 类型):水平地面,绿色茎)。
但是这些模式:
关于最后一点,我应该澄清矛盾的状态是正常的,但是很少发生(如this论文第6页的中部和this文章中所述)
数小时的调试使我确信入门步骤(1至5)是正确的(计数和存储模式,邻接关系和熵计算,数组初始化)。 这使我认为某事必须与算法的核心部分不符(步骤6至8)。我可能没有正确执行这些步骤之一,或者我错过了逻辑的关键要素。
因此,非常感谢您对此事的任何帮助!
此外,任何基于提供的脚本(是否使用处理)的答案都将受到欢迎。
有用的其他资源:
Stephen Sherratt的详细article和Karth&Smith的解释性paper。 另外,为了进行比较,我建议检查其他Python implementation(包含非强制性的回溯机制)。
注意:我已尽力使这个问题尽可能清晰(带有GIF和插图的全面解释,带有有用链接和资源的带有完整注释的代码),但是如果出于某些原因您决定拒绝它,请简要说明一下评论以解释您为什么这样做。
答案 0 :(得分:4)
在查看示例中链接的live demo的同时,基于对原始算法代码的快速回顾,我相信您的错误在于“传播”步骤。
传播不仅将相邻的4个单元更新为折叠的单元。您还必须递归更新所有这些单元的邻居,然后更新这些单元的邻居,等等。好吧,具体地说,一旦您更新了一个相邻的小区,就可以更新它的邻居(在到达第一个小区的其他邻居之前),即深度优先更新,而不是广度优先更新。至少,这就是我从现场演示中收集的信息。
原始算法的实际C#代码实现非常复杂,我还不完全理解,但是关键点似乎在于创建“传播器”对象here以及Propagate函数本身here。
答案 1 :(得分:4)
@mbrig和@Leon提出的假设是,传播步骤遍历整个细胞堆栈(而不是局限于4个直接邻居的集合)是正确的。以下是在回答我自己的问题时提供更多详细信息的尝试。
该问题在传播时发生在步骤7。原始算法确实更新了特定单元格BUT的4个直接邻居:
换句话说,正如评论中所提到的,这是一种递归传播类型,它不仅更新折叠单元的邻居,而且还更新邻居的邻居。所以只要有可能就可以邻接。
详细算法
单元格折叠后,其索引将放入堆栈中。该堆栈意味着稍后临时存储相邻单元格的索引
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)
总体改进
除了这些修复程序外,我还做了一些次要的代码优化,以加快观察和传播步骤,并缩短了加权选择计算。
“波动”现在由索引的Python 组组成,其大小随着“折叠”单元而减小(替换固定的布尔大尺寸列表)
熵存储在 defaultdict 中,该密钥的键将逐渐删除。
起始熵值替换为随机整数(由于开始时存在相当高的不确定性,因此无需进行第一熵计算)
单元格显示一次(避免将它们存储在数组中并在每帧重绘)
现在,加权选择是单线的(避免使用列表理解的几行)