22

I need an algorithm to convert the HCL color to RGB and backward RGB to HCL keeping in mind that these color spaces have different gamuts (I need to constrain the HCL colors to those that can be reproduced in RGB color space). What is the algorithm for this (the algorithm is intended to be implemented in Wolfram Mathematica that supports natively only RGB color)? I have no experience in working with color spaces.

P.S. Some articles about HCL color:

M. Sarifuddin (2005). A new perceptually uniform color space with associated color similarity measure for content–based image and video retrieval.

Zeileis, Hornik and Murrell (2009): Escaping RGBland: Selecting Colors for Statistical Graphics // Computational Statistics & Data Analysis Volume 53, Issue 9, 1 July 2009, Pages 3259-3270

UPDATE: As pointed out by Jonathan Jansson, in the above two articles different color spaces are described by the name "HCL": "The second article uses LCh(uv) which is the same as Luv* but described in polar coordiates where h(uv) is the angle of the u* and v* coordinate and C* is the magnitude of that vector". So in fact I need an algorithm for converting RGB to Luv* and backward.

denis
  • 21,378
  • 10
  • 65
  • 88
Alexey Popkov
  • 9,355
  • 4
  • 42
  • 93
  • 3
    Have you seen http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip ? Appears to be by the same authors (Sarifuddin / Missaoui), but includes algorithms for going both ways. – AakashM Sep 23 '11 at 15:15
  • @AakashM Thank you for referencing this paper but this paper does not contain the complete algorithm: it is not clear what the value of `gamma` should be for example. – Alexey Popkov Sep 23 '11 at 15:37
  • 2
    `gamma` appears to be a tuning parameter to be adjusted based on (I think) overall light level. The code @Sjoerd links to uses `3`. – AakashM Sep 23 '11 at 16:04
  • 3
    Somewhere on page 3 in the paper: `γ is a correction factor whose value (= 3) coincides with the one used in L*a*b* space.` – Ishtar Sep 23 '11 at 16:06

6 Answers6

14

I was just learing about the HCL colorspace too. The colorspace used in the two articles in your question seems to be different color spaces though.

The second article uses L*C*h(uv) which is the same as L*u*v* but described in polar coordiates where h(uv) is the angle of the u* and v* coordiate and C* is the magnitude of that vector.

The LCH color space in the first article seems to describe another color space than that uses a more algorithmical conversion. There is also another version of the first paper here: http://isjd.pdii.lipi.go.id/admin/jurnal/14209102121.pdf

If you meant to use the CIE L*u*v* you need to first convert sRGB to CIE XYZ and then convert to CIE L*u*v*. RGB actually refers to sRGB in most cases so there is no need to convert from RGB to sRGB.

All source code needed

Good article about how conversion to XYZ works

Nice online converter

But I can't answer your question about how to constrain the colors to the sRGB space. You could just throw away RGB colors which are outside the range 0 to 1 after conversion. Just clamping colors can give quite weird results. Try to go to the converter and enter the color RGB 0 0 255 and convert to L*a*b* (similar to L*u*v*) and then increase L* to say 70 and convert it back and the result is certainly not blue anymore.

Edit: Corrected the URL Edit: Merged another answer into this answer

Igor Brejc
  • 18,714
  • 13
  • 76
  • 95
  • Good points! In this case I need an algorithm for converting RGB to L*u*v* and backward. I have updated my question. BTW, the link in your answer is dead. – Alexey Popkov Sep 27 '11 at 03:52
  • 1
    I merged my previous answer into this one. There is probably no need to convert RGB to sRGB. Just saying RGB is not a specific standard so you can probably assume it is sRGB. You have to read the manual for Mathematica and see if they mention anything about it. If you know that the RGB color you have is actually sRGB (or some other standardised RGB variant) then that color is device independent. – Jonathan Jansson Sep 27 '11 at 09:52
8

