颜色量化之中位切分算法(Color quantization)

最近在翻一些webGL的入门相关的东西,发现有这么个概念,觉得挺有意思的。故看了看相关的介绍,顺手熟悉一下相关的API和概念 维科介绍如下: wikipedia--Color_quantization

计算机图形学中,常采用的一种方法是把颜色看成是基于红、绿、蓝三种颜色的混合,也可以采用色度、彩度、亮度等描述颜色,用多种不同的描述符来表示颜色,就称为颜色模型(Color Model),如果有人能量化这三种不同的描述符的数值,就可以用一个三元组来表示一种颜色,例如(r,g,b),这就形成了一个描述颜色的三维坐标系统,选择不同的颜色模型能形成不同坐标系统,坐标系统上所有颜色的集合就称为颜色空间(Color spaces)。 在图形学中,颜色量化是为了减少一张图像中的颜色数并且使用它尽可能的与原始图像一样,在一些由于内存限制只能显示有限颜色的设备上,颜色量化就显得特别的重要

常见的颜色量化方法有三种

流行色算法(popularity algorithm)

是对彩色图像中所有颜色出现的次数所统计分析,即创建一个数组记录各种颜色和该颜色出现的频率,然后把出现次数最多(频率最大)的236中颜色作为调色板的颜色.该算法实现起来比较简单,但该算法可能丢失一些出现频率较低但是视觉效果明显的信息.

中位切割法(Median Cut Algorithm

在RGB彩色空间中,R,G,B三基色对应于空间的3个坐标轴,并将每一个坐标轴都量化到0-255. 0对应全黑,255对应全白. 这样就形成了一个边长256的彩色立方体.所有可能的颜色都对应于立方体上的一个点 颜色量化之中位切分算法(Color quantization) 第1张

八叉树颜色量化(Octree)

最后介绍八叉树颜色量化方法,该算法最早见于文章最早是在1988, M. Gervautz 和 W. Purgathofer 发表的论文《A Simple Method for Color quantization: Octree Quantization》,算法的最大优点是效率高,占用内存少(仅需要不超过(颜色数量+1)个节点,加上一些中间节点所占用的内存),选出的调色板最合理,显示效果最好

本文主要介绍中位切割算法。

中位切割算法的描述很简单 * 1. 找到包含图像中所有颜色的最小块 这一步很简单就是把图片所有的颜色取出来 * 2.分割区块时都选择所有区块中最大(最长的边长最大,或体积最大,或像素最多)的区块 * 3.把这个些点分割成两个区域。

对每个区域重复上述动作,直到取得所需要的颜色数量块。比如你需要64个颜色块,那么就分割成64个区域. 此方法简单直观。但是只能得到2的幂数个色块。当你需要的色块不是2的幂数的时候就需要一定的方法再对结果色块进行集合。 这里推荐一个采用中位切分法实现(JavaScript)的颜色量子化项目:Color Thief。 此方法内部也做了一些优化。 demo示例链接 

颜色量化之中位切分算法(Color quantization) 第2张

我顺便把图片的色块提取出来按照rgb的色值绘制在一个3d的坐标上, 点击查看demo 颜色量化之中位切分算法(Color quantization) 第3张


颜色提取代码如下

let canvas = document.getElementById("myCanvas");
const maxColor = 32;//最终提取色个数
let vBoxList = {};
const image = new Image();
image.onload = function(){
    drawImageActualSize(image);
};
image.src = 'https://gw.alicdn.com/tfs/TB1lgZGuFzqK1RjsZFCXXbbxVXa-1920-1080.png?ac='+Math.random();
image.crossOrigin = 'anonymous';
function drawImageActualSize(img) {
    let t = init(img);
    let vBoxArray=[];
    vBoxArray.push(t);

    while(vBoxArray.length<maxColor&&vBoxArray.length){
        let tVbox=vBoxArray;
        vBoxArray=[];
        tVbox.forEach(function (v) {
             sortColor(v);
            vBoxArray = vBoxArray.concat(cutColorArray(v));
        });
    }
    console.log(vBoxArray);
    let result = averageColor(vBoxArray);
    console.log(result);
    result.sort(function (a,b) {
        return a.r-b.r;
    });
    drawResult(result);

    thiefColor(img);
}

function thiefColor(sourceImage) {
    var colorThief = new ColorThief();
    var thief  =  colorThief.getPalette(sourceImage, 32);
    thief.sort(function (a,b) {
        return a[0]-b[0];
    });
    console.log("thief==="+JSON.stringify(thief));
}
function drawResult(colorResult) {
    let html = "";
    colorResult.forEach(function (v) {
        html+=createDiv(v);
    })
    let resultBox = document.getElementById("resultBox");
    resultBox.innerHTML=html;
}
function createDiv(color) {
    return `<div class="box" style="background-color:rgb(${color.r},${color.g},${color.b})">rgb(${color.r},${color.g},${color.b})</div>`
}


function Vbox(r, g, b, count) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.count = count;
}

