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, …
Read a split
Section titled “Read a split”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 jsonfrom 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]Load one patch with NumPy
Section titled “Load one patch with NumPy”import numpy as npfrom PIL import Image
image_path, mask_path = pairs[0]image = np.asarray(Image.open(image_path).convert("RGB")) # (H, W, 3) uint8mask = np.asarray(Image.open(mask_path)) # (H, W) uint8import numpy as npfrom PIL import Image
image_path, mask_path = pairs[0]bands = manifest["sources"][0]["bands"] # e.g. ['b04', 'b03', 'b02', 'b08']image = np.load(image_path) # (bands, H, W) float32, in that ordermask = np.asarray(Image.open(mask_path)) # (H, W) uint8Values are decoded surface reflectance (typically 0–1). Pixels without data are NaN.
Read tif and npy patches and masks
Section titled “Read tif and npy patches and masks”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 imagerymask = np.load(root / files["mask"]) # (H, W), the mask's dtypemask = np.asarray(Image.open(root / files["mask"])) # (H, W) uint16; do not convert to "L"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.
Class names
Section titled “Class names”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 themA PyTorch Dataset
Section titled “A PyTorch Dataset”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 lossDetection 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).
splitistrain,val,test, a labeled subset such as10/labeled, orall.normalize=Truestandardises each band with the train split’s mean and standard deviation fromstats.json(computed if the file is missing; seemapcv stats); pixels without data become 0.- Items also hold
images(every source by name) for several sources,labels(class IDs) for classification, andboxes([x, y, width, height]in pixels),categoriesand the COCOannotationsfor 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.
NoData and ignore pixels
Section titled “NoData and ignore pixels”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_indexto an unused ID, or tonullto write background (0) there as mapcv 0.2 did. - Sentinel-2 images still contain
NaNwhere data is missing; replace it before feeding a network (the class uses0). - To keep mostly-empty patches out entirely, set
sampler.max_empty_ratio(for example0.2) when generating. Each manifest entry’ssummary.empty_ratiotells you how empty a patch is.
Normalise inputs
Section titled “Normalise inputs”- 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 datasetcomputes them (leaving outNaN, NoData and ignored pixels, streaming, so any dataset size fits in memory) and writesstats.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) / stdThe same values go into TerraTorch’s means and stds. Run mapcv stats again after re-splitting.
Class weights from the manifest
Section titled “Class weights from the manifest”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.
Semi-supervised subsets
Section titled “Semi-supervised subsets”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.
Map predictions back to the ground
Section titled “Map predictions back to the ground”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.geojsonin. Each polygon is a patch with itsfilename,split,row,colandclass_pixels, in WGS-84. Style bysplitto see what is train, validation and test, or filter onclass_pixelsto find the patches that contain a class. - QGIS, the patches themselves. With
writer.image_format: tif(andmask_format: tif) dragImages/*.tifandMasks/*.tifin: they are georeferenced, the mask aligns with its image and its ignore pixels are transparent. For PNG or JPG patches setwriter.world_files: true, and QGIS places them from the.pgw/.jgwfiles (it asks for the CRS: the manifest’ssources[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.profileprofile.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.
Predict a whole region
Section titled “Predict a whole region”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 npimport 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 imageryIt 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.
Share or archive a dataset
Section titled “Share or archive a dataset”Before uploading a dataset (to the Hugging Face Hub, Zenodo or a team bucket), give it a card and pin its contents:
$ mapcv stats dataset # normalisation values, shown in the card$ mapcv card dataset # README.md with Hugging Face metadata$ mapcv verify dataset --deep --write-checksumsEdit 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.
Hand it to Hugging Face or TerraTorch
Section titled “Hand it to Hugging Face or TerraTorch”mapcv export writes a dataset in the form other tools read:
$ mapcv export dataset --format hf-parquet --out dataset-hf # pip install "mapcv[export]"$ mapcv export dataset --format terratorch # dataset/terratorch.yamlFor 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.