micro-reader 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {micro_reader-0.3.0 → micro_reader-0.4.0}/.gitignore +2 -1
  2. {micro_reader-0.3.0 → micro_reader-0.4.0}/PKG-INFO +69 -40
  3. {micro_reader-0.3.0 → micro_reader-0.4.0}/README.md +67 -38
  4. {micro_reader-0.3.0 → micro_reader-0.4.0}/pyproject.toml +2 -2
  5. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_axes.py +21 -8
  6. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_base.py +75 -15
  7. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_groups.py +17 -4
  8. micro_reader-0.4.0/src/micro_reader/_lambda.py +147 -0
  9. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_memory.py +7 -2
  10. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_metadata.py +30 -3
  11. micro_reader-0.4.0/src/micro_reader/_naming.py +113 -0
  12. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_outline.py +3 -3
  13. micro_reader-0.4.0/src/micro_reader/_parts.py +366 -0
  14. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_plate.py +83 -10
  15. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_pool.py +6 -2
  16. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_registry.py +111 -20
  17. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_spatial.py +2 -2
  18. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_stack.py +5 -0
  19. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_version.py +1 -1
  20. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_czi_metadata.py +18 -0
  21. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_hdf5.py +17 -1
  22. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_lif_metadata.py +3 -2
  23. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_nd2_legacy.py +11 -2
  24. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_nd2_metadata.py +45 -11
  25. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_nd2_structure.py +47 -1
  26. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_tiff_metadata.py +110 -8
  27. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/bdv.py +21 -3
  28. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/columbus.py +6 -0
  29. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/companion.py +58 -8
  30. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/cv7000.py +16 -5
  31. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/czi.py +22 -2
  32. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/dv.py +4 -1
  33. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/harmony.py +20 -0
  34. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/hdf5.py +6 -3
  35. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/ims.py +47 -11
  36. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/incell.py +14 -0
  37. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/jdce.py +3 -0
  38. micro_reader-0.4.0/src/micro_reader/backends/klb.py +175 -0
  39. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/lif.py +67 -2
  40. micro_reader-0.4.0/src/micro_reader/backends/luxendo.py +718 -0
  41. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/metamorph.py +36 -1
  42. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/metaxpress.py +10 -6
  43. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/micromanager.py +41 -7
  44. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/nd2.py +44 -6
  45. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/oir.py +28 -2
  46. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/raster.py +7 -0
  47. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/tiff.py +84 -2
  48. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/vsi.py +6 -2
  49. micro_reader-0.3.0/src/micro_reader/_parts.py +0 -188
  50. {micro_reader-0.3.0 → micro_reader-0.4.0}/LICENSE +0 -0
  51. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/__init__.py +0 -0
  52. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_cfb.py +0 -0
  53. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_codecs.py +0 -0
  54. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_errors.py +0 -0
  55. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_eubi_guard.py +0 -0
  56. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_fileset.py +0 -0
  57. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_index.py +0 -0
  58. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_io.py +0 -0
  59. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_jpeg_restarts.py +0 -0
  60. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_mosaic.py +0 -0
  61. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/_stream.py +0 -0
  62. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/__init__.py +0 -0
  63. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_ets.py +0 -0
  64. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_nd2_run.py +0 -0
  65. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_tiff_variants.py +0 -0
  66. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/_vsi_tree.py +0 -0
  67. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/cellomics.py +0 -0
  68. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/flex.py +0 -0
  69. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/ics.py +0 -0
  70. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/ilastik.py +0 -0
  71. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/mrc.py +0 -0
  72. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/ndpis.py +0 -0
  73. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/oif.py +0 -0
  74. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/omp2info.py +0 -0
  75. {micro_reader-0.3.0 → micro_reader-0.4.0}/src/micro_reader/backends/scanr.py +0 -0
@@ -20,5 +20,6 @@ Thumbs.db
20
20
  # irrelevant
21
21
  smoke_tests/
22
22
  harness/
23
- docs/
23
+ docs_private/
24
+ site/
24
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: micro-reader
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Low-level, memory-bounded pixel reading for microscopy formats: read exactly the rectangle you ask for.
5
5
  Project-URL: Homepage, https://github.com/bugraoezdemir/micro-reader
6
6
  Project-URL: Source, https://github.com/bugraoezdemir/micro-reader
@@ -9,7 +9,7 @@ Author-email: Bugra Oezdemir <bugraa.ozdemir@gmail.com>
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Keywords: bioimage,cryo-em,czi,eer,image-io,imaris,ims,leica,lif,lsm,memory-bounded,microscopy,mrc,nd2,oir,ome-tiff,tiff
12
- Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Science/Research
14
14
  Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.11
@@ -56,9 +56,9 @@ Description-Content-Type: text/markdown
56
56
 
57
57
  **Memory-bounded pixel reader for microscopy images.**
58
58
 
59
- `micro-reader` reads regions of large microscopy files without loading the entire image into memory. It provides a common interface across TIFF, CZI, ND2, Leica, OIR, OIB / OIF, Olympus VSI, Imaris, DeltaVision, MRC, common raster formats, OME companion file sets, and high-content screening plates (Harmony, Columbus, MetaXpress, ImageXpress HCS.ai, IN Cell, CellVoyager, CQ1, Opera Flex, scanR, Cellomics).
59
+ `micro-reader` reads regions of large microscopy files without loading the entire image into memory. It provides a common interface across TIFF, CZI, ND2, Leica, OIR, OIB / OIF, Olympus VSI, Imaris, DeltaVision, MRC, KLB, common raster formats, OME companion file sets, and high-content screening plates (Harmony, Columbus, MetaXpress, ImageXpress HCS.ai, IN Cell, CellVoyager, CQ1, Opera Flex, scanR, Cellomics).
60
60
 
61
- > **Status: pre-alpha (0.0.x)**
61
+ > **Status: alpha (0.x)**
62
62
  >
63
63
  > The API may still change.
64
64
 
@@ -77,14 +77,6 @@ pip install "micro-reader[ims]" # Imaris (.ims)
77
77
  pip install "micro-reader[codecs]" # additional codecs
78
78
  ```
79
79
 
80
- ## Compatibility with eubi-bridge
81
-
82
- micro-reader 0.1.0 renamed the image index `scene` to `series`. eubi-bridge 0.1.3 to 0.2.x and eubi-bridge-lite below 0.3.0 still read `scene`, so `import micro_reader` refuses to load next to them. It raises an `ImportError` that names the installed eubi version and the two ways out: upgrade eubi (`pip install -U 'eubi-bridge>=0.3.0'`, or the same for `eubi-bridge-lite`), or keep that eubi with the micro-reader it was built for (`pip install 'micro-reader<0.1'`). The check reads the versions of the installed packages; it never imports eubi. An editable install of eubi is checked like any other: its recorded version can be out of date, so the message then says to reinstall it.
83
-
84
- To skip the check, set the environment variable `MICRO_READER_SKIP_EUBI_CHECK=1`. Only the value `1` skips it; any other value keeps the check. micro-reader then warns once that the check was skipped. This is meant for work on an eubi checkout, not for normal use.
85
-
86
- The check will be removed when every eubi-bridge / eubi-bridge-lite line below 0.3.0 has left the support window, checked by the release version matrix; not before.
87
-
88
80
  ## Quick start
89
81
 
90
82
  ```python
@@ -197,6 +189,17 @@ f.select(series=1, view=0)
197
189
 
198
190
  This returns the images matching the requested indices. For example, if a file contains several series, tiles, and views, selecting `series=1, view=0` returns the tiles belonging to that series and view.
199
191
 
192
+ ### Image names
193
+
194
+ `image.name` follows one rule for every format, and is the same whichever file of a dataset you open:
195
+
196
+ 1. A plate's image is named by its well and field: `"well B2 field 0"` (the field counted from 0, as in `select(field=0)`; the plate's name in front when the file holds several plates).
197
+ 2. Otherwise the name the format gives the image: a Leica element's name, an OME `Image Name`, an Imaris image name, a slide's associated image by its role in lower case (`label`, `macro`, `thumbnail`). A slide's main scan, which its format does not name, takes the name of its file.
198
+ 3. Otherwise, in a file of several images, a label the file records for each one (a CZI scene name, an ND2 point name, a Micro-Manager or MetaMorph position label), or else the index values that tell them apart: `"series 2"`, `"series 1, tile 3"`.
199
+ 4. Otherwise (one image) the name of the dataset's main file without its extension: the index file of a set, the main file of a multi-file format, a Luxendo experiment's folder.
200
+
201
+ Two images left with one name get the index values that differ between them appended, for example `"scan, series 2"`. Resolution levels carry their image's name.
202
+
200
203
  ### Available index dimensions
201
204
 
202
205
  `micro-reader` uses these index names when a format provides the corresponding dimensions:
@@ -233,7 +236,7 @@ f.path # the main file: "cells_R3D.dv"
233
236
  f.opened # what was opened: "cells_R3D.dv.log"
234
237
  ```
235
238
 
236
- You can also open the folder. A folder opens the image it belongs to (an NDTiff dataset folder, a Harmony or CellVoyager measurement folder, a folder of Opera `.flex` files), or the one image it holds. An Opera Flex plate has no index file: any of its `.flex` files opens the whole folder as the plate (`multifile=False` reads only that file). A folder with several images raises an error that lists them.
239
+ You can also open the folder. A folder opens the image it belongs to (an NDTiff dataset folder, a Harmony or CellVoyager measurement folder, a folder of Opera `.flex` files, a Luxendo experiment), or the one image it holds. A folder that holds no image itself, such as a downloaded sample folder with the export one or two levels down, opens the one dataset found in its sub-folders. An Opera Flex plate has no index file: any of its `.flex` files opens the whole folder as the plate (`multifile=False` reads only that file). A folder of one NIS-Elements well-plate run opens as that plate (with `plates=False`, its files are several images). A folder with several images or datasets raises an error that lists them; an OME-Zarr store is refused with a pointer to `ome_zarr_pyramid`, which reads it.
237
240
 
238
241
  To sort many files at once, for example everything under a folder you want to convert, `group_files` says which files make one image:
239
242
 
@@ -246,7 +249,7 @@ for group in g.groups:
246
249
  g.singles # every other file, as given
247
250
  ```
248
251
 
249
- A file that a plate or companion index names is listed only in that index's group. When the main file of a multi-file image is missing, its group has `main=None`. Pass `plates=True` to group as `open(..., plates=True)` reads: a folder of ND2 files from one NIS-Elements well-plate run is then one group.
252
+ A file that a plate or companion index names is listed only in that index's group. When the main file of a multi-file image is missing, its group has `main=None`. It groups as `open()` reads, plates included (`plates=True`, the default): a folder of ND2 files from one NIS-Elements well-plate run is one group; `plates=False` groups as `open(..., plates=False)` reads.
250
253
 
251
254
  ## Axes
252
255
 
@@ -274,37 +277,47 @@ image.shape
274
277
  # (1, 3, 40, 5120, 6144)
275
278
  ```
276
279
 
277
- Other dimensions, such as illumination, emission, tile, etc., are represented as image indices rather than array axes, as discussed above.
280
+ Other dimensions, such as illumination, excitation, tile, etc., are represented as image indices rather than array axes, as discussed above.
278
281
 
279
- ### Changing axis order
282
+ ### Lambda scans
280
283
 
281
- You can request a specific axis order:
284
+ A lambda (spectral) scan is one image in every format. Its bands lie along `c`, a detector's bands next to each other in wavelength order, and each channel has its `emission_wavelength` and its wavelength in its name (`"HyD S 1 420 nm"`).
282
285
 
283
286
  ```python
284
- image = f.images[0].as_axes("tczyx")
287
+ f = micro_reader.open("scan.lif")
288
+ f.images[0].axes # 'tczyx'
289
+ [c.emission_wavelength for c in f.images[0].metadata.channels]
290
+
291
+ f = micro_reader.open("scan.lif", axes="native")
292
+ f.images[0].axes # 'tlczyx': a real lambda axis
293
+ f.images[0].metadata.index_values["emission"] # its wavelengths
285
294
  ```
286
295
 
287
- or when opening the file:
296
+ Asking for axes that name `l`, such as `axes="tlczyx"`, also gives the axis. An LSM stores its bands as channels, so it has no `l` axis; its channels carry their wavelengths. Excitation scans stay an image index.
297
+
298
+ `open` gives every image in this canonical order by default, whatever order the file stores: axes the file lacks are added with size 1 (a 2-D TIFF is `(1, 1, 1, Y, X)`) and the others are reordered. Reading costs the same either way.
299
+
300
+ ### Changing axis order
301
+
302
+ To keep the order the file stores (ICS `ctzyx`, Micro-Manager OME-TIFF `tzcyx`, a 2-D TIFF `yx`):
288
303
 
289
304
  ```python