function componentToHex(c) {
    let hex = c.toString(16);
    return hex.length === 1 ? "0" + hex : hex;
}

function rgbToHex(r, g, b) {
    return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}

function init(img) {
    //canvas.style.cssText = `height:${img.naturalHeight}px;width:${img.naturalWidth}px`;
    canvas.width = img.naturalWidth;
    canvas.height = img.naturalHeight;
    let ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0,img.width,img.height);
   let colorData = ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight).data;
   let rgbArray = [];
    console.log(rgbArray);
    //按照色素点填充到数组,需要注意数组存为rgba,
    for (let i = 0; i < colorData.length; i = i + 4) {
        let r = colorData[i];
        let g = colorData[i + 1];
        let b = colorData[i + 2];

       let a = colorData[ i+ 3];
        let hex = rgbToHex(r, g, b);
        if (!vBoxList[hex]) {
            let v = new Vbox(r, g, b, 1);
            vBoxList[hex] = v;
            rgbArray.push(v);
        } else {
            vBoxList[hex].count++;
        }

        //let index = getColorIndex(r, g, b);
    }
    return rgbArray;
}

function sortColor(colorArray) {
    //获取rgb三原色的最大值和最小值
    let maxR = colorArray[0].r;
    let maxG = colorArray[0].g;
    let maxB = colorArray[0].b;
    let minR = colorArray[0].r;
    let minG = colorArray[0].g;
    let minB = colorArray[0].b;

    for (let m = 1; m < colorArray.length; m++) {
        let t = colorArray[m];
        maxR = maxR > t.r ? maxR : t.r;
        maxG = maxG > t.g ? maxG : t.g;
        maxB = maxB > t.b ? maxB : t.b;
        minR = minR < t.r ? minR : t.r;
        minG = minG < t.g ? minG : t.g;
        minB = minB < t.b ? minB : t.b;
    }
    console.log(`maxR==${maxR}`);
    console.log(`maxG==${maxG}`);
    console.log(`maxB==${maxB}`);

    console.log(`minR==${minR}`);
    console.log(`minG==${minG}`);
    console.log(`minB==${minB}`);
    //取rgb三原色中差值最大的那个
    let differR = maxR-minR;
    let differG = maxG-minG;
    let differB = maxB-minB;

    let max = Math.max.call(this,differB,differG,differR);
    switch (max){
        case differR:
            //按照r排序
            console.log("按照r排序");
            colorArray.sort(function (a,b) {
                return a.r-b.r;
            });
            break;
        case differG:
            //按照g排序
            console.log("按照g排序");
            colorArray.sort(function (a,b) {
                return a.g-b.g;
            });
            break;
        case differB:
            //按照b排序
            console.log("按照b排序");
            colorArray.sort(function (a,b) {
                return a.b-b.b;
            });
            break;
    }
    return colorArray;
}

function cutColorArray(colorArray) {
    //按照中位数切割
    let midanNum = Math.floor(colorArray.length/2);
    let vbox1 = colorArray.splice(0,midanNum);
    return [vbox1,colorArray];
}

function averageColor(colorArray) {
    //获取颜色最大值最小值
    let color=[];
    colorArray.forEach(function (v) {
       let t =  averageRGB(v);
        color.push(t);
    });
    return color;
}

function averageRGB(colors) {
    let r = 0;
    let g = 0;
    let b = 0;
    colors.forEach(function (v) {
        r+=v.r;
        g+=v.g;
        b+=v.b;
    });
    return{
        r:Math.ceil(r/colors.length),
        g:Math.ceil(g/colors.length),
        b:Math.ceil(b/colors.length)
    };
}

webGL的代码如下

var renderCanvas = document.getElementById("renderCanvas");
//let imgUrl="https://img.alicdn.com/tfs/TB1MHw9rlLoK1RjSZFuXXXn0XXa-600-450.jpg";
let imgUrl="https://gw.alicdn.com/tfs/TB1NzwSrhTpK1RjSZFKXXa2wXXa-600-450.jpg";
// load the 3D engine
var engine = new BABYLON.Engine(renderCanvas, true);
var createScene = function () {
    let scene = new BABYLON.Scene(engine);
    let camera = initCamera(scene,renderCanvas);
    showAxis(256, scene);
    let negative = addNegative(scene);
    return scene;
};

const scene = createScene();
engine.runRenderLoop(function () {
    scene.render();
});


function initCamera(scene,canvas) {
    var camera = new BABYLON.ArcRotateCamera("Camera", -Math.PI / 2, Math.PI / 2, 5, BABYLON.Vector3.Zero(), scene);
    camera.attachControl(canvas, true);
    //camera.inputs.attached.mousewheel.detachControl(canvas);
    camera.setPosition(new BABYLON.Vector3(400, 400, -200));
    //lights
    var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(256,256, 256), scene);
    light.intensity = 0.8;
    return camera;
}


