micro-reader 0.2.0__tar.gz → 0.3.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 (71) hide show
  1. {micro_reader-0.2.0 → micro_reader-0.3.0}/PKG-INFO +29 -10
  2. {micro_reader-0.2.0 → micro_reader-0.3.0}/README.md +28 -9
  3. {micro_reader-0.2.0 → micro_reader-0.3.0}/pyproject.toml +1 -1
  4. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/__init__.py +2 -2
  5. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_axes.py +53 -3
  6. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_base.py +58 -8
  7. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_errors.py +9 -2
  8. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_fileset.py +77 -42
  9. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_groups.py +12 -5
  10. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_io.py +4 -2
  11. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_outline.py +1 -1
  12. micro_reader-0.3.0/src/micro_reader/_plate.py +485 -0
  13. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_registry.py +21 -10
  14. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_version.py +1 -1
  15. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_ets.py +8 -1
  16. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_lif_metadata.py +63 -12
  17. micro_reader-0.3.0/src/micro_reader/backends/_nd2_run.py +266 -0
  18. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_tiff_metadata.py +24 -9
  19. micro_reader-0.3.0/src/micro_reader/backends/cellomics.py +228 -0
  20. micro_reader-0.3.0/src/micro_reader/backends/columbus.py +107 -0
  21. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/companion.py +225 -33
  22. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/cv7000.py +109 -37
  23. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/czi.py +8 -3
  24. micro_reader-0.3.0/src/micro_reader/backends/flex.py +351 -0
  25. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/harmony.py +65 -17
  26. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/incell.py +1 -1
  27. micro_reader-0.3.0/src/micro_reader/backends/jdce.py +250 -0
  28. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/lif.py +55 -8
  29. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/metamorph.py +19 -6
  30. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/metaxpress.py +101 -44
  31. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/micromanager.py +50 -13
  32. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/nd2.py +76 -7
  33. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/oif.py +59 -15
  34. micro_reader-0.3.0/src/micro_reader/backends/scanr.py +491 -0
  35. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/tiff.py +44 -8
  36. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/vsi.py +3 -2
  37. micro_reader-0.2.0/src/micro_reader/_plate.py +0 -195
  38. {micro_reader-0.2.0 → micro_reader-0.3.0}/.gitignore +0 -0
  39. {micro_reader-0.2.0 → micro_reader-0.3.0}/LICENSE +0 -0
  40. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_cfb.py +0 -0
  41. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_codecs.py +0 -0
  42. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_eubi_guard.py +0 -0
  43. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_index.py +0 -0
  44. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_jpeg_restarts.py +0 -0
  45. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_memory.py +0 -0
  46. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_metadata.py +0 -0
  47. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_mosaic.py +0 -0
  48. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_parts.py +0 -0
  49. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_pool.py +0 -0
  50. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_spatial.py +0 -0
  51. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_stack.py +0 -0
  52. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/_stream.py +0 -0
  53. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/__init__.py +0 -0
  54. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_czi_metadata.py +0 -0
  55. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_hdf5.py +0 -0
  56. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_nd2_legacy.py +0 -0
  57. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_nd2_metadata.py +0 -0
  58. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_nd2_structure.py +0 -0
  59. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_tiff_variants.py +0 -0
  60. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/_vsi_tree.py +0 -0
  61. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/bdv.py +0 -0
  62. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/dv.py +0 -0
  63. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/hdf5.py +0 -0
  64. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/ics.py +0 -0
  65. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/ilastik.py +0 -0
  66. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/ims.py +0 -0
  67. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/mrc.py +0 -0
  68. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/ndpis.py +0 -0
  69. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/oir.py +0 -0
  70. {micro_reader-0.2.0 → micro_reader-0.3.0}/src/micro_reader/backends/omp2info.py +0 -0
  71. {micro_reader-0.2.0 → micro_reader-0.3.0}/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.2.0
3
+ Version: 0.3.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
@@ -56,7 +56,7 @@ 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, MetaXpress, IN Cell, CellVoyager).
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).
60
60
 
61
61
  > **Status: pre-alpha (0.0.x)**
62
62
  >
@@ -233,7 +233,7 @@ f.path # the main file: "cells_R3D.dv"
233
233
  f.opened # what was opened: "cells_R3D.dv.log"
234
234
  ```
235
235
 
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), or the one image it holds. A folder with several images raises an error that lists them.
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.
237
237
 
238
238
  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
239
 
@@ -246,7 +246,7 @@ for group in g.groups:
246
246
  g.singles # every other file, as given
247
247
  ```
