Skip to content

Use your dataset

mapcv generate leaves you a folder of plain files. This page shows how to read them. The format is specified in Dataset format; here is the short version:

  • Directorydataset/
    • DirectoryImages/ patch_0000000.png (RGB uint8), .npy (bands-first, the source’s data type) or .tif (GeoTIFF)
      • …
    • DirectoryMasks/ patch_0000000.png (uint8: 0 = background, 1..255 = class), .npy or .tif
      • …
    • manifest.json task, bands, CRS, class names, per-patch files and stats
    • patches.geojson where every patch is, with its split
    • Directorysplits/
      • train.txt
      • val.txt
      • test.txt
      • 10/labeled.txt, 10/unlabeled.txt, …

Each split file lists image file names, one per line. Each manifest entry’s files maps image and mask to paths relative to the dataset folder; use them instead of building paths yourself (the mask’s extension follows writer.mask_format, which can differ from the image’s):

import json
from pathlib import Path
root = Path("dataset")
manifest = json.loads((root / "manifest.json").read_text())
files_of = {Path(p["files"]["image"]).name: p["files"] for p in manifest["patches"]}
train = (root / "splits" / "train.txt").read_text().split()
pairs = [(root / files_of[name]["image"], root / files_of[name]["mask"]) for name in train]
import numpy as np
from PIL import Image
image_path, mask_path = pairs[0]
image = np.asarray(Image.open(image_path).convert("RGB")) # (H, W, 3) uint8
mask = np.asarray(Image.open(mask_path)) # (H, W) uint8

The loaders above read the default formats. With writer.image_format: tif or writer.mask_format: npy | tif the paths in the manifest end in .tif or .npy. The mask holds the same values in every format, ignore_index included, so only the reading changes:

import rasterio
with rasterio.open(root / files["image"]) as src:
image = src.read() # (bands, H, W), the stored dtype (bands-first, like NPY)
with rasterio.open(root / files["mask"]) as src:
mask = src.read(1) # (H, W); 255 (the ignore index) is also the no-data value
valid = src.read_masks(1) > 0 # False where the pixel has no imagery

rasterio is pip install rasterio; a GeoTIFF also reads with GDAL, tifffile (pixels only) and TerraTorch, which reads GeoTIFF but not NPY. The mask’s data type is uint8 for segmentation and change detection, uint16 for instance-ID masks and float32 for regression targets, so avoid hard-coding uint8 when you load them.

The manifest’s target.class_map maps label values to mask IDs; 0 is always background. When the config had no label_field, class_map is empty and every polygon is 1. An image-only dataset has "target": null.

target = manifest["target"] or {}
names = {cid: name for name, cid in target.get("class_map", {}).items()}
names.setdefault(0, "background")
num_classes = max(names) + 1 # IDs need not be contiguous if you pinned them

mapcv.data.MapcvDataset reads one split of any mapcv dataset: every image and mask format, several sources and stacks, and each task’s targets. It is a map-style dataset (len and indexing), so DataLoader takes it directly, and it needs no torch itself: items are torch tensors when torch is installed, numpy arrays otherwise.

from torch.utils.data import DataLoader
from mapcv.data import MapcvDataset
train = MapcvDataset("dataset", split="train", normalize=True)
val = MapcvDataset("dataset", split="val", normalize=True)
item = train[0] # {"image": (C, H, W), "mask": (H, W), "name": ..., "row": ..., ...}
loader = DataLoader(train, batch_size=8, shuffle=True, num_workers=2)
batch = next(iter(loader)) # batch["image"]: (8, C, H, W), batch["mask"]: (8, H, W)
print(train.ignore_index, train.classes) # 255, {1: "building"}: pass the first to your loss

