From 97f3ad1a6b3a681102703de3d2e067dc92118823 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 14 Dec 2015 16:59:56 +1100 Subject: [PATCH 1/9] NetworkX is required so remove optional import --- skimage/future/graph/rag.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py index 2975a027..4be37b05 100644 --- a/skimage/future/graph/rag.py +++ b/skimage/future/graph/rag.py @@ -1,15 +1,4 @@ -try: - import networkx as nx -except ImportError: - msg = "Graph functions require networkx, which is not installed" - - class nx: - class Graph: - def __init__(self, *args, **kwargs): - raise ImportError(msg) - import warnings - warnings.warn(msg) - +import networkx as nx import numpy as np from scipy import ndimage as ndi import math From c3d52d602c438c5f2b9f8c639350537e33b57a2e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 14 Dec 2015 17:00:51 +1100 Subject: [PATCH 2/9] Simplify reference to generate_binary_structure --- skimage/future/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py index 4be37b05..e27b0b83 100644 --- a/skimage/future/graph/rag.py +++ b/skimage/future/graph/rag.py @@ -213,7 +213,7 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', Pixels with a squared distance less than `connectivity` from each other are considered adjacent. It can range from 1 to `labels.ndim`. Its behavior is the same as `connectivity` parameter in - `scipy.ndimage.filters.generate_binary_structure`. + `scipy.ndimage.generate_binary_structure`. mode : {'distance', 'similarity'}, optional The strategy to assign edge weights. From e9c4c87519d043857469efd6b931d8b16b3a61ef Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 14 Dec 2015 17:01:19 +1100 Subject: [PATCH 3/9] Make RAG generic, requiring only label_image --- skimage/future/graph/rag.py | 139 ++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 60 deletions(-) diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py index e27b0b83..00ab7a48 100644 --- a/skimage/future/graph/rag.py +++ b/skimage/future/graph/rag.py @@ -40,16 +40,93 @@ def min_weight(graph, src, dst, n): return min(w1, w2) +def _add_edge_filter(values, graph): + """Create edge in `g` between the first element of `values` and the rest. + + Add an edge between the first element in `values` and + all other elements of `values` in the graph `g`. `values[0]` + is expected to be the central value of the footprint used. + + Parameters + ---------- + values : array + The array to process. + graph : RAG + The graph to add edges in. + + Returns + ------- + 0 : float + Always returns 0. The return value is required so that `generic_filter` + can put it in the output array, but it is ignored by this filter. + """ + values = values.astype(int) + current = values[0] + for value in values[1:]: + if value != current: + graph.add_edge(current, value) + return 0. + + class RAG(nx.Graph): """ The Region Adjacency Graph (RAG) of an image, subclasses `networx.Graph `_ + + Parameters + ---------- + label_image : array of int + An initial segmentation, with each region labeled as a different + integer. Every unique value in ``label_image`` will correspond to + a node in the graph. + connectivity : int in {1, ..., ``label_image.ndim``}, optional + The connectivity between pixels in ``label_image``. For a 2D image, + a connectivity of 1 corresponds to immediate neighbors up, down, + left, and right, while a connectivity of 2 also includes diagonal + neighbors. See `scipy.ndimage.generate_binary_structure`. + data : networkx Graph specification, optional + Initial or additional edges to pass to the NetworkX Graph + constructor. See `networkx.Graph`. Valid edge specifications + include edge list (list of tuples), NumPy arrays, and SciPy + sparse matrices. + **attr : keyword arguments, optional + Additional attributes to add to the graph. """ - def __init__(self, data=None, **attr): + def __init__(self, label_image=None, connectivity=1, data=None, **attr): super(RAG, self).__init__(data, **attr) + + if label_image is not None: + # The footprint is constructed such that the first + # element in the array being passed to _add_edge_filter is + # the central value. + # + # For example + # if labels.ndim = 2 and connectivity = 1, then + # fp = [[0,0,0], + # [0,1,1], + # [0,1,0]] + # + # if labels.ndim = 2 and connectivity = 2, then + # fp = [[0,0,0], + # [0,1,1], + # [0,1,1]] + fp = ndi.generate_binary_structure(label_image.ndim, connectivity) + for d in range(fp.ndim): + fp = fp.swapaxes(0, d) + fp[0, ...] = 0 + fp = fp.swapaxes(0, d) + + ndi.generic_filter( + label_image, + function=_add_edge_filter, + footprint=fp, + mode='nearest', + output=np.empty(labels_image.shape, dtype=np.uint8), + extra_arguments=(self,)) + try: self.max_id = max(self.nodes_iter()) except ValueError: @@ -161,36 +238,6 @@ class RAG(nx.Graph): super(RAG, self).add_node(n) -def _add_edge_filter(values, graph): - """Create edge in `g` between the first element of `values` and the rest. - - Add an edge between the first element in `values` and - all other elements of `values` in the graph `g`. `values[0]` - is expected to be the central value of the footprint used. - - Parameters - ---------- - values : array - The array to process. - graph : RAG - The graph to add edges in. - - Returns - ------- - 0 : int - Always returns 0. The return value is required so that `generic_filter` - can put it in the output array. - - """ - values = values.astype(int) - current = values[0] - for value in values[1:]: - if value != current: - graph.add_edge(current, value) - - return 0 - - def rag_mean_color(image, labels, connectivity=2, mode='distance', sigma=255.0): """Compute the Region Adjacency Graph using mean colors. @@ -252,35 +299,7 @@ def rag_mean_color(image, labels, connectivity=2, mode='distance', http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.11.5274 """ - graph = RAG() - - # The footprint is constructed in such a way that the first - # element in the array being passed to _add_edge_filter is - # the central value. - fp = ndi.generate_binary_structure(labels.ndim, connectivity) - for d in range(fp.ndim): - fp = fp.swapaxes(0, d) - fp[0, ...] = 0 - fp = fp.swapaxes(0, d) - - # For example - # if labels.ndim = 2 and connectivity = 1 - # fp = [[0,0,0], - # [0,1,1], - # [0,1,0]] - # - # if labels.ndim = 2 and connectivity = 2 - # fp = [[0,0,0], - # [0,1,1], - # [0,1,1]] - - ndi.generic_filter( - labels, - function=_add_edge_filter, - footprint=fp, - mode='nearest', - output=np.zeros(labels.shape, dtype=np.uint8), - extra_arguments=(graph,)) + graph = RAG(labels, connectivity=connectivity) for n in graph: graph.node[n].update({'labels': [n], From d5819f664ab8bb14653915b4b5d99d5eb713359f Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 14 Dec 2015 17:11:11 +1100 Subject: [PATCH 4/9] Add tests for generic RAG construction --- skimage/future/graph/tests/test_rag.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/skimage/future/graph/tests/test_rag.py b/skimage/future/graph/tests/test_rag.py index 35c22218..e5882e20 100644 --- a/skimage/future/graph/tests/test_rag.py +++ b/skimage/future/graph/tests/test_rag.py @@ -178,3 +178,21 @@ def test_ncut_stable_subgraph(): new_labels, _, _ = segmentation.relabel_sequential(new_labels) assert new_labels.max() == 0 + + +def test_generic_rag_2d(): + labels = np.array([[1, 2], [3, 4]], dtype=np.uint8) + g = graph.RAG(labels) + assert g.has_edge(1, 2) and g.has_edge(2, 4) and not g.has_edge(1, 4) + h = graph.RAG(labels, connectivity=2) + assert h.has_edge(1, 2) and h.has_edge(1, 4) and h.has_edge(2, 3) + + +def test_generic_rag_3d(): + labels = np.arange(8, dtype=np.uint8).reshape((2, 2, 2)) + g = graph.RAG(labels) + assert g.has_edge(0, 1) and g.has_edge(1, 3) and not g.has_edge(0, 3) + h = graph.RAG(labels, connectivity=2) + assert h.has_edge(0, 1) and h.has_edge(0, 3) and not h.has_edge(0, 7) + k = graph.RAG(labels, connectivity=3) + assert k.has_edge(0, 1) and k.has_edge(1, 2) and k.has_edge(2, 5) From 49aa7cf999093019493568815edd985ed1bc9714 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 14 Dec 2015 18:42:25 +1100 Subject: [PATCH 5/9] Fix typo in variable name --- skimage/future/graph/rag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py index 00ab7a48..da5d8606 100644 --- a/skimage/future/graph/rag.py +++ b/skimage/future/graph/rag.py @@ -124,7 +124,7 @@ class RAG(nx.Graph): function=_add_edge_filter, footprint=fp, mode='nearest', - output=np.empty(labels_image.shape, dtype=np.uint8), + output=np.empty(label_image.shape, dtype=np.uint8), extra_arguments=(self,)) try: From 53d3154f09d77bcd6ef9efae93bd6bba95f8df54 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 14 Dec 2015 18:57:29 +1100 Subject: [PATCH 6/9] Use a strided 1-element array as dummy filter out When using generic_filter to build a graph, the numerical filter output is actually ignored. Building a full array, even of the smallest dtype, was wasteful. Using stride_tricks solves this by creating a single-element buffer into which all output is fed. --- skimage/future/graph/rag.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py index da5d8606..e97a2293 100644 --- a/skimage/future/graph/rag.py +++ b/skimage/future/graph/rag.py @@ -1,5 +1,6 @@ import networkx as nx import numpy as np +from numpy.lib.stride_tricks import as_strided from scipy import ndimage as ndi import math from ... import draw, measure, segmentation, util, color @@ -124,7 +125,9 @@ class RAG(nx.Graph): function=_add_edge_filter, footprint=fp, mode='nearest', - output=np.empty(label_image.shape, dtype=np.uint8), + output=as_strided(np.empty((1,), dtype=np.float_), + shape=label_image.shape, + strides=((0,) * label_image.ndim)), extra_arguments=(self,)) try: From 4deeb1f802bca6a849f1077a7371655cd296557f Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Mon, 14 Dec 2015 19:06:43 +1100 Subject: [PATCH 7/9] Move max_id logic to top of RAG constructor max_id is used during RAG building iteration, resulting in an error if it is not defined ahead of the generic_filter stage. --- skimage/future/graph/rag.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py index e97a2293..8710f9bd 100644 --- a/skimage/future/graph/rag.py +++ b/skimage/future/graph/rag.py @@ -98,6 +98,10 @@ class RAG(nx.Graph): def __init__(self, label_image=None, connectivity=1, data=None, **attr): super(RAG, self).__init__(data, **attr) + if self.number_of_nodes() == 0: + self.max_id = 0 + else: + self.max_id = max(self.nodes_iter()) if label_image is not None: # The footprint is constructed such that the first @@ -130,12 +134,6 @@ class RAG(nx.Graph): strides=((0,) * label_image.ndim)), extra_arguments=(self,)) - try: - self.max_id = max(self.nodes_iter()) - except ValueError: - # Empty sequence - self.max_id = 0 - def merge_nodes(self, src, dst, weight_func=min_weight, in_place=True, extra_arguments=[], extra_keywords={}): """Merge node `src` and `dst`. From 5c03f2aff5d131d85914d49732b187fc707e57df Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 15 Dec 2015 10:56:04 +1100 Subject: [PATCH 8/9] Bug: not all edges found by asymmetric footprint If we have a label image: [[1, 2], [3, 4]] Then the footprint: [[0, 0, 0], [0, 1, 1], [0, 1, 1]] will not discover the edge (2, 3), even though that edge should be present when `connectivity=2`. This commit fixes the bug by using a complete footprint, without the top/left surfaces zeroed out. See also: https://github.com/scikit-image/scikit-image/pull/1826#issuecomment-164597442 --- skimage/future/graph/rag.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py index 8710f9bd..8790281c 100644 --- a/skimage/future/graph/rag.py +++ b/skimage/future/graph/rag.py @@ -62,10 +62,10 @@ def _add_edge_filter(values, graph): can put it in the output array, but it is ignored by this filter. """ values = values.astype(int) - current = values[0] - for value in values[1:]: - if value != current: - graph.add_edge(current, value) + center = values[len(values) // 2] + for value in values: + if value != center and not graph.has_edge(center, value): + graph.add_edge(center, value) return 0. @@ -104,26 +104,7 @@ class RAG(nx.Graph): self.max_id = max(self.nodes_iter()) if label_image is not None: - # The footprint is constructed such that the first - # element in the array being passed to _add_edge_filter is - # the central value. - # - # For example - # if labels.ndim = 2 and connectivity = 1, then - # fp = [[0,0,0], - # [0,1,1], - # [0,1,0]] - # - # if labels.ndim = 2 and connectivity = 2, then - # fp = [[0,0,0], - # [0,1,1], - # [0,1,1]] fp = ndi.generate_binary_structure(label_image.ndim, connectivity) - for d in range(fp.ndim): - fp = fp.swapaxes(0, d) - fp[0, ...] = 0 - fp = fp.swapaxes(0, d) - ndi.generic_filter( label_image, function=_add_edge_filter, From 06186710d5b77c91e3dc81a796a07824e2bd0a20 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 16 Dec 2015 08:31:30 +1100 Subject: [PATCH 9/9] Update outdated docstring of _add_edge_filter --- skimage/future/graph/rag.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/future/graph/rag.py b/skimage/future/graph/rag.py index 8790281c..6a45447b 100644 --- a/skimage/future/graph/rag.py +++ b/skimage/future/graph/rag.py @@ -42,10 +42,10 @@ def min_weight(graph, src, dst, n): def _add_edge_filter(values, graph): - """Create edge in `g` between the first element of `values` and the rest. + """Create edge in `graph` between central element of `values` and the rest. - Add an edge between the first element in `values` and - all other elements of `values` in the graph `g`. `values[0]` + Add an edge between the middle element in `values` and + all other elements of `values` into `graph`. ``values[len(values) // 2]`` is expected to be the central value of the footprint used. Parameters