过滤 1bpp 图像

Posted

技术标签:

【中文标题】过滤 1bpp 图像【英文标题】:Filtering 1bpp images 【发布时间】:2012-06-21 20:14:15 【问题描述】:

我希望使用 3x3 过滤器过滤每像素 1 位的图像:对于每个输入像素,如果周围像素的加权和(权重由过滤器确定),则相应的输出像素设置为 1超过某个阈值。

我希望这比转换为 8 bpp 然后过滤它更有效,但我想不出一个好方法来做到这一点。一种简单的方法是跟踪指向字节的九个指针(三个连续的行以及指向每行中当前字节任一侧的指针,用于计算这些字节中第一个和最后一个位的输出)和每个输入像素计算

sum = filter[0] * (lastRowPtr & aMask > 0) + filter[1] * (lastRowPtr & bMask > 0) + ... + filter[8] * (nextRowPtr & hMask > 0),

为字节边缘的位添加额外的 faff。但是,这很慢,而且看起来真的很难看。每个字节有 8 个像素这一事实并没有使您获得任何并行性,而是不得不做大量额外的工作来屏蔽事物。

对于如何最好地完成此类事情,有什么好的资源吗?这个特定问题的解决方案将是惊人的,但我很高兴有人指出任何在 C/C++ 中对 1bpp 图像进行高效图像处理的示例。我想在将来用 1 bpp 算法替换更多 8 bpp 的东西,以避免图像转换和复制,所以任何关于这方面的一般资源将不胜感激。

【问题讨论】:

您的图像宽度是 8 的倍数,还是填充行以使每行都从字节边界开始?这通常是这种情况,它使事情变得更容易一些。你只需要 3 个口罩而不是 9 个。 图像宽度是任意的,但每一行都从一个字节边界开始。行可以填充到更大的边界,但保证填充到字节。 边缘的填充方法是什么?重复还是钳制? 我不太担心最外层像素环会发生什么。任何明智的事情都可以。 不,我的意思是逻辑上,在你的卷积期间。什么是必需的行为?超出边缘的像素是否等于边缘的最后一个像素?还是该行/列另一侧的第一个像素? 【参考方案1】:

几年前,我发现将位解包为字节,进行过滤,然后将字节打包回位比直接处理位要快。这似乎违反直觉,因为它是 3 个循环而不是 1 个,但每个循环的简单性足以弥补它。

我不能保证它仍然是最快的;编译器,尤其是处理器很容易发生变化。然而,简化每个循环不仅使优化更容易,而且更易于阅读。这一定是值得的。

解包到单独的缓冲区的另一个优点是它为您在边缘执行的操作提供了灵活性。通过使缓冲区比输入大 2 个字节,您可以从字节 1 开始解包,然后将字节 0 和 n 设置为您喜欢的任何值,并且过滤循环根本不必担心边界条件。

【讨论】:

好的,谢谢。我希望我可以通过不解包更快地做一些事情,但看起来我最好坚持现有的代码。 8 bpp 图像的代码已经进行了相当好的优化,因此听起来要通过 1 bpp 操作来解决这个问题需要做很多工作。【参考方案2】:

查看可分离的过滤器。除其他外,它们允许在它们工作的情况下实现大规模并行。

例如,在您的 3x3 样本权重和过滤器案例中:

    将 1x3(水平)像素样本放入缓冲区。这可以针对每个像素单独完成,因此 1024x1024 图像可以同时运行 1024^2 个任务,所有这些任务都执行 3 个样本。 从缓冲区中采样 3x1(垂直)像素。同样,这可以同时在每个像素上完成。 使用缓冲区的内容从原始纹理中剔除像素。

这种方法在数学上的优势在于它将样本操作的数量从n^2 减少到2n,尽管它需要一个与源相同大小的缓冲区(如果您已经在执行复制,可以用作缓冲区;您不能修改步骤 2) 的原始源。为了保持2n 的内存使用,您可以一起执行步骤 2 和 3(这有点棘手,并不完全令人愉快);如果内存不是问题,您可以在两个缓冲区(source、hblur、vblur)上花费3n

由于每个操作都在与不可变源完全隔离的情况下工作,因此如果您有足够的内核,您可以同时对每个像素执行过滤。或者,在更现实的场景中,您可以利用分页和缓存来加载和处理单个列或行。这在处理奇数步幅、行尾填充等时很方便。第二轮样本(垂直)可能会破坏您的缓存,但最糟糕的是,一轮将是缓存友好的,您已经将处理从指数切割为线性。

现在,我还没有具体谈到以位存储数据的情况。这确实使事情变得稍微复杂了一点,但并不是非常复杂。假设您可以使用滚动窗口,例如:

d = s[x-1] + s[x] + s[x+1]

有效。有趣的是,如果您在步骤 1 的输出过程中将图像旋转 90 度(微不足道,读取时来自 (y,x) 的样本),您可以为任何样本加载最多两个水平相邻的字节,并且只加载一个字节大约 75% 的时间。这在读取过程中对缓存的友好度降低了一点,但大大简化了算法(足以让它重新获得损失)。

伪代码:

buffer source, dest, vbuf, hbuf;

for_each (y, x)   // Loop over each row, then each column. Generally works better wrt paging

    hbuf(x, y) = (source(y, x-1) + source(y, x) + source(y, x+1)) / 3   // swap x and y to spin 90 degrees

for_each (y, x)

    vbuf(x, 1-y) = (hbuf(y, x-1) + hbuf(y, x) + hbuf(y, x+1)) / 3    // 1-y to reverse the 90 degree spin

for_each (y, x)

    dest(x, y) = threshold(hbuf(x, y))

访问字节内的位(source(x, y) 表示访问/样本)相对简单,但在这里写出来有点痛苦,所以留给读者。该原理,特别是以这种方式实现的(旋转 90 度),每次只需要 2 次 n 样本,并且总是从紧邻的位/字节中采样(不需要您计算下一个位的位置排)。总而言之,它比任何替代方案都更快、更简单。

【讨论】:

很少有过滤器是可分离的,对于 3x3 来说几乎不值得。 大多数盒子过滤器都是,并且在处理存储在字节中的位的情况下,避免从下一行中找到一些任意位是非常值得的。将滤波器分离为 3x3 的纯性能增益很小(仅在 5x5 左右之前不值得这样做),但位的东西在这里更有用。 我认为既然明确提到了过滤器权重,它不是盒式过滤器。我想每个重量都可能是1/9... 如果没有,有几种方法可以使用不均匀的权重半分离。如果 OP 无法拆分过滤器,那将是一种耻辱,因为我不确定在没有它的情况下保持旋转和单字节部分的方法(IMO 可能非常好)。必须等待他们的澄清。【参考方案3】:

而不是将整个图像扩展到 1 位/字节(或 8bpp,本质上,正如您所指出的),您可以简单地扩展当前窗口 - 读取第一行的第一个字节,移位和掩码,然后读出你需要的三个位;对其他两行做同样的事情。然后,对于下一个窗口,您只需丢弃左列并从每一行中再获取一位。做到这一点的逻辑和代码并不像简单地扩展整个图像那么简单,但它会占用更少的内存。

作为中间立场,您可以扩展您当前正在处理的三行。这样编码可能更容易。

【讨论】:

以上是关于过滤 1bpp 图像的主要内容,如果未能解决你的问题,请参考以下文章

P1066 图像过滤

1bpp 单色 BMP

使用 CoreImage 过滤图像会导致图像旋转

图像过滤器

iOS - 创建核心图像过滤器循环时保留原始图像

核心图像过滤器崩溃问题