JavaScript图像处理(6) - 减色算法(Color Reduction)

之前在对图像进行基于直方图的处理时提到过,RGB图像每个颜色通道有256个色阶,所以RGB颜色空间最多可以有16777216(即256^3)种颜色。这是一个巨大的空间,一个显而易见的事实是对于大多数图像而言,这个颜色空间太大了——一千六百多万种颜色,几乎可以给全高清画质(1080p)的图像每个像素点分配一种颜色!大多数情况下,一个图像里面只有远远少得多的颜色,RGB颜色空间大量的颜色并没有利用上。也就是说,我们往往可以用少得多的颜色(几十到几百之间)完全描述一幅图像。

为什么要用更少的颜色描述同一幅图像?答案是其实很简单,更少的颜色意味着可以用更少的空间存储同一幅图像,在传输图像的时候也可以在不影响内容的前提下节省流量/带宽。举个简单的例子,如果我们可以用256种颜色来描述一副图像的话,那么每个像素只需要用1个字节存储;相比之下,RGB图像每个像素需要3个字节来存储。256色的图像可以节省66%的存储空间!当然,这个假设成立的前提是我们能够找到一组颜色来很好地代表原图像里的色彩,而这并不是一个简单的事情。

根据给定的图像计算出最优颜色组合的算法称为减色算法——顾名思义,就是减少描述图像所需要的颜色数量的算法。这类算法在计算机图形学发展的早起以及互联网兴起的时候曾引起研究人员的极大兴趣,因为它们可以帮助人们利用有限的硬件、网络资源获得最好的效果或最大的收益。在计算机图形学技术日趋成熟、互联网高度发达的今天,这些算法依然在不同的应用场合发挥着重要的作用。

减色算法主要有直接量化、统计量化(population based)、颜色空间分割以及聚类等方法。此外,利用人工神经网络来进行减色操作也可以获得良好的效果。

1. 直接量化

直接量化是最简单的减色算法,它只是对颜色空间进行直接的重采样来减少颜色。具体来说,直接量化对每个颜色通道单独重新采样,将每个通道的色阶从256减少到某个制定的数字。这样得到一个新的小的多的颜色空间,而原图像中的每一个像素则用在新的颜色空间中的最近邻取代:

...
// colors:  the desired number of colors
// it is restricted to the cube of an integer here
// levs:    color levels per channel           
var levs = Math.ceil(Math.pow(colors, 1.0/3.0));
return src.map(function(c) {
    // discretize each channel
    var r = Math.round(Math.round((c.r / 255.0) * levs) / levs * 255.0);
    var g = Math.round(Math.round((c.g / 255.0) * levs) / levs * 255.0);
    var b = Math.round(Math.round((c.b / 255.0) * levs) / levs * 255.0);
    return new Color(r, g, b, c.a);
});
直接量化的效果其实已经很不错了,用256色已经可以基本上恢复出原图的绝大部分颜色了,当然如果用更多的颜色效果会好得多。从图像的直方图也可以很清楚的看到直接量化的效果。直接量化之后的直方图只包含有限的几个峰值,它们正是直接量化时的颜色采样点。

原图

64色图像


256色图像



2. 统计量化

直接量化是对颜色空间进行均匀采样,这样做的最大弊端在于大量的采样点落在真实像素值分布区域之外,因而对最终的图像没有任何贡献,反而浪费了大多数采样点。这个问题和之前在做直方图均衡时提到过的问题其实是一样的,即颜色空间没有被充分利用。在做直方图均衡时,我们采样的方法是调整直方图使得累积分布曲线呈线性,从而使图像像素点的亮度值尽可能均匀地分布。这里我们可以用类似的方法来提高颜色空间中采样点的利用率——利用原图的直方图来引导采样点的选取,使得每个采样点可以大致覆盖相同数量的像素点。要做到这一点,我们需要将直方图均衡算法扩展到三维颜色空间,即在三维颜色空间中建立直方图,然后根据得到的直方图来进行有效的采样。

