2
$\begingroup$

I have a restricted palette (hue values given below)

palette = {cobaltviolet, pthaloblue, cyan, sapgreen, emerald, 
permenentgreen, rawsienna, yellowochre, magenta, vermillion, 
cadmiumred, cadmiumyellow, 
titaniumwhite} = {#/360, #2/100, #3/100} & @@@ {{328, 35, 
  32}, {233, 100, 27}, {212, 86, 62}, {130, 73, 44}, {180, 88, 
  60}, {152, 96, 46}, {30, 65, 52}, {42, 75, 78}, {352, 75, 
  76}, {5, 76, 55}, {10, 91, 78}, {58, 100, 100}, {0, 0, 100}};

that I am using to blend colours subtractively using Blend interpolation. Using the functions cc and cwf from here, I can blend an approximation to a desired colour by eye using different colours like this:

enter image description here

Row[{With[{co = {cobaltviolet, titaniumwhite, sapgreen, yellowochre}},
cwf[Join[
 co, {{#/360, #2/100, #3/100} & @@ {19, 22, 66}, 
  cc@Blend[
    ColorConvert[Hue[#, #2, #3], "RGB"] & @@@ co, {20, 9, 1, 6}]}]]],
With[{co = {magenta, titaniumwhite, pthaloblue, cadmiumyellow}}, 
cwf[Join[
 co, {{#/360, #2/100, #3/100} & @@ {19, 22, 66}, 
  cc@Blend[
    ColorConvert[Hue[#, #2, #3], "RGB"] & @@@ co, {2, 4, 5, 2}]}]]]}]

I can then either blend the colour with the following ratios: {{magenta, titaniumwhite, pthaloblue, cadmiumyellow} , {2, 4, 5, 2}}, or {{cobaltviolet, titaniumwhite, sapgreen, yellowochre} , {20, 9, 1, 6}}

This is fairly time consuming though, and I was wondering whether it would be possible to automate the process?

$\endgroup$
4
  • $\begingroup$ Are you given the desired color in a certain form, by the way? Or would you like to be able to pick it interactively? $\endgroup$
    – thorimur
    Mar 23 at 10:15
  • $\begingroup$ @thoimur I have the colour values already, so don't need to pick it interactivly. I realise some values won't be possible to mix, for example strongly saturated oranges, but as near as possible would be good. $\endgroup$
    – martin
    Mar 23 at 10:23
  • $\begingroup$ ah, I see. And just to be sure, I take it you’re forced to blend in RGB, since Blend interpolates differently depending on the colorspace, right? $\endgroup$
    – thorimur
    Mar 23 at 10:33
  • 1
    $\begingroup$ @thorimur yes, have to blend in RGB $\endgroup$
    – martin
    Mar 23 at 10:34
2
+500
$\begingroup$

Get the palette in RGB:

RGBpalette = (List @@ ColorConvert[Hue[##], "RGB"]) & @@@ palette

Get the convex hull in RGB, and pick only those colors on the boundary—we'll use these for interpolation:

region = ConvexHullRegion[RGBpalette]
generators = 
 Select[(List @@@ RGBpalette), 
  RegionMember[RegionBoundary@ConvexHullRegion[List @@@ RGBpalette], #] &]

Get the nearest color to a point RGBpt in RGB space by choosing the nearest point with RegionNearest[regionmesh, RGBpt]; for this we need regionmesh = ConvexHullMesh[RGBpalette].

We'll then use linear programming to find a suitable combination of generators to produce this. The padding and extra 1's are to enforce the ratios sum to 1.

ratios[RGBpt : {_, _, _}] := 
 LinearProgramming[Table[1, {Length[generators]}], 
  ArrayPad[Transpose[generators], {{0, 1}, {0, 0}}, 1.], {#, 0} & /@ 
   Append[RegionNearest[regionmesh, RGBpt], 1]]

If we want to reconstruct the color:

recreate[RGBpt : {_, _, _}] := Blend[RGBColor /@ generators, ratios[RGBpt]]

Code Block

RGBpalette = (List @@ ColorConvert[Hue[##], "RGB"]) & @@@ palette;

region = ConvexHullRegion[RGBpalette];

generators = 
 Select[(List @@@ RGBpalette), 
  RegionMember[RegionBoundary@ConvexHullRegion[List @@@ RGBpalette], #] &];

regionmesh = ConvexHullMesh[RGBpalette];

ratios[RGBpt : {_, _, _}] := 
 LinearProgramming[Table[1, {Length[generators]}], 
  ArrayPad[Transpose[generators], {{0, 1}, {0, 0}}, 1.], {#, 0} & /@ 
   Append[RegionNearest[regionmesh, RGBpt], 1]]

recreate[RGBpt : {_, _, _}] := Blend[RGBColor /@ generators, ratios[RGBpt]]

(* Test: for a point in the hull, we recover that point: *)

List @@ recreate[{0.5, 0.6, 0.7}]

(* produces {0.5, 0.6, 0.7}. *)

Recreation

Here's how it recreates the brightness = 1 hue circle:

A somewhat darker hue circle, since the source colors are overall a bit darker

It's a lot darker, but unfortunately we need to trade off something given that the generating colors don't include the whole colorspace under interpolation.

We could use different "nearest" colors, though, by potentially using something else instead of RegionNearest[regionMesh, RGBpt]. For example, we could intersect our mesh with the region of colors with brightness equal to RGBpt, and then take RegionNearest with respect to the intersected region—then the colors wouldn't appear darker, but they would appear completely desaturated at brightness 1. It just depends on the goal!

RegionPlot[x^2 + y^2 <= 1, {x, -1, 1}, {y, -1, 1}, 
 ColorFunction -> (RGBColor @@ recreate[List @@ ColorConvert[Hue[ArcTan[#1, #2]/(2 Pi), Sqrt[#1^2 + #2^2], 1], "RGB"]] &), 
 Axes -> False, Frame -> False, ColorFunctionScaling -> False, 
 Mesh -> None, PlotPoints -> 200]
$\endgroup$
2
  • 1
    $\begingroup$ great, thank you. A linear programming method is far smarter! I can get what I need with li = Thread@ Select[Thread@{ratios[{0.5, 0.6, 0.7}], RGBColor /@ generators}, #[[1]] > 0 &]; Thread@{{"cobaltviolet", "pthaloblue", "cyan", "sapgreen", "emerald", "permenentgreen", "rawsienna", "yellowochre", "magenta", "vermillion", "cadmiumred", "cadmiumyellow", "titaniumwhite"}[[#]] & /@ Flatten[Position[RGBColor /@ RGBpalette, #] & /@ li[[2]]], li[[1]]} $\endgroup$
    – martin
    Mar 23 at 17:51
  • $\begingroup$ amazing, thank you! $\endgroup$
    – martin
    Mar 23 at 18:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.