VirtualiZarr Parsers¶
VirtualiZarr parser for generating virtual Zarr datasets from imagery files.
OversightMLParser implements the VirtualiZarr Parser protocol and produces
ManifestStore objects that can be serialized to Kerchunk JSON indices. It works
for any format supported by IO.open(): NITF, standalone JPEG 2000, TIFF, and
GeoTIFF.
The parser conforms to the VirtualiZarr parser callable protocol
(url, registry) -> ManifestStore. It reads bytes by opening url with fsspec
and handing the seekable handle to IO.open(), which issues on-demand byte-range
reads for the tiled formats (NITF, TIFF/GeoTIFF, JPEG 2000) — so a local path and an
s3:// URL follow the same code path and neither downloads the whole file to build
the index.
Single file — pass one URL. If the file contains overview assets (e.g. COG overview IFDs), the parser builds a hierarchical store automatically.
Multi-file pyramid — pass the base URL; sibling
.r1/.r2/… R-set companions are discovered on the same filesystem and mapped to overview levels. The parser builds a hierarchical store with GeoZarrmultiscalesmetadata describing the pyramid structure.
Note
virtualizarr is an optional dependency. Install with pip install osml-imagery-io[virtualizarr]
to enable parser support.
OversightMLParser¶
- class aws.osml.io.virtualizarr_parsers.OversightMLParser¶
Bases:
objectVirtualiZarr parser for any imagery format supported by IO.open().
Supports NITF (2.0, 2.1, NSIF 1.0, SICD, SIDD), standalone JPEG 2000 (.j2k, .jp2), TIFF, and GeoTIFF. Format detection is derived from the URL extension — the parser itself is format-agnostic.
Conforms to the VirtualiZarr parser protocol: an instance is a callable
(url: str, registry) -> ManifestStore. Bytes are read by opening url with fsspec and handing the seekable handle toIO.open(), which issues on-demand byte-range reads for the block-capable formats (TIFF, JPEG 2000, NITF, DTED) — so a local path and ans3://URL follow the same code path and neither downloads the whole file to build the index.Multi-file R-set pyramids are reconstructed automatically: given a base url, sibling
<base>.r1/.r2/ … files are discovered on the same filesystem and mapped to overview levels.Examples
Index a local file (chunk refs point at the same local path):
parser = OversightMLParser() manifest_store = parser("/data/image.ntf")
Index a remote file directly (range reads, no full download):
parser = OversightMLParser() manifest_store = parser("s3://bucket/image.ntf")
Multi-file pyramid (
image.ntf+image.ntf.r1auto-discovered):parser = OversightMLParser() manifest_store = parser("s3://bucket/image.ntf")
See
write_tile_index()for portable ({{base}}-template) and URL-rewritten output — relocating chunk references is a serialization-time concern, not a parse-time one.
Constructor¶
OversightMLParser() takes no parse-time configuration — the URL passed when
the parser is called is the single source of truth for both reading and chunk
references.
parser = OversightMLParser()
Calling the parser¶
parser(url, registry=None) reads and indexes the imagery at url. Local
paths, file:// URIs, and s3:// URIs all work (opened via fsspec). Chunk
references in the returned store point at url. R-set overview companions
(<url>.r1, <url>.r2, …) are discovered automatically.
# Local file (chunk refs point at the local path)
store = parser("/data/image.ntf")
# Remote file — range reads, no full download
store = parser("s3://bucket/image.ntf")
# Multi-file pyramid — image.ntf.r1 etc. auto-discovered from the base URL
store = parser("s3://bucket/image.ntf")
To relocate chunk references (portable {{base}} indexes, or index a local copy
but reference an s3:// location), use write_tile_index’s template_base /
url_overrides arguments — see below.
Flat vs hierarchical output¶
When the parser detects overview assets (keys matching image:N:overview:M),
it produces a hierarchical ManifestStore with one subgroup per resolution
level. Otherwise it produces a flat store with arrays at the root — identical
to the pre-multiscale behavior.
For hierarchical stores, each subgroup contains a single array named "data",
and the root group’s attributes include GeoZarr multiscales metadata and a
zarr_conventions array declaring convention identity:
ManifestGroup (root)
├── groups:
│ ├── "0" → ManifestGroup(arrays={"data": level_0_array})
│ ├── "1" → ManifestGroup(arrays={"data": level_1_array})
│ └── "2" → ManifestGroup(arrays={"data": level_2_array})
└── attributes:
├── "source": "s3://bucket/image.ntf"
├── "zarr_conventions": [{ ... }]
└── "multiscales": { ... }
multiscales metadata structure¶
The root group’s multiscales attribute conforms to the
GeoZarr multiscales convention
(UUID d35379db-88df-4056-af3a-620245f8e347). It contains:
layout — one entry per resolution level with an
assetpath matching the subgroup name, an optionalderived_fromreferencing the parent level, and atransformobject with relativescaleandtranslationarraysresampling_method — optional; recorded when a
downsampling_methodkeyword argument is provided to the parser
Scale transforms use relative factors between adjacent levels (not absolute from
level 0). The scale and translation arrays have two elements: [Y, X].
A zarr_conventions array in the root attributes declares convention identity:
{
"source": "s3://bucket/image.tif",
"zarr_conventions": [
{
"uuid": "d35379db-88df-4056-af3a-620245f8e347",
"schema_url": "https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json",
"spec_url": "https://github.com/zarr-conventions/multiscales/blob/v1/README.md",
"name": "multiscales",
"description": "Multiscale layout of zarr datasets"
}
],
"multiscales": {
"layout": [
{
"asset": "0",
"transform": {"scale": [1.0, 1.0], "translation": [0.0, 0.0]}
},
{
"asset": "1",
"derived_from": "0",
"transform": {"scale": [2.0, 2.0], "translation": [0.0, 0.0]}
}
],
"resampling_method": "average"
}
}
write_tile_index¶
- aws.osml.io.virtualizarr_parsers.write_tile_index(store, output, segments=None, *, template_base=None, url_overrides=None, zarr_format=2)¶
Write a tile index to JSON or Parquet with multi-range support.
This is the recommended way to serialize a
ManifestStoreproduced byOversightMLParser. It handles the multi-range reference entries that VirtualiZarr’s built-in serialization does not support.zarr_formatselects which Zarr version the index describes. Both are Kerchunk reference files served through fsspec — the difference is the store keys inside, and therefore which consumer path reads them:2(default) — a.zgroup/.zarray/.zattrslayout. Codecs resolve through the numcodecs registry byidand are called synchronously with a single buffer.3— a nativezarr.jsonlayout withc.<band>.<row>.<col>chunk keys. Codecs resolve by URI through thezarr.codecsentry points and are called by zarr’s asynchronous batched codec pipeline.
By default chunk references point at the URL the store was parsed from. Relocating those references is a serialization-time concern controlled here:
Portable / relocatable index — pass
template_base="{{base}}"to rewrite every chunk-reference URL to{{base}}<filename>and emit a Kerchunk v1"templates": {"base": ""}dict. At read time the base is supplied viatemplate_overrides={"base": "s3://bucket/path/"}toMultiReferenceFileSystem/ReferenceFileSystem.Explicit URL rewrite — pass
url_overrides={old_url: new_url}to remap concrete URLs (e.g. index by reading a local copy, then point the references at thes3://location the data will be served from).
template_baseandurl_overridesare mutually exclusive.- Parameters:
store (ManifestStore) – The manifest store returned by
OversightMLParser().output (
str) – Output file path. Extension determines format:.jsonfor Kerchunk JSON,.parquetfor a Kerchunk Parquet directory. Parquet requireszarr_format=2, cannot carry multi-range chunk references, and needspyarrow(theosml-imagery-io[zarr]extra) — see_emit_refs(). It must be read back withMultiReferenceFileSystem, which a stock fsspecReferenceFileSystemcannot do. Multi-resolution pyramids are supported in both formats.segments (
list[str] |None) – Subgroup keys to include (e.g.["0", "2"]). IfNone, all subgroups are included.template_base (
str|None) – When given (typically"{{base}}"), produces a portable index whose chunk-reference URLs are rewritten totemplate_base + basename.url_overrides (
dict[str,str] |None) – Explicit{concrete_url: replacement_url}rewrite applied to chunk references and the rootsourceattribute.zarr_format (
int) –2for the Kerchunk / Zarr v2 layout read through numcodecs,3for the native Zarr v3 layout read through thezarr.codecsentry-point pipeline.
- Raises:
ValueError – If the output extension is not
.jsonor.parquet, if a requested segment is not found, if bothtemplate_baseandurl_overridesare given, or if zarr_format is not 2 or 3.ImportError – If
.parquetoutput is requested withoutpyarrowinstalled.
Examples
- Return type:
Absolute URL index (references point at the parsed URL):
parser = OversightMLParser() store = parser("s3://my-bucket/imagery/image.ntf") write_tile_index(store, "image.tile_index.json")
Native Zarr v3 index (read with no Kerchunk-to-v2 translation):
parser = OversightMLParser() store = parser("s3://my-bucket/imagery/image.ntf") write_tile_index(store, "image.v3.json", zarr_format=3)
Portable index (resolve base URL at read time):
parser = OversightMLParser() store = parser("local/image.ntf") write_tile_index(store, "image.tile_index.json", template_base="{{base}}")
Index a local copy, reference the remote location:
parser = OversightMLParser() store = parser("local/image.ntf") write_tile_index( store, "image.tile_index.json", url_overrides={os.path.abspath("local/image.ntf"): "s3://bucket/image.ntf"}, )
write_tile_index() automatically detects whether the store is flat or
hierarchical and serializes accordingly. For hierarchical stores, the output
Kerchunk JSON uses path-prefixed keys (e.g. 0/data/0.0.0, 1/data/0.0.0)
and includes the root multiscales metadata in .zattrs.
Choosing a Zarr format¶
zarr_format selects which Zarr version the index describes. Both outputs are
Kerchunk reference files served through fsspec — no pixel data is copied either
way. What differs is the store keys inside, and therefore which consumer path
reads them:
|
|
|
|---|---|---|
Metadata keys |
|
|
Chunk keys |
|
|
Codec resolution |
numcodecs registry, by |
|
Codec call convention |
synchronous, single buffer |
async, batched pipeline |
Output formats |
|
|
parser = OversightMLParser()
store = parser("s3://bucket/image.ntf")
# Kerchunk / Zarr v2 (read through numcodecs)
write_tile_index(store, "image.tile_index.json")
# Native Zarr v3 (read through the entry-point codec pipeline)
write_tile_index(store, "image.v3.json", zarr_format=3)
Parquet output requires zarr_format=2, and requesting .parquet with
zarr_format=3 raises ValueError rather than writing an index nothing can read
back.
This is a deferral, not a structural limit. The Kerchunk Parquet container
indexes chunk references positionally, deriving each row from the array’s
chunk-grid shape and parsing the chunk key’s last segment as grid coordinates. A
v3 store carries everything that needs — zarr.json states shape and
chunk_grid.configuration.chunk_shape, and chunk_key_encoding names the chunk
key separator — just not under the v2 names. What is missing is read-side support:
deriving chunk counts from zarr.json rather than {field}/.zarray, parsing
c.<band>.<row>.<col> chunk keys whose leading c is not a coordinate, and
recognizing zarr.json as metadata. That is a bounded piece of work — a few more
overrides alongside the ones MultiReferenceFileSystem already carries — and is
simply not implemented.
Reading a Parquet index¶
Parquet indexes must be opened with MultiReferenceFileSystem.
A stock fsspec ReferenceFileSystem cannot open one, and multi-resolution
Parquet is fully supported:
from aws.osml.io.multi_reference_fs import MultiReferenceFileSystem
import zarr
fs = MultiReferenceFileSystem(fo="image.tile_index.parquet")
root = zarr.open_group(fs.get_mapper(""), mode="r", zarr_format=2)
full_res = root["0/data"] # level 0
half_res = root["1/data"] # level 1 — the pyramid survives the round-trip
Four properties of the Parquet container make this necessary, all handled by
MultiReferenceFileSystem:
Multi-range chunks need extra columns. Upstream’s record schema has only scalar
offsetandsizecolumns, so a chunk assembled from several non-contiguous fragments — every chunk, in RPCL JPEG 2000 imagery — has nowhere to go. This library appendsrange_path(the URL), plusoffsetsandsizesaslist<int64>columns, and leavespathnull on those rows. A reader that does not know the columns therefore fails on such a chunk rather than handing back one fragment of six as if it were the whole thing.MultiReferenceFileSystemdecodes them into the multi-range reference form and fetches the fragments — concurrently on the async path — joining them in the stored order.The record loader routes through pandas, and nulls decode inconsistently. fsspec loads each record with
pandas.read_parquet(...)and then immediately callsto_numpy()on every column — pandas decodes via pyarrow and is thrown away, which would makepandasa runtime requirement of merely reading an index. Worse, the writer’s engine is not recorded in the store whileLazyReferenceMapperdefaults tofastparqueton read, and the two engines decode nulls differently — enough to misread every chunk reference.MultiReferenceFileSystemloads withpyarrowdirectly and normalizes nulls, so this library never imports pandas and an index written by another tool withfastparquetreads correctly too.Nested group keys confuse the stock field listing. A hierarchical store has a
.zgroupper pyramid level, which fsspec’slistdir()mistakes for an array field and then fails to find a.zarrayfor.Templates cannot be stored. A Parquet store has nowhere to keep the Kerchunk
"templates"dict, so a portable index (template_base="{{base}}") resolves its placeholders fromtemplate_overridesalone. The substitution is applied torange_pathas well aspath, so multi-range chunks of a portable index resolve too:fs = MultiReferenceFileSystem( fo="image.tile_index.parquet", template_overrides={"base": "s3://bucket/path/"}, )
Reading and writing a Parquet index requires pyarrow, supplied by the zarr
extra:
pip install "osml-imagery-io[zarr]"
Without it, both paths raise an ImportError naming the package and this extra.
Note
One cosmetic consequence of leaving path null on multi-range rows: fsspec’s
ls() builds its chunk listing from rows with a non-null path, so multi-range
chunks do not appear in a listing of their array field. Reads are unaffected —
Zarr fetches chunks by key, and both consumer paths decode them correctly — but a
fs.ls("0/data") on an index of interleaved imagery will show the metadata keys
and no chunks. The JSON container does list them.
Relocating chunk references¶
Relocating chunk references is a serialization-time concern controlled by two mutually exclusive keyword arguments, and applies to both formats:
template_base— pass"{{base}}"to produce a portable index whose chunk-reference URLs are rewritten to{{base}}<filename>and emit a Kerchunk v1"templates": {"base": ""}dict. At read time the base is supplied viatemplate_overrides={"base": "s3://bucket/path/"}.url_overrides— an explicit{old_url: new_url}mapping, e.g. index a local copy and point the references at thes3://location the data will be served from.
# Portable index (resolve base URL at read time)
parser = OversightMLParser()
store = parser("local/image.ntf")
write_tile_index(store, "image.json", template_base="{{base}}")
# Index a local copy, reference the remote location
write_tile_index(
store, "image.json",
url_overrides={os.path.abspath("local/image.ntf"): "s3://bucket/image.ntf"},
)