Answer

专注于Mobile,WinCE
  首页  :: 新随笔  :: 联系 :: 管理

使用GDI+将24位真彩色图像转换为8位灰度图像

Posted on 2010-10-18 17:49  answer  阅读(1303)  评论(0编辑  收藏  举报

在图像处理中,我们经常需要将真彩色图像转换为黑白图像。严格的讲应该是灰度图,因为真正的黑白图像是二色,即只有纯黑,纯白二色。开始之前,我们先简单补充一下计算机中图像的表示原理。计算机中的图像大致可以分成两类:位图(Bitmap)和矢量图(Metafile)。 位图可以视为一个二维的网格,整个图像就是由很多个点组成的,点的个数等于位图的宽乘以高。每个点被称为一个像素点,每个像素点有确定的颜色,当很多个像 素合在一起时就形成了一幅完整的图像。我们通常使用的图像大部分都是位图,如数码相机拍摄的照片,都是位图。因为位图可以完美的表示图像的细节,能较好的 还原图像的原景。但位图也有缺点:第一是体积比较大,所以人们开发了很多压缩图像格式来储存位图图像,目前应用最广的是JPEG格式,在WEB上得到了广泛应用,另外还有GIF,PNG等 等。第二是位图在放大时,不可避免的会出现“锯齿”现象,这也由位图的本质特点决定的。所以在现实中,我们还需要使用到另一种图像格式:矢量图。同位图不 同,矢量图同位图的原理不同,矢量图是利用数学公式通过圆,线段等绘制出来的,所以不管如何放大都不会出现变形,但矢量图不能描述非常复杂的图像。所以矢 量图都是用来描述图形图案,各种CAD软件等等都是使用矢量格式来保存文件的。

在讲解颜色转换之前,我们要先对位图的颜色表示方式做一了解。位图中通常是用RGB三色方式来表示颜色的(位数很少时要使用调色板) 。所以每个像素采用不同的位数,就可以表示出不同数量的颜色。如下图所示:

每像素的位数

一个像素可分配到的颜色数量

1

2^1 = 2

2

2^2 = 4

4

2^4 = 16

8

2^8 = 256

16

2^16 = 65,536

24

2^24 = 16,777,216

从中我们可以看出,当使用24位色(3个字节)时,我们可以得到1600多万种颜色,这已经非常丰富了

,应该已接近人眼所能分辨的颜色了。现在计算机中使用最多的就是24位色,别外在GDI+中还有一种32位色,多出来的一个通道用来描述Alpha,即透明分量。

24位色中3个字节分别用来描述R,G,B三种颜色分量,我们看到这其中是没有亮度分量的,这是因为在RGB表示方式中,亮度也是直接可以从颜色分量中得到的,每一颜色分量值的范围都是从0到255, 某一颜色分量的值越大,就表示这一分量的亮度值越高,所以255表示最亮,0表示最暗。那么一个真彩色像素点转换为灰度图时它的亮度值应该是多少呢,首先我们想到的平均值,即将R+G+B/3。但现实中我们使用的却是如下的公式:

Y = 0.299R+0.587G+0.114B

这个公式通常都被称为心理学灰度公式。这里面我们看到绿色分量所占比重最大。因为科学家发现使用上述公式进行转换时所得到的灰度图最接近人眼对灰度图的感觉。

因为灰度图中颜色数量一共只有256种(1个字节),所以转换后的图像我们通常保存为8位格式而不是24位格式,这样比较节省空间。而8位图像是使用调色板方式来保存颜色的。而不是直接保存颜色值。调色板中可以保存256颜色,所以可以正好可以将256种灰度颜色保存到调色版中。代码如下:


using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace ConsoleApplication2
{
    class Program
    {
        unsafe static void Main(string[] args)
        {
            Bitmap img = (Bitmap)Image.FromFile(@"E:\My Documents\My Pictures\cherry_blossom_1002.jpg");
            Bitmap bit = new Bitmap(img.Width, img.Height, PixelFormat.Format8bppIndexed);
            BitmapData data = img.LockBits(new Rectangle(0, 0, img.Width, img.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
            byte* bp = (byte*)data.Scan0.ToPointer();
            BitmapData data2 = bit.LockBits(new Rectangle(0, 0, bit.Width, bit.Height), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);
            byte* bp2 = (byte*)data2.Scan0.ToPointer();
            for (int i = 0; i != data.Height; i++)
            {
                for (int j = 0; j != data.Width; j++)
                {
                    //0.3R+0.59G+0.11B
                    float value = 0.11F * bp[i * data.Stride + j * 3] + 0.59F * bp[i * data.Stride + j * 3 + 1] + 0.3F * bp[i * data.Stride + j * 3 + 2];
                    bp2[i * data2.Stride + j] = (byte)value;

                }
            }
            img.UnlockBits(data);
            bit.UnlockBits(data2);
            ColorPalette palette = bit.Palette;
            for (int i = 0; i != palette.Entries.Length; i++)
            {
                palette.Entries[i] = Color.FromArgb(i, i, i);
            }
            bit.Palette = palette;
            bit.Save(@"E:\TEMP\bb.jpeg", ImageFormat.Jpeg);
            img.Dispose();
            bit.Dispose();
        }
    }
}

代码中我使用了指针直接操作位图数据,同样的操作要比使用GetPixel, SetPixel快非常多。我们要感谢微软在C#中保留了这一强有力的工具。