Repository: scikit-image/scikit-image

Issue:
threshold_adaptive: odd block_size justified? if yes, then no exception raised.
Hi,

I played with threshold_adaptive and the docstring says that the block size must be odd (written uneven, is it common to say uneven?). I looked at the docstrings of ndimage and I didn't find that the functions used in threshold_adaptive need an odd argument. Maybe I missed something.

However, if one function needs an odd value and not the other, my suggestion would be to accept all values and to jump to a close odd value for the function and add a note in the docstring + an inline comment in the code as a reminder.

If we keep the constraint (odd values), then even values must raise an exception ValueError.



Discussion:
Yes, uneven is an odd way of writing odd :)

The problem with jumping is: do you jump up or down?  We can't make that
decision for a user.  If they want to test muliple methods, they must still
pass in valid arguments.

Another approach would be: round to even, and then raise a warning.

Sure, we need to make a choice. +1 for the warning.

Have you indentified where a odd value must be passed?

No, I haven't had a look--I trust your judgement on this.

I'd guess symmetric behavior is only observed for an odd block size. If an even size can be passed, it's probably doing truncating division somewhere and the blocks are not symmetric relative to the point being considered. This should be very obvious for small, even block sizes (2, 4).

A check to enforce odd block size would be reasonable.

It's always a bit tricky choosing between letting things pass through and
raising an exception--mostly, a person calls it on gut feel.  In this case
it seems right on the edge.  The question I normally ask myself is: will
this read to unexpected results--especially unexpected results that are
hard to detect?  In this case, if the filter is truncated, probably.  At
the same time, if the user uses a large filter, then it's much less
relevant whether it is precisely positioned.


Resolving patch:
diff --git a/doc/examples/segmentation/plot_threshold_adaptive.py b/doc/examples/segmentation/plot_threshold_adaptive.py
--- a/doc/examples/segmentation/plot_threshold_adaptive.py
+++ b/doc/examples/segmentation/plot_threshold_adaptive.py
@@ -26,7 +26,7 @@
 global_thresh = threshold_otsu(image)
 binary_global = image > global_thresh
 
-block_size = 40
+block_size = 35
 binary_adaptive = threshold_adaptive(image, block_size, offset=10)
 
 fig, axes = plt.subplots(nrows=3, figsize=(7, 8))
diff --git a/skimage/filters/thresholding.py b/skimage/filters/thresholding.py
--- a/skimage/filters/thresholding.py
+++ b/skimage/filters/thresholding.py
@@ -25,7 +25,7 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0,
     image : (N, M) ndarray
         Input image.
     block_size : int
-        Uneven size of pixel neighborhood which is used to calculate the
+        Odd size of pixel neighborhood which is used to calculate the
         threshold value (e.g. 3, 5, 7, ..., 21, ...).
     method : {'generic', 'gaussian', 'mean', 'median'}, optional
         Method used to determine adaptive threshold for local neighbourhood in
@@ -68,6 +68,9 @@ def threshold_adaptive(image, block_size, method='gaussian', offset=0,
     >>> func = lambda arr: arr.mean()
     >>> binary_image2 = threshold_adaptive(image, 15, 'generic', param=func)
     """
+    if block_size % 2 == 0:
+        raise ValueError("The kwarg ``block_size`` must be odd! Given "
+                         "``block_size`` {0} is even.".format(block_size))
     assert_nD(image, 2)
     thresh_image = np.zeros(image.shape, 'double')
     if method == 'generic':


Test patch:
diff --git a/skimage/filters/tests/test_thresholding.py b/skimage/filters/tests/test_thresholding.py
--- a/skimage/filters/tests/test_thresholding.py
+++ b/skimage/filters/tests/test_thresholding.py
@@ -1,5 +1,7 @@
 import numpy as np
-from numpy.testing import assert_equal, assert_almost_equal
+from numpy.testing import (assert_equal,
+                           assert_almost_equal,
+                           assert_raises)
 
 import skimage
 from skimage import data
@@ -196,6 +198,11 @@ def test_yen_coins_image_as_float():
     assert 0.43 < threshold_yen(coins) < 0.44
 
 
+def test_adaptive_even_block_size_error():
+    img = data.camera()
+    assert_raises(ValueError, threshold_adaptive, img, block_size=4)
+
+
 def test_isodata_camera_image():
     camera = skimage.img_as_ubyte(data.camera())