function createColorBox(scene, rgb) {
    let r = rgb.r / 256;
    let g = rgb.g / 256;
    let b = rgb.b / 256;
    let rgbBox = BABYLON.MeshBuilder.CreateBox("myBox", {height: 1, width: 1, depth:1}, scene);
    let materialBox = new BABYLON.StandardMaterial("mat", scene);
    materialBox.diffuseColor = new BABYLON.Color3(r, g, b);
    rgbBox.material = materialBox;
    rgbBox.position = new BABYLON.Vector3(rgb.r , rgb.g , rgb.b );
    //rgbBox.position = new BABYLON.Vector3(10,2,1);
}

function addNegative(scene) {
    var ground = BABYLON.Mesh.CreateGround("ground", 120, 90, 0, scene);
    var mat = new BABYLON.StandardMaterial("dog", scene);
    mat.diffuseTexture = new BABYLON.Texture(imgUrl, scene);
    mat.diffuseTexture.hasAlpha = true;
    mat.backFaceCulling = false;
    ground.material = mat;
    ground.position = new BABYLON.Vector3(60, 0, 45);
    return ground;
}

function showAxis(size, scene) {
    var makeTextPlane = function (text, color, size) {
        var dynamicTexture = new BABYLON.DynamicTexture("DynamicTexture", 50, scene, true);
        dynamicTexture.hasAlpha = true;
        dynamicTexture.drawText(text, 5, 40, "bold 36px Arial", color, "transparent", true);
        var plane = new BABYLON.Mesh.CreatePlane("TextPlane", size, scene, true);
        plane.material = new BABYLON.StandardMaterial("TextPlaneMaterial", scene);
        plane.material.backFaceCulling = false;
        plane.material.specularColor = new BABYLON.Color3(0, 0, 0);
        plane.material.diffuseTexture = dynamicTexture;
        return plane;
    };

    var axisX = BABYLON.Mesh.CreateLines("axisX", [
        new BABYLON.Vector3.Zero(), new BABYLON.Vector3(size, 0, 0), new BABYLON.Vector3(size * 0.95, 0.05 * size, 0),
        new BABYLON.Vector3(size, 0, 0), new BABYLON.Vector3(size * 0.95, -0.05 * size, 0)
    ], scene);
    axisX.color = new BABYLON.Color3(1, 0, 0);
    var xChar = makeTextPlane("R", "red", size / 10);
    xChar.position = new BABYLON.Vector3(0.9 * size, -0.05 * size, 0);
    var axisY = BABYLON.Mesh.CreateLines("axisY", [
        new BABYLON.Vector3.Zero(), new BABYLON.Vector3(0, size, 0), new BABYLON.Vector3(-0.05 * size, size * 0.95, 0),
        new BABYLON.Vector3(0, size, 0), new BABYLON.Vector3(0.05 * size, size * 0.95, 0)
    ], scene);
    axisY.color = new BABYLON.Color3(0, 1, 0);
    var yChar = makeTextPlane("G", "green", size / 10);
    yChar.position = new BABYLON.Vector3(0, 0.9 * size, -0.05 * size);
    var axisZ = BABYLON.Mesh.CreateLines("axisZ", [
        new BABYLON.Vector3.Zero(), new BABYLON.Vector3(0, 0, size), new BABYLON.Vector3(0, -0.05 * size, size * 0.95),
        new BABYLON.Vector3(0, 0, size), new BABYLON.Vector3(0, 0.05 * size, size * 0.95)
    ], scene);
    axisZ.color = new BABYLON.Color3(0, 0, 1);
    var zChar = makeTextPlane("B", "blue", size / 10);
    zChar.position = new BABYLON.Vector3(0, 0.05 * size, 0.9 * size);
}


window.addEventListener("message",function (event) {
    if(event.data.name==="vBoxColor"){
        let colorArray = event.data.rgb;
        console.log(colorArray);
        stepColorArray(colorArray);
    }
});

function samplingColor(colorArray) {
    console.log(colorArray.length);
    let s = [];
    for(let m = 0; m <colorArray.length;m=m+20){
        s.push(colorArray[m]);
    }
    return s;
}
function drawColorBox(list) {
    list.forEach(function (v) {
        createColorBox(scene,v);
    });
}

function stepColorArray(colorArray) {
    let step  = 200;
    if(colorArray.length>step){
        let stepColor = colorArray.splice(step);
        drawColorBox(colorArray);
        stepSplice(stepColor);
    }
}
function stepSplice(color) {
    window.requestAnimationFrame(function () {
        stepColorArray(color);
    })
}


相关文档

https://avaminzhang.wordpress.com/2013/08/23/%E9%A2%9C%E8%89%B2%E9%87%8F%E5%8C%96/
http://www.voidcn.com/article/p-hybxbtsc-e.html
https://xinyo.org/archives/66352
http://blog.rainy.im/2015/11/24/extract-color-themes-from-images/


本文标题:颜色量化之中位切分算法(Color quantization)
本文链接:https://56way.com/p/121.html
作者授权:除特别说明外,本文由 无路 原创编译并授权 小无路 刊载发布。
版权声明:本文不使用任何协议授权,您可以任何形式自由转载或使用。

发表评论

必填

选填

选填

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。