Repository navigation
Read masks one pixel high or wide as 2D YX masks - #63
Merged
Merged
Conversation
A valid 2D label mask of 1 x N, N x 1 or 1 x 1 pixels was refused: read_mask_file said it had the axes 'X' (or 'Y', or ''), and find_masks gave mask_not_2d. tifffile reports such a mask as YX, with or without axis metadata (plain, shaped, ImageJ and OME-TIFF; checked with tifffile 2023.7.10 to 2026.10.15), and OME-NGFF names y and x too. The squeeze was this brick's own: masks._mask_header dropped every length-1 axis, Y and X included, and the plane readers (_read_tiff_plane and the OME-Zarr branch of read_mask) did the same. Y and X are now kept whatever their length, in the header (MaskFile axes/shape, the manifest's masks entries) and in both readers (one _plane_index helper), so the plane comes back as (1, N), (N, 1) or (1, 1); a named Z or T stack of such planes keeps its planes. Nothing is guessed: Y and X are the header's, and an unnamed dimension (a plain stack, IYX) is still refused, the message now giving the header's axes and shape and naming the dimension instead of the axes left after dropping Y or X. Images were not affected (their axes are tifffile's). Contract §9, §15 and §16, the user guide and plan §12 question 44 record it for the next release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
tifffile's default metadata saves a 1D array as {"shape": [N]} and reads
it back with the axes 'X' and no Y (tifffile 2023.7.10 to 2026.10.15),
which is the "axes 'X'" of the fiberfeatures report. The file is one TIFF
page of 1 row and N columns, so a mask series without Y or X stored in
one 2D page is read as that page, YX (1, N), as any plain 2D page is
(decision D-d); any other series without Y or X is still refused.
Plan §12 question 44 records it, and leaves open for the owner whether
an image saved that way (header axes 'X', refused by the run with
missing_xy_axis) should be read as one row too: that would change the
shared header-axes rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
The test opened tifffile.TiffFile without a context manager, which leaves the file open (a ResourceWarning, and a lock on Windows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reported by the fiberfeatures agent:
read_mask_filerefuses a valid TIFF label mask that is 1 pixel high, and the error message is misleading ("has axes 'X'").Root cause. Two cases, both investigated with tifffile 2023.7.10, 2025.10.16, 2026.3.3, 2026.5.2, 2026.9.9 and 2026.10.15:
A 1×N, N×1 or 1×1 array written plain, shaped
{"axes": "YX"}, ImageJ or OME-TIFF. tifffile reports it asYXwith shape(1, N)/(N, 1)/(1, 1). OME-NGFF label images namey/xtoo. The squeeze was this brick's own:masks._mask_headerdropped every length-1 axis, Y and X included;_read_tiff_plane, and the OME-Zarr branch ofread_mask).The effects:
find_masksgavemask_not_2d;read_mask_filesaid "has axes 'X'" (or'Y', or'');scan_masksraised "is not the 2D plane its header gave (X)";ZYX/TYXstacks of such planes failed the same way ('ZX','TY').A 1-D array written with tifffile's default metadata (
{"shape": [N]}). Here tifffile itself gives the series axesXand no Y. This is the only case where tifffile reports'X'. The file is a single TIFF page of 1 row × N columns.Image side.
inspect_tiff,named_axes,header_axes,check_declared_axes, and image reading/runs (TIFF directly, TIFF→OME-Zarr conversion, OME-Zarr input) handle 1×N, N×1 and 1×1 images correctly on main, because tifffile keeps Y and X. Guard tests were added. The 1-D case (2) for images is left open for the owner (see Notes).By module
masks:_mask_header, so also inMaskFile.axes/shapeand the manifest'smasksentries._plane_indexhelper does the same for both readers. The plane comes back as(1, N),(N, 1)or(1, 1)in the mask's dtype._tiff_axes: a series without Y or X that is a single 2-DYXpage of the same size is read as that page,YX (1, N). This is case 2; it follows decision D-d for a plain 2-D page. Any other series without Y or X stays refused.read_mask_filerefusal gives the header's axes and shape and names the unnamed dimension, e.g.has axes 'IYX' (shape (3, 1, 9)): the dimension I (length 3) is neither Y nor X, and its metadata does not name it depth (Z) or time (T). Before, it showed the axes left over after dropping Y or X ('IX').masksaxes/shape), §15 (header checks), §16 (header-axes note,read_mask_file);mask_not_2d;Decisions
No new rule. The existing "length-1 axes aside" rule now leaves out Y and X, which are always the axes the header or the TIFF page gives. Nothing is guessed. A dimension the metadata does not name (a plain stack
IYX, shaped without axesQYXwith Q > 1) is still refused.Notes for review
X.header_axesreturns'X'andcheck_declared_axesaccepts'X'and refuses'YX'. The folder check suggests a mapping without Y, and the run then refuses it with an accuratemissing_xy_axis. Reading it as one row would change the shared D86 header-axes rule (contract §16), so it is recorded in plan §12 question 44 rather than done.YXQ(1, 9, 1)) is read as YX. That was already the rule on main forYXQ(2, 5, 1); only Y/X of length 1 are new.Tests
masks.py). They cover:read_mask_file, written plain, shaped YX, ImageJ and OME (12);IYXstack;find_masks+read_mask+scan_masksfor 1×9, 9×1 and 1×1 (3);TYXstack of 1-pixel masks;read_mask_fileand viafind_masks/read_mask(2)..[dev,notebook,zarr], zarr 3.4.2, tifffile 2026.10.15):ruff check .: passed.ruff format --check .: passed.pytest: 1496 passed, 1 skipped. The skip istest_discovery.py:665, which needs file-mode permissions and cannot run as root.-W error.🤖 Generated with Claude Code
https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a