Browse Source

ICC: Add Profile::to_lab()

This can be used to convert a profile-dependent color to the L*a*b*
color space.

(I'd like to use this to implement the DeltaE (CIE 2000) algorithm,
which is a metric for how similar two colors are perceived.
(And I'd like to use that to evaluate color conversion roundtrip
quality, once I've implemented full conversions.)
Nico Weber 2 years ago
parent
commit
f3dbfb85d9

+ 50 - 0
Tests/LibGfx/TestICCProfile.cpp

@@ -153,3 +153,53 @@ TEST_CASE(to_pcs)
     float f192 = sRGB_curve.evaluate(192 / 255.f);
     EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(64, 128, 192), r_xyz * f64 + g_xyz * f128 + b_xyz * f192);
 }
+
+TEST_CASE(to_lab)
+{
+    auto sRGB = MUST(Gfx::ICC::sRGB());
+    auto lab_from_sRGB = [sRGB](u8 r, u8 g, u8 b) {
+        u8 rgb[3] = { r, g, b };
+        return MUST(sRGB->to_lab(rgb));
+    };
+
+    // The `expected` numbers are from https://colorjs.io/notebook/ for this snippet of code:
+    //     new Color("srgb", [0, 0, 0]).lab.toString();
+    //
+    //     new Color("srgb", [1, 0, 0]).lab.toString();
+    //     new Color("srgb", [0, 1, 0]).lab.toString();
+    //     new Color("srgb", [0, 0, 1]).lab.toString();
+    //
+    //     new Color("srgb", [1, 1, 0]).lab.toString();
+    //     new Color("srgb", [1, 0, 1]).lab.toString();
+    //     new Color("srgb", [0, 1, 1]).lab.toString();
+    //
+    //     new Color("srgb", [1, 1, 1]).lab.toString();
+
+    Gfx::ICC::Profile::CIELAB expected[] = {
+        { 0, 0, 0 },
+        { 54.29054294696968, 80.80492033462421, 69.89098825896275 },
+        { 87.81853633115202, -79.27108223854806, 80.99459785152247 },
+        { 29.56829715344471, 68.28740665215547, -112.02971798617645 },
+        { 97.60701009682253, -15.749846639252663, 93.39361164266089 },
+        { 60.16894098715946, 93.53959546199253, -60.50080231921204 },
+        { 90.66601315791455, -50.65651077286893, -14.961666625736525 },
+        { 100.00000139649632, -0.000007807961277528364, 0.000006766250648659877 },
+    };
+
+    // We're off by more than the default EXPECT_APPROXIMATE() error, so use EXPECT_APPROXIMATE_WITH_ERROR().
+    // The difference is not too bad: ranges for L*, a*, b* are [0, 100], [-125, 125], [-125, 125],
+    // so this is an error of considerably less than 0.1 for u8 channels.
+#define EXPECT_APPROXIMATE_LAB(l1, l2)                   \
+    EXPECT_APPROXIMATE_WITH_ERROR((l1).L, (l2).L, 0.01); \
+    EXPECT_APPROXIMATE_WITH_ERROR((l1).a, (l2).a, 0.03); \
+    EXPECT_APPROXIMATE_WITH_ERROR((l1).b, (l2).b, 0.02);
+
+    EXPECT_APPROXIMATE_LAB(lab_from_sRGB(0, 0, 0), expected[0]);
+    EXPECT_APPROXIMATE_LAB(lab_from_sRGB(255, 0, 0), expected[1]);
+    EXPECT_APPROXIMATE_LAB(lab_from_sRGB(0, 255, 0), expected[2]);
+    EXPECT_APPROXIMATE_LAB(lab_from_sRGB(0, 0, 255), expected[3]);
+    EXPECT_APPROXIMATE_LAB(lab_from_sRGB(255, 255, 0), expected[4]);
+    EXPECT_APPROXIMATE_LAB(lab_from_sRGB(255, 0, 255), expected[5]);
+    EXPECT_APPROXIMATE_LAB(lab_from_sRGB(0, 255, 255), expected[6]);
+    EXPECT_APPROXIMATE_LAB(lab_from_sRGB(255, 255, 255), expected[7]);
+}

