File: FastHue.txt
Name: 快速計算Hue色環
Author: zyl910
Blog: http://blog.csdn.net/zyl910/
Version: V1.00
Updata: 2006-11-3
下載(注意修改下載後的副檔名)
一、HSV色彩空間
H: 色調(Hue)。範圍: [0, 360)
0度: 紅色,RGB:(255, 0, 0), 255:R, 0:B,G+
60度: 黃色,RGB:(255,255, 0),255:G, 0:B, R-
120度: 綠色,RGB:( 0,255, 0),255:G, 0:R,B+
180度: 青色,RGB:( 0,255,255),255:B, 0:R,G-
240度: 藍色,RGB:( 0, 0,255),255:B, 0:G,R+
300度: 紫色,RGB:(255, 0,255),255:R, 0:G,B-
360度: 紅色,RGB:(255, 0, 0),255:R, 0:B,G+
在這些標準的顏色值之間的顏色是通過線性插值得到的。如30度的橙色,它是0度紅色與60度黃色之間的顏色,所以它的RGB值是 (255, 0, 0)*50% + (255,255, 0)*50% = (255,127.5,0)。
由於在同一個60度區間中的顏色值只有一個分量不同,所以只需要對一個分量進行線性插值。
S: 飽和度(Saturation)。範圍: [0%, 100%]。是 H所代表的顏色 與 白色 混合的比率。
假設某個顏色的H分量為30、S分量為80%(、V分量為100%),它的RGB值是: (255,127.5,0)*80% + (255, 255, 255)*20% = (255,153,51)
V: 亮度( Value 或 Brightness,所以有時也叫HSB)。範圍: [0%, 100%]。是 H、S所代表的顏色 與 黑色 混合的比率。
假設某個顏色的H分量為30、S分量為80%、V分量為60%,它的RGB值是: (255,153,51)*60% + (0,0,0)*40% = (255,153,51)*60% = (153,91.8,30.6)
也就是說計算步驟是:先根據H算出純色顏色值,然後根據S將結果與白色混合,再根據V將結果與黑色混合。
二、快速計算Hue色環
2.1 分析[0,60)區間
我們先觀察一下[0,60)區間的顏色值:
0: R=255, B=0, G = 0 * 255 / 60 = 0/60 = 0 + 0/60
1: R=255, B=0, G = 1 * 255 / 60 = 255/60 = 4 + 15/60
2: R=255, B=0, G = 2 * 255 / 60 = 510/60 = 8 + 30/60
3: R=255, B=0, G = 3 * 255 / 60 = 765/60 = 12 + 45/60
4: R=255, B=0, G = 4 * 255 / 60 = 1020/60 = 17 + 0/60
...
56: R=255, B=0, G = 56 * 255 / 60 = 14280/60 = 238 + 0/60
57: R=255, B=0, G = 57 * 255 / 60 = 14535/60 = 242 + 15/60
58: R=255, B=0, G = 58 * 255 / 60 = 14790/60 = 246 + 30/60
59: R=255, B=0, G = 59 * 255 / 60 = 15045/60 = 250 + 45/60
60: R=255, B=0, G = 60 * 255 / 60 = 15300/60 = 255 + 0/60
由於RGB分量的最大值是255、區間的尺寸是60,所以計算公式為:G = i * 255 / 60
最終結果我寫成帶分數形式,因為這種形式比較容易理解——整數部分是就是RGB值。至於分數部分,可以使用四捨五入的,但我個人覺得不進行舍入處理顯得更平均一些。
可以看出,由於是線性插值,下一個比上一個的多出了 255/60(或 4 + 15/60)。最終到達60時,恰好整數部分為255、分數部分為0。
於是我們得到這樣的演算法:
整數部分 = 0
分數部分 = 0
while(整數部分 < 255){
繪製像素(RGB(255, 整數部分, 0))
整數部分 += 4 // 255/60 = 4 + 15/60
分數部分 += 15
if (分數部分 >= 60) {
分數部分 -= 60
整數部分++
}
}
是不是感覺有點像Bresenham演算法。我就是在看懂Bresenham演算法時,才發現自己這才開始理解有理數的。有理數是兩個數位比值(分子和分母),寫成假分數或帶分數形式是最容易理解的,生活上慣用的帶小數寫法反而有堵塞思維之嫌。
2.2 分析[60,120)區間
先前的[0,60)區間的G分量是增長的,對於像[60,120)區間這樣的R分量減少的區間又該怎麼呢?
我們來觀察一下:
60: G=255, B=0, R = 255 - ( 60 - 60) * 255 / 60 = 255 - 0 * 255 / 60 = 255 - 0/60 = 255 - ( 0 + 0/60)
61: G=255, B=0, R = 255 - ( 61 - 60) * 255 / 60 = 255 - 1 * 255 / 60 = 255 - 255/60 = 255 - ( 4 + 15/60)
62: G=255, B=0, R = 255 - ( 62 - 60) * 255 / 60 = 255 - 2 * 255 / 60 = 255 - 510/60 = 255 - ( 8 + 30/60)
63: G=255, B=0, R = 255 - ( 63 - 60) * 255 / 60 = 255 - 3 * 255 / 60 = 255 - 765/60 = 255 - ( 12 + 45/60)
64: G=255, B=0, R = 255 - ( 64 - 60) * 255 / 60 = 255 - 4 * 255 / 60 = 255 - 1020/60 = 255 - ( 17 + 0/60)
...
116: G=255, B=0, R = 255 - (116 - 60) * 255 / 60 = 255 - 56 * 255 / 60 = 255 - 14280/60 = 255 - (238 + 0/60)
117: G=255, B=0, R = 255 - (117 - 60) * 255 / 60 = 255 - 57 * 255 / 60 = 255 - 14535/60 = 255 - (242 + 15/60)
118: G=255, B=0, R = 255 - (118 - 60) * 255 / 60 = 255 - 58 * 255 / 60 = 255 - 14790/60 = 255 - (246 + 30/60)
119: G=255, B=0, R = 255 - (119 - 60) * 255 / 60 = 255 - 59 * 255 / 60 = 255 - 15045/60 = 255 - (250 + 45/60)
120: G=255, B=0, R = 255 - (120 - 60) * 255 / 60 = 255 - 60 * 255 / 60 = 255 - 15300/60 = 255 - (255 + 0/60)
由於現在是[60,120)區間,且現在是減少,所以計算公式為:R = (i-60) * 255 / 60 = (120 - i) * 255 / 60
可以看出計算帶分數的方法是一樣的,只是在繪製時R分量為“255 - 帶分數”而已
2.3 處理任意寬度的演算法
剛才我們分析了 [0,60)區間 和 [60,120)區間 的Hue色環。對於其他區間,計算顏色值的方法是一樣的,只不過所填寫的RGB分量不同而已。所以我們應該考慮編寫一個完整的計算Hue色環的辦法。
如果單純是產生寬度是360的Hue色環的話,那我們沒必要寫程式,只用一個有360個元素的數組來查表就行了,所以我們需要的能處理任意寬度的演算法。由於使用者輸入的色環寬度值不一定是6的倍數,所以每個區間的長度不是整數。
先回顧一下我們分析[0,60)區間時,說“由於RGB分量的最大值是255、區間的尺寸是60,所以計算公式為:G = i * 255 / 60”。如果我們將這兩個係數同時放大6倍,那麼式子變為“G = i * (255*6) / 360”。根據比例性質,結果與原來的式子相同。所以任意寬度下的計算公式為:G = i * (255*6) / huesize
然後我們考慮如何設計函數。由於現在Windows平台很流行,所以我希望程式直接輸出真彩色的DIB(裝置無關位元影像)位元影像資料。為了適應不同情況(24位或32位),我又提供了cbPixel參數已得知每個像素所佔位元組。
最終代碼是:
// 計算Hue色環
// Return: 成功返回非0,失敗返回0。
// Args:
// lpBuf: 真彩色DIB位元影像資料緩衝區
// cbPixel: 一個像素所佔位元組
// huesize: Hue色環的寬度
BOOL MakeHue(LPVOID lpBuf, int cbPixel, int huesize)
{
int value, fract; // (255*6)/huesize 的整數部分和分數部分
int i, ifract; // 當前值
LPBYTE pby = (LPBYTE)lpBuf;
ASSERT(lpBuf != 0);
ASSERT(huesize > 0);
// (255*6)/huesize 的整數部分和分數部分
value = (255*6) / huesize;
fract = (255*6) % huesize;
i = ifract = 0;
// red ~ yellow: [0, 60)
while(i < 255) {
// Draw
pby[2] = 0xff;
pby[1] = i;
pby[0] = 0x00;
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
};
i -= 255;
// yellow ~ green: [60, 120)
while(i < 255) {
// Draw
pby[2] = 0xff - i;
pby[1] = 0xff;
pby[0] = 0x00;
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
};
i -= 255;
// green ~ cyan: [120, 180)
while(i < 255) {
// Draw
pby[2] = 0x00;
pby[1] = 0xff;
pby[0] = i;
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
};
i -= 255;
// cyan ~ blue: [180, 240)
while(i < 255) {
// Draw
pby[2] = 0x00;
pby[1] = 0xff - i;
pby[0] = 0xff;
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
};
i -= 255;
// blue ~ magenta: [240, 300)
while(i < 255) {
// Draw
pby[2] = i;
pby[1] = 0x00;
pby[0] = 0xff;
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
};
i -= 255;
// magenta ~ red: [300, 360)
while(i < 255) {
// Draw
pby[2] = 0xff;
pby[1] = 0x00;
pby[0] = 0xff - i;
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
};
//i -= 255;
return FALSE;
}
我承認這樣的代碼不夠簡潔,因為計算i的方式是一樣,只是繪製RGB值的代碼不同而已,這樣不同完全可以通過查表法解決。但是那樣做不利於編譯最佳化(索引是動態),影響速度。
三、快速產生指定飽和度和亮度下的Hue色環
既然是指定了飽和度和亮度,那麼需要根據s、v計算最終的顏色值。
注意每個RGB分量都是單獨計算的,即每個分量都進行了如下的變換:
f(x) = (x*s + 255*(1-s)) * v
= (255 + (x-255)*s)*v
= (255 - (255-x)*s)*v
由於浮點運算很慢,所以我們需要整數演算法。Windows系統是32位作業系統,所以整數是32位。RGB分量是8位,(32-8) / 2 = 24 / 2 = 12,所以s和v可以有12位精度:
is = (DWORD)(s * 1<<12)
iv = (DWORD)(v * 1<<12)
f(x) = (255 - (255-x) * is / (1<<12)) * iv / (1<<12)
= ((255<<12 - (255-x) * is) / (1<<12)) * iv / (1<<12)
= (255<<12 - (255-x) * is) * iv / (1<<24)
= ((255<<12 - (255-x) * is) * iv) >> 24
由於RGB分量的取值範圍是[0,255],所以我們還可以查表最佳化。
最終代碼:
// 計算指定飽和度和亮度時的Hue色環
// Return: 成功返回非0,失敗返回0。
// Args:
// lpBuf: 真彩色DIB位元影像資料緩衝區
// cbPixel: 一個像素所佔位元組
// huesize: Hue色環的寬度
// fS: 飽和度,[0,1]。對數值做飽和處理
// fV: 亮度度,[0,1]。對數值做飽和處理
BOOL MakeHueEx(LPVOID lpBuf, int cbPixel, int huesize, float fS, float fV)
{
BYTE tbl[0x100]; // 顏色值對應表格
DWORD iS, iV; // 12位精度的飽和度與亮度
int value, fract; // (255*6)/huesize 的整數部分和分數部分
int i, ifract; // 當前值
LPBYTE pby = (LPBYTE)lpBuf;
ASSERT(lpBuf != 0);
ASSERT(huesize >= 6);
// 12位精度的飽和度與亮度
if (fS < 0) fS = 0;
else if (fS > 1) fS = 1;
if (fV < 0) fV = 0;
else if (fV > 1) fV = 1;
iS = (DWORD)(fS * (1<<12));
iV = (DWORD)(fV * (1<<12));
// 亮度為0——黑色
if (iV == 0)
{
while(huesize > 0)
{
pby[2] = 0;
pby[1] = 0;
pby[0] = 0;
pby += cbPixel;
huesize--;
}
return TRUE;
}
// 計算 顏色值對應表格
for(i=0; i<=0xff; i++)
{
tbl[i] = (BYTE)( (((255<<12) - (255-i) * iS) * iV + (1<<23)) >> 24 ); // "+ 1<<23" 是為了四捨五入
}
// (255*6)/huesize 的整數部分和分數部分
value = (255*6) / huesize;
fract = (255*6) % huesize;
i = ifract = 0;
// red ~ yellow: [0, 60)
do{
// Draw
pby[2] = tbl[0xff];
pby[1] = tbl[i];
pby[0] = tbl[0x00];
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
}while(i < 255);
i -= 255;
// yellow ~ green: [60, 120)
do{
// Draw
pby[2] = tbl[0xff - i];
pby[1] = tbl[0xff];
pby[0] = tbl[0x00];
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
}while(i < 255);
i -= 255;
// green ~ cyan: [120, 180)
do{
// Draw
pby[2] = tbl[0x00];
pby[1] = tbl[0xff];
pby[0] = tbl[i];
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
}while(i < 255);
i -= 255;
// cyan ~ blue: [180, 240)
do{
// Draw
pby[2] = tbl[0x00];
pby[1] = tbl[0xff - i];
pby[0] = tbl[0xff];
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
}while(i < 255);
i -= 255;
// blue ~ magenta: [240, 300)
do{
// Draw
pby[2] = tbl[i];
pby[1] = tbl[0x00];
pby[0] = tbl[0xff];
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
}while(i < 255);
i -= 255;
// magenta ~ red: [300, 360)
do{
// Draw
pby[2] = tbl[0xff];
pby[1] = tbl[0x00];
pby[0] = tbl[0xff - i];
pby += cbPixel;
// Next
i += value;
ifract += fract;
if (ifract >= huesize)
{
ifract -= huesize;
i++;
}
}while(i < 255);
i -= 255;
return FALSE;
}