-
Notifications
You must be signed in to change notification settings - Fork 16
Adding feature tracking operator #2148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Adam Gainford (A-Gainford)
wants to merge
21
commits into
main
Choose a base branch
from
simple-track
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
f479cb9
added simple-track operator and tests
A-Gainford 4f56efb
added example tracking recipe
A-Gainford 7d89cbb
added temporary feature cbar definitons (limits to be set dynamically)
A-Gainford 7fa3a8d
Merge remote-tracking branch 'origin/main' into simple-track
A-Gainford 98f39d9
added simple-track dependency to pyproject and env.yml
A-Gainford b8b1f6a
removed unnecessary logging, added set_under option to feature plot
A-Gainford ca3eea6
Merge branch 'main' into simple-track
jfrost-mo ecac8cc
added restriction of xy grid spacing on input cube
A-Gainford 038d2c0
changed feature colorbar properties
A-Gainford 356696b
Update conda lockfiles
A-Gainford 9e4e484
Update src/CSET/operators/__init__.py
A-Gainford 4326eb8
Update src/CSET/operators/feature.py
A-Gainford e66ef41
Update .gitignore
A-Gainford 7308be8
updated docstrings
A-Gainford 1f22fcb
Update tests/operators/test_feature.py
A-Gainford 31bea32
Update src/CSET/recipes/example_recipes/example_feature_track.yaml
A-Gainford 21f3deb
Update tests/operators/test_feature.py
A-Gainford 782137c
added custom feature colorbar to _colormaps
A-Gainford b2e8b7f
replaced deprecated cmap setting method, moved to _colormaps
A-Gainford a2de8cf
added new line to gitignore
A-Gainford 080988f
removed feature entries from _colorbar_definition.json
A-Gainford File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,3 +138,6 @@ dmypy.json | |
|
|
||
| # NFS synchronisation files | ||
| .nfs* | ||
|
|
||
| # MacOS temp files | ||
| .DS_Store | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ dependencies = [ | |
| "scikit-image", | ||
| "scores", | ||
| "dask", | ||
| "simple-track", | ||
| "xarray", | ||
| ] | ||
|
|
||
|
|
||
|
A-Gainford marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| 3b279c2f279476b8a7ab53f290620f1f6b157fdd95a89eba4792ee0af6b5730e requirements/environment.yml | ||
| 7cc3817dbb45d3ab5a22dba86386f620270a7f637d70978695c0fbc6f329c148 requirements/environment.yml |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
A-Gainford marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| # © Crown copyright, Met Office (2022-2026) and CSET contributors. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| """Operators for identifying and tracking features.""" | ||
|
|
||
| import logging | ||
| import os | ||
|
|
||
| import iris | ||
| import iris.cube | ||
| import iris.util | ||
| import numpy as np | ||
| from simpletrack.track import Tracker | ||
|
|
||
|
|
||
| def track( | ||
| cube: iris.cube.Cube, | ||
| threshold: float, | ||
| under_threshold: bool = False, | ||
| min_size: int = 4, | ||
| retain_lifetime_on_split: bool = True, | ||
| tracking_nbhood: int = 5, | ||
| overlap_threshold: float = 0.3, | ||
| save_data: bool = False, | ||
| ): | ||
| """Track features between subsequent timesteps. | ||
|
|
||
| Parameters | ||
| ---------- | ||
|
A-Gainford marked this conversation as resolved.
|
||
| cube: iris.cube.Cube | ||
| The cube to identify features in. The cube must be 3D and contain a time coordinate | ||
| and horizontal coordinates of xy type (not latitude/longitude). | ||
| threshold: float | ||
| The threshold value for feature detection. | ||
| under_threshold: bool, optional | ||
| If set to True, features are identified where the data is below the threshold. | ||
| If set to False, features are identified where the data is above the threshold. | ||
| Default is False. | ||
| min_size: int, optional | ||
| The minimum number of contiguous grid points required for a feature to be tracked. | ||
| Default is 4. | ||
| retain_lifetime_on_split: bool, optional | ||
| If set to True, the lifetime of a feature is retained when it splits into | ||
| multiple features. If set to False, the lifetime is reset when a feature splits. | ||
| Default is True. | ||
| tracking_nbhood: int, optional | ||
| The size of the neighbourhood used for tracking features between timesteps. | ||
| This dictates the maximum pixel radius from a feature centroid at which new features could | ||
| reasonably be spawned. | ||
| Default is 5. | ||
| overlap_threshold: float, optional | ||
| The minimum overlap required between features in consecutive timesteps for | ||
| them to be considered the same feature. | ||
| Default is 0.3. | ||
| save_data: bool, optional | ||
| If set to True, all tracking data is saved to disk for further analysis (including csv | ||
| and txt files containing feature properties that are not returned in output cubes). | ||
| Default is False. | ||
|
|
||
| Returns | ||
| ------- | ||
| tracking_cubes: iris.cube.CubeList | ||
| A list of iris cubes containing tracking data, including feature ID, lifetime, | ||
| and locations of initiating features. | ||
|
|
||
| Notes | ||
| ----- | ||
| This operator uses the Simple-Track package to track features between timesteps. Simple-Track is a | ||
| data-agnostic, threshold-based object tracking algorithm for 2D data. Features are tracked between | ||
| consecutive frames of data by projecting feature fields onto common timeframes and matching | ||
| between them based on the degree of overlap. Matched features retain the same identification | ||
| between all tracked fields, while new features are assigned a unique label. | ||
| Thus, Simple-Track compiles comprehensive information about feature merging, splitting, accretion, | ||
| initiation and dissipation. | ||
|
|
||
| Currently outputs three cubes containing the following data: | ||
| "feature_id": | ||
| A 2D field containing the unique label assigned to each feature, which is retained | ||
| if the feature is tracked across multiple timesteps. This cube can be used as a mask | ||
| to identify the location of the tracked feature throughout the evaluation period. | ||
| "feature_lifetime": | ||
| A 2D field containing the lifetime of each feature in terms of the number of | ||
| timesteps it has been tracked for. This cube can be used to distinguish between | ||
| mature and fresh features. | ||
| "feature_init": | ||
| A 2D binary field indicating the location of newly initiated features at each timestep. | ||
| These features are identified as having a lifetime of 1 AND have initiated sufficiently | ||
| far from other, existing features that they are not considered to have spawned from them. | ||
|
|
||
| Links | ||
| ---------- | ||
| .. https://github.com/ParaChute-UK/simple-track | ||
|
|
||
| Examples | ||
| -------- | ||
| >>> tracking_cubes = feature.track(threshold=2) | ||
| >>> lifetime_cube = tracking_cubes.extract_cube("feature_lifetime") | ||
| # Plot the final timestep of lifetime cube. This will show | ||
| # the lifetime of features that have been tracked for multiple previous | ||
| # timesteps, as well as new features that have just been initiated. | ||
| >>> iplt.pcolormesh(lifetime_cube[-1,:,:],cmap=mpl.cm.bwr) | ||
| >>> plt.gca().coastlines('10m') | ||
| >>> plt.clim(-5,5) | ||
| >>> plt.colorbar() | ||
| >>> plt.show() | ||
|
|
||
| """ | ||
| # Check that the input cube has horizontal coordinates of xy type, not latitude/longitude | ||
| _check_xy_coords(cube) | ||
|
|
||
| # Setup config | ||
| tracker_config = { | ||
| "FEATURE": { | ||
| "threshold": threshold, | ||
| "under_threshold": under_threshold, | ||
| "min_size": min_size, | ||
| }, | ||
| "TRACKING": { | ||
| "retain_lifetime_on_split": retain_lifetime_on_split, | ||
| "overlap_nbhood": tracking_nbhood, | ||
| "overlap_threshold": overlap_threshold, | ||
| }, | ||
| "OUTPUT": { | ||
| "save_data": save_data, | ||
| "experiment_name": "feature_tracking", | ||
| "path": f"{os.getcwd()}/tracking_data", | ||
| }, | ||
| } | ||
| logging.debug(f"Tracker config: {tracker_config}") | ||
|
|
||
| # Get cube data into a dict to pass to Tracker | ||
| times = cube.coord("time").points | ||
| time_units = cube.coord("time").units | ||
| times_dt = [time_units.num2pydate(t) for t in times] | ||
| cube_dict = { | ||
| time: cube_slice.data | ||
| for time, cube_slice in zip(times_dt, cube.slices_over("time"), strict=True) | ||
| } | ||
|
|
||
| # Run tracking, returning Timeline object | ||
| timeline = Tracker(tracker_config).run(cube_dict) | ||
| logging.debug("Tracking completed") | ||
|
|
||
| # Use input cube as template to make returned cube | ||
| # By iterating over all cube times, this will ensure all data is present | ||
| # If a Frame at the given time is not contained in the timeline, error is raised | ||
| output_type_and_methods = { | ||
| "lifetime": { | ||
| "getter": "lifetime_field", | ||
| "cube_name": "feature_lifetime", | ||
| }, | ||
| "feature": { | ||
| "getter": "feature_field", | ||
| "cube_name": "feature_id", | ||
| }, | ||
| "init": { | ||
| "getter": "get_init_field", | ||
| "cube_name": "feature_init", | ||
| }, | ||
| } | ||
|
|
||
| tracking_cubelist = iris.cube.CubeList() | ||
| for output_type in output_type_and_methods: | ||
| tracking_data = [] | ||
| for time in times_dt: | ||
| frame = timeline.get_frame(time) | ||
| getter = getattr(frame, output_type_and_methods[output_type]["getter"]) | ||
| if callable(getter): | ||
| tracking_data.append(getter()) | ||
| else: | ||
| tracking_data.append(getter) | ||
|
|
||
| # Convert to numpy arrays | ||
| tracking_data = np.stack(tracking_data, axis=0) | ||
|
|
||
| # Create cubes | ||
| tracking_cube = cube.copy(data=tracking_data) | ||
| tracking_cube.long_name = output_type_and_methods[output_type]["cube_name"] | ||
| tracking_cube.standard_name = None | ||
| tracking_cube.var_name = None | ||
| tracking_cube.units = "1" | ||
| tracking_cubelist.append(tracking_cube) | ||
|
|
||
| return tracking_cubelist | ||
|
|
||
|
|
||
| def _check_xy_coords(cube: iris.cube.Cube) -> None: | ||
| """Check that the input cube has horizontal coordinates of xy type, not latitude/longitude. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| cube: iris.cube.Cube | ||
| An iris cube containing horizontal coordinates. | ||
|
|
||
| Raises | ||
| ------ | ||
| ValueError | ||
| If the input cube has horizontal coordinates of latitude/longitude type. | ||
| """ | ||
| hzntl_coords = [ | ||
| coord | ||
| for coord in cube.coords() | ||
| if iris.util.guess_coord_axis(coord) in ["X", "Y"] | ||
| ] | ||
| invalid_coord_names = ["latitude", "longitude", "grid_latitude", "grid_longitude"] | ||
| for coord in hzntl_coords: | ||
| if coord.name() in invalid_coord_names: | ||
| raise ValueError( | ||
| f"Input cube {cube} has horizontal coordinate {coord}, " | ||
| "which is not of xy type. Please provide a cube with horizontal " | ||
| "coordinates of xy type." | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
src/CSET/recipes/example_recipes/example_feature_track.yaml
|
A-Gainford marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| category: Quick Look | ||
| title: Precipitation flux feature lifetime spatial plot | ||
| description: | | ||
| Uses the feature.track operator to identify and track features in a cube with "time" series coordinate, | ||
| and then plots the lifetime of the identified features as a spatial plot. | ||
|
|
||
| steps: | ||
| - operator: read.read_cubes | ||
| file_paths: $INPUT_PATHS | ||
|
|
||
| - operator: filters.filter_cubes | ||
| constraint: | ||
| operator: constraints.generate_var_constraint | ||
| varname: precipitation_flux | ||
|
|
||
| - operator: feature.track | ||
| threshold: 3 | ||
| save_data: False # Whether to save raw tracking data for further analysis | ||
|
|
||
| # Filter tracking cubelist to just one of "feature_lifetime", "feature_id" or "feature_init" | ||
| - operator: filters.filter_cubes | ||
| constraint: | ||
| operator: constraints.generate_var_constraint | ||
| varname: feature_lifetime | ||
|
|
||
| - operator: plot.spatial_pcolormesh_plot |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.