def _sal_detect_objects(precip, thr_factor, thr_quantile, tstorm_kwargs): """ Detect coherent precipitation objects using a multi-threshold approach from :cite:`Feldmann2021`. Parameters ---------- precip: array-like Array of shape (m,n) containing input data. Nan values are ignored. thr_factor: float Factor used to compute the detection threshold as in eq. 1 of :cite:`WHZ2009`. If not None, this is used to identify coherent objects enclosed by the threshold contour `thr_factor * thr_quantile(precip)`. thr_quantile: float The wet quantile between 0 and 1 used to define the detection threshold. Required if `thr_factor` is not None. tstorm_kwargs: dict Optional dictionary containing keyword arguments for the tstorm feature detection algorithm. If None, default values are used. See the documentation of :py:func:`pysteps.feature.tstorm.detection`. Returns ------- precip_objects: pd.DataFrame Dataframe containing all detected cells and their respective properties. """ if not PANDAS_IMPORTED: raise MissingOptionalDependency( "The pandas package is required for the SAL " "verification method but it is not installed" ) if not SKIMAGE_IMPORTED: raise MissingOptionalDependency( "The scikit-image package is required for the SAL " "verification method but it is not installed" ) if thr_factor is not None and thr_quantile is None: raise ValueError("You must pass thr_quantile, too") if tstorm_kwargs is None: tstorm_kwargs = dict() if thr_factor is not None: zero_value = np.nanmin(precip) threshold = thr_factor * np.nanquantile( precip[precip > zero_value], thr_quantile ) tstorm_kwargs = { "minmax": tstorm_kwargs.get("minmax", threshold), "maxref": tstorm_kwargs.get("maxref", threshold + 1e-5), "mindiff": tstorm_kwargs.get("mindiff", 1e-5), "minref": tstorm_kwargs.get("minref", threshold), } _, labels = tstorm_detect.detection(precip, **tstorm_kwargs) labels = labels.astype(int) precip_objects = regionprops_table( labels, intensity_image=precip, properties=REGIONPROPS ) return pd.DataFrame(precip_objects)
def test_feature_tstorm_detection(source, output_feat, dry_input, max_num_features): pytest.importorskip("skimage") pytest.importorskip("pandas") if not dry_input: input, metadata = get_precipitation_fields(0, 0, True, True, None, source) input = input.squeeze() input, __ = to_reflectivity(input, metadata) else: input = np.zeros((50, 50)) time = "000" output = detection(input, time=time, output_feat=output_feat, max_num_features=max_num_features) if output_feat: assert isinstance(output, np.ndarray) assert output.ndim == 2 assert output.shape[1] == 2 if max_num_features is not None: assert output.shape[0] <= max_num_features else: assert isinstance(output, tuple) assert len(output) == 2 assert isinstance(output[0], DataFrame) assert isinstance(output[1], np.ndarray) if max_num_features is not None: assert output[0].shape[0] <= max_num_features assert output[0].shape[1] == 9 assert list(output[0].columns) == [ "ID", "time", "x", "y", "cen_x", "cen_y", "max_ref", "cont", "area", ] assert (output[0].time == time).all() assert output[1].ndim == 2 assert output[1].shape == input.shape if not dry_input: assert output[0].shape[0] > 0 assert sorted(list(output[0].ID)) == sorted( list(np.unique(output[1]))[1:]) else: assert output[0].shape[0] == 0 assert output[1].sum() == 0
# Extract the list of timestamps timelist = metadata["timestamps"] pprint(metadata) ############################################################################### # Example of thunderstorm identification in a single timestep. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # The function tstorm_detect.detection requires a 2-D input image, all further inputs are # optional. input_image = Z[2, :, :].copy() time = timelist[2] cells_id, labels = tstorm_detect.detection( input_image, time=time, ) ############################################################################### # Properties of one of the identified cells: print(cells_id.iloc[0]) ############################################################################### # Example of thunderstorm tracking over a timeseries # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # The tstorm-dating function requires the entire pre-loaded time series. # The first two timesteps are required to initialize the # flow prediction and are not used to compute tracks. track_list, cell_list, label_list = tstorm_dating.dating( input_video=Z,
def dating( input_video, timelist, mintrack=3, cell_list=None, label_list=None, start=0, minref=35, maxref=48, mindiff=6, minsize=50, minmax=41, mindis=10, dyn_thresh=False, ): """ This function performs the thunderstorm detection and tracking DATing. It requires a 3-D input array that contains the temporal succession of the 2-D data array of each timestep. On each timestep the detection is performed, the identified objects are advected with a flow prediction and the advected objects are matched to the newly identified objects of the next timestep. The last portion re-arranges the data into tracks sorted by ID-number. Parameters ---------- input_video : array-like Array of shape (t,m,n) containing input image, with t being the temporal dimension and m,n the spatial dimensions. Thresholds are tuned to maximum reflectivity in dBZ with a spatial resolution of 1 km and a temporal resolution of 5 min. Nan values are ignored. timelist : list List of length t containing string of time and date of each (m,n) field. mintrack : int, optional minimum duration of cell-track to be counted. The default is 3 time steps. cell_list : list or None, optional If you wish to expand an existing list of cells, insert previous cell-list here. The default is None. If not None, requires that label_list has the same length. label_list : list or None, optional If you wish to expand an existing list of cells, insert previous label-list here. The default is None. If not None, requires that cell_list has the same length. start : int, optional If you wish to expand an existing list of cells, the input video must contain 2 timesteps prior to the merging. The start can then be set to 2, allowing the motion vectors to be formed from the first three grids and continuing the cell tracking from there. The default is 0, which initiates a new tracking sequence. minref : float, optional Lower threshold for object detection. Lower values will be set to NaN. The default is 35 dBZ. maxref : float, optional Upper threshold for object detection. Higher values will be set to this value. The default is 48 dBZ. mindiff : float, optional Minimal difference between two identified maxima within same area to split area into two objects. The default is 6 dBZ. minsize : float, optional Minimal area for possible detected object. The default is 50 pixels. minmax : float, optional Minimum value of maximum in identified objects. Objects with a maximum lower than this will be discarded. The default is 41 dBZ. mindis : float, optional Minimum distance between two maxima of identified objects. Objects with a smaller distance will be merged. The default is 10 km. Returns ------- track_list : list of dataframes Each dataframe contains the track and properties belonging to one cell ID. Columns of dataframes: ID - cell ID, time - time stamp, x - array of all x-coordinates of cell, y - array of all y-coordinates of cell, cen_x - x-coordinate of cell centroid, cen_y - y-coordinate of cell centroid, max_ref - maximum (reflectivity) value of cell, cont - cell contours cell_list : list of dataframes Each dataframe contains the detected cells and properties belonging to one timestep. The IDs are already matched to provide a track. Columns of dataframes: ID - cell ID, time - time stamp, x - array of all x-coordinates of cell, y - array of all y-coordinates of cell, cen_x - x-coordinate of cell centroid, cen_y - y-coordinate of cell centroid, max_ref - maximum (reflectivity) value of cell, cont - cell contours label_list : list of arrays Each (n,m) array contains the gridded IDs of the cells identified in the corresponding timestep. The IDs are already matched to provide a track. """ if not SKIMAGE_IMPORTED: raise MissingOptionalDependency( "skimage is required for thunderstorm DATing " "but it is not installed") if not PANDAS_IMPORTED: raise MissingOptionalDependency( "pandas is required for thunderstorm DATing " "but it is not installed") # Check arguments if cell_list is None or label_list is None: cell_list = [] label_list = [] else: if not len(cell_list) == len(label_list): raise ValueError("len(cell_list) != len(label_list)") if start > len(timelist): raise ValueError("start > len(timelist)") oflow_method = motion.get_method("LK") max_ID = 0 for t in range(start, len(timelist)): cells_id, labels = tstorm_detect.detection( input_video[t, :, :], minref=minref, maxref=maxref, mindiff=mindiff, minsize=minsize, minmax=minmax, mindis=mindis, time=timelist[t], ) if len(cell_list) < 2: cell_list.append(cells_id) label_list.append(labels) cid = np.unique(labels) max_ID = np.nanmax([np.nanmax(cid), max_ID]) + 1 continue if t >= 2: flowfield = oflow_method(input_video[t - 2:t + 1, :, :]) cells_id, max_ID, newlabels = tracking(cells_id, cell_list[-1], labels, flowfield, max_ID) cid = np.unique(newlabels) # max_ID = np.nanmax([np.nanmax(cid), max_ID]) cell_list.append(cells_id) label_list.append(newlabels) track_list = couple_track(cell_list[2:], int(max_ID), mintrack) return track_list, cell_list, label_list