dea-tools 0.4.8.dev12__tar.gz → 0.4.8.dev13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/PKG-INFO +3 -1
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/bom.py +2 -2
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/temporal.py +359 -61
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/pyproject.toml +3 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/.gitignore +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/LICENSE +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/README.md +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/__init__.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/__main__.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/__init__.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/animations.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/changefilmstrips.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/crophealth.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/deacoastlines.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/geomedian.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/imageexport.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/miningrehab.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/wetlandsinsighttool.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/app/widgetconstructors.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/bandindices.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/classification.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/coastal.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/dask.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/datahandling.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/landcover.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/maps.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/mosaics/README.md +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/mosaics/__init__.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/mosaics/cog.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/mosaics/styling.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/mosaics/utils.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/mosaics/vrt.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/plotting.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/spatial.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/validation.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/waterbodies.py +0 -0
- {dea_tools-0.4.8.dev12 → dea_tools-0.4.8.dev13}/Tools/dea_tools/wetlands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dea-tools
|
|
3
|
-
Version: 0.4.8.
|
|
3
|
+
Version: 0.4.8.dev13
|
|
4
4
|
Summary: Open-source tools for geospatial analysis with Digital Earth Australia, Open Data Cube, and Xarray
|
|
5
5
|
Project-URL: Homepage, https://knowledge.dea.ga.gov.au/notebooks/Tools/
|
|
6
6
|
Project-URL: Repository, https://github.com/GeoscienceAustralia/dea-notebooks
|
|
@@ -77,6 +77,8 @@ Requires-Dist: opencv-python>=4.6.0.66; extra == 'all'
|
|
|
77
77
|
Requires-Dist: pydotplus>=2.0.0; extra == 'all'
|
|
78
78
|
Requires-Dist: statsmodels>=0.14.0; extra == 'all'
|
|
79
79
|
Requires-Dist: sunriset>=1.0.0; extra == 'all'
|
|
80
|
+
Provides-Extra: cv
|
|
81
|
+
Requires-Dist: opencv-python>=4.6.0.66; extra == 'cv'
|
|
80
82
|
Provides-Extra: dask-gateway
|
|
81
83
|
Requires-Dist: dask-gateway>=2023.1.0; extra == 'dask-gateway'
|
|
82
84
|
Provides-Extra: datacube
|
|
@@ -31,7 +31,7 @@ from dateutil import parser
|
|
|
31
31
|
def get_stations(
|
|
32
32
|
time=None,
|
|
33
33
|
observation="http://bom.gov.au/waterdata/services/parameters/Water Course Discharge",
|
|
34
|
-
url="
|
|
34
|
+
url="https://www.bom.gov.au/waterdata/services"
|
|
35
35
|
):
|
|
36
36
|
"""Get list of stations
|
|
37
37
|
|
|
@@ -56,7 +56,7 @@ def get_station_data(
|
|
|
56
56
|
station,
|
|
57
57
|
time=None,
|
|
58
58
|
observation="http://bom.gov.au/waterdata/services/parameters/Water Course Discharge",
|
|
59
|
-
url="
|
|
59
|
+
url="https://www.bom.gov.au/waterdata/services"
|
|
60
60
|
):
|
|
61
61
|
"""
|
|
62
62
|
Query Gauge Data.
|
|
@@ -16,13 +16,13 @@ here: https://gis.stackexchange.com/questions/tagged/open-data-cube).
|
|
|
16
16
|
If you would like to report an issue with this script, file one on
|
|
17
17
|
GitHub: https://github.com/GeoscienceAustralia/dea-notebooks/issues/new
|
|
18
18
|
|
|
19
|
-
Last modified:
|
|
19
|
+
Last modified: October 2025
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import warnings
|
|
23
23
|
|
|
24
24
|
import dask
|
|
25
|
-
import dask.array as
|
|
25
|
+
import dask.array as daskarray
|
|
26
26
|
import numpy as np
|
|
27
27
|
import pandas as pd
|
|
28
28
|
import scipy.signal
|
|
@@ -30,6 +30,9 @@ import xarray as xr
|
|
|
30
30
|
from odc.geo.xr import assign_crs
|
|
31
31
|
from packaging import version
|
|
32
32
|
from scipy.stats import t
|
|
33
|
+
import concurrent.futures
|
|
34
|
+
from tqdm import tqdm
|
|
35
|
+
from skimage.registration import optical_flow_ilk, optical_flow_tvl1
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
def allNaN_arg(da, dim, stat):
|
|
@@ -298,13 +301,9 @@ def xr_phenology(
|
|
|
298
301
|
"ROS": np.float32,
|
|
299
302
|
}
|
|
300
303
|
da_template = da.isel(time=0).drop("time")
|
|
301
|
-
template = xr.Dataset(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
for var_name, var_dtype in stats_dtype.items()
|
|
305
|
-
if var_name in stats
|
|
306
|
-
}
|
|
307
|
-
)
|
|
304
|
+
template = xr.Dataset({
|
|
305
|
+
var_name: da_template.astype(var_dtype) for var_name, var_dtype in stats_dtype.items() if var_name in stats
|
|
306
|
+
})
|
|
308
307
|
da_all_time = da.chunk({"time": -1})
|
|
309
308
|
|
|
310
309
|
lazy_phenology = da_all_time.map_blocks(
|
|
@@ -431,9 +430,7 @@ def fourier_mean(x, n=3, step=5):
|
|
|
431
430
|
for j in range(x.shape[1]):
|
|
432
431
|
y = np.fft.fft(x[i, j, :])
|
|
433
432
|
for k in range(n):
|
|
434
|
-
result[i, j, k] = np.mean(
|
|
435
|
-
np.abs(y[1 + k * step : ((k + 1) * step + 1) or None])
|
|
436
|
-
)
|
|
433
|
+
result[i, j, k] = np.mean(np.abs(y[1 + k * step : ((k + 1) * step + 1) or None]))
|
|
437
434
|
|
|
438
435
|
return result
|
|
439
436
|
|
|
@@ -448,9 +445,7 @@ def fourier_std(x, n=3, step=5):
|
|
|
448
445
|
for j in range(x.shape[1]):
|
|
449
446
|
y = np.fft.fft(x[i, j, :])
|
|
450
447
|
for k in range(n):
|
|
451
|
-
result[i, j, k] = np.std(
|
|
452
|
-
np.abs(y[1 + k * step : ((k + 1) * step + 1) or None])
|
|
453
|
-
)
|
|
448
|
+
result[i, j, k] = np.std(np.abs(y[1 + k * step : ((k + 1) * step + 1) or None]))
|
|
454
449
|
|
|
455
450
|
return result
|
|
456
451
|
|
|
@@ -465,9 +460,7 @@ def fourier_median(x, n=3, step=5):
|
|
|
465
460
|
for j in range(x.shape[1]):
|
|
466
461
|
y = np.fft.fft(x[i, j, :])
|
|
467
462
|
for k in range(n):
|
|
468
|
-
result[i, j, k] = np.median(
|
|
469
|
-
np.abs(y[1 + k * step : ((k + 1) * step + 1) or None])
|
|
470
|
-
)
|
|
463
|
+
result[i, j, k] = np.median(np.abs(y[1 + k * step : ((k + 1) * step + 1) or None]))
|
|
471
464
|
|
|
472
465
|
return result
|
|
473
466
|
|
|
@@ -602,9 +595,7 @@ def temporal_statistics(da, stats):
|
|
|
602
595
|
da_all_time = da.chunk({"time": -1})
|
|
603
596
|
|
|
604
597
|
# apply function across chunks
|
|
605
|
-
lazy_ds = da_all_time.map_blocks(
|
|
606
|
-
temporal_statistics, kwargs={"stats": stats}, template=template
|
|
607
|
-
)
|
|
598
|
+
lazy_ds = da_all_time.map_blocks(temporal_statistics, kwargs={"stats": stats}, template=template)
|
|
608
599
|
|
|
609
600
|
try:
|
|
610
601
|
crs = da.odc.geobox.crs
|
|
@@ -650,28 +641,23 @@ def temporal_statistics(da, stats):
|
|
|
650
641
|
n3 = zz[:, :, 2]
|
|
651
642
|
|
|
652
643
|
# intialise dataset with first statistic
|
|
653
|
-
ds = xr.DataArray(
|
|
654
|
-
|
|
655
|
-
)
|
|
644
|
+
ds = xr.DataArray(n1, attrs=attrs, coords={x_dim: x, y_dim: y}, dims=[y_dim, x_dim]).to_dataset(
|
|
645
|
+
name=stats[0] + "_n1"
|
|
646
|
+
)
|
|
656
647
|
|
|
657
648
|
# add other datasets
|
|
658
649
|
for i, j in zip([n2, n3], ["n2", "n3"]):
|
|
659
|
-
ds[stats[0] + "_" + j] = xr.DataArray(
|
|
660
|
-
i, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"]
|
|
661
|
-
)
|
|
650
|
+
ds[stats[0] + "_" + j] = xr.DataArray(i, attrs=attrs, coords={"x": x, "y": y}, dims=["y", "x"])
|
|
662
651
|
else:
|
|
663
652
|
# simpler if first function isn't fourier transform
|
|
664
653
|
first_func = stats_dict.get(str(stats[0]))
|
|
665
654
|
ds = first_func(da)
|
|
666
655
|
|
|
667
656
|
# convert back to xarray dataset
|
|
668
|
-
ds = xr.DataArray(
|
|
669
|
-
ds, attrs=attrs, coords={x_dim: x, y_dim: y}, dims=[y_dim, x_dim]
|
|
670
|
-
).to_dataset(name=stats[0])
|
|
657
|
+
ds = xr.DataArray(ds, attrs=attrs, coords={x_dim: x, y_dim: y}, dims=[y_dim, x_dim]).to_dataset(name=stats[0])
|
|
671
658
|
|
|
672
659
|
# loop through the other functions
|
|
673
660
|
for stat in stats[1:]:
|
|
674
|
-
|
|
675
661
|
# handle the fourier transform examples
|
|
676
662
|
if stat in ("f_std", "f_median", "f_mean"):
|
|
677
663
|
stat_func = stats_dict.get(str(stat))
|
|
@@ -681,9 +667,7 @@ def temporal_statistics(da, stats):
|
|
|
681
667
|
n3 = zz[:, :, 2]
|
|
682
668
|
|
|
683
669
|
for i, j in zip([n1, n2, n3], ["n1", "n2", "n3"]):
|
|
684
|
-
ds[stat + "_" + j] = xr.DataArray(
|
|
685
|
-
i, attrs=attrs, coords={x_dim: x, y_dim: y}, dims=[y_dim, x_dim]
|
|
686
|
-
)
|
|
670
|
+
ds[stat + "_" + j] = xr.DataArray(i, attrs=attrs, coords={x_dim: x, y_dim: y}, dims=[y_dim, x_dim])
|
|
687
671
|
|
|
688
672
|
else:
|
|
689
673
|
# Select a stats function from the dictionary
|
|
@@ -731,12 +715,8 @@ def time_buffer(input_date, buffer="30 days", output_format="%Y-%m-%d"):
|
|
|
731
715
|
`input_date='2018-01-01'` and `buffer='30 days'`
|
|
732
716
|
"""
|
|
733
717
|
# Use assertions to check we have the correct function input
|
|
734
|
-
assert isinstance(
|
|
735
|
-
|
|
736
|
-
), "Input date must be a string in quotes in 'yyyy-mm-dd' format"
|
|
737
|
-
assert isinstance(
|
|
738
|
-
buffer, str
|
|
739
|
-
), "Buffer must be a string supported by `pandas.Timedelta`, e.g. '5 days'"
|
|
718
|
+
assert isinstance(input_date, str), "Input date must be a string in quotes in 'yyyy-mm-dd' format"
|
|
719
|
+
assert isinstance(buffer, str), "Buffer must be a string supported by `pandas.Timedelta`, e.g. '5 days'"
|
|
740
720
|
|
|
741
721
|
# Convert inputs to pandas format
|
|
742
722
|
buffer = pd.Timedelta(buffer)
|
|
@@ -828,11 +808,7 @@ class LinregressResult:
|
|
|
828
808
|
|
|
829
809
|
def __repr__(self):
|
|
830
810
|
return "LinregressResult({})".format(
|
|
831
|
-
", ".join(
|
|
832
|
-
"{}={}".format(k, getattr(self, k))
|
|
833
|
-
for k in dir(self)
|
|
834
|
-
if not k.startswith("_")
|
|
835
|
-
)
|
|
811
|
+
", ".join("{}={}".format(k, getattr(self, k)) for k in dir(self) if not k.startswith("_"))
|
|
836
812
|
)
|
|
837
813
|
|
|
838
814
|
|
|
@@ -1027,9 +1003,7 @@ def xr_regression(
|
|
|
1027
1003
|
assert dim in x.dims, f"Array `x` does not contain dimension '{dim}'."
|
|
1028
1004
|
|
|
1029
1005
|
# Assert that both arrays have the same length along "dim"
|
|
1030
|
-
assert len(x[dim]) == len(
|
|
1031
|
-
y[dim]
|
|
1032
|
-
), f"Arrays `x` and `y` have different lengths along dimension '{dim}'."
|
|
1006
|
+
assert len(x[dim]) == len(y[dim]), f"Arrays `x` and `y` have different lengths along dimension '{dim}'."
|
|
1033
1007
|
|
|
1034
1008
|
# Apply optional outlier masking to x and y variable
|
|
1035
1009
|
if outliers_y is not None:
|
|
@@ -1069,7 +1043,7 @@ def xr_regression(
|
|
|
1069
1043
|
if dask.is_dask_collection(cor):
|
|
1070
1044
|
_pvalue_lazy = dask.delayed(_pvalue)
|
|
1071
1045
|
pval = xr.DataArray(
|
|
1072
|
-
|
|
1046
|
+
daskarray.from_delayed(
|
|
1073
1047
|
_pvalue_lazy(tstats, n, alternative),
|
|
1074
1048
|
shape=cor.shape,
|
|
1075
1049
|
dtype=cor.dtype,
|
|
@@ -1086,18 +1060,16 @@ def xr_regression(
|
|
|
1086
1060
|
)
|
|
1087
1061
|
|
|
1088
1062
|
# Combine into single dataset
|
|
1089
|
-
regression_ds = xr.merge(
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
]
|
|
1100
|
-
)
|
|
1063
|
+
regression_ds = xr.merge([
|
|
1064
|
+
cov.rename("cov").astype(np.float32),
|
|
1065
|
+
cor.rename("cor").astype(np.float32),
|
|
1066
|
+
r2.rename("r2").astype(np.float32),
|
|
1067
|
+
slope.rename("slope").astype(np.float32),
|
|
1068
|
+
intercept.rename("intercept").astype(np.float32),
|
|
1069
|
+
pval.rename("pvalue").astype(np.float32),
|
|
1070
|
+
stderr.rename("stderr").astype(np.float32),
|
|
1071
|
+
n.rename("n").astype(np.int16),
|
|
1072
|
+
])
|
|
1101
1073
|
|
|
1102
1074
|
return regression_ds
|
|
1103
1075
|
|
|
@@ -1155,3 +1127,329 @@ def calculate_stsad(vec, window_size=365, step=10, progress=None, window="hann")
|
|
|
1155
1127
|
progress=progress,
|
|
1156
1128
|
window=window,
|
|
1157
1129
|
)
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def _ilk_optical_flow(a, b, feature_kwargs=None, **kwargs):
|
|
1133
|
+
"""Compute optical flow using the scikit-image `optical_flow_ilk` method."""
|
|
1134
|
+
|
|
1135
|
+
# Set default params for optical flow analysis
|
|
1136
|
+
params = {"radius": 20}
|
|
1137
|
+
params.update(kwargs)
|
|
1138
|
+
|
|
1139
|
+
# Run optical flow analysis
|
|
1140
|
+
return optical_flow_ilk(a, b, **params)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _tvl1_optical_flow(a, b, feature_kwargs=None, **kwargs):
|
|
1144
|
+
"""Compute optical flow using the scikit-image `optical_flow_tvl1` method."""
|
|
1145
|
+
|
|
1146
|
+
# Run optical flow analysis
|
|
1147
|
+
return optical_flow_tvl1(a, b, **kwargs)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _farneback_optical_flow(a, b, feature_kwargs=None, **kwargs):
|
|
1151
|
+
"""Compute optical flow using the OpenCV `cv.calcOpticalFlowFarneback` method."""
|
|
1152
|
+
|
|
1153
|
+
# Attempt to import OpenCV and raise an error if not available
|
|
1154
|
+
try:
|
|
1155
|
+
import cv2 as cv
|
|
1156
|
+
except ImportError as e:
|
|
1157
|
+
raise ImportError(
|
|
1158
|
+
"`cv2` is required for optical flow analysis with `method='farneback'`. "
|
|
1159
|
+
"Please install DEA Tools with the `[cv]` or `[notebooks]` extra, e.g.: "
|
|
1160
|
+
"`pip install dea-tools[notebooks]`"
|
|
1161
|
+
) from e
|
|
1162
|
+
|
|
1163
|
+
# Set default params for optical flow analysis
|
|
1164
|
+
params = {
|
|
1165
|
+
"pyr_scale": 0.5,
|
|
1166
|
+
"levels": 3,
|
|
1167
|
+
"winsize": 15,
|
|
1168
|
+
"iterations": 3,
|
|
1169
|
+
"poly_n": 5,
|
|
1170
|
+
"poly_sigma": 1.2,
|
|
1171
|
+
"flags": 0,
|
|
1172
|
+
}
|
|
1173
|
+
params.update(kwargs)
|
|
1174
|
+
|
|
1175
|
+
# Run optical flow analysis
|
|
1176
|
+
flow = cv.calcOpticalFlowFarneback(a, b, None, **params)
|
|
1177
|
+
return flow[..., 1], flow[..., 0]
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def _deepflow_optical_flow(a, b, feature_kwargs=None, **kwargs):
|
|
1181
|
+
"""Compute optical flow using the OpenCV `cv.optflow.createOptFlow_DeepFlow` method."""
|
|
1182
|
+
|
|
1183
|
+
# Attempt to import OpenCV and raise an error if not available
|
|
1184
|
+
try:
|
|
1185
|
+
import cv2 as cv
|
|
1186
|
+
except ImportError as e:
|
|
1187
|
+
raise ImportError(
|
|
1188
|
+
"`cv2` is required for optical flow analysis with `method='deepflow'`. "
|
|
1189
|
+
"Please install DEA Tools with the `[cv]` or `[notebooks]` extra, e.g.: "
|
|
1190
|
+
"`pip install dea-tools[notebooks]`"
|
|
1191
|
+
) from e
|
|
1192
|
+
|
|
1193
|
+
# Run optical flow analysis
|
|
1194
|
+
flow = cv.optflow.createOptFlow_DeepFlow().calc(a, b, None)
|
|
1195
|
+
return flow[..., 1], flow[..., 0]
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def _lucas_kanade_optical_flow(a, b, feature_kwargs=None, **kwargs):
|
|
1199
|
+
"""Compute optical flow using the OpenCV `cv.calcOpticalFlowPyrLK` method.
|
|
1200
|
+
|
|
1201
|
+
This is a sparse optical flow method, which will return optical flow
|
|
1202
|
+
for a series of point locations identified using `cv.goodFeaturesToTrack`.
|
|
1203
|
+
"""
|
|
1204
|
+
# Attempt to import OpenCV and raise an error if not available
|
|
1205
|
+
try:
|
|
1206
|
+
import cv2 as cv
|
|
1207
|
+
except ImportError as e:
|
|
1208
|
+
raise ImportError(
|
|
1209
|
+
"`cv2` is required for optical flow analysis with `method='lucas_kanade'`. "
|
|
1210
|
+
"Please install DEA Tools with the `[cv]` or `[notebooks]` extra, e.g.: "
|
|
1211
|
+
"`pip install dea-tools[notebooks]`"
|
|
1212
|
+
) from e
|
|
1213
|
+
|
|
1214
|
+
# Use empty dict if nothing is provided
|
|
1215
|
+
if feature_kwargs is None:
|
|
1216
|
+
feature_kwargs = {}
|
|
1217
|
+
|
|
1218
|
+
# Set default params for feature extraction (ShiTomasi corner detection)
|
|
1219
|
+
feature_params = {
|
|
1220
|
+
"mask": None,
|
|
1221
|
+
"maxCorners": 20000,
|
|
1222
|
+
"qualityLevel": 0.1,
|
|
1223
|
+
"minDistance": 10,
|
|
1224
|
+
"blockSize": 15,
|
|
1225
|
+
}
|
|
1226
|
+
feature_params.update(feature_kwargs)
|
|
1227
|
+
|
|
1228
|
+
# Set default params for optical flow analysis
|
|
1229
|
+
params = {
|
|
1230
|
+
"winSize": (25, 25),
|
|
1231
|
+
"maxLevel": 1,
|
|
1232
|
+
"criteria": (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 30, 0.03),
|
|
1233
|
+
}
|
|
1234
|
+
params.update(kwargs)
|
|
1235
|
+
|
|
1236
|
+
# Extract good features to track
|
|
1237
|
+
p0 = cv.goodFeaturesToTrack(a, **feature_params)
|
|
1238
|
+
|
|
1239
|
+
# Raise error if no points were found
|
|
1240
|
+
if p0 is None:
|
|
1241
|
+
raise ValueError("No valid points to track found.")
|
|
1242
|
+
|
|
1243
|
+
# Run optical flow analysis
|
|
1244
|
+
p1, st, _ = cv.calcOpticalFlowPyrLK(a, b, p0, None, **params)
|
|
1245
|
+
|
|
1246
|
+
# Compute displacement vectors
|
|
1247
|
+
u = p1[:, 0, 0] - p0[:, 0, 0] # horizontal displacement (x)
|
|
1248
|
+
v = p1[:, 0, 1] - p0[:, 0, 1] # vertical displacement (y)
|
|
1249
|
+
|
|
1250
|
+
# Mask out invalid values
|
|
1251
|
+
v[st.squeeze() != 1] = np.nan
|
|
1252
|
+
u[st.squeeze() != 1] = np.nan
|
|
1253
|
+
|
|
1254
|
+
return v, u, p1
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def xr_optical_flow(
|
|
1258
|
+
da,
|
|
1259
|
+
baseline="dynamic",
|
|
1260
|
+
method="ilk",
|
|
1261
|
+
rescale_units=False,
|
|
1262
|
+
parallel=True,
|
|
1263
|
+
feature_kwargs=None,
|
|
1264
|
+
**kwargs,
|
|
1265
|
+
):
|
|
1266
|
+
"""
|
|
1267
|
+
Compute optical flow between xarray.DataArray observations.
|
|
1268
|
+
|
|
1269
|
+
Optical flow can be computed using a variety of dense and sparse
|
|
1270
|
+
methods from scikit-image or OpenCV. Several different baselines
|
|
1271
|
+
are supported, including dynamic baselines where change is computed
|
|
1272
|
+
between each consecutive pair of timesteps.
|
|
1273
|
+
|
|
1274
|
+
Parameters
|
|
1275
|
+
----------
|
|
1276
|
+
da : xarray.DataArray
|
|
1277
|
+
Input data representing either a temporal image sequence, or a single
|
|
1278
|
+
array that will be compared against `baseline`.
|
|
1279
|
+
baseline : str or xr.DataArray, optional
|
|
1280
|
+
Defines the baseline or reference array used to compute optical flow:
|
|
1281
|
+
* "dynamic": Calculate optical flow independently on each pair of
|
|
1282
|
+
timesteps, using the first array in each pair as the reference
|
|
1283
|
+
* "first": Compare every timestep against the first timestep
|
|
1284
|
+
* ``xr.DataArray``: Compare every timestep against a custom array
|
|
1285
|
+
method : str, optional
|
|
1286
|
+
Optical flow algorithm to use:
|
|
1287
|
+
- "ilk": Dense iterative Lucas–Kanade (scikit-image, fast and robust)
|
|
1288
|
+
- "tvl1": Dense Total Variation L1 (scikit-image, more accurate but slower)
|
|
1289
|
+
- "farneback": Dense Gunnar Farneback dense optical flow (OpenCV)
|
|
1290
|
+
- "deepflow": Dense DeepFlow (OpenCV, accurate but slower)
|
|
1291
|
+
- "lucas_kanade": Sparse pyramidal Lucas–Kanade (OpenCV, uses goodFeaturesToTrack)
|
|
1292
|
+
rescale_units : bool, optional
|
|
1293
|
+
By default, ``u``, ``v`` and ``magnitude`` are returned in pixel
|
|
1294
|
+
units. Optionally, results can instead be re-scaled by pixel
|
|
1295
|
+
resolution to get outputs in real-world units (note however
|
|
1296
|
+
that this can interfere with quiver plotting using ``xarray``.)
|
|
1297
|
+
parallel : bool, optional
|
|
1298
|
+
If True, computations are parallelised across time steps.
|
|
1299
|
+
feature_kwargs : dict, optional
|
|
1300
|
+
Extra keyword arguments passed to feature detection functions
|
|
1301
|
+
(used only for sparse 'lucas_kanade' method).
|
|
1302
|
+
**kwargs : dict
|
|
1303
|
+
Additional keyword arguments passed to optical flow functions.
|
|
1304
|
+
|
|
1305
|
+
Returns
|
|
1306
|
+
-------
|
|
1307
|
+
xarray.Dataset
|
|
1308
|
+
Dataset containing:
|
|
1309
|
+
- ``v``: Vertical (y-axis) component of optical flow.
|
|
1310
|
+
- ``u``: Horizontal (x-axis) component of optical flow.
|
|
1311
|
+
- ``magnitude``: Euclidean norm of the vertical and horizontal
|
|
1312
|
+
flow components, often representing either displacement distance
|
|
1313
|
+
or speed.
|
|
1314
|
+
"""
|
|
1315
|
+
# Get dimension names
|
|
1316
|
+
y_dim, x_dim = da.odc.spatial_dims
|
|
1317
|
+
|
|
1318
|
+
# Determine if baseline is an array or a keyword
|
|
1319
|
+
is_array = isinstance(baseline, xr.DataArray)
|
|
1320
|
+
has_time = "time" in da.dims
|
|
1321
|
+
|
|
1322
|
+
# Define dict linking functions to each analysis method
|
|
1323
|
+
method_dict = {
|
|
1324
|
+
"ilk": _ilk_optical_flow,
|
|
1325
|
+
"tvl1": _tvl1_optical_flow,
|
|
1326
|
+
"farneback": _farneback_optical_flow,
|
|
1327
|
+
"deepflow": _deepflow_optical_flow,
|
|
1328
|
+
"lucas_kanade": _lucas_kanade_optical_flow,
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
# Select relevant function
|
|
1332
|
+
try:
|
|
1333
|
+
flow_func = method_dict[method]
|
|
1334
|
+
except KeyError:
|
|
1335
|
+
raise ValueError(
|
|
1336
|
+
f"Unsupported method '{method}'. Use one of 'ilk', 'tvl1', 'farneback', 'deepflow', or 'lucas_kanade'."
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
# Raise error if a time series baseline is provided but `da` does not contain time
|
|
1340
|
+
if not is_array and not has_time:
|
|
1341
|
+
raise ValueError(
|
|
1342
|
+
f"The '{baseline}' baseline option requires `da` to have a time dimension. "
|
|
1343
|
+
"Provide time-series data to `da`, or use a different `baseline`."
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
# Raise error if a baseline array is provided, but
|
|
1347
|
+
if is_array and "time" in baseline.dims:
|
|
1348
|
+
if len(baseline.time) > 1:
|
|
1349
|
+
raise ValueError(f"The provided `baseline` array must not contain multiple timesteps.")
|
|
1350
|
+
|
|
1351
|
+
# Raise error if "lucas_kanade" is provided alongside a "dynamic" baseline
|
|
1352
|
+
if not is_array:
|
|
1353
|
+
if (method == "lucas_kanade") and (baseline == "dynamic"):
|
|
1354
|
+
raise ValueError(
|
|
1355
|
+
"To ensure that consistent features are returned for all timesteps, "
|
|
1356
|
+
"the `lucas_kanade` method is not compatible with `baseline='dynamic'`. "
|
|
1357
|
+
"Try `baseline='first'` or pass a custom array to `baseline`."
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
# Rescale both array and baseline to 8 bit for analysis
|
|
1361
|
+
da_min, da_max = da.min(), da.max()
|
|
1362
|
+
da = ((da - da_min) / (da_max - da_min) * 255).astype(np.uint8)
|
|
1363
|
+
if is_array:
|
|
1364
|
+
baseline = ((baseline - da_min) / (da_max - da_min) * 255).astype(np.uint8)
|
|
1365
|
+
|
|
1366
|
+
# Determine indices to iterate over for different baseline options
|
|
1367
|
+
if is_array and not has_time:
|
|
1368
|
+
indices = [0]
|
|
1369
|
+
elif is_array and has_time:
|
|
1370
|
+
indices = range(len(da.time))
|
|
1371
|
+
elif baseline in ("dynamic", "first"):
|
|
1372
|
+
indices = range(1, len(da.time))
|
|
1373
|
+
else:
|
|
1374
|
+
raise ValueError(
|
|
1375
|
+
f"Invalid baseline: {baseline}. Use one of 'dynamic', 'first', or provide a custom `xr.DataArray`."
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
def select_pair(t):
|
|
1379
|
+
# Custom baseline arrray
|
|
1380
|
+
if is_array:
|
|
1381
|
+
return baseline, da.isel(time=t) if has_time else da
|
|
1382
|
+
|
|
1383
|
+
# First: Compare every array against the first array
|
|
1384
|
+
if baseline == "first":
|
|
1385
|
+
return da.isel(time=0), da.isel(time=t)
|
|
1386
|
+
|
|
1387
|
+
# Dynamic: Compare every array against the previous array
|
|
1388
|
+
return da.isel(time=t - 1), da.isel(time=t)
|
|
1389
|
+
|
|
1390
|
+
def compute_flow(t):
|
|
1391
|
+
# Select pairs of arrays to analyse
|
|
1392
|
+
a, b = select_pair(t)
|
|
1393
|
+
|
|
1394
|
+
# Run optical flow analysis
|
|
1395
|
+
flow_outputs = flow_func(a.values, b.values, feature_kwargs, **kwargs)
|
|
1396
|
+
|
|
1397
|
+
# Unpack outputs of function
|
|
1398
|
+
try:
|
|
1399
|
+
v, u = flow_outputs
|
|
1400
|
+
except ValueError:
|
|
1401
|
+
v, u, p1 = flow_outputs
|
|
1402
|
+
|
|
1403
|
+
# Optionally re-scale coordinates by resolution
|
|
1404
|
+
if rescale_units:
|
|
1405
|
+
v *= da.odc.geobox.resolution.y
|
|
1406
|
+
u *= da.odc.geobox.resolution.x
|
|
1407
|
+
|
|
1408
|
+
# Add time dimension if necessary
|
|
1409
|
+
if has_time:
|
|
1410
|
+
b = b.expand_dims("time")
|
|
1411
|
+
v, u = v[None], u[None] # add time axis
|
|
1412
|
+
|
|
1413
|
+
# Return as xarray data
|
|
1414
|
+
if method == "lucas_kanade":
|
|
1415
|
+
# Convert point coordinates to spatial coordinates
|
|
1416
|
+
x, y = da.odc.geobox.translate_pix(0.5, 0.5).affine * p1.squeeze().T
|
|
1417
|
+
|
|
1418
|
+
# Determine coords and dims
|
|
1419
|
+
dims = ("time", "feature") if has_time else ("feature",)
|
|
1420
|
+
coords = {"x": (("feature",), x), "y": (("feature",), y)}
|
|
1421
|
+
|
|
1422
|
+
# Add time coordinates if required
|
|
1423
|
+
if has_time:
|
|
1424
|
+
coords["time"] = b.time
|
|
1425
|
+
|
|
1426
|
+
return xr.Dataset(
|
|
1427
|
+
data_vars={"v": (dims, v), "u": (dims, u)},
|
|
1428
|
+
coords=coords,
|
|
1429
|
+
)
|
|
1430
|
+
else:
|
|
1431
|
+
return xr.Dataset(
|
|
1432
|
+
data_vars={"v": (b.dims, v), "u": (b.dims, u)},
|
|
1433
|
+
coords=b.coords,
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
# Run analysis in parallel
|
|
1437
|
+
if parallel:
|
|
1438
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
1439
|
+
flow_results = list(
|
|
1440
|
+
tqdm(
|
|
1441
|
+
executor.map(compute_flow, indices),
|
|
1442
|
+
total=len(indices),
|
|
1443
|
+
desc=f"Computing optical flow ({method}) in parallel",
|
|
1444
|
+
)
|
|
1445
|
+
)
|
|
1446
|
+
|
|
1447
|
+
# Run analysis in series
|
|
1448
|
+
else:
|
|
1449
|
+
flow_results = [compute_flow(t) for t in tqdm(indices, desc=f"Computing optical flow ({method})")]
|
|
1450
|
+
|
|
1451
|
+
# Combine all outputs
|
|
1452
|
+
ds = flow_results[0] if len(flow_results) == 1 else xr.concat(flow_results, dim="time")
|
|
1453
|
+
|
|
1454
|
+
# Add magnitude
|
|
1455
|
+
return ds.assign({"magnitude": (ds.u**2 + ds.v**2) ** 0.5})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|