閱讀提示:
《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 FLOATBWParams, *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;//---------------------------------------------------------------------------// 交換像素分量FORCEINLINEVOID SwapRgb(RGBIndex &a, RGBIndex &b){a.tmp ^= b.tmp;b.tmp ^= a.tmp;a.tmp ^= b.tmp;}//---------------------------------------------------------------------------// 擷取黑白灰階FORCEINLINEINTGetBWGray(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 * 100INT 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 = 0if (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 * hueCoefif (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;}elsepparams = (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,而且誤差率也不高。
綜合看來,本文代碼的可行度是較高的。
因水平有限,錯誤在所難免,歡迎指正和指導。郵箱地址:maozefa@hotmail.com
這裡可訪問《C++影像處理 -- 文章索引》。