Skip to content

feat(lif): extract objective lens metadata (magnification, NA, immersion) into audit + OME-XML #52

Description

@ferrinm

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

  • per_scene[i].objective populated on the reporter's LIF file (Stellaris confocal — objective info is present in that vintage of Leica XML).
  • OME-XML raw.lif.xml sibling contains <Instrument><Objective .../></Instrument> and the per-image <ObjectiveSettings ID=... /> reference.
  • Missing fields → key omitted from the audit dict (no null/0); missing objective entirely → objective key absent from per_scene[i].
  • AUDIT_SCHEMA_VERSION bumped to 7; audit.py docstring updated.
  • Tests: LIF fixture with a complete objective block (all fields populated), LIF fixture with a partial objective (nominal_magnification + NA only), LIF fixture with no objective info at all.
  • Post-conversion OME-NGFF validation still passes.
  • CHANGELOG entry under a minor version bump.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentTriaged and ready for an agent to implement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions