1
0
mirror of https://git.savannah.gnu.org/git/emacs.git synced 2024-11-24 07:20:37 +00:00

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.
This commit is contained in:
Mark Oteiza 2017-09-26 17:13:36 -04:00
parent 157007b58e
commit 645ff6c702
3 changed files with 314 additions and 34 deletions

View File

@ -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 top of that library. The new configure option '--without-lcms2' can
be used to build without lcms2 support even if it is installed. Emacs be used to build without lcms2 support even if it is installed. Emacs
linked to Little CMS exposes color management functions in Lisp: the 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', ** The configure option '--with-gameuser' now defaults to 'no',
as this appears to be the most common configuration in practice. as this appears to be the most common configuration in practice.

View File

@ -25,6 +25,13 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
#include "lisp.h" #include "lisp.h"
typedef struct
{
double J;
double a;
double b;
} lcmsJab_t;
#ifdef WINDOWSNT #ifdef WINDOWSNT
# include <windows.h> # include <windows.h>
# include "w32.h" # include "w32.h"
@ -36,6 +43,8 @@ DEF_DLL_FN (cmsHANDLE, cmsCIECAM02Init,
(cmsContext ContextID, const cmsViewingConditions* pVC)); (cmsContext ContextID, const cmsViewingConditions* pVC));
DEF_DLL_FN (void, cmsCIECAM02Forward, DEF_DLL_FN (void, cmsCIECAM02Forward,
(cmsHANDLE hModel, const cmsCIEXYZ* pIn, cmsJCh* pOut)); (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 (void, cmsCIECAM02Done, (cmsHANDLE hModel));
DEF_DLL_FN (cmsBool, cmsWhitePointFromTemp, DEF_DLL_FN (cmsBool, cmsWhitePointFromTemp,
(cmsCIExyY* WhitePoint, cmsFloat64Number TempK)); (cmsCIExyY* WhitePoint, cmsFloat64Number TempK));
@ -54,6 +63,7 @@ init_lcms_functions (void)
LOAD_DLL_FN (library, cmsCIE2000DeltaE); LOAD_DLL_FN (library, cmsCIE2000DeltaE);
LOAD_DLL_FN (library, cmsCIECAM02Init); LOAD_DLL_FN (library, cmsCIECAM02Init);
LOAD_DLL_FN (library, cmsCIECAM02Forward); LOAD_DLL_FN (library, cmsCIECAM02Forward);
LOAD_DLL_FN (library, cmsCIECAM02Reverse);
LOAD_DLL_FN (library, cmsCIECAM02Done); LOAD_DLL_FN (library, cmsCIECAM02Done);
LOAD_DLL_FN (library, cmsWhitePointFromTemp); LOAD_DLL_FN (library, cmsWhitePointFromTemp);
LOAD_DLL_FN (library, cmsxyY2XYZ); LOAD_DLL_FN (library, cmsxyY2XYZ);
@ -63,6 +73,7 @@ init_lcms_functions (void)
# undef cmsCIE2000DeltaE # undef cmsCIE2000DeltaE
# undef cmsCIECAM02Init # undef cmsCIECAM02Init
# undef cmsCIECAM02Forward # undef cmsCIECAM02Forward
# undef cmsCIECAM02Reverse
# undef cmsCIECAM02Done # undef cmsCIECAM02Done
# undef cmsWhitePointFromTemp # undef cmsWhitePointFromTemp
# undef cmsxyY2XYZ # undef cmsxyY2XYZ
@ -70,6 +81,7 @@ init_lcms_functions (void)
# define cmsCIE2000DeltaE fn_cmsCIE2000DeltaE # define cmsCIE2000DeltaE fn_cmsCIE2000DeltaE
# define cmsCIECAM02Init fn_cmsCIECAM02Init # define cmsCIECAM02Init fn_cmsCIECAM02Init
# define cmsCIECAM02Forward fn_cmsCIECAM02Forward # define cmsCIECAM02Forward fn_cmsCIECAM02Forward
# define cmsCIECAM02Reverse fn_cmsCIECAM02Reverse
# define cmsCIECAM02Done fn_cmsCIECAM02Done # define cmsCIECAM02Done fn_cmsCIECAM02Done
# define cmsWhitePointFromTemp fn_cmsWhitePointFromTemp # define cmsWhitePointFromTemp fn_cmsWhitePointFromTemp
# define cmsxyY2XYZ fn_cmsxyY2XYZ # define cmsxyY2XYZ fn_cmsxyY2XYZ
@ -145,6 +157,12 @@ deg2rad (double degrees)
return M_PI * degrees / 180.0; 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 cmsCIEXYZ illuminant_d65 = { .X = 95.0455, .Y = 100.0, .Z = 108.8753 };
static void static void
@ -180,6 +198,46 @@ parse_xyz_list (Lisp_Object xyz_list, cmsCIEXYZ *color)
return true; 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 static bool
parse_viewing_conditions (Lisp_Object view, const cmsCIEXYZ *wp, parse_viewing_conditions (Lisp_Object view, const cmsCIEXYZ *wp,
cmsViewingConditions *vc) cmsViewingConditions *vc)
@ -216,6 +274,204 @@ parse_viewing_conditions (Lisp_Object view, const cmsCIEXYZ *wp,
return true; 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: /* References:
Li, Luo et al. "The CRI-CAM02UCS colour rendering index." COLOR research Li, Luo et al. "The CRI-CAM02UCS colour rendering index." COLOR research
and application, 37 No.3, 2012. and application, 37 No.3, 2012.
@ -239,10 +495,9 @@ The default viewing conditions are (20 100 1 1). */)
{ {
cmsViewingConditions vc; cmsViewingConditions vc;
cmsJCh jch1, jch2; cmsJCh jch1, jch2;
cmsHANDLE h1, h2;
cmsCIEXYZ xyz1, xyz2, xyzw; cmsCIEXYZ xyz1, xyz2, xyzw;
double Jp1, ap1, bp1, Jp2, ap2, bp2; lcmsJab_t jab1, jab2;
double Mp1, Mp2, FL, k, k4; double FL, k, k4;
#ifdef WINDOWSNT #ifdef WINDOWSNT
if (!lcms_initialized) 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))) else if (!(CONSP (view) && parse_viewing_conditions (view, &xyzw, &vc)))
signal_error ("Invalid view conditions", view); signal_error ("Invalid view conditions", view);
h1 = cmsCIECAM02Init (0, &vc); xyz_to_jch (&xyz1, &jch1, &vc);
h2 = cmsCIECAM02Init (0, &vc); xyz_to_jch (&xyz2, &jch2, &vc);
cmsCIECAM02Forward (h1, &xyz1, &jch1);
cmsCIECAM02Forward (h2, &xyz2, &jch2);
cmsCIECAM02Done (h1);
cmsCIECAM02Done (h2);
/* 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)); k = 1.0 / (1.0 + (5.0 * vc.La));
k4 = k * k * k * k; k4 = k * k * k * k;
FL = vc.La * k4 + 0.1 * (1 - k4) * (1 - k4) * cbrt (5.0 * vc.La); 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)))); jch_to_jab (&jch1, &jab1, FL, 0.007, 0.0228);
Mp2 = 43.86 * log (1.0 + 0.0228 * (jch2.C * sqrt (sqrt (FL)))); jch_to_jab (&jch2, &jab2, FL, 0.007, 0.0228);
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) + return make_float (hypot (jab2.J - jab1.J,
(ap2 - ap1) * (ap2 - ap1) + hypot (jab2.a - jab1.a, jab2.b - jab1.b)));
(bp2 - bp1) * (bp2 - bp1)));
} }
DEFUN ("lcms-temp->white-point", Flcms_temp_to_white_point, Slcms_temp_to_white_point, 1, 1, 0, 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) syms_of_lcms2 (void)
{ {
defsubr (&Slcms_cie_de2000); 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 (&Slcms_cam02_ucs);
defsubr (&Slcms2_available_p); defsubr (&Slcms2_available_p);
defsubr (&Slcms_temp_to_white_point); defsubr (&Slcms_temp_to_white_point);

View File

@ -94,6 +94,38 @@ B is considered the exact value."
(apply #'color-xyz-to-xyy (lcms-temp->white-point 7504)) (apply #'color-xyz-to-xyy (lcms-temp->white-point 7504))
'(0.29902 0.31485 1.0)))) '(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 () (ert-deftest lcms-dE-cam02-ucs-silver ()
"Test CRI-CAM02-UCS deltaE metric values from colorspacious." "Test CRI-CAM02-UCS deltaE metric values from colorspacious."
(skip-unless (featurep 'lcms2)) (skip-unless (featurep 'lcms2))
@ -114,4 +146,16 @@ B is considered the exact value."
8.503323264883667 8.503323264883667
0.04))) 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 ;;; lcms-tests.el ends here