Package for creating typed Python models from Capella Single Look Complex (SLC) GeoTIFF images and JSON metadata.
Capella embeds product metadata in the TIFF tags of its SLC GeoTIFF files1; this package provides utilities for reading and working with that metadata based on the Capella Product Specification version 1.8. Note that the JSON can be parsed with Python's built-in json module; however, this package provides typed models with validation and descriptions for each field for easier integration with other SAR processing libraries.
Additionally, this package includes an ISCE3 adapter via the adapters.isce3 subpackage for working with the ISCE3 processing library.
Example datasets can be found on the Capella Open Data catalog.
pip install capella-readerfrom capella_reader import CapellaSLC
# Load a Capella SLC GeoTIFF
slc = CapellaSLC.from_file("tests/data/CAPELLA_C14_SM_SLC_HH_20240626150051_20240626150055.tif")
# Access metadata
print(slc.shape) # (rows, cols)
print(slc.meta.collect.platform) # 'capella-14'
print(slc.meta.collect.image.center_pixel.incidence_angle)You can also load metadata directly from extended metadata JSON files:
from pathlib import Path
from capella_reader import CapellaSLC, CapellaSLCMetadata
md_file = "CAPELLA_C13_SP_SLC_HH_20241126045307_20241126045346_extended.json"
slc = CapellaSLC.from_file(md_file)
# Or from a Python dict
import json
data = json.loads(Path(md_file).read_text())
meta = CapellaSLCMetadata.model_validate(data)
# Round-trip: dump back to JSON
json_str = meta.model_dump_json(indent=2)With the optional fsspec dependency, you can read directly from URLs:
pip install capella-reader[fsspec]
capella-reader bounds https://capella-open-data.s3.amazonaws.com/data/2025/5/6/CAPELLA_C13_SP_SLC_HH_20250506043806_20250506043816/CAPELLA_C13_SP_SLC_HH_20250506043806_20250506043816.tif
# -121.92123 37.27596 -121.81966 37.36195# Or from python:
slc = CapellaSLC.from_file("https://capella-open-data.s3.amazonaws.com/.../file.tif")The docs/examples/ directory contains visualization scripts demonstrating orbit and image footprint analysis:
# Combined map with orbit track and image footprint
python examples/orbit_footprint_map.pySee docs/examples/README.md for installation requirements and details.
For users working with ISCE3, conversion utilities are available in the adapters.isce3 subpackage:
from capella_reader import CapellaSLC
from capella_reader import adapters
slc = CapellaSLC.from_file("path/to/slc.tif")
# Convert to ISCE3 data structures
radar_grid = adapters.isce3.get_radar_grid(slc)
orbit = adapters.isce3.get_orbit(slc)
doppler_poly = adapters.isce3.get_doppler_poly(slc)
doppler_lut = adapters.isce3.get_doppler_lut2d(slc)These utilities require ISCE3 to be installed separately. They are not required for basic metadata parsing.
CapellaSLC(
path=PosixPath('tests/data/CAPELLA_C14_SM_SLC_HH_20240626150051_20240626150055.tif'),
meta=CapellaSLCMetadata(
software_version='3.2.1',
software_revision='f8884c31ba9ba129f4d838d6d02dea2c45a114ba-dirty',
processing_time=datetime.datetime(2025, 5, 8, 17, 38, 5, 669807, tzinfo=TzInfo(UTC)),
processing_deployment='production',
product_version='1.10',
product_type='SLC',
collect=Collect(
start_timestamp=datetime.datetime(2024, 6, 26, 15, 0, 51, 295975, tzinfo=TzInfo(UTC)),
stop_timestamp=datetime.datetime(2024, 6, 26, 15, 0, 55, 793597, tzinfo=TzInfo(UTC)),
local_datetime=datetime.datetime(2024, 6, 26, 9, 0, 53, 544786, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=64800))),
local_timezone='America/Mexico_City',
platform='capella-14',
mode='stripmap',
collect_id='0141157d-3cef-4db0-9239-66c8478e09e6',
image=ImageMetadata(
data_type='CInt16',
length=20395.09163067352,
width=10071.72523110632,
rows=19191,
columns=10631,
pixel_spacing_row=1.0627985769047086,
pixel_spacing_column=0.9474459172784211,
algorithm='backprojection',
scale_factor=0.004915546693498318,
range_autofocus=None,
azimuth_autofocus=None,
range_window=Window(name='rectangular', parameters={}, broadening_factor=0.8844848400382688),
processed_range_bandwidth=200000000.0,
azimuth_window=Window(name='antenna-taper', parameters={'proc_beamwidth': 0.011835009078692163}, broadening_factor=0.9743063402100489),
processed_azimuth_bandwidth=5022.581708747391,
image_geometry=ImageGeometry(
type='slant_plane',
doppler_centroid_polynomial=Poly2D(degree=(3, 3), coefficients=array([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])),
first_line_time=datetime.datetime(2024, 6, 26, 15, 0, 51, 889124, tzinfo=TzInfo(UTC)),
delta_line_time=0.00016105733333333333,
range_to_first_sample=761275.5576171188,
delta_range_sample=0.6171875
),
center_pixel=CenterPixel(
incidence_angle=40.93041272468091,
look_angle=36.797539101417286,
squint_angle=-0.05586304095603156,
layover_angle=0.05014323818914027,
target_position=ECEFPosition(x=-940324.8666604777, y=-5946543.280362384, z=2098893.4900432685),
center_time=datetime.datetime(2024, 6, 26, 15, 0, 53, 544786, tzinfo=TzInfo(UTC))
),
range_resolution=0.6629047106470235,
ground_range_resolution=1.011849005778887,
azimuth_resolution=1.280051064876099,
ground_azimuth_resolution=1.2800517167779046,
azimuth_looks=1.0,
range_looks=1.0,
enl=1.0,
reference_antenna_position=ECEFPosition(x=-684525.5142771972, y=-6655343.817559726, z=1969607.6195497077),
reference_target_position=ECEFPosition(x=-942416.8362992472, y=-5947750.607837195, z=2101298.743469007),
azimuth_beam_pattern_corrected=True,
elevation_beam_pattern_corrected=True,
radiometry='beta_nought',
calibration='full',
calibration_id='calibration_bundle/61b7b4ca-81a8-45e2-932e-11e1489c02c6',
nesz_polynomial=Poly1D(degree=3, coefficients=array([ 2.10754181e+05, -5.51826980e-01, 3.61176889e-07, 0.00000000e+00])),
nesz_peak=-24.100213130335916,
terrain_models=TerrainModels(
focusing=TerrainModelRef(link='ExplicitInflatedWGS84[2234.0048828125]+https://en.wikipedia.org/wiki/World_Geodetic_System', name='ExplicitInflatedWGS84[2234.0048828125]')
),
reference_doppler_centroid=0.0,
frequency_doppler_centroid_polynomial=Poly2D(
degree=(3, 3),
coefficients=array([[-1.44553946e-20, -7.48708712e-15, -2.85415191e-09,
2.73100984e-15],
[-1.74404613e-18, 3.05520758e-17, 1.18415007e-11,
-7.73815716e-18],
[ 4.66622572e-23, 2.51364523e-17, 1.01345640e-11,
-2.31147587e-17],
[-3.83941864e-23, -1.36857307e-17, -2.95147334e-12,
5.47845610e-18]])
),
quantization=Quantization(type='block_adaptive_quantization', block_sample_size=64, mean_bits=0, std_bits=0, sample_bits=6)
),
radar=Radar(
rank=50,
center_frequency=9649999872.0,
pointing=<LookSide.LEFT: 'left'>,
sampling_frequency=750000000.0,
transmit_polarization='H',
receive_polarization='H',
time_varying_parameters=[
RadarTimeVaryingParams(
start_timestamps=[datetime.datetime(2024, 6, 26, 15, 0, 51, 301024, tzinfo=TzInfo(UTC))],
prf=9901.251518191899,
pulse_bandwidth=200000000.0,
pulse_duration=2.0197333333333334e-05,
rank=50
),
RadarTimeVaryingParams(
start_timestamps=[
datetime.datetime(2024, 6, 26, 15, 0, 51, 801024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 52, 301024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 52, 801024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 53, 301024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 53, 801024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 54, 301024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 54, 801024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 55, 301024, tzinfo=TzInfo(UTC))
],
prf=9900.72869363185,
pulse_bandwidth=200000000.0,
pulse_duration=2.0197333333333334e-05,
rank=50
)
],
prf=[
PRFEntry(start_timestamps=[datetime.datetime(2024, 6, 26, 15, 0, 51, 301024, tzinfo=TzInfo(UTC))], prf=9901.251518191899),
PRFEntry(
start_timestamps=[
datetime.datetime(2024, 6, 26, 15, 0, 51, 801024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 52, 301024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 52, 801024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 53, 301024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 53, 801024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 54, 301024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 54, 801024, tzinfo=TzInfo(UTC)),
datetime.datetime(2024, 6, 26, 15, 0, 55, 301024, tzinfo=TzInfo(UTC))
],
prf=9900.72869363185
)
]
),
state=State(
coordinate_system=CoordinateSystem(type='ecef'),
direction='ascending',
state_vectors=[
StateVector(
time=datetime.datetime(2024, 6, 26, 15, 0, 21, 199958, tzinfo=TzInfo(UTC)),
position=ECEFPosition(x=-850598.581307973, y=-6682238.839189964, z=1807837.287228866),
velocity=ECEFVelocity(vx=5117.104280428376, vy=717.1797056134662, vz=5036.528855949608)
),
StateVector(
time=datetime.datetime(2024, 6, 26, 15, 0, 21, 799957, tzinfo=TzInfo(UTC)),
position=ECEFPosition(x=-847528.1256471712, y=-6681807.258344243, z=1810858.8158753526),
velocity=ECEFVelocity(vx=5117.763610991626, vy=721.4251640291432, vz=5035.248748046506)
),
...
StateVector(
time=datetime.datetime(2024, 6, 26, 15, 1, 24, 799857, tzinfo=TzInfo(UTC)),
position=ECEFPosition(x=-523139.33015611547, y=-6622363.191185433, z=2123595.7839055755),
velocity=ECEFVelocity(vx=5177.000593219978, vy=1164.868056941853, vz=4889.0522055202055)
),
StateVector(
time=datetime.datetime(2024, 6, 26, 15, 1, 25, 399856, tzinfo=TzInfo(UTC)),
position=ECEFPosition(x=-520032.9939516515, y=-6621663.012097265, z=2126528.759803662),
velocity=ECEFVelocity(vx=5177.469063184616, vy=1169.066041486259, vz=4887.548741599158)
)
],
source='precise_determination'
),
pointing=[
PointingSample(
time=datetime.datetime(2024, 6, 26, 15, 0, 21, 199958, tzinfo=TzInfo(UTC)),
attitude=AttitudeQuaternion(q0=0.7514354558972666, q1=0.539089174592113, q2=0.33782447576219665, q3=0.17493464855872806)
),
PointingSample(
time=datetime.datetime(2024, 6, 26, 15, 0, 21, 799957, tzinfo=TzInfo(UTC)),
attitude=AttitudeQuaternion(q0=0.7513778110356235, q1=0.5391998219192001, q2=0.3379087735187681, q3=0.1746782481659656)
),
...
PointingSample(
time=datetime.datetime(2024, 6, 26, 15, 1, 25, 399856, tzinfo=TzInfo(UTC)),
attitude=AttitudeQuaternion(q0=0.7464363921729759, q1=0.549879633720447, q2=0.3455829675461274, q3=0.14504242371384654)
)
],
transmit_antenna=Antenna(
azimuth_beamwidth=0.01211729820681252,
elevation_beamwidth=0.01219859298030783,
gain=49.58,
beam_pattern=Poly2D(
degree=(7, 7),
coefficients=array([[ 4.96288054e+01, 3.54236916e-04, -8.23751862e+04,
-5.46972016e+00, -3.85558272e+07, 3.86615092e+04,
5.23604124e+11, 2.89923661e+08],
[-3.48671932e-04, 2.20787614e+00, -5.08022213e+00,
-9.12656092e+03, 6.91149686e+04, 5.63776331e+06,
...
0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00],
[-7.85159183e+07, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00]])
)
),
receive_antenna=Antenna(
azimuth_beamwidth=0.01211729820681252,
elevation_beamwidth=0.01219859298030783,
gain=49.58,
beam_pattern=Poly2D(
degree=(7, 7),
coefficients=array([[ 4.96288054e+01, 3.54236916e-04, -8.23751862e+04,
-5.46972016e+00, -3.85558272e+07, 3.86615092e+04,
5.23604124e+11, 2.89923661e+08],
[-3.48671932e-04, 2.20787614e+00, -5.08022213e+00,
-9.12656092e+03, 6.91149686e+04, 5.63776331e+06,
...
[-7.85159183e+07, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00]])
)
)
)
)
)Using pixi for pacakage management and task running:
# after installing pixi: curl -fsSL https://pixi.sh/install.sh | sh
pixi installTo create a shell for development
pixi shell -e test
pre-commit installYou can see all pixi tasks with:
pixi task listDownload test data (required for integration tests):
pixi run download-test-data # Get the sample data images
pixi run test # Run all tests (requires test data)Run linting and formatting:
pixi run --environment test check
pixi run --environment test testCapella Product Specification version 1.8
See LICENSE.txt