Merge pull request #1063 from nlsn/3d-fallback

Add fallback decorator for 3D images

When an image is 3D or higher, our morphology functions will now call out to scipy.
This commit is contained in:
Juan Nunez-Iglesias
2014-07-17 00:55:29 -05:00
6 changed files with 207 additions and 46 deletions
+11 -11
View File
@@ -1,9 +1,14 @@
import warnings
import numpy as np
from scipy import ndimage
from .selem import _default_selem
from .misc import default_fallback
# Our functions only work in 2D, so for 3D or higher input we should fall back
# on `scipy.ndimage`. Additionally, we want to use a cross-shaped structuring
# element of the appropriate dimension for each of these functions.
# The `default_callback` provides all these.
@default_fallback
def binary_erosion(image, selem=None, out=None):
"""Return fast binary morphological erosion of an image.
@@ -32,10 +37,6 @@ def binary_erosion(image, selem=None, out=None):
"""
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
selem = (selem != 0)
selem_sum = np.sum(selem)
@@ -48,10 +49,11 @@ def binary_erosion(image, selem=None, out=None):
ndimage.convolve(binary, selem, mode='constant', cval=1, output=conv)
if out is None:
out = conv
out = np.empty_like(conv, dtype=np.bool)
return np.equal(conv, selem_sum, out=out)
@default_fallback
def binary_dilation(image, selem=None, out=None):
"""Return fast binary morphological dilation of an image.
@@ -81,10 +83,6 @@ def binary_dilation(image, selem=None, out=None):
"""
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
selem = (selem != 0)
if np.sum(selem) <= 255:
@@ -96,10 +94,11 @@ def binary_dilation(image, selem=None, out=None):
ndimage.convolve(binary, selem, mode='constant', cval=0, output=conv)
if out is None:
out = conv
out = np.empty_like(conv, dtype=np.bool)
return np.not_equal(conv, 0, out=out)
@default_fallback
def binary_opening(image, selem=None, out=None):
"""Return fast binary morphological opening of an image.
@@ -133,6 +132,7 @@ def binary_opening(image, selem=None, out=None):
return out
@default_fallback
def binary_closing(image, selem=None, out=None):
"""Return fast binary morphological closing of an image.
+8 -28
View File
@@ -1,8 +1,6 @@
import warnings
from skimage import img_as_ubyte
from scipy import ndimage
from .selem import _default_selem
from .misc import default_fallback
from . import cmorph
@@ -10,7 +8,7 @@ from . import cmorph
__all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat',
'black_tophat']
@default_fallback
def erosion(image, selem=None, out=None, shift_x=False, shift_y=False):
"""Return greyscale morphological erosion of an image.
@@ -56,10 +54,6 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False):
"""
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
if image is out:
raise NotImplementedError("In-place erosion not supported!")
image = img_as_ubyte(image)
@@ -68,6 +62,7 @@ def erosion(image, selem=None, out=None, shift_x=False, shift_y=False):
shift_x=shift_x, shift_y=shift_y)
@default_fallback
def dilation(image, selem=None, out=None, shift_x=False, shift_y=False):
"""Return greyscale morphological dilation of an image.
@@ -114,18 +109,16 @@ def dilation(image, selem=None, out=None, shift_x=False, shift_y=False):
"""
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
if image is out:
raise NotImplementedError("In-place dilation not supported!")
image = img_as_ubyte(image)
selem = img_as_ubyte(selem)
return cmorph._dilate(image, selem, out=out,
shift_x=shift_x, shift_y=shift_y)
@default_fallback
def opening(image, selem=None, out=None):
"""Return greyscale morphological opening of an image.
@@ -169,10 +162,6 @@ def opening(image, selem=None, out=None):
"""
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
h, w = selem.shape
shift_x = True if (w % 2) == 0 else False
shift_y = True if (h % 2) == 0 else False
@@ -182,6 +171,7 @@ def opening(image, selem=None, out=None):
return out
@default_fallback
def closing(image, selem=None, out=None):
"""Return greyscale morphological closing of an image.
@@ -225,10 +215,6 @@ def closing(image, selem=None, out=None):
"""
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
h, w = selem.shape
shift_x = True if (w % 2) == 0 else False
shift_y = True if (h % 2) == 0 else False
@@ -238,6 +224,7 @@ def closing(image, selem=None, out=None):
return out
@default_fallback
def white_tophat(image, selem=None, out=None):
"""Return white top hat of an image.
@@ -280,10 +267,6 @@ def white_tophat(image, selem=None, out=None):
"""
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
if image is out:
raise NotImplementedError("Cannot perform white top hat in place.")
@@ -292,6 +275,7 @@ def white_tophat(image, selem=None, out=None):
return out
@default_fallback
def black_tophat(image, selem=None, out=None):
"""Return black top hat of an image.
@@ -335,10 +319,6 @@ def black_tophat(image, selem=None, out=None):
"""
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
if image is out:
raise NotImplementedError("Cannot perform white top hat in place.")
+48
View File
@@ -1,5 +1,53 @@
import numpy as np
import scipy.ndimage as nd
from .selem import _default_selem
# Our function names don't exactly correspond to ndimages.
# This dictionary translates from our names to scipy's.
funcs = ('erosion', 'dilation', 'opening', 'closing')
skimage2ndimage = {x: 'grey_' + x for x in funcs}
# These function names are the same in ndimage.
funcs = ('binary_erosion', 'binary_dilation', 'binary_opening',
'binary_closing', 'black_tophat', 'white_tophat')
skimage2ndimage.update({x: x for x in funcs})
def default_fallback(func):
"""Decorator to fall back on ndimage for images with more than 2 dimensions
Decorator also provides a default structuring element, `selem`, with the
appropriate dimensionality if none is specified.
Parameters
----------
func : function
A morphology function such as erosion, dilation, opening, closing,
white_tophat, or black_tophat.
Returns
-------
func_out : function
If the image dimentionality is greater than 2D, the ndimage
function is returned, otherwise skimage function is used.
"""
def func_out(image, selem=None, out=None, **kwargs):
# Default structure element
if selem is None:
selem = _default_selem(image.ndim)
# If image has more than 2 dimensions, use scipy.ndimage
if image.ndim > 2:
function = getattr(nd, skimage2ndimage[func.__name__])
try:
return function(image, footprint=selem, output=out, **kwargs)
except TypeError:
# nd.binary_* take structure instead of footprint
return function(image, structure=selem, output=out, **kwargs)
else:
return func(image, selem=selem, out=out, **kwargs)
return func_out
def remove_small_objects(ar, min_size=64, connectivity=1, in_place=False):
-7
View File
@@ -311,10 +311,3 @@ def _default_selem(ndim):
"""
return ndimage.morphology.generate_binary_structure(ndim, 1)
+80
View File
@@ -4,6 +4,7 @@ from numpy import testing
from skimage import data, color
from skimage.util import img_as_bool
from skimage.morphology import binary, grey, selem
from scipy import ndimage
lena = color.rgb2gray(data.lena())
@@ -86,5 +87,84 @@ def test_default_selem():
im_test = function(image)
yield testing.assert_array_equal, im_expected, im_test
def test_3d_fallback_default_selem():
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), np.bool)
image[2:-2, 2:-2, 2:-2] = 1
opened = binary.binary_opening(image)
# expect a "hyper-cross" centered in the 5x5x5:
image_expected = np.zeros((7, 7, 7), dtype=bool)
image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1)
testing.assert_array_equal(opened, image_expected)
def test_3d_fallback_cube_selem():
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), np.bool)
image[2:-2, 2:-2, 2:-2] = 1
cube = np.ones((3, 3, 3), dtype=np.uint8)
for function in [binary.binary_closing, binary.binary_opening]:
new_image = function(image, cube)
yield testing.assert_array_equal, new_image, image
def test_2d_ndimage_equivalence():
image = np.zeros((9, 9), np.uint16)
image[2:-2, 2:-2] = 2**14
image[3:-3, 3:-3] = 2**15
image[4, 4] = 2**16-1
bin_opened = binary.binary_opening(image)
bin_closed = binary.binary_closing(image)
selem = ndimage.generate_binary_structure(2, 1)
ndimage_opened = ndimage.binary_opening(image, structure=selem)
ndimage_closed = ndimage.binary_closing(image, structure=selem)
testing.assert_array_equal(bin_opened, ndimage_opened)
testing.assert_array_equal(bin_closed, ndimage_closed)
def test_binary_output_2d():
image = np.zeros((9, 9), np.uint16)
image[2:-2, 2:-2] = 2**14
image[3:-3, 3:-3] = 2**15
image[4, 4] = 2**16-1
bin_opened = binary.binary_opening(image)
bin_closed = binary.binary_closing(image)
int_opened = np.empty_like(image, dtype=np.uint8)
int_closed = np.empty_like(image, dtype=np.uint8)
binary.binary_opening(image, out=int_opened)
binary.binary_closing(image, out=int_closed)
testing.assert_equal(bin_opened.dtype, np.bool)
testing.assert_equal(bin_closed.dtype, np.bool)
testing.assert_equal(int_opened.dtype, np.uint8)
testing.assert_equal(int_closed.dtype, np.uint8)
def test_binary_output_3d():
image = np.zeros((9, 9, 9), np.uint16)
image[2:-2, 2:-2, 2:-2] = 2**14
image[3:-3, 3:-3, 3:-3] = 2**15
image[4, 4, 4] = 2**16-1
bin_opened = binary.binary_opening(image)
bin_closed = binary.binary_closing(image)
int_opened = np.empty_like(image, dtype=np.uint8)
int_closed = np.empty_like(image, dtype=np.uint8)
binary.binary_opening(image, out=int_opened)
binary.binary_closing(image, out=int_closed)
testing.assert_equal(bin_opened.dtype, np.bool)
testing.assert_equal(bin_closed.dtype, np.bool)
testing.assert_equal(int_opened.dtype, np.uint8)
testing.assert_equal(int_closed.dtype, np.uint8)
if __name__ == '__main__':
testing.run_module_suite()
+60
View File
@@ -2,6 +2,7 @@ import os.path
import numpy as np
from numpy import testing
from scipy import ndimage
import skimage
from skimage import data_dir
@@ -142,6 +143,65 @@ def test_default_selem():
im_test = function(image)
yield testing.assert_array_equal, im_expected, im_test
def test_3d_fallback_default_selem():
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), np.bool)
image[2:-2, 2:-2, 2:-2] = 1
opened = grey.opening(image)
# expect a "hyper-cross" centered in the 5x5x5:
image_expected = np.zeros((7, 7, 7), dtype=bool)
image_expected[2:5, 2:5, 2:5] = ndimage.generate_binary_structure(3, 1)
testing.assert_array_equal(opened, image_expected)
def test_3d_fallback_cube_selem():
# 3x3x3 cube inside a 7x7x7 image:
image = np.zeros((7, 7, 7), np.bool)
image[2:-2, 2:-2, 2:-2] = 1
cube = np.ones((3, 3, 3), dtype=np.uint8)
for function in [grey.closing, grey.opening]:
new_image = function(image, cube)
yield testing.assert_array_equal, new_image, image
def test_3d_fallback_white_tophat():
image = np.zeros((7, 7, 7), dtype=bool)
image[2, 2:4, 2:4] = 1
image[3, 2:5, 2:5] = 1
image[4, 3:5, 3:5] = 1
new_image = grey.white_tophat(image)
footprint = ndimage.generate_binary_structure(3,1)
image_expected = ndimage.white_tophat(image,footprint=footprint)
testing.assert_array_equal(new_image, image_expected)
def test_3d_fallback_black_tophat():
image = np.ones((7, 7, 7), dtype=bool)
image[2, 2:4, 2:4] = 0
image[3, 2:5, 2:5] = 0
image[4, 3:5, 3:5] = 0
new_image = grey.black_tophat(image)
footprint = ndimage.generate_binary_structure(3,1)
image_expected = ndimage.black_tophat(image,footprint=footprint)
testing.assert_array_equal(new_image, image_expected)
def test_2d_ndimage_equivalence():
image = np.zeros((9, 9), np.uint8)
image[2:-2, 2:-2] = 128
image[3:-3, 3:-3] = 196
image[4, 4] = 255
opened = grey.opening(image)
closed = grey.closing(image)
selem = ndimage.generate_binary_structure(2, 1)
ndimage_opened = ndimage.grey_opening(image, footprint=selem)
ndimage_closed = ndimage.grey_closing(image, footprint=selem)
testing.assert_array_equal(opened, ndimage_opened)
testing.assert_array_equal(closed, ndimage_closed)
class TestDTypes():
def setUp(self):