【发布时间】:2021-04-25 11:08:06
【问题描述】:
很久以前,我做了一个游戏,其中需要一种连接组件标签来实现 AI 部分。我当时在不知不觉中使用了two-pass算法。
最近,我知道我可以改用基于位扫描的方法使它们更快。它使用每像素 1 位数据作为输入,而不是典型的每像素字节输入。然后它使用 BSF 指令在每个扫描行中找到每个线性块。请看下面的代码。 Cut 是一个结构,它在扫描线中保存位 1 的线性块的信息。
Cut* get_cuts_in_row(const u32* bits, const u32* bit_final, Cut* cuts) {
u32 working_bits = *bits;
u32 basepos = 0, bitpos = 0;
for (;; cuts++) {
//find starting position
while (!_BitScanForward(&bitpos, working_bits)) {
bits++, basepos += 32;
if (bits == bit_final) {
cuts->start_pos = (short)0xFFFF;
cuts->end_pos = (short)0xFFFF;
return cuts + 1;
}
working_bits = *bits;
}
cuts->start_pos = short(basepos + bitpos);
//find ending position
working_bits = (~working_bits) & (0xFFFFFFFF << bitpos);
while (!_BitScanForward(&bitpos, working_bits)) {
bits++, basepos += 32;
working_bits = ~(*bits);
}
working_bits = (~working_bits) & (0xFFFFFFFF << bitpos);
cuts->end_pos = short(basepos + bitpos);
}
}
首先,它使用汇编BSF指令找到第1位出现的位置。找到后,通过位反转和位掩码找到该位置后出现的第一个位置位0,然后重复此过程。
在获得每条扫描线中所有线性块 1 的起始位置和结束位置(我更喜欢将它们称为“剪切”)后,它以 CCL 方式为它们提供标签。对于第一行,每个剪辑都有不同的标签。
对于其余行中的每个切口,它首先检查是否有上切口与其相连。如果没有上切连接到它,它会得到新的标签。如果只有一个上切口连接到它,它会得到标签的副本。如果许多上切口连接到它,则这些标签将被合并并获得合并的标签。这可以使用上块和下块的两个渐进指针轻松完成。这是完成该部分的完整代码。
Label* get_labels_8c(Cut* cuts, Cut* cuts_end, Label* label_next) {
Cut* cuts_up = cuts;
//generate labels for the first row
for (; cuts->start_pos != 0xFFFF; cuts++) cuts->label = [GET NEW LABEL FROM THE POOL];
cuts++;
//generate labels for the rests
for (; cuts != cuts_end; cuts++) {
Cut* cuts_save = cuts;
for (;; cuts++) {
u32 start_pos = cuts->start_pos;
if (start_pos == 0xFFFF) break;
//Skip upper slices ends before this slice starts
for (; cuts_up->end_pos < start_pos; cuts_up++);
//No upper slice meets this
u32 end_pos = cuts->end_pos;
if (cuts_up->start_pos > end_pos) {
cuts->label = [GET NEW LABEL FROM THE POOL];
continue;
};
Label* label = label_equiv_recursion(cuts_up->label);
//Next upper slice can not meet this
if (end_pos <= cuts_up->end_pos) {
cuts->label = label;
continue;
}
//Find next upper slices meet this
for (; cuts_up->start_pos <= end_pos; cuts_up++) {
Label* label_other = label_equiv_recursion(cuts_up->label);
if (label != label_other) [MERGE TWO LABELS]
if (end_pos <= cuts_up->end_pos) break;
}
cuts->label = label;
}
cuts_up = cuts_save;
}
return label_next;
}
在此之后,可以使用这些信息为每条扫描线直接制作标签数组或任何他想要的输出。
我检查了这个方法的执行时间,然后我发现它比我以前使用的两次扫描方法快得多。令人惊讶的是,即使输入数据是随机的,它也比两次扫描更快。显然,位扫描算法最适合结构相对简单的数据,其中扫描行中的每个块都很大。它并非设计用于随机图像。
让我感到困惑的是,实际上没有人谈论这种方法。坦率地说,这似乎不是一个很难想出的想法。很难相信我是第一个尝试过的人。
也许虽然我的方法比原始的两次扫描方法更好,但它比基于两次扫描思想的更发达的方法更差,所以无论如何都不值得一提。
但是,如果可以改进两次扫描方法,那么位扫描方法也可以。我自己发现 8 连接有一个很好的改进。它分析相邻的两条扫描线,我使用位或指令将它们合并。你可以在here找到完整的代码和详细的解释。
我知道有一个名为 YACCLAB 的 CCL 算法基准。我将用最好的 CCL 算法测试我的算法,看看它们有多好。在此之前,我想在这里问几件事。
我的问题是,
-
我发现的这些算法真的很新吗?仍然很难相信没有人曾经想过使用位扫描的 CCL 算法。如果它已经是一件事,为什么我找不到任何人说它?基于位扫描的算法是否被证明是坏的和被遗忘的?
-
如果我真的找到了一个新的算法,接下来我该怎么做?当然,我会在 YACCLAB 等更可靠的系统中对其进行测试。我在问我接下来应该做什么。我应该怎么做才能让这些算法被挖掘和传播?
【问题讨论】:
-
这对我来说就像使用比特摆弄来做一个标准的 CCL 算法。但也许我错过了什么?
-
您指的标准 CCL 算法是什么?我不知道任何可以称为标准的 CCL 算法,除了两个,一次一个组件和两遍算法。两者都与我尝试的完全不同。即使我在不使用位摆弄的情况下做同样的事情,它仍然是与两者完全不同的算法。
-
所以用更简单的术语重申:您水平扫描一次,并为每一行制作一组标签。然后我对下一步有点困惑,因为你的描述有点手忙脚乱。请问您如何有效地将您的行标签转换为整个图像的标签? (例如,如何快速实现合并)。
-
我很抱歉没有解释。我只是想知道是否有任何像我一样使用位扫描的 CCL 算法。我不认为我需要为此详细解释算法的每个部分。我觉得现在问这个本身就是一个错误,但这个错误已经犯了所以......请稍等。我也会在这部分添加一些解释。
标签: c++ algorithm optimization connected-components