scikit-image
302 строки · 9.3 Кб
1import networkx as nx
2import numpy as np
3from scipy.sparse import linalg
4
5from . import _ncut, _ncut_cy
6
7
8def cut_threshold(labels, rag, thresh, in_place=True):
9"""Combine regions separated by weight less than threshold.
10
11Given an image's labels and its RAG, output new labels by
12combining regions whose nodes are separated by a weight less
13than the given threshold.
14
15Parameters
16----------
17labels : ndarray
18The array of labels.
19rag : RAG
20The region adjacency graph.
21thresh : float
22The threshold. Regions connected by edges with smaller weights are
23combined.
24in_place : bool
25If set, modifies `rag` in place. The function will remove the edges
26with weights less that `thresh`. If set to `False` the function
27makes a copy of `rag` before proceeding.
28
29Returns
30-------
31out : ndarray
32The new labelled array.
33
34Examples
35--------
36>>> from skimage import data, segmentation, graph
37>>> img = data.astronaut()
38>>> labels = segmentation.slic(img)
39>>> rag = graph.rag_mean_color(img, labels)
40>>> new_labels = graph.cut_threshold(labels, rag, 10)
41
42References
43----------
44.. [1] Alain Tremeau and Philippe Colantoni
45"Regions Adjacency Graph Applied To Color Image Segmentation"
46:DOI:`10.1109/83.841950`
47
48"""
49if not in_place:
50rag = rag.copy()
51
52# Because deleting edges while iterating through them produces an error.
53to_remove = [(x, y) for x, y, d in rag.edges(data=True) if d['weight'] >= thresh]
54rag.remove_edges_from(to_remove)
55
56comps = nx.connected_components(rag)
57
58# We construct an array which can map old labels to the new ones.
59# All the labels within a connected component are assigned to a single
60# label in the output.
61map_array = np.arange(labels.max() + 1, dtype=labels.dtype)
62for i, nodes in enumerate(comps):
63for node in nodes:
64for label in rag.nodes[node]['labels']:
65map_array[label] = i
66
67return map_array[labels]
68
69
70def cut_normalized(
71labels,
72rag,
73thresh=0.001,
74num_cuts=10,
75in_place=True,
76max_edge=1.0,
77*,
78rng=None,
79):
80"""Perform Normalized Graph cut on the Region Adjacency Graph.
81
82Given an image's labels and its similarity RAG, recursively perform
83a 2-way normalized cut on it. All nodes belonging to a subgraph
84that cannot be cut further are assigned a unique label in the
85output.
86
87Parameters
88----------
89labels : ndarray
90The array of labels.
91rag : RAG
92The region adjacency graph.
93thresh : float
94The threshold. A subgraph won't be further subdivided if the
95value of the N-cut exceeds `thresh`.
96num_cuts : int
97The number or N-cuts to perform before determining the optimal one.
98in_place : bool
99If set, modifies `rag` in place. For each node `n` the function will
100set a new attribute ``rag.nodes[n]['ncut label']``.
101max_edge : float, optional
102The maximum possible value of an edge in the RAG. This corresponds to
103an edge between identical regions. This is used to put self
104edges in the RAG.
105rng : {`numpy.random.Generator`, int}, optional
106Pseudo-random number generator.
107By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`).
108If `rng` is an int, it is used to seed the generator.
109
110The `rng` is used to determine the starting point
111of `scipy.sparse.linalg.eigsh`.
112
113Returns
114-------
115out : ndarray
116The new labeled array.
117
118Examples
119--------
120>>> from skimage import data, segmentation, graph
121>>> img = data.astronaut()
122>>> labels = segmentation.slic(img)
123>>> rag = graph.rag_mean_color(img, labels, mode='similarity')
124>>> new_labels = graph.cut_normalized(labels, rag)
125
126References
127----------
128.. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation",
129Pattern Analysis and Machine Intelligence,
130IEEE Transactions on, vol. 22, no. 8, pp. 888-905, August 2000.
131
132"""
133rng = np.random.default_rng(rng)
134if not in_place:
135rag = rag.copy()
136
137for node in rag.nodes():
138rag.add_edge(node, node, weight=max_edge)
139
140_ncut_relabel(rag, thresh, num_cuts, rng)
141
142map_array = np.zeros(labels.max() + 1, dtype=labels.dtype)
143# Mapping from old labels to new
144for n, d in rag.nodes(data=True):
145map_array[d['labels']] = d['ncut label']
146
147return map_array[labels]
148
149
150def partition_by_cut(cut, rag):
151"""Compute resulting subgraphs from given bi-partition.
152
153Parameters
154----------
155cut : array
156A array of booleans. Elements set to `True` belong to one
157set.
158rag : RAG
159The Region Adjacency Graph.
160
161Returns
162-------
163sub1, sub2 : RAG
164The two resulting subgraphs from the bi-partition.
165"""
166# `cut` is derived from `D` and `W` matrices, which also follow the
167# ordering returned by `rag.nodes()` because we use
168# nx.to_scipy_sparse_matrix.
169
170# Example
171# rag.nodes() = [3, 7, 9, 13]
172# cut = [True, False, True, False]
173# nodes1 = [3, 9]
174# nodes2 = [7, 10]
175
176nodes1 = [n for i, n in enumerate(rag.nodes()) if cut[i]]
177nodes2 = [n for i, n in enumerate(rag.nodes()) if not cut[i]]
178
179sub1 = rag.subgraph(nodes1)
180sub2 = rag.subgraph(nodes2)
181
182return sub1, sub2
183
184
185def get_min_ncut(ev, d, w, num_cuts):
186"""Threshold an eigenvector evenly, to determine minimum ncut.
187
188Parameters
189----------
190ev : array
191The eigenvector to threshold.
192d : ndarray
193The diagonal matrix of the graph.
194w : ndarray
195The weight matrix of the graph.
196num_cuts : int
197The number of evenly spaced thresholds to check for.
198
199Returns
200-------
201mask : array
202The array of booleans which denotes the bi-partition.
203mcut : float
204The value of the minimum ncut.
205"""
206mcut = np.inf
207mn = ev.min()
208mx = ev.max()
209
210# If all values in `ev` are equal, it implies that the graph can't be
211# further sub-divided. In this case the bi-partition is the the graph
212# itself and an empty set.
213min_mask = np.zeros_like(ev, dtype=bool)
214if np.allclose(mn, mx):
215return min_mask, mcut
216
217# Refer Shi & Malik 2001, Section 3.1.3, Page 892
218# Perform evenly spaced n-cuts and determine the optimal one.
219for t in np.linspace(mn, mx, num_cuts, endpoint=False):
220mask = ev > t
221cost = _ncut.ncut_cost(mask, d, w)
222if cost < mcut:
223min_mask = mask
224mcut = cost
225
226return min_mask, mcut
227
228
229def _label_all(rag, attr_name):
230"""Assign a unique integer to the given attribute in the RAG.
231
232This function assumes that all labels in `rag` are unique. It
233picks up a random label from them and assigns it to the `attr_name`
234attribute of all the nodes.
235
236rag : RAG
237The Region Adjacency Graph.
238attr_name : string
239The attribute to which a unique integer is assigned.
240"""
241node = min(rag.nodes())
242new_label = rag.nodes[node]['labels'][0]
243for n, d in rag.nodes(data=True):
244d[attr_name] = new_label
245
246
247def _ncut_relabel(rag, thresh, num_cuts, random_generator):
248"""Perform Normalized Graph cut on the Region Adjacency Graph.
249
250Recursively partition the graph into 2, until further subdivision
251yields a cut greater than `thresh` or such a cut cannot be computed.
252For such a subgraph, indices to labels of all its nodes map to a single
253unique value.
254
255Parameters
256----------
257rag : RAG
258The region adjacency graph.
259thresh : float
260The threshold. A subgraph won't be further subdivided if the
261value of the N-cut exceeds `thresh`.
262num_cuts : int
263The number or N-cuts to perform before determining the optimal one.
264random_generator : `numpy.random.Generator`
265Provides initial values for eigenvalue solver.
266"""
267d, w = _ncut.DW_matrices(rag)
268m = w.shape[0]
269
270if m > 2:
271d2 = d.copy()
272# Since d is diagonal, we can directly operate on its data
273# the inverse of the square root
274d2.data = np.reciprocal(np.sqrt(d2.data, out=d2.data), out=d2.data)
275
276# Refer Shi & Malik 2001, Equation 7, Page 891
277A = d2 * (d - w) * d2
278# Initialize the vector to ensure reproducibility.
279v0 = random_generator.random(A.shape[0])
280vals, vectors = linalg.eigsh(A, which='SM', v0=v0, k=min(100, m - 2))
281
282# Pick second smallest eigenvector.
283# Refer Shi & Malik 2001, Section 3.2.3, Page 893
284vals, vectors = np.real(vals), np.real(vectors)
285index2 = _ncut_cy.argmin2(vals)
286ev = vectors[:, index2]
287
288cut_mask, mcut = get_min_ncut(ev, d, w, num_cuts)
289if mcut < thresh:
290# Sub divide and perform N-cut again
291# Refer Shi & Malik 2001, Section 3.2.5, Page 893
292sub1, sub2 = partition_by_cut(cut_mask, rag)
293
294_ncut_relabel(sub1, thresh, num_cuts, random_generator)
295_ncut_relabel(sub2, thresh, num_cuts, random_generator)
296return
297
298# The N-cut wasn't small enough, or could not be computed.
299# The remaining graph is a region.
300# Assign `ncut label` by picking any label from the existing nodes, since
301# `labels` are unique, `new_label` is also unique.
302_label_all(rag, 'ncut label')
303