Projection#
Estimates bulk echo motion via optical flow (Farneback) and extrapolates each cell’s position forward by up to N scan intervals.
Lagrangian persistence: estimate cell motion and project future positions using optical flow.
The lagrangian extrapolation produces advection of the latest reflectivity field; evolution of the advection field is not taken into account.
This module computes cell motion vectors from radar reflectivity patterns using optical flow (Farneback algorithm). Motion vectors are then used to project cell positions forward in time, providing future cell locations for impact prediction and nowcasting.
Key capability: - Registration: Project previous frame’s cells to current position (backward projection) - Future projections: Project current cells forward 1-5 steps (forward extrapolation) - Optical flow computed on reflectivity fields, then applied to cell labels - Generates both displacement vectors (heading_x, heading_y) and projected labels
Output enables cell tracking and motion-based warnings in operational systems.
@TODO: Handle tiny cells (Delaunay/qhull failures) with convex hull fallback @TODO: Calibrate cell size thresholds from dataset analysis
- class adapt.modules.projection.module.RadarCellProjector(config)#
Bases:
objectCompute cell motion vectors and project future cell positions.
This class uses optical flow (Farneback algorithm) to estimate motion vectors from radar reflectivity patterns. Motion is then applied to segmented cell labels to generate:
Registration: Project previous frame’s cells to current position (validation: cells should match current segmentation if no change)
Future projections: Extrapolate current cells 1-5 steps forward (nowcasting: where will cells be in next minutes?)
Motion fields: Displacement vectors (heading_x, heading_y) for each pixel in pixels/frame
The projector operates on 2D datasets (already sliced at fixed altitude by processor). Flow is computed on reflectivity patterns; labels are projected using computed flow vectors.
Configuration#
Config dict structure:
- methodstr, default “adapt_default”
Currently only “adapt_default” (Farneback optical flow) supported.
- max_projection_stepsint, default 1, max 10
Number of future projections to compute (beyond registration).
- max_time_interval_minutesint, default 30
Maximum time gap between consecutive frames. Skips processing if time gap exceeds this (motion model breaks down with large gaps).
- nan_fill_valuefloat, default 0
Value to replace NaNs in reflectivity before flow computation. (NaNs from missing data, clutter removal, etc.)
- flow_paramsdict, optional
OpenCV Farneback optical flow parameters:
- pyr_scalefloat, default 0.5
Image pyramid scale (0.5 = 2x reduction per level)
- levelsint, default 3
Number of pyramid levels
- winsizeint, default 10
Averaging window size
- iterationsint, default 3
Iterations at each pyramid level
- poly_nint, default 5
Polynomial expansion size
- poly_sigmafloat, default 1.2
Gaussian standard deviation
- globaldict, optional
- tracking_fieldstr
Canonical field driving the optical flow (default: “reflectivity”)
Notes
Requires exactly 2 datasets (previous frame and current frame)
Flow is computed at previous-to-current transition
Registration (offset=0) uses flow from t-1→t0, projects t-1 labels
Future projections (offset=1+) extrapolate t0 forward using same flow
Fails gracefully: returns current dataset without projections if time gap too large
Processing time: 50-200 ms per frame pair
Projection results are cumulative: each step adds to displacement
Examples
>>> config = { ... "method": "adapt_default", ... "max_projection_steps": 3, ... "max_time_interval_minutes": 30, ... "flow_params": {"levels": 3, "winsize": 10} ... } >>> projector = RadarCellProjector(config) >>> ds_with_motion = projector.project([ds_t1, ds_t0]) >>> num_projections = ds_with_motion["cell_projections"].shape[0] >>> print( ... f"Generated {num_projections} projections (1 registration + {num_projections-1} future)" ... )
- project(ds_list)#
Project cells forward using optical flow motion vectors.
Computes optical flow from reflectivity patterns between two consecutive frames, then applies the flow to project cell labels backward (registration) and forward (future predictions). Output includes: - Projected cell labels (one per projection step) - Motion vectors (heading_x, heading_y in pixels/frame)
- Parameters:
ds_list (list of xr.Dataset) –
Exactly two datasets: [ds_previous, ds_current]
Each dataset must contain: - Dimensions: (y, x) - 2D (pre-sliced at fixed altitude) - Data variables: cell_labels, reflectivity - Coordinates: x, y - Attributes: time (datetime), z_level_m (altitude)
- Returns:
Modified copy of ds_current with additional variables:
- cell_projectionsDataArray with dims (frame_offset, y, x)
Projected cell labels for each time offset - frame_offset=0: Registration (t-1 cells projected to t0) - frame_offset=1+: Future projections (t0 cells extrapolated) - Values: cell label IDs (0=background, 1+=cells)
- heading_x, heading_yDataArray with dims (y, x)
Optical flow displacement vectors in pixels/frame - Positive: rightward/downward motion - Negative: leftward/upward motion
If time gap exceeds max_time_interval_minutes or other validation fails, returns ds_current WITHOUT projections (logged as warning).
- Return type:
xr.Dataset
Notes
Requires time coordinates in both datasets for gap validation
Dispatches to implementation based on config[“method”]
Currently only “adapt_default” (Farneback) is supported
Processing time: 50-200 ms per frame pair
Flow computation is robust to NaN values (filled with nan_fill_value)
Projection is deterministic: same input always produces same output
Examples
>>> projector = RadarCellProjector(config) >>> ds_with_motion = projector.project([ds_frame_t1, ds_frame_t0]) >>> >>> # Access projections >>> registration = ds_with_motion["cell_projections"].sel(frame_offset=0) >>> future_1 = ds_with_motion["cell_projections"].sel(frame_offset=1) >>> >>> # Access motion vectors >>> vx = ds_with_motion["heading_x"] >>> vy = ds_with_motion["heading_y"] >>> speed = np.sqrt(vx**2 + vy**2) # pixels/frame