| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- """TV-L1 optical flow algorithm implementation."""
- from functools import partial
- from itertools import combinations_with_replacement
- import numpy as np
- from scipy import ndimage as ndi
- from .._shared.filters import gaussian as gaussian_filter
- from .._shared.utils import _supported_float_type
- from ..transform import warp
- from ._optical_flow_utils import _coarse_to_fine, _get_warp_points
- def _tvl1(
- reference_image,
- moving_image,
- flow0,
- attachment,
- tightness,
- num_warp,
- num_iter,
- tol,
- prefilter,
- ):
- """TV-L1 solver for optical flow estimation.
- Parameters
- ----------
- reference_image : ndarray, shape (M, N[, P[, ...]])
- The first grayscale image of the sequence.
- moving_image : ndarray, shape (M, N[, P[, ...]])
- The second grayscale image of the sequence.
- flow0 : ndarray, shape (image0.ndim, M, N[, P[, ...]])
- Initialization for the vector field.
- attachment : float
- Attachment parameter. The smaller this parameter is,
- the smoother is the solutions.
- tightness : float
- Tightness parameter. It should have a small value in order to
- maintain attachment and regularization parts in
- correspondence.
- num_warp : int
- Number of times moving_image is warped.
- num_iter : int
- Number of fixed point iteration.
- tol : float
- Tolerance used as stopping criterion based on the L² distance
- between two consecutive values of (u, v).
- prefilter : bool
- Whether to prefilter the estimated optical flow before each
- image warp.
- Returns
- -------
- flow : ndarray, shape (image0.ndim, M, N[, P[, ...]])
- The estimated optical flow components for each axis.
- """
- dtype = reference_image.dtype
- grid = np.meshgrid(
- *[np.arange(n, dtype=dtype) for n in reference_image.shape],
- indexing='ij',
- sparse=True,
- )
- # dt corresponds to tau in [3]_, i.e. the time step
- dt = 0.5 / reference_image.ndim
- reg_num_iter = 2
- f0 = attachment * tightness
- f1 = dt / tightness
- tol *= reference_image.size
- flow_current = flow_previous = flow0
- g = np.zeros((reference_image.ndim,) + reference_image.shape, dtype=dtype)
- proj = np.zeros(
- (
- reference_image.ndim,
- reference_image.ndim,
- )
- + reference_image.shape,
- dtype=dtype,
- )
- s_g = [
- slice(None),
- ] * g.ndim
- s_p = [
- slice(None),
- ] * proj.ndim
- s_d = [
- slice(None),
- ] * (proj.ndim - 2)
- for _ in range(num_warp):
- if prefilter:
- flow_current = ndi.median_filter(
- flow_current, [1] + reference_image.ndim * [3]
- )
- image1_warp = warp(
- moving_image, _get_warp_points(grid, flow_current), mode='edge'
- )
- grad = np.array(np.gradient(image1_warp))
- NI = (grad * grad).sum(0)
- NI[NI == 0] = 1
- rho_0 = image1_warp - reference_image - (grad * flow_current).sum(0)
- for _ in range(num_iter):
- # Data term
- rho = rho_0 + (grad * flow_current).sum(0)
- idx = abs(rho) <= f0 * NI
- flow_auxiliary = flow_current
- flow_auxiliary[:, idx] -= rho[idx] * grad[:, idx] / NI[idx]
- idx = ~idx
- srho = f0 * np.sign(rho[idx])
- flow_auxiliary[:, idx] -= srho * grad[:, idx]
- # Regularization term
- flow_current = flow_auxiliary.copy()
- for idx in range(reference_image.ndim):
- s_p[0] = idx
- for _ in range(reg_num_iter):
- for ax in range(reference_image.ndim):
- s_g[0] = ax
- s_g[ax + 1] = slice(0, -1)
- g[tuple(s_g)] = np.diff(flow_current[idx], axis=ax)
- s_g[ax + 1] = slice(None)
- norm = np.sqrt((g**2).sum(0))[np.newaxis, ...]
- norm *= f1
- norm += 1.0
- proj[idx] -= dt * g
- proj[idx] /= norm
- # d will be the (negative) divergence of proj[idx]
- d = -proj[idx].sum(0)
- for ax in range(reference_image.ndim):
- s_p[1] = ax
- s_p[ax + 2] = slice(0, -1)
- s_d[ax] = slice(1, None)
- d[tuple(s_d)] += proj[tuple(s_p)]
- s_p[ax + 2] = slice(None)
- s_d[ax] = slice(None)
- flow_current[idx] = flow_auxiliary[idx] + d
- flow_previous -= flow_current # The difference as stopping criteria
- if (flow_previous * flow_previous).sum() < tol:
- break
- flow_previous = flow_current
- return flow_current
- def optical_flow_tvl1(
- reference_image,
- moving_image,
- *,
- attachment=15,
- tightness=0.3,
- num_warp=5,
- num_iter=10,
- tol=1e-4,
- prefilter=False,
- dtype=np.float32,
- ):
- r"""Coarse to fine optical flow estimator.
- The TV-L1 solver is applied at each level of the image
- pyramid. TV-L1 is a popular algorithm for optical flow estimation
- introduced by Zack et al. [1]_, improved in [2]_ and detailed in [3]_.
- Parameters
- ----------
- reference_image : ndarray, shape (M, N[, P[, ...]])
- The first grayscale image of the sequence.
- moving_image : ndarray, shape (M, N[, P[, ...]])
- The second grayscale image of the sequence.
- attachment : float, optional
- Attachment parameter (:math:`\lambda` in [1]_). The smaller
- this parameter is, the smoother the returned result will be.
- tightness : float, optional
- Tightness parameter (:math:`\theta` in [1]_). It should have
- a small value in order to maintain attachment and
- regularization parts in correspondence.
- num_warp : int, optional
- Number of times moving_image is warped.
- num_iter : int, optional
- Number of fixed point iteration.
- tol : float, optional
- Tolerance used as stopping criterion based on the L² distance
- between two consecutive values of (u, v).
- prefilter : bool, optional
- Whether to prefilter the estimated optical flow before each
- image warp. When True, a median filter with window size 3
- along each axis is applied. This helps to remove potential
- outliers.
- dtype : dtype, optional
- Output data type: must be floating point. Single precision
- provides good results and saves memory usage and computation
- time compared to double precision.
- Returns
- -------
- flow : ndarray, shape (image0.ndim, M, N[, P[, ...]])
- The estimated optical flow components for each axis.
- Notes
- -----
- Color images are not supported.
- References
- ----------
- .. [1] Zach, C., Pock, T., & Bischof, H. (2007, September). A
- duality based approach for realtime TV-L 1 optical flow. In Joint
- pattern recognition symposium (pp. 214-223). Springer, Berlin,
- Heidelberg. :DOI:`10.1007/978-3-540-74936-3_22`
- .. [2] Wedel, A., Pock, T., Zach, C., Bischof, H., & Cremers,
- D. (2009). An improved algorithm for TV-L 1 optical flow. In
- Statistical and geometrical approaches to visual motion analysis
- (pp. 23-45). Springer, Berlin, Heidelberg.
- :DOI:`10.1007/978-3-642-03061-1_2`
- .. [3] Pérez, J. S., Meinhardt-Llopis, E., & Facciolo,
- G. (2013). TV-L1 optical flow estimation. Image Processing On
- Line, 2013, 137-150. :DOI:`10.5201/ipol.2013.26`
- Examples
- --------
- >>> from skimage.color import rgb2gray
- >>> from skimage.data import stereo_motorcycle
- >>> from skimage.registration import optical_flow_tvl1
- >>> image0, image1, disp = stereo_motorcycle()
- >>> # --- Convert the images to gray level: color is not supported.
- >>> image0 = rgb2gray(image0)
- >>> image1 = rgb2gray(image1)
- >>> flow = optical_flow_tvl1(image1, image0)
- """
- solver = partial(
- _tvl1,
- attachment=attachment,
- tightness=tightness,
- num_warp=num_warp,
- num_iter=num_iter,
- tol=tol,
- prefilter=prefilter,
- )
- if np.dtype(dtype) != _supported_float_type(dtype):
- msg = f"dtype={dtype} is not supported. Try 'float32' or 'float64.'"
- raise ValueError(msg)
- return _coarse_to_fine(reference_image, moving_image, solver, dtype=dtype)
- def _ilk(reference_image, moving_image, flow0, radius, num_warp, gaussian, prefilter):
- """Iterative Lucas-Kanade (iLK) solver for optical flow estimation.
- Parameters
- ----------
- reference_image : ndarray, shape (M, N[, P[, ...]])
- The first grayscale image of the sequence.
- moving_image : ndarray, shape (M, N[, P[, ...]])
- The second grayscale image of the sequence.
- flow0 : ndarray, shape (reference_image.ndim, M, N[, P[, ...]])
- Initialization for the vector field.
- radius : int
- Radius of the window considered around each pixel.
- num_warp : int
- Number of times moving_image is warped.
- gaussian : bool
- if True, a gaussian kernel is used for the local
- integration. Otherwise, a uniform kernel is used.
- prefilter : bool
- Whether to prefilter the estimated optical flow before each
- image warp. This helps to remove potential outliers.
- Returns
- -------
- flow : ndarray, shape (reference_image.ndim, M, N[, P[, ...]])
- The estimated optical flow components for each axis.
- """
- dtype = reference_image.dtype
- ndim = reference_image.ndim
- size = 2 * radius + 1
- if gaussian:
- sigma = ndim * (size / 4,)
- filter_func = partial(gaussian_filter, sigma=sigma, mode='mirror')
- else:
- filter_func = partial(ndi.uniform_filter, size=ndim * (size,), mode='mirror')
- flow = flow0
- # For each pixel location (i, j), the optical flow X = flow[:, i, j]
- # is the solution of the ndim x ndim linear system
- # A[i, j] * X = b[i, j]
- A = np.zeros(reference_image.shape + (ndim, ndim), dtype=dtype)
- b = np.zeros(reference_image.shape + (ndim, 1), dtype=dtype)
- grid = np.meshgrid(
- *[np.arange(n, dtype=dtype) for n in reference_image.shape],
- indexing='ij',
- sparse=True,
- )
- for _ in range(num_warp):
- if prefilter:
- flow = ndi.median_filter(flow, (1,) + ndim * (3,))
- moving_image_warp = warp(
- moving_image, _get_warp_points(grid, flow), mode='edge'
- )
- grad = np.stack(np.gradient(moving_image_warp), axis=0)
- error_image = (grad * flow).sum(axis=0) + reference_image - moving_image_warp
- # Local linear systems creation
- for i, j in combinations_with_replacement(range(ndim), 2):
- A[..., i, j] = A[..., j, i] = filter_func(grad[i] * grad[j])
- for i in range(ndim):
- b[..., i, 0] = filter_func(grad[i] * error_image)
- # Don't consider badly conditioned linear systems
- idx = abs(np.linalg.det(A)) < 1e-14
- A[idx] = np.eye(ndim, dtype=dtype)
- b[idx] = 0
- # Solve the local linear systems
- flow = np.moveaxis(np.linalg.solve(A, b)[..., 0], ndim, 0)
- return flow
- def optical_flow_ilk(
- reference_image,
- moving_image,
- *,
- radius=7,
- num_warp=10,
- gaussian=False,
- prefilter=False,
- dtype=np.float32,
- ):
- """Coarse to fine optical flow estimator.
- The iterative Lucas-Kanade (iLK) solver is applied at each level
- of the image pyramid. iLK [1]_ is a fast and robust alternative to
- TVL1 algorithm although less accurate for rendering flat surfaces
- and object boundaries (see [2]_).
- Parameters
- ----------
- reference_image : ndarray, shape (M, N[, P[, ...]])
- The first grayscale image of the sequence.
- moving_image : ndarray, shape (M, N[, P[, ...]])
- The second grayscale image of the sequence.
- radius : int, optional
- Radius of the window considered around each pixel.
- num_warp : int, optional
- Number of times moving_image is warped.
- gaussian : bool, optional
- If True, a Gaussian kernel is used for the local
- integration. Otherwise, a uniform kernel is used.
- prefilter : bool, optional
- Whether to prefilter the estimated optical flow before each
- image warp. When True, a median filter with window size 3
- along each axis is applied. This helps to remove potential
- outliers.
- dtype : dtype, optional
- Output data type: must be floating point. Single precision
- provides good results and saves memory usage and computation
- time compared to double precision.
- Returns
- -------
- flow : ndarray, shape (reference_image.ndim, M, N[, P[, ...]])
- The estimated optical flow components for each axis.
- Notes
- -----
- - The implemented algorithm is described in **Table2** of [1]_.
- - Color images are not supported.
- References
- ----------
- .. [1] Le Besnerais, G., & Champagnat, F. (2005, September). Dense
- optical flow by iterative local window registration. In IEEE
- International Conference on Image Processing 2005 (Vol. 1,
- pp. I-137). IEEE. :DOI:`10.1109/ICIP.2005.1529706`
- .. [2] Plyer, A., Le Besnerais, G., & Champagnat,
- F. (2016). Massively parallel Lucas Kanade optical flow for
- real-time video processing applications. Journal of Real-Time
- Image Processing, 11(4), 713-730. :DOI:`10.1007/s11554-014-0423-0`
- Examples
- --------
- >>> from skimage.color import rgb2gray
- >>> from skimage.data import stereo_motorcycle
- >>> from skimage.registration import optical_flow_ilk
- >>> reference_image, moving_image, disp = stereo_motorcycle()
- >>> # --- Convert the images to gray level: color is not supported.
- >>> reference_image = rgb2gray(reference_image)
- >>> moving_image = rgb2gray(moving_image)
- >>> flow = optical_flow_ilk(moving_image, reference_image)
- """
- solver = partial(
- _ilk, radius=radius, num_warp=num_warp, gaussian=gaussian, prefilter=prefilter
- )
- if np.dtype(dtype) != _supported_float_type(dtype):
- msg = f"dtype={dtype} is not supported. Try 'float32' or 'float64.'"
- raise ValueError(msg)
- return _coarse_to_fine(reference_image, moving_image, solver, dtype=dtype)
|