0

I want to identify all the yellow pixels that lie between two colours, for example [255, 255, 0] is bright yellow, and [200, 200, 50] is a mid yellow.

c = color_array = np.array([ 
  [255, 255, 0],  # bright yellow
  [200, 200, 50]])  # mid yellow

So the rgb ranges could be represented as :

(min, max) tuple... 
r (200, 255)
g (200, 255)
b (0, 50)

I have a 2D (height of image x width of image) [r, g, b] array:

image = np.random.randint(0, 255, (5, 4, 3))
array([[[169, 152,  65],
    [ 46, 123,  39],
    [116, 190, 227],
    [ 95, 138, 243]],
   [[120,  75, 156],
    [ 94, 168, 139],
    [131,   0,   0],
    [233,  43,  28]],
   [[195,  65, 198],
    [ 33, 161, 231],
    [125,  31, 101],
    [ 56, 123, 151]],
   [[118, 124, 220],
    [241, 221, 137],
    [ 71,  65,  23],
    [ 91,  75, 178]],
   [[153, 238,   7],
    [  2, 154,  45],
    [144,  33,  94],
    [ 52, 188,   4]]])

I'd like to produce a 2D array with True if the r,g,b values are in the range between the 2 color values in the color arrays.

[[T, F, F, T], 
 [T, F, F, T],
  ...       ]]

I've been struggling to get the indexing right.

Blue Shrapnel
  • 95
  • 1
  • 13
  • Can you make your array say `5x4` instead of `5x3`? That would make it much more clear that the 3rd dimension is the color. – Mad Physicist Dec 13 '17 at 21:47
  • 1
    Why is `color_array` 4-dimensional? – user2357112 Dec 13 '17 at 21:48
  • Can you specify very clearly what it means to be within range? Does it mean `255 >= r >= 200 and 255 >= g >= 200 and 0 <= g <= 50`? – Mad Physicist Dec 13 '17 at 21:49
  • Just changed the dimension, agree that's clearer. So this would result in a 5x4 boolean array, where the colour matches as follows:if the r, g, b colour was 200 < r < 255, 200 < g < 255, 0 < b < 50 – Blue Shrapnel Dec 13 '17 at 21:55
  • @BlueShrapnel. Just to be clear, you want to match *all* the conditions, not *any* or "at least two" or something weird like that, right? – Mad Physicist Dec 13 '17 at 21:58
  • For yellow, it would be all, but to find red, it would just the be the r value. And for green, the g value. – Blue Shrapnel Dec 13 '17 at 22:01

2 Answers2

2

I can imagine several ways to tackle this:

The one way would be to compare all elements individually:

c = color_array

within_box = np.all(
    np.logical_and(
        np.min(c, axis=0) < image,
        image < np.max(c, axis=0)
    ),
    axis=-1
)

This will be True for all pixels where

200 < R < 255 and 200 < G < 255 and 0 < B < 50

This is equivalent to looking for all pixels inside a small subset (a box), defined by color_array, in the RGB color space (a bigger box).

An alternative solution would be to take the line between the two points in color_array, and calculate each pixel's individual euclidean distance to that line:

distance = np.linalg.norm(np.cross(c[1,:] - c[0,:], c[0,:] - image), axis=-1)/np.linalg.norm(c[1,:] - c[0,:])

Afterwards you can find all pixels that are within a certain distance to that line, i.e.

within_distance = distance < 25

A third solution is to calculate the euclidean distance of each pixel to the mean value of your two colors:

distance_to_mean = np.linalg.norm(image - np.mean(c, axis=0), axis=-1)

finding all pixels within a limit can then interpreted as finding all pixels in a sphere around the average color of your two limit colors. E.g. if you chose the distance to be half the distance between the two points

within_sphere = distance_to_mean < (np.linalg.norm(c) / 2)

you get all pixels that fall in a sphere for wich both limiting colors exactly touch the opposite ends of the surface.

And of course if you want all pixels that are perceptually similar to your two limit colors, you should convert your data to a perceptual color space, like Lab

import skimage
image_lab = skimage.color.rgb2lab(image / 255)
color_array_lab = skimage.color.rgb2lab(color_array[np.newaxis, ...] / 255)

and do the computations in that space instead.

Nils Werner
  • 34,832
  • 7
  • 76
  • 98
0

Here is a solution that is not particularly elegant, but should work:

def color_mask(array, r_lim, g_lim, b_lim):
    """
    array : m x n x 3 array of colors
    *_lim are 2-element tuples, where the first element is expected to be <= the second.
    """
    r_mask = ((array[..., 0] >= r_lim[0]) & (array[..., 0] <= r_lim[1]))
    g_mask = ((array[..., 1] >= g_lim[0]) & (array[..., 1] <= g_lim[1]))
    b_mask = ((array[..., 2] >= b_lim[0]) & (array[..., 2] <= b_lim[1]))
    return r_mask & g_mask & b_mask

You could easily extend this to handle arbitrary numbers of colors in the last dimension using numpy's broadcasting rules:

def color_mask(array, *lims):
    lims = np.asarray(lims)
    lower = (array >= lims[:, 0])
    upper = (array <= lims[:, 1])
    return np.logical_and.reduce(upper & lower, axis=2)
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264