micro-reader 0.0.3__tar.gz → 0.0.4__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 (50) hide show
  1. {micro_reader-0.0.3 → micro_reader-0.0.4}/PKG-INFO +73 -5
  2. {micro_reader-0.0.3 → micro_reader-0.0.4}/README.md +72 -4
  3. {micro_reader-0.0.3 → micro_reader-0.0.4}/pyproject.toml +1 -1
  4. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/__init__.py +5 -4
  5. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_base.py +52 -1
  6. micro_reader-0.0.4/src/micro_reader/_cfb.py +200 -0
  7. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_errors.py +6 -0
  8. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_fileset.py +49 -1
  9. micro_reader-0.0.4/src/micro_reader/_groups.py +216 -0
  10. micro_reader-0.0.4/src/micro_reader/_parts.py +188 -0
  11. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_registry.py +33 -10
  12. micro_reader-0.0.4/src/micro_reader/backends/_ets.py +149 -0
  13. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/_tiff_metadata.py +228 -3
  14. micro_reader-0.0.4/src/micro_reader/backends/_tiff_variants.py +239 -0
  15. micro_reader-0.0.4/src/micro_reader/backends/_vsi_tree.py +154 -0
  16. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/companion.py +78 -5
  17. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/cv7000.py +67 -0
  18. micro_reader-0.0.4/src/micro_reader/backends/dv.py +372 -0
  19. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/harmony.py +49 -0
  20. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/incell.py +7 -0
  21. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/lif.py +19 -25
  22. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/metaxpress.py +39 -1
  23. micro_reader-0.0.4/src/micro_reader/backends/oif.py +479 -0
  24. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/oir.py +57 -7
  25. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/tiff.py +254 -26
  26. micro_reader-0.0.4/src/micro_reader/backends/vsi.py +448 -0
  27. {micro_reader-0.0.3 → micro_reader-0.0.4}/.gitignore +0 -0
  28. {micro_reader-0.0.3 → micro_reader-0.0.4}/LICENSE +0 -0
  29. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_axes.py +0 -0
  30. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_codecs.py +0 -0
  31. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_index.py +0 -0
  32. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_io.py +0 -0
  33. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_jpeg_restarts.py +0 -0
  34. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_memory.py +0 -0
  35. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_metadata.py +0 -0
  36. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_outline.py +0 -0
  37. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_plate.py +0 -0
  38. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_pool.py +0 -0
  39. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/_stream.py +0 -0
  40. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/__init__.py +0 -0
  41. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/_czi_metadata.py +0 -0
  42. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/_lif_metadata.py +0 -0
  43. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/_nd2_legacy.py +0 -0
  44. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/_nd2_metadata.py +0 -0
  45. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/_nd2_structure.py +0 -0
  46. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/czi.py +0 -0
  47. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/ims.py +0 -0
  48. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/mrc.py +0 -0
  49. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/nd2.py +0 -0
  50. {micro_reader-0.0.3 → micro_reader-0.0.4}/src/micro_reader/backends/raster.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: micro-reader
3
- Version: 0.0.3
3
+ Version: 0.0.4
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
@@ -54,7 +54,7 @@ Description-Content-Type: text/markdown
54
54
 
55
55
  **Memory-bounded pixel reader for microscopy images.**
56
56
 
57
- `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, Imaris, MRC, and common raster formats.
57
+ `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, MetaXpress, IN Cell, CellVoyager).
58
58
 
59
59
  > **Status: pre-alpha (0.0.x)**
60
60
  >
@@ -213,6 +213,31 @@ resolution
213
213
 
214
214
  Only dimensions actually present in a file appear in an image's index.
215
215
 
