gtaylor / python-colormath

A python module that abstracts common color math operations. For example, converting from CIE L*a*b to XYZ, or from RGB to CMYK
python-colormath.readthedocs.org
BSD 3-Clause "New" or "Revised" License
456 stars 83 forks source link

LabColor object behaves differently when returned by convert_color than when constructed. #98

Closed philipwilson closed 5 years ago

philipwilson commented 5 years ago

If I convert_colour a CYMKColor object to LabColor then back again, I get my starting CMYK values, modulo floating point noise. But if I construct a new LabColor object using the Lab* values from that conversion, then convert_color it back to CMYK, the CMYK is wrong.

There's a demo here: https://github.com/philipwilson/colormath-bug

Here's my recipe:

>>> from colormath.color_objects import LabColor, XYZColor, sRGBColor, CMYKColor
>>> from colormath.color_conversions import convert_color
>>> c = CMYKColor(0.75, 0.0, 0.75, 0.2)
>>> c
CMYKColor(cmyk_c=0.75,cmyk_m=0.0,cmyk_y=0.75,cmyk_k=0.2)
>>> l = convert_color(c, LabColor)
>>> l
LabColor(lab_l=72.31273945504653,lab_a=-66.59723871927287,lab_b=60.81823869993287)
>>> new_c = convert_color(l, CMYKColor)
>>> new_c
CMYKColor(cmyk_c=0.7499998329730001,cmyk_m=0.0,cmyk_y=0.749999613025276,cmyk_k=0.20000142321528624)
>>> constructor = repr(l)
>>> constructor
'LabColor(lab_l=72.31273945504653,lab_a=-66.59723871927287,lab_b=60.81823869993287)'
>>> new_l = eval(constructor)
>>> new_l
LabColor(lab_l=72.31273945504653,lab_a=-66.59723871927287,lab_b=60.81823869993287)
>>> new2_c = convert_color(new_l, CMYKColor)
>>> new2_c
CMYKColor(cmyk_c=1.0,cmyk_m=0.0,cmyk_y=0.7777262206328267,cmyk_k=0.1938677926080944)

Note the cyan value has jumped from 0.75 to 1.0!

KelSolaar commented 5 years ago

Interestingly if you don't use eval, which you should not really, there is no such issue:

print(repr(convert_color(convert_color(CMYKColor(0.75, 0.0, 0.75, 0.2), LabColor), CMYKColor)))
CMYKColor(cmyk_c=0.7499998329730001,cmyk_m=0.0,cmyk_y=0.749999613025276,cmyk_k=0.20000142321528624)
KelSolaar commented 5 years ago

Not quite actually, defining the colour manually, e.g. new_l = LabColor(lab_l=72.31273945504653,lab_a=-66.59723871927287,lab_b=60.81823869993287) generates the problem. The illuminants are actually different:

print(l, l.observer, l.illuminant)
print(new_l, new_l.observer, new_l.illuminant)
(LabColor(lab_l=72.31273945504653,lab_a=-66.59723871927287,lab_b=60.81823869993287), '2', 'd65')
(LabColor(lab_l=72.31273945504653,lab_a=-66.59723871927287,lab_b=60.81823869993287), '2', 'd50')
KelSolaar commented 5 years ago

The reason as I suspected it is because the conversion path from CMYK to CIE Lab goes through the sRGB colourspace which uses D65 while the CIE Lab colours are instantiated with D50 by default:

('Converting %s to %s', LabColor(lab_l=72.31273945504653,lab_a=-66.59723871927287,lab_b=60.81823869993287), <class 'colormath.color_objects.CMYKColor'>)
(' @ Conversion path: %s', [<function Lab_to_XYZ at 0xa121aad70>, <function XYZ_to_RGB at 0xa121b3230>, <function RGB_to_CMY at 0xa121b35f0>, <function CMY_to_CMYK at 0xa121b36e0>])
(' * Conversion: %s passed to %s()', 'LabColor', <function Lab_to_XYZ at 0xa121aad70>)
(' |->  in %s', LabColor(lab_l=72.31273945504653,lab_a=-66.59723871927287,lab_b=60.81823869993287))
(' |-< out %s', XYZColor(xyz_x=0.23895088974480086,xyz_y=0.44126156667878313,xyz_z=0.07887799933599972))
(' * Conversion: %s passed to %s()', 'XYZColor', <function XYZ_to_RGB at 0xa121b3230>)
(' |->  in %s', XYZColor(xyz_x=0.23895088974480086,xyz_y=0.44126156667878313,xyz_z=0.07887799933599972))
('  \\- Target RGB space: %s', <class 'colormath.color_objects.sRGBColor'>)
('  \\- Target native illuminant: %s', 'd65')
("  \\- XYZ color's illuminant: %s", 'd50')
('  \\* Applying transformation from %s to %s ', 'd50', 'd65')
('  \\*   New values: %.3f, %.3f, %.3f', 0.22315172597947527, 0.4405456503646499, 0.09880091224360746)
('  \\* Applying RGB conversion matrix: %s->%s', 'type', 'xyz_to_rgb')
(' |-< out %s', sRGBColor(rgb_r=0.0,rgb_g=0.8061322073919057,rgb_b=0.17918205240660026))
(' * Conversion: %s passed to %s()', 'sRGBColor', <function RGB_to_CMY at 0xa121b35f0>)
(' |->  in %s', sRGBColor(rgb_r=0.0,rgb_g=0.8061322073919057,rgb_b=0.17918205240660026))
(' |-< out %s', CMYColor(cmy_c=1.0,cmy_m=0.19386779260809428,cmy_y=0.8208179475933998))
(' * Conversion: %s passed to %s()', 'CMYColor', <function CMY_to_CMYK at 0xa121b36e0>)
(' |->  in %s', CMYColor(cmy_c=1.0,cmy_m=0.19386779260809428,cmy_y=0.8208179475933998))
(' |-< out %s', CMYKColor(cmyk_c=1.0,cmyk_m=0.0,cmyk_y=0.7777262206328275,cmyk_k=0.19386779260809428))

For now, you would need to explicitly instantiate with D65.

I will look at adding the observer and illuminant to the string representation and it should hopefully sort the problem.

KelSolaar commented 5 years ago

@philipwilson : It should be fixed as per the above PR:

CMYKColor(cmyk_c=0.75, cmyk_m=0.0, cmyk_y=0.75, cmyk_k=0.2)
LabColor(lab_l=72.31273945504653, lab_a=-66.59723871927287, lab_b=60.81823869993287, observer='2', illuminant='d65')
LabColor(lab_l=72.31273945504653, lab_a=-66.59723871927287, lab_b=60.81823869993287, observer='2', illuminant='d65')
CMYKColor(cmyk_c=0.7499998329730001, cmyk_m=0.0, cmyk_y=0.749999613025276, cmyk_k=0.20000142321528624)
philipwilson commented 5 years ago

Thanks for the fix @KelSolaar