顏色空間系列代碼下載鏈接:http://files.cnblogs.com/Imageshop/ImageInfo.rar (同文章同步更新)
在幾個常用的顏色空間中,LAB顏色空間是除了RGB外,最常用的一種之一,不同於RGB色彩空間,Lab 顏色被設計來接近人類視覺。它致力於感知均勻性,它的 L 分量密切匹配人類亮度感知。因此可以被用來通過修改 a 和 b 分量的輸色階來做精確的顏色平衡,或使用 L 分量來調整亮度對比。這些變換在 RGB 或 CMYK 中是困難或不可能的,它們建模物理設備的輸出,而不是人類視覺感知。
關於CIELAB顏色空間的更多原理說明,可見:http://en.wikipedia.org/wiki/Lab_color_space
本文研究的重點是RGB和LAB之間的快速轉換過程。
首先,RGB和LAB之間沒有直接的轉換公式,其必須用通道XYZ顏色空間作為中間層,關於RGB和XYZ顏色空間的轉換及優化,詳見顏色空間系列1。
XYZ------>LAB轉換公式如下:一般情況下我們認為Yn,Xn,Zn都為1。
其中
在上述表達式中,X,Y,Z及t變量的取值范圍都是[0,1],對應的L分量的取值范圍為[0,100],A和B分量都為[-127,127],因此,如果把L拉升至[0,255],把A,B位移至於[0,255],就可以同RGB顏色空間表達為同一個范圍了。即使這樣映射后,一般來說,LAB各分量的結果仍為浮點數,這個和RGB不同,但是在很多情況下,為了速度計效率,我們這需結果的取整部分,得到類似於RGB空間的布局。因此,對這類結果的優化更有實際意義。
關於這樣的優化,OpenCv已經做了非常好的工作,各位看客也可以先看看OpenCv的代碼,本文未直接沿用其優化,但本文的算法更簡單明了,在保證結果無明顯變化的同時,速度和效率都有30%以上的提升。
第一步,我們來看看f(t)這個函數的優化,f(t)是個分段函數,如果直接在函數體中判斷,會多一些跳轉和比較語句,不利於CPU的流水線工作,因此,我考慮的第一步是是否能用查表法來做。
在顏色空間系列1文章中,我們知道,轉換后的XYZ值得范圍是[0,255],而這里的t值范圍為[0,1],把if t>(6/29)^3這個算法映射到[0,255],則為 if t>2.26 ,因為XYZ都為整數,即此條件和if t>2等價,可見這里會出現一些漏判點;考慮2.26這個數字的特點,如果我們在把這個結果放大4倍,即XYZ范圍為[0,1020],則判斷條件隨之升級為if t>9.04,取整if t>9,則漏判現象大為減少。這是提的第一點。
接着上面,這樣的話我們就定義一個查找表,查找表大小應該和XYZ的域相同的,即上面的1020(我更喜歡1024),對於表中的元素值,為求速度,當然必須為int 類型,
也就是說,需要把計算出來的小數值放大一定倍數。這里不多說,見下面的代碼:
for (I = 0; I < 1024; I++) { if (I > Threshold) LabTab[I] = (int)(Math.Pow((float)I / 1020, 1.0F / 3) * (1 << Shift) + 0.5 ); else LabTab[I] = (int)((29 * 29.0 * I / (6 * 6 * 3 * 1020) + 4.0 / 29) * (1 << Shift) + 0.5 ); }
C#語言是強類型語言,一定要注意運算式中各變量的類型,比如上式中的1.0F/3,我常常寫成1/3(這個的運算結果為0),結果往往是總覺得程序寫得沒問題,但運行效果就是不對,找半天BUG也找不到。
I / 1020的目的還是把值映射到[0,1]范圍的。 表達式最后的+0.5是因為(int)強制類型轉換時向下取整的,+0.5則為四舍五入的效果。顯然,這是我們需要的。
OK,有了這個查找表,下面的過程就簡單了,對於A,B分量,就是進行簡單的乘法、移位及加法,而對於L分量,必須有一個放大的過程,而這個過程我們應該直接從其系數入手,如下所示:
const int ScaleLC = (int)(16 * 2.55 * (1 << Shift) + 0.5); const int ScaleLT = (int)(116 * 2.55 + 0.5);
2.55即為放大倍數,注意116這個數字,由於,其后的 f(x)已經進行了放大,該數字就不能再放大了。
通過以上分析,一個簡單的而有高效轉換算法就有了:
public static unsafe void ToLAB(byte* From, byte* To, int Length = 1) { if (Length < 1) return; byte* End = From + Length * 3; int X, Y, Z, L, A, B; byte Red, Green, Blue; while (From != End) { Blue = *From; Green = *(From + 1); Red = *(From + 2); X = (Blue * LABXBI + Green * LABXGI + Red * LABXRI + HalfShiftValue) >> (Shift - 2); //RGB->XYZ放大四倍后的結果 Y = (Blue * LABYBI + Green * LABYGI + Red * LABYRI + HalfShiftValue) >> (Shift - 2); Z = (Blue * LABZBI + Green * LABZGI + Red * LABZRI + HalfShiftValue) >> (Shift - 2); X = LabTab[X]; // 進行查表 Y = LabTab[Y]; Z = LabTab[Z]; L = ((ScaleLT * Y - ScaleLC + HalfShiftValue) >>Shift); A = ((500 * (X - Y) + HalfShiftValue) >> Shift) + 128; B = ((200 * (Y - Z) + HalfShiftValue) >> Shift) + 128; *To = (byte)L; // 不要把直接計算的代碼放在這里,會降低速度 *(To + 1) = (byte)A; // 無需判斷是否存在溢出,因為測試過整個RGB空間的所有顏色值,無顏色存在溢出 *(To + 2) = (byte)B; From += 3; To += 3; } }
再來看看反轉的過程,即LAB-XYZ的算法,理論公式如下:
其中:
注意,我這里說的轉換有個前期條件,即LAB的數據是用類似於RGB空間的布局表達的,也就是說LAB各元素為byte類型。
我曾自己的研究過這些算法,如果完全像上面那樣靠整數乘法及移位來實現,主要的難度是t^3這個表達式的計算結果會超出int類型的表達范圍,而如果用64位的long類型,在目前32位機器依舊占主流配置的情況下,速度會下降很多。因此,我最后的研究還是以空間換時間的方法來實現。具體分析如下:
觀察上式分析,Y的值只於L有關,而L由於我們的限定,只能取[0,255]這256個值,因此建立一個256個元素的查找表即可,而X及Z的值分別於L及A/B有關,需要建立256*256個元素的查找表即可,大約占用0.25MB的內存。查找表的建立如下:
for (I = 0; I < 256; I++) { T = I * Div116 + Add16; if (T > ThresoldF) Y = T * T * T; else Y = MulT * (T - Sub4Div29); TabY[I] = (int)(Y * 255 + 0.5); // 映射到[0,255] for (J = 0; J < 256; J++) { X = T + Div500 * (J - 128); if (X > ThresoldF) X = X * X * X; else X = MulT * (X - Sub4Div29); TabX[Index] = (int)(X * 255 + 0.5); Z = T - Div200 * (J - 128); if (Z > ThresoldF) Z = Z * Z * Z; else Z = MulT * (Z - Sub4Div29); TabZ[Index] = (int)(Z * 255 + 0.5); Index++; } }
最終的LAB-RGB轉換算法如下:
public static unsafe void ToRGB(byte* From, byte* To, int Length = 1) { if (Length < 1) return; byte* End = From + Length * 3; int L, A, B, X, Y, Z; int Blue, Green, Red; while (From != End) { L = *(From); A = *(From + 1); B = *(From + 2); X = TabX[L * 256 + A]; // *256編譯后會自動優化為移位的 Y = TabY[L]; Z = TabZ[L * 256 + B]; Blue = (X * LABBXI + Y * LABBYI + Z * LABBZI + HalfShiftValue) >> Shift; Green = (X * LABGXI + Y * LABGYI + Z * LABGZI + HalfShiftValue) >> Shift; Red = (X * LABRXI + Y * LABRYI + Z * LABRZI + HalfShiftValue) >> Shift; if (Red > 255) Red = 255; else if (Red < 0) Red = 0; if (Green > 255) Green = 255; else if (Green < 0) Green = 0; // 需要有這個判斷 if (Blue > 255) Blue = 255; else if (Blue < 0) Blue = 0; *(To) = (byte)Blue; *(To + 1) = (byte)Green; *(To + 2) = (byte)Red; From += 3; To += 3; } }
通過以上的分析,可以看出,這個轉換的過程代碼很簡單,清晰,而且效率不菲,對一副4000*3000的數碼照片進行RGB->LAB,然后再LAB->RGB算法本體的時間只有250ms。
還有幾個優化的地方就是我的所有的查找表都不是用的C#的數組,而是直接分配內存,這是因為C#的數組在很多情況下會有一個判斷是否越界的匯編碼,而用非托管內存則不會。
比如,以下是用非托管內存的數組訪問的反匯編:
static int* TabX = (int*)Marshal.AllocHGlobal(256 * 256 * 4); // 這是原始的定義
X = TabX[L * 256 + A]; // *256編譯后會自動優化為移位的 00000037 mov eax,edi 00000039 shl eax,8 // 看到這里的移位沒有 0000003c add eax,edx 0000003e mov edx,dword ptr ds:[005A1F0Ch] 00000044 mov eax,dword ptr [edx+eax*4] 00000047 mov dword ptr [ebp-14h],eax
而用C#的數組方式生產的匯編如下:
static int[] TabX = new int[256 * 256]; // 這是原始的定義
X = TabX[L * 256 + A]; // *256編譯后會自動優化為移位的 0000003c mov eax,edi 0000003e shl eax,8 00000041 add eax,edx 00000043 mov edx,dword ptr ds:[02A27C68h] 00000049 cmp eax,dword ptr [edx+4] // 多出這兩句代碼 0000004c jae 00000133 00000052 mov eax,dword ptr [edx+eax*4+8] 00000056 mov dword ptr [ebp-14h],eax
其實還有很多細節上的優化的東西,比如語句的順序的講究,有的時候就是調換下不同行的語句,程序的執行效率就有很多的不同,這主要是編譯器的優化不同造成的,比如適當的順序會讓編譯器選擇某個常用變量為寄存器變量。 還比如有人喜歡用下面的代碼
*To++ = (byte)L; *To++ = (byte)A; *To++ = (byte)B;
來代替:
*To = (byte)L; *(To + 1) = (byte)A; *(To + 2) = (byte)B; To += 3;
雖然代碼看上去簡潔了,可你執行后就知道速度反而慢了,為什么,我想我會在適當時候寫一些關於C#優化方面的粗淺文章在對此進行解釋吧。
最后附上一些處理的效果,還是拿系列1文章中那些崇洋的新貴門來做實驗吧:
原圖:
轉換后的綜合圖像:
L通道:
A通道:
B通道:
同樣的道理,上述快速算法如果進行多次轉換,必然也存在精度上的損失。
LAB空間在以后的膚色檢測文章中還會有提到。
'*********************************************************************
轉載請保留以下信息:
作者: laviewpbt
時間:2013.2.2 11點於家中
QQ:33184777
E-Mail : laviewpbt@sina.com