HCL is a very generic name there are a lot of ways to have a hue, a chroma, and a lightness. Chroma.js for example has something it calls HCL which is polar coord converted Lab (when you look at the actual code). Other implementations, even ones linked from that same site use Polar Luv. Since you can simply borrow the L factor and derive the hue by converting to polar coords these are both valid ways to get those three elements. It is far better to call them Polar Lab and Polar Luv, because of the confusion factor.

M. Sarifuddin (2005)'s algorithm is not Polar Luv or Polar Lab and is computationally simpler (you don't need to derive Lab or Luv space first), and may actually be better. There are some things that seem wrong in the paper. For example applying a Euclidean distance to a CIE L*C*H* colorspace. The use of a Hue means it's necessarily round, and just jamming that number into A²+B²+C² is going to give you issues. The same is true to apply a hue-based colorspace to D94 or D00 as these are distance algorithms with built in corrections specific to Lab colorspace. Unless I'm missing something there, I'd disregard figures 6-8. And I question the rejection tolerances in the graphics. You could set a lower threshold and do better, and the numbers between color spaces are not normalized. In any event, despite a few seeming flaws in the paper, the algorithm described is worth a shot. You might want to do Euclidean on RGB if it doesn't really matter much. But, if you're shopping around for color distance algorithms, here you go.

Here is HCL as given by M. Sarifuddin implemented in Java. Having read the paper repeatedly I cannot avoid the conclusion that it scales the distance by a factor of between 0.16 and 180.16 with regard to the change in hue in the distance_hcl routine. This is such a profound factor that it almost cannot be right at all. And makes the color matching suck. I have the paper's line commented out and use a line with only the Al factor. Scaling Luminescence by constant ~1.4 factor isn't going to make it unusable. With neither scale factor it ends up being identical to cycldistance.

http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip is corrected and improved version of the paper.

static final public double Y0 = 100;
static final public double gamma = 3;
static final public double Al = 1.4456;
static final public double Ach_inc = 0.16;

public void rgb2hcl(double[] returnarray, int r, int g, int b) {
    double min = Math.min(Math.min(r, g), b);
    double max = Math.max(Math.max(r, g), b);
    if (max == 0) {
        returnarray[0] = 0;
        returnarray[1] = 0;
        returnarray[2] = 0;
        return;
    }

    double alpha = (min / max) / Y0;
    double Q = Math.exp(alpha * gamma);
    double rg = r - g;
    double gb = g - b;
    double br = b - r;
    double L = ((Q * max) + ((1 - Q) * min)) / 2;
    double C = Q * (Math.abs(rg) + Math.abs(gb) + Math.abs(br)) / 3;
    double H = Math.toDegrees(Math.atan2(gb, rg));

    /*
    //the formulae given in paper, don't work.
    if (rg >= 0 && gb >= 0) {
        H = 2 * H / 3;
    } else if (rg >= 0 && gb < 0) {
        H = 4 * H / 3;
    } else if (rg < 0 && gb >= 0) {
        H = 180 + 4 * H / 3;
    } else if (rg < 0 && gb < 0) {
        H = 2 * H / 3 - 180;
    } // 180 causes the parts to overlap (green == red) and it oddly crumples up bits of the hue for no good reason. 2/3H and 4/3H expanding and contracting quandrants.
    */

    if (rg <  0) {
        if (gb >= 0) H = 90 + H;
        else { H = H - 90; }
    } //works


    returnarray[0] = H;
    returnarray[1] = C;
    returnarray[2] = L;
}

public double cycldistance(double[] hcl1, double[] hcl2) {
    double dL = hcl1[2] - hcl2[2];
    double dH = Math.abs(hcl1[0] - hcl2[0]);
    double C1 = hcl1[1];
    double C2 = hcl2[1];
    return Math.sqrt(dL*dL + C1*C1 + C2*C2 - 2*C1*C2*Math.cos(Math.toRadians(dH)));
}