248
248
 
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`.
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.
250
250
 
251
251
  ## Axes
252
252
 
@@ -324,18 +324,18 @@ Only axes with size 1 can be removed. `y` and `x` are never removed.
324
324
 
325
325
  ### RGB samples
326
326
 
327
- By default, RGB samples are represented by the `s` axis.
327
+ `micro_reader.open` turns RGB samples into channels: an RGB image has 3 channels, named "<name> R", "G" and "B" and coloured red, green and blue (a file without a channel name gives three unnamed channels in those colours). An image that has channels already gets c × s of them: "<name> R" ... for each.
328
328
 
329
- You can instead represent RGB as channels:
329
+ To keep RGB samples as their own `s` axis, the way the file stores them:
330
330
 
331
331
  ```python
332
332
  f = micro_reader.open(
333
333
  "image.tif",
334
- samples="channels",
334
+ samples="keep",
335
335
  )
336
336
  ```
337
337
 
338
- If there are existing channels already, RGB samples will fold with them, leading to a total of c × s channels.
338
+ `image.source` is then not needed: with the default, it is the image as stored, with its `s` axis. `image.as_axes(...)` keeps `s` unless asked for `samples="channels"`.
339
339
 
340
340
 
341
341
  ## Ambiguous dimensions
@@ -475,15 +475,32 @@ The plate layout is available through:
475
475
  plate = f.plates[0]
476
476
  plate.wells[0].name
477
477
  # "B2"
478
+ plate.row_names, plate.column_names # ('A', 'B', ...), ('1', '2', ...)
479
+ part = plate.rows[2:5].columns[3:7] # a part of the plate: still a plate
478
480
  ```
479
481
 
482
+ Every level knows what it is and what holds it: `.kind` is `"file"`, `"plate"`, `"well"`
483
+ or `"field"`, and `.parent` is the level above. Accessors go down only: a file holds
484
+ `.plates`, a plate `.wells`, a well `.fields`, a field `.images`. `plate.rows` and
485
+ `plate.columns` slice a plate by position, name or a list of them, as `f.select(row=...,
486
+ column=...)` does; rows and columns are a well's address, not a level.
487
+
480
488
  Images can be reached through the wells, or selected by well:
481
489
 
482
490
  ```python
483
491
  image = plate.wells[0].fields[0].image
484
- f.select(well=plate.well_index("B2"))
492
+ f.select(well=plate.well_index("B2")) # by its place in plate.wells
493
+ f.select(well_name="B2") # by name
494
+ f.select(row_name="B", column=range(1, 4)) # rows and columns: names, or 0-based numbers
495
+ f.select(well=["B2", "C5"], field=[0, 1]) # a list, tuple, range or set: any of them
496
+ f.select(row=slice(2, None)) # a slice of positions: row C onward
485
497
  ```
486
498
 
499
+ A plate's places have a position and a name, a keyword each: `plate` / `plate_name`,
500
+ `acquisition` / `acquisition_name`, `well` / `well_name`, `row` / `row_name`, `column` /
501
+ `column_name` (the columns of ome-zarr-pyramid's table). A name given to a position keyword
502
+ is its name (`well="B2"` is `well_name="B2"`).
503
+
487
504
  `f.outline()` shows the plates, wells and fields of a file.
488
505
 
489
506
  Zeiss CZI plates are read as plates with `plates=True`, and as series otherwise:
@@ -581,7 +598,7 @@ The position is omitted when it cannot be determined reliably.
581
598
  | **Micro-Manager 1.4**| image-per-file acquisitions (`metadata.txt` + one TIFF per plane) |
582
599
  | **DeltaVision** | `.dv`, `.r3d` (softWoRx, OMX, Priism) |
583
600
  | **MRC** | Common MRC modes and extended headers |
584
- | **Plates (HCS)** | Harmony, MetaXpress, IN Cell and CellVoyager plates |
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 |
585
602
  | **JPEG / PNG / BMP** | Common raster image formats |
586
603
 
587
604
  All formats use the same `File`, `Image`, axes, indexing, metadata, and reading interface wherever possible.
@@ -645,6 +662,8 @@ f = micro_reader.open("data_t001.ome.tif", multifile=False)
645
662
 
646
663
  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.
647
664
 
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.
666
+
648
667
  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.
649
668
 
650
669
  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.
@@ -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, OIB / OIF, Olympus VSI, Imaris, DeltaVision, MRC, common raster formats, OME companion file sets, and high-content screening plates (Harmony, MetaXpress, IN Cell, CellVoyager).
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).
6
6
 