Detection and instance items hold a different number of boxes each, so give DataLoader a collate_fn that keeps them as lists (for example collate_fn=lambda items: items).

  • split is train, val, test, a labeled subset such as 10/labeled, or all.
  • normalize=True standardises each band with the train split’s mean and standard deviation from stats.json (computed if the file is missing; see mapcv stats); pixels without data become 0.
  • Items also hold images (every source by name) for several sources, labels (class IDs) for classification, and boxes ([x, y, width, height] in pixels), categories and the COCO annotations for detection and instance segmentation.
  • transform= takes a callable applied to each item dict, for augmentation.

The repository keeps a hand-written version of such a class, to adapt freely, in examples/scripts/torch_dataset.py.

Run against the Buildings tutorial dataset, train[0]["image"] is (3, 256, 256) and its mask (256, 256) with values {0, 1}; against the Sentinel-2 tutorial, (4, 128, 128) and values {0, 1, 2, 3, 4}. For a full training loop, see Train a model on your dataset.

Masks mark pixels that have no imagery under them with 255, mapcv’s default labels.ignore_index: padding from edge_strategy: pad, failed tiles, provider NoData (all-black XYZ pixels) and Sentinel-2 pixels where any selected band is NaN. Background is 0 only where there is real imagery and no polygon. Pass the same value to your loss so those pixels don’t count:

loss_fn = torch.nn.CrossEntropyLoss(ignore_index=255)
  • The manifest records the value as target.ignore_index; the class above reads it from there.
  • A class can’t use 255 while it is the ignore value. If you need 255 classes, set labels.ignore_index to an unused ID, or to null to write background (0) there as mapcv 0.2 did.
  • Sentinel-2 images still contain NaN where data is missing; replace it before feeding a network (the class uses 0).
  • To keep mostly-empty patches out entirely, set sampler.max_empty_ratio (for example 0.2) when generating. Each manifest entry’s summary.empty_ratio tells you how empty a patch is.
  • XYZ RGB: divide by 255, then optionally apply ImageNet mean/std if you fine-tune an ImageNet-pretrained encoder.
  • Sentinel-2 and GeoTIFF: standardise each band with its mean and standard deviation over the train split only. mapcv stats dataset computes them (leaving out NaN, NoData and ignored pixels, streaming, so any dataset size fits in memory) and writes stats.json:
import json
import numpy as np
stats = json.loads(Path("dataset/stats.json").read_text())["sources"]["image"]
mean = np.array(stats["mean"], dtype=np.float32)[:, None, None] # (bands, 1, 1)
std = np.array(stats["std"], dtype=np.float32)[:, None, None]
image = (np.nan_to_num(np.load(image_path)) - mean) / std

The same values go into TerraTorch’s means and stds. Run mapcv stats again after re-splitting.

Every manifest entry’s summary carries class_pixels, the mask’s pixel count per class ID, so class frequencies need no image reads:

from collections import Counter
train_names = set(train)
counts = Counter()
for p in manifest["patches"]:
if Path(p["files"]["image"]).name in train_names:
counts.update({int(k): v for k, v in p["summary"]["class_pixels"].items() if k != "255"})
total = sum(counts.values())
weights = {cid: total / (len(counts) * n) for cid, n in sorted(counts.items())}

mapcv info dataset prints the same class shares over the whole dataset. mapcv stats dataset writes median-frequency weights (Eigen & Fergus), which are less extreme than inverse frequency for rare classes, to stats.json under classes.median_frequency_weights.

splits/<percent>/labeled.txt holds a random <percent> % of train.txt, and unlabeled.txt the rest. With the class above, MapcvDataset("dataset", split="10/labeled") loads the labeled 10 %; for the unlabeled part, use the images and ignore the masks. Change the fractions with split.labeled_ratios in the config or mapcv split --labeled-ratios.

Each patch’s row and col, together with the source’s transform and crs, give its footprint in map coordinates; mapcv.Manifest.patch_transform(entry) and patch_bounds(entry) compute them. A snippet is in Dataset format → georeferencing.

