Source code for pyift.livewire

import numpy as np
import _pyift
import collections
from typing import Union, Sequence, Optional
import warnings


[docs]class LiveWire: current: Optional[np.ndarray] saliency: Optional[np.ndarray]
[docs] def __init__(self, image: np.ndarray, arc_fun: str = 'exp', saliency: Optional[np.ndarray] = None, **kwargs): """ Live-wire object to iteratively compute the optimum-paths [1]_ between user selected points. Parameters ---------- image: array_like Array where the first two dimensions are the image domain, the third and optional are its features. arc_fun: {'exp'}, default='exp' Optimum-path arc-weight function. saliency: array_like, optional Array with the same dimension as the image domain containing the foreground saliency. kwargs: float, optional Key word arguments for arc-weight function parameters. Attributes ---------- arc_fun: {'exp'}, default='exp' Optimum-path arc-weight function. image: array_like Array where the first two dimensions are the image domain, the third and optional are its features. saliency: array_like, optional Array with additional features, usually object saliency. Must have the same domain as `image`. costs: array_like Array containing the optimum-path cost. preds: array_like Array containing the predecessor map to recover the optimal contour. labels: array_like Array indicating optimum-path nodes size: tuple Tuple containing the image domain. sigma: float Image features parameter. gamma: float Saliency features parameter. source: int Current path source node index (flattened array), -1 if inactive. destiny: int Current path destiny node index (flattened array), -1 if inactive. start: int Current contour starting node index, -1 if inactive. current: array_like Active optimum-path, before confirmation. paths: dict Ordered dictionary of paths, key: path source, value: path sequence. Examples -------- >>> import numpy as np >>> from pyift.livewire import LiveWire >>> >>> image = np.array([[8, 1, 0, 2, 0], >>> [5, 7, 2, 0, 1], >>> [6, 7, 6, 1, 0], >>> [6, 8, 7, 0, 3], >>> [6, 7, 8, 8, 9]]) >>> >>> lw = LiveWire(image, sigma=1.0) >>> lw.select((0, 0)) >>> lw.confirm() >>> lw.select((4, 4)) >>> lw.confirm() >>> lw.contour References ---------- .. [1] Falcão, Alexandre X., et al. "User-steered image segmentation paradigms: Live wire and live lane." Graphical models and image processing 60.4 (1998): 233-260. """ if not isinstance(image, np.ndarray): raise TypeError('`image` must be a `ndarray`.') if image.ndim == 2: image = np.expand_dims(image, 2) if image.ndim != 3: raise ValueError('`image` must 2 or 3-dimensional array.') if saliency is not None: if not isinstance(saliency, np.ndarray): raise TypeError('`saliency` must be a `ndarray`.') if saliency.ndim == 2: saliency = np.expand_dims(saliency, 2) if saliency.ndim != 3: raise ValueError('`saliency` must 2 or 3-dimensional array.') if saliency.shape[:2] != image.shape[:2]: raise ValueError('`saliency` and `image` 0,1-dimensions must match.') self.saliency = np.ascontiguousarray(saliency.astype(float)) arc_functions = ('exp', 'exp-saliency') if arc_fun.lower() not in arc_functions: raise ValueError('Arc-weight function not found, must include {}'.format(arc_functions)) self.arc_fun = arc_fun.lower() if self.arc_fun.startswith('exp'): sigma = 1.0 if 'sigma' not in kwargs: warnings.warn('`sigma` not provided, using default, %f' % sigma, Warning) self.sigma = kwargs.pop('sigma', sigma) if self.arc_fun == 'exp-saliency': if saliency is None: raise TypeError('`saliency` must be provided with `exp-saliency` arc-weight.') gamma = 1.0 if 'gamma' not in kwargs: warnings.warn('`gamma` not provided, using default %f' % gamma, Warning) self.gamma = kwargs.pop('gamma', gamma) self.size = image.shape[:2] self.image = np.ascontiguousarray(image.astype(float)) self.costs = np.full(self.size, np.finfo('d').max, dtype=float) self.preds = np.full(self.size, -1, dtype=int) self.labels = np.zeros(self.size, dtype=bool) self.source = -1 self.destiny = -1 self.start = -1 self.paths = collections.OrderedDict() self.current = None
def _opt_path(self, src: int, dst: int) -> Optional[np.ndarray]: """ Compute optimum-path from source to destiny. Parameters ---------- src : int Source index. dst : int Destiny index. Returns ------- array_like Array of flattened indices. """ if self.arc_fun == 'exp': path = _pyift.livewire_path(self.image, self.costs, self.preds, self.labels, self.arc_fun, self.sigma, src, dst) elif self.arc_fun == 'exp-saliency': path = _pyift.livewire_path(self.image, self.saliency, self.costs, self.preds, self.labels, self.arc_fun, self.sigma, self.gamma, src, dst) else: raise NotImplementedError return path def _assert_valid(self, y: int, x: int) -> None: """ Asserts coordinates belong in the image domain. """ if not (0 <= y < self.size[0] and 0 <= x < self.size[1]): raise ValueError('Coordinates out of image boundary, {}'.format(self.size))
[docs] def select(self, position: Union[Sequence[int], int]) -> None: """ Selects next position to compute optimum-path to, or initial position. Parameters ---------- position: Sequence[int, int], int Index or coordinate (y, x) in the image domain """ if isinstance(position, (list, tuple, np.ndarray)): y, x = round(position[0]), round(position[1]) self._assert_valid(y, x) position = int(y * self.size[1] + x) if not isinstance(position, int): raise TypeError('`position` must be a integer, tuple or list.') if self.source != -1: self.cancel() self.current = self._opt_path(self.source, position) else: self.start = position self.destiny = position # must be after cancel
[docs] def cancel(self) -> None: """ Cancel current unconfirmed path. """ if self.current is not None and self.current.size: # reset path self.labels.flat[self.current] = False self.costs.flat[self.current] = np.finfo('d').max # reset path end self.labels.flat[self.destiny] = False self.costs.flat[self.destiny] = np.finfo('d').max
[docs] def confirm(self) -> None: """ Confirms current path and sets it as new path source. """ if self.source != -1: self.paths[self.source] = self.current self.source = self.destiny self.current = None
[docs] def close(self) -> None: """ Connects the current path to the initial coordinate, closing the live-wire contour. Result must be confirmed. """ if len(self.paths) == 0: raise ValueError('Path must be confirmed before closing contour') self.cancel() self.costs.flat[self.start] = np.finfo('d').max self.select(self.start) self.confirm() self.start = -1 self.source = -1 self.destiny = -1
@property def contour(self) -> np.ndarray: """ Returns ------- array_like Optimum-path contour. """ return self.labels