Agree with bdarbell regarding the use of HSV to filter out the gray values. Here's what I would do concretely:
- Convert the image to HSV.
- Create a mask by multiplying the S (saturation) and V (value/brightness) channels. In doing so, you get a mask that is low in regions of low brightness (small V) or low saturation (small S) and high only in bright, saturated regions (large V and S).
- Set S and V to the maximum value in your image (effectively ignoring them), convert back to RGB, then add your new mask as an alpha channel (transparency).
Now you have multiple options, for example:
- Save the resulting image as RGBA: The RGB channels represent your (now fully bright, fully saturated) colors, the alpha channel dims them, as described for the mask above.
- Combine with a constant (e.g. black) background, then save as RGB.
- For visualization, combine with a checkerboard pattern.
Here is exemplary code that implements all three options:
from os.path import expanduser, join
import numpy as np
from PIL import Image
folder = expanduser("~/Desktop") # TODO: provide your actual path here
img = Image.open(join(folder, "rainbow.jpg"))
# Helper functions: array (range 0–1) to image (range 0–255) and back, checkerboard pattern
to_img = lambda i, *a, **kw: Image.fromarray((np.asarray(i) * 255).astype(np.uint8), *a, **kw)
to_arr = lambda i: np.asarray(i).astype(float) / 255
def checkerboard(size, square_size, color1=(191,191,191,255), color2=(255,255,255,255)):
c, r, s = np.arange(size[0]), np.arange(size[1]), square_size
p = (np.add.outer((r // s), (c // s)) % 2).astype(bool)[..., None] # pattern
return p * np.asarray(color1, dtype=np.uint8) + ~p * np.asarray(color2, dtype=np.uint8)
# Convert from RGB to HSV, then split the channels
hue, sat, val = np.split(to_arr(img.convert("HSV")), 3, axis=2)
# Create mask from saturation and value (brightness)
alpha = np.squeeze(sat * val, axis=-1) # H×W×1 → H×W
# Saturate the S and V channels, then create corresponding RGBA image
sat, val = np.ones_like(sat), np.ones_like(val)
rgba_img = to_img(np.concatenate([hue, sat, val], axis=-1), mode="HSV").convert("RGB")
rgba_img.putalpha(to_img(alpha, mode="L"))
# Save as RGBA image
rgba_img.save(join(folder, "rainbow_rgba.png"))
# Blend with black background, then save as RGB image
black_img = Image.new("RGBA", rgba_img.size, (0, 0, 0, 255))
Image.alpha_composite(black_img, rgba_img).convert("RGB").save(join(folder, "rainbow_black.png"))
# Blend with checkerboard background, then save as RGB image
checker_img = Image.fromarray(checkerboard(rgba_img.size, square_size=8), mode="RGBA")
Image.alpha_composite(checker_img, rgba_img).convert("RGB").save(join(folder, "rainbow_checker.png"))
Here's what the result looks like with black background:
Here's the result with checkerboard background:

It might also help to "play" with the mask (array alpha) and/or the corresponding channels (arrays sat, val), e.g. normalize their actual minimum and maximum value to the full 0–1 range and adjust their gamma value, before combining the channels to the mask or the mask with the image.