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;
}