直接在三维空间建立直方图并不是一个十分可行的方法,原因在于三维空间的直方图有一千六百多万个(256^3)单元格,同时可以预见这个直方图必定是十分稀疏的,直接对这样一个巨大的而且稀疏的直方图进行操作只能得到一个效率低下的算法。如何才能高效地处理这个直方图呢?我们可以将这个直方图分解成三个一维的直方图——每个颜色通道建立一个直方图并且单独处理。这样我们可以很自然地得到一下这个量化算法:对每个颜色通道建立直方图,然后根据这些直方图对各个颜色通道单独采样,再利用这些采样点来组合成最终的颜色表,原图中的每个像素点用颜色表中最接近的颜色替换掉。在对颜色通道采样的时候,利用和直方图均衡类似的方法,在像素值分布多的区域进行密集采样,别的区域稀疏采样。

... 
var hist = colorHistogram(src, 0, 0, src.w, src.h);
var rcdf = normalizecdf( buildcdf(hist[0]) );
var gcdf = normalizecdf( buildcdf(hist[1]) );
var bcdf = normalizecdf( buildcdf(hist[2]) );

var levels = Math.ceil(Math.pow(colors, 1.0/3.0));

// get sample points using CDF
var genSamples = function(cdf) {
    var pts = [];
    var step = (1.0 - cdf[0]) / levels;

    for(var j=0;j<=levels;j++) {
        var p = step * j + cdf[0];
        for(var i=1;i<256;i++) {
            if( cdf[i-1] <= p && cdf[i] >= p ) {
                pts.push(i);
                break;
            }
        }
    }
    return pts;
}

// sample points in each channel
var rPoints = genSamples(rcdf),
    gPoints = genSamples(gcdf),
    bPoints = genSamples(bcdf);

// assemble the samples to a color table
return src.map(function(c) {
    // find closet r sample point
    var r = findClosest(c.r, rPoints);

    // find closet g sample point
    var g = findClosest(c.g, gPoints);
 
    // find closet b sample point
    var b = findClosest(c.b, bPoints); return new Color(r, g, b, c.a);
});

64色效果

256色效果


3. 颜色空间分割(Median-Cut)

前面提到利用三维直方图来优化采样点的选择,但是因为直接建立三维直方图可行性不大,所以退而求其次通过三个一维直方图来近似三维直方图。颜色空间算法则是通过另外一种方法来近似三维直方图。由于图像颜色在三维颜色空间中的分布极其稀疏,只是在某些很小的区域有比较密集的分布。如果可以定位这些小区域,进而在这些小区域上分配颜色采样点的话,就可以得到一个对不同图像具有很好针对性的采样方法。定位这些小区域的方法就是下面要描述的颜色空间分割方法——Median-Cut算法。 这个算法的本质是在颜色空间建立一棵二叉树,通过不断地细化这棵树来近似得到一个颜色三维直方图,然后再根据这棵树来分配采样点。具体来说,该算法有以下几个步骤:

a)获取当前图像所有颜色样本

b)在颜色空间中选取一个方向(R,G或者B)将这些样本等分成两部分,一般选择颜色样本包围盒的长轴方向

c)对分出来的两部分按照同样的方法再次进行细分,直到达到所需要的精度为止;叶子节点包含至少一个颜色样本。

d)完成细分后,对于每个叶子节点,考虑其包含的颜色样本。取这些颜色样本的包围盒中心处颜色作为该节点的代表颜色,即我们需要的颜色采样点。所有叶子节点的代表颜色构成最终的颜色表。

具体的实现可以参考 这段示例代码

Median-Cut算法最大的优势在于它是一个基于图像颜色样本分布的自适应方法,不论图像中颜色样本的分布如何,总是可以生成一个和颜色样本分布匹配良好的颜色表:在颜色样本分布密集的区域内采样点分布也相对密集,其他区域则分配了较少的采样点。这是因为在进行颜色空间分割的时候,总是将一个节点内的颜色样本等分成两部分。这意味着相同数目的颜色样本总是用同样数量的采样点来代表,所以颜色样本分布密集的区域,采样点的数量自然就会多,反之则相应的比较少。

