HCL color to RGB and backward

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))));
}

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