Skip to content

Confocal channel identity: implement ome_metadata and name channels by fluorophore#52

Open
aelefebv wants to merge 2 commits into
bioio-devs:mainfrom
aelefebv:feat/ome-metadata-confocal-channels
Open

Confocal channel identity: implement ome_metadata and name channels by fluorophore#52
aelefebv wants to merge 2 commits into
bioio-devs:mainfrom
aelefebv:feat/ome-metadata-confocal-channels

Conversation

@aelefebv

Copy link
Copy Markdown

What this does

Surfaces the per-channel identity that Leica confocal .lif files already
contain but that bioio-lif was discarding:

  1. Reader.ome_metadata is now implemented. It returns a valid
    ome_types.OME for the current scene, with a Pixels built from the reader's
    existing dims/sizes/dtype and Channels carrying Name/Fluor = fluorophore
    and excitation/emission wavelengths. Previously this inherited the base
    reader's raise NotImplementedError().

  2. Confocal channels are named by fluorophore, not display LUT color. The
    coordinate channel names now prefer each channel's DyeName (e.g.
    ALEXA 594) over the bare LUTName (e.g. Green). Widefield naming is
    unchanged.

Closes #51.

Why

Confocal scenes have no WideFieldChannelInfo, so they always fell into the
else: branch of _get_coords_and_physical_px_sizes and were named by
ChannelDescription/@LUTName — a display colormap name (Blue, Gray,
Green, ...) with no acquisition meaning. The real fluorophore, detector, laser
line, and emission band are all present in the same scene XML. And because
ome_metadata was unimplemented, no OME-XML channel metadata was produced for
LIF at all. See the linked issue for the full evidence.

Changes

  • bioio_lif/lif_metadata.py (new) — a small, self-contained helper:
    • extract_channels(scene_root) → one identity dict per channel
      (index, dye, fluor, detector, excitation_nm, emission_low_nm,
      emission_high_nm), in acquisition order.
    • channel_dye_name(channel_desc) → the channel's fluorophore (vendor prefix
      stripped) or None. Used by the reader for naming.
    • channels_to_ome_channels(...)ome_types Channels (the only place that
      imports ome_types). The extraction core is standard-library only.
  • bioio_lif/reader.py
    • Confocal else: branch: dye_name or LUTName instead of LUTName.
    • New ome_metadata property and a small _ome_pixel_type dtype→OME helper.
  • pyproject.toml — declare the now-direct ome-types>=0.3.4 dependency
    (matches bioio-czi; it was already present transitively via bioio-base).
  • Testsbioio_lif/tests/test_confocal_channels.py plus a faithful
    7-channel confocal scene fixture at bioio_lif/tests/data/confocal_7ch_scene.xml.

The algorithm (the join)

A Leica confocal scene stores several copies of its instrument settings; only one
is the genuine acquisition. The helper reads only the
ATLConfocalSettingDefinition blocks under
LDM_Block_Sequential / LDM_Block_Sequential_List (indexed by each channel's
SequentialSettingIndex), and deliberately ignores LDM_Block_Sequential_Master
and the top-level Attachment[HardwareSetting] copies (reference/duplicate
snapshots; the Master copy can carry a phantom laser line that excites nothing).

Within the real sequence:

  • IdentityChannelProperty/DyeName (strip leading "Leica/"),
    DetectorName, SequentialSettingIndex.
  • Excitation — spectral pairing: active laser lines
    (LaserLineSetting@IntensityDev > 0), ascending, pair to active detectors
    (Detector@IsActive == "1", @Name → @Channel), ascending; a channel's
    excitation is the laser paired to its own physical detector channel.
  • Emissionround(LeftWorld)/round(RightWorld) of the MultiBand whose
    @Channel matches that physical channel. The OME EmissionWavelength is the
    band center.

Every field degrades to None (or LUTName for the name) rather than raising,
so unusual or partial scenes never break the reader.

Test evidence

The unit tests run without the git-LFS sample binaries (they parse the XML
fixture), so they exercise this logic in CI even where the large .lif files
aren't fetched.

Reader.ome_metadata on the 7-channel confocal scene serializes to:

<Channel ID="Channel:0:0" Name="DAPI (dsDNA bound)" ExcitationWavelength="405.0" ExcitationWavelengthUnit="nm" EmissionWavelength="464.0" EmissionWavelengthUnit="nm" Fluor="DAPI (dsDNA bound)"/>
<Channel ID="Channel:0:1" Name="ALEXA 594" ExcitationWavelength="590.0" ExcitationWavelengthUnit="nm" EmissionWavelength="620.0" EmissionWavelengthUnit="nm" Fluor="ALEXA 594"/>
<Channel ID="Channel:0:2" Name="ALEXA 750" ExcitationWavelength="753.0" ExcitationWavelengthUnit="nm" EmissionWavelength="798.0" EmissionWavelengthUnit="nm" Fluor="ALEXA 750"/>
<Channel ID="Channel:0:3" Name="ALEXA 488" ExcitationWavelength="499.0" ExcitationWavelengthUnit="nm" EmissionWavelength="527.0" EmissionWavelengthUnit="nm" Fluor="ALEXA 488"/>
<Channel ID="Channel:0:4" Name="ALEXA 647-R-PE" ExcitationWavelength="653.0" ExcitationWavelengthUnit="nm" EmissionWavelength="676.0" EmissionWavelengthUnit="nm" Fluor="ALEXA 647-R-PE"/>
<Channel ID="Channel:0:5" Name="ALEXA 555" ExcitationWavelength="553.0" ExcitationWavelengthUnit="nm" EmissionWavelength="581.0" EmissionWavelengthUnit="nm" Fluor="ALEXA 555"/>
<Channel ID="Channel:0:6" Name="ALEXA 700" ExcitationWavelength="696.0" ExcitationWavelengthUnit="nm" EmissionWavelength="728.0" EmissionWavelengthUnit="nm" Fluor="ALEXA 700"/>
$ pytest bioio_lif/tests/test_confocal_channels.py
......... 9 passed