Median-Cut算法自1980年由Paul Heckbert提出之后由于其出色的表现很快流行开来,是最重要、应用最广泛的减色算法之一。常用的图像处理软件如Photoshop、GIMP等都采用了这个算法或其变种。以下是用该算法得到的效果图,可以从旁边的直方图看出,这个算法所选取的采样点分布比前面两种算法都要复杂,并且是和图像的颜色分布一致的一个采样方案。很自然的,减色后的效果也是相当的好,在只用64色的情况下也能得到远比直接量化和统计量化好的多的结果,而256色图像跟原图相比误差已经很小了。

64色效果


256色效果


4. 颜色聚类(k-Means Clustering)

颜色聚类采用了和前面的算法相同的采样策略,即根据图像颜色的分布的多少来分配采样点,其不同之处在于寻找采样点的方法。颜色聚类顾名思义,即将像素按颜色的相似程度归类。得到了颜色分类之后,从每一类中选取一个代表颜色,即可得到所需的颜色采样点来组合成最终的颜色表。聚类算法(Clustering Algorithms)是机器学习领域中最基本的算法,已经研究得非常成熟了。 在这里我们采用最简单的聚类算法——k均值聚类(k-Means clustering)来展示一下通过聚类进行减色操作的方法。关于k均值聚类的具体细节可以参考这里,下面的是利用这个方法减色得到的效果,实现的细节参考这里。值得注意的是k均值聚类的结果非常依赖于初值,因此要获得好的聚类效果,需要通过一定的呃方法来选取一个好的初始类别。从下面的结果可以看到k均值聚类减色的效果非常好,64色的误差已经很小了,而256色的结果就非常接近原图了。

64色效果


256色效果


5. 神经网络方法(ANN)

神经网络方法是一个非常有趣的方法,他采取了一个和前面四个方法不同的思路来解决减色问题。前面四个方法归结起来都来源于同一个思想,即通过在颜色空间合理地选取采样点来构造颜色表,使得减色后的图像和原图尽可能地接近。神经网络方法则有点反其道而行的意味,它从一个初始的颜色表出发,通过不断修改颜色表来改善减色效果。这个颜色表通过神经元来编码,而修改颜色表的过程则是通过神经元的反馈调节来达到。神经元的反馈调节利用所给的颜色样本来进行,对于每个颜色样本,先找到编码的颜色与之最接近的神经元,然后修改该神经元编码的颜色值,使得新的颜色和这个样本的颜色更接近,即对于某个神经元的颜色(r, g, b):

r = sample.r * alpha + r * (1-alpha)
g = sample.g * alpha + g * (1-alpha)
b = sample.b * alpha + b * (1-alpha)

其中alpha是松弛参数(relaxation factor),它的值大于0小于1。上面三个式子其实就是在样本颜色和神经元颜色之间进行线性插值,并将得到的颜色值赋予神经元。很显然这样做的效果就是使得神经元的颜色更为接近输入样本的颜色。这里的松弛参数是一个很小的值,因为我们必须缓慢的调节神经元的颜色值以保证整个神经网络逐步收敛于最小误差状态(即通过神经网络产生的图像和原图之间误差最小)。如果alpha值过大的话,调整过程中神经元的颜色值会剧烈变化,几乎可以肯定最终最终不会达到收敛。这个反馈调节的过程需要反复进行许多次, 每个颜色样本对神经网络的修正随着反馈调节次数的增加越来越小,最后当颜色样本对神经网络的修正值小于一定阈值的时候停止反馈调节。具体的实现参考这里

64色效果


256色效果



几种方法的效果也可以通过所得到的图像的平均量化误差(RMSE)来比较。可以看到后面三种方法可以得到明显优于直接量化和统计量化。

方法 64 256
直接量化 19.3267 10.7729
统计量化 17.4291 11.2576
Median-Cut 7.2582 4.1104
kmeans 6.7219 4.2924
ANN 8.2066 4.9518

阅读更多

没有更多推荐了,返回首页