C++实现Photoshop图层颜色混合模式

注:改进后的实现代码见《C++实现Photoshop图层颜色混合模式(续)》。

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值,还必须在程序代码中进行调整。下面是我写的一个单像素合成代码。

//--------------------------------------------------------------------------- typedef union // 颜色分量交换结构 { int tmp; // 交换时用的临时变量 struct { short value; // 颜色分量值 short index; // 颜色分量索引:blue=0,green=1,red=2 }; }RgbSwap, *PRgbSwap; typedef struct { unsigned char v[4]; }ArgbArray; //--------------------------------------------------------------------------- inline void SwapRgb(RgbSwap &a, RgbSwap &b) { a.tmp += b.tmp; b.tmp = a.tmp - b.tmp; a.tmp -= b.tmp; } //--------------------------------------------------------------------------- Color ColorMix(Color color, int gray) { const double ys[] = {0.11, 0.59, 0.30}; int e1[4], e2[4], e3[4], e4[4], e5[4], e6[4]; RgbSwap max, mid, min; int newMax, newMid, newMin; int max_min, mid_min; double hueCoef; Color result; max.tmp = color.GetRed() + 0x20000; mid.tmp = color.GetGreen() + 0x10000; min.tmp = color.GetBlue(); if (max.value < mid.value) SwapRgb(max, mid); if (max.value < min.value) SwapRgb(max, min); if (min.value > mid.value) SwapRgb(min, mid); max_min = max.value - min.value; // 饱和度为0,返回灰度 if (max_min == 0) return Color(gray, gray, gray); mid_min = mid.value - min.value; 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 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]; } // 求G解:6) / 100 (因灰度公式缘故,等式右边恒等于100) newMid = (e6[3] + 50) / 100; // 求B解:G代入 2) 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 = 0; } // 否则求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; } } ((ArgbArray*)&result)->v[max.index] = newMax; ((ArgbArray*)&result)->v[mid.index] = newMid; ((ArgbArray*)&result)->v[min.index] = newMin; return result; }

ColorMix函数写出了比较详细的解方程过程代码,并作了相应的注释;解三元一次方程组时,将灰度比例值扩大了100倍,可使用定点数运算,因为灰度比例关系恒等于100的缘故,运算过程中不会产生误差;其中对值超出0 - 255范围RGB值分别使用了2组二元一次方程进行了处理;另外,由于定义了一个RgbSwap类型,使得在比较和交换最大、最小值过程中,保存了原R、G、B信息,这不仅方便了代码中的运算,也使得前面的三元一次方程组的适用范围从色相60度以内和R>G>B,扩展到了色相全范围以及任意大小的R、G、B值,同时也避免了HSB转换为RGB时通常使用的switch条件语句。

经过一定量的颜色混合测试,ColorMix函数表现较好,运算结果与实际误差始终在2的范围内,这属于正常的运算误差和灰度比例取值误差。

下面是一个对灰度图象进行着色的函数和测试代码:

