fix Nd2 wellinfo missing#4448
Conversation
- Add position name extraction from binary ND2 blocks (pPosName/sPosition/dPosName) - Use position names for series image names and series metadata - Move per-series keys (position X/Y/Z, position name) out of global metadata - Fix setSeries() call placement to ensure correct series context when reading timestamps - Remove duplicate global metadata entries for X/Y/Z positions Signed-off-by: Maarten Paul <maarten.paul@gmail.com>
For ND2 files with a TimeLoop → XYPosLoop structure, the binary CLX metadata contains empty-string dPosName placeholder entries (one per position per outer loop level) followed by the actual position name strings. Previously all entries were added to posNames, misaligning the real names (at indices 24-47) with the series (indices 0-23). Fix: skip empty strings when populating posNames from binary metadata. Also capture dPosName as a direct XML attribute in ND2Handler, to handle files that encode position names this way rather than as a CLxListVariant list. Signed-off-by: Maarten Paul <maarten.paul@gmail.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add the well/position name as an OME <StageLabel Name="..."> for each series (matching ZeissCZIReader) and as a "Position name" series metadata entry. The raw pPosName/dPosName keys are already dumped to the global metadata, so the previous addGlobalMetaList call is dropped to avoid duplication. Signed-off-by: Maarten Paul <maarten.paul@gmail.com>
|
Thanks, @maartenpaul. Before we can consider including this, we do need:
See also https://ome-contributing.readthedocs.io/en/latest/code-contributions.html#procedure-for-accepting-code-contributions. Please let us know when both the CLA and test data have been submitted. |
|
@melissalinkert I've submitted the CLA and uploaded test data to OME Zenodo |
|
Thanks @maartenpaul, I confirm we have received the CLA and the test data is https://zenodo.org/records/21162526. |
|
Workflows approved and |
melissalinkert
left a comment
There was a problem hiding this comment.
As expected, this causes some test failures on the ImageName tests: https://bf-testing-results.s3.amazonaws.com/2026/2026-07-06/nd2.log. For the most part, these test failures are expected and match up with the position names reported in NIS Elements.
A few immediate comments when looking through the affected files though:
- In some files, all position names are identical. This change means it is no longer easy to distinguish by name which series is which. Maybe when a position name is present, formatting the Image name like
<position name> (series <index>)instead of<file name> <position name>is a compromise? https://downloads.openmicroscopy.org/images/ND2/zenodo-16921650/ is a public example with identical position names (but we have at least one non-public example with > 100 positions). - When empty position names are stored, this results in an incorrect mapping between the series index and position name. For example, if there are 3 series (positions) with stored names of
A, empty string, andB, the reportedImagenames will befile.nd2 A,file.nd2 B, andfile.nd2 (series 3). I don't have a public example of this unfortunately.
Removing the include label for now, but can add it again once the above points are addressed.
Problem
Users in our facility generate ND2 files containing multiple well positions from multiwell plate acquisitions. When loaded into Fiji or imported into OMERO, the well names are lost and series appear as generic
(series 01),(series 02), etc. ; annoying when working with HCS plate dataThe Python nd2 library parses the well metadata correctly, so I investigated whether this could be fixed in Bio-Formats as well.
The root cause turned out to be two separate issues depending on the file structure of the ND2 files:
Files with a flat XY position loop (e.g. Z-stack per well): for the ND2 files available, the position names are stored in the binary metadata under
pPosName, which the XML parsing path (ND2Handler) already captured. However, the binary parsing path did not extract them at all, so files without XML metadata fell back to(series 01).Files with a nested TimeLoop → XYPosLoop structure: position names are stored under
dPosName. The binary CLX stream contains empty-string placeholder entries for each outer-loop level, followed by the actual names — causing a misalignment between name indices and series indices. Additionally,ND2Handleronly capturedpPosName, notdPosName.Position names were also not accessible in series metadata, as reported in #3449. The same problem, position names not read per-position, with every series taking the name/metadata of the last position, was reported on image.sc: Position info in .nd2 file can not be retrieved correctly by Bioformat.
I would highly appreciate feedback on this PR. I am not an advanced Java programmer and received help from Claude, but given that this would be highly valuable for our users I created this PR. I have tested with ND2 files from our facility and the fix works as expected in Fiji and when imported into OMERO.
Changes
pPosName,sPosition, ordPosName). The binary parsing path (iterateIn) now captures all three key variants, skipping empty-string placeholders that appear in nested loop structures.dPosNamein ND2Handler XML path: Files with aTimeLoop → XYPosLoopstructure store position names underdPosNameas a direct XML attribute.ND2Handlernow captures these alongside the existingpPosNamelist-variant handling.setSeries()call placement:setSeries(i)was previously called inside a conditional block (if (tsT.size() > 0)), meaning it was skipped when no timestamps were present. This caused subsequent series metadata operations (position X/Y/Z, position name) to run in the wrong series context.StageLabeland series metadata: each series' OME-XML now carries a<StageLabel Name="…">(matching the convention used byZeissCZIReader), and a "Position name" entry is added to the series metadata table. The rawpPosName/dPosNamekeys already appear in the global (original) metadata via the standard binary dump, so no separate global-list entry is added.ND2Handler: thedPosX/dPosY/dPosZitems were written to global metadata here and again byND2Reader's per-series loop (which already emits them as both series and global metadata). The duplicateND2Handlerentries are removed;posX/posY/posZare still collected for plane positions, so no position data is lost.Binary and XML sources are combined so that XML-derived names take precedence when present, falling back to the binary-derived names otherwise.
The position naming format matches the Python nd2 library — raw position names from the metadata (e.g.
A10_0000,A3[H2Ax_KRT19]) are used as-is.Relates to #3449 and #4287. This PR partially addresses #4287 by storing the position name as series metadata and by removing the duplicate global X/Y/Z entries in
ND2Handler. A full migration of the remaining per-position keys from global to series metadata (ND2Readerstill also emits X/Y/Z into global) is left out of scope here.Testing (
showinf/ImageInfo)Flat XY position loop —
Seq0000.nd2(24 series):Nested
TimeLoop → XYPosLoop(dPosName):Names match the Python
nd2library'sPoint.name.Verification checklist
developshowinfon flat and nested ND2 files shows correct well names +StageLabel(see Testing section)A10_0000,A3[H2Ax_KRT19]) for both flat and nested loop ND2 files