Skip to content

Commit

Permalink
ENH: add additional color illuminants
Browse files Browse the repository at this point in the history
remove use of fetch from colorconv tests

corresponds to scikit-image/scikit-image#5308
  • Loading branch information
grlee77 committed Jan 17, 2022
1 parent 5e579ce commit 83f3bc8
Show file tree
Hide file tree
Showing 44 changed files with 101 additions and 73 deletions.
85 changes: 52 additions & 33 deletions python/cucim/src/cucim/skimage/color/colorconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,12 +523,15 @@ def hsv2rgb(hsv, *, channel_axis=-1):
# XYZ coordinates of the illuminants, scaled to [0, 1]. For each illuminant I
# we have:
#
# illuminant[I][0] corresponds to the XYZ coordinates for the 2 degree
# illuminant[I]['2'] corresponds to the XYZ coordinates for the 2 degree
# field of view.
#
# illuminant[I][1] corresponds to the XYZ coordinates for the 10 degree
# illuminant[I]['10'] corresponds to the XYZ coordinates for the 10 degree
# field of view.
#
# illuminant[I]['R'] corresponds to the XYZ coordinates for R illuminants
# in grDevices::convertColor
#
# The XYZ coordinates are calculated from [1], using the formula:
#
# X = x * ( Y / y )
Expand All @@ -545,28 +548,41 @@ def hsv2rgb(hsv, *, channel_axis=-1):

illuminants = \
{"A": {'2': (1.098466069456375, 1, 0.3558228003436005),
'10': (1.111420406956693, 1, 0.3519978321919493)},
'10': (1.111420406956693, 1, 0.3519978321919493),
'R': (1.098466069456375, 1, 0.3558228003436005)},
"B": {'2': (0.9909274480248003, 1, 0.8531327322886154),
'10': (0.9917777147717607, 1, 0.8434930535866175),
'R': (0.9909274480248003, 1, 0.8531327322886154)},
"C": {'2': (0.980705971659919, 1, 1.1822494939271255),
'10': (0.9728569189782166, 1, 1.1614480488951577),
'R': (0.980705971659919, 1, 1.1822494939271255)},
"D50": {'2': (0.9642119944211994, 1, 0.8251882845188288),
'10': (0.9672062750333777, 1, 0.8142801513128616)},
'10': (0.9672062750333777, 1, 0.8142801513128616),
'R': (0.9639501491621826, 1, 0.8241280285499208)},
"D55": {'2': (0.956797052643698, 1, 0.9214805860173273),
'10': (0.9579665682254781, 1, 0.9092525159847462)},
'10': (0.9579665682254781, 1, 0.9092525159847462),
'R': (0.9565317453467969, 1, 0.9202554587037198)},
"D65": {'2': (0.95047, 1., 1.08883), # This was: `lab_ref_white`
'10': (0.94809667673716, 1, 1.0730513595166162)},
'10': (0.94809667673716, 1, 1.0730513595166162),
'R': (0.9532057125493769, 1, 1.0853843816469158)},
"D75": {'2': (0.9497220898840717, 1, 1.226393520724154),
'10': (0.9441713925645873, 1, 1.2064272211720228)},
'10': (0.9441713925645873, 1, 1.2064272211720228),
'R': (0.9497220898840717, 1, 1.226393520724154)},
"E": {'2': (1.0, 1.0, 1.0),
'10': (1.0, 1.0, 1.0)}}
'10': (1.0, 1.0, 1.0),
'R': (1.0, 1.0, 1.0)}}


def get_xyz_coords(illuminant, observer):
"""Get the XYZ coordinates of the given illuminant and observer [1]_.
Parameters
----------
illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional
illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional
The name of the illuminant (the function is NOT case sensitive).
observer : {"2", "10"}, optional
The aperture angle of the observer.
observer : {"2", "10", "R"}, optional
One of: 2-degree observer, 10-degree observer, or 'R' observer as in
R function grDevices::convertColor.
dtype: dtype, optional
Output data type.
Expand All @@ -587,15 +603,17 @@ def get_xyz_coords(illuminant, observer):
.. [1] https://en.wikipedia.org/wiki/Standard_illuminant
"""
illuminant = illuminant.upper()
observer = observer.upper()
try:
return illuminants[illuminant][observer]
except KeyError:
raise ValueError("Unknown illuminant/observer combination\
(\'{0}\', \'{1}\')".format(illuminant, observer))
raise ValueError(f'Unknown illuminant/observer combination '
f'(`{illuminant}`, `{observer}`)')


