Repository: scikit-image/scikit-image

Issue:
Median filter does not preserve dtype
The median filter documentation states that the output image will be of the same dtype and the input. However that's not true for gray (float64) inputs.

**Step to reproduce**

```py
from skimage.data import chelsea
from skimage.filters.rank import median
from skimage.color import rgb2gray

X_gray = rgb2gray(chelsea())
print("Input dtype: ", X_gray.dtype)
out = median(X_gray)
print("Output dtype:", out.dtype)
```
which returns
```
Input dtype:  float64
/home/datageek/src/scikit-image/skimage/util/dtype.py:123: UserWarning: Possible precision loss when converting from float64 to uint8
  .format(dtypeobj_in, dtypeobj_out))
Output dtype: uint8
```

Tested on the current master branch (0.14.dev) and 0.13.1 with Python 3.6 on Linux.  
cc @rogersamso  


Discussion:
@rth see https://github.com/scikit-image/scikit-image/issues/1419, https://github.com/scikit-image/scikit-image/pull/1422 . We would appreciate if you decide to take care of the issue.
Thanks for the response. I missed that issue. I will try to have a look. 

Resolving patch:
diff --git a/skimage/filters/__init__.py b/skimage/filters/__init__.py
--- a/skimage/filters/__init__.py
+++ b/skimage/filters/__init__.py
@@ -14,7 +14,7 @@
                            threshold_niblack, threshold_sauvola,
                            try_all_threshold, apply_hysteresis_threshold)
 from . import rank
-from .rank import median
+from ._median import median
 from ._unsharp_mask import unsharp_mask
 
 
