Skip to content

Commit

Permalink
Merge pull request #1073 from tonysyu/feature/adapt_rgb
Browse files Browse the repository at this point in the history
Add `adapt_rgb` decorator and helpers
  • Loading branch information
jni committed Jul 18, 2014
2 parents 0c31487 + ee3b9f2 commit 6aa0b9e
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 34 deletions.
78 changes: 78 additions & 0 deletions 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
83 changes: 83 additions & 0 deletions 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)
18 changes: 3 additions & 15 deletions skimage/exposure/_adapthist.py
Expand Up @@ -15,7 +15,7 @@
"""
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

Expand All @@ -25,6 +25,7 @@
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.
Expand Down Expand Up @@ -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):
Expand Down
36 changes: 17 additions & 19 deletions skimage/exposure/tests/test_exposure.py
Expand Up @@ -11,7 +11,6 @@
from skimage.color import rgb2gray
from skimage.util.dtype import dtype_range

import matplotlib.pyplot as plt

# Test histogram equalization
# ===========================
Expand Down Expand Up @@ -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
Expand All @@ -163,23 +162,23 @@ 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))
adapted = exposure.equalize_adapthist(img, 10, 9, clip_limit=0.01,
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')
Expand All @@ -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))
Expand All @@ -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
----------
Expand All @@ -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)
Expand All @@ -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
----------
Expand All @@ -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())
Expand Down

0 comments on commit 6aa0b9e

Please sign in to comment.