C#中彩色图像转换灰度图的几种方法
前言
为加快处理速度,在图像处理算法中,往往需要把彩色图像抓换成灰色图像,24位彩色图像每个像素用3个字节表示,每个字节对应着R、G、B分量的亮度(红、绿、蓝)。当R、G、B分量值不同是,表示为彩色图像;当R、G、B分量值相同时,表现为灰度图像,也就是求这个值。
公式
一般来说,转换公司有3中。第一种转换公式为:
其中,Gray(i,j)为转换后的灰度图像在(i,j)点处的灰度值。该方面虽然简单,但人眼对颜色的感应是不同的,因此有了第二种转换公式:
观察上面的公司,发现绿色所占的比重最大,所以转换时可以直接使用G 值作为转换后的灰度:
Bitmap类介绍
Bitmap对象封装了GDI+中的一个位图,该位图由图形图像及其属性的像素数据组成。因此Bitmap是用于处理由像素数据定义的图像的对象。该类的主要方法和属性如下:
- GetPixel方法和 SetPixel方法: 获取和设置一个图像的指定像素的颜色。
- PixelFormat : 返回图像的像素格式
- Palette : 获取或设置图像所使用的颜色调色板
- Height 、Width : 返回图像的高度和宽度
- LockBits 、UnlockBits : 分别锁定和解锁系统内存中的位图像素。
在基于像素点的图像处理方法中使用LockBits 和 UnlockBits是一个很好的方式,这两种方法可以使我们通过指定像素的范围来控制位图的任意一部分,从而消除了通过循环对位图的像素逐个进行处理的需要。每次调用LockBits之后都应该调用一次UnlockBits。
图像处理的3种方法
- 提取像素法
该方法使用的是GDI+中的 Bitmap.GetPixel和 Bitmap.SetPixel方法。为了将位图的颜色设置为灰度或其他颜色,就需要使用GetPixel来读取当前像素的颜色,再计算灰度值,最后使用SetPixel来应用新的颜色。代码如下:
//加载图像
var curBitmap = (Bitmap)Image.FromFile(filePath);
Color curColor;
int ret;
//循环读取像素转换灰度值
for (int i = 0; i < curBitmap.Width; i++)
{
for (int j = 0; j < curBitmap.Height ; j++)
{
curColor = curBitmap.GetPixel(i,j);
ret = (int)(curColor.R * 0.299 + curColor.G * 0.587 + curColor.B * 0.114);
curBitmap.SetPixel(i, j, Color.FromArgb(ret, ret, ret));
}
}
- 内存法
该方法就是把图像数据直接复制到内存中,这样就使程序的运行速度大大提高。 代码如下:
//加载图像
var curBitmap = (Bitmap)Image.FromFile(filePath);
Rectangle rect = new Rectangle(0, 0, curBitmap.Width, curBitmap.Height);
System.Drawing.Imaging.BitmapData bmpData = curBitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmap.PixelFormat);
IntPtr ptr = bmpData.Scan0;
int bytes = curBitmap.Width * curBitmap.Height * 3;
byte[] rgbValues = new byte[bytes];
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
double colorTemp = 0;
for (int i = 0; i < rgbValues.Length; i += 3)
{
colorTemp = rgbValues[i + 2] * 0.299 + rgbValues[i + 1] * 0.587 + rgbValues[i] * 0.114;
rgbValues[i] = rgbValues[i + 1] = rgbValues[i + 2] = (byte)colorTemp;
}
System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);
curBitmap.UnlockBits(bmpData);
- 指针法
该方法与内存法相似,开始都是通过LockBits方法来获取位图的首地址。但该方法更简洁,直接应用指针对位图进行操作。
为了保持类型安全,在默认情况下,C#是不支持指针运算的,因为使用指针会带来相关的风险。所以C#只允许在特别标记的代码块中使用指针。通过使用unsafe关键字,可以定义可使用指针的不安全上下文。
//加载图像
var curBitmap = (Bitmap)Image.FromFile(filePath);
Rectangle rect = new Rectangle(0, 0, curBitmap.Width, curBitmap.Height);
System.Drawing.Imaging.BitmapData bmpData = curBitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmap.PixelFormat);
byte temp = 0;
unsafe
{
byte* ptr = (byte*)(bmpData.Scan0);
for (int i = 0; i < bmpData.Height; i++)
{
for (int j = 0; j < bmpData.Width; j++)
{
temp = (byte)(0.299 * ptr[2] + 0.587 * ptr[1] + 0.114 * ptr[0]);
ptr[0] = ptr[1] = ptr[2] = temp;
ptr += 3;
}
ptr += bmpData.Stride - bmpData.Width * 3;
}
}
curBitmap.UnlockBits(bmpData);
效果预览
彩色图像:
灰度图像:
总结
内存法和指针法比提取像素法要快得多。提取像素法应用GDI+中的方法,易于理解,方法简单,很适合于C#的初学者使用,但它的运行速度最慢,效率最低。内存法把图像复制到内存中,直接对内存中的数据进行处理,速度明显提高,程序难度也不大。指针法直接应用指针来对图像进行处理,所以速度最快。但在C#中,是不建议使用指针的,因为使用指针,代码不仅难以编写和调试,而且无法通过 CLR的内存类型安全检查,不能发挥C#的特长。只有对C#和指针有了充分的理解,才能用好该方法。究竟要使用哪种方法,还要看具体情况而定。但3种方法都能有效地对图像进行处理。