290
- f = micro_reader.open("image.tif", axes="tczyx")
305
+ f = micro_reader.open("image.ics", axes="native")
291
306
  ```
292
307
 
293
- You can also request the canonical order:
308
+ You can also request a specific axis order:
294
309
 
295
310
  ```python
296
- f = micro_reader.open("image.tif", axes="canonical")
311
+ image = f.images[0].as_axes("tczyx")
297
312
  ```
298
313
 
299
- Canonical order is:
314
+ or when opening the file:
300
315
 
301
- ```text
302
- t c z y x
316
+ ```python
317
+ f = micro_reader.open("image.tif", axes="zyx")
303
318
  ```
304
319
 
305
- followed by `s` when RGB samples are present.
306
-
307
- Missing axes are added with size 1 and existing axes are reordered.
320
+ `axes="canonical"` (the default) is `t c z y x`, followed by `s` when RGB samples are kept (`samples="keep"`).
308
321
 
309
322
  ### Removing singleton axes
310
323
 
@@ -358,8 +371,8 @@ print(len(f.images))
358
371
  print(f.images[0].index)
359
372
  # {"series": 0, "sequence": 0}
360
373
 
361
- print(f.images[0].axes)
362
- # yx
374
+ print(f.images[0].axes, f.images[0].shape)
375
+ # tczyx (1, 1, 1, 16, 16) (axes="canonical", the default; axes=None gives yx)
363
376
  ```
364
377
 
365
378
  If you know that the pages represent `z`, you can tell the reader:
@@ -371,11 +384,11 @@ f = micro_reader.open(
371
384
  )
372
385
  ```
373
386
 
374
- The result is a single image with `zyx` axes.
387
+ The result is a single image with the 80 pages along `z`.
375
388
 
376
389
  ```python
377
- print(f.images[0].axes)
378
- # zyx
390
+ print(f.images[0].axes, f.images[0].shape)
391
+ # tczyx (1, 1, 80, 16, 16) (axes=None gives zyx)
379
392
  ```
380
393
 
381
394
 
@@ -463,6 +476,8 @@ f = micro_reader.open(
463
476
 
464
477
  CZI tiles are placed by the file's own stitching information, FluoView tiles by the tiling set's grid (where tiles overlap, the later tile is on top; areas without a tile read as 0). Other formats with tiles raise an error for `tiles="mosaic"`.
465
478
 
479
+ A format that has tiles it cannot place refuses `tiles="mosaic"` with an error. A file without tiles has nothing to place and opens as usual: Micro-Manager and MetaMorph stage positions are series, not tiles.
480
+
466
481
  `tiles="stitched"`, the name before 0.1.0, still works and warns.
467
482
 
468
483
  ## Plates
@@ -503,10 +518,10 @@ is its name (`well="B2"` is `well_name="B2"`).
503
518
 
504
519
  `f.outline()` shows the plates, wells and fields of a file.
505
520
 
506
- Zeiss CZI plates are read as plates with `plates=True`, and as series otherwise:
521
+ A plate in a generic format (a CZI, an ND2, an OME-TIFF or a companion with wells) is read as a plate too. `plates=False` reads it by its series instead:
507
522
 
508
523
  ```python
509
- f = micro_reader.open("plate.czi", plates=True)
524
+ f = micro_reader.open("plate.czi")
510
525
  ```
511
526
 
512
527
  Missing plane files are read as zeros, with a `MissingImagesWarning`. A field without any of its files is left out.
@@ -565,6 +580,7 @@ meta.channels[0]
565
580
  `micro-reader` follows a few important rules:
566
581
 
567
582
  * Units use OME-Zarr-compatible names such as `micrometer`, `nanometer`, and `millisecond`.
583
+ * Each value keeps the unit the file records (lengths above a millimetre become micrometres), so two formats can give the same quantity in different units, for example a time step in milliseconds (ND2, OIR) or in seconds (CZI, LIF). Convert to compare: `image.metadata.pixel_sizes["t"].to("s")`, `.to("um")`. Converting a length to a time, or to an unknown unit, raises `ValueError`.
568
584
  * Missing metadata is left as `None` or absent.
569
585
  * Pixel sizes are never invented as `1.0`.
570
586
  * Colours are not invented when the file does not provide them.
@@ -576,7 +592,7 @@ meta.channels[0]
576
592
 
577
593
  Where possible, format-specific coordinate conventions are converted to this common representation.
578
594
 
579
- The position is omitted when it cannot be determined reliably.
595
+ The position is omitted when it cannot be determined reliably. Micro-Manager's separate files and MetaMorph record where the stage was without saying which point of the field that is: their value is in `image.metadata.stage_position`, as recorded, not in `position`.
580
596
 
581
597
  ## Supported formats
582
598
 
@@ -592,13 +608,14 @@ The position is omitted when it cannot be determined reliably.
592
608
  | **VSI** | Olympus / Evident cellSens and VS120 / VS200 slide scanners |
593
609
  | **NDPIS** | Hamamatsu NanoZoomer fluorescence sets (one NDPI per channel) |
594
610
  | **Imaris** | `.ims`, with the `[hdf5]` extra |
595
- | **HDF5** | ilastik, BigDataViewer / BigStitcher, Luxendo, Fiji HDF5, with the `[hdf5]` extra |
611
+ | **HDF5** | ilastik, BigDataViewer / BigStitcher, Luxendo experiments and `.lux.h5`, Fiji HDF5, with the `[hdf5]` extra |
596
612
  | **ICS / IDS** | Image Cytometry Standard 1.0 and 2.0 (SVI Huygens and others) |
597
613
  | **MetaMorph** | `.nd` acquisitions and their TIFF / STK files |
598
614
  | **Micro-Manager 1.4**| image-per-file acquisitions (`metadata.txt` + one TIFF per plane) |
599
615
  | **DeltaVision** | `.dv`, `.r3d` (softWoRx, OMX, Priism) |
600
616
  | **MRC** | Common MRC modes and extended headers |
601
- | **Plates (HCS)** | Harmony, Columbus exports, MetaXpress, ImageXpress HCS.ai (`.jdce`), IN Cell, CellVoyager, Opera Flex, scanR and Cellomics (`.C01`) plates; with `plates=True`, CZI plates, OME companion and OME-TIFF plates (Yokogawa CQ1), ND2 files whose XY points are named after wells, and folders of ND2 files from one NIS-Elements well-plate run |
617
+ | **KLB** | Every time point and channel, bzip2 and zlib blocks |
618
+ | **Plates (HCS)** | Harmony, Columbus exports, MetaXpress, ImageXpress HCS.ai (`.jdce`), IN Cell, CellVoyager, Opera Flex, scanR and Cellomics (`.C01`) plates; also (by default; `plates=False` reads them by series) CZI plates, OME companion and OME-TIFF plates (Yokogawa CQ1), ND2 files whose XY points are named after wells, and folders of ND2 files from one NIS-Elements well-plate run |
602
619
  | **JPEG / PNG / BMP** | Common raster image formats |
603
620
 
604
621
  All formats use the same `File`, `Image`, axes, indexing, metadata, and reading interface wherever possible.
@@ -662,10 +679,14 @@ f = micro_reader.open("data_t001.ome.tif", multifile=False)
662
679
 
663
680
  If the companion is missing or does not list the file, opening the whole set raises an error that names `multifile=False`. With `multifile=False` the file is then read without pixel sizes, with a `CompanionWarning`. A TIFF whose metadata lives in another OME-TIFF (instead of a `.companion.ome`) works the same way, with that OME-TIFF in the companion's place.
664
681
 
665
- A companion that describes a plate (`Plate` / `Well` / `WellSample`) reads as one with `plates=True`, as a CZI plate does: `f.plates` lists its plates and wells, and its images are indexed by plate, acquisition, well and field. RGB images in a companion have a sample axis (`t c z y x s`), as RGB OME-TIFFs do.
682
+ A companion that describes a plate (`Plate` / `Well` / `WellSample`) reads as one, as a CZI plate does: `f.plates` lists its plates and wells, and its images are indexed by plate, acquisition, well and field. RGB images in a companion have a sample axis (`t c z y x s`), as RGB OME-TIFFs do.
666
683
 
667
684
  Some OME-TIFF sets keep the full OME-XML in their TIFF files instead of a companion. Opening one of those files also reads the whole set, and `multifile=False` reads only the planes stored in that file, with the set's metadata.
668
685
 
686
+ Any file of a plate works the same way: opening one plane file opens the plate, and `multifile=False` reads that file alone. Files that belong to the instrument and not to the plate's data, such as CellVoyager shading and dark-current images, open as themselves.
687
+
688
+ Leica's settings files (`FrameProperties`, `IOManagerConfiguation`) open their experiment. A `.xml` beside a `.lif` opens the `.lif`, and so does a `.lifext`; `multifile=False` reads the `.lifext` itself.
689
+
669
690
  Micro-Manager stacks always open whole, also with `multifile=False`: they split their files by size, so one file does not hold a complete part of the image.
670
691
 
671
692
  ### CZI
@@ -763,7 +784,7 @@ These need the `[hdf5]` extra as well.
763
784
 
764
785
  * **ilastik**: every dataset with ilastik's `axistags` (its exports, its input files, steinbock's `img`) is one image, in the dataset's own axis order. Pixel sizes are read where ilastik recorded units (`axis_units`, since 2025-09).