public double distance_hcl(double[] hcl1, double[] hcl2) {
    double c1 = hcl1[1];
    double c2 = hcl2[1];
    double Dh = Math.abs(hcl1[0] - hcl2[0]);
    if (Dh > 180) Dh = 360 - Dh;
    double Ach = Dh + Ach_inc;
    double AlDl = Al * Math.abs(hcl1[2] - hcl2[2]);
    return Math.sqrt(AlDl * AlDl + (c1 * c1 + c2 * c2 - 2 * c1 * c2 * Math.cos(Math.toRadians(Dh))));
    //return Math.sqrt(AlDl * AlDl + Ach * (c1 * c1 + c2 * c2 - 2 * c1 * c2 * Math.cos(Math.toRadians(Dh))));
}
Tatarize
  • 10,238
  • 4
  • 58
  • 64
  • 1
    For the life of me I don't get why the authors thought they could account for 8 degrees of hue being less of a change than 50 Chroma, by mulitplying the entire distance routine by (dH + 8/50). That's just scaling the distance part by ~(0-180) and dwarfing the poor Luminescence part making it pointless. – Tatarize Aug 17 '12 at 06:05
  • 1
    The shoddy math never really works for the hue either. Even the "//works" bit still just has fewer regions colliding. You can't really produce a full range of colors with atan((G-B)/(R-G)) pretty much no matter how you tweak it afterwards. If R == G, then you have an angle 0, period, whether grey 128,128,128, blue 1,1,255, or yellow 255,255,1. – Tatarize Sep 29 '13 at 09:17
  • how would you convert it back with this modification of yours? – user151496 Mar 14 '14 at 14:58
  • 1
    I wouldn't. As far as color distance algorithms or even color spaces goes. It's crap. Seriously. Use LAB if you want better results, use redmean if you want a quick and dirty but ultimately really good results. http://www.compuphase.com/cmetric.htm The fix in the distance can be ignored. Distance isn't going to tweak the color space. As for the actual colorspace itself you'd use polar coords to find the position in the cylinder then use that to find out what RGB colors you'd use. But, really don't bother. It's just RGB and they faked their results. Lab is great. – Tatarize Feb 07 '15 at 11:44
  • we all know l*ab or l*uv is great but is computationally expensive. too expensive for real time applications – user151496 Mar 10 '15 at 09:43
  • It's actually not. I know as much as I do about this subject because I use LAB in a real time application, where I need to compare several thousand different colors a second. The trick is that most colors repeat. You aren't ever dealing with absolute noise. If you implement a cache for the LAB color location conversions and the color conversion then 99% of the time, you end up not doing the compare. – Tatarize Mar 12 '15 at 11:50
  • In the end, I typically use LabDE2000, because given the cache it goes almost as fast as euclidean. The only slight issue I hit was not losing the speed for multi-threaded accesses but a nice volatile and some piggybacking and it's very little time spent at all (when it's basically the core operation). – Tatarize Mar 12 '15 at 11:55
6

As mentioned in other answers, there are a lot of ways to implement an HCL colorspace and map that into RGB.

HSLuv ended up being what I used, and has MIT-licensed implementations in C, C#, Go, Java, PHP, and several other languages. It's similar to CIELUV LCh but fully maps to RGB. The implementations are available on GitHub.

Here's a short graphic from the website describing the HSLuv color space, with the implementation output in the right two panels:

HSLuv color space examples compared to HSL and CIELUV

mopsled
  • 8,445
  • 1
  • 38
  • 40
6

I'm familiar with quite a few color spaces, but this one is new to me. Alas, Mathematica's ColorConvert doesn't know it either.

I found an rgb2hcl routine here, but no routine going the other way.

A more comprehensive colorspace conversion package can be found here. It seems to be able to do conversions to and from all kinds of color spaces. Look for the file colorspace.c in colorspace_1.1-0.tar.gz\colorspace_1.1-0.tar\colorspace\src. Note that HCL is known as PolarLUV in this package.

Sjoerd C. de Vries
  • 16,122
  • 3
  • 42
  • 94
  • From that first link there was in the code a link to http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip which is a paper containing the alorithms/equations for both ways. – Chris Sep 23 '11 at 15:21
  • @Chris See my comment on the AakashM's comment under the question. – Alexey Popkov Sep 23 '11 at 15:39