216
+ ### Files made of several files
217
+
218
+ Some formats always come as several files: an OIR file with its continuation files (`name_00001`), a Micro-Manager stack split over `..._MMStack_Pos0.ome.tif`, `..._MMStack_Pos0_1.ome.tif`, an NDTiff folder, an Olympus OIF with its `.oif.files` folder, an Olympus VSI with its `_name_` folder, a DeltaVision image with its softWoRx `.log`, a plate's side files (Harmony `Assaylayout/` and `FFC_Profile/`, CellVoyager settings files, MetaXpress `_thumb` previews). Opening any of these files opens the whole image:
219
+
220
+ ```python
221
+ f = micro_reader.open("cells_R3D.dv.log")
222
+ f.path # the main file: "cells_R3D.dv"
223
+ f.opened # what was opened: "cells_R3D.dv.log"
224
+ ```
225
+
226
+ You can also open the folder. A folder opens the image it belongs to (an NDTiff dataset folder, a Harmony or CellVoyager measurement folder), or the one image it holds. A folder with several images raises an error that lists them.
227
+
228
+ To sort many files at once, for example everything under a folder you want to convert, `group_files` says which files make one image:
229
+
230
+ ```python
231
+ g = micro_reader.group_files("experiment/")
232
+ for group in g.groups:
233
+ group.main # the file to open: "slide.vsi", "plate/Index.idx.xml"
234
+ group.parts # the other files of a multi-file image
235
+ group.members # for a plate or companion index: the files its images come from
236
+ g.singles # every other file, as given
237
+ ```
238
+
239
+ 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`.
240
+
216
241
  ## Axes
217
242
 
218
243
  An image array uses these axes:
@@ -406,7 +431,7 @@ img_scene0_level2 = f.select(scene=0, resolution=2)[0]
406
431
  Supported pyramids include:
407
432
 
408
433
  * **Imaris**: all stored levels
409
- * **TIFF**: OME-TIFF and other TIFF pyramids such as SVS and NDPI
434
+ * **TIFF**: OME-TIFF and whole-slide pyramids (SVS, NDPI, SCN, BIF, QPTIFF, Philips)
410
435
  * **CZI**: stored pyramids of whole scenes
411
436
 
412
437
  Other formats expose a single resolution level.
@@ -459,6 +484,12 @@ f = micro_reader.open("plate.czi", plates=True)
459
484
 
460
485
  Missing plane files are read as zeros, with a `MissingImagesWarning`. A field without any of its files is left out.
461
486
 
487
+ A plane file whose size or pixel type differs from what the index describes raises an error when it is read. To find such files before reading anything, check them all at once:
488
+
489
+ ```python
490
+ f.check_members() # opens every plane file once; returns how many were checked
491
+ ```
492
+
462
493
  When the channels of a field have different z sampling, `image.parts` gives one image per group of channels that share it.
463
494
 
464
495
  ## Metadata
@@ -524,13 +555,16 @@ The position is omitted when it cannot be determined reliably.
524
555
 
525
556
  | Format | Notes |
526
557
  | -------------------- | ----------------------------------------------------- |
527
- | **TIFF / BigTIFF** | TIFF, OME-TIFF, ImageJ, LSM, STK and TIFF pyramids |
558
+ | **TIFF / BigTIFF** | TIFF, OME-TIFF, ImageJ, LSM, STK, Micro-Manager, whole-slide TIFFs |
528
559
  | **OME companion** | `.companion.ome` sets of OME-TIFF files |
529
560
  | **CZI** | Zeiss CZI, including mosaic tiles and stitched scenes |
530
561
  | **ND2** | Nikon ND2, including legacy JPEG 2000 ND2 |
531
562
  | **Leica** | LIF, LOF, LIFEXT, XLIF, XLEF, XLCF |
532
563
  | **OIR** | Olympus / Evident FluoView |
564
+ | **OIB / OIF** | Olympus FluoView FV1000 / FV1200 / FV3000 |
565
+ | **VSI** | Olympus / Evident cellSens and VS120 / VS200 slide scanners |
533
566
  | **Imaris** | `.ims`, with the `[ims]` extra |
567
+ | **DeltaVision** | `.dv`, `.r3d` (softWoRx, OMX, Priism) |
534
568
  | **MRC** | Common MRC modes and extended headers |
535
569
  | **Plates (HCS)** | Harmony, MetaXpress, IN Cell and CellVoyager plates |
536
570
  | **JPEG / PNG / BMP** | Common raster image formats |
@@ -549,6 +583,14 @@ Supported TIFF formats include:
549
583
  * ImageJ
550
584
  * Zeiss LSM
551
585
  * MetaMorph STK
586
+ * Micro-Manager stacks (MMStack and NDTiff), also when one acquisition is spread over several files
587
+ * Whole-slide TIFFs: Aperio SVS, Hamamatsu NDPI, Leica SCN, Ventana BIF, PerkinElmer / Akoya QPTIFF and Philips
588
+
589
+ Micro-Manager stage positions are scenes. NDTiff channel names and pixel sizes come from the dataset's index and frame metadata.
590
+
591
+ A whole-slide file gives the slide first, then the other pictures it stores as further scenes, as Bio-Formats lists them: `label` and `macro` (overview), and for some formats `thumbnail` (QPTIFF) or `probability` (BIF tissue map). Pixel sizes, positions and channel names come from each vendor's own metadata.
592
+
593
+ Ventana BIF scans store overlapping tiles. They are composed as Roche's description of the format places them. Files from scanners other than the VENTANA DP 200 are not supported.
552
594
 
553
595
  Compressed TIFF data is decoded only for the strips or tiles needed by the requested region.
554
596
 
@@ -580,7 +622,15 @@ An OME companion file (`.companion.ome`) describes images stored in separate TIF
580
622
  f = micro_reader.open("data.companion.ome")
581
623
  ```
582
624
 