765
786
  * **BigDataViewer / BigStitcher**: the XML with its HDF5 file (also partitioned). One image per tile, angle and illumination, channels on `c` and time points on `t`, with the stored resolution levels. Opening the HDF5 file opens its XML.
766
- * **Luxendo / Bruker light-sheet** (`.lux.h5`): one stack per file. A whole acquisition opens through the Imaris file Luxendo writes next to it.
787
+ * **Luxendo / Bruker light-sheet**: an experiment folder (`raw/` with one folder of `.lux.h5` stacks per channel) opens as one image per stack and camera, with all time points and channels. Opening any part opens the whole experiment: the folder, a `.lux.h5`, or the `imaris.ims` / `bdv.xml` next to `raw/`. Files those link files name but `raw/` lacks read as zeros, with a warning. `multifile=False` reads one `.lux.h5` (or `.ims`) alone.
767
788
  * **Fiji HDF5** (the Vibez plugin): datasets with `element_size_um`; `t<n>/channel<n>` datasets make one `t c z y x` image.
768
789
 
769
790
  ### ICS / IDS
@@ -929,6 +950,14 @@ Applications work with the same `File` and `Image` model described in [How it wo
929
950
 
930
951
  The goal is that an application can open a microscopy file, identify the image it needs, read only the required pixels, and obtain standardized metadata without needing to understand the vendor-specific file structure.
931
952
 
953
+ ## Compatibility with eubi-bridge
954
+
955
+ micro-reader 0.1.0 renamed the image index `scene` to `series`. eubi-bridge 0.1.3 to 0.2.x and eubi-bridge-lite below 0.3.0 still read `scene`, so `import micro_reader` refuses to load next to them. It raises an `ImportError` that names the installed eubi version and the two ways out: upgrade eubi (`pip install -U 'eubi-bridge>=0.3.0'`, or the same for `eubi-bridge-lite`), or keep that eubi with the micro-reader it was built for (`pip install 'micro-reader<0.1'`). The check reads the versions of the installed packages; it never imports eubi. An editable install of eubi is checked like any other: its recorded version can be out of date, so the message then says to reinstall it.
956
+
957
+ To skip the check, set the environment variable `MICRO_READER_SKIP_EUBI_CHECK=1`. Only the value `1` skips it; any other value keeps the check. micro-reader then warns once that the check was skipped. This is meant for work on an eubi checkout, not for normal use.
958
+
959
+ The check will be removed when every eubi-bridge / eubi-bridge-lite line below 0.3.0 has left the support window, checked by the release version matrix; not before.
960
+
932
961
  ## License
933
962
 
934
963
  MIT
@@ -2,9 +2,9 @@
2
2
 
3
3
  **Memory-bounded pixel reader for microscopy images.**
4
4
 
5
- `micro-reader` reads regions of large microscopy files without loading the entire image into memory. It provides a common interface across TIFF, CZI, ND2, Leica, OIR, OIB / OIF, Olympus VSI, Imaris, DeltaVision, MRC, common raster formats, OME companion file sets, and high-content screening plates (Harmony, Columbus, MetaXpress, ImageXpress HCS.ai, IN Cell, CellVoyager, CQ1, Opera Flex, scanR, Cellomics).
5
+ `micro-reader` reads regions of large microscopy files without loading the entire image into memory. It provides a common interface across TIFF, CZI, ND2, Leica, OIR, OIB / OIF, Olympus VSI, Imaris, DeltaVision, MRC, KLB, common raster formats, OME companion file sets, and high-content screening plates (Harmony, Columbus, MetaXpress, ImageXpress HCS.ai, IN Cell, CellVoyager, CQ1, Opera Flex, scanR, Cellomics).
6
6
 
7
- > **Status: pre-alpha (0.0.x)**
7
+ > **Status: alpha (0.x)**
8
8
  >
9
9
  > The API may still change.
10
10
 
@@ -23,14 +23,6 @@ pip install "micro-reader[ims]" # Imaris (.ims)
23
23
  pip install "micro-reader[codecs]" # additional codecs
24
24
  ```
25
25
 
26
- ## Compatibility with eubi-bridge
27
-
28
- micro-reader 0.1.0 renamed the image index `scene` to `series`. eubi-bridge 0.1.3 to 0.2.x and eubi-bridge-lite below 0.3.0 still read `scene`, so `import micro_reader` refuses to load next to them. It raises an `ImportError` that names the installed eubi version and the two ways out: upgrade eubi (`pip install -U 'eubi-bridge>=0.3.0'`, or the same for `eubi-bridge-lite`), or keep that eubi with the micro-reader it was built for (`pip install 'micro-reader<0.1'`). The check reads the versions of the installed packages; it never imports eubi. An editable install of eubi is checked like any other: its recorded version can be out of date, so the message then says to reinstall it.
29
-
30
- To skip the check, set the environment variable `MICRO_READER_SKIP_EUBI_CHECK=1`. Only the value `1` skips it; any other value keeps the check. micro-reader then warns once that the check was skipped. This is meant for work on an eubi checkout, not for normal use.
31
-
32
- The check will be removed when every eubi-bridge / eubi-bridge-lite line below 0.3.0 has left the support window, checked by the release version matrix; not before.
33
-
34
26
  ## Quick start
35
27
 
36
28
  ```python
@@ -143,6 +135,17 @@ f.select(series=1, view=0)
143
135
 
144
136
  This returns the images matching the requested indices. For example, if a file contains several series, tiles, and views, selecting `series=1, view=0` returns the tiles belonging to that series and view.
145
137
 
138
+ ### Image names
139
+
140
+ `image.name` follows one rule for every format, and is the same whichever file of a dataset you open:
141
+
142
+ 1. A plate's image is named by its well and field: `"well B2 field 0"` (the field counted from 0, as in `select(field=0)`; the plate's name in front when the file holds several plates).
143
+ 2. Otherwise the name the format gives the image: a Leica element's name, an OME `Image Name`, an Imaris image name, a slide's associated image by its role in lower case (`label`, `macro`, `thumbnail`). A slide's main scan, which its format does not name, takes the name of its file.
144
+ 3. Otherwise, in a file of several images, a label the file records for each one (a CZI scene name, an ND2 point name, a Micro-Manager or MetaMorph position label), or else the index values that tell them apart: `"series 2"`, `"series 1, tile 3"`.
145
+ 4. Otherwise (one image) the name of the dataset's main file without its extension: the index file of a set, the main file of a multi-file format, a Luxendo experiment's folder.
146
+
147
+ Two images left with one name get the index values that differ between them appended, for example `"scan, series 2"`. Resolution levels carry their image's name.
148
+
146
149
  ### Available index dimensions
147
150
 
148
151
  `micro-reader` uses these index names when a format provides the corresponding dimensions:
@@ -179,7 +182,7 @@ f.path # the main file: "cells_R3D.dv"
179
182
  f.opened # what was opened: "cells_R3D.dv.log"
180
183
  ```
181
184
 
182
- You can also open the folder. A folder opens the image it belongs to (an NDTiff dataset folder, a Harmony or CellVoyager measurement folder, a folder of Opera `.flex` files), or the one image it holds. An Opera Flex plate has no index file: any of its `.flex` files opens the whole folder as the plate (`multifile=False` reads only that file). A folder with several images raises an error that lists them.
185
+ You can also open the folder. A folder opens the image it belongs to (an NDTiff dataset folder, a Harmony or CellVoyager measurement folder, a folder of Opera `.flex` files, a Luxendo experiment), or the one image it holds. A folder that holds no image itself, such as a downloaded sample folder with the export one or two levels down, opens the one dataset found in its sub-folders. An Opera Flex plate has no index file: any of its `.flex` files opens the whole folder as the plate (`multifile=False` reads only that file). A folder of one NIS-Elements well-plate run opens as that plate (with `plates=False`, its files are several images). A folder with several images or datasets raises an error that lists them; an OME-Zarr store is refused with a pointer to `ome_zarr_pyramid`, which reads it.
183
186
 
184
187
  To sort many files at once, for example everything under a folder you want to convert, `group_files` says which files make one image:
185
188
 
@@ -192,7 +195,7 @@ for group in g.groups:
192
195
  g.singles # every other file, as given
193
196
  ```
194
197
 
195
- A file that a plate or companion index names is listed only in that index's group. When the main file of a multi-file image is missing, its group has `main=None`. Pass `plates=True` to group as `open(..., plates=True)` reads: a folder of ND2 files from one NIS-Elements well-plate run is then one group.
198
+ A file that a plate or companion index names is listed only in that index's group. When the main file of a multi-file image is missing, its group has `main=None`. It groups as `open()` reads, plates included (`plates=True`, the default): a folder of ND2 files from one NIS-Elements well-plate run is one group; `plates=False` groups as `open(..., plates=False)` reads.
196
199
 
197
200
  ## Axes
198
201
 
@@ -220,37 +223,47 @@ image.shape
220
223
  # (1, 3, 40, 5120, 6144)
221
224
  ```
222
225
 
223
- Other dimensions, such as illumination, emission, tile, etc., are represented as image indices rather than array axes, as discussed above.
226
+ Other dimensions, such as illumination, excitation, tile, etc., are represented as image indices rather than array axes, as discussed above.
224
227
 
225
- ### Changing axis order
228
+ ### Lambda scans
226
229
 
227
- You can request a specific axis order:
230
+ A lambda (spectral) scan is one image in every format. Its bands lie along `c`, a detector's bands next to each other in wavelength order, and each channel has its `emission_wavelength` and its wavelength in its name (`"HyD S 1 420 nm"`).
228
231
 
229
232
  ```python
