diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ac11457c..b304646b 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -145,3 +145,6 @@ - Jostein Bø Fløystad Reconstruction circle mode for Radon transform Simultaneous Algebraic Reconstruction Technique for inverse Radon transform + +- Matt Terry + Color difference functions diff --git a/skimage/color/__init__.py b/skimage/color/__init__.py index 19620cb1..1c61020d 100644 --- a/skimage/color/__init__.py +++ b/skimage/color/__init__.py @@ -15,6 +15,8 @@ from .colorconv import (convert_colorspace, rgb2lab, rgb2hed, hed2rgb, + lab2lch, + lch2lab, separate_stains, combine_stains, rgb_from_hed, @@ -44,6 +46,12 @@ from .colorconv import (convert_colorspace, from .colorlabel import color_dict, label2rgb +from .delta_e import (deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc, + ) + __all__ = ['convert_colorspace', 'guess_spatial_dimensions', @@ -62,6 +70,8 @@ __all__ = ['convert_colorspace', 'rgb2lab', 'rgb2hed', 'hed2rgb', + 'lab2lch', + 'lch2lab', 'separate_stains', 'combine_stains', 'rgb_from_hed', @@ -89,4 +99,9 @@ __all__ = ['convert_colorspace', 'is_rgb', 'is_gray', 'color_dict', - 'label2rgb'] + 'label2rgb', + 'deltaE_cie76', + 'deltaE_ciede94', + 'deltaE_ciede2000', + 'deltaE_cmc', + ] diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py index d2b20316..5d41a063 100644 --- a/skimage/color/colorconv.py +++ b/skimage/color/colorconv.py @@ -26,10 +26,17 @@ Supported color spaces Derived from the RGB CIE color space. Chosen such that ``x == y == z == 1/3`` at the whitepoint, and all color matching functions are greater than zero everywhere. +* LAB CIE : Lightness, a, b + Colorspace derived from XYZ CIE that is intended to be more + perceptually uniform +* LCH CIE : Lightness, Chroma, Hue + Defined in terms of LAB CIE. C and H are the polar representation of + a and b. The polar angle C is defined to be on (0, 2*pi) :author: Nicolas Pinto (rgb2hsv) :author: Ralf Gommers (hsv2rgb) :author: Travis Oliphant (XYZ and RGB CIE functions) +:author: Matt Terry (lab2lch) :license: modified BSD @@ -1026,3 +1033,105 @@ def combine_stains(stains, conv_matrix): 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)) + + +def lab2lch(lab): + """CIE-LAB to CIE-LCH color space conversion. + + LCH is the cylindrical representation of the LAB (Cartesian) colorspace + + Parameters + ---------- + lab : array_like + The N-D image in CIE-LAB format. The last (`N+1`th) dimension must have + at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color + channels. Subsequent elements are copied. + + Returns + ------- + out : ndarray + The image in LCH format, in a N-D array with same shape as input `lab`. + + Raises + ------ + ValueError + If `lch` does not have at least 3 color channels (i.e. l, a, b). + + Notes + ----- + The Hue is expressed as an angle between (0, 2*pi) + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lab2lch + >>> lena = data.lena() + >>> lena_lab = rgb2lab(lena) + >>> lena_lch = lab2lch(lena_lab) + """ + lch = _prepare_lab_array(lab) + + a, b = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = _cart2polar_2pi(a, b) + return lch + + +def _cart2polar_2pi(x, y): + """convert cartesian coordiantes to polar (uses non-standard theta range!) + + NON-STANDARD RANGE! Maps to (0, 2*pi) rather than usual (-pi, +pi) + """ + r, t = np.hypot(x, y), np.arctan2(y, x) + t += np.where(t < 0., 2 * np.pi, 0) + return r, t + + +def lch2lab(lch): + """CIE-LCH to CIE-LAB color space conversion. + + LCH is the cylindrical representation of the LAB (Cartesian) colorspace + + Parameters + ---------- + lch : array_like + The N-D image in CIE-LCH format. The last (`N+1`th) dimension must have + at least 3 elements, corresponding to the ``L``, ``a``, and ``b`` color + channels. Subsequent elements are copied. + + Returns + ------- + out : ndarray + The image in LAB format, with same shape as input `lch`. + + Raises + ------ + ValueError + If `lch` does not have at least 3 color channels (i.e. l, c, h). + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lch2lab + >>> lena = data.lena() + >>> lena_lab = rgb2lab(lena) + >>> lena_lch = lab2lch(lena_lab) + >>> lena_lab2 = lch2lab(lena_lch) + """ + lch = _prepare_lab_array(lch) + + c, h = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = c * np.cos(h), c * np.sin(h) + return lch + + +def _prepare_lab_array(arr): + """Ensure input for lab2lch, lch2lab are well-posed. + + Arrays must be in floating point and have at least 3 elements in + last dimension. Return a new array. + """ + arr = np.asarray(arr) + shape = arr.shape + if shape[-1] < 3: + raise ValueError('Input array has less than 3 color channels') + return dtype.img_as_float(arr, force_copy=True) diff --git a/skimage/color/delta_e.py b/skimage/color/delta_e.py new file mode 100644 index 00000000..18cfca98 --- /dev/null +++ b/skimage/color/delta_e.py @@ -0,0 +1,339 @@ +""" +Functions for calculating the "distance" between colors. + +Implicit in these definitions of "distance" is the notion of "Just Noticeable +Distance" (JND). This represents the distance between colors where a human can +perceive different colors. Humans are more sensitive to certain colors than +others, which different deltaE metrics correct for with varying degrees of +sophistication. + +The literature often mentions 1 as the minimum distance for visual +differentiation, but more recent studies (Mahy 1994) peg JND at 2.3 + +The delta-E notation comes from the German word for "Sensation" (Empfindung). + +Reference +--------- +http://en.wikipedia.org/wiki/Color_difference + +""" +from __future__ import division + +import numpy as np + +from skimage.color.colorconv import lab2lch, _cart2polar_2pi + + +def deltaE_cie76(lab1, lab2): + """Euclidean distance between two points in Lab color space + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] A. R. Robertson, "The CIE 1976 color-difference formulae," + Color Res. Appl. 2, 7-11 (1977). + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + L1, a1, b1 = np.rollaxis(lab1, -1)[:3] + L2, a2, b2 = np.rollaxis(lab2, -1)[:3] + return np.sqrt((L2 - L1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) + + +def deltaE_ciede94(lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015): + """Color difference according to CIEDE 94 standard + + Accommodates perceptual non-uniformities through the use of application + specific scale factors (`kH`, `kC`, `kL`, `k1`, and `k2`). + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + kH : float, optional + Hue scale + kC : float, optional + Chroma scale + kL : float, optional + Lightness scale + k1 : float, optional + first scale parameter + k2 : float, optional + second scale parameter + + Returns + ------- + dE : array_like + color difference between `lab1` and `lab2` + + Notes + ----- + deltaE_ciede94 is not symmetric with respect to lab1 and lab2. CIEDE94 + defines the scales for the lightness, hue, and chroma in terms of the first + color. Consequently, the first color should be regarded as the "reference" + color. + + `kL`, `k1`, `k2` depend on the application and default to the values + suggested for graphic arts + + ========== ============== ========== + Parameter Graphic Arts Textiles + ========== ============== ========== + `kL` 1.000 2.000 + `k1` 0.045 0.048 + `k2` 0.015 0.014 + ========== ============== ========== + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + """ + L1, C1 = np.rollaxis(lab2lch(lab1), -1)[:2] + L2, C2 = np.rollaxis(lab2lch(lab2), -1)[:2] + + dL = L1 - L2 + dC = C1 - C2 + dH2 = get_dH2(lab1, lab2) + + SL = 1 + SC = 1 + k1 * C1 + SH = 1 + k2 * C1 + + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += dH2 / (kH * SH) ** 2 + return np.sqrt(dE2) + + +def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1): + """Color difference as given by the CIEDE 2000 standard. + + CIEDE 2000 is a major revision of CIDE94. The perceptual calibration is + largely based on experience with automotive paint on smooth surfaces. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + kL : float (range), optional + lightness scale factor, 1 for "acceptably close"; 2 for "imperceptible" + see deltaE_cmc + kC : float (range), optional + chroma scale factor, usually 1 + kH : float (range), optional + hue scale factor, usually 1 + + Returns + ------- + deltaE : array_like + The distance between `lab1` and `lab2` + + Notes + ----- + CIEDE 2000 assumes parametric weighting factors for the lightness, chroma, + and hue (`kL`, `kC`, `kH` respectively). These default to 1. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf + (doi:10.1364/AO.33.008069) + .. [3] M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent + color metrics tested with an accurate color-difference tolerance + dataset," Appl. Opt. 33, 8069-8077 (1994). + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + unroll = False + if lab1.ndim == 1 and lab2.ndim == 1: + unroll = True + if lab1.ndim == 1: + lab1 = lab1[None, :] + if lab2.ndim == 1: + lab2 = lab2[None, :] + L1, a1, b1 = np.rollaxis(lab1, -1)[:3] + L2, a2, b2 = np.rollaxis(lab2, -1)[:3] + + # distort `a` based on average chroma + # then convert to lch coordines from distorted `a` + # all subsequence calculations are in the new coordiantes + # (often denoted "prime" in the literature) + Cbar = 0.5 * (np.hypot(a1, b1) + np.hypot(a2, b2)) + c7 = Cbar ** 7 + G = 0.5 * (1 - np.sqrt(c7 / (c7 + 25 ** 7))) + scale = 1 + G + C1, h1 = _cart2polar_2pi(a1 * scale, b1) + C2, h2 = _cart2polar_2pi(a2 * scale, b2) + # recall that c, h are polar coordiantes. c==r, h==theta + + # cide2000 has four terms to delta_e: + # 1) Luminance term + # 2) Hue term + # 3) Chroma term + # 4) hue Rotation term + + # lightness term + Lbar = 0.5 * (L1 + L2) + tmp = (Lbar - 50) ** 2 + SL = 1 + 0.015 * tmp / np.sqrt(20 + tmp) + L_term = (L2 - L1) / (kL * SL) + + # chroma term + Cbar = 0.5 * (C1 + C2) # new coordiantes + SC = 1 + 0.045 * Cbar + C_term = (C2 - C1) / (kC * SC) + + # hue term + h_diff = h2 - h1 + h_sum = h1 + h2 + CC = C1 * C2 + + dH = h_diff.copy() + dH[h_diff > np.pi] -= 2 * np.pi + dH[h_diff < -np.pi] += 2 * np.pi + dH[CC == 0.] = 0. # if r == 0, dtheta == 0 + dH_term = 2 * np.sqrt(CC) * np.sin(dH / 2) + + Hbar = h_sum.copy() + mask = np.logical_and(CC != 0., np.abs(h_diff) > np.pi) + Hbar[mask * (h_sum < 2 * np.pi)] += 2 * np.pi + Hbar[mask * (h_sum >= 2 * np.pi)] -= 2 * np.pi + Hbar[CC == 0.] *= 2 + Hbar *= 0.5 + + T = (1 - + 0.17 * np.cos(Hbar - np.deg2rad(30)) + + 0.24 * np.cos(2 * Hbar) + + 0.32 * np.cos(3 * Hbar + np.deg2rad(6)) - + 0.20 * np.cos(4 * Hbar - np.deg2rad(63)) + ) + SH = 1 + 0.015 * Cbar * T + + H_term = dH_term / (kH * SH) + + # hue rotation + c7 = Cbar ** 7 + Rc = 2 * np.sqrt(c7 / (c7 + 25 ** 7)) + dtheta = np.deg2rad(30) * np.exp(-((np.rad2deg(Hbar) - 275) / 25) ** 2) + R_term = -np.sin(2 * dtheta) * Rc * C_term * H_term + + # put it all together + dE2 = L_term ** 2 + dE2 += C_term ** 2 + dE2 += H_term ** 2 + dE2 += R_term + ans = np.sqrt(dE2) + if unroll: + ans = ans[0] + return ans + + +def deltaE_cmc(lab1, lab2, kL=1, kC=1): + """Color difference from the CMC l:c standard. + + This color difference was developed by the Colour Measurement Committee + (CMC) of the Society of Dyers and Colourists (United Kingdom). It is + intended for use in the textile industry. + + The scale factors `kL`, `kC` set the weight given to differences in + lightness and chroma relative to differences in hue. The usual values are + ``kL=2``, ``kC=1`` for "acceptability" and ``kL=1``, ``kC=1`` for + "imperceptibility". Colors with ``dE > 1`` are "different" for the given + scale factors. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + Notes + ----- + deltaE_cmc the defines the scales for the lightness, hue, and chroma + in terms of the first color. Consequently + ``deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1)`` + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + .. [3] F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the + JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 + (1984). + """ + L1, C1, h1 = np.rollaxis(lab2lch(lab1), -1)[:3] + L2, C2, h2 = np.rollaxis(lab2lch(lab2), -1)[:3] + + dC = C1 - C2 + dL = L1 - L2 + dH2 = get_dH2(lab1, lab2) + + T = np.where(np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345), + 0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))), + 0.36 + 0.4 * np.abs(np.cos(h1 + np.deg2rad(35))) + ) + c1_4 = C1 ** 4 + F = np.sqrt(c1_4 / (c1_4 + 1900)) + + SL = np.where(L1 < 16, 0.511, 0.040975 * L1 / (1. + 0.01765 * L1)) + SC = 0.638 + 0.0638 * C1 / (1. + 0.0131 * C1) + SH = SC * (F * T + 1 - F) + + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += dH2 / (SH ** 2) + return np.sqrt(dE2) + + +def get_dH2(lab1, lab2): + """squared hue difference term occurring in deltaE_cmc and deltaE_ciede94 + + Despite its name "dH" is not a simple difference of hue values. We avoid + working directly with the hue value directly since differencing angles is + troublesome. The hue term is usually written as: + c1 = sqrt(a1**2 + b1**2) + c2 = sqrt(a2**2 + b2**2) + term = (a1-a2)**2 + (b1-b2)**2 - (c1-c2)**2 + dH = sqrt(term) + + However, this has poor roundoff properties when a or b is dominant. + Instead, ab is a vector with elements a and b. The same dH term can be + re-written as: + |ab1-ab2|**2 - (|ab1| - |ab2|)**2 + and then simplified to: + 2*|ab1|*|ab2| - 2*dot(ab1, ab2) + """ + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + a1, b1 = np.rollaxis(lab1, -1)[1:3] + a2, b2 = np.rollaxis(lab2, -1)[1:3] + + # magnitude of (a, b) is the chroma + C1 = np.hypot(a1, b1) + C2 = np.hypot(a2, b2) + + term = (C1 * C2) - (a1 * a2 + b1 * b2) + return 2*term diff --git a/skimage/color/tests/ciede2000_test_data.txt b/skimage/color/tests/ciede2000_test_data.txt new file mode 100644 index 00000000..b7e3fd57 --- /dev/null +++ b/skimage/color/tests/ciede2000_test_data.txt @@ -0,0 +1,38 @@ +# input, intermediate, and output values for CIEDE2000 dE function +# data taken from "The CIEDE2000 Color-Difference Formula: Implementation Notes, ..." http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf +# tab delimited data +# pair 1 L1 a1 b1 ap1 cp1 hp1 hbar1 G T SL SC SH RT dE 2 L2 a2 b2 ap2 cp2 hp2 +1 1 50.0000 2.6772 -79.7751 2.6774 79.8200 271.9222 270.9611 0.0001 0.6907 1.0000 4.6578 1.8421 -1.7042 2.0425 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +2 1 50.0000 3.1571 -77.2803 3.1573 77.3448 272.3395 271.1698 0.0001 0.6843 1.0000 4.6021 1.8216 -1.7070 2.8615 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +3 1 50.0000 2.8361 -74.0200 2.8363 74.0743 272.1944 271.0972 0.0001 0.6865 1.0000 4.5285 1.8074 -1.7060 3.4412 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +4 1 50.0000 -1.3802 -84.2814 -1.3803 84.2927 269.0618 269.5309 0.0001 0.7357 1.0000 4.7584 1.9217 -1.6809 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +5 1 50.0000 -1.1848 -84.8006 -1.1849 84.8089 269.1995 269.5997 0.0001 0.7335 1.0000 4.7700 1.9218 -1.6822 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +6 1 50.0000 -0.9009 -85.5211 -0.9009 85.5258 269.3964 269.6982 0.0001 0.7303 1.0000 4.7862 1.9217 -1.6840 1.0000 2 50.0000 0.0000 -82.7485 0.0000 82.7485 270.0000 +7 1 50.0000 0.0000 0.0000 0.0000 0.0000 0.0000 126.8697 0.5000 1.2200 1.0000 1.0562 1.0229 0.0000 2.3669 2 50.0000 -1.0000 2.0000 -1.5000 2.5000 126.8697 +8 1 50.0000 -1.0000 2.0000 -1.5000 2.5000 126.8697 126.8697 0.5000 1.2200 1.0000 1.0562 1.0229 0.0000 2.3669 2 50.0000 0.0000 0.0000 0.0000 0.0000 0.0000 +9 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 269.9854 0.4998 0.7212 1.0000 1.1681 1.0404 -0.0022 7.1792 2 50.0000 -2.4900 0.0009 -3.7346 3.7346 179.9862 +10 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 269.9847 0.4998 0.7212 1.0000 1.1681 1.0404 -0.0022 7.1792 2 50.0000 -2.4900 0.0010 -3.7346 3.7346 179.9847 +11 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 89.9839 0.4998 0.6175 1.0000 1.1681 1.0346 0.0000 7.2195 2 50.0000 -2.4900 0.0011 -3.7346 3.7346 179.9831 +12 1 50.0000 2.4900 -0.0010 3.7346 3.7346 359.9847 89.9831 0.4998 0.6175 1.0000 1.1681 1.0346 0.0000 7.2195 2 50.0000 -2.4900 0.0012 -3.7346 3.7346 179.9816 +13 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 180.0328 0.4998 0.9779 1.0000 1.1121 1.0365 0.0000 4.8045 2 50.0000 0.0009 -2.4900 0.0013 2.4900 270.0311 +14 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 180.0345 0.4998 0.9779 1.0000 1.1121 1.0365 0.0000 4.8045 2 50.0000 0.0010 -2.4900 0.0015 2.4900 270.0345 +15 1 50.0000 -0.0010 2.4900 -0.0015 2.4900 90.0345 0.0362 0.4998 1.3197 1.0000 1.1121 1.0493 0.0000 4.7461 2 50.0000 0.0011 -2.4900 0.0016 2.4900 270.0380 +16 1 50.0000 2.5000 0.0000 3.7496 3.7496 0.0000 315.0000 0.4998 0.8454 1.0000 1.1406 1.0396 -0.0001 4.3065 2 50.0000 0.0000 -2.5000 0.0000 2.5000 270.0000 +17 1 50.0000 2.5000 0.0000 3.4569 3.4569 0.0000 346.2470 0.3827 1.4453 1.1608 1.9547 1.4599 -0.0003 27.1492 2 73.0000 25.0000 -18.0000 34.5687 38.9743 332.4939 +18 1 50.0000 2.5000 0.0000 3.4954 3.4954 0.0000 51.7766 0.3981 0.6447 1.0640 1.7498 1.1612 0.0000 22.8977 2 61.0000 -5.0000 29.0000 -6.9907 29.8307 103.5532 +19 1 50.0000 2.5000 0.0000 3.5514 3.5514 0.0000 272.2362 0.4206 0.6521 1.0251 1.9455 1.2055 -0.8219 31.9030 2 56.0000 -27.0000 -3.0000 -38.3556 38.4728 184.4723 +20 1 50.0000 2.5000 0.0000 3.5244 3.5244 0.0000 11.9548 0.4098 1.1031 1.0400 1.9120 1.3353 0.0000 19.4535 2 58.0000 24.0000 15.0000 33.8342 37.0102 23.9095 +21 1 50.0000 2.5000 0.0000 3.7494 3.7494 0.0000 3.5056 0.4997 1.2616 1.0000 1.1923 1.0808 0.0000 1.0000 2 50.0000 3.1736 0.5854 4.7596 4.7954 7.0113 +22 1 50.0000 2.5000 0.0000 3.7493 3.7493 0.0000 0.0000 0.4997 1.3202 1.0000 1.1956 1.0861 0.0000 1.0000 2 50.0000 3.2972 0.0000 4.9450 4.9450 0.0000 +23 1 50.0000 2.5000 0.0000 3.7497 3.7497 0.0000 5.8190 0.4999 1.2197 1.0000 1.1486 1.0604 0.0000 1.0000 2 50.0000 1.8634 0.5757 2.7949 2.8536 11.6380 +24 1 50.0000 2.5000 0.0000 3.7493 3.7493 0.0000 1.9603 0.4997 1.2883 1.0000 1.1946 1.0836 0.0000 1.0000 2 50.0000 3.2592 0.3350 4.8879 4.8994 3.9206 +25 1 60.2574 -34.0099 36.2677 -34.0678 49.7590 133.2085 132.0835 0.0017 1.3010 1.1427 3.2946 1.9951 0.0000 1.2644 2 60.4626 -34.1751 39.4387 -34.2333 52.2238 130.9584 +26 1 63.0109 -31.0961 -5.8663 -32.6194 33.1427 190.1951 188.8221 0.0490 0.9402 1.1831 2.4549 1.4560 0.0000 1.2630 2 62.8187 -29.7946 -4.0864 -31.2542 31.5202 187.4490 +27 1 61.2901 3.7196 -5.3901 5.5668 7.7487 315.9240 310.0313 0.4966 0.6952 1.1586 1.3092 1.0717 -0.0032 1.8731 2 61.4292 2.2480 -4.9620 3.3644 5.9950 304.1385 +28 1 35.0831 -44.1164 3.7933 -44.3939 44.5557 175.1161 176.4290 0.0063 1.0168 1.2148 2.9105 1.6476 0.0000 1.8645 2 35.0232 -40.0716 1.5901 -40.3237 40.3550 177.7418 +29 1 22.7233 20.0904 -46.6940 20.1424 50.8532 293.3339 291.3809 0.0026 0.3636 1.4014 3.1597 1.2617 -1.2537 2.0373 2 23.0331 14.9730 -42.5619 15.0118 45.1317 289.4279 +30 1 36.4612 47.8580 18.3852 47.9197 51.3256 20.9901 21.8781 0.0013 0.9239 1.1943 3.3888 1.7357 0.0000 1.4146 2 36.2715 50.5065 21.2231 50.5716 54.8444 22.7660 +31 1 90.8027 -2.0831 1.4410 -3.1245 3.4408 155.2410 167.1011 0.4999 1.1546 1.6110 1.1329 1.0511 0.0000 1.4441 2 91.1528 -1.6435 0.0447 -2.4651 2.4655 178.9612 +32 1 90.9257 -0.5406 -0.9208 -0.8109 1.2270 228.6315 218.4363 0.5000 1.3916 1.5930 1.0620 1.0288 0.0000 1.5381 2 88.6381 -0.8985 -0.7239 -1.3477 1.5298 208.2412 +33 1 6.7747 -0.2908 -2.4247 -0.4362 2.4636 259.8025 263.0049 0.4999 0.9556 1.6517 1.1057 1.0337 -0.0004 0.6377 2 5.8714 -0.0985 -2.2286 -0.1477 2.2335 266.2073 +34 1 2.0776 0.0795 -1.1350 0.1192 1.1412 275.9978 268.0910 0.5000 0.7826 1.7246 1.0383 1.0100 0.0000 0.9082 2 0.9033 -0.0636 -0.5514 -0.0954 0.5596 260.18421 diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py index 4fdaa4c8..fbec9ba6 100644 --- a/skimage/color/tests/test_colorconv.py +++ b/skimage/color/tests/test_colorconv.py @@ -34,6 +34,7 @@ from skimage.color import (rgb2hsv, hsv2rgb, xyz2lab, lab2xyz, lab2rgb, rgb2lab, is_rgb, is_gray, + lab2lch, lch2lab, guess_spatial_dimensions ) @@ -249,6 +250,43 @@ class TestColorconv(TestCase): img_rgb = img_as_float(self.img_rgb) assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb) + def test_lab_lch_roundtrip(self): + rgb = img_as_float(self.img_rgb) + lab = rgb2lab(rgb) + lab2 = lch2lab(lab2lch(lab)) + assert_array_almost_equal(lab2, lab) + + def test_rgb_lch_roundtrip(self): + rgb = img_as_float(self.img_rgb) + lab = rgb2lab(rgb) + lch = lab2lch(lab) + lab2 = lch2lab(lch) + rgb2 = lab2rgb(lab2) + assert_array_almost_equal(rgb, rgb2) + + def test_lab_lch_0d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch2 = lab2lch(lab0[None, None, :]) + assert_array_almost_equal(lch0, lch2[0, 0, :]) + + def test_lab_lch_1d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch1 = lab2lch(lab0[None, :]) + assert_array_almost_equal(lch0, lch1[0, :]) + + def test_lab_lch_3d(self): + lab0 = self._get_lab0() + lch0 = lab2lch(lab0) + lch3 = lab2lch(lab0[None, None, None, :]) + assert_array_almost_equal(lch0, lch3[0, 0, 0, :]) + + def _get_lab0(self): + rgb = img_as_float(self.img_rgb[:1, :1, :]) + return rgb2lab(rgb)[0, 0, :] + + def test_gray2rgb(): x = np.array([0, 0.5, 1]) assert_raises(ValueError, gray2rgb, x) diff --git a/skimage/color/tests/test_delta_e.py b/skimage/color/tests/test_delta_e.py new file mode 100644 index 00000000..84f13b48 --- /dev/null +++ b/skimage/color/tests/test_delta_e.py @@ -0,0 +1,167 @@ +"""Test for correctness of color distance functions""" +from os.path import abspath, dirname, join as pjoin + +import numpy as np +from numpy.testing import assert_allclose + +from skimage.color import (deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc) + + +def test_ciede2000_dE(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede2000(lab1, lab2) + + assert_allclose(dE2, data['dE'], rtol=1.e-4) + + +def load_ciede2000_data(): + dtype = [('pair', int), + ('1', int), + ('L1', float), + ('a1', float), + ('b1', float), + ('a1_prime', float), + ('C1_prime', float), + ('h1_prime', float), + ('hbar_prime', float), + ('G', float), + ('T', float), + ('SL', float), + ('SC', float), + ('SH', float), + ('RT', float), + ('dE', float), + ('2', int), + ('L2', float), + ('a2', float), + ('b2', float), + ('a2_prime', float), + ('C2_prime', float), + ('h2_prime', float), + ] + + # note: ciede_test_data.txt contains several intermediate quantities + path = pjoin(dirname(abspath(__file__)), 'ciede2000_test_data.txt') + return np.loadtxt(path, dtype=dtype) + + +def test_cie76(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_cie76(lab1, lab2) + oracle = np.array([ + 4.00106328, 6.31415011, 9.1776999, 2.06270077, 2.36957073, + 2.91529271, 2.23606798, 2.23606798, 4.98000036, 4.9800004, + 4.98000044, 4.98000049, 4.98000036, 4.9800004, 4.98000044, + 3.53553391, 36.86800781, 31.91002977, 30.25309901, 27.40894015, + 0.89242934, 0.7972, 0.8583065, 0.82982507, 3.1819238, + 2.21334297, 1.53890382, 4.60630929, 6.58467989, 3.88641412, + 1.50514845, 2.3237848, 0.94413208, 1.31910843 + ]) + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_ciede94(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_ciede94(lab1, lab2) + oracle = np.array([ + 1.39503887, 1.93410055, 2.45433566, 0.68449187, 0.6695627, + 0.69194527, 2.23606798, 2.03163832, 4.80069441, 4.80069445, + 4.80069449, 4.80069453, 4.80069441, 4.80069445, 4.80069449, + 3.40774352, 34.6891632, 29.44137328, 27.91408781, 24.93766082, + 0.82213163, 0.71658427, 0.8048753, 0.75284394, 1.39099471, + 1.24808929, 1.29795787, 1.82045088, 2.55613309, 1.42491303, + 1.41945261, 2.3225685, 0.93853308, 1.30654464 + ]) + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_cmc(): + data = load_ciede2000_data() + N = len(data) + lab1 = np.zeros((N, 3)) + lab1[:, 0] = data['L1'] + lab1[:, 1] = data['a1'] + lab1[:, 2] = data['b1'] + + lab2 = np.zeros((N, 3)) + lab2[:, 0] = data['L2'] + lab2[:, 1] = data['a2'] + lab2[:, 2] = data['b2'] + + dE2 = deltaE_cmc(lab1, lab2) + oracle = np.array([ + 1.73873611, 2.49660844, 3.30494501, 0.85735576, 0.88332927, + 0.97822692, 3.50480874, 2.87930032, 6.5783807, 6.57838075, + 6.5783808, 6.57838086, 6.67492321, 6.67492326, 6.67492331, + 4.66852997, 42.10875485, 39.45889064, 38.36005919, 33.93663807, + 1.14400168, 1.00600419, 1.11302547, 1.05335328, 1.42822951, + 1.2548143, 1.76838061, 2.02583367, 3.08695508, 1.74893533, + 1.90095165, 1.70258148, 1.80317207, 2.44934417 + ]) + + assert_allclose(dE2, oracle, rtol=1.e-8) + + +def test_single_color_cie76(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_cie76(lab1, lab2) + + +def test_single_color_ciede94(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_ciede94(lab1, lab2) + + +def test_single_color_ciede2000(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_ciede2000(lab1, lab2) + + +def test_single_color_cmc(): + lab1 = (0.5, 0.5, 0.5) + lab2 = (0.4, 0.4, 0.4) + deltaE_cmc(lab1, lab2) + + +if __name__ == "__main__": + from numpy.testing import run_module_suite + run_module_suite()