Three ways to get the patches onto a map, from least to most work:

  • QGIS, one layer for everything. Drag dataset/patches.geojson in. Each polygon is a patch with its filename, split, row, col and class_pixels, in WGS-84. Style by split to see what is train, validation and test, or filter on class_pixels to find the patches that contain a class.
  • QGIS, the patches themselves. With writer.image_format: tif (and mask_format: tif) drag Images/*.tif and Masks/*.tif in: they are georeferenced, the mask aligns with its image and its ignore pixels are transparent. For PNG or JPG patches set writer.world_files: true, and QGIS places them from the .pgw/.jgw files (it asks for the CRS: the manifest’s sources[0].crs).
  • Write predictions as GeoTIFFs. Copy the patch’s georeferencing onto the prediction so it opens exactly where the patch is:
import rasterio
with rasterio.open(root / files["image"]) as src: # a tif patch
profile = src.profile
profile.update(count=1, dtype="uint8", nodata=255, compress="deflate")
with rasterio.open("prediction_0000000.tif", "w", **profile) as dst:
dst.write(predicted_mask.astype("uint8")[None])

For PNG or NPY patches take rasterio.transform.Affine(*manifest.patch_transform(entry)) and the manifest’s crs instead.

To map a trained model over a whole region, not just the dataset’s patches, mapcv.infer.predict_raster reads the config’s imagery patch by patch, calls your model on batches and blends the overlapping predictions into one georeferenced GeoTIFF:

import numpy as np
import torch
from mapcv.infer import predict_raster
model.eval()
def predict(batch: np.ndarray) -> np.ndarray: # (N, C, P, P) as read
with torch.no_grad():
x = torch.from_numpy(batch.astype(np.float32) / 255.0)
return model(x).softmax(dim=1).numpy() # (N, K, P, P)
predict_raster("mapcv.yaml", predict, "prediction.tif", overlap=0.5) # uint8 classes, 255 = no imagery

It uses the config’s first imagery source, region and sampler.patch_size. Neighbouring patches overlap by overlap and are blended with a cosine window, so the borders of a patch, where models are least sure, count least and there are no seams. output="scores" writes the blended scores as float32 bands instead of the class; a model that returns (N, P, P) values (regression) gives one float32 band. Only one strip of rows is held at a time besides the output itself. See the Python API.

Before uploading a dataset (to the Hugging Face Hub, Zenodo or a team bucket), give it a card and pin its contents:

Terminal window
$ mapcv stats dataset # normalisation values, shown in the card
$ mapcv card dataset # README.md with Hugging Face metadata
$ mapcv verify dataset --deep --write-checksums

Edit the card’s license (it starts as other) to what the imagery provider’s terms allow, and add anything the provider asks you to credit. Whoever receives the folder runs mapcv verify dataset (or sha256sum -c SHA256SUMS) to check that every file arrived unchanged.

mapcv export writes a dataset in the form other tools read:

Terminal window
$ mapcv export dataset --format hf-parquet --out dataset-hf # pip install "mapcv[export]"
$ mapcv export dataset --format terratorch # dataset/terratorch.yaml

For large datasets, --format webdataset packs the patches into tar shards per split (for streaming with the webdataset library) and --format zarr into one Zarr store that MapcvDataset reads directly; both turn hundreds of thousands of small files into a handful. Before reading a Zarr store, MapcvDataset checks that every array holds numbers (bool, int, uint, float) encoded with plain compressors (Blosc, Zstd, Zlib, GZip, LZ4, BZ2, LZMA, Delta, Shuffle), as mapcv export writes them, and refuses any other store: Zarr decodes an array with the codecs the store itself names, and some of them can run code.

hf-parquet writes one Parquet file per split and a dataset card, ready for datasets.load_dataset("dataset-hf") or huggingface-cli upload. terratorch writes the data: section of a TerraTorch training config (the generic segmentation or regression datamodule) pointing at the dataset’s folders and split lists, with the train split’s band means and standard deviations; generate the dataset with writer.image_format: tif and writer.mask_format: tif for it. See mapcv export.