230
- image = f.images[0].as_axes("tczyx")
233
+ f = micro_reader.open("scan.lif")
234
+ f.images[0].axes # 'tczyx'
235
+ [c.emission_wavelength for c in f.images[0].metadata.channels]
236
+
237
+ f = micro_reader.open("scan.lif", axes="native")
238
+ f.images[0].axes # 'tlczyx': a real lambda axis
239
+ f.images[0].metadata.index_values["emission"] # its wavelengths
231
240
  ```
232
241
 
233
- or when opening the file:
242
+ Asking for axes that name `l`, such as `axes="tlczyx"`, also gives the axis. An LSM stores its bands as channels, so it has no `l` axis; its channels carry their wavelengths. Excitation scans stay an image index.
243
+
244
+ `open` gives every image in this canonical order by default, whatever order the file stores: axes the file lacks are added with size 1 (a 2-D TIFF is `(1, 1, 1, Y, X)`) and the others are reordered. Reading costs the same either way.
245
+
246
+ ### Changing axis order
247
+
248
+ To keep the order the file stores (ICS `ctzyx`, Micro-Manager OME-TIFF `tzcyx`, a 2-D TIFF `yx`):
234
249
 
235
250
  ```python
236
- f = micro_reader.open("image.tif", axes="tczyx")
251
+ f = micro_reader.open("image.ics", axes="native")
237
252
  ```
238
253
 
239
- You can also request the canonical order:
254
+ You can also request a specific axis order:
240
255
 
241
256
  ```python
242
- f = micro_reader.open("image.tif", axes="canonical")
257
+ image = f.images[0].as_axes("tczyx")
243
258
  ```
244
259
 
245
- Canonical order is:
260
+ or when opening the file:
246
261
 
247
- ```text
248
- t c z y x
262
+ ```python
263
+ f = micro_reader.open("image.tif", axes="zyx")
249
264
  ```
250
265
 
251
- followed by `s` when RGB samples are present.
252
-
253
- Missing axes are added with size 1 and existing axes are reordered.
266
+ `axes="canonical"` (the default) is `t c z y x`, followed by `s` when RGB samples are kept (`samples="keep"`).
254
267
 
255
268
  ### Removing singleton axes
256
269
 
@@ -304,8 +317,8 @@ print(len(f.images))
304
317
  print(f.images[0].index)
305
318
  # {"series": 0, "sequence": 0}
306
319
 
307
- print(f.images[0].axes)
308
- # yx
320
+ print(f.images[0].axes, f.images[0].shape)
321
+ # tczyx (1, 1, 1, 16, 16) (axes="canonical", the default; axes=None gives yx)
309
322
  ```
310
323
 
311
324
  If you know that the pages represent `z`, you can tell the reader:
@@ -317,11 +330,11 @@ f = micro_reader.open(
317
330
  )
318
331
  ```
319
332
 
320
- The result is a single image with `zyx` axes.
333
+ The result is a single image with the 80 pages along `z`.
321
334
 
322
335
  ```python
323
- print(f.images[0].axes)
324
- # zyx
336
+ print(f.images[0].axes, f.images[0].shape)
337
+ # tczyx (1, 1, 80, 16, 16) (axes=None gives zyx)
325
338
  ```
326
339
 
327
340
 
