Processing (optional)
Install:
Local RGB preview
preview_product() builds a true-color RGB preview (Sentinel-2: B04/B03/B02) from a downloaded product.
from cdse.processing import preview_product
result = preview_product(
safe_path="S2A_MSIL2A_....zip",
bbox=[9.10, 45.40, 9.28, 45.52],
resolution=10,
display=True,
)
print(result["preview_path"])
Crop and stack
from cdse.processing import crop_and_stack
tiff = crop_and_stack(
safe_path="S2A_MSIL2A_....zip",
bbox=[9.10, 45.40, 9.28, 45.52],
bands=["B04", "B03", "B02", "B08"],
resolution=10,
)
Bands and resolution
An L2A product ships three resolution folders, and which bands live in each is not simply "everything at or above its native resolution":
| Folder | Bands |
|---|---|
R10m |
B02, B03, B04, B08 |
R20m |
B01, B02, B03, B04, B05, B06, B07, B8A, B11, B12 |
R60m |
the 20m set, plus B09 |
Two consequences worth knowing. B08 exists only at 10m — B8A is its 20m and 60m counterpart, not a resampled B08 — and B01 and B09 are resampled up, so B01 is available at 20m despite being 60m native. B10 is dropped during L2A processing and exists in L1C only.
Asking for a band the requested folder does not contain raises
ValidationError naming the resolutions where it is available, rather
than quietly dropping it from the result. The agriculture, vegetation
and all_20m entries of BAND_COMBINATIONS need resolution=20.
L1C products have no resolution subfolders at all: every band comes at its
native resolution and the resolution argument selects nothing.
Indices: NDVI, NDWI, NDMI, NBR
All four are the same formula, (a - b) / (a + b), on a different band pair. INDEX_BANDS
lists the pairs; compute_index() goes from a product to a cropped index GeoTIFF in one call:
from cdse.processing import compute_index
ndvi = compute_index("S2A_MSIL2A_....zip", "ndvi", bbox=[9.10, 45.40, 9.28, 45.52])
nbr = compute_index("S2A_MSIL2A_....zip", "nbr", bbox=[9.10, 45.40, 9.28, 45.52])
| Index | Bands | Resolution | Used for |
|---|---|---|---|
ndvi |
B08, B04 | 10 m | vegetation vigour |
ndwi |
B03, B08 | 10 m | open water |
ndmi |
B8A, B11 | 20 m | vegetation moisture |
nbr |
B8A, B12 | 20 m | burn severity (pre minus post = dNBR) |
compute_index picks the native resolution of the pair unless you pass resolution=. For
bands you already have on disk, normalized_difference(a, b, out, name="NBR") does the
arithmetic alone, and calculate_ndvi(nir, red, out) is the same thing named NDVI.
Cloud mask from SCL
The tile-level cloud cover you filter on at search time says nothing about the clouds over
your field. L2A products carry a per-pixel scene classification (SCL, 20 m) that does:
from cdse.processing import cloud_mask_from_scl, extract_bands_from_safe, scl_clear_fraction
scl = extract_bands_from_safe("S2A_MSIL2A_....zip", ["SCL"], resolution=20)["SCL"]
if scl_clear_fraction(scl) < 0.7:
print("mostly cloud over the AOI, skip this date")
mask = cloud_mask_from_scl(scl) # uint8 GeoTIFF: 1 = clear, 0 = cloud/shadow/no-data
SCL_CLOUD_CLASSES (no-data, cloud shadow, medium and high probability cloud, thin cirrus) is
the default set masked out; pass classes= to change it. SCL_CLASSES names all twelve.
SCL is categorical
Never resample it bilinearly: the average of "vegetation" (4) and "cloud" (8) is not a
class. stack_bands uses nearest-neighbour for a band named SCL on its own; for other
tools, or for reproject, pass resampling="nearest" yourself.
Note
On some Windows/Python combinations, rasterio wheels may be unavailable. If you hit install issues, try Python 3.11/3.12 or conda-forge.