项目需要将RGB图像转换为8位灰度图像,之前不了解图像格式,以为只要对像素进行灰度化就能获得灰度图像,以下代码使用System.Drawing.Imaging.ColorMatrix 类配合System.Drawing.Imaging.ImageAttributes 类对组成一个5 x 5的线性转换,转换 ARGB 单色值,再使用GDI+获得新图像。
public static Bitmap MakeGrayScale(Bitmap original) { Bitmap newBitmap = new Bitmap(original.Width, original.Height, PixelFormat.Format24bppRgb); Graphics g = Graphics.FromImage(newBitmap); ColorMatrix colorMatrix = new ColorMatrix( new float [][] { new float [] { .3f, .3f, .3f, 0, 0 }, new float [] { .59f, .59f, .59f, 0, 0 }, new float [] { .11f, .11f, .11f, 0, 0 }, new float [] { 0, 0, 0, 1, 0 }, new float [] { 0, 0, 0, 0, 1 } }); ImageAttributes attributes = new ImageAttributes(); attributes.SetColorMatrix(colorMatrix); g.DrawImage(original, new Rectangle(0, 0, original.Width, original.Height), 0, 0, original.Width, original.Height, GraphicsUnit.Pixel, attributes); g.Dispose(); return newBitmap; }
这种方法无需手动操作图像数据,也不用考虑图像扫描宽度等因素,能非常高效、鲁棒的进行灰度化,但是,灰度化后依旧是RGB图像,PixelFormat值依旧为Format24bppRgb。只不过三个通道的值都变成了T = 0.3R + 0.59G + 0.11B。
图1. 灰度化后依旧是RGB图像,PixelFormat值
需要的8位灰度图像的PixelFormat值为Format8bppIndexed,该格式指定每像素8位,因此不方便将RGB图像直接修改成8位灰度图像,需要创建一个新的8位灰度图像。
Format8bppIndexed为索引格式,已经创建索引。因此颜色表中有 256 种颜色。实际是伪彩颜色。可以看到灰度图像的调色板是灰度的,即Palette.Entries中每个项的RGB值都相等。因此,创建一个新的8位灰度图像是不够的,还需要修改灰度位图的索引表。
不修改索引表的话,有些操作后图像可能出现类似红外图像那样色彩斑斓的伪彩图像。如下面的实例所示:
(1)随便在桌面截取一幅任意尺寸的图像,先进行灰度化,然后用画图程序将灰度化后的RGB图像(Format24bppRgb,256灰度)直接转化为256色灰度图像。
图3.用画图程序将一幅灰度化后的RGB图像转化为256色灰度
转化后参数如下图所示,可以发现图像格式已变为Format8bppIndexed索引格式,但是图像的索引表中每项的RGB值不相同。
图4. 用画图程序转化为256色灰度后图像的参数值
(2)用转化后的图像进行直方图均衡化,可以看到出现了伪彩色。因为转化后“灰度”图像的Format8bppIndexed的索引表并非灰度,而是伪彩的。
以下算法 先将RGB(以Format24bppRgb为例)图像灰度化,然后得到灰度图像的灰度数组,最后构建一个8位灰度图像。
public static Bitmap RgbToGrayScale(Bitmap original) { if (original != null ) { Rectangle rect = new Rectangle(0, 0, original.Width, original.Height); BitmapData bmpData = original.LockBits(rect, ImageLockMode.ReadOnly, original.PixelFormat); int width = bmpData.Width; int height = bmpData.Height; int stride = bmpData.Stride; int offset = stride - width * 3; IntPtr ptr = bmpData.Scan0; int scanBytes = stride * height; int posScan = 0, posDst = 0; byte [] rgbValues = new byte [scanBytes]; Marshal.Copy(ptr, rgbValues, 0, scanBytes); byte [] grayValues = new byte [width * height]; for ( int i = 0; i < height; i++) { for ( int j = 0; j < width; j++) { double temp = rgbValues[posScan++] * 0.11 + rgbValues[posScan++] * 0.59 + rgbValues[posScan++] * 0.3; grayValues[posDst++] = (byte )temp; } posScan += offset; } Marshal.Copy(rgbValues, 0, ptr, scanBytes); original.UnlockBits(bmpData); Bitmap retBitmap = BuiltGrayBitmap(grayValues, width, height); return retBitmap; } else { return null ; } } private static Bitmap BuiltGrayBitmap( byte [] rawValues, int width, int height) { Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format8bppIndexed); BitmapData bmpData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); int offset = bmpData.Stride - bmpData.Width; IntPtr ptr = bmpData.Scan0; int scanBytes = bmpData.Stride * bmpData.Height; byte [] grayValues = new byte [scanBytes]; int posSrc = 0, posScan = 0; for ( int i = 0; i < height; i++) { for ( int j = 0; j < width; j++) { grayValues[posScan++] = rawValues[posSrc++]; } posScan += offset; } Marshal.Copy(grayValues, 0, ptr, scanBytes); bitmap.UnlockBits(bmpData); ColorPalette palette; using (Bitmap bmp = new Bitmap(1, 1, PixelFormat.Format8bppIndexed)) { palette = bmp.Palette; } for ( int i = 0; i < 256; i++) { palette.Entries[i] = Color.FromArgb(i, i, i); } bitmap.Palette = palette; return bitmap; }
用上面算法将数字图像处理常用测试图像之一的PeppersRGB.bmp图像转换为8位灰度图像,并与PeppersRGB.bmp对应的灰度图像Peppers.bmp进行比对。
图6. 重构的8位灰度图像(上)和Peppers.bmp(下)对比
图7. Matlab中使用rgb2gray函数转换的8位灰度图像(上)和Peppers.bmp(下)对比
Matlab中使用rgb2gray函数进行格式转换,源码见rgb2gray.m。rgb2gray的算法原理是将RGB色彩模型转为YIQ模型(北美NTSC彩色制式,灰度信息与彩色信息分离)。YIQ模型中,Y代表亮度、I代表色调、Q代表饱和度。转换后的Y分量即为灰度。
转换公式为:
图8. Matlab.rgb2gray的转换矩阵T
可以看到转换矩阵T的第一行就是灰度转换公式的系数。