How do I ColorQuantize to a predetermined mosiac builder's pallet of limited choices?

If one is already working with a specific pallet of colors (say 9 colors of jelly bean in a mosaic) is there a good way to convert any image into those predetermined color choices? The ColorQuantize function makes its own color selections that when translated and "printed" in the predetermined pallet result in seeming poor art choices with both low contrast and erratic emphasis.

Color quantization works by reducing the number of colors on the basis of colors that already appear in the image. With the reduced set of colors, all pixels in the image are converted to the best possible choice.

The best possible choice is usually a color from the reduced set that is closest to the pixel in the image. Therefore, you need a distance measure between two colors to calculate which color is the best match.

That being said, no one stops you from defining colors yourself and writing a function that gives for each input color the best matching color from the set of manually chosen colors. Let me give an example where I take 32 colors from one of the ColorData indexed color schemes:

With[{cols = List @@@ (ColorData[24] /@ Range[32])},
selectColor[rgb : {_, _, _}] := First[SortBy[cols, Norm[rgb - #] &]]
];

img = Import[
ImageApply[selectColor, img]


An alternative for selectColor function using Nearest.

With[{cols = List @@@ (ColorData[24] /@ Range[32])},
selectColor[rgb : {_, _, _}] := Flatten@Nearest[cols, rgb, 1]];


This uses an image like this

to create

Obviously, this set of colors are missing green tones, so let's try a better set:

If you use this with

With[{cols =
List @@@
Flatten[Table[
ColorConvert[Hue[v, 1, b], "RGB"], {v, 0, 1, .1}, {b, .1,
1, .3}]]},
selectColor[rgb : {_, _, _}] := First[SortBy[cols, Norm[rgb - #] &]]
]


you will get

That should give you a start. For faster processing, you should compile this function and apply it in parallel on the pixel matrix.

Edit: Compiled version

With[{cols =
List @@@
Flatten[Table[
ColorConvert[Hue[v, s, b], "RGB"], {v, 0, 1, .1}, {s, 0,
1, .3}, {b, 0, 1, .3}]]},
fC = Compile[{{rgb, _Real, 1}},
Module[{min = 100.0, currMin = 100.0, minCol = cols[[1]]},
Do[
If[(currMin = Norm[rgb - c]) < min,
minCol = c;
min = currMin
]
, {c, cols}];
minCol
], CompilationTarget -> "C", Parallelization -> True,
RuntimeAttributes -> {Listable}
]
]


And then

Image[fC@ImageData[img]]


Or if you like to make it more autumn-like, give only a small color range

• This is pretty cool. I think Nearest can be used in the selectColor function like this Flatten@Nearest[cols, rgb, 1]. The results look similar. – Anjan Kumar Mar 15 '17 at 1:10
• @AnjanKumar Yes, in high-level code Nearest is the better alternative. I was giving the Norm approach as I had compilability in mind. Feel free to edit my answer if you like to have Nearest included in the answer. – halirutan Mar 15 '17 at 1:13
• Please check the edit. – Anjan Kumar Mar 15 '17 at 1:23
• There is buil-it ColorDistance (starting from version 10) which should give better results than Nearest in RGB colorspace. – Alexey Popkov Mar 16 '17 at 3:17
• @AlexeyPopkov I know. Additionally, RGB is not a good choice for calculating metric color distances. What I tried is to present the general approach. I hope that the OP investigates on his own and reads some introduction about color spaces and perception, if she/he is really interested in the matter. – halirutan Mar 16 '17 at 3:19

You can use ColorQuantize in this application by specifying the desired color palette using Colorize and ColorRules. This code uses a color palette cols (borrowed from halirutan's answer) and applies it to the canoe picture (also borrowed from halirutan):

img = Import["https://i.stack.imgur.com/VG7J1.png"];
cols = Flatten[Table[ColorConvert[Hue[v,1,b],"RGB"],{v,0,1,0.2}, {b,0.1,1,.3}]];
Colorize[ClusteringComponents[ColorQuantize[img, Length[cols]]],

I suppose a less garish color choice might be appropriate... You should keep two things in mind: First, the original colors from ColorQuantize and the chosen colors cols share no similarity, which looks artistic but is different from halirutan's approach, who tries to find nearest colors. As a consequence, you might want to consider turning Dithering->False in ColorQuantize as this tries to simulate more color shades by sprinkling neighbouring colors. This doesn't really make sense with random colors. Here is the output without dithering
• As I understand, effectively you just replace the colors selected by ColorQuantize with your own without taking into account color distances. That's explain why the result is so bad. Probably considering color distances would improve it. – Alexey Popkov Mar 16 '17 at 2:59
In Mathetica 11.3 ColorQuantize can take an image and a list of colors to do exactly what you ask for: