diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 0f56ce4d..45836e7f 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -17,5 +17,5 @@ OpenCV functions and better OSX library loader - Ralf Gommers - Image IO and plots in documentation + Image IO, color spaces and plots in documentation diff --git a/scikits/image/color/colorconv.py b/scikits/image/color/colorconv.py index cd3c3b63..d7c5f4d2 100644 --- a/scikits/image/color/colorconv.py +++ b/scikits/image/color/colorconv.py @@ -1,34 +1,159 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -""" -:author: Nicolas Pinto, 2009 +"""Functions for converting between color spaces. + +The "central" color space in this module is RGB, more specifically the linear +sRGB color space using D65 as a white-point [1]_. This represents a +standard monitor (w/o gamma correction). For a good FAQ on color spaces see +[2]_. + +The API consists of functions to convert to and from RGB as defined above, as +well as a generic function to convert to and from any supported color space +(which is done through RGB in most cases). + + +Supported color spaces +---------------------- +* RGB : Red Green Blue. + Here the sRGB standard [1]_. +* HSV : Hue, Saturation, Value. + Uniquely defined when related to sRGB [3]_. +* RGB CIE : Red Green Blue. + The original RGB CIE standard from 1931 [4]_. Primary colors are 700 nm + (red), 546.1 nm (blue) and 435.8 nm (green). +* XYZ CIE : XYZ + 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. + +:author: Nicolas Pinto (rgb2hsv) +:author: Ralf Gommers (hsv2rgb) +:author: Travis Oliphant (XYZ and RGB CIE functions) + :license: modified BSD + +References +---------- +.. [1] Official specification of sRGB, IEC 61966-2-1:1999. +.. [2] http://www.poynton.com/ColorFAQ.html +.. [3] http://en.wikipedia.org/wiki/HSL_and_HSV +.. [4] http://en.wikipedia.org/wiki/CIE_1931_color_space """ from __future__ import division -__all__ = ["rgb2hsv"] +__all__ = ['convert_colorspace', 'rgb2hsv', 'hsv2rgb', 'rgb2xyz', 'xyz2rgb', + 'rgb2rgbcie', 'rgbcie2rgb'] + __docformat__ = "restructuredtext en" import numpy as np +from scipy import linalg + + +def convert_colorspace(arr, fromspace, tospace): + """Convert an image array to a new color space. + + Parameters + ---------- + arr : array_like + The image to convert. + fromspace : str + The color space to convert from. Valid color space strings are + ['RGB', 'HSV', 'RGB CIE', 'XYZ']. Value may also be specified as lower + case. + tospace : str + The color space to convert to. Valid color space strings are + ['RGB', 'HSV', 'RGB CIE', 'XYZ']. Value may also be specified as lower + case. + + Returns + ------- + newarr : ndarray + The converted image. + + 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. + + Examples + -------- + >>> import os + >>> from scikits.image import data_dir + >>> from scikits.image.io import imread + + >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena_hsv = convert_colorspace(lena, 'RGB', 'HSV') + """ + fromdict = {'RGB':lambda im: im, 'HSV':hsv2rgb, 'RGB CIE':rgbcie2rgb, + 'XYZ':xyz2rgb} + todict = {'RGB':lambda im:im, 'HSV':rgb2hsv, 'RGB CIE':rgb2rgbcie, + 'XYZ':rgb2xyz} + + fromspace = fromspace.upper() + tospace = tospace.upper() + if not fromspace in fromdict.keys(): + raise ValueError, 'fromspace needs to be one of %s'%fromdict.keys() + if not tospace in todict.keys(): + raise ValueError, 'tospace needs to be one of %s'%todict.keys() + + return todict[tospace](fromdict[fromspace](arr)) + + +def _prepare_colorarray(arr, dtype=np.float32): + """Check the shape of the array, and give it the requested type.""" + arr = np.asanyarray(arr) + + if arr.ndim != 3 or arr.shape[2] != 3: + msg = "the input array must be have a shape == (.,.,3))" + raise ValueError, msg + + return arr.astype(dtype) + def rgb2hsv(rgb): + """RGB to HSV color space conversion. + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in HSV format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `rgb` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + The conversion assumes an input data range of [0, 1] for all color components. + + Conversion between RGB and HSV color spaces results in some loss of + precision, due to integer arithmetic and rounding [1]_. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/HSL_and_HSV + + Examples + -------- + >>> import os + >>> from scikits.image import data_dir + >>> from scikits.image.io import imread + + >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena_hsv = color.rgb2hsv(lena) """ - RGB to HSV color space conversion - """ - - if type(rgb) != np.ndarray: - raise TypeError, "the input array 'rgb' must be a numpy.ndarray" - - if rgb.ndim != 3 or rgb.shape[2] != 3: - msg = "the input array 'rgb' must be have a shape == (.,.,3))" - raise ValueError, msg - - arr = rgb.astype("float32") + arr = _prepare_colorarray(rgb) out = np.empty_like(arr) - + # -- V channel out_v = arr.max(-1) @@ -39,15 +164,15 @@ def rgb2hsv(rgb): # -- H channel # red is max - idx = (arr[:,:,0] == out_v) + idx = (arr[:,:,0] == out_v) out[idx, 0] = (arr[idx, 1] - arr[idx, 2]) / delta[idx] # green is max - idx = (arr[:,:,1] == out_v) + idx = (arr[:,:,1] == out_v) out[idx, 0] = 2. + (arr[idx, 2] - arr[idx, 0] ) / delta[idx] # blue is max - idx = (arr[:,:,2] == out_v) + idx = (arr[:,:,2] == out_v) out[idx, 0] = 4. + (arr[idx, 0] - arr[idx, 1] ) / delta[idx] out_h = (out[:,:,0] / 6.) % 1. @@ -61,3 +186,266 @@ def rgb2hsv(rgb): return out + +def hsv2rgb(hsv): + """HSV to RGB color space conversion. + + Parameters + ---------- + hsv : array_like + The image in HSV 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 `hsv` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + The conversion assumes an input data range of [0, 1] for all color components. + + Conversion between RGB and HSV color spaces results in some loss of + precision, due to integer arithmetic and rounding [1]_. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/HSL_and_HSV + + Examples + -------- + >>> import os + >>> from scikits.image import data_dir + >>> from scikits.image.io import imread + + >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena_hsv = rgb2hsv(lena) + >>> lena_rgb = hsv2rgb(lena_hsv) + """ + arr = _prepare_colorarray(hsv) + + hi = np.floor(arr[:,:,0] * 6) + f = arr[:,:,0] * 6 - hi + p = arr[:,:,2] * (1 - arr[:,:,1]) + q = arr[:,:,2] * (1 - f * arr[:,:,1]) + t = arr[:,:,2] * (1 - (1 - f) * arr[:,:,1]) + v = arr[:,:,2] + + hi = np.dstack([hi, hi, hi]).astype(np.uint8) % 6 + out = np.choose(hi, [np.dstack((v, t, p)), + np.dstack((q, v, p)), + np.dstack((p, v, t)), + np.dstack((p, q, v)), + np.dstack((t, p, v)), + np.dstack((v, p, q))]) + + 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], + [0.212671, 0.715160, 0.072169], + [0.019334, 0.119193, 0.950227]]) + +rgb_from_xyz = linalg.inv(xyz_from_rgb) + +# From http://en.wikipedia.org/wiki/CIE_1931_color_space +# Note: Travis' code did not have the divide by 0.17697 +xyz_from_rgbcie = np.array([[0.49, 0.31, 0.20], + [0.17697, 0.81240, 0.01063], + [0.00, 0.01, 0.99]]) / 0.17697 + +rgbcie_from_xyz = linalg.inv(xyz_from_rgbcie) + +# construct matrices to and from rgb: +rgbcie_from_rgb = np.dot(rgbcie_from_xyz, xyz_from_rgb) +rgb_from_rgbcie = np.dot(rgb_from_xyz, xyz_from_rgbcie) + + +#------------------------------------------------------------- +# The conversion functions that make use of the matrices above +#------------------------------------------------------------- + +def _convert(matrix, arr): + """Do the color space conversion. + + Parameters + ---------- + matrix : array_like + The 3x3 matrix to use. + arr : array_like + The input array. + + Returns + ------- + out : ndarray + The converted array. + """ + arr = _prepare_colorarray(arr) + arr = np.swapaxes(arr, 0, 2) + oldshape = arr.shape + arr = np.reshape(arr, (3, -1)) + out = np.dot(matrix, arr) + out.shape = oldshape + out = np.swapaxes(out, 2, 0) + + return out + + +def xyz2rgb(xyz): + """XYZ to RGB color space conversion. + + Parameters + ---------- + xyz : array_like + The image in XYZ 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 `xyz` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + The CIE XYZ color space is derived from the CIE RGB color space. Note + however that this function converts to sRGB. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/CIE_1931_color_space + + Examples + -------- + >>> import os + >>> from scikits.image import data_dir + >>> from scikits.image.io import imread + + >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena_xyz = rgb2xyz(lena) + >>> lena_rgb = xyz2rgb(lena_hsv) + """ + return _convert(rgb_from_xyz, xyz) + +def rgb2xyz(rgb): + """RGB to XYZ color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB 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 `rgb` is not a 3-D array of shape (.., .., 3). + + Notes + ----- + The CIE XYZ color space is derived from the CIE RGB color space. Note + however that this function converts from sRGB. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/CIE_1931_color_space + + Examples + -------- + >>> import os + >>> from scikits.image import data_dir + >>> from scikits.image.io import imread + + >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena_xyz = rgb2xyz(lena) + """ + return _convert(xyz_from_rgb, rgb) + +def rgb2rgbcie(rgb): + """RGB to RGB CIE color space conversion. + + Parameters + ---------- + rgb : array_like + The image in RGB format, in a 3-D array of shape (.., .., 3). + + Returns + ------- + out : ndarray + The image in RGB CIE format, in a 3-D array of shape (.., .., 3). + + Raises + ------ + ValueError + If `rgb` is not a 3-D array of shape (.., .., 3). + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/CIE_1931_color_space + + Examples + -------- + >>> import os + >>> from scikits.image import data_dir + >>> from scikits.image.io import imread + + >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena_rgbcie = rgb2rgbcie(lena) + """ + return _convert(rgbcie_from_rgb, rgb) + +def rgbcie2rgb(rgbcie): + """RGB CIE to RGB color space conversion. + + Parameters + ---------- + rgbcie : array_like + The image in RGB CIE 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 `rgbcie` is not a 3-D array of shape (.., .., 3). + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/CIE_1931_color_space + + Examples + -------- + >>> import os + >>> from scikits.image import data_dir + >>> from scikits.image.io import imread + + >>> lena = imread(os.path.join(data_dir, 'lena.png')) + >>> lena_rgbcie = rgb2rgbcie(lena) + >>> lena_rgb = rgbcie2rgb(lena_hsv) + """ + return _convert(rgb_from_rgbcie, rgbcie) diff --git a/scikits/image/color/tests/test_colorconv.py b/scikits/image/color/tests/test_colorconv.py index 1d8ecc17..ac861555 100644 --- a/scikits/image/color/tests/test_colorconv.py +++ b/scikits/image/color/tests/test_colorconv.py @@ -1,30 +1,47 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -""" -:author: Nicolas Pinto, 2009 +"""Tests for color conversion functions. + +Authors +------- +- the rgb2hsv test was written by Nicolas Pinto, 2009 +- other tests written by Ralf Gommers, 2009 + :license: modified BSD """ -from os import path +import os.path import numpy as np from numpy.testing import * from scikits.image.io import imread from scikits.image.color import ( - rgb2hsv, + rgb2hsv, hsv2rgb, + rgb2xyz, xyz2rgb, + rgb2rgbcie, rgbcie2rgb, + convert_colorspace ) from scikits.image import data_dir import colorsys + class TestColorconv(TestCase): - img_rgb = imread(path.join(data_dir, 'color.png')) - img_grayscale = imread(path.join(data_dir, 'camera.png')) + img_rgb = imread(os.path.join(data_dir, 'color.png')) + img_grayscale = imread(os.path.join(data_dir, 'camera.png')) + colbars = np.array([[1, 1, 0, 0, 1, 1, 0, 0], + [1, 1, 1, 1, 0, 0, 0, 0], + [1, 0, 1, 0, 1, 0, 1, 0]]) + colbars_array = np.swapaxes(colbars.reshape(3, 4, 2), 0, 2) + colbars_point75 = colbars * 0.75 + colbars_point75_array = np.swapaxes(colbars_point75.reshape(3, 4, 2), 0, 2) + + # RGB to HSV def test_rgb2hsv_conversion(self): rgb = self.img_rgb.astype("float32")[::16, ::16] hsv = rgb2hsv(rgb).reshape(-1, 3) @@ -34,14 +51,96 @@ class TestColorconv(TestCase): ) assert_almost_equal(hsv, gt) - def test_rgb2hsv_error_grayscale(self): + def test_rgb2hsv_error_grayscale(self): self.assertRaises(ValueError, rgb2hsv, self.img_grayscale) def test_rgb2hsv_error_one_element(self): self.assertRaises(ValueError, rgb2hsv, self.img_rgb[0,0]) - def test_rgb2hsv_error_list(self): - self.assertRaises(TypeError, rgb2hsv, [self.img_rgb[0,0]]) + + # HSV to RGB + def test_hsv2rgb_conversion(self): + rgb = self.img_rgb.astype("float32")[::16, ::16] + # create HSV image with colorsys + hsv = np.array([colorsys.rgb_to_hsv(pt[0], pt[1], pt[2]) + for pt in rgb.reshape(-1, 3)]).reshape(rgb.shape) + # convert back to RGB and compare with original. + # relative precision for RGB -> HSV roundtrip is about 1e-6 + assert_almost_equal(rgb, hsv2rgb(hsv), decimal=4) + + def test_hsv2rgb_error_grayscale(self): + self.assertRaises(ValueError, hsv2rgb, self.img_grayscale) + + def test_hsv2rgb_error_one_element(self): + self.assertRaises(ValueError, hsv2rgb, self.img_rgb[0,0]) + + + # RGB to XYZ + def test_rgb2xyz_conversion(self): + gt = np.array([[[ 0.950456, 1. , 1.088754], + [ 0.538003, 0.787329, 1.06942 ], + [ 0.592876, 0.28484 , 0.969561], + [ 0.180423, 0.072169, 0.950227]], + [[ 0.770033, 0.927831, 0.138527], + [ 0.35758 , 0.71516 , 0.119193], + [ 0.412453, 0.212671, 0.019334], + [ 0. , 0. , 0. ]]]) + assert_almost_equal(rgb2xyz(self.colbars_array), gt) + + # stop repeating the "raises" checks for all other functions that are + # implemented with color._convert() + def test_rgb2xyz_error_grayscale(self): + self.assertRaises(ValueError, rgb2xyz, self.img_grayscale) + + def test_rgb2xyz_error_one_element(self): + self.assertRaises(ValueError, rgb2xyz, self.img_rgb[0,0]) + + + # XYZ to RGB + def test_xyz2rgb_conversion(self): + # only roundtrip test, we checked rgb2xyz above already + assert_almost_equal(xyz2rgb(rgb2xyz(self.colbars_array)), + self.colbars_array) + + + # RGB to RGB CIE + def test_rgb2rgbcie_conversion(self): + gt = np.array([[[ 0.1488856 , 0.18288098, 0.19277574], + [ 0.01163224, 0.16649536, 0.18948516], + [ 0.12259182, 0.03308008, 0.17298223], + [-0.01466154, 0.01669446, 0.16969164]], + [[ 0.16354714, 0.16618652, 0.0230841 ], + [ 0.02629378, 0.1498009 , 0.01979351], + [ 0.13725336, 0.01638562, 0.00329059], + [ 0. , 0. , 0. ]]]) + assert_almost_equal(rgb2rgbcie(self.colbars_array), gt) + + + # RGB CIE to RGB + def test_rgbcie2rgb_conversion(self): + # only roundtrip test, we checked rgb2rgbcie above already + assert_almost_equal(rgbcie2rgb(rgb2rgbcie(self.colbars_array)), + self.colbars_array) + + def test_convert_colorspace(self): + colspaces = ['HSV', 'RGB CIE', 'XYZ'] + colfuncs_from = [hsv2rgb, rgbcie2rgb, xyz2rgb] + colfuncs_to = [rgb2hsv, rgb2rgbcie, rgb2xyz] + + assert_almost_equal(convert_colorspace(self.colbars_array, 'RGB', + 'RGB'), self.colbars_array) + for i, space in enumerate(colspaces): + gt = colfuncs_from[i](self.colbars_array) + assert_almost_equal(convert_colorspace(self.colbars_array, space, + 'RGB'), gt) + gt = colfuncs_to[i](self.colbars_array) + assert_almost_equal(convert_colorspace(self.colbars_array, 'RGB', + space), gt) + + self.assertRaises(ValueError, convert_colorspace, self.colbars_array, + 'nokey', 'XYZ') + self.assertRaises(ValueError, convert_colorspace, self.colbars_array, + 'RGB', 'nokey') if __name__ == "__main__": run_module_suite()