基本概念

     什么是色彩平衡(Color Balance):在图像处理和编辑中,对颜色强度(尤其是红,绿,蓝这种主色)进行全局调整即是色彩平衡。这种调整的一个重要目的是突出显示某种色调—-尤其是中立色。

 

      调整方法

     图像编辑中的色彩平衡调整方法基本是直接操作RGB通道上的像素点,和其他场景下的色彩平衡很不一样。(比如摄影,是通过物理上的方法来实现:特殊镜头,特殊滤镜等)基于这个观点,亮度调整一文中提到的各种方法都是可以来做色彩平衡(PS Tutorial):曲线调整,色阶调整等—-只需要将作用通道设置为相应的色彩通道即可。在这里阿毛要罗嗦一句:在PS或者是GIMP提供的颜色相关的调整方法中,曲线调整的效果是最好的,因为它的参数完全是自定义的,最精细。

 

      GIMP/PS中色彩平衡

     PS和GIMP中都提供了独立色彩平衡功能,其界面和选项基本都是一样,如下图:

    捕获

     单纯从原理上来说,这种色彩平衡的实现也比较简单:

     滚动条右边的显示的RGB,而左边显示的是CMYK色彩空间中的Cyan(青),Magenta(洋红),Yellow(黄)。RGB是一种加色模型(所谓的自发光),而CMYK是一种减色模型(吸收光)。其关系如下图所示:

捕获2

可以看出,RGB的不同相加组合可以产生CMYK中的任意一种,而相应CMYK空间中各种色彩的缺失组合也可以产生RGB色彩空间中任意一种颜色。所以得出一个结论就是:所有加减色操作完全可以直接在RGB空间内完成,而不需要转换到CMYK色彩空间。比如增加红色就必然引起青色的减少。见过很多写法是先从RGB转换到CMYK空间再进行调整,调整完毕后再转换回来—-这些做法不仅低效而且有画蛇添足之嫌。

     然后就是对亮度区域的鉴别:阴影,中间调,高光。这三个概念相对而言比较模糊:并没有所谓的明确定义表示亮度超过多少可以认为是高光区,而亮度低于多少是阴影区—-只有一个相对模糊的界定。而单个通道上的像素亮度并不能反映这个像素真实的亮度。所以在亮度区域的选择上只是反映了调整力度的大小,而并没有完全真实地反映调整区域范围。(如果进行亮度区域界定并对不同亮度区域进行调整,一来比较耗时间,而来也并不会取得太好的效果)

     最下面的选项:保持明度。明度计算的公式为

                                     Lightness = (Max(RGB) + Min(RGB)) / 2

因为直接操作了各个通道上的亮度值,整个像素点对应的明度也有可能发生变化,保持明度可以让用户感觉整个色彩平衡的变化更平滑。

     原理讲完,上代码:(因为WP本身的限制,一些过长的行真不是一般的难看 =。=)

