How to replace all RGB values in an image if that RGB value belongs to a list

Viewed 490

I need to change all RGB values in an image if that RGB value is on my list without a loop.

I know that in order to change a RGB value I just need to do this:

img[np.all(img == (99, 121, 109), axis=-1)] = (255,255,255)

But the value (99, 121, 109) in my case belongs to a list, for example

   rgbL = [[99, 121, 109], 
           [102, 118, 107], 
           [102, 126, 114], 
                   .......,
           [105, 114, 101]]

In order to change ALL RGB values in an image that belongs to my rgbL list, I need to loop to ALL values of my rgbL. But my list is too long. Is there a way that I can do this without a loop?

3 Answers

Unfortunately there is no fast way to do this. Whether you use a loop or an optimized numpy solution, under the hood you have to check each pixel against each element of rgbL. Given all that, you can use isin combined with a structured array view of the data to perform the correct grouping.

The trick is to make a custom type that represents pixels. That way, you can compare entire elements rather than color channels, which is what isin does by default.

pixel = np.dtype([('r', img.dtype), ('g', img.dtype), ('b', img.dtype)])

You can not define the pixel datatype as a sub-array type like this pixel = np.dtype((img.dtype, img.shape[-1])). That will cause an error when you try to view the data.

You can now view img as a 2D array of pixel elements, rather than a 3D array of uint8 or float, as the case may be:

data = img.view(pixel).squeeze(axis=-1)

You can do the same with your list:

rgbA = np.array(rgbL).view(pixel).squeeze(axis=-1)

You can also do

rgbA = np.array([tuple(p) for p in rgbL], dtype=pixel)

You can't use np.array(rgbL, dtype=pixel) because the elements will not be initialized correctly in this case.

Now isin will give you the mask you want:

mask = np.isin(data, rgbA)

You can apply the mask directly:

data[mask] = 255

If you want to set a value that differs between channels, remember to use a tuple to specify pixel entries:

>>> data[mask] = (255, 255, 255)  # OK
>>> data[mask] = [255, 255, 255]
ValueError: NumPy boolean array indexing assignment cannot assign 3 input values to the 1 output values where the mask is true

Since data is a view into img, you are done.

Note: This will only work if your image is contiguous in the third dimension. Otherwise, you will not be able to use the same datatype for both arrays. If that is not the case, you will have to copy the image data so that it is contiguous at least in the third dimension. For most sane arrays that aren't weird views across a hyperspectral image or something, you will be OK.

TL;DR

pixel = np.dtype([('r', img.dtype), ('g', img.dtype), ('b', img.dtype)])

def replace(img, pixels, value):
    data = img.view(pixel).squeeze(-1)
    pixels = np.array(pixels, dtype=img.dtype).view(pixel).squeeze(-1)
    data[isin(data, pixels)] = tuple(value)

if this works for a single colour:

img[np.all(img == (99, 121, 109), axis=-1)] = (255,255,255)

this should work for a list:

img[np.all(img in rgbL, axis=-1)] = (255,255,255)

use a cv2 LUT (look up table) - you can define your own and specify your replacements directly that way.

Related