+ 53 - 0
Userland/Libraries/LibGfx/ICC/Profile.cpp

@@ -1470,6 +1470,59 @@ ErrorOr<FloatVector3> Profile::to_pcs(ReadonlyBytes color)
     VERIFY_NOT_REACHED();
 }
 
+ErrorOr<Profile::CIELAB> Profile::to_lab(ReadonlyBytes color)
+{
+    auto pcs = TRY(to_pcs(color));
+    if (connection_space() == ColorSpace::PCSLAB)
+        return CIELAB { pcs[0], pcs[1], pcs[2] };
+
+    if (connection_space() != ColorSpace::PCSXYZ) {
+        VERIFY(device_class() == DeviceClass::DeviceLink);
+        return Error::from_string_literal("ICC::Profile::to_lab: conversion for DeviceLink not implemented");
+    }
+
+    // 6.3.2.2 Translation between media-relative colorimetric data and ICC-absolute colorimetric data
+    // 6.3.2.3 Computation of PCSLAB
+    // 6.3.4 Colour space encodings for the PCS
+    // A.3 PCS encodings
+
+    auto f = [](float x) {
+        if (x > powf(6.f / 29.f, 3))
+            return cbrtf(x);
+        return x / (3 * powf(6.f / 29.f, 2)) + 4.f / 29.f;
+    };
+
+    // "X/Xn is replaced by Xr/Xi (or Xa/Xmw)"
+
+    // 6.3.2.2 Translation between media-relative colorimetric data and ICC-absolute colorimetric data
+    // "The translation from ICC-absolute colorimetric data to media-relative colorimetry data is given by Equations
+    //      Xr = (Xi/Xmw) * Xa
+    //  where
+    //      Xr   media-relative colorimetric data (i.e. PCSXYZ);
+    //      Xa   ICC-absolute colorimetric data (i.e. nCIEXYZ);
+    //      Xmw  nCIEXYZ values of the media white point as specified in the mediaWhitePointTag;
+    //      Xi   PCSXYZ values of the PCS white point defined in 6.3.4.3."
+    // 6.3.4.3 PCS encodings for white and black
+    // "Table 14 — Encodings of PCS white point: X 0,9642 Y 1,0000 Z 0,8249"
+    // That's identical to the values in 7.2.16 PCS illuminant field (Bytes 68 to 79).
+    // 9.2.36 mediaWhitePointTag
+    // "For displays, the values specified shall be those of the PCS illuminant as defined in 7.2.16."
+    // ...so for displays, this is all equivalent I think? It's maybe different for OutputDevice profiles?
+
+    float Xn = pcs_illuminant().X;
+    float Yn = pcs_illuminant().Y;
+    float Zn = pcs_illuminant().Z;
+
+    float x = pcs[0] / Xn;
+    float y = pcs[1] / Yn;
+    float z = pcs[2] / Zn;
+
+    float L = 116 * f(y) - 16;
+    float a = 500 * (f(x) - f(y));
+    float b = 200 * (f(y) - f(z));
+    return CIELAB { L, a, b };
+}
+
 XYZ const& Profile::red_matrix_column() const { return xyz_data(redMatrixColumnTag); }
 XYZ const& Profile::green_matrix_column() const { return xyz_data(greenMatrixColumnTag); }
 XYZ const& Profile::blue_matrix_column() const { return xyz_data(blueMatrixColumnTag); }

+ 7 - 0
Userland/Libraries/LibGfx/ICC/Profile.h

@@ -266,6 +266,13 @@ public:
     // Call connection_space() to find out the space the result is in.
     ErrorOr<FloatVector3> to_pcs(ReadonlyBytes);
 
+    struct CIELAB {
+        float L; // L*
+        float a; // a*
+        float b; // b*
+    };
+    ErrorOr<CIELAB> to_lab(ReadonlyBytes);
+
     // Only call these if you know that this is an RGB matrix-based profile.
     XYZ const& red_matrix_column() const;
     XYZ const& green_matrix_column() const;