Blue Shrapnel
Blue Shrapnel

Reputation: 95

create boolean mask of numpy rgb array if matches color

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.

Upvotes: 0

Views: 6292

Answers (2)

Nils Werner
Nils Werner

Reputation: 36849

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.

Upvotes: 2

Mad Physicist
Mad Physicist

Reputation: 114548

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)

Upvotes: 0

Related Questions