Metadata is “data about data”. It describes the content, structure, origin, quality, location, format, and appropriate use of a dataset or file.
Metadata is generally more important than the data itself, as it gives meaning to the dataset and helps understand why the data exists in the first place. Metadata can be outside of data files; this is what the data story presented in the Data Story notebook. It can also be found in the header of data files, such as the GeoTIFF we are working with.
This notebook shows how to use rasterio to read and understand metadata (the coordinate reference system, spatial resolution, raster dimensions, bounds, nodata values, and value ranges) when working with bathymetry.
In this notebook, we will:
Overview¶
Read/Load one bathymetry GeoTIFF with
rasterio.See how to display the metadata with pretty display
Inspect raster’s coordinate reference system, bounds, dimensions, resolution, nodata value, and bathymetry range.
Prerequisites¶
| Concepts | Importance | Notes |
|---|---|---|
| NumPy arrays | Necessary | Raster bands are NumPy arrays |
| Matplotlib | Helpful | Used for all plots |
| Coordinate reference systems | Helpful | Rasters carry a CRS and a transform |
| Seabed Morphology | Helpful | Why bathymetry grids matter here |
Time to learn: 30 minutes
By the end of this notebook, you should be able to read raster metadata critically and explain why metadata quality is a prerequisite for meaningful morphometric analysis.
Imports¶
# Import Path to handle the raster path
from pathlib import Path
# Import NumPy for numerical array operations
import numpy as np
# Import Matplotlib for plotting
import matplotlib.pyplot as plt
# Help to create a nice colorbar
from mpl_toolkits.axes_grid1 import make_axes_locatable
import matplotlib.ticker as mticker
# Formatter without scientific notation
formatter = mticker.FuncFormatter(lambda x, pos: f"{int(x):,}")
# Import RasterIO to read the raster data
import rasterio as rio
# Import plotting_extent to display the raster in map coordinates
from rasterio.plot import plotting_extent
import geopandas as gpd
from shapely import wkt
from shapely.geometry import Point,box
import foliumWhat is a raster?¶
A raster is a regular grid of cells, or pixels, where each cell holds a value. A bathymetry raster holds a water-depth value in every cell. On its own, that grid is just a 2D array of numbers. What makes it a georeferenced raster, and not an ordinary image, is two extra pieces of information:
A coordinate reference system (CRS) that says which place on Earth the grid covers and in what units.
An affine transform that maps each cell’s (row, column) position to a real-world (x, y) coordinate.
A raster can also hold several bands, which are stacked grids of the same size. A satellite image might have red, green, and blue bands; our bathymetry files have a single band. rasterio gives you access to all three parts: the array, the CRS, and the transform.
Reading the data with rasterio¶
Rasterio is a dedicated Python library for raster data manipulation. From Rasterio’s documentation (available at https://
“Rasterio’s goal is to be this kind of raster data library – expressing [Geospatial Data Abstraction Library] GDAL’s data model using fewer nonidiomatic extension classes and more idiomatic Python types and protocols, while performing as fast as GDAL’s Python bindings.
High performance, lower cognitive load, cleaner and more transparent code. This is what Rasterio is about.”
As an example, we are using the gifford_bathy raster.
rasterio.open returns a dataset object. Opening a file does not read the pixel values; it reads only the header, so it is cheap even for very large rasters. We will use the Gifford Canyon bathymetry that ships with this cookbook.
data_path = Path("../data/gifford_bathy.tif")
# rasterio as rio
dataset = rio.open(data_path)
dataset<open DatasetReader name='../data/gifford_bathy.tif' mode='r'>Inspecting the metadata¶
The dataset object exposes the georeferencing as plain attributes. These are the values you will reach for most often.
# print metadata:
print("CRS:", dataset.crs)
print("Bounds:", dataset.bounds)
print("Width:", dataset.width)
print("Height:", dataset.height)
print("Resolution:", dataset.res)
print("Transform:", dataset.transform)
print("Number of bands:", dataset.count)
print("NoData value:", dataset.nodata)
print("Data type:", dataset.dtypes)CRS: EPSG:32757
Bounds: BoundingBox(left=515475.0, bottom=7026925.0, right=561175.0, top=7068675.0)
Width: 914
Height: 835
Resolution: (50.0, 50.0)
Transform: | 50.00, 0.00, 515475.00|
| 0.00,-50.00, 7068675.00|
| 0.00, 0.00, 1.00|
Number of bands: 1
NoData value: 3.3999999521443642e+38
Data type: ('float32',)
We can see a lot of information from the GeoTIFF! Briefly, we have:
CRS (
EPSG:32757): The coordinate reference system used to locate the raster in real-world space. This projected CRS uses meters.Bounds: The rectangular spatial extent of the raster in CRS coordinates.
Width and height: The number of columns and rows in the raster grid.
Resolution: The size of each raster cell on the ground. Here, each cell represents
50 m × 50 m.Transform: The affine transform that maps pixel rows and columns to real-world coordinates.
Number of bands: The number of layers in the raster. A single-band raster stores one variable, here bathymetry.
NoData value: The placeholder value used for missing or invalid cells.
Data type: The numeric format of the raster values, here
float32.Shape: The array dimensions as
(rows, columns).Depth range: The range of valid bathymetry values. Negative values indicate depth below sea level.
For convenience, the whole georeferencing header is available as a single dictionary, the dataset profile. This is exactly what you pass back to rasterio when writing a new file.
dict(dataset.profile){'driver': 'GTiff',
'dtype': 'float32',
'nodata': 3.3999999521443642e+38,
'width': 914,
'height': 835,
'count': 1,
'crs': CRS.from_wkt('PROJCS["WGS 84 / UTM zone 57S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",159],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32757"]]'),
'transform': Affine(50.0, 0.0, 515475.0,
0.0, -50.0, 7068675.0),
'blockxsize': 914,
'blockysize': 2,
'tiled': False,
'interleave': 'band'}Reading pixel values into an array¶
dataset.read(1) reads the first band into a NumPy array. Band indices are 1-based, following the GeoTIFF convention. The array’s shape is (rows, columns).
band1 = dataset.read(1)
print("type :", type(band1).__name__)
print("shape:", band1.shape)
print("dtype:", band1.dtype)type : ndarray
shape: (835, 914)
dtype: float32
Rasters use a sentinel nodata value to mark cells with no measurement, such as gaps between survey lines. If you read naively, those sentinels pollute any statistic you compute. Passing masked=True returns a masked array in which nodata cells are hidden.
bathy_data = dataset.read(1, masked=True)
bathy_datamasked_array(
data=[[--, --, --, ..., --, --, --],
[--, --, --, ..., --, --, --],
[--, --, --, ..., --, --, --],
...,
[--, --, --, ..., --, --, --],
[--, --, --, ..., --, --, --],
[--, --, --, ..., --, --, --]],
mask=[[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True],
...,
[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True]],
fill_value=3.4e+38,
dtype=float32)For numerical work it is often easiest to convert the masked cells to NaN and use the np.nan* family of functions, which skip NaN automatically.
bathy_nan = bathy_data.filled(np.nan)
print("shallowest:", np.nanmax(bathy_nan), "m")
print("deepest :", np.nanmin(bathy_nan), "m")
print("mean depth:", np.nanmean(bathy_nan), "m")shallowest: -255.59682 m
deepest : -3221.865 m
mean depth: -1839.3173 m
Pixel and map coordinates¶
The affine transform is what separates a georeferenced raster from a plain image. It converts a (row, column) pixel index into an (x, y) map coordinate. rasterio wraps this in two convenience methods so you rarely touch the matrix directly: xy goes from pixel to map, and index goes from map to pixel.
# Pixel (row, col) -> map (x, y)
row, col = 200, 300
x, y = dataset.xy(row, col)
print(f"pixel (row={row}, col={col}) -> map (x={x:.1f}, y={y:.1f})")
# Map (x, y) -> pixel (row, col)
r, c = dataset.index(x, y)
print(f"map (x={x:.1f}, y={y:.1f}) -> pixel (row={r}, col={c})")pixel (row=200, col=300) -> map (x=530500.0, y=7058650.0)
map (x=530500.0, y=7058650.0) -> pixel (row=200, col=300)
We are done with the low-level handle, so we close it.
dataset.close()Reading the data using with rio.open¶
For this next part, we reproduce the same as before, showing that we get the same results in a safer way using with rio.open.
We also directly assign a variable to the metdata output.
bathy_path = '../data/gifford_bathy.tif'
# Raster reading
# --------------
with rio.open(bathy_path) as dataset:
bathy = dataset.read(1, masked=True).astype(float).filled(np.nan)
transform = dataset.transform
crs = dataset.crs
bounds = dataset.bounds # <-- correct way to get raster bounds
# print metadata:
print("CRS:", dataset.crs)
print("Bounds:", dataset.bounds)
print("Width:", dataset.width)
print("Height:", dataset.height)
print("Resolution:", dataset.res)
print("Transform:", dataset.transform)
print("Number of bands:", dataset.count)
print("NoData value:", dataset.nodata)
print("Data type:", dataset.dtypes)
# Resolution
dx = abs(transform.a)
dy = abs(transform.e)
print("shape:", bathy.shape)
print("crs:", crs)
print("cell size (resolution):", (dx, dy))
print("depth range:", (np.nanmin(bathy), np.nanmax(bathy)))CRS: EPSG:32757
Bounds: BoundingBox(left=515475.0, bottom=7026925.0, right=561175.0, top=7068675.0)
Width: 914
Height: 835
Resolution: (50.0, 50.0)
Transform: | 50.00, 0.00, 515475.00|
| 0.00,-50.00, 7068675.00|
| 0.00, 0.00, 1.00|
Number of bands: 1
NoData value: 3.3999999521443642e+38
Data type: ('float32',)
shape: (835, 914)
crs: EPSG:32757
cell size (resolution): (50.0, 50.0)
depth range: (np.float64(-3221.864990234375), np.float64(-255.59681701660156))
Looking at the CRS metadata¶
crsCRS.from_wkt('PROJCS["WGS 84 / UTM zone 57S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",159],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32757"]]')print(crs)EPSG:32757
crs and print(crs) gave different results?! Let’s see why below.
Understanding __str__ and __repr__ when printing in Jupyter¶
Let’s create a class that we call MysteriousObject that has those special methods"
import random
class MysteriousObject:
# Printable representation (more for developers)
def __repr__(self):
return f"MysteriousObject({random.randint(1, 100)})"
# Printable String (more for users)
def __str__(self):
return f"I'm the mysterious object number {random.randint(1, 100)}"We can create a object like so:
# Create an instance
obj = MysteriousObject()We have an object named objthat we can display using automatic interactive variable printing from Jupyter and below its “printed version” to see the different outputs:
# Not using print (the __repr__ method is used)
objMysteriousObject(76)with the printfunction
# Using print (the __str__ method is used)
print(obj)I'm the mysterious object number 85
To print the __repr__ method of the CRS with the print function, we would do:
print(crs.__repr__())CRS.from_wkt('PROJCS["WGS 84 / UTM zone 57S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",159],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32757"]]')
We can also use the Well-Known Text (WKT) conversion to_wkt method
print(crs.to_wkt())PROJCS["WGS 84 / UTM zone 57S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",159],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32757"]]
However, prints and outputs are not easy to read. This is where geopandas pretty display can help.
Using geopandas pretty display¶
GeoPandas is a powerful Python package that extends Pandas functionality to work with geospatial data. It can be useful for handling and displaying Coordinate Reference System (CRS) information in a readable format.
# Assuming o_data.crs.to_wkt() returns a valid WKT string
wkt_string = crs.to_wkt()
# Create a GeoDataFrame with a single geometry from the WKT
# A geometry is needed to define a GeoDataFrame, it is just a dummy value here
gdf = gpd.GeoDataFrame(geometry=[wkt.loads("POINT(0 0)")], crs=wkt_string)
# Display the CRS information in a nicely formatted way
print("Formatted CRS WKT:")
print(gdf.crs.to_wkt(pretty=True))Formatted CRS WKT:
PROJCRS["WGS 84 / UTM zone 57S",
BASEGEOGCRS["WGS 84",
DATUM["World Geodetic System 1984",
ELLIPSOID["WGS 84",6378137,298.257223563,
LENGTHUNIT["metre",1]]],
PRIMEM["Greenwich",0,
ANGLEUNIT["degree",0.0174532925199433]],
ID["EPSG",4326]],
CONVERSION["UTM zone 57S",
METHOD["Transverse Mercator",
ID["EPSG",9807]],
PARAMETER["Latitude of natural origin",0,
ANGLEUNIT["degree",0.0174532925199433],
ID["EPSG",8801]],
PARAMETER["Longitude of natural origin",159,
ANGLEUNIT["degree",0.0174532925199433],
ID["EPSG",8802]],
PARAMETER["Scale factor at natural origin",0.9996,
SCALEUNIT["unity",1],
ID["EPSG",8805]],
PARAMETER["False easting",500000,
LENGTHUNIT["metre",1],
ID["EPSG",8806]],
PARAMETER["False northing",10000000,
LENGTHUNIT["metre",1],
ID["EPSG",8807]]],
CS[Cartesian,2],
AXIS["easting",east,
ORDER[1],
LENGTHUNIT["metre",1]],
AXIS["northing",north,
ORDER[2],
LENGTHUNIT["metre",1]],
ID["EPSG",32757]]
CRS and EPSG in more details¶
Understanding the Coordinate Reference System (CRS) information
Coordinate Reference Systems (CRS) define how spatial data aligns with locations on Earth. Let’s explore the WGS 84 / UTM Zone 57S CRS by analyzing its Well-Known Text (WKT) format:
Breakdown of the WKT String¶
1. Projected Coordinate Reference System (PROJCRS)¶
Meaning: Defines a coordinate system that projects Earth’s curved surface onto a flat map.
Name:
WGS 84 / UTM zone 57Sclearly indicates:Datum: World Geodetic System 1984 (WGS 84).
Part of the Universal Transverse Mercator (UTM) projection system, specifically for Zone 57 South.
2. Base Geographic Coordinate Reference System (BASEGEOGCRS)¶
Definition: Identifies coordinates on the Earth’s curved surface prior to projection.
Name:
WGS 84.Components:
Datum:
World Geodetic System 1984Ellipsoid:
WGS 84Semi-major Axis:
6378137 meters(equatorial radius).Inverse Flattening:
298.257223563(indicates Earth’s ellipsoid shape).Unit:
metre.
Prime Meridian:
Greenwich, longitude =0°.Angle Unit:
degree, conversion factor:0.0174532925199433radians per degree.
EPSG Code:
4326identifies this geographic coordinate system.
3. Conversion Method (CONVERSION)¶
Name:
UTM zone 57S.Projection Method:
Transverse MercatorEPSG Code:
9807.Projects Earth’s surface onto a cylinder aligned with a central meridian.
4. Projection Parameters¶
Parameters for customizing the projection:
Latitude of Natural Origin:
0°(equator)EPSG Code:
8801
Longitude of Natural Origin:
159°(central longitude for Zone 57)EPSG Code:
8802
Scale Factor at Natural Origin:
0.9996(reduces distortion)EPSG Code:
8805
False Easting:
500,000 meters(ensures positive eastward coordinates)EPSG Code:
8806
False Northing:
10,000,000 meters(offset used in the southern hemisphere to ensure positive northing values)EPSG Code:
8807
5. Coordinate System (CS)¶
Cartesian Coordinate System: two-dimensional (2D), using X and Y coordinates.
6. Axis Definition (AXIS)¶
Defines axis orientations clearly:
Easting Axis (X-axis):
Positive direction towards the East.
First coordinate component.
Northing Axis (Y-axis):
Positive direction towards the North.
Second coordinate component.
7. EPSG Authority Code¶
EPSG Code:
32757uniquely identifies the “WGS 84 / UTM Zone 57S” CRS.
Using Folium to display the location of the survey¶
We can use the rate bounds and reproject the GeoDataFrame to EPSG:4326 for visualization
# Get the bounding box of the raster and compute its center
center_x = (bounds.left + bounds.right) / 2
center_y = (bounds.bottom + bounds.top) / 2
# Create a GeoDataFrame using the raster's native CRS
gdf = gpd.GeoDataFrame(
geometry=[Point(center_x, center_y)],
crs=crs
)
# Print the original CRS information
print("Original CRS WKT:")
print(gdf.crs.to_wkt(pretty=True))Original CRS WKT:
PROJCRS["WGS 84 / UTM zone 57S",
BASEGEOGCRS["WGS 84",
DATUM["World Geodetic System 1984",
ELLIPSOID["WGS 84",6378137,298.257223563,
LENGTHUNIT["metre",1]]],
PRIMEM["Greenwich",0,
ANGLEUNIT["degree",0.0174532925199433]],
ID["EPSG",4326]],
CONVERSION["UTM zone 57S",
METHOD["Transverse Mercator",
ID["EPSG",9807]],
PARAMETER["Latitude of natural origin",0,
ANGLEUNIT["degree",0.0174532925199433],
ID["EPSG",8801]],
PARAMETER["Longitude of natural origin",159,
ANGLEUNIT["degree",0.0174532925199433],
ID["EPSG",8802]],
PARAMETER["Scale factor at natural origin",0.9996,
SCALEUNIT["unity",1],
ID["EPSG",8805]],
PARAMETER["False easting",500000,
LENGTHUNIT["metre",1],
ID["EPSG",8806]],
PARAMETER["False northing",10000000,
LENGTHUNIT["metre",1],
ID["EPSG",8807]]],
CS[Cartesian,2],
AXIS["easting",east,
ORDER[1],
LENGTHUNIT["metre",1]],
AXIS["northing",north,
ORDER[2],
LENGTHUNIT["metre",1]],
ID["EPSG",32757]]
# Reproject the GeoDataFrame to EPSG:4326 for proper display on Folium
gdf = gdf.to_crs(epsg=4326)
print("Reprojected CRS WKT (EPSG:4326):")
print(gdf.crs.to_wkt(pretty=True))Reprojected CRS WKT (EPSG:4326):
GEOGCRS["WGS 84",
ENSEMBLE["World Geodetic System 1984 ensemble",
MEMBER["World Geodetic System 1984 (Transit)"],
MEMBER["World Geodetic System 1984 (G730)"],
MEMBER["World Geodetic System 1984 (G873)"],
MEMBER["World Geodetic System 1984 (G1150)"],
MEMBER["World Geodetic System 1984 (G1674)"],
MEMBER["World Geodetic System 1984 (G1762)"],
MEMBER["World Geodetic System 1984 (G2139)"],
MEMBER["World Geodetic System 1984 (G2296)"],
ELLIPSOID["WGS 84",6378137,298.257223563,
LENGTHUNIT["metre",1]],
ENSEMBLEACCURACY[2.0]],
PRIMEM["Greenwich",0,
ANGLEUNIT["degree",0.0174532925199433]],
CS[ellipsoidal,2],
AXIS["geodetic latitude (Lat)",north,
ORDER[1],
ANGLEUNIT["degree",0.0174532925199433]],
AXIS["geodetic longitude (Lon)",east,
ORDER[2],
ANGLEUNIT["degree",0.0174532925199433]],
USAGE[
SCOPE["Horizontal component of 3D system."],
AREA["World."],
BBOX[-90,-180,90,180]],
ID["EPSG",4326]]
# Extract the reprojected coordinates (latitude, longitude)
center_point = gdf.geometry.iloc[0]
latitude, longitude = center_point.y, center_point.x
# Create a Folium map centered on the reprojected point
m = folium.Map(location=[latitude, longitude], zoom_start=10)
folium.Marker([latitude, longitude], popup="Raster Center").add_to(m)
# To save the map, uncomment the following line:
# m.save("raster_center_map.html")
# Display the map
mIf needed, we can add more info over the marker and select an different initial zoom to picture the region directly.
# Create polygon from raster bounds in native CRS
bounds_polygon = box(bounds.left, bounds.bottom, bounds.right, bounds.top)
bounds_gdf = gpd.GeoDataFrame(
geometry=[bounds_polygon],
crs=crs
)
# Reproject to WGS84 for Folium
bounds_gdf_4326 = bounds_gdf.to_crs(epsg=4326)
# Reproject center point too, if not already done
gdf_4326 = gdf.to_crs(epsg=4326)
center_point = gdf_4326.geometry.iloc[0]
latitude, longitude = center_point.y, center_point.x
m = folium.Map(location=[latitude, longitude], zoom_start=5)
folium.GeoJson(
bounds_gdf_4326,
name="Raster Bounds",
tooltip="Raster footprint"
).add_to(m)
popup_text = f"""
<b>Raster Center</b><br>
CRS: {crs}<br>
Bounds: {bounds}<br>
Resolution: {transform.a:.2f}, {abs(transform.e):.2f}<br>
Shape: {bathy.shape}
"""
folium.Marker(
[latitude, longitude],
popup=folium.Popup(popup_text, max_width=400)
).add_to(m)
folium.LayerControl().add_to(m)
mDisplaying the 3 sites on Folium from metadata¶
In this section, we provide functions and visualization from metadata.
DATASETS = {
"Gifford Canyon": Path("../data/gifford_bathy.tif"),
"Oceanic Shoals": Path("../data/os_bathy.tif"),
"Point Cloates": Path("../data/pc_bathy.tif"),
}def get_raster_metadata(path):
"""Read raster metadata and return footprint in EPSG:4326 plus metadata."""
with rio.open(path) as src:
data = src.read(1, masked=True)
bounds_polygon = box(
src.bounds.left,
src.bounds.bottom,
src.bounds.right,
src.bounds.top,
)
footprint_native = gpd.GeoDataFrame(
geometry=[bounds_polygon],
crs=src.crs,
)
footprint_4326 = footprint_native.to_crs(epsg=4326)
metadata = {
"file": path.name,
"crs": str(src.crs),
"bounds": src.bounds,
"resolution_x": src.transform.a,
"resolution_y": abs(src.transform.e),
"shape": src.shape,
"width": src.width,
"height": src.height,
"band_count": src.count,
"nodata": src.nodata,
"dtype": src.dtypes[0],
"depth_min": float(data.min()),
"depth_max": float(data.max()),
}
return footprint_4326, metadatasite_footprints = {}
for site_name, path in DATASETS.items():
footprint_4326, metadata = get_raster_metadata(path)
site_footprints[site_name] = {
"footprint": footprint_4326,
"metadata": metadata,
}def raster_bounds_to_wgs84(path):
"""Create a WGS84 footprint polygon from raster metadata."""
with rio.open(path) as src:
bounds = src.bounds
crs = src.crs
transform = src.transform
bounds_polygon = box(
bounds.left,
bounds.bottom,
bounds.right,
bounds.top,
)
bounds_gdf = gpd.GeoDataFrame(
geometry=[bounds_polygon],
crs=crs,
)
bounds_gdf_4326 = bounds_gdf.to_crs(epsg=4326)
metadata = {
"crs": crs,
"bounds": bounds,
"resolution": (transform.a, abs(transform.e)),
"shape": src.shape,
"width": src.width,
"height": src.height,
"nodata": src.nodata,
"dtype": src.dtypes[0],
"band_count": src.count,
}
return bounds_gdf_4326, metadata# Use all site centroids to center the map
centroids = [
item["footprint"].geometry.iloc[0].centroid
for item in site_footprints.values()
]
center_latitude = sum(point.y for point in centroids) / len(centroids)
center_longitude = sum(point.x for point in centroids) / len(centroids)
m = folium.Map(
location=[center_latitude, center_longitude],
zoom_start=3,
tiles="CartoDB positron",
)
for site_name, item in site_footprints.items():
footprint = item["footprint"]
metadata = item["metadata"]
center_point = footprint.geometry.iloc[0].centroid
latitude, longitude = center_point.y, center_point.x
popup_text = f"""
<b>{site_name}</b><br>
CRS: {metadata["crs"]}<br>
Bounds: {metadata["bounds"]}<br>
Resolution: {metadata["resolution_x"]:.2f} m, {metadata["resolution_y"]:.2f} m<br>
Shape: {metadata["shape"]}<br>
Width × height: {metadata["width"]} × {metadata["height"]}<br>
Bands: {metadata["band_count"]}<br>
Data type: {metadata["dtype"]}<br>
NoData: {metadata["nodata"]}
"""
folium.GeoJson(
footprint,
name=site_name,
tooltip=f"{site_name} raster footprint",
style_function=lambda feature: {
"fillOpacity": 0.2,
"weight": 2,
},
).add_to(m)
folium.Marker(
location=[latitude, longitude],
popup=folium.Popup(popup_text, max_width=450),
tooltip=site_name,
).add_to(m)
folium.LayerControl().add_to(m)
mSave the map in HTML¶
# Save to HTML file
m.save("./images/map.html")