# Haematoxylin-Eosin-DAB colorspace
# From original Ruifrok's paper: A. C. Ruifrok and D. A. Johnston,
# "Quantification of histochemical staining by color deconvolution.,"
# "Quantification of histochemical staining by color deconvolution,"
# Analytical and quantitative cytology and histology / the International
# Academy of Cytology [and] American Society of Cytology, vol. 23, no. 4,
# pp. 291-9, Aug. 2001.
Expand Down Expand Up @@ -1087,10 +1105,11 @@ def xyz2lab(xyz, illuminant="D65", observer="2", *, channel_axis=-1):
xyz : (..., 3, ...) array_like
The image in XYZ format. By default, the final dimension denotes
channels.
illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional
illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional
The name of the illuminant (the function is NOT case sensitive).
observer : {"2", "10"}, optional
The aperture angle of the observer.
observer : {"2", "10", "R"}, optional
One of: 2-degree observer, 10-degree observer, or 'R' observer as in
R function grDevices::convertColor.
channel_axis : int, optional
This parameter indicates which axis of the array corresponds to
channels.
Expand All @@ -1110,7 +1129,7 @@ def xyz2lab(xyz, illuminant="D65", observer="2", *, channel_axis=-1):
Notes
-----
By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values
By default Observer="2", Illuminant="D65". CIE XYZ tristimulus values
x_ref=95.047, y_ref=100., z_ref=108.883. See function `get_xyz_coords` for
a list of supported illuminants.
Expand Down Expand Up @@ -1189,9 +1208,9 @@ def lab2xyz(lab, illuminant="D65", observer="2", *, channel_axis=-1):
lab : (..., 3, ...) array_like
The image in Lab format. By default, the final dimension denotes
channels.
illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional
illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional
The name of the illuminant (the function is NOT case sensitive).
observer : {"2", "10"}, optional
observer : {"2", "10", "R"}, optional
The aperture angle of the observer.
channel_axis : int, optional
This parameter indicates which axis of the array corresponds to
Expand All @@ -1214,7 +1233,7 @@ def lab2xyz(lab, illuminant="D65", observer="2", *, channel_axis=-1):
Notes
-----
By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values x_ref
By default Observer="2", Illuminant="D65". CIE XYZ tristimulus values x_ref
= 95.047, y_ref = 100., z_ref = 108.883. See function 'get_xyz_coords' for
a list of supported illuminants.
Expand Down Expand Up @@ -1254,9 +1273,9 @@ def rgb2lab(rgb, illuminant="D65", observer="2", *, channel_axis=-1):
rgb : (..., 3, ...) array_like
The image in RGB format. By default, the final dimension denotes
channels.
illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional
illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional
The name of the illuminant (the function is NOT case sensitive).
observer : {"2", "10"}, optional
observer : {"2", "10", "R"}, optional
The aperture angle of the observer.
channel_axis : int, optional
This parameter indicates which axis of the array corresponds to
Expand All @@ -1279,7 +1298,7 @@ def rgb2lab(rgb, illuminant="D65", observer="2", *, channel_axis=-1):
space.
This function uses rgb2xyz and xyz2lab.
By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values
By default Observer="2", Illuminant="D65". CIE XYZ tristimulus values
x_ref=95.047, y_ref=100., z_ref=108.883. See function `get_xyz_coords` for
a list of supported illuminants.
Expand All @@ -1296,12 +1315,12 @@ def lab2rgb(lab, illuminant="D65", observer="2", *, channel_axis=-1):
Parameters
----------
rgb : (..., 3, ...) array_like
The image in RGB format. By default, the final dimension denotes
lab : (..., 3, ...) array_like
The image in Lab format. By default, the final dimension denotes
channels.
illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional
illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional
The name of the illuminant (the function is NOT case sensitive).
observer : {"2", "10"}, optional
observer : {"2", "10", "R"}, optional
The aperture angle of the observer.
channel_axis : int, optional
This parameter indicates which axis of the array corresponds to
Expand All @@ -1320,7 +1339,7 @@ def lab2rgb(lab, illuminant="D65", observer="2", *, channel_axis=-1):
Notes
-----
This function uses lab2xyz and xyz2rgb.
By default Observer= 2A, Illuminant= D65. CIE XYZ tristimulus values
By default Observer="2", Illuminant="D65". CIE XYZ tristimulus values
x_ref=95.047, y_ref=100., z_ref=108.883. See function `get_xyz_coords` for
a list of supported illuminants.
Expand Down Expand Up @@ -1391,9 +1410,9 @@ def xyz2luv(xyz, illuminant="D65", observer="2", *, channel_axis=-1):
xyz : (..., 3, ...) array_like
The image in XYZ format. By default, the final dimension denotes
channels.
illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional
illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional
The name of the illuminant (the function is NOT case sensitive).
observer : {"2", "10"}, optional
observer : {"2", "10", "R"}, optional
The aperture angle of the observer.
channel_axis : int, optional
This parameter indicates which axis of the array corresponds to
Expand Down Expand Up @@ -1495,9 +1514,9 @@ def luv2xyz(luv, illuminant="D65", observer="2", *, channel_axis=-1):
luv : (..., 3, ...) array_like
The image in CIE-Luv format. By default, the final dimension denotes
channels.
illuminant : {"A", "D50", "D55", "D65", "D75", "E"}, optional
illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional
The name of the illuminant (the function is NOT case sensitive).
observer : {"2", "10"}, optional
observer : {"2", "10", "R"}, optional
The aperture angle of the observer.
channel_axis : int, optional
This parameter indicates which axis of the array corresponds to
Expand Down
Binary file not shown.
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Empty file.
Binary file not shown.
Empty file.
Empty file.
Binary file not shown.
Empty file.
Empty file.
Binary file not shown.
Empty file.
Empty file.
Empty file.
Binary file not shown.
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Empty file.
Empty file.
Binary file not shown.
Empty file.
Empty file.
Binary file not shown.
Empty file.
Empty file.
Binary file not shown.
Empty file.
Empty file.
Empty file.
89 changes: 49 additions & 40 deletions python/cucim/src/cucim/skimage/color/tests/test_colorconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

