阅读提示:
《C++图像处理》系列以代码清晰,可读性为主,全部使用C++代码。
《Delphi图像处理》系列以效率为侧重点,一般代码为PASCAL,核心代码采用BASM。
尽可能保持二者内容一致,可相互对照。
本文代码必须包括《C++图像处理 -- 数据类型及公用函数》文章中的BmpData.h头文件。
Photoshop提供了丰富的图象图层混合模式,其中的颜色混合模式是用下图层图象的亮度与上图层填充颜色或者图象色彩进行混合,形成的结果既有着上图层的色彩,又保留了下层图象的灰度,基于这种混合特性,颜色混合模式常用来对灰度图象进行着色。
如何用程序代码准确地实现Photoshop的图层颜色混合模式,一直是程序员们热衷的话题。本文采用BCB2007和GDI+等工具,较好地实现了其主要功能(不考虑不透明度和填充选项)。
按照Photoshop的解释,颜色混合模式是用上图层图象颜色的色相、饱和度与下图层图象像素的明度进行的混合。如此,我们在程序代码中,就需要首先将上层图象颜色的色相、饱和度和下图层图象颜色的明度(亮度)提取出来,色相、饱和度的提取是按照HSV的方式进行的,然后按照下图层颜色明度按照0.3R +0.59G + 0.11B 的比例逐像素进行运算合成,可事实上,我在颜色合成过程中,无论是采用HSV还是HSL甚或其它HSB方式,均没法达到应有的效果。例如取上层颜色R=225,G=211,B=179,提取的H,S分别为42,20%,下层灰度为179,采用HSV或者SHL合成颜色的G,B均为0,而实际合成的R,G,B应分别为192,178,146。
通过在Photoshop中反复试验,发现上层颜色中的饱和度在合成过程中似乎没起什么作用,最终合成结果只要保证上层颜色色相和下层灰度的比例不变就行了,这也是颜色混合模式的2个必要条件,其中灰度比例是必须保证的,如果二者发生冲突,可不考虑色相比例(如图象某像素的灰度为0或者255)。按照这个思路,我放弃了用HSB进行合成的方法,而按照上面2个条件采用解方程的方法来实现颜色混合。为此,可列出下列等式关系:
1:Max - Min = a
2:Mid - Min = b
3:0.3R + 0.59G + 0.11B = c
其中,Max,Mid,Min分别为上层颜色R、G、B分量中的最大、中间、最小值。等式1和2代表了上层颜色色相的比例关系,等式3则代表着下层颜色的灰度比例。
如果只考虑60度以内的色相和假定R>G>B,那么用上面的3个等式可列为下面的三元一次方程组:
1) R - B = a
2) G - B = b
3) 0.3R + 0.59G + 0.11B = c
可以将满足色相在0 - 60范围,R>G>B的任何颜色的常数代入上面的方程组进行验算,其结果是正确的。但是实际的颜色混合是用2个颜色不同的灰度和色相,采用上面的方程组解出的RGB值有可能会超出0 -- 255的范围,而我们又无法在方程组中加入这种范围限制,因此对于超出范围的RGB值,还必须在程序代码中进行调整。
Photoshop图层颜色混合模式在提取下层图像明度时,采用的是黑白调整方法,它实际上是一种灰度计算,但同一般的像素灰度计算方法有区别。计算公式为:
bwGray = (Max - Min) * ratio_Max + (Mid - Min) * ratio_Max_Mid + Min
公式中,bwGray为黑白灰度;Max,Mid和Min分别为RGB各分量的最大值,中间值和最小值;ratio_Max为像素中Max代表的颜色(单色)比率,ratio_Max_Mid为像素中Max和Mid所形成的间色比率。
这个公式中单色和间色是Photoshop中的概念,不熟悉Photoshop的人可能不容易弄明白。举个例子就好懂了:
某像素的r、g、b分别为200、100、50,那么Max=r=200,Mid=g=100,Min=b=50,显然ratio_Max为r(红色)的比率,而ratio_Max_Mid则为r(红色)与g(绿色)形成的间色(黄色)的比率,按上面的公式和Photoshop黑白调整的缺省比率(红色=40%,黄色=60%)计算该像素的黑白灰度值,则为:
bwGray = (200 - 100) * 0.4 + (100 - 50) * 0.6 + 50 = 120
按照这个原理,在Photoshop黑白功能调整对话框中,只有调整红色和黄色的比率才会对例子中的像素灰度值起作用。
下面是我按照上面思路和公式写的全部程序代码:
//--------------------------------------------------------------------------- typedef FLOAT BWParams, *PBWParams; // 黑白调整缺省参数:红,黄,绿,洋红,蓝,青 CONST INT _BWDefault[] = {410, 614, 410, 819, 205, 614}; enum { BWIndexBlue = 0x40000, BWIndexGreen = 0x20000, BWIndexRed = 0x00000 }; enum { IndexBlue = 0x00000, IndexGreen = 0x10000, IndexRed = 0x20000 }; typedef union // 颜色分量交换结构 { INT tmp; // 交换时用的临时变量 struct { SHORT value; // 颜色分量值 SHORT index; // 颜色分量索引 }; }RGBIndex; //--------------------------------------------------------------------------- // 交换像素分量 FORCEINLINE VOID SwapRgb(RGBIndex &a, RGBIndex &b) { a.tmp ^= b.tmp; b.tmp ^= a.tmp; a.tmp ^= b.tmp; } //--------------------------------------------------------------------------- // 获取黑白灰度 FORCEINLINE INT GetBWGray(CONST PARGBQuad pixel, CONST PINT bwParams) { RGBIndex max, mid, min; min.tmp = pixel->Blue | BWIndexBlue; mid.tmp = pixel->Green | BWIndexGreen; max.tmp = pixel->Red | BWIndexRed; if (max.value < mid.value) SwapRgb(max, mid); if (max.value < min.value) SwapRgb(max, min); if (min.value > mid.value) SwapRgb(min, mid); return (((max.value - mid.value) * bwParams[max.index] + (mid.value - min.value) * bwParams[max.index + mid.index - 1] + 512) >> 10) + min.value; } //--------------------------------------------------------------------------- VOID ColorMix(PARGBQuad pd, CONST PARGBQuad ps, INT gray) { // 灰度计算常数:蓝,绿、红 CONST DOUBLE ys[] = {0.11, 0.59, 0.30}; RGBIndex max, mid, min; min.tmp = ps->Blue | IndexBlue; mid.tmp = ps->Green | IndexGreen; max.tmp = ps->Red | IndexRed; if (max.value < mid.value) SwapRgb(max, mid); if (max.value < min.value) SwapRgb(max, min); if (min.value > mid.value) SwapRgb(min, mid); INT max_min = max.value - min.value; // 饱和度为0,返回灰度 if (max_min == 0) { pd->Blue = pd->Green = pd->Red = gray; return; } INT mid_min = mid.value - min.value; DOUBLE hueCoef = (DOUBLE)mid_min / (DOUBLE)max_min; // 假设最大值=R,中间值=G,最小值=B,设置方程组: // 1): -B + R = max - min // 2): -B + G = mid - min // 3): 11B + 59G + 30R = Gray * 100 INT e1[4], e2[4], e3[4], e4[4], e5[4], e6[4]; e1[max.index] = 1; e1[mid.index] = 0; e1[min.index] = -1; e1[3] = max_min; e2[max.index] = 0; e2[mid.index] = 1; e2[min.index] = -1; e2[3] = mid_min; e3[0] = 11; e3[1] = 59; e3[2] = 30; e3[3] = gray * 100; // 解方程组: // 4): (1) - 2)) * 30 // 5): 2) * 11 // 6): 3) - 4) + 5) for (INT i = 0; i < 4; i ++) { e4[i] = (e1[i] - e2[i]) * e3[max.index]; e5[i] = e2[i] * e3[min.index]; e6[i] = e3[i] - e4[i] + e5[i]; } INT newMax; // 求G解:6) / 100 (因灰度公式缘故,等式右边恒等于100) INT newMid = (e6[3] + 50) / 100; // 求B解:G代入 2) INT newMin = newMid - e2[3]; // 如果B < 0,B = 0,同时按灰度比例和色相比例解二元一次方程求R、G // 方程式:1-1): 0.3R + 0.59G = Gray // 1-2): HueCoef * R - G = 0 if (newMin < 0 || newMid <= 0) { newMax = (int)(gray / (ys[max.index] + ys[mid.index] * hueCoef) + 0.5); newMid = (int)(newMax * hueCoef + 0.5); newMin = 1; } // 否则求R解:G、B代入 1) else { newMax = newMin + e1[3]; // 如果R > 255,R = 255,同时按灰度比例和色相比例解二元一次方程求G、B // 方程式:2-1): 0.59G + 0.11B = gray - 0.3 * 255 // 2-2): G + (hueCoef - 1)B = 255 * hueCoef if (newMax > 255) { newMin = (INT)((gray - (ys[max.index] + ys[mid.index] * hueCoef) * 255) / (ys[min.index] - ys[mid.index] * (hueCoef - 1)) + 1.0); newMid = (INT)(newMin + (255 - newMin) * hueCoef + 0.5); newMax = 255; } } ((LPBYTE)pd)[max.index] = newMax > 255? 255 : newMax; ((LPBYTE)pd)[mid.index] = newMid > 255? 255 : newMid; ((LPBYTE)pd)[min.index] = newMin > 255? 255 : newMin; } //--------------------------------------------------------------------------- // 图像黑白调整。 // 调整参数bwParams为元素数等于6的数组指针,分别为红,黄,绿,青,蓝,洋红 VOID ImageBlackWhite(BitmapData *data, CONST PBWParams bwParams = NULL) { // 拷贝像素灰度参数,并交换青色和洋红色 INT params[6], *pparams; if (bwParams) { for (INT i = 0; i < 6; i ++) params[i] = (INT)(bwParams[i] * 1024 + 0.5); params[3] ^= params[5]; params[5] ^= params[3]; params[3] ^= params[5]; pparams = params; } else pparams = (INT*)_BWDefault; PARGBQuad p = (PARGBQuad)data->Scan0; INT dataOffset = (data->Stride >> 2) - (INT)data->Width; for (UINT y = 0; y < data->Height; y ++, p += dataOffset) { for (UINT x = 0; x < data->Width; x ++, p ++) { INT gray = GetBWGray(p, pparams); p->Blue = p->Green = p->Red = (gray & ~0xff) == 0? gray : gray > 255? 255 : 0; } } } //--------------------------------------------------------------------------- // 灰度图像染色。 VOID ImageTint(BitmapData *grayData, ARGB color) { PARGBQuad p = (PARGBQuad)grayData->Scan0; INT dataOffset = (grayData->Stride >> 2) - (INT)grayData->Width; for (UINT y = 0; y < grayData->Height; y ++, p += dataOffset) { for (UINT x = 0; x < grayData->Width; x ++, p ++) { ColorMix(p, (PARGBQuad)&color, p->Blue); } } } //--------------------------------------------------------------------------- // 图像颜色模式混合 VOID ImageColorMixer(BitmapData *dest, CONST BitmapData *source) { PARGBQuad pd, ps; UINT width, height; INT dstOffset, srcOffset; GetDataCopyParams(dest, source, width, height, pd, ps, dstOffset, srcOffset); for (UINT y = 0; y < height; y ++, pd += dstOffset, ps += srcOffset) { for (UINT x = 0; x < width; x ++, pd ++, ps ++) { ColorMix(pd, ps, GetBWGray(pd, (PINT)_BWDefault)); } } } //---------------------------------------------------------------------------
上面代码中,ColorMix函数写出了比较详细的解方程过程代码,并作了相应的注释;解三元一次方程组时,将灰度比例值扩大了100倍,可使用定点数运算,因为灰度比例关系恒等于100的缘故,运算过程中不会产生误差;其中对值超出0 - 255范围RGB值分别使用了2组二元一次方程进行了处理;另外,由于定义了一个RGBIndex类型,使得在比较和交换最大、最小值过程中,保存了原R、G、B信息,这不仅方便了代码中的运算,也使得前面的三元一次方程组的适用范围从色相60度以内和R>G>B,扩展到了色相全范围以及任意大小的R、G、B值,同时也避免了HSB转换为RGB时通常使用的switch条件语句。
为了检验本文思路和代码是否正确,用本文代码进行黑白调整、灰度图染色和图像颜色混合后,同Photoshop同样参数调整形成的图像逐像素进行了比较,下面是个简单的比较函数代码:
//--------------------------------------------------------------------------- void ImageCompare(Bitmap *bmp1, Bitmap *bmp2) { int count, r_count = 0, g_count = 0, b_count = 0; int diff, r_diff = 0, g_diff = 0, b_diff = 0; BitmapData data1, data2; Gdiplus::Rect r(0, 0, bmp1->GetWidth(), bmp1->GetHeight()); bmp1->LockBits(&r, ImageLockModeRead, PixelFormat24bppRGB, &data1); bmp2->LockBits(&r, ImageLockModeRead, PixelFormat24bppRGB, &data2); try { PRGBTRIPLE p1 = (PRGBTriple)data1.Scan0; PRGBTRIPLE p2 = (PRGBTriple)data2.Scan0; int offset = data1.Stride - data1.Width * sizeof(RGBTRIPLE); for (unsigned y = 0; y < data1.Height; y ++, (char*)p1 += offset, (char*)p2 += offset) { for (unsigned x = 0; x < data1.Width; x ++, p1 ++, p2 ++) { diff = p1->rgbtRed - p2->rgbtRed; if (diff) { r_count ++; if (diff < 0) diff = -diff; if (r_diff < diff) r_diff = diff; } diff = p1->rgbtGreen - p2->rgbtGreen; if (diff) { g_count ++; if (diff < 0) diff = -diff; if (g_diff < diff) g_diff = diff; } diff = p1->rgbtBlue - p2->rgbtBlue; if (diff) { b_count ++; if (diff < 0) diff = -diff; if (b_diff < diff) b_diff = diff; } } } } __finally { bmp2->UnlockBits(&data2); bmp1->UnlockBits(&data1); } count = data1.Width * data1.Height; String s; s.sprintf(L"像素总数:%d\n" \ L"红误差数:%d,误差率:%d%%,最大误差:%d\n" \ L"绿误差数:%d,误差率:%d%%,最大误差:%d\n" \ L"蓝误差数:%d,误差率:%d%%,最大误差:%d", count, r_count, (r_count * 100) / count, r_diff, g_count, (g_count * 100) / count, g_diff, b_count, (b_count * 100) / count, b_diff); ShowMessage(s); } //---------------------------------------------------------------------------
首先对图像进行黑白调整,请看下面的例子:
//--------------------------------------------------------------------------- void __fastcall TForm1::Button1Click(TObject *Sender) { Gdiplus::Bitmap *bmp = new Gdiplus::Bitmap(L"..\\..\\media\\Source.bmp"); BitmapData data; LockBitmap(bmp, &data); ImageBlackWhite(&data); UnlockBitmap(bmp, &data); Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle); g->DrawImage(bmp, 0, 0); delete g; Gdiplus::Bitmap *bmp2 = new Gdiplus::Bitmap(L"..\\..\\media\\GraySource.bmp"); ImageCompare(bmp, bmp2); delete bmp2; delete bmp; } //---------------------------------------------------------------------------
下面是原图像:
黑白调整例子程序运行界面截图,比较后的误差为0:
在做一个图像染色的例子:
//--------------------------------------------------------------------------- void __fastcall TForm1::Button2Click(TObject *Sender) { Gdiplus::Bitmap *bmp = new Gdiplus::Bitmap(L"..\\..\\media\\Source.bmp"); BitmapData data; LockBitmap(bmp, &data); ImageBlackWhite(&data); ImageTint(&data, 0xff314ead); UnlockBitmap(bmp, &data); Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle); g->DrawImage(bmp, 0, 0); delete g; Gdiplus::Bitmap *bmp2 = new Gdiplus::Bitmap(L"..\\..\\media\\Source314ead.bmp"); ImageCompare(bmp, bmp2); delete bmp2; delete bmp; } //---------------------------------------------------------------------------
运行结果如下:
染色误差为1。
最后再以上面的原图为背景(下层图象),和一副风景图象(上层图像)做颜色模式混合:
//--------------------------------------------------------------------------- void __fastcall TForm1::Button3Click(TObject *Sender) { Gdiplus::Bitmap *bmp = new Gdiplus::Bitmap(L"..\\..\\media\\Source.bmp"); Gdiplus::Bitmap *bmp1 = new Gdiplus::Bitmap(L"..\\..\\media\\Test1.bmp"); BitmapData dest, source; LockBitmap(bmp, &dest); LockBitmap(bmp1, &source); ImageColorMixer(&dest, &source); UnlockBitmap(bmp1, &source); UnlockBitmap(bmp, &dest); delete bmp1; Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle); g->DrawImage(bmp, 0, 0); delete g; Gdiplus::Bitmap *bmp2 = new Gdiplus::Bitmap(L"..\\..\\media\\TestMix.bmp"); ImageCompare(bmp, bmp2); delete bmp2; delete bmp; } //---------------------------------------------------------------------------
前景图像:
运行结果图:
误差为2,而且误差率也不高。
综合看来,本文代码的可行度是较高的。
因水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]
这里可访问《C++图像处理 -- 文章索引》。