void BalanceColor(TiBitmapData& bitmap,int cyan, int magenta, int yellow,
        TINYIMAGE_TRANSFERMODE mode,bool preserveLuminosity)
{
 
    TINYIMAGE_ASSERT_VOID(cyan>= -100 && cyan<=100);
    TINYIMAGE_ASSERT_VOID(magenta>= -100 && magenta<=100);
    TINYIMAGE_ASSERT_VOID(yellow>= -100 && yellow<=100);
 
    //初始化色彩调整区域参数
    double  cyan_red[3];
    double  magenta_green[3];
    double  yellow_blue[3];
    for (int i = TINYIMAGE_TRANSFERMODE_SHADOWS; i <= TINYIMAGE_TRANSFERMODE_HIGHLIGHTS; i++)
    {
        cyan_red[i] = 0.0;
        magenta_green[i] = 0.0;
        yellow_blue[i] = 0.0;
    }
    cyan_red[mode] = cyan;
    magenta_green[mode] = magenta;
    yellow_blue[mode] = yellow;
 
 
    //初始化转换用的数组
    InitTransferArray();
 
 
    //创建LOOKUP TABLE
    double  *cyan_red_transfer[3];
    double  *magenta_green_transfer[3];
    double  *yellow_blue_transfer[3];
    int   red, green, blue;
    u8 r_lookup[256],g_lookup[256],b_lookup[256];
 
 
    //设置转换数组
    cyan_red_transfer[TINYIMAGE_TRANSFERMODE_SHADOWS] = (cyan_red[TINYIMAGE_TRANSFERMODE_SHADOWS] > 0) ? shadows_add : shadows_sub;
    cyan_red_transfer[TINYIMAGE_TRANSFERMODE_MIDTONES] = (cyan_red[TINYIMAGE_TRANSFERMODE_MIDTONES] > 0) ? midtones_add : midtones_sub;
    cyan_red_transfer[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] = (cyan_red[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] > 0) ? highlights_add : highlights_sub;
 
    magenta_green_transfer[TINYIMAGE_TRANSFERMODE_SHADOWS] = (magenta_green[TINYIMAGE_TRANSFERMODE_SHADOWS] > 0) ? shadows_add : shadows_sub;
    magenta_green_transfer[TINYIMAGE_TRANSFERMODE_MIDTONES] =   (magenta_green[TINYIMAGE_TRANSFERMODE_MIDTONES] > 0) ? midtones_add : midtones_sub;
    magenta_green_transfer[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] = (magenta_green[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] > 0) ? highlights_add : highlights_sub;
 
    yellow_blue_transfer[TINYIMAGE_TRANSFERMODE_SHADOWS] = (yellow_blue[TINYIMAGE_TRANSFERMODE_SHADOWS] > 0) ? shadows_add : shadows_sub;
    yellow_blue_transfer[TINYIMAGE_TRANSFERMODE_MIDTONES] = (yellow_blue[TINYIMAGE_TRANSFERMODE_MIDTONES] > 0) ? midtones_add : midtones_sub;
    yellow_blue_transfer[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] =   (yellow_blue[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] > 0) ? highlights_add : highlights_sub;
 
    for (int i = 0; i < 256; i++) 
    {
        red = i;
        green = i;
        blue = i;
 
        red += (int)( cyan_red[TINYIMAGE_TRANSFERMODE_SHADOWS] * cyan_red_transfer[TINYIMAGE_TRANSFERMODE_SHADOWS][red] 
                    + cyan_red[TINYIMAGE_TRANSFERMODE_MIDTONES] * cyan_red_transfer[TINYIMAGE_TRANSFERMODE_MIDTONES][red]
                    + cyan_red[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] * cyan_red_transfer[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS][red]);
        red = CLAMP0255 (red);
 
        green += (int)( magenta_green[TINYIMAGE_TRANSFERMODE_SHADOWS] * magenta_green_transfer[TINYIMAGE_TRANSFERMODE_SHADOWS][green]
                      + magenta_green[TINYIMAGE_TRANSFERMODE_MIDTONES] * magenta_green_transfer[TINYIMAGE_TRANSFERMODE_MIDTONES][green]
                      + magenta_green[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] * magenta_green_transfer[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS][green]);
        green = CLAMP0255 (green);
 
        blue +=(int)( yellow_blue[TINYIMAGE_TRANSFERMODE_SHADOWS] * yellow_blue_transfer[TINYIMAGE_TRANSFERMODE_SHADOWS][blue]
                    + yellow_blue[TINYIMAGE_TRANSFERMODE_MIDTONES] * yellow_blue_transfer[TINYIMAGE_TRANSFERMODE_MIDTONES][blue]
                    + yellow_blue[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS] * yellow_blue_transfer[TINYIMAGE_TRANSFERMODE_HIGHLIGHTS][blue]);
        blue = CLAMP0255 (blue);
 
        r_lookup[i] = (u8)red;
        g_lookup[i] = (u8)green;
        b_lookup[i] = (u8)blue;
 
    }
 
    //如果不需要保证亮度不变化,直接赋值就可以了
    if (!preserveLuminosity)
    {
        AdjustCurve(bitmap,r_lookup,g_lookup,b_lookup);
    }
    else
    {
        PreserveLuminosityAdjustCurve(bitmap,r_lookup,g_lookup,b_lookup);
    }
     
}

