diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 1c61020d..a471fd82 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -13,6 +13,10 @@ from .colorconv import (convert_colorspace, lab2xyz, lab2rgb, rgb2lab, + xyz2luv, + luv2xyz, + luv2rgb, + rgb2luv, rgb2hed, hed2rgb, lab2lch, diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index 7562b650..cad4565e 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -849,6 +849,197 @@ def lab2rgb(lab): return xyz2rgb(lab2xyz(lab)) +def xyz2luv(xyz): + """XYZ to CIE-Luv color space conversion. + + Parameters + ---------- + xyz : array_like + The image in XYZ format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). + + Returns + ------- + out : ndarray + The image in CIE-Luv format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). + + Raises + ------ + ValueError + If `xyz` is not a 3-D array of shape (.., ..,[ ..,] 3). + + Notes + ----- + Observer= 2A, Illuminant= D65 + CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=16#text16 + .. [2] http://en.wikipedia.org/wiki/CIELUV + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2xyz, xyz2luv + >>> lena = data.lena() + >>> lena_xyz = rgb2xyz(lena) + >>> lena_luv = xyz2luv(lena_xyz) + """ + arr = _prepare_colorarray(xyz) + + # extract channels + x, y, z = arr[..., 0], arr[..., 1], arr[..., 2] + + machineEps = np.finfo(np.float).eps + + # compute y_r and L + L = y / lab_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 ) + + def div_nan_check( n, d ): + out = n / d + mask = np.isnan( out ) + out[mask] = 0. + return out + + # u' and v' helper functions + def fu( X, Y, Z ): + return ( 4.*X ) / ( X + 15.*Y + 3.*Z + machineEps ) + def fv( X, Y, Z ): + return ( 9.*Y ) / ( X + 15.*Y + 3.*Z + machineEps ) + + # compute u and v using helper functions + u = 13.*L * ( fu(x,y,z) - u0 ) + v = 13.*L * ( fv(x,y,z) - v0 ) + + return np.concatenate([x[..., np.newaxis] for x in [L, u, v]], axis=-1) + + +def luv2xyz(luv): + """CIE-LAB to XYZcolor space conversion. + + Parameters + ---------- + lab : array_like + The image in lab format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in XYZ format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `lab` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + Observer= 2A, Illuminant= D65 + CIE XYZ tristimulus values x_ref = 95.047, y_ref = 100., z_ref = 108.883 + + References + ---------- + .. [1] http://www.easyrgb.com/index.php?X=MATH&H=16#text16 + .. [2] http://en.wikipedia.org/wiki/CIELUV + + """ + + arr = _prepare_colorarray(luv).copy() + + L, u, v = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] + + machineEps = np.finfo(np.float).eps + + # compute y + y = L.copy() + mask = y > 7.999625 + y[mask] = np.power( (y[mask]+16.) / 116., 3.) + y[~mask] = y[~mask] / 903.3 + y *= lab_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 ) + + def div_nan_check( n, d ): + out = n / d + mask = np.isnan( out ) + out[mask] = 0. + return out + + # compute intermediate values + a = u0 + u / ( 13.*L + machineEps ) + b = v0 + v / ( 13.*L + machineEps ) + c = 3*y * (5*b-3) + + # compute x and z + z = ( (a-4)*c - 15*a*b*y ) / ( 12*b ) + x = -( c/b + 3.*z ) + + return np.concatenate([x[..., np.newaxis] for x in [x, y, z]], axis=-1) + + +def rgb2luv(rgb): + """RGB to luv color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). + + Returns + ------- + out : ndarray + The image in Luv format, in a 3- or 4-D array of shape + (.., ..,[ ..,] 3). + + Raises + ------ + ValueError + If `rgb` is not a 3- or 4-D array of shape (.., ..,[ ..,] 3). + + Notes + ----- + This function uses rgb2xyz and xyz2luv. + """ + return xyz2luv(rgb2xyz(rgb)) + + +def luv2rgb(luv): + """Luv to RGB color space conversion. + + Parameters + ---------- + rgb : array_like + The image in Luv format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in RGB format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `luv` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + This function uses luv2xyz and xyz2rgb. + """ + return xyz2rgb(luv2xyz(luv)) + + def rgb2hed(rgb): """RGB to Haematoxylin-Eosin-DAB (HED) color space conversion. diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index fbec9ba6..26b55e35 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -33,6 +33,8 @@ from skimage.color import (rgb2hsv, hsv2rgb, rgb2grey, gray2rgb, xyz2lab, lab2xyz, lab2rgb, rgb2lab, + xyz2luv, luv2xyz, + luv2rgb, rgb2luv, is_rgb, is_gray, lab2lch, lch2lab, guess_spatial_dimensions @@ -81,6 +83,13 @@ class TestColorconv(TestCase): [[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 + ]) + # RGB to HSV def test_rgb2hsv_conversion(self): rgb = img_as_float(self.img_rgb)[::16, ::16] @@ -250,6 +259,39 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) + # test matrices for xyz2luv and luv2xyz generated using http://www.easyrgb.com/index.php?X=CALC + # Note: easyrgb website displays xyz*100 + def test_xyz2luv(self): + assert_array_almost_equal(xyz2luv(self.xyz_array), + self.luv_array, decimal=3) + + def test_luv2xyz(self): + assert_array_almost_equal(luv2xyz(self.luv_array), + self.xyz_array, decimal=3) + + def test_rgb2luv_brucelindbloom(self): + """ + Test the RGB->Lab conversion by comparing to the calculator on the + authoritative Bruce Lindbloom + [website](http://brucelindbloom.com/index.html?ColorCalculator.html). + """ + # Obtained with D65 white point, sRGB model and gamma + gt_for_colbars = np.array([ + [100,0,0], + [97.1393, 7.7056, 106.7866], + [91.1132, -70.4773, -15.2042], + [87.7347, -83.0776, 107.3985], + [60.3242, 84.0714, -108.6834], + [53.2408, 175.0151, 37.7564], + [32.2970, -9.4054, -130.3423], + [0,0,0]]).T + gt_array = np.swapaxes(gt_for_colbars.reshape(3, 4, 2), 0, 2) + assert_array_almost_equal(rgb2luv(self.colbars_array), gt_array, decimal=2) + + def test_luv_rgb_roundtrip(self): + img_rgb = img_as_float(self.img_rgb) + assert_array_almost_equal(luv2rgb(rgb2luv(img_rgb)), img_rgb) + def test_lab_lch_roundtrip(self): rgb = img_as_float(self.img_rgb) lab = rgb2lab(rgb)