2

I am trying to find all the hex colors in an image and if possible, circle or highlight the X, Y position of where the hex color(s) are. My current code is attempting to find all colors and almost crashes my browser and my attempt to find the X,Y coordinates of each image isn't going good either.

I have two functions doing different things, it's what I have tried to work with to give an example of what has been attempted... Any help would be great!

Any assistance would be amazing!

<canvas id="myCanvas" width="240" height="297" style="border:1px solid #d3d3d3;">
    Your browser does not support the HTML5 canvas tag.
</canvas>
<img id="origImage" width="220" height="277" src="loggraph.PNG">
<script>
function getPixel(imgData, index) {
    var i = index*4, d = imgData.data;
    return [d[i],d[i+1],d[i+2],d[i+3]] // [R,G,B,A]
  }

  function getPixelXY(imgData, x, y) {
    return getPixel(imgData, y*imgData.width+x);
  }

    function goCheck() {
            var cvs = document.createElement('canvas'),
            img = document.getElementsByTagName("img")[0]; 
            cvs.width = img.width; cvs.height = img.height;
            var ctx = cvs.getContext("2d");
            ctx.drawImage(img,0,0,cvs.width,cvs.height);
            var idt = ctx.getImageData(0,0,cvs.width,cvs.height);

            console.log(getPixel(idt, 852));  // returns array [red, green, blue, alpha]
            console.log(getPixelXY(idt,1,1)); // same pixel using x,y
    }

        function getColors(){
                var canvas = document.getElementById("myCanvas");
                var devices = canvas.getContext("2d");
                var imageData = devices.getImageData(0, 0, canvas.width, canvas.height);
                var data = imageData.data;


                // iterate over all pixels
                for(var i = 0, n = data.length; i < n; i += 4) {
                var r  = data[i];
                var g  = data[i + 1];
                var b  = data[i + 2];   
                var rgb = "("+r+","+g+","+b+")";

                var incoming = i*4, d = imageData.data;
                var bah = [d[incoming],d[incoming+1],d[incoming+2],d[incoming+3]];
                    $('#list').append("<li>"+rgb+"</li>");
                    colorList.push(rgb);
                    }
                $('#list').append("<li>"+[d[incoming],d[incoming+1],d[incoming+2],d[incoming+3]]+"</li>");     
                    }    

        }
Lance
  • 79
  • 1
  • 11

1 Answers1

4

Must check all pixels

To find a pixel that matches a color will require, in the worst case (pixel of that color not in image), that you step over every pixel in the image.

How not to do it

Converting every pixel to a DOM string is about the worst way to do it, as DOM string use a lot of memory and CPU overhead, especially if instantiated using jQuery (which has its own additional baggage)

Hex color to array

To find the pixel you need only check each pixels color data against the HEX value. You convert the hex value to an array of 3 Bytes.

The following function will convert from CSS Hex formats "#HHH" "#HHHH", "#HHHHHH" and "#HHHHHHHH" ignoring the alpha part if included, to an array of integers 0-255

const hex2RGB = h => {
    if(h.length === 4 || h.length === 5) {
        return [parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16), parseInt(h[3] + h[3], 16)];
    }
    return [parseInt(h[1] + h[2], 16), parseInt(h[3] + h[4], 16), parseInt(h[5] + h[6], 16)];
}

Finding the pixel

I do not know how you plan to use such a feature so the example below is a general purpose method that will help and can be modified as needed

It will always find a pixel if you let it even if there is no perfect match. It does this by finding the closest color to the color you are looking for.

The reason that of finds the closest match is that when you draw an image onto a 2D canvas the pixel values are modified slightly if the image has transparent pixels (pre-multiplied alpha)

The function finds the pixel by measuring the spacial distance between the pixel and the hex color (simple geometry Pythagoras). The closest color is the one that is the smallest distance.

It will return the object

{
   x, // the x coordinate of the match
   y, // the y coordinate of the match
   distance, // how closely the color matches the requested color.
             // 0 means a perfect match 
             // to 441 completely different eg black and white
             // value is floored to an integer value
}

If the image is tainted (cross origin, local device storage), or you pass something that can not be converted to pixels the function will return undefined

The function keeps a canvas that it uses to get pixel data as it assumes that it will be use many times. If the image is tainted it will catch the error (add a warning to the console), cleanup the tainted canvas and be ready for another image.

Usage

To use the function add it to your code base, it will setup automatically.

Get an image and a hex value and call the function with the image, CSS hex color, and optionally the threshold distance for the color match.

Eg find exact match for #FF0000

const result = findPixel(origImage, "#FF0000", 0); // find exact match for red
if (result) { // only if found
     console.log("Found color #FF0000 at pixel " + result.x + ", " + result.y);
} else {
     console.log("The color #FF0000 is not in the image");
}

or find color close to

