Merge pull request #1130 from capitanbatata/more-illuminants

Add more illuminants to color conversion
This commit is contained in:
Stefan van der Walt
2014-10-01 01:46:16 +02:00
22 changed files with 244 additions and 52 deletions
+152 -36
View File
@@ -56,7 +56,6 @@ from __future__ import division
import numpy as np
from scipy import linalg
from ..util import dtype
from skimage._shared.utils import deprecated
def guess_spatial_dimensions(image):
@@ -114,7 +113,8 @@ def convert_colorspace(arr, fromspace, tospace):
Notes
-----
Conversion occurs through the "central" RGB color space, i.e. conversion
from XYZ to HSV is implemented as ``XYZ -> RGB -> HSV`` instead of directly.
from XYZ to HSV is implemented as ``XYZ -> RGB -> HSV`` instead of
directly.
Examples
--------
@@ -129,9 +129,9 @@ def convert_colorspace(arr, fromspace, tospace):
fromspace = fromspace.upper()
tospace = tospace.upper()
if not fromspace in fromdict.keys():
if fromspace not in fromdict.keys():
raise ValueError('fromspace needs to be one of %s' % fromdict.keys())
if not tospace in todict.keys():
if tospace not in todict.keys():
raise ValueError('tospace needs to be one of %s' % todict.keys())
return todict[tospace](fromdict[fromspace](arr))
@@ -287,15 +287,15 @@ def hsv2rgb(hsv):
return out
#---------------------------------------------------------------
# ---------------------------------------------------------------
# Primaries for the coordinate systems
#---------------------------------------------------------------
# ---------------------------------------------------------------
cie_primaries = np.array([700, 546.1, 435.8])
sb_primaries = np.array([1. / 155, 1. / 190, 1. / 225]) * 1e5
#---------------------------------------------------------------
# ---------------------------------------------------------------
# Matrices that define conversion between different color spaces
#---------------------------------------------------------------
# ---------------------------------------------------------------
# From sRGB specification
xyz_from_rgb = np.array([[0.412453, 0.357580, 0.180423],
@@ -321,9 +321,81 @@ gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721],
[0, 0, 0],
[0, 0, 0]])
# CIE LAB constants for Observer= 2A, Illuminant= D65
# CIE LAB constants for Observer=2A, Illuminant=D65
# NOTE: this is actually the XYZ values for the illuminant above.
lab_ref_white = np.array([0.95047, 1., 1.08883])
# 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
# field of view.
#
# illuminant[I][1] corresponds to the XYZ coordinates for the 10 degree
# field of view.
#
# The XYZ coordinates are calculated from [1], using the formula:
#
# X = x * ( Y / y )
# Y = Y
# Z = ( 1 - x - y ) * ( Y / y )
#
# where Y = 1. The only exception is the illuminant "D65" with aperture angle
# 2, whose coordinates are copied from 'lab_ref_white' for
# backward-compatibility reasons.
#
# References
# ----------
# .. [1] http://en.wikipedia.org/wiki/Standard_illuminant
illuminants = \
{"A": {'2': (1.098466069456375, 1, 0.3558228003436005),
'10': (1.111420406956693, 1, 0.3519978321919493)},
"D50": {'2': (0.9642119944211994, 1, 0.8251882845188288),
'10': (0.9672062750333777, 1, 0.8142801513128616)},
"D55": {'2': (0.956797052643698, 1, 0.9214805860173273),
'10': (0.9579665682254781, 1, 0.9092525159847462)},
"D65": {'2': (0.95047, 1., 1.08883), # This was: `lab_ref_white`
'10': (0.94809667673716, 1, 1.0730513595166162)},
"D75": {'2': (0.9497220898840717, 1, 1.226393520724154),
'10': (0.9441713925645873, 1, 1.2064272211720228)},
"E": {'2': (1.0, 1.0, 1.0),
'10': (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
The name of the illuminant (the function is NOT case sensitive).
observer : {"2", "10"}, optional
The aperture angle of the observer.
Returns
-------
(x, y, z) : tuple
A tuple with 3 elements containing the XYZ coordinates of the given
illuminant.
Raises
------
ValueError
If either the illuminant or the observer angle are not supported or
unknown.
References
----------
.. [1] http://en.wikipedia.org/wiki/Standard_illuminant
"""
illuminant = illuminant.upper()
try:
return illuminants[illuminant][observer]
except KeyError:
raise ValueError("Unknown illuminant/observer combination\
(\'{0}\', \'{1}\')".format(illuminant, observer))
# Haematoxylin-Eosin-DAB colorspace
# From original Ruifrok's paper: A. C. Ruifrok and D. A. Johnston,
@@ -408,9 +480,9 @@ rgb_from_hpx = np.array([[0.644211, 0.716556, 0.266844],
rgb_from_hpx[2, :] = np.cross(rgb_from_hpx[0, :], rgb_from_hpx[1, :])
hpx_from_rgb = linalg.inv(rgb_from_hpx)
#-------------------------------------------------------------
# -------------------------------------------------------------
# The conversion functions that make use of the matrices above
#-------------------------------------------------------------
# -------------------------------------------------------------
def _convert(matrix, arr):
@@ -668,7 +740,7 @@ def gray2rgb(image):
raise ValueError("Input image expected to be RGB, RGBA or gray.")
def xyz2lab(xyz):
def xyz2lab(xyz, illuminant="D65", observer="2"):
"""XYZ to CIE-LAB color space conversion.
Parameters
@@ -676,6 +748,10 @@ def xyz2lab(xyz):
xyz : array_like
The image in XYZ format, in a 3- or 4-D array of shape
``(.., ..,[ ..,] 3)``.
illuminant : {"A", "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.
Returns
-------
@@ -687,11 +763,15 @@ def xyz2lab(xyz):
------
ValueError
If `xyz` is not a 3-D array of shape ``(.., ..,[ ..,] 3)``.
ValueError
If either the illuminant or the observer angle is unsupported or
unknown.
Notes
-----
Observer= 2A, Illuminant= D65
CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883
By default Observer= 2A, 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.
References
----------
@@ -705,11 +785,14 @@ def xyz2lab(xyz):
>>> lena = data.lena()
>>> lena_xyz = rgb2xyz(lena)
>>> lena_lab = xyz2lab(lena_xyz)
"""
arr = _prepare_colorarray(xyz)
xyz_ref_white = get_xyz_coords(illuminant, observer)
# scale by CIE XYZ tristimulus values of the reference white point
arr = arr / lab_ref_white
arr = arr / xyz_ref_white
# Nonlinear distortion and linear transformation
mask = arr > 0.008856
@@ -726,13 +809,17 @@ def xyz2lab(xyz):
return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1)
def lab2xyz(lab):
def lab2xyz(lab, illuminant="D65", observer="2"):
"""CIE-LAB to XYZcolor space conversion.
Parameters
----------
lab : array_like
The image in lab format, in a 3-D array of shape ``(.., .., 3)``.
illuminant : {"A", "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.
Returns
-------
@@ -743,11 +830,16 @@ def lab2xyz(lab):
------
ValueError
If `lab` is not a 3-D array of shape ``(.., .., 3)``.
ValueError
If either the illuminant or the observer angle are not supported or
unknown.
Notes
-----
Observer = 2A, Illuminant = D65
CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883
By default Observer= 2A, 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.
References
----------
@@ -769,8 +861,9 @@ def lab2xyz(lab):
out[mask] = np.power(out[mask], 3.)
out[~mask] = (out[~mask] - 16.0 / 116.) / 7.787
# rescale Observer= 2 deg, Illuminant= D65
out *= lab_ref_white
# rescale to the reference white (illuminant)
xyz_ref_white = get_xyz_coords(illuminant, observer)
out *= xyz_ref_white
return out
@@ -826,7 +919,7 @@ def lab2rgb(lab):
return xyz2rgb(lab2xyz(lab))
def xyz2luv(xyz):
def xyz2luv(xyz, illuminant="D65", observer="2"):
"""XYZ to CIE-Luv color space conversion.
Parameters
@@ -834,6 +927,10 @@ def xyz2luv(xyz):
xyz : (M, N, [P,] 3) array_like
The 3 or 4 dimensional image in XYZ format. Final dimension denotes
channels.
illuminant : {"A", "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.
Returns
-------
@@ -844,11 +941,16 @@ def xyz2luv(xyz):
------
ValueError
If `xyz` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``.
ValueError
If either the illuminant or the observer angle are not supported or
unknown.
Notes
-----
XYZ conversion weights use Observer = 2A. Reference whitepoint for D65
Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``.
By default XYZ conversion weights use observer=2A. Reference whitepoint
for D65 Illuminant, with XYZ tristimulus values of ``(95.047, 100.,
108.883)``. See function 'get_xyz_coords' for a list of supported
illuminants.
References
----------
@@ -871,13 +973,14 @@ def xyz2luv(xyz):
eps = np.finfo(np.float).eps
# compute y_r and L
L = y / lab_ref_white[1]
xyz_ref_white = get_xyz_coords(illuminant, observer)
L = y / xyz_ref_white[1]
mask = L > 0.008856
L[mask] = 116. * np.power(L[mask], 1. / 3.) - 16.
L[~mask] = 903.3 * L[~mask]
u0 = 4*lab_ref_white[0] / np.dot([1, 15, 3], lab_ref_white)
v0 = 9*lab_ref_white[1] / np.dot([1, 15, 3], lab_ref_white)
u0 = 4*xyz_ref_white[0] / np.dot([1, 15, 3], xyz_ref_white)
v0 = 9*xyz_ref_white[1] / np.dot([1, 15, 3], xyz_ref_white)
# u' and v' helper functions
def fu(X, Y, Z):
@@ -893,7 +996,7 @@ def xyz2luv(xyz):
return np.concatenate([q[..., np.newaxis] for q in [L, u, v]], axis=-1)
def luv2xyz(luv):
def luv2xyz(luv, illuminant="D65", observer="2"):
"""CIE-Luv to XYZ color space conversion.
Parameters
@@ -901,6 +1004,10 @@ def luv2xyz(luv):
luv : (M, N, [P,] 3) array_like
The 3 or 4 dimensional image in CIE-Luv format. Final dimension denotes
channels.
illuminant : {"A", "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.
Returns
-------
@@ -911,11 +1018,15 @@ def luv2xyz(luv):
------
ValueError
If `luv` is not a 3-D or 4-D array of shape ``(M, N, [P,] 3)``.
ValueError
If either the illuminant or the observer angle are not supported or
unknown.
Notes
-----
XYZ conversion weights use Observer = 2A. Reference whitepoint for D65
Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``.
XYZ conversion weights use observer=2A. Reference whitepoint for D65
Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. See
function 'get_xyz_coords' for a list of supported illuminants.
References
----------
@@ -935,12 +1046,13 @@ def luv2xyz(luv):
mask = y > 7.999625
y[mask] = np.power((y[mask]+16.) / 116., 3.)
y[~mask] = y[~mask] / 903.3
y *= lab_ref_white[1]
xyz_ref_white = get_xyz_coords(illuminant, observer)
y *= xyz_ref_white[1]
# reference white x,z
uv_weights = [1, 15, 3]
u0 = 4*lab_ref_white[0] / np.dot(uv_weights, lab_ref_white)
v0 = 9*lab_ref_white[1] / np.dot(uv_weights, lab_ref_white)
u0 = 4*xyz_ref_white[0] / np.dot(uv_weights, xyz_ref_white)
v0 = 9*xyz_ref_white[1] / np.dot(uv_weights, xyz_ref_white)
# compute intermediate values
a = u0 + u / (13.*L + eps)
@@ -1048,7 +1160,8 @@ def hed2rgb(hed):
Parameters
----------
hed : array_like
The image in the HED color space, in a 3-D array of shape ``(.., .., 3)``.
The image in the HED color space, in a 3-D array of shape
``(.., .., 3)``.
Returns
-------
@@ -1091,7 +1204,8 @@ def separate_stains(rgb, conv_matrix):
Returns
-------
out : ndarray
The image in stain color space, in a 3-D array of shape ``(.., .., 3)``.
The image in stain color space, in a 3-D array of shape
``(.., .., 3)``.
Raises
------
@@ -1139,7 +1253,8 @@ def combine_stains(stains, conv_matrix):
Parameters
----------
stains : array_like
The image in stain color space, in a 3-D array of shape ``(.., .., 3)``.
The image in stain color space, in a 3-D array of shape
``(.., .., 3)``.
conv_matrix: ndarray
The stain separation matrix as described by G. Landini [1]_.
@@ -1190,7 +1305,8 @@ def combine_stains(stains, conv_matrix):
stains = dtype.img_as_float(stains)
logrgb2 = np.dot(-np.reshape(stains, (-1, 3)), conv_matrix)
rgb2 = np.exp(logrgb2)
return rescale_intensity(np.reshape(rgb2 - 2, stains.shape), in_range=(-1, 1))
return rescale_intensity(np.reshape(rgb2 - 2, stains.shape),
in_range=(-1, 1))
def lab2lch(lab):
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+92 -16
View File
@@ -71,24 +71,24 @@ class TestColorconv(TestCase):
colbars_point75 = colbars * 0.75
colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2)
xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red
[[0, 0, 0]], # black
[[.9505, 1., 1.089]], # white
[[.1805, .0722, .9505]], # blue
[[.07719, .15438, .02573]], # green
xyz_array = np.array([[[0.4124, 0.21260, 0.01930]], # red
[[0, 0, 0]], # black
[[.9505, 1., 1.089]], # white
[[.1805, .0722, .9505]], # blue
[[.07719, .15438, .02573]], # green
])
lab_array = np.array([[[53.233, 80.109, 67.220]], # red
[[0., 0., 0.]], # black
[[100.0, 0.005, -0.010]], # white
[[32.303, 79.197, -107.864]], # blue
[[46.229, -51.7, 49.898]], # green
lab_array = np.array([[[53.233, 80.109, 67.220]], # red
[[0., 0., 0.]], # black
[[100.0, 0.005, -0.010]], # white
[[32.303, 79.197, -107.864]], # blue
[[46.229, -51.7, 49.898]], # green
])
luv_array = np.array([[[53.233, 175.053, 37.751]], # red
[[0., 0., 0.]], # black
[[100., 0.001, -0.017]], # white
[[32.303, -9.400, -130.358]], # blue
[[46.228, -43.774, 56.589]], # green
luv_array = np.array([[[53.233, 175.053, 37.751]], # red
[[0., 0., 0.]], # black
[[100., 0.001, -0.017]], # white
[[32.303, -9.400, -130.358]], # blue
[[46.228, -43.774, 56.589]], # green
])
# RGB to HSV
@@ -227,16 +227,60 @@ class TestColorconv(TestCase):
def test_rgb2grey_on_grey(self):
rgb2grey(np.random.rand(5, 5))
# test matrices for xyz2lab and lab2xyz generated using http://www.easyrgb.com/index.php?X=CALC
# test matrices for xyz2lab and lab2xyz generated using
# http://www.easyrgb.com/index.php?X=CALC
# Note: easyrgb website displays xyz*100
def test_xyz2lab(self):
assert_array_almost_equal(xyz2lab(self.xyz_array),
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 = "lab_array_{0}_{1}.npy".format(I, obs)
lab_array_I_obs = np.load(
os.path.join(os.path.dirname(__file__), 'data', fname))
assert_array_almost_equal(lab_array_I_obs,
xyz2lab(self.xyz_array, I, obs),
decimal=2)
for I in ["a", "e"]:
fname = "lab_array_{0}_2.npy".format(I)
lab_array_I_obs = np.load(
os.path.join(os.path.dirname(__file__), 'data', fname))
assert_array_almost_equal(lab_array_I_obs,
xyz2lab(self.xyz_array, I, "2"),
decimal=2)
def test_lab2xyz(self):
assert_array_almost_equal(lab2xyz(self.lab_array),
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 = "lab_array_{0}_{1}.npy".format(I, obs)
lab_array_I_obs = np.load(
os.path.join(os.path.dirname(__file__), 'data', 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, obs)
lab_array_I_obs = np.load(
os.path.join(os.path.dirname(__file__), 'data', 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.
try:
xs = lab2xyz(lab_array_I_obs, "NaI", "2") # Not an illuminant
except ValueError:
pass
try:
xs = lab2xyz(lab_array_I_obs, "d50", "42") # Not a degree
except ValueError:
pass
def test_rgb2lab_brucelindbloom(self):
"""
Test the RGB->Lab conversion by comparing to the calculator on the
@@ -267,10 +311,42 @@ class TestColorconv(TestCase):
assert_array_almost_equal(xyz2luv(self.xyz_array),
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 = "luv_array_{0}_{1}.npy".format(I, obs)
luv_array_I_obs = np.load(
os.path.join(os.path.dirname(__file__), 'data', fname))
assert_array_almost_equal(luv_array_I_obs,
xyz2luv(self.xyz_array, I, obs),
decimal=2)
for I in ["a", "e"]:
fname = "luv_array_{0}_2.npy".format(I)
luv_array_I_obs = np.load(
os.path.join(os.path.dirname(__file__), 'data', fname))
assert_array_almost_equal(luv_array_I_obs,
xyz2luv(self.xyz_array, I, "2"),
decimal=2)
def test_luv2xyz(self):
assert_array_almost_equal(luv2xyz(self.luv_array),
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 = "luv_array_{0}_{1}.npy".format(I, obs)
luv_array_I_obs = np.load(
os.path.join(os.path.dirname(__file__), 'data', fname))
assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, obs),
self.xyz_array, decimal=3)
for I in ["a", "e"]:
fname = "luv_array_{0}_2.npy".format(I, obs)
luv_array_I_obs = np.load(
os.path.join(os.path.dirname(__file__), 'data', fname))
assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, "2"),
self.xyz_array, decimal=3)
def test_rgb2luv_brucelindbloom(self):
"""
Test the RGB->Lab conversion by comparing to the calculator on the