1"""Tests for color conversion functions.
2
3Authors
4-------
5- the rgb2hsv test was written by Nicolas Pinto, 2009
6- other tests written by Ralf Gommers, 2009
7
8:license: modified BSD
9"""
10
11import colorsys
12import numpy as np
13import pytest
14from numpy.testing import (assert_almost_equal, assert_array_almost_equal,
15                           assert_equal)
16
17from skimage import data
18from skimage._shared._warnings import expected_warnings
19from skimage._shared.testing import fetch
20from skimage._shared.utils import _supported_float_type, slice_at_axis
21from skimage.color import (rgb2hsv, hsv2rgb,
22                           rgb2xyz, xyz2rgb,
23                           rgb2hed, hed2rgb,
24                           separate_stains,
25                           combine_stains,
26                           rgb2rgbcie, rgbcie2rgb,
27                           convert_colorspace,
28                           rgb2gray, gray2rgb,
29                           xyz2lab, lab2xyz,
30                           lab2rgb, rgb2lab,
31                           xyz2luv, luv2xyz,
32                           luv2rgb, rgb2luv,
33                           lab2lch, lch2lab,
34                           rgb2yuv, yuv2rgb,
35                           rgb2yiq, yiq2rgb,
36                           rgb2ypbpr, ypbpr2rgb,
37                           rgb2ycbcr, ycbcr2rgb,
38                           rgb2ydbdr, ydbdr2rgb,
39                           rgba2rgb, gray2rgba)
40from skimage.util import img_as_float, img_as_ubyte, img_as_float32
41
42
43class TestColorconv():
44
45    img_rgb = data.colorwheel()
46    img_grayscale = data.camera()
47    img_rgba = np.array([[[0, 0.5, 1, 0],
48                          [0, 0.5, 1, 1],
49                          [0, 0.5, 1, 0.5]]]).astype(float)
50    img_stains = img_as_float(img_rgb) * 0.3
51
52    colbars = np.array([[1, 1, 0, 0, 1, 1, 0, 0],
53                        [1, 1, 1, 1, 0, 0, 0, 0],
54                        [1, 0, 1, 0, 1, 0, 1, 0]]).astype(float)
55    colbars_array = np.swapaxes(colbars.reshape(3, 4, 2), 0, 2)
56    colbars_point75 = colbars * 0.75
57    colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2)
58
59    xyz_array = np.array([[[0.4124, 0.21260, 0.01930]],    # red
60                          [[0, 0, 0]],    # black
61                          [[.9505, 1., 1.089]],    # white
62                          [[.1805, .0722, .9505]],    # blue
63                          [[.07719, .15438, .02573]],    # green
64                          ])
65    lab_array = np.array([[[53.233, 80.109, 67.220]],    # red
66                          [[0., 0., 0.]],    # black
67                          [[100.0, 0.005, -0.010]],    # white
68                          [[32.303, 79.197, -107.864]],    # blue
69                          [[46.229, -51.7, 49.898]],    # green
70                          ])
71
72    luv_array = np.array([[[53.233, 175.053, 37.751]],   # red
73                          [[0., 0., 0.]],   # black
74                          [[100., 0.001, -0.017]],   # white
75                          [[32.303, -9.400, -130.358]],   # blue
76                          [[46.228, -43.774, 56.589]],   # green
77                          ])
78
79    # RGBA to RGB
80    @pytest.mark.parametrize("channel_axis", [0, 1, 2, -1, -2, -3])
81    def test_rgba2rgb_conversion(self, channel_axis):
82        rgba = self.img_rgba
83
84        rgba = np.moveaxis(rgba, source=-1, destination=channel_axis)
85        rgb = rgba2rgb(rgba, channel_axis=channel_axis)
86        rgb = np.moveaxis(rgb, source=channel_axis, destination=-1)
87
88        expected = np.array([[[1, 1, 1],
89                              [0, 0.5, 1],
90                              [0.5, 0.75, 1]]]).astype(float)
91        assert_equal(rgb.shape, expected.shape)
92        assert_almost_equal(rgb, expected)
93
94    def test_rgba2rgb_error_grayscale(self):
95        with pytest.raises(ValueError):
96            rgba2rgb(self.img_grayscale)
97
98    @pytest.mark.parametrize("channel_axis", [None, 1.5])
99    def test_rgba2rgb_error_channel_axis_invalid(self, channel_axis):
100        with pytest.raises(TypeError):
101            rgba2rgb(self.img_rgba, channel_axis=channel_axis)
102
103    @pytest.mark.parametrize("channel_axis", [-4, 3])
104    def test_rgba2rgb_error_channel_axis_out_of_range(self, channel_axis):
105        with pytest.raises(np.AxisError):
106            rgba2rgb(self.img_rgba, channel_axis=channel_axis)
107
108    def test_rgba2rgb_error_rgb(self):
109        with pytest.raises(ValueError):
110            rgba2rgb(self.img_rgb)
111
112    def test_rgba2rgb_dtype(self):
113        rgba = self.img_rgba.astype('float64')
114        rgba32 = img_as_float32(rgba)
115
116        assert rgba2rgb(rgba).dtype == rgba.dtype
117        assert rgba2rgb(rgba32).dtype == rgba32.dtype
118
119    # RGB to HSV
120    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
121    def test_rgb2hsv_conversion(self, channel_axis):
122        rgb = img_as_float(self.img_rgb)[::16, ::16]
123
124        _rgb = np.moveaxis(rgb, source=-1, destination=channel_axis)
125        hsv = rgb2hsv(_rgb, channel_axis=channel_axis)
126        hsv = np.moveaxis(hsv, source=channel_axis, destination=-1)
127        hsv = hsv.reshape(-1, 3)
128
129        # ground truth from colorsys
130        gt = np.array([colorsys.rgb_to_hsv(pt[0], pt[1], pt[2])
131                       for pt in rgb.reshape(-1, 3)]
132                      )
133        assert_almost_equal(hsv, gt)
134
135    def test_rgb2hsv_error_grayscale(self):
136        with pytest.raises(ValueError):
137            rgb2hsv(self.img_grayscale)
138
139    def test_rgb2hsv_dtype(self):
140        rgb = img_as_float(self.img_rgb)
141        rgb32 = img_as_float32(self.img_rgb)
142
143        assert rgb2hsv(rgb).dtype == rgb.dtype
144        assert rgb2hsv(rgb32).dtype == rgb32.dtype
145
146    # HSV to RGB
147    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
148    def test_hsv2rgb_conversion(self, channel_axis):
149        rgb = self.img_rgb.astype("float32")[::16, ::16]
150        # create HSV image with colorsys
151        hsv = np.array([colorsys.rgb_to_hsv(pt[0], pt[1], pt[2])
152                        for pt in rgb.reshape(-1, 3)]).reshape(rgb.shape)
153
154        hsv = np.moveaxis(hsv, source=-1, destination=channel_axis)
155        _rgb = hsv2rgb(hsv, channel_axis=channel_axis)
156        _rgb = np.moveaxis(_rgb, source=channel_axis, destination=-1)
157
158        # convert back to RGB and compare with original.
159        # relative precision for RGB -> HSV roundtrip is about 1e-6
160        assert_almost_equal(rgb, _rgb, decimal=4)
161
162    def test_hsv2rgb_error_grayscale(self):
163        with pytest.raises(ValueError):
164            hsv2rgb(self.img_grayscale)
165
166    def test_hsv2rgb_dtype(self):
167        rgb = self.img_rgb.astype("float32")[::16, ::16]
168        # create HSV image with colorsys
169        hsv = np.array([colorsys.rgb_to_hsv(pt[0], pt[1], pt[2])
170                        for pt in rgb.reshape(-1, 3)],
171                       dtype='float64').reshape(rgb.shape)
172        hsv32 = hsv.astype('float32')
173
174        assert hsv2rgb(hsv).dtype == hsv.dtype
175        assert hsv2rgb(hsv32).dtype == hsv32.dtype
176
177    # RGB to XYZ
178    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
179    def test_rgb2xyz_conversion(self, channel_axis):
180        gt = np.array([[[0.950456, 1.      , 1.088754],
181                        [0.538003, 0.787329, 1.06942 ],
182                        [0.592876, 0.28484 , 0.969561],
183                        [0.180423, 0.072169, 0.950227]],
184                       [[0.770033, 0.927831, 0.138527],
185                        [0.35758 , 0.71516 , 0.119193],
186                        [0.412453, 0.212671, 0.019334],
187                        [0.      , 0.      , 0.      ]]])
188
189        img = np.moveaxis(
190            self.colbars_array, source=-1, destination=channel_axis
191        )
192        out = rgb2xyz(img, channel_axis=channel_axis)
193        out = np.moveaxis(out, source=channel_axis, destination=-1)
194
195        assert_almost_equal(out, gt)
196
197    # stop repeating the "raises" checks for all other functions that are
198    # implemented with color._convert()
199    def test_rgb2xyz_error_grayscale(self):
200        with pytest.raises(ValueError):
201            rgb2xyz(self.img_grayscale)
202
203    def test_rgb2xyz_dtype(self):
204        img = self.colbars_array
205        img32 = img.astype('float32')
206
207        assert rgb2xyz(img).dtype == img.dtype
208        assert rgb2xyz(img32).dtype == img32.dtype
209
210    # XYZ to RGB
211    def test_xyz2rgb_conversion(self):
212        assert_almost_equal(xyz2rgb(rgb2xyz(self.colbars_array)),
213                            self.colbars_array)
214
215    def test_xyz2rgb_dtype(self):
216        img = rgb2xyz(self.colbars_array)
217        img32 = img.astype('float32')
218
219        assert xyz2rgb(img).dtype == img.dtype
220        assert xyz2rgb(img32).dtype == img32.dtype
221
222    # RGB<->XYZ roundtrip on another image
223    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
224    def test_xyz_rgb_roundtrip(self, channel_axis):
225        img_rgb = img_as_float(self.img_rgb)
226
227        img_rgb = np.moveaxis(img_rgb, source=-1, destination=channel_axis)
228        round_trip = xyz2rgb(rgb2xyz(img_rgb, channel_axis=channel_axis),
229                             channel_axis=channel_axis)
230
231        assert_array_almost_equal(round_trip, img_rgb)
232
233    # HED<->RGB roundtrip with ubyte image
234    def test_hed_rgb_roundtrip(self):
235        img_in = img_as_ubyte(self.img_stains)
236        img_out = rgb2hed(hed2rgb(img_in))
237        assert_equal(img_as_ubyte(img_out), img_in)
238
239    # HED<->RGB roundtrip with float image
240    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
241    def test_hed_rgb_float_roundtrip(self, channel_axis):
242        img_in = self.img_stains
243        img_in = np.moveaxis(img_in, source=-1, destination=channel_axis)
244        img_out = rgb2hed(
245            hed2rgb(img_in, channel_axis=channel_axis),
246            channel_axis=channel_axis
247        )
248        assert_array_almost_equal(img_out, img_in)
249
250    # BRO<->RGB roundtrip with ubyte image
251    def test_bro_rgb_roundtrip(self):
252        from skimage.color.colorconv import bro_from_rgb, rgb_from_bro
253        img_in = img_as_ubyte(self.img_stains)
254        img_out = combine_stains(img_in, rgb_from_bro)
255        img_out = separate_stains(img_out, bro_from_rgb)
256        assert_equal(img_as_ubyte(img_out), img_in)
257
258    # BRO<->RGB roundtrip with float image
259    @pytest.mark.parametrize("channel_axis", [0, 1, -1])
260    def test_bro_rgb_roundtrip_float(self, channel_axis):
261        from skimage.color.colorconv import bro_from_rgb, rgb_from_bro
262        img_in = self.img_stains
263        img_in = np.moveaxis(img_in, source=-1, destination=channel_axis)
264        img_out = combine_stains(
265            img_in, rgb_from_bro, channel_axis=channel_axis
266        )
267        img_out = separate_stains(
268            img_out, bro_from_rgb, channel_axis=channel_axis
269        )
270        assert_array_almost_equal(img_out, img_in)
271
272    # RGB to RGB CIE
273    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
274    def test_rgb2rgbcie_conversion(self, channel_axis):
275        gt = np.array([[[ 0.1488856 ,  0.18288098,  0.19277574],
276                        [ 0.01163224,  0.16649536,  0.18948516],
277                        [ 0.12259182,  0.03308008,  0.17298223],
278                        [-0.01466154,  0.01669446,  0.16969164]],
279                       [[ 0.16354714,  0.16618652,  0.0230841 ],
280                        [ 0.02629378,  0.1498009 ,  0.01979351],
281                        [ 0.13725336,  0.01638562,  0.00329059],
282                        [ 0.        ,  0.        ,  0.        ]]])
283
284        img = np.moveaxis(
285            self.colbars_array, source=-1, destination=channel_axis
286        )
287        out = rgb2rgbcie(img, channel_axis=channel_axis)
288
289        out = np.moveaxis(out, source=channel_axis, destination=-1)
290
291        assert_almost_equal(out, gt)
292
293    def test_rgb2rgbcie_dtype(self):
294        img = self.colbars_array.astype('float64')
295        img32 = img.astype('float32')
296
297        assert rgb2rgbcie(img).dtype == img.dtype
298        assert rgb2rgbcie(img32).dtype == img32.dtype
299
300    # RGB CIE to RGB
301    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
302    def test_rgbcie2rgb_conversion(self, channel_axis):
303        rgb = np.moveaxis(
304            self.colbars_array, source=-1, destination=channel_axis
305        )
306        round_trip = rgbcie2rgb(rgb2rgbcie(rgb, channel_axis=channel_axis),
307                                channel_axis=channel_axis)
308        # only roundtrip test, we checked rgb2rgbcie above already
309        assert_almost_equal(round_trip, rgb)
310
311    def test_rgbcie2rgb_dtype(self):
312        img = rgb2rgbcie(self.colbars_array).astype('float64')
313        img32 = img.astype('float32')
314
315        assert rgbcie2rgb(img).dtype == img.dtype
316        assert rgbcie2rgb(img32).dtype == img32.dtype
317
318    @pytest.mark.parametrize("channel_axis", [0, -1])
319    def test_convert_colorspace(self, channel_axis):
320        colspaces = ['HSV', 'RGB CIE', 'XYZ', 'YCbCr', 'YPbPr', 'YDbDr']
321        colfuncs_from = [
322            hsv2rgb, rgbcie2rgb, xyz2rgb,
323            ycbcr2rgb, ypbpr2rgb, ydbdr2rgb
324        ]
325        colfuncs_to = [
326            rgb2hsv, rgb2rgbcie, rgb2xyz,
327            rgb2ycbcr, rgb2ypbpr, rgb2ydbdr
328        ]
329
330        colbars_array = np.moveaxis(
331            self.colbars_array, source=-1, destination=channel_axis
332        )
333
334        kw = dict(channel_axis=channel_axis)
335
336        assert_almost_equal(
337            convert_colorspace(colbars_array, 'RGB', 'RGB', **kw),
338            colbars_array)
339
340        for i, space in enumerate(colspaces):
341            gt = colfuncs_from[i](colbars_array, **kw)
342            assert_almost_equal(
343                convert_colorspace(colbars_array, space, 'RGB', **kw), gt)
344            gt = colfuncs_to[i](colbars_array, **kw)
345            assert_almost_equal(
346                convert_colorspace(colbars_array, 'RGB', space, **kw), gt)
347
348        with pytest.raises(ValueError):
349            convert_colorspace(self.colbars_array, 'nokey', 'XYZ')
350        with pytest.raises(ValueError):
351            convert_colorspace(self.colbars_array, 'RGB', 'nokey')
352
353    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
354    def test_rgb2gray(self, channel_axis):
355        x = np.array([1, 1, 1]).reshape((1, 1, 3)).astype(float)
356        x = np.moveaxis(x, source=-1, destination=channel_axis)
357        g = rgb2gray(x, channel_axis=channel_axis)
358        assert_array_almost_equal(g, 1)
359
360        assert_equal(g.shape, (1, 1))
361
362    def test_rgb2gray_contiguous(self):
363        x = np.random.rand(10, 10, 3)
364        assert rgb2gray(x).flags["C_CONTIGUOUS"]
365        assert rgb2gray(x[:5, :5]).flags["C_CONTIGUOUS"]
366
367    def test_rgb2gray_alpha(self):
368        x = np.empty((10, 10, 4))
369        with pytest.raises(ValueError):
370            rgb2gray(x)
371
372    def test_rgb2gray_on_gray(self):
373        with pytest.raises(ValueError):
374            rgb2gray(np.empty((5, 5)))
375
376    def test_rgb2gray_dtype(self):
377        img = np.random.rand(10, 10, 3).astype('float64')
378        img32 = img.astype('float32')
379
380        assert rgb2gray(img).dtype == img.dtype
381        assert rgb2gray(img32).dtype == img32.dtype
382
383    # test matrices for xyz2lab and lab2xyz generated using
384    # http://www.easyrgb.com/index.php?X=CALC
385    # Note: easyrgb website displays xyz*100
386    def test_xyz2lab(self):
387        assert_array_almost_equal(xyz2lab(self.xyz_array),
388                                  self.lab_array, decimal=3)
389
390        # Test the conversion with the rest of the illuminants.
391        for I in ["A", "B", "C", "d50", "d55", "d65"]:
392            I = I.lower()
393            for obs in ["2", "10", "R"]:
394                obs = obs.lower()
395                fname = f'color/tests/data/lab_array_{I}_{obs}.npy'
396                lab_array_I_obs = np.load(fetch(fname))
397                assert_array_almost_equal(lab_array_I_obs,
398                                          xyz2lab(self.xyz_array, I, obs),
399                                          decimal=2)
400        for I in ["d75", "e"]:
401            fname = f'color/tests/data/lab_array_{I}_2.npy'
402            lab_array_I_obs = np.load(fetch(fname))
403            assert_array_almost_equal(lab_array_I_obs,
404                                      xyz2lab(self.xyz_array, I, "2"),
405                                      decimal=2)
406
407    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
408    def test_xyz2lab_channel_axis(self, channel_axis):
409        # test conversion with channels along a specified axis
410        xyz = np.moveaxis(self.xyz_array, source=-1, destination=channel_axis)
411        lab = xyz2lab(xyz, channel_axis=channel_axis)
412        lab = np.moveaxis(lab, source=channel_axis, destination=-1)
413        assert_array_almost_equal(lab, self.lab_array, decimal=3)
414
415    def test_xyz2lab_dtype(self):
416        img = self.xyz_array.astype('float64')
417        img32 = img.astype('float32')
418
419        assert xyz2lab(img).dtype == img.dtype
420        assert xyz2lab(img32).dtype == img32.dtype
421
422    def test_lab2xyz(self):
423        assert_array_almost_equal(lab2xyz(self.lab_array),
424                                  self.xyz_array, decimal=3)
425
426        # Test the conversion with the rest of the illuminants.
427        for I in ["A", "B", "C", "d50", "d55", "d65"]:
428            I = I.lower()
429            for obs in ["2", "10", "R"]:
430                obs = obs.lower()
431                fname = f'color/tests/data/lab_array_{I}_{obs}.npy'
432                lab_array_I_obs = np.load(fetch(fname))
433                assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, obs),
434                                          self.xyz_array, decimal=3)
435        for I in ["d75", "e"]:
436            fname = f'color/tests/data/lab_array_{I}_2.npy'
437            lab_array_I_obs = np.load(fetch(fname))
438            assert_array_almost_equal(lab2xyz(lab_array_I_obs, I, "2"),
439                                      self.xyz_array, decimal=3)
440
441        # And we include a call to test the exception handling in the code.
442        with pytest.raises(ValueError):
443            lab2xyz(lab_array_I_obs, "NaI", "2")   # Not an illuminant
444
445        with pytest.raises(ValueError):
446            lab2xyz(lab_array_I_obs, "d50", "42")   # Not a degree
447
448    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
449    def test_lab2xyz_channel_axis(self, channel_axis):
450        # test conversion with channels along a specified axis
451        lab = np.moveaxis(self.lab_array, source=-1, destination=channel_axis)
452        xyz = lab2xyz(lab, channel_axis=channel_axis)
453        xyz = np.moveaxis(xyz, source=channel_axis, destination=-1)
454        assert_array_almost_equal(xyz, self.xyz_array, decimal=3)
455
456    def test_lab2xyz_dtype(self):
457        img = self.lab_array.astype('float64')
458        img32 = img.astype('float32')
459
460        assert lab2xyz(img).dtype == img.dtype
461        assert lab2xyz(img32).dtype == img32.dtype
462
463    def test_rgb2lab_brucelindbloom(self):
464        """
465        Test the RGB->Lab conversion by comparing to the calculator on the
466        authoritative Bruce Lindbloom
467        [website](http://brucelindbloom.com/index.html?ColorCalculator.html).
468        """
469        # Obtained with D65 white point, sRGB model and gamma
470        gt_for_colbars = np.array([
471            [100, 0, 0],
472            [97.1393, -21.5537, 94.4780],
473            [91.1132, -48.0875, -14.1312],
474            [87.7347, -86.1827, 83.1793],
475            [60.3242, 98.2343, -60.8249],
476            [53.2408, 80.0925, 67.2032],
477            [32.2970, 79.1875, -107.8602],
478            [0, 0, 0]]).T
479        gt_array = np.swapaxes(gt_for_colbars.reshape(3, 4, 2), 0, 2)
480        assert_array_almost_equal(
481            rgb2lab(self.colbars_array), gt_array, decimal=2
482        )
483
484    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
485    def test_lab_rgb_roundtrip(self, channel_axis):
486        img_rgb = img_as_float(self.img_rgb)
487        img_rgb = np.moveaxis(img_rgb, source=-1, destination=channel_axis)
488        assert_array_almost_equal(
489            lab2rgb(
490                rgb2lab(img_rgb, channel_axis=channel_axis),
491                channel_axis=channel_axis
492            ),
493            img_rgb,
494        )
495
496    def test_rgb2lab_dtype(self):
497        img = self.colbars_array.astype('float64')
498        img32 = img.astype('float32')
499
500        assert rgb2lab(img).dtype == img.dtype
501        assert rgb2lab(img32).dtype == img32.dtype
502
503    def test_lab2rgb_dtype(self):
504        img = self.lab_array.astype('float64')
505        img32 = img.astype('float32')
506
507        assert lab2rgb(img).dtype == img.dtype
508        assert lab2rgb(img32).dtype == img32.dtype
509
510    # test matrices for xyz2luv and luv2xyz generated using
511    # http://www.easyrgb.com/index.php?X=CALC
512    # Note: easyrgb website displays xyz*100
513    def test_xyz2luv(self):
514        assert_array_almost_equal(xyz2luv(self.xyz_array),
515                                  self.luv_array, decimal=3)
516
517        # Test the conversion with the rest of the illuminants.
518        for I in ["A", "B", "C", "d50", "d55", "d65"]:
519            I = I.lower()
520            for obs in ["2", "10", "R"]:
521                obs = obs.lower()
522                fname = f'color/tests/data/luv_array_{I}_{obs}.npy'
523                luv_array_I_obs = np.load(fetch(fname))
524                assert_array_almost_equal(luv_array_I_obs,
525                                          xyz2luv(self.xyz_array, I, obs),
526                                          decimal=2)
527        for I in ["d75", "e"]:
528            fname = f'color/tests/data/luv_array_{I}_2.npy'
529            luv_array_I_obs = np.load(fetch(fname))
530            assert_array_almost_equal(luv_array_I_obs,
531                                      xyz2luv(self.xyz_array, I, "2"),
532                                      decimal=2)
533
534    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
535    def test_xyz2luv_channel_axis(self, channel_axis):
536        # test conversion with channels along a specified axis
537        xyz = np.moveaxis(self.xyz_array, source=-1, destination=channel_axis)
538        luv = xyz2luv(xyz, channel_axis=channel_axis)
539        luv = np.moveaxis(luv, source=channel_axis, destination=-1)
540        assert_array_almost_equal(luv, self.luv_array, decimal=3)
541
542    def test_xyz2luv_dtype(self):
543        img = self.xyz_array.astype('float64')
544        img32 = img.astype('float32')
545
546        assert xyz2luv(img).dtype == img.dtype
547        assert xyz2luv(img32).dtype == img32.dtype
548
549    def test_luv2xyz(self):
550        assert_array_almost_equal(luv2xyz(self.luv_array),
551                                  self.xyz_array, decimal=3)
552
553        # Test the conversion with the rest of the illuminants.
554        for I in ["A", "B", "C", "d50", "d55", "d65"]:
555            I = I.lower()
556            for obs in ["2", "10", "R"]:
557                obs = obs.lower()
558                fname = f'color/tests/data/luv_array_{I}_{obs}.npy'
559                luv_array_I_obs = np.load(fetch(fname))
560                assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, obs),
561                                          self.xyz_array, decimal=3)
562        for I in ["d75", "e"]:
563            fname = f'color/tests/data/luv_array_{I}_2.npy'
564            luv_array_I_obs = np.load(fetch(fname))
565            assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, "2"),
566                                      self.xyz_array, decimal=3)
567
568    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
569    def test_luv2xyz_channel_axis(self, channel_axis):
570        # test conversion with channels along a specified axis
571        luv = np.moveaxis(self.luv_array, source=-1, destination=channel_axis)
572        xyz = luv2xyz(luv, channel_axis=channel_axis)
573        xyz = np.moveaxis(xyz, source=channel_axis, destination=-1)
574        assert_array_almost_equal(xyz, self.xyz_array, decimal=3)
575
576    def test_luv2xyz_dtype(self):
577        img = self.luv_array.astype('float64')
578        img32 = img.astype('float32')
579
580        assert luv2xyz(img).dtype == img.dtype
581        assert luv2xyz(img32).dtype == img32.dtype
582
583    def test_rgb2luv_brucelindbloom(self):
584        """
585        Test the RGB->Lab conversion by comparing to the calculator on the
586        authoritative Bruce Lindbloom
587        [website](http://brucelindbloom.com/index.html?ColorCalculator.html).
588        """
589        # Obtained with D65 white point, sRGB model and gamma
590        gt_for_colbars = np.array([
591            [100, 0, 0],
592            [97.1393, 7.7056, 106.7866],
593            [91.1132, -70.4773, -15.2042],
594            [87.7347, -83.0776, 107.3985],
595            [60.3242, 84.0714, -108.6834],
596            [53.2408, 175.0151, 37.7564],
597            [32.2970, -9.4054, -130.3423],
598            [0, 0, 0]]).T
599        gt_array = np.swapaxes(gt_for_colbars.reshape(3, 4, 2), 0, 2)
600        assert_array_almost_equal(rgb2luv(self.colbars_array),
601                                  gt_array, decimal=2)
602
603    def test_rgb2luv_dtype(self):
604        img = self.colbars_array.astype('float64')
605        img32 = img.astype('float32')
606
607        assert rgb2luv(img).dtype == img.dtype
608        assert rgb2luv(img32).dtype == img32.dtype
609
610    def test_luv2rgb_dtype(self):
611        img = self.luv_array.astype('float64')
612        img32 = img.astype('float32')
613
614        assert luv2rgb(img).dtype == img.dtype
615        assert luv2rgb(img32).dtype == img32.dtype
616
617    @pytest.mark.parametrize("channel_axis", [0, 1, -1 -2])
618    def test_luv_rgb_roundtrip(self, channel_axis):
619        img_rgb = img_as_float(self.img_rgb)
620        img_rgb = np.moveaxis(img_rgb, source=-1, destination=channel_axis)
621        assert_array_almost_equal(
622            luv2rgb(
623                rgb2luv(img_rgb, channel_axis=channel_axis),
624                channel_axis=channel_axis
625            ),
626            img_rgb,
627        )
628
629    def test_lab_rgb_outlier(self):
630        lab_array = np.ones((3, 1, 3))
631        lab_array[0] = [50, -12, 85]
632        lab_array[1] = [50, 12, -85]
633        lab_array[2] = [90, -4, -47]
634        rgb_array = np.array([[[0.501, 0.481, 0]],
635                              [[0, 0.482, 1.]],
636                              [[0.578, 0.914, 1.]],
637                              ])
638        assert_almost_equal(lab2rgb(lab_array), rgb_array, decimal=3)
639
640    def test_lab_full_gamut(self):
641        a, b = np.meshgrid(np.arange(-100, 100), np.arange(-100, 100))
642        L = np.ones(a.shape)
643        lab = np.dstack((L, a, b))
644        for value in [0, 10, 20]:
645            lab[:, :, 0] = value
646            with expected_warnings(['Color data out of range']):
647                lab2xyz(lab)
648
649    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
650    def test_lab_lch_roundtrip(self, channel_axis):
651        rgb = img_as_float(self.img_rgb)
652        rgb = np.moveaxis(rgb, source=-1, destination=channel_axis)
653        lab = rgb2lab(rgb, channel_axis=channel_axis)
654        lab2 = lch2lab(
655            lab2lch(lab, channel_axis=channel_axis),
656            channel_axis=channel_axis,
657        )
658        assert_array_almost_equal(lab2, lab)
659
660    def test_rgb_lch_roundtrip(self):
661        rgb = img_as_float(self.img_rgb)
662        lab = rgb2lab(rgb)
663        lch = lab2lch(lab)
664        lab2 = lch2lab(lch)
665        rgb2 = lab2rgb(lab2)
666        assert_array_almost_equal(rgb, rgb2)
667
668    def test_lab_lch_0d(self):
669        lab0 = self._get_lab0()
670        lch0 = lab2lch(lab0)
671        lch2 = lab2lch(lab0[None, None, :])
672        assert_array_almost_equal(lch0, lch2[0, 0, :])
673
674    def test_lab_lch_1d(self):
675        lab0 = self._get_lab0()
676        lch0 = lab2lch(lab0)
677        lch1 = lab2lch(lab0[None, :])
678        assert_array_almost_equal(lch0, lch1[0, :])
679
680    def test_lab_lch_3d(self):
681        lab0 = self._get_lab0()
682        lch0 = lab2lch(lab0)
683        lch3 = lab2lch(lab0[None, None, None, :])
684        assert_array_almost_equal(lch0, lch3[0, 0, 0, :])
685
686    def _get_lab0(self):
687        rgb = img_as_float(self.img_rgb[:1, :1, :])
688        return rgb2lab(rgb)[0, 0, :]
689
690    def test_yuv(self):
691        rgb = np.array([[[1.0, 1.0, 1.0]]])
692        assert_array_almost_equal(rgb2yuv(rgb), np.array([[[1, 0, 0]]]))
693        assert_array_almost_equal(rgb2yiq(rgb), np.array([[[1, 0, 0]]]))
694        assert_array_almost_equal(rgb2ypbpr(rgb), np.array([[[1, 0, 0]]]))
695        assert_array_almost_equal(
696            rgb2ycbcr(rgb), np.array([[[235, 128, 128]]])
697        )
698        assert_array_almost_equal(rgb2ydbdr(rgb), np.array([[[1, 0, 0]]]))
699        rgb = np.array([[[0.0, 1.0, 0.0]]])
700        assert_array_almost_equal(
701            rgb2yuv(rgb), np.array([[[0.587, -0.28886916, -0.51496512]]])
702        )
703        assert_array_almost_equal(
704            rgb2yiq(rgb), np.array([[[0.587, -0.27455667, -0.52273617]]])
705        )
706        assert_array_almost_equal(
707            rgb2ypbpr(rgb), np.array([[[0.587, -0.331264, -0.418688]]])
708        )
709        assert_array_almost_equal(
710            rgb2ycbcr(rgb), np.array([[[144.553, 53.797, 34.214]]])
711        )
712        assert_array_almost_equal(
713            rgb2ydbdr(rgb), np.array([[[0.587, -0.883, 1.116]]])
714        )
715
716    @pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
717    def test_yuv_roundtrip(self, channel_axis):
718        img_rgb = img_as_float(self.img_rgb)[::16, ::16]
719        img_rgb = np.moveaxis(img_rgb, source=-1, destination=channel_axis)
720        assert_array_almost_equal(
721            yuv2rgb(rgb2yuv(img_rgb, channel_axis=channel_axis),
722                    channel_axis=channel_axis),
723            img_rgb)
724        assert_array_almost_equal(
725            yiq2rgb(rgb2yiq(img_rgb, channel_axis=channel_axis),
726                    channel_axis=channel_axis),
727            img_rgb)
728        assert_array_almost_equal(
729            ypbpr2rgb(rgb2ypbpr(img_rgb, channel_axis=channel_axis),
730                      channel_axis=channel_axis),
731            img_rgb)
732        assert_array_almost_equal(
733            ycbcr2rgb(rgb2ycbcr(img_rgb, channel_axis=channel_axis),
734                      channel_axis=channel_axis),
735            img_rgb)
736        assert_array_almost_equal(
737            ydbdr2rgb(rgb2ydbdr(img_rgb, channel_axis=channel_axis),
738                      channel_axis=channel_axis),
739            img_rgb)
740
741    def test_rgb2yuv_dtype(self):
742        img = self.colbars_array.astype('float64')
743        img32 = img.astype('float32')
744
745        assert rgb2yuv(img).dtype == img.dtype
746        assert rgb2yuv(img32).dtype == img32.dtype
747
748    def test_yuv2rgb_dtype(self):
749        img = rgb2yuv(self.colbars_array).astype('float64')
750        img32 = img.astype('float32')
751
752        assert yuv2rgb(img).dtype == img.dtype
753        assert yuv2rgb(img32).dtype == img32.dtype
754
755    def test_rgb2yiq_conversion(self):
756        rgb = img_as_float(self.img_rgb)[::16, ::16]
757        yiq = rgb2yiq(rgb).reshape(-1, 3)
758        gt = np.array([colorsys.rgb_to_yiq(pt[0], pt[1], pt[2])
759                       for pt in rgb.reshape(-1, 3)]
760                      )
761        assert_almost_equal(yiq, gt, decimal=2)
762
763
764def test_gray2rgb():
765    x = np.array([0, 0.5, 1])
766    w = gray2rgb(x)
767    expected_output = np.array([[ 0, 0, 0 ],
768                                [ 0.5, 0.5, 0.5, ],
769                                [ 1, 1, 1 ]])
770
771    assert_equal(w, expected_output)
772
773    x = x.reshape((3, 1))
774    y = gray2rgb(x)
775
776    assert_equal(y.shape, (3, 1, 3))
777    assert_equal(y.dtype, x.dtype)
778    assert_equal(y[..., 0], x)
779    assert_equal(y[0, 0, :], [0, 0, 0])
780
781    x = np.array([[0, 128, 255]], dtype=np.uint8)
782    z = gray2rgb(x)
783
784    assert_equal(z.shape, (1, 3, 3))
785    assert_equal(z[..., 0], x)
786    assert_equal(z[0, 1, :], [128, 128, 128])
787
788
789def test_gray2rgb_rgb():
790    x = np.random.rand(5, 5, 4)
791    y = gray2rgb(x)
792    assert y.shape == (x.shape + (3,))
793    for i in range(3):
794        assert_equal(x, y[..., i])
795
796
797@pytest.mark.parametrize("shape", [(5, 5), (5, 5, 4), (5, 4, 5, 4)])
798@pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
799def test_gray2rgba(shape, channel_axis):
800    # nD case
801    img = np.random.random(shape)
802    rgba = gray2rgba(img, channel_axis=channel_axis)
803    assert rgba.ndim == img.ndim + 1
804
805    # Shape check
806    new_axis_loc = channel_axis % rgba.ndim
807    assert_equal(rgba.shape,
808                 shape[:new_axis_loc] + (4, ) + shape[new_axis_loc:])
809
810    # dtype check
811    assert rgba.dtype == img.dtype
812
813    # RGB channels check
814    for channel in range(3):
815        assert_equal(rgba[slice_at_axis(channel, axis=new_axis_loc)], img)
816
817    # Alpha channel check
818    assert_equal(rgba[slice_at_axis(3, axis=new_axis_loc)], 1.0)
819
820
821@pytest.mark.parametrize("shape", [(5, 5), (5, 5, 4), (5, 4, 5, 4)])
822@pytest.mark.parametrize("channel_axis", [0, 1, -1, -2])
823def test_gray2rgb_channel_axis(shape, channel_axis):
824    # nD case
825    img = np.random.random(shape)
826    rgb = gray2rgb(img, channel_axis=channel_axis)
827    assert rgb.ndim == img.ndim + 1
828
829    # Shape check
830    new_axis_loc = channel_axis % rgb.ndim
831    assert_equal(rgb.shape,
832                 shape[:new_axis_loc] + (3, ) + shape[new_axis_loc:])
833
834    # dtype check
835    assert rgb.dtype == img.dtype
836
837
838def test_gray2rgba_dtype():
839    img_f64 = np.random.random((5, 5))
840    img_f32 = img_f64.astype('float32')
841    img_u8 = img_as_ubyte(img_f64)
842    img_int = img_u8.astype(int)
843
844    for img in [img_f64, img_f32, img_u8, img_int]:
845        assert gray2rgba(img).dtype == img.dtype
846
847
848def test_gray2rgba_alpha():
849    img = np.random.random((5, 5))
850    img_u8 = img_as_ubyte(img)
851
852    # Default
853    alpha = None
854    rgba = gray2rgba(img, alpha)
855
856    assert_equal(rgba[..., :3], gray2rgb(img))
857    assert_equal(rgba[..., 3], 1.0)
858
859    # Scalar
860    alpha = 0.5
861    rgba = gray2rgba(img, alpha)
862
863    assert_equal(rgba[..., :3], gray2rgb(img))
864    assert_equal(rgba[..., 3], alpha)
865
866    # Array
867    alpha = np.random.random((5, 5))
868    rgba = gray2rgba(img, alpha)
869
870    assert_equal(rgba[..., :3], gray2rgb(img))
871    assert_equal(rgba[..., 3], alpha)
872
873    # Warning about alpha cast
874    alpha = 0.5
875    with expected_warnings(["alpha cannot be safely cast to image dtype"]):
876        rgba = gray2rgba(img_u8, alpha)
877        assert_equal(rgba[..., :3], gray2rgb(img_u8))
878
879    # Invalid shape
880    alpha = np.random.random((5, 5, 1))
881    expected_err_msg = ("alpha.shape must match image.shape")
882
883    with pytest.raises(ValueError) as err:
884        rgba = gray2rgba(img, alpha)
885    assert expected_err_msg == str(err.value)
886
887
888@pytest.mark.parametrize("func", [rgb2gray, gray2rgb, gray2rgba])
889@pytest.mark.parametrize("shape", ([(3, ), (2, 3), (4, 5, 3), (5, 4, 5, 3),
890                                    (4, 5, 4, 5, 3)]))
891def test_nD_gray_conversion(func, shape):
892    img = np.random.rand(*shape)
893    out = func(img)
894    common_ndim = min(out.ndim, len(shape))
895    assert out.shape[:common_ndim] == shape[:common_ndim]
896
897
898@pytest.mark.parametrize("func", [rgb2hsv, hsv2rgb,
899                                  rgb2xyz, xyz2rgb,
900                                  rgb2hed, hed2rgb,
901                                  rgb2rgbcie, rgbcie2rgb,
902                                  xyz2lab, lab2xyz,
903                                  lab2rgb, rgb2lab,
904                                  xyz2luv, luv2xyz,
905                                  luv2rgb, rgb2luv,
906                                  lab2lch, lch2lab,
907                                  rgb2yuv, yuv2rgb,
908                                  rgb2yiq, yiq2rgb,
909                                  rgb2ypbpr, ypbpr2rgb,
910                                  rgb2ycbcr, ycbcr2rgb,
911                                  rgb2ydbdr, ydbdr2rgb])
912@pytest.mark.parametrize("shape", ([(3, ), (2, 3), (4, 5, 3), (5, 4, 5, 3),
913                                    (4, 5, 4, 5, 3)]))
914def test_nD_color_conversion(func, shape):
915    img = np.random.rand(*shape)
916    out = func(img)
917
918    assert out.shape == img.shape
919
920
921@pytest.mark.parametrize("shape", ([(4, ), (2, 4), (4, 5, 4), (5, 4, 5, 4),
922                                    (4, 5, 4, 5, 4)]))
923def test_rgba2rgb_nD(shape):
924    img = np.random.rand(*shape)
925    out = rgba2rgb(img)
926
927    expected_shape = shape[:-1] + (3, )
928
929    assert out.shape == expected_shape
930
931
932@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
933def test_rgba2rgb_dtypes(dtype):
934    rgba = np.array([[[0, 0.5, 1, 0],
935                      [0, 0.5, 1, 1],
936                      [0, 0.5, 1, 0.5]]]).astype(dtype=dtype)
937    rgb = rgba2rgb(rgba)
938    float_dtype = _supported_float_type(rgba.dtype)
939    assert rgb.dtype == float_dtype
940    expected = np.array([[[1, 1, 1],
941                          [0, 0.5, 1],
942                          [0.5, 0.75, 1]]]).astype(float)
943    assert rgb.shape == expected.shape
944    assert_almost_equal(rgb, expected)
945
946
947@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
948def test_lab_lch_roundtrip_dtypes(dtype):
949    rgb = img_as_float(data.colorwheel()).astype(dtype=dtype, copy=False)
950    lab = rgb2lab(rgb)
951    float_dtype = _supported_float_type(dtype)
952    assert lab.dtype == float_dtype
953    lab2 = lch2lab(lab2lch(lab))
954    decimal = 4 if float_dtype == np.float32 else 7
955    assert_array_almost_equal(lab2, lab, decimal=decimal)
956
957
958@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
959def test_rgb2hsv_dtypes(dtype):
960    rgb = img_as_float(data.colorwheel())[::16, ::16]
961    rgb = rgb.astype(dtype=dtype, copy=False)
962    hsv = rgb2hsv(rgb).reshape(-1, 3)
963    float_dtype = _supported_float_type(dtype)
964    assert hsv.dtype == float_dtype
965    # ground truth from colorsys
966    gt = np.array([colorsys.rgb_to_hsv(pt[0], pt[1], pt[2])
967                   for pt in rgb.reshape(-1, 3)]
968                  )
969    decimal = 3 if float_dtype == np.float32 else 7
970    assert_array_almost_equal(hsv, gt, decimal=decimal)
971