cdts 0.1.0__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.
- cdts-0.1.0/PKG-INFO +21 -0
- cdts-0.1.0/README.md +274 -0
- cdts-0.1.0/cdts/__init__.py +27 -0
- cdts-0.1.0/cdts/ccdc.py +89 -0
- cdts-0.1.0/cdts/classify.py +51 -0
- cdts-0.1.0/cdts/cli.py +112 -0
- cdts-0.1.0/cdts/cube.py +134 -0
- cdts-0.1.0/cdts/io.py +165 -0
- cdts-0.1.0/cdts/landtrendr.py +52 -0
- cdts-0.1.0/cdts/masks.py +42 -0
- cdts-0.1.0/cdts/metrics.py +166 -0
- cdts-0.1.0/cdts/raster.py +312 -0
- cdts-0.1.0/cdts/smooth.py +27 -0
- cdts-0.1.0/cdts/spatial.py +53 -0
- cdts-0.1.0/cdts/tmask.py +94 -0
- cdts-0.1.0/cdts/xarray_api.py +66 -0
- cdts-0.1.0/cdts.egg-info/PKG-INFO +21 -0
- cdts-0.1.0/cdts.egg-info/SOURCES.txt +36 -0
- cdts-0.1.0/cdts.egg-info/dependency_links.txt +1 -0
- cdts-0.1.0/cdts.egg-info/entry_points.txt +2 -0
- cdts-0.1.0/cdts.egg-info/not-zip-safe +1 -0
- cdts-0.1.0/cdts.egg-info/requires.txt +18 -0
- cdts-0.1.0/cdts.egg-info/top_level.txt +1 -0
- cdts-0.1.0/pyproject.toml +33 -0
- cdts-0.1.0/setup.cfg +4 -0
- cdts-0.1.0/setup.py +72 -0
- cdts-0.1.0/src/ccdc.cpp +311 -0
- cdts-0.1.0/src/landtrendr.cpp +483 -0
- cdts-0.1.0/src/main.cpp +59 -0
- cdts-0.1.0/tests/test_ai.py +10 -0
- cdts-0.1.0/tests/test_ccdc_synthetic.py +25 -0
- cdts-0.1.0/tests/test_classify.py +62 -0
- cdts-0.1.0/tests/test_io.py +44 -0
- cdts-0.1.0/tests/test_landtrendr.py +41 -0
- cdts-0.1.0/tests/test_memory_leak.py +43 -0
- cdts-0.1.0/tests/test_metrics_ccdc.py +46 -0
- cdts-0.1.0/tests/test_smooth.py +16 -0
- cdts-0.1.0/tests/test_spatial.py +61 -0
cdts-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cdts
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Change Detection Python Library (LandTrendr, CCDC, etc)
|
|
5
|
+
Requires-Dist: numpy
|
|
6
|
+
Requires-Dist: numba>=0.53.0
|
|
7
|
+
Requires-Dist: rasterio
|
|
8
|
+
Requires-Dist: scikit-learn
|
|
9
|
+
Requires-Dist: scipy
|
|
10
|
+
Requires-Dist: dask[array]
|
|
11
|
+
Requires-Dist: xarray
|
|
12
|
+
Requires-Dist: pystac-client
|
|
13
|
+
Requires-Dist: stackstac
|
|
14
|
+
Requires-Dist: geopandas
|
|
15
|
+
Requires-Dist: torch
|
|
16
|
+
Requires-Dist: einops
|
|
17
|
+
Requires-Dist: transformers
|
|
18
|
+
Requires-Dist: planetary-computer
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
cdts-0.1.0/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# CDTS: Change Detection and Time-Series for Python
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/assets/logo.png" alt="CDTS Logo" width="400">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
[](https://github.com/sacridini/cdts/actions/workflows/build_wheels.yml) [](https://github.com/sacridini/cdts/actions/workflows/tests.yml)
|
|
8
|
+
|
|
9
|
+
`cdts` is an ultra-fast, cloud-native Python library for **Remote Sensing Time-Series Analysis and Change Detection**.
|
|
10
|
+
|
|
11
|
+
Designed to replace heavy dependencies on Google Earth Engine, `cdts` handles the entire geospatial pipeline locally or on cloud clusters: from directly streaming satellite imagery via **STAC** APIs, to scaling memory lazily with **Dask/Xarray**, down to executing heavy statistical regression in native **C++**.
|
|
12
|
+
|
|
13
|
+
It provides state-of-the-art algorithms:
|
|
14
|
+
* **LandTrendr** (Landsat-based detection of Trends in Disturbance and Recovery)
|
|
15
|
+
* **CCDC** (Continuous Change Detection and Classification)
|
|
16
|
+
* **COLD** (Continuous monitoring of Land Disturbance)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
The easiest way to install `cdts` is via `pip`. We provide pre-compiled binaries (wheels) for Windows, macOS, and Linux (Python 3.9+), which means **you do not need a C++ compiler** installed on your machine!
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install cdts
|
|
24
|
+
```
|
|
25
|
+
*(This will automatically install Python dependencies like `xarray`, `dask`, `scikit-learn`, `rasterio`, `torch`, and `pystac-client`)*.
|
|
26
|
+
|
|
27
|
+
### Development / From Source
|
|
28
|
+
|
|
29
|
+
If you want to modify the C++ backend or install the bleeding-edge version directly from GitHub, you will need a C++ Compiler (GCC, Clang, or MSVC) and Python 3.9+:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/sacridini/cdts.git
|
|
33
|
+
cd cdts
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### Docker
|
|
40
|
+
Because `cdts` relies on heavy C++ compilation and GPU-accelerated PyTorch, the easiest way to deploy it to the cloud or share it with other researchers is via our official Docker container.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# This will build the C++ engines, install PyTorch with CUDA, and launch a JupyterLab environment on port 8888.
|
|
44
|
+
docker-compose up --build
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Core Architecture
|
|
50
|
+
|
|
51
|
+
1. **C++ Engine (`pybind11` & `Eigen3`)**: The core statistical fitting (OLS, Robust Iteratively Reweighted Least Squares, Exact F-Statistics, Chi-Square CDFs) is fully written in C++ for maximum single-core speed.
|
|
52
|
+
2. **Cloud-Native Data Fetching (`STAC` & `stackstac`)**: Download-free pipelines! Query AWS or Microsoft servers for imagery and stream only the exact pixels you need.
|
|
53
|
+
3. **Horizontal Scaling (`xarray` & `dask`)**: Data is lazily chunked. Run processing over 100,000 km² without blowing up your RAM by distributing tasks across multiple CPU threads or remote Dask workers.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Supported Cloud Data Services (STAC)
|
|
58
|
+
|
|
59
|
+
Because `cdts` relies on the open **SpatioTemporal Asset Catalog (STAC)** standard, it can pull time-series data from virtually any modern satellite provider.
|
|
60
|
+
|
|
61
|
+
| Provider / Service | Default ID in `cdts` | Highlights |
|
|
62
|
+
| :--- | :--- | :--- |
|
|
63
|
+
| **AWS Earth Search (Element84)** | `"earth_search"` | Free, public Sentinel-2 L2A and Landsat Collection 2. No authentication needed! |
|
|
64
|
+
| **Microsoft Planetary Computer** | `"planetary_computer"` | Huge catalog (ALOS, MODIS, NAIP, Sentinel, Landsat). *Note: Requires a free token for heavy downloads.* |
|
|
65
|
+
| **Brazil Data Cube (INPE)** | `"brazil_data_cube"` | High-quality ARD cubes for Brazil (CBERS-4/4A, Amazonia-1). *Note: Requires INPE token.* |
|
|
66
|
+
| **Copernicus Data Space** | Custom URL | The official European Space Agency hub for Sentinel 1/2/3/5P. |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Advanced Tutorial: End-to-End Pipeline
|
|
71
|
+
|
|
72
|
+
Here is a complete workflow demonstrating how to go from zero data to a classified map of persistent water using `cdts`.
|
|
73
|
+
|
|
74
|
+
### 1. Stream Virtual Data (STAC)
|
|
75
|
+
No downloading required. We define a Bounding Box in Mato Grosso (Brazil) and request 2 years of Sentinel-2 data.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from cdts import build_time_series
|
|
79
|
+
import cdts.xarray_api # Registers the .cdts accessor on Xarray
|
|
80
|
+
|
|
81
|
+
# Builds a Dask-backed virtual datacube
|
|
82
|
+
cube = build_time_series(
|
|
83
|
+
source="earth_search",
|
|
84
|
+
collection="sentinel-2-l2a",
|
|
85
|
+
bbox=[-54.0, -12.0, -53.9, -11.9],
|
|
86
|
+
start_date="2020-01-01",
|
|
87
|
+
end_date="2022-12-31",
|
|
88
|
+
cloud_cover_max=30,
|
|
89
|
+
bands=["red", "green", "blue", "nir", "swir16"],
|
|
90
|
+
epsg=3857,
|
|
91
|
+
resolution=30
|
|
92
|
+
)
|
|
93
|
+
print(cube.shape) # e.g., (Time: 45, Bands: 5, Y: 1000, X: 1000)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 2. Run CCDC / COLD (Distributed via Dask)
|
|
97
|
+
Using the Xarray accessor, we pipe the virtual cube directly into our C++ engine.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# 'conseq_anom=6' activates the rigorous COLD algorithm (6 consecutive anomalies required to flag deforestation).
|
|
101
|
+
# This returns a lazy map of harmonic coefficients (Intercept, Slopes, Sine, Cosine).
|
|
102
|
+
ccdc_lazy_results = cube.cdts.run_ccdc(max_segments=6, conseq_anom=6, return_coefs=True)
|
|
103
|
+
|
|
104
|
+
# Actually execute the download + calculation in parallel threads
|
|
105
|
+
coef_stack = ccdc_lazy_results.compute()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Extract Physical Masks & Classify
|
|
109
|
+
With the harmonic coefficients calculated, we can derive physical parameters. For instance, extracting persistent rivers/lakes by comparing the Intercept coefficients of Green (index 1) and SWIR (index 4).
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from cdts import extract_water_mask, predict_synthetic_image
|
|
113
|
+
|
|
114
|
+
# 1. Physical Extraction: Isolate rivers and lakes
|
|
115
|
+
water_mask = extract_water_mask(coef_stack.values, green_band_idx=1, swir_band_idx=4)
|
|
116
|
+
|
|
117
|
+
# 2. Synthetic Imagery: Generate a cloud-free image for Julian Day 150
|
|
118
|
+
cloud_free_rgb = predict_synthetic_image(coef_stack.values, target_julian_day=150, num_bands=3)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If you have training data, you can run a full Random Forest classification on the coefficients:
|
|
122
|
+
```python
|
|
123
|
+
from cdts import train_ccdc_classifier, classify_ccdc_stack
|
|
124
|
+
|
|
125
|
+
clf = train_ccdc_classifier(X_train_data, y_train_labels)
|
|
126
|
+
classify_ccdc_stack(clf, coef_stack_path="output/coefs.tif", output_path="landcover.tif")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Pre & Post-Processing (Smoothing & Spatial Filters)
|
|
130
|
+
Before classifying, it is highly recommended to smooth the temporal trajectories to remove atmospheric noise. After classifying, pixel-based maps often suffer from "salt and pepper" noise. `cdts` provides fast functions to regularize your data in both dimensions:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from cdts import apply_savgol_filter, apply_majority_filter, apply_mmu_filter
|
|
134
|
+
|
|
135
|
+
# 1. Temporal Smoothing: Apply Savitzky-Golay filter across the time axis (e.g. axis 0)
|
|
136
|
+
smoothed_cube = apply_savgol_filter(raw_cube, window_length=5, polyorder=2)
|
|
137
|
+
|
|
138
|
+
# ... (Run Classification to get `land_cover_map`) ...
|
|
139
|
+
|
|
140
|
+
# 2. Spatial Regularization: Force pixels to match their 3x3 neighborhood (Mode filter)
|
|
141
|
+
regularized_map = apply_majority_filter(land_cover_map, size=3)
|
|
142
|
+
|
|
143
|
+
# 3. Minimum Mapping Unit (MMU): Erase any isolated patches smaller than 10 pixels
|
|
144
|
+
final_map = apply_mmu_filter(regularized_map, min_pixels=10)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## LandTrendr Specifics (FTV)
|
|
150
|
+
If your focus is on forest recovery, `cdts` natively supports LandTrendr. A key feature is **FTV (Fitted to Vertices)**, which allows you to find structural breakpoints in an index (like NBR) and apply them to smooth out noisy raw bands (like SWIR).
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from cdts import run_landtrendr, apply_vertices
|
|
154
|
+
|
|
155
|
+
# 1. Fit the trajectory on the main index to find the breakpoint years
|
|
156
|
+
vertices = run_landtrendr(years, nbr_time_series)
|
|
157
|
+
vertex_years = [v["year"] for v in vertices]
|
|
158
|
+
|
|
159
|
+
# 2. Force the raw SWIR band to conform to the NBR breakpoints!
|
|
160
|
+
swir_fitted = apply_vertices(vertex_years, years, raw_swir_time_series)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Deep Learning & Foundation Models (`cdts.ai`)
|
|
166
|
+
Beyond statistical algorithms like CCDC, `cdts` embraces the next generation of Spatio-Temporal Artificial Intelligence. Built on **PyTorch**, the new `cdts.ai` module provides modern neural network architectures tailored for earth observation:
|
|
167
|
+
|
|
168
|
+
### 1. U-TAE and TempCNN (Time-Series Neural Networks)
|
|
169
|
+
Instead of processing individual pixels, **U-TAE** consumes entire 3D Data Cubes (Spatial + Temporal) simultaneously to naturally ignore cloud noise. If you prefer pixel-based time-series classification, `cdts` also provides **TempCNN**, a lightweight 1D-CNN (inspired by INPE's `sits` package) that is incredibly fast to train.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from cdts.ai import UTAE, TempCNN
|
|
173
|
+
import torch
|
|
174
|
+
|
|
175
|
+
# U-TAE: Input shape (Batch, Time, Channels, Height, Width)
|
|
176
|
+
model_3d = UTAE(in_channels=6, num_classes=5)
|
|
177
|
+
|
|
178
|
+
# TempCNN: Input shape (Batch, Channels, Time)
|
|
179
|
+
model_1d = TempCNN(in_channels=6, num_classes=5)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 2. Bi-Temporal Siamese CNNs
|
|
183
|
+
Perfect for disaster mapping (floods, fires, landslides). A Siamese Network processes a T0 ("Before") image and a T1 ("After") image through shared convolutional weights, then extracts absolute differences deep in the feature space.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from cdts.ai import SiameseChangeDetector
|
|
187
|
+
|
|
188
|
+
model = SiameseChangeDetector(in_channels=4, num_classes=2)
|
|
189
|
+
img_before = torch.randn(1, 4, 512, 512)
|
|
190
|
+
img_after = torch.randn(1, 4, 512, 512)
|
|
191
|
+
|
|
192
|
+
# Outputs a spatial change map directly
|
|
193
|
+
change_map = model(img_before, img_after)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 3. Geospatial Foundation Models (ViT)
|
|
197
|
+
`cdts.ai.GeoFoundationViT` acts as a wrapper/stub to plug in large-scale Vision Transformers (like the **NASA/IBM Prithvi** model). It enables you to take pre-trained planetary representations and fine-tune them for specific downstream tasks like deforestation or crop classification.
|
|
198
|
+
|
|
199
|
+
### 4. Specialized Change Detection Losses
|
|
200
|
+
Remote sensing datasets are highly imbalanced (usually >99% unchanged pixels). Standard Cross-Entropy fails here. `cdts.ai.losses` provides battle-tested loss functions specifically for Change Detection:
|
|
201
|
+
```python
|
|
202
|
+
from cdts.ai.losses import FocalLoss, TverskyLoss, ContrastiveSiameseLoss
|
|
203
|
+
|
|
204
|
+
# Focal Loss: Forces the network to focus gradients on hard-to-detect subtle changes
|
|
205
|
+
criterion1 = FocalLoss(alpha=0.25, gamma=2.0)
|
|
206
|
+
|
|
207
|
+
# Tversky Loss: Penalizes False Negatives heavier than False Positives (beta=0.7)
|
|
208
|
+
criterion2 = TverskyLoss(alpha=0.3, beta=0.7)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Tmask: Time-Series Cloud Masking
|
|
214
|
+
Before running CCDC or deep learning models, you must have clean data. While STAC APIs provide QA bands (like Fmask), `cdts` natively implements **Tmask** (Zhu & Woodcock 2014) to dynamically find undetected clouds and shadows.
|
|
215
|
+
|
|
216
|
+
Tmask runs a robust harmonic regression on Green and SWIR bands. If a pixel suddenly flashes bright green or dark SWIR without altering the long-term structural trajectory, it is flagged as noise.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from cdts import apply_tmask_stack
|
|
220
|
+
|
|
221
|
+
# Outputs a Boolean mask (True = Clear, False = Cloud/Shadow)
|
|
222
|
+
# Uses robust Huber regression under the hood
|
|
223
|
+
clear_sky_mask = apply_tmask_stack(dates_julian, green_cube, swir_cube)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Exporting & Saving Data (IO)
|
|
229
|
+
Instead of dealing with complex `rasterio` profiles, `cdts` includes a powerful `save_raster` utility that automatically extracts the geotransform and CRS from the downloaded STAC Datacube and exports your PyTorch/Numpy predictions into professional, ready-to-use GeoTIFFs.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from cdts import save_raster, get_georef
|
|
233
|
+
|
|
234
|
+
# Extract geospatial reference explicitly if needed
|
|
235
|
+
geo_info = get_georef(cube)
|
|
236
|
+
print(geo_info["crs"]) # e.g. "EPSG:3857"
|
|
237
|
+
|
|
238
|
+
# Or save the array seamlessly using the original cube as reference!
|
|
239
|
+
# Handles 2D, 3D, and even 4D cubes out-of-the-box.
|
|
240
|
+
save_raster(prediction_array, "output/final_map.tif", reference_cube=cube, nodata=255)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## End-to-End Examples
|
|
246
|
+
We provide **6 complete example scripts** in the `examples/` directory. They cover everything from downloading STAC data to temporal smoothing and classification using both statistical and AI models. Running these scripts will automatically output georeferenced GeoTIFFs into `examples/data/`.
|
|
247
|
+
|
|
248
|
+
- `example_01_landtrendr.py` (LandTrendr Disturbance Year)
|
|
249
|
+
- `example_02_ccdc_cold.py` (CCDC / COLD Synthetic Image Generation)
|
|
250
|
+
- `example_03_ai_siamese.py` (Siamese Neural Network)
|
|
251
|
+
- `example_04_ai_utae.py` (U-TAE 4D processing)
|
|
252
|
+
- `example_05_ai_tempcnn.py` (TempCNN time-series)
|
|
253
|
+
- `example_06_ai_vit.py` (Geospatial Foundation Model)
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Try one!
|
|
257
|
+
python examples/example_05_ai_tempcnn.py
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Command Line Interface (CLI)
|
|
263
|
+
|
|
264
|
+
Prefer the terminal? If you already have a massive GeoTIFF locally, you can process it chunk-by-chunk using the CLI.
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# Run LandTrendr (Extracting just the break years)
|
|
268
|
+
cdts landtrendr input_stack.tif output_folder/ \
|
|
269
|
+
--start-year 1990 --event-type loss --jobs -1
|
|
270
|
+
|
|
271
|
+
# Run COLD (Extracting the harmonic coefficient matrix)
|
|
272
|
+
cdts ccdc multi_band_stack.tif output_folder/ \
|
|
273
|
+
--num-bands 6 --max-segments 6 --cold --jobs -1
|
|
274
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .landtrendr import run_landtrendr, desawtooth, apply_vertices
|
|
2
|
+
from .raster import run_landtrendr_array, run_landtrendr_image, run_ccdc_array, run_ccdc_image
|
|
3
|
+
from .metrics import extract_events
|
|
4
|
+
from .ccdc import predict_synthetic_image
|
|
5
|
+
from .classify import train_ccdc_classifier, classify_ccdc_stack
|
|
6
|
+
from .spatial import apply_mmu_filter, apply_majority_filter
|
|
7
|
+
from .smooth import apply_savgol_filter
|
|
8
|
+
from .masks import extract_water_mask
|
|
9
|
+
from .tmask import run_tmask_pixel, apply_tmask_stack
|
|
10
|
+
from .cube import build_time_series
|
|
11
|
+
from .io import save_raster, get_georef
|
|
12
|
+
import cdts.xarray_api # This registers the xarray accessor automatically
|
|
13
|
+
import cdts.ai
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"run_landtrendr", "desawtooth", "apply_vertices",
|
|
17
|
+
"run_landtrendr_array", "run_landtrendr_image",
|
|
18
|
+
"run_ccdc_array", "run_ccdc_image",
|
|
19
|
+
"extract_events", "predict_synthetic_image",
|
|
20
|
+
"train_ccdc_classifier", "classify_ccdc_stack",
|
|
21
|
+
"apply_mmu_filter", "apply_majority_filter", "apply_savgol_filter",
|
|
22
|
+
"extract_water_mask",
|
|
23
|
+
"run_tmask_pixel", "apply_tmask_stack",
|
|
24
|
+
"build_time_series",
|
|
25
|
+
"save_raster",
|
|
26
|
+
"ai"
|
|
27
|
+
]
|
cdts-0.1.0/cdts/ccdc.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Dict, Any, Union
|
|
3
|
+
from . import _core
|
|
4
|
+
|
|
5
|
+
def run_ccdc(
|
|
6
|
+
dates: Union[np.ndarray, List[float]],
|
|
7
|
+
values: Union[np.ndarray, List[List[float]]],
|
|
8
|
+
qa: Union[np.ndarray, List[int]],
|
|
9
|
+
min_obs: int = 12,
|
|
10
|
+
conseq_anom: int = 3,
|
|
11
|
+
chi2_prob_threshold: float = 0.99
|
|
12
|
+
) -> List[Dict[str, Any]]:
|
|
13
|
+
"""
|
|
14
|
+
Continuous Change Detection and Classification (CCDC).
|
|
15
|
+
|
|
16
|
+
This function wraps the C++ implementation.
|
|
17
|
+
"""
|
|
18
|
+
dates_list = dates.tolist() if isinstance(dates, np.ndarray) else list(dates)
|
|
19
|
+
|
|
20
|
+
params = _core.ccdc.CCDCParams()
|
|
21
|
+
params.min_obs = min_obs
|
|
22
|
+
params.conseq_anom = conseq_anom
|
|
23
|
+
params.chi2_prob_threshold = chi2_prob_threshold
|
|
24
|
+
|
|
25
|
+
# Ensure values is 2D: (num_bands, num_dates)
|
|
26
|
+
if isinstance(values, np.ndarray):
|
|
27
|
+
if values.ndim == 1:
|
|
28
|
+
values_list = [values.tolist()]
|
|
29
|
+
else:
|
|
30
|
+
values_list = values.tolist()
|
|
31
|
+
else:
|
|
32
|
+
# If it's a list, check if it's 1D or 2D
|
|
33
|
+
if len(values) > 0 and not isinstance(values[0], (list, tuple, np.ndarray)):
|
|
34
|
+
values_list = [values]
|
|
35
|
+
else:
|
|
36
|
+
values_list = values
|
|
37
|
+
|
|
38
|
+
qa_list = qa.tolist() if isinstance(qa, np.ndarray) else list(qa)
|
|
39
|
+
|
|
40
|
+
segments = _core.ccdc.fit_ccdc(dates_list, values_list, qa_list, params)
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
"t_start": s.t_start,
|
|
45
|
+
"t_end": s.t_end,
|
|
46
|
+
"t_break": s.t_break,
|
|
47
|
+
"coefs": s.coefs,
|
|
48
|
+
"rmse": s.rmse,
|
|
49
|
+
"magnitude": s.magnitude
|
|
50
|
+
} for s in segments
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def predict_synthetic_image(ccdc_coefs_stack: np.ndarray, target_julian_day: int, num_bands: int = 6) -> np.ndarray:
|
|
54
|
+
"""
|
|
55
|
+
Generates a cloud-free synthetic image for a specific day using CCDC harmonic coefficients.
|
|
56
|
+
ccdc_coefs_stack: 4D numpy array output from run_ccdc_array() or read from _coefs.tif.
|
|
57
|
+
"""
|
|
58
|
+
import numpy as np
|
|
59
|
+
|
|
60
|
+
_, bands_dim, rows, cols = ccdc_coefs_stack.shape
|
|
61
|
+
|
|
62
|
+
W = 2.0 * np.pi / 365.25
|
|
63
|
+
t = float(target_julian_day)
|
|
64
|
+
|
|
65
|
+
terms = np.array([
|
|
66
|
+
1.0, t, np.cos(W * t), np.sin(W * t), np.cos(2.0 * W * t), np.sin(2.0 * W * t)
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
synthetic_image = np.zeros((num_bands, rows, cols), dtype=np.float32)
|
|
70
|
+
|
|
71
|
+
for r in range(rows):
|
|
72
|
+
for c in range(cols):
|
|
73
|
+
best_seg = 0
|
|
74
|
+
for i in range(ccdc_coefs_stack.shape[0]):
|
|
75
|
+
t_start = ccdc_coefs_stack[i, 0, r, c]
|
|
76
|
+
t_end = ccdc_coefs_stack[i, 1, r, c]
|
|
77
|
+
|
|
78
|
+
if t_start <= t <= t_end:
|
|
79
|
+
best_seg = i
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
idx = 3
|
|
83
|
+
for b in range(num_bands):
|
|
84
|
+
idx += 1 # skip RMSE
|
|
85
|
+
coefs = ccdc_coefs_stack[best_seg, idx:idx+6, r, c]
|
|
86
|
+
idx += 6
|
|
87
|
+
synthetic_image[b, r, c] = np.dot(coefs, terms)
|
|
88
|
+
|
|
89
|
+
return synthetic_image
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
3
|
+
import rasterio
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
def train_ccdc_classifier(X_train: np.ndarray, y_train: np.ndarray, n_estimators: int = 100, random_state: int = 42) -> RandomForestClassifier:
|
|
7
|
+
"""
|
|
8
|
+
Trains a Random Forest classifier for CCDC land cover classification.
|
|
9
|
+
X_train: array-like of shape (n_samples, n_features). Features should be the harmonic coefficients and RMSE.
|
|
10
|
+
y_train: array-like of shape (n_samples,). The land cover class labels.
|
|
11
|
+
"""
|
|
12
|
+
clf = RandomForestClassifier(n_estimators=n_estimators, random_state=random_state, n_jobs=-1)
|
|
13
|
+
clf.fit(X_train, y_train)
|
|
14
|
+
return clf
|
|
15
|
+
|
|
16
|
+
def classify_ccdc_stack(clf: RandomForestClassifier, coef_stack_path: str, output_path: str, chunk_size: int = 512) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Applies the trained Random Forest classifier to a full CCDC coefficient GeoTIFF stack.
|
|
19
|
+
Assumes the model was trained on the exact band configuration present in the TIFF.
|
|
20
|
+
"""
|
|
21
|
+
with rasterio.open(coef_stack_path) as src:
|
|
22
|
+
profile = src.profile
|
|
23
|
+
profile.update(count=1, dtype="uint8", nodata=0)
|
|
24
|
+
|
|
25
|
+
with rasterio.open(output_path, "w", **profile) as dst:
|
|
26
|
+
for row in range(0, src.height, chunk_size):
|
|
27
|
+
for col in range(0, src.width, chunk_size):
|
|
28
|
+
window = rasterio.windows.Window(
|
|
29
|
+
col, row,
|
|
30
|
+
min(chunk_size, src.width - col),
|
|
31
|
+
min(chunk_size, src.height - row)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
data = src.read(window=window)
|
|
35
|
+
n_features, h, w = data.shape
|
|
36
|
+
|
|
37
|
+
data_reshaped = data.transpose(1, 2, 0).reshape(-1, n_features)
|
|
38
|
+
|
|
39
|
+
mask = (data_reshaped[:, 0] != 0) | (data_reshaped[:, 1] != 0)
|
|
40
|
+
|
|
41
|
+
predictions = np.zeros(h * w, dtype=np.uint8)
|
|
42
|
+
|
|
43
|
+
if np.any(mask):
|
|
44
|
+
preds = clf.predict(data_reshaped[mask])
|
|
45
|
+
predictions[mask] = preds
|
|
46
|
+
|
|
47
|
+
out_img = predictions.reshape(1, h, w)
|
|
48
|
+
dst.write(out_img, window=window)
|
|
49
|
+
|
|
50
|
+
print(f"Classification saved to {output_path}")
|
|
51
|
+
|
cdts-0.1.0/cdts/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
from .raster import run_landtrendr_image, run_ccdc_image
|
|
6
|
+
|
|
7
|
+
def run_landtrendr(args: argparse.Namespace) -> None:
|
|
8
|
+
try:
|
|
9
|
+
run_landtrendr_image(
|
|
10
|
+
input_path=args.input,
|
|
11
|
+
output_dir=args.output_dir,
|
|
12
|
+
start_year=args.start_year,
|
|
13
|
+
max_segments=args.max_segments,
|
|
14
|
+
chunk_size=args.chunk_size,
|
|
15
|
+
n_jobs=args.jobs,
|
|
16
|
+
save_vertices=args.save_vertices,
|
|
17
|
+
event_type=args.event_type,
|
|
18
|
+
sort_by=args.sort_by,
|
|
19
|
+
min_mag=args.min_mag,
|
|
20
|
+
min_dur=args.min_dur,
|
|
21
|
+
pre_val_thresh=args.pre_val_thresh,
|
|
22
|
+
prefix=args.prefix,
|
|
23
|
+
output_scale_factor=args.output_scale
|
|
24
|
+
)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"Error running LandTrendr: {e}")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
def run_ccdc_cli(args: argparse.Namespace) -> None:
|
|
30
|
+
try:
|
|
31
|
+
# User needs to provide a list of dates. We'll read it from a text/csv file.
|
|
32
|
+
# Alternatively, if not provided, we can simulate dates for testing (but throw warning)
|
|
33
|
+
import os
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
dates: List[int] = []
|
|
37
|
+
if args.dates_file and os.path.exists(args.dates_file):
|
|
38
|
+
with open(args.dates_file, 'r') as f:
|
|
39
|
+
dates = [int(line.strip()) for line in f if line.strip().isdigit()]
|
|
40
|
+
else:
|
|
41
|
+
print("WARNING: No --dates-file provided. Assuming 1 observation every 16 days (Landsat).")
|
|
42
|
+
# We don't know the number of dates until we open the file, so we'll
|
|
43
|
+
# let process_file_ccdc handle it or we can hack it.
|
|
44
|
+
# Actually, we should force it or read the TIF first.
|
|
45
|
+
import rasterio
|
|
46
|
+
with rasterio.open(args.input) as src:
|
|
47
|
+
num_dates = src.count // args.num_bands
|
|
48
|
+
dates = list(np.arange(1, 1 + num_dates * 16, 16))
|
|
49
|
+
|
|
50
|
+
run_ccdc_image(
|
|
51
|
+
input_path=args.input,
|
|
52
|
+
output_dir=args.output_dir,
|
|
53
|
+
dates=dates,
|
|
54
|
+
num_bands=args.num_bands,
|
|
55
|
+
qa_band_idx=args.qa_band,
|
|
56
|
+
max_segments=args.max_segments,
|
|
57
|
+
chunk_size=args.chunk_size,
|
|
58
|
+
n_jobs=args.jobs,
|
|
59
|
+
prefix=args.prefix,
|
|
60
|
+
conseq_anom=6 if args.cold else 3
|
|
61
|
+
)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"Error running CCDC: {e}")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
def main() -> None:
|
|
67
|
+
parser = argparse.ArgumentParser(description="cdts: Change Detection Python Library")
|
|
68
|
+
subparsers = parser.add_subparsers(dest="command", help="Available algorithms")
|
|
69
|
+
|
|
70
|
+
# LandTrendr Subparser
|
|
71
|
+
lt_parser = subparsers.add_parser("landtrendr", help="Run LandTrendr algorithm")
|
|
72
|
+
lt_parser.add_argument("input", help="Path to input multi-band GeoTIFF")
|
|
73
|
+
lt_parser.add_argument("output_dir", help="Directory to save the outputs")
|
|
74
|
+
lt_parser.add_argument("--start-year", type=int, default=2000, help="Year of the first band (default: 2000)")
|
|
75
|
+
lt_parser.add_argument("--max-segments", type=int, default=6, help="Maximum number of segments (default: 6)")
|
|
76
|
+
lt_parser.add_argument("--jobs", type=int, default=-1, help="Number of CPU cores to use (-1 for all, default: -1)")
|
|
77
|
+
lt_parser.add_argument("--save-vertices", action="store_true", help="Save the raw vertices stack")
|
|
78
|
+
lt_parser.add_argument("--chunk-size", type=int, default=512, help="Size of the image chunks to process at once (default: 512)")
|
|
79
|
+
|
|
80
|
+
# Event Extraction options
|
|
81
|
+
lt_parser.add_argument("--event-type", choices=["loss", "gain"], default="loss", help="Event type to map (default: loss)")
|
|
82
|
+
lt_parser.add_argument("--sort-by", choices=["greatest", "newest", "fastest", "longest"], default="greatest", help="How to select the event (default: greatest)")
|
|
83
|
+
lt_parser.add_argument("--min-mag", type=float, default=0.0, help="Minimum magnitude filter")
|
|
84
|
+
lt_parser.add_argument("--min-dur", type=int, default=1, help="Minimum duration filter")
|
|
85
|
+
lt_parser.add_argument("--pre-val-thresh", type=float, default=0.0, help="Pre-value threshold filter")
|
|
86
|
+
lt_parser.add_argument("--prefix", default="lt_event", help="Prefix for output metric files")
|
|
87
|
+
lt_parser.add_argument("--output-scale", type=float, default=1.0, help="Scale factor to multiply output values (e.g. 0.0001 to convert back to float NDVI)")
|
|
88
|
+
|
|
89
|
+
# CCDC Subparser
|
|
90
|
+
ccdc_parser = subparsers.add_parser("ccdc", help="Run CCDC algorithm")
|
|
91
|
+
ccdc_parser.add_argument("input", help="Path to input stacked multi-band GeoTIFF")
|
|
92
|
+
ccdc_parser.add_argument("output_dir", help="Directory to save the outputs")
|
|
93
|
+
ccdc_parser.add_argument("--num-bands", type=int, default=6, help="Number of bands per date in the stack (default: 6)")
|
|
94
|
+
ccdc_parser.add_argument("--qa-band", type=int, default=-1, help="Index of QA band for cloud masking within the block (0-based, default: -1 for none)")
|
|
95
|
+
ccdc_parser.add_argument("--dates-file", help="Path to text file containing Julian dates (one per line)")
|
|
96
|
+
ccdc_parser.add_argument("--max-segments", type=int, default=6, help="Maximum number of segments (default: 6)")
|
|
97
|
+
ccdc_parser.add_argument("--chunk-size", type=int, default=512, help="Size of the image chunks to process at once (default: 512)")
|
|
98
|
+
ccdc_parser.add_argument("--jobs", type=int, default=-1, help="Number of CPU cores to use (-1 for all, default: -1)")
|
|
99
|
+
ccdc_parser.add_argument("--cold", action="store_true", help="Use COLD algorithm logic (6 consecutive anomalies instead of 3)")
|
|
100
|
+
ccdc_parser.add_argument("--prefix", default="ccdc", help="Prefix for output files (default: ccdc)")
|
|
101
|
+
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
|
|
104
|
+
if args.command == "landtrendr":
|
|
105
|
+
run_landtrendr(args)
|
|
106
|
+
elif args.command == "ccdc":
|
|
107
|
+
run_ccdc_cli(args)
|
|
108
|
+
else:
|
|
109
|
+
parser.print_help()
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|