比Lock Bits更快的图像处理方法

4
我一直在使用C#编写边缘检测程序,并且最近使用了lock bits来加速它的运行。然而,lockBits仍然不够快。虽然问题可能出在我的算法上,但我也想知道是否有比lockBits更好的图像处理方法。
如果问题出在算法上,这里有一个简单的解释。遍历颜色数组(使用lockbits创建,代表像素),对于每个颜色,检查其周围的8个像素的颜色。如果这些像素与当前像素不够匹配,则将当前像素视为边缘。
以下是判断像素是否为边缘的基本代码。它接受一个包含9种颜色的Color[]数组,其中第一个颜色是要检查的像素。
public Boolean isEdgeOptimized(Color[] colors)
{
    //colors[0] should be the checking pixel
    Boolean returnBool = true;
    float percentage = percentageInt; //the percentage used is set
    //equal to the global variable percentageInt

    if (isMatching(colors[0], colors[1], percentage) &&
            isMatching(colors[0], colors[2], percentage) &&
            isMatching(colors[0], colors[3], percentage) &&
            isMatching(colors[0], colors[4], percentage) &&
            isMatching(colors[0], colors[5], percentage) &&
            isMatching(colors[0], colors[6], percentage) &&
            isMatching(colors[0], colors[7], percentage) &&
            isMatching(colors[0], colors[8], percentage))
    {
        returnBool = false;
    }
    return returnBool;
}

这段代码适用于每个像素,使用lockbits获取颜色。

所以基本上问题是,如何让我的程序运行更快?是我的算法有问题,还是有比lockBits更快的方法?

顺便说一下,该项目在gitHub上,这里


1
刚刚编辑过,抱歉之前表达不够清晰。 - vkoves
1
请发布您的代码。我们不想在Github上浏览您的项目以找到相关的部分。 - leonbloy
问题不是“有什么比lockBits更快?”LockBits在实际中必要,以获得对图像的低级访问,并将像素作为数组元素进行操作。 - leonbloy
2
“LockBits”会给你一个字节数组,可以直接访问。这是最快的访问数据的方式。如果你想让别人查看你的算法,请在此处发布相关部分,或者至少告诉我们您要查看哪个GitHub项目中的文件。不要让我们去查找数十个无关的文件,以找到你所询问的内容。 - Jim Mischel
2
你应该使用性能分析器来找出算法中哪一部分运行缓慢,然后对该部分进行优化。 - Pete Baughman
显示剩余3条评论
4个回答

6
你是真的把浮点数作为百分比传递给了isMatching吗?
我查看了你在GitHub上的isMatching代码,嗯,有点糟糕。你是从Java移植的,对吧?C#使用bool而不是Boolean,虽然我不能确定,但我不喜欢那些做了这么多装箱和拆箱的代码。此外,在你不需要时,你正在进行大量的浮点乘法和比较:
public static bool IsMatching(Color a, Color b, int percent)
{
    //this method is used to identify whether two pixels, 
    //of color a and b match, as in they can be considered
    //a solid color based on the acceptance value (percent)

    int thresh = (int)(percent * 255);

    return Math.Abs(a.R - b.R) < thresh &&
           Math.Abs(a.G - b.G) < thresh &&
           Math.Abs(a.B - b.B) < thresh;
}

这将减少每个像素的工作量。我不喜欢它,因为我尽量避免在每个像素循环中进行方法调用,特别是在8x每个像素循环中。我将该方法设置为静态以减少传递未使用的实例。仅这些更改可能会使您的性能翻倍,因为我们只进行了1次乘法,没有装箱,并且现在使用了&&的内在短路来减少工作量。
如果我在做这件事,我更倾向于做这样的事情:
// assert: bitmap.Height > 2 && bitmap.Width > 2
BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                      ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

int scaledPercent = percent * 255;
unsafe {
    byte* prevLine = (byte*)data.Scan0;
    byte* currLine = prevLine + data.Stride;
    byte* nextLine = currLine + data.Stride;

    for (int y=1; y < bitmap.Height - 1; y++) {

       byte* pp = prevLine + 3;
       byte* cp = currLine + 3;
       byte* np = nextLine + 3;
       for (int x = 1; x < bitmap.Width - 1; x++) {
           if (IsEdgeOptimized(pp, cp, np, scaledPercent))
           {
               // do what you need to do
           }
           pp += 3; cp += 3; np += 3;
       }
       prevLine = currLine;
       currLine = nextLine;
       nextLine += data.Stride;
    }
}

private unsafe static bool IsEdgeOptimized(byte* pp, byte* cp, byte* np, int scaledPecent)
{
    return IsMatching(cp, pp - 3, scaledPercent) &&
           IsMatching(cp, pp, scaledPercent) &&
           IsMatching(cp, pp + 3, scaledPercent) &&
           IsMatching(cp, cp - 3, scaledPercent) &&
           IsMatching(cp, cp + 3, scaledPercent) &&
           IsMatching(cp, np - 3, scaledPercent) &&
           IsMatching(cp, np, scaledPercent) &&
           IsMatching(cp, np + 3, scaledPercent);
}

private unsafe static bool IsMatching(byte* p1, byte* p2, int thresh)
{
    return Math.Abs(p1++ - p2++) < thresh &&
           Math.Abs(p1++ - p2++) < thresh &&
           Math.Abs(p1 - p2) < thresh;
}