@@ -409,6 +422,8 @@ f = micro_reader.open(
409
422
 
410
423
  CZI tiles are placed by the file's own stitching information, FluoView tiles by the tiling set's grid (where tiles overlap, the later tile is on top; areas without a tile read as 0). Other formats with tiles raise an error for `tiles="mosaic"`.
411
424
 
425
+ A format that has tiles it cannot place refuses `tiles="mosaic"` with an error. A file without tiles has nothing to place and opens as usual: Micro-Manager and MetaMorph stage positions are series, not tiles.
426
+
412
427
  `tiles="stitched"`, the name before 0.1.0, still works and warns.
413
428
 
414
429
  ## Plates
@@ -449,10 +464,10 @@ is its name (`well="B2"` is `well_name="B2"`).
449
464
 
450
465
  `f.outline()` shows the plates, wells and fields of a file.
451
466
 
452
- Zeiss CZI plates are read as plates with `plates=True`, and as series otherwise:
467
+ A plate in a generic format (a CZI, an ND2, an OME-TIFF or a companion with wells) is read as a plate too. `plates=False` reads it by its series instead:
453
468
 
454
469
  ```python
455
- f = micro_reader.open("plate.czi", plates=True)
470
+ f = micro_reader.open("plate.czi")
456
471
  ```
457
472
 
458
473
  Missing plane files are read as zeros, with a `MissingImagesWarning`. A field without any of its files is left out.
@@ -511,6 +526,7 @@ meta.channels[0]
511
526
  `micro-reader` follows a few important rules:
512
527
 
513
528
  * Units use OME-Zarr-compatible names such as `micrometer`, `nanometer`, and `millisecond`.
529
+ * Each value keeps the unit the file records (lengths above a millimetre become micrometres), so two formats can give the same quantity in different units, for example a time step in milliseconds (ND2, OIR) or in seconds (CZI, LIF). Convert to compare: `image.metadata.pixel_sizes["t"].to("s")`, `.to("um")`. Converting a length to a time, or to an unknown unit, raises `ValueError`.
514
530
  * Missing metadata is left as `None` or absent.
515
531
  * Pixel sizes are never invented as `1.0`.
516
532
  * Colours are not invented when the file does not provide them.
@@ -522,7 +538,7 @@ meta.channels[0]
522
538
 
523
539
  Where possible, format-specific coordinate conventions are converted to this common representation.
524
540
 
525
- The position is omitted when it cannot be determined reliably.
541
+ The position is omitted when it cannot be determined reliably. Micro-Manager's separate files and MetaMorph record where the stage was without saying which point of the field that is: their value is in `image.metadata.stage_position`, as recorded, not in `position`.
526
542
 
527
543
  ## Supported formats
528
544
 
@@ -538,13 +554,14 @@ The position is omitted when it cannot be determined reliably.
538
554
  | **VSI** | Olympus / Evident cellSens and VS120 / VS200 slide scanners |
539
555
  | **NDPIS** | Hamamatsu NanoZoomer fluorescence sets (one NDPI per channel) |
540
556
  | **Imaris** | `.ims`, with the `[hdf5]` extra |
541
- | **HDF5** | ilastik, BigDataViewer / BigStitcher, Luxendo, Fiji HDF5, with the `[hdf5]` extra |
557
+ | **HDF5** | ilastik, BigDataViewer / BigStitcher, Luxendo experiments and `.lux.h5`, Fiji HDF5, with the `[hdf5]` extra |
542
558
  | **ICS / IDS** | Image Cytometry Standard 1.0 and 2.0 (SVI Huygens and others) |
543
559
  | **MetaMorph** | `.nd` acquisitions and their TIFF / STK files |
544
560
  | **Micro-Manager 1.4**| image-per-file acquisitions (`metadata.txt` + one TIFF per plane) |
545
561
  | **DeltaVision** | `.dv`, `.r3d` (softWoRx, OMX, Priism) |
546
562
  | **MRC** | Common MRC modes and extended headers |
547
- | **Plates (HCS)** | Harmony, Columbus exports, MetaXpress, ImageXpress HCS.ai (`.jdce`), IN Cell, CellVoyager, Opera Flex, scanR and Cellomics (`.C01`) plates; with `plates=True`, CZI plates, OME companion and OME-TIFF plates (Yokogawa CQ1), ND2 files whose XY points are named after wells, and folders of ND2 files from one NIS-Elements well-plate run |
563
+ | **KLB** | Every time point and channel, bzip2 and zlib blocks |
564
+ | **Plates (HCS)** | Harmony, Columbus exports, MetaXpress, ImageXpress HCS.ai (`.jdce`), IN Cell, CellVoyager, Opera Flex, scanR and Cellomics (`.C01`) plates; also (by default; `plates=False` reads them by series) CZI plates, OME companion and OME-TIFF plates (Yokogawa CQ1), ND2 files whose XY points are named after wells, and folders of ND2 files from one NIS-Elements well-plate run |
548
565
  | **JPEG / PNG / BMP** | Common raster image formats |
549
566
 
550
567
  All formats use the same `File`, `Image`, axes, indexing, metadata, and reading interface wherever possible.
@@ -608,10 +625,14 @@ f = micro_reader.open("data_t001.ome.tif", multifile=False)
608
625
 
609
626
  If the companion is missing or does not list the file, opening the whole set raises an error that names `multifile=False`. With `multifile=False` the file is then read without pixel sizes, with a `CompanionWarning`. A TIFF whose metadata lives in another OME-TIFF (instead of a `.companion.ome`) works the same way, with that OME-TIFF in the companion's place.
610
627
 
611
- A companion that describes a plate (`Plate` / `Well` / `WellSample`) reads as one with `plates=True`, as a CZI plate does: `f.plates` lists its plates and wells, and its images are indexed by plate, acquisition, well and field. RGB images in a companion have a sample axis (`t c z y x s`), as RGB OME-TIFFs do.
628
+ A companion that describes a plate (`Plate` / `Well` / `WellSample`) reads as one, as a CZI plate does: `f.plates` lists its plates and wells, and its images are indexed by plate, acquisition, well and field. RGB images in a companion have a sample axis (`t c z y x s`), as RGB OME-TIFFs do.
612
629
 
613
630
  Some OME-TIFF sets keep the full OME-XML in their TIFF files instead of a companion. Opening one of those files also reads the whole set, and `multifile=False` reads only the planes stored in that file, with the set's metadata.
614
631
 
632
+ Any file of a plate works the same way: opening one plane file opens the plate, and `multifile=False` reads that file alone. Files that belong to the instrument and not to the plate's data, such as CellVoyager shading and dark-current images, open as themselves.
633
+
634
+ Leica's settings files (`FrameProperties`, `IOManagerConfiguation`) open their experiment. A `.xml` beside a `.lif` opens the `.lif`, and so does a `.lifext`; `multifile=False` reads the `.lifext` itself.
635
+
615
636
  Micro-Manager stacks always open whole, also with `multifile=False`: they split their files by size, so one file does not hold a complete part of the image.
616
637
 
617
638
  ### CZI
@@ -709,7 +730,7 @@ These need the `[hdf5]` extra as well.
709
730
 
710
731
  * **ilastik**: every dataset with ilastik's `axistags` (its exports, its input files, steinbock's `img`) is one image, in the dataset's own axis order. Pixel sizes are read where ilastik recorded units (`axis_units`, since 2025-09).
711
732
  * **BigDataViewer / BigStitcher**: the XML with its HDF5 file (also partitioned). One image per tile, angle and illumination, channels on `c` and time points on `t`, with the stored resolution levels. Opening the HDF5 file opens its XML.
712
- * **Luxendo / Bruker light-sheet** (`.lux.h5`): one stack per file. A whole acquisition opens through the Imaris file Luxendo writes next to it.
733
+ * **Luxendo / Bruker light-sheet**: an experiment folder (`raw/` with one folder of `.lux.h5` stacks per channel) opens as one image per stack and camera, with all time points and channels. Opening any part opens the whole experiment: the folder, a `.lux.h5`, or the `imaris.ims` / `bdv.xml` next to `raw/`. Files those link files name but `raw/` lacks read as zeros, with a warning. `multifile=False` reads one `.lux.h5` (or `.ims`) alone.
713
734
  * **Fiji HDF5** (the Vibez plugin): datasets with `element_size_um`; `t<n>/channel<n>` datasets make one `t c z y x` image.
714
735
 
715
736
  ### ICS / IDS