// 图像着色。bmp:灰度背景图象,color:颜色 void PSGrayImageTint(Bitmap *bmp, Color color) { const double ys[] = {0.11, 0.59, 0.3}; RgbSwap max, mid, min; int newMax, newMid, newMin; int max_min, mid_min; double hueCoef; max.tmp = color.GetRed() + 0x20000; mid.tmp = color.GetGreen() + 0x10000; min.tmp = color.GetBlue(); if (max.value < mid.value) SwapRgb(max, mid); if (max.value < min.value) SwapRgb(max, min); if (min.value > mid.value) SwapRgb(min, mid); max_min = max.value - min.value; // 饱和度为0,不着色返回 if (max_min == 0) return; mid_min = mid.value - min.value; hueCoef = (double)mid_min / (double)max_min; BitmapData data; Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight()); bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite, PixelFormat24bppRGB, &data); try { unsigned char *p = (unsigned char*)data.Scan0; int offset = data.Stride - data.Width * 3; for (unsigned y = 0; y < data.Height; y ++, p += offset) { for (unsigned x = 0; x < data.Width; x ++, p += 3) { newMid = (int)(*p - (max_min - mid_min) * ys[max.index] + mid_min * ys[min.index] + 0.5); newMin = newMid - mid_min; if (newMin < 0 || newMid <= 0) { newMax = (int)(*p / (ys[max.index] + ys[mid.index] * hueCoef) + 0.5); newMid = (int)(newMax * hueCoef + 0.5); newMin = 0; } else { newMax = newMin + max_min; if (newMax > 255) { newMin = (int)((*p - (ys[max.index] + ys[mid.index] * hueCoef) * 255) / (ys[min.index] - ys[mid.index] * (hueCoef - 1)) + 0.5); newMid = (int)(newMin + (255 - newMin) * hueCoef + 0.5); newMax = 255; } } p[max.index] = newMax; p[mid.index] = newMid; p[min.index] = newMin; } } } __finally { bmp->UnlockBits(&data); } } 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("像素总数:%d\n" \ "红误差数:%d,误差率:%d%%,最大误差:%d\n" \ "绿误差数:%d,误差率:%d%%,最大误差:%d\n" \ "蓝误差数:%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::Button2Click(TObject *Sender) { Bitmap *bmp1 = new Bitmap(WideString("d:\\GraySource.bmp")); PSGrayImageTint(bmp1, 0x314ead); Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle); g->DrawImage(bmp1, 0, 0); delete g; // 同PS混合图比较,bmp2为PS混合图像 Bitmap *bmp2 = new Bitmap(WideString("d:\\Source314ead.bmp")); ImageCompare(bmp1, bmp2); delete bmp2; delete bmp1; } //---------------------------------------------------------------------------


PSGrayImageTint函数用来对灰度图象进行着色。在该函数中,简化了解三元一次方程的代码。

ImageCompare函数用来对本文各种颜色混合函数测试混合图象与Photoshop制作的混合图片进行对比的。

为了保证测试的准确率,我用Photoshop制作了灰度图图片和多张不同颜色混合的图片,并保存为BMP格式,避免常用JPEG格式图象的压缩误差。

上面测试代码中用到的PS制作的灰度图象如下(已转换为jpeg格式上传):

使用颜色0x314ead进行着色并对比效果图:

下面是对2张图象进行颜色混合的函数代码:

// 图像颜色模式混合。bmp:灰度背景图象,bmp2:前景图像。混合后图像在bmp中 void PSGrayImageMix(Bitmap *bmp, Bitmap *bmp2) { const double ys[] = {0.11, 0.59, 0.3}; RgbSwap max, mid, min; int newMax, newMid, newMin; int max_min, mid_min; double hueCoef; BitmapData data, data2; Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight()); bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite, PixelFormat24bppRGB, &data); bmp2->LockBits(&r, ImageLockModeRead, PixelFormat24bppRGB, &data2); try { int gray; unsigned char *p = (unsigned char*)data.Scan0; unsigned char *p2 = (unsigned char*)data2.Scan0; int offset = data.Stride - data.Width * 3; for (unsigned y = 0; y < data.Height; y ++, p += offset, p2 += offset) { for (unsigned x = 0; x < data.Width; x ++, p += 3, p2 += 3) { max.tmp = p2[2] + 0x20000; // Red mid.tmp = p2[1] + 0x10000; // Green min.tmp = p2[0]; // Blue if (max.value < mid.value) SwapRgb(max, mid); if (max.value < min.value) SwapRgb(max, min); if (min.value > mid.value) SwapRgb(min, mid); max_min = max.value - min.value; // 饱和度为0,不着色 if (max_min == 0) continue; mid_min = mid.value - min.value; hueCoef = (double)mid_min / (double)max_min; newMid = (int)(*p - (max_min - mid_min) * ys[max.index] + mid_min * ys[min.index] + 0.5); newMin = newMid - mid_min; if (newMin < 0 || newMid <= 0) { newMax = (int)(*p / (ys[max.index] + ys[mid.index] * hueCoef) + 0.5); newMid = (int)(newMax * hueCoef + 0.5); newMin = 0; } else { newMax = newMin + max_min; if (newMax > 255) { newMin = (int)((*p - (ys[max.index] + ys[mid.index] * hueCoef) * 255) / (ys[min.index] - ys[mid.index] * (hueCoef - 1)) + 0.5); newMid = (int)(newMin + (255 - newMin) * hueCoef + 0.5); newMax = 255; } } p[max.index] = newMax; p[mid.index] = newMid; p[min.index] = newMin; } } } __finally { bmp->UnlockBits(&data); bmp2->UnlockBits(&data2); } } inline int CheckRgb(int rgb) { if ((rgb & 0xffffff00) == 0) return rgb; else if ((rgb & 0x80000000) == 0) return 255; else return 0; } // 图像颜色模式混合。bmp:背景图象,bmp2:前景图像,grayBack:是否灰度背景图像 void PSImageMix(Bitmap *bmp, Bitmap *bmp2, BOOL grayBack) { if (grayBack) { PSGrayImageMix(bmp, bmp2); return; } // options: 红,黄,绿,洋红,蓝,青 const int options[] = {40, 60, 40, 80, 20, 60}; RgbSwap max, mid, min; int gray; BitmapData data; Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight()); bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite, PixelFormat24bppRGB, &data); try { unsigned char *p = (unsigned char*)data.Scan0; int offset = data.Stride - data.Width * 3; for (unsigned y = 0; y < data.Height; y ++, p += offset) { for (unsigned x = 0; x < data.Width; x ++, p += 3) { max.tmp = p[2]; // Red mid.tmp = p[1] + 0x20000; // Green min.tmp = p[0] + 0x40000; // Blue if (max.value < mid.value) SwapRgb(max, mid); if (max.value < min.value) SwapRgb(max, min); if (min.value > mid.value) SwapRgb(min, mid); gray = CheckRgb(((max.value - mid.value) * options[max.index] + (mid.value - min.value) * options[max.index + mid.index - 1] + 50) / 100 + min.value); p[0] = p[1] = p[2] = gray; } } } __finally { bmp->UnlockBits(&data); } PSGrayImageMix(bmp, bmp2); }


