Additional Tutorials
Change
05. Calculating 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
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
search_gdf
exploring and filtering search results
search_gdf.plot("mgrs:grid_square", alpha=0.25)
search_gdf["s2:granule_id"].value_counts()
search_gdf["area"] = search_gdf.geometry.area
search_gdf.sort_values("area", ascending=False, inplace=True)
search_gdf[["area"]]
search_gdf.drop_duplicates("mgrs:grid_square", inplace=True)
search_gdf.iloc[0:4].plot("s2:granule_id", alpha=0.5)
# # 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
search_gdf2.plot("mgrs:grid_square", alpha=0.5)
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,
)
m = leafmap.Map()
for layer in search_gdf2_list:
m.add_stac_layer(layer, bands=["nir", "red", "green"], name=layer.split("/")[-1])
m
download files for analysis
def get_raster_band_urls(item: str, bands: list | None = None):
available_bands = leafmap.stac_bands(item)
stac = requests.get(item).json()
band_urls = {
x: stac["assets"][x]["href"]
for x in available_bands
if stac["assets"][x]["href"].startswith("http")
}
# if bands, only return bands in list
if bands:
band_urls = {x: band_urls[x] for x in bands if x 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)
for band, url in band_urls.items():
print(url)
out_file = f"{out_dir}/{layer.split('/')[-1]}_{band}.tif"
leafmap.download_file(url, out_file, overwrite=False)
def get_stac_crs(item):
stac = requests.get(item).json()
return stac["properties"]["proj:epsg"]
stac_crs = get_stac_crs(search_gdf2_list[0])
download_stac_layers(
search_gdf2_list,
"../Data/stac/dallas",
)
# 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
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)
import numpy as np
# fill nan with -2, outside of the range but not so far outside
ndvi[np.isnan(ndvi)] = -1.1
ndvi
ndvi_mult = ndvi * 10 # to be able to visualize more easily
ndvi_mult
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
# get histogram of ndvi image values
import matplotlib.pyplot as plt
plt.hist(ndvi[ndvi != -1.1].ravel(), bins=100, color="red", alpha=0.7)
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
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
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)
# 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
# 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
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,
)
reshaped_img = reshape_as_image(mosaic_bands_all_no_coastal)
reshaped_img.shape
mosaic_bands_all_no_coastal.shape
from sklearn.cluster import KMeans
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)
# 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,
)
# add a legend
m.add_colormap(
"Set1",
label="KMeans",
width=8.0,
height=0.4,
orientation="horizontal",
vmin=0,
vmax=9,
)
m