7
7
  > **Status: pre-alpha (0.0.x)**
8
8
  >
@@ -179,7 +179,7 @@ f.path # the main file: "cells_R3D.dv"
179
179
  f.opened # what was opened: "cells_R3D.dv.log"
180
180
  ```
181
181
 
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), or the one image it holds. A folder with several images raises an error that lists them.
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.
183
183
 
184
184
  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
185
 
@@ -192,7 +192,7 @@ for group in g.groups:
192
192
  g.singles # every other file, as given
193
193
  ```
194
194
 
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`.
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.
196
196
 
197
197
  ## Axes
198
198
 
@@ -270,18 +270,18 @@ Only axes with size 1 can be removed. `y` and `x` are never removed.
270
270
 
271
271
  ### RGB samples
272
272
 
273
- By default, RGB samples are represented by the `s` axis.
273
+ `micro_reader.open` turns RGB samples into channels: an RGB image has 3 channels, named "<name> R", "G" and "B" and coloured red, green and blue (a file without a channel name gives three unnamed channels in those colours). An image that has channels already gets c × s of them: "<name> R" ... for each.
274
274
 
275
- You can instead represent RGB as channels:
275
+ To keep RGB samples as their own `s` axis, the way the file stores them:
276
276
 
277
277
  ```python
278
278
  f = micro_reader.open(
279
279
  "image.tif",
280
- samples="channels",
280
+ samples="keep",
281
281
  )
282
282
  ```
283
283
 
284
- If there are existing channels already, RGB samples will fold with them, leading to a total of c × s channels.
284
+ `image.source` is then not needed: with the default, it is the image as stored, with its `s` axis. `image.as_axes(...)` keeps `s` unless asked for `samples="channels"`.
285
285
 
286
286
 
287
287
  ## Ambiguous dimensions
@@ -421,15 +421,32 @@ The plate layout is available through:
421
421
  plate = f.plates[0]
422
422
  plate.wells[0].name
423
423
  # "B2"
424
+ plate.row_names, plate.column_names # ('A', 'B', ...), ('1', '2', ...)
425
+ part = plate.rows[2:5].columns[3:7] # a part of the plate: still a plate
424
426
  ```
425
427
 
428
+ Every level knows what it is and what holds it: `.kind` is `"file"`, `"plate"`, `"well"`
429
+ or `"field"`, and `.parent` is the level above. Accessors go down only: a file holds
430
+ `.plates`, a plate `.wells`, a well `.fields`, a field `.images`. `plate.rows` and
431
+ `plate.columns` slice a plate by position, name or a list of them, as `f.select(row=...,
432
+ column=...)` does; rows and columns are a well's address, not a level.
433
+
426
434
  Images can be reached through the wells, or selected by well:
427
435
 
428
436
  ```python
429
437
  image = plate.wells[0].fields[0].image
430
- f.select(well=plate.well_index("B2"))
438
+ f.select(well=plate.well_index("B2")) # by its place in plate.wells
439
+ f.select(well_name="B2") # by name
440
+ f.select(row_name="B", column=range(1, 4)) # rows and columns: names, or 0-based numbers
441
+ f.select(well=["B2", "C5"], field=[0, 1]) # a list, tuple, range or set: any of them
442
+ f.select(row=slice(2, None)) # a slice of positions: row C onward
431
443
  ```
432
444
 