5

I was looking to interpolate colors on the web and found HCL to be the most fitting color space, I couldn't find any library making the conversion straightforward and performant so I wrote my own.

There's many constants at play, and some of them vary significantly depending on where you source them from.

My target being the web, I figured I'd be better off matching the chromium source code. Here's a minimized snippet written in Typescript, the sRGB XYZ matrix is precomputed and all constants are inlined.

const rgb255 = (v: number) => (v < 255 ? (v > 0 ? v : 0) : 255);
const b1 = (v: number) => (v > 0.0031308 ? v ** (1 / 2.4) * 269.025 - 14.025 : v * 3294.6);
const b2 = (v: number) => (v > 0.2068965 ? v ** 3 : (v - 4 / 29) * (108 / 841));
const a1 = (v: number) => (v > 10.314724 ? ((v + 14.025) / 269.025) ** 2.4 : v / 3294.6);
const a2 = (v: number) => (v > 0.0088564 ? v ** (1 / 3) : v / (108 / 841) + 4 / 29);

function fromHCL(h: number, c: number, l: number): RGB {
    const y = b2((l = (l + 16) / 116));
    const x = b2(l + (c / 500) * Math.cos((h *= Math.PI / 180)));
    const z = b2(l - (c / 200) * Math.sin(h));
    return [
        rgb255(b1(x * 3.021973625 - y * 1.617392459 - z * 0.404875592)),
        rgb255(b1(x * -0.943766287 + y * 1.916279586 + z * 0.027607165)),
        rgb255(b1(x * 0.069407491 - y * 0.22898585 + z * 1.159737864)),
    ];
}

function toHCL(r: number, g: number, b: number) {
    const y = a2((r = a1(r)) * 0.222488403 + (g = a1(g)) * 0.716873169 + (b = a1(b)) * 0.06060791);
    const l = 500 * (a2(r * 0.452247074 + g * 0.399439023 + b * 0.148375274) - y);
    const q = 200 * (y - a2(r * 0.016863605 + g * 0.117638439 + b * 0.865350722));
    const h = Math.atan2(q, l) * (180 / Math.PI);
    return [h < 0 ? h + 360 : h, Math.sqrt(l * l + q * q), 116 * y - 16];
}

Here's a playground for the above snippet.
It includes d3's interpolateHCL and the browser native css transition for comparaison.
https://svelte.dev/repl/0a40a8348f8841d0b7007c58e4d9b54c

Here's a gist to do the conversion to and from any web color format and interpolate it in the HCL color space.
https://gist.github.com/pushkine/c8ba98294233d32ab71b7e19a0ebdbb9

pushkine
  • 177
  • 2
  • 5
0

I think

if (rg <  0) {
    if (gb >= 0) H = 90 + H;
    else { H = H - 90; }
} //works

is not really necessary because of atan2(,) instead of atan(/) from paper (but dont now anything about java atan2(,) especially

  • The algorithm treats rg and gb like they are complementary colors but they aren't complementary but tristimulus. It generates values between +90° and -90° by using atan2. But, then swings around the other sections with that code. It is very close to what atan2() does normally, to normalize but it's correcting it even more for the specific quadrants. The paper tries to move Quadrant I -> Quadrant III and Quadrant II -> Quadrant IV. But, atan2() produces results in Quadrant I and Quadrant III only. It overlaps the parts of the hue, and then rechecks to move their quadrants. – Tatarize Sep 29 '13 at 05:07
  • 2
    Suffice it to say, it sucks. Even fixing their shoddy work and bad math and needing to not use their flawed work at places and putting the very best foot forward. It's still just a modification of RGB and not a very good one. Most of the tables in their paper are lies. http://godsnotwheregodsnot.blogspot.com/2012/09/hcl-new-color-space-for-pack-of-lies.html -- If you want something good try Hunter Lab. Computationally simple and results are about as good as LAB DE2000. ( http://www.easyrgb.com/index.php?X=MATH&H=02#text2 ) – Tatarize Sep 29 '13 at 05:20