micro-reader 0.2.1__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.
- {micro_reader-0.2.1 → micro_reader-0.3.0}/PKG-INFO +29 -10
- {micro_reader-0.2.1 → micro_reader-0.3.0}/README.md +28 -9
- {micro_reader-0.2.1 → micro_reader-0.3.0}/pyproject.toml +1 -1
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/__init__.py +2 -2
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_axes.py +53 -3
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_base.py +58 -8
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_errors.py +6 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_fileset.py +77 -42
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_groups.py +12 -5
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_io.py +4 -2
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_outline.py +1 -1
- micro_reader-0.3.0/src/micro_reader/_plate.py +485 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_registry.py +21 -10
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_version.py +1 -1
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_ets.py +8 -1
- micro_reader-0.3.0/src/micro_reader/backends/_nd2_run.py +266 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_tiff_metadata.py +24 -9
- micro_reader-0.3.0/src/micro_reader/backends/cellomics.py +228 -0
- micro_reader-0.3.0/src/micro_reader/backends/columbus.py +107 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/companion.py +168 -32
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/cv7000.py +14 -5
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/czi.py +1 -1
- micro_reader-0.3.0/src/micro_reader/backends/flex.py +351 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/harmony.py +31 -8
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/incell.py +1 -1
- micro_reader-0.3.0/src/micro_reader/backends/jdce.py +250 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/metaxpress.py +37 -18
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/nd2.py +76 -7
- micro_reader-0.3.0/src/micro_reader/backends/scanr.py +491 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/tiff.py +44 -8
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/vsi.py +3 -2
- micro_reader-0.2.1/src/micro_reader/_plate.py +0 -195
- {micro_reader-0.2.1 → micro_reader-0.3.0}/.gitignore +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/LICENSE +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_cfb.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_codecs.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_eubi_guard.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_index.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_jpeg_restarts.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_memory.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_metadata.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_mosaic.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_parts.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_pool.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_spatial.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_stack.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/_stream.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/__init__.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_czi_metadata.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_hdf5.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_lif_metadata.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_nd2_legacy.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_nd2_metadata.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_nd2_structure.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_tiff_variants.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/_vsi_tree.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/bdv.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/dv.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/hdf5.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/ics.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/ilastik.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/ims.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/lif.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/metamorph.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/micromanager.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/mrc.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/ndpis.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/oif.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/oir.py +0 -0
- {micro_reader-0.2.1 → micro_reader-0.3.0}/src/micro_reader/backends/omp2info.py +0 -0
- {micro_reader-0.2.1 → 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.
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
334
|
+
samples="keep",
|
|
335
335
|
)
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
280
|
+
samples="keep",
|
|
281
281
|
)
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
|
|
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
|
|
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.
|
|
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
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
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
|
-
|
|
540
|
-
|
|
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
|
|
@@ -58,6 +58,12 @@ class SkewedGridWarning(UserWarning):
|
|
|
58
58
|
the skew."""
|
|
59
59
|
|
|
60
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
|
+
|
|
61
67
|
class PlateLayoutWarning(UserWarning):
|
|
62
68
|
"""A plate's wells lie outside the layout its file describes (e.g. a slide
|
|
63
69
|
holder whose records address the wells of the slide in it); the layout
|