整个色彩平衡的过程很简单:先是初始化各个亮度区域的调整系数,再通过用户传入的调整系数调整相应的变化参数,最后计算得出调整色彩用的三个Lookup Table。整个代码段中最最关键便是InitTransferArray方法,其定义如下:

static bool transferInit = false;
 
//变亮的转换数组
static double  highlights_add[256] = { 0 };
static double  midtones_add[256]   = { 0 };
static double  shadows_add[256]    = { 0 };
 
//变暗的转换数组
static double  highlights_sub[256] = { 0 };
static double  midtones_sub[256]   = { 0 };
static double  shadows_sub[256]    = { 0 };
 
 
void InitTransferArray()
{
    if (!transferInit)
    {
        for (int i = 0; i < 256; i++) 
        {
            highlights_add[i] = shadows_sub[255 - i] = (1.075 - 1 / ((double) i / 16.0 + 1));
            midtones_add[i] = midtones_sub[i] = 0.667 * (1 - SQR(((double) i - 127.0) / 127.0));
            shadows_add[i] = highlights_sub[i] = 0.667 * (1 - SQR (((double) i - 127.0) / 127.0));
        }
        transferInit = true;
    }
}

这里并不细究上面这个Transfer更深层次的意义(事实是:在网上基本没有相应的解释),但是细心的人很容易发现,在Transfer的初始化过程中,高光增等同于阴影减,阴影增等同于高光减,这没有太大问题,但是问题是中间调的增减竟然和阴影增/高光减是同一个公式。这似乎是很奇怪的,而这个问题也引起了GIMP某些开发者的争议—-建议用其他方法如贝叶斯多项式来使得整个调整更平缓。但在最后Release出来的版本中GIMP还是用了上面这个公式。(相关的讨论,喜欢八卦和考古的童鞋可以猛击这里进行考古活动)

     至于上面函数中用到的AdjustCurve的方法已在亮度调整一文中放出,就不重复了。而PreserveLuminosityAdjustCurve方法其实也很简单,只是在用Lookup Table去调整原图各个像素时,进行一次RGB-HSL-RGB的转换,并保证转换前后的L不变即可:(这里有个小技巧:RGB和HSL的转换可以写一个专门的Int型转换,这样可以减少几次浮点除法运算,如下面的Rgb2Hsl_Int)

void PreserveLuminosityAdjustCurve(TiBitmapData& bitmap,u8 (&r_lookup)[256],
                                    u8 (&g_lookup)[256],u8 (&b_lookup)[256])
{
    int width    = bitmap.GetWidth();
    int height    = bitmap.GetHeight();
    int stride    = bitmap.GetStride();
    int bpp        = bitmap.GetBpp();
    u8* bmpData    = bitmap.GetBmpData();
    int offset    = stride - width * bpp;
    int r,g,b;
    int rlookup,glookup,blookup;
 
 
    for (int i = 0; i < height; i ++)
    {
        for (int j = 0; j < width; j++)
        {
            r = (int)bmpData[rIndex];
            g = (int)bmpData[gIndex];
            b = (int)bmpData[bIndex];
 
            rlookup = (int)r_lookup[r];
            glookup = (int)g_lookup[g];
            blookup = (int)b_lookup[b];
 
            Rgb2Hsl_Int(rlookup,glookup,blookup);
            blookup = Rgb2Hsl_L(r,g,b);
            Hsl2Rgb_Int(rlookup,glookup,blookup);
 
 
            bmpData[rIndex] = (u8)rlookup;
            bmpData[gIndex] = (u8)glookup;
            bmpData[bIndex] = (u8)blookup;
            bmpData += bpp;
        }
        bmpData += offset;
    }
}

 

 

转载自: http://www.xiangwangfeng.com/2011/01/20/%E5%9B%BE%E5%83%8F%E7%BC%96%E8%BE%91%E4%B9%8B%E8%89%B2%E5%BD%A9%E5%B9%B3%E8%A1%A1/

posted on 2013-09-05 09:26  裴银祥的博客园  阅读(1228)  评论(0编辑  收藏  举报