Configuration
A mapcv config is one YAML file with six top-level blocks, an optional task and, for task: detection, instance, classification or change, an optional block of the same name with that task’s settings. region, imagery, sampler and writer are required; task, labels, detection, instance, classification, change and split are optional. Unknown keys are errors, reported with their path (for example sampler.stide: Extra inputs are not permitted), so typos never pass silently. A key written twice in the same mapping is an error too (the message names the key and both lines), because YAML would otherwise let the last one win. A number field refuses a YAML boolean (patch_size: true, zoom: yes, test_ratio: on), which YAML 1.1 would otherwise turn into 1: write the number. Whole numbers are fine for decimal fields (max_empty_ratio: 1). Relative paths (region.path; path, files[].path and annotated_area of labels and of change.before/change.after; writer.staging_dir and a local imagery.path) are resolved against the folder that contains the config file, so a config works from any working directory. mapcv validate <file> checks a config; mapcv plan <file> also reads the labels and estimates the run.
task: segmentation # what the dataset is for: segmentation (default), detection, instance, classification, change or regressionregion: { … } # where (required)imagery: { … } # which pixels (required)labels: { … } # which polygons or label raster (optional: omit for an image-only dataset)detection: { … } # box settings (optional, task: detection only)instance: { … } # mask settings (optional, task: instance only)classification: { … } # label rule (optional, task: classification only)change: { … } # change mask settings (optional, task: change only)sampler: { … } # how to cut patches (required)writer: { … } # where and how to write (required)split: { … } # train/val/test lists (optional: omit to skip splitting)region: { west: 4.9375, south: 52.3725, east: 4.9515, north: 52.3780 }imagery: type: xyz zoom: 18 source: esri_satellite max_connections: 4labels: path: buildings.geojson label_field: classsampler: patch_size: 256writer: staging_dir: ./datasetsplit: strategy: spatialregion: { west: 5.40, south: 51.975, east: 5.50, north: 52.035 }imagery: type: eopf_zarr path: /data/S2A_MSIL2A_….zarr resolution: 10 bands: [b04, b03, b02, b08]labels: path: landuse.geojson label_field: class classes: { built_up: 1, farmland: 2, forest: 3, water: 4 }sampler: patch_size: 128 edge_strategy: drop max_empty_ratio: 0.2writer: staging_dir: ./dataset image_format: npysplit: strategy: spatialregion: { west: 3.0010, south: 48.8490, east: 3.0070, north: 48.8520 }imagery: type: geotiff path: ortho/paris-2024.tif # a COG URL (https:// or s3://) works toolabels: path: buildings.geojson # lon/lat; reprojected into the file's CRS label_field: classsampler: patch_size: 256 edge_strategy: dropwriter: staging_dir: ./dataset image_format: png # npy for anything but 8-bit 1- or 3-band filessplit: strategy: spatialWhat the dataset is for. It decides what each patch is annotated with and is recorded in the manifest, so a resumed run must use the same task.
| Value | Status | Annotation |
|---|---|---|
segmentation (default) |
supported | A class mask per patch, from labels (none without labels: an image-only dataset) |
detection |
supported | A box per object, written as COCO JSON and YOLO labels; needs labels. Settings in the detection block |
instance |
supported | A binary mask per object (instance segmentation), written as COCO JSON with RLE masks and optionally an instance-ID PNG per patch; needs vector labels. Settings in the instance block |
classification |
supported | One label, or a set of labels, per patch, written as labels.csv and labels.json; needs labels (vector or raster). Settings in the classification block |
change |
supported | A before/after image pair and a binary change mask per patch, in LEVIR-CD’s A/, B/, label/ layout; needs two imagery sources and labels that mark the change, or two label sets to compare. Settings in the change block |
regression |
supported | A float32 target per pixel (canopy height, biomass, elevation …) read from a raster of values; needs labels.type: continuous. Targets are written as tif (default) or npy masks, NaN where there is no value |
task: segmentationdetection
Section titled “detection”Optional, and only allowed with task: detection (another task with a detection block is an error). Omit it for the defaults. One object is one label feature: a MultiPolygon is one object, and so is a feature that a patch edge cuts into pieces. Its box is the axis-aligned box of the part that is visible in the patch: inside the patch, inside the raster (not in padding) and over pixels with imagery. Holes do not change a box. Walkthrough: Object detection datasets; files: Dataset format.
| Field | Type | Default | Description |
|---|---|---|---|
min_visible |
float, 0–1 | 0.3 |
Keep an object in a patch only if at least this fraction of its area is visible there. 0 keeps every visible part; 1 keeps only whole objects. An object too large for any patch to show this fraction is never kept, and mapcv warns about it |
min_box_pixels |
float ≥ 0 | 2 |
Drop boxes narrower or shorter than this many pixels (slivers along patch edges) |
formats |
list of coco, yolo |
[coco, yolo] |
Which annotation files to write. At least one; no duplicates |
point_box_size |
float > 0 | null |
null |
Turn Point/MultiPoint features (not in KML) into square boxes of this many pixels, centred on the point. null: points are skipped with a warning, as for segmentation |
task: detectionlabels: path: buildings.geojson label_field: classdetection: min_visible: 0.3 min_box_pixels: 2 formats: [coco, yolo]Validation (with task: detection): labels is required; labels.ignore_index, labels.all_touched and writer.mask_format are mask settings and are rejected when set, as is writer.world_files with images that are not PNG or JPG; point_box_size needs point labels from GeoJSON, GeoPackage, Shapefile or GeoParquet (KML points are not read); sampler.pad_mode: reflect is rejected with edge_strategy: pad, because mirrored padding would show objects without boxes. sampler.min_label_ratio keeps a patch only when its boxes’ visible areas (overlaps counted once) cover at least that fraction of it. Detection writes no masks. A resumed run must use the same detection settings.
instance
Section titled “instance”Optional, and only allowed with task: instance (another task with an instance block is an error). Omit it for the defaults. One instance is one label feature: a MultiPolygon is one instance, and so is a feature that a patch edge cuts into pieces. Its mask is the part of the feature that is visible in the patch: inside the patch, inside the raster (not in padding) and over pixels with imagery. Every instance is burned on its own with the same rasterizer and pixel rule as segmentation masks (labels.all_touched applies), so overlapping instances each keep their exact mask. Walkthrough: Instance segmentation datasets; files: Dataset format.
| Field | Type | Default | Description |
|---|---|---|---|
min_visible |
float, 0–1 | 0.3 |
Keep an instance in a patch only if at least this fraction of its area is visible there. 0 keeps every visible part; 1 keeps only whole instances. A feature too large for any patch to show this fraction is never kept, and mapcv warns about it |
min_area |
int ≥ 1 | 4 |
Drop instances whose mask has fewer pixels than this (slivers along patch edges, features smaller than a pixel) |
id_mask |
bool | false |
Also write one 16-bit instance-ID PNG per patch to masks/ (writer.mask_format then sets its format). A patch with more than 65,535 instances is an error |
task: instancelabels: path: buildings.geojson label_field: classinstance: min_visible: 0.3 min_area: 4 id_mask: falseValidation (with task: instance): labels is required and must be vector labels (a label raster has no objects; use task: segmentation); labels.ignore_index is rejected when set (instance masks only hold pixels that have imagery); writer.mask_format is rejected unless instance.id_mask is on; writer.world_files is rejected with images that are not PNG or JPG unless id_mask is on; sampler.pad_mode: reflect is rejected with edge_strategy: pad, because mirrored padding would show objects without masks. sampler.min_label_ratio keeps a patch only when its instances (overlaps counted once) cover at least that fraction of it. A resumed run must use the same instance settings and labels.all_touched.
classification
Section titled “classification”Optional, and only allowed with task: classification (another task with a classification block is an error). Omit it for the defaults. A patch’s label comes from how much of it each class covers. Coverage is a class’s pixels divided by the patch’s valid pixels, counted on the very mask that segmentation would write for the same labels (the same rasterizer and pixel rule for polygons, labels.all_touched included; the same nearest-neighbour sampling for a label raster). A pixel is valid when it has imagery (inside the raster, not NoData or a failed tile) and is not labels.ignore_index; for a label raster that includes pixels outside the label raster, on its NoData and on labels.ignore_values. Background (mask value 0) is not a class and has no coverage. Where polygons overlap, the mask’s later-wins rule applies: the later feature’s class takes the shared pixels. Coverage is computed as pixels / valid pixels in 64-bit floating point. (With a label raster and labels.ignore_index: null, pixels without a label become background in the mask and so stay in the denominator; only pixels without imagery are left out.) Walkthrough: Patch classification datasets; files: Dataset format.
| Field | Type | Default | Description |
|---|---|---|---|
mode |
single | multi |
single |
single: the patch gets the class with the largest coverage (a tie goes to the lowest class ID), if that class qualifies. multi: the patch gets every class that qualifies, in ascending class ID order (a multi-hot label) |
min_fraction |
float, 0–1 | 0.0 |
A class qualifies when its coverage is at least this and above zero, so 0 means “any labeled pixel”. 0.5 with single is a strict majority rule |
empty |
skip | background |
skip |
What happens to a patch no class qualifies for. skip drops it (it is not written); background keeps it with the label background (class ID 0). A patch without a single valid pixel is always dropped, because there is nothing to judge; sampler.max_empty_ratio drops patches that are mostly without imagery |
task: classificationlabels: path: landuse.geojson label_field: classclassification: mode: multi min_fraction: 0.1 empty: backgroundValidation (with task: classification): labels is required, vector or type: raster; writer.mask_format is rejected (no masks are written), as is writer.world_files with images that are not PNG or JPG; sampler.pad_mode: reflect is rejected with edge_strategy: pad, because the mirrored part of an edge patch would show imagery that no label counts; sampler.min_label_ratio greater than 0 is rejected with empty: background, because it would drop exactly the background patches (without background, it keeps its segmentation meaning: the labeled share of the whole patch must reach it). Class names must not hold control characters, and with mode: multi no whitespace, because a patch’s labels are joined with a space in labels.csv; with empty: background no class may be named background. A resumed run must use the same classification settings and labels.all_touched.
change
Section titled “change”Optional, and only allowed with task: change (another task with a change block is an error). A change dataset needs imagery as a list of exactly two sources on one grid: the image before (first) and after (second). Each patch gets both images and a change mask, all on the before image’s grid. Files: Dataset format.
The mask comes from one of two places:
labelsthat mark what changed, vector or a label raster (type: raster): every labeled pixel is change. For a raster, pixels it does not label (outside it, its NoData,ignore_values) keep the ignore value.change.beforeandchange.after, two vector label sets (same fields aslabels): a pixel changed where their masks differ, for example a building that appeared or was demolished. A set given as onepathwithoutlabel_fieldmarks objects of one kind, and only their presence is compared. Sets that name classes (label_field,fileswith aclassorlabel_fieldeach, orosm) are compared class by class, so a change of class counts too: classes match by name. Withoutclassesthe names of both sets get one class map (IDs 1, 2, … in sorted order, or integer names as themselves; recorded astarget.labels.classesin the manifest); withclasses, give the same mapping in both. Both sets must name classes, or neither: to compare a plain file with a named set, write it asfiles: [{path: …, class: <name>}].
| Field | Type | Default | Description |
|---|---|---|---|
change_value |
int, 1–255 | 1 |
Mask value of changed pixels; unchanged pixels are 0. Use 255 (with labels.ignore_index: null) for loaders that expect LEVIR-CD’s 0/255 masks |
before, after |
vector labels | — | The two label sets to compare, instead of labels: a file (path), several (files) or OpenStreetMap (osm, queried over the region unless it sets bbox). Relative paths, annotated_area included, resolve against the config’s folder |
task: changeimagery: - {type: geotiff, name: before, path: city_2023.tif} - {type: geotiff, name: after, path: city_2025.tif}change: before: {path: buildings_2023.geojson} after: {path: buildings_2025.geojson}Pixels without imagery in either image get the ignore value (labels.ignore_index, or the label sets’ shared ignore_index; default 255), as in segmentation masks. So do pixels outside either label set’s annotated_area: nothing was labeled there in that set, so whether the pixel changed is unknown, and it is neither change nor no change. For example, with the same buildings in both sets but before.annotated_area covering the west half, the east half is ignored, not change.
Validation (with task: change): exactly two imagery sources; either labels or both change.before and change.after, not both; the two label sets need the same ignore_index (not null when either has an annotated_area), both sets name classes or neither does, and sets that name classes have the same classes mapping or none; change_value must differ from the ignore value. A resumed run must use the same labels (their files’ hashes are recorded) and change_value.
region
Section titled “region”The area of interest: a WGS-84 longitude/latitude bounding box, or polygons in a file.
| Field | Type | Default | Description |
|---|---|---|---|
west |
float | required | Western longitude in degrees |
south |
float | required | Southern latitude in degrees |
east |
float | required | Eastern longitude in degrees |
north |
float | required | Northern latitude in degrees |
path |
path | — | Instead of the four bounds: a polygon file (any label format, in any CRS it states). The box becomes the polygons’ bounds, and only patches that overlap a polygon (with a positive area; touching an edge is not enough) are made, so an irregular area or several separate areas waste no patches. A relative path is resolved against the config file’s folder |
name_field |
string | — | With path: the attribute naming each polygon’s region (polygons may share a name). Without it, polygons are regions 1, 2, … in file order. At most 255 distinct names |
layer |
string | — | With path: the GeoPackage layer to read |
region: path: study_sites.geojson # several separate areas name_field: site # each patch records its site: summary.region in the manifestsplit: strategy: region # whole sites to one split each: no leakage at allWith path, each patch records the region that covers most of it (summary.region in the manifest; a tie goes to the region listed first), windows are read around the polygons rather than across the gaps between them, and the manifest records the file’s hash (region.aoi_sha256), so a resumed run refuses edited polygons. mapcv plan estimates the patch count from the share of the box the polygons cover.
Validation: longitudes in −180…180, latitudes in −90…90, west < east and south < north; path replaces the four bounds (giving both is an error), must exist and hold at least one polygon when the config is loaded; name_field and layer need path. With XYZ imagery the box must also stay within ±85.0511° latitude, the limit of Web Mercator tiles. A box that crosses the antimeridian is not supported. region.zoom from the 0.1 layout was removed in 0.3.0; set imagery.zoom instead (see Removed 0.1 keys).
For XYZ imagery the box is expanded outward to whole tiles at the chosen zoom. For Sentinel-2 it is reprojected into the product’s CRS and expanded outward to the product’s pixel edges. For a GeoTIFF it is reprojected into the file’s CRS, expanded outward to the file’s pixels and clipped to the raster. See Coordinates and grids.
imagery
Section titled “imagery”Selects an imagery source with the required type key: xyz, eopf_zarr, stac_cog or geotiff. For segmentation, regression and change datasets it can also be a list of named sources (several sources).
Several imagery sources
Section titled “Several imagery sources”Write imagery as a list to sample several sources at the same patches: a before/after pair, optical plus a DEM, or two dates of the same product. Every source in the list needs a name. Each source keeps its own settings.
imagery: - {type: geotiff, name: before, path: 2023.tif} - {type: geotiff, name: after, path: 2025.tif} - {type: geotiff, name: dem, path: dem_2m.tif} # pixels twice as large: repeatedwriter: {staging_dir: ./dataset, image_format: tif}| Rule | Detail |
|---|---|
| Names | name on each source: 1–32 lowercase letters, digits, _ or -, starting with a letter or digit, unique. mask and names ending in _world are reserved. A single (not listed) source takes no name |
| Grid | The first source’s pixel grid is the dataset’s grid; region, patches, masks and georeferencing all follow it |
| Other sources | Same CRS as the first, and pixels the same size or a whole number of them across (2×, 3×…) on a grid that shares pixel corners with the first. Coarser pixels are repeated onto the grid (nearest neighbour, exact); nothing is interpolated. A source with finer pixels must come first |
| Coverage | A pixel has imagery only where every source has it. Pixels outside a source count as empty and are ignore_index in masks |
| Output | Images/<name>/patch_*.<ext> per source, one shared Masks/ folder, and one files key per source name in the manifest. With writer.stack_sources, one file per patch holds every source instead |
| Tasks | segmentation (with or without labels), regression and change (exactly two). Tasks that write one image per patch (detection, instance, classification) take a single source |
A source on a grid that does not line up (another CRS, a half-pixel shift, a non-integer pixel ratio) fails before anything is written, and the error says how it differs. Resample it onto the first source’s grid first (for example with gdalwarp -tap -tr).
XYZ tiles (type: xyz)
Section titled “XYZ tiles (type: xyz)”| Field | Type | Default | Description |
|---|---|---|---|
type |
"xyz" |
required | Selects XYZ tiles |
zoom |
int, 1–22 | required | Tile zoom level. Pixel size ≈ 156 543 × cos(latitude) / 2zoom m |
source |
string | — | A built-in source: esri_satellite, esri_topo, esri_street, cartodb_positron, cartodb_dark_matter |
url_template |
string | — | Your own http:// or https:// URL with {z}, {x} and {y} |
earth_engine |
block | — | Tiles rendered by Google Earth Engine (see below) |
max_connections |
int, 1–64 | 16 |
Concurrent tile requests. Keep this low for shared servers; mapcv init writes 4 |
policy |
strict | lenient | ignore |
lenient |
What happens to a tile that still fails after retries (see below) |
max_failed_ratio |
float, 0–1 | 0.05 |
Under lenient, the run stops if more than this fraction of a chunk’s tiles fail |
strip_rows |
int ≥ 1 | 4 |
Tile rows per processing chunk (4 rows = 1,024 px). Lower it to reduce memory on very wide regions |
cache |
bool | true |
Keep downloaded tiles on disk for as long as the server allows, so re-runs don’t download them again (see below) |
Validation:
- Exactly one of
source,url_templateorearth_engineis required; setting two is an error. sourcemust be a built-in name. The 0.1 presetsgoogle_satelliteandosmwere removed and fail with an explanation (why).url_templatemust behttp(s)with a host (and a valid port), must contain{x},{y}and{z}, and may contain no other placeholder or brace (write{z}, not{{z}}). The host and port may not hold a placeholder. A{s}subdomain placeholder is rejected: replace it with one subdomain, for examplea.- Tiles must decode to 256 × 256 images (PNG, JPEG, WebP or GIF). A 16-bit grayscale PNG is scaled to 8 bits by its high byte, as 16-bit RGB is. Fully transparent pixels (alpha 0) are black and count as empty, so
sampler.max_empty_ratiodrops patches over areas a service leaves transparent. - A tile body larger than 4 MiB is not read: that tile fails (
response too large) andpolicyapplies. writer.image_formatmust bepng,jpgortif.
Failure policy. Each tile is tried up to four times (three retries with backoff that honours Retry-After up to 30 s); a request that times out is tried twice. A connection must open within 10 s, a response (and each piece of its body) must arrive within 10 s, and a whole tile within 30 s. Connection errors, timeouts, truncated bodies, HTTP 429 and 5xx are retried; other 4xx responses, non-image bodies (for example an HTML error page) and bodies over 4 MiB fail immediately.
A fetch stops as soon as it cannot succeed: under lenient, when more than max_failed_ratio of a chunk’s tiles have failed (the rest are not requested), and under every policy when the first max(max_connections, 8) requests all went unanswered (timeouts, refused connections, HTTP 429 or 5xx), which means the server is down or the URL is wrong. A tile that still fails is then handled by policy:
policy |
Failed tile | max_failed_ratio |
|---|---|---|
strict |
Stops the run with an error | not used |
lenient |
Left out; its pixels are black and count as empty | enforced per chunk |
ignore |
Filled with a black tile | never enforced |
Requests identify themselves with the user agent mapcv/<version> (+https://github.com/tahamukhtar20/mapcv) and send no Referer header. A redirect is followed only when it stays on http(s), does not go from https to plain http and does not go from a public server to this machine or a private network; for a url_template with a query string (where providers put keys), only redirects within the same host are followed. The host the URL names is connected wherever it resolves, so a docker-compose service or a server on your own network works by name. Any other host a redirect leads to (or another port of the same host) is judged by the addresses its name resolves to, when connecting: from a URL that names a public host, mapcv does not connect there to a loopback, private (10/8, 172.16/12, 192.168/16, fc00::/7), link-local (169.254/16, fe80::/10), carrier-grade NAT (100.64/10), unspecified or multicast address, IPv4-mapped IPv6 included. A refused redirect or address fails the tile at once, without retries.
Tile cache. Downloaded tiles are kept on disk, so a second run over the same area (another patch size, a new staging_dir, a sweep of settings) reads them from the cache instead of the server. A tile is reused for as long as the server’s caching headers allow: Cache-Control: max-age (less Age), else Expires. A response marked no-store or no-cache is not kept, and one without caching headers is kept for 7 days (the minimum the OpenStreetMap tile usage policy asks clients to cache for). A header asking for longer than a year is capped at a year. Expired tiles are downloaded again. Failed tiles are never cached, and neither is a tile that does not decode (a wrong size, an image that is cut off): it is cached only after it has been decoded, and a copy an earlier run kept is removed when it fails to decode.
- The cache lives in
~/.cache/mapcv/tileson Linux ($XDG_CACHE_HOME/mapcv/tileswhen that is set),~/Library/Caches/mapcv/tileson macOS and%LOCALAPPDATA%\mapcv\Cache\tileson Windows. Set the environment variableMAPCV_CACHE_DIRto put the cache folder elsewhere; tiles then go to itstiles/subfolder. - Tiles are filed under a hash of
url_template: the template, and any key in it, is never written to disk. cache: falsedownloads every tile and keeps none.mapcv cacheshows the cache’s size and clears it.- The MCP server never uses the cache, because it writes only inside its
--root.
Google Earth Engine (imagery.earth_engine)
Section titled “Google Earth Engine (imagery.earth_engine)”Any Earth Engine image, or a collection reduced to one image, rendered as RGB tiles by Earth Engine and read like other XYZ tiles. Needs pip install "mapcv[gee]" and a one-time earthengine authenticate with an account registered for Earth Engine.
imagery: type: xyz zoom: 15 # ≈ 4.8 m/px; Earth Engine renders any zoom earth_engine: collection: COPERNICUS/S2_SR_HARMONIZED start: "2024-06-01" end: "2024-09-01" max_cloud: 40 # skip scenes with more than 40 % cloud cloud_score_plus: 0.6 # mask the cloudy pixels left (Sentinel-2 only) reducer: median # median | mean | mosaic | min | max vis: {bands: [B4, B3, B2], min: 0, max: 3000} project: my-cloud-project # the Cloud project requests are made (and counted) for| Field | Type | Default | Description |
|---|---|---|---|
image |
string | — | An ee.Image asset ID, for example USGS/SRTMGL1_003 |
collection |
string | — | An ee.ImageCollection asset ID, reduced to one image with reducer |
start, end |
date | — | Filter the collection to [start, end): a date (YYYY-MM-DD) or an RFC 3339 time; collection only |
reducer |
median | mean | mosaic | min | max |
median |
How the collection becomes one image |
max_cloud |
number, 0–100 | — | Keep only scenes whose cloud percentage is at most this; collection only |
cloud_property |
string | detected | The scene property max_cloud filters on: CLOUDY_PIXEL_PERCENTAGE for Sentinel-2 and CLOUD_COVER for Landsat are detected; for other collections max_cloud needs it |
cloud_score_plus |
number, above 0 and below 1 | — | Sentinel-2 collections (COPERNICUS/S2…) only: mask each pixel whose Cloud Score+ clear score (cs_cdf) is below this; 0.6 is a good start |
vis.bands |
1 or 3 band names | required | Rendered as grey or a palette (one band) or RGB (three) |
vis.min, vis.max |
number or one per band | 0, 1 |
Values mapped to 0 and 255 |
vis.gamma |
number or one per band | — | Gamma correction |
vis.palette |
list of colours | — | Colours from min to max, for one band only |
project |
string | — | Your Cloud project registered for Earth Engine |
Set exactly one of image or collection. A collection is filtered to the scenes over the tiles the dataset reads (the region snapped outward to whole tiles, so pixels at the edge get all their scenes too), the dates and max_cloud, masked with cloud_score_plus, then reduced. Without cloud settings a summer median in a cloudy climate is mostly cloud; max_cloud alone is enough where clear scenes are common, and cloud_score_plus also keeps the clear parts of partly cloudy scenes. mapcv init --template earth-engine writes this recipe, and the guided mapcv init offers Sentinel-2, Landsat 8 and NAIP with colours that suit them, or any asset. Each time it opens the imagery, mapcv asks Earth Engine for a tile URL (getMapId) with your credentials. That URL holds a short-lived map ID, so it is never written to the config, the manifest, the tile cache or messages. Cached tiles of a collection are kept per tile area, since another area filters other scenes into the composite: a run over another region never reuses them, so the output does not depend on what ran before. The manifest records earth-engine:<asset> and the rendering settings (not project), so a resumed run must ask for the same image and rendering. Earth Engine’s terms, quotas and any billing are yours; see Providers & licensing.
Sentinel-2 L2A EOPF Zarr (type: eopf_zarr)
Section titled “Sentinel-2 L2A EOPF Zarr (type: eopf_zarr)”Requires pip install "mapcv[zarr]" (Python 3.10–3.13).
| Field | Type | Default | Description |
|---|---|---|---|
type |
"eopf_zarr" |
required | Selects one EOPF Sentinel-2 L2A product |
path |
string | — | Local path, file:// URL, or anonymous https:// or s3:// URL of the .zarr product. A relative local path is resolved against the config file’s folder |
search |
mapping | — | Find the product in a STAC catalog instead of naming it (below); set path or search, not both |
resolution |
10 | 20 | 60 |
10 |
Output grid in metres; every band is harmonised onto it |
bands |
list of strings | all 12 | Variables to read, in output order. Default: b01 b02 b03 b04 b05 b06 b07 b08 b8a b09 b11 b12 |
chunk_rows |
int ≥ 1 | 1024 |
Raster rows per processing chunk |
scl_mask |
non-empty list of ints 0–11 | none | Scene classification (SCL) classes whose pixels count as having no imagery, such as [3, 8, 9, 10] (cloud shadows, medium and high probability clouds, cirrus) |
Validation:
- Exactly one of
pathandsearch. pathmay not contain a username, password, query string or fragment; private-store authentication is not supported. Windows paths (C:\data\S2.zarr,file:///C:/data/S2.zarr) work.bandsmust be non-empty and without duplicates; names are lower-cased (B04→b04). Unknown band names fail when the product is opened, with a list of what is available.writer.image_formatmust benpyortif.- The region must intersect the product.
Find the product (imagery.search)
Section titled “Find the product (imagery.search)”imagery: type: eopf_zarr search: datetime: 2025-05-01/2025-05-31 max_cloud: 10 bands: [b02, b03, b04, b08] scl_mask: [3, 8, 9, 10]| Field | Type | Default | Description |
|---|---|---|---|
datetime |
string | required | A date (2025-05-13), an RFC 3339 time, or an interval start/end (.. for an open end); dates cover whole days |
max_cloud |
number 0–100 | 20 |
Highest eo:cloud_cover (percent) of an acceptable product |
catalog |
URL | https://stac.core.eopf.eodc.eu |
A STAC API (https://; http:// only for localhost) |
collection |
string | sentinel-2-l2a |
The collection to search |
asset |
string | product |
The item asset holding the whole EOPF Zarr product |
When the run starts, mapcv searches the catalog for the region’s bounding box and picks, among the items whose footprint covers the whole region with at most max_cloud percent cloud, the least cloudy one; ties go to the earlier acquisition, then to the item ID, so the same catalog gives the same product every time. The manifest’s sources[0].fingerprint records the catalog, collection and item, and product_id the product’s name: resuming refuses to continue with another product if the search later finds a different one. mapcv plan estimates without contacting the catalog. A page of results may be at most 64 MiB and must arrive within 120 s, and next links are followed only on the catalog’s own scheme and host. When no item qualifies, the run stops and says how many items were found and how many covered the region.
scl_mask reads the product’s scene classification layer (20 m, nearest-neighbour onto the output grid) and treats pixels of the listed classes as pixels without imagery: their band values become NaN, segmentation masks mark them with labels.ignore_index, and they count towards sampler.max_empty_ratio, so max_empty_ratio: 0.2 drops patches that are more than a fifth cloud. Values outside 0–11 (the fill at product edges) also count as no imagery.
Sentinel-2 as Cloud Optimized GeoTIFFs (type: stac_cog)
Section titled “Sentinel-2 as Cloud Optimized GeoTIFFs (type: stac_cog)”Each band of a Sentinel-2 L2A scene as its own COG, found in a STAC catalog: by default Earth Search (Element 84), whose COGs sit in a public AWS bucket. No extra install: mapcv reads the COGs with its own reader, over HTTPS range requests.
imagery: type: stac_cog search: datetime: 2025-04-01/2025-06-30 max_cloud: 20 bands: [red, green, blue, nir, swir16] # the item's asset keys, in output order scl_mask: [3, 8, 9, 10] # cloud shadows, clouds, cirrus: no imagerywriter: image_format: npy # or tif| Field | Type | Default | Description |
|---|---|---|---|
search |
mapping | required | As for eopf_zarr: datetime (required), max_cloud (20), collection (sentinel-2-l2a), and catalog, here https://earth-search.aws.element84.com/v1 by default. No asset: the bands are the assets |
bands |
list of strings | [red, green, blue, nir] |
Asset keys of the item, in output order. Earth Search names them coastal, blue, green, red, rededge1–rededge3, nir, nir08, nir09, swir16, swir22 |
scl_mask |
non-empty list of ints 0–11 | none | Scene classes whose pixels count as having no imagery, read from scl_asset |
scl_asset |
string | scl |
The item’s scene classification asset |
chunk_rows |
int ≥ 1 | 1024 |
Raster rows per processing chunk |
The item is chosen as for eopf_zarr search: the least cloudy one covering the whole region, ties to the earlier acquisition, then the ID. Bands come at 10, 20 or 60 m; every band is placed on the grid of the finest band among bands, a coarser band’s pixels repeated (nearest neighbour, exact: Sentinel-2’s grids share their origin), so 20 m bands are not interpolated. Values are the COGs’ as stored (uint16 digital numbers for Earth Search; reflectance = value / 10000, plus the offset its metadata states for processing baseline 04.00 and later). A pixel has imagery when every band has data (not the COGs’ NoData, 0) and, with scl_mask, its scene class is not masked; SCL’s class 0 is “no data” as well. Patches are writer.image_format: npy or tif. The manifest’s sources[0] records source_type: stac_cog, the item ID as product_id, and the catalog, collection and item in its fingerprint, so a resumed run refuses another item.
A catalog may point only to https:// or s3:// COGs (local files only from a localhost catalog, for testing).
GeoTIFF and Cloud Optimized GeoTIFF (type: geotiff)
Section titled “GeoTIFF and Cloud Optimized GeoTIFF (type: geotiff)”Needs no extra install. Reads one file, or a mosaic of local files, as it is: patches keep the file’s CRS, pixel grid, band count and data type, and nothing is resampled. A guide with examples: Use your own GeoTIFF.
| Field | Type | Default | Description |
|---|---|---|---|
type |
"geotiff" |
required | Selects one GeoTIFF or COG, or a mosaic of local GeoTIFFs |
path |
string | required | Local path, file:// URL, or https:// or anonymous s3:// URL (read with HTTP range requests). A relative local path is resolved against the config file’s folder. A local glob pattern (tiles/*.tif, survey/**/*.tif) reads every matching file as one mosaic. A path that exists is always that file, even with [, * or ? in its name (img[12].tif, survey [2024]/ortho.tif); only a path that does not exist is read as a pattern |
bands |
list of ints ≥ 1 | all bands | 1-based band numbers to read, in output order: [3, 2, 1] reverses the bands of a BGR file. Band names in the manifest keep the file’s numbering (b3, b2, b1) |
overview |
int ≥ 0 | 0 |
Overview level: 0 is the full resolution, 1 the first (largest) overview, and so on. Fails when the file has fewer levels |
nodata |
number | .nan |
file’s value | NoData value that overrides (or supplies) the file’s. Pixels where every selected band equals it have no imagery |
chunk_rows |
int ≥ 1 | 1024 |
Raster rows per processing chunk |
Validation:
pathmay not contain a username, password, query string or fragment; private-store authentication is not supported. Plainhttp://is accepted only for a loopback host (localhost,127.0.0.1), for a local test server. A redirect from the server is checked by the same rules: it may not lead to plainhttp://elsewhere, fromhttps://tohttp://, to a URL with credentials, or from a public server to this machine or a private network, also by a host name that resolves there (as for tiles, above). Windows paths work.bandsmust be non-empty and without duplicates.- A glob pattern (
*,?,[...];**also matches folders) is for local files only and must match at least one file. A part of the path that exists as written is taken literally, so a folder calledsurvey [2024]works insurvey [2024]/*.tif.**does not follow links to folders, and a file reached through several links is used once. Dot files and folders are not matched. The files form one mosaic: they must share the CRS, data type, band count, NoData value (or setnodatafor all of them), pixel size and pixel grid (pixel corners line up), and have the overview level asked for. Where files overlap, the first file in sorted path order that has data at a pixel wins; gaps between files are no imagery.bands,overviewandnodataapply to every file. The manifest records each file’s name and fingerprint, so resuming after adding, removing or changing a file is refused. nodatamust be a value the file’s data type can hold; otherwise no pixel could equal it, and mapcv warns that it has no effect.- With
overviewabove 0, the files of a mosaic must also have overviews of one pixel size. Files whose size is not a multiple of the overview factor round it differently; useoverview: 0for those. - The file must have a CRS that is an EPSG code of a map CRS (projected or geographic; a geocentric or vertical code is refused) and a pixel transform, and its compression must be one the reader supports (none, Deflate, LZW, PackBits, ZSTD, JPEG, WebP); the error names the problem. The region must intersect the file.
writer.image_format:pngorjpgneeduint8data in 1 or 3 selected bands (a single band is written as gray, repeated into three channels); any other data type or band count needsnpyortif(both keep the file’s data type and band count;tifalso writes georeferenced patches). A selection that cannot fit PNG/JPG is rejected when the config is loaded; the data type is checked when the file is opened (mapcv plandoes that, without reading pixels).
Empty pixels. A pixel has no imagery when every selected band equals the NoData value, or, for floating-point files, is NaN or infinite. They count as empty for sampler.max_empty_ratio and are labels.ignore_index in masks, like the area outside the raster. A region that extends beyond the file is clipped to it with a warning; one that misses it is an error.
Resuming. The manifest records a fingerprint of the file in sources[0].fingerprint and a resumed run refuses a different file (see Dataset format).
labels
Section titled “labels”Optional. Without it you get an image-only dataset (no Masks/). labels.type picks where the masks come from: vector (the default, so configs without type keep working) burns polygons from a GeoJSON, KML, GeoPackage, Shapefile or GeoParquet file (or several, or OpenStreetMap); raster reads a classified GeoTIFF, see Raster labels; continuous reads a raster of values for task: regression, see Continuous values.
Polygon labels (type: vector)
Section titled “Polygon labels (type: vector)”| Field | Type | Default | Description |
|---|---|---|---|
type |
vector |
vector |
Polygon labels |
path |
path | required | A .geojson, .json, .kml, .gpkg, .shp, .parquet or .geoparquet file. GeoJSON and KML are WGS-84 longitude/latitude; the others may use any CRS they state and are reprojected. A relative path is resolved against the config file’s folder. A local file only: file:// and https:// URLs are for imagery.path and are refused here |
layer |
string | null | null |
GeoPackage only: the layer (table) to read. Required when the file has several; leave unset with one. An error for other formats |
label_field |
string | null | null |
Feature property (GeoJSON, KML) or column (the other formats) holding the class. null: every polygon is class 1 |
classes |
map label → int | null |
Fixed class IDs, each 1–255, for example {building: 1, road: 2}. Requires label_field. Labels not in the map are skipped |
ignore_index |
int 1–255 or null |
255 |
Mask value for pixels with no imagery under them: padding, failed tiles and NoData. Pass it to your loss (CrossEntropyLoss(ignore_index=255)). No class may use it; null writes background (0) there instead. With task: classification no mask is written and imagery-less pixels never count either way |
all_touched |
bool | false |
Burn every pixel a polygon touches, not only pixels whose centre is inside |
buffer |
{line, point} in metres |
null |
Turn line and point features into polygons: a line becomes a band line metres wide (a road, a river), a point a disc point metres across (a tree, a well). Give one or both; features of the other kind are skipped as before. The buffer is drawn in metres on the ground, in the UTM zone of each feature, then the polygon is labeled like any other. Not for KML (mapcv reads only its polygons) |
files |
list | null |
Several label files instead of path (see below) |
osm |
mapping | null |
Labels straight from OpenStreetMap instead of a file (below) |
annotated_area |
path | null |
A polygon file (any of the formats above) outlining where the scene was labeled. Mask pixels outside it get ignore_index instead of background, so a partly labeled scene does not teach that unlabeled ground is background. For task: segmentation and classification (where the outside pixels do not count towards coverage); needs an ignore_index. Feature attributes are ignored. A relative path is resolved against the config file’s folder |
labels: path: osm_roads_and_trees.geojson label_field: kind classes: {road: 1, tree: 2} buffer: {line: 6, point: 8} # roads 6 m wide, tree crowns 8 m across annotated_area: surveyed.geojson # outside it: ignore_index, not backgroundSeveral label files. List them under files instead of giving path: buildings from one source, roads from another, land use from a third. Each entry has a path (any of the formats above), its own layer and buffer, and either a label_field (each feature’s class is that attribute’s value) or a class (one class for every feature of the file). classes, ignore_index, all_touched and annotated_area stay under labels and apply to all files; label_field, layer and buffer move into the entries. Class IDs are given once over the names of all files (with classes when set, so a name has the same ID whichever file it comes from). Later files win where features overlap, so put the most trusted or most specific labels last:
labels: files: - {path: landuse.gpkg, layer: landuse, label_field: landuse} # forest, crop, ... - {path: buildings.geojson, class: building} # drawn over land use - {path: roads.geojson, class: road, buffer: {line: 6}} # drawn over both classes: {building: 1, road: 2, forest: 3, crop: 4}How class IDs are chosen (label_field set, no classes): if every label is an integer from 1 to 255 (digits 0–9), the integer is the ID; otherwise the distinct labels are sorted, numbers by value before names, and numbered 1, 2, 3, …. 3 and 3.0 are the same class. More than 255 distinct labels is an error. Features without polygon geometry, without a label value or with coordinates that are not numbers or beyond ±90° latitude are skipped with a warning that counts them. Where polygons overlap, the one later in the file wins.
Validation: exactly one of path, files and osm; suffix must be one of the above (unzip a KMZ to its KML first; a .tif asks for type: raster); layer needs a .gpkg; classes keys must be non-empty and values integers in 1–255; with path, classes requires label_field. GeoJSON with a projected crs member is rejected. At run time a Shapefile without .prj, a layer without a CRS and a GeoParquet file with crs: null are errors (the CRS would be a guess), GeoParquet needs pip install "mapcv[parquet]", and a label_field that no feature has (in any format) lists the fields there are. Malformed GeoJSON is an error naming the feature; a GeoPackage layer that is a view is read in the order SQLite returns its rows, within work and memory bounded by the file’s size (a view whose query never ends, or that builds values far larger than the file, is an error). A GeoParquet file whose columns decode to more than 64 times its size plus 64 MiB is an error. The manifest records layer and buffer in target.labels when they are set (with files, each file’s settings without its path, and one hash over all files), and the annotated area by its file’s hash (annotated_area_sha256, so editing it is a resume mismatch); annotated_area must be a vector file, needs an ignore_index and applies to segmentation and classification only; buffer needs a distance above 0, and buffer.point and detection.point_box_size cannot both be set. Details: Prepare labels.
Labels from OpenStreetMap (labels.osm)
Section titled “Labels from OpenStreetMap (labels.osm)”labels: osm: classes: - {name: building, tags: {building: "*"}} - {name: water, tags: {natural: water}} - {name: road, tags: {highway: [motorway, primary, secondary, residential]}} buffer: {line: 6} # roads become 6 m wide polygons| Field | Type | Default | Description |
|---|---|---|---|
classes |
list | required | Each class has a unique name and tags: a key with "*" (any value), one value, or a list of values. A feature belongs to the first class whose tags all match. Keys are letters, digits and _:.-; values hold no quotes or backslashes |
overpass_url |
URL | https://overpass-api.de/api/interpreter |
The Overpass API to ask (https://; http:// only for localhost). The public server limits heavy use: for large areas, run your own or use an extract with labels.path. A key in the query string is accepted: the manifest records its values as ***, and messages leave the query out |
timeout |
int seconds, 1–3600 | 180 |
The query’s time limit on the server. The whole answer must arrive within timeout + 60 s, and be at most 512 MiB |
bbox |
[west, south, east, north] |
the region | The box to query |
One Overpass query fetches every matching node, way and relation in the box. Closed ways become polygons (except highways, barriers, railways and waterways, and area=no, which stay lines), multipolygon relations become polygons with their holes (rings are assembled by nesting, as osmium and GDAL do, so an island in a lake is kept), a self-intersecting closed way is repaired keeping all its parts, other ways are lines and nodes are points; as with label files, lines and points become labels only with buffer. Classes get IDs 1, 2, 3, … in the order listed, or the IDs of labels.classes (by class name, so it may only name classes of osm.classes). label_field and layer don’t apply. buffer, all_touched, ignore_index and annotated_area work as for files, with every task that takes polygon labels.
The answer is stored as GeoJSON in the cache folder (<cache dir>/osm/, see the tile cache), with the data’s timestamp (osm_base) and the query, and reused until you delete it: rerunning or resuming does not ask Overpass again, and the labels’ SHA-256 in the manifest identifies the exact data. Fresh data (after deleting the file) has another hash, so a resumed run refuses to mix it with the old. mapcv plan fetches the labels too, to count them; the MCP server does not fetch them (it writes only inside its root).
OpenStreetMap data is © OpenStreetMap contributors under the Open Database License: a dataset whose masks come from it is a derived database, to be shared under the ODbL with that credit. mapcv card adds the credit.
Raster labels (type: raster)
Section titled “Raster labels (type: raster)”A classified raster, such as a land-cover map (ESA WorldCover, Dynamic World), a national map or an earlier model’s output, used as the masks. It may have any CRS, resolution and origin: mapcv reads it on the imagery’s pixel grid.
labels: type: raster path: ESA_WorldCover_10m_2021_v200_N48E000_Map.tif # local, https:// or anonymous s3:// classes: # raster value -> mask ID, or -> {id, name} 10: {id: 1, name: tree_cover} 30: {id: 2, name: grassland} 40: {id: 3, name: cropland} 50: {id: 4, name: built_up} 80: {id: 5, name: water} 90: {id: 6, name: wetland} 95: {id: 6, name: wetland} # several values may share an ID unmapped: background # other values: background | ignore| Field | Type | Default | Description |
|---|---|---|---|
type |
raster |
required | Raster labels |
path |
string | required | A GeoTIFF or COG: a local path (relative to the config file’s folder), an https:// URL or an anonymous s3://bucket/key, read like GeoTIFF imagery (only the tiles under each window). Its CRS must be an EPSG code and its values integers (uint8, uint16, int16, …) |
band |
int ≥ 1 | 1 |
Band holding the classes (1-based) |
classes |
map value → ID or {id, name} |
required | Raster value → mask ID (0–255, 0 is background). The short form 10: 1 names the class value_10 (value_90_95 when several values share the ID); {id: 1, name: tree_cover} names it. One ID has one name. The names and IDs are the manifest’s class_map |
unmapped |
background | ignore |
background |
What values missing from classes become: background (0), or ignore_index |
nodata |
int | null | the file’s NoData | Label value meaning “no label”: such pixels get ignore_index. The file’s NoData does not apply to a value classes maps (a warning says so) |
ignore_values |
list of int | [] |
More label values that get ignore_index (for example “unclassified”) |
resampling |
nearest |
nearest |
How a label pixel is picked; categorical values are never averaged, so nearest is the only choice |
ignore_index |
int 1–255 or null |
255 |
As for polygon labels; it also marks pixels outside the label raster, label NoData and ignore_values. null writes background (0) there |
Alignment. Each imagery pixel takes the value of the label pixel that contains the imagery pixel’s centre. The centre (col + 0.5, row + 0.5 in the corner-based affine transform) is projected exactly into the label raster’s CRS (pyproj; computed in parallel threads, with exactly the coordinates a call per point gives; for a fixed PROJ pipeline, a 16-pixel grid of centres is projected and the rest interpolated, and every centre the interpolation leaves within its measured error, and at least 0.001 label pixel, of a label-pixel boundary is projected exactly, so the label pixels are the same), then the inverse of the label raster’s transform gives a fractional label pixel, which is rounded down (floor(x + 1e-10): a centre exactly on a boundary between two label pixels takes the right / lower one, as GDAL does). Files tagged PixelIsPoint are shifted by half a pixel to corner-based, as in GDAL. The result equals rasterio.warp.reproject(..., resampling=Resampling.nearest, tolerance=0) onto the patch’s transform. (GDAL’s default tolerance=0.125 approximates the transformation and can pick a neighbouring label pixel near boundaries.) When the label raster shares the imagery’s CRS and pixel grid, windows are copied directly, with the same result.
Validation: classes must list at least one value; no ID may be ignore_index; an ID has one name and a name one ID; ID 0 takes no name; ignore_values and nodata may not be in classes; unmapped: ignore needs an ignore_index. Raster labels make masks, so they work with task: segmentation, with task: classification (one label per patch) and as the change mask of task: change; task: detection and task: instance need polygon labels (one feature per object) and refuse type: raster. generate and plan refuse a label raster without an EPSG CRS, with non-integer values or without the configured band, and warn when it does not overlap the imagery. Details: Prepare labels.
Continuous values (type: continuous)
Section titled “Continuous values (type: continuous)”The targets of task: regression: a GeoTIFF or COG of values to predict, such as canopy height, biomass or elevation, in any CRS and resolution. Each imagery pixel takes the value at its centre, with the same exact nearest-neighbour lookup as raster labels (values are never interpolated), and the target is value × scale + offset as float32.
task: regressionlabels: type: continuous path: canopy_height_cm.tif scale: 0.01 # centimetres to metres valid_min: 0writer: {staging_dir: ./dataset, image_format: tif, mask_format: tif}| Field | Type | Default | Description |
|---|---|---|---|
type |
"continuous" |
required | Selects a raster of values |
path |
string | required | Local path, file:// URL, or https:// / anonymous s3:// URL, as for raster labels |
band |
int ≥ 1 | 1 |
Band holding the values |
nodata |
number | .nan |
the file’s NoData | Value meaning “no value” |
scale, offset |
number | 1, 0 |
The target is value × scale + offset, computed in float64 and stored as float32 |
valid_min, valid_max |
number | — | Raw values outside this range have no target |
Target pixels are NaN where there is no value: outside the raster, on its NoData, NaN or out-of-range values, and where the imagery has no data (padding included). sampler.min_label_ratio keeps a patch only when at least that fraction of its pixels has a value. Each manifest entry’s summary.values holds the number of pixels with a value and their min, max and mean; mapcv info sums them up.
Validation: task: regression needs type: continuous labels, and type: continuous needs task: regression. scale must be finite and non-zero, offset, valid_min and valid_max finite, and valid_min at most valid_max. Float targets do not fit a PNG, so writer.mask_format defaults to tif for regression and png is refused. A resumed run must use the same raster (its fingerprint is recorded) and the same scaling.
sampler
Section titled “sampler”| Field | Type | Default | Description |
|---|---|---|---|
patch_size |
int, 1–16384 | required | Side of each square patch in pixels |
stride |
int ≥ 0 | 0 |
Step between grid patches. 0 means patch_size (no overlap); smaller values overlap |
mode |
grid | random |
grid |
Regular grid, or random_count random positions |
edge_strategy |
pad | drop | shift |
pad |
What to do where a patch would cross the raster edge (see below) |
pad_mode |
zero | reflect |
zero |
How pad fills the missing part of an edge patch |
max_empty_ratio |
float, 0–1 | 1.0 |
Skip patches whose empty fraction (pixels without imagery: failed tiles, NoData, outside the raster) is above this. A patch without a single pixel of imagery is always skipped, and generate says how many were |
min_label_ratio |
float, 0–1 | 0.0 |
Skip patches whose labeled fraction (mask pixels that are neither background 0 nor ignore_index) is below this. Only applies with labels. For detection, the labeled fraction is the area of the kept objects’ visible parts; for classification, the valid pixels of any class over the whole patch |
random_seed |
int ≥ 0 | 42 |
Seed for mode: random |
random_count |
int ≥ 0 | 100 |
Number of random patches in total (not per chunk). Capped at the number of distinct positions on the raster (a warning says so), and at most 3,000,000 (see below) |
Patch limit. One run enumerates at most 3,000,000 patches (grid positions over the whole raster, or random_count), because every position is held in memory while the run lasts. plan still counts any grid, from the raster’s size alone, and lists the limit as a problem; generate refuses a larger one before it writes anything, with the number of patches, the limit and what to change: raise sampler.stride (or patch_size), shrink the region or split it into several smaller ones. With region.path the limit applies to the grid over the region’s bounding box, before the polygons are applied. Jobs over 1,000,000 patches ask for confirmation first.
Edge strategies:
edge_strategy |
Grid behaviour | Random behaviour |
|---|---|---|
pad |
Patches start at every stride step inside the raster; the part outside is filled by pad_mode and the patch is marked padded |
Same positions as drop; a raster smaller than the patch gives one padded patch at the top-left |
drop |
Only patches that fit entirely inside | Positions where the patch fits entirely inside; none if the raster is smaller than the patch |
shift |
Like drop, plus one last patch shifted inward to cover the remaining edge (may overlap its neighbour) |
Same as pad |
Empty pixels: for GeoTIFF, a pixel is empty when it is outside the file, equal to NoData in every selected band, or non-finite (floating-point files). For XYZ, a pixel is empty when all three channels are 0 (black tiles, failed tiles, padding). For Sentinel-2, a pixel is empty unless every band has data, i.e. when any band is NaN (NoData); a reflectance of 0 is valid data. Padding in Sentinel-2 patches is 0, not NaN.
writer
Section titled “writer”| Field | Type | Default | Description |
|---|---|---|---|
staging_dir |
path | required | Output folder, created if needed; a relative path is resolved against the config file’s folder. Re-running into a folder with a compatible manifest.json resumes |
image_format |
png | jpg | npy | tif |
png |
png/jpg for XYZ (RGB uint8); npy for Sentinel-2 (bands-first: float32 for eopf_zarr, the COGs’ uint16 for stac_cog); tif for any source: a georeferenced GeoTIFF with the source’s own band count and data type. For GeoTIFF imagery png/jpg need uint8 in 1 or 3 bands, otherwise use npy (bands-first, the file’s data type) or tif |
jpg_quality |
int, 1–100 | 95 |
JPEG quality when image_format: jpg |
jpg_subsampling |
4:2:0 | 4:4:4 |
4:2:0 |
JPEG chroma subsampling when image_format: jpg. 4:2:0 stores colour at half resolution in each direction, like Pillow’s default, and gives smaller files (about 10% on typical aerial imagery at quality 95, up to half on noisy colour imagery). Use 4:4:4 to keep full colour resolution. JPEG datasets made by mapcv 0.2 are 4:4:4, so resume them with jpg_subsampling: 4:4:4 |
mask_format |
png | npy | tif |
png (tif for regression) |
How masks are stored (with labels). png is a single-channel PNG (8-bit; 16-bit for 16-bit masks); npy a NumPy array; tif a GeoTIFF that opens aligned with the image. The mask values, including labels.ignore_index, are identical in every format |
world_files |
bool | false |
Write a world file next to every PNG (.pgw) and JPG (.jgw) patch, images and masks, so QGIS and GDAL place them on the map. World files hold no CRS (it is crs in the manifest). Not needed for tif, which carries its georeferencing inside; an error when neither images nor masks are PNG or JPG |
footprints |
bool | true |
Write patches.geojson after the split: one WGS-84 footprint polygon per patch with its file name, split, row and column, and class pixel counts. Open it in QGIS to see where each patch is |
stack_sources |
bool | false |
With several imagery sources (a segmentation or regression dataset), write them into one file per patch instead of one per source: npy stores a (T, C, H, W) array (T sources in config order, C bands each), tif a GeoTIFF of T × C bands named <source>_<band>. For time series and temporal models (several dates of one product). Every source needs the same band count and data type |
The manifest records the writer as {layout: files, image_format, jpg_quality, jpg_subsampling, mask_format} (plus world_files: true and stack_sources: true when they are on). Changing image_format, mask_format or world_files of an existing dataset is a configuration mismatch, so resuming it is refused; footprints does not affect the patches and may change. Layout and naming: Dataset format. Detection and classification datasets record no mask_format (setting it is an error: they write no masks) and write images to images/ (lowercase, where Ultralytics looks for them) instead of Images/; Ultralytics reads PNG, JPG and TIF images, not NPY. Instance datasets also write images/, and record mask_format only with instance.id_mask (its PNGs go to masks/). Change datasets write A/, B/ and label/.
GeoTIFF output. tif patches (and masks) are written by mapcv itself: striped, Deflate-compressed with the horizontal predictor (the floating-point predictor for float32), one band per channel. They carry the source’s EPSG code and each patch’s own affine transform, so QGIS, rasterio and TerraTorch open them in place. Images declare NaN as no-data when they are floating point (and nothing for integer data, where 0 is a real value); masks declare labels.ignore_index as no-data, so GIS tools show the pixels without imagery as transparent. See Dataset format.
Validation: XYZ needs png, jpg or tif; EOPF and stac_cog need npy or tif; GeoTIFF imagery needs npy or tif unless it is uint8 in 1 or 3 bands (see GeoTIFF). world_files needs PNG or JPG files to write next to. stack_sources needs imagery as a list of two or more sources, task: segmentation or regression, and image_format npy or tif; sources with different band counts or data types are refused when they are opened, before anything is written.
Optional. When present, mapcv generate writes split lists at the end; mapcv split can redo them later with different settings.
| Field | Type | Default | Description |
|---|---|---|---|
strategy |
spatial | stratified | random | region |
spatial |
spatial assigns whole raster blocks to one split and drops train/val patches overlapping a held-out patch. stratified and random split individual patches. region assigns whole regions of an area of interest to one split each (in a seeded random order), and drops train/val patches overlapping a held-out one where regions touch: the cleanest leakage-free split when the regions are apart. It needs a dataset made with region.path, and at least three regions to fill train, val and test |
test_ratio |
float, 0–1 | 0.20 |
Fraction of patches for test.txt |
val_ratio |
float, 0–1 | 0.10 |
Fraction of the remaining patches for val.txt |
block_size |
int ≥ 1 | auto | spatial block side in pixels. Default 4 × patch_size, reduced (never below patch_size) so the raster has at least 10 blocks |
labeled_ratios |
list of floats in (0, 1] | [0.10, 0.20, 0.30] |
Write splits/<percent>/labeled.txt and unlabeled.txt subsets of train for each ratio. [] writes none |
seed |
int | 42 |
Seed for shuffling blocks or patches |
sample_limit |
int ≥ 1 | — | Split only this many patches (subsampled, keeping the stratum mix unless strategy: random) |
Validation: ratios within bounds; labeled_ratios may not contain duplicates (after conversion to folder names such as 0.125 → 12.5). With spatial, the achieved ratios are approximate because whole blocks move together. See Splits without leakage.
Removed 0.1 keys
Section titled “Removed 0.1 keys”mapcv 0.2 still loaded the 0.1 layout (region.zoom plus a top-level tiles: block) with a deprecation notice. Both were removed in 0.3.0 and now fail validation, with the replacement named in the error:
$ mapcv validate old.yamlConfig error in old.yaml: • config: `tiles:` was replaced by `imagery:` in mapcv 0.2 and removed in 0.3; move it under `imagery: {type: xyz, zoom: ..., source: ...}` and delete `region.zoom` (see MIGRATION.md)| Removed | Use instead |
|---|---|
region.zoom |
imagery.zoom |
tiles: |
imagery: {type: xyz, zoom: …, source: …} |
Full guide: Migrating.