Repository: scikit-image/scikit-image

Issue:
Undefined 'min_mask' in "get_min_ncut"
In my current installation (scikit-image 0.11.3) I get:
`UnboundLocalError: local variable 'min_mask' referenced before assignment` in line 208 of future/graph/graph_cut.py (function `get_min_ncut`).
This only can happen if `cost` keeps the value `np.inf`. `min_mask` will never be assigned before it is returned.

To fix this `min_mask` should be initialized properly at the beginning of the loop.



Discussion:
Thanks for pointing this out. I will try to fix this within the next day or two.

@christian-rauch Can you share the code that lead to this ? I am surprised that `cost` was never less that infinity. 

@vighneshbirodkar That is difficult as I am using the library within a more complex framework. I will try to identify the image data and the parameters with which this error is caused. At least I can tell that this is happening for calling `cut_normalized` with larger thresholds on a RAG with weights from similarity mode. E.g., I was testing with thresholds [0.05, 0.1, 0.2, 0.5] and it happened with threshold 0.5.

@christian-rauch In any case I think putting the necessary check won't cause any harm. If possible, can you tell me what exactly are you using this for (just for my curiosity) ?

Those are the parameters given to get_min_ncut(ev, d, w, num_cuts) that cause the error:
ev:
  [ nan  nan  nan]
d:
  (0, 0)    3.0
  (1, 1)    3.0
  (2, 2)    3.0 
w:
  (0, 0)    1.0
  (1, 0)    1.0
  (2, 0)    1.0
  (0, 1)    1.0
  (1, 1)    1.0
  (2, 1)    1.0
  (0, 2)    1.0
  (1, 2)    1.0
  (2, 2)    1.0 
num_cuts:
  10

@vighneshbirodkar The nan eigen vector results from the normalization of the vector whose values are all equal (at "ev = _ncut.normalize(vectors[:, index2])" in "_ncut_relabel").

I remember @stefanv pointing this out and @jni suggested to let that remain as error. Seeing that you ran into this problem, we have to fix this.
The bigger question is, what is the normalized value given that all your samples are equal ?

@vighneshbirodkar I did not follow the whole algorithm but I think it is resulting from the weight matrix having all weights equal to 1.0 at some place in the subdivision of the graph. So the questions is basically how the original algorithm is dealing with this situation.


Resolving patch:
diff --git a/skimage/future/graph/_ncut.py b/skimage/future/graph/_ncut.py
--- a/skimage/future/graph/_ncut.py
+++ b/skimage/future/graph/_ncut.py
@@ -29,6 +29,7 @@ def DW_matrices(graph):
     W = nx.to_scipy_sparse_matrix(graph, format='csc')
     entries = W.sum(axis=0)
     D = sparse.dia_matrix((entries, 0), shape=W.shape).tocsc()
+
     return D, W
 
 
@@ -65,21 +66,3 @@ def ncut_cost(cut, D, W):
     assoc_b = D.data[~cut].sum()
 
     return (cut_cost / assoc_a) + (cut_cost / assoc_b)
-
-
-def normalize(a):
-    """Normalize values in an array between `0` and `1`.
-
-    Parameters
-    ----------
-    a : ndarray
-        The array to be normalized.
-
-    Returns
-    -------
-    out : ndarray
-        The normalized array.
-    """
-    mi = a.min()
-    mx = a.max()
-    return (a - mi) / (mx - mi)
diff --git a/skimage/future/graph/graph_cut.py b/skimage/future/graph/graph_cut.py
--- a/skimage/future/graph/graph_cut.py
+++ b/skimage/future/graph/graph_cut.py
@@ -195,10 +195,19 @@ def get_min_ncut(ev, d, w, num_cuts):
         The value of the minimum ncut.
     """
     mcut = np.inf
+    mn = ev.min()
+    mx = ev.max()
+
+    # If all values in `ev` are equal, it implies that the graph can't be
+    # further sub-divided. In this case the bi-partition is the the graph
+    # itself and an empty set.
+    min_mask = np.zeros_like(ev, dtype=np.bool)
+    if np.allclose(mn, mx):
+        return min_mask, mcut
 
     # Refer Shi & Malik 2001, Section 3.1.3, Page 892
     # Perform evenly spaced n-cuts and determine the optimal one.
-    for t in np.linspace(0, 1, num_cuts, endpoint=False):
+    for t in np.linspace(mn, mx, num_cuts, endpoint=False):
         mask = ev > t
         cost = _ncut.ncut_cost(mask, d, w)
         if cost < mcut:
@@ -266,7 +275,7 @@ def _ncut_relabel(rag, thresh, num_cuts):
         # Refer Shi & Malik 2001, Section 3.2.3, Page 893
         vals, vectors = np.real(vals), np.real(vectors)
         index2 = _ncut_cy.argmin2(vals)
-        ev = _ncut.normalize(vectors[:, index2])
+        ev = vectors[:, index2]
 
         cut_mask, mcut = get_min_ncut(ev, d, w, num_cuts)
         if (mcut < thresh):


Test patch:
diff --git a/skimage/future/graph/tests/test_rag.py b/skimage/future/graph/tests/test_rag.py
--- a/skimage/future/graph/tests/test_rag.py
+++ b/skimage/future/graph/tests/test_rag.py
@@ -159,3 +159,22 @@ def test_rag_hierarchical():
 
     result = graph.cut_threshold(labels, g, thresh)
     assert np.all(result == result[0, 0])
+
+
+@skipif(not is_installed('networkx'))
+def test_ncut_stable_subgraph():
+    """ Test to catch an error thrown when subgraph has all equal edges. """
+
+    img = np.zeros((100, 100, 3), dtype='uint8')
+
+    labels = np.zeros((100, 100), dtype='uint8')
+    labels[...] = 0
+    labels[:50, :50] = 1
+    labels[:50, 50:] = 2
+
+    rag = graph.rag_mean_color(img, labels, mode='similarity')
+
+    new_labels = graph.cut_normalized(labels, rag, in_place=False)
+    new_labels, _, _ = segmentation.relabel_sequential(new_labels)
+
+    assert new_labels.max() == 0