Programmatically determine best foreground color to be placed onto an image

前端 未结 3 1134
[愿得一人]
[愿得一人] 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: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 **/

提交回复
热议问题