Skip to content

OIR: fix two parse failures on Olympus LEXT (OLS) acquisitions - #4465

Open
PEEKPerformer wants to merge 2 commits into
ome:developfrom
PEEKPerformer:oir-lext-parse-fixes
Open

OIR: fix two parse failures on Olympus LEXT (OLS) acquisitions#4465
PEEKPerformer wants to merge 2 commits into
ome:developfrom
PEEKPerformer:oir-lext-parse-fixes

Conversation

@PEEKPerformer

@PEEKPerformer PEEKPerformer commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Refs: Opening POIR Olympus files on image.sc.

OIRReader cannot open .oir files produced by Olympus/Evident LEXT (OLS) instruments. Nothing structural is wrong: the reader already locates the pixel blocks, resolves the channels and reads the metadata correctly once it gets past two NumberFormatException/FormatException sites during initialisation.

Neither case is LEXT-specific in principle. Both are places where a field that is present, or scaled a particular way, in every FV-series file behaves differently elsewhere.

No open issue tracks either of these. #4132, #3808 and #3748 were the recent OIR reports and all were closed by #4360.

1. Missing order attribute in parseImageProperties

Introduced in bc3a949 (2017), "Use order attribute to set channel ordering":

int index = Integer.parseInt(channelNode.getAttribute("order")) - 1;

Element.getAttribute returns "", not null, for an absent attribute, so this throws NumberFormatException out of setId and the file cannot be opened at all.

LEXT files omit it: the commonphase:channel elements under commonimage:imageInfo carry only an id, and the ordering lives on the lsmimage:LSMChannel elements elsewhere in the document. Those ids appear under imageInfo in the same sequence as their recorded order, so document order is the correct index when the attribute is missing.

For comparison, in the two public sample sets under downloads.openmicroscopy.org/images/Olympus-OIR/ the same elements read order="2" and order="3", so the new branch is unreachable for them.

2. RGB depth divided below one byte, in parseFrameProperties and parseImageProperties

This one extends recent work. 76eb916 ("OIR: support data with component channels", #4360, fixes #4132) added component channel handling, which splits an RGB channel into one OME channel per component and divides the recorded depth by three:

int bytes = Integer.parseInt(depth.getTextContent());
if (rgb) {
  bytes /= 3;
}
m.pixelType = FormatTools.pixelTypeFromBytes(bytes, false, false);

That is right where depth is the total across all three components. The LEXT
camera image is a genuine component channel case, one commonphase:channel
with order="1" and three ElementChannelInfo children, but it records depth
per component, so the division yields 0:

FormatException: Unsupported byte depth: 0

Correcting something I said earlier in this description: I had claimed the file was self-inconsistent here. It is not. The commonframe:imageDefinition for the 1024x1024 image is flat and unambiguous, colorType RGB with depth 1 and bitCounts 8. The depth 3 / bitCounts 24 I was comparing it against belongs to a different, 1024x755 image in the same file. So the file uses the per-component convention for one image and the total convention for another, and divisibility tells them apart correctly.

A total across three components is always a multiple of three, so the fix only divides when the value divides evenly, with the same test applied to bitCounts. A depth that is not a multiple of three cannot be a total, so this also stops bitCounts 8 on an RGB component being reported as 2 valid bits per pixel.

This is a heuristic. Since it sits on top of very recent work, I would rather defer on it: if there is a better way to tell the two conventions apart (component count, bitCounts, an explicit variant check) I am happy to rework this half.

One limit of the divisibility test worth recording. It is sound for byte depths, since the valid per-component values are 1, 2 and 4 and none of those divide by three. It is weaker for bitCounts, where 12 is both a plausible per-component depth and a multiple of three, so a 12-bit-per-component RGB file recorded per-component would be halved to 4. I have no such file to test against, so I have not tried to special-case it.

Code

+15 -5 in OIRReader.java, three hunks, split across two commits so each bug can be reviewed and reverted independently. Both commits build on their own.

Validation

Against the README PR testing checklist:

  • This branch merges cleanly into the current develop (git merge-tree, 0 conflicts).
  • ant clean jars tools: BUILD SUCCESSFUL.
  • Maven build: mvn -DskipTests install completes successfully.
  • ant test: 1897 tests, 0 failures, 0 skips, 5m01s.
  • No syntax or API beyond Java 11 (plain if/String.trim/isEmpty).
  • showinf on every affected format, below.

Regression against the public corpus

showinf -nopix -novalid output on both public Olympus-OIR sample sets is
byte-identical before and after this branch (1343 and 857 lines
respectively, diff clean):

file result
imagesc-105684/1202-interval_10sec_sequence_frame.oir identical to stock develop
etienne/amy slice z stack_0001.oir identical to stock develop

This is the expected outcome given the analysis above: fix 1's fallback is unreachable when order is present, and fix 2 only alters inputs that currently raise FormatException.

LEXT files, previously unopenable

These come from a single OLS5500 session in our lab on 2026-08-03. Both .poir files below are deposited under CC-BY 4.0, see the note at the end, so this is reproducible once the deposit clears review. Each row is a distinct case rather than a distinct dataset: two of the three come out of the same .poir.

inner .oir from size before after
260803_1437_000_LSM3D^3D_LSM.oir 001.poir 12.5 MB NumberFormatException 256×256, SizeC=3, uint16
260803_1448_000_LSM3D^3D_LSM.oir 002.poir 15.4 MB NumberFormatException 1024×1024, SizeC=3, uint16
260803_1448_000_COLOR3D^XY_Camera.oir 002.poir 12.3 MB Unsupported byte depth: 0 1024×1024, SizeC=3, uint8

LSM3D is the laser confocal result, three channels of colour, height and intensity for a single plane. COLOR3D is the companion colour camera image, and is the component channel case discussed above. Every .poir here except 001.poir contains one of each.

Metadata now resolves as expected, e.g.

<Objective ID="Objective:0:0" LensNA="0.3" NominalMagnification="10.0"
           WorkingDistance="10.4" WorkingDistanceUnit="mm"/>
<Pixels PhysicalSizeX="5.0" PhysicalSizeXUnit="µm"
        PhysicalSizeY="5.0" PhysicalSizeYUnit="µm" .../>

5 µm/px at 10× over 256 px is a 1.28 mm field, which matches the objective.

End-to-end, bfconvert produced OME-TIFF for 79 inner .oir files with 0 failures: 9 from five single acquisitions, and 70 from one 35-field mosaic. All from the same instrument and session, so this is depth of coverage on one LEXT variant rather than breadth across the family.

One thing that looks wrong but is not: showinf reports Valid bits per pixel = 7 on one LSM3D channel. The file really does record bitCounts 7 there, and that channel's pixel values top out at 90.

Notes

The instrument here is an OLS5500; files came off it as .poir and .mpoir. Those are Zip64 wrappers. .poir contains one or more .oir, and .mpoir contains matl.omp2info plus one .poir per field (entries stored, not deflated). This PR does not add readers for either wrapper; it only makes the inner .oir readable once extracted, which is what the image.sc thread ran aground on six years ago.

If a .poir/.mpoir reader would be welcome I'm happy to work on it, but per the external-readers guidance I'd rather ask first whether you'd want it in core or as an external reader. Happy to go either way.

I don't have access to the curated data repo, so ant test-automated is needed.

Five .poir files from this session, including the two used above, are deposited under CC-BY 4.0 as 10.5281/zenodo.21792018 and submitted to the Bio-Formats Zenodo community, where they are awaiting approval.

Between them they cover both parse paths this PR touches, so the change should be testable against the curated corpus if you want to add them. Polymer composite surface metrology, no biological content. Happy to deposit the .mpoir mosaic as well if that case is useful, though it is 323 MiB and exercises nothing the single acquisitions do not.

One sibling worth noting, not changed here: parseImageProperties also does an unguarded Integer.parseInt(component.getAttribute("order")) on elementChannel components, added by the same commit as case 2. The LEXT files do populate order there, so it is not reachable for this data, but it is the same pattern as case 1.

parseImageProperties assumed every commonphase:channel under
commonimage:imageInfo carries an 'order' attribute:

  int index = Integer.parseInt(channelNode.getAttribute("order")) - 1;

Element.getAttribute returns an empty string rather than null for an
absent attribute, so on files that omit it this throws
NumberFormatException from setId and the file cannot be opened at all.

Olympus/Evident LEXT (OLS) acquisitions do omit it: their imageInfo
channel elements carry only an id, and the ordering is recorded on the
lsmimage:LSMChannel elements elsewhere in the document. Those ids appear
under imageInfo in the same sequence as their recorded order, so
document order is the correct index when the attribute is missing.

Fall back to the loop counter in that case. Files that do carry 'order'
are parsed exactly as before, so this cannot affect any dataset that
opens today.
// 'depth' is normally the total byte count across all components, but
// some RGB frames record the per-component count instead; only divide
// when the result would still be a valid byte depth.
if (rgb && bytes >= 3) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here and in the other places where a check for >= 3 is introduced, would it make more sense to check for something like bytes % 3 == 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that is much better, thanks Melissa! I changed at all four sites in 503f590. Appreciate your speedy feedback!

76eb916 added component channel support, splitting an RGB channel
into one OME channel per component and dividing the recorded depth by
three to get bytes per component:

  int bytes = Integer.parseInt(depth.getTextContent());
  if (rgb) {
    bytes /= 3;
  }
  m.pixelType = FormatTools.pixelTypeFromBytes(bytes, false, false);

That is correct where 'depth' is the total across all three components.
Olympus/Evident LEXT (OLS) camera images record it per component
instead, so the integer division yields 0 and pixelTypeFromBytes throws:

  FormatException: Unsupported byte depth: 0

The file is inconsistent about which convention it uses. The channel
level imageDefinition gives depth 1 for a channel that carries three
ElementChannelInfo components, while the frame level definitions for the
same data give depth 3 with bitCounts 24.

A total across three components is always a multiple of three, so only
divide when the value divides evenly, and apply the same test to
bitCounts. This also corrects bitsPerPixel on the same files: with
bitCounts 8 and colorType RGB, dividing unconditionally reported 2 valid
bits per pixel instead of 8.
@melissalinkert

Copy link
Copy Markdown
Member

If a .poir/.mpoir reader would be welcome I'm happy to work on it, but per the external-readers guidance I'd rather ask first whether you'd want it in core or as an external reader. Happy to go either way.

Are you able to provide a wider range of .poir/.mpoir data that covers a range of dimensions (channels, time, Z) and modalities? Is .poir/.mpoir the default for OLS5500 acquisitions, or is this an option that needs to be enabled?

@PEEKPerformer

PEEKPerformer commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Yeah, the instrument manual describes .poir as the 'OLS5000/OLS5100/OLS5500 standard file format' and the save dialogs default to it. Stitching acquisitions produce .mpoir so it's what these instruments emit unless SDF/JPEG/BMP/PNG is manually exported by the user.

On breadth, more than happy to spend more time on the instrument. I'll check which modes our instrument supports and will deposit as much variation as I can. Happy to try and fill special requests if you have any!

Edit: will ping you when i get the files :)

@PEEKPerformer

Copy link
Copy Markdown
Contributor Author

@melissalinkert submitted, pending approval. 10.5281/zenodo.22017632

I found some files that trigger the bug from the description above. Four snapshot tiles in the set hit the unguarded elementChannel order parseInt (no order attribute on their ElementChannelInfo). A near-identical passing file is included. Happy to fix it like the imageInfo site, here or as a follow-up. Just let me know :)

Would be happy to take a stab at the reader as well with your blessing.

Nothing has a SizeT > 1 because the instrument lacks a time series mode.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Olympus OIR: MetadataTools.populatePixelsOnly: 0 must be non-null and strictly positive.

2 participants