diff --git a/skimage/filters/_median.py b/skimage/filters/_median.py
new file mode 100644
--- /dev/null
+++ b/skimage/filters/_median.py
@@ -0,0 +1,99 @@
+
+from warnings import warn
+
+import numpy as np
+from scipy import ndimage as ndi
+
+from .rank import generic
+
+
+@generic._default_selem
+def median(image, selem=None, out=None, mask=None, shift_x=False,
+           shift_y=False, mode='nearest', cval=0.0, behavior='rank'):
+    """Return local median of an image.
+
+    Parameters
+    ----------
+    image : array-like
+        Input image.
+    selem : ndarray, optional
+        If ``behavior=='rank'``, ``selem`` is a 2-D array of 1's and 0's.
+        If ``behavior=='ndimage'``, ``selem`` is a N-D array of 1's and 0's
+        with the same number of dimension than ``image``.
+        If None, ``selem`` will be a N-D array with 3 elements for each
+        dimension (e.g., vector, square, cube, etc.)
+    out : ndarray, (same dtype as image), optional
+        If None, a new array is allocated.
+    mask : ndarray, optional
+        Mask array that defines (>0) area of the image included in the local
+        neighborhood. If None, the complete image is used (default). Only valid
+        when ``behavior='rank'``
+
+        .. deprecated:: 0.16
+           ``mask`` is deprecated in 0.16 and will be removed 0.17.
+    shift_x, shift_y : int, optional
+        Offset added to the structuring element center point. Shift is bounded
+        by the structuring element sizes (center must be inside the given
+        structuring element). Only valid when ``behavior='rank'``.
+
+        .. deprecated:: 0.16
+           ``shift_x`` and ``shift_y`` are deprecated in 0.16 and will be
+           removed in 0.17.
+    mode : {'reflect', 'constant', 'nearest', 'mirror','‘wrap'}, optional
+        The mode parameter determines how the array borders are handled, where
+        ``cval`` is the value when mode is equal to 'constant'.
+        Default is 'nearest'.
+
+        .. versionadded:: 0.15
+           ``mode`` is used when ``behavior='ndimage'``.
+    cval : scalar, optional
+        Value to fill past edges of input if mode is 'constant'. Default is 0.0
+
+        .. versionadded:: 0.15
+           ``cval`` was added in 0.15 is used when ``behavior='ndimage'``.
+    behavior : {'rank', 'ndimage'}, optional
+        Either to use the old behavior (i.e., < 0.15) or the new behavior.
+        The old behavior will call the :func:`skimage.filters.rank.median`.
+        The new behavior will call the :func:`scipy.ndimage.median_filter`.
+        Default is 'rank'.
+
+        .. versionadded:: 0.15
+           ``behavior`` is introduced in 0.15
+        .. deprecated:: 0.15
+           Default ``behavior`` will change from 'rank' to 'ndimage' in 0.16
+
+    Returns
+    -------
+    out : 2-D array (same dtype as input image)
+        Output image.
+
+    See also
+    --------
+    skimage.filters.rank.median : Rank-based implementation of the median
+        filtering offering more flexibility with additional parameters but
+        dedicated for unsigned integer images.
+
+    Examples
+    --------
+    >>> from skimage import data
+    >>> from skimage.morphology import disk
+    >>> from skimage.filters import median
+    >>> img = data.camera()
+    >>> med = median(img, disk(5))
+
+    """
+    if behavior == 'rank':
+        warn("Default 'behavior' will change from 'rank' to 'ndimage' in 0.16",
+             DeprecationWarning)
+        if mode != 'nearest' or not np.isclose(cval, 0.0):
+            warn("Change 'behavior' to 'ndimage' if you want to use the "
+                 "parameters 'mode' or 'cval'. They will be discarded "
+                 "otherwise.")
+        return generic.median(image, selem=selem, out=out, mask=mask,
+                              shift_x=shift_x, shift_y=shift_y)
+    if mask is not None or shift_x or shift_y:
+        warn("Change 'behavior' to 'rank' if you want to use the "
+             "parameters 'mask', 'shift_x', 'shift_y'. They will be "
+             "discarded otherwise.")
+    return ndi.median_filter(image, footprint=selem, output=out, mode=mode,
+                             cval=cval)
diff --git a/skimage/filters/rank/generic.py b/skimage/filters/rank/generic.py
--- a/skimage/filters/rank/generic.py
+++ b/skimage/filters/rank/generic.py
@@ -514,6 +514,11 @@ def median(image, selem=None, out=None, mask=None,
     out : 2-D array (same dtype as input image)
         Output image.
 
+    See also
+    --------
+    skimage.filters.median : Implementation of a median filtering which handles
+        images with floating precision.
+
     Examples
     --------
     >>> from skimage import data


Test patch:
diff --git a/skimage/filters/tests/test_median.py b/skimage/filters/tests/test_median.py
--- a/skimage/filters/tests/test_median.py
+++ b/skimage/filters/tests/test_median.py
@@ -1,2 +1,79 @@
-def test_median_in_filters():
-    from skimage.filters import median
+import pytest
+
+import numpy as np
+from scipy import ndimage
+
+from skimage.filters import median
+from skimage.filters import rank
+from skimage._shared.testing import assert_allclose
+
+
+@pytest.fixture
+def image():
+    return np.array([[1, 2, 3, 2, 1],
+                     [1, 1, 2, 2, 3],
+                     [3, 2, 1, 2, 1],
+                     [3, 2, 1, 1, 1],
+                     [1, 2, 1, 2, 3]],
+                    dtype=np.uint8)
+
+
+@pytest.mark.parametrize(
+    "mask, shift_x, shift_y, mode, cval, behavior, n_warning, warning_type",
+    [(True, None, None, 'nearest', 0.0, 'ndimage', 1, (UserWarning,)),
+     (None, 1, None, 'nearest', 0.0, 'ndimage', 1, (UserWarning,)),
+     (None, None, 1, 'nearest', 0.0, 'ndimage', 1, (UserWarning,)),
+     (True, 1, 1, 'nearest', 0.0, 'ndimage', 1, (UserWarning,)),
+     (None, False, False, 'constant', 0.0, 'rank', 2, (DeprecationWarning,
+                                                       UserWarning,)),
+     (None, False, False, 'nearest', 0.0, 'rank', 1, (DeprecationWarning,)),
+     (None, False, False, 'nearest', 0.0, 'ndimage', 0, [])]
+)
+def test_median_warning(image, mask, shift_x, shift_y, mode, cval, behavior,
+                        n_warning, warning_type):
+    if mask:
+        mask = np.ones((image.shape), dtype=np.bool_)
+
+    with pytest.warns(None) as records:
+        median(image, mask=mask, shift_x=shift_x, shift_y=shift_y, mode=mode,
+               behavior=behavior)
+
+    assert len(records) == n_warning
+    for rec in records:
+        assert isinstance(rec.message, warning_type)
+
+
+@pytest.mark.parametrize(
+    "behavior, func, params",
+    [('ndimage', ndimage.median_filter, {'size': (3, 3)}),
+     ('rank', rank.median, {'selem': np.ones((3, 3), dtype=np.uint8)})]
+)
+@pytest.mark.filterwarnings("ignore:Default 'behavior' will change")
+def test_median_behavior(image, behavior, func, params):
+    assert_allclose(median(image, behavior=behavior), func(image, **params))
+
+
+@pytest.mark.parametrize(
+    "dtype", [np.uint8, np.uint16, np.float32, np.float64]
+)
+def test_median_preserve_dtype(image, dtype):
+    median_image = median(image.astype(dtype), behavior='ndimage')
+    assert median_image.dtype == dtype
+
+
+@pytest.mark.filterwarnings("ignore:Default 'behavior' will change")
+def test_median_error_ndim():
+    img = np.random.randint(0, 10, size=(5, 5, 5), dtype=np.uint8)
+    with pytest.raises(ValueError):
+        median(img, behavior='rank')
+
+
+@pytest.mark.parametrize(
+    "img, behavior",
+    [(np.random.randint(0, 10, size=(3, 3), dtype=np.uint8), 'rank'),
+     (np.random.randint(0, 10, size=(3, 3), dtype=np.uint8), 'ndimage'),
+     (np.random.randint(0, 10, size=(3, 3, 3), dtype=np.uint8), 'ndimage')]
+)
+@pytest.mark.filterwarnings("ignore:Default 'behavior' will change")
+def test_median(img, behavior):
+    median(img, behavior=behavior)