Tutorials
Calculating Landcover Change
05. Calculating landcover change over time
imports
import leafmap
import leafmap.colormaps as cm
import requests
import rasterio as rio
from rasterio.merge import merge
import glob
from rasterio.plot import reshape_as_image
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
searching for data
url = "https://earth-search.aws.element84.com/v1/"
collection = "sentinel-2-l2a"
time_range = "2023-08-01/2023-08-31"
# bbox for dallas metro
bbox = [
-97.06213756027009,
32.97324551867027,
-96.46807822577594,
33.3578329610085,
]
search_gdf = leafmap.stac_search(
url=url,
max_items=10,
collections=[collection],
bbox=bbox,
datetime=time_range,
query={"eo:cloud_cover": {"lt": 20}},
sortby=[{"field": "properties.eo:cloud_cover", "direction": "asc"}],
get_gdf=True,
)
search_gdf.columns
Index(['geometry', 'created', 'platform', 'constellation', 'instruments',
'eo:cloud_cover', 'mgrs:utm_zone', 'mgrs:latitude_band',
'mgrs:grid_square', 'grid:code', 'view:sun_azimuth',
'view:sun_elevation', 's2:degraded_msi_data_percentage',
's2:nodata_pixel_percentage', 's2:saturated_defective_pixel_percentage',
's2:dark_features_percentage', 's2:cloud_shadow_percentage',
's2:vegetation_percentage', 's2:not_vegetated_percentage',
's2:water_percentage', 's2:unclassified_percentage',
's2:medium_proba_clouds_percentage', 's2:high_proba_clouds_percentage',
's2:thin_cirrus_percentage', 's2:snow_ice_percentage',
's2:product_type', 's2:processing_baseline', 's2:product_uri',
's2:generation_time', 's2:datatake_id', 's2:datatake_type',
's2:datastrip_id', 's2:granule_id', 's2:reflectance_conversion_factor',
'datetime', 's2:sequence', 'earthsearch:s3_path',
'earthsearch:payload_id', 'earthsearch:boa_offset_applied',
'processing:software', 'updated', 'proj:code'],
dtype='str')
search_gdf
| geometry | created | platform | constellation | instruments | eo:cloud_cover | mgrs:utm_zone | mgrs:latitude_band | mgrs:grid_square | grid:code | ... | s2:granule_id | s2:reflectance_conversion_factor | datetime | s2:sequence | earthsearch:s3_path | earthsearch:payload_id | earthsearch:boa_offset_applied | processing:software | updated | proj:code | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | POLYGON ((-96.84938 33.42092, -96.87321 32.431... | 2023-08-20T04:40:37.325Z | sentinel-2b | sentinel-2 | [msi] | 0.004539 | 14 | S | QB | MGRS-14SQB | ... | S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A0337... | 0.975198 | 2023-08-19T17:14:46.266000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/Q... | roda-sentinel2/workflow-sentinel2-to-stac/a707... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-20T04:40:37.325Z | EPSG:32614 |
| 1 | POLYGON ((-96.84938 33.42092, -96.87321 32.431... | 2023-08-05T04:59:04.872Z | sentinel-2a | sentinel-2 | [msi] | 0.005262 | 14 | S | QB | MGRS-14SQB | ... | S2A_OPER_MSI_L2A_TL_2APS_20230804T235402_A0423... | 0.970632 | 2023-08-04T17:14:46.231000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/Q... | roda-sentinel2/workflow-sentinel2-to-stac/73a7... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-05T04:59:04.872Z | EPSG:32614 |
| 2 | POLYGON ((-97.91284 34.33683, -97.92531 33.346... | 2023-08-18T06:10:25.042Z | sentinel-2a | sentinel-2 | [msi] | 0.006437 | 14 | S | PC | MGRS-14SPC | ... | S2A_OPER_MSI_L2A_TL_2APS_20230818T002256_A0425... | 0.974489 | 2023-08-17T17:24:31.907000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/P... | roda-sentinel2/workflow-sentinel2-to-stac/a898... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-18T06:10:25.042Z | EPSG:32614 |
| 3 | POLYGON ((-97.0579 33.42436, -97.31949 32.4379... | 2023-08-20T04:36:25.463Z | sentinel-2b | sentinel-2 | [msi] | 0.009056 | 14 | S | PB | MGRS-14SPB | ... | S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A0337... | 0.975198 | 2023-08-19T17:14:49.046000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/P... | roda-sentinel2/workflow-sentinel2-to-stac/ee80... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-20T04:36:25.463Z | EPSG:32614 |
| 4 | POLYGON ((-97.06177 33.42442, -97.32332 32.437... | 2023-08-15T04:07:22.379Z | sentinel-2a | sentinel-2 | [msi] | 0.014211 | 14 | S | PB | MGRS-14SPB | ... | S2A_OPER_MSI_L2A_TL_2APS_20230815T002553_A0425... | 0.973476 | 2023-08-14T17:14:49.390000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/P... | roda-sentinel2/workflow-sentinel2-to-stac/ccee... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-15T04:07:22.379Z | EPSG:32614 |
| 5 | POLYGON ((-96.84938 33.42092, -96.87321 32.431... | 2023-08-15T04:10:46.325Z | sentinel-2a | sentinel-2 | [msi] | 0.014575 | 14 | S | QB | MGRS-14SQB | ... | S2A_OPER_MSI_L2A_TL_2APS_20230815T002553_A0425... | 0.973476 | 2023-08-14T17:14:46.612000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/Q... | roda-sentinel2/workflow-sentinel2-to-stac/cac3... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-15T04:10:46.325Z | EPSG:32614 |
| 6 | POLYGON ((-96.81576 34.32217, -97.08134 33.336... | 2023-08-20T04:30:59.255Z | sentinel-2b | sentinel-2 | [msi] | 0.017386 | 14 | S | PC | MGRS-14SPC | ... | S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A0337... | 0.975198 | 2023-08-19T17:14:35.561000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/P... | roda-sentinel2/workflow-sentinel2-to-stac/741d... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-20T04:30:59.255Z | EPSG:32614 |
| 7 | POLYGON ((-96.82664 34.32236, -96.85153 33.333... | 2023-08-18T06:09:55.100Z | sentinel-2a | sentinel-2 | [msi] | 0.040440 | 14 | S | QC | MGRS-14SQC | ... | S2A_OPER_MSI_L2A_TL_2APS_20230818T002256_A0425... | 0.974489 | 2023-08-17T17:24:28.416000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/Q... | roda-sentinel2/workflow-sentinel2-to-stac/c613... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-18T06:09:55.100Z | EPSG:32614 |
| 8 | POLYGON ((-97.05919 33.42438, -97.32056 32.437... | 2023-08-05T04:52:48.750Z | sentinel-2a | sentinel-2 | [msi] | 0.059570 | 14 | S | PB | MGRS-14SPB | ... | S2A_OPER_MSI_L2A_TL_2APS_20230804T235402_A0423... | 0.970632 | 2023-08-04T17:14:49.014000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/P... | roda-sentinel2/workflow-sentinel2-to-stac/a5ff... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-05T04:52:48.750Z | EPSG:32614 |
| 9 | POLYGON ((-96.81576 34.32217, -96.8278 34.2776... | 2023-08-20T04:44:38.807Z | sentinel-2b | sentinel-2 | [msi] | 0.076934 | 14 | S | QC | MGRS-14SQC | ... | S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A0337... | 0.975198 | 2023-08-19T17:14:31.780000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/Q... | roda-sentinel2/workflow-sentinel2-to-stac/2524... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-20T04:44:38.807Z | EPSG:32614 |
10 rows × 42 columns
exploring and filtering search results
search_gdf.plot("mgrs:grid_square", alpha=0.25)
<Axes: >

search_gdf["s2:granule_id"].value_counts()
s2:granule_id
S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A033700_T14SQB_N05.09 1
S2A_OPER_MSI_L2A_TL_2APS_20230804T235402_A042394_T14SQB_N05.09 1
S2A_OPER_MSI_L2A_TL_2APS_20230818T002256_A042580_T14SPC_N05.09 1
S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A033700_T14SPB_N05.09 1
S2A_OPER_MSI_L2A_TL_2APS_20230815T002553_A042537_T14SPB_N05.09 1
S2A_OPER_MSI_L2A_TL_2APS_20230815T002553_A042537_T14SQB_N05.09 1
S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A033700_T14SPC_N05.09 1
S2A_OPER_MSI_L2A_TL_2APS_20230818T002256_A042580_T14SQC_N05.09 1
S2A_OPER_MSI_L2A_TL_2APS_20230804T235402_A042394_T14SPB_N05.09 1
S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A033700_T14SQC_N05.09 1
Name: count, dtype: int64
search_gdf["area"] = search_gdf.geometry.area
search_gdf.sort_values("area", ascending=False, inplace=True)
search_gdf[["area"]]
| area | |
|---|---|
| 2 | 1.174444 |
| 9 | 1.172798 |
| 0 | 1.161135 |
| 1 | 1.161135 |
| 5 | 1.161135 |
| 7 | 0.620451 |
| 4 | 0.432199 |
| 8 | 0.429551 |
| 3 | 0.428386 |
| 6 | 0.213553 |
search_gdf.drop_duplicates("mgrs:grid_square", inplace=True)
search_gdf.iloc[0:4].plot("s2:granule_id", alpha=0.5)
<Axes: >

# # create a query searching for unique datastrip ids based on the above
q = {"s2:granule_id": {"in": search_gdf["s2:granule_id"].iloc[0:4].unique().tolist()}}
search_gdf2 = leafmap.stac_search(
url=url,
max_items=4,
collections=[collection],
bbox=bbox,
datetime=time_range,
query=q,
get_gdf=True,
)
search_gdf2
| geometry | created | platform | constellation | instruments | eo:cloud_cover | mgrs:utm_zone | mgrs:latitude_band | mgrs:grid_square | grid:code | ... | s2:granule_id | s2:reflectance_conversion_factor | datetime | s2:sequence | earthsearch:s3_path | earthsearch:payload_id | earthsearch:boa_offset_applied | processing:software | updated | proj:code | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | POLYGON ((-96.84938 33.42092, -96.87321 32.431... | 2023-08-20T04:40:37.325Z | sentinel-2b | sentinel-2 | [msi] | 0.004539 | 14 | S | QB | MGRS-14SQB | ... | S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A0337... | 0.975198 | 2023-08-19T17:14:46.266000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/Q... | roda-sentinel2/workflow-sentinel2-to-stac/a707... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-20T04:40:37.325Z | EPSG:32614 |
| 1 | POLYGON ((-96.81576 34.32217, -96.8278 34.2776... | 2023-08-20T04:44:38.807Z | sentinel-2b | sentinel-2 | [msi] | 0.076934 | 14 | S | QC | MGRS-14SQC | ... | S2B_OPER_MSI_L2A_TL_2BPS_20230819T223356_A0337... | 0.975198 | 2023-08-19T17:14:31.780000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/Q... | roda-sentinel2/workflow-sentinel2-to-stac/2524... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-20T04:44:38.807Z | EPSG:32614 |
| 2 | POLYGON ((-97.91284 34.33683, -97.92531 33.346... | 2023-08-18T06:10:25.042Z | sentinel-2a | sentinel-2 | [msi] | 0.006437 | 14 | S | PC | MGRS-14SPC | ... | S2A_OPER_MSI_L2A_TL_2APS_20230818T002256_A0425... | 0.974489 | 2023-08-17T17:24:31.907000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/P... | roda-sentinel2/workflow-sentinel2-to-stac/a898... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-18T06:10:25.042Z | EPSG:32614 |
| 3 | POLYGON ((-97.06177 33.42442, -97.32332 32.437... | 2023-08-15T04:07:22.379Z | sentinel-2a | sentinel-2 | [msi] | 0.014211 | 14 | S | PB | MGRS-14SPB | ... | S2A_OPER_MSI_L2A_TL_2APS_20230815T002553_A0425... | 0.973476 | 2023-08-14T17:14:49.390000Z | 0 | s3://sentinel-cogs/sentinel-s2-l2a-cogs/14/S/P... | roda-sentinel2/workflow-sentinel2-to-stac/ccee... | True | {'sentinel2-to-stac': '0.1.1'} | 2023-08-15T04:07:22.379Z | EPSG:32614 |
4 rows × 42 columns
search_gdf2.plot("mgrs:grid_square", alpha=0.5)
<Axes: >

plot to accentuate paved and non-paved divide
search_gdf2_list = leafmap.stac_search(
url=url,
max_items=4,
collections=[collection],
bbox=bbox,
datetime=time_range,
query=q,
get_links=True,
)
search_gdf2_list
['https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2B_14SQB_20230819_0_L2A',
'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2B_14SQC_20230819_0_L2A',
'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2A_14SPC_20230817_0_L2A',
'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2A_14SPB_20230814_0_L2A']
download files for analysis
def get_item_json(item_url: str) -> dict:
r = requests.get(item_url, timeout=30)
r.raise_for_status()
return r.json()
def get_raster_band_urls(item_url: str, bands: list | None = None) -> dict:
"""Return {band_name: cog_url} for a STAC item, no titiler dependency."""
stac = get_item_json(item_url)
band_urls = {
key: asset["href"]
for key, asset in stac["assets"].items()
if asset["href"].startswith("http")
and "image/tiff"
in asset.get("type", "") # COGs only; skips thumbnail jpg, xml metadata, jp2
}
if bands:
band_urls = {b: band_urls[b] for b in bands if b in band_urls}
return band_urls
def download_stac_layers(layers, out_dir, bands=None):
for layer in layers:
band_urls = get_raster_band_urls(layer, bands)
if not band_urls:
print(f"WARNING: no COG assets found for {layer}")
continue
for band, url in band_urls.items():
out_file = f"{out_dir}/{layer.split('/')[-1]}_{band}.tif"
leafmap.download_file(url, out_file, overwrite=False)
def get_stac_crs(item_url: str) -> int:
"""Handle both proj:epsg (projection ext v1) and proj:code (v2, e.g. 'EPSG:32614')."""
stac = get_item_json(item_url)
props = stac.get("properties", {})
if "proj:epsg" in props:
return props["proj:epsg"]
if "proj:code" in props:
return int(props["proj:code"].split(":")[-1])
# some catalogs moved projection info to the asset level
for asset in stac["assets"].values():
if "proj:code" in asset:
return int(asset["proj:code"].split(":")[-1])
if "proj:epsg" in asset:
return asset["proj:epsg"]
raise KeyError(f"No projection metadata found on {item_url}")
stac_crs = get_stac_crs(search_gdf2_list[0])
stac_crs
32614
download_stac_layers(
search_gdf2_list,
"../Data/stac/dallas",
)
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_aot.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_blue.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_coastal.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_green.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_nir.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_nir08.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_nir09.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_red.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_rededge1.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_rededge2.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_rededge3.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_scl.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_swir16.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_swir22.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_visual.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQB_20230819_0_L2A_wvp.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_aot.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_blue.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_coastal.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_green.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_nir.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_nir08.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_nir09.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_red.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_rededge1.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_rededge2.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_rededge3.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_scl.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_swir16.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_swir22.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_visual.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2B_14SQC_20230819_0_L2A_wvp.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_aot.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_blue.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_coastal.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_green.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_nir.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_nir08.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_nir09.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_red.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_rededge1.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_rededge2.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_rededge3.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_scl.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_swir16.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_swir22.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_visual.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPC_20230817_0_L2A_wvp.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_aot.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_blue.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_coastal.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_green.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_nir.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_nir08.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_nir09.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_red.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_rededge1.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_rededge2.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_rededge3.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_scl.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_swir16.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_swir22.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_visual.tif already exists. Skip downloading. Set overwrite=True to overwrite.
../Data/stac/dallas/S2A_14SPB_20230814_0_L2A_wvp.tif already exists. Skip downloading. Set overwrite=True to overwrite.
# mosaic files based on band name
def mosaic_by_band(
dir,
bands: list = [
"red",
"blue",
"green",
"nir",
"coastal",
"nir08",
"nir09",
"rededge1",
"rededge2",
"rededge3",
"scl",
"swir16",
"swir22",
],
crs: str = None,
):
mosaics = {}
for band in bands:
files = glob.glob(f"{dir}/*{band}.tif")
out_file = f"{dir}/mosaic_{band}_.tif"
raster_data = [rio.open(f) for f in files]
mosaic, out_trans = merge(raster_data)
out_meta = raster_data[0].meta.copy()
out_meta.update(
{
"driver": "GTiff",
"height": mosaic.shape[1],
"width": mosaic.shape[2],
"transform": out_trans,
"crs": f"epsg:{crs}",
}
)
with rio.open(out_file, "w", **out_meta) as dest:
dest.write(mosaic)
mosaics[band] = out_file
return mosaics
mosaic_bands_all = mosaic_by_band("../Data/stac/dallas", crs=stac_crs)
mosaic_bands_all
{'red': '../Data/stac/dallas/mosaic_red_.tif',
'blue': '../Data/stac/dallas/mosaic_blue_.tif',
'green': '../Data/stac/dallas/mosaic_green_.tif',
'nir': '../Data/stac/dallas/mosaic_nir_.tif',
'coastal': '../Data/stac/dallas/mosaic_coastal_.tif',
'nir08': '../Data/stac/dallas/mosaic_nir08_.tif',
'nir09': '../Data/stac/dallas/mosaic_nir09_.tif',
'rededge1': '../Data/stac/dallas/mosaic_rededge1_.tif',
'rededge2': '../Data/stac/dallas/mosaic_rededge2_.tif',
'rededge3': '../Data/stac/dallas/mosaic_rededge3_.tif',
'scl': '../Data/stac/dallas/mosaic_scl_.tif',
'swir16': '../Data/stac/dallas/mosaic_swir16_.tif',
'swir22': '../Data/stac/dallas/mosaic_swir22_.tif'}
nir = rio.open(mosaic_bands_all["nir"]).read(1).astype("float32")
red = rio.open(mosaic_bands_all["red"]).read(1).astype("float32")
calculate NDVI
ndvi = (nir - red) / (nir + red)
# fill nan with -2, outside of the range but not so far outside
ndvi[np.isnan(ndvi)] = -1.1
ndvi
array([[ 0.36826923, 0.36826923, 0.36826923, ..., 0.72460496,
0.7580539 , 0.7661823 ],
[ 0.36826923, 0.36826923, 0.36826923, ..., 0.75057286,
0.75632185, 0.7632707 ],
[ 0.36826923, 0.36826923, 0.36826923, ..., 0.75344265,
0.7732096 , 0.7714475 ],
...,
[-1.1 , -1.1 , -1.1 , ..., 0.28488854,
0.29016393, 0.29233912],
[-1.1 , -1.1 , -1.1 , ..., 0.2907453 ,
0.2860676 , 0.29248366],
[-1.1 , -1.1 , -1.1 , ..., 0.29306123,
0.27983367, 0.277882 ]], shape=(20982, 20976), dtype=float32)
ndvi_mult = ndvi * 10 # to be able to visualize more easily
ndvi_mult
array([[ 3.6826923, 3.6826923, 3.6826923, ..., 7.24605 ,
7.5805387, 7.6618233],
[ 3.6826923, 3.6826923, 3.6826923, ..., 7.5057287,
7.5632186, 7.6327066],
[ 3.6826923, 3.6826923, 3.6826923, ..., 7.5344267,
7.7320957, 7.7144747],
...,
[-11. , -11. , -11. , ..., 2.8488853,
2.9016395, 2.923391 ],
[-11. , -11. , -11. , ..., 2.9074528,
2.860676 , 2.9248366],
[-11. , -11. , -11. , ..., 2.9306123,
2.7983367, 2.77882 ]], shape=(20982, 20976), dtype=float32)
ndvi_image = leafmap.array_to_image(ndvi, source=mosaic_bands_all["nir"])
ndvi_multi_image = leafmap.array_to_image(ndvi_mult, source=mosaic_bands_all["nir"])
m = leafmap.Map()
m.add_raster(ndvi_multi_image, layer_name="NDVI", vmin=-10, vmax=10, cmap="PiYG")
m
Map(center=[33.371449, -96.78545], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title',…
# get histogram of ndvi image values
plt.hist(ndvi[ndvi != -1.1].ravel(), bins=100, color="red", alpha=0.7)
(array([1.0000000e+00, 1.0000000e+00, 0.0000000e+00, 0.0000000e+00,
0.0000000e+00, 1.0000000e+00, 0.0000000e+00, 2.0000000e+00,
5.0000000e+00, 1.8000000e+01, 2.7000000e+01, 9.5000000e+01,
2.7300000e+02, 5.7100000e+02, 9.5100000e+02, 1.4730000e+03,
2.2680000e+03, 3.1480000e+03, 4.4330000e+03, 6.0950000e+03,
8.7460000e+03, 1.2123000e+04, 1.5750000e+04, 1.9139000e+04,
2.3716000e+04, 2.8782000e+04, 3.4653000e+04, 4.5633000e+04,
6.4668000e+04, 7.6474000e+04, 8.9700000e+04, 1.2927000e+05,
1.6543500e+05, 2.3599500e+05, 3.1663400e+05, 3.8703400e+05,
4.7900100e+05, 6.2561400e+05, 7.5519000e+05, 9.0728100e+05,
1.0850600e+06, 1.1202380e+06, 8.8427400e+05, 6.4577800e+05,
6.0382200e+05, 5.3926600e+05, 3.4452000e+05, 3.3818100e+05,
3.4321800e+05, 4.3010100e+05, 5.9539900e+05, 7.7239800e+05,
1.0676070e+06, 1.3586870e+06, 1.6881750e+06, 2.1018380e+06,
2.6668380e+06, 3.6052190e+06, 4.7291470e+06, 6.0544950e+06,
7.6989430e+06, 9.4903910e+06, 1.1130270e+07, 1.2534731e+07,
1.3674737e+07, 1.4563089e+07, 1.5116707e+07, 1.5522868e+07,
1.5675501e+07, 1.5614456e+07, 1.5352237e+07, 1.4917200e+07,
1.4369598e+07, 1.3655650e+07, 1.2803444e+07, 1.1886788e+07,
1.0984785e+07, 1.0146013e+07, 9.3278850e+06, 8.6188730e+06,
7.9977950e+06, 7.4678020e+06, 7.0248890e+06, 6.6651540e+06,
6.4014320e+06, 6.2831160e+06, 6.3451950e+06, 6.6194690e+06,
7.1241650e+06, 7.7236790e+06, 7.7266050e+06, 5.2941400e+06,
1.6410080e+06, 1.6154700e+05, 8.3670000e+03, 7.4200000e+02,
1.5000000e+01, 8.0000000e+00, 5.0000000e+00, 2.6400000e+02]),
array([-0.99696511, -0.97700125, -0.95703739, -0.93707347, -0.91710961,
-0.89714575, -0.87718189, -0.85721803, -0.83725417, -0.81729025,
-0.79732639, -0.77736253, -0.75739866, -0.73743474, -0.71747088,
-0.69750702, -0.67754316, -0.6575793 , -0.63761544, -0.61765158,
-0.59768766, -0.5777238 , -0.55775994, -0.53779602, -0.51783216,
-0.49786833, -0.47790444, -0.45794058, -0.43797672, -0.41801286,
-0.39804894, -0.37808508, -0.35812122, -0.33815736, -0.3181935 ,
-0.29822958, -0.27826571, -0.25830185, -0.23833799, -0.21837413,
-0.19841021, -0.17844635, -0.15848249, -0.13851863, -0.11855477,
-0.09859091, -0.07862699, -0.05866313, -0.03869927, -0.01873541,
0.00122845, 0.02119237, 0.04115623, 0.06112009, 0.08108395,
0.10104781, 0.12101167, 0.14097553, 0.1609394 , 0.18090338,
0.20086724, 0.2208311 , 0.24079496, 0.26075882, 0.28072268,
0.30068654, 0.3206504 , 0.34061426, 0.36057812, 0.38054198,
0.40050596, 0.42046982, 0.44043368, 0.46039754, 0.4803614 ,
0.50032526, 0.52028912, 0.54025298, 0.56021684, 0.5801807 ,
0.60014468, 0.62010854, 0.64007241, 0.66003627, 0.68000013,
0.69996399, 0.71992785, 0.73989171, 0.75985557, 0.77981943,
0.79978329, 0.81974727, 0.83971113, 0.85967499, 0.87963885,
0.89960271, 0.91956657, 0.93953043, 0.95949429, 0.97945815,
0.99942213]),
<BarContainer object of 100 artists>)

def categorize_np_array(array):
cat_array = np.zeros(array.shape)
cat_array[array < 1] = 5
cat_array[array < 0.8] = 4
cat_array[array < 0.6] = 3
cat_array[array < 0.4] = 2
cat_array[array < 0.2] = 1
return cat_array
cat_ndvi = categorize_np_array(ndvi)
cat_ndvi
array([[2., 2., 2., ..., 4., 4., 4.],
[2., 2., 2., ..., 4., 4., 4.],
[2., 2., 2., ..., 4., 4., 4.],
...,
[1., 1., 1., ..., 2., 2., 2.],
[1., 1., 1., ..., 2., 2., 2.],
[1., 1., 1., ..., 2., 2., 2.]], shape=(20982, 20976))
ndvi_image_cat = leafmap.array_to_image(cat_ndvi, source=mosaic_bands_all["nir"])
m = leafmap.Map()
m.add_raster(
ndvi_image_cat, layer_name="NDVI", vmin=1, vmax=5, cmap="Set1", draw_control=False
)
m.add_colormap(
"Set1",
label="ndvi",
width=8.0,
height=0.4,
orientation="horizontal",
vmin=1,
vmax=5,
)
m
Map(center=[33.371449, -96.78545], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title',…
k-means
def resample_raster(in_file, out_file, upscale_factor=0.5):
with rio.open(in_file) as dataset:
# resample data to target shape
data = dataset.read(
out_shape=(
dataset.count,
int(dataset.height * upscale_factor),
int(dataset.width * upscale_factor),
),
resampling=rio.enums.Resampling.bilinear,
)
# scale image transform
transform = dataset.transform * dataset.transform.scale(
(dataset.width / data.shape[-1]), (dataset.height / data.shape[-2])
)
# update metadata
meta = dataset.meta.copy()
meta.update(
{
"height": data.shape[1],
"width": data.shape[2],
"transform": transform,
}
)
with rio.open(out_file, "w", **meta) as dst:
dst.write(data)
for band in mosaic_bands_all.keys():
print(band, rio.open(mosaic_bands_all[band]).read(1).shape)
red (20982, 20976)
blue (20982, 20976)
green (20982, 20976)
nir (20982, 20976)
coastal (3497, 3496)
nir08 (10491, 10488)
nir09 (3497, 3496)
rededge1 (10491, 10488)
rededge2 (10491, 10488)
rededge3 (10491, 10488)
scl (10491, 10488)
swir16 (10491, 10488)
swir22 (10491, 10488)
# set resampling ratio for each band- note that some are higher resolution than others and need a different scaling factor
bands_to_resample = {
"red": 0.25,
"blue": 0.25,
"green": 0.25,
"nir": 0.25,
"nir08": 0.5,
"rededge1": 0.5,
"rededge2": 0.5,
"rededge3": 0.5,
"scl": 0.5,
"swir16": 0.5,
"swir22": 0.5,
}
# resample the bands so they are all smaller and uniform
for band, ratio in bands_to_resample.items():
resample_raster(
mosaic_bands_all[band], f"../Data/stac/dallas/upscaled_{band}.tif", ratio
)
mosaic_bands_all[f"{band}"] = f"../Data/stac/dallas/upscaled_{band}.tif"
mosaic_bands_all
{'red': '../Data/stac/dallas/upscaled_red.tif',
'blue': '../Data/stac/dallas/upscaled_blue.tif',
'green': '../Data/stac/dallas/upscaled_green.tif',
'nir': '../Data/stac/dallas/upscaled_nir.tif',
'coastal': '../Data/stac/dallas/mosaic_coastal_.tif',
'nir08': '../Data/stac/dallas/upscaled_nir08.tif',
'nir09': '../Data/stac/dallas/mosaic_nir09_.tif',
'rededge1': '../Data/stac/dallas/upscaled_rededge1.tif',
'rededge2': '../Data/stac/dallas/upscaled_rededge2.tif',
'rededge3': '../Data/stac/dallas/upscaled_rededge3.tif',
'scl': '../Data/stac/dallas/upscaled_scl.tif',
'swir16': '../Data/stac/dallas/upscaled_swir16.tif',
'swir22': '../Data/stac/dallas/upscaled_swir22.tif'}
# create a new dict with all bands excpct coastal
mosaic_bands_all_no_coastal = {
k: v for k, v in mosaic_bands_all.items() if k not in ["coastal", "nir09"]
}
mosaic_bands_all_no_coastal
{'red': '../Data/stac/dallas/upscaled_red.tif',
'blue': '../Data/stac/dallas/upscaled_blue.tif',
'green': '../Data/stac/dallas/upscaled_green.tif',
'nir': '../Data/stac/dallas/upscaled_nir.tif',
'nir08': '../Data/stac/dallas/upscaled_nir08.tif',
'rededge1': '../Data/stac/dallas/upscaled_rededge1.tif',
'rededge2': '../Data/stac/dallas/upscaled_rededge2.tif',
'rededge3': '../Data/stac/dallas/upscaled_rededge3.tif',
'scl': '../Data/stac/dallas/upscaled_scl.tif',
'swir16': '../Data/stac/dallas/upscaled_swir16.tif',
'swir22': '../Data/stac/dallas/upscaled_swir22.tif'}
mosaic_bands_all_no_coastal_cols = mosaic_bands_all_no_coastal.keys()
for band in mosaic_bands_all_no_coastal.keys():
print(band, rio.open(mosaic_bands_all_no_coastal[band]).read(1).shape)
mosaic_bands_all_no_coastal = np.stack(
[
rio.open(mosaic_bands_all_no_coastal[band]).read(1)
for band in mosaic_bands_all_no_coastal.keys()
],
axis=-1,
)
red (5245, 5244)
blue (5245, 5244)
green (5245, 5244)
nir (5245, 5244)
nir08 (5245, 5244)
rededge1 (5245, 5244)
rededge2 (5245, 5244)
rededge3 (5245, 5244)
scl (5245, 5244)
swir16 (5245, 5244)
swir22 (5245, 5244)
reshaped_img = reshape_as_image(mosaic_bands_all_no_coastal)
reshaped_img.shape
(5244, 11, 5245)
mosaic_bands_all_no_coastal.shape
(5245, 5244, 11)
rows, cols, bands = mosaic_bands_all_no_coastal.shape
k = 10 # num of clusters
kmeans_predictions = KMeans(n_clusters=k, random_state=0).fit(
mosaic_bands_all_no_coastal.reshape(rows * cols, bands)
)
kmeans_predictions_2d = kmeans_predictions.labels_.reshape(rows, cols)
kmeans_predictions_2d
array([[7, 3, 4, ..., 5, 5, 5],
[7, 4, 3, ..., 5, 5, 5],
[9, 3, 3, ..., 5, 5, 5],
...,
[1, 1, 1, ..., 4, 4, 4],
[1, 1, 1, ..., 4, 4, 3],
[1, 1, 1, ..., 4, 3, 3]], shape=(5245, 5244), dtype=int32)
# plot prediction results
m = leafmap.Map()
m.add_raster(
leafmap.array_to_image(
kmeans_predictions_2d,
source=mosaic_bands_all["red"],
),
layer_name="KMeans",
colormap="Set1",
draw_control=False,
vmin=0,
vmax=9,
)
# add a legend
m.add_colormap(
"Set1",
label="KMeans",
width=8.0,
height=0.4,
orientation="horizontal",
vmin=0,
vmax=9,
)
m
Map(center=[33.371449, -96.78545], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title',…
xb, yb = "red", "nir" # two test bands
labels = kmeans_predictions_2d.reshape(-1)
df = pd.DataFrame(
mosaic_bands_all_no_coastal.reshape(rows * cols, bands)[idx],
columns=mosaic_bands_all_no_coastal_cols,
)
df["cluster"] = labels[idx]
print(df.columns.to_list())
g = sns.FacetGrid(
df,
col="cluster",
col_wrap=4,
height=2.6,
aspect=1,
)
def panel(data, color, **kws):
ax = plt.gca()
# reference points
ax.scatter(
df[xb],
df[yb],
s=2,
color="0.85",
linewidth=0,
rasterized=True,
)
k = data["cluster"].iloc[0]
ax.scatter(data[xb], data[yb], s=3, color=palette[k], alpha=0.5, linewidth=0)
g.map_dataframe(panel)
g.set_axis_labels(f"{xb} (DN)", f"{yb} (DN)")
g.set_titles("cluster {col_name}")
g.figure.suptitle("NIR vs Red by cluster", y=1.03)
['red', 'blue', 'green', 'nir', 'nir08', 'rededge1', 'rededge2', 'rededge3', 'scl', 'swir16', 'swir22', 'cluster']
Text(0.5, 1.03, 'NIR vs Red by cluster')
