diff --git a/skimage/color/adapt_rgb.py b/skimage/color/adapt_rgb.py new file mode 100644 index 00000000..c35d4087 --- /dev/null +++ b/skimage/color/adapt_rgb.py @@ -0,0 +1,83 @@ +import functools + +import numpy as np + +from skimage import img_as_float +from skimage import color +from skimage.exposure import rescale_intensity +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 + 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. + """ + # XXX: Are these 2 lines really necessary? + image = img_as_float(image[:, :, :3]) + image = rescale_intensity(image) + # Slice the first three channels so that we remove any alpha channels. + hsv = color.rgb2hsv(image) + 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..6c80dbd9 100644 --- a/skimage/exposure/_adapthist.py +++ b/skimage/exposure/_adapthist.py @@ -16,6 +16,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 +26,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 +67,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..6c3c2a4c 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 # ===========================