pygndc 1.0.0__py3-none-any.whl
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.
- pygndc/__init__.py +92 -0
- pygndc/__main__.py +202 -0
- pygndc/_version.py +11 -0
- pygndc/analysis/__init__.py +24 -0
- pygndc/analysis/vegetation.py +160 -0
- pygndc/dataset/__init__.py +9 -0
- pygndc/dataset/dataset.py +684 -0
- pygndc/exceptions.py +48 -0
- pygndc/interop/__init__.py +19 -0
- pygndc/interop/interop.py +209 -0
- pygndc/models/GNDCNetwork.py +228 -0
- pygndc/models/GNDCNetworkTorch.py +459 -0
- pygndc/models/__init__.py +5 -0
- pygndc/reader/__init__.py +9 -0
- pygndc/reader/reader.py +1154 -0
- pygndc/utils/__init__.py +20 -0
- pygndc/utils/compression.py +19 -0
- pygndc/utils/config_utils.py +101 -0
- pygndc/utils/date_utils.py +94 -0
- pygndc/utils/math_utils.py +156 -0
- pygndc/viewer/__init__.py +9 -0
- pygndc/viewer/interactive.py +1867 -0
- pygndc-1.0.0.dist-info/METADATA +247 -0
- pygndc-1.0.0.dist-info/RECORD +28 -0
- pygndc-1.0.0.dist-info/WHEEL +5 -0
- pygndc-1.0.0.dist-info/entry_points.txt +2 -0
- pygndc-1.0.0.dist-info/licenses/LICENSE +21 -0
- pygndc-1.0.0.dist-info/top_level.txt +1 -0
pygndc/__init__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pygndc - Geographic Neural Data Cube
|
|
3
|
+
|
|
4
|
+
A Python library for compressing geospatial time-series data using neural networks
|
|
5
|
+
|
|
6
|
+
Quick Start:
|
|
7
|
+
import pygndc
|
|
8
|
+
|
|
9
|
+
# High-level API (recommended)
|
|
10
|
+
with pygndc.open("satellite.gndc") as ds:
|
|
11
|
+
print(ds.crs, ds.bounds, ds.shape)
|
|
12
|
+
frame = ds.read(t=0) # (H, W, C)
|
|
13
|
+
frame = ds.read_at_time("2024-06-15") # continuous interpolation
|
|
14
|
+
ts = ds.sample(lon=116.5, lat=39.9) # (T, C) point query
|
|
15
|
+
|
|
16
|
+
# Low-level API (backward compatible)
|
|
17
|
+
reader = pygndc.GNDCReader()
|
|
18
|
+
reader.load("model.gndc")
|
|
19
|
+
frame = reader.reconstruct_single_frame(0)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Fix Intel OpenMP conflict
|
|
23
|
+
import os
|
|
24
|
+
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
25
|
+
|
|
26
|
+
from pygndc._version import __version__, __author__, __license__
|
|
27
|
+
|
|
28
|
+
# Public API - Reader
|
|
29
|
+
from pygndc.reader import GNDCReader
|
|
30
|
+
|
|
31
|
+
# Public API - Dataset (high-level)
|
|
32
|
+
from pygndc.dataset import GNDCDataset
|
|
33
|
+
|
|
34
|
+
# Public API - Analysis
|
|
35
|
+
from pygndc.analysis import (
|
|
36
|
+
compute_ndvi,
|
|
37
|
+
compute_evi,
|
|
38
|
+
compute_ndwi,
|
|
39
|
+
compute_ndmi,
|
|
40
|
+
compute_temporal_gradient,
|
|
41
|
+
compute_spatial_gradient
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Public API - Viewer
|
|
45
|
+
from pygndc.viewer import GNDCInteractiveViewer, start_viewer
|
|
46
|
+
|
|
47
|
+
def open(path: str, mode: str = "auto") -> GNDCDataset:
|
|
48
|
+
"""
|
|
49
|
+
Open a .gndc file and return a GNDCDataset.
|
|
50
|
+
|
|
51
|
+
This is the recommended entry point for working with .gndc files.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
path: Path to the .gndc file.
|
|
55
|
+
mode: Inference mode.
|
|
56
|
+
- 'auto': auto-detect best available (default)
|
|
57
|
+
- 'tcnn_cuda': tinycudann on GPU (fastest)
|
|
58
|
+
- 'torch_gpu': pure PyTorch on GPU
|
|
59
|
+
- 'torch_cpu': pure PyTorch on CPU
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
GNDCDataset instance.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
>>> ds = pygndc.open("data.gndc")
|
|
66
|
+
>>> with pygndc.open("data.gndc", mode="torch_cpu") as ds:
|
|
67
|
+
... frame = ds.read(t=0)
|
|
68
|
+
"""
|
|
69
|
+
return GNDCDataset(path, mode=mode)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
# Version info
|
|
74
|
+
"__version__",
|
|
75
|
+
"__author__",
|
|
76
|
+
"__license__",
|
|
77
|
+
# High-level API
|
|
78
|
+
"open",
|
|
79
|
+
"GNDCDataset",
|
|
80
|
+
# Reader
|
|
81
|
+
"GNDCReader",
|
|
82
|
+
# Analysis
|
|
83
|
+
"compute_ndvi",
|
|
84
|
+
"compute_evi",
|
|
85
|
+
"compute_ndwi",
|
|
86
|
+
"compute_ndmi",
|
|
87
|
+
"compute_temporal_gradient",
|
|
88
|
+
"compute_spatial_gradient",
|
|
89
|
+
# Viewer
|
|
90
|
+
"GNDCInteractiveViewer",
|
|
91
|
+
"start_viewer",
|
|
92
|
+
]
|
pygndc/__main__.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for pygndc.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from pygndc import __version__
|
|
10
|
+
from pygndc.reader import GNDCReader
|
|
11
|
+
|
|
12
|
+
MODE_CHOICES = ["auto", "tcnn_cuda", "torch_gpu", "torch_cpu"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _add_mode_arg(parser):
|
|
16
|
+
"""Add --mode argument to a subparser."""
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--mode", choices=MODE_CHOICES, default="auto",
|
|
19
|
+
help="Inference mode (default: auto)"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
description="pygndc - Geographic Neural Data Compression",
|
|
26
|
+
prog="pygndc"
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument("--version", action="version", version=f"pygndc {__version__}")
|
|
29
|
+
|
|
30
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
31
|
+
|
|
32
|
+
# Decompress command
|
|
33
|
+
decompress_parser = subparsers.add_parser("decompress", help="Decompress/reconstruct data")
|
|
34
|
+
decompress_parser.add_argument("-i", "--input", required=True, help="Input .gndc model file")
|
|
35
|
+
decompress_parser.add_argument("-o", "--output", required=True, help="Output path (file or directory)")
|
|
36
|
+
decompress_parser.add_argument("--timestamp", help="Specific timestamp (YYYY-MM-DD)")
|
|
37
|
+
decompress_parser.add_argument("--start", dest="start_date", help="Start date (YYYY-MM-DD)")
|
|
38
|
+
decompress_parser.add_argument("--end", dest="end_date", help="End date (YYYY-MM-DD)")
|
|
39
|
+
decompress_parser.add_argument("--interval", type=int, default=5, help="Interval in days")
|
|
40
|
+
decompress_parser.add_argument("--format", choices=["tif", "npy"], default="tif", help="Output format")
|
|
41
|
+
_add_mode_arg(decompress_parser)
|
|
42
|
+
|
|
43
|
+
# Info command
|
|
44
|
+
info_parser = subparsers.add_parser("info", help="Show model information")
|
|
45
|
+
info_parser.add_argument("model", help="Path to .gndc model file")
|
|
46
|
+
info_parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
47
|
+
|
|
48
|
+
# Viewer command
|
|
49
|
+
viewer_parser = subparsers.add_parser("viewer", help="Launch interactive viewer")
|
|
50
|
+
viewer_parser.add_argument("-i", "--input", required=True, help="Input .gndc model file")
|
|
51
|
+
_add_mode_arg(viewer_parser)
|
|
52
|
+
|
|
53
|
+
# NDVI command
|
|
54
|
+
ndvi_parser = subparsers.add_parser("ndvi", help="Compute NDVI and export as TIF")
|
|
55
|
+
ndvi_parser.add_argument("-i", "--input", required=True, help="Input .gndc model file")
|
|
56
|
+
ndvi_parser.add_argument("-o", "--output", required=True, help="Output NDVI TIF file")
|
|
57
|
+
ndvi_parser.add_argument("--timestamp", type=int, default=0, help="Time index (default: 0)")
|
|
58
|
+
ndvi_parser.add_argument("--red-band", type=int, default=0, help="0-based red band index")
|
|
59
|
+
ndvi_parser.add_argument("--nir-band", type=int, default=1, help="0-based NIR band index")
|
|
60
|
+
_add_mode_arg(ndvi_parser)
|
|
61
|
+
|
|
62
|
+
# Derivative command
|
|
63
|
+
deriv_parser = subparsers.add_parser("derivative", help="Compute spatial gradients")
|
|
64
|
+
deriv_parser.add_argument("-i", "--input", required=True, help="Input .gndc model file")
|
|
65
|
+
deriv_parser.add_argument("-o", "--output", required=True, help="Output gradient TIF file")
|
|
66
|
+
deriv_parser.add_argument("--grad-mode", choices=["dx", "dy", "mag", "dir"], default="mag",
|
|
67
|
+
help="Derivative mode (default: mag)")
|
|
68
|
+
deriv_parser.add_argument("--timestamp", type=int, default=0, help="Time index (default: 0)")
|
|
69
|
+
_add_mode_arg(deriv_parser)
|
|
70
|
+
|
|
71
|
+
# Sample command
|
|
72
|
+
sample_parser = subparsers.add_parser("sample", help="Point query by coordinates")
|
|
73
|
+
sample_parser.add_argument("-i", "--input", required=True, help="Input .gndc model file")
|
|
74
|
+
sample_parser.add_argument("--lon", type=float, required=True, help="Longitude")
|
|
75
|
+
sample_parser.add_argument("--lat", type=float, required=True, help="Latitude")
|
|
76
|
+
sample_parser.add_argument("--timestamp", type=int, default=None,
|
|
77
|
+
help="Time index (omit for full series)")
|
|
78
|
+
_add_mode_arg(sample_parser)
|
|
79
|
+
|
|
80
|
+
# Timeseries command
|
|
81
|
+
ts_parser = subparsers.add_parser("timeseries", help="Export pixel time series to CSV")
|
|
82
|
+
ts_parser.add_argument("-i", "--input", required=True, help="Input .gndc model file")
|
|
83
|
+
ts_parser.add_argument("--row", type=int, required=True, help="Pixel row")
|
|
84
|
+
ts_parser.add_argument("--col", type=int, required=True, help="Pixel column")
|
|
85
|
+
ts_parser.add_argument("-o", "--output", required=True, help="Output CSV file")
|
|
86
|
+
_add_mode_arg(ts_parser)
|
|
87
|
+
|
|
88
|
+
args = parser.parse_args()
|
|
89
|
+
|
|
90
|
+
if args.command == "decompress":
|
|
91
|
+
reader = GNDCReader(mode=args.mode)
|
|
92
|
+
reader.load(args.input)
|
|
93
|
+
|
|
94
|
+
if args.timestamp:
|
|
95
|
+
from datetime import datetime
|
|
96
|
+
from pygndc.utils.date_utils import normalize_time
|
|
97
|
+
import numpy as np
|
|
98
|
+
meta = reader.dataset_meta
|
|
99
|
+
g_start = datetime.strptime(str(meta['global_start_date']), "%Y-%m-%d %H:%M:%S")
|
|
100
|
+
g_end = datetime.strptime(str(meta['global_end_date']), "%Y-%m-%d %H:%M:%S")
|
|
101
|
+
t_norms = meta.get('t_norms', [])
|
|
102
|
+
target_dt = datetime.strptime(args.timestamp, "%Y-%m-%d")
|
|
103
|
+
target_tnorm = normalize_time(target_dt, g_start, g_end)
|
|
104
|
+
t_idx = min(range(len(t_norms)), key=lambda i: abs(t_norms[i] - target_tnorm))
|
|
105
|
+
frame = reader.reconstruct_single_frame(t_idx)
|
|
106
|
+
np.save(args.output, frame)
|
|
107
|
+
print(f"Saved frame {t_idx} ({args.timestamp}) to {args.output}")
|
|
108
|
+
else:
|
|
109
|
+
recons_dates = None
|
|
110
|
+
if args.start_date and args.end_date:
|
|
111
|
+
recons_dates = (args.start_date, args.end_date, args.interval)
|
|
112
|
+
reader.reconstruct_and_save_all(args.output, recons_dates=recons_dates)
|
|
113
|
+
|
|
114
|
+
elif args.command == "info":
|
|
115
|
+
import json
|
|
116
|
+
import pygndc
|
|
117
|
+
ds = pygndc.open(args.model)
|
|
118
|
+
|
|
119
|
+
if args.json:
|
|
120
|
+
print(json.dumps(ds.meta, indent=2, default=str))
|
|
121
|
+
else:
|
|
122
|
+
print(ds.info())
|
|
123
|
+
ds.close()
|
|
124
|
+
|
|
125
|
+
elif args.command == "viewer":
|
|
126
|
+
from pygndc.viewer.interactive import GNDCInteractiveViewer
|
|
127
|
+
app = GNDCInteractiveViewer()
|
|
128
|
+
app.root.mainloop()
|
|
129
|
+
|
|
130
|
+
elif args.command == "ndvi":
|
|
131
|
+
import pygndc
|
|
132
|
+
import numpy as np
|
|
133
|
+
ds = pygndc.open(args.input, mode=args.mode)
|
|
134
|
+
ndvi = ds.ndvi(t=args.timestamp, red_band=args.red_band, nir_band=args.nir_band)
|
|
135
|
+
|
|
136
|
+
from pygndc.interop.interop import save_frame_as_tif
|
|
137
|
+
save_frame_as_tif(
|
|
138
|
+
ndvi[:, :, np.newaxis], args.output, ds.crs, ds.transform, "float32"
|
|
139
|
+
)
|
|
140
|
+
print(f"NDVI saved to {args.output}")
|
|
141
|
+
ds.close()
|
|
142
|
+
|
|
143
|
+
elif args.command == "derivative":
|
|
144
|
+
import pygndc
|
|
145
|
+
ds = pygndc.open(args.input, mode=args.mode)
|
|
146
|
+
result = ds.gradient(t=args.timestamp, mode=args.grad_mode)
|
|
147
|
+
if isinstance(result, tuple):
|
|
148
|
+
import numpy as np
|
|
149
|
+
result = np.sqrt(result[0] ** 2 + result[1] ** 2)
|
|
150
|
+
|
|
151
|
+
from pygndc.interop.interop import save_frame_as_tif
|
|
152
|
+
if result.ndim == 2:
|
|
153
|
+
import numpy as np
|
|
154
|
+
result = result[:, :, np.newaxis]
|
|
155
|
+
save_frame_as_tif(result, args.output, ds.crs, ds.transform, "float32")
|
|
156
|
+
print(f"Gradient ({args.grad_mode}) saved to {args.output}")
|
|
157
|
+
ds.close()
|
|
158
|
+
|
|
159
|
+
elif args.command == "sample":
|
|
160
|
+
import pygndc
|
|
161
|
+
import numpy as np
|
|
162
|
+
ds = pygndc.open(args.input, mode=args.mode)
|
|
163
|
+
|
|
164
|
+
if args.timestamp is not None:
|
|
165
|
+
vals = ds.sample(lon=args.lon, lat=args.lat, t=args.timestamp)
|
|
166
|
+
print(f"Values at ({args.lon}, {args.lat}), t={args.timestamp}: {vals}")
|
|
167
|
+
else:
|
|
168
|
+
series = ds.sample(lon=args.lon, lat=args.lat)
|
|
169
|
+
print(f"Time series at ({args.lon}, {args.lat}): shape={series.shape}")
|
|
170
|
+
for t_idx in range(series.shape[0]):
|
|
171
|
+
ts_str = ds.timestamps[t_idx] if t_idx < len(ds.timestamps) else str(t_idx)
|
|
172
|
+
vals_str = ", ".join(f"{v:.4f}" for v in series[t_idx])
|
|
173
|
+
print(f" [{t_idx}] {ts_str}: {vals_str}")
|
|
174
|
+
ds.close()
|
|
175
|
+
|
|
176
|
+
elif args.command == "timeseries":
|
|
177
|
+
import pygndc
|
|
178
|
+
import numpy as np
|
|
179
|
+
ds = pygndc.open(args.input, mode=args.mode)
|
|
180
|
+
series = ds.pixel_series(row=args.row, col=args.col)
|
|
181
|
+
|
|
182
|
+
# Write CSV
|
|
183
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
|
|
184
|
+
n_bands = series.shape[1]
|
|
185
|
+
band_headers = [f"band_{i}" for i in range(n_bands)]
|
|
186
|
+
|
|
187
|
+
with open(args.output, "w") as f:
|
|
188
|
+
f.write("t_idx,timestamp," + ",".join(band_headers) + "\n")
|
|
189
|
+
for t_idx in range(series.shape[0]):
|
|
190
|
+
ts_str = str(ds.timestamps[t_idx]) if t_idx < len(ds.timestamps) else str(t_idx)
|
|
191
|
+
vals_str = ",".join(f"{v:.6f}" for v in series[t_idx])
|
|
192
|
+
f.write(f"{t_idx},{ts_str},{vals_str}\n")
|
|
193
|
+
|
|
194
|
+
print(f"Time series ({args.row}, {args.col}) saved to {args.output}")
|
|
195
|
+
ds.close()
|
|
196
|
+
|
|
197
|
+
else:
|
|
198
|
+
parser.print_help()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
main()
|
pygndc/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analysis module for GNDC.
|
|
3
|
+
|
|
4
|
+
This module provides functions for analyzing compressed geospatial data,
|
|
5
|
+
including vegetation indices and derivatives.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pygndc.analysis.vegetation import (
|
|
9
|
+
compute_ndvi,
|
|
10
|
+
compute_evi,
|
|
11
|
+
compute_ndwi,
|
|
12
|
+
compute_ndmi,
|
|
13
|
+
compute_temporal_gradient,
|
|
14
|
+
compute_spatial_gradient
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'compute_ndvi',
|
|
19
|
+
'compute_evi',
|
|
20
|
+
'compute_ndwi',
|
|
21
|
+
'compute_ndmi',
|
|
22
|
+
'compute_temporal_gradient',
|
|
23
|
+
'compute_spatial_gradient'
|
|
24
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analysis module for GNDC.
|
|
3
|
+
|
|
4
|
+
This module provides functions for analyzing compressed data,
|
|
5
|
+
including derivatives and vegetation indices.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_ndvi(red, nir):
|
|
12
|
+
"""
|
|
13
|
+
Compute Normalized Difference Vegetation Index.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
red: Red band data
|
|
17
|
+
nir: Near-infrared band data
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
NDVI values in range [-1, 1]
|
|
21
|
+
"""
|
|
22
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
23
|
+
ndvi = (nir - red) / (nir + red + 1e-8)
|
|
24
|
+
ndvi = np.where(np.isfinite(ndvi), ndvi, np.nan)
|
|
25
|
+
return ndvi
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compute_evi(blue, green, nir, g=2.5, c1=6, c2=7.5, l=1):
|
|
29
|
+
"""
|
|
30
|
+
Compute Enhanced Vegetation Index.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
blue: Blue band data
|
|
34
|
+
green: Green band data
|
|
35
|
+
nir: Near-infrared band data
|
|
36
|
+
g: Gain factor
|
|
37
|
+
c1, c2: Coefficient parameters
|
|
38
|
+
l: Canopy background adjustment
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
EVI values
|
|
42
|
+
"""
|
|
43
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
44
|
+
evi = g * (nir - red) / (nir + c1 * red - c2 * blue + l + 1e-8)
|
|
45
|
+
evi = np.where(np.isfinite(evi), evi, np.nan)
|
|
46
|
+
return evi
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_ndwi(green, nir):
|
|
50
|
+
"""
|
|
51
|
+
Compute Normalized Difference Water Index.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
green: Green band data
|
|
55
|
+
nir: Near-infrared band data
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
NDWI values in range [-1, 1]
|
|
59
|
+
"""
|
|
60
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
61
|
+
ndwi = (green - nir) / (green + nir + 1e-8)
|
|
62
|
+
ndwi = np.where(np.isfinite(ndwi), ndwi, np.nan)
|
|
63
|
+
return ndwi
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def compute_ndmi(nir, swir):
|
|
67
|
+
"""
|
|
68
|
+
Compute Normalized Difference Moisture Index.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
nir: Near-infrared band data
|
|
72
|
+
swir: Short-wave infrared band data
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
NDMI values in range [-1, 1]
|
|
76
|
+
"""
|
|
77
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
78
|
+
ndmi = (nir - swir) / (nir + swir + 1e-8)
|
|
79
|
+
ndmi = np.where(np.isfinite(ndmi), ndmi, np.nan)
|
|
80
|
+
return ndmi
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_temporal_gradient(time_series_data):
|
|
84
|
+
"""
|
|
85
|
+
Compute temporal gradient (first derivative) of time series.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
time_series_data: 3D array [T, H, W] or 2D array [T, pixels]
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Temporal gradient array of same shape
|
|
92
|
+
"""
|
|
93
|
+
if time_series_data.ndim == 3:
|
|
94
|
+
T, H, W = time_series_data.shape
|
|
95
|
+
gradient = np.zeros_like(time_series_data)
|
|
96
|
+
gradient[1:-1] = (time_series_data[2:] - time_series_data[:-2]) / 2
|
|
97
|
+
gradient[0] = time_series_data[1] - time_series_data[0]
|
|
98
|
+
gradient[-1] = time_series_data[-1] - time_series_data[-2]
|
|
99
|
+
else:
|
|
100
|
+
T = time_series_data.shape[0]
|
|
101
|
+
gradient = np.zeros_like(time_series_data)
|
|
102
|
+
gradient[1:-1] = (time_series_data[2:] - time_series_data[:-2]) / 2
|
|
103
|
+
gradient[0] = time_series_data[1] - time_series_data[0]
|
|
104
|
+
gradient[-1] = time_series_data[-1] - time_series_data[-2]
|
|
105
|
+
|
|
106
|
+
return gradient
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def compute_spatial_gradient(image_data):
|
|
110
|
+
"""
|
|
111
|
+
Compute spatial gradient using Sobel operator.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
image_data: 2D array [H, W] or 3D array [H, W, C]
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Tuple of (grad_x, grad_y) gradient arrays
|
|
118
|
+
"""
|
|
119
|
+
if image_data.ndim == 3:
|
|
120
|
+
grad_x = np.zeros_like(image_data)
|
|
121
|
+
grad_y = np.zeros_like(image_data)
|
|
122
|
+
for c in range(image_data.shape[2]):
|
|
123
|
+
gx, gy = _sobel_2d(image_data[:, :, c])
|
|
124
|
+
grad_x[:, :, c] = gx
|
|
125
|
+
grad_y[:, :, c] = gy
|
|
126
|
+
else:
|
|
127
|
+
grad_x, grad_y = _sobel_2d(image_data)
|
|
128
|
+
|
|
129
|
+
return grad_x, grad_y
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _sobel_2d(img):
|
|
133
|
+
"""
|
|
134
|
+
Apply Sobel operator to 2D image.
|
|
135
|
+
"""
|
|
136
|
+
sx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
|
|
137
|
+
sy = sx.T
|
|
138
|
+
|
|
139
|
+
pad_img = np.pad(img, 1, mode='edge')
|
|
140
|
+
|
|
141
|
+
gx = np.zeros_like(img)
|
|
142
|
+
gy = np.zeros_like(img)
|
|
143
|
+
|
|
144
|
+
for i in range(img.shape[0]):
|
|
145
|
+
for j in range(img.shape[1]):
|
|
146
|
+
window = pad_img[i:i+3, j:j+3]
|
|
147
|
+
gx[i, j] = np.sum(window * sx)
|
|
148
|
+
gy[i, j] = np.sum(window * sy)
|
|
149
|
+
|
|
150
|
+
return gx, gy
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
__all__ = [
|
|
154
|
+
'compute_ndvi',
|
|
155
|
+
'compute_evi',
|
|
156
|
+
'compute_ndwi',
|
|
157
|
+
'compute_ndmi',
|
|
158
|
+
'compute_temporal_gradient',
|
|
159
|
+
'compute_spatial_gradient'
|
|
160
|
+
]
|