Programmatically determine best foreground color to be placed onto an image

前端 未结 3 1130
[愿得一人]
[愿得一人] 2021-02-02 00:11

I\'m working on a node module that will return the color that will look best onto a background image which of course will have multiple colors.

Here\'s what I have so fa

相关标签:
3条回答
  • 2021-02-02 00:46

    It depends on where the text is that is overlayed on the background image. If the background has some large feature on part of it, the text will likely be placed away from that, so must contrast with that part of the image, but you may also want to pick up a certain color or complement the other colors in the image. I think practically speaking you will need to create a widget for people to easily slide/adjust the foreground color interactively. Or you will need to create a deep learning system in order to do this really effectively.

    0 讨论(0)
  • 2021-02-02 00:53

    Sounds like an interesting problem to have!

    Each algorithm you're using to generate colors likely has a bias toward certain colors in their respective random color algorithms.

    What you're likely seeing is the end result of that bias for each. Both are selecting darker and lighter colors independently.

    It may make more sense to keep a hash of common colors and use that hash as opposed to using randomly generated colors.

    Either way your 'fitness' check, the algorithm that checks to see which color has the best average contrast is picking lighter and darker colors for both color sets. This makes sense, lighter images should have darker backgrounds and darker images should have lighter backgrounds.

    Although you don't explicitly say, I'd bet my bottom dollar you're getting dark background for lighter average images and brighter backgrounds on darker images.

    Alternatively rather than using a hash of colors, you could generate multiple random color palettes and combine the result sets to average them out.

    Or rather than taking the 6 most commonly occurring colors, why not take the overall color gradient and try against that?

    I've put together an example where I get the most commonly occurring color and invert it to get the complementary color. This in theory at least should provide a good contrast ratio for the image as a whole.

    Using the most commonly occurring color in the image seems to work quite well. as outlined in my example below. This is a similar technique that Blindman67 uses without the massive bloating of including libraries and performing un-necessary steps, I borrowed the same images that Blindman67 uses for a fair comparison of the result set.

    See Get average color of image via Javascript for getting average color (getAverageRGB() function written by 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;
    }

    0 讨论(0)
  • 2021-02-02 00:54

    Finding dominant hue.

    The provided snippet show an example of how to find a dominant colour. It works by breaking the image into its Hue, saturation and luminance components.

    The image reduction

    To speed up the process the image is reduced to a smaller image (in this case 128 by 128 pixels). Part of the reduction process also trims some of the outside pixels from the image.

    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 mean luminance

    Once reduced I scan the pixels converting them to hsl values and get the mean luminance.

    Note that luminance is a logarithmic scale so the mean is the square root of the sum of the squares divided by the count.

    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);
    

    Hue histograms for luminance and saturation ranges.

    The code can find the dominant colour in a range of saturation and luminance extents. In the example I only use one extent, but you can use as many as you wish. Only pixels that are inside the lum (luminance) and sat (saturation) ranges are used. I record a histogram of the hue for pixels that pass.

    Example of hue ranges (one of)

    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
        }]
    

    In the example I use the mean Luminance to automatically set the lum range.

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

    Once the range is set I get the hue histogram for each range (one in this case)

    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;
                }
            }
        }
    }
    

    Weighted mean hue from hue histogram.

    Then using the histogram I find the weighted mean hue for the range

    // 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));
    }
    

    And that is about it. The rest is just display and stuff. The above code requires the imageTools interface (provided) that has tools for manipulating images.

    The ugly complement

    What you do with the colour/s found is up to you. If you want the complementary colour just convert the rgb to hsl imageTools.rgb2hsl and rotate the hue 180 deg, then convert back to rgb.

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

    Personally only some colours work well with their complement. Adding to a pallet is risky, doing it via code is just crazy. Stick with colours in the image. Reduce the lum and sat range if you wish to find accented colours. Each range will have a count of the number of pixels found, use that to find the extent of pixels using the colors in the associated histogram.

    Demo "Border the birds"

    The demo finds the dominant hue around the mean luminance and uses that hue and mean saturation and luminance to create a border.

    The demo using images from wikipedia's image of the day collection as they allow cross site access.

    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 **/

    0 讨论(0)
提交回复
热议问题