javascript - nodejs - Determine programáticamente el mejor color de primer plano para colocar en una imagen
npm colores (3)
Estoy trabajando en un módulo de nodo que devolverá el color que se verá mejor en una imagen de fondo que, por supuesto, tendrá varios colores.
Esto es lo que tengo hasta ahora:
''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);
};
Así que la forma en que funciona es primero. Proporciono este guión con los 6 colores más dominantes en una imagen como esta:
[ { 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 } ]
y luego generará 10,000 colores aleatorios diferentes y luego elegirá el que tenga la mejor relación de contraste promedio con los 6 colores dados.
El problema es que dependiendo de qué script use para generar los colores aleatorios, básicamente obtendré los mismos resultados independientemente de la imagen proporcionada.
Con tinycolor2
siempre terminaré con un gris muy oscuro (casi negro) o gris muy claro (casi blanco). Y con randomcolor
terminaré con un color azul oscuro o un color melocotón claro.
Mi guión podría no ser la mejor manera de abordar esto, pero, ¿alguien tiene alguna idea?
Gracias
Encontrar matiz dominante.
El fragmento proporcionado muestra un ejemplo de cómo encontrar un color dominante. Funciona al dividir la imagen en sus componentes Hue, Saturación y Luminancia.
La reducción de imagen
Para acelerar el proceso, la imagen se reduce a una imagen más pequeña (en este caso, 128 por 128 píxeles). Parte del proceso de reducción también recorta algunos de los píxeles externos de la imagen.
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
Encuentra la luminancia media
Una vez reducido, escaneo los píxeles convirtiéndolos en valores hsl y obtengo la luminancia media.
Tenga en cuenta que la luminancia es una escala logarítmica, por lo que la media es la raíz cuadrada de la suma de los cuadrados divididos por el recuento.
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);
Histogramas de tono para rangos de luminancia y saturación.
El código puede encontrar el color dominante en un rango de extensiones de saturación y luminancia. En el ejemplo, solo uso una extensión, pero puede usar tantas como desee. Solo se usan los píxeles que están dentro de los rangos lum (luminancia) y sat (saturación). Grabo un histograma del tono para los píxeles que pasan.
Ejemplo de rangos de tono (uno de)
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
}]
En el ejemplo, uso la Luminancia media para establecer automáticamente el rango del lum.
hues[0].lum.low = l - 30;
hues[0].lum.high = l + 30;
Una vez que se establece el rango, obtengo el histograma de matiz para cada rango (uno en este caso)
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;
}
}
}
}
Tonalidad media ponderada del histograma de tono.
Luego, usando el histograma, encuentro el matiz medio ponderado para el rango
// 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));
}
Y eso es todo. El resto es solo pantalla y esas cosas. El código anterior requiere la interfaz imageTools (incluida) que tiene herramientas para manipular imágenes.
El feo complemento
Lo que hagas con el color / s encontrado depende de ti. Si desea el color complementario simplemente convierta el rgb a hsl imageTools.rgb2hsl
y gire el tono 180 grados, luego convierta nuevamente a rgb.
var hsl = imageTools.rgb2hsl(rgb.r, rgb.g, rgb.b);
hsl.h += 180;
var complementRgb = imageTools.rgb2hsl(hsl.h, hsl.s, hsl.l);
Personalmente, solo algunos colores funcionan bien con su complemento. Agregar a una paleta es arriesgado, hacerlo a través de un código es una locura. Quédate con los colores en la imagen. Reduzca el rango lum y sat si desea encontrar colores acentuados. Cada rango tendrá un recuento de la cantidad de píxeles encontrados, use eso para encontrar la extensión de los píxeles usando los colores en el histograma asociado.
Demostración "Limita las aves"
La demostración encuentra el matiz dominante alrededor de la luminancia media y usa ese matiz y significa saturación y luminancia para crear un borde.
La demo usando imágenes de la imagen de wikipedia de la colección del día, ya que permiten el acceso cruzado del sitio.
var images = [
// "https://upload.wikimedia.org/wikipedia/commons/f/fe/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 = r.g;
b = r.b;
a = r.a !== undefined ? r.a : a;
r = r.r;
}
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 **/
Depende de dónde está el texto superpuesto en la imagen de fondo. Si el fondo tiene alguna característica grande en una parte, el texto probablemente se alejará de eso, por lo que debe contrastar con esa parte de la imagen, pero también puede elegir un cierto color o complementar los otros colores en el imagen. Creo que, en términos prácticos, deberá crear un widget para que las personas puedan deslizar / ajustar fácilmente el color de primer plano de forma interactiva. O tendrá que crear un sistema de aprendizaje profundo para hacerlo de manera efectiva.
¡Suena como un problema interesante de tener!
Cada algoritmo que está utilizando para generar colores probablemente tenga un sesgo hacia ciertos colores en sus respectivos algoritmos de color aleatorio.
Lo que probablemente vea es el resultado final de ese sesgo para cada uno. Ambos están seleccionando colores más oscuros y claros de forma independiente.
Puede tener más sentido mantener un hash de colores comunes y usar ese hash en lugar de usar colores generados aleatoriamente.
De cualquier manera, su comprobación de "estado físico", el algoritmo que verifica qué color tiene el mejor contraste promedio, es elegir colores más claros y más oscuros para ambos conjuntos de colores. Esto tiene sentido, las imágenes más claras deberían tener fondos más oscuros y las imágenes más oscuras deberían tener fondos más claros.
Aunque no diga explícitamente que apostaría mi último dólar, obtendrá un fondo oscuro para obtener imágenes más claras y fondos más brillantes en imágenes más oscuras.
Alternativamente, en lugar de utilizar un hash de colores, puede generar múltiples paletas de colores aleatorias y combinar los conjuntos de resultados para promediarlos.
O en lugar de tomar los 6 colores más comunes, ¿por qué no tomar el gradiente general de color e intentarlo?
He creado un ejemplo en el que obtengo el color más común y lo invierto para obtener el color complementario. Esto en teoría al menos debería proporcionar una buena relación de contraste para la imagen como un todo.
Usar el color más común en la imagen parece funcionar bastante bien. como se describe en mi ejemplo a continuación. Esta es una técnica similar que usa Blindman67 sin la hinchazón masiva de incluir bibliotecas y realizar pasos innecesarios, tomé prestadas las mismas imágenes que Blindman67 utiliza para una comparación justa del conjunto de resultados.
Consulte Obtener el color promedio de la imagen mediante Javascript para obtener el color promedio (función getAverageRGB()
escrita por James ).
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;
}