583
- Opening one of the TIFF files on its own reads only that file, with its pixel sizes, channels and position taken from the companion. If the companion is missing, the file is read without pixel sizes, with a `CompanionWarning`.
625
+ Opening one of the TIFF files on its own reads only that file, with its pixel sizes, channels and position taken from the companion. If the companion is missing, the file is read without pixel sizes, with a `CompanionWarning`. A TIFF whose metadata lives in another OME-TIFF (instead of a `.companion.ome`) takes it from that file in the same way.
626
+
627
+ Some OME-TIFF sets keep the full OME-XML in their TIFF files instead of a companion. Opening one of those files reads the whole set. To read only the planes stored in that file, with the set's metadata:
628
+
629
+ ```python
630
+ f = micro_reader.open("set_t001.ome.tif", multifile=False)
631
+ ```
632
+
633
+ 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.
584
634
 
585
635
  ### CZI
586
636
 
@@ -643,6 +693,18 @@ These are represented using the common image/index model.
643
693
 
644
694
  Olympus / Evident FluoView OIR files are supported, including companion files and spectral scans.
645
695
 
696
+ ### Olympus OIB and OIF
697
+
698
+ Older FluoView files come in two forms with the same content: an OIB is one file, an OIF is a `.oif` settings file plus a `.oif.files` folder with one TIFF per plane. Both open as one `t c z y x` image, with pixel sizes, z and time steps, channel names, colours and wavelengths. Opening any file in the `.oif.files` folder opens the OIF.
699
+
700
+ ### Olympus VSI
701
+
702
+ A cellSens VSI is a `.vsi` file plus a `_<name>_` folder that holds the pixels (`stack1/frame_t.ets`, `stack10000/frame_t.ets`, ...). The images are listed as Bio-Formats lists them: one per scanned area, named as in cellSens, with the slide label (`label`) and overview (`overview`) when the scanner took them, and the slide photo (`macro`) last. Each scanned area keeps its resolution levels.
703
+
704
+ Images are `t c z y x` (`t c z y x s` for brightfield RGB scans), with pixel sizes, z and time steps, and each channel's name, colour and emission wavelength. Stage positions are not read.
705
+
706
+ Opening the `_<name>_` folder, a `stack` folder or a file in it opens the `.vsi`. An `.ets` file whose `.vsi` is missing is read alone, with a warning, when it is a plain 2-D image; its size is then rounded up to whole tiles.
707
+
646
708
  ### Imaris
647
709
 
648
710
  Install Imaris support with:
@@ -655,6 +717,12 @@ Imaris resolution level 0 is exposed at the actual image size, with Imaris chunk
655
717
 
656
718
  Chunked compression is decoded in parallel.
657
719
 
720
+ ### DeltaVision
721
+
722
+ DeltaVision files (`.dv`, `.r3d`, `.r3d_d3d`, also deconvolved and projected ones) are recognised by their content, whatever their extension. The image keeps the file's own order of time, waves and z (for example `ctzyx`).
723
+
724
+ Pixel sizes, and each channel's emission and excitation wavelengths, are read from the file. Channel names come from the softWoRx acquisition log next to it (`<file>.log`); a deconvolved or projected file takes the names from the log of the raw file it was made from. Stage positions are not read.
725
+
658
726
  ### MRC
659
727
 
660
728
  Supported MRC modes include:
@@ -2,7 +2,7 @@
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, Imaris, MRC, and common raster formats.
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, MetaXpress, IN Cell, CellVoyager).
6
6
 
7
7
  > **Status: pre-alpha (0.0.x)**
8
8
  >
@@ -161,6 +161,31 @@ resolution
161
161
 
162
162
  Only dimensions actually present in a file appear in an image's index.
163
163
 
