From 645ff6c7029daef082b3a558407121207fa64ff5 Mon Sep 17 00:00:00 2001 From: Mark Oteiza Date: Tue, 26 Sep 2017 17:13:36 -0400 Subject: [PATCH] Add CAM02 JCh and CAM02-UCS J'a'b' conversions * src/lcms.c (rad2deg, parse_jch_list, parse_jab_list, xyz_to_jch): (jch_to_xyz, jch_to_jab, jab_to_jch): New functions. (lcms-jch->xyz, lcms-jch->xyz, lcms-jch->jab, lcms-jab->jch): New Lisp functions. (lcms-cam02-ucs): Refactor. (syms_of_lcms2): Declare new functions. * test/src/lcms-tests.el (lcms-roundtrip, lcms-ciecam02-gold): (lcms-jmh->cam02-ucs-silver): New tests. * etc/NEWS: Mention new functions. --- etc/NEWS | 3 +- src/lcms.c | 303 ++++++++++++++++++++++++++++++++++++----- test/src/lcms-tests.el | 44 ++++++ 3 files changed, 315 insertions(+), 35 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index ab9a2a5f32d..adeee9e6ef2 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -76,7 +76,8 @@ If the lcms2 library is installed, Emacs will enable features built on top of that library. The new configure option '--without-lcms2' can be used to build without lcms2 support even if it is installed. Emacs linked to Little CMS exposes color management functions in Lisp: the -color metrics 'lcms-cie-de2000' and 'lcms-cam02-ucs'. +color metrics 'lcms-cie-de2000' and 'lcms-cam02-ucs', as well as +functions for conversion to and from CIE CAM02 and CAM02-UCS. ** The configure option '--with-gameuser' now defaults to 'no', as this appears to be the most common configuration in practice. diff --git a/src/lcms.c b/src/lcms.c index a5e527911ef..c7da57658a9 100644 --- a/src/lcms.c +++ b/src/lcms.c @@ -25,6 +25,13 @@ along with GNU Emacs. If not, see . */ #include "lisp.h" +typedef struct +{ + double J; + double a; + double b; +} lcmsJab_t; + #ifdef WINDOWSNT # include # include "w32.h" @@ -36,6 +43,8 @@ DEF_DLL_FN (cmsHANDLE, cmsCIECAM02Init, (cmsContext ContextID, const cmsViewingConditions* pVC)); DEF_DLL_FN (void, cmsCIECAM02Forward, (cmsHANDLE hModel, const cmsCIEXYZ* pIn, cmsJCh* pOut)); +DEF_DLL_FN (void, cmsCIECAM02Reverse, + (cmsHANDLE hModel, const cmsJCh* pIn, cmsCIEXYZ* pOut)); DEF_DLL_FN (void, cmsCIECAM02Done, (cmsHANDLE hModel)); DEF_DLL_FN (cmsBool, cmsWhitePointFromTemp, (cmsCIExyY* WhitePoint, cmsFloat64Number TempK)); @@ -54,6 +63,7 @@ init_lcms_functions (void) LOAD_DLL_FN (library, cmsCIE2000DeltaE); LOAD_DLL_FN (library, cmsCIECAM02Init); LOAD_DLL_FN (library, cmsCIECAM02Forward); + LOAD_DLL_FN (library, cmsCIECAM02Reverse); LOAD_DLL_FN (library, cmsCIECAM02Done); LOAD_DLL_FN (library, cmsWhitePointFromTemp); LOAD_DLL_FN (library, cmsxyY2XYZ); @@ -63,6 +73,7 @@ init_lcms_functions (void) # undef cmsCIE2000DeltaE # undef cmsCIECAM02Init # undef cmsCIECAM02Forward +# undef cmsCIECAM02Reverse # undef cmsCIECAM02Done # undef cmsWhitePointFromTemp # undef cmsxyY2XYZ @@ -70,6 +81,7 @@ init_lcms_functions (void) # define cmsCIE2000DeltaE fn_cmsCIE2000DeltaE # define cmsCIECAM02Init fn_cmsCIECAM02Init # define cmsCIECAM02Forward fn_cmsCIECAM02Forward +# define cmsCIECAM02Reverse fn_cmsCIECAM02Reverse # define cmsCIECAM02Done fn_cmsCIECAM02Done # define cmsWhitePointFromTemp fn_cmsWhitePointFromTemp # define cmsxyY2XYZ fn_cmsxyY2XYZ @@ -145,6 +157,12 @@ deg2rad (double degrees) return M_PI * degrees / 180.0; } +static double +rad2deg (double radians) +{ + return 180.0 * radians / M_PI; +} + static cmsCIEXYZ illuminant_d65 = { .X = 95.0455, .Y = 100.0, .Z = 108.8753 }; static void @@ -180,6 +198,46 @@ parse_xyz_list (Lisp_Object xyz_list, cmsCIEXYZ *color) return true; } +static bool +parse_jch_list (Lisp_Object jch_list, cmsJCh *color) +{ +#define PARSE_JCH_LIST_FIELD(field) \ + if (CONSP (jch_list) && NUMBERP (XCAR (jch_list))) \ + { \ + color->field = XFLOATINT (XCAR (jch_list)); \ + jch_list = XCDR (jch_list); \ + } \ + else \ + return false; + + PARSE_JCH_LIST_FIELD (J); + PARSE_JCH_LIST_FIELD (C); + PARSE_JCH_LIST_FIELD (h); + + if (! NILP (jch_list)) + return false; + return true; +} + +static bool +parse_jab_list (Lisp_Object jab_list, lcmsJab_t *color) +{ +#define PARSE_JAB_LIST_FIELD(field) \ + if (CONSP (jab_list) && NUMBERP (XCAR (jab_list))) \ + { \ + color->field = XFLOATINT (XCAR (jab_list)); \ + jab_list = XCDR (jab_list); \ + } \ + else \ + return false; + + PARSE_JAB_LIST_FIELD (J); + PARSE_JAB_LIST_FIELD (a); + PARSE_JAB_LIST_FIELD (b); + + return true; +} + static bool parse_viewing_conditions (Lisp_Object view, const cmsCIEXYZ *wp, cmsViewingConditions *vc) @@ -216,6 +274,204 @@ parse_viewing_conditions (Lisp_Object view, const cmsCIEXYZ *wp, return true; } +static void +xyz_to_jch (const cmsCIEXYZ *xyz, cmsJCh *jch, const cmsViewingConditions *vc) +{ + cmsHANDLE h; + + h = cmsCIECAM02Init (0, vc); + cmsCIECAM02Forward (h, xyz, jch); + cmsCIECAM02Done (h); +} + +static void +jch_to_xyz (const cmsJCh *jch, cmsCIEXYZ *xyz, const cmsViewingConditions *vc) +{ + cmsHANDLE h; + + h = cmsCIECAM02Init (0, vc); + cmsCIECAM02Reverse (h, jch, xyz); + cmsCIECAM02Done (h); +} + +static void +jch_to_jab (const cmsJCh *jch, lcmsJab_t *jab, double FL, double c1, double c2) +{ + double Mp = 43.86 * log (1.0 + c2 * (jch->C * sqrt (sqrt (FL)))); + jab->J = 1.7 * jch->J / (1.0 + (c1 * jch->J)); + jab->a = Mp * cos (deg2rad (jch->h)); + jab->b = Mp * sin (deg2rad (jch->h)); +} + +static void +jab_to_jch (const lcmsJab_t *jab, cmsJCh *jch, double FL, double c1, double c2) +{ + jch->J = jab->J / (1.0 + c1 * (100.0 - jab->J)); + jch->h = atan2 (jab->b, jab->a); + double Mp = hypot (jab->a, jab->b); + jch->h = rad2deg (jch->h); + if (jch->h < 0.0) + jch->h += 360.0; + jch->C = (exp (c2 * Mp) - 1.0) / (c2 * sqrt (sqrt (FL))); +} + +DEFUN ("lcms-xyz->jch", Flcms_xyz_to_jch, Slcms_xyz_to_jch, 1, 3, 0, + doc: /* Convert CIE CAM02 JCh to CIE XYZ. +COLOR is a list (X Y Z), with Y scaled about unity. +Optional arguments WHITEPOINT and VIEW are the same as in `lcms-cam02-ucs', +which see. */) + (Lisp_Object color, Lisp_Object whitepoint, Lisp_Object view) +{ + cmsViewingConditions vc; + cmsJCh jch; + cmsCIEXYZ xyz, xyzw; + +#ifdef WINDOWSNT + if (!lcms_initialized) + lcms_initialized = init_lcms_functions (); + if (!lcms_initialized) + { + message1 ("lcms2 library not found"); + return Qnil; + } +#endif + + if (!(CONSP (color) && parse_xyz_list (color, &xyz))) + signal_error ("Invalid color", color); + if (NILP (whitepoint)) + xyzw = illuminant_d65; + else if (!(CONSP (whitepoint) && parse_xyz_list (whitepoint, &xyzw))) + signal_error ("Invalid white point", whitepoint); + if (NILP (view)) + default_viewing_conditions (&xyzw, &vc); + else if (!(CONSP (view) && parse_viewing_conditions (view, &xyzw, &vc))) + signal_error ("Invalid viewing conditions", view); + + xyz_to_jch(&xyz, &jch, &vc); + return list3 (make_float (jch.J), make_float (jch.C), make_float (jch.h)); +} + +DEFUN ("lcms-jch->xyz", Flcms_jch_to_xyz, Slcms_jch_to_xyz, 1, 3, 0, + doc: /* Convert CIE XYZ to CIE CAM02 JCh. +COLOR is a list (J C h), where lightness of white is equal to 100, and hue +is given in degrees. +Optional arguments WHITEPOINT and VIEW are the same as in `lcms-cam02-ucs', +which see. */) + (Lisp_Object color, Lisp_Object whitepoint, Lisp_Object view) +{ + cmsViewingConditions vc; + cmsJCh jch; + cmsCIEXYZ xyz, xyzw; + +#ifdef WINDOWSNT + if (!lcms_initialized) + lcms_initialized = init_lcms_functions (); + if (!lcms_initialized) + { + message1 ("lcms2 library not found"); + return Qnil; + } +#endif + + if (!(CONSP (color) && parse_jch_list (color, &jch))) + signal_error ("Invalid color", color); + if (NILP (whitepoint)) + xyzw = illuminant_d65; + else if (!(CONSP (whitepoint) && parse_xyz_list (whitepoint, &xyzw))) + signal_error ("Invalid white point", whitepoint); + if (NILP (view)) + default_viewing_conditions (&xyzw, &vc); + else if (!(CONSP (view) && parse_viewing_conditions (view, &xyzw, &vc))) + signal_error ("Invalid viewing conditions", view); + + jch_to_xyz(&jch, &xyz, &vc); + return list3 (make_float (xyz.X / 100.0), + make_float (xyz.Y / 100.0), + make_float (xyz.Z / 100.0)); +} + +DEFUN ("lcms-jch->jab", Flcms_jch_to_jab, Slcms_jch_to_jab, 1, 3, 0, + doc: /* Convert CIE CAM02 JCh to CAM02-UCS J'a'b'. +COLOR is a list (J C h) as described in `lcms-jch->xyz', which see. +Optional arguments WHITEPOINT and VIEW are the same as in `lcms-cam02-ucs', +which see. */) + (Lisp_Object color, Lisp_Object whitepoint, Lisp_Object view) +{ + cmsViewingConditions vc; + lcmsJab_t jab; + cmsJCh jch; + cmsCIEXYZ xyzw; + double FL, k, k4; + +#ifdef WINDOWSNT + if (!lcms_initialized) + lcms_initialized = init_lcms_functions (); + if (!lcms_initialized) + { + message1 ("lcms2 library not found"); + return Qnil; + } +#endif + + if (!(CONSP (color) && parse_jch_list (color, &jch))) + signal_error ("Invalid color", color); + if (NILP (whitepoint)) + xyzw = illuminant_d65; + else if (!(CONSP (whitepoint) && parse_xyz_list (whitepoint, &xyzw))) + signal_error ("Invalid white point", whitepoint); + if (NILP (view)) + default_viewing_conditions (&xyzw, &vc); + else if (!(CONSP (view) && parse_viewing_conditions (view, &xyzw, &vc))) + signal_error ("Invalid viewing conditions", view); + + k = 1.0 / (1.0 + (5.0 * vc.La)); + k4 = k * k * k * k; + FL = vc.La * k4 + 0.1 * (1 - k4) * (1 - k4) * cbrt (5.0 * vc.La); + jch_to_jab (&jch, &jab, FL, 0.007, 0.0228); + return list3 (make_float (jab.J), make_float (jab.a), make_float (jab.b)); +} + +DEFUN ("lcms-jab->jch", Flcms_jab_to_jch, Slcms_jab_to_jch, 1, 3, 0, + doc: /* Convert CAM02-UCS J'a'b' to CIE CAM02 JCh. +COLOR is a list (J' a' b'), where white corresponds to lightness J equal to 100. +Optional arguments WHITEPOINT and VIEW are the same as in `lcms-cam02-ucs', +which see. */) + (Lisp_Object color, Lisp_Object whitepoint, Lisp_Object view) +{ + cmsViewingConditions vc; + cmsJCh jch; + lcmsJab_t jab; + cmsCIEXYZ xyzw; + double FL, k, k4; + +#ifdef WINDOWSNT + if (!lcms_initialized) + lcms_initialized = init_lcms_functions (); + if (!lcms_initialized) + { + message1 ("lcms2 library not found"); + return Qnil; + } +#endif + + if (!(CONSP (color) && parse_jab_list (color, &jab))) + signal_error ("Invalid color", color); + if (NILP (whitepoint)) + xyzw = illuminant_d65; + else if (!(CONSP (whitepoint) && parse_xyz_list (whitepoint, &xyzw))) + signal_error ("Invalid white point", whitepoint); + if (NILP (view)) + default_viewing_conditions (&xyzw, &vc); + else if (!(CONSP (view) && parse_viewing_conditions (view, &xyzw, &vc))) + signal_error ("Invalid viewing conditions", view); + + k = 1.0 / (1.0 + (5.0 * vc.La)); + k4 = k * k * k * k; + FL = vc.La * k4 + 0.1 * (1 - k4) * (1 - k4) * cbrt (5.0 * vc.La); + jab_to_jch (&jab, &jch, FL, 0.007, 0.0228); + return list3 (make_float (jch.J), make_float (jch.C), make_float (jch.h)); +} + /* References: Li, Luo et al. "The CRI-CAM02UCS colour rendering index." COLOR research and application, 37 No.3, 2012. @@ -239,10 +495,9 @@ The default viewing conditions are (20 100 1 1). */) { cmsViewingConditions vc; cmsJCh jch1, jch2; - cmsHANDLE h1, h2; cmsCIEXYZ xyz1, xyz2, xyzw; - double Jp1, ap1, bp1, Jp2, ap2, bp2; - double Mp1, Mp2, FL, k, k4; + lcmsJab_t jab1, jab2; + double FL, k, k4; #ifdef WINDOWSNT if (!lcms_initialized) @@ -267,41 +522,17 @@ The default viewing conditions are (20 100 1 1). */) else if (!(CONSP (view) && parse_viewing_conditions (view, &xyzw, &vc))) signal_error ("Invalid view conditions", view); - h1 = cmsCIECAM02Init (0, &vc); - h2 = cmsCIECAM02Init (0, &vc); - cmsCIECAM02Forward (h1, &xyz1, &jch1); - cmsCIECAM02Forward (h2, &xyz2, &jch2); - cmsCIECAM02Done (h1); - cmsCIECAM02Done (h2); + xyz_to_jch (&xyz1, &jch1, &vc); + xyz_to_jch (&xyz2, &jch2, &vc); - /* Now have colors in JCh, need to calculate J'a'b' - - M = C * F_L^0.25 - J' = 1.7 J / (1 + 0.007 J) - M' = 43.86 ln(1 + 0.0228 M) - a' = M' cos(h) - b' = M' sin(h) - - where - - F_L = 0.2 k^4 (5 L_A) + 0.1 (1 - k^4)^2 (5 L_A)^(1/3), - k = 1/(5 L_A + 1) - */ k = 1.0 / (1.0 + (5.0 * vc.La)); k4 = k * k * k * k; FL = vc.La * k4 + 0.1 * (1 - k4) * (1 - k4) * cbrt (5.0 * vc.La); - Mp1 = 43.86 * log (1.0 + 0.0228 * (jch1.C * sqrt (sqrt (FL)))); - Mp2 = 43.86 * log (1.0 + 0.0228 * (jch2.C * sqrt (sqrt (FL)))); - Jp1 = 1.7 * jch1.J / (1.0 + (0.007 * jch1.J)); - Jp2 = 1.7 * jch2.J / (1.0 + (0.007 * jch2.J)); - ap1 = Mp1 * cos (deg2rad (jch1.h)); - ap2 = Mp2 * cos (deg2rad (jch2.h)); - bp1 = Mp1 * sin (deg2rad (jch1.h)); - bp2 = Mp2 * sin (deg2rad (jch2.h)); - - return make_float (sqrt ((Jp2 - Jp1) * (Jp2 - Jp1) + - (ap2 - ap1) * (ap2 - ap1) + - (bp2 - bp1) * (bp2 - bp1))); + jch_to_jab (&jch1, &jab1, FL, 0.007, 0.0228); + jch_to_jab (&jch2, &jab2, FL, 0.007, 0.0228); + + return make_float (hypot (jab2.J - jab1.J, + hypot (jab2.a - jab1.a, jab2.b - jab1.b))); } DEFUN ("lcms-temp->white-point", Flcms_temp_to_white_point, Slcms_temp_to_white_point, 1, 1, 0, @@ -359,6 +590,10 @@ void syms_of_lcms2 (void) { defsubr (&Slcms_cie_de2000); + defsubr (&Slcms_xyz_to_jch); + defsubr (&Slcms_jch_to_xyz); + defsubr (&Slcms_jch_to_jab); + defsubr (&Slcms_jab_to_jch); defsubr (&Slcms_cam02_ucs); defsubr (&Slcms2_available_p); defsubr (&Slcms_temp_to_white_point); diff --git a/test/src/lcms-tests.el b/test/src/lcms-tests.el index d6d1d16b9ad..cc324af68ba 100644 --- a/test/src/lcms-tests.el +++ b/test/src/lcms-tests.el @@ -94,6 +94,38 @@ B is considered the exact value." (apply #'color-xyz-to-xyy (lcms-temp->white-point 7504)) '(0.29902 0.31485 1.0)))) +(ert-deftest lcms-roundtrip () + "Test accuracy of converting to and from different color spaces" + (skip-unless (featurep 'lcms2)) + (should + (let ((color '(.5 .3 .7))) + (lcms-triple-approx-p (lcms-jch->xyz (lcms-xyz->jch color)) + color + 0.0001))) + (should + (let ((color '(.8 -.2 .2))) + (lcms-triple-approx-p (lcms-jch->jab (lcms-jab->jch color)) + color + 0.0001)))) + +(ert-deftest lcms-ciecam02-gold () + "Test CIE CAM02 JCh gold values" + (skip-unless (featurep 'lcms2)) + (should + (lcms-triple-approx-p + (lcms-xyz->jch '(0.1931 0.2393 0.1014) + '(0.9888 0.900 0.3203) + '(18 200 1 1.0)) + '(48.0314 38.7789 191.0452) + 0.02)) + (should + (lcms-triple-approx-p + (lcms-xyz->jch '(0.1931 0.2393 0.1014) + '(0.9888 0.90 0.3203) + '(18 20 1 1.0)) + '(47.6856 36.0527 185.3445) + 0.09))) + (ert-deftest lcms-dE-cam02-ucs-silver () "Test CRI-CAM02-UCS deltaE metric values from colorspacious." (skip-unless (featurep 'lcms2)) @@ -114,4 +146,16 @@ B is considered the exact value." 8.503323264883667 0.04))) +(ert-deftest lcms-jmh->cam02-ucs-silver () + "Compare JCh conversion to CAM02-UCS to values from colorspacious." + (skip-unless (featurep 'lcms2)) + (should + (lcms-triple-approx-p (lcms-jch->jab '(50 20 10)) + '(62.96296296 16.22742674 2.86133316) + 0.05)) + (should + (lcms-triple-approx-p (lcms-jch->jab '(10 60 100)) + '(15.88785047 -6.56546789 37.23461867) + 0.04))) + ;;; lcms-tests.el ends here -- 2.39.2