raster_mosaic
Reprojects and mosaics an arbitrary set of GDAL-readable rasters onto one common CRS/pixel grid,
respecting each input's own validity -- an internal valid-data mask band (gdalinfo's "Mask Flags:
PER_DATASET") or a NoData value, whichever the inputs actually carry -- and choosing which of the
two the output uses accordingly (see "Output validity: mask band vs NoData value" below), rather
than forcing every run through a single reserved NoData value regardless of what the inputs have.
Runs locally for testing, or as a PBS Pro / SLURM job array + final job on an HPC cluster.
To run it on a cluster -- with the bundled Prefect + Dask flow, or with plain PBS/SLURM job scripts -- see docs/running_on_hpc.md.
Features:
- Respects each input's validity -- internal valid-data mask bands and NoData values are both
carried through reprojection and mosaicking, and the output uses one or the other (default
"auto": decided from the inputs; setoutput_validity = "nodata"to always get a NoData value). See "Output validity: mask band vs NoData value". - Target-aligned pixels -- every input is warped with
-tap, so the output origin is an exact multiple of the resolution in the target CRS (EPSG:3577 by default). - Policy-conforming COG -- 512×512 blocks, ZSTD level 9, product-type predictor and overview
resampling,
PROCESSING_*provenance tags. See "COG policy compliance". - Optional statistics and histograms embedded in the file (off by default:
compute_stats,compute_histogram). See "Band statistics" and "Histograms". - Parallel on PBS Pro / SLURM -- reprojection runs as a job array or a Dask worker fan-out,
with up to
ncpustiles warped concurrently in each job. - Containerised -- cluster jobs run inside the published apptainer image, so the host needs no GDAL install. See "HPC image".
Works with any band count and dtype. A single-band uint8 classification/palette raster is a common
case (resampling defaults to nearest-neighbour for exactly this reason -- see
MosaicConfig.resampling), but nothing here is specific to that.
Install
From PyPI -- no clone needed:
uvx raster-mosaic --help # run it without installing
uvx --from 'raster-mosaic[flow]' raster-mosaic run # the Prefect + Dask flow
uv add raster-mosaic # or add it to your own project
The local reproject-tile / mosaic commands need the GDAL command-line tools (gdalwarp,
gdalbuildvrt, gdal_translate) on PATH. On a cluster, the jobs run inside the container
instead (see "HPC image" below), so the host only submits.
For development:
uv sync --group dev --group test
Commands
# 1. Discover inputs (cfg.input_globs in mosaic.toml) -> JSONL manifest
uv run raster-mosaic build-manifest --config mosaic.toml --manifest-out run/manifest.jsonl
# 1'. ...or from an explicit list of paths (one per line, '-' for stdin) instead of globs
uv run raster-mosaic build-manifest --config mosaic.toml --manifest-out run/manifest.jsonl \
--inputs-from inputs.txt --summary-out run/summary.json
# 2. Reproject each input onto the common grid (local -- one line of the manifest at a time)
uv run raster-mosaic reproject-tile --manifest run/manifest.jsonl --index 1 --config mosaic.toml
# 2'. ...or submit the whole manifest as a PBS/SLURM job array
uv run raster-mosaic submit-reproject --scheduler pbs --manifest run/manifest.jsonl \
--config mosaic.toml --container raster_mosaic.sif --account your_account
# 3. Mosaic all reprojected tiles into the final COG
uv run raster-mosaic mosaic --config mosaic.toml
# 3'. ...or submit as a single job, after the reproject array job finishes
uv run raster-mosaic submit-mosaic --scheduler pbs --config mosaic.toml \
--container raster_mosaic.sif --account your_account \
--depends-on <reproject_array_jobid>
--scheduler is pbs or slurm on both submit-* commands.
Chunking many small/fast inputs
If your manifest has many inputs that each reproject quickly, submitting one PBS/SLURM array task
per input can be counter-productive: per-job scheduling overhead (apptainer startup, PBS/SLURM
dispatch) can dwarf the actual work. A real job was observed to spend most of its ~11s walltime on
overhead alone, and cpupercent=100 in the PBS accounting despite requesting 4 CPUs -- a single
small warp doesn't scale with more threads, so those extra CPUs went unused.
--chunk-size on submit-reproject fixes both: each array task processes a chunk of several
manifest lines instead of just one (shrinking the array and amortizing overhead across more work
per job), and the tiles within a chunk run concurrently -- up to --ncpus at a time -- instead
of handing all those CPUs to one warp that can't use them:
uv run raster-mosaic submit-reproject --scheduler pbs --manifest run/manifest.jsonl \
--config mosaic.toml --container raster_mosaic.sif --account your_account \
--chunk-size 20 --ncpus 4
Defaults to 1 (one task per input, unchanged from before) since the right chunk size depends on how many inputs you have and how fast each one is -- there's no universally-safe non-1 default.
Driving it from a workflow orchestrator
The submit-* commands are one way to run the three stages on a cluster. A workflow orchestrator
(e.g. a Prefect flow fanning tasks out over Dask workers -- this package ships one,
raster_mosaic.flow, see docs/running_on_hpc.md) can run them itself
instead, calling the same CLI inside this package's container: one build-manifest, then one reproject-tile --index i
per chunk, then one mosaic. A few options exist mainly for that:
-
build-manifest --inputs-from FILEtakes an explicit input list instead of the config'sinput_globs(exactly one of the two must be given) -- e.g. the outputs of an upstream workflow step. One path per line; blank lines and#comments are ignored;-reads stdin; relative paths are resolved against the current directory and stored absolute. Every listed path must exist. -
build-manifest --summary-out FILEwrites a JSON summary next to the human-readable output:{"manifest": "/abs/run/manifest.jsonl", "n_inputs": 3, "chunk_size": 2, "n_chunks": 2, "validity": {"mode": "mask", "nodata": null}}
n_chunksis how manyreproject-tile --indexvalues to run (1..n_chunks). It assumes the config's ownchunk_size, so don't also pass a different--chunk-sizetoreproject-tile. -
reproject-tile --skip-existing(orskip_existing_tiles = truein the config) skips inputs whose reprojected tile already exists, so a retried task only redoes what didn't finish. Tiles only get their final.tifname once every step on them is done, so an existing tile is always complete -- but it may have been built with a different config, which is why this is off by default.
build-manifest (both input modes) also rejects inputs that share a file stem, since reprojected
tiles are named by stem alone and same-stemmed inputs would otherwise overwrite each other's tile.
HPC image
submit-reproject / submit-mosaic jobs run entirely inside the --container image via
apptainer exec -- no host-side uv sync, no live project checkout at job time. .gitlab-ci.yml
builds Dockerfile (this package layered on the official GDAL CLI image) and pushes
it to this project's GitLab Container Registry on every push -- :latest tracks the default branch
and any git tag; every branch build is also pushed tagged with its ref slug and short commit SHA,
so a specific build stays pullable after the branch moves on.
Pull it with apptainer -- no docker needed on the HPC side, no local image build at all:
apptainer pull raster_mosaic.sif docker://registry.gitlab.com/jrsrp/sys/raster_mosaic:<tag>
The image is public, so no login is needed. Pick <tag> to match your installed package
(raster-mosaic --version): X.Y.Z for a release, the short commit SHA for a branch build, or
latest for the newest default-branch build. The Prefect flow refuses a mismatched image -- see
docs/running_on_hpc.md. Then point --container at the
pulled raster_mosaic.sif.
Pulling once and reusing the .sif for a whole job array (as in the examples above) is the
simplest. apptainer exec docker://... also works directly: the converted image is cached
(~/.apptainer/cache), so with that cache on a shared filesystem and warmed once beforehand, the
array tasks reuse it -- but each still checks the tag with the registry (compute nodes need network
access), and a mutable tag like latest could change mid-run, so use a fixed one.
Running submit-reproject / submit-mosaic itself from inside a container
The image above has no PBS/SLURM client (qsub/sbatch) -- it only needs to run the actual
reproject-tile/mosaic work, which the job templates already invoke via apptainer exec. But if
you're also running raster-mosaic submit-reproject/submit-mosaic itself from inside a
container (rather than directly on a login node where qsub/sbatch are on PATH), pass
--submit-via:
uv run raster-mosaic submit-reproject --scheduler pbs ... --submit-via "ssh localhost"
This prefixes the qsub/sbatch invocation with the given (shell-split) command -- ssh localhost qsub ... reaches the real PBS/SLURM client on the underlying host from inside a
container that doesn't have one installed. Every path this command itself hands to the rendered
job script (WORKDIR, MANIFEST, CONFIG, the job script's own temp-file path, and the job's own
-o/--output log location) is already absolute for exactly this reason: an SSH-wrapped
submission starts from the SSH session's home directory, not this command's own working directory,
so a relative path would resolve against the wrong place.
Site-specific PBS/SLURM job configuration
The rendered job scripts (hpc/templates/*.sh.tmpl) are deliberately generic -- no site's
particular account, filesystem layout, or scheduler quirks are baked in. Where a site needs
something beyond the common flags (--account, --walltime, --mem, --ncpus, --partition,
--qos, --module-load), four escape hatches on submit-reproject/submit-mosaic cover it, all
opt-in and no-op by default:
--extra-directive-- one extra raw scheduler directive line (#PBS .../#SBATCH ...as appropriate for--scheduler), inserted verbatim into the job script. Repeatable for more than one line. For anything with no dedicated flag, e.g. a site that requires#PBS -W sandbox=PRIVATEbefore a job can use node-local scratch.--select-extra-- PBS only. Extra:name=valuechunk resource(s) (leading:required) appended onto the auto-builtselect=1:ncpus=...:mem=...string, e.g.:flashreq=500g:flashtmp=500gon a site that provisions flash/NVMe scratch as its ownselectchunk resource rather than a top-level-lflag. Ignored on--scheduler slurm-- SLURM expresses extra resources via its own--gres/--constraintdirectives, which belong in--extra-directiveinstead.--apptainer-bind-- an extrasrc[:dst[:opts]]bind spec for theapptainer execinvocation itself (not a scheduler directive -- a flag toapptainer). See "Container visibility of site-specific paths" below; almost always needed alongside--stage-diron a path apptainer doesn't auto-bind. Repeatable -- every occurrence is comma-joined into one--bindargument.--stage-dir(mosaic only) -- see "Staging the mosaic build through fast scratch" below.
Example: a PBS Pro site where #PBS -W sandbox=PRIVATE alone makes PBS create a per-job private
directory under /flash/tmp and cd the job into it, destroying it automatically when the job ends
(no --select-extra needed for capacity in this case -- only add it if your site also requires
an explicit resource reservation to get that scratch allocated, e.g.
--select-extra ":flashreq=500g:flashtmp=500g"):
uv run raster-mosaic submit-mosaic --scheduler pbs --config mosaic.toml \
--container raster_mosaic.sif --account your_account \
--extra-directive "#PBS -W sandbox=PRIVATE" \
--apptainer-bind "/flash:/flash" \
--stage-dir '$PBS_JOBDIR' \
--depends-on <reproject_array_jobid>
sandbox=PRIVATE's directory is exposed via an environment variable that PBS Pro populates once
the job starts -- $PBS_JOBDIR or $TMPDIR depending on your PBS version/site configuration, not
a fixed name across every site. Check yours with a trivial interactive job (qsub -I -W sandbox=PRIVATE then env | grep -i job) before relying on either.
Container visibility of site-specific paths
apptainer exec only auto-binds a small default set of paths inside the container -- your home
directory, /tmp, /proc, /sys, /dev, cwd, plus whatever a site added globally to
apptainer.conf's bind path. A path outside that set -- most commonly wherever --stage-dir
points, e.g. a site's flash/NVMe mount -- exists on the host but is invisible inside the
container without an explicit bind. The symptom is a FileNotFoundError for a path that
demonstrably exists on the host (confirm with ls outside the container), which looks like a
--stage-dir/env-var resolution bug but usually isn't one -- it's a missing --apptainer-bind.
Quick way to confirm before chasing anything else: apptainer exec $CONTAINER ls /your/mount --
if that fails while ls /your/mount on the bare host succeeds, add
--apptainer-bind "/your/mount:/your/mount" (or whatever the mount's top-level path is; a bind of
the parent covers subdirectories created under it later, e.g. per-job sandbox=PRIVATE
directories, since apptainer resolves the bind live at container start, not as a snapshot).
Staging the mosaic build through fast scratch
mosaic/submit-mosaic --stage-dir (equivalently, MosaicConfig.stage_dir in mosaic.toml)
copies the reprojected tiles into a temp subdirectory of --stage-dir, builds the VRT and COG
there, then copies only the finished COG back out to output_dir -- useful when
reprojected_dir/output_dir sit on a slower shared filesystem and that's the mosaic stage's
bottleneck compared to node-local fast scratch (flash/NVMe). Omit it (the default) to build
directly in reprojected_dir/output_dir, unchanged.
--stage-dir's value on submit-mosaic is inserted verbatim into the rendered job script's
raster-mosaic mosaic --stage-dir "..." invocation, not passed through qsub -v/sbatch --export the way WORKDIR/CONTAINER/CONFIG are -- this lets it be a scheduler-populated
environment variable (e.g. $PBS_JOBDIR, $TMPDIR, SLURM's $SLURM_TMPDIR) that's only resolved
once the job is actually running, in addition to a literal path. Quote it in single quotes on the
command line as in the example above so your local shell doesn't expand it early.
Provisioning the scratch directory itself (if your site requires an explicit resource request for
it, as PBS flash/NVMe chunk resources typically do) is exactly what --select-extra/
--extra-directive are for; making it visible inside the container is --apptainer-bind's job
(see "Container visibility of site-specific paths" above) -- --stage-dir only tells run_mosaic
where to build once the scratch exists and is visible.
If your site's sandbox=PRIVATE (or equivalent) also changes the job's initial working directory,
that's independent of --stage-dir and nothing to worry about: the rendered job script does its
own cd "$WORKDIR" right after the directives (so logs/manifest/config still resolve against your
shared-filesystem run directory), and --stage-dir's value is passed as its own absolute path/env
var reference, not read relative to whatever the cwd happens to be at that point.
Config (mosaic.toml)
Unknown keys are an error, not ignored -- they're either a typo or an option the running version doesn't have (e.g. a container image older than the install that wrote the config).
input_globs = ["egimages/*.tif"] # glob pattern(s); ** for recursive matching. Omit if
# using build-manifest --inputs-from instead
reprojected_dir = "run/reprojected"
output_dir = "run/out"
output_name = "mosaic.tif"
resolution = 10.0
dst_crs = "EPSG:3577" # default; GDA94 Australian Albers
resampling = "near" # "near" is the only safe choice for categorical/palette data
product_type = "categorical" # default; "continuous" or "mask" also accepted
ncpus = 4
output_validity = "auto" # default; "mask" or "nodata" also accepted -- see below
# output_nodata = 255.0 # only meaningful (and only ever required) with output_validity
# = "nodata" -- see "Output validity" below
# optional -- omit entirely to skip land masking
land_mask = "land_mask/Land_any_epsg3577_100m.tif"
# optional -- omit entirely to build directly in reprojected_dir/output_dir; see "Staging the
# mosaic build through fast scratch" above. Usually site-specific, so more often passed per-run
# via --stage-dir instead of committed here.
# stage_dir = "/flash/tmp"
# optional -- extra -mo KEY=VALUE tags on the final mosaic, on top of cog_policy.md's own
# mandated PROCESSING_* tags. See "Extra metadata (extra_mo_tags)" below.
# [extra_mo_tags]
# PIXEL_SCALING = "GAIN:0.01 OFFSET:0.0 UNITS:fraction"
# optional -- GDAL's own native per-band scale/offset (-a_scale/-a_offset), applied uniformly to
# every band. Distinct from extra_mo_tags -- see "Extra metadata" below.
# output_scale = 0.01
# output_offset = 0.0
# optional -- extra metadata scoped to one band, keyed by band number as a string. See "Extra
# metadata" below.
# [extra_band_mo_tags.1]
# PIXEL_SCALING = "GAIN:0.01 OFFSET:0.0 UNITS:fraction"
# optional -- per-band scale/offset, for the (unusual, not recommended) case where bands genuinely
# need to differ. Mutually exclusive with output_scale/output_offset -- see "Extra metadata" below.
# [extra_band_scale]
# 1 = 0.01
# [extra_band_offset]
# 1 = 0.0
# optional -- compute and embed GDAL's standard per-band STATISTICS_* metadata. See "Band
# statistics" below.
# compute_stats = "exact" # or "approx"; default "none" adds nothing
# optional -- embed a per-band histogram (STATISTICS_HISTO* metadata). See "Histograms" below.
# compute_histogram = true # default false
# optional -- skip inputs whose reprojected tile already exists (for retries). See "Driving it
# from a workflow orchestrator" above.
# skip_existing_tiles = true # default false
# optional -- build overviews on each reprojected tile so run_mosaic can reuse them instead of
# recomputing the mosaic's own overviews from scratch. See "Reusing tile overviews" below.
# build_tile_overviews = true # default false
product_type drives the final mosaic's compression/predictor/overview-resampling and
PROCESSING_* provenance tags to match cog_policy.md -- see "COG policy
compliance" below. It's unrelated to resampling, which only affects the reprojection step.
land_mask is optional -- when set, every non-land pixel (per the mask raster, pixel value 1 =
land) is excluded from each tile's valid-data mask during reproject-tile, combined with (never
overriding back to valid) whatever the tile's own mask already marked invalid. Can be in any
CRS/resolution GDAL can read; no manual reprojection/prep step needed. Applied per-tile in the
parallel reproject stage, not once at the final serial mosaic step, so the cost is spread across
the already-parallel array job -- see gdal_ops._apply_land_mask.
Extra metadata (extra_mo_tags)
extra_mo_tags is a plain escape hatch for embedding whatever extra -mo KEY=VALUE tags a caller
wants on the final mosaic, on top of (never instead of) cog_policy.md's own mandated PROCESSING_*
tags -- keys can't collide with those. This tool makes no attempt to compute the values itself:
gdalbuildvrt doesn't carry per-tile dataset metadata onto the VRT run_mosaic builds and reads
from (confirmed empirically -- a source tile's own tags simply aren't there once it's part of a
VRT), so there's nothing generic to summarise from the inputs even if this tried to. Working out
what should go here (e.g. an organisation-specific summary of the inputs' own provenance/scaling
metadata) is deliberately left to a site/product-specific wrapper around this package, same as
land_mask/stage_dir's site-specific values.
output_scale/output_offset are the same passthrough idea but for GDAL's own native per-band
scale/offset (gdalinfo's "Offset"/"Scale" fields, set via gdal_translate -a_scale/-a_offset)
rather than an arbitrary metadata string -- distinct from, and can be set alongside, an
extra_mo_tags entry describing the same gain/offset relationship in a custom format. Applied
uniformly to every band (confirmed empirically -- -a_scale/-a_offset take one value each, not
one per band). None (the default, for either) omits the flag entirely rather than writing GDAL's
own default (scale 1, offset 0) explicitly.
Both -mo/-dmo (extra_mo_tags) and -a_scale/-a_offset are dataset-level only -- neither
gdal_translate flag can target an individual band. (output_scale/output_offset are fine
regardless, since GDAL's Scale/Offset has no dataset-level form at all -- it's purely a
GDALRasterBand property, so even one uniform -a_scale/-a_offset value already lands on every
band correctly, which is what QGIS and other GDAL-based tools read for display.)
For genuine band-scoped arbitrary metadata, use extra_band_mo_tags instead -- keyed by band
number as a string ("1", not 1; TOML table keys are always strings), each value its own
KEY=VALUE mapping. This works differently from extra_mo_tags: since gdal_translate has no
band-scoped metadata flag at all, run_mosaic instead injects <Metadata>/<MDI> XML elements
directly into its own _mosaic.vrt (gdal_ops._write_band_metadata) before the final
gdal_translate -of COG reads it -- confirmed empirically that a VRT's per-band <Metadata>
survives onto the real output band, even though gdalbuildvrt never writes any there itself.
Editing that VRT isn't the "invalidates COG structure" case cog_policy.md's gdal_edit.py note
warns about -- it's run_mosaic's own working intermediate, not the released COG product. A band
number with no matching band in the built mosaic raises rather than silently dropping the tag.
Prefer GDAL's own Scale/Offset (output_scale/output_offset) over an extra_mo_tags string
like PIXEL_SCALING whenever you can. The official mechanism is what QGIS and other GDAL-based
tools actually read for display -- a custom string tag isn't understood by anything unless it
parses that specific key itself. Use a custom tag (PIXEL_SCALING or similar) alongside it only
for backward compatibility with existing tooling/consumers that already expect that convention;
don't use it as a substitute for setting the real Scale/Offset.
We recommend keeping scale/offset consistent across every band of a mosaic -- i.e. reach for
output_scale/output_offset, not extra_band_scale/extra_band_offset, in the common case.
Different scale/offset per band is possible via extra_band_scale/extra_band_offset (same
VRT-injection mechanism as extra_band_mo_tags, via gdal_ops._write_band_scale_offset), but
isn't recommended -- it's easy for downstream consumers to silently misread pixel values without
per-band-aware code. The two mechanisms are mutually exclusive by construction: MosaicConfig
rejects setting output_scale/output_offset alongside extra_band_scale/extra_band_offset,
since -a_scale/-a_offset on the final gdal_translate step unconditionally overwrites whatever
per-band <Scale>/<Offset> the VRT already has (confirmed empirically) -- silently discarding
the per-band values rather than erroring would be worse than refusing to run.
Band statistics
compute_stats ("none" default / "exact" / "approx") computes and embeds GDAL's standard per-band
STATISTICS_MINIMUM/MAXIMUM/MEAN/STDDEV/VALID_PERCENT metadata -- e.g. for consumers
expecting them the way HFA-produced imagery traditionally carries them. Unlike extra_mo_tags/
extra_band_mo_tags/output_scale/extra_band_scale above, this isn't a passthrough: GDAL
computes the values itself, directly from the mosaic's own output pixels, as part of the same final
gdal_translate call (-stats/-approx_stats) -- there's nothing for a caller to supply. Confirmed
empirically that this embeds the statistics in the output file itself at creation time (no
.aux.xml sidecar) and excludes invalid pixels from them.
Mask-band mosaics compute their own statistics. When the run resolves to a mask band (see
"Output validity" below), GDAL's statistics can't be trusted: on a VRT of masked tiles that don't
overlap each other (side-by-side tiles, or tiles with gaps between them), GDAL takes a shortcut
that ignores the masked pixels, reporting STATISTICS_VALID_PERCENT=100 and a standard deviation
divided by the total rather than the valid pixel count (confirmed with GDAL 3.11; min/max/mean
are right, and there's no GDAL option to turn the shortcut off). So in mask mode, run_mosaic
computes the statistics itself, in one parallel pass over the mosaic, and doesn't use -stats/
-approx_stats at all. These are always exact, even with compute_stats = "approx", so "approx"
gives no speed-up in mask mode. NoData-mode runs still use GDAL's own flags. Mosaics made before
this fix with compute_stats in mask mode may carry wrong STATISTICS_STDDEV/VALID_PERCENT
values.
"exact" reads every pixel (accurate, but a second full pass over a potentially very large mosaic);
"approx" samples via overviews instead (much cheaper, approximate values) -- pick based on how
much that cost matters for your mosaic size. cog_policy.md is silent on statistics entirely, so
this is purely a convenience, not a compliance requirement.
Note that "approx"'s speed depends on overviews actually being available to sample from -- with
no overviews anywhere in run_mosaic's input VRT (the default, unless build_tile_overviews is
also set -- see below), "approx" still falls back to a slow, sparse full-resolution read before
it can produce anything, which can dominate a large mosaic's runtime. "exact" always reads every
pixel regardless of overviews, so build_tile_overviews doesn't speed it up at all -- if you need
compute_stats's numbers to match a true full-resolution min/max exactly ("approx"'s sampling can
miss rare/sparse extreme values entirely, by design), use "exact", not "approx" sped up by
overviews.
Histograms
compute_histogram = true (default false) embeds a per-band histogram in the final mosaic, as
the STATISTICS_HISTOBINVALUES / STATISTICS_HISTOMIN / STATISTICS_HISTOMAX /
STATISTICS_HISTONUMBINS / STATISTICS_HISTOBINFUNCTION band metadata items that HFA-produced
imagery and RIOS's calcstats use. GDAL's own histogram support isn't used because it only
persists histograms to a .aux.xml sidecar, never into the GeoTIFF itself.
- It's computed over the mosaic (its VRT), not by adding up per-tile histograms: where tiles overlap, only the pixel the mosaic actually keeps is counted. Invalid pixels (mask band or NoData value) and non-finite floats are excluded.
- It's exact: every valid pixel is read twice, once to find each band's minimum/maximum (bin edges
have to be fixed before counting starts) and once to count. Both passes run in parallel over
windows, up to
ncpusat a time. - Binning: an integer band whose valid values span at most 256 distinct values gets one bin per
value (
direct). Any other band, including every float band, gets 256 equal-width bins between its minimum and maximum (linear). A band with no valid pixels gets no histogram. - The tags are injected into the VRT the same way as
extra_band_mo_tags, so they're written when the COG is created rather than edited in afterwards.extra_band_mo_tagscan't also setSTATISTICS_HISTO*keys while this is on.
Interaction with compute_stats: GDAL's -stats/-approx_stats, combined with the -mo tags
run_mosaic always writes, drop every STATISTICS_* item the VRT carries, including the
histogram (confirmed empirically). So with both options on, run_mosaic doesn't use those flags:
the histogram pass computes the STATISTICS_* values itself, from the same reads, at almost no
extra cost. That makes them exact even for compute_stats = "approx".
Reusing tile overviews (build_tile_overviews)
run_mosaic's final gdal_translate -of COG step always used to pass -co OVERVIEWS=IGNORE_EXISTING, forcing a full-resolution overview rebuild on every run. Setting
build_tile_overviews = true (default false) has reproject-tile build internal overviews on
each reprojected tile as it's produced -- using the same OVERVIEW_RESAMPLING
cog_policy.md/product_type mandate for the final mosaic -- so run_mosaic can potentially reuse
them instead (-co OVERVIEWS=FORCE_USE_EXISTING), skipping the recompute. This also makes
compute_stats = "approx" (above) much cheaper, since GDAL's approximate-stats sampling prefers
overviews over full-resolution reads when they're available.
run_mosaic never trusts this blindly: it checks that every reprojected tile actually has
overviews and is tagged with the exact resampling this run's product_type expects, falling back
to today's IGNORE_EXISTING behaviour (and logging why) if any tile disagrees or lacks
overviews -- e.g. reprojected_dir holding a mix of tiles from before and after this setting was
turned on, or from a run with a different product_type. This is purely a performance decision with
an always-safe fallback, so a mismatch never fails the run the way an output_validity conflict
does.
The overviews are built during reproject-tile, not mosaic. Turning this on and re-running
mosaic against tiles from an earlier reproject-tile run won't do anything -- those tiles have no
overviews on disk yet, so run_mosaic will fall back to IGNORE_EXISTING as before. You need
build_tile_overviews = true set before (re-)running reproject-tile/submit-reproject to
actually get the benefit on the following mosaic run. Building per-tile overviews costs real time
in the reproject stage itself (though it's already parallel across the array job), which is why this
stays opt-in rather than becoming the default -- worthwhile once run_mosaic's own overview
build/compute_stats="approx" pass is the bottleneck, not before.
COG policy compliance
run_mosaic's final output follows cog_policy.md unconditionally: COG driver,
512x512 blocks, ZSTD level 9, PREDICTOR/OVERVIEW_RESAMPLING keyed off cfg.product_type
(PRODUCT_TYPES in config.py), and all six PROCESSING_* provenance tags written at creation
time. reproject-tile's own per-input output is just a working intermediate for gdalbuildvrt,
not the released product the policy governs, so it isn't held to the same creation options.
Whether the output ends up with a mask band or a NoData value (see below) is itself following
cog_policy.md, not deviating from it: Section 4.7 makes an embedded NoData value the default for
"the standard large COG products", with an internal mask band as its narrower, explicit exception
("selected products where 0 is a valid data value... not used in the standard large COG products
covered by this specification"). This tool follows exactly that: a mask band only when the inputs
actually have one.
Output validity: mask band vs NoData value
A raster can mark invalid pixels in two ways:
- Mask band: a separate valid/invalid layer (
gdalinfo's "Mask Flags: PER_DATASET"). Every pixel value stays usable as real data. - NoData value: one reserved value means "invalid". A real pixel with that value is a problem.
Invalid pixels in the inputs are always respected while reprojecting and mosaicking, whichever way
each input marks them. output_validity (default "auto") only decides which of the two the
reprojected tiles and the final mosaic carry.
What build-manifest sees in each input
Each input is classified (from band 1) as one of:
- mask: has a mask band. This includes inputs with both a mask band and a NoData value: the
mask takes precedence and the NoData value is ignored, as in GDAL itself (
gdalinfo, QGIS and rasterio all read such a file that way).gdalwarpon its own does the opposite, soreproject-tilepasses it-srcnodata Nonefor these inputs to keep it consistent. - nodata=X: has a NoData value and no mask band.
- neither: every pixel is valid.
"auto" (the default)
| Inputs | Output | Notes |
|---|---|---|
| All masked | Mask band | No value is reserved, so nothing can collide. |
| All NoData, same value X | NoData = X | The standard product form under cog_policy.md §4.7. |
| Some NoData X, the rest neither | NoData = X | ⚠️ A real pixel equal to X in a "neither" input can't stay X: gdalwarp nudges it (e.g. 0 becomes 1) and only logs a warning. |
| All neither | Mask band | The output still needs validity: reprojection leaves fill around each tile, and the mosaic has gaps between tiles. A mask marks those invalid without reserving a value. |
| Masked mixed with unmasked | Error | Not guessed, because dropping a mask risks the collision this tool exists to avoid. |
| NoData values differ (e.g. 0 and 255) | Error | There's no single value to adopt. |
Overriding it
"mask"always carries a mask band, for every input; any NoData values are dropped from the output, though the pixels they marked stay invalid. It never alters data, so it's the safe fix for either error above."nodata"always embeds a NoData value. Setoutput_nodataunless every input already has the same NoData value and none is masked. Every input's own invalid pixels (masked or NoData, whatever value) becomeoutput_nodatain the output. You're promisingoutput_nodatanever occurs as real data: a valid pixel equal to it gets nudged to a neighbouring value, with only a warning.
Example: inputs with different NoData values (A uses 0, B uses 255). Either set
output_validity = "mask" (both inputs' NoData pixels become masked, nothing reserved), or
output_validity = "nodata" with an output_nodata that can't be real data -- e.g. 255 for
0-100 percentage data -- in which case A's 0s and B's 255s both come out as 255.
This is resolved once for the whole run by build-manifest (over every input, not just one array
task's chunk) and persisted next to the manifest for reproject-tile to read back -- see
src/raster_mosaic/validity.py's module docstring for why it can't be re-derived independently per
array task, and for the full decision logic.
How the mask/NoData value is preserved
See the module docstrings in src/raster_mosaic/gdal_ops.py and validity.py for the full
mechanism. Briefly, for mask mode: gdalwarp already respects a source's internal mask band during
resampling by default (no -srcnodata needed), but doesn't carry it through to its output unless
asked. reproject_one asks via -dstalpha, then folds the resulting alpha band back into a real
internal mask band with gdal_translate -mask. gdalbuildvrt then respects each input's mask when
stacking overlapping tiles (a masked-invalid pixel from one tile does not overwrite valid data from
another tile underneath it), and the final -of COG write preserves the mask and excludes masked
pixels from overview generation. NoData mode works the same way in spirit but more simply -- a
single gdalwarp -dstnodata call, gdalbuildvrt -srcnodata/-vrtnodata for the same
overlapping-tile protection, and -a_nodata on the final gdal_translate -- since there's no mask
band to fold in the first place.
Overlapping tiles
Where two or more reprojected tiles cover the same pixel, gdalbuildvrt resolves it per pixel as
it composites _mosaic.vrt's sources: the last-listed tile that's actually valid there wins --
not a blend or average. If the later tile is invalid at that pixel (masked out, or NoData), its
invalidity doesn't overwrite a valid pixel from an earlier tile underneath (see "How the mask/NoData
value is preserved" above); if both are valid, the later one's value is simply used outright, with
no feathering or distance-weighting at the seam.
"Later" means later in the list run_mosaic hands to gdalbuildvrt, which is every reprojected
tile in reprojected_dir sorted alphabetically by filename (i.e. by each input's own stem) --
input_globs' own match order isn't preserved into this. There's currently no way to prefer one
overlapping tile over another for any other reason (e.g. a more recent acquisition, a cloud-free
scene, a quality flag) -- alphabetical order is all there is.
That's a meaningful distinction for anyone wanting different behaviour here:
- A different priority order (e.g. newest-wins, or an explicit per-input priority) only needs
run_mosaicto handgdalbuildvrtits tile list in a different order -- still no custom pixel processing,gdalbuildvrt's own compositing already does the right thing once the order is right. - Anything other than a hard per-pixel cutover -- feathering across the seam, distance-weighted
blending, a quality-score-weighted average -- is not something
gdalbuildvrt/gdal_translatecan do at all. That's genuine custom pixel processing (reading tile arrays and computing the output directly, e.g. viarasterio/numpy, or a different tool entirely), a real departure from this package's "GDAL CLI only" architecture (seegdal_ops.py's module docstring), not a small change.
Metadata
Release files for raster-mosaic 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| raster_mosaic-0.1.0.tar.gz | 111.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| raster_mosaic-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 187.1 kB
Release files / raster_mosaic-0.1.0.tar.gz
| Download URL | raster_mosaic-0.1.0.tar.gz |
|---|---|
| Size | 111.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
de4bb8062a63288d5f800efdb16c63b3c1891256219dc28b2bf89558f60f00fa
|
|
BLAKE2b-256 checksum How to use checksums |
f21290520926aeaa4d8ff027b8224f9f707a6d8a438a7617c41c594e05f421ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / raster_mosaic-0.1.0-py3-none-any.whl
| Download URL | raster_mosaic-0.1.0-py3-none-any.whl |
|---|---|
| Size | 76.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2beacef417827542d43bd424724cde40ee71107dfaaba42dc3f2d17e5ae04918
|
|
BLAKE2b-256 checksum How to use checksums |
667a4b6f9058b7b40effa1ac29d515d0a1c8b863d1d7172064c3ecd730ab2de6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|