scikit-image
198 строк · 8.3 Кб
1import numpy as np2from scipy import sparse3from scipy.sparse import csgraph4from ..morphology._util import _raveled_offsets_and_distances5from ..util._map_array import map_array6
7
8def _weighted_abs_diff(values0, values1, distances):9"""A default edge function for complete image graphs.10
11A pixel graph on an image with no edge values and no mask is a very
12boring regular lattice, so we define a default edge weight to be the
13absolute difference between values *weighted* by the distance
14between them.
15
16Parameters
17----------
18values0 : array
19The pixel values for each node.
20values1 : array
21The pixel values for each neighbor.
22distances : array
23The distance between each node and its neighbor.
24
25Returns
26-------
27edge_values : array of float
28The computed values: abs(values0 - values1) * distances.
29"""
30return np.abs(values0 - values1) * distances31
32
33def pixel_graph(image, *, mask=None, edge_function=None, connectivity=1, spacing=None):34"""Create an adjacency graph of pixels in an image.35
36Pixels where the mask is True are nodes in the returned graph, and they are
37connected by edges to their neighbors according to the connectivity
38parameter. By default, the *value* of an edge when a mask is given, or when
39the image is itself the mask, is the Euclidean distance between the pixels.
40
41However, if an int- or float-valued image is given with no mask, the value
42of the edges is the absolute difference in intensity between adjacent
43pixels, weighted by the Euclidean distance.
44
45Parameters
46----------
47image : array
48The input image. If the image is of type bool, it will be used as the
49mask as well.
50mask : array of bool
51Which pixels to use. If None, the graph for the whole image is used.
52edge_function : callable
53A function taking an array of pixel values, and an array of neighbor
54pixel values, and an array of distances, and returning a value for the
55edge. If no function is given, the value of an edge is just the
56distance.
57connectivity : int
58The square connectivity of the pixel neighborhood: the number of
59orthogonal steps allowed to consider a pixel a neighbor. See
60`scipy.ndimage.generate_binary_structure` for details.
61spacing : tuple of float
62The spacing between pixels along each axis.
63
64Returns
65-------
66graph : scipy.sparse.csr_matrix
67A sparse adjacency matrix in which entry (i, j) is 1 if nodes i and j
68are neighbors, 0 otherwise.
69nodes : array of int
70The nodes of the graph. These correspond to the raveled indices of the
71nonzero pixels in the mask.
72"""
73if mask is None:74if image.dtype == bool:75mask = image76else:77mask = np.ones_like(image, dtype=bool)78
79if edge_function is None:80if image.dtype == bool:81
82def edge_function(x, y, distances):83return distances84
85else:86edge_function = _weighted_abs_diff87
88# Strategy: we are going to build the (i, j, data) arrays of a scipy89# sparse COO matrix, then convert to CSR (which is fast).90# - grab the raveled IDs of the foreground (mask == True) parts of the91# image **in the padded space**.92# - broadcast them together with the raveled offsets to their neighbors.93# This gives us for each foreground pixel a list of neighbors (that94# may or may not be selected by the mask). (We also track the *distance*95# to each neighbor.)96# - select "valid" entries in the neighbors and distance arrays by indexing97# into the mask, which we can do since these are raveled indices.98# - use np.repeat() to repeat each source index according to the number99# of neighbors selected by the mask it has. Each of these repeated100# indices will be lined up with its neighbor, i.e. **this is the i101# array** of the COO format matrix.102# - use the mask as a boolean index to get a 1D view of the selected103# neighbors. **This is the j array.**104# - by default, the same boolean indexing can be applied to the distances105# to each neighbor, to give the **data array.** Optionally, a106# provided edge function can be computed on the pixel values and the107# distances to give a different value for the edges.108# Note, we use map_array to map the raveled coordinates in the padded109# image to the ones in the original image, and those are the returned110# nodes.111padded = np.pad(mask, 1, mode='constant', constant_values=False)112nodes_padded = np.flatnonzero(padded)113neighbor_offsets_padded, distances_padded = _raveled_offsets_and_distances(114padded.shape, connectivity=connectivity, spacing=spacing115)116neighbors_padded = nodes_padded[:, np.newaxis] + neighbor_offsets_padded117neighbor_distances_full = np.broadcast_to(distances_padded, neighbors_padded.shape)118nodes = np.flatnonzero(mask)119nodes_sequential = np.arange(nodes.size)120# neighbors outside the mask get mapped to 0, which is a valid index,121# BUT, they will be masked out in the next step.122neighbors = map_array(neighbors_padded, nodes_padded, nodes)123neighbors_mask = padded.reshape(-1)[neighbors_padded]124num_neighbors = np.sum(neighbors_mask, axis=1)125indices = np.repeat(nodes, num_neighbors)126indices_sequential = np.repeat(nodes_sequential, num_neighbors)127neighbor_indices = neighbors[neighbors_mask]128neighbor_distances = neighbor_distances_full[neighbors_mask]129neighbor_indices_sequential = map_array(neighbor_indices, nodes, nodes_sequential)130
131image_r = image.reshape(-1)132data = edge_function(133image_r[indices], image_r[neighbor_indices], neighbor_distances134)135
136m = nodes_sequential.size137mat = sparse.coo_matrix(138(data, (indices_sequential, neighbor_indices_sequential)), shape=(m, m)139)140graph = mat.tocsr()141return graph, nodes142
143
144def central_pixel(graph, nodes=None, shape=None, partition_size=100):145"""Find the pixel with the highest closeness centrality.146
147Closeness centrality is the inverse of the total sum of shortest distances
148from a node to every other node.
149
150Parameters
151----------
152graph : scipy.sparse.csr_matrix
153The sparse matrix representation of the graph.
154nodes : array of int
155The raveled index of each node in graph in the image. If not provided,
156the returned value will be the index in the input graph.
157shape : tuple of int
158The shape of the image in which the nodes are embedded. If provided,
159the returned coordinates are a NumPy multi-index of the same
160dimensionality as the input shape. Otherwise, the returned coordinate
161is the raveled index provided in `nodes`.
162partition_size : int
163This function computes the shortest path distance between every pair
164of nodes in the graph. This can result in a very large (N*N) matrix.
165As a simple performance tweak, the distance values are computed in
166lots of `partition_size`, resulting in a memory requirement of only
167partition_size*N.
168
169Returns
170-------
171position : int or tuple of int
172If shape is given, the coordinate of the central pixel in the image.
173Otherwise, the raveled index of that pixel.
174distances : array of float
175The total sum of distances from each node to each other reachable
176node.
177"""
178if nodes is None:179nodes = np.arange(graph.shape[0])180if partition_size is None:181num_splits = 1182else:183num_splits = max(2, graph.shape[0] // partition_size)184idxs = np.arange(graph.shape[0])185total_shortest_path_len_list = []186for partition in np.array_split(idxs, num_splits):187shortest_paths = csgraph.shortest_path(graph, directed=False, indices=partition)188shortest_paths_no_inf = np.nan_to_num(shortest_paths)189total_shortest_path_len_list.append(np.sum(shortest_paths_no_inf, axis=1))190total_shortest_path_len = np.concatenate(total_shortest_path_len_list)191nonzero = np.flatnonzero(total_shortest_path_len)192min_sp = np.argmin(total_shortest_path_len[nonzero])193raveled_index = nodes[nonzero[min_sp]]194if shape is not None:195central = np.unravel_index(raveled_index, shape)196else:197central = raveled_index198return central, total_shortest_path_len199