快速计算Hue色环

来源:互联网 发布:java urlencoder 编辑:程序博客网 时间:2024/05/11 00:35

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;
}
原创粉丝点击