From 4ca99ae6aae744893e4e91ea95e7d8c7eedaac2d Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 10:54:06 +0200 Subject: [PATCH 01/54] Specify in docstrings how it works when T>2 (#101) --- pysteps/motion/lucaskanade.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index fee0b89a7..10a95c76d 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -1,4 +1,5 @@ """ + pysteps.motion.lucaskanade ========================== @@ -50,7 +51,9 @@ def dense_lucaskanade(R, **kwargs): ---------- R : ndarray_ or MaskedArray_ Array of shape (T,m,n) containing a sequence of T two-dimensional input - images of shape (m,n). + images of shape (m,n). T = 2 is the minimum required number of images. + With T > 2, the sparse vectors detected by Lucas-Kanade are pooled + together prior to the final interpolation. In case of an ndarray_, invalid values (Nans or infs) are masked. The mask in the MaskedArray_ defines a region where velocity vectors are From 8946cdcaae3357cd8f5a53d874155a57a4856588 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 12:10:18 +0200 Subject: [PATCH 02/54] Change variable name --- pysteps/motion/lucaskanade.py | 80 ++++++++++++++++------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 10a95c76d..f778b34e7 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -28,7 +28,7 @@ import warnings -def dense_lucaskanade(R, **kwargs): +def dense_lucaskanade(input_images, **kwargs): """ .. _opencv: https://opencv.org/ @@ -49,9 +49,9 @@ def dense_lucaskanade(R, **kwargs): Parameters ---------- - R : ndarray_ or MaskedArray_ - Array of shape (T,m,n) containing a sequence of T two-dimensional input - images of shape (m,n). T = 2 is the minimum required number of images. + input_images : ndarray_ or MaskedArray_ + Array of shape (T, m, n) containing a sequence of T two-dimensional input + images of shape (m, n). T = 2 is the minimum required number of images. With T > 2, the sparse vectors detected by Lucas-Kanade are pooled together prior to the final interpolation. @@ -165,7 +165,7 @@ def dense_lucaskanade(R, **kwargs): nbr. of vectors) to be integrated with the sparse vectors from the Lucas-Kanade local tracking. x and y must be in pixel coordinates, with (0,0) being the upper-left - corner of the field R. u and v must be in pixel units. By default this + corner of the field input_images. u and v must be in pixel units. By default this is set to None. verbose : bool, optional @@ -176,7 +176,7 @@ def dense_lucaskanade(R, **kwargs): out : ndarray_ If dense=True (the default), it returns the three-dimensional array (2,m,n) containing the dense x- and y-components of the motion field in units of - pixels / timestep as given by the input array R. + pixels / timestep as given by the input array input_images. If dense=False, it returns a tuple containing the one-dimensional arrays x, y, u, v, where x, y define the vector locations, u, v define the x and y direction components of the vectors. @@ -195,18 +195,12 @@ def dense_lucaskanade(R, **kwargs): """ - if len(R.shape) != 3: - raise ValueError( - "R has %i dimensions, but a three-dimensional array is expected" - % len(R.shape) - ) - - if R.shape[0] < 2: - raise ValueError( - "R has %i frame, but at least two frames are expected" % R.shape[0] - ) + if (input_images.ndim != 3) or input_images.shape[0] < 2: + raise ValueError("input_images dimension mismatch.\n" + + "input_images.shape: " + str(input_images.shape) + + "\n(>1, m, n) expected") - R = R.copy() + input_images = input_images.copy() # defaults dense = kwargs.get("dense", True) @@ -254,15 +248,15 @@ def dense_lucaskanade(R, **kwargs): t0 = time.time() # Get mask - if isinstance(R, MaskedArray): - mask = np.ma.getmaskarray(R).copy() + if isinstance(input_images, MaskedArray): + mask = np.ma.getmaskarray(input_images).copy() else: - R = np.ma.masked_invalid(R) - mask = np.ma.getmaskarray(R).copy() - R[mask] = np.nanmin(R) # Remove any Nan from the raw data + input_images = np.ma.masked_invalid(input_images) + mask = np.ma.getmaskarray(input_images).copy() + input_images[mask] = np.nanmin(input_images) # Remove any Nan from the raw data - nr_fields = R.shape[0] - domain_size = (R.shape[1], R.shape[2]) + nr_fields = input_images.shape[0] + domain_size = (input_images.shape[1], input_images.shape[2]) y0Stack = [] x0Stack = [] uStack = [] @@ -270,8 +264,8 @@ def dense_lucaskanade(R, **kwargs): for n in range(nr_fields - 1): # extract consecutive images - prvs = R[n, :, :].copy() - next = R[n + 1, :, :].copy() + prvs = input_images[n, :, :].copy() + next = input_images[n + 1, :, :].copy() # skip loop if no precip if ~np.any(prvs > prvs.min()) or ~np.any(next > next.min()): @@ -392,14 +386,14 @@ def dense_lucaskanade(R, **kwargs): def _ShiTomasi_features_to_track( - R, max_corners_ST, quality_level_ST, min_distance_ST, block_size_ST, mask + input_image, max_corners_ST, quality_level_ST, min_distance_ST, block_size_ST, mask ): """Call the Shi-Tomasi corner detection algorithm. Parameters ---------- - R : array-like - Array of shape (m,n) containing the input precipitation field passed as + input_image : array-like + Array of shape (m, n) containing the input precipitation field passed as 8-bit image. max_corners_ST : int @@ -433,10 +427,10 @@ def _ShiTomasi_features_to_track( "optical flow method but it is not installed" ) - if len(R.shape) != 2: - raise ValueError("R must be a two-dimensional array") - if R.dtype != "uint8": - raise ValueError("R must be passed as 8-bit image") + if len(input_image.shape) != 2: + raise ValueError("input_image must be a two-dimensional array") + if input_image.dtype != "uint8": + raise ValueError("input_image must be passed as 8-bit image") # ShiTomasi corner detection parameters ShiTomasi_params = dict( @@ -447,7 +441,7 @@ def _ShiTomasi_features_to_track( ) # detect corners - p0 = cv2.goodFeaturesToTrack(R, mask=mask, **ShiTomasi_params) + p0 = cv2.goodFeaturesToTrack(input_image, mask=mask, **ShiTomasi_params) return p0 @@ -458,9 +452,9 @@ def _LucasKanade_features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): Parameters ---------- prvs : array-like - Array of shape (m,n) containing the first 8-bit input image. + Array of shape (m, n) containing the first 8-bit input image. next : array-like - Array of shape (m,n) containing the successive 8-bit input image. + Array of shape (m, n) containing the successive 8-bit input image. p0 : list Vector of 2D points for which the flow needs to be found. Point coordinates must be single-precision floating-point numbers. @@ -517,13 +511,13 @@ def _LucasKanade_features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): return x0, y0, u, v -def _clean_image(R, n=3, thr=0): +def _clean_image(input_image, n=3, thr=0): """Apply a binary morphological opening to filter small isolated echoes. Parameters ---------- - R : array-like - Array of shape (m,n) containing the input precipitation field. + input_image : array-like + Array of shape (m, n) containing the input precipitation field. n : int The structuring element size [px]. thr : float @@ -531,7 +525,7 @@ def _clean_image(R, n=3, thr=0): Returns ------- - R : array + input_image : array Array of shape (m,n) containing the cleaned precipitation field. """ @@ -542,7 +536,7 @@ def _clean_image(R, n=3, thr=0): ) # convert to binary image (rain/no rain) - field_bin = np.ndarray.astype(R > thr, "uint8") + field_bin = np.ndarray.astype(input_image > thr, "uint8") # build a structuring element of size (nx) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (n, n)) @@ -554,9 +548,9 @@ def _clean_image(R, n=3, thr=0): mask = (field_bin - field_bin_out) > 0 # filter out small isolated echoes based on mask - R[mask] = np.nanmin(R) + input_image[mask] = np.nanmin(input_image) - return R + return input_image def _outlier_removal(x, y, u, v, thr, multivariate=True, k=30, verbose=False): From b26c7745497fbd9705978c3ca9e2aa32f15154d7 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 15:42:22 +0200 Subject: [PATCH 03/54] Set private methods as public --- pysteps/motion/lucaskanade.py | 255 +++++++++++++++++----------------- 1 file changed, 127 insertions(+), 128 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index f778b34e7..04e78487e 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -43,10 +43,10 @@ def dense_lucaskanade(input_images, **kwargs): .. _ndarray:\ https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html - + .. _Shi-Tomasi: https://docs.opencv.org/3.4.1/dd/d1a/group__\ imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 - + Parameters ---------- input_images : ndarray_ or MaskedArray_ @@ -110,12 +110,12 @@ def dense_lucaskanade(input_images, **kwargs): number of standard deviations. Any anomaly larger than this value is flagged as outlier and excluded from the interpolation. By default this is set to 3. - + multivariate_outlier : bool, optional If true (the default), the outlier detection is computed in terms of - the Mahalanobis distance. If false, the outlier detection is simply + the Mahalanobis distance. If false, the outlier detection is simply computed in terms of velocity. - + k_outlier : int, optional The number of nearest neighbours used to localize the outlier detection. If set equal to 0, it employs all the data points. @@ -184,21 +184,24 @@ def dense_lucaskanade(input_images, **kwargs): References ---------- - - Bouguet, J.-Y.: Pyramidal implementation of the affine Lucas Kanade - feature tracker description of the algorithm, Intel Corp., 5, 4, + + Bouguet, J.-Y.: Pyramidal implementation of the affine Lucas Kanade + feature tracker description of the algorithm, Intel Corp., 5, 4, https://doi.org/10.1109/HPDC.2004.1323531, 2001 - Lucas, B. D. and Kanade, T.: An iterative image registration technique with - an application to stereo vision, in: Proceedings of the 1981 DARPA Imaging + Lucas, B. D. and Kanade, T.: An iterative image registration technique with + an application to stereo vision, in: Proceedings of the 1981 DARPA Imaging Understanding Workshop, pp. 121–130, 1981. - + """ if (input_images.ndim != 3) or input_images.shape[0] < 2: - raise ValueError("input_images dimension mismatch.\n" + - "input_images.shape: " + str(input_images.shape) + - "\n(>1, m, n) expected") + raise ValueError( + "input_images dimension mismatch.\n" + + "input_images.shape: " + + str(input_images.shape) + + "\n(>1, m, n) expected" + ) input_images = input_images.copy() @@ -289,13 +292,13 @@ def dense_lucaskanade(input_images, **kwargs): # remove small noise with a morphological operator (opening) if size_opening > 0: - prvs = _clean_image(prvs, n=size_opening) - next = _clean_image(next, n=size_opening) + prvs = clean_image(prvs, n=size_opening) + next = clean_image(next, n=size_opening) # Shi-Tomasi good features to track # TODO: implement different feature detection algorithms (e.g. Harris) mask_ = (-1 * mask_ + 1).astype("uint8") - p0 = _ShiTomasi_features_to_track( + p0 = features_to_track( prvs, max_corners_ST, quality_level_ST, @@ -309,9 +312,7 @@ def dense_lucaskanade(input_images, **kwargs): continue # get sparse u, v vectors with Lucas-Kanade tracking - x0, y0, u, v = _LucasKanade_features_tracking( - prvs, next, p0, winsize_LK, nr_levels_LK - ) + x0, y0, u, v = features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK) # skip loop if no vectors if x0 is None: continue @@ -337,7 +338,7 @@ def dense_lucaskanade(input_images, **kwargs): v = np.vstack(vStack) # exclude outlier vectors - x, y, u, v = _outlier_removal( + x, y, u, v = remove_outliers( x, y, u, v, nr_std_outlier, multivariate_outlier, k_outlier, verbose ) @@ -350,7 +351,7 @@ def dense_lucaskanade(input_images, **kwargs): # decluster sparse motion vectors if decl_grid > 1: - x, y, u, v = _declustering(x, y, u, v, decl_grid, min_nr_samples) + x, y, u, v = declustering(x, y, u, v, decl_grid, min_nr_samples) # append extra vectors if provided if extra_vectors is not None: @@ -367,12 +368,15 @@ def dense_lucaskanade(input_images, **kwargs): print("--- %i sparse vectors left after declustering ---" % x.size) # kernel interpolation - _, _, UV = _interpolate_sparse_vectors( + xgrid = np.arange(domain_size[1]) + ygrid = np.arange(domain_size[0]) + UV = interpolate_sparse_vectors( x, y, u, v, - domain_size, + xgrid, + ygrid, rbfunction=rbfunction, k=k, epsilon=epsilon, @@ -385,32 +389,33 @@ def dense_lucaskanade(input_images, **kwargs): return UV -def _ShiTomasi_features_to_track( +def features_to_track( input_image, max_corners_ST, quality_level_ST, min_distance_ST, block_size_ST, mask ): - """Call the Shi-Tomasi corner detection algorithm. + """ + .. _Shi-Tomasi: https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature\ + .html#ga1d6bb77486c8f92d79c8793ad995d541 + .. _ndarray:\ + https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html + + Interface to the `Shi-Tomasi`_ 'Good features to track' corner detection + algorithm implemented in OpenCV. Parameters ---------- - input_image : array-like - Array of shape (m, n) containing the input precipitation field passed as - 8-bit image. - + input_image : ndarray_ + Array of shape (m, n) containing the input 8-bit image. max_corners_ST : int Maximum number of corners to return. If there are more corners than are found, the strongest of them is returned. - quality_level_ST : float Parameter characterizing the minimal accepted quality of image corners. See original documentation for more details (https://docs.opencv.org). - min_distance_ST : int Minimum possible Euclidean distance between the returned corners [px]. - block_size_ST : int Size of an average block for computing a derivative covariation matrix over each pixel neighborhood. - mask : ndarray_ Array of shape (m,n). It specifies the region in which the corners are detected. @@ -423,11 +428,11 @@ def _ShiTomasi_features_to_track( """ if not cv2_imported: raise MissingOptionalDependency( - "opencv package is required for the Lucas-Kanade " - "optical flow method but it is not installed" + "opencv package is required for the Shi-Tomasi " + "corner detection method but it is not installed" ) - if len(input_image.shape) != 2: + if input_image.ndim != 2: raise ValueError("input_image must be a two-dimensional array") if input_image.dtype != "uint8": raise ValueError("input_image must be passed as 8-bit image") @@ -446,13 +451,18 @@ def _ShiTomasi_features_to_track( return p0 -def _LucasKanade_features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): - """Call the Lucas-Kanade features tracking algorithm. +def features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): + """ + .. _`Lucas-Kanade`: https://docs.opencv.org/3.4/dc/d6b/group__video__track\ + .html#ga473e4b886d0bcc6b65831eb88ed93323 + + Interface to the `Lucas-Kanade`_ features tracking algorithm implemented + in OpenCV. Parameters ---------- prvs : array-like - Array of shape (m, n) containing the first 8-bit input image. + Array of shape (m, n) containing the initial 8-bit input image. next : array-like Array of shape (m, n) containing the successive 8-bit input image. p0 : list @@ -479,7 +489,7 @@ def _LucasKanade_features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): """ if not cv2_imported: raise MissingOptionalDependency( - "opencv package is required for the Lucas-Kanade method " + "opencv package is required for the Lucas-Kanade " "optical flow method but it is not installed" ) @@ -490,7 +500,7 @@ def _LucasKanade_features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), ) - # Lucas-Kande + # Lucas-Kanade p1, st, err = cv2.calcOpticalFlowPyrLK(prvs, next, p0, None, **lk_params) # keep only features that have been found @@ -501,23 +511,23 @@ def _LucasKanade_features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): err = err[st, :] # extract vectors - x0 = p0[:, :, 0] - y0 = p0[:, :, 1] + x = p0[:, :, 0] + y = p0[:, :, 1] u = np.array((p1 - p0)[:, :, 0]) v = np.array((p1 - p0)[:, :, 1]) else: - x0 = y0 = u = v = None + x = y = u = v = None - return x0, y0, u, v + return x, y, u, v -def _clean_image(input_image, n=3, thr=0): - """Apply a binary morphological opening to filter small isolated echoes. +def clean_image(input_image, n=3, thr=0): + """Apply a binary morphological opening to filter out small scale noise. Parameters ---------- input_image : array-like - Array of shape (m, n) containing the input precipitation field. + Array of shape (m, n) containing the input images. n : int The structuring element size [px]. thr : float @@ -531,8 +541,8 @@ def _clean_image(input_image, n=3, thr=0): """ if not cv2_imported: raise MissingOptionalDependency( - "opencv package is required for the Lucas-Kanade method " - "optical flow method but it is not installed" + "opencv package is required for the morphological opening " + "method but it is not installed" ) # convert to binary image (rain/no rain) @@ -553,10 +563,10 @@ def _clean_image(input_image, n=3, thr=0): return input_image -def _outlier_removal(x, y, u, v, thr, multivariate=True, k=30, verbose=False): +def remove_outliers(x, y, u, v, thr, multivariate=True, k=30, verbose=False): + + """Remove the motion vectors that are identified as outliers. - """Outlier removal. - Parameters ---------- x : array_like @@ -568,21 +578,24 @@ def _outlier_removal(x, y, u, v, thr, multivariate=True, k=30, verbose=False): v : array_like Y-components of the velocities. thr : float - Threshold for outlier detection defined as measure of deviation from - the mean/median in terms of standard deviations. + Threshold to detect the outliers, defined in terms of number of + standard deviation from the mean (median). multivariate : bool, optional If true (the default), the outlier detection is computed in terms of - the Mahalanobis distance. If false, the outlier detection is simply - computed in terms of velocity. - k : int, optinal + the Mahalanobis distance. If false, the outlier detection is computed + with respect to the velocity of the motion vectors. + k : int, optional The number of nearest neighbours used to localize the outlier detection. If set equal to 0, it employs all the data points. The default is 30. + verbose : bool, optional + Print the number of vectors that have been removed. Returns ------- - A four-element tuple (x,y,u,v) containing the x- and y-coordinates and - velocity components of the motion vectors. + out : tuple of ndarrays + A four-element tuple (x, y, u, v) containing the x- and y-coordinates, + and the x- and y- components of the motion vectors. """ if multivariate: @@ -616,7 +629,7 @@ def _outlier_removal(x, y, u, v, thr, multivariate=True, k=30, verbose=False): points = np.concatenate((x, y), axis=1) tree = scipy.spatial.cKDTree(points) - _, inds = tree.query(points, k=np.min((k + 1, points.shape[0]))) + __, inds = tree.query(points, k=np.min((k + 1, points.shape[0]))) keep = [] for i in range(inds.shape[0]): @@ -662,10 +675,9 @@ def _outlier_removal(x, y, u, v, thr, multivariate=True, k=30, verbose=False): return x, y, u, v -def _declustering(x, y, u, v, decl_grid, min_nr_samples): - """Filter out outliers in a sparse motion field and get more representative - data points. The method assigns data points to a (RxR) declustering grid - and then take the median of all values within one cell. +def declustering(x, y, u, v, decl_grid, min_nr_samples): + """Decluster a set of sparse vectors by aggregating (taking the median value) + the initial data points over a coarser grid. Parameters ---------- @@ -677,46 +689,43 @@ def _declustering(x, y, u, v, decl_grid, min_nr_samples): X-components of the velocities. v : array_like Y-components of the velocities. - decl_grid : int - Size of the declustering grid [px]. + decl_grid : float + The size of the declustering grid in the same units as the input. min_nr_samples : int - The minimum number of samples for computing the median within given + The minimum number of samples for computing the median within a given declustering cell. Returns ------- - A four-element tuple (x,y,u,v) containing the x- and y-coordinates and - velocity components of the declustered motion vectors. + out : tuple of ndarrays + A four-element tuple (x, y, u, v) containing the x- and y-coordinates, + and the x- and y- components of the declustered motion vectors. """ - # make sure these are all numpy vertical arrays + # Return empty arrays if the number of sparse vectors is < min_nr_samples + if x.size < min_nr_samples: + return np.array([]), np.array([]), np.array([]), np.array([]) + + # Make sure these are all numpy vertical arrays x = np.array(x).flatten()[:, None] y = np.array(y).flatten()[:, None] u = np.array(u).flatten()[:, None] v = np.array(v).flatten()[:, None] - # return empty arrays if the number of sparse vectors is < min_nr_samples - if x.size < min_nr_samples: - return np.array([]), np.array([]), np.array([]), np.array([]) - - # discretize coordinates into declustering grid - xT = x / float(decl_grid) - yT = y / float(decl_grid) + # Discretize coordinates into declustering grid + xT = np.floor(x / float(decl_grid)) + yT = np.floor(y / float(decl_grid)) - # round coordinates to low integer - xT = np.floor(xT) - yT = np.floor(yT) - - # keep only unique combinations of coordinates + # Keep only unique combinations of the reduced coordinates xy = np.concatenate((xT, yT), axis=1) xyb = np.ascontiguousarray(xy).view( np.dtype((np.void, xy.dtype.itemsize * xy.shape[1])) ) - _, idx = np.unique(xyb, return_index=True) + __, idx = np.unique(xyb, return_index=True) uxy = xy[idx] - # now loop through these unique values and average vectors which belong to + # Loop through these unique values and average vectors which belong to # the same declustering grid cell xN = [] yN = [] @@ -731,7 +740,7 @@ def _declustering(x, y, u, v, decl_grid, min_nr_samples): uN.append(np.median(u[idx])) vN.append(np.median(v[idx])) - # convert to numpy arrays + # Convert to numpy arrays x = np.array(xN) y = np.array(yN) u = np.array(uN) @@ -740,50 +749,46 @@ def _declustering(x, y, u, v, decl_grid, min_nr_samples): return x, y, u, v -def _interpolate_sparse_vectors( - x, y, u, v, domain_size, rbfunction="inverse", k=20, epsilon=None, nchunks=5 +def interpolate_sparse_vectors( + x, y, u, v, xgrid, ygrid, rbfunction="inverse", k=20, epsilon=None, nchunks=5 ): - """Interpolation of sparse motion vectors to produce a dense field of motion - vectors. + """Interpolate a set of sparse motion vectors to produce a dense field of + motion vectors. Parameters ---------- x : array-like - x coordinates of the sparse motion vectors + The x-coordinates of the sparse motion vectors. y : array-like - y coordinates of the sparse motion vectors + The y-coordinates of the sparse motion vectors. u : array_like - u components of the sparse motion vectors + The x-components of the sparse motion vectors. v : array_like - v components of the sparse motion vectors - domain_size : tuple - size of the domain of the dense motion field [px] - rbfunction : string - the radial basis rbfunction, based on the Euclidian norm, d. - default : inverse - available : nearest, inverse, gaussian - k : int or "all" - the number of nearest neighbours used to speed-up the interpolation - If set equal to "all", it employs all the sparse vectors - default : 20 - epsilon : float - adjustable constant for gaussian or inverse functions - default : median distance between sparse vectors + The y-components of the sparse motion vectors. + xgrid : array_like + Array of shape (n) containing the x-coordinates of the final grid. + ygrid : array_like + Array of shape (m) containing the y-coordinates of the final grid. + rbfunction : {"nearest", "inverse", "gaussian"}, optional + The radial basis rbfunction based on the Euclidian norm. + k : int or "all", optional + The number of nearest neighbours used to speed-up the interpolation. + If set equal to "all", it employs all the sparse vectors. + epsilon : float, optional + The adjustable constant for the gaussian and inverse radial basis rbfunction. + If set equal to None (the default), epsilon is estimated as the median + distance between the sparse vectors. nchunks : int - split the grid points in n chunks to limit the memory usage during the - interpolation - default : 5 + The number of chunks in which the grid points are split to limit the + memory usage during the interpolation. Returns ------- - X : array-like - grid - Y : array-like - grid - UV : array-like - Three-dimensional array (2,domain_size[0],domain_size[1]) - containing the dense U, V motion fields. + out : ndarray + The interpolated advection field having shape (2, m, n), where out[0, :, :] + contains the x-components of the motion vectors and out[1, :, :] contains + the y-components. The units are given by the input sparse motion vectors. """ @@ -795,12 +800,7 @@ def _interpolate_sparse_vectors( points = np.concatenate((x, y), axis=1) npoints = points.shape[0] - if len(domain_size) == 1: - domain_size = (domain_size, domain_size) - - # generate the grid - xgrid = np.arange(domain_size[1]) - ygrid = np.arange(domain_size[0]) + # generate the full grid X, Y = np.meshgrid(xgrid, ygrid) grid = np.column_stack((X.ravel(), Y.ravel())) @@ -877,8 +877,7 @@ def _interpolate_sparse_vectors( i0 += idelta # reshape back to original size - U = U.reshape(domain_size[0], domain_size[1]) - V = V.reshape(domain_size[0], domain_size[1]) - UV = np.stack([U, V]) + U = U.reshape(ygrid.size, xgrid.size) + V = V.reshape(ygrid.size, xgrid.size) - return X, Y, UV + return np.stack([U, V]) From 8e4234d373e7165499b4ce20e1c52c673f0b2136 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 15:57:30 +0200 Subject: [PATCH 04/54] Fix auto_summary --- pysteps/motion/lucaskanade.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 04e78487e..bfa88f455 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -3,14 +3,19 @@ pysteps.motion.lucaskanade ========================== -OpenCV implementation of the Lucas-Kanade method with interpolated motion -vectors for areas with no precipitation. +Methods that make use of Lucas-Kanade method as implemented in OpenCV to estimate +the advection field in a sequence of two or more images. .. autosummary:: :toctree: ../generated/ dense_lucaskanade - + features_to_track + features_tracking + clean_image + remove_outliers + declustering + interpolate_sparse_vectors """ import numpy as np @@ -35,9 +40,6 @@ def dense_lucaskanade(input_images, **kwargs): .. _`Lucas-Kanade`: https://docs.opencv.org/3.4/dc/d6b/\ group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 - OpenCV_ implementation of the local `Lucas-Kanade`_ method with - interpolation of the sparse motion vectors to fill the whole grid. - .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ maskedarray.baseclass.html#numpy.ma.MaskedArray @@ -47,6 +49,10 @@ def dense_lucaskanade(input_images, **kwargs): .. _Shi-Tomasi: https://docs.opencv.org/3.4.1/dd/d1a/group__\ imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 + Interface to the OpenCV_ implementation of the local `Lucas-Kanade`_ optical + flow method, including the `Shi-Tomasi`_ corner detection routine and the + final interpolation of the sparse motion vectors to fill the whole grid. + Parameters ---------- input_images : ndarray_ or MaskedArray_ @@ -398,8 +404,8 @@ def features_to_track( .. _ndarray:\ https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html - Interface to the `Shi-Tomasi`_ 'Good features to track' corner detection - algorithm implemented in OpenCV. + Interface to the OpenCV goodFeaturesToTrack method to detect strong corners + on an image. Parameters ---------- From 81f37ffdce2776ac8d300a8540665920de8bfc77 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 16:09:54 +0200 Subject: [PATCH 05/54] Rename methods --- pysteps/motion/lucaskanade.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index bfa88f455..261e97933 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -3,16 +3,19 @@ pysteps.motion.lucaskanade ========================== -Methods that make use of Lucas-Kanade method as implemented in OpenCV to estimate -the advection field in a sequence of two or more images. +The Lucas-Kanade (LK) Module. + +This module implements the interface to the local Lucas-Kanade routine available +in OpenCV, as well as methods to interpolate the sparse vectors over a grid. + .. autosummary:: :toctree: ../generated/ dense_lucaskanade features_to_track - features_tracking - clean_image + lucaskanade + morph_opening remove_outliers declustering interpolate_sparse_vectors @@ -34,7 +37,8 @@ def dense_lucaskanade(input_images, **kwargs): - """ + """Run the Lucas-Kanade optical flow and interpolate the motion vectors. + .. _opencv: https://opencv.org/ .. _`Lucas-Kanade`: https://docs.opencv.org/3.4/dc/d6b/\ @@ -298,8 +302,8 @@ def dense_lucaskanade(input_images, **kwargs): # remove small noise with a morphological operator (opening) if size_opening > 0: - prvs = clean_image(prvs, n=size_opening) - next = clean_image(next, n=size_opening) + prvs = morph_opening(prvs, n=size_opening) + next = morph_opening(next, n=size_opening) # Shi-Tomasi good features to track # TODO: implement different feature detection algorithms (e.g. Harris) @@ -318,7 +322,7 @@ def dense_lucaskanade(input_images, **kwargs): continue # get sparse u, v vectors with Lucas-Kanade tracking - x0, y0, u, v = features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK) + x0, y0, u, v = lucaskanade(prvs, next, p0, winsize_LK, nr_levels_LK) # skip loop if no vectors if x0 is None: continue @@ -457,7 +461,7 @@ def features_to_track( return p0 -def features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): +def lucaskanade(prvs, next, p0, winsize_LK, nr_levels_LK): """ .. _`Lucas-Kanade`: https://docs.opencv.org/3.4/dc/d6b/group__video__track\ .html#ga473e4b886d0bcc6b65831eb88ed93323 @@ -527,7 +531,7 @@ def features_tracking(prvs, next, p0, winsize_LK, nr_levels_LK): return x, y, u, v -def clean_image(input_image, n=3, thr=0): +def morph_opening(input_image, n=3, thr=0): """Apply a binary morphological opening to filter out small scale noise. Parameters From fe95a6d7ceffc190d29b75c7caddde465ee93695 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 16:26:59 +0200 Subject: [PATCH 06/54] Pass optional params as dict --- pysteps/motion/lucaskanade.py | 89 +++++++++++------------------------ 1 file changed, 27 insertions(+), 62 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 261e97933..6a054b8fd 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -305,24 +305,27 @@ def dense_lucaskanade(input_images, **kwargs): prvs = morph_opening(prvs, n=size_opening) next = morph_opening(next, n=size_opening) - # Shi-Tomasi good features to track - # TODO: implement different feature detection algorithms (e.g. Harris) + # Find good features to track mask_ = (-1 * mask_ + 1).astype("uint8") - p0 = features_to_track( - prvs, - max_corners_ST, - quality_level_ST, - min_distance_ST, - block_size_ST, - mask_, + gf_params = dict( + maxCorners=max_corners_ST, + qualityLevel=quality_level_ST, + minDistance=min_distance_ST, + blockSize=block_size_ST, ) + p0 = features_to_track(prvs, mask_, gf_params) # skip loop if no features to track if p0 is None: continue # get sparse u, v vectors with Lucas-Kanade tracking - x0, y0, u, v = lucaskanade(prvs, next, p0, winsize_LK, nr_levels_LK) + lk_params = dict( + winSize=winsize_LK, + maxLevel=nr_levels_LK, + criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), + ) + x0, y0, u, v = lucaskanade(prvs, next, p0, lk_params) # skip loop if no vectors if x0 is None: continue @@ -399,36 +402,21 @@ def dense_lucaskanade(input_images, **kwargs): return UV -def features_to_track( - input_image, max_corners_ST, quality_level_ST, min_distance_ST, block_size_ST, mask -): +def features_to_track(input_image, mask, params): """ - .. _Shi-Tomasi: https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature\ - .html#ga1d6bb77486c8f92d79c8793ad995d541 - .. _ndarray:\ - https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html - - Interface to the OpenCV goodFeaturesToTrack method to detect strong corners + Interface to the OpenCV goodFeaturesToTrack() method to detect strong corners on an image. Parameters ---------- input_image : ndarray_ Array of shape (m, n) containing the input 8-bit image. - max_corners_ST : int - Maximum number of corners to return. If there are more corners than are - found, the strongest of them is returned. - quality_level_ST : float - Parameter characterizing the minimal accepted quality of image corners. - See original documentation for more details (https://docs.opencv.org). - min_distance_ST : int - Minimum possible Euclidean distance between the returned corners [px]. - block_size_ST : int - Size of an average block for computing a derivative covariation matrix - over each pixel neighborhood. mask : ndarray_ - Array of shape (m,n). It specifies the region in which the corners are - detected. + Array of shape (m,n). It specifies the image region in which the corners + can be detected. + params : dict + Any additional parameter to the original routine as described in the + corresponding documentation. Returns ------- @@ -447,27 +435,14 @@ def features_to_track( if input_image.dtype != "uint8": raise ValueError("input_image must be passed as 8-bit image") - # ShiTomasi corner detection parameters - ShiTomasi_params = dict( - maxCorners=max_corners_ST, - qualityLevel=quality_level_ST, - minDistance=min_distance_ST, - blockSize=block_size_ST, - ) - - # detect corners - p0 = cv2.goodFeaturesToTrack(input_image, mask=mask, **ShiTomasi_params) + p0 = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) return p0 -def lucaskanade(prvs, next, p0, winsize_LK, nr_levels_LK): +def lucaskanade(prvs, next, p0, params): """ - .. _`Lucas-Kanade`: https://docs.opencv.org/3.4/dc/d6b/group__video__track\ - .html#ga473e4b886d0bcc6b65831eb88ed93323 - - Interface to the `Lucas-Kanade`_ features tracking algorithm implemented - in OpenCV. + Interface to the OpenCV `Lucas-Kanade`_ features tracking algorithm. Parameters ---------- @@ -478,12 +453,9 @@ def lucaskanade(prvs, next, p0, winsize_LK, nr_levels_LK): p0 : list Vector of 2D points for which the flow needs to be found. Point coordinates must be single-precision floating-point numbers. - winsize_LK : tuple - Size of the search window at each pyramid level. - Small windows (e.g. 10) lead to unrealistic motion. - nr_levels_LK : int - 0-based maximal pyramid level number. - Not very sensitive parameter. + params : dict + Any additional parameter to the original routine as described in the + corresponding documentation. Returns ------- @@ -503,15 +475,8 @@ def lucaskanade(prvs, next, p0, winsize_LK, nr_levels_LK): "optical flow method but it is not installed" ) - # LK parameters - lk_params = dict( - winSize=winsize_LK, - maxLevel=nr_levels_LK, - criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), - ) - # Lucas-Kanade - p1, st, err = cv2.calcOpticalFlowPyrLK(prvs, next, p0, None, **lk_params) + p1, st, err = cv2.calcOpticalFlowPyrLK(prvs, next, p0, None, **params) # keep only features that have been found st = st[:, 0] == 1 From 72d1cffdb2ee7dc31216a47ec2410adce08e4fa2 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 16:37:54 +0200 Subject: [PATCH 07/54] Improve docstrings --- pysteps/motion/lucaskanade.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 6a054b8fd..8d3eee4c8 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -426,8 +426,8 @@ def features_to_track(input_image, mask, params): """ if not cv2_imported: raise MissingOptionalDependency( - "opencv package is required for the Shi-Tomasi " - "corner detection method but it is not installed" + "opencv package is required for the goodFeaturesToTrack() " + "routine but it is not installed" ) if input_image.ndim != 2: @@ -442,7 +442,7 @@ def features_to_track(input_image, mask, params): def lucaskanade(prvs, next, p0, params): """ - Interface to the OpenCV `Lucas-Kanade`_ features tracking algorithm. + Interface to the OpenCV calcOpticalFlowPyrLK() features tracking algorithm. Parameters ---------- @@ -471,8 +471,8 @@ def lucaskanade(prvs, next, p0, params): """ if not cv2_imported: raise MissingOptionalDependency( - "opencv package is required for the Lucas-Kanade " - "optical flow method but it is not installed" + "opencv package is required for the calcOpticalFlowPyrLK() " + "routine but it is not installed" ) # Lucas-Kanade @@ -516,8 +516,8 @@ def morph_opening(input_image, n=3, thr=0): """ if not cv2_imported: raise MissingOptionalDependency( - "opencv package is required for the morphological opening " - "method but it is not installed" + "opencv package is required for the morphologyEx " + "routine but it is not installed" ) # convert to binary image (rain/no rain) From f993832dd9140c441292c74eaf4fc58144232576 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 16:42:21 +0200 Subject: [PATCH 08/54] Improve docstrings --- pysteps/motion/lucaskanade.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 8d3eee4c8..5dff9120f 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -6,7 +6,7 @@ The Lucas-Kanade (LK) Module. This module implements the interface to the local Lucas-Kanade routine available -in OpenCV, as well as methods to interpolate the sparse vectors over a grid. +in OpenCV, as well as methods to interpolate the LK vectors over a grid. .. autosummary:: @@ -54,8 +54,9 @@ def dense_lucaskanade(input_images, **kwargs): imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 Interface to the OpenCV_ implementation of the local `Lucas-Kanade`_ optical - flow method, including the `Shi-Tomasi`_ corner detection routine and the - final interpolation of the sparse motion vectors to fill the whole grid. + flow method applied in combination to the `Shi-Tomasi`_ corner detection + routine. The sparse motion vectors are finally interpolated to return the whole + motion field. Parameters ---------- From 207703ed35434ad77e103f9bd4e7f2143e2296f1 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 16:50:31 +0200 Subject: [PATCH 09/54] Improve docstrings --- pysteps/motion/lucaskanade.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 5dff9120f..64deeecb6 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -405,9 +405,12 @@ def dense_lucaskanade(input_images, **kwargs): def features_to_track(input_image, mask, params): """ - Interface to the OpenCV goodFeaturesToTrack() method to detect strong corners + Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect strong corners on an image. + .. _`goodFeaturesToTrack()`: https://docs.opencv.org/3.4.1/dd/d1a/group__\ + imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 + Parameters ---------- input_image : ndarray_ @@ -443,7 +446,10 @@ def features_to_track(input_image, mask, params): def lucaskanade(prvs, next, p0, params): """ - Interface to the OpenCV calcOpticalFlowPyrLK() features tracking algorithm. + Interface to the OpenCV `calcOpticalFlowPyrLK()`_ features tracking algorithm. + + .. _`calcOpticalFlowPyrLK()`: https://docs.opencv.org/3.4/dc/d6b/\ + group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 Parameters ---------- @@ -498,21 +504,22 @@ def lucaskanade(prvs, next, p0, params): def morph_opening(input_image, n=3, thr=0): - """Apply a binary morphological opening to filter out small scale noise. + """Filter out small scale noise on the image by applying a binary morphological + opening (i.e., erosion then dilation). Parameters ---------- input_image : array-like - Array of shape (m, n) containing the input images. + Array of shape (m, n) containing the input image. n : int - The structuring element size [px]. + The structuring element size [pixels]. thr : float - The rain/no-rain threshold to convert the image into a binary image. + The threshold used to convert the image into a binary image. Returns ------- input_image : array - Array of shape (m,n) containing the cleaned precipitation field. + Array of shape (m,n) containing the resulting image """ if not cv2_imported: @@ -521,19 +528,19 @@ def morph_opening(input_image, n=3, thr=0): "routine but it is not installed" ) - # convert to binary image (rain/no rain) + # Convert to binary image field_bin = np.ndarray.astype(input_image > thr, "uint8") - # build a structuring element of size (nx) + # Build a structuring element of size n kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (n, n)) - # apply morphological opening (i.e. erosion then dilation) + # Apply morphological opening (i.e. erosion then dilation) field_bin_out = cv2.morphologyEx(field_bin, cv2.MORPH_OPEN, kernel) - # build mask to be applied on the original image + # Build mask to be applied on the original image mask = (field_bin - field_bin_out) > 0 - # filter out small isolated echoes based on mask + # Filter out small isolated pixels based on mask input_image[mask] = np.nanmin(input_image) return input_image From c944f4c08317049583c34d1f7d09a79fc37c5464 Mon Sep 17 00:00:00 2001 From: ned Date: Thu, 25 Jul 2019 17:59:29 +0200 Subject: [PATCH 10/54] Improve docstrings --- pysteps/motion/lucaskanade.py | 57 +++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 64deeecb6..086c3aff9 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -1,23 +1,30 @@ """ -pysteps.motion.lucaskanade +pysteps.motion.track_features ========================== The Lucas-Kanade (LK) Module. This module implements the interface to the local Lucas-Kanade routine available -in OpenCV, as well as methods to interpolate the LK vectors over a grid. +in OpenCV, as well as other auxiliary methods such as the interpolation of the +LK vectors over a grid. +.. _`goodFeaturesToTrack()`:\ + https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 + + +.. _`calcOpticalFlowPyrLK()`:\ + https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 .. autosummary:: :toctree: ../generated/ dense_lucaskanade - features_to_track - lucaskanade + detect_features + track_features morph_opening remove_outliers - declustering + decluster_vectors interpolate_sparse_vectors """ @@ -314,7 +321,7 @@ def dense_lucaskanade(input_images, **kwargs): minDistance=min_distance_ST, blockSize=block_size_ST, ) - p0 = features_to_track(prvs, mask_, gf_params) + p0 = detect_features(prvs, mask_, gf_params, False) # skip loop if no features to track if p0 is None: @@ -326,7 +333,7 @@ def dense_lucaskanade(input_images, **kwargs): maxLevel=nr_levels_LK, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), ) - x0, y0, u, v = lucaskanade(prvs, next, p0, lk_params) + x0, y0, u, v = track_features(prvs, next, p0, lk_params, False) # skip loop if no vectors if x0 is None: continue @@ -365,7 +372,7 @@ def dense_lucaskanade(input_images, **kwargs): # decluster sparse motion vectors if decl_grid > 1: - x, y, u, v = declustering(x, y, u, v, decl_grid, min_nr_samples) + x, y, u, v = decluster_vectors(x, y, u, v, decl_grid, min_nr_samples, verbose) # append extra vectors if provided if extra_vectors is not None: @@ -378,9 +385,6 @@ def dense_lucaskanade(input_images, **kwargs): if x.size == 0: return np.zeros((2, domain_size[0], domain_size[1])) - if verbose: - print("--- %i sparse vectors left after declustering ---" % x.size) - # kernel interpolation xgrid = np.arange(domain_size[1]) ygrid = np.arange(domain_size[0]) @@ -398,18 +402,18 @@ def dense_lucaskanade(input_images, **kwargs): ) if verbose: - print("--- %.2f seconds ---" % (time.time() - t0)) + print("--- total time: %.2f seconds ---" % (time.time() - t0)) return UV -def features_to_track(input_image, mask, params): +def detect_features(input_image, mask, params, verbose=False): """ Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect strong corners on an image. - .. _`goodFeaturesToTrack()`: https://docs.opencv.org/3.4.1/dd/d1a/group__\ - imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 + .. _`goodFeaturesToTrack()`:\ + https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 Parameters ---------- @@ -421,6 +425,8 @@ def features_to_track(input_image, mask, params): params : dict Any additional parameter to the original routine as described in the corresponding documentation. + verbose : bool, optional + Print the number of features detected. Returns ------- @@ -441,15 +447,18 @@ def features_to_track(input_image, mask, params): p0 = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) + if verbose: + print("--- %i good features to track detected ---" % len(p0)) + return p0 -def lucaskanade(prvs, next, p0, params): +def track_features(prvs, next, p0, params, verbose=False): """ Interface to the OpenCV `calcOpticalFlowPyrLK()`_ features tracking algorithm. - .. _`calcOpticalFlowPyrLK()`: https://docs.opencv.org/3.4/dc/d6b/\ - group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 + .. _`calcOpticalFlowPyrLK()`:\ + https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 Parameters ---------- @@ -463,6 +472,8 @@ def lucaskanade(prvs, next, p0, params): params : dict Any additional parameter to the original routine as described in the corresponding documentation. + verbose : bool, optional + Print the number of vectors that have been found. Returns ------- @@ -500,6 +511,9 @@ def lucaskanade(prvs, next, p0, params): else: x = y = u = v = None + if verbose: + print("--- %i sparse vectors found ---" % x.size) + return x, y, u, v @@ -658,7 +672,7 @@ def remove_outliers(x, y, u, v, thr, multivariate=True, k=30, verbose=False): return x, y, u, v -def declustering(x, y, u, v, decl_grid, min_nr_samples): +def decluster_vectors(x, y, u, v, decl_grid, min_nr_samples, verbose=False): """Decluster a set of sparse vectors by aggregating (taking the median value) the initial data points over a coarser grid. @@ -677,6 +691,8 @@ def declustering(x, y, u, v, decl_grid, min_nr_samples): min_nr_samples : int The minimum number of samples for computing the median within a given declustering cell. + verbose : bool, optional + Print the number of vectors after declustering. Returns ------- @@ -729,6 +745,9 @@ def declustering(x, y, u, v, decl_grid, min_nr_samples): u = np.array(uN) v = np.array(vN) + if verbose: + print("--- %i sparse vectors left after declustering ---" % x.size) + return x, y, u, v From 6e1a6f4d0c72c91def6d0bb73a0ebfef3ca1f3d4 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 26 Jul 2019 16:27:47 +0200 Subject: [PATCH 11/54] Simplify syntax, improve docstrings --- pysteps/motion/lucaskanade.py | 86 +++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 086c3aff9..0d1efd1ab 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -1,6 +1,6 @@ """ -pysteps.motion.track_features +pysteps.motion.lucaskanade ========================== The Lucas-Kanade (LK) Module. @@ -60,8 +60,8 @@ def dense_lucaskanade(input_images, **kwargs): .. _Shi-Tomasi: https://docs.opencv.org/3.4.1/dd/d1a/group__\ imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 - Interface to the OpenCV_ implementation of the local `Lucas-Kanade`_ optical - flow method applied in combination to the `Shi-Tomasi`_ corner detection + Interface to the OpenCV_ implementation of the local `Lucas-Kanade`_ optical + flow method applied in combination to the `Shi-Tomasi`_ corner detection routine. The sparse motion vectors are finally interpolated to return the whole motion field. @@ -334,29 +334,29 @@ def dense_lucaskanade(input_images, **kwargs): criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), ) x0, y0, u, v = track_features(prvs, next, p0, lk_params, False) + # skip loop if no vectors if x0 is None: continue - # stack vectors within time window as column vectors - x0Stack.append(x0.flatten()[:, None]) - y0Stack.append(y0.flatten()[:, None]) - uStack.append(u.flatten()[:, None]) - vStack.append(v.flatten()[:, None]) + # stack vectors + x0Stack.append(x0) + y0Stack.append(y0) + uStack.append(u) + vStack.append(v) # return zero motion field is no sparse vectors are found if len(x0Stack) == 0: if dense: return np.zeros((2, domain_size[0], domain_size[1])) else: - rzero = np.array([0]) - return rzero, rzero, rzero, rzero + return np.array([]), np.array([]), np.array([]), np.array([]) # convert lists of arrays into single arrays - x = np.vstack(x0Stack) - y = np.vstack(y0Stack) - u = np.vstack(uStack) - v = np.vstack(vStack) + x = np.concatenate(x0Stack) + y = np.concatenate(y0Stack) + u = np.concatenate(uStack) + v = np.concatenate(vStack) # exclude outlier vectors x, y, u, v = remove_outliers( @@ -409,8 +409,8 @@ def dense_lucaskanade(input_images, **kwargs): def detect_features(input_image, mask, params, verbose=False): """ - Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect strong corners - on an image. + Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect strong corners + on an image. .. _`goodFeaturesToTrack()`:\ https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 @@ -450,7 +450,7 @@ def detect_features(input_image, mask, params, verbose=False): if verbose: print("--- %i good features to track detected ---" % len(p0)) - return p0 + return p0.squeeze() def track_features(prvs, next, p0, params, verbose=False): @@ -494,20 +494,20 @@ def track_features(prvs, next, p0, params, verbose=False): ) # Lucas-Kanade - p1, st, err = cv2.calcOpticalFlowPyrLK(prvs, next, p0, None, **params) + # TODO: use the error returned by the OpenCV routine + p1, st, __ = cv2.calcOpticalFlowPyrLK(prvs, next, p0, None, **params) # keep only features that have been found - st = st[:, 0] == 1 + st = st.squeeze() == 1 if np.any(st): - p1 = p1[st, :, :] - p0 = p0[st, :, :] - err = err[st, :] + p1 = p1[st, :] + p0 = p0[st, :] # extract vectors - x = p0[:, :, 0] - y = p0[:, :, 1] - u = np.array((p1 - p0)[:, :, 0]) - v = np.array((p1 - p0)[:, :, 1]) + x = p0[:, 0] + y = p0[:, 1] + u = np.array((p1 - p0)[:, 0]) + v = np.array((p1 - p0)[:, 1]) else: x = y = u = v = None @@ -533,7 +533,7 @@ def morph_opening(input_image, n=3, thr=0): Returns ------- input_image : array - Array of shape (m,n) containing the resulting image + Array of shape (m,n) containing the resulting image """ if not cv2_imported: @@ -564,23 +564,27 @@ def remove_outliers(x, y, u, v, thr, multivariate=True, k=30, verbose=False): """Remove the motion vectors that are identified as outliers. + Assume a (multivariate) Gaussian distribution of the motion vectors and + defines outliers based on their deviation from the (local) sample mean. + Parameters ---------- x : array_like - X-coordinates of the origins of the velocity vectors. + Array of shape (n) containing the x-coordinates of the origins of the + velocity vectors. y : array_like - Y-coordinates of the origins of the velocity vectors. + Array of shape (n) containing the y-coordinates of the origins of the + velocity vectors. u : array_like - X-components of the velocities. + Array of shape (n) containing the x-components of the velocities. v : array_like - Y-components of the velocities. + Array of shape (n) containing the y-components of the velocities. thr : float - Threshold to detect the outliers, defined in terms of number of - standard deviation from the mean (median). + Threshold in number of standard deviation from the mean. multivariate : bool, optional If true (the default), the outlier detection is computed in terms of the Mahalanobis distance. If false, the outlier detection is computed - with respect to the velocity of the motion vectors. + with respect to the magnitude of the motion vectors. k : int, optional The number of nearest neighbours used to localize the outlier detection. If set equal to 0, it employs all the data points. @@ -596,7 +600,7 @@ def remove_outliers(x, y, u, v, thr, multivariate=True, k=30, verbose=False): """ if multivariate: - data = np.concatenate((u, v), axis=1) + data = np.stack((u, v)).T # globally if k <= 0: @@ -624,7 +628,7 @@ def remove_outliers(x, y, u, v, thr, multivariate=True, k=30, verbose=False): # locally else: - points = np.concatenate((x, y), axis=1) + points = np.stack((x, y)).T tree = scipy.spatial.cKDTree(points) __, inds = tree.query(points, k=np.min((k + 1, points.shape[0]))) keep = [] @@ -679,13 +683,15 @@ def decluster_vectors(x, y, u, v, decl_grid, min_nr_samples, verbose=False): Parameters ---------- x : array_like - X-coordinates of the origins of the velocity vectors. + Array of shape (n) containing the x-coordinates of the origins of the + velocity vectors. y : array_like - Y-coordinates of the origins of the velocity vectors. + Array of shape (n) containing the y-coordinates of the origins of the + velocity vectors. u : array_like - X-components of the velocities. + Array of shape (n) containing the x-components of the velocities. v : array_like - Y-components of the velocities. + Array of shape (n) containing the y-components of the velocities. decl_grid : float The size of the declustering grid in the same units as the input. min_nr_samples : int From ebc0605c6457a025e018dde8d6ff14e412901d31 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 26 Jul 2019 16:30:23 +0200 Subject: [PATCH 12/54] Remove option to include extra_vectors --- pysteps/motion/lucaskanade.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 0d1efd1ab..ec1bc017d 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -178,14 +178,6 @@ def dense_lucaskanade(input_images, **kwargs): interpolation. By default this is set to 5, if set to 1 the interpolation is computed with the whole grid. - extra_vectors : ndarray_, optional - Additional sparse motion vectors as 2d array (columns: x,y,u,v; rows: - nbr. of vectors) to be integrated with the sparse vectors from the - Lucas-Kanade local tracking. - x and y must be in pixel coordinates, with (0,0) being the upper-left - corner of the field input_images. u and v must be in pixel units. By default this - is set to None. - verbose : bool, optional If set to True, it prints information about the program (True by default). @@ -249,18 +241,6 @@ def dense_lucaskanade(input_images, **kwargs): k = kwargs.get("k", 50) epsilon = kwargs.get("epsilon", None) nchunks = kwargs.get("nchunks", 5) - extra_vectors = kwargs.get("extra_vectors", None) - if extra_vectors is not None: - if len(extra_vectors.shape) != 2: - raise ValueError( - "extra_vectors has %i dimensions, but 2 dimensions are expected" - % len(extra_vectors.shape) - ) - if extra_vectors.shape[1] != 4: - raise ValueError( - "extra_vectors has %i columns, but 4 columns are expected" - % extra_vectors.shape[1] - ) verbose = kwargs.get("verbose", True) buffer_mask = kwargs.get("buffer_mask", 0) @@ -374,13 +354,6 @@ def dense_lucaskanade(input_images, **kwargs): if decl_grid > 1: x, y, u, v = decluster_vectors(x, y, u, v, decl_grid, min_nr_samples, verbose) - # append extra vectors if provided - if extra_vectors is not None: - x = np.concatenate((x, extra_vectors[:, 0])) - y = np.concatenate((y, extra_vectors[:, 1])) - u = np.concatenate((u, extra_vectors[:, 2])) - v = np.concatenate((v, extra_vectors[:, 3])) - # return zero motion field if no sparse vectors are left for interpolation if x.size == 0: return np.zeros((2, domain_size[0], domain_size[1])) From 30ca79bb9bcebe5f2b7bbe4901db12ca40887e78 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 26 Jul 2019 16:44:35 +0200 Subject: [PATCH 13/54] Move 8-bit conversion down 1 level --- pysteps/motion/lucaskanade.py | 43 +++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index ec1bc017d..4a3b13438 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -238,11 +238,11 @@ def dense_lucaskanade(input_images, **kwargs): decl_grid = kwargs.get("decl_grid", 20) min_nr_samples = kwargs.get("min_nr_samples", 2) rbfunction = kwargs.get("rbfunction", "inverse") - k = kwargs.get("k", 50) + k = kwargs.get("k", 100) epsilon = kwargs.get("epsilon", None) nchunks = kwargs.get("nchunks", 5) verbose = kwargs.get("verbose", True) - buffer_mask = kwargs.get("buffer_mask", 0) + buffer_mask = kwargs.get("buffer_mask", 10) if verbose: print("Computing the motion field with the Lucas-Kanade method.") @@ -267,25 +267,17 @@ def dense_lucaskanade(input_images, **kwargs): # extract consecutive images prvs = input_images[n, :, :].copy() next = input_images[n + 1, :, :].copy() + mask_ = mask[n, :, :].copy() # skip loop if no precip if ~np.any(prvs > prvs.min()) or ~np.any(next > next.min()): continue - # scale between 0 and 255 - prvs = (prvs - prvs.min()) / (prvs.max() - prvs.min()) * 255 - next = (next - next.min()) / (next.max() - next.min()) * 255 - - # convert to 8-bit - prvs = np.ndarray.astype(prvs, "uint8") - next = np.ndarray.astype(next, "uint8") - mask_ = np.ndarray.astype(mask[n, :, :], "uint8") - # buffer the quality mask to ensure that no vectors are computed nearby # the edges of the radar mask if buffer_mask > 0: mask_ = cv2.dilate( - mask_, np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), 1 + mask_.astype("uint8"), np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), 1 ) # remove small noise with a morphological operator (opening) @@ -391,7 +383,7 @@ def detect_features(input_image, mask, params, verbose=False): Parameters ---------- input_image : ndarray_ - Array of shape (m, n) containing the input 8-bit image. + Array of shape (m, n) containing the input image. mask : ndarray_ Array of shape (m,n). It specifies the image region in which the corners can be detected. @@ -415,8 +407,17 @@ def detect_features(input_image, mask, params, verbose=False): if input_image.ndim != 2: raise ValueError("input_image must be a two-dimensional array") - if input_image.dtype != "uint8": - raise ValueError("input_image must be passed as 8-bit image") + + # scale image between 0 and 255 + input_image = ( + (input_image - input_image.min()) + / (input_image.max() - input_image.min()) + * 255 + ) + + # convert to 8-bit + input_image = np.ndarray.astype(input_image, "uint8") + mask = np.ndarray.astype(mask, "uint8") p0 = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) @@ -436,9 +437,9 @@ def track_features(prvs, next, p0, params, verbose=False): Parameters ---------- prvs : array-like - Array of shape (m, n) containing the initial 8-bit input image. + Array of shape (m, n) containing the initial image. next : array-like - Array of shape (m, n) containing the successive 8-bit input image. + Array of shape (m, n) containing the successive image. p0 : list Vector of 2D points for which the flow needs to be found. Point coordinates must be single-precision floating-point numbers. @@ -466,6 +467,14 @@ def track_features(prvs, next, p0, params, verbose=False): "routine but it is not installed" ) + # scale between 0 and 255 + prvs = (prvs - prvs.min()) / (prvs.max() - prvs.min()) * 255 + next = (next - next.min()) / (next.max() - next.min()) * 255 + + # convert to 8-bit + prvs = np.ndarray.astype(prvs, "uint8") + next = np.ndarray.astype(next, "uint8") + # Lucas-Kanade # TODO: use the error returned by the OpenCV routine p1, st, __ = cv2.calcOpticalFlowPyrLK(prvs, next, p0, None, **params) From 89fb53d72516fd2b1b33ca77e32431636a07984d Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 9 Aug 2019 11:03:14 +0200 Subject: [PATCH 14/54] Refactor outlier detection --- pysteps/motion/lucaskanade.py | 184 ++++++++++++++++++---------------- 1 file changed, 100 insertions(+), 84 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 4a3b13438..d1e363dce 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -124,19 +124,14 @@ def dense_lucaskanade(input_images, **kwargs): is set to 3. nr_std_outlier : int, optional - Maximum acceptable deviation from the mean/median in terms of - number of standard deviations. Any anomaly larger than - this value is flagged as outlier and excluded from the interpolation. + Maximum acceptable deviation from the mean in terms of number of + standard deviations. Any anomaly larger than this value is flagged as + outlier and excluded from the interpolation. By default this is set to 3. - multivariate_outlier : bool, optional - If true (the default), the outlier detection is computed in terms of - the Mahalanobis distance. If false, the outlier detection is simply - computed in terms of velocity. - - k_outlier : int, optional + k_outlier : int or None, optional The number of nearest neighbours used to localize the outlier detection. - If set equal to 0, it employs all the data points. + If set to None, it employs all the data points (global detection). The default is 30. size_opening : int, optional @@ -232,7 +227,6 @@ def dense_lucaskanade(input_images, **kwargs): + "use 'nr_std_outlier' instead.", category=FutureWarning, ) - multivariate_outlier = kwargs.get("multivariate_outlier", True) k_outlier = kwargs.get("k_outlier", 30) size_opening = kwargs.get("size_opening", 3) decl_grid = kwargs.get("decl_grid", 20) @@ -277,7 +271,9 @@ def dense_lucaskanade(input_images, **kwargs): # the edges of the radar mask if buffer_mask > 0: mask_ = cv2.dilate( - mask_.astype("uint8"), np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), 1 + mask_.astype("uint8"), + np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), + 1, ) # remove small noise with a morphological operator (opening) @@ -330,10 +326,14 @@ def dense_lucaskanade(input_images, **kwargs): u = np.concatenate(uStack) v = np.concatenate(vStack) - # exclude outlier vectors - x, y, u, v = remove_outliers( - x, y, u, v, nr_std_outlier, multivariate_outlier, k_outlier, verbose + # detect outlier vectors + outliers = detect_outliers( + np.stack((u, v)).T, nr_std_outlier, np.stack((u, v)).T, k_outlier, verbose ) + x = x[~outliers] + y = y[~outliers] + u = u[~outliers] + v = v[~outliers] if verbose: print("--- LK found %i sparse vectors ---" % x.size) @@ -542,120 +542,136 @@ def morph_opening(input_image, n=3, thr=0): return input_image -def remove_outliers(x, y, u, v, thr, multivariate=True, k=30, verbose=False): +def detect_outliers(input, thr, coord=None, k=None, verbose=False): + + """Detect outliers. - """Remove the motion vectors that are identified as outliers. + Assume a (multivariate) Gaussian distribution and detect outliers based on + the number of standard deviations from the mean. - Assume a (multivariate) Gaussian distribution of the motion vectors and - defines outliers based on their deviation from the (local) sample mean. + If spatial information is provided through coordinates, the outlier + detection can be localized by considering only the k-nearest neighbours + when computing the local mean and standard deviation. Parameters ---------- - x : array_like - Array of shape (n) containing the x-coordinates of the origins of the - velocity vectors. - y : array_like - Array of shape (n) containing the y-coordinates of the origins of the - velocity vectors. - u : array_like - Array of shape (n) containing the x-components of the velocities. - v : array_like - Array of shape (n) containing the y-components of the velocities. + + input : array_like + Array of shape (n) or (n, m), where n is the number of samples and m + the number of variables. If m > 1, it employs the Mahalanobis distance. + All values in the input array are required to have finite values. + thr : float - Threshold in number of standard deviation from the mean. - multivariate : bool, optional - If true (the default), the outlier detection is computed in terms of - the Mahalanobis distance. If false, the outlier detection is computed - with respect to the magnitude of the motion vectors. - k : int, optional + The number of standard deviations from the mean that defines an outlier. + + coord : array_like, optional + Array of shape (n, d) containing the coordinates the input data into a + space of d dimensions. Setting coord requires that k is not None. + + k : int or None, optional The number of nearest neighbours used to localize the outlier detection. - If set equal to 0, it employs all the data points. - The default is 30. + If set to None (the default), it employs all the data points (global + detection). Setting k requires that coord is not None. + verbose : bool, optional - Print the number of vectors that have been removed. + Print information. Returns ------- - out : tuple of ndarrays - A four-element tuple (x, y, u, v) containing the x- and y-coordinates, - and the x- and y- components of the motion vectors. + + out : array_like + A boolean array of the same shape as the input, with True values + indicating the outliers detected in the input array. """ - if multivariate: - data = np.stack((u, v)).T + if np.any(~np.isfinite(input)): + raise ValueError("input contains non-finite values") + + if k is not None and coord is None: + raise ValueError("k is set but coord=None") + + if coord is not None: + + if k is None: + raise ValueError("coord is set but k is None") + + if coord.shape[0] != input.shape[0]: + raise ValueError( + "the number of samples of the input array does not match the " + + "number of coordinates %i!=%i" % (input.shape[0], coord.shape[0]) + ) + + coord = np.copy(coord) + k = np.min((coord.shape[0], k + 1)) + + input = np.copy(input) + nvar = input.squeeze().ndim + + # global - # globally - if k <= 0: + if k is None: - if not multivariate: + if nvar == 1: - # in terms of velocity + # univariate - vel = np.sqrt(u ** 2 + v ** 2) # [px/timesteps] - q1, q2, q3 = np.percentile(vel, [16, 50, 84]) - min_speed_thr = np.max((0, q2 - thr * (q3 - q1) / 2)) - max_speed_thr = q2 + thr * (q3 - q1) / 2 - keep = np.logical_and(vel < max_speed_thr, vel >= min_speed_thr) + zdata = (input - np.mean(input)) / np.std(input) + outliers = zdata > thr else: - # mahalanobis distance + # multivariate (mahalanobis distance) - data = data - np.mean(data, axis=0) - V = np.cov(data.T) + zdata = input - np.mean(input, axis=0) + V = np.cov(zdata.T) VI = np.linalg.inv(V) - MD = np.sqrt(np.dot(np.dot(data, VI), data.T).diagonal()) - keep = MD < thr + try: + VI = np.linalg.inv(V) + MD = np.sqrt(np.dot(np.dot(zdata, VI), zdata.T).diagonal()) + except np.linalg.LinAlgError: + MD = np.zeros(input.shape) + outliers = MD > thr + + # local - # locally else: - points = np.stack((x, y)).T - tree = scipy.spatial.cKDTree(points) - __, inds = tree.query(points, k=np.min((k + 1, points.shape[0]))) - keep = [] + tree = scipy.spatial.cKDTree(coord) + __, inds = tree.query(coord, k=k) + outliers = [] for i in range(inds.shape[0]): - if not multivariate: + if nvar == 1: # in terms of velocity - thisvel = np.sqrt(u[i] ** 2 + v[i] ** 2) # [px/timesteps] - neighboursvel = np.sqrt(u[inds[i, 1:]] ** 2 + v[inds[i, 1:]] ** 2) - q1, q2, q3 = np.percentile(neighboursvel, [16, 50, 84]) - min_speed_thr = np.max((0, q2 - thr * (q3 - q1) / 2)) - max_speed_thr = q2 + thr * (q3 - q1) / 2 - keep.append(thisvel < max_speed_thr and thisvel > min_speed_thr) + thisdata = input[i] + neighbours = input[inds[i, 1:]] + thiszdata = (thisdata - np.mean(neighbours)) / np.std(neighbours) + outliers.append(thiszdata > thr) else: # mahalanobis distance - thisdata = data[i, :] - neighbours = data[inds[i, 1:], :].copy() - thisdata = thisdata - np.mean(neighbours, axis=0) + thisdata = input[i, :] + neighbours = input[inds[i, 1:], :].copy() + thiszdata = thisdata - np.mean(neighbours, axis=0) neighbours = neighbours - np.mean(neighbours, axis=0) V = np.cov(neighbours.T) try: VI = np.linalg.inv(V) - MD = np.sqrt(np.dot(np.dot(thisdata, VI), thisdata.T)) - + MD = np.sqrt(np.dot(np.dot(thiszdata, VI), thiszdata.T)) except np.linalg.LinAlgError: MD = 0 + outliers.append(MD > thr) - keep.append(MD < thr) - - keep = np.array(keep) + outliers = np.array(outliers) if verbose: - print("--- %i outliers removed ---" % np.sum(~keep)) + print("--- %i outliers removed ---" % np.sum(~outliers)) - x = x[keep] - y = y[keep] - u = u[keep] - v = v[keep] - - return x, y, u, v + return outliers def decluster_vectors(x, y, u, v, decl_grid, min_nr_samples, verbose=False): From 5cd283e1510ee1fb19aa268ae16f07553bdd1d53 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 9 Aug 2019 14:09:28 +0200 Subject: [PATCH 15/54] Change method name --- pysteps/motion/lucaskanade.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index d1e363dce..67fee8d8b 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -20,10 +20,10 @@ :toctree: ../generated/ dense_lucaskanade - detect_features + features_to_track track_features morph_opening - remove_outliers + detect_outliers decluster_vectors interpolate_sparse_vectors """ @@ -124,14 +124,14 @@ def dense_lucaskanade(input_images, **kwargs): is set to 3. nr_std_outlier : int, optional - Maximum acceptable deviation from the mean in terms of number of - standard deviations. Any anomaly larger than this value is flagged as + Maximum acceptable deviation from the mean in terms of number of + standard deviations. Any anomaly larger than this value is flagged as outlier and excluded from the interpolation. By default this is set to 3. k_outlier : int or None, optional The number of nearest neighbours used to localize the outlier detection. - If set to None, it employs all the data points (global detection). + If set to None, it employs all the data points (global detection). The default is 30. size_opening : int, optional @@ -271,9 +271,7 @@ def dense_lucaskanade(input_images, **kwargs): # the edges of the radar mask if buffer_mask > 0: mask_ = cv2.dilate( - mask_.astype("uint8"), - np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), - 1, + mask_.astype("uint8"), np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), 1 ) # remove small noise with a morphological operator (opening) @@ -289,7 +287,7 @@ def dense_lucaskanade(input_images, **kwargs): minDistance=min_distance_ST, blockSize=block_size_ST, ) - p0 = detect_features(prvs, mask_, gf_params, False) + p0 = features_to_track(prvs, mask_, gf_params, False) # skip loop if no features to track if p0 is None: @@ -372,7 +370,7 @@ def dense_lucaskanade(input_images, **kwargs): return UV -def detect_features(input_image, mask, params, verbose=False): +def features_to_track(input_image, mask, params, verbose=False): """ Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect strong corners on an image. @@ -607,6 +605,7 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): input = np.copy(input) nvar = input.squeeze().ndim + # global if k is None: From d39766e50be1b7d4932c9406af710170395a2ec8 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 9 Aug 2019 14:40:51 +0200 Subject: [PATCH 16/54] Improve docstrings, refactor variable checks --- pysteps/motion/lucaskanade.py | 42 ++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 67fee8d8b..8b48158f7 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -271,7 +271,9 @@ def dense_lucaskanade(input_images, **kwargs): # the edges of the radar mask if buffer_mask > 0: mask_ = cv2.dilate( - mask_.astype("uint8"), np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), 1 + mask_.astype("uint8"), + np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), + 1, ) # remove small noise with a morphological operator (opening) @@ -542,7 +544,7 @@ def morph_opening(input_image, n=3, thr=0): def detect_outliers(input, thr, coord=None, k=None, verbose=False): - """Detect outliers. + """Detect outliers in a (multivariate and georeferenced) dataset. Assume a (multivariate) Gaussian distribution and detect outliers based on the number of standard deviations from the mean. @@ -563,8 +565,8 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): The number of standard deviations from the mean that defines an outlier. coord : array_like, optional - Array of shape (n, d) containing the coordinates the input data into a - space of d dimensions. Setting coord requires that k is not None. + Array of shape (n, d) containing the coordinates of the input data into + a space of d dimensions. Setting coord requires that k is not None. k : int or None, optional The number of nearest neighbours used to localize the outlier detection. @@ -572,39 +574,47 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): detection). Setting k requires that coord is not None. verbose : bool, optional - Print information. + Print out information. Returns ------- out : array_like - A boolean array of the same shape as the input, with True values + A boolean array of the same shape as the input array, with True values indicating the outliers detected in the input array. """ + input = np.copy(input) + nvar = input.squeeze().ndim + if np.any(~np.isfinite(input)): raise ValueError("input contains non-finite values") - if k is not None and coord is None: - raise ValueError("k is set but coord=None") - if coord is not None: - if k is None: - raise ValueError("coord is set but k is None") + coord = np.copy(coord) + if coord.ndim == 1: + coord = coord[:, None] + + elif coord.ndim > 2: + raise ValueError( + "coord must have 2 dimensions (n, d), but it has %i" % coord.ndim + ) if coord.shape[0] != input.shape[0]: raise ValueError( - "the number of samples of the input array does not match the " + "the number of samples in the input array does not match the " + "number of coordinates %i!=%i" % (input.shape[0], coord.shape[0]) ) - coord = np.copy(coord) - k = np.min((coord.shape[0], k + 1)) + if k is None: + raise ValueError("coord is set but k is None") - input = np.copy(input) - nvar = input.squeeze().ndim + k = np.min((coord.shape[0], k + 1)) + else: + if k is not None: + raise ValueError("k is set but coord=None") # global From 3f55af6ac8adeba8f8bdb5a1bdbe7d25bb469e31 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 9 Aug 2019 15:00:51 +0200 Subject: [PATCH 17/54] Fix nvar --- pysteps/motion/lucaskanade.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 8b48158f7..670a2c964 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -585,11 +585,19 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): """ input = np.copy(input) - nvar = input.squeeze().ndim if np.any(~np.isfinite(input)): raise ValueError("input contains non-finite values") + if input.ndim == 1: + nvar = 1 + elif input.ndim == 2: + nvar = input.shape[1] + else: + raise ValueError( + "input must have 1 (n) or 2 dimensions (n, m), but it has %i" % coord.ndim + ) + if coord is not None: coord = np.copy(coord) From ca002daa680942f0c10743e1b7435119d0bf41e8 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 9 Aug 2019 15:36:38 +0200 Subject: [PATCH 18/54] Refactor declustering --- pysteps/motion/lucaskanade.py | 158 +++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 68 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 670a2c964..b35e85e22 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -24,7 +24,7 @@ track_features morph_opening detect_outliers - decluster_vectors + decluster_data interpolate_sparse_vectors """ @@ -140,15 +140,14 @@ def dense_lucaskanade(input_images, **kwargs): filter isolated echoes due to clutter. By default this is set to 3. If set to zero, the fitlering is not perfomed. - decl_grid : int, optional - The cell size in pixels of the declustering grid that is used to filter - out outliers in a sparse motion field and get more representative data - points before the interpolation. This simply computes new sparse vectors - over a coarser grid by taking the median of all vectors within one cell. + decl_scale : int, optional + The scale declustering parameter in pixels used to reduce the number of + redundant sparse vectors before the interpolation. + Sparse vectors within this declustering scale are averaged together. By default this is set to 20 pixels. If set to less than 2 pixels, the declustering is not perfomed. - min_nr_samples : int, optional + min_decl_samples : int, optional The minimum number of samples necessary for computing the median vector within given declustering cell, otherwise all sparse vectors in that cell are discarded. By default this is set to 2. @@ -229,8 +228,8 @@ def dense_lucaskanade(input_images, **kwargs): ) k_outlier = kwargs.get("k_outlier", 30) size_opening = kwargs.get("size_opening", 3) - decl_grid = kwargs.get("decl_grid", 20) - min_nr_samples = kwargs.get("min_nr_samples", 2) + decl_scale = kwargs.get("decl_scale", 20) + min_decl_samples = kwargs.get("min_decl_samples", 2) rbfunction = kwargs.get("rbfunction", "inverse") k = kwargs.get("k", 100) epsilon = kwargs.get("epsilon", None) @@ -343,8 +342,18 @@ def dense_lucaskanade(input_images, **kwargs): return x, y, u, v # decluster sparse motion vectors - if decl_grid > 1: - x, y, u, v = decluster_vectors(x, y, u, v, decl_grid, min_nr_samples, verbose) + if decl_scale > 1: + data, coord = decluster_data( + np.stack((u, v)).T, + np.stack((x, v)).T, + decl_scale, + min_decl_samples, + verbose, + ) + u = data[:, 0] + v = data[:, 1] + x = coord[:, 0] + y = coord[:, 1] # return zero motion field if no sparse vectors are left for interpolation if x.size == 0: @@ -543,7 +552,6 @@ def morph_opening(input_image, n=3, thr=0): def detect_outliers(input, thr, coord=None, k=None, verbose=False): - """Detect outliers in a (multivariate and georeferenced) dataset. Assume a (multivariate) Gaussian distribution and detect outliers based on @@ -691,85 +699,99 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): return outliers -def decluster_vectors(x, y, u, v, decl_grid, min_nr_samples, verbose=False): - """Decluster a set of sparse vectors by aggregating (taking the median value) - the initial data points over a coarser grid. +def decluster_data(input, coord, scale, min_samples, verbose=False): + """Decluster a data set by aggregating (median value) over a coarse grid. Parameters ---------- - x : array_like - Array of shape (n) containing the x-coordinates of the origins of the - velocity vectors. - y : array_like - Array of shape (n) containing the y-coordinates of the origins of the - velocity vectors. - u : array_like - Array of shape (n) containing the x-components of the velocities. - v : array_like - Array of shape (n) containing the y-components of the velocities. - decl_grid : float - The size of the declustering grid in the same units as the input. - min_nr_samples : int + + input : array_like + Array of shape (n) or (n, m), where n is the number of samples and m + the number of variables. + All values in the input array are required to have finite values. + + coord : array_like + Array of shape (n, 2) containing the coordinates of the input data into + a 2-dimensional space. + + scale : float or array_like + The scale parameter in the same units of coord. Data points within this + declustering scale are averaged together. + + min_samples : int The minimum number of samples for computing the median within a given declustering cell. + verbose : bool, optional - Print the number of vectors after declustering. + Print out information. Returns ------- + out : tuple of ndarrays - A four-element tuple (x, y, u, v) containing the x- and y-coordinates, - and the x- and y- components of the declustered motion vectors. + A two-element tuple (dinput, dcoord) containing the declustered input + (d, m) and coordinates (d, 2), where d is the new number of samples + (d < n). """ - # Return empty arrays if the number of sparse vectors is < min_nr_samples - if x.size < min_nr_samples: - return np.array([]), np.array([]), np.array([]), np.array([]) + input = np.copy(input) + coord = np.copy(coord) + scale = np.float(scale) - # Make sure these are all numpy vertical arrays - x = np.array(x).flatten()[:, None] - y = np.array(y).flatten()[:, None] - u = np.array(u).flatten()[:, None] - v = np.array(v).flatten()[:, None] + # check inputs + if np.any(~np.isfinite(input)): + raise ValueError("input contains non-finite values") - # Discretize coordinates into declustering grid - xT = np.floor(x / float(decl_grid)) - yT = np.floor(y / float(decl_grid)) + if input.ndim == 1: + nvar = 1 + elif input.ndim == 2: + nvar = input.shape[1] + else: + raise ValueError( + "input must have 1 (n) or 2 dimensions (n, m), but it has %i" % coord.ndim + ) - # Keep only unique combinations of the reduced coordinates - xy = np.concatenate((xT, yT), axis=1) - xyb = np.ascontiguousarray(xy).view( - np.dtype((np.void, xy.dtype.itemsize * xy.shape[1])) + if coord.ndim != 2: + raise ValueError( + "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim + ) + + if coord.shape[0] != input.shape[0]: + raise ValueError( + "the number of samples in the input array does not match the " + + "number of coordinates %i!=%i" % (input.shape[0], coord.shape[0]) + ) + + # reduce original coordinates + coord_ = np.floor(coord / scale) + + # keep only unique pairs of the reduced coordinates + coordb_ = np.ascontiguousarray(coord_).view( + np.dtype((np.void, coord_.dtype.itemsize * coord_.shape[1])) ) - __, idx = np.unique(xyb, return_index=True) - uxy = xy[idx] + __, idx = np.unique(coordb_, return_index=True) + ucoord_ = coord_[idx] - # Loop through these unique values and average vectors which belong to + # loop through these unique values and average vectors which belong to # the same declustering grid cell - xN = [] - yN = [] - uN = [] - vN = [] - for i in range(uxy.shape[0]): - idx = np.logical_and(xT == uxy[i, 0], yT == uxy[i, 1]) + dinput = [] + dcoord = [] + for i in range(ucoord_.shape[0]): + idx = np.logical_and( + coord_[:, 0] == ucoord_[i, 0], coord_[:, 1] == ucoord_[i, 1] + ) npoints = np.sum(idx) - if npoints >= min_nr_samples: - xN.append(np.median(x[idx])) - yN.append(np.median(y[idx])) - uN.append(np.median(u[idx])) - vN.append(np.median(v[idx])) - - # Convert to numpy arrays - x = np.array(xN) - y = np.array(yN) - u = np.array(uN) - v = np.array(vN) + if npoints >= min_samples: + dinput.append(mp.median(input[idx, :], axis=0)) + dcoord.append(mp.median(coord[idx, :], axis=0)) + dinput = np.stack(dinput).squeeze() + dcoord = np.stack(dcoord) if verbose: - print("--- %i sparse vectors left after declustering ---" % x.size) + print("--- %i samples left after declustering ---" % dinput.shape[0]) - return x, y, u, v + return dinput, dcoord def interpolate_sparse_vectors( From 1d270aa8180be8743f9132bd8607a112dbb25acb Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 9 Aug 2019 15:37:00 +0200 Subject: [PATCH 19/54] Fix inputs --- pysteps/motion/lucaskanade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index b35e85e22..713b1fca1 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -327,7 +327,7 @@ def dense_lucaskanade(input_images, **kwargs): # detect outlier vectors outliers = detect_outliers( - np.stack((u, v)).T, nr_std_outlier, np.stack((u, v)).T, k_outlier, verbose + np.stack((u, v)).T, nr_std_outlier, np.stack((x, y)).T, k_outlier, verbose ) x = x[~outliers] y = y[~outliers] From 21b7f0ae1ea62b404f35e90c2b7cf654c65a5ab0 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 9 Aug 2019 15:38:50 +0200 Subject: [PATCH 20/54] Fix typo --- pysteps/motion/lucaskanade.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 713b1fca1..7deda4b3f 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -783,8 +783,8 @@ def decluster_data(input, coord, scale, min_samples, verbose=False): ) npoints = np.sum(idx) if npoints >= min_samples: - dinput.append(mp.median(input[idx, :], axis=0)) - dcoord.append(mp.median(coord[idx, :], axis=0)) + dinput.append(np.median(input[idx, :], axis=0)) + dcoord.append(np.median(coord[idx, :], axis=0)) dinput = np.stack(dinput).squeeze() dcoord = np.stack(dcoord) From e9cd4c5d2270d0e406f96d0fc03383e32800b768 Mon Sep 17 00:00:00 2001 From: aperezhortal Date: Sat, 10 Aug 2019 03:16:01 +0000 Subject: [PATCH 21/54] Add decorator to check the input_images shape The decorator is used in the optical flow functions to check the input_images shape. --- pysteps/decorators.py | 52 ++++++++++++++++++++++++++++++++ pysteps/motion/darts.py | 35 +++++++++++++--------- pysteps/motion/lucaskanade.py | 39 +++++++++++------------- pysteps/motion/proesmans.py | 16 ++++++---- pysteps/motion/vet.py | 12 ++++---- pysteps/tests/test_motion.py | 56 ++++++++++++++++++++++++++++++++--- 6 files changed, 157 insertions(+), 53 deletions(-) create mode 100644 pysteps/decorators.py diff --git a/pysteps/decorators.py b/pysteps/decorators.py new file mode 100644 index 000000000..ed3d3193b --- /dev/null +++ b/pysteps/decorators.py @@ -0,0 +1,52 @@ +""" +pysteps.decorators +================== + +Decorators used to define reusable building blocks that can change or extend +the behavior of some functions in pysteps. + +.. autosummary:: + :toctree: ../generated/ + + check_motion_input_image +""" + +import numpy as np + + +def check_input_frames(minimum_input_frames, maximum_input_frames=np.inf): + """ + Check that the input_images used as inputs in the optical-flow + methods has the correct shape (t, x, y ). + """ + + def _check_input_frames(motion_method_func): + def new_function(*args, **kwargs): + """ + Return new function with the checks prepended to the + target motion_method_func function. + """ + + input_images = args[0] + print(input_images.ndim) + if input_images.ndim != 3: + raise ValueError( + "input_images dimension mismatch.\n" + f"input_images.shape: {str(input_images.shape)}\n" + "(t, x, y ) dimensions expected" + ) + + num_of_frames = input_images.shape[0] + + if minimum_input_frames < num_of_frames > maximum_input_frames: + raise ValueError( + f"input_images frames {num_of_frames} mismatch.\n" + f"Minimum frames: {minimum_input_frames}\n" + f"Maximum frames: {maximum_input_frames}\n" + ) + + return motion_method_func(*args, **kwargs) + + return new_function + + return _check_input_frames diff --git a/pysteps/motion/darts.py b/pysteps/motion/darts.py index 085cef25c..b4e0dde9c 100644 --- a/pysteps/motion/darts.py +++ b/pysteps/motion/darts.py @@ -16,13 +16,13 @@ from numpy.linalg import lstsq, svd from .. import utils -def DARTS(R, **kwargs): +def DARTS(input_images, **kwargs): """Compute the advection field from a sequence of input images by using the DARTS method. :cite:`RCW2011` Parameters ---------- - R : array-like + input_images : array-like Array of shape (T,m,n) containing a sequence of T two-dimensional input images of shape (m,n). @@ -80,8 +80,15 @@ def DARTS(R, **kwargs): lsq_method = kwargs.get("lsq_method", 2) verbose = kwargs.get("verbose", True) - if N_t >= R.shape[0]: - raise ValueError("N_t = %d >= %d = T, but N_t < T required" % (N_t, R.shape[0])) + if input_images.ndim != 3: + raise ValueError( + "input_images dimension mismatch.\n" + f"input_images.shape: {str(input_images.shape)}\n" + "(t, x, y ) dimensions expected" + ) + + if N_t >= input_images.shape[0]: + raise ValueError("N_t = %d >= %d = T, but N_t < T required" % (N_t, input_images.shape[0])) if output_type not in ["spatial", "spectral"]: raise ValueError("invalid output_type=%s, must be 'spatial' or 'spectral'" % output_type) @@ -90,14 +97,14 @@ def DARTS(R, **kwargs): print("Computing the motion field with the DARTS method.") t0 = time.time() - R = np.moveaxis(R, (0, 1, 2), (2, 0, 1)) + input_images = np.moveaxis(input_images, (0, 1, 2), (2, 0, 1)) - fft = utils.get_method(fft_method, shape=R.shape[:2], fftn_shape=R.shape, + fft = utils.get_method(fft_method, shape=input_images.shape[:2], fftn_shape=input_images.shape, **kwargs) - T_x = R.shape[1] - T_y = R.shape[0] - T_t = R.shape[2] + T_x = input_images.shape[1] + T_y = input_images.shape[0] + T_t = input_images.shape[2] if print_info: print("-----") @@ -108,7 +115,7 @@ def DARTS(R, **kwargs): sys.stdout.flush() starttime = time.time() - R = fft.fftn(R) + input_images = fft.fftn(input_images) if print_info: print("Done in %.2f seconds." % (time.time() - starttime)) @@ -129,7 +136,7 @@ def DARTS(R, **kwargs): k_y_ = k_y[i] - N_y k_t_ = k_t[i] - N_t - R_ = R[k_y_, k_x_, k_t_] + R_ = input_images[k_y_, k_x_, k_t_] y[i] = k_t_ * R_ @@ -159,7 +166,7 @@ def DARTS(R, **kwargs): i_ = k_y_ - kp_y_ j_ = k_x_ - kp_x_ - R_ = R[i_, j_, k_t_] + R_ = input_images[i_, j_, k_t_] c2 = c1 / T_y * i_ A[i, :] = c2 * R_ @@ -195,8 +202,8 @@ def DARTS(R, **kwargs): k_x, k_y = np.meshgrid(np.arange(-M_x, M_x+1), np.arange(-M_y, M_y+1)) if output_type == "spatial": - U = np.real(fft.ifft2(_fill(U, R.shape[0], R.shape[1], k_x, k_y))) - V = np.real(fft.ifft2(_fill(V, R.shape[0], R.shape[1], k_x, k_y))) + U = np.real(fft.ifft2(_fill(U, input_images.shape[0], input_images.shape[1], k_x, k_y))) + V = np.real(fft.ifft2(_fill(V, input_images.shape[0], input_images.shape[1], k_x, k_y))) if verbose: print("--- %s seconds ---" % (time.time() - t0)) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 7deda4b3f..446eae104 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -30,19 +30,23 @@ import numpy as np from numpy.ma.core import MaskedArray + +from pysteps.decorators import check_input_frames from pysteps.exceptions import MissingOptionalDependency try: import cv2 - cv2_imported = True + CV2_IMPORTED = True except ImportError: - cv2_imported = False + CV2_IMPORTED = False + import scipy.spatial import time import warnings +@check_input_frames(2) def dense_lucaskanade(input_images, **kwargs): """Run the Lucas-Kanade optical flow and interpolate the motion vectors. @@ -199,14 +203,6 @@ def dense_lucaskanade(input_images, **kwargs): """ - if (input_images.ndim != 3) or input_images.shape[0] < 2: - raise ValueError( - "input_images dimension mismatch.\n" - + "input_images.shape: " - + str(input_images.shape) - + "\n(>1, m, n) expected" - ) - input_images = input_images.copy() # defaults @@ -408,7 +404,7 @@ def features_to_track(input_image, mask, params, verbose=False): Output vector of detected corners. """ - if not cv2_imported: + if not CV2_IMPORTED: raise MissingOptionalDependency( "opencv package is required for the goodFeaturesToTrack() " "routine but it is not installed" @@ -419,9 +415,9 @@ def features_to_track(input_image, mask, params, verbose=False): # scale image between 0 and 255 input_image = ( - (input_image - input_image.min()) - / (input_image.max() - input_image.min()) - * 255 + (input_image - input_image.min()) + / (input_image.max() - input_image.min()) + * 255 ) # convert to 8-bit @@ -470,7 +466,7 @@ def track_features(prvs, next, p0, params, verbose=False): Output vector of v-components of detected point motions. """ - if not cv2_imported: + if not CV2_IMPORTED: raise MissingOptionalDependency( "opencv package is required for the calcOpticalFlowPyrLK() " "routine but it is not installed" @@ -527,7 +523,7 @@ def morph_opening(input_image, n=3, thr=0): Array of shape (m,n) containing the resulting image """ - if not cv2_imported: + if not CV2_IMPORTED: raise MissingOptionalDependency( "opencv package is required for the morphologyEx " "routine but it is not installed" @@ -795,9 +791,8 @@ def decluster_data(input, coord, scale, min_samples, verbose=False): def interpolate_sparse_vectors( - x, y, u, v, xgrid, ygrid, rbfunction="inverse", k=20, epsilon=None, nchunks=5 + x, y, u, v, xgrid, ygrid, rbfunction="inverse", k=20, epsilon=None, nchunks=5 ): - """Interpolate a set of sparse motion vectors to produce a dense field of motion vectors. @@ -874,8 +869,8 @@ def interpolate_sparse_vectors( # find indices of the nearest neighbours _, inds = tree.query(subgrid, k=1) - U[i0 : (i0 + idelta)] = u.ravel()[inds] - V[i0 : (i0 + idelta)] = v.ravel()[inds] + U[i0: (i0 + idelta)] = u.ravel()[inds] + V[i0: (i0 + idelta)] = v.ravel()[inds] else: if k <= 0: @@ -912,10 +907,10 @@ def interpolate_sparse_vectors( if not np.all(np.sum(w, axis=1)): w[np.sum(w, axis=1) == 0, :] = 1.0 - U[i0 : (i0 + idelta)] = np.sum(w * u.ravel()[inds], axis=1) / np.sum( + U[i0: (i0 + idelta)] = np.sum(w * u.ravel()[inds], axis=1) / np.sum( w, axis=1 ) - V[i0 : (i0 + idelta)] = np.sum(w * v.ravel()[inds], axis=1) / np.sum( + V[i0: (i0 + idelta)] = np.sum(w * v.ravel()[inds], axis=1) / np.sum( w, axis=1 ) diff --git a/pysteps/motion/proesmans.py b/pysteps/motion/proesmans.py index feb46d35f..bca43fbf7 100644 --- a/pysteps/motion/proesmans.py +++ b/pysteps/motion/proesmans.py @@ -12,9 +12,14 @@ import numpy as np from scipy.ndimage import gaussian_filter + +from pysteps.decorators import check_input_frames from pysteps.motion._proesmans import _compute_advection_field -def proesmans(input_images, lam=50.0, num_iter=100, num_levels=6, filter_std=0.0): + +@check_input_frames(2, 2) +def proesmans(input_images, lam=50.0, num_iter=100, + num_levels=6, filter_std=0.0, verbose=True, ): """Implementation of the anisotropic diffusion method of Proesmans et al. (1994). @@ -32,6 +37,8 @@ def proesmans(input_images, lam=50.0, num_iter=100, num_levels=6, filter_std=0.0 filter_std : float Standard deviation of an optional Gaussian filter that is applied before computing the optical flow. + verbose : bool, optional + Verbosity enabled if True (default). Returns ------- @@ -46,11 +53,8 @@ def proesmans(input_images, lam=50.0, num_iter=100, num_levels=6, filter_std=0.0 :cite:`PGPO1994` """ - if (input_images.ndim != 3) or input_images.shape[0] != 2: - raise ValueError("input_images dimension mismatch.\n" + - "input_images.shape: " + str(input_images.shape) + - "\n(2, m, n) expected") - + del verbose # Not used + im1 = input_images[-2, :, :].copy() im2 = input_images[-1, :, :].copy() diff --git a/pysteps/motion/vet.py b/pysteps/motion/vet.py index 6474af3db..a3363540a 100644 --- a/pysteps/motion/vet.py +++ b/pysteps/motion/vet.py @@ -39,6 +39,7 @@ from scipy.ndimage.interpolation import zoom from scipy.optimize import minimize +from pysteps.decorators import check_input_frames from pysteps.motion._vet import _warp, _cost_function @@ -314,6 +315,7 @@ def vet_cost_function(sector_displacement_1d, return residuals + smoothness_penalty +@check_input_frames(2, 3) def vet(input_images, sectors=((32, 16, 4, 2), (32, 16, 4, 2)), smooth_gain=1e6, @@ -488,15 +490,10 @@ def debug_print(*args, **kwargs): debug_print("Running VET algorithm") - if (input_images.ndim != 3) or (1 < input_images.shape[0] > 3): - raise ValueError("input_images dimension mismatch.\n" + - "input_images.shape: " + str(input_images.shape) + - "\n(2, x, y ) or (2, x, y ) dimensions expected") - valid_indexing = ['yx', 'xy', 'ij'] if indexing not in valid_indexing: - raise ValueError("Invalid indexing valus: {0}\n".format(indexing) + raise ValueError("Invalid indexing values: {0}\n".format(indexing) + "Supported values: {0}".format(str(valid_indexing))) # Get mask @@ -570,7 +567,8 @@ def debug_print(*args, **kwargs): if (pad_i != (0, 0)) or (pad_j != (0, 0)): - _input_images = numpy.pad(input_images, ((0, 0), pad_i, pad_j), 'edge') + _input_images = numpy.pad(input_images, ((0, 0), pad_i, pad_j), + 'edge') _mask = numpy.pad(mask, (pad_i, pad_j), 'constant', diff --git a/pysteps/tests/test_motion.py b/pysteps/tests/test_motion.py index 84b8f0d09..e17838e01 100644 --- a/pysteps/tests/test_motion.py +++ b/pysteps/tests/test_motion.py @@ -17,6 +17,8 @@ the retrieval. """ +from contextlib import contextmanager + import numpy as np import pytest from scipy.ndimage import uniform_filter @@ -26,6 +28,15 @@ from pysteps.motion.vet import morph from pysteps.tests.helpers import get_precipitation_fields + +@contextmanager +def not_raises(exception): + try: + yield + except exception: + raise pytest.fail("DID RAISE {0}".format(exception)) + + reference_field = get_precipitation_fields(num_prev_files=0) @@ -226,9 +237,13 @@ def test_optflow_method_convergence(input_precip, optflow_method_name, no_precip_args_names = ("optflow_method_name, num_times") -no_precip_args_values = [('lk', 2), ('lk', 3), - ('vet', 2), ('vet', 3), - ('darts', 9)] +no_precip_args_values = [('lk', 2), + ('lk', 3), + ('vet', 2), + ('vet', 3), + ('darts', 9), + #('proesmans', 2) + ] @pytest.mark.parametrize(no_precip_args_names, no_precip_args_values) @@ -256,9 +271,42 @@ def test_no_precipitation(optflow_method_name, num_times): assert np.abs(uv_motion).max() < 0.01 +input_tests_args_names = ("optflow_method_name", + "minimum_input_frames", + "maximum_input_frames") +input_tests_args_values = [ + ('lk', 2, np.inf), + ('vet', 2, 3), + # ('proesmans', 2,2), +] + + +@pytest.mark.parametrize(input_tests_args_names, input_tests_args_values) +def test_input_shape_checks(optflow_method_name, + minimum_input_frames, + maximum_input_frames): + motion_method = motion.get_method(optflow_method_name) + + if maximum_input_frames == np.inf: + maximum_input_frames = minimum_input_frames + 10 + + with not_raises(Exception): + for frames in range(minimum_input_frames, maximum_input_frames + 1): + motion_method(np.ones((frames, 30, 10)), verbose=False) + + with pytest.raises(ValueError): + motion_method(np.zeros((2,))) + motion_method(np.zeros((2, 2))) + for frames in range(minimum_input_frames): + motion_method(np.zeros((frames, 30, 10)), verbose=False) + for frames in range(maximum_input_frames + 1, maximum_input_frames + 4): + motion_method(np.zeros((frames, 30, 10)), verbose=False) + + def test_vet_cost_function(): """ - Test that the vet cost_function computation gives always the same result with the same input. + Test that the vet cost_function computation gives always the same result + with the same input. Useful to test if the parallelization in VET produce undesired results. """ From e007b560a60e7d1dafa159cc869457bd041b5d8d Mon Sep 17 00:00:00 2001 From: aperezhortal Date: Sat, 10 Aug 2019 03:46:31 +0000 Subject: [PATCH 22/54] Add keyword to check ndims only --- pysteps/decorators.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pysteps/decorators.py b/pysteps/decorators.py index ed3d3193b..c8fccf163 100644 --- a/pysteps/decorators.py +++ b/pysteps/decorators.py @@ -14,7 +14,9 @@ import numpy as np -def check_input_frames(minimum_input_frames, maximum_input_frames=np.inf): +def check_input_frames(minimum_input_frames=2, + maximum_input_frames=np.inf, + just_ndim=False): """ Check that the input_images used as inputs in the optical-flow methods has the correct shape (t, x, y ). @@ -28,7 +30,6 @@ def new_function(*args, **kwargs): """ input_images = args[0] - print(input_images.ndim) if input_images.ndim != 3: raise ValueError( "input_images dimension mismatch.\n" @@ -36,14 +37,15 @@ def new_function(*args, **kwargs): "(t, x, y ) dimensions expected" ) - num_of_frames = input_images.shape[0] + if not just_ndim: + num_of_frames = input_images.shape[0] - if minimum_input_frames < num_of_frames > maximum_input_frames: - raise ValueError( - f"input_images frames {num_of_frames} mismatch.\n" - f"Minimum frames: {minimum_input_frames}\n" - f"Maximum frames: {maximum_input_frames}\n" - ) + if minimum_input_frames < num_of_frames > maximum_input_frames: + raise ValueError( + f"input_images frames {num_of_frames} mismatch.\n" + f"Minimum frames: {minimum_input_frames}\n" + f"Maximum frames: {maximum_input_frames}\n" + ) return motion_method_func(*args, **kwargs) From 5c1bc3f069f345fd7351ebfbc4db24e8c88209c6 Mon Sep 17 00:00:00 2001 From: aperezhortal Date: Sat, 10 Aug 2019 03:47:14 +0000 Subject: [PATCH 23/54] Add darts to input shape tests --- pysteps/tests/test_motion.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pysteps/tests/test_motion.py b/pysteps/tests/test_motion.py index e17838e01..65bda6c43 100644 --- a/pysteps/tests/test_motion.py +++ b/pysteps/tests/test_motion.py @@ -30,11 +30,11 @@ @contextmanager -def not_raises(exception): +def not_raises(_exception): try: yield - except exception: - raise pytest.fail("DID RAISE {0}".format(exception)) + except _exception: + raise pytest.fail("DID RAISE {0}".format(_exception)) reference_field = get_precipitation_fields(num_prev_files=0) @@ -277,6 +277,7 @@ def test_no_precipitation(optflow_method_name, num_times): input_tests_args_values = [ ('lk', 2, np.inf), ('vet', 2, 3), + ('darts', 9, 9), # ('proesmans', 2,2), ] @@ -285,6 +286,8 @@ def test_no_precipitation(optflow_method_name, num_times): def test_input_shape_checks(optflow_method_name, minimum_input_frames, maximum_input_frames): + + image_size = 100 motion_method = motion.get_method(optflow_method_name) if maximum_input_frames == np.inf: @@ -292,15 +295,17 @@ def test_input_shape_checks(optflow_method_name, with not_raises(Exception): for frames in range(minimum_input_frames, maximum_input_frames + 1): - motion_method(np.ones((frames, 30, 10)), verbose=False) + motion_method(np.zeros((frames, image_size, image_size)), verbose=False) with pytest.raises(ValueError): motion_method(np.zeros((2,))) motion_method(np.zeros((2, 2))) for frames in range(minimum_input_frames): - motion_method(np.zeros((frames, 30, 10)), verbose=False) + motion_method(np.zeros((frames, image_size, image_size)), + verbose=False) for frames in range(maximum_input_frames + 1, maximum_input_frames + 4): - motion_method(np.zeros((frames, 30, 10)), verbose=False) + motion_method(np.zeros((frames, image_size, image_size)), + verbose=False) def test_vet_cost_function(): @@ -329,7 +334,6 @@ def test_vet_cost_function(): 1e6, # smooth_gain debug=False) - print(returned_values) tolerance = 1e-12 errors = np.abs(returned_values - returned_values[0]) # errors should contain all zeros From 3450561a54ed8ac6dc8964a6b9cbfd6449c42a7e Mon Sep 17 00:00:00 2001 From: aperezhortal Date: Sat, 10 Aug 2019 12:40:45 +0000 Subject: [PATCH 24/54] Remove unused variables --- pysteps/motion/darts.py | 63 +++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/pysteps/motion/darts.py b/pysteps/motion/darts.py index b4e0dde9c..1391a739e 100644 --- a/pysteps/motion/darts.py +++ b/pysteps/motion/darts.py @@ -11,11 +11,16 @@ """ import sys -import time + import numpy as np +import time from numpy.linalg import lstsq, svd -from .. import utils +from pysteps import utils +from pysteps.decorators import check_input_frames + + +@check_input_frames(just_ndim=True) def DARTS(input_images, **kwargs): """Compute the advection field from a sequence of input images by using the DARTS method. :cite:`RCW2011` @@ -51,7 +56,7 @@ def DARTS(input_images, **kwargs): n_threads : int Number of threads to use for the FFT computation. Applicable if fft_method is 'pyfftw'. - print_info : bool + verbose : bool If True, print information messages. lsq_method : {1, 2} The method to use for solving the linear equations in the least squares @@ -76,17 +81,9 @@ def DARTS(input_images, **kwargs): M_y = kwargs.get("M_y", 2) fft_method = kwargs.get("fft_method", "numpy") output_type = kwargs.get("output_type", "spatial") - print_info = kwargs.get("print_info", False) lsq_method = kwargs.get("lsq_method", 2) verbose = kwargs.get("verbose", True) - if input_images.ndim != 3: - raise ValueError( - "input_images dimension mismatch.\n" - f"input_images.shape: {str(input_images.shape)}\n" - "(t, x, y ) dimensions expected" - ) - if N_t >= input_images.shape[0]: raise ValueError("N_t = %d >= %d = T, but N_t < T required" % (N_t, input_images.shape[0])) @@ -106,7 +103,7 @@ def DARTS(input_images, **kwargs): T_y = input_images.shape[0] T_t = input_images.shape[2] - if print_info: + if verbose: print("-----") print("DARTS") print("-----") @@ -117,43 +114,41 @@ def DARTS(input_images, **kwargs): input_images = fft.fftn(input_images) - if print_info: + if verbose: print("Done in %.2f seconds." % (time.time() - starttime)) print(" Constructing the y-vector..."), sys.stdout.flush() starttime = time.time() - m = (2*N_x+1)*(2*N_y+1)*(2*N_t+1) - n = (2*M_x+1)*(2*M_y+1) + m = (2 * N_x + 1) * (2 * N_y + 1) * (2 * N_t + 1) + n = (2 * M_x + 1) * (2 * M_y + 1) y = np.zeros(m, dtype=complex) - k_t, k_y, k_x = np.unravel_index(np.arange(m), (2*N_t+1, 2*N_y+1, 2*N_x+1)) + k_t, k_y, k_x = np.unravel_index(np.arange(m), (2 * N_t + 1, 2 * N_y + 1, 2 * N_x + 1)) for i in range(m): k_x_ = k_x[i] - N_x k_y_ = k_y[i] - N_y k_t_ = k_t[i] - N_t - R_ = input_images[k_y_, k_x_, k_t_] + y[i] = k_t_ * input_images[k_y_, k_x_, k_t_] - y[i] = k_t_ * R_ - - if print_info: + if verbose: print("Done in %.2f seconds." % (time.time() - starttime)) A = np.zeros((m, n), dtype=complex) B = np.zeros((m, n), dtype=complex) - if print_info: + if verbose: print(" Constructing the H-matrix..."), sys.stdout.flush() starttime = time.time() - c1 = -1.0*T_t / (T_x * T_y) + c1 = -1.0 * T_t / (T_x * T_y) - kp_y, kp_x = np.unravel_index(np.arange(n), (2*M_y+1, 2*M_x+1)) + kp_y, kp_x = np.unravel_index(np.arange(n), (2 * M_y + 1, 2 * M_x + 1)) for i in range(m): k_x_ = k_x[i] - N_x @@ -174,7 +169,7 @@ def DARTS(input_images, **kwargs): c2 = c1 / T_x * j_ B[i, :] = c2 * R_ - if print_info: + if verbose: print("Done in %.2f seconds." % (time.time() - starttime)) print(" Solving the linear systems..."), @@ -186,20 +181,20 @@ def DARTS(input_images, **kwargs): else: x = _leastsq(A, B, y) - if print_info: + if verbose: print("Done in %.2f seconds." % (time.time() - starttime)) - h, w = 2*M_y+1, 2*M_x+1 + h, w = 2 * M_y + 1, 2 * M_x + 1 U = np.zeros((h, w), dtype=complex) V = np.zeros((h, w), dtype=complex) - i, j = np.unravel_index(np.arange(h*w), (h, w)) + i, j = np.unravel_index(np.arange(h * w), (h, w)) - V[i, j] = x[0:h*w] - U[i, j] = x[h*w:2*h*w] + V[i, j] = x[0:h * w] + U[i, j] = x[h * w:2 * h * w] - k_x, k_y = np.meshgrid(np.arange(-M_x, M_x+1), np.arange(-M_y, M_y+1)) + k_x, k_y = np.meshgrid(np.arange(-M_x, M_x + 1), np.arange(-M_y, M_y + 1)) if output_type == "spatial": U = np.real(fft.ifft2(_fill(U, input_images.shape[0], input_images.shape[1], k_x, k_y))) @@ -210,16 +205,15 @@ def DARTS(input_images, **kwargs): return np.stack([U, V]) + def _leastsq(A, B, y): M = np.hstack([A, B]) M_ct = M.conjugate().T MM = np.dot(M_ct, M) - M = None - U, s, V = svd(MM, full_matrices=False) - MM = None - mask = s > 0.01*s[0] + + mask = s > 0.01 * s[0] s = 1.0 / s[mask] MM_inv = np.dot(np.dot(V[:len(s), :].conjugate().T, np.diag(s)), @@ -227,6 +221,7 @@ def _leastsq(A, B, y): return np.dot(MM_inv, np.dot(M_ct, y)) + def _fill(X, h, w, k_x, k_y): X_f = np.zeros((h, w), dtype=complex) X_f[k_y, k_x] = X From 0b15f999c1ee3ebe83590c835bbc854ca98dfacc Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 12 Aug 2019 09:19:35 +0200 Subject: [PATCH 25/54] Change input variable name --- pysteps/motion/lucaskanade.py | 126 ++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 52 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 446eae104..3fb3b2c4b 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -243,7 +243,9 @@ def dense_lucaskanade(input_images, **kwargs): else: input_images = np.ma.masked_invalid(input_images) mask = np.ma.getmaskarray(input_images).copy() - input_images[mask] = np.nanmin(input_images) # Remove any Nan from the raw data + input_images[mask] = np.nanmin( + input_images + ) # Remove any Nan from the raw data nr_fields = input_images.shape[0] domain_size = (input_images.shape[1], input_images.shape[2]) @@ -323,7 +325,11 @@ def dense_lucaskanade(input_images, **kwargs): # detect outlier vectors outliers = detect_outliers( - np.stack((u, v)).T, nr_std_outlier, np.stack((x, y)).T, k_outlier, verbose + np.stack((u, v)).T, + nr_std_outlier, + np.stack((x, y)).T, + k_outlier, + verbose, ) x = x[~outliers] y = y[~outliers] @@ -415,9 +421,9 @@ def features_to_track(input_image, mask, params, verbose=False): # scale image between 0 and 255 input_image = ( - (input_image - input_image.min()) - / (input_image.max() - input_image.min()) - * 255 + (input_image - input_image.min()) + / (input_image.max() - input_image.min()) + * 255 ) # convert to 8-bit @@ -547,7 +553,7 @@ def morph_opening(input_image, n=3, thr=0): return input_image -def detect_outliers(input, thr, coord=None, k=None, verbose=False): +def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): """Detect outliers in a (multivariate and georeferenced) dataset. Assume a (multivariate) Gaussian distribution and detect outliers based on @@ -560,10 +566,10 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): Parameters ---------- - input : array_like + input_array : array_like Array of shape (n) or (n, m), where n is the number of samples and m the number of variables. If m > 1, it employs the Mahalanobis distance. - All values in the input array are required to have finite values. + All values in input_array are required to have finite values. thr : float The number of standard deviations from the mean that defines an outlier. @@ -584,22 +590,23 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): ------- out : array_like - A boolean array of the same shape as the input array, with True values + A boolean array of the same shape as input_array, with True values indicating the outliers detected in the input array. """ - input = np.copy(input) + input_array = np.copy(input_array) - if np.any(~np.isfinite(input)): - raise ValueError("input contains non-finite values") + if np.any(~np.isfinite(input_array)): + raise ValueError("input_array contains non-finite values") - if input.ndim == 1: + if input_array.ndim == 1: nvar = 1 - elif input.ndim == 2: - nvar = input.shape[1] + elif input_array.ndim == 2: + nvar = input_array.shape[1] else: raise ValueError( - "input must have 1 (n) or 2 dimensions (n, m), but it has %i" % coord.ndim + "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" + % coord.ndim ) if coord is not None: @@ -610,13 +617,15 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): elif coord.ndim > 2: raise ValueError( - "coord must have 2 dimensions (n, d), but it has %i" % coord.ndim + "coord must have 2 dimensions (n, d), but it has %i" + % coord.ndim ) - if coord.shape[0] != input.shape[0]: + if coord.shape[0] != input_array.shape[0]: raise ValueError( - "the number of samples in the input array does not match the " - + "number of coordinates %i!=%i" % (input.shape[0], coord.shape[0]) + "the number of samples in input_array does not match the " + + "number of coordinates %i!=%i" + % (input_array.shape[0], coord.shape[0]) ) if k is None: @@ -636,21 +645,21 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): # univariate - zdata = (input - np.mean(input)) / np.std(input) + zdata = (input_array - np.mean(input_array)) / np.std(input_array) outliers = zdata > thr else: # multivariate (mahalanobis distance) - zdata = input - np.mean(input, axis=0) + zdata = input_array - np.mean(input_array, axis=0) V = np.cov(zdata.T) VI = np.linalg.inv(V) try: VI = np.linalg.inv(V) MD = np.sqrt(np.dot(np.dot(zdata, VI), zdata.T).diagonal()) except np.linalg.LinAlgError: - MD = np.zeros(input.shape) + MD = np.zeros(input_array.shape) outliers = MD > thr # local @@ -666,17 +675,19 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): # in terms of velocity - thisdata = input[i] - neighbours = input[inds[i, 1:]] - thiszdata = (thisdata - np.mean(neighbours)) / np.std(neighbours) + thisdata = input_array[i] + neighbours = input_array[inds[i, 1:]] + thiszdata = (thisdata - np.mean(neighbours)) / np.std( + neighbours + ) outliers.append(thiszdata > thr) else: # mahalanobis distance - thisdata = input[i, :] - neighbours = input[inds[i, 1:], :].copy() + thisdata = input_array[i, :] + neighbours = input_array[inds[i, 1:], :].copy() thiszdata = thisdata - np.mean(neighbours, axis=0) neighbours = neighbours - np.mean(neighbours, axis=0) V = np.cov(neighbours.T) @@ -695,16 +706,16 @@ def detect_outliers(input, thr, coord=None, k=None, verbose=False): return outliers -def decluster_data(input, coord, scale, min_samples, verbose=False): +def decluster_data(input_array, coord, scale, min_samples, verbose=False): """Decluster a data set by aggregating (median value) over a coarse grid. Parameters ---------- - input : array_like + input_array : array_like Array of shape (n) or (n, m), where n is the number of samples and m the number of variables. - All values in the input array are required to have finite values. + All values in input_array are required to have finite values. coord : array_like Array of shape (n, 2) containing the coordinates of the input data into @@ -725,27 +736,28 @@ def decluster_data(input, coord, scale, min_samples, verbose=False): ------- out : tuple of ndarrays - A two-element tuple (dinput, dcoord) containing the declustered input + A two-element tuple (dinput, dcoord) containing the declustered input_array (d, m) and coordinates (d, 2), where d is the new number of samples (d < n). """ - input = np.copy(input) + input_array = np.copy(input_array) coord = np.copy(coord) scale = np.float(scale) # check inputs - if np.any(~np.isfinite(input)): - raise ValueError("input contains non-finite values") + if np.any(~np.isfinite(input_array)): + raise ValueError("input_array contains non-finite values") - if input.ndim == 1: + if input_array.ndim == 1: nvar = 1 - elif input.ndim == 2: - nvar = input.shape[1] + elif input_array.ndim == 2: + nvar = input_array.shape[1] else: raise ValueError( - "input must have 1 (n) or 2 dimensions (n, m), but it has %i" % coord.ndim + "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" + % coord.ndim ) if coord.ndim != 2: @@ -753,10 +765,11 @@ def decluster_data(input, coord, scale, min_samples, verbose=False): "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim ) - if coord.shape[0] != input.shape[0]: + if coord.shape[0] != input_array.shape[0]: raise ValueError( - "the number of samples in the input array does not match the " - + "number of coordinates %i!=%i" % (input.shape[0], coord.shape[0]) + "the number of samples in the input_array does not match the " + + "number of coordinates %i!=%i" + % (input_array.shape[0], coord.shape[0]) ) # reduce original coordinates @@ -779,7 +792,7 @@ def decluster_data(input, coord, scale, min_samples, verbose=False): ) npoints = np.sum(idx) if npoints >= min_samples: - dinput.append(np.median(input[idx, :], axis=0)) + dinput.append(np.median(input_array[idx, :], axis=0)) dcoord.append(np.median(coord[idx, :], axis=0)) dinput = np.stack(dinput).squeeze() dcoord = np.stack(dcoord) @@ -791,7 +804,16 @@ def decluster_data(input, coord, scale, min_samples, verbose=False): def interpolate_sparse_vectors( - x, y, u, v, xgrid, ygrid, rbfunction="inverse", k=20, epsilon=None, nchunks=5 + x, + y, + u, + v, + xgrid, + ygrid, + rbfunction="inverse", + k=20, + epsilon=None, + nchunks=5, ): """Interpolate a set of sparse motion vectors to produce a dense field of motion vectors. @@ -869,8 +891,8 @@ def interpolate_sparse_vectors( # find indices of the nearest neighbours _, inds = tree.query(subgrid, k=1) - U[i0: (i0 + idelta)] = u.ravel()[inds] - V[i0: (i0 + idelta)] = v.ravel()[inds] + U[i0 : (i0 + idelta)] = u.ravel()[inds] + V[i0 : (i0 + idelta)] = v.ravel()[inds] else: if k <= 0: @@ -907,12 +929,12 @@ def interpolate_sparse_vectors( if not np.all(np.sum(w, axis=1)): w[np.sum(w, axis=1) == 0, :] = 1.0 - U[i0: (i0 + idelta)] = np.sum(w * u.ravel()[inds], axis=1) / np.sum( - w, axis=1 - ) - V[i0: (i0 + idelta)] = np.sum(w * v.ravel()[inds], axis=1) / np.sum( - w, axis=1 - ) + U[i0 : (i0 + idelta)] = np.sum( + w * u.ravel()[inds], axis=1 + ) / np.sum(w, axis=1) + V[i0 : (i0 + idelta)] = np.sum( + w * v.ravel()[inds], axis=1 + ) / np.sum(w, axis=1) i0 += idelta From f3a2a84820c40f67eecfee54f38b41fc4138ee0f Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 12 Aug 2019 15:14:25 +0200 Subject: [PATCH 26/54] Refacator interpolation routine --- pysteps/motion/lucaskanade.py | 286 ++++++++++++++++++++-------------- 1 file changed, 168 insertions(+), 118 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 3fb3b2c4b..cc3db50d6 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -25,7 +25,7 @@ morph_opening detect_outliers decluster_data - interpolate_sparse_vectors + rbfinterp2d """ import numpy as np @@ -156,20 +156,19 @@ def dense_lucaskanade(input_images, **kwargs): within given declustering cell, otherwise all sparse vectors in that cell are discarded. By default this is set to 2. - rbfunction : string, optional - The name of the radial basis function used for the interpolation of the - sparse vectors. This is based on the Euclidian norm d. By default this - is set to "inverse" and the available names are "nearest", "inverse", - "gaussian". + rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse + multiquadric", "bump"}, optional + The name of one of the available radial basis function based on the + Euclidean norm. "gaussian" by default. - k : int, optional - The number of nearest neighbours used for fast interpolation, by default - this is set to 20. If set equal to zero, it employs all the neighbours. + k : int or None, optional + The number of nearest neighbours used to speed-up the interpolation. + If set to None, it interpolates based on all the data points. + This is 50 by default. epsilon : float, optional - The adjustable constant used in the gaussian and inverse radial basis - functions. by default this is computed as the median distance between - the sparse vectors. + The shape parameter > 0 used to scale the input to the radial kernel. + It defaults to 1.0. nchunks : int, optional Split the grid points in n chunks to limit the memory usage during the @@ -226,9 +225,9 @@ def dense_lucaskanade(input_images, **kwargs): size_opening = kwargs.get("size_opening", 3) decl_scale = kwargs.get("decl_scale", 20) min_decl_samples = kwargs.get("min_decl_samples", 2) - rbfunction = kwargs.get("rbfunction", "inverse") - k = kwargs.get("k", 100) - epsilon = kwargs.get("epsilon", None) + rbfunction = kwargs.get("rbfunction", "gaussian") + k = kwargs.get("k", 50) + epsilon = kwargs.get("epsilon", 1.0) nchunks = kwargs.get("nchunks", 5) verbose = kwargs.get("verbose", True) buffer_mask = kwargs.get("buffer_mask", 10) @@ -364,17 +363,15 @@ def dense_lucaskanade(input_images, **kwargs): # kernel interpolation xgrid = np.arange(domain_size[1]) ygrid = np.arange(domain_size[0]) - UV = interpolate_sparse_vectors( - x, - y, - u, - v, - xgrid, - ygrid, - rbfunction=rbfunction, - k=k, - epsilon=epsilon, - nchunks=nchunks, + UV = rbfinterp2d( + np.stack((u, v)).T, + np.stack((x, v)).T, + xgrid, + ygrid, + rbfunction=rbfunction, + epsilon=epsilon, + k=k, + nchunks=nchunks, ) if verbose: @@ -701,7 +698,7 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): outliers = np.array(outliers) if verbose: - print("--- %i outliers removed ---" % np.sum(~outliers)) + print("--- %i outliers detected ---" % np.sum(outliers)) return outliers @@ -803,143 +800,196 @@ def decluster_data(input_array, coord, scale, min_samples, verbose=False): return dinput, dcoord -def interpolate_sparse_vectors( - x, - y, - u, - v, +def rbfinterp2d( + input_array, + coord, xgrid, ygrid, - rbfunction="inverse", - k=20, - epsilon=None, + rbfunction="gaussian", + epsilon=1, + k=50, nchunks=5, ): - """Interpolate a set of sparse motion vectors to produce a dense field of - motion vectors. + """Fast interpolation of a (multivariate) array over a 2D grid. Parameters ---------- - x : array-like - The x-coordinates of the sparse motion vectors. - y : array-like - The y-coordinates of the sparse motion vectors. - u : array_like - The x-components of the sparse motion vectors. - v : array_like - The y-components of the sparse motion vectors. - xgrid : array_like - Array of shape (n) containing the x-coordinates of the final grid. - ygrid : array_like - Array of shape (m) containing the y-coordinates of the final grid. - rbfunction : {"nearest", "inverse", "gaussian"}, optional - The radial basis rbfunction based on the Euclidian norm. - k : int or "all", optional - The number of nearest neighbours used to speed-up the interpolation. - If set equal to "all", it employs all the sparse vectors. + + input_array : array_like + Array of shape (n) or (n, m), where n is the number of data points and + m the number of co-located variables. + All values in input_array are required to have finite values. + + coord : array_like + Array of shape (n, 2) containing the coordinates of the data points into + a 2-dimensional space. + + xgrid, ygrid : array_like + 1D arrays representing the coordinates of the target grid. + + rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse + multiquadric", "bump"}, optional + The name of one of the available radial basis function based on the Euclidian + norm. See section "Notes" below. + epsilon : float, optional - The adjustable constant for the gaussian and inverse radial basis rbfunction. - If set equal to None (the default), epsilon is estimated as the median - distance between the sparse vectors. - nchunks : int + The shape parameter > 0 used to scale the input to the radial kernel. + + k : int or None, optional + The number of nearest neighbours used to speed-up the interpolation. + If set to None, it interpolates based on all the data points. + + nchunks : int, optional The number of chunks in which the grid points are split to limit the memory usage during the interpolation. Returns ------- - out : ndarray - The interpolated advection field having shape (2, m, n), where out[0, :, :] - contains the x-components of the motion vectors and out[1, :, :] contains - the y-components. The units are given by the input sparse motion vectors. + output_array : array_like + The interpolated field(s) having shape (m, ygrid.size, xgrid.size). + + Notes + ----- + + The input coordinates are normalized before computing the euclidean norms: + + x = (x - median(x)) / MAD / 1.4826 + + where MAD = median(|x - median(x)|). + + The definitions of the radial basis functions are taken from the following + wikipedia page: https://en.wikipedia.org/wiki/Radial_basis_function """ - # make sure these are vertical arrays - x = np.array(x).flatten()[:, None] - y = np.array(y).flatten()[:, None] - u = np.array(u).flatten()[:, None] - v = np.array(v).flatten()[:, None] - points = np.concatenate((x, y), axis=1) - npoints = points.shape[0] + _rbfunctions = [ + "nearest", + "gaussian", + "inverse quadratic", + "inverse multiquadric", + "bump", + ] + + input_array = np.copy(input_array) + + if np.any(~np.isfinite(input_array)): + raise ValueError("input_array contains non-finite values") + + if input_array.ndim == 1: + nvar = 1 + input_array = input_array[:, None] + + elif input_array.ndim == 2: + nvar = input_array.shape[1] - # generate the full grid + else: + raise ValueError( + "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" + % input_array.ndim + ) + + npoints = input_array.shape[0] + + coord = np.copy(coord) + + if coord.ndim != 2: + raise ValueError( + "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim + ) + + if npoints != coord.shape[0]: + raise ValueError( + "the number of samples in the input_array does not match the " + + "number of coordinates %i!=%i" % (npoints, coord.shape[0]) + ) + + # normalize coordinates + mcoord = np.median(coord, axis=0) + madcoord = 1.4826 * np.median(np.abs(coord - mcoord), axis=0) + coord = (coord - mcoord) / madcoord + + rbfunction = rbfunction.lower() + if rbfunction not in _rbfunctions: + raise ValueError( + "Unknown rbfunction '{}'\n".format(rbfunction) + + "The available rbfunctions are: " + + str(_rbfunctions) + ) from None + + # generate the final grid X, Y = np.meshgrid(xgrid, ygrid) grid = np.column_stack((X.ravel(), Y.ravel())) + grid = (grid - mcoord) / madcoord + + # k-nearest interpolation + if k is not None and k > 0: + k = int(np.min((k, npoints))) - U = np.zeros(grid.shape[0]) - V = np.zeros(grid.shape[0]) + # create cKDTree object to represent source grid + tree = scipy.spatial.cKDTree(coord) - # create cKDTree object to represent source grid - if k > 0: - k = np.min((k, npoints)) - tree = scipy.spatial.cKDTree(points) + else: + k = 0 # split grid points in n chunks if nchunks > 1: subgrids = np.array_split(grid, nchunks, 0) subgrids = [x for x in subgrids if x.size > 0] + else: subgrids = [grid] # loop subgrids i0 = 0 + output_array = np.zeros((grid.shape[0], nvar)) for i, subgrid in enumerate(subgrids): - idelta = subgrid.shape[0] - if rbfunction.lower() == "nearest": - # find indices of the nearest neighbours - _, inds = tree.query(subgrid, k=1) + if k == 0: + # use all points + d = scipy.spatial.distance.cdist( + coord, subgrid, "euclidean" + ).transpose() + inds = np.arange(npoints)[None, :] * np.ones( + (subgrid.shape[0], npoints) + ).astype(int) + + else: + # use k-nearest neighbours + d, inds = tree.query(subgrid, k=k) - U[i0 : (i0 + idelta)] = u.ravel()[inds] - V[i0 : (i0 + idelta)] = v.ravel()[inds] + if k == 1: + # nearest neighbour + output_array[i0 : (i0 + idelta), :] = input_array[inds, :] else: - if k <= 0: - d = scipy.spatial.distance.cdist( - points, subgrid, "euclidean" - ).transpose() - inds = np.arange(u.size)[None, :] * np.ones( - (subgrid.shape[0], u.size) - ).astype(int) - else: - # find indices of the k-nearest neighbours - d, inds = tree.query(subgrid, k=k) + # the interpolation weights + if rbfunction == "gaussian": + w = np.exp(-(d * epsilon) ** 2) - if inds.ndim == 1: - inds = inds[:, None] - d = d[:, None] + elif rbfunction == "inverse quadratic": + w = 1.0 / (1 + (epsilon * d) ** 2) - # the bandwidth - if epsilon is None: - epsilon = 1 - if npoints > 1: - dpoints = scipy.spatial.distance.pdist(points, "euclidean") - epsilon = np.median(dpoints) + elif rbfunction == "inverse multiquadric": + w = 1.0 / np.sqrt(1 + (epsilon * d) ** 2) - # the interpolation weights - if rbfunction.lower() == "inverse": - w = 1.0 / np.sqrt((d / epsilon) ** 2 + 1) - elif rbfunction.lower() == "gaussian": - w = np.exp(-0.5 * (d / epsilon) ** 2) - else: - raise ValueError("unknown radial fucntion %s" % rbfunction) + elif rbfunction == "bump": + w = np.exp(-1.0 / (1 - (epsilon * d) ** 2)) + w[d >= 1 / epsilon] = 0.0 if not np.all(np.sum(w, axis=1)): w[np.sum(w, axis=1) == 0, :] = 1.0 - U[i0 : (i0 + idelta)] = np.sum( - w * u.ravel()[inds], axis=1 - ) / np.sum(w, axis=1) - V[i0 : (i0 + idelta)] = np.sum( - w * v.ravel()[inds], axis=1 - ) / np.sum(w, axis=1) + # interpolate + for j in range(nvar): + output_array[i0 : (i0 + idelta), j] = np.sum( + w * input_array[inds, j], axis=1 + ) / np.sum(w, axis=1) i0 += idelta - # reshape back to original size - U = U.reshape(ygrid.size, xgrid.size) - V = V.reshape(ygrid.size, xgrid.size) + # reshape to final grid size + output_array = output_array.reshape(ygrid.size, xgrid.size, nvar) - return np.stack([U, V]) + return np.moveaxis(output_array, -1, 0).squeeze() From c775a37e9d9e17064b4affbb0575ba59aed2a09e Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 12 Aug 2019 15:38:50 +0200 Subject: [PATCH 27/54] Use x,y and u,v as 2d array --- pysteps/motion/lucaskanade.py | 134 ++++++++++++++++------------------ 1 file changed, 62 insertions(+), 72 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index cc3db50d6..118757bca 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -248,10 +248,8 @@ def dense_lucaskanade(input_images, **kwargs): nr_fields = input_images.shape[0] domain_size = (input_images.shape[1], input_images.shape[2]) - y0Stack = [] - x0Stack = [] - uStack = [] - vStack = [] + xy_stack = [] + uv_stack = [] for n in range(nr_fields - 1): # extract consecutive images @@ -297,81 +295,59 @@ def dense_lucaskanade(input_images, **kwargs): maxLevel=nr_levels_LK, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), ) - x0, y0, u, v = track_features(prvs, next, p0, lk_params, False) + xy, uv = track_features(prvs, next, p0, lk_params, False) # skip loop if no vectors - if x0 is None: + if xy is None: continue # stack vectors - x0Stack.append(x0) - y0Stack.append(y0) - uStack.append(u) - vStack.append(v) + xy_stack.append(xy) + uv_stack.append(uv) # return zero motion field is no sparse vectors are found - if len(x0Stack) == 0: + if len(xy_stack) == 0: if dense: return np.zeros((2, domain_size[0], domain_size[1])) else: - return np.array([]), np.array([]), np.array([]), np.array([]) + return np.array([]), np.array([]) # convert lists of arrays into single arrays - x = np.concatenate(x0Stack) - y = np.concatenate(y0Stack) - u = np.concatenate(uStack) - v = np.concatenate(vStack) + xy = np.concatenate(xy_stack) + uv = np.concatenate(uv_stack) # detect outlier vectors - outliers = detect_outliers( - np.stack((u, v)).T, - nr_std_outlier, - np.stack((x, y)).T, - k_outlier, - verbose, - ) - x = x[~outliers] - y = y[~outliers] - u = u[~outliers] - v = v[~outliers] + outliers = detect_outliers(uv, nr_std_outlier, xy, k_outlier, verbose) + xy = xy[~outliers, :] + uv = uv[~outliers, :] if verbose: - print("--- LK found %i sparse vectors ---" % x.size) + print("--- LK found %i sparse vectors ---" % xy.shape[0]) # return sparse vectors if required if not dense: - return x, y, u, v + return xy, uv # decluster sparse motion vectors if decl_scale > 1: - data, coord = decluster_data( - np.stack((u, v)).T, - np.stack((x, v)).T, - decl_scale, - min_decl_samples, - verbose, - ) - u = data[:, 0] - v = data[:, 1] - x = coord[:, 0] - y = coord[:, 1] + uv, xy = decluster_data(uv, xy, decl_scale, min_decl_samples, verbose) # return zero motion field if no sparse vectors are left for interpolation - if x.size == 0: + if xy.shape[0] == 0: return np.zeros((2, domain_size[0], domain_size[1])) # kernel interpolation xgrid = np.arange(domain_size[1]) ygrid = np.arange(domain_size[0]) UV = rbfinterp2d( - np.stack((u, v)).T, - np.stack((x, v)).T, - xgrid, - ygrid, - rbfunction=rbfunction, - epsilon=epsilon, - k=k, - nchunks=nchunks, + uv, + xy, + xgrid, + ygrid, + rbfunction=rbfunction, + epsilon=epsilon, + k=k, + nchunks=nchunks, ) if verbose: @@ -390,22 +366,27 @@ def features_to_track(input_image, mask, params, verbose=False): Parameters ---------- - input_image : ndarray_ + + input_image : array_like Array of shape (m, n) containing the input image. - mask : ndarray_ + All values in input_image are required to have finite values. + + mask : array_like Array of shape (m,n). It specifies the image region in which the corners can be detected. + params : dict Any additional parameter to the original routine as described in the corresponding documentation. + verbose : bool, optional Print the number of features detected. Returns ------- + p0 : list Output vector of detected corners. - """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -413,6 +394,8 @@ def features_to_track(input_image, mask, params, verbose=False): "routine but it is not installed" ) + input_image = np.copy(input_image) + if input_image.ndim != 2: raise ValueError("input_image must be a two-dimensional array") @@ -435,7 +418,7 @@ def features_to_track(input_image, mask, params, verbose=False): return p0.squeeze() -def track_features(prvs, next, p0, params, verbose=False): +def track_features(prvs_image, next_image, points, params, verbose=False): """ Interface to the OpenCV `calcOpticalFlowPyrLK()`_ features tracking algorithm. @@ -444,30 +427,34 @@ def track_features(prvs, next, p0, params, verbose=False): Parameters ---------- - prvs : array-like + + prvs_image : array_like Array of shape (m, n) containing the initial image. - next : array-like + All values in prvs_image are required to have finite values. + + next_image : array_like Array of shape (m, n) containing the successive image. - p0 : list + All values in next_image are required to have finite values. + + points : list Vector of 2D points for which the flow needs to be found. Point coordinates must be single-precision floating-point numbers. + params : dict Any additional parameter to the original routine as described in the corresponding documentation. + verbose : bool, optional Print the number of vectors that have been found. Returns ------- - x0 : array-like + + xy : array_like Output vector of x-coordinates of detected point motions. - y0 : array-like - Output vector of y-coordinates of detected point motions. - u : array-like - Output vector of u-components of detected point motions. - v : array-like - Output vector of v-components of detected point motions. + uv : array_like + Output vector of u-components of detected point motions. """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -475,6 +462,10 @@ def track_features(prvs, next, p0, params, verbose=False): "routine but it is not installed" ) + prvs = np.copy(prvs_image) + next = np.copy(next_image) + p0 = np.copy(points) + # scale between 0 and 255 prvs = (prvs - prvs.min()) / (prvs.max() - prvs.min()) * 255 next = (next - next.min()) / (next.max() - next.min()) * 255 @@ -494,17 +485,15 @@ def track_features(prvs, next, p0, params, verbose=False): p0 = p0[st, :] # extract vectors - x = p0[:, 0] - y = p0[:, 1] - u = np.array((p1 - p0)[:, 0]) - v = np.array((p1 - p0)[:, 1]) + xy = p0 + uv = p1 - p0 else: - x = y = u = v = None + xy = uv = None if verbose: - print("--- %i sparse vectors found ---" % x.size) + print("--- %i sparse vectors found ---" % xy.shape[0]) - return x, y, u, v + return xy, uv def morph_opening(input_image, n=3, thr=0): @@ -791,8 +780,9 @@ def decluster_data(input_array, coord, scale, min_samples, verbose=False): if npoints >= min_samples: dinput.append(np.median(input_array[idx, :], axis=0)) dcoord.append(np.median(coord[idx, :], axis=0)) - dinput = np.stack(dinput).squeeze() - dcoord = np.stack(dcoord) + if len(dinput) > 0: + dinput = np.stack(dinput).squeeze() + dcoord = np.stack(dcoord) if verbose: print("--- %i samples left after declustering ---" % dinput.shape[0]) From 88d1e02e0d3dde80cd354e398a900e57f2e17afe Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 12 Aug 2019 15:51:51 +0200 Subject: [PATCH 28/54] Use numpy.empty instead of lists --- pysteps/motion/lucaskanade.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 118757bca..3ce7879f1 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -248,8 +248,9 @@ def dense_lucaskanade(input_images, **kwargs): nr_fields = input_images.shape[0] domain_size = (input_images.shape[1], input_images.shape[2]) - xy_stack = [] - uv_stack = [] + + xy = np.empty(shape=(0, 2)) + uv = np.empty(shape=(0, 2)) for n in range(nr_fields - 1): # extract consecutive images @@ -295,28 +296,24 @@ def dense_lucaskanade(input_images, **kwargs): maxLevel=nr_levels_LK, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), ) - xy, uv = track_features(prvs, next, p0, lk_params, False) + xy_, uv_ = track_features(prvs, next, p0, lk_params, False) # skip loop if no vectors - if xy is None: + if xy_ is None: continue # stack vectors - xy_stack.append(xy) - uv_stack.append(uv) + xy = np.append(xy, xy_, axis=0) + uv = np.append(uv, uv_, axis=0) # return zero motion field is no sparse vectors are found - if len(xy_stack) == 0: + if xy.shape[0] == 0: if dense: return np.zeros((2, domain_size[0], domain_size[1])) else: - return np.array([]), np.array([]) - - # convert lists of arrays into single arrays - xy = np.concatenate(xy_stack) - uv = np.concatenate(uv_stack) + return xy, uv - # detect outlier vectors + # detect and remove outliers outliers = detect_outliers(uv, nr_std_outlier, xy, k_outlier, verbose) xy = xy[~outliers, :] uv = uv[~outliers, :] From e9904319237a332ae5fba8558454698bfc76f6ac Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 12 Aug 2019 16:08:20 +0200 Subject: [PATCH 29/54] Use numpy.empty instead of lists (part 2) --- pysteps/motion/lucaskanade.py | 54 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 3ce7879f1..52f4bfba0 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -327,7 +327,7 @@ def dense_lucaskanade(input_images, **kwargs): # decluster sparse motion vectors if decl_scale > 1: - uv, xy = decluster_data(uv, xy, decl_scale, min_decl_samples, verbose) + xy, uv = decluster_data(xy, uv, decl_scale, min_decl_samples, verbose) # return zero motion field if no sparse vectors are left for interpolation if xy.shape[0] == 0: @@ -337,8 +337,8 @@ def dense_lucaskanade(input_images, **kwargs): xgrid = np.arange(domain_size[1]) ygrid = np.arange(domain_size[0]) UV = rbfinterp2d( - uv, xy, + uv, xgrid, ygrid, rbfunction=rbfunction, @@ -651,7 +651,7 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): tree = scipy.spatial.cKDTree(coord) __, inds = tree.query(coord, k=k) - outliers = [] + outliers = np.empty(shape=0, dtype=bool) for i in range(inds.shape[0]): if nvar == 1: @@ -663,7 +663,7 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): thiszdata = (thisdata - np.mean(neighbours)) / np.std( neighbours ) - outliers.append(thiszdata > thr) + outliers = np.append(outliers, thiszdata > thr) else: @@ -679,9 +679,7 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): MD = np.sqrt(np.dot(np.dot(thiszdata, VI), thiszdata.T)) except np.linalg.LinAlgError: MD = 0 - outliers.append(MD > thr) - - outliers = np.array(outliers) + outliers = np.append(outliers, MD > thr) if verbose: print("--- %i outliers detected ---" % np.sum(outliers)) @@ -689,21 +687,21 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): return outliers -def decluster_data(input_array, coord, scale, min_samples, verbose=False): +def decluster_data(coord, input_array, scale, min_samples, verbose=False): """Decluster a data set by aggregating (median value) over a coarse grid. Parameters ---------- + coord : array_like + Array of shape (n, 2) containing the coordinates of the input data into + a 2-dimensional space. + input_array : array_like Array of shape (n) or (n, m), where n is the number of samples and m the number of variables. All values in input_array are required to have finite values. - coord : array_like - Array of shape (n, 2) containing the coordinates of the input data into - a 2-dimensional space. - scale : float or array_like The scale parameter in the same units of coord. Data points within this declustering scale are averaged together. @@ -725,8 +723,8 @@ def decluster_data(input_array, coord, scale, min_samples, verbose=False): """ - input_array = np.copy(input_array) coord = np.copy(coord) + input_array = np.copy(input_array) scale = np.float(scale) # check inputs @@ -735,12 +733,13 @@ def decluster_data(input_array, coord, scale, min_samples, verbose=False): if input_array.ndim == 1: nvar = 1 + input_array = input_array[:, None] elif input_array.ndim == 2: nvar = input_array.shape[1] else: raise ValueError( "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" - % coord.ndim + % input_array.ndim ) if coord.ndim != 2: @@ -767,29 +766,30 @@ def decluster_data(input_array, coord, scale, min_samples, verbose=False): # loop through these unique values and average vectors which belong to # the same declustering grid cell - dinput = [] - dcoord = [] + dinput = np.empty(shape=(0, nvar)) + dcoord = np.empty(shape=(0, 2)) for i in range(ucoord_.shape[0]): idx = np.logical_and( coord_[:, 0] == ucoord_[i, 0], coord_[:, 1] == ucoord_[i, 1] ) npoints = np.sum(idx) if npoints >= min_samples: - dinput.append(np.median(input_array[idx, :], axis=0)) - dcoord.append(np.median(coord[idx, :], axis=0)) - if len(dinput) > 0: - dinput = np.stack(dinput).squeeze() - dcoord = np.stack(dcoord) + dinput = np.append( + dinput, np.median(input_array[idx, :], axis=0)[None, :], axis=0 + ) + dcoord = np.append( + dcoord, np.median(coord[idx, :], axis=0)[None, :], axis=0 + ) if verbose: print("--- %i samples left after declustering ---" % dinput.shape[0]) - return dinput, dcoord + return dcoord.squeeze(), dinput def rbfinterp2d( - input_array, coord, + input_array, xgrid, ygrid, rbfunction="gaussian", @@ -802,15 +802,15 @@ def rbfinterp2d( Parameters ---------- + coord : array_like + Array of shape (n, 2) containing the coordinates of the data points into + a 2-dimensional space. + input_array : array_like Array of shape (n) or (n, m), where n is the number of data points and m the number of co-located variables. All values in input_array are required to have finite values. - coord : array_like - Array of shape (n, 2) containing the coordinates of the data points into - a 2-dimensional space. - xgrid, ygrid : array_like 1D arrays representing the coordinates of the target grid. From 3585c4f5c73f104a779e3973d013987d8982951e Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 12 Aug 2019 18:00:28 +0200 Subject: [PATCH 30/54] Improve usage of MaskedArray --- pysteps/motion/lucaskanade.py | 166 +++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 73 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 52f4bfba0..158994264 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -58,9 +58,6 @@ def dense_lucaskanade(input_images, **kwargs): .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ maskedarray.baseclass.html#numpy.ma.MaskedArray - .. _ndarray:\ - https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html - .. _Shi-Tomasi: https://docs.opencv.org/3.4.1/dd/d1a/group__\ imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 @@ -71,13 +68,13 @@ def dense_lucaskanade(input_images, **kwargs): Parameters ---------- - input_images : ndarray_ or MaskedArray_ + input_images : array_like or MaskedArray_ Array of shape (T, m, n) containing a sequence of T two-dimensional input images of shape (m, n). T = 2 is the minimum required number of images. With T > 2, the sparse vectors detected by Lucas-Kanade are pooled together prior to the final interpolation. - In case of an ndarray_, invalid values (Nans or infs) are masked. + In case of an array_like, invalid values (Nans or infs) are masked. The mask in the MaskedArray_ defines a region where velocity vectors are not computed. @@ -180,14 +177,17 @@ def dense_lucaskanade(input_images, **kwargs): Returns ------- - out : ndarray_ + + out : array_like or tuple If dense=True (the default), it returns the three-dimensional array (2,m,n) containing the dense x- and y-components of the motion field in units of pixels / timestep as given by the input array input_images. - If dense=False, it returns a tuple containing the one-dimensional arrays - x, y, u, v, where x, y define the vector locations, u, v define the x + + If dense=False, it returns a tuple containing the 2-dimensional arrays + xy and uv, where x, y define the vector locations, u, v define the x and y direction components of the vectors. - Return an empty array when no motion vectors are found. + + Return a zero motion field when no motion is detected. References ---------- @@ -199,7 +199,6 @@ def dense_lucaskanade(input_images, **kwargs): Lucas, B. D. and Kanade, T.: An iterative image registration technique with an application to stereo vision, in: Proceedings of the 1981 DARPA Imaging Understanding Workshop, pp. 121–130, 1981. - """ input_images = input_images.copy() @@ -236,16 +235,6 @@ def dense_lucaskanade(input_images, **kwargs): print("Computing the motion field with the Lucas-Kanade method.") t0 = time.time() - # Get mask - if isinstance(input_images, MaskedArray): - mask = np.ma.getmaskarray(input_images).copy() - else: - input_images = np.ma.masked_invalid(input_images) - mask = np.ma.getmaskarray(input_images).copy() - input_images[mask] = np.nanmin( - input_images - ) # Remove any Nan from the raw data - nr_fields = input_images.shape[0] domain_size = (input_images.shape[1], input_images.shape[2]) @@ -256,38 +245,31 @@ def dense_lucaskanade(input_images, **kwargs): # extract consecutive images prvs = input_images[n, :, :].copy() next = input_images[n + 1, :, :].copy() - mask_ = mask[n, :, :].copy() - # skip loop if no precip - if ~np.any(prvs > prvs.min()) or ~np.any(next > next.min()): - continue + if ~isinstance(prvs, MaskedArray): + prvs = np.ma.masked_invalid(prvs) + np.ma.set_fill_value(prvs, prvs.min()) - # buffer the quality mask to ensure that no vectors are computed nearby - # the edges of the radar mask - if buffer_mask > 0: - mask_ = cv2.dilate( - mask_.astype("uint8"), - np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), - 1, - ) + if ~isinstance(next, MaskedArray): + next = np.ma.masked_invalid(next) + np.ma.set_fill_value(next, next.min()) # remove small noise with a morphological operator (opening) if size_opening > 0: - prvs = morph_opening(prvs, n=size_opening) - next = morph_opening(next, n=size_opening) + prvs = morph_opening(prvs, prvs.min(), size_opening) + next = morph_opening(next, next.min(), size_opening) - # Find good features to track - mask_ = (-1 * mask_ + 1).astype("uint8") + # find good features to track gf_params = dict( maxCorners=max_corners_ST, qualityLevel=quality_level_ST, minDistance=min_distance_ST, blockSize=block_size_ST, ) - p0 = features_to_track(prvs, mask_, gf_params, False) + points = features_to_track(prvs, gf_params, buffer_mask, False) # skip loop if no features to track - if p0 is None: + if points.shape[0] == 0: continue # get sparse u, v vectors with Lucas-Kanade tracking @@ -296,10 +278,10 @@ def dense_lucaskanade(input_images, **kwargs): maxLevel=nr_levels_LK, criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), ) - xy_, uv_ = track_features(prvs, next, p0, lk_params, False) + xy_, uv_ = track_features(prvs, next, points, lk_params, False) # skip loop if no vectors - if xy_ is None: + if xy_.shape[0] == 0: continue # stack vectors @@ -353,7 +335,7 @@ def dense_lucaskanade(input_images, **kwargs): return UV -def features_to_track(input_image, mask, params, verbose=False): +def features_to_track(input_image, params, buffer_mask=0, verbose=False): """ Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect strong corners on an image. @@ -364,26 +346,31 @@ def features_to_track(input_image, mask, params, verbose=False): Parameters ---------- - input_image : array_like + input_image : array_like or MaskedArray_ Array of shape (m, n) containing the input image. - All values in input_image are required to have finite values. + In case of an array_like, invalid values (Nans or infs) define the mask + and the fill value is taken as the minimum of all valid pixels. - mask : array_like - Array of shape (m,n). It specifies the image region in which the corners - can be detected. + The mask defines a region where velocity vectors are not computed. params : dict Any additional parameter to the original routine as described in the corresponding documentation. + buffer_mask : int, optional + A mask buffer width in pixels. This extends the input mask (if any) + to help avoiding the erroneous interpretation of velocities near the + maximum range of the radars (0 by default). + verbose : bool, optional Print the number of features detected. Returns ------- - p0 : list - Output vector of detected corners. + points : array_like + Array of shape (p, 2) indicating the pixel coordinates of p detected + corners. """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -396,23 +383,43 @@ def features_to_track(input_image, mask, params, verbose=False): if input_image.ndim != 2: raise ValueError("input_image must be a two-dimensional array") + # masked array + if ~isinstance(input_image, MaskedArray): + input_image = np.ma.masked_invalid(input_image) + np.ma.set_fill_value(input_image, input_image.min()) + + # buffer the quality mask to ensure that no vectors are computed nearby + # the edges of the radar mask + mask = np.ma.getmaskarray(input_image).astype("uint8") + if buffer_mask > 0: + mask = cv2.dilate( + mask, + np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), + 1, + ) + input_image[mask] = np.ma.masked + # scale image between 0 and 255 input_image = ( - (input_image - input_image.min()) + (input_image.filled() - input_image.min()) / (input_image.max() - input_image.min()) * 255 ) # convert to 8-bit input_image = np.ndarray.astype(input_image, "uint8") - mask = np.ndarray.astype(mask, "uint8") + mask = (-1 * mask + 1).astype("uint8") - p0 = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) + points = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) + if points is None: + points = np.empty(shape=(0,2)) + else: + points = p0.squeeze() if verbose: - print("--- %i good features to track detected ---" % len(p0)) + print("--- %i good features to track detected ---" % points.shape[0]) - return p0.squeeze() + return points def track_features(prvs_image, next_image, points, params, verbose=False): @@ -425,17 +432,17 @@ def track_features(prvs_image, next_image, points, params, verbose=False): Parameters ---------- - prvs_image : array_like - Array of shape (m, n) containing the initial image. - All values in prvs_image are required to have finite values. + prvs_image : array_like or MaskedArray_ + Array of shape (m, n) containing the first image. + Invalid values (Nans or infs) are filled using the min value. - next_image : array_like + next_image : array_like or MaskedArray_ Array of shape (m, n) containing the successive image. - All values in next_image are required to have finite values. + Invalid values (Nans or infs) are filled using the min value. - points : list - Vector of 2D points for which the flow needs to be found. - Point coordinates must be single-precision floating-point numbers. + points : array_like + Array of shape (p, 2) indicating the (i, j) pixel coordinates of the + tracking points. params : dict Any additional parameter to the original routine as described in the @@ -463,9 +470,17 @@ def track_features(prvs_image, next_image, points, params, verbose=False): next = np.copy(next_image) p0 = np.copy(points) + if ~isinstance(prvs, MaskedArray): + prvs = np.ma.masked_invalid(prvs) + np.ma.set_fill_value(prvs, prvs.min()) + + if ~isinstance(next, MaskedArray): + next = np.ma.masked_invalid(next) + np.ma.set_fill_value(next, next.min()) + # scale between 0 and 255 - prvs = (prvs - prvs.min()) / (prvs.max() - prvs.min()) * 255 - next = (next - next.min()) / (next.max() - next.min()) * 255 + prvs = (prvs.filled() - prvs.min()) / (prvs.max() - prvs.min()) * 255 + next = (next.filled() - next.min()) / (next.max() - next.min()) * 255 # convert to 8-bit prvs = np.ndarray.astype(prvs, "uint8") @@ -484,8 +499,9 @@ def track_features(prvs_image, next_image, points, params, verbose=False): # extract vectors xy = p0 uv = p1 - p0 + else: - xy = uv = None + xy = uv = np.empty(shape=(0, 2)) if verbose: print("--- %i sparse vectors found ---" % xy.shape[0]) @@ -493,24 +509,27 @@ def track_features(prvs_image, next_image, points, params, verbose=False): return xy, uv -def morph_opening(input_image, n=3, thr=0): +def morph_opening(input_image, thr, n): """Filter out small scale noise on the image by applying a binary morphological opening (i.e., erosion then dilation). Parameters ---------- - input_image : array-like + + input_image : array_like Array of shape (m, n) containing the input image. - n : int - The structuring element size [pixels]. + thr : float The threshold used to convert the image into a binary image. + n : int + The structuring element size [pixels]. + Returns ------- - input_image : array - Array of shape (m,n) containing the resulting image + input_image : array_like + Array of shape (m,n) containing the resulting image """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -797,7 +816,8 @@ def rbfinterp2d( k=50, nchunks=5, ): - """Fast interpolation of a (multivariate) array over a 2D grid. + """Fast kernel interpolation of a (multivariate) array over a 2D grid using + radial basis functions. Parameters ---------- @@ -817,7 +837,7 @@ def rbfinterp2d( rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse multiquadric", "bump"}, optional The name of one of the available radial basis function based on the Euclidian - norm. See section "Notes" below. + norm. See also the Notes section below. epsilon : float, optional The shape parameter > 0 used to scale the input to the radial kernel. @@ -903,7 +923,7 @@ def rbfinterp2d( + str(_rbfunctions) ) from None - # generate the final grid + # generate the target grid X, Y = np.meshgrid(xgrid, ygrid) grid = np.column_stack((X.ravel(), Y.ravel())) grid = (grid - mcoord) / madcoord From 6d98028c1f3933a0f22f73898dd82bf0c0e500b0 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 12 Aug 2019 18:08:05 +0200 Subject: [PATCH 31/54] Add interpolate module --- pysteps/motion/lucaskanade.py | 202 +----------------------- pysteps/postprocessing/interpolate.py | 211 ++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 198 deletions(-) create mode 100644 pysteps/postprocessing/interpolate.py diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 158994264..d3fc0f8fe 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -1,5 +1,5 @@ +# -*- coding: utf-8 -*- """ - pysteps.motion.lucaskanade ========================== @@ -25,7 +25,6 @@ morph_opening detect_outliers decluster_data - rbfinterp2d """ import numpy as np @@ -34,6 +33,8 @@ from pysteps.decorators import check_input_frames from pysteps.exceptions import MissingOptionalDependency +from pysteps.postprocessing.interpolate import rbfinterp2d + try: import cv2 @@ -414,7 +415,7 @@ def features_to_track(input_image, params, buffer_mask=0, verbose=False): if points is None: points = np.empty(shape=(0,2)) else: - points = p0.squeeze() + points = points.squeeze() if verbose: print("--- %i good features to track detected ---" % points.shape[0]) @@ -805,198 +806,3 @@ def decluster_data(coord, input_array, scale, min_samples, verbose=False): return dcoord.squeeze(), dinput - -def rbfinterp2d( - coord, - input_array, - xgrid, - ygrid, - rbfunction="gaussian", - epsilon=1, - k=50, - nchunks=5, -): - """Fast kernel interpolation of a (multivariate) array over a 2D grid using - radial basis functions. - - Parameters - ---------- - - coord : array_like - Array of shape (n, 2) containing the coordinates of the data points into - a 2-dimensional space. - - input_array : array_like - Array of shape (n) or (n, m), where n is the number of data points and - m the number of co-located variables. - All values in input_array are required to have finite values. - - xgrid, ygrid : array_like - 1D arrays representing the coordinates of the target grid. - - rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse - multiquadric", "bump"}, optional - The name of one of the available radial basis function based on the Euclidian - norm. See also the Notes section below. - - epsilon : float, optional - The shape parameter > 0 used to scale the input to the radial kernel. - - k : int or None, optional - The number of nearest neighbours used to speed-up the interpolation. - If set to None, it interpolates based on all the data points. - - nchunks : int, optional - The number of chunks in which the grid points are split to limit the - memory usage during the interpolation. - - Returns - ------- - - output_array : array_like - The interpolated field(s) having shape (m, ygrid.size, xgrid.size). - - Notes - ----- - - The input coordinates are normalized before computing the euclidean norms: - - x = (x - median(x)) / MAD / 1.4826 - - where MAD = median(|x - median(x)|). - - The definitions of the radial basis functions are taken from the following - wikipedia page: https://en.wikipedia.org/wiki/Radial_basis_function - """ - - _rbfunctions = [ - "nearest", - "gaussian", - "inverse quadratic", - "inverse multiquadric", - "bump", - ] - - input_array = np.copy(input_array) - - if np.any(~np.isfinite(input_array)): - raise ValueError("input_array contains non-finite values") - - if input_array.ndim == 1: - nvar = 1 - input_array = input_array[:, None] - - elif input_array.ndim == 2: - nvar = input_array.shape[1] - - else: - raise ValueError( - "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" - % input_array.ndim - ) - - npoints = input_array.shape[0] - - coord = np.copy(coord) - - if coord.ndim != 2: - raise ValueError( - "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim - ) - - if npoints != coord.shape[0]: - raise ValueError( - "the number of samples in the input_array does not match the " - + "number of coordinates %i!=%i" % (npoints, coord.shape[0]) - ) - - # normalize coordinates - mcoord = np.median(coord, axis=0) - madcoord = 1.4826 * np.median(np.abs(coord - mcoord), axis=0) - coord = (coord - mcoord) / madcoord - - rbfunction = rbfunction.lower() - if rbfunction not in _rbfunctions: - raise ValueError( - "Unknown rbfunction '{}'\n".format(rbfunction) - + "The available rbfunctions are: " - + str(_rbfunctions) - ) from None - - # generate the target grid - X, Y = np.meshgrid(xgrid, ygrid) - grid = np.column_stack((X.ravel(), Y.ravel())) - grid = (grid - mcoord) / madcoord - - # k-nearest interpolation - if k is not None and k > 0: - k = int(np.min((k, npoints))) - - # create cKDTree object to represent source grid - tree = scipy.spatial.cKDTree(coord) - - else: - k = 0 - - # split grid points in n chunks - if nchunks > 1: - subgrids = np.array_split(grid, nchunks, 0) - subgrids = [x for x in subgrids if x.size > 0] - - else: - subgrids = [grid] - - # loop subgrids - i0 = 0 - output_array = np.zeros((grid.shape[0], nvar)) - for i, subgrid in enumerate(subgrids): - idelta = subgrid.shape[0] - - if k == 0: - # use all points - d = scipy.spatial.distance.cdist( - coord, subgrid, "euclidean" - ).transpose() - inds = np.arange(npoints)[None, :] * np.ones( - (subgrid.shape[0], npoints) - ).astype(int) - - else: - # use k-nearest neighbours - d, inds = tree.query(subgrid, k=k) - - if k == 1: - # nearest neighbour - output_array[i0 : (i0 + idelta), :] = input_array[inds, :] - - else: - - # the interpolation weights - if rbfunction == "gaussian": - w = np.exp(-(d * epsilon) ** 2) - - elif rbfunction == "inverse quadratic": - w = 1.0 / (1 + (epsilon * d) ** 2) - - elif rbfunction == "inverse multiquadric": - w = 1.0 / np.sqrt(1 + (epsilon * d) ** 2) - - elif rbfunction == "bump": - w = np.exp(-1.0 / (1 - (epsilon * d) ** 2)) - w[d >= 1 / epsilon] = 0.0 - - if not np.all(np.sum(w, axis=1)): - w[np.sum(w, axis=1) == 0, :] = 1.0 - - # interpolate - for j in range(nvar): - output_array[i0 : (i0 + idelta), j] = np.sum( - w * input_array[inds, j], axis=1 - ) / np.sum(w, axis=1) - - i0 += idelta - - # reshape to final grid size - output_array = output_array.reshape(ygrid.size, xgrid.size, nvar) - - return np.moveaxis(output_array, -1, 0).squeeze() diff --git a/pysteps/postprocessing/interpolate.py b/pysteps/postprocessing/interpolate.py new file mode 100644 index 000000000..54878d973 --- /dev/null +++ b/pysteps/postprocessing/interpolate.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +""" +pysteps.postprocessing.interpolate +================================== + +Interpolation routines for pysteps. + +.. autosummary:: + :toctree: ../generated/ + + rbfinterp2d + +""" + +import numpy as np +import scipy.spatial + +def rbfinterp2d( + coord, + input_array, + xgrid, + ygrid, + rbfunction="gaussian", + epsilon=1, + k=50, + nchunks=5, +): + """Fast kernel interpolation of a (multivariate) array over a 2D grid using + a radial basis function. + + Parameters + ---------- + + coord : array_like + Array of shape (n, 2) containing the coordinates of the data points into + a 2-dimensional space. + + input_array : array_like + Array of shape (n) or (n, m), where n is the number of data points and + m the number of co-located variables. + All values in input_array are required to have finite values. + + xgrid, ygrid : array_like + 1D arrays representing the coordinates of the target grid. + + rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse + multiquadric", "bump"}, optional + The name of one of the available radial basis function based on the Euclidian + norm. See also the Notes section below. + + epsilon : float, optional + The shape parameter > 0 used to scale the input to the radial kernel. + + k : int or None, optional + The number of nearest neighbours used to speed-up the interpolation. + If set to None, it interpolates based on all the data points. + + nchunks : int, optional + The number of chunks in which the grid points are split to limit the + memory usage during the interpolation. + + Returns + ------- + + output_array : array_like + The interpolated field(s) having shape (m, ygrid.size, xgrid.size). + + Notes + ----- + + The input coordinates are normalized before computing the euclidean norms: + + x = (x - median(x)) / MAD / 1.4826 + + where MAD = median(|x - median(x)|). + + The definitions of the radial basis functions are taken from the following + wikipedia page: https://en.wikipedia.org/wiki/Radial_basis_function + """ + + _rbfunctions = [ + "nearest", + "gaussian", + "inverse quadratic", + "inverse multiquadric", + "bump", + ] + + input_array = np.copy(input_array) + + if np.any(~np.isfinite(input_array)): + raise ValueError("input_array contains non-finite values") + + if input_array.ndim == 1: + nvar = 1 + input_array = input_array[:, None] + + elif input_array.ndim == 2: + nvar = input_array.shape[1] + + else: + raise ValueError( + "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" + % input_array.ndim + ) + + npoints = input_array.shape[0] + + coord = np.copy(coord) + + if coord.ndim != 2: + raise ValueError( + "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim + ) + + if npoints != coord.shape[0]: + raise ValueError( + "the number of samples in the input_array does not match the " + + "number of coordinates %i!=%i" % (npoints, coord.shape[0]) + ) + + # normalize coordinates + mcoord = np.median(coord, axis=0) + madcoord = 1.4826 * np.median(np.abs(coord - mcoord), axis=0) + coord = (coord - mcoord) / madcoord + + rbfunction = rbfunction.lower() + if rbfunction not in _rbfunctions: + raise ValueError( + "Unknown rbfunction '{}'\n".format(rbfunction) + + "The available rbfunctions are: " + + str(_rbfunctions) + ) from None + + # generate the target grid + X, Y = np.meshgrid(xgrid, ygrid) + grid = np.column_stack((X.ravel(), Y.ravel())) + grid = (grid - mcoord) / madcoord + + # k-nearest interpolation + if k is not None and k > 0: + k = int(np.min((k, npoints))) + + # create cKDTree object to represent source grid + tree = scipy.spatial.cKDTree(coord) + + else: + k = 0 + + # split grid points in n chunks + if nchunks > 1: + subgrids = np.array_split(grid, nchunks, 0) + subgrids = [x for x in subgrids if x.size > 0] + + else: + subgrids = [grid] + + # loop subgrids + i0 = 0 + output_array = np.zeros((grid.shape[0], nvar)) + for i, subgrid in enumerate(subgrids): + idelta = subgrid.shape[0] + + if k == 0: + # use all points + d = scipy.spatial.distance.cdist( + coord, subgrid, "euclidean" + ).transpose() + inds = np.arange(npoints)[None, :] * np.ones( + (subgrid.shape[0], npoints) + ).astype(int) + + else: + # use k-nearest neighbours + d, inds = tree.query(subgrid, k=k) + + if k == 1: + # nearest neighbour + output_array[i0 : (i0 + idelta), :] = input_array[inds, :] + + else: + + # the interpolation weights + if rbfunction == "gaussian": + w = np.exp(-(d * epsilon) ** 2) + + elif rbfunction == "inverse quadratic": + w = 1.0 / (1 + (epsilon * d) ** 2) + + elif rbfunction == "inverse multiquadric": + w = 1.0 / np.sqrt(1 + (epsilon * d) ** 2) + + elif rbfunction == "bump": + w = np.exp(-1.0 / (1 - (epsilon * d) ** 2)) + w[d >= 1 / epsilon] = 0.0 + + if not np.all(np.sum(w, axis=1)): + w[np.sum(w, axis=1) == 0, :] = 1.0 + + # interpolate + for j in range(nvar): + output_array[i0 : (i0 + idelta), j] = np.sum( + w * input_array[inds, j], axis=1 + ) / np.sum(w, axis=1) + + i0 += idelta + + # reshape to final grid size + output_array = output_array.reshape(ygrid.size, xgrid.size, nvar) + + return np.moveaxis(output_array, -1, 0).squeeze() \ No newline at end of file From d22db79f3b74a110e02dabac5239b7156c83aac6 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 12 Aug 2019 18:17:59 +0200 Subject: [PATCH 32/54] Fix interface --- examples/LK_buffer_mask.py | 49 +++++++++++++++++++++++++++-------- pysteps/motion/lucaskanade.py | 11 ++++---- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/examples/LK_buffer_mask.py b/examples/LK_buffer_mask.py index a6b38b35d..0a6cd5300 100644 --- a/examples/LK_buffer_mask.py +++ b/examples/LK_buffer_mask.py @@ -1,15 +1,16 @@ +# -*- coding: utf-8 -*- """ Handling of no-data in Lucas-Kanade =================================== Areas of missing data in radar images are typically caused by visibility limits -such as beam blockage and the radar coverage itself. These artifacts can mislead +such as beam blockage and the radar coverage itself. These artifacts can mislead the echo tracking algorithms. For instance, precipitation leaving the domain might be erroneously detected as having nearly stationary velocity. -This example shows how the Lucas-Kanade algorithm can be tuned to avoid the -erroneous interpretation of velocities near the maximum range of the radars by -buffering the no-data mask in the radar image in order to exclude all vectors +This example shows how the Lucas-Kanade algorithm can be tuned to avoid the +erroneous interpretation of velocities near the maximum range of the radars by +buffering the no-data mask in the radar image in order to exclude all vectors detected nearby no-data areas. """ @@ -71,7 +72,9 @@ mask[~np.isnan(ref_mm)] = np.nan # Log-transform the data [dBR] -R, metadata = transformation.dB_transform(R, metadata, threshold=0.1, zerovalue=-15.0) +R, metadata = transformation.dB_transform( + R, metadata, threshold=0.1, zerovalue=-15.0 +) # Keep the reference frame in dBR (for plotting purposes) ref_dbr = R[0].copy() @@ -109,10 +112,19 @@ R.data[R.mask] = np.nan # Use default settings (i.e., no buffering of the radar mask) -x, y, u, v = LK_optflow(R, dense=False, buffer_mask=0, quality_level_ST=0.1) +xy, uv = LK_optflow(R, dense=False, buffer_mask=0, quality_level_ST=0.1) plt.imshow(ref_dbr, cmap=plt.get_cmap("Greys")) plt.imshow(mask, cmap=colors.ListedColormap(["black"]), alpha=0.5) -plt.quiver(x, y, u, v, color="red", angles="xy", scale_units="xy", scale=0.2) +plt.quiver( + xy[:, 0], + xy[:, 1], + uv[:, 0], + uv[:, 1], + color="red", + angles="xy", + scale_units="xy", + scale=0.2, +) circle = plt.Circle((620, 245), 100, color="b", clip_on=False, fill=False) plt.gca().add_artist(circle) plt.title("buffer_mask = 0 (default)") @@ -129,10 +141,19 @@ # 'x,y,u,v = LK_optflow(.....)'. # with buffer -x, y, u, v = LK_optflow(R, dense=False, buffer_mask=20, quality_level_ST=0.2) +xy, uv = LK_optflow(R, dense=False, buffer_mask=20, quality_level_ST=0.2) plt.imshow(ref_dbr, cmap=plt.get_cmap("Greys")) plt.imshow(mask, cmap=colors.ListedColormap(["black"]), alpha=0.5) -plt.quiver(x, y, u, v, color="red", angles="xy", scale_units="xy", scale=0.2) +plt.quiver( + xy[:, 0], + xy[:, 1], + uv[:, 0], + uv[:, 1], + color="red", + angles="xy", + scale_units="xy", + scale=0.2, +) circle = plt.Circle((620, 245), 100, color="b", clip_on=False, fill=False) plt.gca().add_artist(circle) plt.title("buffer_mask = 20") @@ -154,7 +175,7 @@ V1 = np.sqrt(UV1[0] ** 2 + UV1[1] ** 2) V2 = np.sqrt(UV2[0] ** 2 + UV2[1] ** 2) -plt.imshow((V1 - V2) / V2, cmap=cm.RdBu_r, vmin=-0.1, vmax=0.1) +plt.imshow((V1 - V2) / V2, cmap=cm.RdBu_r, vmin=-0.5, vmax=0.5) plt.colorbar(fraction=0.04, pad=0.04) plt.title("Relative difference in motion speed") plt.show() @@ -184,7 +205,13 @@ # Find the veriyfing observations in the archive fns = io.archive.find_by_date( - date, root_path, path_fmt, fn_pattern, fn_ext, timestep=5, num_next_files=12 + date, + root_path, + path_fmt, + fn_pattern, + fn_ext, + timestep=5, + num_next_files=12, ) # Read and convert the radar composites diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index d3fc0f8fe..d875aaaa3 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -24,7 +24,7 @@ track_features morph_opening detect_outliers - decluster_data + decluster_sparse_data """ import numpy as np @@ -310,7 +310,7 @@ def dense_lucaskanade(input_images, **kwargs): # decluster sparse motion vectors if decl_scale > 1: - xy, uv = decluster_data(xy, uv, decl_scale, min_decl_samples, verbose) + xy, uv = decluster_sparse_data(xy, uv, decl_scale, min_decl_samples, verbose) # return zero motion field if no sparse vectors are left for interpolation if xy.shape[0] == 0: @@ -707,8 +707,9 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): return outliers -def decluster_data(coord, input_array, scale, min_samples, verbose=False): - """Decluster a data set by aggregating (median value) over a coarse grid. +def decluster_sparse_data(coord, input_array, scale, min_samples, verbose=False): + """Decluster a set of sparse data points by aggregating (i.e., taking the + median value) all points within a certain distance (i.e., a cluster). Parameters ---------- @@ -728,7 +729,7 @@ def decluster_data(coord, input_array, scale, min_samples, verbose=False): min_samples : int The minimum number of samples for computing the median within a given - declustering cell. + cluster. verbose : bool, optional Print out information. From 3d7800ae18c7c802425aa36e449f1f79aff72885 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 09:11:58 +0200 Subject: [PATCH 33/54] Move decluster method to interpolation module --- pysteps/motion/lucaskanade.py | 105 +------------------------- pysteps/postprocessing/interpolate.py | 101 +++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 103 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index d875aaaa3..da2b88707 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -24,7 +24,7 @@ track_features morph_opening detect_outliers - decluster_sparse_data + """ import numpy as np @@ -33,7 +33,7 @@ from pysteps.decorators import check_input_frames from pysteps.exceptions import MissingOptionalDependency -from pysteps.postprocessing.interpolate import rbfinterp2d +from pysteps.postprocessing.interpolate import decluster_sparse_data, rbfinterp2d try: import cv2 @@ -706,104 +706,3 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): return outliers - -def decluster_sparse_data(coord, input_array, scale, min_samples, verbose=False): - """Decluster a set of sparse data points by aggregating (i.e., taking the - median value) all points within a certain distance (i.e., a cluster). - - Parameters - ---------- - - coord : array_like - Array of shape (n, 2) containing the coordinates of the input data into - a 2-dimensional space. - - input_array : array_like - Array of shape (n) or (n, m), where n is the number of samples and m - the number of variables. - All values in input_array are required to have finite values. - - scale : float or array_like - The scale parameter in the same units of coord. Data points within this - declustering scale are averaged together. - - min_samples : int - The minimum number of samples for computing the median within a given - cluster. - - verbose : bool, optional - Print out information. - - Returns - ------- - - out : tuple of ndarrays - A two-element tuple (dinput, dcoord) containing the declustered input_array - (d, m) and coordinates (d, 2), where d is the new number of samples - (d < n). - - """ - - coord = np.copy(coord) - input_array = np.copy(input_array) - scale = np.float(scale) - - # check inputs - if np.any(~np.isfinite(input_array)): - raise ValueError("input_array contains non-finite values") - - if input_array.ndim == 1: - nvar = 1 - input_array = input_array[:, None] - elif input_array.ndim == 2: - nvar = input_array.shape[1] - else: - raise ValueError( - "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" - % input_array.ndim - ) - - if coord.ndim != 2: - raise ValueError( - "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim - ) - - if coord.shape[0] != input_array.shape[0]: - raise ValueError( - "the number of samples in the input_array does not match the " - + "number of coordinates %i!=%i" - % (input_array.shape[0], coord.shape[0]) - ) - - # reduce original coordinates - coord_ = np.floor(coord / scale) - - # keep only unique pairs of the reduced coordinates - coordb_ = np.ascontiguousarray(coord_).view( - np.dtype((np.void, coord_.dtype.itemsize * coord_.shape[1])) - ) - __, idx = np.unique(coordb_, return_index=True) - ucoord_ = coord_[idx] - - # loop through these unique values and average vectors which belong to - # the same declustering grid cell - dinput = np.empty(shape=(0, nvar)) - dcoord = np.empty(shape=(0, 2)) - for i in range(ucoord_.shape[0]): - idx = np.logical_and( - coord_[:, 0] == ucoord_[i, 0], coord_[:, 1] == ucoord_[i, 1] - ) - npoints = np.sum(idx) - if npoints >= min_samples: - dinput = np.append( - dinput, np.median(input_array[idx, :], axis=0)[None, :], axis=0 - ) - dcoord = np.append( - dcoord, np.median(coord[idx, :], axis=0)[None, :], axis=0 - ) - - if verbose: - print("--- %i samples left after declustering ---" % dinput.shape[0]) - - return dcoord.squeeze(), dinput - diff --git a/pysteps/postprocessing/interpolate.py b/pysteps/postprocessing/interpolate.py index 54878d973..0976e96e1 100644 --- a/pysteps/postprocessing/interpolate.py +++ b/pysteps/postprocessing/interpolate.py @@ -8,6 +8,7 @@ .. autosummary:: :toctree: ../generated/ + decluster_sparse_data rbfinterp2d """ @@ -15,6 +16,106 @@ import numpy as np import scipy.spatial +def decluster_sparse_data(coord, input_array, scale, min_samples, verbose=False): + """Decluster a set of sparse data points by aggregating (i.e., taking the + median value) all points within a certain distance (i.e., a cluster). + + Parameters + ---------- + + coord : array_like + Array of shape (n, 2) containing the coordinates of the input data into + a 2-dimensional space. + + input_array : array_like + Array of shape (n) or (n, m), where n is the number of samples and m + the number of variables. + All values in input_array are required to have finite values. + + scale : float or array_like + The scale parameter in the same units of coord. Data points within this + declustering scale are averaged together. + + min_samples : int + The minimum number of samples for computing the median within a given + cluster. + + verbose : bool, optional + Print out information. + + Returns + ------- + + out : tuple of ndarrays + A two-element tuple (dinput, dcoord) containing the declustered input_array + (d, m) and coordinates (d, 2), where d is the new number of samples + (d < n). + + """ + + coord = np.copy(coord) + input_array = np.copy(input_array) + scale = np.float(scale) + + # check inputs + if np.any(~np.isfinite(input_array)): + raise ValueError("input_array contains non-finite values") + + if input_array.ndim == 1: + nvar = 1 + input_array = input_array[:, None] + elif input_array.ndim == 2: + nvar = input_array.shape[1] + else: + raise ValueError( + "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" + % input_array.ndim + ) + + if coord.ndim != 2: + raise ValueError( + "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim + ) + + if coord.shape[0] != input_array.shape[0]: + raise ValueError( + "the number of samples in the input_array does not match the " + + "number of coordinates %i!=%i" + % (input_array.shape[0], coord.shape[0]) + ) + + # reduce original coordinates + coord_ = np.floor(coord / scale) + + # keep only unique pairs of the reduced coordinates + coordb_ = np.ascontiguousarray(coord_).view( + np.dtype((np.void, coord_.dtype.itemsize * coord_.shape[1])) + ) + __, idx = np.unique(coordb_, return_index=True) + ucoord_ = coord_[idx] + + # loop through these unique values and average vectors which belong to + # the same declustering grid cell + dinput = np.empty(shape=(0, nvar)) + dcoord = np.empty(shape=(0, 2)) + for i in range(ucoord_.shape[0]): + idx = np.logical_and( + coord_[:, 0] == ucoord_[i, 0], coord_[:, 1] == ucoord_[i, 1] + ) + npoints = np.sum(idx) + if npoints >= min_samples: + dinput = np.append( + dinput, np.median(input_array[idx, :], axis=0)[None, :], axis=0 + ) + dcoord = np.append( + dcoord, np.median(coord[idx, :], axis=0)[None, :], axis=0 + ) + + if verbose: + print("--- %i samples left after declustering ---" % dinput.shape[0]) + + return dcoord.squeeze(), dinput + def rbfinterp2d( coord, input_array, From 87bf732a19783c6c960be43cd67d4e5d4e3f12da Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 11:20:55 +0200 Subject: [PATCH 34/54] Improve docstrings --- pysteps/motion/lucaskanade.py | 63 ++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index da2b88707..2e408f8f9 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -24,7 +24,7 @@ track_features morph_opening detect_outliers - + """ import numpy as np @@ -33,7 +33,10 @@ from pysteps.decorators import check_input_frames from pysteps.exceptions import MissingOptionalDependency -from pysteps.postprocessing.interpolate import decluster_sparse_data, rbfinterp2d +from pysteps.postprocessing.interpolate import ( + decluster_sparse_data, + rbfinterp2d, +) try: import cv2 @@ -310,7 +313,9 @@ def dense_lucaskanade(input_images, **kwargs): # decluster sparse motion vectors if decl_scale > 1: - xy, uv = decluster_sparse_data(xy, uv, decl_scale, min_decl_samples, verbose) + xy, uv = decluster_sparse_data( + xy, uv, decl_scale, min_decl_samples, verbose + ) # return zero motion field if no sparse vectors are left for interpolation if xy.shape[0] == 0: @@ -341,22 +346,28 @@ def features_to_track(input_image, params, buffer_mask=0, verbose=False): Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect strong corners on an image. + Corners are used for local tracking methods. + .. _`goodFeaturesToTrack()`:\ https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 + .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ + maskedarray.baseclass.html#numpy.ma.MaskedArray + Parameters ---------- input_image : array_like or MaskedArray_ Array of shape (m, n) containing the input image. - In case of an array_like, invalid values (Nans or infs) define the mask - and the fill value is taken as the minimum of all valid pixels. - The mask defines a region where velocity vectors are not computed. + In case of an array_like, invalid values (Nans or infs) define a + validity mask, which represents the region where velocity vectors are not + computed. The corresponding fill value is taken as the minimum of all + valid pixels. params : dict Any additional parameter to the original routine as described in the - corresponding documentation. + `goodFeaturesToTrack()`_ documentation. buffer_mask : int, optional A mask buffer width in pixels. This extends the input mask (if any) @@ -372,6 +383,11 @@ def features_to_track(input_image, params, buffer_mask=0, verbose=False): points : array_like Array of shape (p, 2) indicating the pixel coordinates of p detected corners. + + See also + -------- + + pysteps.motion.lucaskanade.track_features """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -394,9 +410,7 @@ def features_to_track(input_image, params, buffer_mask=0, verbose=False): mask = np.ma.getmaskarray(input_image).astype("uint8") if buffer_mask > 0: mask = cv2.dilate( - mask, - np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), - 1, + mask, np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), 1 ) input_image[mask] = np.ma.masked @@ -413,7 +427,7 @@ def features_to_track(input_image, params, buffer_mask=0, verbose=False): points = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) if points is None: - points = np.empty(shape=(0,2)) + points = np.empty(shape=(0, 2)) else: points = points.squeeze() @@ -430,6 +444,9 @@ def track_features(prvs_image, next_image, points, params, verbose=False): .. _`calcOpticalFlowPyrLK()`:\ https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 + .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ + maskedarray.baseclass.html#numpy.ma.MaskedArray + Parameters ---------- @@ -442,12 +459,12 @@ def track_features(prvs_image, next_image, points, params, verbose=False): Invalid values (Nans or infs) are filled using the min value. points : array_like - Array of shape (p, 2) indicating the (i, j) pixel coordinates of the - tracking points. + Array of shape (p, 2) indicating the pixel coordinates of the + tracking points (corners). params : dict Any additional parameter to the original routine as described in the - corresponding documentation. + `calcOpticalFlowPyrLK()`_ documentation. verbose : bool, optional Print the number of vectors that have been found. @@ -456,10 +473,17 @@ def track_features(prvs_image, next_image, points, params, verbose=False): ------- xy : array_like - Output vector of x-coordinates of detected point motions. + Array of shape (d, 2) with the x- and y-coordinates of d <= p detected + sparse motion vectors. uv : array_like - Output vector of u-components of detected point motions. + Array of shape (d, 2) with the u- and v-components of d <= p detected + sparse motion vectors. + + See also + -------- + + pysteps.motion.lucaskanade.features_to_track """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -530,7 +554,7 @@ def morph_opening(input_image, thr, n): ------- input_image : array_like - Array of shape (m,n) containing the resulting image + Array of shape (m,n) containing the filtered image. """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -571,7 +595,7 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): input_array : array_like Array of shape (n) or (n, m), where n is the number of samples and m - the number of variables. If m > 1, it employs the Mahalanobis distance. + the number of variables. If m > 1, the Mahalanobis distance is used. All values in input_array are required to have finite values. thr : float @@ -594,7 +618,7 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): out : array_like A boolean array of the same shape as input_array, with True values - indicating the outliers detected in the input array. + indicating the outliers detected in input_array. """ input_array = np.copy(input_array) @@ -705,4 +729,3 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): print("--- %i outliers detected ---" % np.sum(outliers)) return outliers - From da9625bf486e75a12d074b09538baf76dc0ea705 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 11:25:08 +0200 Subject: [PATCH 35/54] Move interpolate to utils --- pysteps/motion/lucaskanade.py | 2 +- pysteps/{postprocessing => utils}/interpolate.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pysteps/{postprocessing => utils}/interpolate.py (100%) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 2e408f8f9..722531a2c 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -33,7 +33,7 @@ from pysteps.decorators import check_input_frames from pysteps.exceptions import MissingOptionalDependency -from pysteps.postprocessing.interpolate import ( +from pysteps.utils.interpolate import ( decluster_sparse_data, rbfinterp2d, ) diff --git a/pysteps/postprocessing/interpolate.py b/pysteps/utils/interpolate.py similarity index 100% rename from pysteps/postprocessing/interpolate.py rename to pysteps/utils/interpolate.py From 07743aba56de34dd4867ab7463eb6e2fc4ab1682 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 12:32:29 +0200 Subject: [PATCH 36/54] Reorganize modules --- pysteps/motion/lucaskanade.py | 316 +--------------------------------- pysteps/utils/__init__.py | 3 + pysteps/utils/cleansing.py | 268 ++++++++++++++++++++++++++++ pysteps/utils/images.py | 165 ++++++++++++++++++ pysteps/utils/interface.py | 213 ++++++++++++++--------- pysteps/utils/interpolate.py | 106 +----------- 6 files changed, 582 insertions(+), 489 deletions(-) create mode 100644 pysteps/utils/cleansing.py create mode 100644 pysteps/utils/images.py diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 722531a2c..6aae192da 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -22,8 +22,6 @@ dense_lucaskanade features_to_track track_features - morph_opening - detect_outliers """ @@ -33,10 +31,9 @@ from pysteps.decorators import check_input_frames from pysteps.exceptions import MissingOptionalDependency -from pysteps.utils.interpolate import ( - decluster_sparse_data, - rbfinterp2d, -) +from pysteps.utils.cleansing import decluster, detect_outliers +from pysteps.utils.interpolate import rbfinterp2d +from pysteps.utils.images import corner_detection, morph_opening try: import cv2 @@ -45,7 +42,6 @@ except ImportError: CV2_IMPORTED = False -import scipy.spatial import time import warnings @@ -270,7 +266,7 @@ def dense_lucaskanade(input_images, **kwargs): minDistance=min_distance_ST, blockSize=block_size_ST, ) - points = features_to_track(prvs, gf_params, buffer_mask, False) + points = corner_detection(prvs, gf_params, buffer_mask, False) # skip loop if no features to track if points.shape[0] == 0: @@ -313,9 +309,7 @@ def dense_lucaskanade(input_images, **kwargs): # decluster sparse motion vectors if decl_scale > 1: - xy, uv = decluster_sparse_data( - xy, uv, decl_scale, min_decl_samples, verbose - ) + xy, uv = decluster(xy, uv, decl_scale, min_decl_samples, verbose) # return zero motion field if no sparse vectors are left for interpolation if xy.shape[0] == 0: @@ -341,102 +335,6 @@ def dense_lucaskanade(input_images, **kwargs): return UV -def features_to_track(input_image, params, buffer_mask=0, verbose=False): - """ - Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect strong corners - on an image. - - Corners are used for local tracking methods. - - .. _`goodFeaturesToTrack()`:\ - https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 - - .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ - maskedarray.baseclass.html#numpy.ma.MaskedArray - - Parameters - ---------- - - input_image : array_like or MaskedArray_ - Array of shape (m, n) containing the input image. - - In case of an array_like, invalid values (Nans or infs) define a - validity mask, which represents the region where velocity vectors are not - computed. The corresponding fill value is taken as the minimum of all - valid pixels. - - params : dict - Any additional parameter to the original routine as described in the - `goodFeaturesToTrack()`_ documentation. - - buffer_mask : int, optional - A mask buffer width in pixels. This extends the input mask (if any) - to help avoiding the erroneous interpretation of velocities near the - maximum range of the radars (0 by default). - - verbose : bool, optional - Print the number of features detected. - - Returns - ------- - - points : array_like - Array of shape (p, 2) indicating the pixel coordinates of p detected - corners. - - See also - -------- - - pysteps.motion.lucaskanade.track_features - """ - if not CV2_IMPORTED: - raise MissingOptionalDependency( - "opencv package is required for the goodFeaturesToTrack() " - "routine but it is not installed" - ) - - input_image = np.copy(input_image) - - if input_image.ndim != 2: - raise ValueError("input_image must be a two-dimensional array") - - # masked array - if ~isinstance(input_image, MaskedArray): - input_image = np.ma.masked_invalid(input_image) - np.ma.set_fill_value(input_image, input_image.min()) - - # buffer the quality mask to ensure that no vectors are computed nearby - # the edges of the radar mask - mask = np.ma.getmaskarray(input_image).astype("uint8") - if buffer_mask > 0: - mask = cv2.dilate( - mask, np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), 1 - ) - input_image[mask] = np.ma.masked - - # scale image between 0 and 255 - input_image = ( - (input_image.filled() - input_image.min()) - / (input_image.max() - input_image.min()) - * 255 - ) - - # convert to 8-bit - input_image = np.ndarray.astype(input_image, "uint8") - mask = (-1 * mask + 1).astype("uint8") - - points = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) - if points is None: - points = np.empty(shape=(0, 2)) - else: - points = points.squeeze() - - if verbose: - print("--- %i good features to track detected ---" % points.shape[0]) - - return points - - def track_features(prvs_image, next_image, points, params, verbose=False): """ Interface to the OpenCV `calcOpticalFlowPyrLK()`_ features tracking algorithm. @@ -480,10 +378,11 @@ def track_features(prvs_image, next_image, points, params, verbose=False): Array of shape (d, 2) with the u- and v-components of d <= p detected sparse motion vectors. - See also - -------- + Notes + ----- - pysteps.motion.lucaskanade.features_to_track + The tracking points can be obtained with the pysteps.utils.images.corner_detection + routine. """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -532,200 +431,3 @@ def track_features(prvs_image, next_image, points, params, verbose=False): print("--- %i sparse vectors found ---" % xy.shape[0]) return xy, uv - - -def morph_opening(input_image, thr, n): - """Filter out small scale noise on the image by applying a binary morphological - opening (i.e., erosion then dilation). - - Parameters - ---------- - - input_image : array_like - Array of shape (m, n) containing the input image. - - thr : float - The threshold used to convert the image into a binary image. - - n : int - The structuring element size [pixels]. - - Returns - ------- - - input_image : array_like - Array of shape (m,n) containing the filtered image. - """ - if not CV2_IMPORTED: - raise MissingOptionalDependency( - "opencv package is required for the morphologyEx " - "routine but it is not installed" - ) - - # Convert to binary image - field_bin = np.ndarray.astype(input_image > thr, "uint8") - - # Build a structuring element of size n - kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (n, n)) - - # Apply morphological opening (i.e. erosion then dilation) - field_bin_out = cv2.morphologyEx(field_bin, cv2.MORPH_OPEN, kernel) - - # Build mask to be applied on the original image - mask = (field_bin - field_bin_out) > 0 - - # Filter out small isolated pixels based on mask - input_image[mask] = np.nanmin(input_image) - - return input_image - - -def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): - """Detect outliers in a (multivariate and georeferenced) dataset. - - Assume a (multivariate) Gaussian distribution and detect outliers based on - the number of standard deviations from the mean. - - If spatial information is provided through coordinates, the outlier - detection can be localized by considering only the k-nearest neighbours - when computing the local mean and standard deviation. - - Parameters - ---------- - - input_array : array_like - Array of shape (n) or (n, m), where n is the number of samples and m - the number of variables. If m > 1, the Mahalanobis distance is used. - All values in input_array are required to have finite values. - - thr : float - The number of standard deviations from the mean that defines an outlier. - - coord : array_like, optional - Array of shape (n, d) containing the coordinates of the input data into - a space of d dimensions. Setting coord requires that k is not None. - - k : int or None, optional - The number of nearest neighbours used to localize the outlier detection. - If set to None (the default), it employs all the data points (global - detection). Setting k requires that coord is not None. - - verbose : bool, optional - Print out information. - - Returns - ------- - - out : array_like - A boolean array of the same shape as input_array, with True values - indicating the outliers detected in input_array. - """ - - input_array = np.copy(input_array) - - if np.any(~np.isfinite(input_array)): - raise ValueError("input_array contains non-finite values") - - if input_array.ndim == 1: - nvar = 1 - elif input_array.ndim == 2: - nvar = input_array.shape[1] - else: - raise ValueError( - "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" - % coord.ndim - ) - - if coord is not None: - - coord = np.copy(coord) - if coord.ndim == 1: - coord = coord[:, None] - - elif coord.ndim > 2: - raise ValueError( - "coord must have 2 dimensions (n, d), but it has %i" - % coord.ndim - ) - - if coord.shape[0] != input_array.shape[0]: - raise ValueError( - "the number of samples in input_array does not match the " - + "number of coordinates %i!=%i" - % (input_array.shape[0], coord.shape[0]) - ) - - if k is None: - raise ValueError("coord is set but k is None") - - k = np.min((coord.shape[0], k + 1)) - - else: - if k is not None: - raise ValueError("k is set but coord=None") - - # global - - if k is None: - - if nvar == 1: - - # univariate - - zdata = (input_array - np.mean(input_array)) / np.std(input_array) - outliers = zdata > thr - - else: - - # multivariate (mahalanobis distance) - - zdata = input_array - np.mean(input_array, axis=0) - V = np.cov(zdata.T) - VI = np.linalg.inv(V) - try: - VI = np.linalg.inv(V) - MD = np.sqrt(np.dot(np.dot(zdata, VI), zdata.T).diagonal()) - except np.linalg.LinAlgError: - MD = np.zeros(input_array.shape) - outliers = MD > thr - - # local - - else: - - tree = scipy.spatial.cKDTree(coord) - __, inds = tree.query(coord, k=k) - outliers = np.empty(shape=0, dtype=bool) - for i in range(inds.shape[0]): - - if nvar == 1: - - # in terms of velocity - - thisdata = input_array[i] - neighbours = input_array[inds[i, 1:]] - thiszdata = (thisdata - np.mean(neighbours)) / np.std( - neighbours - ) - outliers = np.append(outliers, thiszdata > thr) - - else: - - # mahalanobis distance - - thisdata = input_array[i, :] - neighbours = input_array[inds[i, 1:], :].copy() - thiszdata = thisdata - np.mean(neighbours, axis=0) - neighbours = neighbours - np.mean(neighbours, axis=0) - V = np.cov(neighbours.T) - try: - VI = np.linalg.inv(V) - MD = np.sqrt(np.dot(np.dot(thiszdata, VI), thiszdata.T)) - except np.linalg.LinAlgError: - MD = 0 - outliers = np.append(outliers, MD > thr) - - if verbose: - print("--- %i outliers detected ---" % np.sum(outliers)) - - return outliers diff --git a/pysteps/utils/__init__.py b/pysteps/utils/__init__.py index 07752892d..283ad3883 100644 --- a/pysteps/utils/__init__.py +++ b/pysteps/utils/__init__.py @@ -1,9 +1,12 @@ """Miscellaneous utility functions.""" from .arrays import * +from .cleansing import * from .conversion import * from .dimension import * +from .images import * from .interface import get_method +from .interpolate import * from .fft import * from .spectral import * from .transformation import * \ No newline at end of file diff --git a/pysteps/utils/cleansing.py b/pysteps/utils/cleansing.py new file mode 100644 index 000000000..00fae1d5e --- /dev/null +++ b/pysteps/utils/cleansing.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +""" +pysteps.utils.cleansing +======================= + +Data cleansing routines for pysteps. + +.. autosummary:: + :toctree: ../generated/ + + decluster + detect_outliers +""" + +import numpy as np +import scipy.spatial + + +def decluster(coord, input_array, scale, min_samples, verbose=False): + """Decluster a set of sparse data points by aggregating (i.e., taking the + median value) all points within a certain distance (i.e., a cluster). + + Parameters + ---------- + + coord : array_like + Array of shape (n, 2) containing the coordinates of the input data into + a 2-dimensional space. + + input_array : array_like + Array of shape (n) or (n, m), where n is the number of samples and m + the number of variables. + All values in input_array are required to have finite values. + + scale : float or array_like + The scale parameter in the same units of coord. Data points within this + declustering scale are averaged together. + + min_samples : int + The minimum number of samples for computing the median within a given + cluster. + + verbose : bool, optional + Print out information. + + Returns + ------- + + out : tuple of ndarrays + A two-element tuple (dinput, dcoord) containing the declustered input_array + (d, m) and coordinates (d, 2), where d is the new number of samples + (d < n). + + """ + + coord = np.copy(coord) + input_array = np.copy(input_array) + scale = np.float(scale) + + # check inputs + if np.any(~np.isfinite(input_array)): + raise ValueError("input_array contains non-finite values") + + if input_array.ndim == 1: + nvar = 1 + input_array = input_array[:, None] + elif input_array.ndim == 2: + nvar = input_array.shape[1] + else: + raise ValueError( + "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" + % input_array.ndim + ) + + if coord.ndim != 2: + raise ValueError( + "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim + ) + + if coord.shape[0] != input_array.shape[0]: + raise ValueError( + "the number of samples in the input_array does not match the " + + "number of coordinates %i!=%i" + % (input_array.shape[0], coord.shape[0]) + ) + + # reduce original coordinates + coord_ = np.floor(coord / scale) + + # keep only unique pairs of the reduced coordinates + coordb_ = np.ascontiguousarray(coord_).view( + np.dtype((np.void, coord_.dtype.itemsize * coord_.shape[1])) + ) + __, idx = np.unique(coordb_, return_index=True) + ucoord_ = coord_[idx] + + # loop through these unique values and average vectors which belong to + # the same declustering grid cell + dinput = np.empty(shape=(0, nvar)) + dcoord = np.empty(shape=(0, 2)) + for i in range(ucoord_.shape[0]): + idx = np.logical_and( + coord_[:, 0] == ucoord_[i, 0], coord_[:, 1] == ucoord_[i, 1] + ) + npoints = np.sum(idx) + if npoints >= min_samples: + dinput = np.append( + dinput, np.median(input_array[idx, :], axis=0)[None, :], axis=0 + ) + dcoord = np.append( + dcoord, np.median(coord[idx, :], axis=0)[None, :], axis=0 + ) + + if verbose: + print("--- %i samples left after declustering ---" % dinput.shape[0]) + + return dcoord.squeeze(), dinput + + +def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): + """Detect outliers in a (multivariate and georeferenced) dataset. + + Assume a (multivariate) Gaussian distribution and detect outliers based on + the number of standard deviations from the mean. + + If spatial information is provided through coordinates, the outlier + detection can be localized by considering only the k-nearest neighbours + when computing the local mean and standard deviation. + + Parameters + ---------- + + input_array : array_like + Array of shape (n) or (n, m), where n is the number of samples and m + the number of variables. If m > 1, the Mahalanobis distance is used. + All values in input_array are required to have finite values. + + thr : float + The number of standard deviations from the mean that defines an outlier. + + coord : array_like, optional + Array of shape (n, d) containing the coordinates of the input data into + a space of d dimensions. Setting coord requires that k is not None. + + k : int or None, optional + The number of nearest neighbours used to localize the outlier detection. + If set to None (the default), it employs all the data points (global + detection). Setting k requires that coord is not None. + + verbose : bool, optional + Print out information. + + Returns + ------- + + out : array_like + A boolean array of the same shape as input_array, with True values + indicating the outliers detected in input_array. + """ + + input_array = np.copy(input_array) + + if np.any(~np.isfinite(input_array)): + raise ValueError("input_array contains non-finite values") + + if input_array.ndim == 1: + nvar = 1 + elif input_array.ndim == 2: + nvar = input_array.shape[1] + else: + raise ValueError( + "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" + % coord.ndim + ) + + if coord is not None: + + coord = np.copy(coord) + if coord.ndim == 1: + coord = coord[:, None] + + elif coord.ndim > 2: + raise ValueError( + "coord must have 2 dimensions (n, d), but it has %i" + % coord.ndim + ) + + if coord.shape[0] != input_array.shape[0]: + raise ValueError( + "the number of samples in input_array does not match the " + + "number of coordinates %i!=%i" + % (input_array.shape[0], coord.shape[0]) + ) + + if k is None: + raise ValueError("coord is set but k is None") + + k = np.min((coord.shape[0], k + 1)) + + else: + if k is not None: + raise ValueError("k is set but coord=None") + + # global + + if k is None: + + if nvar == 1: + + # univariate + + zdata = (input_array - np.mean(input_array)) / np.std(input_array) + outliers = zdata > thr + + else: + + # multivariate (mahalanobis distance) + + zdata = input_array - np.mean(input_array, axis=0) + V = np.cov(zdata.T) + VI = np.linalg.inv(V) + try: + VI = np.linalg.inv(V) + MD = np.sqrt(np.dot(np.dot(zdata, VI), zdata.T).diagonal()) + except np.linalg.LinAlgError: + MD = np.zeros(input_array.shape) + outliers = MD > thr + + # local + + else: + + tree = scipy.spatial.cKDTree(coord) + __, inds = tree.query(coord, k=k) + outliers = np.empty(shape=0, dtype=bool) + for i in range(inds.shape[0]): + + if nvar == 1: + + # in terms of velocity + + thisdata = input_array[i] + neighbours = input_array[inds[i, 1:]] + thiszdata = (thisdata - np.mean(neighbours)) / np.std( + neighbours + ) + outliers = np.append(outliers, thiszdata > thr) + + else: + + # mahalanobis distance + + thisdata = input_array[i, :] + neighbours = input_array[inds[i, 1:], :].copy() + thiszdata = thisdata - np.mean(neighbours, axis=0) + neighbours = neighbours - np.mean(neighbours, axis=0) + V = np.cov(neighbours.T) + try: + VI = np.linalg.inv(V) + MD = np.sqrt(np.dot(np.dot(thiszdata, VI), thiszdata.T)) + except np.linalg.LinAlgError: + MD = 0 + outliers = np.append(outliers, MD > thr) + + if verbose: + print("--- %i outliers detected ---" % np.sum(outliers)) + + return outliers diff --git a/pysteps/utils/images.py b/pysteps/utils/images.py new file mode 100644 index 000000000..84317614e --- /dev/null +++ b/pysteps/utils/images.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +""" +pysteps.utils.images +==================== + +Image processing routines for pysteps. + +.. autosummary:: + :toctree: ../generated/ + + corner_detection + morph_opening +""" + +import numpy as np +from numpy.ma.core import MaskedArray + +try: + import cv2 + + CV2_IMPORTED = True +except ImportError: + CV2_IMPORTED = False + + +def corner_detection(input_image, params, buffer_mask=0, verbose=False): + """ + Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect corners + on an image. + + Corners are used for local tracking methods. + + .. _`goodFeaturesToTrack()`:\ + https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 + + .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ + maskedarray.baseclass.html#numpy.ma.MaskedArray + + Parameters + ---------- + + input_image : array_like or MaskedArray_ + Array of shape (m, n) containing the input image. + + In case of an array_like, invalid values (Nans or infs) define a + validity mask, which represents the region where velocity vectors are not + computed. The corresponding fill value is taken as the minimum of all + valid pixels. + + params : dict + Any additional parameter to the original routine as described in the + `goodFeaturesToTrack()`_ documentation. + + buffer_mask : int, optional + A mask buffer width in pixels. This extends the input mask (if any) + to help avoiding the erroneous interpretation of velocities near the + maximum range of the radars (0 by default). + + verbose : bool, optional + Print the number of features detected. + + Returns + ------- + + points : array_like + Array of shape (p, 2) indicating the pixel coordinates of p detected + corners. + + See also + -------- + + pysteps.motion.lucaskanade.track_features + """ + if not CV2_IMPORTED: + raise MissingOptionalDependency( + "opencv package is required for the goodFeaturesToTrack() " + "routine but it is not installed" + ) + + input_image = np.copy(input_image) + + if input_image.ndim != 2: + raise ValueError("input_image must be a two-dimensional array") + + # masked array + if ~isinstance(input_image, MaskedArray): + input_image = np.ma.masked_invalid(input_image) + np.ma.set_fill_value(input_image, input_image.min()) + + # buffer the quality mask to ensure that no vectors are computed nearby + # the edges of the radar mask + mask = np.ma.getmaskarray(input_image).astype("uint8") + if buffer_mask > 0: + mask = cv2.dilate( + mask, np.ones((int(buffer_mask), int(buffer_mask)), np.uint8), 1 + ) + input_image[mask] = np.ma.masked + + # scale image between 0 and 255 + input_image = ( + (input_image.filled() - input_image.min()) + / (input_image.max() - input_image.min()) + * 255 + ) + + # convert to 8-bit + input_image = np.ndarray.astype(input_image, "uint8") + mask = (-1 * mask + 1).astype("uint8") + + points = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) + if points is None: + points = np.empty(shape=(0, 2)) + else: + points = points.squeeze() + + if verbose: + print("--- %i good features to track detected ---" % points.shape[0]) + + return points + + +def morph_opening(input_image, thr, n): + """Filter out small scale noise on the image by applying a binary morphological + opening (i.e., erosion then dilation). + + Parameters + ---------- + + input_image : array_like + Array of shape (m, n) containing the input image. + + thr : float + The threshold used to convert the image into a binary image. + + n : int + The structuring element size [pixels]. + + Returns + ------- + + input_image : array_like + Array of shape (m,n) containing the filtered image. + """ + if not CV2_IMPORTED: + raise MissingOptionalDependency( + "opencv package is required for the morphologyEx " + "routine but it is not installed" + ) + + # Convert to binary image + field_bin = np.ndarray.astype(input_image > thr, "uint8") + + # Build a structuring element of size n + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (n, n)) + + # Apply morphological opening (i.e. erosion then dilation) + field_bin_out = cv2.morphologyEx(field_bin, cv2.MORPH_OPEN, kernel) + + # Build mask to be applied on the original image + mask = (field_bin - field_bin_out) > 0 + + # Filter out small isolated pixels based on mask + input_image[mask] = np.nanmin(input_image) + + return input_image diff --git a/pysteps/utils/interface.py b/pysteps/utils/interface.py index 10e0ac726..19dc11f0c 100644 --- a/pysteps/utils/interface.py +++ b/pysteps/utils/interface.py @@ -11,11 +11,15 @@ """ from . import arrays +from . import cleansing from . import conversion -from . import transformation from . import dimension from . import fft +from . import images +from . import interpolate from . import spectral +from . import transformation + def get_method(name, **kwargs): """Return a callable function for the utility method corresponding to the @@ -23,66 +27,79 @@ def get_method(name, **kwargs): Arrays methods: - +-------------------+--------------------------------------------------------+ - | Name | Description | - +===================+========================================================+ - | centred_coord | compute a 2D coordinate array | - +-------------------+--------------------------------------------------------+ - - Conversion methods: + +-------------------+-----------------------------------------------------+ + | Name | Description | + +===================+=====================================================+ + | centred_coord | compute a 2D coordinate array | + +-------------------+-----------------------------------------------------+ + + Cleansing methods: - +-------------------+--------------------------------------------------------+ - | Name | Description | - +===================+========================================================+ - | mm/h or rainrate | convert to rain rate [mm/h] | - +-------------------+--------------------------------------------------------+ - | mm or raindepth | convert to rain depth [mm] | - +-------------------+--------------------------------------------------------+ - | dbz or | convert to reflectivity [dBZ] | - | reflectivity | | - +-------------------+--------------------------------------------------------+ + +-------------------+-----------------------------------------------------+ + | Name | Description | + +===================+=====================================================+ + | decluster | decluster a set of sparse data points | + +-------------------+-----------------------------------------------------+ + | detect_outliers | detect outliers in a dataset | + +-------------------+-----------------------------------------------------+ - Transformation methods: + Conversion methods: - +-------------------+--------------------------------------------------------+ - | Name | Description | - +===================+========================================================+ - | boxcox or box-cox | one-parameter Box-Cox transform | - +-------------------+--------------------------------------------------------+ - | db or decibel | transform to units of decibel | - +-------------------+--------------------------------------------------------+ - | log | log transform | - +-------------------+--------------------------------------------------------+ - | nqt | Normal Quantile Transform | - +-------------------+--------------------------------------------------------+ - | sqrt | square-root transform | - +-------------------+--------------------------------------------------------+ + +-------------------+-----------------------------------------------------+ + | Name | Description | + +===================+=====================================================+ + | mm/h or rainrate | convert to rain rate [mm/h] | + +-------------------+-----------------------------------------------------+ + | mm or raindepth | convert to rain depth [mm] | + +-------------------+-----------------------------------------------------+ + | dbz or | convert to reflectivity [dBZ] | + | reflectivity | | + +-------------------+-----------------------------------------------------+ Dimension methods: - +-------------------+--------------------------------------------------------+ - | Name | Description | - +===================+========================================================+ - | accumulate | aggregate fields in time | - +-------------------+--------------------------------------------------------+ - | clip | resize the field domain by geographical coordinates | - +-------------------+--------------------------------------------------------+ - | square | either pad or crop the data to get a square domain | - +-------------------+--------------------------------------------------------+ - | upscale | upscale the field | - +-------------------+--------------------------------------------------------+ + +-------------------+-----------------------------------------------------+ + | Name | Description | + +===================+=====================================================+ + | accumulate | aggregate fields in time | + +-------------------+-----------------------------------------------------+ + | clip | resize the field domain by geographical coordinates | + +-------------------+-----------------------------------------------------+ + | square | either pad or crop the data to get a square domain | + +-------------------+-----------------------------------------------------+ + | upscale | upscale the field | + +-------------------+-----------------------------------------------------+ FFT methods (wrappers to different implementations): - +-------------------+--------------------------------------------------------+ - | Name | Description | - +===================+========================================================+ - | numpy | numpy.fft | - +-------------------+--------------------------------------------------------+ - | scipy | scipy.fftpack | - +-------------------+--------------------------------------------------------+ - | pyfftw | pyfftw.interfaces.numpy_fft | - +-------------------+--------------------------------------------------------+ + +-------------------+-----------------------------------------------------+ + | Name | Description | + +===================+=====================================================+ + | numpy | numpy.fft | + +-------------------+-----------------------------------------------------+ + | scipy | scipy.fftpack | + +-------------------+-----------------------------------------------------+ + | pyfftw | pyfftw.interfaces.numpy_fft | + +-------------------+-----------------------------------------------------+ + + Image processing methods: + + +-------------------+-----------------------------------------------------+ + | Name | Description | + +===================+=====================================================+ + | corner_detection | detect corners on an image | + +-------------------+-----------------------------------------------------+ + | morph_opening | filter small scale noise on an image | + +-------------------+-----------------------------------------------------+ + + Interpolation methods: + + +-------------------+-----------------------------------------------------+ + | Name | Description | + +===================+=====================================================+ + | rbfinterp2d | fast kernel interpolation of a (multivariate) array | + | | over a 2D grid using a radial basis function | + +-------------------+-----------------------------------------------------+ Additional keyword arguments are passed to the initializer of the FFT methods, see utils.fft. @@ -97,6 +114,22 @@ def get_method(name, **kwargs): | rm_rdisc | remove the rain / no-rain discontinuity | +-------------------+-----------------------------------------------------+ + Transformation methods: + + +-------------------+-----------------------------------------------------+ + | Name | Description | + +===================+=====================================================+ + | boxcox or box-cox | one-parameter Box-Cox transform | + +-------------------+-----------------------------------------------------+ + | db or decibel | transform to units of decibel | + +-------------------+-----------------------------------------------------+ + | log | log transform | + +-------------------+-----------------------------------------------------+ + | nqt | Normal Quantile Transform | + +-------------------+-----------------------------------------------------+ + | sqrt | square-root transform | + +-------------------+-----------------------------------------------------+ + """ if name is None: @@ -107,30 +140,30 @@ def get_method(name, **kwargs): def donothing(R, metadata=None, *args, **kwargs): return R.copy(), {} if metadata is None else metadata.copy() - methods_objects = dict() - methods_objects["none"] = donothing + methods_objects = dict() + methods_objects["none"] = donothing + # arrays methods methods_objects["centred_coord"] = arrays.compute_centred_coord_array + # conversion methods - methods_objects["mm/h"] = conversion.to_rainrate - methods_objects["rainrate"] = conversion.to_rainrate - methods_objects["mm"] = conversion.to_raindepth - methods_objects["raindepth"] = conversion.to_raindepth - methods_objects["dbz"] = conversion.to_reflectivity - methods_objects["reflectivity"] = conversion.to_reflectivity - # transformation methods - methods_objects["boxcox"] = transformation.boxcox_transform - methods_objects["box-cox"] = transformation.boxcox_transform - methods_objects["db"] = transformation.dB_transform - methods_objects["decibel"] = transformation.dB_transform - methods_objects["log"] = transformation.boxcox_transform - methods_objects["nqt"] = transformation.NQ_transform - methods_objects["sqrt"] = transformation.sqrt_transform + methods_objects["mm/h"] = conversion.to_rainrate + methods_objects["rainrate"] = conversion.to_rainrate + methods_objects["mm"] = conversion.to_raindepth + methods_objects["raindepth"] = conversion.to_raindepth + methods_objects["dbz"] = conversion.to_reflectivity + methods_objects["reflectivity"] = conversion.to_reflectivity + + # cleansing methods + methods_objects["decluster"] = cleansing.decluster + methods_objects["detect_outliers"] = cleansing.detect_outliers + # dimension methods - methods_objects["accumulate"] = dimension.aggregate_fields_time - methods_objects["clip"] = dimension.clip_domain - methods_objects["square"] = dimension.square_domain - methods_objects["upscale"] = dimension.aggregate_fields_space + methods_objects["accumulate"] = dimension.aggregate_fields_time + methods_objects["clip"] = dimension.clip_domain + methods_objects["square"] = dimension.square_domain + methods_objects["upscale"] = dimension.aggregate_fields_space + # FFT methods if name in ["numpy", "pyfftw", "scipy"]: if "shape" not in kwargs.keys(): @@ -140,11 +173,31 @@ def donothing(R, metadata=None, *args, **kwargs): try: return methods_objects[name] except KeyError as e: - raise ValueError("Unknown method %s\n" % e + - "Supported methods:%s" % str(methods_objects.keys())) + raise ValueError( + "Unknown method %s\n" % e + + "Supported methods:%s" % str(methods_objects.keys()) + ) + + # image processing methods + methods_objects["corner_detection"] = images.corner_detection + methods_objects["morph_opening"] = images.morph_opening + + # interpolation methods + methods_objects["rapsd"] = interpolate.rbfinterp2d + # spectral methods - methods_objects["rapsd"] = spectral.rapsd - methods_objects["rm_rdisc"] = spectral.remove_rain_norain_discontinuity + methods_objects["rapsd"] = spectral.rapsd + methods_objects["rm_rdisc"] = spectral.remove_rain_norain_discontinuity + + # transformation methods + methods_objects["boxcox"] = transformation.boxcox_transform + methods_objects["box-cox"] = transformation.boxcox_transform + methods_objects["db"] = transformation.dB_transform + methods_objects["decibel"] = transformation.dB_transform + methods_objects["log"] = transformation.boxcox_transform + methods_objects["nqt"] = transformation.NQ_transform + methods_objects["sqrt"] = transformation.sqrt_transform + def _get_fft_method(name, **kwargs): kwargs = kwargs.copy() @@ -158,6 +211,8 @@ def _get_fft_method(name, **kwargs): elif name == "pyfftw": return fft.get_pyfftw(shape, **kwargs) else: - raise ValueError("Unknown method {}\n".format(name) - + "The available methods are:" - + str(["numpy", "pyfftw", "scipy"])) from None + raise ValueError( + "Unknown method {}\n".format(name) + + "The available methods are:" + + str(["numpy", "pyfftw", "scipy"]) + ) from None diff --git a/pysteps/utils/interpolate.py b/pysteps/utils/interpolate.py index 0976e96e1..874669ffe 100644 --- a/pysteps/utils/interpolate.py +++ b/pysteps/utils/interpolate.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- """ -pysteps.postprocessing.interpolate -================================== +pysteps.utils.interpolate +========================= Interpolation routines for pysteps. .. autosummary:: :toctree: ../generated/ - decluster_sparse_data rbfinterp2d """ @@ -16,105 +15,6 @@ import numpy as np import scipy.spatial -def decluster_sparse_data(coord, input_array, scale, min_samples, verbose=False): - """Decluster a set of sparse data points by aggregating (i.e., taking the - median value) all points within a certain distance (i.e., a cluster). - - Parameters - ---------- - - coord : array_like - Array of shape (n, 2) containing the coordinates of the input data into - a 2-dimensional space. - - input_array : array_like - Array of shape (n) or (n, m), where n is the number of samples and m - the number of variables. - All values in input_array are required to have finite values. - - scale : float or array_like - The scale parameter in the same units of coord. Data points within this - declustering scale are averaged together. - - min_samples : int - The minimum number of samples for computing the median within a given - cluster. - - verbose : bool, optional - Print out information. - - Returns - ------- - - out : tuple of ndarrays - A two-element tuple (dinput, dcoord) containing the declustered input_array - (d, m) and coordinates (d, 2), where d is the new number of samples - (d < n). - - """ - - coord = np.copy(coord) - input_array = np.copy(input_array) - scale = np.float(scale) - - # check inputs - if np.any(~np.isfinite(input_array)): - raise ValueError("input_array contains non-finite values") - - if input_array.ndim == 1: - nvar = 1 - input_array = input_array[:, None] - elif input_array.ndim == 2: - nvar = input_array.shape[1] - else: - raise ValueError( - "input_array must have 1 (n) or 2 dimensions (n, m), but it has %i" - % input_array.ndim - ) - - if coord.ndim != 2: - raise ValueError( - "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim - ) - - if coord.shape[0] != input_array.shape[0]: - raise ValueError( - "the number of samples in the input_array does not match the " - + "number of coordinates %i!=%i" - % (input_array.shape[0], coord.shape[0]) - ) - - # reduce original coordinates - coord_ = np.floor(coord / scale) - - # keep only unique pairs of the reduced coordinates - coordb_ = np.ascontiguousarray(coord_).view( - np.dtype((np.void, coord_.dtype.itemsize * coord_.shape[1])) - ) - __, idx = np.unique(coordb_, return_index=True) - ucoord_ = coord_[idx] - - # loop through these unique values and average vectors which belong to - # the same declustering grid cell - dinput = np.empty(shape=(0, nvar)) - dcoord = np.empty(shape=(0, 2)) - for i in range(ucoord_.shape[0]): - idx = np.logical_and( - coord_[:, 0] == ucoord_[i, 0], coord_[:, 1] == ucoord_[i, 1] - ) - npoints = np.sum(idx) - if npoints >= min_samples: - dinput = np.append( - dinput, np.median(input_array[idx, :], axis=0)[None, :], axis=0 - ) - dcoord = np.append( - dcoord, np.median(coord[idx, :], axis=0)[None, :], axis=0 - ) - - if verbose: - print("--- %i samples left after declustering ---" % dinput.shape[0]) - - return dcoord.squeeze(), dinput def rbfinterp2d( coord, @@ -309,4 +209,4 @@ def rbfinterp2d( # reshape to final grid size output_array = output_array.reshape(ygrid.size, xgrid.size, nvar) - return np.moveaxis(output_array, -1, 0).squeeze() \ No newline at end of file + return np.moveaxis(output_array, -1, 0).squeeze() From 1b282c11ca7c1eae6d8241132ce3a40ab8e17ac1 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 12:45:36 +0200 Subject: [PATCH 37/54] Fix utils.interface --- pysteps/tests/test_interfaces.py | 26 ++++++++++++++------ pysteps/utils/interface.py | 42 ++++++++++++++++---------------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index 76177c80c..0d1838493 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -206,19 +206,35 @@ def test_nowcasts_interface(): def test_utils_interface(): """Test utils module interface.""" + from pysteps.utils import arrays + from pysteps.utils import cleansing from pysteps.utils import conversion - from pysteps.utils import transformation from pysteps.utils import dimension + from pysteps.utils import images + from pysteps.utils import interpolate + from pysteps.utils import spectral + from pysteps.utils import transformation method_getter = pysteps.utils.interface.get_method - valid_names_func_pair = [('mm/h', conversion.to_rainrate), + valid_names_func_pair = [('centred_coord', arrays.compute_centred_coord_array), + ('decluster', cleansing.decluster), + ('detect_outliers', cleansing.detect_outliers), + ('mm/h', conversion.to_rainrate), ('rainrate', conversion.to_rainrate), ('mm', conversion.to_raindepth), ('raindepth', conversion.to_raindepth), ('dbz', conversion.to_reflectivity), ('reflectivity', conversion.to_reflectivity), - ('rainrate', conversion.to_rainrate), + ('accumulate', dimension.aggregate_fields_time), + ('clip', dimension.clip_domain), + ('square', dimension.square_domain), + ('upscale', dimension.aggregate_fields_space), + ('corner_detection', images.corner_detection), + ('morph_opening', images.morph_opening), + ('rbfinterp2d', interpolate.rbfinterp2d), + ('rapsd', spectral.rapsd), + ('rm_rdisc', spectral.remove_rain_norain_discontinuity), ('boxcox', transformation.boxcox_transform), ('box-cox', transformation.boxcox_transform), ('db', transformation.dB_transform), @@ -226,10 +242,6 @@ def test_utils_interface(): ('log', transformation.boxcox_transform), ('nqt', transformation.NQ_transform), ('sqrt', transformation.sqrt_transform), - ('accumulate', dimension.aggregate_fields_time), - ('clip', dimension.clip_domain), - ('square', dimension.square_domain), - ('upscale', dimension.aggregate_fields_space), ] invalid_names = ['random', 'invalid'] diff --git a/pysteps/utils/interface.py b/pysteps/utils/interface.py index 19dc11f0c..8b2ba69e5 100644 --- a/pysteps/utils/interface.py +++ b/pysteps/utils/interface.py @@ -32,7 +32,7 @@ def get_method(name, **kwargs): +===================+=====================================================+ | centred_coord | compute a 2D coordinate array | +-------------------+-----------------------------------------------------+ - + Cleansing methods: +-------------------+-----------------------------------------------------+ @@ -81,7 +81,7 @@ def get_method(name, **kwargs): +-------------------+-----------------------------------------------------+ | pyfftw | pyfftw.interfaces.numpy_fft | +-------------------+-----------------------------------------------------+ - + Image processing methods: +-------------------+-----------------------------------------------------+ @@ -146,6 +146,10 @@ def donothing(R, metadata=None, *args, **kwargs): # arrays methods methods_objects["centred_coord"] = arrays.compute_centred_coord_array + # cleansing methods + methods_objects["decluster"] = cleansing.decluster + methods_objects["detect_outliers"] = cleansing.detect_outliers + # conversion methods methods_objects["mm/h"] = conversion.to_rainrate methods_objects["rainrate"] = conversion.to_rainrate @@ -154,36 +158,18 @@ def donothing(R, metadata=None, *args, **kwargs): methods_objects["dbz"] = conversion.to_reflectivity methods_objects["reflectivity"] = conversion.to_reflectivity - # cleansing methods - methods_objects["decluster"] = cleansing.decluster - methods_objects["detect_outliers"] = cleansing.detect_outliers - # dimension methods methods_objects["accumulate"] = dimension.aggregate_fields_time methods_objects["clip"] = dimension.clip_domain methods_objects["square"] = dimension.square_domain methods_objects["upscale"] = dimension.aggregate_fields_space - # FFT methods - if name in ["numpy", "pyfftw", "scipy"]: - if "shape" not in kwargs.keys(): - raise KeyError("mandatory keyword argument shape not given") - return _get_fft_method(name, **kwargs) - else: - try: - return methods_objects[name] - except KeyError as e: - raise ValueError( - "Unknown method %s\n" % e - + "Supported methods:%s" % str(methods_objects.keys()) - ) - # image processing methods methods_objects["corner_detection"] = images.corner_detection methods_objects["morph_opening"] = images.morph_opening # interpolation methods - methods_objects["rapsd"] = interpolate.rbfinterp2d + methods_objects["rbfinterp2d"] = interpolate.rbfinterp2d # spectral methods methods_objects["rapsd"] = spectral.rapsd @@ -198,6 +184,20 @@ def donothing(R, metadata=None, *args, **kwargs): methods_objects["nqt"] = transformation.NQ_transform methods_objects["sqrt"] = transformation.sqrt_transform + # FFT methods + if name in ["numpy", "pyfftw", "scipy"]: + if "shape" not in kwargs.keys(): + raise KeyError("mandatory keyword argument shape not given") + return _get_fft_method(name, **kwargs) + else: + try: + return methods_objects[name] + except KeyError as e: + raise ValueError( + "Unknown method %s\n" % e + + "Supported methods:%s" % str(methods_objects.keys()) + ) + def _get_fft_method(name, **kwargs): kwargs = kwargs.copy() From adcbe308af6c4dd1ec75ac8c8266c3f41b599e99 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 14:04:11 +0200 Subject: [PATCH 38/54] Refactor interfaces --- examples/LK_buffer_mask.py | 10 +- pysteps/motion/lucaskanade.py | 233 +++++++++++++------------------ pysteps/tests/test_interfaces.py | 2 +- pysteps/utils/cleansing.py | 4 +- pysteps/utils/images.py | 70 ++++++++-- pysteps/utils/interface.py | 4 +- 6 files changed, 169 insertions(+), 154 deletions(-) diff --git a/examples/LK_buffer_mask.py b/examples/LK_buffer_mask.py index 0a6cd5300..83d9b61f6 100644 --- a/examples/LK_buffer_mask.py +++ b/examples/LK_buffer_mask.py @@ -112,7 +112,8 @@ R.data[R.mask] = np.nan # Use default settings (i.e., no buffering of the radar mask) -xy, uv = LK_optflow(R, dense=False, buffer_mask=0, quality_level_ST=0.1) +fd_kwargs1 = {"buffer_mask":0, "quality_level_ST":0.1} +xy, uv = LK_optflow(R, dense=False, fd_kwargs=fd_kwargs1) plt.imshow(ref_dbr, cmap=plt.get_cmap("Greys")) plt.imshow(mask, cmap=colors.ListedColormap(["black"]), alpha=0.5) plt.quiver( @@ -141,7 +142,8 @@ # 'x,y,u,v = LK_optflow(.....)'. # with buffer -xy, uv = LK_optflow(R, dense=False, buffer_mask=20, quality_level_ST=0.2) +fd_kwargs2 = {"buffer_mask":20, "quality_level_ST":0.2} +xy, uv = LK_optflow(R, dense=False, fd_kwargs=fd_kwargs2) plt.imshow(ref_dbr, cmap=plt.get_cmap("Greys")) plt.imshow(mask, cmap=colors.ListedColormap(["black"]), alpha=0.5) plt.quiver( @@ -169,8 +171,8 @@ # the negative bias that is introduced by the the erroneous interpretation of # velocities near the maximum range of the radars. -UV1 = LK_optflow(R, dense=True, buffer_mask=0, quality_level_ST=0.1) -UV2 = LK_optflow(R, dense=True, buffer_mask=20, quality_level_ST=0.2) +UV1 = LK_optflow(R, dense=True, fd_kwargs=fd_kwargs1) +UV2 = LK_optflow(R, dense=True, fd_kwargs=fd_kwargs2) V1 = np.sqrt(UV1[0] ** 2 + UV1[1] ** 2) V2 = np.sqrt(UV2[0] ** 2 + UV2[1] ** 2) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 6aae192da..639240008 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -20,7 +20,6 @@ :toctree: ../generated/ dense_lucaskanade - features_to_track track_features """ @@ -31,9 +30,9 @@ from pysteps.decorators import check_input_frames from pysteps.exceptions import MissingOptionalDependency +from pysteps import utils from pysteps.utils.cleansing import decluster, detect_outliers -from pysteps.utils.interpolate import rbfinterp2d -from pysteps.utils.images import corner_detection, morph_opening +from pysteps.utils.images import morph_opening try: import cv2 @@ -47,7 +46,20 @@ @check_input_frames(2) -def dense_lucaskanade(input_images, **kwargs): +def dense_lucaskanade( + input_images, + lk_kwargs=None, + fd_method="ShiTomasi", + fd_kwargs=None, + interp_method="rbfinterp2d", + interp_kwargs=None, + dense=True, + nr_std_outlier=3, + k_outlier=30, + size_opening=3, + decl_scale=10, + verbose=False +): """Run the Lucas-Kanade optical flow and interpolate the motion vectors. .. _opencv: https://opencv.org/ @@ -68,6 +80,7 @@ def dense_lucaskanade(input_images, **kwargs): Parameters ---------- + input_images : array_like or MaskedArray_ Array of shape (T, m, n) containing a sequence of T two-dimensional input images of shape (m, n). T = 2 is the minimum required number of images. @@ -78,8 +91,27 @@ def dense_lucaskanade(input_images, **kwargs): The mask in the MaskedArray_ defines a region where velocity vectors are not computed. - Other Parameters - ---------------- + lk_kwargs : dict, optional + Optional dictionary containing keyword arguments for the Lucas-Kanade + features tracking algorithm. See the documentation of + pysteps.motion.lucaskanade.track_features. + + fd_method : {"ShiTomasi"}, optional + Name of the feature detection method to use. See the documentation + of pysteps.utils.interpolate. + + fd_kwargs : dict, optional + Optional dictionary containing keyword arguments for the features detection + algorithm. See the documentation of pysteps.utils.iamges.corner_detection. + + interp_method : {"rbfinterp2d"}, optional + Name of the interpolation method to use. See the documentation + of pysteps.utils.interpolate. + + interp_kwargs : dict, optional + Optional dictionary containing keyword arguments for the interpolation + algorithm. See the documentation of pysteps.utils.interpolate. + dense : bool, optional If True (the default), it returns the three-dimensional array (2,m,n) containing the dense x- and y-components of the motion field. If false, @@ -87,93 +119,32 @@ def dense_lucaskanade(input_images, **kwargs): x, y define the vector locations, u, v define the x and y direction components of the vectors. - buffer_mask : int, optional - A mask buffer width in pixels. This extends the input mask (if any) - to help avoiding the erroneous interpretation of velocities near the - maximum range of the radars (0 by default). - - max_corners_ST : int, optional - The maxCorners parameter in the `Shi-Tomasi`_ corner detection method. - It represents the maximum number of points to be tracked (corners), - by default this is 500. If set to zero, all detected corners are used. - - quality_level_ST : float, optional - The qualityLevel parameter in the `Shi-Tomasi`_ corner detection method. - It represents the minimal accepted quality for the points to be tracked - (corners), by default this is set to 0.1. Higher quality thresholds can - lead to no detection at all. - - min_distance_ST : int, optional - The minDistance parameter in the `Shi-Tomasi`_ corner detection method. - It represents minimum possible Euclidean distance in pixels - between corners, by default this is set to 3 pixels. - - block_size_ST : int, optional - The blockSize parameter in the `Shi-Tomasi`_ corner detection method. - It represents the window size in pixels used for computing a derivative - covariation matrix over each pixel neighborhood, by default this is set - to 15 pixels. - - winsize_LK : tuple of int, optional - The winSize parameter in the `Lucas-Kanade`_ optical flow method. - It represents the size of the search window that it is used at each - pyramid level, by default this is set to (50, 50) pixels. - - nr_levels_LK : int, optional - The maxLevel parameter in the `Lucas-Kanade`_ optical flow method. - It represents the 0-based maximal pyramid level number, by default this - is set to 3. - nr_std_outlier : int, optional Maximum acceptable deviation from the mean in terms of number of standard deviations. Any anomaly larger than this value is flagged as outlier and excluded from the interpolation. - By default this is set to 3. k_outlier : int or None, optional The number of nearest neighbours used to localize the outlier detection. + If set to None, it employs all the data points (global detection). - The default is 30. size_opening : int, optional The size of the structuring element kernel in pixels. This is used to perform a binary morphological opening on the input fields in order to - filter isolated echoes due to clutter. By default this is set to 3. + filter isolated echoes due to clutter. + If set to zero, the fitlering is not perfomed. decl_scale : int, optional The scale declustering parameter in pixels used to reduce the number of redundant sparse vectors before the interpolation. Sparse vectors within this declustering scale are averaged together. - By default this is set to 20 pixels. If set to less than 2 pixels, the - declustering is not perfomed. - - min_decl_samples : int, optional - The minimum number of samples necessary for computing the median vector - within given declustering cell, otherwise all sparse vectors in that - cell are discarded. By default this is set to 2. - - rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse - multiquadric", "bump"}, optional - The name of one of the available radial basis function based on the - Euclidean norm. "gaussian" by default. - - k : int or None, optional - The number of nearest neighbours used to speed-up the interpolation. - If set to None, it interpolates based on all the data points. - This is 50 by default. - - epsilon : float, optional - The shape parameter > 0 used to scale the input to the radial kernel. - It defaults to 1.0. - nchunks : int, optional - Split the grid points in n chunks to limit the memory usage during the - interpolation. By default this is set to 5, if set to 1 the interpolation - is computed with the whole grid. + If set to less than 2 pixels, the declustering is not perfomed. verbose : bool, optional - If set to True, it prints information about the program (True by default). + If set to True, it prints information about the program. Returns ------- @@ -203,33 +174,14 @@ def dense_lucaskanade(input_images, **kwargs): input_images = input_images.copy() - # defaults - dense = kwargs.get("dense", True) - max_corners_ST = kwargs.get("max_corners_ST", 500) - quality_level_ST = kwargs.get("quality_level_ST", 0.1) - min_distance_ST = kwargs.get("min_distance_ST", 3) - block_size_ST = kwargs.get("block_size_ST", 15) - winsize_LK = kwargs.get("winsize_LK", (50, 50)) - nr_levels_LK = kwargs.get("nr_levels_LK", 3) - nr_std_outlier = kwargs.get("nr_std_outlier", 3) - nr_IQR_outlier = kwargs.get("nr_IQR_outlier", None) - if nr_IQR_outlier is not None: - nr_std_outlier = nr_IQR_outlier - warnings.warn( - "the 'nr_IQR_outlier' argument will be deprecated in the next release; " - + "use 'nr_std_outlier' instead.", - category=FutureWarning, - ) - k_outlier = kwargs.get("k_outlier", 30) - size_opening = kwargs.get("size_opening", 3) - decl_scale = kwargs.get("decl_scale", 20) - min_decl_samples = kwargs.get("min_decl_samples", 2) - rbfunction = kwargs.get("rbfunction", "gaussian") - k = kwargs.get("k", 50) - epsilon = kwargs.get("epsilon", 1.0) - nchunks = kwargs.get("nchunks", 5) - verbose = kwargs.get("verbose", True) - buffer_mask = kwargs.get("buffer_mask", 10) + if fd_kwargs is None: + fd_kwargs = dict() + + if lk_kwargs is None: + lk_kwargs = dict() + + if interp_kwargs is None: + interp_kwargs = dict() if verbose: print("Computing the motion field with the Lucas-Kanade method.") @@ -238,6 +190,9 @@ def dense_lucaskanade(input_images, **kwargs): nr_fields = input_images.shape[0] domain_size = (input_images.shape[1], input_images.shape[2]) + feature_detection_method = utils.get_method(fd_method) + interpolation_method = utils.get_method(interp_method) + xy = np.empty(shape=(0, 2)) uv = np.empty(shape=(0, 2)) for n in range(nr_fields - 1): @@ -259,26 +214,15 @@ def dense_lucaskanade(input_images, **kwargs): prvs = morph_opening(prvs, prvs.min(), size_opening) next = morph_opening(next, next.min(), size_opening) - # find good features to track - gf_params = dict( - maxCorners=max_corners_ST, - qualityLevel=quality_level_ST, - minDistance=min_distance_ST, - blockSize=block_size_ST, - ) - points = corner_detection(prvs, gf_params, buffer_mask, False) + # features detection + points = feature_detection_method(prvs, **fd_kwargs) # skip loop if no features to track if points.shape[0] == 0: continue # get sparse u, v vectors with Lucas-Kanade tracking - lk_params = dict( - winSize=winsize_LK, - maxLevel=nr_levels_LK, - criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0), - ) - xy_, uv_ = track_features(prvs, next, points, lk_params, False) + xy_, uv_ = track_features(prvs, next, points, **lk_kwargs) # skip loop if no vectors if xy_.shape[0] == 0: @@ -309,25 +253,16 @@ def dense_lucaskanade(input_images, **kwargs): # decluster sparse motion vectors if decl_scale > 1: - xy, uv = decluster(xy, uv, decl_scale, min_decl_samples, verbose) + xy, uv = decluster(xy, uv, decl_scale, 1, verbose) # return zero motion field if no sparse vectors are left for interpolation if xy.shape[0] == 0: return np.zeros((2, domain_size[0], domain_size[1])) - # kernel interpolation + # interpolation xgrid = np.arange(domain_size[1]) ygrid = np.arange(domain_size[0]) - UV = rbfinterp2d( - xy, - uv, - xgrid, - ygrid, - rbfunction=rbfunction, - epsilon=epsilon, - k=k, - nchunks=nchunks, - ) + UV = interpolation_method(xy, uv, xgrid, ygrid, **interp_kwargs) if verbose: print("--- total time: %.2f seconds ---" % (time.time() - t0)) @@ -335,15 +270,25 @@ def dense_lucaskanade(input_images, **kwargs): return UV -def track_features(prvs_image, next_image, points, params, verbose=False): +def track_features( + prvs_image, + next_image, + points, + winsize=(50, 50), + nr_levels=3, + criteria=(3, 10, 0), + flags=0, + min_eig_thr=1e-4, + verbose=False, +): """ - Interface to the OpenCV `calcOpticalFlowPyrLK()`_ features tracking algorithm. + Interface to the OpenCV calcOpticalFlowPyrLK_ features tracking algorithm. - .. _`calcOpticalFlowPyrLK()`:\ + .. _calcOpticalFlowPyrLK:\ https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 - .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ - maskedarray.baseclass.html#numpy.ma.MaskedArray + .. _MaskedArray:\ + https://docs.scipy.org/doc/numpy/reference/maskedarray.baseclass.html#numpy.ma.MaskedArray Parameters ---------- @@ -360,9 +305,24 @@ def track_features(prvs_image, next_image, points, params, verbose=False): Array of shape (p, 2) indicating the pixel coordinates of the tracking points (corners). - params : dict - Any additional parameter to the original routine as described in the - `calcOpticalFlowPyrLK()`_ documentation. + winsize : tuple of int, optional + The winSize parameter in calcOpticalFlowPyrLK_. + It represents the size of the search window that it is used at each + pyramid level. + + nr_levels : int, optional + The maxLevel parameter in calcOpticalFlowPyrLK_. + It represents the 0-based maximal pyramid level number. + + criteria : tuple of int, optional + The TermCriteria parameter in calcOpticalFlowPyrLK_ , + which specifies the termination criteria of the iterative search algorithm + + flags : int, optional + Operation flags, see documentation calcOpticalFlowPyrLK_. + + min_eig_thr : float, optional + The minEigThreshold parameter in calcOpticalFlowPyrLK_. verbose : bool, optional Print the number of vectors that have been found. @@ -412,6 +372,13 @@ def track_features(prvs_image, next_image, points, params, verbose=False): # Lucas-Kanade # TODO: use the error returned by the OpenCV routine + params = dict( + winSize=winsize, + maxLevel=nr_levels, + criteria=criteria, + flags=flags, + minEigThreshold=min_eig_thr, + ) p1, st, __ = cv2.calcOpticalFlowPyrLK(prvs, next, p0, None, **params) # keep only features that have been found diff --git a/pysteps/tests/test_interfaces.py b/pysteps/tests/test_interfaces.py index 0d1838493..583fd7cd6 100644 --- a/pysteps/tests/test_interfaces.py +++ b/pysteps/tests/test_interfaces.py @@ -230,7 +230,7 @@ def test_utils_interface(): ('clip', dimension.clip_domain), ('square', dimension.square_domain), ('upscale', dimension.aggregate_fields_space), - ('corner_detection', images.corner_detection), + ('shitomasi', images.ShiTomasi_detection), ('morph_opening', images.morph_opening), ('rbfinterp2d', interpolate.rbfinterp2d), ('rapsd', spectral.rapsd), diff --git a/pysteps/utils/cleansing.py b/pysteps/utils/cleansing.py index 00fae1d5e..7188070d4 100644 --- a/pysteps/utils/cleansing.py +++ b/pysteps/utils/cleansing.py @@ -16,7 +16,7 @@ import scipy.spatial -def decluster(coord, input_array, scale, min_samples, verbose=False): +def decluster(coord, input_array, scale, min_samples=1, verbose=False): """Decluster a set of sparse data points by aggregating (i.e., taking the median value) all points within a certain distance (i.e., a cluster). @@ -36,7 +36,7 @@ def decluster(coord, input_array, scale, min_samples, verbose=False): The scale parameter in the same units of coord. Data points within this declustering scale are averaged together. - min_samples : int + min_samples : int, optional The minimum number of samples for computing the median within a given cluster. diff --git a/pysteps/utils/images.py b/pysteps/utils/images.py index 84317614e..b4fadc388 100644 --- a/pysteps/utils/images.py +++ b/pysteps/utils/images.py @@ -8,7 +8,7 @@ .. autosummary:: :toctree: ../generated/ - corner_detection + ShiTomasi_detection morph_opening """ @@ -23,18 +23,29 @@ CV2_IMPORTED = False -def corner_detection(input_image, params, buffer_mask=0, verbose=False): +def ShiTomasi_detection(input_image, max_corners=500, quality_level=0.1, + min_distance=3, block_size=15, buffer_mask=0, + use_harris = False, k = 0.04, + verbose=False, + **kwargs): """ - Interface to the OpenCV `goodFeaturesToTrack()`_ method to detect corners - on an image. + Interface to the OpenCV `Shi-Tomasi`_ features detection method to detect + corners in the image. Corners are used for local tracking methods. - .. _`goodFeaturesToTrack()`:\ + .. _`Shi-Tomasi`:\ https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 - .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ - maskedarray.baseclass.html#numpy.ma.MaskedArray + .. _MaskedArray:\ + https://docs.scipy.org/doc/numpy/reference/maskedarray.baseclass.html#numpy.ma.MaskedArray + + .. _`Harris detector`:\ + https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#gac1fc3598018010880e370e2f709b4345 + + .. _cornerMinEigenVal:\ + https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga3dbce297c1feb859ee36707e1003e0a8 + Parameters ---------- @@ -47,14 +58,35 @@ def corner_detection(input_image, params, buffer_mask=0, verbose=False): computed. The corresponding fill value is taken as the minimum of all valid pixels. - params : dict - Any additional parameter to the original routine as described in the - `goodFeaturesToTrack()`_ documentation. + max_corners : int, optional + The maxCorners parameter in the `Shi-Tomasi`_ corner detection method. + It represents the maximum number of points to be tracked (corners). + If set to zero, all detected corners are used. + + quality_level : float, optional + The qualityLevel parameter in the `Shi-Tomasi`_ corner detection method. + It represents the minimal accepted quality for the points to be tracked + (corners). + + min_distance : int, optional + The minDistance parameter in the `Shi-Tomasi`_ corner detection method. + It represents minimum possible Euclidean distance in pixels between corners. + + block_size : int, optional + The blockSize parameter in the `Shi-Tomasi`_ corner detection method. + It represents the window size in pixels used for computing a derivative + covariation matrix over each pixel neighborhood. + + use_harris : bool, optional + Whether to use a `Harris detector`_ or cornerMinEigenVal_. + + k : float, optional + Free parameter of the Harris detector. buffer_mask : int, optional A mask buffer width in pixels. This extends the input mask (if any) to help avoiding the erroneous interpretation of velocities near the - maximum range of the radars (0 by default). + maximum range of the radars. verbose : bool, optional Print the number of features detected. @@ -70,6 +102,13 @@ def corner_detection(input_image, params, buffer_mask=0, verbose=False): -------- pysteps.motion.lucaskanade.track_features + + References + ---------- + + Jianbo Shi and Carlo Tomasi. Good features to track. In Computer Vision and + Pattern Recognition, 1994. Proceedings CVPR'94., 1994 IEEE Computer Society + Conference on, pages 593–600. IEEE, 1994. """ if not CV2_IMPORTED: raise MissingOptionalDependency( @@ -106,7 +145,14 @@ def corner_detection(input_image, params, buffer_mask=0, verbose=False): # convert to 8-bit input_image = np.ndarray.astype(input_image, "uint8") mask = (-1 * mask + 1).astype("uint8") - + + params = dict( + maxCorners=max_corners, + qualityLevel=quality_level, + minDistance=min_distance, + useHarrisDetector=use_harris, + k=k, + ) points = cv2.goodFeaturesToTrack(input_image, mask=mask, **params) if points is None: points = np.empty(shape=(0, 2)) diff --git a/pysteps/utils/interface.py b/pysteps/utils/interface.py index 8b2ba69e5..fcbe00096 100644 --- a/pysteps/utils/interface.py +++ b/pysteps/utils/interface.py @@ -87,7 +87,7 @@ def get_method(name, **kwargs): +-------------------+-----------------------------------------------------+ | Name | Description | +===================+=====================================================+ - | corner_detection | detect corners on an image | + | ShiTomasi | Shi-Tomasi corner detection on an image | +-------------------+-----------------------------------------------------+ | morph_opening | filter small scale noise on an image | +-------------------+-----------------------------------------------------+ @@ -165,7 +165,7 @@ def donothing(R, metadata=None, *args, **kwargs): methods_objects["upscale"] = dimension.aggregate_fields_space # image processing methods - methods_objects["corner_detection"] = images.corner_detection + methods_objects["shitomasi"] = images.ShiTomasi_detection methods_objects["morph_opening"] = images.morph_opening # interpolation methods From d62d65320691dd3ae8ed740e7eca222d66278782 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 14:04:24 +0200 Subject: [PATCH 39/54] Update tests --- pysteps/tests/test_nowcasts_steps.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pysteps/tests/test_nowcasts_steps.py b/pysteps/tests/test_nowcasts_steps.py index bf7f46716..bf4de1ffd 100644 --- a/pysteps/tests/test_nowcasts_steps.py +++ b/pysteps/tests/test_nowcasts_steps.py @@ -64,11 +64,11 @@ def _import_mch_gif(prv, nxt): ) steps_arg_values = [ - (5, 6, 2, None, None, 1.51), - (5, 6, 2, "incremental", None, 6.38), - (5, 6, 2, "sprog", None, 7.35), - (5, 6, 2, "obs", None, 7.36), - (5, 6, 2, None, "cdf", 0.66), + (5, 6, 2, None, None, 1.55), + (5, 6, 2, "incremental", None, 6.65), + (5, 6, 2, "sprog", None, 7.65), + (5, 6, 2, "obs", None, 7.65), + (5, 6, 2, None, "cdf", 0.70), (5, 6, 2, None, "mean", 1.55), ] From 96ec87e7b5788acec9881b5f234f05a4e63f0856 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 14:23:18 +0200 Subject: [PATCH 40/54] Update utils doc --- doc/source/pysteps_reference/utils.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/source/pysteps_reference/utils.rst b/doc/source/pysteps_reference/utils.rst index f43454bfe..cd13c95ca 100644 --- a/doc/source/pysteps_reference/utils.rst +++ b/doc/source/pysteps_reference/utils.rst @@ -7,8 +7,11 @@ Implementation of miscellaneous utility functions. .. automodule:: pysteps.utils.interface .. automodule:: pysteps.utils.arrays +.. automodule:: pysteps.utils.cleansing .. automodule:: pysteps.utils.conversion .. automodule:: pysteps.utils.dimension .. automodule:: pysteps.utils.fft +.. automodule:: pysteps.utils.images +.. automodule:: pysteps.utils.interpolate .. automodule:: pysteps.utils.spectral .. automodule:: pysteps.utils.transformation From 984291b22d2c3acf14d611c9cff2f17c28db50e4 Mon Sep 17 00:00:00 2001 From: ned Date: Tue, 13 Aug 2019 14:24:50 +0200 Subject: [PATCH 41/54] Improve docstrings --- pysteps/motion/lucaskanade.py | 76 ++++++++++++++++++----------------- pysteps/utils/images.py | 5 --- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 639240008..089409170 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -3,25 +3,21 @@ pysteps.motion.lucaskanade ========================== -The Lucas-Kanade (LK) Module. +The Lucas-Kanade (LK) local feature tracking module. -This module implements the interface to the local Lucas-Kanade routine available -in OpenCV, as well as other auxiliary methods such as the interpolation of the -LK vectors over a grid. +This module implements the interface to the local `Lucas-Kanade`_ routine available +in OpenCV_, including the interpolation of the sparse vectors over a regular grid. -.. _`goodFeaturesToTrack()`:\ - https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 +.. _OpenCV: https://opencv.org/ - -.. _`calcOpticalFlowPyrLK()`:\ - https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 +.. _`Lucas-Kanade`:\ + https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 .. autosummary:: :toctree: ../generated/ dense_lucaskanade track_features - """ import numpy as np @@ -62,16 +58,16 @@ def dense_lucaskanade( ): """Run the Lucas-Kanade optical flow and interpolate the motion vectors. - .. _opencv: https://opencv.org/ + .. _OpenCV: https://opencv.org/ - .. _`Lucas-Kanade`: https://docs.opencv.org/3.4/dc/d6b/\ - group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 + .. _`Lucas-Kanade`:\ + https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 - .. _MaskedArray: https://docs.scipy.org/doc/numpy/reference/\ - maskedarray.baseclass.html#numpy.ma.MaskedArray + .. _MaskedArray:\ + https://docs.scipy.org/doc/numpy/reference/maskedarray.baseclass.html#numpy.ma.MaskedArray - .. _Shi-Tomasi: https://docs.opencv.org/3.4.1/dd/d1a/group__\ - imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 + .. _`Shi-Tomasi`:\ + https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 Interface to the OpenCV_ implementation of the local `Lucas-Kanade`_ optical flow method applied in combination to the `Shi-Tomasi`_ corner detection @@ -84,16 +80,16 @@ def dense_lucaskanade( input_images : array_like or MaskedArray_ Array of shape (T, m, n) containing a sequence of T two-dimensional input images of shape (m, n). T = 2 is the minimum required number of images. - With T > 2, the sparse vectors detected by Lucas-Kanade are pooled - together prior to the final interpolation. + With T > 2, all the sparse vectors detected are pooled together before + the final interpolation on a regular grid. In case of an array_like, invalid values (Nans or infs) are masked. The mask in the MaskedArray_ defines a region where velocity vectors are not computed. lk_kwargs : dict, optional - Optional dictionary containing keyword arguments for the Lucas-Kanade - features tracking algorithm. See the documentation of + Optional dictionary containing keyword arguments for the `Lucas-Kanade`_ + features tracking algorithm. See the documentation of pysteps.motion.lucaskanade.track_features. fd_method : {"ShiTomasi"}, optional @@ -102,7 +98,7 @@ def dense_lucaskanade( fd_kwargs : dict, optional Optional dictionary containing keyword arguments for the features detection - algorithm. See the documentation of pysteps.utils.iamges.corner_detection. + algorithm. See the documentation of pysteps.utils.images.corner_detection. interp_method : {"rbfinterp2d"}, optional Name of the interpolation method to use. See the documentation @@ -113,10 +109,10 @@ def dense_lucaskanade( algorithm. See the documentation of pysteps.utils.interpolate. dense : bool, optional - If True (the default), it returns the three-dimensional array (2,m,n) - containing the dense x- and y-components of the motion field. If false, - it returns the sparse motion vectors as 1D arrays x, y, u, v, where - x, y define the vector locations, u, v define the x and y direction + If True, it returns the three-dimensional array (2,m,n) containing the + dense x- and y-components of the motion field. + If false, it returns the sparse motion vectors as 2-D xy and uv arrays, + where xy defines the vector locations, uv defines the x and y direction components of the vectors. nr_std_outlier : int, optional @@ -160,6 +156,11 @@ def dense_lucaskanade( Return a zero motion field when no motion is detected. + See also + -------- + + pysteps.motion.lucaskanade.track_features + References ---------- @@ -174,15 +175,6 @@ def dense_lucaskanade( input_images = input_images.copy() - if fd_kwargs is None: - fd_kwargs = dict() - - if lk_kwargs is None: - lk_kwargs = dict() - - if interp_kwargs is None: - interp_kwargs = dict() - if verbose: print("Computing the motion field with the Lucas-Kanade method.") t0 = time.time() @@ -193,6 +185,15 @@ def dense_lucaskanade( feature_detection_method = utils.get_method(fd_method) interpolation_method = utils.get_method(interp_method) + if fd_kwargs is None: + fd_kwargs = dict() + + if lk_kwargs is None: + lk_kwargs = dict() + + if interp_kwargs is None: + interp_kwargs = dict() + xy = np.empty(shape=(0, 2)) uv = np.empty(shape=(0, 2)) for n in range(nr_fields - 1): @@ -282,7 +283,8 @@ def track_features( verbose=False, ): """ - Interface to the OpenCV calcOpticalFlowPyrLK_ features tracking algorithm. + Interface to the OpenCV calcOpticalFlowPyrLK_ Lucas-Kanade features tracking + algorithm. .. _calcOpticalFlowPyrLK:\ https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 @@ -341,7 +343,7 @@ def track_features( Notes ----- - The tracking points can be obtained with the pysteps.utils.images.corner_detection + The tracking points can be obtained with the pysteps.utils.images.ShiTomasi_detection routine. """ if not CV2_IMPORTED: diff --git a/pysteps/utils/images.py b/pysteps/utils/images.py index b4fadc388..82280cb5a 100644 --- a/pysteps/utils/images.py +++ b/pysteps/utils/images.py @@ -97,11 +97,6 @@ def ShiTomasi_detection(input_image, max_corners=500, quality_level=0.1, points : array_like Array of shape (p, 2) indicating the pixel coordinates of p detected corners. - - See also - -------- - - pysteps.motion.lucaskanade.track_features References ---------- From 8f2d3a0f78a7db69ddeaad28b0998ca0185e2ca4 Mon Sep 17 00:00:00 2001 From: Andres Perez Hortal Date: Tue, 13 Aug 2019 09:15:08 -0400 Subject: [PATCH 42/54] Update a wrapper function to look like the wrapped function --- pysteps/decorators.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pysteps/decorators.py b/pysteps/decorators.py index c8fccf163..166eaca45 100644 --- a/pysteps/decorators.py +++ b/pysteps/decorators.py @@ -10,6 +10,7 @@ check_motion_input_image """ +from functools import wraps import numpy as np @@ -23,6 +24,7 @@ def check_input_frames(minimum_input_frames=2, """ def _check_input_frames(motion_method_func): + @wraps(motion_method_func) def new_function(*args, **kwargs): """ Return new function with the checks prepended to the From 4085dbfc59a44626051dd2b4801dbb5bcd6bb2bb Mon Sep 17 00:00:00 2001 From: Andres Perez Hortal Date: Thu, 15 Aug 2019 11:15:34 -0400 Subject: [PATCH 43/54] Code formating Enforece maximum number of characters per line in docstrings Replace variable names (next was used and this is a reserved keyword). --- pysteps/motion/lucaskanade.py | 126 ++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 60 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 089409170..f450e3a7a 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -5,8 +5,9 @@ The Lucas-Kanade (LK) local feature tracking module. -This module implements the interface to the local `Lucas-Kanade`_ routine available -in OpenCV_, including the interpolation of the sparse vectors over a regular grid. +This module implements the interface to the local `Lucas-Kanade`_ routine +available in OpenCV_, including the interpolation of the sparse vectors over a +regular grid. .. _OpenCV: https://opencv.org/ @@ -42,20 +43,18 @@ @check_input_frames(2) -def dense_lucaskanade( - input_images, - lk_kwargs=None, - fd_method="ShiTomasi", - fd_kwargs=None, - interp_method="rbfinterp2d", - interp_kwargs=None, - dense=True, - nr_std_outlier=3, - k_outlier=30, - size_opening=3, - decl_scale=10, - verbose=False -): +def dense_lucaskanade(input_images, + lk_kwargs=None, + fd_method="ShiTomasi", + fd_kwargs=None, + interp_method="rbfinterp2d", + interp_kwargs=None, + dense=True, + nr_std_outlier=3, + k_outlier=30, + size_opening=3, + decl_scale=10, + verbose=False): """Run the Lucas-Kanade optical flow and interpolate the motion vectors. .. _OpenCV: https://opencv.org/ @@ -71,15 +70,16 @@ def dense_lucaskanade( Interface to the OpenCV_ implementation of the local `Lucas-Kanade`_ optical flow method applied in combination to the `Shi-Tomasi`_ corner detection - routine. The sparse motion vectors are finally interpolated to return the whole - motion field. + routine. The sparse motion vectors are finally interpolated to return the + whole motion field. Parameters ---------- input_images : array_like or MaskedArray_ - Array of shape (T, m, n) containing a sequence of T two-dimensional input - images of shape (m, n). T = 2 is the minimum required number of images. + Array of shape (T, m, n) containing a sequence of T two-dimensional + input images of shape (m, n). T = 2 is the minimum required number of + images. With T > 2, all the sparse vectors detected are pooled together before the final interpolation on a regular grid. @@ -97,8 +97,9 @@ def dense_lucaskanade( of pysteps.utils.interpolate. fd_kwargs : dict, optional - Optional dictionary containing keyword arguments for the features detection - algorithm. See the documentation of pysteps.utils.images.corner_detection. + Optional dictionary containing keyword arguments for the features + detection algorithm. + See the documentation of pysteps.utils.images.corner_detection. interp_method : {"rbfinterp2d"}, optional Name of the interpolation method to use. See the documentation @@ -146,9 +147,9 @@ def dense_lucaskanade( ------- out : array_like or tuple - If dense=True (the default), it returns the three-dimensional array (2,m,n) - containing the dense x- and y-components of the motion field in units of - pixels / timestep as given by the input array input_images. + If dense=True (the default), it returns the three-dimensional array + (2,m,n) containing the dense x- and y-components of the motion field in + units of pixels / timestep as given by the input array input_images. If dense=False, it returns a tuple containing the 2-dimensional arrays xy and uv, where x, y define the vector locations, u, v define the x @@ -199,31 +200,31 @@ def dense_lucaskanade( for n in range(nr_fields - 1): # extract consecutive images - prvs = input_images[n, :, :].copy() - next = input_images[n + 1, :, :].copy() + prvs_img = input_images[n, :, :].copy() + next_img = input_images[n + 1, :, :].copy() - if ~isinstance(prvs, MaskedArray): - prvs = np.ma.masked_invalid(prvs) - np.ma.set_fill_value(prvs, prvs.min()) + if ~isinstance(prvs_img, MaskedArray): + prvs_img = np.ma.masked_invalid(prvs_img) + np.ma.set_fill_value(prvs_img, prvs_img.min()) - if ~isinstance(next, MaskedArray): - next = np.ma.masked_invalid(next) - np.ma.set_fill_value(next, next.min()) + if ~isinstance(next_img, MaskedArray): + next_img = np.ma.masked_invalid(next_img) + np.ma.set_fill_value(next_img, next_img.min()) # remove small noise with a morphological operator (opening) if size_opening > 0: - prvs = morph_opening(prvs, prvs.min(), size_opening) - next = morph_opening(next, next.min(), size_opening) + prvs_img = morph_opening(prvs_img, prvs_img.min(), size_opening) + next_img = morph_opening(next_img, next_img.min(), size_opening) # features detection - points = feature_detection_method(prvs, **fd_kwargs) + points = feature_detection_method(prvs_img, **fd_kwargs) # skip loop if no features to track if points.shape[0] == 0: continue # get sparse u, v vectors with Lucas-Kanade tracking - xy_, uv_ = track_features(prvs, next, points, **lk_kwargs) + xy_, uv_ = track_features(prvs_img, next_img, points, **lk_kwargs) # skip loop if no vectors if xy_.shape[0] == 0: @@ -272,15 +273,15 @@ def dense_lucaskanade( def track_features( - prvs_image, - next_image, - points, - winsize=(50, 50), - nr_levels=3, - criteria=(3, 10, 0), - flags=0, - min_eig_thr=1e-4, - verbose=False, + prvs_image, + next_image, + points, + winsize=(50, 50), + nr_levels=3, + criteria=(3, 10, 0), + flags=0, + min_eig_thr=1e-4, + verbose=False, ): """ Interface to the OpenCV calcOpticalFlowPyrLK_ Lucas-Kanade features tracking @@ -318,7 +319,8 @@ def track_features( criteria : tuple of int, optional The TermCriteria parameter in calcOpticalFlowPyrLK_ , - which specifies the termination criteria of the iterative search algorithm + which specifies the termination criteria of the iterative search + algorithm. flags : int, optional Operation flags, see documentation calcOpticalFlowPyrLK_. @@ -352,25 +354,28 @@ def track_features( "routine but it is not installed" ) - prvs = np.copy(prvs_image) - next = np.copy(next_image) + prvs_img = np.copy(prvs_image) + next_img = np.copy(next_image) p0 = np.copy(points) - if ~isinstance(prvs, MaskedArray): - prvs = np.ma.masked_invalid(prvs) - np.ma.set_fill_value(prvs, prvs.min()) + if ~isinstance(prvs_img, MaskedArray): + prvs_img = np.ma.masked_invalid(prvs_img) + np.ma.set_fill_value(prvs_img, prvs_img.min()) - if ~isinstance(next, MaskedArray): - next = np.ma.masked_invalid(next) - np.ma.set_fill_value(next, next.min()) + if ~isinstance(next_img, MaskedArray): + next_img = np.ma.masked_invalid(next_img) + np.ma.set_fill_value(next_img, next_img.min()) # scale between 0 and 255 - prvs = (prvs.filled() - prvs.min()) / (prvs.max() - prvs.min()) * 255 - next = (next.filled() - next.min()) / (next.max() - next.min()) * 255 + prvs_img = ((prvs_img.filled() - prvs_img.min()) / + (prvs_img.max() - prvs_img.min()) * 255) + + next_img = ((next_img.filled() - next_img.min()) / + (next_img.max() - next_img.min()) * 255) # convert to 8-bit - prvs = np.ndarray.astype(prvs, "uint8") - next = np.ndarray.astype(next, "uint8") + prvs_img = np.ndarray.astype(prvs_img, "uint8") + next_img = np.ndarray.astype(next_img, "uint8") # Lucas-Kanade # TODO: use the error returned by the OpenCV routine @@ -381,7 +386,8 @@ def track_features( flags=flags, minEigThreshold=min_eig_thr, ) - p1, st, __ = cv2.calcOpticalFlowPyrLK(prvs, next, p0, None, **params) + p1, st, __ = cv2.calcOpticalFlowPyrLK(prvs_img, next_img, + p0, None, **params) # keep only features that have been found st = st.squeeze() == 1 From 8935dee77933a340c1076ae6a5f63c9265c2007e Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 16 Aug 2019 09:30:38 +0200 Subject: [PATCH 44/54] Improve LK docstrings --- pysteps/motion/lucaskanade.py | 124 +++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 46 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index f450e3a7a..93c03bbb7 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -6,8 +6,10 @@ The Lucas-Kanade (LK) local feature tracking module. This module implements the interface to the local `Lucas-Kanade`_ routine -available in OpenCV_, including the interpolation of the sparse vectors over a -regular grid. +available in OpenCV_. + +For its dense method, it additionally interpolates the sparse vectors over a +regular grid to return a motion field. .. _OpenCV: https://opencv.org/ @@ -55,7 +57,8 @@ def dense_lucaskanade(input_images, size_opening=3, decl_scale=10, verbose=False): - """Run the Lucas-Kanade optical flow and interpolate the motion vectors. + """Run the Lucas-Kanade optical flow routine and interpolate the motion + vectors. .. _OpenCV: https://opencv.org/ @@ -65,80 +68,85 @@ def dense_lucaskanade(input_images, .. _MaskedArray:\ https://docs.scipy.org/doc/numpy/reference/maskedarray.baseclass.html#numpy.ma.MaskedArray - .. _`Shi-Tomasi`:\ - https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 - Interface to the OpenCV_ implementation of the local `Lucas-Kanade`_ optical - flow method applied in combination to the `Shi-Tomasi`_ corner detection - routine. The sparse motion vectors are finally interpolated to return the - whole motion field. + flow method applied in combination to a feature detection routine. + + The sparse motion vectors are finally interpolated to return the whole + motion field. Parameters ---------- input_images : array_like or MaskedArray_ Array of shape (T, m, n) containing a sequence of T two-dimensional - input images of shape (m, n). T = 2 is the minimum required number of - images. - With T > 2, all the sparse vectors detected are pooled together before + input images of shape (m, n). The indexing order in input_images is + assumed to be (time, latitude, longitude). + + T = 2 is the minimum required number of images. + With T > 2, all the resulting sparse vectors are pooled together for the final interpolation on a regular grid. - In case of an array_like, invalid values (Nans or infs) are masked. - The mask in the MaskedArray_ defines a region where velocity vectors are - not computed. + In case of array_like, invalid values (Nans or infs) are masked, + otherwise the mask of the MaskedArray_ is used. Such mask defines a + region where features are not detected for the tracking algorithm. lk_kwargs : dict, optional Optional dictionary containing keyword arguments for the `Lucas-Kanade`_ features tracking algorithm. See the documentation of - pysteps.motion.lucaskanade.track_features. + :py:func:`pysteps.motion.lucaskanade.track_features`. fd_method : {"ShiTomasi"}, optional - Name of the feature detection method to use. See the documentation - of pysteps.utils.interpolate. + Name of the feature detection routine. See the available methods in + :py:mod:`pysteps.utils.images`. fd_kwargs : dict, optional Optional dictionary containing keyword arguments for the features detection algorithm. - See the documentation of pysteps.utils.images.corner_detection. + See the documentation of :py:mod:`pysteps.utils.images`. interp_method : {"rbfinterp2d"}, optional - Name of the interpolation method to use. See the documentation - of pysteps.utils.interpolate. + Name of the interpolation method to use. See the available methods in + :py:mod:`pysteps.utils.interpolate`. interp_kwargs : dict, optional Optional dictionary containing keyword arguments for the interpolation - algorithm. See the documentation of pysteps.utils.interpolate. + algorithm. See the documentation of :py:mod:`pysteps.utils.interpolate`. dense : bool, optional If True, it returns the three-dimensional array (2,m,n) containing the dense x- and y-components of the motion field. If false, it returns the sparse motion vectors as 2-D xy and uv arrays, - where xy defines the vector locations, uv defines the x and y direction + where xy defines the vector positions, uv defines the x and y direction components of the vectors. nr_std_outlier : int, optional Maximum acceptable deviation from the mean in terms of number of - standard deviations. Any anomaly larger than this value is flagged as - outlier and excluded from the interpolation. + standard deviations. Any sparse vector with a deviation larger than + this threshold is flagged as outlier and excluded from the + interpolation. + See the documentation of + :py:func:`pysteps.utils.cleansing.detect_outliers`. k_outlier : int or None, optional The number of nearest neighbours used to localize the outlier detection. - If set to None, it employs all the data points (global detection). + See the documentation of + :py:func:`pysteps.utils.cleansing.detect_outliers`. size_opening : int, optional The size of the structuring element kernel in pixels. This is used to perform a binary morphological opening on the input fields in order to - filter isolated echoes due to clutter. - - If set to zero, the fitlering is not perfomed. + filter isolated echoes due to clutter. If set to zero, the filtering is not perfomed. + See the documentation of + :py:func:`pysteps.utils.images.morph_opening`. decl_scale : int, optional The scale declustering parameter in pixels used to reduce the number of redundant sparse vectors before the interpolation. Sparse vectors within this declustering scale are averaged together. - If set to less than 2 pixels, the declustering is not perfomed. + See the documentation of + :py:func:`pysteps.cleansing.cleansing.decluster`. verbose : bool, optional If set to True, it prints information about the program. @@ -147,15 +155,18 @@ def dense_lucaskanade(input_images, ------- out : array_like or tuple - If dense=True (the default), it returns the three-dimensional array - (2,m,n) containing the dense x- and y-components of the motion field in - units of pixels / timestep as given by the input array input_images. + If **dense=True** (the default), it returns the three-dimensional array + (2, m, n) containing the dense x- and y-components of the motion field + in units of [pixels/timestep] as given by the input array input_images. + Return a zero motion field of shape (2, m, n) when no motion is + detected. + + If **dense=False**, it returns a tuple containing the 2-dimensional + arrays xy and uv, where x, y define the vector locations, u, v define + the x and y direction components of the vectors. + Return two empty arrays when no motion is detected. - If dense=False, it returns a tuple containing the 2-dimensional arrays - xy and uv, where x, y define the vector locations, u, v define the x - and y direction components of the vectors. - Return a zero motion field when no motion is detected. See also -------- @@ -284,12 +295,16 @@ def track_features( verbose=False, ): """ - Interface to the OpenCV calcOpticalFlowPyrLK_ Lucas-Kanade features tracking - algorithm. + Interface to the OpenCV `Lucas-Kanade`_ features tracking algorithm + (cv.calcOpticalFlowPyrLK). + + .. _`Lucas-Kanade`:\ + https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 .. _calcOpticalFlowPyrLK:\ https://docs.opencv.org/3.4/dc/d6b/group__video__track.html#ga473e4b886d0bcc6b65831eb88ed93323 + .. _MaskedArray:\ https://docs.scipy.org/doc/numpy/reference/maskedarray.baseclass.html#numpy.ma.MaskedArray @@ -309,24 +324,24 @@ def track_features( tracking points (corners). winsize : tuple of int, optional - The winSize parameter in calcOpticalFlowPyrLK_. + The **winSize** parameter in calcOpticalFlowPyrLK_. It represents the size of the search window that it is used at each pyramid level. nr_levels : int, optional - The maxLevel parameter in calcOpticalFlowPyrLK_. + The **maxLevel** parameter in calcOpticalFlowPyrLK_. It represents the 0-based maximal pyramid level number. criteria : tuple of int, optional - The TermCriteria parameter in calcOpticalFlowPyrLK_ , - which specifies the termination criteria of the iterative search + The **TermCriteria** parameter in calcOpticalFlowPyrLK_ , + which specifies the termination criteria of the iterative search algorithm. flags : int, optional Operation flags, see documentation calcOpticalFlowPyrLK_. min_eig_thr : float, optional - The minEigThreshold parameter in calcOpticalFlowPyrLK_. + The **minEigThreshold** parameter in calcOpticalFlowPyrLK_. verbose : bool, optional Print the number of vectors that have been found. @@ -345,9 +360,26 @@ def track_features( Notes ----- - The tracking points can be obtained with the pysteps.utils.images.ShiTomasi_detection - routine. + The tracking points can be obtained with the + :py:func:`pysteps.utils.images.ShiTomasi_detection` routine. + + See also + -------- + + pysteps.motion.lucaskanade.dense_lucaskanade + + References + ---------- + + Bouguet, J.-Y.: Pyramidal implementation of the affine Lucas Kanade + feature tracker description of the algorithm, Intel Corp., 5, 4, + https://doi.org/10.1109/HPDC.2004.1323531, 2001 + + Lucas, B. D. and Kanade, T.: An iterative image registration technique with + an application to stereo vision, in: Proceedings of the 1981 DARPA Imaging + Understanding Workshop, pp. 121–130, 1981. """ + if not CV2_IMPORTED: raise MissingOptionalDependency( "opencv package is required for the calcOpticalFlowPyrLK() " From 43fb57fe13c211c5e945f6466b29c6cb69482dfb Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 16 Aug 2019 10:35:34 +0200 Subject: [PATCH 45/54] Allow for arbitrary number of dimensions --- pysteps/utils/cleansing.py | 78 ++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/pysteps/utils/cleansing.py b/pysteps/utils/cleansing.py index 7188070d4..67895d452 100644 --- a/pysteps/utils/cleansing.py +++ b/pysteps/utils/cleansing.py @@ -17,24 +17,26 @@ def decluster(coord, input_array, scale, min_samples=1, verbose=False): - """Decluster a set of sparse data points by aggregating (i.e., taking the - median value) all points within a certain distance (i.e., a cluster). + """Decluster a set of sparse data points by aggregating, that is, taking + the median value of all values lying within a certain distance (i.e., a + cluster). Parameters ---------- coord : array_like - Array of shape (n, 2) containing the coordinates of the input data into - a 2-dimensional space. + Array of shape (n, d) containing the coordinates of the input data into + a space of *d* dimensions. input_array : array_like - Array of shape (n) or (n, m), where n is the number of samples and m - the number of variables. - All values in input_array are required to have finite values. + Array of shape (n) or (n, m), where *n* is the number of samples and + *m* the number of variables. + All values in **input_array** are required to have finite values. scale : float or array_like - The scale parameter in the same units of coord. Data points within this - declustering scale are averaged together. + The **scale** parameter in the same units of **coord**. + It can be a scalar or an array of shape (d). + Data points within the declustering **scale** are aggregated. min_samples : int, optional The minimum number of samples for computing the median within a given @@ -47,15 +49,13 @@ def decluster(coord, input_array, scale, min_samples=1, verbose=False): ------- out : tuple of ndarrays - A two-element tuple (dinput, dcoord) containing the declustered input_array - (d, m) and coordinates (d, 2), where d is the new number of samples - (d < n). - + A two-element tuple (**out_coord**, **output_array**) containing the + declustered coordinates (l, d) and **input_array** (l, m), where *l* is + the new number of samples with (l <= n). """ coord = np.copy(coord) input_array = np.copy(input_array) - scale = np.float(scale) # check inputs if np.any(~np.isfinite(input_array)): @@ -74,9 +74,8 @@ def decluster(coord, input_array, scale, min_samples=1, verbose=False): if coord.ndim != 2: raise ValueError( - "coord must have 2 dimensions (n, 2), but it has %i" % coord.ndim + "coord must have 2 dimensions (n, d), but it has %i" % coord.ndim ) - if coord.shape[0] != input_array.shape[0]: raise ValueError( "the number of samples in the input_array does not match the " @@ -84,6 +83,21 @@ def decluster(coord, input_array, scale, min_samples=1, verbose=False): % (input_array.shape[0], coord.shape[0]) ) + if np.isscalar(scale): + scale = np.float(scale) + else: + scale = np.copy(scale) + if scale.ndim != 1: + raise ValueError( + "scale must have 1 dimension (d), but it has %i" % scale.ndim + ) + if scale.shape[0] != coord.shape[1]: + raise ValueError( + "scale must have %i elements, but it has %i" + % (coord.shape[1], scale.shape[0]) + ) + scale = scale[None, :] + # reduce original coordinates coord_ = np.floor(coord / scale) @@ -92,16 +106,14 @@ def decluster(coord, input_array, scale, min_samples=1, verbose=False): np.dtype((np.void, coord_.dtype.itemsize * coord_.shape[1])) ) __, idx = np.unique(coordb_, return_index=True) - ucoord_ = coord_[idx] + # TODO: why not simply using np.unique(coord_, axis=0) ? - # loop through these unique values and average vectors which belong to - # the same declustering grid cell + # loop through these unique values and average data points which belong to + # the same cluster dinput = np.empty(shape=(0, nvar)) - dcoord = np.empty(shape=(0, 2)) + dcoord = np.empty(shape=(0, coord.shape[1])) for i in range(ucoord_.shape[0]): - idx = np.logical_and( - coord_[:, 0] == ucoord_[i, 0], coord_[:, 1] == ucoord_[i, 1] - ) + idx = np.all(coord_ == ucoord_[i, :], axis=1) npoints = np.sum(idx) if npoints >= min_samples: dinput = np.append( @@ -131,21 +143,23 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): ---------- input_array : array_like - Array of shape (n) or (n, m), where n is the number of samples and m - the number of variables. If m > 1, the Mahalanobis distance is used. - All values in input_array are required to have finite values. + Array of shape (n) or (n, m), where *n* is the number of samples and + *m* the number of variables. If *m* > 1, the Mahalanobis distance + is used. + All values in **input_array** are required to have finite values. thr : float The number of standard deviations from the mean that defines an outlier. coord : array_like, optional Array of shape (n, d) containing the coordinates of the input data into - a space of d dimensions. Setting coord requires that k is not None. + a space of *d* dimensions. + Passing **coord** requires that **k** is not None. k : int or None, optional The number of nearest neighbours used to localize the outlier detection. If set to None (the default), it employs all the data points (global - detection). Setting k requires that coord is not None. + detection). Setting **k** requires that **coord** is not None. verbose : bool, optional Print out information. @@ -154,8 +168,8 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): ------- out : array_like - A boolean array of the same shape as input_array, with True values - indicating the outliers detected in input_array. + A boolean array of the same shape as **input_array**, with True values + indicating the outliers detected in **input_array**. """ input_array = np.copy(input_array) @@ -237,7 +251,7 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): if nvar == 1: - # in terms of velocity + # univariate thisdata = input_array[i] neighbours = input_array[inds[i, 1:]] @@ -248,7 +262,7 @@ def detect_outliers(input_array, thr, coord=None, k=None, verbose=False): else: - # mahalanobis distance + # multivariate (mahalanobis distance) thisdata = input_array[i, :] neighbours = input_array[inds[i, 1:], :].copy() From 0ace92b8cce34eb167c34f481066818874673433 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 16 Aug 2019 10:39:51 +0200 Subject: [PATCH 46/54] Improve docstrings --- pysteps/motion/lucaskanade.py | 35 ++++++++++---------- pysteps/utils/images.py | 60 ++++++++++++++++++++--------------- pysteps/utils/interpolate.py | 17 +++++----- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index 93c03bbb7..f10fb2eb2 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -78,12 +78,12 @@ def dense_lucaskanade(input_images, ---------- input_images : array_like or MaskedArray_ - Array of shape (T, m, n) containing a sequence of T two-dimensional - input images of shape (m, n). The indexing order in input_images is + Array of shape (T, m, n) containing a sequence of *T* two-dimensional + input images of shape (m, n). The indexing order in **input_images** is assumed to be (time, latitude, longitude). - T = 2 is the minimum required number of images. - With T > 2, all the resulting sparse vectors are pooled together for + *T* = 2 is the minimum required number of images. + With *T* > 2, all the resulting sparse vectors are pooled together for the final interpolation on a regular grid. In case of array_like, invalid values (Nans or infs) are masked, @@ -113,11 +113,11 @@ def dense_lucaskanade(input_images, algorithm. See the documentation of :py:mod:`pysteps.utils.interpolate`. dense : bool, optional - If True, it returns the three-dimensional array (2,m,n) containing the - dense x- and y-components of the motion field. - If false, it returns the sparse motion vectors as 2-D xy and uv arrays, - where xy defines the vector positions, uv defines the x and y direction - components of the vectors. + If True, it returns the three-dimensional array (2, m, n) containing + the dense x- and y-components of the motion field. + If false, it returns the sparse motion vectors as 2-D **xy** and **uv** + arrays, where **xy** defines the vector positions, **uv** defines the + x and y direction components of the vectors. nr_std_outlier : int, optional Maximum acceptable deviation from the mean in terms of number of @@ -136,7 +136,8 @@ def dense_lucaskanade(input_images, size_opening : int, optional The size of the structuring element kernel in pixels. This is used to perform a binary morphological opening on the input fields in order to - filter isolated echoes due to clutter. If set to zero, the filtering is not perfomed. + filter isolated echoes due to clutter. If set to zero, the filtering + is not perfomed. See the documentation of :py:func:`pysteps.utils.images.morph_opening`. @@ -162,12 +163,10 @@ def dense_lucaskanade(input_images, detected. If **dense=False**, it returns a tuple containing the 2-dimensional - arrays xy and uv, where x, y define the vector locations, u, v define - the x and y direction components of the vectors. + arrays **xy** and **uv**, where x, y define the vector locations, + u, v define the x and y direction components of the vectors. Return two empty arrays when no motion is detected. - - See also -------- @@ -350,12 +349,12 @@ def track_features( ------- xy : array_like - Array of shape (d, 2) with the x- and y-coordinates of d <= p detected - sparse motion vectors. + Array of shape (d, 2) with the x- and y-coordinates of *d* <= *p* + detected sparse motion vectors. uv : array_like - Array of shape (d, 2) with the u- and v-components of d <= p detected - sparse motion vectors. + Array of shape (d, 2) with the u- and v-components of *d* <= *p* + detected sparse motion vectors. Notes ----- diff --git a/pysteps/utils/images.py b/pysteps/utils/images.py index 82280cb5a..b0972808a 100644 --- a/pysteps/utils/images.py +++ b/pysteps/utils/images.py @@ -5,9 +5,13 @@ Image processing routines for pysteps. +.. _`Shi-Tomasi`:\ + https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#ga1d6bb77486c8f92d79c8793ad995d541 + + .. autosummary:: :toctree: ../generated/ - + ShiTomasi_detection morph_opening """ @@ -24,13 +28,13 @@ def ShiTomasi_detection(input_image, max_corners=500, quality_level=0.1, - min_distance=3, block_size=15, buffer_mask=0, + min_distance=3, block_size=15, buffer_mask=0, use_harris = False, k = 0.04, - verbose=False, + verbose=False, **kwargs): """ - Interface to the OpenCV `Shi-Tomasi`_ features detection method to detect - corners in the image. + Interface to the OpenCV `Shi-Tomasi`_ features detection method to detect + corners in an image. Corners are used for local tracking methods. @@ -39,7 +43,7 @@ def ShiTomasi_detection(input_image, max_corners=500, quality_level=0.1, .. _MaskedArray:\ https://docs.scipy.org/doc/numpy/reference/maskedarray.baseclass.html#numpy.ma.MaskedArray - + .. _`Harris detector`:\ https://docs.opencv.org/3.4.1/dd/d1a/group__imgproc__feature.html#gac1fc3598018010880e370e2f709b4345 @@ -53,40 +57,44 @@ def ShiTomasi_detection(input_image, max_corners=500, quality_level=0.1, input_image : array_like or MaskedArray_ Array of shape (m, n) containing the input image. - In case of an array_like, invalid values (Nans or infs) define a - validity mask, which represents the region where velocity vectors are not - computed. The corresponding fill value is taken as the minimum of all + In case of array_like, invalid values (Nans or infs) are masked, + otherwise the mask of the MaskedArray_ is used. Such mask defines a + region where features are not detected. + + The fill value for the masked pixels is taken as the minimum of all valid pixels. max_corners : int, optional - The maxCorners parameter in the `Shi-Tomasi`_ corner detection method. + The **maxCorners** parameter in the `Shi-Tomasi`_ corner detection + method. It represents the maximum number of points to be tracked (corners). If set to zero, all detected corners are used. quality_level : float, optional - The qualityLevel parameter in the `Shi-Tomasi`_ corner detection method. - It represents the minimal accepted quality for the points to be tracked - (corners). + The **qualityLevel** parameter in the `Shi-Tomasi`_ corner detection + method. + It represents the minimal accepted quality for the image corners. min_distance : int, optional - The minDistance parameter in the `Shi-Tomasi`_ corner detection method. - It represents minimum possible Euclidean distance in pixels between corners. + The **minDistance** parameter in the `Shi-Tomasi`_ corner detection + method. + It represents minimum possible Euclidean distance in pixels between + corners. block_size : int, optional - The blockSize parameter in the `Shi-Tomasi`_ corner detection method. + The **blockSize** parameter in the `Shi-Tomasi`_ corner detection method. It represents the window size in pixels used for computing a derivative covariation matrix over each pixel neighborhood. - + use_harris : bool, optional Whether to use a `Harris detector`_ or cornerMinEigenVal_. - + k : float, optional Free parameter of the Harris detector. buffer_mask : int, optional A mask buffer width in pixels. This extends the input mask (if any) - to help avoiding the erroneous interpretation of velocities near the - maximum range of the radars. + to limit edge effects. verbose : bool, optional Print the number of features detected. @@ -95,12 +103,12 @@ def ShiTomasi_detection(input_image, max_corners=500, quality_level=0.1, ------- points : array_like - Array of shape (p, 2) indicating the pixel coordinates of p detected + Array of shape (p, 2) indicating the pixel coordinates of *p* detected corners. - + References ---------- - + Jianbo Shi and Carlo Tomasi. Good features to track. In Computer Vision and Pattern Recognition, 1994. Proceedings CVPR'94., 1994 IEEE Computer Society Conference on, pages 593–600. IEEE, 1994. @@ -140,7 +148,7 @@ def ShiTomasi_detection(input_image, max_corners=500, quality_level=0.1, # convert to 8-bit input_image = np.ndarray.astype(input_image, "uint8") mask = (-1 * mask + 1).astype("uint8") - + params = dict( maxCorners=max_corners, qualityLevel=quality_level, @@ -161,8 +169,8 @@ def ShiTomasi_detection(input_image, max_corners=500, quality_level=0.1, def morph_opening(input_image, thr, n): - """Filter out small scale noise on the image by applying a binary morphological - opening (i.e., erosion then dilation). + """Filter out small scale noise on the image by applying a binary + morphological opening, that is, erosion followed by dilation. Parameters ---------- diff --git a/pysteps/utils/interpolate.py b/pysteps/utils/interpolate.py index 874669ffe..aea06866e 100644 --- a/pysteps/utils/interpolate.py +++ b/pysteps/utils/interpolate.py @@ -37,20 +37,21 @@ def rbfinterp2d( a 2-dimensional space. input_array : array_like - Array of shape (n) or (n, m), where n is the number of data points and + Array of shape (n) or (n, m), where *n* is the number of data points and m the number of co-located variables. - All values in input_array are required to have finite values. + All values in **input_array** are required to have finite values. xgrid, ygrid : array_like 1D arrays representing the coordinates of the target grid. - rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse - multiquadric", "bump"}, optional - The name of one of the available radial basis function based on the Euclidian - norm. See also the Notes section below. + rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse multiquadric", "bump"}, optional + The name of one of the available radial basis function based on the + Euclidian norm. See also the Notes section below. epsilon : float, optional - The shape parameter > 0 used to scale the input to the radial kernel. + The shape parameter used to scale the input to the radial kernel. + + A larger value for **epsilon** produces a smoother interpolation. k : int or None, optional The number of nearest neighbours used to speed-up the interpolation. @@ -73,7 +74,7 @@ def rbfinterp2d( x = (x - median(x)) / MAD / 1.4826 - where MAD = median(|x - median(x)|). + where MAD = median(abs(x - median(x))). The definitions of the radial basis functions are taken from the following wikipedia page: https://en.wikipedia.org/wiki/Radial_basis_function From 35fd3e71e1131efc2bdb50178d198b323602e5bd Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 16 Aug 2019 10:43:05 +0200 Subject: [PATCH 47/54] Fix wrongly deleted line --- pysteps/utils/cleansing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pysteps/utils/cleansing.py b/pysteps/utils/cleansing.py index 67895d452..309a66308 100644 --- a/pysteps/utils/cleansing.py +++ b/pysteps/utils/cleansing.py @@ -106,6 +106,7 @@ def decluster(coord, input_array, scale, min_samples=1, verbose=False): np.dtype((np.void, coord_.dtype.itemsize * coord_.shape[1])) ) __, idx = np.unique(coordb_, return_index=True) + ucoord_ = coord_[idx] # TODO: why not simply using np.unique(coord_, axis=0) ? # loop through these unique values and average data points which belong to From 32cffa03b415f098f6bbb7b3b32d98e96391bee8 Mon Sep 17 00:00:00 2001 From: Seppo Pulkkinen Date: Fri, 16 Aug 2019 13:19:07 -0600 Subject: [PATCH 48/54] Add check that the images don't contain only zero values --- pysteps/motion/proesmans.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pysteps/motion/proesmans.py b/pysteps/motion/proesmans.py index bca43fbf7..8e987b286 100644 --- a/pysteps/motion/proesmans.py +++ b/pysteps/motion/proesmans.py @@ -61,7 +61,8 @@ def proesmans(input_images, lam=50.0, num_iter=100, im = np.stack([im1, im2]) im_min = np.min(im) im_max = np.max(im) - im = (im - im_min) / (im_max - im_min) * 255.0 + if im_max - im_min > 1e-8: + im = (im - im_min) / (im_max - im_min) * 255.0 if filter_std > 0.0: im[0, :, :] = gaussian_filter(im[0, :, :], filter_std) From 3b961f90d43c8c0fe4e03efd875ef28901c807f1 Mon Sep 17 00:00:00 2001 From: Andres Perez Hortal Date: Fri, 16 Aug 2019 16:09:49 -0400 Subject: [PATCH 49/54] Add zero precipitation test for proesmans function --- pysteps/tests/test_motion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysteps/tests/test_motion.py b/pysteps/tests/test_motion.py index 65bda6c43..14a343f3a 100644 --- a/pysteps/tests/test_motion.py +++ b/pysteps/tests/test_motion.py @@ -242,7 +242,7 @@ def test_optflow_method_convergence(input_precip, optflow_method_name, ('vet', 2), ('vet', 3), ('darts', 9), - #('proesmans', 2) + ('proesmans', 2) ] From 878054c9774a3faa9dca71a3dd63a5d2912d62ca Mon Sep 17 00:00:00 2001 From: Andres Perez Hortal Date: Fri, 16 Aug 2019 16:12:04 -0400 Subject: [PATCH 50/54] Add test for proesmans's input arguments --- pysteps/tests/test_motion.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pysteps/tests/test_motion.py b/pysteps/tests/test_motion.py index 14a343f3a..8d3d366e4 100644 --- a/pysteps/tests/test_motion.py +++ b/pysteps/tests/test_motion.py @@ -278,7 +278,7 @@ def test_no_precipitation(optflow_method_name, num_times): ('lk', 2, np.inf), ('vet', 2, 3), ('darts', 9, 9), - # ('proesmans', 2,2), + ('proesmans', 2, 2), ] @@ -286,7 +286,6 @@ def test_no_precipitation(optflow_method_name, num_times): def test_input_shape_checks(optflow_method_name, minimum_input_frames, maximum_input_frames): - image_size = 100 motion_method = motion.get_method(optflow_method_name) From bf5170577f3c42de2b28c0ca8941a2b85cadd3e6 Mon Sep 17 00:00:00 2001 From: ned Date: Fri, 16 Aug 2019 10:55:00 +0200 Subject: [PATCH 51/54] More small docstring improvements --- pysteps/motion/lucaskanade.py | 11 ++++++----- pysteps/utils/cleansing.py | 4 ++-- pysteps/utils/interpolate.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pysteps/motion/lucaskanade.py b/pysteps/motion/lucaskanade.py index f10fb2eb2..db0eccf83 100644 --- a/pysteps/motion/lucaskanade.py +++ b/pysteps/motion/lucaskanade.py @@ -96,7 +96,7 @@ def dense_lucaskanade(input_images, :py:func:`pysteps.motion.lucaskanade.track_features`. fd_method : {"ShiTomasi"}, optional - Name of the feature detection routine. See the available methods in + Name of the feature detection routine. See feature detection methods in :py:mod:`pysteps.utils.images`. fd_kwargs : dict, optional @@ -105,7 +105,7 @@ def dense_lucaskanade(input_images, See the documentation of :py:mod:`pysteps.utils.images`. interp_method : {"rbfinterp2d"}, optional - Name of the interpolation method to use. See the available methods in + Name of the interpolation method to use. See interpolation methods in :py:mod:`pysteps.utils.interpolate`. interp_kwargs : dict, optional @@ -113,9 +113,10 @@ def dense_lucaskanade(input_images, algorithm. See the documentation of :py:mod:`pysteps.utils.interpolate`. dense : bool, optional - If True, it returns the three-dimensional array (2, m, n) containing + If True, return the three-dimensional array (2, m, n) containing the dense x- and y-components of the motion field. - If false, it returns the sparse motion vectors as 2-D **xy** and **uv** + + If False, return the sparse motion vectors as 2-D **xy** and **uv** arrays, where **xy** defines the vector positions, **uv** defines the x and y direction components of the vectors. @@ -150,7 +151,7 @@ def dense_lucaskanade(input_images, :py:func:`pysteps.cleansing.cleansing.decluster`. verbose : bool, optional - If set to True, it prints information about the program. + If set to True, print some information about the program. Returns ------- diff --git a/pysteps/utils/cleansing.py b/pysteps/utils/cleansing.py index 309a66308..4c38cdfc5 100644 --- a/pysteps/utils/cleansing.py +++ b/pysteps/utils/cleansing.py @@ -35,7 +35,7 @@ def decluster(coord, input_array, scale, min_samples=1, verbose=False): scale : float or array_like The **scale** parameter in the same units of **coord**. - It can be a scalar or an array of shape (d). + It can be a scalar or an array_like of shape (d). Data points within the declustering **scale** are aggregated. min_samples : int, optional @@ -51,7 +51,7 @@ def decluster(coord, input_array, scale, min_samples=1, verbose=False): out : tuple of ndarrays A two-element tuple (**out_coord**, **output_array**) containing the declustered coordinates (l, d) and **input_array** (l, m), where *l* is - the new number of samples with (l <= n). + the new number of samples with *l* <= *n*. """ coord = np.copy(coord) diff --git a/pysteps/utils/interpolate.py b/pysteps/utils/interpolate.py index aea06866e..66d13154c 100644 --- a/pysteps/utils/interpolate.py +++ b/pysteps/utils/interpolate.py @@ -38,7 +38,7 @@ def rbfinterp2d( input_array : array_like Array of shape (n) or (n, m), where *n* is the number of data points and - m the number of co-located variables. + *m* the number of co-located variables. All values in **input_array** are required to have finite values. xgrid, ygrid : array_like From bc85aeed02e0d3deef76d5c8e7eb5b14641f9afa Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 19 Aug 2019 10:52:54 +0200 Subject: [PATCH 52/54] Change coordinate normalization --- pysteps/utils/interpolate.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pysteps/utils/interpolate.py b/pysteps/utils/interpolate.py index 66d13154c..dcc621020 100644 --- a/pysteps/utils/interpolate.py +++ b/pysteps/utils/interpolate.py @@ -22,12 +22,12 @@ def rbfinterp2d( xgrid, ygrid, rbfunction="gaussian", - epsilon=1, + epsilon=5, k=50, nchunks=5, ): - """Fast kernel interpolation of a (multivariate) array over a 2D grid using - a radial basis function. + """Fast 2-D grid interpolation of a sparse (multivariate) array using a + radial basis function. Parameters ---------- @@ -37,16 +37,19 @@ def rbfinterp2d( a 2-dimensional space. input_array : array_like - Array of shape (n) or (n, m), where *n* is the number of data points and - *m* the number of co-located variables. + Array of shape (n) or (n, m) containing the values of the data points, + where *n* is the number of data points and *m* the number of co-located + variables. All values in **input_array** are required to have finite values. xgrid, ygrid : array_like - 1D arrays representing the coordinates of the target grid. + 1D arrays representing the coordinates of the 2-D output grid. rbfunction : {"gaussian", "multiquadric", "inverse quadratic", "inverse multiquadric", "bump"}, optional - The name of one of the available radial basis function based on the - Euclidian norm. See also the Notes section below. + The name of one of the available radial basis function based on a + normalized Euclidian norm. + + See also the Notes section below. epsilon : float, optional The shape parameter used to scale the input to the radial kernel. @@ -70,11 +73,12 @@ def rbfinterp2d( Notes ----- - The input coordinates are normalized before computing the euclidean norms: + The coordinates are normalized before computing the Euclidean norms: - x = (x - median(x)) / MAD / 1.4826 + x = (x - min(x)) / max[max(x) - min(x), max(y) - min(y)], + y = (y - min(y)) / max[max(x) - min(x), max(y) - min(y)], - where MAD = median(abs(x - median(x))). + where the min and max values are taken as the 2nd and 98th percentiles. The definitions of the radial basis functions are taken from the following wikipedia page: https://en.wikipedia.org/wiki/Radial_basis_function @@ -122,9 +126,9 @@ def rbfinterp2d( ) # normalize coordinates - mcoord = np.median(coord, axis=0) - madcoord = 1.4826 * np.median(np.abs(coord - mcoord), axis=0) - coord = (coord - mcoord) / madcoord + qcoord = np.percentile(coord, [2, 98], axis=0) + dextent = np.max(np.diff(qcoord, axis=0)) + coord = ( coord - qcoord[0, :] ) / dextent rbfunction = rbfunction.lower() if rbfunction not in _rbfunctions: @@ -137,7 +141,8 @@ def rbfinterp2d( # generate the target grid X, Y = np.meshgrid(xgrid, ygrid) grid = np.column_stack((X.ravel(), Y.ravel())) - grid = (grid - mcoord) / madcoord + # normalize the grid coordinates + grid = (grid - qcoord[0, :] ) / dextent # k-nearest interpolation if k is not None and k > 0: From 5cd0584218665bbbb422a05fbee94e6235225f56 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 19 Aug 2019 10:53:32 +0200 Subject: [PATCH 53/54] Fix arguments --- examples/LK_buffer_mask.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/LK_buffer_mask.py b/examples/LK_buffer_mask.py index 83d9b61f6..2a3c90a72 100644 --- a/examples/LK_buffer_mask.py +++ b/examples/LK_buffer_mask.py @@ -112,7 +112,7 @@ R.data[R.mask] = np.nan # Use default settings (i.e., no buffering of the radar mask) -fd_kwargs1 = {"buffer_mask":0, "quality_level_ST":0.1} +fd_kwargs1 = {"buffer_mask":0} xy, uv = LK_optflow(R, dense=False, fd_kwargs=fd_kwargs1) plt.imshow(ref_dbr, cmap=plt.get_cmap("Greys")) plt.imshow(mask, cmap=colors.ListedColormap(["black"]), alpha=0.5) @@ -142,7 +142,8 @@ # 'x,y,u,v = LK_optflow(.....)'. # with buffer -fd_kwargs2 = {"buffer_mask":20, "quality_level_ST":0.2} +buffer = 10 +fd_kwargs2 = {"buffer_mask":buffer} xy, uv = LK_optflow(R, dense=False, fd_kwargs=fd_kwargs2) plt.imshow(ref_dbr, cmap=plt.get_cmap("Greys")) plt.imshow(mask, cmap=colors.ListedColormap(["black"]), alpha=0.5) @@ -158,7 +159,7 @@ ) circle = plt.Circle((620, 245), 100, color="b", clip_on=False, fill=False) plt.gca().add_artist(circle) -plt.title("buffer_mask = 20") +plt.title("buffer_mask = %i" % buffer) plt.show() ################################################################################ @@ -229,8 +230,8 @@ score_2.append(skill(R_f2[i, :, :], R_o[i + 1, :, :])["corr_s"]) x = (np.arange(12) + 1) * 5 # [min] -plt.plot(x, score_1, label="no mask buffer") -plt.plot(x, score_2, label="with mask buffer") +plt.plot(x, score_1, label="buffer_mask = 0") +plt.plot(x, score_2, label="buffer_mask = %i" % buffer) plt.legend() plt.xlabel("Lead time [min]") plt.ylabel("Corr. coeff. []") From 3ccb17fc9de2c5a2326ed3805243a67511151661 Mon Sep 17 00:00:00 2001 From: ned Date: Mon, 19 Aug 2019 11:05:09 +0200 Subject: [PATCH 54/54] Add reference --- pysteps/utils/interpolate.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pysteps/utils/interpolate.py b/pysteps/utils/interpolate.py index dcc621020..c137952ea 100644 --- a/pysteps/utils/interpolate.py +++ b/pysteps/utils/interpolate.py @@ -54,7 +54,8 @@ def rbfinterp2d( epsilon : float, optional The shape parameter used to scale the input to the radial kernel. - A larger value for **epsilon** produces a smoother interpolation. + A smaller value for **epsilon** produces a smoother interpolation. More + details provided in the wikipedia reference page. k : int or None, optional The number of nearest neighbours used to speed-up the interpolation. @@ -80,8 +81,12 @@ def rbfinterp2d( where the min and max values are taken as the 2nd and 98th percentiles. - The definitions of the radial basis functions are taken from the following - wikipedia page: https://en.wikipedia.org/wiki/Radial_basis_function + References + ---------- + + Wikipedia contributors, "Radial basis function," Wikipedia, The Free Encyclopedia, + https://en.wikipedia.org/w/index.php?title=Radial_basis_function&oldid=906155047 + (accessed August 19, 2019). """ _rbfunctions = [