164
+ ### Files made of several files
165
+
166
+ Some formats always come as several files: an OIR file with its continuation files (`name_00001`), a Micro-Manager stack split over `..._MMStack_Pos0.ome.tif`, `..._MMStack_Pos0_1.ome.tif`, an NDTiff folder, an Olympus OIF with its `.oif.files` folder, an Olympus VSI with its `_name_` folder, a DeltaVision image with its softWoRx `.log`, a plate's side files (Harmony `Assaylayout/` and `FFC_Profile/`, CellVoyager settings files, MetaXpress `_thumb` previews). Opening any of these files opens the whole image:
167
+
168
+ ```python
169
+ f = micro_reader.open("cells_R3D.dv.log")
170
+ f.path # the main file: "cells_R3D.dv"
171
+ f.opened # what was opened: "cells_R3D.dv.log"
172
+ ```
173
+
174
+ You can also open the folder. A folder opens the image it belongs to (an NDTiff dataset folder, a Harmony or CellVoyager measurement folder), or the one image it holds. A folder with several images raises an error that lists them.
175
+
176
+ To sort many files at once, for example everything under a folder you want to convert, `group_files` says which files make one image:
177
+
178
+ ```python
179
+ g = micro_reader.group_files("experiment/")
180
+ for group in g.groups:
181
+ group.main # the file to open: "slide.vsi", "plate/Index.idx.xml"
182
+ group.parts # the other files of a multi-file image
183
+ group.members # for a plate or companion index: the files its images come from
184
+ g.singles # every other file, as given
185
+ ```
186
+
187
+ 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`.
188
+
164
189
  ## Axes
165
190
 
166
191
  An image array uses these axes:
@@ -354,7 +379,7 @@ img_scene0_level2 = f.select(scene=0, resolution=2)[0]
354
379
  Supported pyramids include:
355
380
 
356
381
  * **Imaris**: all stored levels
357
- * **TIFF**: OME-TIFF and other TIFF pyramids such as SVS and NDPI
382
+ * **TIFF**: OME-TIFF and whole-slide pyramids (SVS, NDPI, SCN, BIF, QPTIFF, Philips)
358
383
  * **CZI**: stored pyramids of whole scenes
359
384
 
360
385
  Other formats expose a single resolution level.
@@ -407,6 +432,12 @@ f = micro_reader.open("plate.czi", plates=True)
407
432
 
408
433
  Missing plane files are read as zeros, with a `MissingImagesWarning`. A field without any of its files is left out.
409
434
 
435
+ A plane file whose size or pixel type differs from what the index describes raises an error when it is read. To find such files before reading anything, check them all at once:
436
+
437
+ ```python
438
+ f.check_members() # opens every plane file once; returns how many were checked
439
+ ```
440
+
410
441
  When the channels of a field have different z sampling, `image.parts` gives one image per group of channels that share it.
411
442
 
412
443
  ## Metadata
@@ -472,13 +503,16 @@ The position is omitted when it cannot be determined reliably.
472
503
 
473
504
  | Format | Notes |
474
505
  | -------------------- | ----------------------------------------------------- |
475
- | **TIFF / BigTIFF** | TIFF, OME-TIFF, ImageJ, LSM, STK and TIFF pyramids |
506
+ | **TIFF / BigTIFF** | TIFF, OME-TIFF, ImageJ, LSM, STK, Micro-Manager, whole-slide TIFFs |
476
507
  | **OME companion** | `.companion.ome` sets of OME-TIFF files |
477
508
  | **CZI** | Zeiss CZI, including mosaic tiles and stitched scenes |
478
509
  | **ND2** | Nikon ND2, including legacy JPEG 2000 ND2 |
479
510
  | **Leica** | LIF, LOF, LIFEXT, XLIF, XLEF, XLCF |
480
511
  | **OIR** | Olympus / Evident FluoView |
512
+ | **OIB / OIF** | Olympus FluoView FV1000 / FV1200 / FV3000 |
513
+ | **VSI** | Olympus / Evident cellSens and VS120 / VS200 slide scanners |
481
514
  | **Imaris** | `.ims`, with the `[ims]` extra |
515
+ | **DeltaVision** | `.dv`, `.r3d` (softWoRx, OMX, Priism) |
482
516
  | **MRC** | Common MRC modes and extended headers |
483
517
  | **Plates (HCS)** | Harmony, MetaXpress, IN Cell and CellVoyager plates |
484
518
  | **JPEG / PNG / BMP** | Common raster image formats |
@@ -497,6 +531,14 @@ Supported TIFF formats include:
497
531
  * ImageJ
498
532
  * Zeiss LSM
499
533
  * MetaMorph STK
534
+ * Micro-Manager stacks (MMStack and NDTiff), also when one acquisition is spread over several files
535
+ * Whole-slide TIFFs: Aperio SVS, Hamamatsu NDPI, Leica SCN, Ventana BIF, PerkinElmer / Akoya QPTIFF and Philips
536
+
537
+ Micro-Manager stage positions are scenes. NDTiff channel names and pixel sizes come from the dataset's index and frame metadata.
538
+
539
+ A whole-slide file gives the slide first, then the other pictures it stores as further scenes, as Bio-Formats lists them: `label` and `macro` (overview), and for some formats `thumbnail` (QPTIFF) or `probability` (BIF tissue map). Pixel sizes, positions and channel names come from each vendor's own metadata.
540
+
541
+ Ventana BIF scans store overlapping tiles. They are composed as Roche's description of the format places them. Files from scanners other than the VENTANA DP 200 are not supported.
500
542
 
501
543
  Compressed TIFF data is decoded only for the strips or tiles needed by the requested region.
502
544
 
@@ -528,7 +570,15 @@ An OME companion file (`.companion.ome`) describes images stored in separate TIF
528
570
  f = micro_reader.open("data.companion.ome")
529
571
  ```
530
572
 
531
- Opening one of the TIFF files on its own reads only that file, with its pixel sizes, channels and position taken from the companion. If the companion is missing, the file is read without pixel sizes, with a `CompanionWarning`.
573
+ Opening one of the TIFF files on its own reads only that file, with its pixel sizes, channels and position taken from the companion. If the companion is missing, the file is read without pixel sizes, with a `CompanionWarning`. A TIFF whose metadata lives in another OME-TIFF (instead of a `.companion.ome`) takes it from that file in the same way.
574
+
575
+ Some OME-TIFF sets keep the full OME-XML in their TIFF files instead of a companion. Opening one of those files reads the whole set. To read only the planes stored in that file, with the set's metadata:
576
+
577
+ ```python
578
+ f = micro_reader.open("set_t001.ome.tif", multifile=False)
579
+ ```
580
+
581
+ 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.
532
582
 