445
+ A plate's places have a position and a name, a keyword each: `plate` / `plate_name`,
446
+ `acquisition` / `acquisition_name`, `well` / `well_name`, `row` / `row_name`, `column` /
447
+ `column_name` (the columns of ome-zarr-pyramid's table). A name given to a position keyword
448
+ is its name (`well="B2"` is `well_name="B2"`).
449
+
433
450
  `f.outline()` shows the plates, wells and fields of a file.
434
451
 
435
452
  Zeiss CZI plates are read as plates with `plates=True`, and as series otherwise:
@@ -527,7 +544,7 @@ The position is omitted when it cannot be determined reliably.
527
544
  | **Micro-Manager 1.4**| image-per-file acquisitions (`metadata.txt` + one TIFF per plane) |
528
545
  | **DeltaVision** | `.dv`, `.r3d` (softWoRx, OMX, Priism) |
529
546
  | **MRC** | Common MRC modes and extended headers |
530
- | **Plates (HCS)** | Harmony, MetaXpress, IN Cell and CellVoyager plates |
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 |
531
548
  | **JPEG / PNG / BMP** | Common raster image formats |
532
549
 
533
550
  All formats use the same `File`, `Image`, axes, indexing, metadata, and reading interface wherever possible.
@@ -591,6 +608,8 @@ f = micro_reader.open("data_t001.ome.tif", multifile=False)
591
608
 
592
609
  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.
593
610
 
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.
612
+
594
613
  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.
595
614
 
596
615
  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.
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "micro-reader"
9
- version = "0.2.0"
9
+ version = "0.3.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"
@@ -13,7 +13,7 @@ from ._plate import Acquisition, Field, Plate, Well
13
13
  from ._spatial import Axis, Collection, CoordinateSystem, Transformation
14
14
  from ._errors import (CompanionWarning, CompositeWarning, CorruptFileError, InvariantError,
15
15
  MicroReaderError, MissingImagesWarning, PlateLayoutWarning,
16
- SkewedGridWarning, UnsupportedFormatError)
16
+ SkewedGridWarning, StoredValuesWarning, UnsupportedFormatError)
17
17
  from ._memory import MemoryLimitError, memory_limit, set_memory_limit
18
18
  from ._io import cache_limit, cached_bytes, set_cache_limit
19
19
  from ._pool import decode_threads, set_decode_threads
@@ -30,5 +30,5 @@ __all__ = [
30
30
  "MicroReaderError", "UnsupportedFormatError", "CorruptFileError", "InvariantError",
31
31
  "MemoryLimitError", "DamagedFileWarning",
32
32
  "MissingImagesWarning", "CompanionWarning", "CompositeWarning", "PlateLayoutWarning",
33
- "SkewedGridWarning", "__version__",
33
+ "SkewedGridWarning", "StoredValuesWarning", "__version__",
34
34
  ]
@@ -12,7 +12,11 @@ guessing:
12
12
  - axes the file lacks are inserted with size 1;
13
13
  - RGB samples ``s`` stay a trailing ``s`` when asked for, or are folded into
