Processing
cdse.processing
Post-download processing utilities for CDSE products.
This module provides functions for cropping, reprojecting, extracting bands, and previewing downloaded Sentinel products. These operations help users work with data centered on their area of interest rather than full tiles.
Key features: - Extract specific bands from SAFE/ZIP products - Crop to bounding box (city, AOI) - Stack bands into multi-band GeoTIFF - Generate RGB preview images (PNG/JPEG) - Display interactive previews in Jupyter notebooks - Calculate vegetation indices (NDVI)
Requires: rasterio (install with: pip install cdse-client[processing])
crop_to_bbox(input_path: Union[str, Path], bbox: list[float], output_path: Optional[Union[str, Path]] = None, bands: Optional[list[str]] = None) -> Path
Crop a raster file to a bounding box.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_path
|
Union[str, Path]
|
Path to input raster file (GeoTIFF or JP2) |
required |
bbox
|
list[float]
|
Bounding box [min_lon, min_lat, max_lon, max_lat] in WGS84 |
required |
output_path
|
Optional[Union[str, Path]]
|
Output path (default: input_cropped.tif) |
None
|
bands
|
Optional[list[str]]
|
List of band indices to extract (1-based), None for all |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to cropped output file |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If input is invalid |
ImportError
|
If rasterio is not installed |
Example
cropped = crop_to_bbox( ... "S2A_MSIL2A.../B04.jp2", ... bbox=[9.15, 45.45, 9.20, 45.50], # Milan center ... )
extract_bands_from_safe(safe_path: Union[str, Path], bands: list[str], output_dir: Optional[Union[str, Path]] = None, resolution: int = 10) -> dict[str, Path]
Extract specific bands from a Sentinel-2 SAFE folder or ZIP.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
safe_path
|
Union[str, Path]
|
Path to .SAFE folder or .zip file |
required |
bands
|
list[str]
|
List of band names (e.g., ["B02", "B03", "B04", "B08"]) |
required |
output_dir
|
Optional[Union[str, Path]]
|
Output directory (default: same as input) |
None
|
resolution
|
int
|
Target resolution in meters (10, 20, or 60) |
10
|
Returns:
| Type | Description |
|---|---|
dict[str, Path]
|
Dictionary mapping band names to extracted file paths, one entry per |
dict[str, Path]
|
requested band |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If the path is missing or unsupported, if a band name is
unknown, if an L2A product is asked for a band coarser than
|
Example
bands = extract_bands_from_safe( ... "S2A_MSIL2A_20240115.zip", ... bands=["B04", "B03", "B02"], # RGB ... resolution=10 ... )
stack_bands(band_paths: dict[str, Path], output_path: Union[str, Path], band_order: Optional[list[str]] = None, resampling: Optional[str] = None) -> Path
Stack multiple bands into a single multi-band GeoTIFF.
Bands whose size differs from the first one are resampled to match. The
default is bilinear for spectral bands and nearest-neighbour for SCL,
which is categorical: averaging class 4 (vegetation) with class 8 (cloud)
does not give a class.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
band_paths
|
dict[str, Path]
|
Dictionary mapping band names to file paths |
required |
output_path
|
Union[str, Path]
|
Output GeoTIFF path |
required |
band_order
|
Optional[list[str]]
|
Order of bands in output (default: sorted keys) |
None
|
resampling
|
Optional[str]
|
rasterio resampling method name ( |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to output stacked GeoTIFF |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If |
Example
stacked = stack_bands( ... {"B04": "red.jp2", "B03": "green.jp2", "B02": "blue.jp2"}, ... "rgb_stack.tif", ... band_order=["B04", "B03", "B02"] ... )
crop_and_stack(safe_path: Union[str, Path], bbox: list[float], bands: Optional[list[str]] = None, output_path: Optional[Union[str, Path]] = None, resolution: int = 10) -> Path
Extract bands, crop to bbox, and stack into single GeoTIFF.
This is a convenience function combining extract, crop, and stack operations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
safe_path
|
Union[str, Path]
|
Path to .SAFE folder or .zip file |
required |
bbox
|
list[float]
|
Bounding box [min_lon, min_lat, max_lon, max_lat] |
required |
bands
|
Optional[list[str]]
|
Band names (default: true color B04, B03, B02) |
None
|
output_path
|
Optional[Union[str, Path]]
|
Output path (default: auto-generated) |
None
|
resolution
|
int
|
Target resolution in meters |
10
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to output cropped and stacked GeoTIFF |
Example
result = crop_and_stack( ... "S2A_MSIL2A_20240115.zip", ... bbox=[9.15, 45.45, 9.25, 45.55], # Milan ... bands=["B04", "B03", "B02", "B08"], # RGB + NIR ... )
normalized_difference(a_path: Union[str, Path], b_path: Union[str, Path], output_path: Union[str, Path], name: str = 'index') -> Path
Compute a normalized-difference index (a - b) / (a + b).
NDVI, NDWI, NDMI and NBR are all this formula with a different band pair;
see INDEX_BANDS. Pixels where a + b == 0 (no data in both bands)
are written as 0 without raising a numpy warning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a_path
|
Union[str, Path]
|
Path to the first band (e.g. NIR for NDVI) |
required |
b_path
|
Union[str, Path]
|
Path to the second band (e.g. Red for NDVI) |
required |
output_path
|
Union[str, Path]
|
Output GeoTIFF path |
required |
name
|
str
|
Band description written to the output (e.g. |
'index'
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to the output file (float32, values -1 to 1) |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If the two bands do not have the same shape |
calculate_ndvi(nir_path: Union[str, Path], red_path: Union[str, Path], output_path: Union[str, Path]) -> Path
Calculate NDVI (Normalized Difference Vegetation Index).
NDVI = (NIR - Red) / (NIR + Red). A thin wrapper over
normalized_difference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nir_path
|
Union[str, Path]
|
Path to NIR band (B08 for Sentinel-2) |
required |
red_path
|
Union[str, Path]
|
Path to Red band (B04 for Sentinel-2) |
required |
output_path
|
Union[str, Path]
|
Output GeoTIFF path |
required |
Returns:
| Type | Description |
|---|---|
Path
|
Path to NDVI output file (values -1 to 1) |
compute_index(safe_path: Union[str, Path], index: str, bbox: Optional[list[float]] = None, output_path: Optional[Union[str, Path]] = None, resolution: Optional[int] = None) -> Path
Compute a named index (ndvi, ndwi, ndmi, nbr) from a product.
Extracts the two bands of INDEX_BANDS[index] from a SAFE folder or ZIP,
optionally crops them to bbox, and writes the normalized difference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
safe_path
|
Union[str, Path]
|
Path to .SAFE folder or .zip file |
required |
index
|
str
|
One of the keys of |
required |
bbox
|
Optional[list[float]]
|
Optional bounding box [min_lon, min_lat, max_lon, max_lat] to crop to |
None
|
output_path
|
Optional[Union[str, Path]]
|
Output GeoTIFF path (default: |
None
|
resolution
|
Optional[int]
|
Resolution in metres. Default: the native one of the pair (10 for ndvi/ndwi, 20 for ndmi/nbr) |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to the index GeoTIFF (float32, -1 to 1, band named after the index) |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If |
Example
nbr = compute_index("S2A_MSIL2A_....zip", "nbr", bbox=[9.1, 45.4, 9.3, 45.5])
cloud_mask_from_scl(scl_path: Union[str, Path], output_path: Optional[Union[str, Path]] = None, classes: Any = SCL_CLOUD_CLASSES) -> Path
Build a clear-sky mask from a Sentinel-2 L2A scene classification (SCL) band.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scl_path
|
Union[str, Path]
|
Path to the SCL raster (extract it with
|
required |
output_path
|
Optional[Union[str, Path]]
|
Output GeoTIFF path (default: |
None
|
classes
|
Any
|
SCL classes to mask out. Default |
SCL_CLOUD_CLASSES
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to a uint8 GeoTIFF where 1 = clear and 0 = masked |
Example
mask = cloud_mask_from_scl("SCL_20m.jp2")
scl_clear_fraction(scl_path: Union[str, Path], classes: Any = SCL_CLOUD_CLASSES) -> float
Fraction of pixels in an SCL raster that are not cloud, shadow or no-data.
Useful to skip an acquisition whose tile-level cloud cover looked fine but whose clouds sit exactly over the area of interest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scl_path
|
Union[str, Path]
|
Path to the SCL raster, typically already cropped to the AOI |
required |
classes
|
Any
|
SCL classes counted as not clear (default |
SCL_CLOUD_CLASSES
|
Returns:
| Type | Description |
|---|---|
float
|
A value between 0.0 (fully masked) and 1.0 (fully clear) |
get_bounds_from_raster(raster_path: Union[str, Path]) -> tuple[list[float], str]
Get bounding box and CRS from a raster file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
Union[str, Path]
|
Path to raster file |
required |
Returns:
| Type | Description |
|---|---|
tuple[list[float], str]
|
Tuple of (bbox in WGS84 [min_lon, min_lat, max_lon, max_lat], original CRS string) |
reproject(input_path: Union[str, Path], output_path: Union[str, Path], target_crs: str = 'EPSG:4326', resolution: Optional[float] = None, resampling: str = 'bilinear') -> Path
Reproject a raster to a different CRS.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_path
|
Union[str, Path]
|
Input raster path |
required |
output_path
|
Union[str, Path]
|
Output raster path |
required |
target_crs
|
str
|
Target CRS (default: WGS84) |
'EPSG:4326'
|
resolution
|
Optional[float]
|
Target resolution (default: preserve original) |
None
|
resampling
|
str
|
rasterio resampling method name. Use |
'bilinear'
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to reprojected raster |
create_rgb_preview(input_path: Union[str, Path], output_path: Optional[Union[str, Path]] = None, bands: tuple[int, int, int] = (1, 2, 3), percentile_stretch: tuple[float, float] = (2, 98), size: Optional[tuple[int, int]] = None, format: str = 'PNG') -> Path
Create an RGB preview image from a multi-band raster.
Generates a color-balanced preview image suitable for visual inspection. Applies percentile stretching to enhance contrast.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_path
|
Union[str, Path]
|
Path to multi-band raster (GeoTIFF) |
required |
output_path
|
Optional[Union[str, Path]]
|
Output image path (default: input_preview.png) |
None
|
bands
|
tuple[int, int, int]
|
Tuple of band indices for R, G, B (1-based, default: 1, 2, 3) |
(1, 2, 3)
|
percentile_stretch
|
tuple[float, float]
|
Low and high percentiles for contrast stretch |
(2, 98)
|
size
|
Optional[tuple[int, int]]
|
Output size (width, height) or None for original size |
None
|
format
|
str
|
Output format ("PNG" or "JPEG") |
'PNG'
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to preview image |
Example
preview = create_rgb_preview( ... "milan_rgb.tif", ... bands=(1, 2, 3), # R, G, B order ... percentile_stretch=(2, 98) ... )
preview_product(safe_path: Union[str, Path], bbox: Optional[list[float]] = None, bands: Optional[list[str]] = None, resolution: int = 10, output_path: Optional[Union[str, Path]] = None, display: bool = True, size: tuple[int, int] = (800, 800)) -> dict[str, Any]
Generate and optionally display a preview of a Sentinel product.
This is a convenience function that extracts RGB bands, optionally crops to a bounding box, and generates a preview image. When run in Jupyter, it can display the preview inline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
safe_path
|
Union[str, Path]
|
Path to .SAFE folder or .zip file |
required |
bbox
|
Optional[list[float]]
|
Optional bounding box to crop [min_lon, min_lat, max_lon, max_lat] |
None
|
bands
|
Optional[list[str]]
|
Band names for RGB (default: ["B04", "B03", "B02"] for true color) |
None
|
resolution
|
int
|
Resolution in meters (10, 20, or 60) |
10
|
output_path
|
Optional[Union[str, Path]]
|
Where to save the preview (default: auto-generated) |
None
|
display
|
bool
|
Whether to display in Jupyter (default: True) |
True
|
size
|
tuple[int, int]
|
Preview size (width, height) |
(800, 800)
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dictionary with: |
dict[str, Any]
|
|
dict[str, Any]
|
|
dict[str, Any]
|
|
dict[str, Any]
|
|
dict[str, Any]
|
|
Example
result = preview_product( ... "S2A_MSIL2A_20240115.zip", ... bbox=[9.15, 45.45, 9.25, 45.55], # Milan ... display=True ... ) print(f"Preview saved to: {result['preview_path']}")
quick_preview(tiff_path: Union[str, Path], bands: tuple[int, int, int] = (1, 2, 3), figsize: tuple[int, int] = (10, 10), title: Optional[str] = None) -> Any
Quick preview using matplotlib (for Jupyter notebooks).
Simpler alternative to preview_product when you already have a processed GeoTIFF and want a quick visualization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tiff_path
|
Union[str, Path]
|
Path to GeoTIFF file |
required |
bands
|
tuple[int, int, int]
|
Band indices for RGB (1-based) |
(1, 2, 3)
|
figsize
|
tuple[int, int]
|
Figure size in inches |
(10, 10)
|
title
|
Optional[str]
|
Optional title for the plot |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
matplotlib figure object |
Example
fig = quick_preview("milan_rgb.tif", title="Milan - Sentinel-2")
create_thumbnail(input_path: Union[str, Path], output_path: Optional[Union[str, Path]] = None, size: tuple[int, int] = (256, 256), bands: tuple[int, int, int] = (1, 2, 3)) -> Path
Create a small thumbnail from a raster.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_path
|
Union[str, Path]
|
Path to input raster |
required |
output_path
|
Optional[Union[str, Path]]
|
Output thumbnail path |
None
|
size
|
tuple[int, int]
|
Thumbnail size (width, height) |
(256, 256)
|
bands
|
tuple[int, int, int]
|
Band indices for RGB |
(1, 2, 3)
|
Returns:
| Type | Description |
|---|---|
Path
|
Path to thumbnail |
Example
thumb = create_thumbnail("milan_rgb.tif", size=(128, 128))
compare_previews(paths: list[Union[str, Path]], titles: Optional[list[str]] = None, figsize: tuple[int, int] = (15, 5)) -> Any
Display multiple previews side by side for comparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
list[Union[str, Path]]
|
List of GeoTIFF paths to compare |
required |
titles
|
Optional[list[str]]
|
Optional titles for each image |
None
|
figsize
|
tuple[int, int]
|
Figure size |
(15, 5)
|
Returns:
| Type | Description |
|---|---|
Any
|
matplotlib figure |
Example
compare_previews( ... ["before.tif", "after.tif"], ... titles=["Before", "After"] ... )