533
583
  ### CZI
534
584
 
@@ -591,6 +641,18 @@ These are represented using the common image/index model.
591
641
 
592
642
  Olympus / Evident FluoView OIR files are supported, including companion files and spectral scans.
593
643
 
644
+ ### Olympus OIB and OIF
645
+
646
+ Older FluoView files come in two forms with the same content: an OIB is one file, an OIF is a `.oif` settings file plus a `.oif.files` folder with one TIFF per plane. Both open as one `t c z y x` image, with pixel sizes, z and time steps, channel names, colours and wavelengths. Opening any file in the `.oif.files` folder opens the OIF.
647
+
648
+ ### Olympus VSI
649
+
650
+ A cellSens VSI is a `.vsi` file plus a `_<name>_` folder that holds the pixels (`stack1/frame_t.ets`, `stack10000/frame_t.ets`, ...). The images are listed as Bio-Formats lists them: one per scanned area, named as in cellSens, with the slide label (`label`) and overview (`overview`) when the scanner took them, and the slide photo (`macro`) last. Each scanned area keeps its resolution levels.
651
+
652
+ Images are `t c z y x` (`t c z y x s` for brightfield RGB scans), with pixel sizes, z and time steps, and each channel's name, colour and emission wavelength. Stage positions are not read.
653
+
654
+ Opening the `_<name>_` folder, a `stack` folder or a file in it opens the `.vsi`. An `.ets` file whose `.vsi` is missing is read alone, with a warning, when it is a plain 2-D image; its size is then rounded up to whole tiles.
655
+
594
656
  ### Imaris
595
657
 
596
658
  Install Imaris support with:
@@ -603,6 +665,12 @@ Imaris resolution level 0 is exposed at the actual image size, with Imaris chunk
603
665
 
604
666
  Chunked compression is decoded in parallel.
605
667
 
668
+ ### DeltaVision
669
+
670
+ DeltaVision files (`.dv`, `.r3d`, `.r3d_d3d`, also deconvolved and projected ones) are recognised by their content, whatever their extension. The image keeps the file's own order of time, waves and z (for example `ctzyx`).
671
+
672
+ Pixel sizes, and each channel's emission and excitation wavelengths, are read from the file. Channel names come from the softWoRx acquisition log next to it (`<file>.log`); a deconvolved or projected file takes the names from the log of the raw file it was made from. Stage positions are not read.
673
+
606
674
  ### MRC
607
675
 
608
676
  Supported MRC modes include:
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "micro-reader"
9
- version = "0.0.3"
9
+ version = "0.0.4"
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"
@@ -7,22 +7,23 @@
7
7
  from ._base import INDEX_NAMES, PLATE_INDICES, SAMPLED_INDICES, File, Image, ReadStats
8
8
  from ._metadata import Channel, Metadata, Quantity
9
9
  from ._plate import Acquisition, Field, Plate, Well