@@ -875,6 +896,14 @@ Applications work with the same `File` and `Image` model described in [How it wo
875
896
 
876
897
  The goal is that an application can open a microscopy file, identify the image it needs, read only the required pixels, and obtain standardized metadata without needing to understand the vendor-specific file structure.
877
898
 
899
+ ## Compatibility with eubi-bridge
900
+
901
+ micro-reader 0.1.0 renamed the image index `scene` to `series`. eubi-bridge 0.1.3 to 0.2.x and eubi-bridge-lite below 0.3.0 still read `scene`, so `import micro_reader` refuses to load next to them. It raises an `ImportError` that names the installed eubi version and the two ways out: upgrade eubi (`pip install -U 'eubi-bridge>=0.3.0'`, or the same for `eubi-bridge-lite`), or keep that eubi with the micro-reader it was built for (`pip install 'micro-reader<0.1'`). The check reads the versions of the installed packages; it never imports eubi. An editable install of eubi is checked like any other: its recorded version can be out of date, so the message then says to reinstall it.
902
+
903
+ To skip the check, set the environment variable `MICRO_READER_SKIP_EUBI_CHECK=1`. Only the value `1` skips it; any other value keeps the check. micro-reader then warns once that the check was skipped. This is meant for work on an eubi checkout, not for normal use.
904
+
905
+ The check will be removed when every eubi-bridge / eubi-bridge-lite line below 0.3.0 has left the support window, checked by the release version matrix; not before.
906
+
878
907
  ## License
879
908
 
880
909
  MIT
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "micro-reader"
9
- version = "0.3.0"
9
+ version = "0.4.0"
10
10
  description = "Low-level, memory-bounded pixel reading for microscopy formats: read exactly the rectangle you ask for."
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.11"
@@ -16,7 +16,7 @@ authors = [{ name = "Bugra Oezdemir", email = "bugraa.ozdemir@gmail.com" }]
16
16
  keywords = ["microscopy", "bioimage", "image-io", "memory-bounded", "tiff", "ome-tiff", "czi", "nd2",
17
17
  "lif", "leica", "oir", "imaris", "ims", "mrc", "lsm", "eer", "cryo-em"]
18
18
  classifiers = [
19
- "Development Status :: 2 - Pre-Alpha",
19
+ "Development Status :: 3 - Alpha",
20
20
  "Intended Audience :: Science/Research",
21
21
  "Programming Language :: Python :: 3",
22
22
  "Programming Language :: Python :: 3.11",
@@ -7,8 +7,9 @@ axes an image has: formats with an order of their own (TIFF, MRC) in that
7
7
  order, the others in ``tczyx``. An axes image rearranges them without
8
8
  guessing:
9
9
 
10
- - ``axes=None`` keeps the image's order; ``"canonical"`` gives ``t c z y x``,
11
- then ``s``; any other string is the exact axes wanted;
10
+ - ``axes=None`` keeps the image's order (``open``'s ``"native"``);
11
+ ``"canonical"`` gives ``t c z y x``, then ``s`` (``open``'s default since
12
+ 0.4.0, decision MR-4); any other string is the exact axes wanted;
12
13
  - axes the file lacks are inserted with size 1;
13
14
  - RGB samples ``s`` stay a trailing ``s`` when asked for, or are folded into
14
15
  the channels with ``samples="channels"`` (channel ``k`` = sample ``k % S``
@@ -50,8 +51,9 @@ def folded_channels(channels, n: int, samples: int) -> tuple:
50
51
  (Bio-Formats-style: one channel per sample) is kept; otherwise each
51
52
  channel becomes "<name> R" / "G" / "B" ("A"), tinted from ``SAMPLE_COLORS``,
52
53
  keeping its wavelengths; an unnamed one among several is "Channel {c} R";
53
- a single unnamed channel gives unnamed, tinted channels; sample counts
54
- without labels are numbered ("<name> 0") and untinted."""
54
+ a single unnamed channel gives unnamed, tinted channels; two channels of
55
+ one name are "<name> {c} R"; sample counts without labels are numbered
56
+ ("<name> 0") and untinted."""
55
57
  from ._metadata import Channel
56
58
  channels = tuple(channels)
57
59
  if len(channels) == n * samples:
@@ -61,10 +63,13 @@ def folded_channels(channels, n: int, samples: int) -> tuple:
61
63
  labels = SAMPLE_LABELS.get(samples, tuple(str(i) for i in range(samples)))
62
64
  colors = SAMPLE_COLORS.get(samples) or (None,) * samples
63
65
  single_unnamed = n == 1 and not channels[0].name
66
+ names = [ch.name for ch in channels]
64
67
  out = []
65
68
  for c, ch in enumerate(channels):
69
+ # two channels of one name (an RGB ND2): told apart by their position
70
+ label = ch.name if names.count(ch.name) == 1 else f"{ch.name} {c}"
66
71
  for i in range(samples):
67
- name = None if single_unnamed else f"{ch.name or f'Channel {c}'} {labels[i]}"
72
+ name = None if single_unnamed else f"{label if ch.name else f'Channel {c}'} {labels[i]}"
68
73
  out.append(Channel(name=name, color=colors[i],
69
74
  emission_wavelength=ch.emission_wavelength,
70
75
  excitation_wavelength=ch.excitation_wavelength))
@@ -96,6 +101,8 @@ class AxesImage(Image):
96
101
  if left:
97
102
  hint = ("; samples='channels' folds RGB samples into the channels"
98
103
  if "s" in left else "")
104
+ if "l" in left:
105
+ hint += "; opened without axes='native', a lambda scan has its bands along c"
99
106
  raise ValueError(f"{source.name!r} has axes {''.join(left)!r} that {axes!r} cannot "
100
107
  f"hold{hint}")
101
108
  if self._fold and "c" not in axes:
@@ -130,7 +137,7 @@ class AxesImage(Image):
130
137
  return STANDARD + ("s" if "s" in free and not fold else "")
131
138
  if len(set(axes)) != len(axes) or not axes.isalpha():
132
139
  raise ValueError(f"axes {axes!r}: one letter per axis, each once")
133
- stray = sorted(set(axes) - set(CANONICAL))
140
+ stray = sorted(set(axes) - set(CANONICAL) - set(names))
134
141
  if stray:
135
142
  raise ValueError(f"axes {axes!r}: {stray} are not array axes (arrays have "
136
143
  f"{CANONICAL!r}; other dimensions are image indices)")
@@ -201,11 +208,17 @@ class AxesImage(Image):
201
208
  @property
202
209
  def metadata(self):
203
210
  """The native image's metadata; with samples folded into the channels,
204
- one channel per folded channel (``folded_channels``)."""
211
+ one channel per folded channel (``folded_channels``). The sizes of axes this view
212
+ does not have are left out (``squeeze`` dropped them: a ``yx`` view listed the
213
+ file's z size; Bugra, 2026-11-04); an axis it has keeps what the file gives,
214
+ a size-1 one too, and none is made up for an axis the view adds."""
215
+ from dataclasses import replace
205
216
  meta = self.source.metadata
217
+ sizes = dict(meta.pixel_sizes or {})
218
+ if any(axis not in self.axes for axis in sizes):
219
+ meta = replace(meta, pixel_sizes={a: q for a, q in sizes.items() if a in self.axes})
206
220
  if not self._fold:
207
221
  return meta
208
- from dataclasses import replace
209
222
  n = self.source.shape[self._names.index("c")] if "c" in self._names else 1
210
223
  return replace(meta, channels=folded_channels(meta.channels, n, self._samples))
211
224