scikit-image
909 строк · 34.8 Кб
1#cython: cdivision=True
2#cython: nonecheck=False
3"""
4Cython implementation of Dijkstra's minimum cost path algorithm,
5for use with data on a n-dimensional lattice.
6"""
7import cython
8import numpy as np
9from . import heap
10from .._shared.utils import warn
11
12cimport numpy as cnp
13from . cimport heap
14
15cnp.import_array()
16
17OFFSET_D = np.int8
18OFFSETS_INDEX_D = np.int16
19EDGE_D = np.int8
20INDEX_D = np.intp
21FLOAT_D = np.float64
22
23
24@cython.boundscheck(False)
25@cython.wraparound(False)
26def _get_edge_map(shape):
27"""Return an array with edge points/lines/planes/hyperplanes marked.
28
29Given a shape (of length n), return an edge_map array with a shape of
30original_shape + (n,), where, for each dimension, edge_map[...,dim] will
31have zeros at indices not along an edge in that dimension, -1s at indices
32along the lower boundary, and +1s on the upper boundary.
33
34This allows one to, given an nd index, calculate not only if the index is
35at the edge of the array, but if so, which edge(s) it lies along.
36
37"""
38d = len(shape)
39edges = np.zeros(shape+(d,), order='F', dtype=EDGE_D)
40for i in range(d):
41slices = [slice(None)] * (d+1)
42slices[d] = i
43slices[i] = 0
44edges[tuple(slices)] = -1
45slices[i] = -1
46edges[tuple(slices)] = 1
47return edges
48
49
50@cython.boundscheck(False)
51@cython.wraparound(False)
52def _offset_edge_map(shape, offsets):
53"""Return an array with positions marked where offsets will step
54out of bounds.
55
56Given a shape (of length n) and a list of n-d offsets, return a two arrays
57of (n,) + shape: pos_edge_map and neg_edge_map.
58For each dimension xxx_edge_map[dim, ...] has zeros at indices at which
59none of the given offsets (in that dimension) of the given sign (positive
60or negative, respectively) will step out of bounds. If the value is
61nonzero, it gives the largest offset (in terms of absolute value) that
62will step out of bounds in that direction.
63
64An example will be explanatory:
65>>> offsets = [[-2,0], [1,1], [0,2]]
66>>> pos_edge_map, neg_edge_map = _offset_edge_map((4,4), offsets)
67>>> neg_edge_map[0]
68array([[-1, -1, -1, -1],
69[-2, -2, -2, -2],
70[ 0, 0, 0, 0],
71[ 0, 0, 0, 0]], dtype=int8)
72
73>>> pos_edge_map[1]
74array([[0, 0, 2, 1],
75[0, 0, 2, 1],
76[0, 0, 2, 1],
77[0, 0, 2, 1]], dtype=int8)
78
79"""
80indices = np.indices(shape) # indices.shape = (n,)+shape
81
82#get the distance from each index to the upper or lower edge in each dim
83pos_edges = (shape - indices.T).T
84neg_edges = -1 - indices
85# now set the distances to zero if none of the given offsets could reach
86offsets = np.asarray(offsets)
87maxes = offsets.max(axis=0)
88mins = offsets.min(axis=0)
89for pos, neg, mx, mn in zip(pos_edges, neg_edges, maxes, mins):
90pos[pos > mx] = 0
91neg[neg < mn] = 0
92return pos_edges.astype(EDGE_D), neg_edges.astype(EDGE_D)
93
94
95@cython.boundscheck(False)
96@cython.wraparound(False)
97def make_offsets(d, fully_connected):
98"""Make a list of offsets from a center point defining a n-dim
99neighborhood.
100
101Parameters
102----------
103d : int
104dimension of the offsets to produce
105fully_connected : bool
106whether the neighborhood should be singly- of fully-connected
107
108Returns
109-------
110offsets : list of tuples of length `d`
111
112Examples
113--------
114
115The singly-connected 2-d neighborhood is four offsets:
116
117>>> make_offsets(2, False)
118[(-1,0), (1,0), (0,-1), (0,1)]
119
120While the fully-connected 2-d neighborhood is the full cartesian product
121of {-1, 0, 1} (less the origin (0,0)).
122
123"""
124if fully_connected:
125mask = np.ones([3]*d, dtype=np.uint8)
126mask[tuple([1]*d)] = 0
127else:
128mask = np.zeros([3]*d, dtype=np.uint8)
129for i in range(d):
130indices = [1]*d
131indices[i] = (0, -1)
132mask[tuple(indices)] = 1
133offsets = []
134for indices, value in np.ndenumerate(mask):
135if value == 1:
136indices = np.array(indices) - 1
137offsets.append(indices)
138return offsets
139
140
141@cython.boundscheck(True)
142@cython.wraparound(True)
143def _unravel_index_fortran(flat_indices, shape):
144"""_unravel_index_fortran(flat_indices, shape)
145
146Given a flat index into an n-d fortran-strided array, return an
147index tuple.
148
149"""
150strides = np.multiply.accumulate([1] + list(shape[:-1]))
151indices = [tuple((idx // strides) % shape) for idx in flat_indices]
152return indices
153
154
155@cython.boundscheck(True)
156@cython.wraparound(True)
157def _ravel_index_fortran(indices, shape):
158"""_ravel_index_fortran(flat_indices, shape)
159
160Given an index tuple into an n-d fortran-strided array, return a
161flat index.
162
163"""
164strides = np.multiply.accumulate([1] + list(shape[:-1]))
165flat_indices = [np.sum(strides * idx) for idx in indices]
166return flat_indices
167
168
169@cython.boundscheck(False)
170@cython.wraparound(False)
171def _normalize_indices(indices, shape):
172"""_normalize_indices(indices, shape)
173
174Make all indices positive. If an index is out-of-bounds, return None.
175
176"""
177new_indices = []
178for index in indices:
179if len(index) != len(shape):
180return None
181new_index = []
182for i, s in zip(index, shape):
183i = int(i)
184if i < 0:
185i = s + i
186if not (0 <= i < s):
187return None
188new_index.append(i)
189new_indices.append(new_index)
190return new_indices
191
192
193@cython.boundscheck(True)
194@cython.wraparound(True)
195def _reverse(arr):
196"""Reverse index an array safely, with bounds/wraparound checks on.
197"""
198return arr[::-1]
199
200
201@cython.boundscheck(False)
202@cython.wraparound(False)
203cdef class MCP:
204"""MCP(costs, offsets=None, fully_connected=True, sampling=None)
205
206A class for finding the minimum cost path through a given n-d costs array.
207
208Given an n-d costs array, this class can be used to find the minimum-cost
209path through that array from any set of points to any other set of points.
210Basic usage is to initialize the class and call find_costs() with a one
211or more starting indices (and an optional list of end indices). After
212that, call traceback() one or more times to find the path from any given
213end-position to the closest starting index. New paths through the same
214costs array can be found by calling find_costs() repeatedly.
215
216The cost of a path is calculated simply as the sum of the values of the
217`costs` array at each point on the path. The class MCP_Geometric, on the
218other hand, accounts for the fact that diagonal vs. axial moves are of
219different lengths, and weights the path cost accordingly.
220
221Array elements with infinite or negative costs will simply be ignored, as
222will paths whose cumulative cost overflows to infinite.
223
224Parameters
225----------
226costs : ndarray
227offsets : iterable, optional
228A list of offset tuples: each offset specifies a valid move from a
229given n-d position.
230If not provided, offsets corresponding to a singly- or fully-connected
231n-d neighborhood will be constructed with make_offsets(), using the
232`fully_connected` parameter value.
233fully_connected : bool, optional
234If no `offsets` are provided, this determines the connectivity of the
235generated neighborhood. If true, the path may go along diagonals
236between elements of the `costs` array; otherwise only axial moves are
237permitted.
238sampling : tuple, optional
239For each dimension, specifies the distance between two cells/voxels.
240If not given or None, the distance is assumed unit.
241
242Attributes
243----------
244offsets : ndarray
245Equivalent to the `offsets` provided to the constructor, or if none
246were so provided, the offsets created for the requested n-d
247neighborhood. These are useful for interpreting the `traceback` array
248returned by the find_costs() method.
249
250"""
251
252def __init__(self, costs, offsets=None, fully_connected=True,
253sampling=None):
254"""__init__(costs, offsets=None, fully_connected=True, sampling=None)
255
256See class documentation.
257"""
258costs = np.asarray(costs)
259if not np.can_cast(costs.dtype, FLOAT_D):
260raise TypeError('cannot cast costs array to ' + str(FLOAT_D))
261
262# Check sampling
263if sampling is None:
264sampling = np.array([1.0 for s in costs.shape], FLOAT_D)
265elif isinstance(sampling, (list, tuple)):
266sampling = np.array(sampling, FLOAT_D)
267if sampling.ndim != 1 or len(sampling) != costs.ndim:
268raise ValueError('Need one sampling element per dimension.')
269else:
270raise ValueError('Invalid type for sampling: %r.' % type(sampling))
271
272# We use flat, fortran-style indexing here (could use C-style,
273# but this is my code and I like fortran-style! Also, it's
274# faster when working with image arrays, which are often
275# already fortran-strided.)
276try:
277self.flat_costs = costs.astype(FLOAT_D, copy=False).ravel('F')
278except TypeError:
279self.flat_costs = costs.astype(FLOAT_D).flatten('F')
280warn('Upgrading NumPy should decrease memory usage and increase'
281' speed.')
282size = self.flat_costs.shape[0]
283self.flat_cumulative_costs = np.empty(size, dtype=FLOAT_D)
284self.dim = len(costs.shape)
285self.costs_shape = costs.shape
286self.costs_heap = heap.FastUpdateBinaryHeap(initial_capacity=128,
287max_reference=size-1)
288
289# This array stores, for each point, the index into the offset
290# array (see below) that leads to that point from the
291# predecessor point.
292self.traceback_offsets = np.empty(size, dtype=OFFSETS_INDEX_D)
293
294# The offsets are a list of relative offsets from a central
295# point to each point in the relevant neighborhood. (e.g. (-1,
296# 0) might be a 2d offset).
297# These offsets are raveled to provide flat, 1d offsets that can be
298# used in the same way for flat indices to move to neighboring points.
299if offsets is None:
300offsets = make_offsets(self.dim, fully_connected)
301self.offsets = np.array(offsets, dtype=OFFSET_D)
302self.flat_offsets = np.array(
303_ravel_index_fortran(self.offsets, self.costs_shape),
304dtype=INDEX_D)
305
306# Instead of unraveling each index during the pathfinding algorithm, we
307# will use a pre-computed "edge map" that specifies for each dimension
308# whether a given index is on a lower or upper boundary (or none at
309# all). Flatten this map to get something that can be indexed as by the
310# same flat indices as elsewhere.
311# The edge map stores more than a boolean "on some edge" flag so as to
312# allow us to examine the non-out-of-bounds neighbors for a given edge
313# point while excluding the neighbors which are outside the array.
314pos, neg = _offset_edge_map(costs.shape, self.offsets)
315self.flat_pos_edge_map = pos.reshape((self.dim, size), order='F')
316self.flat_neg_edge_map = neg.reshape((self.dim, size), order='F')
317
318
319# The offset lengths are the distances traveled along each offset
320self.offset_lengths = np.sqrt(np.sum((sampling * self.offsets)**2,
321axis=1)).astype(FLOAT_D)
322self.dirty = 0
323self.use_start_cost = 1
324
325
326def _reset(self):
327"""_reset()
328Clears paths found by find_costs().
329"""
330
331cdef INDEX_T start
332
333self.costs_heap.reset()
334self.traceback_offsets[...] = -2 # -2 is not reached, -1 is start
335self.flat_cumulative_costs[...] = np.inf
336self.dirty = 0
337
338# Get starts and ends
339# We do not pass them in as arguments for backwards compat
340starts, _ = self._starts, self._ends
341
342# push each start point into the heap. Note that we use flat indexing!
343for start in _ravel_index_fortran(starts, self.costs_shape):
344self.traceback_offsets[start] = -1
345if self.use_start_cost:
346self.costs_heap.push_fast(self.flat_costs[start], start)
347else:
348self.costs_heap.push_fast(0, start)
349
350
351cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost,
352FLOAT_T new_cost, FLOAT_T offset_length):
353""" float _travel_cost(float old_cost, float new_cost,
354float offset_length)
355The travel cost for going from the current node to the next.
356Default is simply the cost of the next node.
357"""
358return new_cost
359
360
361cpdef int goal_reached(self, INDEX_T index, FLOAT_T cumcost):
362""" int goal_reached(int index, float cumcost)
363This method is called each iteration after popping an index
364from the heap, before examining the neighbors.
365
366This method can be overloaded to modify the behavior of the MCP
367algorithm. An example might be to stop the algorithm when a
368certain cumulative cost is reached, or when the front is a
369certain distance away from the seed point.
370
371This method should return 1 if the algorithm should not check
372the current point's neighbors and 2 if the algorithm is now
373done.
374"""
375return 0
376
377
378cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index,
379FLOAT_T offset_length) noexcept:
380""" _examine_neighbor(int index, int new_index, float offset_length)
381This method is called once for every pair of neighboring nodes,
382as soon as both nodes become frozen.
383"""
384pass
385
386
387cdef void _update_node(self, INDEX_T index, INDEX_T new_index,
388FLOAT_T offset_length) noexcept:
389""" _update_node(int index, int new_index, float offset_length)
390This method is called when a node is updated.
391"""
392pass
393
394
395def find_costs(self, starts, ends=None, find_all_ends=True,
396max_coverage=1.0, max_cumulative_cost=None, max_cost=None):
397"""
398Find the minimum-cost path from the given starting points.
399
400This method finds the minimum-cost path to the specified ending
401indices from any one of the specified starting indices. If no end
402positions are given, then the minimum-cost path to every position in
403the costs array will be found.
404
405Parameters
406----------
407starts : iterable
408A list of n-d starting indices (where n is the dimension of the
409`costs` array). The minimum cost path to the closest/cheapest
410starting point will be found.
411ends : iterable, optional
412A list of n-d ending indices.
413find_all_ends : bool, optional
414If 'True' (default), the minimum-cost-path to every specified
415end-position will be found; otherwise the algorithm will stop when
416a a path is found to any end-position. (If no `ends` were
417specified, then this parameter has no effect.)
418
419Returns
420-------
421cumulative_costs : ndarray
422Same shape as the `costs` array; this array records the minimum
423cost path from the nearest/cheapest starting index to each index
424considered. (If `ends` were specified, not all elements in the
425array will necessarily be considered: positions not evaluated will
426have a cumulative cost of inf. If `find_all_ends` is 'False', only
427one of the specified end-positions will have a finite cumulative
428cost.)
429traceback : ndarray
430Same shape as the `costs` array; this array contains the offset to
431any given index from its predecessor index. The offset indices
432index into the `offsets` attribute, which is a array of n-d
433offsets. In the 2-d case, if offsets[traceback[x, y]] is (-1, -1),
434that means that the predecessor of [x, y] in the minimum cost path
435to some start position is [x+1, y+1]. Note that if the
436offset_index is -1, then the given index was not considered.
437
438"""
439# basic variables to use for end-finding; also fix up the start and end
440# lists
441cdef BOOL_T use_ends = 0
442cdef INDEX_T num_ends = 0
443cdef BOOL_T all_ends = find_all_ends
444cdef INDEX_T[:] flat_ends
445starts = _normalize_indices(starts, self.costs_shape)
446if starts is None:
447raise ValueError('start points must all be within the costs array')
448elif not starts:
449raise ValueError('no valid start points to start front' +
450'propagation')
451if ends is not None:
452ends = _normalize_indices(ends, self.costs_shape)
453if ends is None:
454raise ValueError('end points must all be within '
455'the costs array')
456use_ends = 1
457num_ends = len(ends)
458flat_ends = np.array(_ravel_index_fortran(
459ends, self.costs_shape), dtype=INDEX_D)
460
461# Always perform a reset to (re)initialize our arrays and start
462# positions
463self._starts, self._ends = starts, ends
464self._reset()
465
466# Get shorter names for arrays
467cdef FLOAT_T[:] flat_costs = self.flat_costs
468cdef FLOAT_T[:] flat_cumulative_costs = self.flat_cumulative_costs
469cdef OFFSETS_INDEX_T[:] traceback_offsets = self.traceback_offsets
470cdef EDGE_T[:, :] flat_pos_edge_map = self.flat_pos_edge_map
471cdef EDGE_T[:, :] flat_neg_edge_map = self.flat_neg_edge_map
472cdef OFFSET_T[:, :] offsets = self.offsets
473cdef INDEX_T[:] flat_offsets = self.flat_offsets
474cdef FLOAT_T[:] offset_lengths = self.offset_lengths
475
476# Short names for other attributes
477cdef heap.FastUpdateBinaryHeap costs_heap = self.costs_heap
478cdef DIM_T dim = self.dim
479cdef int num_offsets = len(flat_offsets)
480
481# Variables used during front propagation
482cdef FLOAT_T cost, new_cost, cumcost, new_cumcost, offset_length
483cdef INDEX_T index, new_index
484cdef BOOL_T is_at_edge, use_offset
485cdef INDEX_T d, i, iter
486cdef OFFSET_T offset
487cdef EDGE_T pos_edge_val, neg_edge_val
488cdef int num_ends_found = 0
489cdef FLOAT_T inf = np.inf
490cdef int goal_reached
491
492cdef INDEX_T maxiter = int(max_coverage * flat_costs.size)
493
494for iter in range(maxiter):
495
496# This is rather like a while loop, except we are guaranteed to
497# exit, which is nice during developing to prevent eternal loops.
498
499# Find the point with the minimum cost in the heap. Once
500# popped, this point's minimum cost path has been found.
501if costs_heap.count == 0:
502# nothing in the heap: we've found paths to every
503# point in the array
504break
505
506# Get current cumulative cost and index from the heap
507cumcost = costs_heap.pop_fast()
508index = costs_heap._popped_ref
509
510# Record the cost we found to this point
511flat_cumulative_costs[index] = cumcost
512
513# Check if goal is reached
514goal_reached = self.goal_reached(index, cumcost)
515if goal_reached > 0:
516if goal_reached == 1:
517continue # Skip neighbors
518else:
519break # Done completely
520
521if use_ends:
522# If we're only tracing out a path to one or more
523# endpoints, check to see if this is an endpoint, and
524# if so, if we're done pathfinding.
525for i in range(num_ends):
526if index == flat_ends[i]:
527num_ends_found += 1
528break
529if (num_ends_found and not all_ends) or \
530num_ends_found == num_ends:
531# if we've found one or all of the end points (as
532# requested), stop searching
533break
534
535# Look into the edge map to see if this point is at an
536# edge along any axis
537is_at_edge = 0
538for d in range(dim):
539if (flat_pos_edge_map[d, index] != 0 or
540flat_neg_edge_map[d, index] != 0):
541is_at_edge = 1
542break
543
544# Now examine the points neighboring the given point
545for i in range(num_offsets):
546# First, if we're at some edge, scrutinize the offset
547# to ensure that it won't put us out-of-bounds. If,
548# for example, the edge_map at (x, y) is (-1, 0) --
549# though of course we use flat indexing below -- that
550# means that (x, y) is along the lower edge of the
551# array; thus offsets with -1 or more negative in the
552# x-dimension should not be used!
553use_offset = 1
554if is_at_edge:
555for d in range(dim):
556offset = offsets[i, d]
557pos_edge_val = flat_pos_edge_map[d, index]
558neg_edge_val = flat_neg_edge_map[d, index]
559if (pos_edge_val > 0 and offset >= pos_edge_val) or \
560(neg_edge_val < 0 and offset <= neg_edge_val):
561# the offset puts us out of bounds...
562use_offset = 0
563break
564# If not at an edge, or the specific offset doesn't
565# push over the edge, then we go on.
566if not use_offset:
567continue
568
569# using the flat offsets, calculate the new flat index
570new_index = index + flat_offsets[i]
571
572# Get offset length
573offset_length = offset_lengths[i]
574
575# If we have already found the best path here then
576# ignore this point
577if flat_cumulative_costs[new_index] != inf:
578# Give subclass the opportunity to examine these two nodes
579# Note that only when both nodes are "frozen" their
580# cumulative cost is set. By doing the check here, each
581# pair of nodes is checked exactly once.
582self._examine_neighbor(index, new_index, offset_length)
583continue
584
585# Get cost and new cost
586cost = flat_costs[index]
587new_cost = flat_costs[new_index]
588
589# If the cost at this point is negative or infinite, ignore it
590if new_cost < 0 or new_cost == inf:
591continue
592
593# Calculate new cumulative cost
594new_cumcost = cumcost + self._travel_cost(cost, new_cost,
595offset_length)
596
597# Now we ask the heap to append or update the cost to
598# this new point, but only if that point isn't already
599# in the heap, or it is but the new cost is lower.
600# don't push infs into the heap though!
601if new_cumcost != inf:
602costs_heap.push_if_lower_fast(new_cumcost, new_index)
603# If we did perform an append or update, we should
604# record the offset from the predecessor to this new
605# point
606if costs_heap._pushed:
607traceback_offsets[new_index] = i
608self._update_node(index, new_index, offset_length)
609
610
611# Un-flatten the costs and traceback arrays for human consumption.
612cumulative_costs = np.asarray(flat_cumulative_costs)
613cumulative_costs = cumulative_costs.reshape(self.costs_shape,
614order='F')
615traceback = np.asarray(traceback_offsets)
616traceback = traceback.reshape(self.costs_shape, order='F')
617self.dirty = 1
618return cumulative_costs, traceback
619
620
621def traceback(self, end):
622"""traceback(end)
623
624Trace a minimum cost path through the pre-calculated traceback array.
625
626This convenience function reconstructs the the minimum cost path to a
627given end position from one of the starting indices provided to
628find_costs(), which must have been called previously. This function
629can be called as many times as desired after find_costs() has been
630run.
631
632Parameters
633----------
634end : iterable
635An n-d index into the `costs` array.
636
637Returns
638-------
639traceback : list of n-d tuples
640A list of indices into the `costs` array, starting with one of
641the start positions passed to find_costs(), and ending with the
642given `end` index. These indices specify the minimum-cost path
643from any given start index to the `end` index. (The total cost
644of that path can be read out from the `cumulative_costs` array
645returned by find_costs().)
646"""
647if not self.dirty:
648raise Exception('find_costs() must be run before traceback()')
649ends = _normalize_indices([end], self.costs_shape)
650if ends is None:
651raise ValueError('the specified end point must be '
652'within the costs array')
653traceback = [tuple(ends[0])]
654
655cdef INDEX_T flat_position =\
656_ravel_index_fortran(ends, self.costs_shape)[0]
657if self.flat_cumulative_costs[flat_position] == np.inf:
658raise ValueError('no minimum-cost path was found '
659'to the specified end point')
660
661# Short names for arrays
662cdef OFFSETS_INDEX_T [:] traceback_offsets = self.traceback_offsets
663cdef OFFSET_T [:,:] offsets = self.offsets
664cdef INDEX_T [:] flat_offsets = self.flat_offsets
665# New array
666cdef INDEX_T [:] position = np.array(ends[0], dtype=INDEX_D)
667
668cdef OFFSETS_INDEX_T offset
669cdef DIM_T d
670cdef DIM_T dim = self.dim
671while 1:
672offset = traceback_offsets[flat_position]
673if offset == -1:
674# At a point where we can go no further: probably a start point
675break
676flat_position -= flat_offsets[offset]
677for d in range(dim):
678position[d] -= offsets[offset, d]
679traceback.append(tuple(position))
680return _reverse(traceback)
681
682
683
684@cython.boundscheck(False)
685@cython.wraparound(False)
686cdef class MCP_Geometric(MCP):
687"""MCP_Geometric(costs, offsets=None, fully_connected=True)
688
689Find distance-weighted minimum cost paths through an n-d costs array.
690
691See the documentation for MCP for full details. This class differs from
692MCP in that the cost of a path is not simply the sum of the costs along
693that path.
694
695This class instead assumes that the costs array contains at each position
696the "cost" of a unit distance of travel through that position. For
697example, a move (in 2-d) from (1, 1) to (1, 2) is assumed to originate in
698the center of the pixel (1, 1) and terminate in the center of (1, 2). The
699entire move is of distance 1, half through (1, 1) and half through (1, 2);
700thus the cost of that move is `(1/2)*costs[1,1] + (1/2)*costs[1,2]`.
701
702On the other hand, a move from (1, 1) to (2, 2) is along the diagonal and
703is sqrt(2) in length. Half of this move is within the pixel (1, 1) and the
704other half in (2, 2), so the cost of this move is calculated as
705`(sqrt(2)/2)*costs[1,1] + (sqrt(2)/2)*costs[2,2]`.
706
707These calculations don't make a lot of sense with offsets of magnitude
708greater than 1. Use the `sampling` argument in order to deal with
709anisotropic data.
710"""
711
712def __init__(self, costs, offsets=None, fully_connected=True,
713sampling=None):
714"""__init__(costs, offsets=None, fully_connected=True, sampling=None)
715
716See class documentation.
717"""
718MCP.__init__(self, costs, offsets, fully_connected, sampling)
719if np.absolute(self.offsets).max() > 1:
720raise ValueError('all offset components must be 0, 1, or -1')
721self.use_start_cost = 0
722
723cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost,
724FLOAT_T offset_length):
725return offset_length * 0.5 * (old_cost + new_cost)
726
727
728
729@cython.boundscheck(True)
730@cython.wraparound(True)
731cdef class MCP_Connect(MCP):
732"""MCP_Connect(costs, offsets=None, fully_connected=True)
733
734Connect source points using the distance-weighted minimum cost function.
735
736A front is grown from each seed point simultaneously, while the
737origin of the front is tracked as well. When two fronts meet,
738create_connection() is called. This method must be overloaded to
739deal with the found edges in a way that is appropriate for the
740application.
741"""
742
743cdef INDEX_T [:] flat_idmap
744
745
746def __init__(self, costs, offsets=None, fully_connected=True,
747sampling=None):
748MCP.__init__(self, costs, offsets, fully_connected, sampling)
749
750# Create id map to keep track of origin of nodes
751self.flat_idmap = np.zeros(self.costs_shape, INDEX_D).ravel('F')
752
753
754def _reset(self):
755""" Reset the id map.
756"""
757cdef INDEX_T start
758
759MCP._reset(self)
760starts, _ = self._starts, self._ends
761
762# Reset idmap
763self.flat_idmap[...] = -1
764id = 0
765for start in _ravel_index_fortran(starts, self.costs_shape):
766self.flat_idmap[start] = id
767id += 1
768
769
770cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost,
771FLOAT_T offset_length):
772""" Equivalent to MCP_Geometric.
773"""
774return offset_length * 0.5 * (old_cost + new_cost)
775
776
777cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index,
778FLOAT_T offset_length) noexcept:
779""" Check whether two fronts are meeting. If so, the flat_traceback
780is obtained and a connection is created.
781"""
782
783# Short names
784cdef INDEX_T [:] flat_idmap = self.flat_idmap
785cdef FLOAT_T [:] flat_cumulative_costs = self.flat_cumulative_costs
786
787# Get ids
788cdef INDEX_T id1 = flat_idmap[index]
789cdef INDEX_T id2 = flat_idmap[new_index]
790
791if id2 < 0 or id1 < 0:
792pass
793elif id2 != id1:
794# We reached the 'front' of another seed point!
795# Get position/coordinates
796pos1, pos2 = _unravel_index_fortran([index, new_index],
797self.costs_shape)
798# Also get the costs, so we can keep the path with the least cost
799cost1 = flat_cumulative_costs[index]
800cost2 = flat_cumulative_costs[new_index]
801# Create connection
802self.create_connection(id1, id2, pos1, pos2, cost1, cost2)
803
804
805def create_connection(self, id1, id2, tb1, tb2, cost1, cost2):
806""" create_connection id1, id2, pos1, pos2, cost1, cost2)
807
808Overload this method to keep track of the connections that are
809found during MCP processing. Note that a connection with the
810same ids can be found multiple times (but with different
811positions and costs).
812
813At the time that this method is called, both points are "frozen"
814and will not be visited again by the MCP algorithm.
815
816Parameters
817----------
818id1 : int
819The seed point id where the first neighbor originated from.
820id2 : int
821The seed point id where the second neighbor originated from.
822pos1 : tuple
823The index of of the first neighbor in the connection.
824pos2 : tuple
825The index of of the second neighbor in the connection.
826cost1 : float
827The cumulative cost at `pos1`.
828cost2 : float
829The cumulative costs at `pos2`.
830"""
831pass
832
833
834cdef void _update_node(self, INDEX_T index, INDEX_T new_index,
835FLOAT_T offset_length) noexcept:
836""" Keep track of the id map so that we know which seed point
837a certain front originates from.
838"""
839self.flat_idmap[new_index] = self.flat_idmap[index]
840
841
842
843@cython.boundscheck(False)
844@cython.wraparound(False)
845cdef class MCP_Flexible(MCP):
846"""MCP_Flexible(costs, offsets=None, fully_connected=True)
847
848Find minimum cost paths through an N-d costs array.
849
850See the documentation for MCP for full details. This class differs from
851MCP in that several methods can be overloaded (from pure Python) to
852modify the behavior of the algorithm and/or create custom algorithms
853based on MCP. Note that goal_reached can also be overloaded in the
854MCP class.
855
856"""
857
858def travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost,
859FLOAT_T offset_length):
860""" travel_cost(old_cost, new_cost, offset_length)
861This method calculates the travel cost for going from the
862current node to the next. The default implementation returns
863new_cost. Overload this method to adapt the behaviour of the
864algorithm.
865"""
866return new_cost
867
868
869def examine_neighbor(self, INDEX_T index, INDEX_T new_index,
870FLOAT_T offset_length):
871""" examine_neighbor(index, new_index, offset_length)
872This method is called once for every pair of neighboring nodes,
873as soon as both nodes are frozen.
874
875This method can be overloaded to obtain information about
876neighboring nodes, and/or to modify the behavior of the MCP
877algorithm. One example is the MCP_Connect class, which checks
878for meeting fronts using this hook.
879"""
880pass
881
882
883def update_node(self, INDEX_T index, INDEX_T new_index,
884FLOAT_T offset_length):
885""" update_node(index, new_index, offset_length)
886This method is called when a node is updated, right after
887new_index is pushed onto the heap and the traceback map is
888updated.
889
890This method can be overloaded to keep track of other arrays
891that are used by a specific implementation of the algorithm.
892For instance the MCP_Connect class uses it to update an id map.
893"""
894pass
895
896
897cdef FLOAT_T _travel_cost(self, FLOAT_T old_cost, FLOAT_T new_cost,
898FLOAT_T offset_length):
899return self.travel_cost(old_cost, new_cost, offset_length)
900
901
902cdef void _examine_neighbor(self, INDEX_T index, INDEX_T new_index,
903FLOAT_T offset_length) noexcept:
904self.examine_neighbor(index, new_index, offset_length)
905
906
907cdef void _update_node(self, INDEX_T index, INDEX_T new_index,
908FLOAT_T offset_length) noexcept:
909self.update_node(index, new_index, offset_length)
910