import colorsys
import os

import cupy as cp
import numpy as np
Expand All @@ -18,7 +19,6 @@
from skimage import data

from cucim.skimage._shared._warnings import expected_warnings
from cucim.skimage._shared.testing import fetch
from cucim.skimage._shared.utils import _supported_float_type, slice_at_axis
from cucim.skimage.color import (combine_stains, convert_colorspace, gray2rgb,
gray2rgba, hed2rgb, hsv2rgb, lab2lch, lab2rgb,
Expand All @@ -31,6 +31,8 @@
yuv2rgb)
from cucim.skimage.util import img_as_float, img_as_float32, img_as_ubyte

data_dir = os.path.join(os.path.dirname(__file__), 'data')


class TestColorconv():

Expand Down Expand Up @@ -396,17 +398,19 @@ def test_xyz2lab(self):
self.lab_array, decimal=3)

# Test the conversion with the rest of the illuminants.
for i in ["d50", "d55", "d65", "d75"]:
for obs in ["2", "10"]:
fname = "color/tests/data/lab_array_{0}_{1}.npy".format(i, obs)
lab_array_I_obs = np.load(fetch(fname))
assert_array_almost_equal(lab_array_I_obs,
for i in ["A", "B", "C", "d50", "d55", "d65"]:
i = i.lower()
for obs in ["2", "10", "R"]:
obs = obs.lower()
fname = os.path.join(data_dir, f'lab_array_{i}_{obs}.npy')
lab_array_i_obs = np.load(fname)
assert_array_almost_equal(lab_array_i_obs,
xyz2lab(self.xyz_array, i, obs),
decimal=2)
for i in ["a", "e"]:
fname = "color/tests/data/lab_array_{0}_2.npy".format(i)
lab_array_I_obs = np.load(fetch(fname))
assert_array_almost_equal(lab_array_I_obs,
for i in ["d75", "e"]:
fname = os.path.join(data_dir, f'lab_array_{i}_2.npy')
lab_array_i_obs = np.load(fname)
assert_array_almost_equal(lab_array_i_obs,
xyz2lab(self.xyz_array, i, "2"),
decimal=2)

Expand All @@ -430,25 +434,26 @@ def test_lab2xyz(self):
self.xyz_array, decimal=3)

# Test the conversion with the rest of the illuminants.
for i in ["d50", "d55", "d65", "d75"]:
for obs in ["2", "10"]:
fname = "color/tests/data/lab_array_{0}_{1}.npy".format(i, obs)
lab_array_I_obs = cp.array(np.load(fetch(fname)))
assert_array_almost_equal(lab2xyz(lab_array_I_obs, i, obs),
for i in ["A", "B", "C", "d50", "d55", "d65"]:
i = i.lower()
for obs in ["2", "10", "R"]:
obs = obs.lower()
fname = os.path.join(data_dir, f'lab_array_{i}_{obs}.npy')
lab_array_i_obs = cp.array(np.load(fname))
assert_array_almost_equal(lab2xyz(lab_array_i_obs, i, obs),
self.xyz_array, decimal=3)
for i in ["a", "e"]:
fname = "lab_array_{0}_2.npy".format(i)
lab_array_I_obs = cp.array(
np.load(fetch('color/tests/data/' + fname)))
assert_array_almost_equal(lab2xyz(lab_array_I_obs, i, "2"),
for i in ["d75", "e"]:
fname = os.path.join(data_dir, f'lab_array_{i}_2.npy')
lab_array_i_obs = cp.array(np.load(fname))
assert_array_almost_equal(lab2xyz(lab_array_i_obs, i, "2"),
self.xyz_array, decimal=3)

# And we include a call to test the exception handling in the code.
with pytest.raises(ValueError):
lab2xyz(lab_array_I_obs, "NaI", "2") # Not an illuminant
lab2xyz(lab_array_i_obs, "NaI", "2") # Not an illuminant

with pytest.raises(ValueError):
lab2xyz(lab_array_I_obs, "d50", "42") # Not a degree
lab2xyz(lab_array_i_obs, "d50", "42") # Not a degree

@pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
def test_lab2xyz_channel_axis(self, channel_axis):
Expand Down Expand Up @@ -522,17 +527,19 @@ def test_xyz2luv(self):
self.luv_array, decimal=3)

# Test the conversion with the rest of the illuminants.
for i in ["d50", "d55", "d65", "d75"]:
for obs in ["2", "10"]:
fname = "color/tests/data/luv_array_{0}_{1}.npy".format(i, obs)
luv_array_I_obs = cp.array(np.load(fetch(fname)))
assert_array_almost_equal(luv_array_I_obs,
for i in ["A", "B", "C", "d50", "d55", "d65"]:
i = i.lower()
for obs in ["2", "10", "R"]:
obs = obs.lower()
fname = os.path.join(data_dir, f'luv_array_{i}_{obs}.npy')
luv_array_i_obs = np.load(fname)
assert_array_almost_equal(luv_array_i_obs,
xyz2luv(self.xyz_array, i, obs),
decimal=2)
for i in ["a", "e"]:
fname = "color/tests/data/luv_array_{0}_2.npy".format(i)
luv_array_I_obs = cp.array(np.load(fetch(fname)))
assert_array_almost_equal(luv_array_I_obs,
for i in ["d75", "e"]:
fname = os.path.join(data_dir, f'luv_array_{i}_2.npy')
luv_array_i_obs = np.load(fname)
assert_array_almost_equal(luv_array_i_obs,
xyz2luv(self.xyz_array, i, "2"),
decimal=2)

Expand All @@ -556,16 +563,18 @@ def test_luv2xyz(self):
self.xyz_array, decimal=3)

# Test the conversion with the rest of the illuminants.
for i in ["d50", "d55", "d65", "d75"]:
for obs in ["2", "10"]:
fname = "color/tests/data/luv_array_{0}_{1}.npy".format(i, obs)
luv_array_I_obs = cp.array(np.load(fetch(fname)))
assert_array_almost_equal(luv2xyz(luv_array_I_obs, i, obs),
for i in ["A", "B", "C", "d50", "d55", "d65"]:
i = i.lower()
for obs in ["2", "10", "R"]:
obs = obs.lower()
fname = os.path.join(data_dir, f'luv_array_{i}_{obs}.npy')
luv_array_i_obs = cp.array(np.load(fname))
assert_array_almost_equal(luv2xyz(luv_array_i_obs, i, obs),
self.xyz_array, decimal=3)
for i in ["a", "e"]:
fname = "color/tests/data/luv_array_{0}_2.npy".format(i)
luv_array_I_obs = cp.array(np.load(fetch(fname)))
assert_array_almost_equal(luv2xyz(luv_array_I_obs, i, "2"),
for i in ["d75", "e"]:
fname = os.path.join(data_dir, f'luv_array_{i}_2.npy')
luv_array_i_obs = cp.array(np.load(fname))
assert_array_almost_equal(luv2xyz(luv_array_i_obs, i, "2"),
self.xyz_array, decimal=3)

@pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
Expand Down

0 comments on commit 83f3bc8

Please sign in to comment.