# 如何对前端图片主题色进行提取？这篇文章详细告诉你

## 一、主题色算法

### 八叉树算法

``````R: 1111 1111
G: 0111 1000
B: 0000 0000``````

## 二、中位切分法实践

``````(function () {

/**
* 颜色盒子类
*
* @param {Array} colorRange    [[rMin, rMax],[gMin, gMax], [bMin, bMax]] 颜色范围
* @param {any} total   像素总数, imageData / 4
* @param {any} data    像素数据集合
*/
function ColorBox(colorRange, total, data) {
this.colorRange = colorRange;
this.total = total;
this.data = data;
this.volume = (colorRange[0][1] - colorRange[0][0]) * (colorRange[1][1] - colorRange[1][0]) * (colorRange[2][1] - colorRange[2][0]);
this.rank = this.total * (this.volume);
}

ColorBox.prototype.getColor = function () {
var total = this.total;
var data = this.data;

var redCount = 0,
greenCount = 0,
blueCount = 0;

for (var i = 0; i < total; i++) {
redCount += data[i * 4];
greenCount += data[i * 4 + 1];
blueCount += data[i * 4 + 2];
}

return [parseInt(redCount / total), parseInt(greenCount / total), parseInt(blueCount / total)];
}

// 获取切割边
function getCutSide(colorRange) {   // r:0,g:1,b:2
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(colorRange[i][1] - colorRange[i][0]);
}
return arr.indexOf(Math.max(arr[0], arr[1], arr[2]));
}

// 切割颜色范围
function cutRange(colorRange, colorSide, cutValue) {
var arr1 = [];
var arr2 = [];
colorRange.forEach(function (item) {
arr1.push(item.slice());
arr2.push(item.slice());
})
arr1[colorSide][1] = cutValue;
arr2[colorSide][0] = cutValue;
return [arr1, arr2];
}

// 找到出现次数为中位数的颜色
function getMedianColor(colorCountMap, total) {
var arr = [];
for (var key in colorCountMap) {
arr.push({
color: parseInt(key),
count: colorCountMap[key]
})
}

var sortArr = __quickSort(arr);
var medianCount = 0;
var medianColor = 0;
var medianIndex = Math.floor(sortArr.length / 2)

for (var i = 0; i <= medianIndex; i++) {
medianCount += sortArr[i].count;
}

return {
color: parseInt(sortArr[medianIndex].color),
count: medianCount
}

// 另一种切割颜色判断方法，根据数量和差值的乘积进行判断，自己试验后发现效果不如中位数方法，但是少了排序，性能应该有所提高
// var count = 0;
// var colorMin = arr[0].color;
// var colorMax = arr[arr.length - 1].color
// for (var i = 0; i < arr.length; i++) {
//     count += arr[i].count;

//     var item = arr[i];

//     if (count * (item.color - colorMin) > (total - count) * (colorMax - item.color)) {
//         return {
//             color: item.color,
//             count: count
//         }
//     }
// }

return {
color: colorMax,
count: count
}

function __quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
var pivotIndex = Math.floor(arr.length / 2),
pivot = arr.splice(pivotIndex, 1)[0];

var left = [],
right = [];
for (var i = 0; i < arr.length; i++) {
if (arr[i].count <= pivot.count) {
left.push(arr[i]);
}
else {
right.push(arr[i]);
}
}
return __quickSort(left).concat([pivot], __quickSort(right));
}
}

// 切割颜色盒子
function cutBox(colorBox) {
var colorRange = colorBox.colorRange,
cutSide = getCutSide(colorRange),
colorCountMap = {},
total = colorBox.total,
data = colorBox.data;

// 统计出各个值的数量
for (var i = 0; i < total; i++) {
var color = data[i * 4 + cutSide];

if (colorCountMap[color]) {
colorCountMap[color] += 1;
}
else {
colorCountMap[color] = 1;
}
}
var medianColor = getMedianColor(colorCountMap, total);
var cutValue = medianColor.color;
var cutCount = medianColor.count;
var newRange = cutRange(colorRange, cutSide, cutValue);
var box1 = new ColorBox(newRange[0], cutCount, data.slice(0, cutCount * 4)),
box2 = new ColorBox(newRange[1], total - cutCount, data.slice(cutCount * 4))
return [box1, box2];
}

// 队列切割
function queueCut(queue, num) {

while (queue.length < num) {

queue.sort(function (a, b) {
return a.rank - b.rank
});
var colorBox = queue.pop();
var result = cutBox(colorBox);
queue = queue.concat(result);
}

return queue.slice(0, 8)
}

