【问题标题】:Is this connected-component labeling algorithm new?这种连通分量标记算法是新的吗?
【发布时间】: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 算法测试我的算法,看看它们有多好。在此之前,我想在这里问几件事。

我的问题是,

  1. 我发现的这些算法真的很新吗?仍然很难相信没有人曾经想过使用位扫描的 CCL 算法。如果它已经是一件事,为什么我找不到任何人说它?基于位扫描的算法是否被证明是坏的和被遗忘的?

  2. 如果我真的找到了一个新的算法,接下来我该怎么做?当然,我会在 YACCLAB 等更可靠的系统中对其进行测试。我在问我接下来应该做什么。我应该怎么做才能让这些算法被挖掘和传播?

【问题讨论】:

  • 这对我来说就像使用比特摆弄来做一个标准的 CCL 算法。但也许我错过了什么?
  • 您指的标准 CCL 算法是什么?我不知道任何可以称为标准的 CCL 算法,除了两个,一次一个组件和两遍算法。两者都与我尝试的完全不同。即使我在不​​使用位摆弄的情况下做同样的事情,它仍然是与两者完全不同的算法。
  • 所以用更简单的术语重申:您水平扫描一次,并为每一行制作一组标签。然后我对下一步有点困惑,因为你的描述有点手忙脚乱。请问您如何有效地将您的行标签转换为整个图像的标签? (例如,如何快速实现合并)。
  • 我很抱歉没有解释。我只是想知道是否有任何像我一样使用位扫描的 CCL 算法。我不认为我需要为此详细解释算法的每个部分。我觉得现在问这个本身就是一个错误,但这个错误已经犯了所以......请稍等。我也会在这部分添加一些解释。

标签: c++ algorithm optimization connected-components


【解决方案1】:

到目前为止,我有点怀疑

我的推理太长了,无法发表评论,所以我们到了。有很多东西要解压。我非常喜欢这个问题,尽管它可能更适合computer science 网站。

问题是,这个问题有两层:

  1. 是否发现了新算法?
  2. 位扫描部分呢?

您将这两者结合起来,所以首先我将解释为什么我想分别考虑它们:
算法 是一组与语言无关的步骤(more formal definition)。因此,即使不需要位扫描,它也应该可以工作。
另一方面,位扫描我认为是一种优化技术——我们使用的是一种计算机可以适应的结构,它可以为我们带来性能提升。 p>

除非我们将这两者分开,否则问题会变得有点模糊,因为可能会发生几种可能的情况:

  1. 算法是新的和改进的,位扫描使其更快。那太棒了。
  2. 该算法只是表示“两次通过” 或类似内容的一种新方式。如果它超过了基准,那仍然会很好。在这种情况下,可能值得将其添加到 CCL 库中。
  3. 该算法非常适合某些情况,但在其他情况下却以某种方式失败(速度方面,而不是纠正方面)。此处的位扫描使比较变得困难。
  4. 该算法非常适合某些情况,但在其他情况下完全失败(产生不正确的结果)。你只是还没有找到反例。

让我们假设 4 不是这种情况,我们想在 1 到 3 之间做出决定。在每种情况下,位扫描都会使事情变得模糊,因为它很可能会加快速度 - 因此在某些情况下,即使是较慢的算法也可能胜过更好的算法。

所以首先我会尝试删除位扫描并重新评估性能。快速浏览后,CCL 的算法似乎具有线性复杂性,具体取决于图像大小 - 您需要至少检查每个像素一次。休息是尽可能降低常数的斗争。 (通过次数,要检查的邻居数量等)我认为可以安全地假设,你不能比线性做得更好 - 所以第一个问题是:你的算法是否通过乘法常数提高了复杂性?由于该算法是线性的,因此该因素直接转化为良好的性能。

第二个问题是:位扫描会进一步提高算法的性能吗?

另外,既然我已经开始考虑了,那么棋盘模式和 4 连接呢?或者,一个 3x3 的棋盘交叉用于 8 连接。

【讨论】:

  • 回答第一个问题,我的算法绝对是线性的。我怀疑是否有任何比线性更快的 CCL 算法。在第二个问题上,我从未尝试过在不使用位扫描时检查算法是否更快。但是,我会最直接地回答这个问题。它肯定会比没有位扫描的两遍算法慢。它的大部分好处来自于它可以使用位操作进行优化。双扫描算法需要同时检查一个像素及其上像素。使用位操作来优化它是不可能的。
  • 我发现的其他使用位扫描的 CCL 算法也是如此。它使用硬件可以非常快速有效地计算位或运算的事实。是否可以在不考虑硬件的情况下处理性能问题?像指令,缓存等等。当我们纯粹从数学的角度谈论算法时,它们确实是无关紧要的。但是,我认为在处理性能时应该考虑到这一点。
  • 棋盘模式是基于位扫描的算法表现最差的地方。在这种情况下,它比两次通过要慢得多。 (尽管我的算法在大多数情况下似乎更快,但这并不意味着它们总是更快。)我提到的我的替代算法(你可以在这里找到它的名称 BMS,github.com/attltb/connected_component_labeling),但是,它们很好地处理了它们.
  • 我没有检查所有的 CCL 算法,但它与我知道的任何算法都不同,即使我不使用位操作。人们可能想出了相同的算法,但他们会简单地放弃这个想法,因为它比两次扫描慢得多。我不认为算法本身可以有竞争力的CCL算法而不考虑到它可以更好地优化。
  • 这听起来很合理。格式为 11101110111011101、10111011101110111(每行移动两行)的行会导致大量合并的情况如何? (对于 4 连接)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-29
  • 2019-04-06
  • 2012-04-19
  • 1970-01-01
  • 2011-05-12
相关资源
最近更新 更多