Python - Find similar colors, best way
Asked Answered
A

10

23

I've made a function to find a color within a image, and return x, y. Now I need to add a new function, where I can find a color with a given tolerence. Should be easy?

Code to find color in image, and return x, y:

def FindColorIn(r,g,b, xmin, xmax, ymin, ymax):
    image = ImageGrab.grab()
    for x in range(xmin, xmax):
        for y in range(ymin,ymax):
            px = image.getpixel((x, y))
            if px[0] == r and px[1] == g and px[2] == b:
                return x, y

def FindColor(r,g,b):
    image = ImageGrab.grab()
    size = image.size
    pos = FindColorIn(r,g,b, 1, size[0], 1, size[1])
    return pos

Outcome:

Taken from the answers the normal methods of comparing two colors are in Euclidean distance, or Chebyshev distance.

I decided to mostly use (squared) euclidean distance, and multiple different color-spaces. LAB, deltaE (LCH), XYZ, HSL, and RGB. In my code, most color-spaces use squared euclidean distance to compute the difference.

For example with LAB, RGB and XYZ a simple squared euc. distance does the trick:

if ((X-X1)^2 + (Y-Y1)^2 + (Z-Z1)^2) <= (Tol^2) then
  ...

LCH, and HSL is a little more complicated as both have a cylindrical hue, but some piece of math solves that, then it's on to using squared eucl. here as well.

In most these cases I've added "separate parameters" for tolerance for each channel (using 1 global tolerance, and alternative "modifiers" HueTol := Tolerance * hueMod or LightTol := Tolerance * LightMod).


It seems like colorspaces built on top of XYZ (LAB, LCH) does perform best in many of my scenarios. Tho HSL yields very good results in some cases, and it's much cheaper to convert to from RGB, RGB is also great tho, and fills most of my needs.

Andesine answered 14/1, 2012 at 17:6 Comment(4)
You should return something if you don't find the color in the image. ie, an error code.An
How are you defining the tolerance? Separate ranges for r, g and b?Lyonnesse
I'm with John: what have you already tried? You might look at cosine similarity and search for Python implementations.Ralline
@jb.: returning None is Pythonic, and that's what his code already does.Kingery
M
26

Computing distances between RGB colours, in a way that's meaningful to the eye, isn't as easy a just taking the Euclidian distance between the two RGB vectors.

There is an interesting article about this here: http://www.compuphase.com/cmetric.htm

The example implementation in C is this:

typedef struct {
   unsigned char r, g, b;
} RGB;

double ColourDistance(RGB e1, RGB e2)
{
  long rmean = ( (long)e1.r + (long)e2.r ) / 2;
  long r = (long)e1.r - (long)e2.r;
  long g = (long)e1.g - (long)e2.g;
  long b = (long)e1.b - (long)e2.b;
  return sqrt((((512+rmean)*r*r)>>8) + 4*g*g + (((767-rmean)*b*b)>>8));
}

It shouldn't be too difficult to port to Python.

EDIT:

Alternatively, as suggested in this answer, you could use HLS and HSV. The colorsys module seems to have functions to make the conversion from RGB. Its documentation also links to these pages, which are worth reading to understand why RGB Euclidian distance doesn't really work:

EDIT 2:

According to this answer, this library should be useful: http://code.google.com/p/python-colormath/

Mindimindless answered 14/1, 2012 at 17:28 Comment(2)
See my answer for an optimized Python version.Katlin
colormath library worked for me, thanks :)Barbur
K
6

Here is an optimized Python version adapted from Bruno's asnwer:

def ColorDistance(rgb1,rgb2):
    '''d = {} distance between two colors(3)'''
    rm = 0.5*(rgb1[0]+rgb2[0])
    d = sum((2+rm,4,3-rm)*(rgb1-rgb2)**2)**0.5
    return d

usage:

