Repository: scikit-image/scikit-image

Issue:
Key error when using `skimage.measure.find_contours`
## Description

Hi all, a few months back I raised #4497, where certain arrays from my satellite time series data caused a `KeyError` when `skimage.measure.find_contours` was applied to them. It seemed like this was solved by #4505, but I've just had the issue re-occur on `scikit-image version: 0.17.2`. 

I've attached a reproducible example below; my full arrays are larger (around 2000 x 2000 pixels).

## Way to reproduce
```python
import numpy as np
from skimage.measure import find_contours

# Example array
array = np.array([[ 0.08, -0.03, -0.17,  -0.08,  0.24,  0.06,  0.17, -0.02],
                  [ 0.12,  0.  ,  np.nan, 0.24,  0.  , -0.53,  0.26,  0.16],
                  [ 0.39,  0.  ,  0.  ,   0.  ,  0.  , -0.02, -0.3 ,  0.01],
                  [ 0.28, -0.04, -0.03,   0.16,  0.12,  0.01, -0.87,  0.16],
                  [ 0.26,  0.08,  0.08,   0.08,  0.12,  0.13,  0.11,  0.19],
                  [ 0.27,  0.24,  0.  ,   0.25,  0.32,  0.19,  0.26,  0.22]],
      dtype=np.float32)

# Extract 0 contour
find_contours(array, 0)
```
```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-138-952e103d773c> in <module>
     12 
     13 # Extract 0 contour
---> 14 find_contours(array, 0)

/g/data/v10/public/modules/dea-env/20200612/lib/python3.6/site-packages/skimage/measure/_find_contours.py in find_contours(array, level, fully_connected, positive_orientation, mask)
    135     segments = _get_contour_segments(array.astype(np.double), float(level),
    136                                      fully_connected == 'high', mask=mask)
--> 137     contours = _assemble_contours(segments)
    138     if positive_orientation == 'high':
    139         contours = [c[::-1] for c in contours]

/g/data/v10/public/modules/dea-env/20200612/lib/python3.6/site-packages/skimage/measure/_find_contours.py in _assemble_contours(segments)
    172                     head.extend(tail)
    173                     # remove all traces of tail:
--> 174                     ends.pop(tail[-1])
    175                     contours.pop(tail_num, None)
    176                     # Update contour starts end ends

KeyError: (2.0, 3)
```


## Version information
```python
# Paste the output of the following python commands
from __future__ import print_function
import sys; print(sys.version)
import platform; print(platform.platform())
import skimage; print("scikit-image version: {}".format(skimage.__version__))
import numpy; print("numpy version: {}".format(numpy.__version__))
```

```python
3.6.10 | packaged by conda-forge | (default, Apr 24 2020, 16:44:11) 
[GCC 7.3.0]
Linux-2.6.32-754.18.2.el6.x86_64-x86_64-with-centos-6.10-Final
scikit-image version: 0.17.2
numpy version: 1.18.5

```

Error randomly appearing line 188 in "assemble_contours" while using find_contours
Hi,

