Repository: scikit-image/scikit-image

Issue:
Image dtype of `rank` filters
The `rank` filters operate on `uint8` and `uint16` images only. We should allow the user to pass in any dtype and convert if necessary, [as suggested in the user guide](http://scikit-image.org/docs/dev/user_guide/data_types.html#input-types).

Also, only the first 12 bits of `uint16` inputs are used. Wouldn't it make sense to use the largest 12 bits instead? (That way, we could just use the output of `img_as_uint`.) That said, I'm not sure how to go about changing the behavior. The most obvious change would be to check for values greater than 4095 (as is done now) and do some bit-shifting if values are larger (i.e. a real `uint16` value) and issue a deprecation warning if all values are smaller (i.e. a `uint12` value put into `uint16` array).



Discussion:

Resolving patch:
diff --git a/skimage/filter/rank/__init__.py b/skimage/filter/rank/__init__.py
--- a/skimage/filter/rank/__init__.py
+++ b/skimage/filter/rank/__init__.py
@@ -1,6 +1,6 @@
-from .rank import (autolevel, bottomhat, equalize, gradient, maximum, mean,
-                   meansubtraction, median, minimum, modal, morph_contr_enh,
-                   pop, threshold, tophat, noise_filter, entropy, otsu)
+from ._rank import (autolevel, bottomhat, equalize, gradient, maximum, mean,
+                    meansubtraction, median, minimum, modal, morph_contr_enh,
+                    pop, threshold, tophat, noise_filter, entropy, otsu)
 from .percentile_rank import (percentile_autolevel, percentile_gradient,
                               percentile_mean, percentile_mean_subtraction,
                               percentile_morph_contr_enh, percentile,
diff --git a/skimage/filter/rank/rank.pyx b/skimage/filter/rank/_rank.py
similarity index 97%
rename from skimage/filter/rank/rank.pyx
rename to skimage/filter/rank/_rank.py
--- a/skimage/filter/rank/rank.pyx
+++ b/skimage/filter/rank/_rank.py
@@ -17,7 +17,7 @@
 """
 
 import numpy as np
-from skimage import img_as_ubyte
+from skimage import img_as_ubyte, img_as_uint
 from skimage.filter.rank import _crank8, _crank16
 from skimage.filter.rank.generic import find_bitdepth
 
@@ -40,29 +40,33 @@ def _apply(func8, func16, image, selem, out, mask, shift_x, shift_y):
     if image is out:
         raise NotImplementedError("Cannot perform rank operation in place.")
 
-    if image.dtype == np.uint8:
-        if func8 is None:
-            raise TypeError("Not implemented for uint8 image.")
-        if out is None:
-            out = np.zeros(image.shape, dtype=np.uint8)
-        func8(image, selem, shift_x=shift_x, shift_y=shift_y,
-              mask=mask, out=out)
-    elif image.dtype == np.uint16:
-        if func16 is None:
-            raise TypeError("Not implemented for uint16 image.")
+    is_8bit = image.dtype in (np.uint8, np.int8)
+
+    if func8 is not None and (is_8bit or func16 is None):
+        out = _apply8(func8, image, selem, out, mask, shift_x, shift_y)
+    else:
+        image = img_as_uint(image)
         if out is None:
             out = np.zeros(image.shape, dtype=np.uint16)
         bitdepth = find_bitdepth(image)
         if bitdepth > 11:
-            raise ValueError("Only uint16 <4096 image (12bit) supported.")
+            image = image >> 4
+            bitdepth = find_bitdepth(image)
         func16(image, selem, shift_x=shift_x, shift_y=shift_y, mask=mask,
                bitdepth=bitdepth + 1, out=out)
-    else:
-        raise TypeError("Only uint8 and uint16 image supported.")
 
     return out
 
 
+def _apply8(func8, image, selem, out, mask, shift_x, shift_y):
+    if out is None:
+        out = np.zeros(image.shape, dtype=np.uint8)
+    image = img_as_ubyte(image)
+    func8(image, selem, shift_x=shift_x, shift_y=shift_y,
+          mask=mask, out=out)
+    return out
+
+
 def autolevel(image, selem, out=None, mask=None, shift_x=False, shift_y=False):
     """Autolevel image using local histogram.
 
diff --git a/skimage/filter/setup.py b/skimage/filter/setup.py
--- a/skimage/filter/setup.py
+++ b/skimage/filter/setup.py
@@ -21,7 +21,6 @@ def configuration(parent_package='', top_path=None):
     cython(['rank/_crank16.pyx'], working_path=base_path)
     cython(['rank/_crank16_percentiles.pyx'], working_path=base_path)
     cython(['rank/_crank16_bilateral.pyx'], working_path=base_path)
-    cython(['rank/rank.pyx'], working_path=base_path)
     cython(['rank/percentile_rank.pyx'], working_path=base_path)
     cython(['rank/bilateral_rank.pyx'], working_path=base_path)
 
@@ -46,9 +45,6 @@ def configuration(parent_package='', top_path=None):
     config.add_extension(
         'rank._crank16_bilateral', sources=['rank/_crank16_bilateral.c'],
         include_dirs=[get_numpy_include_dirs()])
-    config.add_extension(
-        'rank.rank', sources=['rank/rank.c'],
-        include_dirs=[get_numpy_include_dirs()])
     config.add_extension(
         'rank.percentile_rank', sources=['rank/percentile_rank.c'],
         include_dirs=[get_numpy_include_dirs()])


Test patch:
diff --git a/skimage/filter/rank/tests/test_rank.py b/skimage/filter/rank/tests/test_rank.py
--- a/skimage/filter/rank/tests/test_rank.py
+++ b/skimage/filter/rank/tests/test_rank.py
@@ -1,6 +1,7 @@
 import numpy as np
 from numpy.testing import run_module_suite, assert_array_equal, assert_raises
 
+from skimage import img_as_ubyte, img_as_uint, img_as_float
 from skimage import data, util
 from skimage.morphology import cmorph, disk
 from skimage.filter import rank
@@ -186,6 +187,47 @@ def test_compare_autolevels_16bit():
     assert_array_equal(loc_autolevel, loc_perc_autolevel)
 
 
+def test_compare_uint_vs_float():
+    # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of
+    # dynamic) should be identical
+
+    # Create signed int8 image that and convert it to uint8
+    image_uint = img_as_uint(data.camera())
+    image_float = img_as_float(image_uint)
+
+    methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'threshold',
+               'meansubtraction', 'morph_contr_enh', 'pop', 'tophat']
+
+    for method in methods:
+        func = getattr(rank, method)
+        out_u = func(image_uint, disk(3))
+        out_f = func(image_float, disk(3))
+        assert_array_equal(out_u, out_f)
+
+
+def test_compare_8bit_unsigned_vs_signed():
+    # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of
+    # dynamic) should be identical
+
+    # Create signed int8 image that and convert it to uint8
+    image = img_as_ubyte(data.camera())
+    image[image > 127] = 0
+    image_s = image.astype(np.int8)
+    image_u = img_as_ubyte(image_s)
+
+    assert_array_equal(image_u, img_as_ubyte(image_s))
+
+    methods = ['autolevel', 'bottomhat', 'equalize', 'gradient', 'maximum',
+               'mean', 'meansubtraction', 'median', 'minimum', 'modal',
+               'morph_contr_enh', 'pop', 'threshold', 'tophat']
+
+    for method in methods:
+        func = getattr(rank, method)
+        out_u = func(image_u, disk(3))
+        out_s = func(image_s, disk(3))
+        assert_array_equal(out_u, out_s)
+
+
 def test_compare_8bit_vs_16bit():
     # filters applied on 8-bit image ore 16-bit image (having only real 8-bit of
     # dynamic) should be identical