>>> import numpy
>>> rgb1 = numpy.array([1,1,0])
>>> rgb2 = numpy.array([0,0,0])
>>> ColorDistance(rgb1,rgb2)
2.5495097567963922
Katlin answered 31/12, 2012 at 5:3 Comment(5)
From what I've seen, x**0.5 is much slower than from math import sqrt, then use sqrt(x). But if you import math and use math.sqrt(x) you will see little to no difference.Andesine
(35,255,24) vs (38,38,120) returns nanPolypetalous
This doesn't work at all and I just wasted 30 minutes debugging itMurdoch
Interesting that someone orbits through here every few years. The NaN values show up because the divisors from the original C snippet (In @Mindimindless 's answer and the paper by Thiadmer Riemersma) weren't quite factored out correctly, and it tries to take the root of a negative result. Adding an abs() before the root helps, but the results are skewed much higher the larger the differences in the two RGB tuples. But it's still probably better than straight euclidean.Marybelle
@MarcelWilson if you normalize your rgb values to 1 it works, i.e., rgb / 255.0Eponymy
K
3

Instead of this:

if px[0] == r and px[1] == g and px[2] == b:

Try this:

if max(map(lambda a,b: abs(a-b), px, (r,g,b))) < tolerance:

Where tolerance is the maximum difference you're willing to accept in any of the color channels.

What it does is to subtract each channel from your target values, take the absolute values, then the max of those.

Kingery answered 14/1, 2012 at 17:26 Comment(4)
@SLACKY, you'd need to import operator first. (This being said, this is still a formula for Euclidian distance: this will not give you the results you expect visually.)Mindimindless
@Bruno: my metric is even worse than the Euclidean distance! I didn't put any emphasis on that part (but upvoted your answer for doing so). If the tolerance is small it may not matter, but if the tolerance is large it probably will matter.Kingery
@JohnZwinch: ah yes, sorry, I misread the formula (no squares...). It looks like a Chebyshev distance.Mindimindless
Oops--Python abs only does one item at a time. I've updated my answer to use a lambda to do the subtraction and abs on each element.Kingery
P
2

Assuming that rtol, gtol, and btol are the tolerances for r,g, and b respectively, why not do:

if abs(px[0]- r) <= rtol and \
   abs(px[1]- g) <= gtol and \
   abs(px[2]- b) <= btol:
    return x, y
Pauperize answered 14/1, 2012 at 17:25 Comment(0)
S
1

from pyautogui source code

def pixelMatchesColor(x, y, expectedRGBColor, tolerance=0):
r, g, b = screenshot().getpixel((x, y))
exR, exG, exB = expectedRGBColor

return (abs(r - exR) <= tolerance) and (abs(g - exG) <= tolerance) and (abs(b - exB) <= tolerance)

you just need a little fix and you're ready to go.

Scupper answered 4/3, 2017 at 19:31 Comment(0)
O
1

Here's a vectorised Python (numpy) version of Bruno and Developer's answers (i.e. an implementation of the approximation derived here) that accepts a pair of numpy arrays of shape (x, 3) where individual rows are in [R, G, B] order and individual colour values ∈[0, 1].

You can reduce it two a two-liner at the expense of readability. I'm not entirely sure whether it's the most optimised version possible, but it should be good enough.

def colour_dist(fst, snd):
    rm = 0.5 * (fst[:, 0] + snd[:, 0])
    drgb = (fst - snd) ** 2
    t = np.array([2 + rm, 4 + 0 * rm, 3 - rm]).T
    return np.sqrt(np.sum(t * drgb, 1))

It was evaluated against Developer's per-element version above, and produces the same results (save for floating precision errors in two cases out of one thousand).

Overwhelm answered 9/6, 2019 at 13:58 Comment(0)
S
1

A cleaner python implementation of the function stated here, the function takes 2 image paths, reads them using cv.imread and the outputs a matrix with each matrix cell having difference of colors. you can change it to just match 2 colors easily

        import numpy as np
        import cv2 as cv    
        
        def col_diff(img1, img2):
            img_bgr1 = cv.imread(img1) # since opencv reads as B, G, R
            img_bgr2 = cv.imread(img2)
            r_m = 0.5 * (img_bgr1[:, :, 2] + img_bgr2[:, :, 2])
            delta_rgb = np.square(img_bgr1- img_bgr2)
            cols_diffs = delta_rgb[:, :, 2] * (2 + r_m / 256) + delta_rgb[:, :, 1] * (4) + 
                            delta_rgb[:, :, 0] * (2 + (255 - r_m) / 256)
            cols_diffs = np.sqrt(cols_diffs)            
            # lets normalized the values to range [0 , 1] 
            cols_diffs_min = np.min(cols_diffs)
            cols_diffs_max = np.max(cols_diffs)
            cols_diffs_normalized = (cols_diffs - cols_diffs_min) / (cols_diffs_max - cols_diffs_min)
            
            return np.sqrt(cols_diffs_normalized)
Swanherd answered 1/7, 2020 at 11:25 Comment(0)
A
0

Simple:

def eq_with_tolerance(a, b, t):
    return a-t <= b <= a+t

def FindColorIn(r,g,b, xmin, xmax, ymin, ymax, tolerance=0):
    image = ImageGrab.grab()
    for x in range(xmin, xmax):
        for y in range(ymin,ymax):
            px = image.getpixel((x, y))
            if eq_with_tolerance(r, px[0], tolerance) and eq_with_tolerance(g, px[1], tolerance) and eq_with_tolerance(b, px[2], tolerance):
                return x, y
Analemma answered 14/1, 2012 at 17:17 Comment(0)
O
0

I compared the Euclidean approach with the perceived color approach posted by Bruno for 512 different colors. The results are normalized, so 1 means maximum difference and 0 means same color. Here you can see both values plotted against each other: Euclidean vs. perceived color distance (normalized)

As you can see, the Euklidean distance does not derive extemely much from the perceived color distance. Here's the code to get the values in Python:

def get_perceived_color_distance_unnormalized(color1, color2):
    r1, g1, b1 = color1
    r2, g2, b2 = color2

    r = r1 - r2
    g = g1 - g2
    b = b1 - b2
    r_mean = (r1 + r2) / 2

    return math.sqrt(
        (512 + r_mean) / 256 * r ** 2 # >> 8 is same as dividing by 256
      + 4 * g ** 2
      + (767 - r_mean) / 256 * b ** 2)

def get_euclidean_color_distance_unnormalized(color1, color2):
    r1, g1, b1 = color1
    r2, g2, b2 = color2

    return math.sqrt(
        (r1 - r2) ** 2
      + (g1 - g2) ** 2
      + (b1 - b2) ** 2)

Black = (0, 0, 0)
White = (255, 255, 255)

maximum_perceived_distance = get_perceived_color_distance_unnormalized(White, Black)
maximum_euclidean_distance = get_euclidean_color_distance_unnormalized(White, Black)

def get_perceived_color_distance(color1, color2):
    """ Calculates a normalized distance (from 0 to 1) between two colors meaningful to the eyes (https://www.compuphase.com/cmetric.htm). """
    return get_perceived_color_distance_unnormalized(color1, color2) / maximum_perceived_distance

def get_euclidean_color_distance(color1, color2):
    """ Calculates a normalized Euclidean distance (from 0 to 1) between two colors. """
    return get_euclidean_color_distance_unnormalized(color1, color2) / maximum_euclidean_distance
Open answered 6/10, 2023 at 17:57 Comment(0)
G
-1

Here is a simple function that does not require any libraries:

def color_distance(rgb1, rgb2):
    rm = 0.5 * (rgb1[0] + rgb2[0])
    rd = ((2 + rm) * (rgb1[0] - rgb2[0])) ** 2
    gd = (4 * (rgb1[1] - rgb2[1])) ** 2
    bd = ((3 - rm) * (rgb1[2] - rgb2[2])) ** 2
    return (rd + gd + bd) ** 0.5

assuming that rgb1 and rgb2 are RBG tuples

Grosbeak answered 26/5, 2019 at 22:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.