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.
Files changed (15) hide show
  1. {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/PKG-INFO +8 -5
  2. {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/README.rst +20 -40
  3. {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/large_image_source_zarr/__init__.py +471 -54
  4. {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
  5. large_image_source_zarr-1.30.7.dev12/large_image_source_zarr.egg-info/requires.txt +10 -0
  6. {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/setup.py +9 -6
  7. large-image-source-zarr-1.28.2.dev12/large_image_source_zarr.egg-info/requires.txt +0 -9
  8. {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/LICENSE +0 -0
  9. {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
  10. {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
  11. {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
  12. {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
  13. {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
  14. {large-image-source-zarr-1.28.2.dev12 → large_image_source_zarr-1.30.7.dev12}/pyproject.toml +0 -0
  15. {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.28.2.dev12
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.28.2.dev12
21
- Requires-Dist: zarr
22
- Requires-Dist: imagecodecs-numcodecs
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.28.2.dev12; extra == "girder"
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
- Install all tile sources and all Girder plugins on linux::
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. Some of the source modules are available on conda-forge. You can install the following::
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 images.
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 some multi-frame OMETiff files.
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
- - As a Girder plugin:
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
- - ``girder-large-image``: Large Image as a Girder_ 3.x plugin.
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-annotation``: Annotations for large images as a Girder_ 3.x plugin.
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://github.com/girder/girder
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.TemporaryDirectory(path)
109
- self._zarr_store = zarr.DirectoryStore(self._tempdir.name)
110
- self._zarr = zarr.open(self._zarr_store, mode='w')
111
- # Make unpickleable
112
- self._unpickleable = True
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._addLock = threading.RLock()
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._tempdir.cleanup()
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' are from the best array.
192
- 'associated' is a list of all groups and arrays that might be
193
- associated images. These have to be culled for the actual groups
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 = {k.lower(): v for k, v in found['axes'].items() if baseArray.shape[v] > 1}
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
- (g, a) for g, a in found['associated'] if not any(g is gb for gb, _ in self._series)]
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((-'tzc'.index(k) if k in 'tzc' else 1, k)
356
- for k in self._axes if k not in 'xys'):
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 [f'image_{idx}' for idx in range(len(self._associatedImages))]
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 imageKey.startswith('image_'):
454
- return
455
- try:
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._addLock:
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
- arr.resize(*tuple(new_dims.values()))
607
- if arr.chunks[-1] != new_dims.get('s'):
608
- # rechunk if length of samples axis changes
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 and OME metadata
802
+ # If base data changed, update large_image attributes
629
803
  if store_path == '0':
630
- self._zarr.attrs.update({
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 = np.prod([
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=list(self._axes.keys()),
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.name, path)
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 = Path(source._tempdir.name) / '.zattrs'
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.28.2.dev12
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.28.2.dev12
21
- Requires-Dist: zarr
22
- Requires-Dist: imagecodecs-numcodecs
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.28.2.dev12; extra == "girder"
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
 
@@ -0,0 +1,10 @@
1
+ large-image>=1.30.7.dev12
2
+ zarr<3
3
+ numcodecs
4
+ imagecodecs-numcodecs!=2024.9.22
5
+
6
+ [all]
7
+ large-image-converter
8
+
9
+ [girder]
10
+ girder-large-image>=1.30.7.dev12
@@ -19,8 +19,7 @@ def prerelease_local_scheme(version):
19
19
 
20
20
  if os.getenv('CIRCLE_BRANCH') in ('master', ):
21
21
  return ''
22
- else:
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
- 'zarr',
56
- # I am uncertain why this is required, since numcodecs is required by
57
- # zarr; but without it some jpeg encoded data cannot be read
58
- 'imagecodecs-numcodecs',
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}',
@@ -1,9 +0,0 @@
1
- large-image>=1.28.2.dev12
2
- zarr
3
- imagecodecs-numcodecs
4
-
5
- [all]
6
- large-image-converter
7
-
8
- [girder]
9
- girder-large-image>=1.28.2.dev12