14
14
  the channels with ``samples="channels"`` (channel ``k`` = sample ``k % S``
15
- of channel ``k // S``, as eubi-bridge stores them);
15
+ of channel ``k // S``, as eubi-bridge stores them; ``open``'s default).
16
+ The metadata then lists the folded channels: "<name> R" / "G" / "B" ("A"),
17
+ tinted red / green / blue (white for A); a single unnamed channel gives
18
+ unnamed, tinted channels; other sample counts are numbered, untinted -- the
19
+ names eubi-bridge gives (``sample_channels.sample_channel_labels``);
16
20
  - ``squeeze=True`` drops every size-1 axis except ``y`` and ``x``;
17
21
  ``squeeze="tz"`` drops just those (each must have size 1);
18
22
  - an axis the requested ones cannot hold is an error that names it.
@@ -29,6 +33,10 @@ import numpy as np
29
33
  from ._base import Image
30
34
 
31
35
  SAMPLE_POLICIES = ("keep", "channels")
36
+ #: labels and colours of the channels that samples become (as eubi-bridge's)
37
+ SAMPLE_LABELS = {3: ("R", "G", "B"), 4: ("R", "G", "B", "A")}
38
+ SAMPLE_COLORS = {3: ("#FF0000", "#00FF00", "#0000FF"),
39
+ 4: ("#FF0000", "#00FF00", "#0000FF", "#FFFFFF")}
32
40
  #: the array axes, outermost first; ``s`` (RGB samples) after them
33
41
  STANDARD = "tczyx"
34
42
  CANONICAL = STANDARD + "s"
@@ -36,6 +44,33 @@ CANONICAL = STANDARD + "s"
36
44
  KEPT_BY_SQUEEZE = "yx"
37
45
 
38
46
 
47
+ def folded_channels(channels, n: int, samples: int) -> tuple:
48
+ """The channels of *n* channels of *samples* samples each, folded into
49
+ ``n * samples`` channels, channel-major. A file that lists them already
50
+ (Bio-Formats-style: one channel per sample) is kept; otherwise each
51
+ channel becomes "<name> R" / "G" / "B" ("A"), tinted from ``SAMPLE_COLORS``,
52
+ 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."""
55
+ from ._metadata import Channel
56
+ channels = tuple(channels)
57
+ if len(channels) == n * samples:
58
+ return channels
59
+ if len(channels) != n:
60
+ channels = (Channel(),) * n
61
+ labels = SAMPLE_LABELS.get(samples, tuple(str(i) for i in range(samples)))
62
+ colors = SAMPLE_COLORS.get(samples) or (None,) * samples
63
+ single_unnamed = n == 1 and not channels[0].name
64
+ out = []
65
+ for c, ch in enumerate(channels):
66
+ for i in range(samples):
67
+ name = None if single_unnamed else f"{ch.name or f'Channel {c}'} {labels[i]}"
68
+ out.append(Channel(name=name, color=colors[i],
69
+ emission_wavelength=ch.emission_wavelength,
70
+ excitation_wavelength=ch.excitation_wavelength))
71
+ return tuple(out)
72
+
73
+
39
74
  class AxesImage(Image):
40
75
  """*source* presented with *axes* (see the module docstring)."""
41
76
 
@@ -165,8 +200,14 @@ class AxesImage(Image):
165
200
 
166
201
  @property
167
202
  def metadata(self):
168
- """The native image's metadata (channels follow its own c axis)."""
169
- return self.source.metadata
203
+ """The native image's metadata; with samples folded into the channels,
204
+ one channel per folded channel (``folded_channels``)."""
205
+ meta = self.source.metadata
206
+ if not self._fold:
207
+ return meta
208
+ from dataclasses import replace
209
+ n = self.source.shape[self._names.index("c")] if "c" in self._names else 1
210
+ return replace(meta, channels=folded_channels(meta.channels, n, self._samples))
170
211
 
171
212
  @property
172
213
  def resolutions(self) -> list:
@@ -192,5 +233,14 @@ class AxesImage(Image):
192
233
  def close(self) -> None:
193
234
  self.source.close()
194
235
 
236
+ def __getattr__(self, name):
237
+ """The native image's own public attributes (a CZI series' ``origin``, ...):
238
+ an RGB image folded by ``open``'s default stays the backend's image to read
239
+ them from. Private ones are not passed on."""
240
+ source = self.__dict__.get("source")
241
+ if source is None or name.startswith("_"):
242
+ raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
243
+ return getattr(source, name)
244
+
195
245
  def __repr__(self) -> str:
196
246
  return super().__repr__()[:-1] + f" of {self.source.axes!r}>"
@@ -4,6 +4,8 @@ Each image is one spatial frame of the file; its index says which (series,
4
4
  tile, rotation, view: see docs/AXES.md)."""
5
5
  from __future__ import annotations
6
6
 
7
+ import os
8
+
7
9
  import threading
8
10
  from dataclasses import dataclass, field
9
11
  from typing import Optional, Sequence
@@ -367,6 +369,19 @@ class File:
367
369
  backend's own class is an implementation detail."""
368
370
 
369
371
  backend: str = "base"
372
+ #: what the object is in the containment tree (decision C-19): a file holds
373
+ #: plates (``plates``), a plate wells, a well fields, a field images
374
+ kind = "file"
375
+ #: what holds it: nothing, for a file (a plate's ``parent`` is its file)
376
+ parent = None
377
+
378
+ def __getattr__(self, name):
379
+ # an accessor of a lower level (f.wells): name the way down
380
+ if name in ("wells", "fields", "rows", "columns"):
381
+ raise AttributeError(f"a file holds plates: f.plates[p].{name}"
382
+ if name != "fields" else
383
+ "a file holds plates: f.plates[p].wells[w].fields")
384
+ raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
370
385
  #: (coordinate_systems, coordinate_transformations) once declared and checked
371
386
  _graph: Optional[tuple] = None
372
387
  #: (the images it was built from, collection) once built (``collection``)
@@ -393,6 +408,8 @@ class File:
393
408
  if self.plates:
394
409
  from ._plate import navigate
395
410
  self.plates = navigate(self.plates, self.images)
411
+ for plate in self.plates:
412
+ object.__setattr__(plate, "parent", self)
396
413
 
397
414
  def _index_resolutions(self) -> None:
398
415
  """``resolution`` is in every index when the file stores a pyramid of
@@ -441,17 +458,48 @@ class File:
441
458
  return {name: len(seen[name]) for name in INDEX_NAMES if name in seen}
442
459
 
443
460
  def select(self, **index) -> list:
444
- """The images whose index matches, e.g. ``select(series=1, view=0)``."""
445
- unknown = set(index) - set(INDEX_NAMES)
461
+ """The images whose index matches, e.g. ``select(series=1, view=0)``.
462
+
463
+ - A value may be a list, tuple, range or set: any of them
464
+ (``select(illumination=[2, 3])``).
465
+ - A plate's places have a position and a name, a keyword each (the
466
+ columns of ozp's table): ``plate`` / ``plate_name``, ``acquisition``
467
+ / ``acquisition_name``, ``well`` / ``well_name``, ``row`` /
468
+ ``row_name``, ``column`` / ``column_name``. Positions count from 0
469
+ (``well`` is a place in ``plates[p].wells``, ``column=2`` the column
470
+ named "3"); a name given to a position keyword is its name
471
+ (``well="C3"`` is ``well_name="C3"``). ``row`` and ``column`` are
472
+ another address of the well, so they do not go with ``well``.
473
+ - A ``Plate``, ``Well``, ``Field`` or ``Acquisition`` object is no
474
+ value (TypeError).
475
+
476
+ An unknown index name raises ``KeyError``; a known one no image has
477
+ selects nothing."""
478
+ from ._plate import (PLACE_NAMES, WELL_ADDRESS, index_values, place_conditions,
479
+ place_matches, places_of)
480
+ unknown = set(index) - set(INDEX_NAMES) - set(WELL_ADDRESS) - set(PLACE_NAMES)
446
481
  if unknown:
447
482
  if _OLD_SERIES in unknown:
448
483
  raise KeyError(_RENAMED)
449
- raise KeyError(f"unknown indices {sorted(unknown)}; the indices are {INDEX_NAMES}")
450
- get, wanted, out = dict.get, tuple(index.items()), []
484
+ raise KeyError(f"unknown indices {sorted(unknown)}; the indices are {INDEX_NAMES}, "
485
+ f"and a plate's {', '.join(WELL_ADDRESS + PLACE_NAMES)}")
486
+ wanted = {name: index_values(name, value) for name, value in index.items()}
487
+ conditions = place_conditions(wanted)
488
+ plates = self.plates if conditions else ()
489
+ if conditions and not plates:
490
+ raise ValueError("select: plate names, well names, rows and columns are a "
491
+ "plate's places; the file has no plates (open it with "
492
+ "plates=True)")
493
+ get, out = dict.get, []
494
+ items = tuple(wanted.items())
451
495
  for image in self.images: # dict.get: not ImageIndex's checks
452
496
  key = image.index
453
- for name, value in wanted:
454
- if not get(key, name) == value:
497
+ if conditions:
498
+ place = places_of(plates, key)
499
+ if place is None or not place_matches(conditions, *place):
500
+ continue
501
+ for name, allowed in items:
502
+ if get(key, name) not in allowed:
455
503
  break
456
504
  else:
457
505
  out.append(image)
@@ -536,8 +584,10 @@ class File:
536
584
  paths = getattr(each, "_member_paths", None)
537
585
  if paths is not None:
538
586
  out.update(paths())
539
- out.discard(self.path)
540
- return out
587
+ # the file itself (a .flex or .C01 file opens its folder's set, itself
588
+ # among them), however its path was spelled (relative, other case)
589
+ own = os.path.normcase(os.path.abspath(self.path))
590
+ return {p for p in out if os.path.normcase(os.path.abspath(p)) != own}
541
591
 
542
592
  def _side_files(self) -> set:
543
593
  """Files that belong to this file set without being read as its images
@@ -45,8 +45,9 @@ class CompositeWarning(UserWarning):
45
45
 
46
46
  class CompanionWarning(UserWarning):
47
47
  """An OME companion set's metadata came from somewhere other than the file
48
- opened, or a member path had to be resolved differently than written
49
- (docs/COMPANION.md)."""
48
+ opened, a member path had to be resolved differently than written, or the
49
+ companion's bytes had to be read in another encoding than its XML
50
+ declares (docs/COMPANION.md)."""
50
51
 
51
52
 
52
53
  class SkewedGridWarning(UserWarning):
@@ -57,6 +58,12 @@ class SkewedGridWarning(UserWarning):
57
58
  the skew."""
58
59
 
59
60
 
61
+ class StoredValuesWarning(UserWarning):
62
+ """The file stores its pixels in a form that is not the measured values, and
63
+ micro-reader reads the measured ones; the warning says what was changed
64
+ (e.g. an older scanR plate's 12-bit values stored with the top bit set)."""
65
+
66
+
60
67
  class PlateLayoutWarning(UserWarning):
61
68
  """A plate's wells lie outside the layout its file describes (e.g. a slide
62
69
  holder whose records address the wells of the slide in it); the layout