Covered: identity extraction, DyeName-over-LUTName preference, "Leica/"
prefix stripping, fail-soft on empty/missing structure, the ome_types
projection (fields present and unknowns omitted), dtype→PixelType mapping, and
the end-to-end ome_metadata build (geometry + XYZCT dimension order +
channels + valid OME-XML round-trip).

Lint/format/type all pass at the versions pinned in .pre-commit-config.yaml
(black 23.3.0, isort 5.12.0, flake8 6.0.0 + plugins, mypy 1.4.1).

Backward compatibility

  • Widefield naming is unchanged (LUT--ContrastingMethodName--FluoCubeName).
  • Confocal naming changes by design for channels that record a DyeName:
    GreenALEXA 488, etc. Confocal channels with no DyeName still fall back
    to LUTName, so the old value is preserved where no better one exists.

    Note: the existing tiled.lif parametrization in test_reader.py asserts
    confocal names ["Gray", "Red", "Green", "Cyan"]. If that sample records
    DyeNames, those expected values should be updated to the fluorophores; if
    it records none, the names are unchanged. (That sample is a git-LFS binary
    not available in this environment, so I couldn't confirm which — flagging it
    for a maintainer with LFS access.)

  • ome_metadata goes from raising NotImplementedError to returning a valid
    OME — strictly additive for callers.
  • ome-types was already an install-time dependency transitively (via
    bioio-base); this only makes the direct use explicit.

Leica confocal .lif files carry per-channel identity (fluorophore, detector,
laser line, emission band) in their scene XML, but bioio-lif discarded it:
confocal channels were named by the bare display LUT color
(ChannelDescription/@LUTName, e.g. "Green") and ome_metadata inherited the base
reader's NotImplementedError, so no OME-XML channel metadata was produced.

- Add bioio_lif/lif_metadata.py: a self-contained, stdlib-only extractor.
  extract_channels() resolves each channel's dye/detector/excitation/emission
  from the genuine acquisition sequence (the ATLConfocalSettingDefinition blocks
  under LDM_Block_Sequential_List, indexed by SequentialSettingIndex), ignoring
  the LDM_Block_Sequential_Master and Attachment[HardwareSetting] decoy copies.
  Excitation is recovered by spectral pairing of active laser lines to active
  detectors; emission from the matching MultiBand. channels_to_ome_channels()
  is the only piece that imports ome_types.
- reader.py: confocal channel naming now prefers DyeName (vendor prefix
  stripped) over LUTName, falling back to LUTName when no dye is recorded;
  widefield naming is unchanged. Implement Reader.ome_metadata, returning a
  valid ome_types.OME whose Pixels uses the reader's dims/sizes/dtype and whose
  Channels carry name/fluor + excitation/emission wavelengths.
- pyproject.toml: declare the now-direct ome-types dependency (already present
  transitively via bioio-base).
- Add a data-light unit test plus a faithful 7-channel confocal scene XML
  fixture, so this logic is covered without the git-LFS sample binaries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lint CI job runs mypy via pre-commit over all files, including tests,
which flagged two errors in test_confocal_channels.py:

- extract_channels() already handles None but was typed as requiring an
  ET.Element. Widen the parameter to Optional[ET.Element] so the type
  matches the behavior.
- the ome_metadata test stub was a standalone class, so the property
  getter's `self: Reader` argument was invalid. Make _StubReader subclass
  Reader and override the properties the getter reads with correctly typed
  values.

No `# type: ignore` or `cast`; reader.py unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@BrianWhitneyAI BrianWhitneyAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @aelefebv thank you for opening a PR! we are still learning how to incorporate generated code into the bioio ecosystem. In order to get this PR off the ground we should start by getting the CI passing with linting. Then I would go through and match the style of docstrings to the rest of the repository and make sure that they are understandable to humans who stumble onto them for the first time. I have some concerns about the architecture here that we can dig into after we get to that point!

@aelefebv

Copy link
Copy Markdown
Author

@BrianWhitneyAI Totally makes sense! Let me get the CI figured out and see if the architecture can't get cleaned up a bit to be more conformant. One question, are you against adding ome-types as a dependency?

@BrianWhitneyAI

Copy link
Copy Markdown
Contributor

@BrianWhitneyAI Totally makes sense! Let me get the CI figured out and see if the architecture can't get cleaned up a bit to be more conformant. One question, are you against adding ome-types as a dependency?

No not really if its already being pulled in by base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Confocal channel metadata is dropped: channels named by display LUT color, and ome_metadata is not implemented

2 participants