Repository: scikit-image/scikit-image

Issue:
skimage.morphology.flood bug with F-ordered Numpy Arrays
## Description
I am trying to create a flood mask with skimage.morphology.flood that encompassed all points connected to a given start point in a numpy array. This works when i create a small window around my start point but doesn't when i run it on the full image. I posted this issue on stack overflow and it was pointed out to me that my array is in F-ordering in this entails a bug since skimage expects a C-ordering array. 

<!--
(Note: for guidance on how to use `scikit-image`, please post instead on https://forum.image.sc/tag/scikit-image)
-->

## Way to reproduce
```python
# I have a large array node_space and a point of interest current_point
In [1]: node_space.shape                                                                         
Out[1]: (267, 321, 206)

In [2]: current_point                                                                            
Out[2]: (127, 171, 51)

# When I take a small cube and flood it from my point of interest flood works fine

In [3]: array = node_space[current_point[0]-1: current_point[0]+2, current_point[1]-1: current_point[1]+2, current_point[2]-1: current_point[2]+2]                                        

In [3]: mask = flood(array, (1,1,1))                                                             

In [4]: mask                                                                                     
Out[4]: 
array([[[False, False, False],
        [False, False,  True],
        [False, False, False]],

       [[False, False, False],
        [False,  True, False],
        [False, False, False]],

       [[False, False, False],
        [False, False, False],
        [False, False, False]]])

# However when I run The same operation on the larger image it fails
In [5]: mask = flood(node_space, current_point)    

In [6]: mask[current_point[0]-1: current_point[0]+2, current_point[1]-1: current_point[1]+2, current_point[2]-1: current_point[2]+2]                                                      
Out[6]: 
array([[[False, False, False],
        [False, False, False],
        [False, False, False]],

       [[False, False, False],
        [False, False, False],
        [False, False, False]],

       [[False, False, False],
        [False, False, False],
        [False, False, False]]])
```

## Version information
```python3.7
# Paste the output of the following python commands
from __future__ import print_function
import sys; print(sys.version):
3.7.6 (default, Jan  8 2020, 13:42:34) 
[Clang 4.0.1 (tags/RELEASE_401/final)]

import platform; print(platform.platform()):
Darwin-18.7.0-x86_64-i386-64bit

import skimage; print("scikit-image version: {}".format(skimage.__version__)):
scikit-image version: 0.16.2

import numpy; print("numpy version: {}".format(numpy.__version__))
numpy version: 1.18.2
```

```python
# your output here

```



Discussion:
Thank you @MarcoPerezCaceres for reporting this.

> it was pointed out to me that my array is in F-ordering

Can you please confirm this assumption using
```python
mask = flood(node_space.copy(order='C'), current_point) 
```
@rfezzani sorry, I meant to provide some extra context here but didn't get a chance. I've verified the behaviour myself with the original dataset at the [SO question](https://stackoverflow.com/q/60934550/224254). 
`node_space` is actually a super-sparse array, so you can verify the behaviour by creating it as follows:

```python
node_space = np.zeros((267, 321, 206), dtype=np.float64, order='F')
points = (
    array([121, 121, 122, 123, 124, 125, 126, 127]),
    array([173, 174, 173, 172, 171, 171, 171, 171]),
    array([56, 55, 55, 55, 54, 53, 52, 51]),
)
node_space[points] = 1
```
CC @JDWarner @lagru 

The quick fix is to always pass the input through `np.ascontiguousarray`, which we should do, before deciding whether we want to go through a "proper" fix of ensuring that the neighbors code works with F-ordering.
@MarcoPerezCaceres thank you for reporting! 😊 
OK, excellent, thank you @jni for all these information. The quick fix sound reasonable.
Creating a new C ordered node_space got the flood operation working!

Thank you for your help,

Marco

On Sat, Apr 4, 2020 at 6:02 AM Riadh Fezzani <notifications@github.com>
wrote:

> Thank you @MarcoPerezCaceres <https://github.com/MarcoPerezCaceres> for
> reporting this.
>
> it was pointed out to me that my array is in F-ordering
>
> Can you please confirm this assumption using
>
> mask = flood(node_space.copy(order='C'), current_point)
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/scikit-image/scikit-image/issues/4549#issuecomment-609005907>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AND2RTQW5AZL65DS33OPHLTRK4ARXANCNFSM4L247TRA>
> .
>

Wew, quite a few things here which might be oversights:

- `_fast_pad` always returns a c-contiguous array regardless of the input's order
- `_offsets_to_raveled_neighbors` is missing the `order` parameter
- `order` parameter is missing in all calls to `ravel()` which therefore defaults to `order="C"`

I'll make a PR in a moment.

Thanks for the ping. :wink: 

Resolving patch:
diff --git a/skimage/morphology/_flood_fill.py b/skimage/morphology/_flood_fill.py
--- a/skimage/morphology/_flood_fill.py
+++ b/skimage/morphology/_flood_fill.py
@@ -240,16 +240,16 @@ def flood(image, seed_point, *, selem=None, connectivity=None, tolerance=None):
     selem = _resolve_neighborhood(selem, connectivity, image.ndim)
 
     # Must annotate borders
-    working_image = _fast_pad(image, image.min())
+    working_image = _fast_pad(image, image.min(), order=order)
 
     # Stride-aware neighbors - works for both C- and Fortran-contiguity
     ravelled_seed_idx = np.ravel_multi_index([i+1 for i in seed_point],
                                              working_image.shape, order=order)
     neighbor_offsets = _offsets_to_raveled_neighbors(
-        working_image.shape, selem, center=((1,) * image.ndim))
+        working_image.shape, selem, center=((1,) * image.ndim), order=order)
 
     # Use a set of flags; see _flood_fill_cy.pyx for meanings
-    flags = np.zeros(working_image.shape, dtype=np.uint8)
+    flags = np.zeros(working_image.shape, dtype=np.uint8, order=order)
     _set_edge_values_inplace(flags, value=2)
 
     try:
@@ -265,16 +265,16 @@ def flood(image, seed_point, *, selem=None, connectivity=None, tolerance=None):
             high_tol = min(max_value, seed_value + tolerance)
             low_tol = max(min_value, seed_value - tolerance)
 
-            _flood_fill_tolerance(working_image.ravel(),
-                                  flags.ravel(),
+            _flood_fill_tolerance(working_image.ravel(order),
+                                  flags.ravel(order),
                                   neighbor_offsets,
                                   ravelled_seed_idx,
                                   seed_value,
                                   low_tol,
                                   high_tol)
         else:
-            _flood_fill_equal(working_image.ravel(),
-                              flags.ravel(),
+            _flood_fill_equal(working_image.ravel(order),
+                              flags.ravel(order),
                               neighbor_offsets,
                               ravelled_seed_idx,
                               seed_value)
diff --git a/skimage/morphology/extrema.py b/skimage/morphology/extrema.py
--- a/skimage/morphology/extrema.py
+++ b/skimage/morphology/extrema.py
@@ -239,7 +239,7 @@ def _set_edge_values_inplace(image, value):
         image[tuple(sl)] = value
 
 
-def _fast_pad(image, value):
+def _fast_pad(image, value, *, order="C"):
     """Pad an array on all axes with one constant value.
 
     Parameters
@@ -248,6 +248,8 @@ def _fast_pad(image, value):
         Image to pad.
     value : scalar
          The value to use. Should be compatible with `image`'s dtype.
+    order : "C" or "F"
+        Specify the memory layout of the padded image (C or Fortran style).
 
     Returns
     -------
@@ -273,7 +275,7 @@ def _fast_pad(image, value):
     """
     # Allocate padded image
     new_shape = np.array(image.shape) + 2
-    new_image = np.empty(new_shape, dtype=image.dtype, order="C")
+    new_image = np.empty(new_shape, dtype=image.dtype, order=order)
 
     # Copy old image into new space
     original_slice = tuple(slice(1, -1) for _ in range(image.ndim))


Test patch:
diff --git a/skimage/morphology/tests/test_flood_fill.py b/skimage/morphology/tests/test_flood_fill.py
--- a/skimage/morphology/tests/test_flood_fill.py
+++ b/skimage/morphology/tests/test_flood_fill.py
@@ -1,4 +1,5 @@
 import numpy as np
+import pytest
 from pytest import raises
 
 from skimage.morphology import flood, flood_fill
@@ -254,5 +255,25 @@ def test_basic_nd():
             filled, np.pad(np.ones((3,)*dimension) * 2, 1, 'constant'))
 
 
+@pytest.mark.parametrize("tolerance", [None, 0])
+def test_f_order(tolerance):
+    image = np.array([
+        [0, 0, 0, 0],
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+    ], order="F")
+    expected = np.array([
+        [0, 0, 0, 0],
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+    ], dtype=bool)
+
+    mask = flood(image, seed_point=(1, 0), tolerance=tolerance)
+    np.testing.assert_array_equal(expected, mask)
+
+    mask = flood(image, seed_point=(2, 1), tolerance=tolerance)
+    np.testing.assert_array_equal(expected, mask)
+
+
 if __name__ == "__main__":
     np.testing.run_module_suite()