Confocal channel identity: implement ome_metadata and name channels by fluorophore#52
Confocal channel identity: implement ome_metadata and name channels by fluorophore#52aelefebv wants to merge 2 commits into
ome_metadata and name channels by fluorophore#52Conversation
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>
There was a problem hiding this comment.
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!
|
@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. |
What this does
Surfaces the per-channel identity that Leica confocal
.liffiles alreadycontain but that
bioio-lifwas discarding:Reader.ome_metadatais now implemented. It returns a validome_types.OMEfor the current scene, with aPixelsbuilt from the reader'sexisting dims/sizes/dtype and
Channels carryingName/Fluor= fluorophoreand excitation/emission wavelengths. Previously this inherited the base
reader's
raise NotImplementedError().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 bareLUTName(e.g.Green). Widefield naming isunchanged.
Closes #51.
Why
Confocal scenes have no
WideFieldChannelInfo, so they always fell into theelse:branch of_get_coords_and_physical_px_sizesand were named byChannelDescription/@LUTName— a display colormap name (Blue,Gray,Green, ...) with no acquisition meaning. The real fluorophore, detector, laserline, and emission band are all present in the same scene XML. And because
ome_metadatawas unimplemented, no OME-XML channel metadata was produced forLIF 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 prefixstripped) or
None. Used by the reader for naming.channels_to_ome_channels(...)→ome_typesChannels (the only place thatimports
ome_types). The extraction core is standard-library only.bioio_lif/reader.pyelse:branch:dye_name or LUTNameinstead ofLUTName.ome_metadataproperty and a small_ome_pixel_typedtype→OME helper.pyproject.toml— declare the now-directome-types>=0.3.4dependency(matches
bioio-czi; it was already present transitively viabioio-base).bioio_lif/tests/test_confocal_channels.pyplus a faithful7-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
ATLConfocalSettingDefinitionblocks underLDM_Block_Sequential / LDM_Block_Sequential_List(indexed by each channel'sSequentialSettingIndex), and deliberately ignoresLDM_Block_Sequential_Masterand the top-level
Attachment[HardwareSetting]copies (reference/duplicatesnapshots; the Master copy can carry a phantom laser line that excites nothing).
Within the real sequence:
ChannelProperty/DyeName(strip leading"Leica/"),DetectorName,SequentialSettingIndex.(
LaserLineSetting@IntensityDev > 0), ascending, pair to active detectors(
Detector@IsActive == "1",@Name → @Channel), ascending; a channel'sexcitation is the laser paired to its own physical detector channel.
round(LeftWorld)/round(RightWorld)of theMultiBandwhose@Channelmatches that physical channel. The OMEEmissionWavelengthis theband center.
Every field degrades to
None(orLUTNamefor 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
.liffilesaren't fetched.
Reader.ome_metadataon the 7-channel confocal scene serializes to:Covered: identity extraction,
DyeName-over-LUTNamepreference,"Leica/"prefix stripping, fail-soft on empty/missing structure, the
ome_typesprojection (fields present and unknowns omitted), dtype→PixelType mapping, and
the end-to-end
ome_metadatabuild (geometry +XYZCTdimension 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
LUT--ContrastingMethodName--FluoCubeName).DyeName:Green→ALEXA 488, etc. Confocal channels with noDyeNamestill fall backto
LUTName, so the old value is preserved where no better one exists.ome_metadatagoes from raisingNotImplementedErrorto returning a validOME— strictly additive for callers.ome-typeswas already an install-time dependency transitively (viabioio-base); this only makes the direct use explicit.