图像处理中,大部分的处理方法都需要事先把彩色图转换成灰度图才能进行相关的计算、识别。 彩色图转换灰度图的原理如下: 我们知道彩色位图是由R/G/B三个分量组成,其文件存储格式为 BITMAPFILEHEADER+BITMAPINFOHEADER,紧跟后面的可能是: 如果是24位真彩图,则每个点是由三个字节分别表示R/G/B,所以这里直接跟着图像的色彩信息; 如果是8位(256色),4位(16色),1位(单色)图,则紧跟后面的是调色板数据,一个RGBQUAD类型的数组,其长度由BITMAPINFOHEADER.biClrUsed来决定。 然后后面紧跟的才是图像数据(24位图是真实的图像数据,其他的则是调色板的索引数据)。 灰度图是指只含亮度信息,不含色彩信息的图象,就象我们平时看到的黑白照片:亮度由暗到明,变化是连续的。因此,要表示灰度图,就需要把亮度值进行 量化。通常划分成0到255共256个级别,其中0最暗(全黑),255最亮(全白)。在表示颜色的方法中,除了RGB外,还有一种叫YUV的表示方法, 应用也很多。电视信号中用的就是一种类似于YUV的颜色表示方法。在这种表示方法中,Y分量的物理含义就是亮度,Y分量包含了灰度图的所有信息,只用Y分 量就能完全能够表示出一幅灰度图来。 从 RGB 到 YUV 空间的 Y 转换公式为: Y = 0.299R+0.587G+0.114B 在 WINDOWS 中,表示 16 位以上的图和以下的图有点不同; 16 位以下的图使用一个调色板来表示选择具体的颜色,调色板的每个单元是 4 个字节,其中一个透明度;而具体的像素值存储的是索引,分别是 1 、 2 、 4 、 8 位。 16 位以上的图直接使用像素表示颜色。 那么如何将彩色图转换为灰度图呢? 灰度图中有调色板,首先需要确定调色板的具体颜色取值。我们前面提到了,灰度图的三个分量相等。 当转换为 8 位的时候,调色板中有 256 个颜色,每个正好从 0 到 255 个,三个分量都相等。 当转换为 4 位的时候,调色板中 16 个颜色,等间隔平分 255 个颜色值,三个分量都相等。 当转换为 2 位的时候,调色板中 4 个颜色,等间隔平分 255 个颜色,三个分量相等。 当转换为 1 位的时候,调色板中两个颜色,是 0 和 255 ,表示黑和白。 将彩色转换为灰度时候,按照公式计算出对应的值,该值实际上是亮度的级别;亮度从 0 到 255 ;由于不同的位有不同的亮度级别,所以 Y 的具体取值如下: Y = Y/ (1<<(8- 转换的位数 )); 所以,我们要转化成灰度图,并且存储成一幅可以看到的图像,需要做如下转换: 16位以上的图像不带调色板,只需要把图像数据按每个点的位数都转换成相同的灰度值即可 16位以下的图像,则需要修改调色板的数值,并且按照每个点所占位数修改灰度值索引即可。 |
以下是256色图转换成灰度图示例代码:
- /****************************************************************
- * 函数名称:
- * Convert256toGray()
- *
- * 参数:
- * HDIB hDIB -图像的句柄
- *
- * 返回值:
- * 无
- *
- * 功能:
- * 将256色位图转化为灰度图
- *
- ***************************************************************/
- void Convert256toGray(HDIB hDIB)
- {
- LPSTR lpDIB;
- // 由DIB句柄得到DIB指针并锁定DIB
- lpDIB = (LPSTR) ::GlobalLock((HGLOBAL)hDIB);
- // 指向DIB象素数据区的指针
- LPSTR lpDIBBits;
- // 指向DIB象素的指针
- BYTE * lpSrc;
- // 图像宽度
- LONG lWidth;
- // 图像高度
- LONG lHeight;
- // 图像每行的字节数
- LONG lLineBytes;
- // 指向BITMAPINFO结构的指针(Win3.0)
- LPBITMAPINFO lpbmi;
- // 指向BITMAPCOREINFO结构的指针
- LPBITMAPCOREINFO lpbmc;
- // 获取指向BITMAPINFO结构的指针(Win3.0)
- lpbmi = (LPBITMAPINFO)lpDIB;
- // 获取指向BITMAPCOREINFO结构的指针
- lpbmc = (LPBITMAPCOREINFO)lpDIB;
- // 灰度映射表
- BYTE bMap[256];
- // 计算灰度映射表(保存各个颜色的灰度值),并更新DIB调色板
- int i,j;
- for (i = 0; i < 256; i ++)
- {
- // 计算该颜色对应的灰度值
- bMap[i] = (BYTE)(0.299 * lpbmi->bmiColors[i].rgbRed +
- 0.587 * lpbmi->bmiColors[i].rgbGreen +
- 0.114 * lpbmi->bmiColors[i].rgbBlue + 0.5);
- // 更新DIB调色板红色分量
- lpbmi->bmiColors[i].rgbRed = i;
- // 更新DIB调色板绿色分量
- lpbmi->bmiColors[i].rgbGreen = i;
- // 更新DIB调色板蓝色分量
- lpbmi->bmiColors[i].rgbBlue = i;
- // 更新DIB调色板保留位
- lpbmi->bmiColors[i].rgbReserved = 0;
- }
- // 找到DIB图像象素起始位置
- lpDIBBits = ::FindDIBBits(lpDIB);
- // 获取图像宽度
- lWidth = ::DIBWidth(lpDIB);
- // 获取图像高度
- lHeight = ::DIBHeight(lpDIB);
- // 计算图像每行的字节数
- lLineBytes = WIDTHBYTES(lWidth * 8);
- // 更换每个象素的颜色索引(即按照灰度映射表换成灰度值)
- //逐行扫描
- for(i = 0; i < lHeight; i++)
- {
- //逐列扫描
- for(j = 0; j < lWidth; j++)
- {
- // 指向DIB第i行,第j个象素的指针
- lpSrc = (unsigned char*)lpDIBBits + lLineBytes * (lHeight - 1 - i) + j;
- // 变换
- *lpSrc = bMap[*lpSrc];
- }
- }
- //解除锁定
- ::GlobalUnlock ((HGLOBAL)hDIB);
- }
24位彩色图转换成4位灰度图
首先要声明的是,这个4位(16)色图比较特殊,不是彩色的16色图,而已一个用4位16色,模拟的灰度图
什么是灰度图?
灰度图是指只含亮度信息不含彩色信息的图象,就像我们平时看到的亮度由暗到明的黑白照片,亮度变化是连续的。因此,要表示
灰度图,就需要把亮度值进行亮化。通常分成0-255共256个级别,0最暗(全黑),255最亮(全白)。
BMP格式的文件中并没有灰度图这个概念,但是可以很容易的用BMP文件来表示灰度图。一般的方法是用256色的调色板,这个调色板
每一项的RGB值都是相同的,即从(0,0,0),(1,1,1)一直到(255,255,255),(0,0,0)表示全黑(255,255,255)表示全白
1.BMP位图的格式
BMP文件的结构分为4部分,本文假定读者都已经了解BMP位图的格式(几乎所有教VC的书上多媒体部分都有讲,再google一下也很容
易就查得到,这里主要介绍其中的调色板,和图象数据部分。
对于非真彩的位图,都有一个调色板,调色板的格式如下
typedef struct tagRGBQUAD{
BYTE rgbBlue;//蓝色的分量
BYTE rgbGreen;//绿色的分量
BYTE rgbRed;//红色的分量
BYTE rgbReserved;//保留值不用管它为0就好
}RGBQUAD;
一般的调色版是一个,由上面的结构体组成的结构体数组,存储具体的颜色信息,而位图中,图象数据部分存储的只是调色板的下标
。这样做就可以大大的节省空间。
例如:
RGBQUAD rgb[2];
rgb[0].rgbBlue = 0;
rgb[0].rgbGreen = 0;
rgb[0].rgbRed = 0;
rgb[0].rgbReserved = 0;
rgb[1].rgbBlue = 255;
rgb[1].rgbGreen = 255;
rgb[1].rgbRed = 255;
rgb[1].rgbReserved = 255;
这个长度为2的RGBQUAD数组就是一个1位2色黑白图的调色板,
在位图数据部分只需要用1位的长度存储0表示黑,1表示白就可以了,1字节可以表示8个像素的信息,比用3字节直接表示R,G,B节
省了24倍的存储空间
而真彩图则不然,比如24位图,那么他就需要一个数组大小为2的24次方的调色板,而调色板的下标也需要3个字节才储存,这样还
不如直接就R,G,B这三个分量来直接表示每一个像素的色值。使用调色板技术还浪费了一个256*256*256*3字节大的调色板空间.
而这里要用4位表示一个灰度图,那么它的调色板只有16项,每一项的RGB值同通常由256色构成的灰度图的调色板一样的道理
这里这样建立这个调色板
RGBQUAD pa[16];
BYTE c;
for(int i=0;i<16;i++)
{
c= i * 17;
pa[i].rgbRed = c;
pa[i].rgbGreen = c;
pa[i].rgbBlue = c;
pa[i].rgbReserved = 0;
}
2.转换算法
现在的图象是24位真彩的,表示它的数据部分,3字节表示一个像素,这三个字节分别表示RGB。
我们现在要做的是求每一像素点的RGB值的平均值,然后用16色调色板中最接近这个颜色亮度的值来表示它。
而4位的图象是1个字节表示2个像素,在这里需要特殊注意
具体算法实现代码如下,pBuffer是储存图象数据的数组
USHORT R,G,B;
// 第一个像素
R = pBuffer[dwIndex++];
G = pBuffer[dwIndex++];
B = pBuffer[dwIndex++];
int maxcolor = (R+G+B)/3;
maxcolor /= 17;//计算在16色调色板中的下标
//第二个像素
R = pBuffer[dwIndex++];
G = pBuffer[dwIndex++];
B = pBuffer[dwIndex++];
int maxcolor2 = (R+G+B)/3;
maxcolor2 /= 17;
pNew[dwOldIndex++] = ( maxcolor<<4 )| maxcolor2;//合成一个字节表示两个像素
3.实现代码
完整的实现代码如下
BOOL Convert24To4(LPCTSTR lpszSrcFile, LPCTSTR lpszDestFile)//24->4灰度图
{
BITMAPFILEHEADER bmHdr; // BMP文件头
BITMAPINFOHEADER bmInfo; // BMP文件信息
HANDLE hFile, hNewFile;
DWORD dwByteWritten = 0;
// 打开源文件句柄
hFile = CreateFile(lpszSrcFile,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
// 创建新文件
hNewFile = CreateFile(lpszDestFile,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hNewFile == INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
return FALSE;
}
// 读取源文件BMP头和文件信息
ReadFile(hFile, &bmHdr, sizeof(bmHdr), &dwByteWritten, NULL);
ReadFile(hFile, &bmInfo, sizeof(bmInfo), &dwByteWritten, NULL);
TRACE("biSize: %d , biWidth: %d , biHeight: %d , biBitCount: %d , biSizeImage: %d
/n",bmInfo.biSize,bmInfo.biWidth,bmInfo.biHeight,bmInfo.biBitCount,bmInfo.biSizeImage);
TRACE("biX: %d , biY: %d , biClrUsed: %d , biClrImportant: %d
/n",bmInfo.biXPelsPerMeter,bmInfo.biYPelsPerMeter,bmInfo.biClrUsed,bmInfo.biClrImportant);
// 只处理24位未压缩的图像
if (bmInfo.biBitCount != 24 || bmInfo.biCompression!=0)
{
CloseHandle(hNewFile);
CloseHandle(hFile);
DeleteFile(lpszDestFile);
return FALSE;
}
// 计算图像数据大小
DWORD dwOldSize = bmInfo.biSizeImage;
if(dwOldSize == 0) // 重新计算
{
dwOldSize = bmHdr.bfSize - sizeof(bmHdr) - sizeof(bmInfo);
}
TRACE("Old Width: %d , Old Height: %d ,Old Size: %d bytes/n",bmInfo.biWidth,bmInfo.biHeight,dwOldSize);
long wid = bmInfo.biWidth % 4;
if(wid>0)
{
wid = 4 - wid;
}
wid += bmInfo.biWidth;
DWORD dwNewSize;
dwNewSize = wid * bmInfo.biHeight / 2; //计算转换后新图象大小
TRACE("New Size: %d bytes/n", dwNewSize);
// 读取原始数据
UCHAR *pBuffer = NULL;
pBuffer = new UCHAR[dwOldSize]; // 申请原始数据空间
if(pBuffer == NULL)
{
CloseHandle(hNewFile);
CloseHandle(hFile);
DeleteFile(lpszDestFile);
return FALSE;
}
// 读取数据
ReadFile(hFile, pBuffer, dwOldSize, &dwByteWritten, NULL);
UCHAR *pNew = new UCHAR[dwNewSize];
UCHAR color = 0;
DWORD dwIndex = 0, dwOldIndex = 0;
while( dwIndex < dwOldSize )//一字节表示两个像素
{
USHORT R,G,B;
// 第一个像素
R = pBuffer[dwIndex++];
G = pBuffer[dwIndex++];
B = pBuffer[dwIndex++];
int maxcolor = (R+G+B)/3;
maxcolor /= 17;
//第二个像素
R = pBuffer[dwIndex++];
G = pBuffer[dwIndex++];
B = pBuffer[dwIndex++];
int maxcolor2 = (R+G+B)/3;
maxcolor2 /= 17;
pNew[dwOldIndex++] = ( maxcolor<<4 )| maxcolor2;//合成一个字节表示两个像素
}
// 完工, 把结果保存到新文件中
// 修改属性
bmHdr.bfSize = sizeof(bmHdr)+sizeof(bmInfo)+sizeof(RGBQUAD)*16+dwNewSize;
bmHdr.bfOffBits = bmHdr.bfSize - dwNewSize;
bmInfo.biBitCount = 4;
bmInfo.biSizeImage = dwNewSize;
// 创建调色板
RGBQUAD pa[16];
UCHAR c;
for(int i=0;i<16;i++)
{
c= i * 17;
pa[i].rgbRed = c;
pa[i].rgbGreen = c;
pa[i].rgbBlue = c;
pa[i].rgbReserved = 0;
}
// BMP头
WriteFile(hNewFile, &bmHdr, sizeof(bmHdr), &dwByteWritten, NULL);
// 文件信息头
WriteFile(hNewFile, &bmInfo, sizeof(bmInfo), &dwByteWritten, NULL);
// 调色板
WriteFile(hNewFile, pa, sizeof(RGBQUAD)*16, &dwByteWritten, NULL);
// 文件数据
WriteFile(hNewFile, pNew, dwNewSize, &dwByteWritten, NULL);
delete []pBuffer;
delete []pNew;
// 关闭文件句柄
CloseHandle(hNewFile);
CloseHandle(hFile);
return TRUE;
}
4.疑问
既然可以由24位真菜图转换为4位灰度图,那么一定有一个合适的方法把它转换成4位彩色图,而具体的区别就是
调色板不同(调色板都要表示哪些颜色),再有最重要的是原来的颜色用现有的16色,哪个表示更合适.
技术交流、商务合作请直接联系博主
扫码或搜索:猿说编程
猿说编程
微信公众号 扫一扫关注