function themeColor(img, callback) {

var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
width = 0,
height = 0,
imageData = null,
length = 0,
blockSize = 1,
cubeArr = [];

width = canvas.width = img.width;
height = canvas.height = img.height;

ctx.drawImage(img, 0, 0, width, height);

imageData = ctx.getImageData(0, 0, width, height).data;

var total = imageData.length / 4;

var rMin = 255,
rMax = 0,
gMin = 255,
gMax = 0,
bMin = 255,
bMax = 0;

// 获取范围
for (var i = 0; i < total; i++) {
var red = imageData[i * 4],
green = imageData[i * 4 + 1],
blue = imageData[i * 4 + 2];

if (red < rMin) {
rMin = red;
}

if (red > rMax) {
rMax = red;
}

if (green < gMin) {
gMin = green;
}

if (green > gMax) {
gMax = green;
}

if (blue < bMin) {
bMin = blue;
}

if (blue > bMax) {
bMax = blue;
}
}

var colorRange = [[rMin, rMax], [gMin, gMax], [bMin, bMax]];
var colorBox = new ColorBox(colorRange, total, imageData);

var colorBoxArr = queueCut([colorBox], 8);

var colorArr = [];
for (var j = 0; j < colorBoxArr.length; j++) {
colorBoxArr[j].total && colorArr.push(colorBoxArr[j].getColor())
}

callback(colorArr);
}

window.themeColor = themeColor

})()``````

## 三、八叉树算法实践

``````(function () {

var OctreeNode = function () {
this.isLeaf = false;
this.pixelCount = 0;
this.red = 0;
this.green = 0;
this.blue = 0;
this.children = [null, null, null, null, null, null, null, null];
this.next = null;
}

var root = null,
leafNum = 0,
colorMap = null,
reducible = null;

function createNode(index, level) {
var node = new OctreeNode();
if (level === 7) {
node.isLeaf = true;
leafNum++;
} else {
// 将其丢到第 level 层的 reducible 链表中
node.next = reducible[level];
reducible[level] = node;
}

return node;
}

if (node.isLeaf) {
node.pixelCount += 1;
node.red += color.r;
node.green += color.g;
node.bllue += color.b;
}
else {
var str = "";
var r = color.r.toString(2);
var g = color.g.toString(2);
var b = color.b.toString(2);
while (r.length < 8) r = '0' + r;
while (g.length < 8) g = '0' + g;
while (b.length < 8) b = '0' + b;

str += r[level];
str += g[level];
str += b[level];

var index = parseInt(str, 2);

if (null === node.children[index]) {
node.children[index] = createNode(index, level + 1);
}

if (undefined === node.children[index]) {
console.log(index, level, color.r.toString(2));
}

}
}

function reduceTree() {

// 找到最深层次的并且有可合并节点的链表
var level = 6;
while (null == reducible[level]) {
level -= 1;
}

// 取出链表头并将其从链表中移除
var node = reducible[level];
reducible[level] = node.next;

// 合并子节点
var r = 0;
var g = 0;
var b = 0;
var count = 0;
for (var i = 0; i < 8; i++) {
if (null === node.children[i]) continue;
r += node.children[i].red;
g += node.children[i].green;
b += node.children[i].blue;
count += node.children[i].pixelCount;
leafNum--;
}

// 赋值
node.isLeaf = true;
node.red = r;
node.green = g;
node.blue = b;
node.pixelCount = count;
leafNum++;
}

function buidOctree(imageData, maxColors) {
var total = imageData.length / 4;
for (var i = 0; i < total; i++) {
// 添加颜色
r: imageData[i * 4],
g: imageData[i * 4 + 1],
b: imageData[i * 4 + 2]
}, 0);

// 合并叶子节点
while (leafNum > maxColors) reduceTree();
}
}

if (node.isLeaf) {
var r = parseInt(node.red / node.pixelCount);
var g = parseInt(node.green / node.pixelCount);
var b = parseInt(node.blue / node.pixelCount);

var color = r + ',' + g + ',' + b;
if (object[color]) object[color] += node.pixelCount;
else object[color] = node.pixelCount;
return;
}

for (var i = 0; i < 8; i++) {
if (null !== node.children[i]) {
}
}
}

window.themeColor = function (img, callback) {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
width = 0,
height = 0,
imageData = null,
length = 0,
blockSize = 1;

width = canvas.width = img.width;
height = canvas.height = img.height;

ctx.drawImage(img, 0, 0, width, height);

imageData = ctx.getImageData(0, 0, width, height).data;

root = new OctreeNode();
colorMap = {};
reducible = {};
leafNum = 0;

buidOctree(imageData, 8)

var arr = [];
for (var key in colorMap) {
arr.push(key);
}
arr.sort(function (a, b) {
return colorMap[a] - colorMap[b];
})
arr.forEach(function (item, index) {
arr[index] = item.split(',')
})
callback(arr)
}
})()``````

## 五、小结

#### 参考文章

http://acm.nudt.edu.cn/~twcou...

https://xcoder.in/2014/09/17/...

http://blog.rainy.im/2015/11/...

https://xinyo.org/archives/66115

https://xinyo.org/archives/66352

https://github.com/lokesh/col...

http://y.qq.com/m/demo/2018/m...

21.3k 声望
17.2k 粉丝
0 条评论