Reading and Writing GDAL Tags and Band Descriptions
Give every band a name and record provenance as tags, so the file explains itself:
import rasterio
with rasterio.open("stack.tif", "r+") as dst:
for i, name in enumerate(["B02", "B03", "B04", "B08"], start=1):
dst.set_band_description(i, name)
dst.update_tags(source="S2B_36NYF_20260614", processing="l2a-stack v1.3")
dst.update_tags(4, wavelength_nm="842", role="near infrared")
A band order that exists only in a script’s comments is an accident waiting to happen; one written into the file is a contract. This page belongs to extracting and parsing raster metadata in Core Raster Fundamentals & STAC Mapping.
Where Each Kind of Fact Lives
The practical rule is short. Band descriptions are for identity, and code should select bands by them. Band tags are for facts that apply to one band — its wavelength, its decoding rule, whether it is categorical. Dataset tags are for provenance: what the file was made from, by what, and when.
Environment & Setup
| Package | Version pin | Used for |
|---|---|---|
rasterio |
>=1.3.0 |
descriptions, tags, update_tags, set_band_description |
rio-cogeo |
>=5.0 |
Checking that conversion preserves tags |
pip install "rasterio>=1.3.0" "rio-cogeo>=5.0"
Complete Working Example
import getpass
import json
import platform
from datetime import datetime, timezone
import rasterio
def describe_stack(path: str, bands: list[dict], *, sources: list[str],
software: str) -> None:
"""Write band identities and provenance into an existing raster, in place."""
with rasterio.open(path, "r+") as dst:
if len(bands) != dst.count:
raise ValueError(f"{len(bands)} band specs for {dst.count} bands")
for i, spec in enumerate(bands, start=1):
dst.set_band_description(i, spec["name"])
dst.update_tags(i, **{k: str(v) for k, v in spec.items() if k != "name"})
dst.update_tags(
sources=json.dumps(sources),
software=software,
created=datetime.now(timezone.utc).isoformat(timespec="seconds"),
host=platform.node(),
author=getpass.getuser(),
)
def band_index(src: rasterio.DatasetReader, name: str) -> int:
"""1-based band index by description — the only safe way to select a band."""
names = list(src.descriptions)
if name not in names:
raise KeyError(f"band {name!r} not in {names}")
return names.index(name) + 1
if __name__ == "__main__":
describe_stack(
"stack.tif",
[{"name": "B04", "wavelength_nm": 665, "role": "red"},
{"name": "B08", "wavelength_nm": 842, "role": "near infrared"},
{"name": "SCL", "categorical": True, "classes": "0..11"}],
sources=["S2B_36NYF_20260614_0_L2A"],
software="l2a-stack 1.3.0",
)
with rasterio.open("stack.tif") as src:
nir = src.read(band_index(src, "B08"))
print(src.descriptions, src.tags())
band_index is the function the rest of a codebase should use. A stack built by another team, or by an earlier version of your own pipeline, may order bands differently, and selecting by position silently reads the wrong one; selecting by description fails loudly instead, which is the right behaviour. The consequences of getting this wrong for a model are set out in feature engineering for pixel-based models.
What Survives Which Conversion
The rebuild-from-arrays row is where most metadata dies. A processing step that reads bands into NumPy, computes, and writes a fresh file from a hand-built profile starts with an empty header. The fix is a small helper that copies descriptions and tags from the source dataset to the destination as part of every write, so preservation is the default rather than something each step has to remember.
def copy_metadata(src: rasterio.DatasetReader, dst: rasterio.DatasetWriter,
band_map: dict[int, int] | None = None) -> None:
"""Copy dataset tags, and band descriptions/tags for mapped bands."""
dst.update_tags(**src.tags())
band_map = band_map or {i: i for i in range(1, min(src.count, dst.count) + 1)}
for s, d in band_map.items():
if src.descriptions[s - 1]:
dst.set_band_description(d, src.descriptions[s - 1])
dst.update_tags(d, **src.tags(s))
Provenance Worth Recording
A short, consistent set of dataset tags answers almost every later question about where a file came from. The source identifiers — catalog item ids or input file names — say what went in. The software name and version say what processed it. The creation timestamp says when. For derived products, a hash of the configuration used says exactly how.
Keep these as identifiers rather than documents. A tag holding a catalog item id is small and precise; a tag holding the full item JSON bloats every file header and duplicates what the catalog already stores. The richer record belongs in a catalog, as described in attaching model metadata to STAC items, with the file’s tags pointing to it.
Verification
import rasterio
with rasterio.open("stack_cog.tif") as src:
assert all(src.descriptions), f"missing descriptions: {src.descriptions}"
assert len(set(src.descriptions)) == src.count, "duplicate band names"
tags = src.tags()
for key in ("sources", "software", "created"):
assert key in tags, f"provenance tag {key!r} lost"
print(src.descriptions, {k: tags[k] for k in ("software", "created")})
Run this check after every conversion step in a pipeline, not only at the end: finding that tags disappeared at step three of seven is much easier when step three is the one that fails. The broader audit of a whole collection is covered in automating metadata extraction for batch raster jobs.
Common Errors
Descriptions are None after writing
They were set on a dataset opened read-only, or before the band was written in a way that reset them. Open with r+ or set them after writing in w mode.
Tags vanish after a processing step
The step rebuilt the file from arrays. Copy metadata from the source as part of the write.
Band tags describe the wrong band
A step reordered or dropped bands without remapping tags. Pass an explicit band map when copying.
File opens slowly after adding tags
A very large value — a whole JSON document — was stored. Keep tags to identifiers and move bulk to a catalog.
Frequently Asked Questions
Q: What is the difference between a band description and a band tag? A description is a single short string per band, shown by most GIS tools as the band’s name. Tags are arbitrary key-value pairs attached to a band or to the whole dataset. Use the description for the band’s identity and tags for everything else.
Q: Do tags survive conversion to a COG? With rio-cogeo and gdal_translate, dataset and band tags in the default namespace are usually carried through. Some paths drop them, notably conversions that rebuild the file from arrays. Always read them back after a conversion rather than assuming.
Q: How large can tag values be? Large enough for provenance strings and small JSON documents, but tags are stored in the file header, so very large values slow every open. Keep them to identifiers and short facts, and put anything bulky in a sidecar or a catalog record.
Q: What are tag namespaces for?
They separate metadata by purpose — GDAL uses them for things like image structure and resampling hints. Your own tags belong in the default namespace unless a tool expects a specific one; overview resampling hints, for example, go in the rio_overview namespace.
Related
- Extracting and Parsing Raster Metadata — the parent topic.
- Extracting Nodata and Dtype from a GeoTIFF — the structural metadata alongside these descriptive ones.
- Stacking Spectral Indices as Model Features — where band descriptions become a model contract.
- Writing Prediction Rasters as COGs — provenance tags on model outputs.