diff --git a/skimage/color/adapt_rgb.py b/skimage/color/adapt_rgb.py new file mode 100644 index 00000000..d5966813 --- /dev/null +++ b/skimage/color/adapt_rgb.py @@ -0,0 +1,78 @@ +import functools + +import numpy as np + +from skimage import color +from skimage.util.dtype import convert + + +__all__ = ['adapt_rgb', 'hsv_value', 'each_channel'] + + +def is_rgb_like(image): + """Return True if the image *looks* like it's RGB. + + This function should not be public because it is only intended to be used + for functions that don't accept volumes as input, since checking an image's + shape is fragile. + """ + return (image.ndim == 3) and (image.shape[2] in (3, 4)) + + +def adapt_rgb(apply_to_rgb): + """Return decorator that adapts to RGB images to a gray-scale filter. + + This function is only intended to be used for functions that don't accept + volumes as input, since checking an image's shape is fragile. + + Parameters + ---------- + apply_to_rgb : function + Function that returns a filtered image from an image-filter and RGB + image. This will only be called if the image is RGB-like. + """ + def decorator(image_filter): + @functools.wraps(image_filter) + def image_filter_adapted(image, *args, **kwargs): + if is_rgb_like(image): + return apply_to_rgb(image_filter, image, *args, **kwargs) + else: + return image_filter(image, *args, **kwargs) + return image_filter_adapted + return decorator + + +def hsv_value(image_filter, image, *args, **kwargs): + """Return color image by applying `image_filter` on HSV-value of `image`. + + Note that this function is intended for use with `adapt_rgb`. + + Parameters + ---------- + image_filter : function + Function that filters a gray-scale image. + image : array + Input image. Note that RGBA images are treated as RGB. + """ + # Slice the first three channels so that we remove any alpha channels. + hsv = color.rgb2hsv(image[:, :, :3]) + value = hsv[:, :, 2].copy() + value = image_filter(value, *args, **kwargs) + hsv[:, :, 2] = convert(value, hsv.dtype) + return color.hsv2rgb(hsv) + + +def each_channel(image_filter, image, *args, **kwargs): + """Return color image by applying `image_filter` on channels of `image`. + + Note that this function is intended for use with `adapt_rgb`. + + Parameters + ---------- + image_filter : function + Function that filters a gray-scale image. + image : array + Input image. + """ + c_new = [image_filter(c, *args, **kwargs) for c in image.T] + return np.array(c_new).T diff --git a/skimage/color/tests/test_adapt_rgb.py b/skimage/color/tests/test_adapt_rgb.py new file mode 100644 index 00000000..869a1ccc --- /dev/null +++ b/skimage/color/tests/test_adapt_rgb.py @@ -0,0 +1,83 @@ +from functools import partial + +import numpy as np + +from skimage import img_as_float, img_as_uint +from skimage import color, data, filter +from skimage.color.adapt_rgb import adapt_rgb, each_channel, hsv_value + + +# Down-sample image for quicker testing. +COLOR_IMAGE = data.lena()[::5, ::5] +GRAY_IMAGE = data.camera()[::5, ::5] + +SIGMA = 3 +smooth = partial(filter.gaussian_filter, sigma=SIGMA) +assert_allclose = partial(np.testing.assert_allclose, atol=1e-8) + + +@adapt_rgb(each_channel) +def edges_each(image): + return filter.sobel(image) + + +@adapt_rgb(each_channel) +def smooth_each(image, sigma): + return filter.gaussian_filter(image, sigma) + + +@adapt_rgb(hsv_value) +def edges_hsv(image): + return filter.sobel(image) + + +@adapt_rgb(hsv_value) +def smooth_hsv(image, sigma): + return filter.gaussian_filter(image, sigma) + + +@adapt_rgb(hsv_value) +def edges_hsv_uint(image): + return img_as_uint(filter.sobel(image)) + + +def test_gray_scale_image(): + # We don't need to test both `hsv_value` and `each_channel` since + # `adapt_rgb` is handling gray-scale inputs. + assert_allclose(edges_each(GRAY_IMAGE), filter.sobel(GRAY_IMAGE)) + + +def test_each_channel(): + filtered = edges_each(COLOR_IMAGE) + for i, channel in enumerate(np.rollaxis(filtered, axis=-1)): + expected = img_as_float(filter.sobel(COLOR_IMAGE[:, :, i])) + assert_allclose(channel, expected) + + +def test_each_channel_with_filter_argument(): + filtered = smooth_each(COLOR_IMAGE, SIGMA) + for i, channel in enumerate(np.rollaxis(filtered, axis=-1)): + assert_allclose(channel, smooth(COLOR_IMAGE[:, :, i])) + + +def test_hsv_value(): + filtered = edges_hsv(COLOR_IMAGE) + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + assert_allclose(color.rgb2hsv(filtered)[:, :, 2], filter.sobel(value)) + + +def test_hsv_value_with_filter_argument(): + filtered = smooth_hsv(COLOR_IMAGE, SIGMA) + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + assert_allclose(color.rgb2hsv(filtered)[:, :, 2], smooth(value)) + + +def test_hsv_value_with_non_float_output(): + # Since `rgb2hsv` returns a float image and the result of the filtered + # result is inserted into the HSV image, we want to make sure there isn't + # a dtype mismatch. + filtered = edges_hsv_uint(COLOR_IMAGE) + filtered_value = color.rgb2hsv(filtered)[:, :, 2] + value = color.rgb2hsv(COLOR_IMAGE)[:, :, 2] + # Reduce tolerance because dtype conversion. + assert_allclose(filtered_value, filter.sobel(value), rtol=1e-5, atol=1e-5) diff --git a/skimage/exposure/_adapthist.py b/skimage/exposure/_adapthist.py index 39d35b54..5da50029 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -15,7 +15,7 @@ comes with no guarantee. """ import numpy as np import skimage -from skimage import color +from skimage.color.adapt_rgb import adapt_rgb, hsv_value from skimage.exposure import rescale_intensity from skimage.util import view_as_blocks @@ -25,6 +25,7 @@ MAX_REG_Y = 16 # max. # contextual regions in y-direction */ NR_OF_GREY = 2**14 # number of grayscale levels to use in CLAHE algorithm +@adapt_rgb(hsv_value) def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, nbins=256): """Contrast Limited Adaptive Histogram Equalization. @@ -65,25 +66,12 @@ def equalize_adapthist(image, ntiles_x=8, ntiles_y=8, clip_limit=0.01, .. [1] http://tog.acm.org/resources/GraphicsGems/gems.html#gemsvi .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE """ - ndim = image.ndim - if ndim == 3: - if image.shape[2] == 4: - image = image[:, :, :3] - image = skimage.img_as_float(image) - image = rescale_intensity(image) - hsv_img = color.rgb2hsv(image) - image = hsv_img[:, :, 2].copy() image = skimage.img_as_uint(image) image = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1)) out = _clahe(image, ntiles_x, ntiles_y, clip_limit * nbins, nbins) image[:out.shape[0], :out.shape[1]] = out image = skimage.img_as_float(image) - if ndim == 3: - hsv_img[:, :, 2] = rescale_intensity(image) - image = color.hsv2rgb(hsv_img) - else: - image = rescale_intensity(image) - return image + return rescale_intensity(image) def _clahe(image, ntiles_x, ntiles_y, clip_limit, nbins=128): diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py index c793c2c8..72a4e413 100644 --- a/skimage/exposure/tests/test_exposure.py +++ b/skimage/exposure/tests/test_exposure.py @@ -11,7 +11,6 @@ from skimage.exposure.exposure import intensity_range from skimage.color import rgb2gray from skimage.util.dtype import dtype_range -import matplotlib.pyplot as plt # Test histogram equalization # =========================== @@ -146,8 +145,8 @@ def test_rescale_uint14_limits(): # ==================================== def test_adapthist_scalar(): - '''Test a scalar uint8 image - ''' + """Test a scalar uint8 image + """ img = skimage.img_as_ubyte(data.moon()) adapted = exposure.equalize_adapthist(img, clip_limit=0.02) assert adapted.min() == 0.0 @@ -163,8 +162,8 @@ def test_adapthist_scalar(): def test_adapthist_grayscale(): - '''Test a grayscale float image - ''' + """Test a grayscale float image + """ img = skimage.img_as_float(data.lena()) img = rgb2gray(img) img = np.dstack((img, img, img)) @@ -172,14 +171,14 @@ def test_adapthist_grayscale(): nbins=128) assert_almost_equal = np.testing.assert_almost_equal assert img.shape == adapted.shape - assert_almost_equal(peak_snr(img, adapted), 104.307, 3) + assert_almost_equal(peak_snr(img, adapted), 104.3277, 3) assert_almost_equal(norm_brightness_err(img, adapted), 0.0265, 3) return data, adapted def test_adapthist_color(): - '''Test an RGB color uint16 image - ''' + """Test an RGB color uint16 image + """ img = skimage.img_as_uint(data.lena()) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') @@ -192,15 +191,14 @@ def test_adapthist_color(): assert adapted.max() == 1.0 assert img.shape == adapted.shape full_scale = skimage.exposure.rescale_intensity(img) - assert_almost_equal(peak_snr(full_scale, adapted), 105.50517, 3) - assert_almost_equal(norm_brightness_err(full_scale, adapted), - 0.0544, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 106.9, 1) + assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.05, 2) return data, adapted def test_adapthist_alpha(): - '''Test an RGBA color image - ''' + """Test an RGBA color image + """ img = skimage.img_as_float(data.lena()) alpha = np.ones((img.shape[0], img.shape[1]), dtype=float) img = np.dstack((img, alpha)) @@ -210,12 +208,12 @@ def test_adapthist_alpha(): full_scale = skimage.exposure.rescale_intensity(img) assert img.shape == adapted.shape assert_almost_equal = np.testing.assert_almost_equal - assert_almost_equal(peak_snr(full_scale, adapted), 105.50198, 3) - assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0544, 3) + assert_almost_equal(peak_snr(full_scale, adapted), 106.86, 2) + assert_almost_equal(norm_brightness_err(full_scale, adapted), 0.0509, 3) def peak_snr(img1, img2): - '''Peak signal to noise ratio of two images + """Peak signal to noise ratio of two images Parameters ---------- @@ -226,7 +224,7 @@ def peak_snr(img1, img2): ------- peak_snr : float Peak signal to noise ratio - ''' + """ if img1.ndim == 3: img1, img2 = rgb2gray(img1.copy()), rgb2gray(img2.copy()) img1 = skimage.img_as_float(img1) @@ -237,7 +235,7 @@ def peak_snr(img1, img2): def norm_brightness_err(img1, img2): - '''Normalized Absolute Mean Brightness Error between two images + """Normalized Absolute Mean Brightness Error between two images Parameters ---------- @@ -248,7 +246,7 @@ def norm_brightness_err(img1, img2): ------- norm_brightness_error : float Normalized absolute mean brightness error - ''' + """ if img1.ndim == 3: img1, img2 = rgb2gray(img1), rgb2gray(img2) ambe = np.abs(img1.mean() - img2.mean())