函数PSGrayImageMix用相同大小的灰度背景图与前景图象进行颜色混合,而PSImageMix函数对任意2张图片相同大小的图片进行颜色混合。

其中PSImageMix函数只是将彩色图像转换为灰度图,然后调用PSGrayImageMix来实现颜色混合的。注意函数中图象像素的灰度计算是Photoshop特有的图象灰度转换方法,而并非我们常用的几种灰度转换公式。有关这种灰度转换原理我将在另一篇关于图象黑白调整进行描述。

上面2个函数的测试代码:

/--------------------------------------------------------------------------- void __fastcall TForm1::Button3Click(TObject *Sender) { Bitmap *bmp1 = new Bitmap(WideString("d:\\GraySource.bmp"));// 背景图 Bitmap *bmp2 = new Bitmap(WideString("d:\\Test1.bmp")); // 前景图 PSGrayImageMix(bmp1, bmp2); delete bmp2; Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle); g->DrawImage(bmp1, 0, 0); delete g; // 同PS混合图比较,bmp2为PS混合图像 bmp2 = new Bitmap(WideString("d:\\TestMix.bmp")); ImageCompare(bmp1, bmp2); delete bmp2; delete bmp1; } //--------------------------------------------------------------------------- void __fastcall TForm1::Button4Click(TObject *Sender) { Bitmap *bmp1 = new Bitmap(WideString("d:\\Source.bmp")); // 背景图 Bitmap *bmp2 = new Bitmap(WideString("d:\\Test1.bmp")); // 前景图 PSImageMix(bmp1, bmp2, FALSE); delete bmp2; Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle); g->DrawImage(bmp1, 0, 0); delete g; // 同PS混合图比较,bmp2为PS混合图像 bmp2 = new Bitmap(WideString("d:\\TestMix.bmp")); ImageCompare(bmp1, bmp2); delete bmp2; delete bmp1; } //---------------------------------------------------------------------------


第一个测试函数是使用前面的灰度图象作背景进行的图象颜色混合,而第二个测试函数则是使用彩色图像作背景进行图像颜色混合。前景图片和彩色背景图如下:

2个函数的图像混合和对比结果完全一样,其效果图如下:

如有错误或者建议,请来信指导: maozefa@hotmail.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值