Repository: scikit-image/scikit-image

Issue:
Make level parameter optional in measure.find_contours
## Description


I think that we could put a default value to level in find_contours. I would suggest (image.max() - image.min())/2. This would make this function able to be used with an image and I think this default value is meaningful.

I'm open to discussion.


Discussion:
:+1: for having default values whenever it is possible! The default which you are suggesting works well for binary arrays.
cool.
I added the good first issue label because it would indeed be a good first issue, but we need to make a decision first. I'm :+1: 
I'm :+1: also. The mean or median might also be good defaults.
I started to play with this function a lot recently, and my first choice was the mean as you suggested. Then, it appeared to be a bad idea in practice. On an image sequence, although the light conditions were invariant, the quantity of bright and dark regions were changing, which changed the level significantly. This is not meaningful at all in my application.

Perhaps there are applications where mean or median can be relevant, but then, I thought max/min to be more robust from the universality perspective.
Sure, I was going off intuition, not any actual experience!
I would not say that using max and min is more robust because max and min are very sensitive to noise and outliers. For me the strongest argument in favor of using min and max is that it works well for binary arrays.
Ming is working on this issue at the sprint.

Resolving patch:
diff --git a/skimage/measure/_find_contours.py b/skimage/measure/_find_contours.py
--- a/skimage/measure/_find_contours.py
+++ b/skimage/measure/_find_contours.py
@@ -9,7 +9,7 @@
 
 
 @deprecate_kwarg({'array': 'image'}, removed_version="0.20")
-def find_contours(image, level,
+def find_contours(image, level=None,
                   fully_connected='low', positive_orientation='low',
                   *,
                   mask=None):
@@ -23,8 +23,9 @@ def find_contours(image, level,
     ----------
     image : 2D ndarray of double
         Input image in which to find contours.
-    level : float
-        Value along which to find contours in the array.
+    level : float, optional
+        Value along which to find contours in the array. By default, the level
+        is set to (max(image) - min(image)) / 2
     fully_connected : str, {'low', 'high'}
          Indicates whether array elements below the given level value are to be
          considered fully-connected (and hence elements above the value will
@@ -139,6 +140,8 @@ def find_contours(image, level,
         if not np.can_cast(mask.dtype, bool, casting='safe'):
             raise TypeError('Parameter "mask" must be a binary array.')
         mask = mask.astype(np.uint8, copy=False)
+    if level is None:
+        level = (np.nanmax(image) - np.nanmin(image)) / 2.0
 
     segments = _get_contour_segments(image.astype(np.double), float(level),
                                      fully_connected == 'high', mask=mask)


Test patch:
diff --git a/skimage/measure/tests/test_find_contours.py b/skimage/measure/tests/test_find_contours.py
--- a/skimage/measure/tests/test_find_contours.py
+++ b/skimage/measure/tests/test_find_contours.py
@@ -101,7 +101,7 @@ def test_mask_shape():
 
 
 def test_mask_dtype():
-    bad_mask = np.ones((8,8), dtype=np.uint8)
+    bad_mask = np.ones((8, 8), dtype=np.uint8)
     with raises(TypeError, match='binary'):
         find_contours(a, 0, mask=bad_mask)
 
@@ -130,3 +130,40 @@ def test_invalid_input():
         find_contours(r, 0.5, 'foo', 'bar')
     with testing.raises(ValueError):
         find_contours(r[..., None], 0.5)
+
+
+def test_nodata_levelNone():
+    # Test missing data via NaNs in input array
+    b = np.copy(a)
+    b[~mask] = np.nan
+    contours = find_contours(b, level=None, positive_orientation='high')
+    assert len(contours) == 1
+    assert_array_equal(contours[0], mask_contour)
+
+
+def test_mask_levelNone():
+    # Test missing data via explicit masking
+    contours = find_contours(a, level=None, positive_orientation='high',
+                             mask=mask)
+    assert len(contours) == 1
+    assert_array_equal(contours[0], mask_contour)
+
+
+def test_mask_shape_levelNone():
+    bad_mask = np.ones((8, 7), dtype=bool)
+    with raises(ValueError, match='shape'):
+        find_contours(a, level=None, mask=bad_mask)
+
+
+def test_mask_dtype_levelNone():
+    bad_mask = np.ones((8, 8), dtype=np.uint8)
+    with raises(TypeError, match='binary'):
+        find_contours(a, level=None, mask=bad_mask)
+
+
+def test_memory_order_levelNone():
+    contours = find_contours(np.ascontiguousarray(r), level=None)
+    assert len(contours) == 1
+
+    contours = find_contours(np.asfortranarray(r), level=None)
+    assert len(contours) == 1