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 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
+ [![Build and Publish Wheels](https://github.com/sacridini/cdts/actions/workflows/build_wheels.yml/badge.svg)](https://github.com/sacridini/cdts/actions/workflows/build_wheels.yml) [![Tests](https://github.com/sacridini/cdts/actions/workflows/tests.yml/badge.svg)](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
+ ]
@@ -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()