4

我一直在用 C# 编写一个边缘检测程序,为了让它运行得更快,我最近让它使用了锁定位。但是,lockBits 仍然没有我希望它运行的那么快。虽然问题可能是我的一般算法,但我也想知道是否有比 lockBits 更好的东西可以用于图像处理。

如果问题是算法,这里有一个基本的解释。遍历一个颜色数组(使用代表像素的锁定位制作),对于每种颜色,检查该像素周围八个像素的颜色。如果这些像素与当前像素的匹配不够紧密,则将当前像素视为边缘。

这是定义像素是否为边缘的基本代码。它接受九种颜色的 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 更快的东西?

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

4

4 回答 4

6

您是否真的将浮点数作为百分比传递给isMatching

我在 GitHub 上查看了您的 isMatching 代码,好吧,哎呀。你是从 Java 移植过来的,对吧?C# 不使用boolBoolean虽然我不确定,但我不喜欢执行那么多装箱和拆箱的代码的外观。此外,当您不需要时,您正在执行大量浮点乘法和比较:

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;
}

这将减少您在每个像素上所做的工作量。我仍然不喜欢它,因为我尽量避免在每像素循环的中间进行方法调用,尤其是每像素 8 倍的循环。我将方法设为静态以减少传入的未使用的实例。仅这些更改可能会使您的性能翻倍,因为我们只做 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,但除此之外,代码不会改变。如果需要,您可以参数化每个像素的字节数,以便它可以处理任何一个。

仅供参考,像素中的通道通常是 b, g, r, (a)。

颜色以字节形式存储在内存中。您的实际位图(如果是 24 位图像)存储为字节块。扫描线是data.Stride字节宽,至少是 3 * 一行中的像素数(它可能更大,因为扫描线经常被填充)。

当我在 C# 中声明一个类型的变量时byte *,我正在做一些事情。首先,我说这个变量包含内存中字节位置的地址。其次,我是说我即将违反 .NET 中的所有安全措施,因为我现在可以在内存中读取和写入任何字节,这可能很危险。

所以当我有类似的东西时:

Math.Abs(*p1++ - *p2++) < thresh

它说的是(这会很长):

  1. 获取 p1 指向的字节并保留它
  2. 将 1 添加到 p1(这是 ++ - 它使指针指向下一个字节)
  3. 获取 p2 指向的字节并保留它
  4. 将 1 添加到 p2
  5. 从步骤 1 中减去步骤 3
  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 行以上读取”的情况,这消除了一大类潜在的错误,这些错误将涉及在合法内存块之外读取,这在许多运行时条件下可能是良性的。

于 2013-04-18T19:39:33.447 回答
5

而不是将每个图像复制到 a byte[],然后复制到 a Color[],为每个像素创建另一个临时Color[9]值,然后使用SetPixel设置颜色,使用/unsafe标志编译,将方法标记为不安全,将复制到 a 替换byte[]Marshal.Copy

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

确保将 SetPixel 调用替换为设置正确的字节。这不是 LockBits 的问题,您需要 LockBits,问题是您在处理图像相关的所有其他事情上效率低下。

于 2013-04-18T18:28:05.573 回答
0

如果要使用并行任务执行,可以使用 System.Threading.Tasks 命名空间中的 Parallel 类。以下链接有一些示例和解释。

于 2014-09-03T11:46:53.227 回答
0

您可以将图像拆分为 10 个位图并处理每个位图,然后最后将它们组合起来(只是一个想法)。

于 2018-04-08T19:27:40.237 回答