10
- from ._errors import (CompanionWarning, CorruptFileError, MicroReaderError,
10
+ from ._errors import (CompanionWarning, CompositeWarning, CorruptFileError, MicroReaderError,
11
11
  MissingImagesWarning, UnsupportedFormatError)
12
12
  from ._memory import MemoryLimitError, memory_limit, set_memory_limit
13
13
  from ._io import cache_limit, cached_bytes, set_cache_limit
14
14
  from ._pool import decode_threads, set_decode_threads
15
15
  from ._registry import BACKENDS, open # noqa: A004 - the public entry point
16
+ from ._groups import FileGroup, Grouping, group_files
16
17
  from .backends.czi import DamagedFileWarning
17
18
 
18
- __version__ = "0.0.3"
19
+ __version__ = "0.0.4"
19
20
 
20
21
  __all__ = [
21
- "open", "File", "Image", "INDEX_NAMES", "PLATE_INDICES", "SAMPLED_INDICES", "ReadStats",
22
+ "open", "group_files", "FileGroup", "Grouping", "File", "Image", "INDEX_NAMES", "PLATE_INDICES", "SAMPLED_INDICES", "ReadStats",
22
23
  "BACKENDS", "Metadata", "Channel", "Quantity", "Plate", "Well", "Field", "Acquisition",
23
24
  "decode_threads", "set_decode_threads", "memory_limit", "set_memory_limit",
24
25
  "cache_limit", "set_cache_limit", "cached_bytes",
25
26
  "MicroReaderError", "UnsupportedFormatError", "CorruptFileError", "MemoryLimitError", "DamagedFileWarning",
26
- "MissingImagesWarning", "CompanionWarning",
27
+ "MissingImagesWarning", "CompanionWarning", "CompositeWarning",
27
28
  "__version__",
28
29
  ]
@@ -247,9 +247,17 @@ SAMPLED_INDICES = ("illumination", "phase", "block", "emission", "excitation", "
247
247
  "sequence")
248
248
 
249
249
 
250
+ def _counted(result, bad: list) -> int:
251
+ """One member checked (``File.check_members``): *result* is 1, or the
252
+ text saying why it does not fit (kept in *bad*)."""
253
+ if result != 1:
254
+ bad.append(result)
255
+ return 1
256
+
257
+
250
258
  class File:
251
259
  """An opened file: its images, the backend that reads them (``backend``:
252
- "tiff", "czi", "nd2", "lif", "oir", "ims", "mrc", "raster") and the
260
+ "tiff", "czi", "nd2", "lif", "oir", "oif", "vsi", "ims", "dv", "mrc", "raster") and the
253
261
  format variant it is (``format``: e.g. "ome-tiff", "imagej", "lsm",
254
262
  "nd2-legacy", "lof", "xlef", "png"). Every format opens as a File; a
255
263
  backend's own class is an implementation detail."""
@@ -257,7 +265,10 @@ class File:
257
265
  backend: str = "base"
258
266
 
259
267
  def __init__(self, path: str, images: Sequence[Image], plates: Sequence = ()):
268
+ #: the file read: for a composite format its main file (docs/COMPOSITE.md)
260
269
  self.path = str(path)
270
+ #: the path ``open()`` was given: a part of a composite, a folder, or ``path``
271
+ self.opened = self.path
261
272
  self.images = list(images)
262
273
  #: the plates the images belong to (``_plate.Plate``), like ``images``
263
274
  #: a list; empty for a file that is not a plate (docs/HCS.md)
@@ -323,6 +334,46 @@ class File:
323
334
  return [image for image in self.images
324
335
  if all(image.index.get(name) == value for name, value in index.items())]
325
336
 
337
+ def _member_files(self) -> set:
338
+ """The other files this file's images are read from -- a file set's
339
+ members -- for ``group_files``. Empty for a file that reads only itself."""
340
+ out = set()
341
+ for image in self.images:
342
+ image = getattr(image, "source", image)
343
+ for each in [image] + [p for p in image.parts if p is not image]:
344
+ paths = getattr(each, "_member_paths", None)
345
+ if paths is not None:
346
+ out.update(paths())
347
+ out.discard(self.path)
348
+ return out
349
+
350
+ def check_members(self) -> int:
351
+ """For a file set (a plate, an OME companion set): open every member
352
+ file once and check that it is the plane its index describes (size,
353
+ type, one plane). Reading makes the same check plane by plane, at no
354
+ cost; this finds a file that does not fit before anything is read, at
355
+ the cost of opening every member. Returns the number of members
356
+ checked (0 for a file that is not a file set); raises
357
+ ``CorruptFileError`` naming the members that do not fit."""
358
+ from . import _pool
359
+ from ._errors import CorruptFileError
360
+ seen, bad = set(), []
361
+
362
+ def tasks(): # lazy: a plate may have millions of members
363
+ for image in self.images:
364
+ image = getattr(image, "source", image) # open(axes=...) wraps it
365
+ for each in [image] + [p for p in image.parts if p is not image]:
366
+ check = getattr(each, "_check_members", None)
367
+ if check is not None:
368
+ for task in check(seen):
369
+ yield lambda task=task: _counted(task(), bad)
370
+ checked = _pool.run(tasks())
371
+ if bad:
372
+ shown = "; ".join(bad[:5]) + (f"; ... ({len(bad)} in all)" if len(bad) > 5 else "")
373
+ raise CorruptFileError(f"{self.path!r}: {len(bad)} of {checked} member files do "
374
+ f"not fit the index: {shown}")
375
+ return checked
376
+
326
377
  def close(self) -> None:
327
378
  for image in self.images:
328
379
  image.close()
@@ -0,0 +1,200 @@
1
+ """Compound File Binary (OLE2) containers: Olympus OIB, later Zeiss ZVI.
2
+
3
+ Written from Microsoft's published specification [MS-CFB] (Compound File
4
+ Binary File Format); oiffile's ``CompoundFile`` (Gohlke, BSD-3) was a
5
+ reference. A container is a little file system: fixed-size sectors, a FAT
6
+ chaining them, a directory of storages (folders) and streams (files); streams
7
+ smaller than the mini-stream cutoff live in 64-byte mini sectors inside the
8
+ root entry's stream.
9
+
10
+ Only what reading needs: the directory (stream paths ``Storage/name``), small
11
+ streams read whole, and for a large stream its *runs* -- (offset in the
12
+ stream, offset in the file, length) of each stretch of consecutive sectors --
13
+ so that any byte range of the stream is read with positioned reads, never the
14
+ whole stream.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import bisect
19
+ import builtins
20
+ import struct
21
+
22
+ from ._errors import CorruptFileError
23
+
24
+ MAGIC = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"
25
+ FREESECT, ENDOFCHAIN, FATSECT, DIFSECT = 0xFFFFFFFF, 0xFFFFFFFE, 0xFFFFFFFD, 0xFFFFFFFC
26
+ MAXREGSECT = 0xFFFFFFFA
27
+ NOSTREAM = 0xFFFFFFFF
28
+ STORAGE, STREAM, ROOT = 1, 2, 5
29
+ #: directory entries are 128 bytes
30
+ DIR_ENTRY = struct.Struct("<64sHBBIII16sIQQIQ")
31
+
32
+
33
+ class Stream:
34
+ """A stream of the container: *size* bytes, read through *runs*
35
+ (stream offset, file offset, length), or held in memory (*data*:
36
+ streams in the mini stream, which are small)."""
37
+
38
+ def __init__(self, path: str, size: int, runs=None, data: bytes = None, on_disk: str = None):
39
+ self.path, self.size, self.runs, self.data = path, size, runs, data
40
+ #: the compound file the stream is in
41
+ self.path_on_disk = on_disk
42
+ self._starts = [r[0] for r in runs] if runs else []
43
+
44
+ def read_all(self) -> bytes:
45
+ """The whole stream (for small ones: settings, text)."""
46
+ if self.data is not None:
47
+ return self.data
48
+ out = bytearray()
49
+ with builtins.open(self.path_on_disk, "rb") as fh:
50
+ for at, n in self.pieces(0, self.size):
51
+ fh.seek(at)
52
+ out += fh.read(n)
53
+ return bytes(out)
54
+
55
+ def pieces(self, offset: int, size: int):
56
+ """(file offset, length) pieces that hold bytes offset:offset+size."""
57
+ if offset < 0 or offset + size > self.size:
58
+ raise CorruptFileError(f"{self.path!r}: bytes {offset}:{offset + size} of a "
59
+ f"{self.size}-byte stream")
60
+ k = bisect.bisect_right(self._starts, offset) - 1
61
+ end = offset + size
62
+ while offset < end:
63
+ start, at, length = self.runs[k]
64
+ take = min(end, start + length) - offset
65
+ yield at + offset - start, take
66
+ offset += take
67
+ k += 1
68
+
69
+
70
+ class CompoundFile:
71
+ """The directory of compound file *path*: ``streams`` maps each stream's
72
+ path (``Storage00001/s_C001Z001.tif``, ``/`` between levels) to a
73
+ ``Stream``."""
74
+
75
+ def __init__(self, path: str):
76
+ self.path = path
77
+ with builtins.open(path, "rb") as fh:
78
+ head = fh.read(512)
79
+ if head[:8] != MAGIC:
80
+ raise CorruptFileError(f"{path!r} is no compound (OLE) file")
81
+ (_minor, major, byte_order, sector_shift, mini_shift, n_fat, first_dir, self.cutoff,
82
+ first_minifat, n_minifat, first_difat, n_difat) = self._header(head)
83
+ #: version 3 files keep only the low 32 bits of a stream size meaningful
84
+ self._size_mask = 0xFFFFFFFF if major == 3 else (1 << 64) - 1
85
+ if byte_order != 0xFFFE:
86
+ raise CorruptFileError(f"{path!r}: compound file byte order {byte_order:#x}")
87
+ self.sector = 1 << sector_shift
88
+ self.mini = 1 << mini_shift
89
+ fh.seek(0, 2)
90
+ self._file_size = fh.tell()
91
+ difat = list(struct.unpack_from("<109I", head, 76))
92
+ sect = first_difat
93
+ for _ in range(n_difat):
94
+ if sect >= MAXREGSECT:
95
+ break
96
+ data = self._sector(fh, sect)
97
+ values = struct.unpack(f"<{self.sector // 4}I", data)
98
+ difat.extend(values[:-1])
99
+ sect = values[-1]
100
+ fat_sectors = [s for s in difat[:n_fat] if s < MAXREGSECT]
101
+ fat = []
102
+ for s in fat_sectors:
103
+ fat.extend(struct.unpack(f"<{self.sector // 4}I", self._sector(fh, s)))
104
+ self._fat = fat
105
+ entries = []
106
+ for chunk in self._chain_data(fh, first_dir):
107
+ for k in range(0, len(chunk), 128):
108
+ entries.append(DIR_ENTRY.unpack_from(chunk, k))
109
+ minifat = []
110
+ if n_minifat and first_minifat < MAXREGSECT:
111
+ for chunk in self._chain_data(fh, first_minifat):
112
+ minifat.extend(struct.unpack(f"<{len(chunk) // 4}I", chunk))
113
+ self._minifat = minifat
114
+ root = entries[0]
115
+ root_size = root[12] & self._size_mask
116
+ mini_stream = b"".join(self._chain_data(fh, root[11]))[:root_size] if root_size else b""
117
+ self.streams = {}
118
+ self._walk(fh, entries, entries[0][6], "", mini_stream, set())
119
+
120
+ @staticmethod
121
+ def _header(head: bytes):
122
+ minor, major, byte_order, sector_shift, mini_shift = struct.unpack_from("<HHHHH", head, 24)
123
+ n_fat, first_dir = struct.unpack_from("<II", head, 44)
124
+ cutoff, first_minifat, n_minifat, first_difat, n_difat = struct.unpack_from(
125
+ "<IIIII", head, 56)
126
+ return (minor, major, byte_order, sector_shift, mini_shift, n_fat, first_dir, cutoff,
127
+ first_minifat, n_minifat, first_difat, n_difat)
128
+
129
+ def _sector(self, fh, sect: int) -> bytes:
130
+ fh.seek((sect + 1) * self.sector)
131
+ data = fh.read(self.sector)
132
+ if len(data) != self.sector:
133
+ raise CorruptFileError(f"{self.path!r}: sector {sect} beyond the end of the file")
134
+ return data
135
+
136
+ def _chain(self, start: int) -> list:
137
+ chain, sect, seen = [], start, set()
138
+ while sect < MAXREGSECT:
139
+ if sect in seen or sect >= len(self._fat):
140
+ raise CorruptFileError(f"{self.path!r}: broken sector chain at {sect}")
141
+ seen.add(sect)
142
+ chain.append(sect)
143
+ sect = self._fat[sect]
144
+ return chain
145
+
146
+ def _chain_data(self, fh, start: int):
147
+ for sect in self._chain(start):
148
+ yield self._sector(fh, sect)
149
+
150
+ def _runs(self, start: int, size: int) -> list:
151
+ """(stream offset, file offset, length) of each stretch of consecutive sectors."""
152
+ runs, offset = [], 0
153
+ for sect in self._chain(start):
154
+ at = (sect + 1) * self.sector
155
+ length = min(self.sector, size - offset)
156
+ if length <= 0:
157
+ break
158
+ if runs and runs[-1][1] + runs[-1][2] == at:
159
+ s, a, n = runs[-1]
160
+ runs[-1] = (s, a, n + length)
161
+ else:
162
+ runs.append((offset, at, length))
163
+ offset += length
164
+ if offset < size:
165
+ raise CorruptFileError(f"{self.path!r}: a stream's sectors end before its size")
166
+ return runs
167
+
168
+ def _mini_data(self, mini_stream: bytes, start: int, size: int) -> bytes:
169
+ out, sect, seen = bytearray(), start, set()
170
+ while sect < MAXREGSECT and len(out) < size:
171
+ if sect in seen or sect >= len(self._minifat):
172
+ raise CorruptFileError(f"{self.path!r}: broken mini sector chain at {sect}")
173
+ seen.add(sect)
174
+ out += mini_stream[sect * self.mini:(sect + 1) * self.mini]
175
+ sect = self._minifat[sect]
176
+ return bytes(out[:size])
177
+
178
+ def _walk(self, fh, entries, sid: int, prefix: str, mini_stream: bytes, seen: set):
179
+ """Every entry of the red-black tree at *sid* (siblings left / right,
180
+ a storage's children below it)."""
181
+ stack = [sid]
182
+ while stack:
183
+ sid = stack.pop()
184
+ if sid == NOSTREAM or sid in seen or sid >= len(entries):
185
+ continue
186
+ seen.add(sid)
187
+ (raw, name_len, kind, _color, left, right, child, _clsid, _state, _times, _times2,
188
+ start, size) = entries[sid]
189
+ size &= self._size_mask
190
+ stack += [left, right]
191
+ name = raw[:max(0, name_len - 2)].decode("utf-16-le", "replace")
192
+ path = prefix + name
193
+ if kind == STORAGE:
194
+ self._walk(fh, entries, child, path + "/", mini_stream, seen)
195
+ elif kind == STREAM:
196
+ if size < self.cutoff:
197
+ self.streams[path] = Stream(path, size, data=self._mini_data(mini_stream, start, size))
198
+ else:
199
+ self.streams[path] = Stream(path, size, runs=self._runs(start, size),
200
+ on_disk=self.path)
@@ -19,6 +19,12 @@ class MissingImagesWarning(UserWarning):
19
19
  (docs/HCS.md)."""
20
20
 
21
21
 
22
+ class CompositeWarning(UserWarning):
23
+ """A part of a composite format was read on its own because its main file
24
+ is missing, or a part could not be told apart from another file
25
+ (docs/COMPOSITE.md)."""
26
+
27
+
22
28
  class CompanionWarning(UserWarning):
23
29
  """An OME companion set's metadata came from somewhere other than the file
24
30
  opened, or a member path had to be resolved differently than written