您是否真的将浮点数作为百分比传递给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;
}
这将减少您在每个像素上所做的工作量。我仍然不喜欢它,因为我尽量避免在每像素循环的中间进行方法调用,尤其是每像素 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
它说的是(这会很长):
- 获取 p1 指向的字节并保留它
- 将 1 添加到 p1(这是 ++ - 它使指针指向下一个字节)
- 获取 p2 指向的字节并保留它
- 将 1 添加到 p2
- 从步骤 1 中减去步骤 3
- 将其传递给
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 行以上读取”的情况,这消除了一大类潜在的错误,这些错误将涉及在合法内存块之外读取,这在许多运行时条件下可能是良性的。