SAM Label Correction
build-sam-corrected-masks uses the Segment Anything Model (SAM, AMG mode) to correct noisy
segmentation masks produced from cartographic sources. Instead of training with hard or soft labels
derived from imperfect map databases, this tool replaces them with spatially coherent labels
obtained by majority vote within each SAM segment.
How it works
- For each tile in the coreset CSV, the tool loads the original GeoTIFF mask and optional auxiliary LULC rasters (e.g. MapBiomas, ESRI, Dynamic World).
- SAM AMG generates segments for the RGB image chip read from an MBTiles file.
- For each SAM segment that contains at least one pixel from the target class set, a majority vote is computed across all sources (original mask + LULC rasters).
- The winning class label is written to every pixel in the segment.
- Pixels belonging to non-target classes are never modified.
- SAM masks are processed ascending by
(predicted_iou, area)so high-confidence, larger segments overwrite smaller ones in overlapping regions.
An NPZ cache can be configured to store SAM segments on disk, avoiding redundant SAM inference across multiple experiments with different target class sets.
Imagery source: four ways to point at it
mbtiles_path and each entry of lulc_paths accept any of the following. All four are read through
the same read_source_aligned_to_mask_window — a resolved candidate is always warped onto the exact
pixel grid of the mask window being corrected, whichever form pointed at it.
1. Single file (existing behavior, unchanged) — MBTiles, VRT, or any rasterio-readable raster:
mbtiles_path: /data/imagery/tiles.mbtiles
2. Directory, matched by spatial bounds — every candidate file's bounds are indexed once (a cheap, header-only open); a window read only opens the files that actually overlap that window, and composites them (first file with data wins per pixel — files are expected to be non-overlapping):
mbtiles_path:
directory: /data/imagery/scenes
extensions: [".tif", ".jp2"] # default: [".tif", ".tiff"]
recursive: false # default: false
match_by: bounds # default
3. Directory, matched by basename — no file is opened to build the index, just listed. A mask
tile named tile_001.tif pairs with tile_001.<any extension> in this folder, by filename stem:
mbtiles_path:
directory: /data/imagery/per_tile
match_by: basename
4. CSV manifest, one row per tile — a path column is required. An optional mask_path column
enables the same direct tile-keyed lookup as basename matching (matched by stem, no bounds needed);
optional minx,miny,maxx,maxy[,crs] columns enable bounds-based matching as a fallback for rows
without a mask_path hit. This mirrors the wide tile_id,image_path,mask_path,<lulc columns> CSV
already used by tools.soft_labels.
mbtiles_path:
csv_path: /data/imagery_manifest.csv
path_column: path # default
mask_path_column: mask_path # default; omit the CSV column to skip direct lookup
default_crs: "" # fallback CRS when the CSV has no crs column
path,mask_path,minx,miny,maxx,maxy,crs
/data/img_001.tif,/data/masks/tile_001.tif,,,,,
/data/img_002.tif,,-6461070,-1803914,-6460458,-1803302,EPSG:3857
Direct tile-keyed lookup (mode 3, or mode 4 with a mask_path hit) is tried first whenever
available — a plain dict lookup, no spatial computation — falling back to bounds-based search
(mode 2, or mode 4's bounds columns) only when no direct entry exists.
Overlapping I/O with GPU compute
SAM's public API (SamAutomaticMaskGenerator.generate()) processes one image per call — there is no
cross-image batching to enable. Two throughput knobs instead:
points_per_batch(default 64): SAM AMG's own internal point-prompt batching within one image — raising it trades GPU memory for fewer forward passes per chunk (points_per_side=32gives 1024 points/chunk = 16 passes at the default 64/batch).prefetch(defaultTrue): the next chunk's aligned imagery/LULC arrays are read on a background thread while SAM processes the current chunk, overlapping I/O with GPU compute. This hides the I/O wait, not the GPU compute time itself.
Installation
SAM is not a dependency of the framework itself. Install it separately:
pip install git+https://github.com/facebookresearch/segment-anything.git
Download a SAM checkpoint (ViT-B recommended for speed):
wget https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth
CLI usage
pytorch-smt-tools build-sam-corrected-masks path/to/config.yaml
Configuration
# Required
coreset_csv: /data/coreset.csv # CSV with mask_path, row_off, col_off, patch_size columns
masks_dir: /data/masks # Directory containing original GeoTIFF masks
sam_checkpoint: /models/sam_vit_b_01ec64.pth
mbtiles_path: /data/imagery/tiles.mbtiles
# One or more output targets (different class sets)
targets:
- classes: [3, 5] # grassland + cropland
output_dir: /data/masks_sam_gc
- classes: [1, 3, 5] # forest + grassland + cropland
output_dir: /data/masks_sam_gcf
- classes: [0, 1, 2, 3, 4, 5] # all classes
output_dir: /data/masks_sam_all
# Optional auxiliary LULC rasters included in the majority vote
lulc_paths:
- /data/lulc/mapbiomas.vrt
- /data/lulc/esri.vrt
- /data/lulc/dynamic_world.vrt
include_base_mask: true # include the mask being corrected as one vote source (default: true)
# SAM AMG parameters
sam_model_type: vit_b
device: cuda:0
points_per_side: 32
points_per_batch: 64 # SAM AMG's own point-prompt batching, per image
pred_iou_thresh: 0.80
stability_score_thresh: 0.90
min_mask_region_area: 200
# Processing
num_classes: 6
nodata_val: 255
chunk_size: 1024 # tile processing chunk size in pixels
prefetch: true # overlap next chunk's I/O with current chunk's GPU compute
# NPZ segment cache (set to "" to disable)
cache_dir: /data/sam_cache
# Multi-GPU splits (process tiles [start_idx, end_idx) on each GPU)
start_idx: 0
end_idx: 999999
Multi-GPU parallelism
Run multiple processes with non-overlapping start_idx/end_idx slices and point each to
a shared cache_dir. The first process to process a chunk writes the cache; subsequent
processes with overlapping class sets load from cache instead of running SAM again.
# GPU 0 — tiles 0..9999
pytorch-smt-tools build-sam-corrected-masks config_gpu0.yaml &
# GPU 1 — tiles 10000..19999
pytorch-smt-tools build-sam-corrected-masks config_gpu1.yaml &
Output
For each target, a copy of every tile is written to output_dir with SAM-corrected pixels. The
original files in masks_dir are never modified.
The run() method returns a summary dict:
{
"n_tiles": 1234,
"elapsed_s": 5432.1,
"tiles": [
{
"tile": "tile_001.tif",
"n_chunks": 4,
"n_skipped": 1,
"per_target": [
{
"output_dir": "/data/masks_sam_gc",
"classes": [3, 5],
"n_target": 89432,
"n_changed": 12340,
"pct_changed": 13.8
}
]
}
]
}
Python API
from pytorch_segmentation_models_trainer.tools.sam_correction import (
SAMLabelCorrectionConfig,
SamLabelCorrector,
apply_sam_correction,
)
config = SAMLabelCorrectionConfig(
coreset_csv="/data/coreset.csv",
masks_dir="/data/masks",
targets=[{"classes": [3, 5], "output_dir": "/data/out"}],
sam_checkpoint="/models/sam_vit_b_01ec64.pth",
mbtiles_path="/data/tiles.mbtiles",
lulc_paths=["/data/lulc/mapbiomas.vrt"],
cache_dir="/data/sam_cache",
)
stats = SamLabelCorrector(config).run()
print(f"Processed {stats['n_tiles']} tiles in {stats['elapsed_s']}s")
The apply_sam_correction function is also available as a pure, dependency-free utility
(no SAM or rasterio needed) for unit testing or custom integration:
import numpy as np
from pytorch_segmentation_models_trainer.tools.sam_correction import apply_sam_correction
corrected = apply_sam_correction(
base_mask=original_mask, # (H, W) uint8 — the mask being corrected
sam_masks=sam_output, # list of SAM mask dicts
lulc_maps=[lulc_array], # list of (H, W) uint8 arrays
classes_to_correct=frozenset([3, 5]),
num_classes=6,
include_base_mask=True,
)
It is a thin wrapper over the generator-agnostic
pytorch_segmentation_models_trainer.tools.region_correction.apply_region_correction,
shared with SLICO Label Correction — the same majority-vote
consensus and eligibility rule, applied to a different region generator's output.
See also
- SLICO Label Correction — same correction rule with SLICO superpixels instead of SAM AMG; useful as a region-generation ablation.