C# 中轻松但危险的灰度转换






4.60/5 (17投票s)
2006 年 4 月 17 日
3分钟阅读

173221
本文介绍了一种使用指针运算快速将图像转换为灰度图像的方法。
引言
我当时正忙于用 C# 编写运动检测算法。为了确定运动,我比较了前一张图像的变化值,通过计算像素差的绝对值。 这样做,灰度更简单,可能也更快。
背景
如果麻省理工学院认为使用灰度进行运动检测是个好主意,那么我的运动检测算法肯定走在正确的轨道上。 参见 Cog 项目作为参考。
我从这里获得了图像循环的代码,因为我最初尝试使用 SetPixel
,速度非常慢。
使用代码
如果在多线程环境中使用此代码,请注意,参数图像将被锁定,直到此方法将其解锁。 当另一个代码尝试锁定位图时(例如,客户 OnPaint
),这将引发异常。 为了避免这种情况,您可以始终制作位图的副本。
这是完整的方法
public Bitmap processImage(Bitmap image){
Bitmap returnMap = new Bitmap(image.Width, image.Height,
PixelFormat.Format32bppArgb);
BitmapData bitmapData1 = image.LockBits(new Rectangle(0, 0,
image.Width, image.Height),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb);
BitmapData bitmapData2 = returnMap.LockBits(new Rectangle(0, 0,
returnMap.Width, returnMap.Height),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb);
int a = 0;
unsafe {
byte* imagePointer1 = (byte*)bitmapData1.Scan0;
byte* imagePointer2 = (byte*)bitmapData2.Scan0;
for(int i = 0; i < bitmapData1.Height; i++) {
for(int j = 0; j < bitmapData1.Width; j++) {
// write the logic implementation here
a = (imagePointer1[0] + imagePointer1[1] +
imagePointer1[2])/3;
imagePointer2[0] = (byte)a;
imagePointer2[1] = (byte)a;
imagePointer2[2] = (byte)a;
imagePointer2[3] = imagePointer1[3];
//4 bytes per pixel
imagePointer1 += 4;
imagePointer2 += 4;
}//end for j
//4 bytes per pixel
imagePointer1 += bitmapData1.Stride -
(bitmapData1.Width * 4);
imagePointer2 += bitmapData1.Stride -
(bitmapData1.Width * 4);
}//end for i
}//end unsafe
returnMap.UnlockBits(bitmapData2);
image.UnlockBits(bitmapData1);
return returnMap;
}//end processImage
PixelFormat
至关重要。 此格式确定位图数据的布局。 这就是为什么文字 4 是硬编码的(实际上,它是硬编码的,因为我很懒)。 如果您使用不同的 PixelFormat
,您需要确定这些格式的布局和偏移量。
BitmapData bitmapData1 = image.LockBits(new Rectangle(0, 0,
image.Width, image.Height),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb);
BitmapData bitmapData2 = returnMap.LockBits(new Rectangle(0, 0,
returnMap.Width, returnMap.Height),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
此代码锁定位图并返回其图像数据。 由于我要将整个图像转换为灰度,所以我锁定整个位图。 可以锁定较小的区域。
byte* imagePointer1 = (byte*)bitmapData1.Scan0;
byte* imagePointer2 = (byte*)bitmapData2.Scan0;
指针运算在 C# 中是不安全的 :(. 太糟糕了,因为它很快! 虽然在 C# 中出现访问冲突错误很奇怪。 如果您更改代码,请使用数学方法进行彻底测试,以确保您不会覆盖任何受保护的内存。 Scan0
是指向位图数据第一个字节的指针。
for(int i = 0; i < bitmapData1.Height; i++) {
for(int j = 0; j < bitmapData1.Width; j++) {
精明的人会立即认识到这仅在位图大小相同时才有效。 幸运的是,总是这样,因为此方法将基于输入位图创建 returnMap
位图。 不正确的循环也会导致访问冲突。
imagePointer2[0] = (byte)a; //Array index 0 is blue
imagePointer2[1] = (byte)a; //Array index 1 is green
imagePointer2[2] = (byte)a; //Array index 2 is red
imagePointer2[3] = imagePointer1[3]; //Array index 3 is alpha
请参阅上面代码片段中的注释,以了解位图数据的布局。
a = (imagePointer1[0] + imagePointer1[1] + imagePointer1[2])/3;
对原始位图中的三个颜色分量求平均值。 保持 alpha 不变,否则您的灰度也会导致不希望的混合。
imagePointer1 += 4;
imagePointer2 += 4;
向前移动 4 个字节
imagePointer1 += bitmapData1.Stride - (bitmapData1.Width * 4);
imagePointer2 += bitmapData1.Stride - (bitmapData1.Width * 4);
这绝对是最令人困惑的部分。 请参阅 此处,了解正在发生的事情的图片。 位图的宽度实际上是组合宽度和一个未使用的缓冲区,用于填充位图,使其成为 4 的倍数。宽度 + 缓冲区 = 步幅。 它可以工作,对我来说足够了。
returnMap.UnlockBits(bitmapData2);
image.UnlockBits(bitmapData1);
return returnMap;
UnlockBits
解锁位图数据,可能是一件好事。
关注点
找到难题答案的好地方:Google。
比我聪明得多的人:麻省理工学院 Cog 项目。
可能的错误
我使用来自 bitmapData1
的位图数据用于 bitmapData1
和 bitmapData2
。 如果数据以某种方式不同,这可能会导致意外错误。 但是,在测试中,一切正常。 更加谨慎的编码器会在将此类代码放入任何关键内容之前验证很多事情。
如果还有更多错误,我非常确定你们当中过于挑剔的人会很乐意指出它们作为明显的批评。