# How to separate n colors from image into n binary images?

I have some images in which I expect to have some red, green, blue, or yellow areas. Like this image:

myImage = Now, I'd like to get 4 binary images, representing each of the 4 colors. What I'd like to have is a process that decides, pixel by pixel, which of the 4 colors is closest (or black). I know we can do something like this to split the RGB channels:

Binarize[#, 0.25] & /@ ColorSeparate[myImage] My primary question is, how could I add the yellow color (or any other color) to the color separation? A related question, does ColorSeparate ensures that no two binary images share a white (foreground) pixel? By eye I don't see much overlap, but I imagine if two colors are close together they might be assigned to two binary images(?), say, orange & yellow. If so, how to ensure that each foreground pixel is only present in exactly one of the binary images? I was thinking on using something similar to what is in this question, but it seems that a distance from a color would not work with similar colors (some pixels would be assigned to two binary images).

Thanks!

• Do you have a precise definition of color closeness in mind? Jul 14, 2021 at 15:55
• @bRost03 Nope, but say we have a list of 4 desired colors + black. Then, I imagine we can use ColorDistance[] to figure out, for each pixel of the query image, which of the 4 colors (or black) has the smallest distance. Then, assign this pixel to the foreground (white) of a binary image representing that color binarization, getting 4 binary images at the end. Jul 14, 2021 at 16:08
• Yeah, but working with Color objects and functions seems to be somewhat slow in my experience. I was going to do it using the raw channel data, but just wanted to know if you had a color distance in mind. I believe the default is Euclidean distance using LAB color data. Jul 14, 2021 at 16:14
• @bRost03 Hmmm, I don't have a specific color distance in mind. The default should be fine, and I imagine if there was a different color space needed, one could adapt it. Jul 14, 2021 at 16:19

The ColorQuantize approach is nice but it doesn't given you any control over how to decide which pixel becomes what color. Here's a different approach using ColorDistance which is of comparable speed to the ColorQuantize approach and somewhere around 5x faster than the approach given by @flinty.

img = Import["https://i.stack.imgur.com/q9Q5A.png"];
cols = {Black, Red, Yellow, Green, Blue};


We then make a list which holds the index corresponding to the color in cols which is closest to each pixel

inds = Map[First@Ordering[#, 1] &,
Transpose[ImageData /@ ColorDistance[img, cols], {3, 1, 2}], {2}]


The ColorDistance gives a list of greyscale images with the $$i^{\text{th}}$$ image having pixel values corresponding to the color distance from the color cols[[i]]. Ordering gives the index of the minimum value in that list.

You can then produce the desired images with

images = Image[1 - Sign@Abs[inds - #]] & /@ Range[2, Length[cols]];


with the 2 being to exclude the black mask.

This has the benefit over ColorQuantize that you can specify an arbitrary DistanceFunction in ColorDistance to select the colors according to whatever criteria you desire. Using the default function gives me whereas using DistanceFunction -> "DeltaL" gives You can use ColorQuantize with a list of preferred colours:

Remove["Global*"]
cols = {Red, Yellow, Green, Blue};
cq = RemoveAlphaChannel@
ColorQuantize[img, Append[cols, Black], Dithering -> False];
planes = With[{imd = Round@ImageData@cq},
Table[Image[Map[Boole[# == c] &, imd, {2}]],
{c, cols /. RGBColor -> List}]
] Use Dithering->True if you prefer that on the ColorQuantize, but note much less of the blue will show up because it's so dark - that's why I did the initial ImageAdjust.

• You can then do HighlightImage[img, #] & /@ planes - your image is very noisy so this method ignores a lot of those patches. Jul 9, 2021 at 21:50
• Wow, ColorQuantize seems very cool. And yeah, the noise in the image is something inherent to this data set :/ Jul 9, 2021 at 21:52

This technique comparing the pixel hues in HSB to a list of known hues seems to produce much better masks than my ColorQuantize approach:

img = Import["https://i.stack.imgur.com/q9Q5A.png"] // ImageAdjust;
himg = ColorConvert[img, "HSB"];
cols = ColorConvert[{Red, Yellow, Green, Blue}, "HSB"];
diffHue[c1_, c2_] := .5 - Abs[Abs[c1[] - c2[]] - .5]
nearestcolpos[c_] := First@OrderingBy[cols, diffHue[c, #] &]
saturationThreshold = .1;
brightnessThreshold = .15;
isblack[c_] := c[] < brightnessThreshold || c[] < saturationThreshold;
hpalette = Map[If[isblack[#], 0, nearestcolpos[#]] &, ImageData[himg], {2}];
masks = Table[Image[hpalette /. (x_ /; x != i -> 0)], {i, Length[cols]}] g = 2.2;
grid = Union @@ ImageData[myImage];
gGrid = grid^g;

dists[cols_] := Sqrt[Total[Power[# - ConstantArray[gGrid, 4], 2], {3}]] &[
MapThread[Outer[Times, ##] &, {cols.Transpose[gGrid]/Total[cols^2, {2}], cols}]];


Above, dists computes the (gamma uncorrected RGB) distance from each pixel color in the image to each of the colors in its argument that is a Nx3 list of gamma uncorrected colors.

To find the most appropiate red/yellow/green/blue, the sum of such distances over all pixel of the image will be minimized:

ws = Lookup[Counts[Catenate[ImageData[myImage]]], grid];

varMat = Abs[Partition[Table[x[i], {i, 12}], 3]];
cols = Normalize /@ Abs[Partition[#, 3]] &[FindArgMin[distSum[varMat],
Thread[{Union[Cases[varMat, _x, All]], 0.001 + {1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0}}],
Method -> "PrincipalAxis", MaxIterations -> Infinity]];
RGBColor @@@ ((cols/Max /@ cols)^(1/g))


Now I find linear combinations of the cols to obtain each pixel color in the image. The distances are used as cost function so that similar colors are preferred as much as possible. The coefficient of each color will be how white each binary image is.

Equality may not be possible because the coefficients must be positive, so the constraints has to be greater or equal.

cs = Transpose[dists[cols]] /. {z : {(0.) ..} :> 1 + z};
m = Transpose[cols];
bs = Thread[{#, 1}] & /@ grid;
` 