const result = findPixel(origImage, "#FF0000", 20); // find a match for red
                                                    // within 20 units. 
                                                    // A unit is 1 of 256
if (result) { // only if found
     console.log("Found closest color within " + result.distance + "units of #FF0000 at pixel " + result.x + ", " + result.y);
}

or find closest

// find the closest, no threshold ensures a result
const result = findPixel(origImage, "#FF0000"); 
console.log("Found closest color within " + result.distance + "units of #FF0000 at pixel " + result.x + ", " + result.y);

Code

The function is as follows.

const findPixel = (() => {
    var can, ctx;
    function createCanvas(w, h) {
        if (can === undefined){
            can = document.createElement("canvas");
            ctx = can.getContext("2d");     
        }
        can.width = w;
        can.height = h;
    }
    function getPixels(img) {
        const w = img.naturalWidth || img.width, h =  img.naturalHeight || img.height;
        createCanvas(w, h);
        ctx.drawImage(img, 0, 0);
        try {
            const imgData = ctx.getImageData(0, 0, w, h);
            can.width = can.height = 1; // make canvas as small as possible so it wont 
                                        // hold memory. Leave in place to avoid instantiation overheads
            return imgData;
        } catch(e) { 
            console.warn("Image is un-trusted and pixel access is blocked");
            ctx = can = undefined; // canvas and context can no longer be used so dump them
        }
        return {width: 0, height: 0, data: []}; // return empty pixel data
    }
    const hex2RGB = h => { // Hex color to array of 3 values
        if(h.length === 4 || h.length === 5) {
            return [parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16), parseInt(h[3] + h[3], 16)];
        }
        return [parseInt(h[1] + h[2], 16), parseInt(h[3] + h[4], 16), parseInt(h[5] + h[6], 16)];
    }
    const idx2Coord = (idx, w) => ({x: idx % w, y: idx / w | 0});
    return function (img, hex, minDist = Infinity) {
        const [r, g, b] = hex2RGB(hex);         
        const {width, height, data} = getPixels(img);
        var idx = 0, found;
        while (idx < data.length) {
            const R = data[idx] - r;
            const G = data[idx + 1] - g;
            const B = data[idx + 2] - b;
            const d = R * R + G * G + B * B;
            if (d === 0) { // found exact match 
                return {...idx2Coord(idx / 4, width), distance: 0};
            }
            if (d < minDist) {
                minDist = d;
                found = idx;
            }
            idx += 4;
        }
        return found ? {...idx2Coord(found / 4, width), distance: minDist ** 0.5 | 0 } : undefined;
    }
})();

This function has been tested and works as described above.

Note Going by the code in the your question the alpha value of the image and CSS hex color is ignored.

Note that if you intend to find many colors from the same image this function is not the best suited for you needs. If this is the case let me know in the comment and I can make changes or instruct you how to optimism the code for such uses.

Note It is not well suited for single use only. However if this is the case change the line const findPixel = (() => { to var findPixel = (() => { and after you have used it remove the reference findpixel = undefined; and JS will clean up any resources it holds.

Note If you also want to get the actual color of the closest found color that is trivial to add as well. Ask in the comments.

Note It is reasonably quick (you will be hard pressed to get a quicker result) but be warned that for very large images 4K and above it may take a bit, and on very low end devices it may cause a out of memory error. If this is a problem then another solution is possible but is far slower.

Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Holy smokes, you're amazing. I am basically trying to find when lines intersect on a graph and seem the best way to do so is by color. So when a new color shows up beside the main two colors, that third color would be the "intersection" but I could have up to five lines that could intersect which means five new colors. If you visit: https://stackoverflow.com/questions/60731702/detect-if-lines-intersect-in-google-charts-or-plot-ly you'll see that the lines intersect, as I was not successful at detecting when lines "cross", color detection seemed to be the next best thing. – Lance Mar 26 '20 at 21:35
  • @Lance Finding the intercept of two lines is trivial, a8 lines of code and a zillion times quicker than locating pixels (can check 2 lines in the same time it takes to check 2 pixel's color). Line intercept function https://stackoverflow.com/a/58703966/3877726 – Blindman67 Mar 27 '20 at 02:03
  • Sorry to keep bothering you, but how would I even run my lines through function interceptLines(l1, l2)? Do I do console.log(interceptLines(L1:{Test.x,Test.y},l2:{Test2.x,Test2.y}));? I'm not understanding how to run my array of lines X,Y values. – Lance Mar 27 '20 at 08:25
  • @Lance Sorry I did not notice that the question you linked was directly related to this question (namely it was your question) I have added an answer https://stackoverflow.com/a/60886369/3877726 to that question that should get you moving to a solution. – Blindman67 Mar 27 '20 at 12:53
  • Amazing, thank you so much for all this new knowledge and methods. I was not aware at all. I am pretty new to JS and this has opened a whole new world to me. – Lance Mar 27 '20 at 20:54