What to build
Parse per-scene objective lens metadata from the LIF XML and surface it in two places: the zarrmony audit record (queryable) and the OME-XML <Instrument>/<Objective> element (standards-compliant, consumed by napari, OMERO, etc.).
Motivation
Zarrmony currently records nothing about the objective used to acquire the image. Physical pixel size lands in coordinateTransformations, but users routinely need to know "what objective was this taken with" for downstream analysis (choice of PSF, deconvolution kernel, dye/lens compatibility, publishing figure legends). LIF stores this in the scene XML; extraction is a pure XML walk with no data-plane cost.
Fields to extract
At minimum, per scene:
nominal_magnification (e.g., 10, 20, 40, 63) — LIF <ATLConfocalSettingDefinition ObjectiveMag=...> or <Objective NumericalAperture=...> sibling
numerical_aperture (e.g., 1.4, 0.8)
immersion (Air / Water / Oil / Glycerol / Multi / Other) — mapped to the OME Immersion enum
model (freeform, e.g., "HC PL APO CS2 63x/1.40 OIL")
working_distance_um (optional, when present)
Some fields are per-<ATLConfocalSettingDefinition>, some per-<Objective> — walk both and merge.
Where the values land
Audit (per_scene[i].objective):
{
"objective": {
"nominal_magnification": 63,
"numerical_aperture": 1.4,
"immersion": "Oil",
"model": "HC PL APO CS2 63x/1.40 OIL",
"working_distance_um": 0.14
}
}
Missing scene entirely if no objective info is extractable — no dummy zeros.
OME-XML: populate the top-level <Instrument> element with an <Objective> child, and set <Image>/<ObjectiveSettings> to reference it. Use ome_types.model.Instrument / Objective / ObjectiveSettings.
Implementation notes
- Add
extract_objective(scene_xml) to src/zarrmony/metadata/ (new file objective.py or extension of lif_channels.py; either is fine — pick whichever keeps the module small).
- Call site:
_lif_scene_channels already walks the scene XML; extract objective alongside channels so we only parse once. Thread the returned dict through to the audit record and to _lif_ome_image for OME-XML injection.
- Fail-safe like the channel extraction: any parse failure returns
None and conversion continues without objective metadata (never crash over metadata).
- Bump
AUDIT_SCHEMA_VERSION to 7 (new objective key under per_scene).
Acceptance criteria
Follow-up issues (linked separately)
Objective metadata for CZI, ND2, and the default (OME-TIFF etc.) plugin paths are separate issues so those readers can be tackled independently and this one can ship as soon as LIF works.
Blocked by
None — can start immediately.
What to build
Parse per-scene objective lens metadata from the LIF XML and surface it in two places: the zarrmony audit record (queryable) and the OME-XML
<Instrument>/<Objective>element (standards-compliant, consumed by napari, OMERO, etc.).Motivation
Zarrmony currently records nothing about the objective used to acquire the image. Physical pixel size lands in
coordinateTransformations, but users routinely need to know "what objective was this taken with" for downstream analysis (choice of PSF, deconvolution kernel, dye/lens compatibility, publishing figure legends). LIF stores this in the scene XML; extraction is a pure XML walk with no data-plane cost.Fields to extract
At minimum, per scene:
nominal_magnification(e.g., 10, 20, 40, 63) — LIF<ATLConfocalSettingDefinition ObjectiveMag=...>or<Objective NumericalAperture=...>siblingnumerical_aperture(e.g., 1.4, 0.8)immersion(Air/Water/Oil/Glycerol/Multi/Other) — mapped to the OMEImmersionenummodel(freeform, e.g.,"HC PL APO CS2 63x/1.40 OIL")working_distance_um(optional, when present)Some fields are per-
<ATLConfocalSettingDefinition>, some per-<Objective>— walk both and merge.Where the values land
Audit (
per_scene[i].objective):{ "objective": { "nominal_magnification": 63, "numerical_aperture": 1.4, "immersion": "Oil", "model": "HC PL APO CS2 63x/1.40 OIL", "working_distance_um": 0.14 } }Missing scene entirely if no objective info is extractable — no dummy zeros.
OME-XML: populate the top-level
<Instrument>element with an<Objective>child, and set<Image>/<ObjectiveSettings>to reference it. Useome_types.model.Instrument/Objective/ObjectiveSettings.Implementation notes
extract_objective(scene_xml)tosrc/zarrmony/metadata/(new fileobjective.pyor extension oflif_channels.py; either is fine — pick whichever keeps the module small)._lif_scene_channelsalready walks the scene XML; extract objective alongside channels so we only parse once. Thread the returned dict through to the audit record and to_lif_ome_imagefor OME-XML injection.Noneand conversion continues without objective metadata (never crash over metadata).AUDIT_SCHEMA_VERSIONto 7 (newobjectivekey underper_scene).Acceptance criteria
per_scene[i].objectivepopulated on the reporter's LIF file (Stellaris confocal — objective info is present in that vintage of Leica XML).raw.lif.xmlsibling contains<Instrument><Objective .../></Instrument>and the per-image<ObjectiveSettings ID=... />reference.null/0); missing objective entirely →objectivekey absent fromper_scene[i].AUDIT_SCHEMA_VERSIONbumped to 7; audit.py docstring updated.Follow-up issues (linked separately)
Objective metadata for CZI, ND2, and the default (OME-TIFF etc.) plugin paths are separate issues so those readers can be tackled independently and this one can ship as soon as LIF works.
Blocked by
None — can start immediately.