I'm using skimage.measure.find_contours in a script where I loop over thousands of images, and I sometimes encounter the following error:
```
line 188, in _assemble_contours
    ends.pop(tail[-1])

KeyError: (0.0, 963)
```
The key is not always the same, but the error appears randomly for about one in ~100 images (it's not always on the same image, it depends on how I crop my images before running the loop).

I never had this error before updating skimage, and I noticed that in older versions, there was a `try` that forbid the error to occur. See for example lines 189 to 192 in v.0.16:
https://github.com/scikit-image/scikit-image/blob/99c4902e26c9ec586b51120efcae26ffbae9eb0f/skimage/measure/_find_contours.py#L189-L192

In case this might help, I was told on twitter by @Glyg7 to replace:
`ends.pop(tail[-1])`
by:
`ends.pop(tails[-1], (None, None))`
in line 188 of the current version:
https://github.com/scikit-image/scikit-image/blob/28e53a24b460883758bb71dddf71900d4c723635/skimage/measure/_find_contours.py#L188
and it seems to work fine for me now.

Best regards,
Antoine.


Discussion:
I've found another example where the default `find_contours` parameters fail with a `KeyError`, but `fully_connected='high'` passes:
```
import numpy as np
from skimage.measure import find_contours

# Example array
array = np.array([[-0.18,  np.nan, np.nan,  0.22, -0.14, -0.23, -0.2 , -0.17, -0.19, -0.24],
                  [ 0.  ,  np.nan, np.nan, np.nan, -0.1 , -0.24, -0.15, -0.02, -0.09, -0.21],
                  [ 0.43,  0.19,  np.nan, np.nan, -0.01, -0.2 , -0.22, -0.18, -0.16, -0.07],
                  [ 0.23,  0.  ,  np.nan, -0.06, -0.07, -0.21, -0.24, -0.25, -0.23, -0.13],
                  [-0.05, -0.11,  0.  ,  0.1 , -0.19, -0.23, -0.23, -0.18, -0.19,   -0.16],
                  [-0.19, -0.05,  0.13, -0.08, -0.22, -0.23, -0.26, -0.15, -0.12, -0.13],
                  [-0.2 , -0.11, -0.11, -0.24, -0.29, -0.27, -0.35, -0.36, -0.27, -0.13],
                  [-0.28, -0.33, -0.31, -0.36, -0.39, -0.37, -0.38, -0.32, -0.34, -0.2 ],
                  [-0.28, -0.33, -0.39, -0.4 , -0.42, -0.38, -0.35, -0.39, -0.35,  -0.34],
                  [-0.38, -0.35, -0.41, -0.42, -0.39, -0.36, -0.34, -0.36, -0.28,  -0.34]], 
                 dtype=np.float32)

# This passes
find_contours(array, 0, fully_connected='high')

# This fails
find_contours(array, 0)
```

Thank you @robbibt for reopening this, I will investigate: the example that you provide will help us for sure :wink:
It looks like the [assertion](https://github.com/scikit-image/scikit-image/blob/master/skimage/measure/_find_contours.py#L166) saying that "degenerated segments are picked up later" in the processing loop is not always true when there is masked areas!!

I will investigate on this side...
Hi @rfezzani @jni, I've just run into this `KeyError` issue again when trying to automatically extract contours from large remote sensing data arrays (`skimage.__version__ '0.17.2'`). In the past I've been able to work around this issue by selecting `fully_connected="high"` which in most cases avoids the error. However, the following minimal example produces a `KeyError` for both `fully_connected="low"` and `fully_connected="high"`, which is the first time I've encountered this.

![image](https://user-images.githubusercontent.com/17680388/134267308-f3138b00-4456-4717-b2d0-eff563092d80.png)

```
import numpy as np
from skimage.measure import find_contours

# Example array (subset of a larger 2242 x 1988 pixel array)
array = np.array(
    [[0.13680, 0.11220, 0.0, 0.0, 0.0, 0.19417, 0.19417, 0.33701],
     [0.0, 0.15140, 0.10267, 0.0, np.nan, 0.14908, 0.18158, 0.19178],
     [0.0, 0.06949, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01860],
     [0.0, 0.06949, 0.0, 0.17852, 0.08469, 0.02135, 0.08198, np.nan],
     [0.0, 0.08244, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan],
     [0.12342, 0.21330, 0.0, np.nan, 0.01301, 0.04335, 0.0, 0.0]],
    dtype=np.float32,
)

# Extract 0 contour
find_contours(array, 0, fully_connected="low")
find_contours(array, 0, fully_connected="high")
```

![image](https://user-images.githubusercontent.com/17680388/134266708-15b8c04c-e5d3-47c8-be7e-80f58b406f7d.png)

You can also reproduce the error using the full 2242 x 1988 pixel numpy array `array.npy`: [array.zip](https://github.com/scikit-image/scikit-image/files/7207059/array.zip):

```
import numpy as np
from skimage.measure import find_contours

# Load numpy array 
array = np.load('array.npy')

# Extract 0 contour
find_contours(array, 0, fully_connected="low")
find_contours(array, 0, fully_connected="high")
```



I'd love to know if you have any ideas for a temporary work-around for this - I've tried a few different options for filling the `np.nan`s but most things I've tried adversely affect the contour extraction as well. Let me know if I can help provide any further examples!

Thank you @robbibt for the reminder and sorry, I completely forgot about this issue :pray:. It's on the top of my list now ;)

> I'd love to know if you have any ideas for a temporary work-around for this

Sorry but currently not :confused: 
No worries at all @rfezzani. I really appreciate the help!
@robbibt, I found that in the 3 failing examples that you provided, the pixels causing the `keyerror` have a value equal to the contour level that you set.
So, I tried to use a slightly different `level` then the one you use (1e-12 instead of 0):
```python
import numpy as np
from skimage.measure import find_contours

# Load numpy array 
img = np.load('array.npy')

# Extract 0 contour
find_contours(img, level=1e-12, fully_connected="low")
```
which gives
![Figure_1](https://user-images.githubusercontent.com/3438227/134352904-96c446a3-1e7a-41d9-a9ea-88170f4c568e.png)

You can also slightly perturb your input image
```python
find_contours(img+1e-12, level=0, fully_connected="low")
```
I need to investigate more to provide a proper bug fix ^^
Related to #5401 
Would it be possible for you to share one of these failing images?
Sure.
For example if I use this image (I zipped the file because github does not accept .tiff files) : [test.zip](https://github.com/scikit-image/scikit-image/files/6517775/test.zip)
Then try to do:
```
import skimage as sk
img = sk.io.imread('test.tiff')
ListeContours = sk.measure.find_contours(img,130)
```
I get the following error:

```
Traceback (most recent call last):

  File "<ipython-input-5-a51b93c65676>", line 1, in <module>
    ListeContours = sk.measure.find_contours(img,130)

  File "C:\Users\Antoine\anaconda3\lib\site-packages\skimage\_shared\utils.py", line 156, in fixed_func
    return func(*args, **kwargs)

  File "C:\Users\Antoine\anaconda3\lib\site-packages\skimage\measure\_find_contours.py", line 151, in find_contours
    contours = _assemble_contours(segments)

  File "C:\Users\Antoine\anaconda3\lib\site-packages\skimage\measure\_find_contours.py", line 188, in _assemble_contours
    ends.pop(tail[-1])

KeyError: (0.0, 943)
```
The weird part is that I use thousands of images that all look the same (at least to me), and only some of them give the error. It seems really random to me, and for example I tried to convert the .tiff image above to .jpeg to attach it directly to this comment, but then the converted image did not give me the error anymore...
It appears that it comes out in a particular combination during the reconstruction of the contour, this is the reason for the rareness. jpeg has a destructive compression, the resulting image is not identical, that's why you do not have necessarily the same behavior.

@grlee77 I understand why you suggested this workaround, but do you have a deep understanding of the algorithm? I'm facing difficulties to propose a simple testcase for this.
Hi, is anyone actively looking at this?

I'm not familiar with this algorithm, but have worked with contour finding algorithms in the past. I did reproduce the error with the given image and would love to read up on the algorithm to help figure out what's going on here & think of a potential test case to verify the fix assuming nobody is currently working on it.
> Hi, is anyone actively looking at this?

I don't think so. If you want to try sorting this out that would be very welcome!

> @grlee77 I understand why you suggested this workaround, but do you have a deep understanding of the algorithm? I'm facing difficulties to propose a simple testcase for this.

I was not the person who suggested the workaround (it was a different twitter user mentioned up top) and have not looked into this issue
Great, thanks for the quick response @grlee77. I'll look into it this week 😄 

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
@@ -174,8 +174,7 @@ def _assemble_contours(segments):
         if tail is not None and head is not None:
             # We need to connect these two contours.
             if tail is head:
-                # We need to closed a contour.
-                # Add the end point
+                # We need to closed a contour: add the end point
                 head.append(to_point)
             else:  # tail is not head
                 # We need to join two distinct contours.
@@ -184,37 +183,38 @@ def _assemble_contours(segments):
                 if tail_num > head_num:
                     # tail was created second. Append tail to head.
                     head.extend(tail)
-                    # remove all traces of tail:
-                    ends.pop(tail[-1])
+                    # Remove tail from the detected contours
                     contours.pop(tail_num, None)
-                    # Update contour starts end ends
+                    # Update starts and ends
                     starts[head[0]] = (head, head_num)
                     ends[head[-1]] = (head, head_num)
                 else:  # tail_num <= head_num
                     # head was created second. Prepend head to tail.
                     tail.extendleft(reversed(head))
-                    # remove all traces of head:
-                    starts.pop(head[0])
+                    # Remove head from the detected contours
+                    starts.pop(head[0], None)  # head[0] can be == to_point!
                     contours.pop(head_num, None)
-                    # Update contour starts end ends
+                    # Update starts and ends
                     starts[tail[0]] = (tail, tail_num)
                     ends[tail[-1]] = (tail, tail_num)
         elif tail is None and head is None:
-            # we need to add a new contour
+            # We need to add a new contour
             new_contour = deque((from_point, to_point))
             contours[current_index] = new_contour
             starts[from_point] = (new_contour, current_index)
             ends[to_point] = (new_contour, current_index)
             current_index += 1
         elif head is None:  # tail is not None
-            # We've found a single contour to which the new segment should be
+            # tail first element is to_point: the new segment should be
             # prepended.
             tail.appendleft(from_point)
+            # Update starts
             starts[from_point] = (tail, tail_num)
         else:  # tail is None and head is not None:
-            # We've found a single contour to which the new segment should be
+            # head last element is from_point: the new segment should be
             # appended
             head.append(to_point)
+            # Update ends
             ends[to_point] = (head, head_num)
 
     return [np.array(contour) for _, contour in sorted(contours.items())]


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
@@ -1,9 +1,8 @@
 import numpy as np
 from skimage.measure import find_contours
 
-from skimage._shared import testing
 from skimage._shared.testing import assert_array_equal
-from pytest import raises
+import pytest
 
 
 a = np.ones((8, 8), dtype=np.float32)
@@ -15,31 +14,31 @@
 
 
 def test_binary():
-    ref = [[6. ,  1.5],
-           [5. ,  1.5],
-           [4. ,  1.5],
-           [3. ,  1.5],
-           [2. ,  1.5],
-           [1.5,  2. ],
-           [1.5,  3. ],
-           [1.5,  4. ],
-           [1.5,  5. ],
-           [1.5,  6. ],
-           [1. ,  6.5],
-           [0.5,  6. ],
-           [0.5,  5. ],
-           [0.5,  4. ],
-           [0.5,  3. ],
-           [0.5,  2. ],
-           [0.5,  1. ],
-           [1. ,  0.5],
-           [2. ,  0.5],
-           [3. ,  0.5],
-           [4. ,  0.5],
-           [5. ,  0.5],
-           [6. ,  0.5],
-           [6.5,  1. ],
-           [6. ,  1.5]]
+    ref = [[6.0, 1.5],
+           [5.0, 1.5],
+           [4.0, 1.5],
+           [3.0, 1.5],
+           [2.0, 1.5],
+           [1.5, 2.0],
+           [1.5, 3.0],
+           [1.5, 4.0],
+           [1.5, 5.0],
+           [1.5, 6.0],
+           [1.0, 6.5],
+           [0.5, 6.0],
+           [0.5, 5.0],
+           [0.5, 4.0],
+           [0.5, 3.0],
+           [0.5, 2.0],
+           [0.5, 1.0],
+           [1.0, 0.5],
+           [2.0, 0.5],
+           [3.0, 0.5],
+           [4.0, 0.5],
+           [5.0, 0.5],
+           [6.0, 0.5],
+           [6.5, 1.0],
+           [6.0, 1.5]]
 
     contours = find_contours(a, 0.5, positive_orientation='high')
     assert len(contours) == 1
@@ -48,29 +47,29 @@ def test_binary():
 
 # target contour for mask tests
 mask_contour = [
-    [6. ,  0.5],
-    [5. ,  0.5],
-    [4. ,  0.5],
-    [3. ,  0.5],
-    [2. ,  0.5],
-    [1. ,  0.5],
-    [0.5,  1. ],
-    [0.5,  2. ],
-    [0.5,  3. ],
-    [0.5,  4. ],
-    [0.5,  5. ],
-    [0.5,  6. ],
-    [1. ,  6.5],
-    [1.5,  6. ],
-    [1.5,  5. ],
-    [1.5,  4. ],
-    [1.5,  3. ],
-    [1.5,  2. ],
-    [2. ,  1.5],
-    [3. ,  1.5],
-    [4. ,  1.5],
-    [5. ,  1.5],
-    [6. ,  1.5],
+    [6.0, 0.5],
+    [5.0, 0.5],
+    [4.0, 0.5],
+    [3.0, 0.5],
+    [2.0, 0.5],
+    [1.0, 0.5],
+    [0.5, 1.0],
+    [0.5, 2.0],
+    [0.5, 3.0],
+    [0.5, 4.0],
+    [0.5, 5.0],
+    [0.5, 6.0],
+    [1.0, 6.5],
+    [1.5, 6.0],
+    [1.5, 5.0],
+    [1.5, 4.0],
+    [1.5, 3.0],
+    [1.5, 2.0],
+    [2.0, 1.5],
+    [3.0, 1.5],
+    [4.0, 1.5],
+    [5.0, 1.5],
+    [6.0, 1.5],
 ]
 
 mask = np.ones((8, 8), dtype=bool)
@@ -78,100 +77,99 @@ def test_binary():
 mask[7, 0:3] = False
 
 
-def test_nodata():
+@pytest.mark.parametrize("level", [0.5, None])
+def test_nodata(level):
     # Test missing data via NaNs in input array
     b = np.copy(a)
     b[~mask] = np.nan
-    contours = find_contours(b, 0.5, positive_orientation='high')
+    contours = find_contours(b, level, positive_orientation='high')
     assert len(contours) == 1
     assert_array_equal(contours[0], mask_contour)
 
 
-def test_mask():
+@pytest.mark.parametrize("level", [0.5, None])
+def test_mask(level):
     # Test missing data via explicit masking
-    contours = find_contours(a, 0.5, positive_orientation='high', mask=mask)
+    contours = find_contours(a, level, positive_orientation='high', mask=mask)
     assert len(contours) == 1
     assert_array_equal(contours[0], mask_contour)
 
 
-def test_mask_shape():
+@pytest.mark.parametrize("level", [0, None])
+def test_mask_shape(level):
     bad_mask = np.ones((8, 7), dtype=bool)
-    with raises(ValueError, match='shape'):
-        find_contours(a, 0, mask=bad_mask)
+    with pytest.raises(ValueError, match='shape'):
+        find_contours(a, level, mask=bad_mask)
 
 
-def test_mask_dtype():
+@pytest.mark.parametrize("level", [0, None])
+def test_mask_dtype(level):
     bad_mask = np.ones((8, 8), dtype=np.uint8)
-    with raises(TypeError, match='binary'):
-        find_contours(a, 0, mask=bad_mask)
+    with pytest.raises(TypeError, match='binary'):
+        find_contours(a, level, mask=bad_mask)
 
 
 def test_float():
     contours = find_contours(r, 0.5)
     assert len(contours) == 1
-    assert_array_equal(contours[0],
-                    [[ 2.,  3.],
-                     [ 1.,  2.],
-                     [ 2.,  1.],
-                     [ 3.,  2.],
-                     [ 2.,  3.]])
+    assert_array_equal(contours[0], [[2., 3.],
+                                     [1., 2.],
+                                     [2., 1.],
+                                     [3., 2.],
+                                     [2., 3.]])
 
 
-def test_memory_order():
-    contours = find_contours(np.ascontiguousarray(r), 0.5)
+@pytest.mark.parametrize("level", [0.5, None])
+def test_memory_order(level):
+    contours = find_contours(np.ascontiguousarray(r), level)
     assert len(contours) == 1
 
-    contours = find_contours(np.asfortranarray(r), 0.5)
+    contours = find_contours(np.asfortranarray(r), level)
     assert len(contours) == 1
 
 
 def test_invalid_input():
-    with testing.raises(ValueError):
+    with pytest.raises(ValueError):
         find_contours(r, 0.5, 'foo', 'bar')
-    with testing.raises(ValueError):
+    with pytest.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
-
-
 def test_level_default():
     # image with range [0.9, 0.91]
     image = np.random.random((100, 100)) * 0.01 + 0.9
     contours = find_contours(image)  # use default level
     # many contours should be found
     assert len(contours) > 1
+
+
+@pytest.mark.parametrize("image", [
+    [[0.13680, 0.11220, 0.0, 0.0, 0.0, 0.19417, 0.19417, 0.33701],
+     [0.0, 0.15140, 0.10267, 0.0, np.nan, 0.14908, 0.18158, 0.19178],
+     [0.0, 0.06949, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01860],
+     [0.0, 0.06949, 0.0, 0.17852, 0.08469, 0.02135, 0.08198, np.nan],
+     [0.0, 0.08244, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan],
+     [0.12342, 0.21330, 0.0, np.nan, 0.01301, 0.04335, 0.0, 0.0]],
+
+    [[0.08, -0.03, -0.17, -0.08, 0.24, 0.06, 0.17, -0.02],
+     [0.12, 0., np.nan, 0.24, 0., -0.53, 0.26, 0.16],
+     [0.39, 0., 0., 0., 0., -0.02, -0.3, 0.01],
+     [0.28, -0.04, -0.03, 0.16, 0.12, 0.01, -0.87, 0.16],
+     [0.26, 0.08, 0.08, 0.08, 0.12, 0.13, 0.11, 0.19],
+     [0.27, 0.24, 0., 0.25, 0.32, 0.19, 0.26, 0.22]],
+
+    [[-0.18, np.nan, np.nan, 0.22, -0.14, -0.23, -0.2, -0.17, -0.19, -0.24],
+     [0., np.nan, np.nan, np.nan, -0.1, -0.24, -0.15, -0.02, -0.09, -0.21],
+     [0.43, 0.19, np.nan, np.nan, -0.01, -0.2, -0.22, -0.18, -0.16, -0.07],
+     [0.23, 0., np.nan, -0.06, -0.07, -0.21, -0.24, -0.25, -0.23, -0.13],
+     [-0.05, -0.11, 0., 0.1, -0.19, -0.23, -0.23, -0.18, -0.19, -0.16],
+     [-0.19, -0.05, 0.13, -0.08, -0.22, -0.23, -0.26, -0.15, -0.12, -0.13],
+     [-0.2, -0.11, -0.11, -0.24, -0.29, -0.27, -0.35, -0.36, -0.27, -0.13],
+     [-0.28, -0.33, -0.31, -0.36, -0.39, -0.37, -0.38, -0.32, -0.34, -0.2],
+     [-0.28, -0.33, -0.39, -0.4, -0.42, -0.38, -0.35, -0.39, -0.35, -0.34],
+     [-0.38, -0.35, -0.41, -0.42, -0.39, -0.36, -0.34, -0.36, -0.28, -0.34]]
+])
+def test_keyerror_fix(image):
+    """Failing samples from issue #4830
+    """
+    find_contours(np.array(image, np.float32), 0)