large-image-source-zarr 1.28.2.dev12__tar.gz → 1.30.7.dev12__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.
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/PKG-INFO +8 -5
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/README.rst +20 -40
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/large_image_source_zarr/__init__.py +471 -54
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/large_image_source_zarr.egg-info/PKG-INFO +8 -5
- large_image_source_zarr-1.30.7.dev12/large_image_source_zarr.egg-info/requires.txt +10 -0
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/setup.py +9 -6
- large-image-source-zarr-1.28.2.dev12/large_image_source_zarr.egg-info/requires.txt +0 -9
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/LICENSE +0 -0
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/large_image_source_zarr/girder_source.py +0 -0
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/large_image_source_zarr.egg-info/SOURCES.txt +0 -0
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/large_image_source_zarr.egg-info/dependency_links.txt +0 -0
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/large_image_source_zarr.egg-info/entry_points.txt +0 -0
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/large_image_source_zarr.egg-info/top_level.txt +0 -0
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/pyproject.toml +0 -0
- {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: large-image-source-zarr
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.7.dev12
|
|
4
4
|
Summary: A OME Zarr tilesource for large_image.
|
|
5
5
|
Home-page: https://github.com/girder/large_image
|
|
6
6
|
Author: Kitware, Inc.
|
|
@@ -15,13 +15,16 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
19
|
Requires-Python: >=3.6
|
|
20
|
+
Description-Content-Type: text/x-rst
|
|
19
21
|
License-File: LICENSE
|
|
20
|
-
Requires-Dist: large-image>=1.
|
|
21
|
-
Requires-Dist: zarr
|
|
22
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: large-image>=1.30.7.dev12
|
|
23
|
+
Requires-Dist: zarr<3
|
|
24
|
+
Requires-Dist: numcodecs
|
|
25
|
+
Requires-Dist: imagecodecs-numcodecs!=2024.9.22
|
|
23
26
|
Provides-Extra: girder
|
|
24
|
-
Requires-Dist: girder-large-image>=1.
|
|
27
|
+
Requires-Dist: girder-large-image>=1.30.7.dev12; extra == "girder"
|
|
25
28
|
Provides-Extra: all
|
|
26
29
|
Requires-Dist: large-image-converter; extra == "all"
|
|
27
30
|
|
|
@@ -15,7 +15,7 @@ Large Image
|
|
|
15
15
|
:target: https://codecov.io/github/girder/large_image?branch=master
|
|
16
16
|
:alt: codecov.io
|
|
17
17
|
|
|
18
|
-
.. |doi-badge| image:: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.4723355-blue
|
|
18
|
+
.. |doi-badge| image:: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.4723355-blue.svg
|
|
19
19
|
:target: https://zenodo.org/badge/latestdoi/45569214
|
|
20
20
|
|
|
21
21
|
.. |pypi-badge| image:: https://img.shields.io/pypi/v/large-image.svg?logo=python&logoColor=white
|
|
@@ -39,7 +39,7 @@ Highlights
|
|
|
39
39
|
Installation
|
|
40
40
|
------------
|
|
41
41
|
|
|
42
|
-
In addition to installing the ``large-image`` package, you'll need at least one tile source (a ``large-image-source-xxx`` package). You can install everything from the main project with one of these commands:
|
|
42
|
+
In addition to installing the base ``large-image`` package, you'll need at least one tile source which corresponds to your target file format(s) (a ``large-image-source-xxx`` package). You can install everything from the main project with one of these commands:
|
|
43
43
|
|
|
44
44
|
Pip
|
|
45
45
|
~~~
|
|
@@ -52,7 +52,7 @@ Install all tile sources on linux::
|
|
|
52
52
|
|
|
53
53
|
pip install large-image[all] --find-links https://girder.github.io/large_image_wheels
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
When using large-image with an instance of `Girder`_, install all tile sources and all Girder plugins on linux::
|
|
56
56
|
|
|
57
57
|
pip install large-image[all] girder-large-image-annotation[tasks] --find-links https://girder.github.io/large_image_wheels
|
|
58
58
|
|
|
@@ -60,8 +60,9 @@ Install all tile sources and all Girder plugins on linux::
|
|
|
60
60
|
Conda
|
|
61
61
|
~~~~~
|
|
62
62
|
|
|
63
|
-
Conda makes dependency management a bit easier if not on Linux.
|
|
63
|
+
Conda makes dependency management a bit easier if not on Linux. The base module, converter module, and two of the source modules are available on conda-forge. You can install the following::
|
|
64
64
|
|
|
65
|
+
conda install -c conda-forge large-image
|
|
65
66
|
conda install -c conda-forge large-image-source-gdal
|
|
66
67
|
conda install -c conda-forge large-image-source-tiff
|
|
67
68
|
conda install -c conda-forge large-image-converter
|
|
@@ -118,7 +119,7 @@ Large Image consists of several Python modules designed to work together. These
|
|
|
118
119
|
|
|
119
120
|
- ``large-image-source-deepzoom``: A tile source for reading Deepzoom tiles.
|
|
120
121
|
|
|
121
|
-
- ``large-image-source-dicom``: A tile source for reading DICOM WSI
|
|
122
|
+
- ``large-image-source-dicom``: A tile source for reading DICOM Whole Slide Images (WSI).
|
|
122
123
|
|
|
123
124
|
- ``large-image-source-gdal``: A tile source for reading geotiff files via GDAL. This handles source data with more complex transforms than the mapnik tile source.
|
|
124
125
|
|
|
@@ -128,62 +129,41 @@ Large Image consists of several Python modules designed to work together. These
|
|
|
128
129
|
|
|
129
130
|
- ``large-image-source-nd2``: A tile source for reading nd2 (NIS Element) images.
|
|
130
131
|
|
|
131
|
-
- ``large-image-source-ometiff``: A tile source using the tiff library that can handle
|
|
132
|
+
- ``large-image-source-ometiff``: A tile source using the tiff library that can handle most multi-frame OMETiff files that are compliant with the specification.
|
|
132
133
|
|
|
133
134
|
- ``large-image-source-openjpeg``: A tile source using the Glymur library to read jp2 (JPEG 2000) files.
|
|
134
135
|
|
|
135
136
|
- ``large-image-source-openslide``: A tile source using the OpenSlide library. This works with svs, ndpi, Mirax, tiff, vms, and other file formats.
|
|
136
137
|
|
|
137
|
-
- ``large-image-source-pil``: A tile source for small images via the Python Imaging Library (Pillow).
|
|
138
|
+
- ``large-image-source-pil``: A tile source for small images via the Python Imaging Library (Pillow). By default, the maximum size is 4096, but the maximum size can be configured.
|
|
138
139
|
|
|
139
140
|
- ``large-image-source-tiff``: A tile source for reading pyramidal tiff files in common compression formats.
|
|
140
141
|
|
|
141
142
|
- ``large-image-source-tifffile``: A tile source using the tifffile library that can handle a wide variety of tiff-like files.
|
|
142
143
|
|
|
143
|
-
- ``large-image-source-vips``: A tile source for reading any files handled by libvips. This also can be used for writing tiled images from numpy arrays.
|
|
144
|
+
- ``large-image-source-vips``: A tile source for reading any files handled by libvips. This also can be used for writing tiled images from numpy arrays (up to 4 dimensions).
|
|
144
145
|
|
|
145
|
-
- ``large-image-source-zarr``: A tile source using the zarr library that can handle OME-Zarr (OME-NGFF) files as well as some other zarr files.
|
|
146
|
+
- ``large-image-source-zarr``: A tile source using the zarr library that can handle OME-Zarr (OME-NGFF) files as well as some other zarr files. This can also be used for writing N-dimensional tiled images from numpy arrays. Written images can be saved as any supported format.
|
|
146
147
|
|
|
147
148
|
- ``large-image-source-test``: A tile source that generates test tiles, including a simple fractal pattern. Useful for testing extreme zoom levels.
|
|
148
149
|
|
|
149
|
-
- ``large-image-source-dummy``: A tile source that does nothing.
|
|
150
|
+
- ``large-image-source-dummy``: A tile source that does nothing. This is an absolutely minimal implementation of a tile source used for testing. If you want to create a custom tile source, start with this implementation.
|
|
150
151
|
|
|
151
|
-
Most tile sources can be used with girder-large-image. You can specific an extras_require of ``girder`` to include ``girder-large-image`` with the source.
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
As a `Girder`_ plugin, ``large-image`` adds end points to access all of the image formats it can read both to get metadata and to act as a tile server.
|
|
154
|
+
In the Girder UI, ``large-image`` shows images on item pages, and can show thumbnails in item lists when browsing folders.
|
|
155
|
+
There is also cache management to balance memory use and speed of response in Girder when ``large-image`` is used as a tile server.
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
You can specify extras_require of ``tasks`` to install a Girder Worker task that can convert otherwise unreadable images to pyramidal tiff files.
|
|
157
|
+
Most tile sources can be used with Girder Large Image. You can specify an extras_require of ``girder`` to install the following packages:
|
|
157
158
|
|
|
158
|
-
- ``girder-large-image
|
|
159
|
+
- ``girder-large-image``: Large Image as a Girder 3.x plugin.
|
|
160
|
+
You can install ``large-image[tasks]`` to install a Girder Worker task that can convert otherwise unreadable images to pyramidal tiff files.
|
|
161
|
+
|
|
162
|
+
- ``girder-large-image-annotation``: Adds models to the Girder database for supporting annotating large images. These annotations can be rendered on images. Annotations can include polygons, points, image overlays, and other types. Each annotation can have a label and metadata.
|
|
159
163
|
|
|
160
164
|
- ``large-image-tasks``: A utility for running the converter via Girder Worker.
|
|
161
165
|
You can specify an extras_require of ``girder`` to include modules needed to work with the Girder remote worker or ``worker`` to include modules needed on the remote side of the Girder remote worker. If neither is specified, some conversion tasks can be run using Girder local jobs.
|
|
162
166
|
|
|
163
167
|
|
|
164
|
-
Developer Installation
|
|
165
|
-
----------------------
|
|
166
|
-
|
|
167
|
-
To install all packages from source, clone the repository::
|
|
168
|
-
|
|
169
|
-
git clone https://github.com/girder/large_image.git
|
|
170
|
-
cd large_image
|
|
171
|
-
|
|
172
|
-
Install all packages and dependencies::
|
|
173
|
-
|
|
174
|
-
pip install -e . -r requirements-dev.txt
|
|
175
|
-
|
|
176
|
-
If you aren't developing with Girder 3, you can skip installing those components. Use ``requirements-dev-core.txt`` instead of ``requirements-dev.txt``::
|
|
177
|
-
|
|
178
|
-
pip install -e . -r requirements-dev-core.txt
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Tile source prerequisites
|
|
182
|
-
=========================
|
|
183
|
-
|
|
184
|
-
Many tile sources have complex prerequisites. These can be installed directly using your system's package manager or from some prebuilt Python wheels for Linux. The prebuilt wheels are not official packages, but they can be used by instructing pip to use them by preference::
|
|
185
|
-
|
|
186
|
-
pip install -e . -r requirements-dev.txt --find-links https://girder.github.io/large_image_wheels
|
|
187
|
-
|
|
188
168
|
|
|
189
|
-
.. _Girder: https://
|
|
169
|
+
.. _Girder: https://girder.readthedocs.io/en/latest/
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import math
|
|
2
|
+
import multiprocessing
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
import tempfile
|
|
5
6
|
import threading
|
|
6
7
|
import uuid
|
|
8
|
+
import warnings
|
|
7
9
|
from importlib.metadata import PackageNotFoundError
|
|
8
10
|
from importlib.metadata import version as _importlib_version
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
11
13
|
import numpy as np
|
|
12
14
|
import packaging.version
|
|
13
|
-
import zarr
|
|
14
15
|
|
|
15
16
|
import large_image
|
|
16
17
|
from large_image.cache_util import LruCacheMetaclass, methodcache
|
|
@@ -27,6 +28,26 @@ except PackageNotFoundError:
|
|
|
27
28
|
pass
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
zarr = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _lazyImport():
|
|
35
|
+
"""
|
|
36
|
+
Import the zarr module. This is done when needed rather than in the module
|
|
37
|
+
initialization because it is slow.
|
|
38
|
+
"""
|
|
39
|
+
global zarr
|
|
40
|
+
|
|
41
|
+
if zarr is None:
|
|
42
|
+
try:
|
|
43
|
+
import zarr
|
|
44
|
+
|
|
45
|
+
warnings.filterwarnings('ignore', category=FutureWarning, module='.*zarr.*')
|
|
46
|
+
except ImportError:
|
|
47
|
+
msg = 'zarr module not found.'
|
|
48
|
+
raise TileSourceError(msg)
|
|
49
|
+
|
|
50
|
+
|
|
30
51
|
class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
31
52
|
"""
|
|
32
53
|
Provides tile access to files that the zarr library can read.
|
|
@@ -66,6 +87,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
66
87
|
"""
|
|
67
88
|
super().__init__(path, **kwargs)
|
|
68
89
|
|
|
90
|
+
_lazyImport()
|
|
69
91
|
if str(path).startswith(NEW_IMAGE_PATH_FLAG):
|
|
70
92
|
self._initNew(path, **kwargs)
|
|
71
93
|
else:
|
|
@@ -75,6 +97,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
75
97
|
def _initOpen(self, **kwargs):
|
|
76
98
|
self._largeImagePath = str(self._getLargeImagePath())
|
|
77
99
|
self._zarr = None
|
|
100
|
+
self._editable = False
|
|
101
|
+
self._frameValues = None
|
|
102
|
+
self._frameAxes = None
|
|
103
|
+
self._frameUnits = None
|
|
78
104
|
if not os.path.isfile(self._largeImagePath) and '//:' not in self._largeImagePath:
|
|
79
105
|
raise TileSourceFileNotFoundError(self._largeImagePath) from None
|
|
80
106
|
try:
|
|
@@ -105,11 +131,12 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
105
131
|
"""
|
|
106
132
|
Initialize the tile class for creating a new image.
|
|
107
133
|
"""
|
|
108
|
-
self._tempdir = tempfile.
|
|
109
|
-
self.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
self.
|
|
134
|
+
self._tempdir = Path(tempfile.gettempdir(), path)
|
|
135
|
+
self._created = False
|
|
136
|
+
if not self._tempdir.exists():
|
|
137
|
+
self._created = True
|
|
138
|
+
self._zarr_store = zarr.DirectoryStore(str(self._tempdir))
|
|
139
|
+
self._zarr = zarr.open(self._zarr_store, mode='a')
|
|
113
140
|
self._largeImagePath = None
|
|
114
141
|
self._dims = {}
|
|
115
142
|
self.sizeX = self.sizeY = self.levels = 0
|
|
@@ -118,11 +145,24 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
118
145
|
self._output = None
|
|
119
146
|
self._editable = True
|
|
120
147
|
self._bandRanges = None
|
|
121
|
-
self.
|
|
148
|
+
self._threadLock = threading.RLock()
|
|
149
|
+
self._processLock = multiprocessing.Lock()
|
|
122
150
|
self._framecount = 0
|
|
123
151
|
self._mm_x = 0
|
|
124
152
|
self._mm_y = 0
|
|
153
|
+
self._channelNames = []
|
|
154
|
+
self._channelColors = []
|
|
155
|
+
self._imageDescription = None
|
|
125
156
|
self._levels = []
|
|
157
|
+
self._associatedImages = {}
|
|
158
|
+
self._frameValues = None
|
|
159
|
+
self._frameAxes = None
|
|
160
|
+
self._frameUnits = None
|
|
161
|
+
if not self._created:
|
|
162
|
+
try:
|
|
163
|
+
self._validateZarr()
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
126
166
|
|
|
127
167
|
def __del__(self):
|
|
128
168
|
if not hasattr(self, '_derivedSource'):
|
|
@@ -131,7 +171,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
131
171
|
except Exception:
|
|
132
172
|
pass
|
|
133
173
|
try:
|
|
134
|
-
self.
|
|
174
|
+
if self._created:
|
|
175
|
+
shutil.rmtree(self._tempdir)
|
|
135
176
|
except Exception:
|
|
136
177
|
pass
|
|
137
178
|
|
|
@@ -188,10 +229,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
188
229
|
tuple that is used to find the maximum size array, preferring ome
|
|
189
230
|
arrays, then total pixels, then channels. 'is_ome' is a boolean.
|
|
190
231
|
'series' is a list of the found groups and arrays that match the
|
|
191
|
-
best criteria. 'axes' and 'channels'
|
|
192
|
-
'associated' is a list of all groups and
|
|
193
|
-
associated images. These have to be culled
|
|
194
|
-
used in the series.
|
|
232
|
+
best criteria. 'axes', 'axes_values', 'axes_units', and 'channels'
|
|
233
|
+
are from the best array. 'associated' is a list of all groups and
|
|
234
|
+
arrays that might be associated images. These have to be culled
|
|
235
|
+
for the actual groups used in the series.
|
|
195
236
|
"""
|
|
196
237
|
attrs = group.attrs.asdict() if group is not None else {}
|
|
197
238
|
min_version = packaging.version.Version('0.4')
|
|
@@ -203,9 +244,19 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
203
244
|
all(packaging.version.Version(m['version']) >= min_version
|
|
204
245
|
for m in attrs['multiscales'] if 'version' in m))
|
|
205
246
|
channels = None
|
|
247
|
+
axes_values = None
|
|
248
|
+
axes_units = None
|
|
206
249
|
if is_ome:
|
|
207
250
|
axes = {axis['name']: idx for idx, axis in enumerate(
|
|
208
251
|
attrs['multiscales'][0]['axes'])}
|
|
252
|
+
axes_values = {
|
|
253
|
+
axis['name']: axis.get('values')
|
|
254
|
+
for axis in attrs['multiscales'][0]['axes']
|
|
255
|
+
}
|
|
256
|
+
axes_units = {
|
|
257
|
+
axis['name']: axis.get('unit')
|
|
258
|
+
for axis in attrs['multiscales'][0]['axes']
|
|
259
|
+
}
|
|
209
260
|
if isinstance(attrs['omero'].get('channels'), list):
|
|
210
261
|
channels = [channel['label'] for channel in attrs['omero']['channels']]
|
|
211
262
|
if all(channel.startswith('Channel ') for channel in channels):
|
|
@@ -224,6 +275,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
224
275
|
results['series'] = [(group, arr)]
|
|
225
276
|
results['is_ome'] = is_ome
|
|
226
277
|
results['axes'] = axes
|
|
278
|
+
results['axes_values'] = axes_values
|
|
279
|
+
results['axes_units'] = axes_units
|
|
227
280
|
results['channels'] = channels
|
|
228
281
|
elif check == results['best']:
|
|
229
282
|
results['series'].append((group, arr))
|
|
@@ -303,11 +356,53 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
303
356
|
except Exception:
|
|
304
357
|
pass
|
|
305
358
|
|
|
359
|
+
def _readFrameValues(self, found, baseArray):
|
|
360
|
+
"""
|
|
361
|
+
Populate frame values and frame units from image metadata.
|
|
362
|
+
"""
|
|
363
|
+
axes_values = found.get('axes_values')
|
|
364
|
+
axes_units = found.get('axes_units')
|
|
365
|
+
if isinstance(axes_values, dict):
|
|
366
|
+
self._frameAxes = [
|
|
367
|
+
a for a, i in sorted(self._axes.items(), key=lambda x: x[1])
|
|
368
|
+
if axes_values.get(a) is not None
|
|
369
|
+
]
|
|
370
|
+
self._frameUnits = {k: axes_units.get(k) for k in self.frameAxes if k in axes_units}
|
|
371
|
+
self._frameValues = None
|
|
372
|
+
frame_values_shape = [baseArray.shape[self._axes[a]] for a in self.frameAxes]
|
|
373
|
+
frame_values_shape.append(len(frame_values_shape))
|
|
374
|
+
frame_values = np.empty(frame_values_shape, dtype=object)
|
|
375
|
+
all_frame_specs = self.getMetadata().get('frames')
|
|
376
|
+
for axis, values in axes_values.items():
|
|
377
|
+
if axis in self.frameAxes:
|
|
378
|
+
slicing = [slice(None) for i in range(len(frame_values_shape))]
|
|
379
|
+
axis_index = self.frameAxes.index(axis)
|
|
380
|
+
slicing[-1] = axis_index
|
|
381
|
+
if len(values) == frame_values_shape[axis_index]:
|
|
382
|
+
# uniform values have same length as axis
|
|
383
|
+
for i, value in enumerate(values):
|
|
384
|
+
slicing[axis_index] = i
|
|
385
|
+
frame_values[tuple(slicing)] = value
|
|
386
|
+
elif len(values) == self._framecount:
|
|
387
|
+
# non-uniform values have a value for every frame
|
|
388
|
+
for i, value in enumerate(values):
|
|
389
|
+
if all_frame_specs and len(all_frame_specs) > i:
|
|
390
|
+
for k, j in all_frame_specs[i].items():
|
|
391
|
+
if 'Index' in k:
|
|
392
|
+
name = k.replace('Index', '').lower()
|
|
393
|
+
if name:
|
|
394
|
+
slicing[self._frameAxes.index(name)] = j
|
|
395
|
+
frame_values[tuple(slicing)] = value
|
|
396
|
+
if frame_values.size > 0:
|
|
397
|
+
self._frameValues = frame_values
|
|
398
|
+
|
|
306
399
|
def _validateZarr(self):
|
|
307
400
|
"""
|
|
308
401
|
Validate that we can read tiles from the zarr parent group in
|
|
309
402
|
self._zarr. Set up the appropriate class variables.
|
|
310
403
|
"""
|
|
404
|
+
if self._editable and hasattr(self, '_axes'):
|
|
405
|
+
self._writeInternalMetadata()
|
|
311
406
|
found = self._scanZarrGroup(self._zarr)
|
|
312
407
|
if found['best'] is None:
|
|
313
408
|
msg = 'No data array that can be used.'
|
|
@@ -315,13 +410,19 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
315
410
|
self._series = found['series']
|
|
316
411
|
baseGroup, baseArray = self._series[0]
|
|
317
412
|
self._is_ome = found['is_ome']
|
|
318
|
-
self._axes = {
|
|
413
|
+
self._axes = {
|
|
414
|
+
k.lower(): v
|
|
415
|
+
for k, v in found['axes'].items()
|
|
416
|
+
if baseArray.shape[v] > 1 or k in found.get('axes_values', {})
|
|
417
|
+
}
|
|
319
418
|
if len(self._series) > 1 and 'xy' in self._axes:
|
|
320
419
|
msg = 'Conflicting xy axis data.'
|
|
321
420
|
raise TileSourceError(msg)
|
|
322
421
|
self._channels = found['channels']
|
|
323
|
-
self._associatedImages =
|
|
324
|
-
(
|
|
422
|
+
self._associatedImages = {
|
|
423
|
+
g.name.replace('/', ''): (g, a)
|
|
424
|
+
for g, a in found['associated'] if not any(g is gb for gb, _ in self._series)
|
|
425
|
+
}
|
|
325
426
|
self.sizeX = baseArray.shape[self._axes['x']]
|
|
326
427
|
self.sizeY = baseArray.shape[self._axes['y']]
|
|
327
428
|
self.tileWidth = (
|
|
@@ -339,7 +440,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
339
440
|
# self.tileWidth = self.tileHeight = baseArray.chunks[self._axes['x']]
|
|
340
441
|
self.levels = int(max(1, math.ceil(math.log(max(
|
|
341
442
|
self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
|
|
342
|
-
self._dtype = baseArray.dtype
|
|
443
|
+
self._dtype = np.dtype(baseArray.dtype)
|
|
343
444
|
self._bandCount = 1
|
|
344
445
|
if ('c' in self._axes and 's' not in self._axes and not self._channels and
|
|
345
446
|
baseArray.shape[self._axes.get('c')] in {1, 3, 4}):
|
|
@@ -352,8 +453,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
352
453
|
stride = 1
|
|
353
454
|
self._strides = {}
|
|
354
455
|
self._axisCounts = {}
|
|
355
|
-
for _, k in sorted(
|
|
356
|
-
|
|
456
|
+
for _, k in sorted(
|
|
457
|
+
(-self._axes.get(k, 'tzc'.index(k) if k in 'tzc' else -1), k)
|
|
458
|
+
for k in self._axes if k not in 'xys'
|
|
459
|
+
):
|
|
357
460
|
self._strides[k] = stride
|
|
358
461
|
self._axisCounts[k] = baseArray.shape[self._axes[k]]
|
|
359
462
|
stride *= baseArray.shape[self._axes[k]]
|
|
@@ -362,6 +465,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
362
465
|
self._axisCounts['xy'] = len(self._series)
|
|
363
466
|
stride *= len(self._series)
|
|
364
467
|
self._framecount = stride
|
|
468
|
+
self._readFrameValues(found, baseArray)
|
|
365
469
|
|
|
366
470
|
def _nonemptyLevelsList(self, frame=0):
|
|
367
471
|
"""
|
|
@@ -413,6 +517,16 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
413
517
|
for axis in self._strides:
|
|
414
518
|
frame['Index' + axis.upper()] = (
|
|
415
519
|
idx // self._strides[axis]) % self._axisCounts[axis]
|
|
520
|
+
if self.frameValues is not None and self.frameAxes is not None:
|
|
521
|
+
current_frame_slice = tuple(
|
|
522
|
+
frame['Index' + axis.upper()] for axis in self.frameAxes
|
|
523
|
+
)
|
|
524
|
+
current_frame_values = self.frameValues[current_frame_slice]
|
|
525
|
+
for i, axis in enumerate(self.frameAxes):
|
|
526
|
+
value = current_frame_values[i]
|
|
527
|
+
# ensure that values are returned as native python types
|
|
528
|
+
native_value = getattr(value, 'tolist', lambda v=value: v)()
|
|
529
|
+
frame['Value' + axis.upper()] = native_value
|
|
416
530
|
frames.append(frame)
|
|
417
531
|
self._addMetadataFrameInformation(result, getattr(self, '_channels', None))
|
|
418
532
|
return result
|
|
@@ -425,6 +539,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
425
539
|
|
|
426
540
|
:returns: a dictionary of data or None.
|
|
427
541
|
"""
|
|
542
|
+
if self._editable:
|
|
543
|
+
self._writeInternalMetadata()
|
|
428
544
|
result = {}
|
|
429
545
|
result['zarr'] = {
|
|
430
546
|
'base': self._zarr.attrs.asdict(),
|
|
@@ -441,7 +557,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
441
557
|
|
|
442
558
|
:return: the list of image keys.
|
|
443
559
|
"""
|
|
444
|
-
return
|
|
560
|
+
return list(self._associatedImages.keys())
|
|
445
561
|
|
|
446
562
|
def _getAssociatedImage(self, imageKey):
|
|
447
563
|
"""
|
|
@@ -450,15 +566,9 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
450
566
|
:param imageKey: the key of the associated image.
|
|
451
567
|
:return: the image in PIL format or None.
|
|
452
568
|
"""
|
|
453
|
-
if not
|
|
454
|
-
return
|
|
455
|
-
|
|
456
|
-
idx = int(imageKey[6:])
|
|
457
|
-
except Exception:
|
|
458
|
-
return
|
|
459
|
-
if idx < 0 or idx >= len(self._associatedImages):
|
|
460
|
-
return
|
|
461
|
-
group, arr = self._associatedImages[idx]
|
|
569
|
+
if imageKey not in self._associatedImages:
|
|
570
|
+
return None
|
|
571
|
+
group, arr = self._associatedImages[imageKey]
|
|
462
572
|
axes = self._getGeneralAxes(arr)
|
|
463
573
|
trans = [idx for idx in range(len(arr.shape))
|
|
464
574
|
if idx not in axes.values()] + [axes['y'], axes['x']]
|
|
@@ -490,7 +600,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
490
600
|
y1 //= scale
|
|
491
601
|
step //= scale
|
|
492
602
|
if step > 2 ** self._maxSkippedLevels:
|
|
493
|
-
tile = self._getTileFromEmptyLevel(x, y, z, **kwargs)
|
|
603
|
+
tile, _format = self._getTileFromEmptyLevel(x, y, z, **kwargs)
|
|
494
604
|
tile = large_image.tilesource.base._imageToNumpy(tile)[0]
|
|
495
605
|
else:
|
|
496
606
|
idx = [slice(None) for _ in arr.shape]
|
|
@@ -542,6 +652,50 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
542
652
|
|
|
543
653
|
return tile, mask, placement, axes
|
|
544
654
|
|
|
655
|
+
def _updateFrameValues(self, frame_values, placement, axes, new_axes, new_dims):
|
|
656
|
+
self._frameAxes = [
|
|
657
|
+
a for a in axes
|
|
658
|
+
if a in frame_values or
|
|
659
|
+
(self.frameAxes is not None and a in self.frameAxes)
|
|
660
|
+
]
|
|
661
|
+
frames_shape = [new_dims[a] for a in self.frameAxes]
|
|
662
|
+
frames_shape.append(len(frames_shape))
|
|
663
|
+
if self.frameValues is None:
|
|
664
|
+
self._frameValues = np.empty(frames_shape, dtype=object)
|
|
665
|
+
elif self.frameValues.shape != frames_shape:
|
|
666
|
+
if len(new_axes):
|
|
667
|
+
for i in new_axes.values():
|
|
668
|
+
self._frameValues = np.expand_dims(self._frameValues, axis=i)
|
|
669
|
+
frame_padding = [
|
|
670
|
+
(0, s - self.frameValues.shape[i])
|
|
671
|
+
for i, s in enumerate(frames_shape)
|
|
672
|
+
]
|
|
673
|
+
frame_padding[-1] = (0, 0)
|
|
674
|
+
self._frameValues = np.pad(self._frameValues, frame_padding)
|
|
675
|
+
for i in new_axes.values():
|
|
676
|
+
self._frameValues = np.insert(
|
|
677
|
+
self._frameValues, i, 0, axis=len(frames_shape) - 1,
|
|
678
|
+
)
|
|
679
|
+
current_frame_slice = tuple(placement.get(a) for a in self.frameAxes)
|
|
680
|
+
for i, k in enumerate(self.frameAxes):
|
|
681
|
+
self.frameValues[(*current_frame_slice, i)] = frame_values.get(k)
|
|
682
|
+
|
|
683
|
+
def _resizeImage(self, arr, new_shape, new_axes, chunking):
|
|
684
|
+
if new_shape != arr.shape:
|
|
685
|
+
if len(new_axes):
|
|
686
|
+
for i in new_axes.values():
|
|
687
|
+
arr = np.expand_dims(arr, axis=i)
|
|
688
|
+
arr = np.pad(
|
|
689
|
+
arr,
|
|
690
|
+
[(0, s - arr.shape[i]) for i, s in enumerate(new_shape)],
|
|
691
|
+
)
|
|
692
|
+
new_arr = zarr.empty(new_shape, chunks=chunking, dtype=arr.dtype)
|
|
693
|
+
new_arr[:] = arr[:]
|
|
694
|
+
arr = new_arr
|
|
695
|
+
else:
|
|
696
|
+
arr.resize(*new_shape)
|
|
697
|
+
return arr
|
|
698
|
+
|
|
545
699
|
def addTile(self, tile, x=0, y=0, mask=None, axes=None, **kwargs):
|
|
546
700
|
"""
|
|
547
701
|
Add a numpy or image tile to the image, expanding the image as needed
|
|
@@ -561,21 +715,32 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
561
715
|
:param kwargs: start locations for any additional axes. Note that
|
|
562
716
|
``level`` is a reserved word and not permitted for an axis name.
|
|
563
717
|
"""
|
|
564
|
-
# TODO: improve band bookkeeping
|
|
565
|
-
|
|
566
718
|
self._checkEditable()
|
|
719
|
+
try:
|
|
720
|
+
# read any info written by other processes
|
|
721
|
+
self._validateZarr()
|
|
722
|
+
except TileSourceError:
|
|
723
|
+
pass
|
|
724
|
+
updateMetadata = False
|
|
567
725
|
store_path = str(kwargs.pop('level', 0))
|
|
568
726
|
placement = {
|
|
569
727
|
'x': x,
|
|
570
728
|
'y': y,
|
|
571
|
-
**kwargs,
|
|
729
|
+
**{k: v for k, v in kwargs.items() if not k.endswith('_value')},
|
|
730
|
+
}
|
|
731
|
+
frame_values = {
|
|
732
|
+
k.replace('_value', ''): v for k, v in kwargs.items()
|
|
733
|
+
if k not in placement
|
|
572
734
|
}
|
|
573
735
|
tile, mask, placement, axes = self._validateNewTile(tile, mask, placement, axes)
|
|
574
736
|
|
|
575
|
-
with self.
|
|
737
|
+
with self._threadLock and self._processLock:
|
|
738
|
+
old_axes = self._axes if hasattr(self, '_axes') else {}
|
|
576
739
|
self._axes = {k: i for i, k in enumerate(axes)}
|
|
740
|
+
new_axes = {k: i for k, i in self._axes.items() if k not in old_axes}
|
|
577
741
|
new_dims = {
|
|
578
742
|
a: max(
|
|
743
|
+
self._axisCounts.get(a, 0) if hasattr(self, '_axisCounts') else 0,
|
|
579
744
|
self._dims.get(store_path, {}).get(a, 0),
|
|
580
745
|
placement.get(a, 0) + tile.shape[i],
|
|
581
746
|
)
|
|
@@ -587,6 +752,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
587
752
|
for i, a in enumerate(axes)
|
|
588
753
|
])
|
|
589
754
|
|
|
755
|
+
if len(frame_values.keys()) > 0:
|
|
756
|
+
# update self.frameValues
|
|
757
|
+
updateMetadata = True
|
|
758
|
+
self._updateFrameValues(frame_values, placement, axes, new_axes, new_dims)
|
|
759
|
+
|
|
590
760
|
current_arrays = dict(self._zarr.arrays())
|
|
591
761
|
if store_path == '0':
|
|
592
762
|
# if writing to base data, invalidate generated levels
|
|
@@ -603,14 +773,18 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
603
773
|
])
|
|
604
774
|
else:
|
|
605
775
|
arr = current_arrays[store_path]
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
776
|
+
new_shape = tuple(
|
|
777
|
+
max(v, arr.shape[old_axes[k]] if k in old_axes else 0)
|
|
778
|
+
for k, v in new_dims.items()
|
|
779
|
+
)
|
|
780
|
+
if arr.chunks[-1] != new_dims.get('s') or len(new_axes):
|
|
781
|
+
# rechunk if length of samples axis changed or any new axis added
|
|
609
782
|
chunking = tuple([
|
|
610
783
|
self._tileSize if a in ['x', 'y'] else
|
|
611
784
|
new_dims.get('s') if a == 's' else 1
|
|
612
785
|
for a in axes
|
|
613
786
|
])
|
|
787
|
+
arr = self._resizeImage(arr, new_shape, new_axes, chunking)
|
|
614
788
|
|
|
615
789
|
if mask is not None:
|
|
616
790
|
arr[placement_slices] = np.where(mask, tile, arr[placement_slices])
|
|
@@ -625,25 +799,13 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
625
799
|
path=store_path,
|
|
626
800
|
)
|
|
627
801
|
|
|
628
|
-
# If base data changed, update large_image attributes
|
|
802
|
+
# If base data changed, update large_image attributes
|
|
629
803
|
if store_path == '0':
|
|
630
|
-
self.
|
|
631
|
-
'multiscales': [{
|
|
632
|
-
'version': '0.5-dev',
|
|
633
|
-
'axes': [{
|
|
634
|
-
'name': a,
|
|
635
|
-
'type': 'space' if a in ['x', 'y'] else 'other',
|
|
636
|
-
} for a in axes],
|
|
637
|
-
'datasets': [{'path': 0}],
|
|
638
|
-
}],
|
|
639
|
-
'omero': {'version': '0.5-dev'},
|
|
640
|
-
})
|
|
641
|
-
|
|
642
|
-
self._dtype = tile.dtype
|
|
804
|
+
self._dtype = np.dtype(tile.dtype)
|
|
643
805
|
self._bandCount = new_dims.get(axes[-1]) # last axis is assumed to be bands
|
|
644
806
|
self.sizeX = new_dims.get('x')
|
|
645
807
|
self.sizeY = new_dims.get('y')
|
|
646
|
-
self._framecount =
|
|
808
|
+
self._framecount = math.prod([
|
|
647
809
|
length
|
|
648
810
|
for axis, length in new_dims.items()
|
|
649
811
|
if axis in axes[:-3]
|
|
@@ -652,6 +814,157 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
652
814
|
self._levels = None
|
|
653
815
|
self.levels = int(max(1, math.ceil(math.log(max(
|
|
654
816
|
self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
|
|
817
|
+
updateMetadata = True
|
|
818
|
+
if updateMetadata:
|
|
819
|
+
self._writeInternalMetadata()
|
|
820
|
+
|
|
821
|
+
def addAssociatedImage(self, image, imageKey=None):
|
|
822
|
+
"""
|
|
823
|
+
Add an associated image to this source.
|
|
824
|
+
|
|
825
|
+
:param image: a numpy array, PIL Image, or a binary string
|
|
826
|
+
with an image. The numpy array can have 2 or 3 dimensions.
|
|
827
|
+
"""
|
|
828
|
+
data, _ = _imageToNumpy(image)
|
|
829
|
+
with self._threadLock and self._processLock:
|
|
830
|
+
if imageKey is None:
|
|
831
|
+
# Each associated image should be in its own group
|
|
832
|
+
num_existing = len(self.getAssociatedImagesList())
|
|
833
|
+
imageKey = f'image_{num_existing + 1}'
|
|
834
|
+
group = self._zarr.require_group(imageKey)
|
|
835
|
+
arr = zarr.array(
|
|
836
|
+
data,
|
|
837
|
+
store=self._zarr_store,
|
|
838
|
+
path=f'{imageKey}/image',
|
|
839
|
+
)
|
|
840
|
+
self._associatedImages[imageKey] = (group, arr)
|
|
841
|
+
|
|
842
|
+
def _getAxisInternalMetadata(self, axis_name):
|
|
843
|
+
"""
|
|
844
|
+
Get the metadata structure describing an axis in the image.
|
|
845
|
+
This will be written to the image metadata.
|
|
846
|
+
|
|
847
|
+
:param axis_name: a string corresponding to an axis in the image
|
|
848
|
+
:returns: a dictionary describing the target axis
|
|
849
|
+
"""
|
|
850
|
+
axis_metadata = {'name': axis_name}
|
|
851
|
+
if axis_name in ['x', 'y']:
|
|
852
|
+
axis_metadata['type'] = 'space'
|
|
853
|
+
axis_metadata['unit'] = 'millimeter'
|
|
854
|
+
elif axis_name in ['s', 'c']:
|
|
855
|
+
axis_metadata['type'] = 'channel'
|
|
856
|
+
if self.frameAxes is not None:
|
|
857
|
+
axis_index = self.frameAxes.index(axis_name) if axis_name in self.frameAxes else None
|
|
858
|
+
if axis_index is not None and self.frameValues is not None:
|
|
859
|
+
all_frame_values = self.frameValues[..., axis_index]
|
|
860
|
+
split = np.split(
|
|
861
|
+
all_frame_values,
|
|
862
|
+
all_frame_values.shape[axis_index],
|
|
863
|
+
axis=axis_index,
|
|
864
|
+
)
|
|
865
|
+
uniform = all(len(np.unique(
|
|
866
|
+
# convert all values to strings for mixed type comparison with np.unique
|
|
867
|
+
(a[np.not_equal(a, None)]).astype(str),
|
|
868
|
+
)) == 1 for a in split)
|
|
869
|
+
if uniform:
|
|
870
|
+
values = [a.flat[0] for a in split]
|
|
871
|
+
else:
|
|
872
|
+
values = all_frame_values.flatten().tolist()
|
|
873
|
+
axis_metadata['values'] = values
|
|
874
|
+
unit = self.frameUnits.get(axis_name) if self.frameUnits is not None else None
|
|
875
|
+
if unit is not None:
|
|
876
|
+
axis_metadata['unit'] = unit
|
|
877
|
+
return axis_metadata
|
|
878
|
+
|
|
879
|
+
def _writeInternalMetadata(self):
|
|
880
|
+
"""
|
|
881
|
+
Write metadata to Zarr attributes.
|
|
882
|
+
Metadata structure adheres to OME schema from https://ngff.openmicroscopy.org/latest/
|
|
883
|
+
"""
|
|
884
|
+
self._checkEditable()
|
|
885
|
+
with self._threadLock and self._processLock:
|
|
886
|
+
name = str(self._tempdir.name).split('/')[-1]
|
|
887
|
+
arrays = dict(self._zarr.arrays())
|
|
888
|
+
channel_axis = self._axes.get('c', self._axes.get('s'))
|
|
889
|
+
datasets = []
|
|
890
|
+
axes = []
|
|
891
|
+
channels = []
|
|
892
|
+
rdefs = {'model': 'color' if len(self._channelColors) else 'greyscale'}
|
|
893
|
+
sorted_axes = [a[0] for a in sorted(self._axes.items(), key=lambda item: item[1])]
|
|
894
|
+
for arr_name in arrays:
|
|
895
|
+
level = int(arr_name)
|
|
896
|
+
scale = [1.0 for a in sorted_axes]
|
|
897
|
+
scale[self._axes.get('x')] = (self._mm_x or 0) * (2 ** level)
|
|
898
|
+
scale[self._axes.get('y')] = (self._mm_y or 0) * (2 ** level)
|
|
899
|
+
dataset_metadata = {
|
|
900
|
+
'path': arr_name,
|
|
901
|
+
'coordinateTransformations': [{
|
|
902
|
+
'type': 'scale',
|
|
903
|
+
'scale': scale,
|
|
904
|
+
}],
|
|
905
|
+
}
|
|
906
|
+
datasets.append(dataset_metadata)
|
|
907
|
+
for a in sorted_axes:
|
|
908
|
+
if a == 't':
|
|
909
|
+
rdefs['defaultT'] = 0
|
|
910
|
+
elif a == 'z':
|
|
911
|
+
rdefs['defaultZ'] = 0
|
|
912
|
+
axes.append(self._getAxisInternalMetadata(a))
|
|
913
|
+
if channel_axis is not None and len(arrays) > 0:
|
|
914
|
+
base_array = list(arrays.values())[0]
|
|
915
|
+
base_shape = base_array.shape
|
|
916
|
+
for c in range(base_shape[channel_axis]):
|
|
917
|
+
channel_metadata = {
|
|
918
|
+
'active': True,
|
|
919
|
+
'coefficient': 1,
|
|
920
|
+
'color': 'FFFFFF',
|
|
921
|
+
'family': 'linear',
|
|
922
|
+
'inverted': False,
|
|
923
|
+
'label': f'Band {c + 1}',
|
|
924
|
+
}
|
|
925
|
+
# slicing = tuple(
|
|
926
|
+
# slice(None)
|
|
927
|
+
# if k != ('c' if 'c' in self._axes else 's')
|
|
928
|
+
# else c
|
|
929
|
+
# for k, v in self._axes.items()
|
|
930
|
+
# )
|
|
931
|
+
# channel_data = base_array[slicing]
|
|
932
|
+
# channel_min = np.min(channel_data)
|
|
933
|
+
# channel_max = np.max(channel_data)
|
|
934
|
+
# channel_metadata['window'] = {
|
|
935
|
+
# 'end': channel_max,
|
|
936
|
+
# 'max': channel_max,
|
|
937
|
+
# 'min': channel_min,
|
|
938
|
+
# 'start': channel_min,
|
|
939
|
+
# }
|
|
940
|
+
if len(self._channelNames) > c:
|
|
941
|
+
channel_metadata['label'] = self._channelNames[c]
|
|
942
|
+
if len(self._channelColors) > c:
|
|
943
|
+
channel_metadata['color'] = self._channelColors[c]
|
|
944
|
+
channels.append(channel_metadata)
|
|
945
|
+
# Guidelines from https://ngff.openmicroscopy.org/latest/
|
|
946
|
+
self._zarr.attrs.update({
|
|
947
|
+
'multiscales': [{
|
|
948
|
+
'version': '0.5',
|
|
949
|
+
'name': name,
|
|
950
|
+
'axes': axes,
|
|
951
|
+
'datasets': datasets,
|
|
952
|
+
'metadata': {
|
|
953
|
+
'description': self._imageDescription or '',
|
|
954
|
+
'kwargs': {
|
|
955
|
+
'multichannel': (channel_axis is not None),
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
}],
|
|
959
|
+
'omero': {
|
|
960
|
+
'id': 1,
|
|
961
|
+
'version': '0.5',
|
|
962
|
+
'name': name,
|
|
963
|
+
'channels': channels,
|
|
964
|
+
'rdefs': rdefs,
|
|
965
|
+
},
|
|
966
|
+
'bioformats2raw.layout': 3,
|
|
967
|
+
})
|
|
655
968
|
|
|
656
969
|
@property
|
|
657
970
|
def crop(self):
|
|
@@ -678,6 +991,102 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
678
991
|
raise TileSourceError(msg)
|
|
679
992
|
self._crop = (x, y, w, h)
|
|
680
993
|
|
|
994
|
+
@property
|
|
995
|
+
def mm_x(self):
|
|
996
|
+
return self._mm_x
|
|
997
|
+
|
|
998
|
+
@mm_x.setter
|
|
999
|
+
def mm_x(self, value):
|
|
1000
|
+
self._checkEditable()
|
|
1001
|
+
value = float(value) if value is not None else None
|
|
1002
|
+
if value is not None and value <= 0:
|
|
1003
|
+
msg = 'mm_x must be positive or None'
|
|
1004
|
+
raise TileSourceError(msg)
|
|
1005
|
+
self._mm_x = value
|
|
1006
|
+
|
|
1007
|
+
@property
|
|
1008
|
+
def mm_y(self):
|
|
1009
|
+
return self._mm_y
|
|
1010
|
+
|
|
1011
|
+
@mm_y.setter
|
|
1012
|
+
def mm_y(self, value):
|
|
1013
|
+
self._checkEditable()
|
|
1014
|
+
value = float(value) if value is not None else None
|
|
1015
|
+
if value is not None and value <= 0:
|
|
1016
|
+
msg = 'mm_y must be positive or None'
|
|
1017
|
+
raise TileSourceError(msg)
|
|
1018
|
+
self._mm_y = value
|
|
1019
|
+
|
|
1020
|
+
@property
|
|
1021
|
+
def imageDescription(self):
|
|
1022
|
+
return self._imageDescription
|
|
1023
|
+
|
|
1024
|
+
@imageDescription.setter
|
|
1025
|
+
def imageDescription(self, description):
|
|
1026
|
+
self._checkEditable()
|
|
1027
|
+
self._imageDescription = description
|
|
1028
|
+
|
|
1029
|
+
@property
|
|
1030
|
+
def channelNames(self):
|
|
1031
|
+
return self._channelNames
|
|
1032
|
+
|
|
1033
|
+
@channelNames.setter
|
|
1034
|
+
def channelNames(self, names):
|
|
1035
|
+
self._checkEditable()
|
|
1036
|
+
self._channelNames = names
|
|
1037
|
+
|
|
1038
|
+
@property
|
|
1039
|
+
def channelColors(self):
|
|
1040
|
+
return self._channelColors
|
|
1041
|
+
|
|
1042
|
+
@channelColors.setter
|
|
1043
|
+
def channelColors(self, colors):
|
|
1044
|
+
self._checkEditable()
|
|
1045
|
+
self._channelColors = colors
|
|
1046
|
+
|
|
1047
|
+
@property
|
|
1048
|
+
def frameAxes(self):
|
|
1049
|
+
return self._frameAxes
|
|
1050
|
+
|
|
1051
|
+
@frameAxes.setter
|
|
1052
|
+
def frameAxes(self, axes):
|
|
1053
|
+
self._checkEditable()
|
|
1054
|
+
self._frameAxes = axes
|
|
1055
|
+
self._writeInternalMetadata()
|
|
1056
|
+
|
|
1057
|
+
@property
|
|
1058
|
+
def frameUnits(self):
|
|
1059
|
+
return self._frameUnits
|
|
1060
|
+
|
|
1061
|
+
@frameUnits.setter
|
|
1062
|
+
def frameUnits(self, units):
|
|
1063
|
+
self._checkEditable()
|
|
1064
|
+
if self.frameAxes is None:
|
|
1065
|
+
err = 'frameAxes must be set first with a list of frame axis names.'
|
|
1066
|
+
raise ValueError(err)
|
|
1067
|
+
if not isinstance(units, dict) or not all(
|
|
1068
|
+
k in self.frameAxes for k in units.keys()
|
|
1069
|
+
):
|
|
1070
|
+
err = 'frameUnits must be a dictionary with keys that exist in frameAxes.'
|
|
1071
|
+
raise ValueError(err)
|
|
1072
|
+
self._frameUnits = units
|
|
1073
|
+
|
|
1074
|
+
@property
|
|
1075
|
+
def frameValues(self):
|
|
1076
|
+
return self._frameValues
|
|
1077
|
+
|
|
1078
|
+
@frameValues.setter
|
|
1079
|
+
def frameValues(self, a):
|
|
1080
|
+
self._checkEditable()
|
|
1081
|
+
if self.frameAxes is None:
|
|
1082
|
+
err = 'frameAxes must be set first with a list of frame axis names.'
|
|
1083
|
+
raise ValueError(err)
|
|
1084
|
+
if len(a.shape) != len(self.frameAxes) + 1:
|
|
1085
|
+
err = f'frameValues must have {len(self.frameAxes) + 1} dimensions.'
|
|
1086
|
+
raise ValueError(err)
|
|
1087
|
+
self._frameValues = a
|
|
1088
|
+
self._writeInternalMetadata()
|
|
1089
|
+
|
|
681
1090
|
def _generateDownsampledLevels(self, resample_method):
|
|
682
1091
|
self._checkEditable()
|
|
683
1092
|
current_arrays = dict(self._zarr.arrays())
|
|
@@ -701,6 +1110,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
701
1110
|
width=4096 + tile_overlap['x'],
|
|
702
1111
|
height=4096 + tile_overlap['y'],
|
|
703
1112
|
)
|
|
1113
|
+
sorted_axes = [a[0] for a in sorted(self._axes.items(), key=lambda item: item[1])]
|
|
704
1114
|
for level in range(1, self.levels):
|
|
705
1115
|
scale_factor = 2 ** level
|
|
706
1116
|
iterator_output = dict(
|
|
@@ -735,7 +1145,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
735
1145
|
x=x,
|
|
736
1146
|
y=y,
|
|
737
1147
|
**frame_position,
|
|
738
|
-
axes=
|
|
1148
|
+
axes=sorted_axes,
|
|
739
1149
|
level=level,
|
|
740
1150
|
)
|
|
741
1151
|
self._validateZarr() # refresh self._levels before continuing
|
|
@@ -747,6 +1157,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
747
1157
|
alpha=True,
|
|
748
1158
|
overwriteAllowed=True,
|
|
749
1159
|
resample=None,
|
|
1160
|
+
**converterParams,
|
|
750
1161
|
):
|
|
751
1162
|
"""
|
|
752
1163
|
Output the current image to a file.
|
|
@@ -759,6 +1170,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
759
1170
|
:param resample: one of the ``ResampleMethod`` enum values. Defaults
|
|
760
1171
|
to ``NP_NEAREST`` for lossless and non-uint8 data and to
|
|
761
1172
|
``PIL_LANCZOS`` for lossy uint8 data.
|
|
1173
|
+
:param converterParams: options to pass to the large_image_converter if
|
|
1174
|
+
the output is not a zarr variant.
|
|
762
1175
|
"""
|
|
763
1176
|
if os.path.exists(path):
|
|
764
1177
|
if overwriteAllowed:
|
|
@@ -795,6 +1208,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
795
1208
|
**frame_position,
|
|
796
1209
|
)
|
|
797
1210
|
|
|
1211
|
+
source._validateZarr()
|
|
1212
|
+
|
|
798
1213
|
if suffix in ['.zarr', '.db', '.sqlite', '.zip']:
|
|
799
1214
|
if resample is None:
|
|
800
1215
|
resample = (
|
|
@@ -803,9 +1218,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
803
1218
|
else ResampleMethod.NP_NEAREST
|
|
804
1219
|
)
|
|
805
1220
|
source._generateDownsampledLevels(resample)
|
|
1221
|
+
source._writeInternalMetadata() # rewrite with new level datasets
|
|
806
1222
|
|
|
807
1223
|
if suffix == '.zarr':
|
|
808
|
-
shutil.copytree(source._tempdir
|
|
1224
|
+
shutil.copytree(str(source._tempdir), path)
|
|
809
1225
|
elif suffix in ['.db', '.sqlite']:
|
|
810
1226
|
sqlite_store = zarr.SQLiteStore(path)
|
|
811
1227
|
zarr.copy_store(source._zarr_store, sqlite_store, if_exists='replace')
|
|
@@ -818,10 +1234,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
818
1234
|
else:
|
|
819
1235
|
from large_image_converter import convert
|
|
820
1236
|
|
|
821
|
-
attrs_path =
|
|
1237
|
+
attrs_path = source._tempdir / '.zattrs'
|
|
822
1238
|
params = {}
|
|
823
1239
|
if lossy and self.dtype == np.uint8:
|
|
824
1240
|
params['compression'] = 'jpeg'
|
|
1241
|
+
params.update(converterParams)
|
|
825
1242
|
convert(str(attrs_path), path, overwrite=overwriteAllowed, **params)
|
|
826
1243
|
|
|
827
1244
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: large-image-source-zarr
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.7.dev12
|
|
4
4
|
Summary: A OME Zarr tilesource for large_image.
|
|
5
5
|
Home-page: https://github.com/girder/large_image
|
|
6
6
|
Author: Kitware, Inc.
|
|
@@ -15,13 +15,16 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
19
|
Requires-Python: >=3.6
|
|
20
|
+
Description-Content-Type: text/x-rst
|
|
19
21
|
License-File: LICENSE
|
|
20
|
-
Requires-Dist: large-image>=1.
|
|
21
|
-
Requires-Dist: zarr
|
|
22
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: large-image>=1.30.7.dev12
|
|
23
|
+
Requires-Dist: zarr<3
|
|
24
|
+
Requires-Dist: numcodecs
|
|
25
|
+
Requires-Dist: imagecodecs-numcodecs!=2024.9.22
|
|
23
26
|
Provides-Extra: girder
|
|
24
|
-
Requires-Dist: girder-large-image>=1.
|
|
27
|
+
Requires-Dist: girder-large-image>=1.30.7.dev12; extra == "girder"
|
|
25
28
|
Provides-Extra: all
|
|
26
29
|
Requires-Dist: large-image-converter; extra == "all"
|
|
27
30
|
|
|
@@ -19,8 +19,7 @@ def prerelease_local_scheme(version):
|
|
|
19
19
|
|
|
20
20
|
if os.getenv('CIRCLE_BRANCH') in ('master', ):
|
|
21
21
|
return ''
|
|
22
|
-
|
|
23
|
-
return get_local_node_and_date(version)
|
|
22
|
+
return get_local_node_and_date(version)
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
try:
|
|
@@ -37,6 +36,7 @@ setup(
|
|
|
37
36
|
'fallback_version': '0.0.0'},
|
|
38
37
|
description=description,
|
|
39
38
|
long_description=long_description,
|
|
39
|
+
long_description_content_type='text/x-rst',
|
|
40
40
|
license='Apache Software License 2.0',
|
|
41
41
|
author='Kitware, Inc.',
|
|
42
42
|
author_email='kitware@kitware.com',
|
|
@@ -49,13 +49,16 @@ setup(
|
|
|
49
49
|
'Programming Language :: Python :: 3.10',
|
|
50
50
|
'Programming Language :: Python :: 3.11',
|
|
51
51
|
'Programming Language :: Python :: 3.12',
|
|
52
|
+
'Programming Language :: Python :: 3.13',
|
|
52
53
|
],
|
|
53
54
|
install_requires=[
|
|
54
55
|
f'large-image{limit_version}',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
'
|
|
56
|
+
# zarr 3 removes the SQLiteStore
|
|
57
|
+
'zarr<3',
|
|
58
|
+
# numcodecs had been required by zarr, but now needs to be asked for
|
|
59
|
+
'numcodecs',
|
|
60
|
+
# Without imagecodecs-numcodecs, some jpeg encoded data cannot be read
|
|
61
|
+
'imagecodecs-numcodecs!=2024.9.22',
|
|
59
62
|
],
|
|
60
63
|
extras_require={
|
|
61
64
|
'girder': f'girder-large-image{limit_version}',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|