现在它会进行各种可怕的指针操作,以减少数组访问等。如果所有这些指针操作让您感到不舒服,您可以为prevLine、currLine和nextLine分配字节数组,并在每行进行Marshal.Copy。
算法如下:从左上角开始一个像素,遍历图像中除外部边缘(没有边缘条件!太棒了!)。我保留对每行的开始 prevLine、currLine 和 nextLine 的指针。然后当我开始 x 循环时,我创建 pp、cp、np,它们是前一个像素、当前像素和下一个像素。 当前像素是我们关心的像素。pp 是其正上方的像素,np 是其正下方的像素。 我将它们传递给 IsEdgeOptimized,它查看 cp 周围并调用 IsMatching。
现在这都假设每个像素为 24 位。如果您查看每个像素为 32 位,则其中所有奇怪的 3 都需要成为 4,但代码并没有改变。如果您愿意,可以将每像素的字节数作为参数化,以便处理任何像素。
FYI,在像素通道中通常为 b、g、r、(a)。
颜色在内存中作为字节存储。 如果您的位图是 24 位图像,则实际的位图存储为一块字节。扫描线为 data.Stride 字节宽,至少与一行中像素的 3 倍一样大(它可能会更大,因为扫描线通常是填充的)。
当我在 C# 中声明类型为 byte* 的变量时,我正在做一些事情。首先,我说这个变量包含内存中字节位置的地址。其次,我说我将要违反 .NET 中的所有安全措施,因为我现在可以读取和写入内存中的任何字节,这可能很危险。
因此,当我有类似以下的内容:
Math.Abs(*p1++ - *p2++) < thresh

它的意思是(而且这会很长):
  1. 获取p1指向的字节并保留它
  2. p1加1(这是++ - 它使指针指向下一个字节)
  3. 获取p2指向的字节并保留它
  4. p2加1
  5. 将步骤3从步骤1中减去
  6. 将结果传递给Math.Abs
其背后的原因是,从历史上看,读取字节内容和前进是一个非常常见的操作,许多CPU将其构建为一对指令的单个操作,这些指令可以流水线化为单个周期左右。
当我们进入IsMatching时,p1指向像素1,p2指向像素2,并且在内存中它们的排列方式如下:
p1    : B
p1 + 1: G
p1 + 2: R

p2    : B
p2 + 1: G
p2 + 2: R

因此,IsMatching在遍历内存时只是做了绝对差。

您的后续问题告诉我您并不真正了解指针。那没关系——您可能会学会。说实话,这些概念并不难,但问题是如果没有太多经验,您很可能会犯错误,所以也许您应该考虑在代码上使用性能分析工具,调整最糟糕的热点,就行了。

例如,您会注意到,我从第一行到倒数第二行,从第一列到倒数第二列进行查找。这是故意为之,以避免处理“我无法读取第0行以上”的情况,从而消除涉及读取非法内存块的潜在错误,因为这可能在许多运行时条件下是无害的。


你能解释一下你每个变量的含义以及星号的用法吗?你是否以某种方式将颜色存储为字节? - vkoves
指针绝对是在C#中进行图像处理的方法,但我会避免在嵌套循环中调用函数。这在C#中效率不高。总的来说,我发现在C#中进行图像操作至少需要比等效的C++函数多花费两倍的时间。如果你真的需要效率,我建议要么从C#调用自己的本地C++函数,要么使用OpenCV的C#包装器之一 - 这非常快速。 - morishuz
你的isMatching语句对我来说没有意义。Math.Abs(*p1 ++ - p2 ++)<thresh和Math.Abs(p1 ++ - p2 ++)<thresh是相同的,那么为什么要使用两次?它应该是Math.Abs(p1 + 2- *p2 + 2)<thresh吗? - vkoves
因为 ++ 会影响指针。在 ++ 执行后,实际的指针值是不同的。这就像 Math.Abs(arr1[i++] - arr[j++]) < thresh。 - plinth

5

使用/unsafe标志进行编译,将方法标记为不安全的,在替换复制到byte[]的情况下,使用Marshal.Copy来避免每个图像都要复制到byte[],然后再复制到Color[],为每个像素创建另一个临时的Color[9],再使用SetPixel设置颜色。

using (byte* bytePtr = ptr)
{
    //code goes here
}

请确保您用正确的字节设置替换SetPixel调用。这不是LockBits的问题,您需要LockBits,问题在于您对处理图像相关的其他所有内容都不够高效。


你能详细解释一下你的实现吗?我不确定该如何更改我的代码来使用你展示的内容。 - vkoves
你正在使用Marshal.Copy将整个图像复制到一个单独分配的byte[],然后将byte[]复制到另一个单独分配的Color[],使用了原始图像3倍的内存,并且花费了大量时间进行复制。相反,你应该将IntPtr转换为byte*并直接操作指针。这样可以避免对图像进行任何复制,并且是访问图像数据最快的方法。 - Robert Rouhani
这很有道理,但你能解释一下什么是byte*吗?星号只是一个特殊的修饰符吗? - vkoves
@vkoves:SetPixel 是一个巨大的时间浪费。如果你正在频繁使用它,那么这可能是你最大的性能问题。byte* 是一个指针。你需要了解不安全代码和指针:http://msdn.microsoft.com/en-us/library/vstudio/t2yzs44b.aspx - Jim Mischel
谢谢,这解释了很多。 - vkoves

0

0
你可以将图像分成10个位图,分别处理每一个,最后再合并它们(只是一个想法)。

1
当你说“分割图像”时,你会怎样做? - d219

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接