Writing Imagery Assets¶
The Simple Path¶
For common write tasks — saving a NumPy array to a file with sensible defaults —
imsave handles format detection, compression, and blocking for you:
from aws.osml.io import imsave
import numpy as np
data = np.random.randint(0, 255, (3, 512, 512), dtype=np.uint8)
# Format inferred from extension, compression and block size auto-selected
imsave("output.tif", data) # GeoTIFF with Deflate compression, 256×256 tiles
imsave("output.ntf", data) # NITF with JPEG 2000 lossless, 1024×1024 blocks
imsave("output.png", data) # PNG
# 2D arrays are treated as single-band images
grayscale = np.zeros((256, 256), dtype=np.uint8)
imsave("gray.png", grayscale)
# Add georeferencing
imsave("geo.tif", data,
corners=[(-77.0, 39.0), (-76.5, 39.0), (-76.5, 38.5), (-77.0, 38.5)],
crs="EPSG:4326")
When you need full control — custom metadata, specific compression parameters, multi-asset datasets, COG overviews, R-set pyramids, or TRE preservation — the low-level write API described below gives you direct access to every encoding detail.
Choosing the Output Format¶
This library supports writing imagery in multiple formats. NITF 2.1, NSIF 1.0,
GeoTIFF, PNG, JPEG 2000, and JPEG are supported. You can either specify the format
explicitly or let IO.open auto-detect it from the file extension:
from aws.osml.io import IO
# Explicit format — always works
with IO.open(["output.ntf"], "w", "nitf") as writer:
...
# Auto-detected from extension — format parameter omitted
with IO.open(["output.ntf"], "w") as writer:
... # Detected as NITF from .ntf extension
with IO.open(["output.tif"], "w") as writer:
... # Detected as TIFF from .tif extension
When format is omitted, the extension of the first path determines the format. For
R-set paths like output.ntf.r1, the .rN suffix is stripped before detection so the
underlying .ntf extension is used. If the extension is not recognized, a ValueError
is raised — pass the format explicitly in that case.
When format is provided, it takes precedence over the file extension.
Accepted format strings and auto-detected extensions:
Format string |
Output |
Auto-detected extensions |
|---|---|---|
|
NITF 2.1 |
|
|
NSIF 1.0 |
|
|
GeoTIFF |
|
|
PNG |
|
|
JPEG 2000 |
|
|
JPEG |
|
For reading, IO.open auto-detects the format from the file extension or from magic
bytes in the file header. You can also override detection with the format parameter.
Metadata Controls Encoding¶
Metadata drives how the image is encoded. The BufferedMetadataProvider is a mutable,
in-memory key-value store that serves two roles:
Create metadata from scratch when building new images.
Copy and modify metadata from an existing image when transcoding or chipping.
The metadata values you set are highly dependent on the desired output format. A NITF
file needs fields like IC, IMODE, and COMRAT; a GeoTIFF file uses TIFF tags
and GeoKeys. The field names match what you see when reading files — no translation
layer sits between you and the format.
from aws.osml.io import BufferedMetadataProvider
# Create from scratch
metadata = BufferedMetadataProvider()
metadata["IMODE"] = "B"
metadata["IC"] = "NC"
# Copy from an existing provider and modify
copied = BufferedMetadataProvider(source=existing_provider)
copied["IC"] = "C8" # Switch to JPEG 2000
copied["COMRAT"] = "N001.0" # Lossless
# Query
value = metadata.get("IMODE") # "B" or None
all_pairs = metadata.entries()
filtered = metadata.entries("NPP") # {"NPPBH": "256", "NPPBV": "256"}
Basic Write Workflow¶
Create a BufferedImageAssetProvider with tiled assets and a BufferedMetadataProvider
with encoding hints, then write through the IO interface. This example shows a
GeoTIFF workflow with Deflate compression and UTM georeferencing. The TIFF writer
expects numeric tag IDs as metadata keys (per the TIFF 6.0 specification). Use
TagNameResolver for convenient name-based access. GeoModelType,
GeoRasterType, GeoProjectedCRS, GeoPixelScale, and GeoTiepoints are derived
from GeoTIFF GeoKeys and coordinate transformation tags:
from aws.osml.io import IO, BufferedImageAssetProvider, BufferedMetadataProvider, PixelType
from aws.osml.io.tiff.utils import TagNameResolver
import numpy as np
# Set up TIFF encoding hints using TagNameResolver for readable names
metadata = BufferedMetadataProvider()
tag_dict = metadata.entries()
resolver = TagNameResolver(tag_dict)
resolver["Compression"] = "Deflate" # Tag 259: Deflate compression (LZW, None also supported)
resolver["TileWidth"] = "256" # Tag 322: 256-pixel tile width
resolver["TileLength"] = "256" # Tag 323: 256-pixel tile height
resolver["Predictor"] = "Horizontal" # Tag 317: Horizontal differencing predictor
# Write resolved numeric keys back into the metadata provider
for key, value in tag_dict.items():
metadata[key] = value
# GeoTIFF coordinate reference system (EPSG:32618 = WGS 84 / UTM zone 18N)
metadata["GeoModelType"] = "Projected"
metadata["GeoRasterType"] = "PixelIsArea"
metadata["GeoProjectedCRS"] = "32618"
# Pixel-to-model coordinate transformation
# ModelPixelScaleTag: [scale_x, scale_y, scale_z] in CRS units (meters for UTM)
metadata["GeoPixelScale"] = "[0.5, 0.5, 0.0]"
# ModelTiepointTag: [pixel_x, pixel_y, pixel_z, geo_x, geo_y, geo_z]
metadata["GeoTiepoints"] = "[0, 0, 0, 300000.0, 4500000.0, 0.0]"
# Create a tiled image asset
image_data = np.random.randint(0, 255, (3, 512, 512), dtype=np.uint8)
provider = BufferedImageAssetProvider.create(
key="output_image",
num_columns=512,
num_rows=512,
num_bands=3,
block_width=256,
block_height=256,
pixel_type=PixelType.UInt8,
metadata=metadata,
)
provider.set_full_image(image_data)
# Write to disk
with IO.open(["output.tif"], "w", "geotiff") as writer:
writer.add_asset("image_0", provider,
title="Synthetic RGB Image",
description="UTM-referenced test image",
roles=["data"])
The same pattern applies to NITF — only the metadata field names change. Where
GeoTIFF uses Compression and TileWidth, NITF uses IC and NPPBH. See the
Format-Specific Encoding Options section below
for the full set of fields per format.
Copy-and-Modify Workflow¶
Read an existing file, modify metadata, and write a new file. This is the typical pattern for transcoding, chipping, or re-compressing imagery.
The writer needs two kinds of metadata:
File metadata (NITF only) — populates the file header (security markings, originator, etc.). Set this on the writer via
writer.metadata.Image metadata — controls per-image encoding (compression, blocking, GeoTIFF tags, etc.). Attach this to the
BufferedImageAssetProvider.
For TIFF, all IFD content (encoding hints and GeoTIFF tags) comes from the provider’s
metadata — writer.metadata is not used. For NITF, file-level metadata and image-level
metadata are separate. Both can be copied from the original file and selectively
overridden:
from aws.osml.io import IO, BufferedImageAssetProvider, BufferedMetadataProvider
with IO.open(["input.ntf"], "r") as reader:
image = reader.get_asset("image:0")
block = image.get_block(0, 0, resolution_level=0)
# Copy file-level metadata from the original dataset
file_metadata = BufferedMetadataProvider(source=reader.metadata)
# Copy image-level metadata and override encoding hints
image_metadata = BufferedMetadataProvider(source=image.metadata)
image_metadata["IC"] = "C8"
image_metadata["COMRAT"] = "00.5"
image_metadata["J2K_DECOMPOSITION_LEVELS"] = "5"
image_metadata["NPPBH"] = "1024"
image_metadata["NPPBV"] = "1024"
provider = BufferedImageAssetProvider.create(
key="compressed",
num_columns=image.num_columns,
num_rows=image.num_rows,
num_bands=image.num_bands,
pixel_type=image.pixel_value_type,
metadata=image_metadata,
)
provider.set_full_image(block)
with IO.open(["compressed.ntf"], "w", "nitf") as writer:
writer.metadata = file_metadata
writer.add_asset("image_0", provider,
title="Re-compressed image",
description="JPEG 2000 compressed copy",
roles=["data"])
Custom Python Image Providers¶
add_asset() also accepts plain Python objects that implement the image provider
interface via duck typing. This enables lazy per-block processing pipelines — your
get_block() is called during encoding, so you only need O(block_size) memory
instead of materializing the entire image up front.
import numpy as np
from aws.osml.io import IO, BufferedMetadataProvider
class InvertProvider:
"""Wraps a source image and inverts pixel values on the fly."""
def __init__(self, source, metadata):
self.key = source.key
self.title = source.title
self.description = source.description
self.num_rows = source.num_rows
self.num_columns = source.num_columns
self.num_bands = source.num_bands
self.num_bits_per_pixel = source.num_bits_per_pixel
self.actual_bits_per_pixel = source.actual_bits_per_pixel
self.pixel_value_type = source.pixel_value_type
self.num_pixels_per_block_horizontal = source.num_pixels_per_block_horizontal
self.num_pixels_per_block_vertical = source.num_pixels_per_block_vertical
self.num_resolution_levels = source.num_resolution_levels
self.pad_pixel_value = source.pad_pixel_value
self._source = source
self._metadata = metadata
def get_block(self, block_row, block_col, resolution_level, bands=None):
block = self._source.get_block(block_row, block_col, resolution_level)
return np.iinfo(block.dtype).max - block # invert
@property
def metadata(self):
return self._metadata
with IO.open(["input.ntf"], "r") as reader:
source = reader.get_asset("image:0")
# Copy source metadata and set encoding hints
metadata = BufferedMetadataProvider(source=source.metadata)
metadata["IC"] = "NC"
provider = InvertProvider(source, metadata)
with IO.open(["inverted.ntf"], "w", "nitf") as writer:
writer.add_asset("image:0", provider,
"Inverted", "Lazy inversion", ["data"])
A few things to keep in mind:
GIL on every block access. The adapter acquires the Python GIL each time it calls
get_block()orhas_block(). All other properties (num_rows,pixel_value_type, etc.) are cached in Rust at construction time and never re-acquire the GIL.Optional methods have safe defaults. If your object has no
has_blockmethod, all blocks are assumed present. If it has nometadataproperty, empty metadata is used.Required attributes. The object must have all 14 attributes:
key,title,description,get_block,num_rows,num_columns,num_bands,num_bits_per_pixel,actual_bits_per_pixel,pixel_value_type,num_pixels_per_block_horizontal,num_pixels_per_block_vertical,num_resolution_levels, andpad_pixel_value. A missing attribute produces aTypeError.Exception propagation. If
get_block()raises, the exception surfaces as aRuntimeErrorcontaining the original message.
Asset Roles and COG Writing¶
The roles parameter on add_asset() assigns semantic labels to each asset. Roles
affect how the writer encodes the asset — most importantly, the TIFF writer uses roles
to set NewSubfileType and control GeoTIFF tag propagation:
Assets with role
"data"getNewSubfileType = 0(full-resolution image) and GeoTIFF tags from the provider’s metadata are written to the IFD.Assets with role
"overview"getNewSubfileType = 1(reduced-resolution image) and GeoTIFF tags are suppressed on that IFD, per the OGC COG standard.
To write a COG with overviews, add the full-resolution image with role "data" and
each overview with role "overview":
from aws.osml.io import IO, BufferedImageAssetProvider, PixelType
import numpy as np
# Attach GeoTIFF tags to the full-res provider's metadata.
# Overview providers should NOT carry GeoTIFF tags — the writer
# suppresses them automatically for overview IFDs.
with IO.open(["output_cog.tif"], "w", "tiff") as writer:
# Full-resolution image (GeoTIFF tags come from full_res_provider.metadata)
writer.add_asset("image:0", full_res_provider,
title="Full Resolution", description="Primary image",
roles=["data"])
# First overview (2x downsampled)
writer.add_asset("image:0:overview:1", overview_1_provider,
title="Overview 1", description="2x reduced",
roles=["overview"])
# Second overview (4x downsampled)
writer.add_asset("image:0:overview:2", overview_2_provider,
title="Overview 2", description="4x reduced",
roles=["overview"])
If roles are empty but the key contains :overview:, the writer falls back to
inferring NewSubfileType = 1 from the key pattern. Explicit roles are preferred.
Writing Multi-File R-Set Pyramids¶
To write a multi-file R-set pyramid (separate files per resolution level), pass
multiple output paths to IO.open() — the same pattern used for reading. The first
path is the base file, and additional paths must follow the .rN naming convention:
from aws.osml.io import IO, BufferedImageAssetProvider, PixelType
import numpy as np
# Create providers for each resolution level
full_res = np.random.randint(0, 255, (3, 1024, 1024), dtype=np.uint8)
overview_1 = np.random.randint(0, 255, (3, 512, 512), dtype=np.uint8)
overview_2 = np.random.randint(0, 255, (3, 256, 256), dtype=np.uint8)
base_provider = BufferedImageAssetProvider.create(
key="image:0", num_columns=1024, num_rows=1024, num_bands=3,
block_width=256, block_height=256, pixel_type=PixelType.UInt8,
)
base_provider.set_full_image(full_res)
ovr1_provider = BufferedImageAssetProvider.create(
key="image:0", num_columns=512, num_rows=512, num_bands=3,
block_width=256, block_height=256, pixel_type=PixelType.UInt8,
)
ovr1_provider.set_full_image(overview_1)
ovr2_provider = BufferedImageAssetProvider.create(
key="image:0", num_columns=256, num_rows=256, num_bands=3,
block_width=256, block_height=256, pixel_type=PixelType.UInt8,
)
ovr2_provider.set_full_image(overview_2)
# Write — each overview is routed to its own file
with IO.open(
["output.ntf", "output.ntf.r1", "output.ntf.r2"], "w", "nitf"
) as writer:
writer.add_asset("image:0", base_provider,
title="Full Resolution", description="Base image",
roles=["data"])
writer.add_asset("image:0:overview:1", ovr1_provider,
title="Overview 1", description="2x reduced",
roles=["overview"])
writer.add_asset("image:0:overview:2", ovr2_provider,
title="Overview 2", description="4x reduced",
roles=["overview"])
The overview level in the asset key (image:0:overview:1) determines which output
file receives the data — it is matched to the .r1 suffix in the path list. The
key is re-mapped to image:0 before writing to the inner file, so each R-set file
contains a standard single-image dataset.
Reading the pyramid back uses the same multi-path pattern:
from aws.osml.io import IO, AssetType
with IO.open(
["output.ntf", "output.ntf.r1", "output.ntf.r2"], "r"
) as dataset:
for key in dataset.get_asset_keys(asset_type=AssetType.Image):
asset = dataset.get_asset(key)
print(f"{key}: {asset.num_columns}x{asset.num_rows}")
# image:0: 1024x1024
# image:0:overview:1: 512x512
# image:0:overview:2: 256x256
Some things to keep in mind:
All files in the R-set use the same format. The format is specified once and applied to every path.
Additional paths must have a
.rNsuffix. Paths without it raise aValueError.File-level metadata set via
writer.metadatais forwarded to all output files.The writer closes files in order: base first, then R-set files in ascending level.
Format-Specific Encoding Options¶
NITF¶
The NITF writer reads encoding hints from the asset’s metadata. The IC field selects
the compression algorithm and determines which other fields are relevant. The IMODE
field controls how bands are interleaved within each block.
Uncompressed (IC=NC, NM)¶
No compression is applied. NM enables the blocked mask table, allowing sparse images
where some blocks contain no data.
Field |
Values |
Description |
|---|---|---|
IC |
|
No compression / no compression with mask |
IMODE |
|
Band interleave mode |
NPPBH |
1–8192 |
Pixels per block horizontal |
NPPBV |
1–8192 |
Pixels per block vertical |
metadata = BufferedMetadataProvider()
metadata["IC"] = "NC"
metadata["IMODE"] = "B"
metadata["NPPBH"] = "256"
metadata["NPPBV"] = "256"
For sparse images, use NM and only set the blocks that contain data. Missing blocks
are treated as masked:
metadata = BufferedMetadataProvider()
metadata["IC"] = "NM"
provider = BufferedImageAssetProvider.create(
key="sparse",
num_columns=1024, num_rows=1024, num_bands=3,
block_width=256, block_height=256,
pixel_type=PixelType.UInt8,
metadata=metadata,
)
# Only populate blocks that have data
provider.set_block(0, 0, block_data)
provider.set_block(1, 2, block_data)
JPEG 2000 (IC=C8, M8, CD, MD)¶
JPEG 2000 compression. C8 is standard J2K, CD is HTJ2K (Part 15) for faster
encode/decode. M8 and MD are the masked variants that support sparse images.
Field |
Values |
Description |
|---|---|---|
IC |
|
J2K / J2K masked / HTJ2K / HTJ2K masked |
IMODE |
|
Band interleave mode. Must be |
NPPBH |
1–8192 |
Pixels per block horizontal |
NPPBV |
1–8192 |
Pixels per block vertical |
NBPP |
1–38 |
Number of bits per pixel. Must be 1–38 for JPEG 2000 per BPJ2K01.20 |
ABPP |
equals NBPP |
Actual bits per pixel. Must equal NBPP for JPEG 2000 per BPJ2K01.20 |
COMRAT |
|
Compression ratio indicator representing bits-per-pixel-per-band. |
JPEG 2000 Encoder Parameters (J2K_ fields)¶
The J2K_ prefixed metadata fields are unique: they do not get written into the NITF
image subheader or any TRE. They exist only as encoding hints that guide the JPEG 2000
compression algorithm itself. When you read a NITF file, you will not see these fields
in the metadata — they are write-only parameters consumed by the encoder and discarded
afterward.
Field |
Values |
Default |
Description |
|---|---|---|---|
J2K_DECOMPOSITION_LEVELS |
1–32 |
|
Wavelet decomposition levels (resolution levels) |
J2K_QUALITY_LAYERS |
1–255 |
|
Quality layers for progressive refinement |
HTJ2K mode (IC=CD or MD) is determined by the IC code — you do not set it
separately. Lossless vs lossy encoding and the compression ratio are derived from the
COMRAT field: Nnnn.n selects numerically lossless, Vnnn.n selects visually
lossless at the given bpp, and nn.n selects lossy at the target bpp.
# Lossless JPEG 2000
metadata = BufferedMetadataProvider()
metadata["IC"] = "C8"
metadata["IMODE"] = "B"
metadata["COMRAT"] = "N001.0"
metadata["J2K_DECOMPOSITION_LEVELS"] = "5"
metadata["NPPBH"] = "1024"
metadata["NPPBV"] = "1024"
# Lossy JPEG 2000 at ~1.0 bpp (approximately 8:1 compression)
metadata = BufferedMetadataProvider()
metadata["IC"] = "C8"
metadata["IMODE"] = "B"
metadata["COMRAT"] = "01.0"
metadata["J2K_DECOMPOSITION_LEVELS"] = "5"
metadata["J2K_QUALITY_LAYERS"] = "1"
metadata["NPPBH"] = "1024"
metadata["NPPBV"] = "1024"
# HTJ2K (faster encode/decode)
metadata = BufferedMetadataProvider()
metadata["IC"] = "CD"
metadata["IMODE"] = "B"
metadata["COMRAT"] = "01.0"
metadata["NPPBH"] = "1024"
metadata["NPPBV"] = "1024"
JPEG DCT (IC=C3, M3)¶
JPEG DCT compression. C3 is standard JPEG, M3 is the masked variant. I1 is
downsampled JPEG with a 2048×2048 dimension limit.
Field |
Values |
Description |
|---|---|---|
IC |
|
JPEG / JPEG masked / downsampled JPEG. |
IMODE |
|
Band interleave mode. |
NPPBH |
1–8192 |
Pixels per block horizontal |
NPPBV |
1–8192 |
Pixels per block vertical |
NBPP |
|
Number of bits per pixel. Must be 8 (8-bit pixels only) for JPEG DCT |
COMRAT |
|
Quality factor (default: 75.0) |
# JPEG at quality 85
metadata = BufferedMetadataProvider()
metadata["IC"] = "C3"
metadata["IMODE"] = "B"
metadata["COMRAT"] = "85.0"
metadata["NPPBH"] = "256"
metadata["NPPBV"] = "256"
GeoTIFF¶
The GeoTIFF writer reads encoding hints from the asset’s metadata using numeric TIFF
tag IDs as keys. Use TagNameResolver for convenient name-based access. The writer
supports uncompressed, LZW, Deflate, and PackBits compression.
TIFF Encoding Hints¶
Standard TIFF tags that control how the image is stored on disk:
Field |
Values |
Default |
Description |
|---|---|---|---|
Compression |
|
|
Compression algorithm |
TileWidth |
multiple of 16 |
|
Tile width in pixels |
TileHeight |
multiple of 16 |
|
Tile height in pixels |
Predictor |
|
|
Differencing predictor |
GeoTIFF Metadata¶
GeoKeys and coordinate transformation tags from the OGC GeoTIFF 1.1 standard. These control the georeferencing of the image:
Field |
Values |
Description |
|---|---|---|
GeoModelType |
|
GTModelTypeGeoKey — model coordinate type |
GeoRasterType |
|
GTRasterTypeGeoKey — raster space interpretation |
GeoProjectedCRS |
EPSG code (e.g. |
ProjectedCRSGeoKey — projected CRS |
GeoGeographicCRS |
EPSG code (e.g. |
GeodeticCRSGeoKey — geographic CRS |
GeoPixelScale |
|
ModelPixelScaleTag — pixel size in CRS units |
GeoTiepoints |
|
ModelTiepointTag — raster-to-model tie points |
GeoTransform |
|
6-element affine transform (GDAL convention) |
# Deflate-compressed GeoTIFF with UTM Zone 18N georeferencing
from aws.osml.io.tiff.utils import TagNameResolver
metadata = BufferedMetadataProvider()
tag_dict = metadata.entries()
resolver = TagNameResolver(tag_dict)
resolver["Compression"] = "Deflate" # Tag 259
resolver["TileWidth"] = "256" # Tag 322
resolver["TileLength"] = "256" # Tag 323
for key, value in tag_dict.items():
metadata[key] = value
metadata["GeoModelType"] = "Projected"
metadata["GeoRasterType"] = "PixelIsArea"
metadata["GeoProjectedCRS"] = "32618"
metadata["GeoPixelScale"] = "[0.5, 0.5, 0.0]"
metadata["GeoTiepoints"] = "[0, 0, 0, 300000.0, 4500000.0, 0.0]"
Example: NITF Chip with TRE Preservation¶
Create a NITF chip from an arbitrary pixel region — not just a single block — while carrying forward all metadata including TREs. The chip region may span multiple blocks, so you need to find the overlapping blocks, read each one, and assemble the relevant portions into a single output array. An ICHIPB TRE is added to record where the chip came from in the original image, which is required for downstream mensuration and geopositioning tools to work correctly.
import numpy as np
from aws.osml.io import IO, BufferedImageAssetProvider, BufferedMetadataProvider
# Define the chip region in pixel coordinates (column/row)
x_min, y_min = 100, 200 # top-left corner
x_max, y_max = 612, 456 # bottom-right corner (exclusive)
with IO.open(["input.ntf"], "r") as reader:
image = reader.get_asset("image:0")
# Get image and block dimensions
img_width = image.num_columns
img_height = image.num_rows
block_width = image.num_pixels_per_block_horizontal
block_height = image.num_pixels_per_block_vertical
# Handle non-blocked images (block size 0 means single block = full image)
if block_width == 0:
block_width = img_width
if block_height == 0:
block_height = img_height
# Clamp bounds to image dimensions
x_min = max(0, x_min)
y_min = max(0, y_min)
x_max = min(img_width, x_max)
y_max = min(img_height, y_max)
chip_width = x_max - x_min
chip_height = y_max - y_min
# Determine which blocks overlap the chip region
block_col_start = x_min // block_width
block_col_end = (x_max - 1) // block_width + 1
block_row_start = y_min // block_height
block_row_end = (y_max - 1) // block_height + 1
# Allocate the output chip array (bands, height, width)
dtype = np.dtype(image.pixel_value_type.to_numpy_dtype())
chip = np.zeros((image.num_bands, chip_height, chip_width), dtype=dtype)
# Read overlapping blocks and assemble the chip
for block_row in range(block_row_start, block_row_end):
for block_col in range(block_col_start, block_col_end):
if not image.has_block(block_row, block_col, resolution_level=0):
continue
block = image.get_block(block_row, block_col, resolution_level=0)
# Block's pixel coordinates in image space
bx = block_col * block_width
by = block_row * block_height
# Overlap between this block and the chip region
src_x0 = max(0, x_min - bx)
src_y0 = max(0, y_min - by)
src_x1 = min(block.shape[2], x_max - bx)
src_y1 = min(block.shape[1], y_max - by)
# Corresponding destination in the chip array
dst_x0 = max(0, bx - x_min)
dst_y0 = max(0, by - y_min)
dst_x1 = dst_x0 + (src_x1 - src_x0)
dst_y1 = dst_y0 + (src_y1 - src_y0)
chip[:, dst_y0:dst_y1, dst_x0:dst_x1] = \
block[:, src_y0:src_y1, src_x0:src_x1]
# Preserve file-level metadata (security markings, originator, etc.)
file_metadata = BufferedMetadataProvider(source=reader.metadata)
# Preserve image-level metadata (TREs, etc.) and set chip encoding
image_metadata = BufferedMetadataProvider(source=image.metadata)
image_metadata["IC"] = "NC"
image_metadata["IMODE"] = "B"
# Add ICHIPB TRE to record the chip's origin in the full image.
# This is required by STDI-0002 Vol 1 App B so that mensuration and
# geopositioning tools can map chip coordinates back to the original image.
#
# Grid point layout (row, col):
# (1,1) = top-left (1,2) = top-right
# (2,1) = bottom-left (2,2) = bottom-right
#
# OP_ fields are the output product (chip) corner coordinates.
# FI_ fields are the corresponding full image coordinates.
# The .500 offset places the coordinate at the center of the pixel (grid
# convention per ICHIPB spec Annex A).
image_metadata["ICHIPB"] = {
"XFRM_FLAG": "00", # Non-dewarped imagery
"SCALE_FACTOR": "0001.00000", # Full resolution (R0)
"ANAMRPH_CORR": "00", # No anamorphic correction
"SCANBLK_NUM": "00", # No scan blocks
# Output product corner coordinates (chip space)
"OP_ROW_11": "00000000.500", # top-left row
"OP_COL_11": "00000000.500", # top-left col
"OP_ROW_12": "00000000.500", # top-right row
"OP_COL_12": f"{chip_width - 1:08d}.500", # top-right col
"OP_ROW_21": f"{chip_height - 1:08d}.500", # bottom-left row
"OP_COL_21": "00000000.500", # bottom-left col
"OP_ROW_22": f"{chip_height - 1:08d}.500", # bottom-right row
"OP_COL_22": f"{chip_width - 1:08d}.500", # bottom-right col
# Full image corner coordinates (where the chip came from)
"FI_ROW_11": f"{y_min:08d}.500", # top-left row
"FI_COL_11": f"{x_min:08d}.500", # top-left col
"FI_ROW_12": f"{y_min:08d}.500", # top-right row
"FI_COL_12": f"{x_max - 1:08d}.500", # top-right col
"FI_ROW_21": f"{y_max - 1:08d}.500", # bottom-left row
"FI_COL_21": f"{x_min:08d}.500", # bottom-left col
"FI_ROW_22": f"{y_max - 1:08d}.500", # bottom-right row
"FI_COL_22": f"{x_max - 1:08d}.500", # bottom-right col
# Full image dimensions
"FI_ROW": f"{img_height:08d}",
"FI_COL": f"{img_width:08d}",
}
provider = BufferedImageAssetProvider.create(
key="chip",
num_columns=chip_width,
num_rows=chip_height,
num_bands=image.num_bands,
pixel_type=image.pixel_value_type,
metadata=image_metadata,
)
provider.set_full_image(chip)
with IO.open(["chip.ntf"], "w", "nitf") as writer:
writer.metadata = file_metadata
writer.add_asset("image_0", provider,
title="Chipped image",
description="Region chip with ICHIPB provenance",
roles=["data"])
The ICHIPB fields follow the STDI-0002 Volume 1, Appendix B specification. The key relationships:
Field group |
Purpose |
|---|---|
|
|
|
Scale relative to full resolution R0. |
|
The four corner grid points in the output chip’s coordinate space. |
|
The same four corners mapped to the original full image coordinate space. |
|
Total rows and columns of the original full image (the extent to which the SDEs apply). |
The .500 offset on all coordinates places the point at the center of the pixel,
following the ICHIPB grid convention (see Annex A of the ICHIPB spec). For a chip
starting at pixel (100, 200) in the full image, FI_COL_11 is
00000100.500 and FI_ROW_11 is 00000200.500.