摘要:由此,我嘗試著利用在前端進(jìn)行圖片主題色的提取。一主題色算法目前比較常用的主題色提取算法有最小差值法中位切分法八叉樹(shù)算法聚類(lèi)色彩建模法等。
本文由云+社區(qū)發(fā)表
圖片主題色在圖片所占比例較大的頁(yè)面中,能夠配合圖片起到很好視覺(jué)效果,給人一種和諧、一致的感覺(jué)。同時(shí)也可用在圖像分類(lèi),搜索識(shí)別等方面。通常主題色的提取都是在后端完成的,前端將需要處理的圖片以鏈接或id的形式提供給后端,后端通過(guò)運(yùn)行相應(yīng)的算法來(lái)提取出主題色后,再返回相應(yīng)的結(jié)果。
這樣可以滿(mǎn)足大多數(shù)展示類(lèi)的場(chǎng)景,但對(duì)于需要根據(jù)用戶(hù)“定制”、“生成”的圖片,這樣的方式就有了一個(gè)上傳圖片---->后端計(jì)算---->返回結(jié)果的時(shí)間,等待時(shí)間也許就比較長(zhǎng)了。由此,我嘗試著利用 canvas在前端進(jìn)行圖片主題色的提取。
一、主題色算法目前比較常用的主題色提取算法有:最小差值法、中位切分法、八叉樹(shù)算法、聚類(lèi)、色彩建模法等。其中聚類(lèi)和色彩建模法需要對(duì)提取函數(shù)和樣本、特征變量等進(jìn)行調(diào)參和回歸計(jì)算,用到 python的數(shù)值計(jì)算庫(kù) numpy和機(jī)器學(xué)習(xí)庫(kù) scikit-learn,用 python來(lái)實(shí)現(xiàn)相對(duì)比較簡(jiǎn)單,而目前這兩種都沒(méi)有成熟的js庫(kù),并且js本身也不擅長(zhǎng)回歸計(jì)算這種比較復(fù)雜的計(jì)算。我也就沒(méi)有深入的研究,而主要將目光放在了前面的幾個(gè)顏色量化算法上。
而最小差值法是在給定給定調(diào)色板的情況下找到與色差最小的顏色,使用的場(chǎng)景比較小,所以我主要看了中位切分法和八叉樹(shù)算法,并進(jìn)行了實(shí)踐。
中位切分法中位切分法通常是在圖像處理中降低圖像位元深度的算法,可用來(lái)將高位的圖轉(zhuǎn)換位低位的圖,如將24bit的圖轉(zhuǎn)換為8bit的圖。我們也可以用來(lái)提取圖片的主題色,其原理是是將圖像每個(gè)像素顏色看作是以R、G、B為坐標(biāo)軸的一個(gè)三維空間中的點(diǎn),由于三個(gè)顏色的取值范圍為0~255,所以圖像中的顏色都分布在這個(gè)顏色立方體內(nèi),如下圖所示。
之后將RGB中最長(zhǎng)的一邊從顏色統(tǒng)計(jì)的中位數(shù)一切為二,使得到的兩個(gè)長(zhǎng)方體所包含的像素?cái)?shù)量相同,如下圖所示
重復(fù)這個(gè)過(guò)程直到切出長(zhǎng)方體數(shù)量等于主題色數(shù)量為止,最后取每個(gè)長(zhǎng)方體的中點(diǎn)即可。
在實(shí)際使用中如果只是按照中點(diǎn)進(jìn)行切割,會(huì)出現(xiàn)有些長(zhǎng)方體的體積很大但是像素?cái)?shù)量很少的情況。解決的辦法是在切割前對(duì)長(zhǎng)方體進(jìn)行優(yōu)先級(jí)排序,排序的系數(shù)為體積 * 像素?cái)?shù)。這樣就可以基本解決此類(lèi)問(wèn)題了。
八叉樹(shù)算法八叉樹(shù)算法也是在顏色量化中比較常見(jiàn)的,主要思路是將R、G、B通道的數(shù)值做二進(jìn)制轉(zhuǎn)換后逐行放下,可得到八列數(shù)字。如 #FF7880轉(zhuǎn)換后為
R: 1111 1111 G: 0111 1000 B: 0000 0000
再將RGB通道逐列粘合,可以得到8個(gè)數(shù)字,即為該顏色在八叉樹(shù)中的位置,如圖。
在將所有顏色插入之后,再進(jìn)行合并運(yùn)算,直到得到所需要的顏色數(shù)量為止。
在實(shí)際操作中,由于需要對(duì)圖像像素進(jìn)行遍歷后插入八叉樹(shù)中,并且插入過(guò)程有較多的遞歸操作,所以比中位切分法要消耗更長(zhǎng)的時(shí)間。
二、中位切分法實(shí)踐根據(jù)之前的介紹和網(wǎng)上的相關(guān)資料,此處貼上我自己理解實(shí)現(xiàn)的中位切分法代碼,并且找了幾張圖片將結(jié)果與QQ音樂(lè)已有的魔法色相關(guān)算法進(jìn)行比較,圖一為中位切分法結(jié)果,圖二為后臺(tái)cgi返回結(jié)果
圖一
圖二
可以看到有一定的差異,但是差值相對(duì)都還比較小的,處理速度在pc上面還是比較快的,三張圖分別在70ms,100ms,130ms左右。這里貼上代碼,待后續(xù)批量處理進(jìn)行對(duì)比之后再分析。
(function () { /** * 顏色盒子類(lèi) * * @param {Array} colorRange [[rMin, rMax],[gMin, gMax], [bMin, bMax]] 顏色范圍 * @param {any} total 像素總數(shù), imageData / 4 * @param {any} data 像素?cái)?shù)據(jù)集合 */ 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]; } // 找到出現(xiàn)次數(shù)為中位數(shù)的顏色 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 } // 另一種切割顏色判斷方法,根據(jù)數(shù)量和差值的乘積進(jìn)行判斷,自己試驗(yàn)后發(fā)現(xiàn)效果不如中位數(shù)方法,但是少了排序,性能應(yīng)該有所提高 // 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; // 統(tǒng)計(jì)出各個(gè)值的數(shù)量 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]; } // 隊(duì)列切割 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 })()三、八叉樹(shù)算法實(shí)踐
也許是我算法實(shí)現(xiàn)的問(wèn)題,使用八叉樹(shù)算法得到的最終結(jié)果并不理想,所消耗的時(shí)間相對(duì)于中位切分法也長(zhǎng)了不少,平均時(shí)間分別為160ms,250ms,400ms還是主要看八叉樹(shù)算法吧...同樣貼上代碼
(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; } function addColor(node, color, level) { 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)); } addColor(node.children[index], color, level + 1); } } function reduceTree() { // 找到最深層次的并且有可合并節(jié)點(diǎn)的鏈表 var level = 6; while (null == reducible[level]) { level -= 1; } // 取出鏈表頭并將其從鏈表中移除 var node = reducible[level]; reducible[level] = node.next; // 合并子節(jié)點(diǎn) 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++) { // 添加顏色 addColor(root, { r: imageData[i * 4], g: imageData[i * 4 + 1], b: imageData[i * 4 + 2] }, 0); // 合并葉子節(jié)點(diǎn) while (leafNum > maxColors) reduceTree(); } } function colorsStats(node, object) { 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]) { colorsStats(node.children[i], object); } } } 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) colorsStats(root, colorMap) 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) } })()四、結(jié)果對(duì)比
在批量跑了10000張圖片之后,得到了下面的結(jié)果
平均耗時(shí)對(duì)比(js-cgi)
可以看到在不考慮圖片加載時(shí)間的情況下,用中位切分法提取的耗時(shí)相對(duì)較短,而圖片加載的耗時(shí)可以說(shuō)是難以逾越的障礙了(整整拖慢了450ms),不過(guò)目前的代碼還有不錯(cuò)的優(yōu)化空間,比如間隔采樣,繪制到canvas時(shí)減小圖片尺寸,優(yōu)化切割點(diǎn)查找等,就需要后續(xù)進(jìn)行更深一點(diǎn)的探索了。
顏色偏差
所以看來(lái)準(zhǔn)確性還是可以的,約76%的顏色與cgi提取結(jié)果相近,在大于100的中抽查后發(fā)現(xiàn)有部分圖片兩者提取到的主題色各有特點(diǎn),或者平分秋色,比如
五、小結(jié)總結(jié)來(lái)看,通過(guò)canvas的中位切分法與cgi提取的結(jié)果相似程度還是比較高的,也有許多圖片有很大差異,需要在后續(xù)的實(shí)踐中不斷優(yōu)化。同時(shí),圖片加載時(shí)間也是一個(gè)難以逾越的障礙,不過(guò)目前的代碼還有不錯(cuò)的優(yōu)化空間,比如間隔采樣,繪制到canvas時(shí)減小圖片尺寸,優(yōu)化切割點(diǎn)查找等,就需要后續(xù)進(jìn)行更深一點(diǎn)的探索了。
參考文章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...
此文已由作者授權(quán)騰訊云+社區(qū)在各渠道發(fā)布
獲取更多新鮮技術(shù)干貨,可以關(guān)注我們騰訊云技術(shù)社區(qū)-云加社區(qū)官方號(hào)及知乎機(jī)構(gòu)號(hào)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/53562.html
摘要:由此,我嘗試著利用在前端進(jìn)行圖片主題色的提取。一主題色算法目前比較常用的主題色提取算法有最小差值法中位切分法八叉樹(shù)算法聚類(lèi)色彩建模法等。 本文由云+社區(qū)發(fā)表 圖片主題色在圖片所占比例較大的頁(yè)面中,能夠配合圖片起到很好視覺(jué)效果,給人一種和諧、一致的感覺(jué)。同時(shí)也可用在圖像分類(lèi),搜索識(shí)別等方面。通常主題色的提取都是在后端完成的,前端將需要處理的圖片以鏈接或id的形式提供給后端,后端通過(guò)運(yùn)行相...
摘要:由此,我嘗試著利用在前端進(jìn)行圖片主題色的提取。一主題色算法目前比較常用的主題色提取算法有最小差值法中位切分法八叉樹(shù)算法聚類(lèi)色彩建模法等。 本文由云+社區(qū)發(fā)表 圖片主題色在圖片所占比例較大的頁(yè)面中,能夠配合圖片起到很好視覺(jué)效果,給人一種和諧、一致的感覺(jué)。同時(shí)也可用在圖像分類(lèi),搜索識(shí)別等方面。通常主題色的提取都是在后端完成的,前端將需要處理的圖片以鏈接或id的形式提供給后端,后端通過(guò)運(yùn)行相...
摘要:工作時(shí)遇到一個(gè)需求提取圖片主題色,通過(guò)某種映射關(guān)系,選取給出的對(duì)應(yīng)顏色。腦海中浮現(xiàn)如果只是純前端如何實(shí)現(xiàn)呢一思路與準(zhǔn)備利用獲取圖像像素信息,然后用某種算法將主題顏色提取出來(lái)。 工作時(shí)遇到一個(gè)需求:提取圖片主題色,通過(guò)某種映射關(guān)系,選取ui給出的對(duì)應(yīng)顏色。腦海中浮現(xiàn)如果只是純前端如何實(shí)現(xiàn)呢? 一、思路與準(zhǔn)備 利用canvas獲取圖像像素信息,然后用某種算法將主題顏色提取出來(lái)。 1.1 了...
閱讀 888·2021-11-15 11:38
閱讀 1611·2021-09-24 09:48
閱讀 848·2021-09-24 09:47
閱讀 2281·2021-08-26 14:15
閱讀 3507·2019-08-30 11:09
閱讀 2614·2019-08-29 16:55
閱讀 1592·2019-08-26 14:01
閱讀 3042·2019-08-23 16:47