large-image-source-zarr 1.29.3.dev39__py3-none-any.whl → 1.33.6a188__py3-none-any.whl
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/__init__.py +511 -85
- large_image_source_zarr-1.33.6a188.dist-info/METADATA +45 -0
- large_image_source_zarr-1.33.6a188.dist-info/RECORD +8 -0
- {large_image_source_zarr-1.29.3.dev39.dist-info → large_image_source_zarr-1.33.6a188.dist-info}/WHEEL +1 -1
- large_image_source_zarr-1.29.3.dev39.dist-info/METADATA +0 -30
- large_image_source_zarr-1.29.3.dev39.dist-info/RECORD +0 -8
- {large_image_source_zarr-1.29.3.dev39.dist-info → large_image_source_zarr-1.33.6a188.dist-info}/entry_points.txt +0 -0
- {large_image_source_zarr-1.29.3.dev39.dist-info → large_image_source_zarr-1.33.6a188.dist-info/licenses}/LICENSE +0 -0
- {large_image_source_zarr-1.29.3.dev39.dist-info → large_image_source_zarr-1.33.6a188.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import importlib.metadata
|
|
3
|
+
import json
|
|
1
4
|
import math
|
|
2
5
|
import multiprocessing
|
|
3
6
|
import os
|
|
@@ -6,8 +9,6 @@ import tempfile
|
|
|
6
9
|
import threading
|
|
7
10
|
import uuid
|
|
8
11
|
import warnings
|
|
9
|
-
from importlib.metadata import PackageNotFoundError
|
|
10
|
-
from importlib.metadata import version as _importlib_version
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
13
14
|
import numpy as np
|
|
@@ -21,15 +22,10 @@ from large_image.tilesource import FileTileSource
|
|
|
21
22
|
from large_image.tilesource.resample import ResampleMethod, downsampleTileHalfRes
|
|
22
23
|
from large_image.tilesource.utilities import _imageToNumpy, nearPowerOfTwo
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
__version__ =
|
|
26
|
-
except PackageNotFoundError:
|
|
27
|
-
# package is not installed
|
|
28
|
-
pass
|
|
25
|
+
with contextlib.suppress(importlib.metadata.PackageNotFoundError):
|
|
26
|
+
__version__ = importlib.metadata.version(__name__)
|
|
29
27
|
|
|
30
28
|
|
|
31
|
-
warnings.filterwarnings('ignore', category=FutureWarning, module='zarr')
|
|
32
|
-
|
|
33
29
|
zarr = None
|
|
34
30
|
|
|
35
31
|
|
|
@@ -43,6 +39,8 @@ def _lazyImport():
|
|
|
43
39
|
if zarr is None:
|
|
44
40
|
try:
|
|
45
41
|
import zarr
|
|
42
|
+
|
|
43
|
+
warnings.filterwarnings('ignore', category=FutureWarning, module='.*zarr.*')
|
|
46
44
|
except ImportError:
|
|
47
45
|
msg = 'zarr module not found.'
|
|
48
46
|
raise TileSourceError(msg)
|
|
@@ -92,12 +90,19 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
92
90
|
self._initNew(path, **kwargs)
|
|
93
91
|
else:
|
|
94
92
|
self._initOpen(**kwargs)
|
|
93
|
+
internal = self.getInternalMetadata().get('zarr', {}).get('base', {})
|
|
94
|
+
multiscale = internal.get('multiscales', [None])[0]
|
|
95
|
+
if multiscale is not None:
|
|
96
|
+
self._imageDescription = multiscale.get('metadata', {}).get('description')
|
|
95
97
|
self._tileLock = threading.RLock()
|
|
96
98
|
|
|
97
99
|
def _initOpen(self, **kwargs):
|
|
98
100
|
self._largeImagePath = str(self._getLargeImagePath())
|
|
99
101
|
self._zarr = None
|
|
100
102
|
self._editable = False
|
|
103
|
+
self._frameValues = None
|
|
104
|
+
self._frameAxes = None
|
|
105
|
+
self._frameUnits = None
|
|
101
106
|
if not os.path.isfile(self._largeImagePath) and '//:' not in self._largeImagePath:
|
|
102
107
|
raise TileSourceFileNotFoundError(self._largeImagePath) from None
|
|
103
108
|
try:
|
|
@@ -107,10 +112,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
107
112
|
self._zarr = zarr.open(self._largeImagePath, mode='r')
|
|
108
113
|
except Exception:
|
|
109
114
|
if os.path.basename(self._largeImagePath) in {'.zgroup', '.zattrs', '.zarray'}:
|
|
110
|
-
|
|
115
|
+
with contextlib.suppress(Exception):
|
|
111
116
|
self._zarr = zarr.open(os.path.dirname(self._largeImagePath), mode='r')
|
|
112
|
-
except Exception:
|
|
113
|
-
pass
|
|
114
117
|
if self._zarr is None:
|
|
115
118
|
if not os.path.isfile(self._largeImagePath):
|
|
116
119
|
raise TileSourceFileNotFoundError(self._largeImagePath) from None
|
|
@@ -145,6 +148,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
145
148
|
self._threadLock = threading.RLock()
|
|
146
149
|
self._processLock = multiprocessing.Lock()
|
|
147
150
|
self._framecount = 0
|
|
151
|
+
self._minWidth = None
|
|
152
|
+
self._minHeight = None
|
|
148
153
|
self._mm_x = 0
|
|
149
154
|
self._mm_y = 0
|
|
150
155
|
self._channelNames = []
|
|
@@ -152,13 +157,19 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
152
157
|
self._imageDescription = None
|
|
153
158
|
self._levels = []
|
|
154
159
|
self._associatedImages = {}
|
|
160
|
+
self._frameValues = None
|
|
161
|
+
self._frameAxes = None
|
|
162
|
+
self._frameUnits = None
|
|
163
|
+
self._projection = None
|
|
164
|
+
self._gcps = None
|
|
165
|
+
if not self._created:
|
|
166
|
+
with contextlib.suppress(Exception):
|
|
167
|
+
self._validateZarr()
|
|
155
168
|
|
|
156
169
|
def __del__(self):
|
|
157
170
|
if not hasattr(self, '_derivedSource'):
|
|
158
|
-
|
|
171
|
+
with contextlib.suppress(Exception):
|
|
159
172
|
self._zarr.close()
|
|
160
|
-
except Exception:
|
|
161
|
-
pass
|
|
162
173
|
try:
|
|
163
174
|
if self._created:
|
|
164
175
|
shutil.rmtree(self._tempdir)
|
|
@@ -218,10 +229,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
218
229
|
tuple that is used to find the maximum size array, preferring ome
|
|
219
230
|
arrays, then total pixels, then channels. 'is_ome' is a boolean.
|
|
220
231
|
'series' is a list of the found groups and arrays that match the
|
|
221
|
-
best criteria. 'axes' and 'channels'
|
|
222
|
-
'associated' is a list of all groups and
|
|
223
|
-
associated images. These have to be culled
|
|
224
|
-
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.
|
|
225
236
|
"""
|
|
226
237
|
attrs = group.attrs.asdict() if group is not None else {}
|
|
227
238
|
min_version = packaging.version.Version('0.4')
|
|
@@ -233,9 +244,19 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
233
244
|
all(packaging.version.Version(m['version']) >= min_version
|
|
234
245
|
for m in attrs['multiscales'] if 'version' in m))
|
|
235
246
|
channels = None
|
|
247
|
+
axes_values = None
|
|
248
|
+
axes_units = None
|
|
236
249
|
if is_ome:
|
|
237
250
|
axes = {axis['name']: idx for idx, axis in enumerate(
|
|
238
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
|
+
}
|
|
239
260
|
if isinstance(attrs['omero'].get('channels'), list):
|
|
240
261
|
channels = [channel['label'] for channel in attrs['omero']['channels']]
|
|
241
262
|
if all(channel.startswith('Channel ') for channel in channels):
|
|
@@ -254,11 +275,13 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
254
275
|
results['series'] = [(group, arr)]
|
|
255
276
|
results['is_ome'] = is_ome
|
|
256
277
|
results['axes'] = axes
|
|
278
|
+
results['axes_values'] = axes_values
|
|
279
|
+
results['axes_units'] = axes_units
|
|
257
280
|
results['channels'] = channels
|
|
258
281
|
elif check == results['best']:
|
|
259
282
|
results['series'].append((group, arr))
|
|
260
283
|
if not any(group is g for g, _ in results['associated']):
|
|
261
|
-
axes = {k: v for k, v in axes.items() if arr.shape[axes[k]] > 1}
|
|
284
|
+
axes = {k: v for k, v in axes.items() if arr.shape[axes[k]] > 1 or k in {'x', 'y'}}
|
|
262
285
|
if (len(axes) <= 3 and
|
|
263
286
|
self._minAssociatedImageSize <= arr.shape[axes['x']] <=
|
|
264
287
|
self._maxAssociatedImageSize and
|
|
@@ -333,6 +356,46 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
333
356
|
except Exception:
|
|
334
357
|
pass
|
|
335
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.zeros(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
|
+
|
|
336
399
|
def _validateZarr(self):
|
|
337
400
|
"""
|
|
338
401
|
Validate that we can read tiles from the zarr parent group in
|
|
@@ -347,7 +410,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
347
410
|
self._series = found['series']
|
|
348
411
|
baseGroup, baseArray = self._series[0]
|
|
349
412
|
self._is_ome = found['is_ome']
|
|
350
|
-
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
|
+
}
|
|
351
418
|
if len(self._series) > 1 and 'xy' in self._axes:
|
|
352
419
|
msg = 'Conflicting xy axis data.'
|
|
353
420
|
raise TileSourceError(msg)
|
|
@@ -373,7 +440,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
373
440
|
# self.tileWidth = self.tileHeight = baseArray.chunks[self._axes['x']]
|
|
374
441
|
self.levels = int(max(1, math.ceil(math.log(max(
|
|
375
442
|
self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
|
|
376
|
-
self._dtype = baseArray.dtype
|
|
443
|
+
self._dtype = np.dtype(baseArray.dtype)
|
|
377
444
|
self._bandCount = 1
|
|
378
445
|
if ('c' in self._axes and 's' not in self._axes and not self._channels and
|
|
379
446
|
baseArray.shape[self._axes.get('c')] in {1, 3, 4}):
|
|
@@ -386,8 +453,16 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
386
453
|
stride = 1
|
|
387
454
|
self._strides = {}
|
|
388
455
|
self._axisCounts = {}
|
|
389
|
-
|
|
390
|
-
|
|
456
|
+
# If we aren't in editable mode, prefer the channel axis to have a
|
|
457
|
+
# stride of 1, then the z axis, then the t axis, then sorted by the
|
|
458
|
+
# axis name.
|
|
459
|
+
axisOrder = ((-'tzc'.index(k) if k in 'tzc' else 1, k)
|
|
460
|
+
for k in self._axes if k not in {'x', 'y', 's'})
|
|
461
|
+
# In editable mode, prefer the order that the axes are being written.
|
|
462
|
+
if self._editable:
|
|
463
|
+
axisOrder = ((-self._axes.get(k, 'tzc'.index(k) if k in 'tzc' else -1), k)
|
|
464
|
+
for k in self._axes if k not in {'x', 'y', 's'})
|
|
465
|
+
for _, k in sorted(axisOrder):
|
|
391
466
|
self._strides[k] = stride
|
|
392
467
|
self._axisCounts[k] = baseArray.shape[self._axes[k]]
|
|
393
468
|
stride *= baseArray.shape[self._axes[k]]
|
|
@@ -396,6 +471,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
396
471
|
self._axisCounts['xy'] = len(self._series)
|
|
397
472
|
stride *= len(self._series)
|
|
398
473
|
self._framecount = stride
|
|
474
|
+
self._readFrameValues(found, baseArray)
|
|
399
475
|
|
|
400
476
|
def _nonemptyLevelsList(self, frame=0):
|
|
401
477
|
"""
|
|
@@ -447,6 +523,16 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
447
523
|
for axis in self._strides:
|
|
448
524
|
frame['Index' + axis.upper()] = (
|
|
449
525
|
idx // self._strides[axis]) % self._axisCounts[axis]
|
|
526
|
+
if self.frameValues is not None and self.frameAxes is not None:
|
|
527
|
+
current_frame_slice = tuple(
|
|
528
|
+
frame['Index' + axis.upper()] for axis in self.frameAxes
|
|
529
|
+
)
|
|
530
|
+
current_frame_values = self.frameValues[current_frame_slice]
|
|
531
|
+
for i, axis in enumerate(self.frameAxes):
|
|
532
|
+
value = current_frame_values[i]
|
|
533
|
+
# ensure that values are returned as native python types
|
|
534
|
+
native_value = getattr(value, 'tolist', lambda v=value: v)()
|
|
535
|
+
frame['Value' + axis.upper()] = native_value
|
|
450
536
|
frames.append(frame)
|
|
451
537
|
self._addMetadataFrameInformation(result, getattr(self, '_channels', None))
|
|
452
538
|
return result
|
|
@@ -465,10 +551,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
465
551
|
result['zarr'] = {
|
|
466
552
|
'base': self._zarr.attrs.asdict(),
|
|
467
553
|
}
|
|
468
|
-
|
|
554
|
+
with contextlib.suppress(Exception):
|
|
469
555
|
result['zarr']['main'] = self._series[0][0].attrs.asdict()
|
|
470
|
-
except Exception:
|
|
471
|
-
pass
|
|
472
556
|
return result
|
|
473
557
|
|
|
474
558
|
def getAssociatedImagesList(self):
|
|
@@ -487,7 +571,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
487
571
|
:return: the image in PIL format or None.
|
|
488
572
|
"""
|
|
489
573
|
if imageKey not in self._associatedImages:
|
|
490
|
-
return
|
|
574
|
+
return None
|
|
491
575
|
group, arr = self._associatedImages[imageKey]
|
|
492
576
|
axes = self._getGeneralAxes(arr)
|
|
493
577
|
trans = [idx for idx in range(len(arr.shape))
|
|
@@ -520,7 +604,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
520
604
|
y1 //= scale
|
|
521
605
|
step //= scale
|
|
522
606
|
if step > 2 ** self._maxSkippedLevels:
|
|
523
|
-
tile = self._getTileFromEmptyLevel(x, y, z, **kwargs)
|
|
607
|
+
tile, _format = self._getTileFromEmptyLevel(x, y, z, **kwargs)
|
|
524
608
|
tile = large_image.tilesource.base._imageToNumpy(tile)[0]
|
|
525
609
|
else:
|
|
526
610
|
idx = [slice(None) for _ in arr.shape]
|
|
@@ -549,7 +633,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
549
633
|
|
|
550
634
|
def _validateNewTile(self, tile, mask, placement, axes):
|
|
551
635
|
if not isinstance(tile, np.ndarray) or axes is None:
|
|
552
|
-
axes = 'yxs'
|
|
636
|
+
axes = self._axes if hasattr(self, '_axes') else 'yxs'
|
|
553
637
|
tile, mode = _imageToNumpy(tile)
|
|
554
638
|
elif not isinstance(axes, str) and not isinstance(axes, list):
|
|
555
639
|
err = 'Invalid type for axes. Must be str or list[str].'
|
|
@@ -557,6 +641,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
557
641
|
axes = [x.lower() for x in axes]
|
|
558
642
|
if axes[-1] != 's':
|
|
559
643
|
axes.append('s')
|
|
644
|
+
tile = tile[..., np.newaxis]
|
|
560
645
|
if mask is not None and len(axes) - 1 == len(mask.shape):
|
|
561
646
|
mask = mask[:, :, np.newaxis]
|
|
562
647
|
if 'x' not in axes or 'y' not in axes:
|
|
@@ -572,6 +657,52 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
572
657
|
|
|
573
658
|
return tile, mask, placement, axes
|
|
574
659
|
|
|
660
|
+
def _updateFrameValues(self, frame_values, placement, axes, new_axes, new_dims):
|
|
661
|
+
self._frameAxes = [
|
|
662
|
+
a for a in axes
|
|
663
|
+
if a in frame_values or
|
|
664
|
+
(self.frameAxes is not None and a in self.frameAxes)
|
|
665
|
+
]
|
|
666
|
+
frames_shape = [new_dims[a] for a in self.frameAxes]
|
|
667
|
+
frames_shape.append(len(frames_shape))
|
|
668
|
+
if self.frameValues is None:
|
|
669
|
+
self._frameValues = np.empty(frames_shape, dtype=object)
|
|
670
|
+
elif self.frameValues.shape != frames_shape:
|
|
671
|
+
if len(new_axes):
|
|
672
|
+
for i in new_axes.values():
|
|
673
|
+
self._frameValues = np.expand_dims(self._frameValues, axis=i)
|
|
674
|
+
frame_padding = [
|
|
675
|
+
(0, s - self.frameValues.shape[i])
|
|
676
|
+
for i, s in enumerate(frames_shape)
|
|
677
|
+
]
|
|
678
|
+
frame_padding[-1] = (0, 0)
|
|
679
|
+
self._frameValues = np.pad(self._frameValues, frame_padding)
|
|
680
|
+
for i in new_axes.values():
|
|
681
|
+
self._frameValues = np.insert(
|
|
682
|
+
self._frameValues, i, 0, axis=len(frames_shape) - 1,
|
|
683
|
+
)
|
|
684
|
+
current_frame_slice = tuple(placement.get(a) for a in self.frameAxes)
|
|
685
|
+
for i, k in enumerate(self.frameAxes):
|
|
686
|
+
self.frameValues[(*current_frame_slice, i)] = frame_values.get(k)
|
|
687
|
+
|
|
688
|
+
def _resizeImage(self, arr, new_shape, new_axes, chunking):
|
|
689
|
+
if new_shape != arr.shape:
|
|
690
|
+
if len(new_axes):
|
|
691
|
+
for i in new_axes.values():
|
|
692
|
+
arr = np.expand_dims(arr, axis=i)
|
|
693
|
+
arr = np.pad(
|
|
694
|
+
arr,
|
|
695
|
+
[(0, s - arr.shape[i]) for i, s in enumerate(new_shape)],
|
|
696
|
+
)
|
|
697
|
+
new_arr = zarr.zeros(
|
|
698
|
+
new_shape, chunks=chunking, dtype=arr.dtype,
|
|
699
|
+
write_empty_chunks=False)
|
|
700
|
+
new_arr[:] = arr[:]
|
|
701
|
+
arr = new_arr
|
|
702
|
+
else:
|
|
703
|
+
arr.resize(*new_shape)
|
|
704
|
+
return arr
|
|
705
|
+
|
|
575
706
|
def addTile(self, tile, x=0, y=0, mask=None, axes=None, **kwargs):
|
|
576
707
|
"""
|
|
577
708
|
Add a numpy or image tile to the image, expanding the image as needed
|
|
@@ -592,18 +723,29 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
592
723
|
``level`` is a reserved word and not permitted for an axis name.
|
|
593
724
|
"""
|
|
594
725
|
self._checkEditable()
|
|
726
|
+
with contextlib.suppress(TileSourceError):
|
|
727
|
+
# read any info written by other processes
|
|
728
|
+
self._validateZarr()
|
|
729
|
+
updateMetadata = False
|
|
595
730
|
store_path = str(kwargs.pop('level', 0))
|
|
596
731
|
placement = {
|
|
597
732
|
'x': x,
|
|
598
733
|
'y': y,
|
|
599
|
-
**kwargs,
|
|
734
|
+
**{k: v for k, v in kwargs.items() if not k.endswith('_value')},
|
|
735
|
+
}
|
|
736
|
+
frame_values = {
|
|
737
|
+
k.replace('_value', ''): v for k, v in kwargs.items()
|
|
738
|
+
if k not in placement
|
|
600
739
|
}
|
|
601
740
|
tile, mask, placement, axes = self._validateNewTile(tile, mask, placement, axes)
|
|
602
741
|
|
|
603
742
|
with self._threadLock and self._processLock:
|
|
743
|
+
old_axes = self._axes if hasattr(self, '_axes') else {}
|
|
604
744
|
self._axes = {k: i for i, k in enumerate(axes)}
|
|
745
|
+
new_axes = {k: i for k, i in self._axes.items() if k not in old_axes}
|
|
605
746
|
new_dims = {
|
|
606
747
|
a: max(
|
|
748
|
+
self._axisCounts.get(a, 0) if hasattr(self, '_axisCounts') else 0,
|
|
607
749
|
self._dims.get(store_path, {}).get(a, 0),
|
|
608
750
|
placement.get(a, 0) + tile.shape[i],
|
|
609
751
|
)
|
|
@@ -615,6 +757,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
615
757
|
for i, a in enumerate(axes)
|
|
616
758
|
])
|
|
617
759
|
|
|
760
|
+
if len(frame_values.keys()) > 0:
|
|
761
|
+
# update self.frameValues
|
|
762
|
+
updateMetadata = True
|
|
763
|
+
self._updateFrameValues(frame_values, placement, axes, new_axes, new_dims)
|
|
764
|
+
|
|
618
765
|
current_arrays = dict(self._zarr.arrays())
|
|
619
766
|
if store_path == '0':
|
|
620
767
|
# if writing to base data, invalidate generated levels
|
|
@@ -623,22 +770,36 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
623
770
|
self._zarr_store.rmdir(path)
|
|
624
771
|
chunking = None
|
|
625
772
|
if store_path not in current_arrays:
|
|
626
|
-
arr = np.empty(tuple(new_dims.values()), dtype=tile.dtype)
|
|
627
773
|
chunking = tuple([
|
|
628
774
|
self._tileSize if a in ['x', 'y'] else
|
|
629
775
|
new_dims.get('s') if a == 's' else 1
|
|
630
776
|
for a in axes
|
|
631
777
|
])
|
|
778
|
+
# If we have to create the array, do so with the desired store
|
|
779
|
+
# and chunking so we don't have to immediately rechunk it
|
|
780
|
+
arr = zarr.zeros(
|
|
781
|
+
tuple(new_dims.values()),
|
|
782
|
+
dtype=tile.dtype,
|
|
783
|
+
chunks=chunking,
|
|
784
|
+
store=self._zarr_store,
|
|
785
|
+
path=store_path,
|
|
786
|
+
write_empty_chunks=False,
|
|
787
|
+
)
|
|
788
|
+
chunking = None
|
|
632
789
|
else:
|
|
633
790
|
arr = current_arrays[store_path]
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
791
|
+
new_shape = tuple(
|
|
792
|
+
max(v, arr.shape[old_axes[k]] if k in old_axes else 0)
|
|
793
|
+
for k, v in new_dims.items()
|
|
794
|
+
)
|
|
795
|
+
if arr.chunks[-1] != new_dims.get('s') or len(new_axes):
|
|
796
|
+
# rechunk if length of samples axis changed or any new axis added
|
|
637
797
|
chunking = tuple([
|
|
638
798
|
self._tileSize if a in ['x', 'y'] else
|
|
639
799
|
new_dims.get('s') if a == 's' else 1
|
|
640
800
|
for a in axes
|
|
641
801
|
])
|
|
802
|
+
arr = self._resizeImage(arr, new_shape, new_axes, chunking)
|
|
642
803
|
|
|
643
804
|
if mask is not None:
|
|
644
805
|
arr[placement_slices] = np.where(mask, tile, arr[placement_slices])
|
|
@@ -655,11 +816,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
655
816
|
|
|
656
817
|
# If base data changed, update large_image attributes
|
|
657
818
|
if store_path == '0':
|
|
658
|
-
self._dtype = tile.dtype
|
|
819
|
+
self._dtype = np.dtype(tile.dtype)
|
|
659
820
|
self._bandCount = new_dims.get(axes[-1]) # last axis is assumed to be bands
|
|
660
821
|
self.sizeX = new_dims.get('x')
|
|
661
822
|
self.sizeY = new_dims.get('y')
|
|
662
|
-
self._framecount =
|
|
823
|
+
self._framecount = math.prod([
|
|
663
824
|
length
|
|
664
825
|
for axis, length in new_dims.items()
|
|
665
826
|
if axis in axes[:-3]
|
|
@@ -668,6 +829,9 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
668
829
|
self._levels = None
|
|
669
830
|
self.levels = int(max(1, math.ceil(math.log(max(
|
|
670
831
|
self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
|
|
832
|
+
updateMetadata = True
|
|
833
|
+
if updateMetadata:
|
|
834
|
+
self._writeInternalMetadata()
|
|
671
835
|
|
|
672
836
|
def addAssociatedImage(self, image, imageKey=None):
|
|
673
837
|
"""
|
|
@@ -690,42 +854,79 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
690
854
|
)
|
|
691
855
|
self._associatedImages[imageKey] = (group, arr)
|
|
692
856
|
|
|
857
|
+
def _getAxisInternalMetadata(self, axis_name):
|
|
858
|
+
"""
|
|
859
|
+
Get the metadata structure describing an axis in the image.
|
|
860
|
+
This will be written to the image metadata.
|
|
861
|
+
|
|
862
|
+
:param axis_name: a string corresponding to an axis in the image
|
|
863
|
+
:returns: a dictionary describing the target axis
|
|
864
|
+
"""
|
|
865
|
+
axis_metadata = {'name': axis_name}
|
|
866
|
+
if axis_name in ['x', 'y']:
|
|
867
|
+
axis_metadata['type'] = 'space'
|
|
868
|
+
axis_metadata['unit'] = 'millimeter'
|
|
869
|
+
elif axis_name in ['s', 'c']:
|
|
870
|
+
axis_metadata['type'] = 'channel'
|
|
871
|
+
if self.frameAxes is not None:
|
|
872
|
+
axis_index = self.frameAxes.index(axis_name) if axis_name in self.frameAxes else None
|
|
873
|
+
if axis_index is not None and self.frameValues is not None:
|
|
874
|
+
all_frame_values = self.frameValues[..., axis_index]
|
|
875
|
+
split = np.split(
|
|
876
|
+
all_frame_values,
|
|
877
|
+
all_frame_values.shape[axis_index],
|
|
878
|
+
axis=axis_index,
|
|
879
|
+
)
|
|
880
|
+
uniform = all(len(np.unique(
|
|
881
|
+
# convert all values to strings for mixed type comparison with np.unique
|
|
882
|
+
(a[np.not_equal(a, None)]).astype(str),
|
|
883
|
+
)) == 1 for a in split)
|
|
884
|
+
if uniform:
|
|
885
|
+
values = [a.flat[0] for a in split]
|
|
886
|
+
else:
|
|
887
|
+
values = all_frame_values.flatten().tolist()
|
|
888
|
+
axis_metadata['values'] = values
|
|
889
|
+
unit = self.frameUnits.get(axis_name) if self.frameUnits is not None else None
|
|
890
|
+
if unit is not None:
|
|
891
|
+
axis_metadata['unit'] = unit
|
|
892
|
+
return axis_metadata
|
|
893
|
+
|
|
693
894
|
def _writeInternalMetadata(self):
|
|
895
|
+
"""
|
|
896
|
+
Write metadata to Zarr attributes.
|
|
897
|
+
Metadata structure adheres to OME schema from https://ngff.openmicroscopy.org/latest/
|
|
898
|
+
"""
|
|
694
899
|
self._checkEditable()
|
|
695
900
|
with self._threadLock and self._processLock:
|
|
696
901
|
name = str(self._tempdir.name).split('/')[-1]
|
|
697
902
|
arrays = dict(self._zarr.arrays())
|
|
698
|
-
channel_axis = self._axes.get('c', self._axes.get('s'))
|
|
699
903
|
datasets = []
|
|
700
904
|
axes = []
|
|
701
905
|
channels = []
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
'
|
|
711
|
-
'
|
|
712
|
-
|
|
713
|
-
'
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
elif a == 'z':
|
|
727
|
-
rdefs['defaultZ'] = 0
|
|
728
|
-
axes.append(axis_metadata)
|
|
906
|
+
channel_axis = rdefs = None
|
|
907
|
+
if hasattr(self, '_axes'):
|
|
908
|
+
channel_axis = self._axes.get('c', self._axes.get('s'))
|
|
909
|
+
rdefs = {'model': 'color' if len(self._channelColors) else 'greyscale'}
|
|
910
|
+
sorted_axes = [a[0] for a in sorted(self._axes.items(), key=lambda item: item[1])]
|
|
911
|
+
for arr_name in arrays:
|
|
912
|
+
level = int(arr_name)
|
|
913
|
+
scale = [1.0 for a in sorted_axes]
|
|
914
|
+
scale[self._axes.get('x')] = (self._mm_x or 0) * (2 ** level)
|
|
915
|
+
scale[self._axes.get('y')] = (self._mm_y or 0) * (2 ** level)
|
|
916
|
+
dataset_metadata = {
|
|
917
|
+
'path': arr_name,
|
|
918
|
+
'coordinateTransformations': [{
|
|
919
|
+
'type': 'scale',
|
|
920
|
+
'scale': scale,
|
|
921
|
+
}],
|
|
922
|
+
}
|
|
923
|
+
datasets.append(dataset_metadata)
|
|
924
|
+
for a in sorted_axes:
|
|
925
|
+
if a == 't':
|
|
926
|
+
rdefs['defaultT'] = 0
|
|
927
|
+
elif a == 'z':
|
|
928
|
+
rdefs['defaultZ'] = 0
|
|
929
|
+
axes.append(self._getAxisInternalMetadata(a))
|
|
729
930
|
if channel_axis is not None and len(arrays) > 0:
|
|
730
931
|
base_array = list(arrays.values())[0]
|
|
731
932
|
base_shape = base_array.shape
|
|
@@ -738,21 +939,21 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
738
939
|
'inverted': False,
|
|
739
940
|
'label': f'Band {c + 1}',
|
|
740
941
|
}
|
|
741
|
-
slicing = tuple(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
)
|
|
747
|
-
channel_data = base_array[slicing]
|
|
748
|
-
channel_min = np.min(channel_data)
|
|
749
|
-
channel_max = np.max(channel_data)
|
|
750
|
-
channel_metadata['window'] = {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
942
|
+
# slicing = tuple(
|
|
943
|
+
# slice(None)
|
|
944
|
+
# if k != ('c' if 'c' in self._axes else 's')
|
|
945
|
+
# else c
|
|
946
|
+
# for k, v in self._axes.items()
|
|
947
|
+
# )
|
|
948
|
+
# channel_data = base_array[slicing]
|
|
949
|
+
# channel_min = np.min(channel_data)
|
|
950
|
+
# channel_max = np.max(channel_data)
|
|
951
|
+
# channel_metadata['window'] = {
|
|
952
|
+
# 'end': channel_max,
|
|
953
|
+
# 'max': channel_max,
|
|
954
|
+
# 'min': channel_min,
|
|
955
|
+
# 'start': channel_min,
|
|
956
|
+
# }
|
|
756
957
|
if len(self._channelNames) > c:
|
|
757
958
|
channel_metadata['label'] = self._channelNames[c]
|
|
758
959
|
if len(self._channelColors) > c:
|
|
@@ -807,6 +1008,32 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
807
1008
|
raise TileSourceError(msg)
|
|
808
1009
|
self._crop = (x, y, w, h)
|
|
809
1010
|
|
|
1011
|
+
@property
|
|
1012
|
+
def minWidth(self):
|
|
1013
|
+
return self._minWidth
|
|
1014
|
+
|
|
1015
|
+
@minWidth.setter
|
|
1016
|
+
def minWidth(self, value):
|
|
1017
|
+
self._checkEditable()
|
|
1018
|
+
value = int(value) if value is not None else None
|
|
1019
|
+
if value is not None and value <= 0:
|
|
1020
|
+
msg = 'minWidth must be positive or None'
|
|
1021
|
+
raise TileSourceError(msg)
|
|
1022
|
+
self._minWidth = value
|
|
1023
|
+
|
|
1024
|
+
@property
|
|
1025
|
+
def minHeight(self):
|
|
1026
|
+
return self._minHeight
|
|
1027
|
+
|
|
1028
|
+
@minHeight.setter
|
|
1029
|
+
def minHeight(self, value):
|
|
1030
|
+
self._checkEditable()
|
|
1031
|
+
value = int(value) if value is not None else None
|
|
1032
|
+
if value is not None and value <= 0:
|
|
1033
|
+
msg = 'minHeight must be positive or None'
|
|
1034
|
+
raise TileSourceError(msg)
|
|
1035
|
+
self._minHeight = value
|
|
1036
|
+
|
|
810
1037
|
@property
|
|
811
1038
|
def mm_x(self):
|
|
812
1039
|
return self._mm_x
|
|
@@ -835,21 +1062,65 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
835
1062
|
|
|
836
1063
|
@property
|
|
837
1064
|
def imageDescription(self):
|
|
1065
|
+
if not hasattr(self, '_imageDescription'):
|
|
1066
|
+
return None
|
|
1067
|
+
if isinstance(self._imageDescription, dict):
|
|
1068
|
+
return self._imageDescription.get('description')
|
|
838
1069
|
return self._imageDescription
|
|
839
1070
|
|
|
840
1071
|
@imageDescription.setter
|
|
841
1072
|
def imageDescription(self, description):
|
|
842
1073
|
self._checkEditable()
|
|
843
|
-
|
|
1074
|
+
try:
|
|
1075
|
+
json.dumps(description)
|
|
1076
|
+
except TypeError:
|
|
1077
|
+
msg = 'Description must be JSON serializable'
|
|
1078
|
+
raise TileSourceError(msg)
|
|
1079
|
+
if (
|
|
1080
|
+
hasattr(self, '_imageDescription') and
|
|
1081
|
+
isinstance(self._imageDescription, dict)
|
|
1082
|
+
):
|
|
1083
|
+
self._imageDescription['description'] = description
|
|
1084
|
+
else:
|
|
1085
|
+
self._imageDescription = description
|
|
1086
|
+
|
|
1087
|
+
@property
|
|
1088
|
+
def additionalMetadata(self):
|
|
1089
|
+
if not hasattr(self, '_imageDescription'):
|
|
1090
|
+
return None
|
|
1091
|
+
if isinstance(self._imageDescription, dict):
|
|
1092
|
+
return self._imageDescription.get('additionalMetadata')
|
|
1093
|
+
return None
|
|
1094
|
+
|
|
1095
|
+
@additionalMetadata.setter
|
|
1096
|
+
def additionalMetadata(self, data):
|
|
1097
|
+
self._checkEditable()
|
|
1098
|
+
try:
|
|
1099
|
+
json.dumps(data)
|
|
1100
|
+
except TypeError:
|
|
1101
|
+
msg = 'Metadata must be JSON serializable'
|
|
1102
|
+
raise TileSourceError(msg)
|
|
1103
|
+
if (
|
|
1104
|
+
hasattr(self, '_imageDescription') and
|
|
1105
|
+
isinstance(self._imageDescription, dict)
|
|
1106
|
+
):
|
|
1107
|
+
self._imageDescription['additionalMetadata'] = data
|
|
1108
|
+
else:
|
|
1109
|
+
self.imageDescription = dict(
|
|
1110
|
+
description=self._imageDescription,
|
|
1111
|
+
additionalMetadata=data,
|
|
1112
|
+
)
|
|
844
1113
|
|
|
845
1114
|
@property
|
|
846
1115
|
def channelNames(self):
|
|
847
|
-
|
|
1116
|
+
if hasattr(self, '_channelNames'):
|
|
1117
|
+
return self._channelNames or None
|
|
1118
|
+
return super().channelNames
|
|
848
1119
|
|
|
849
1120
|
@channelNames.setter
|
|
850
1121
|
def channelNames(self, names):
|
|
851
1122
|
self._checkEditable()
|
|
852
|
-
self._channelNames = names
|
|
1123
|
+
self._channelNames = names or []
|
|
853
1124
|
|
|
854
1125
|
@property
|
|
855
1126
|
def channelColors(self):
|
|
@@ -860,6 +1131,119 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
860
1131
|
self._checkEditable()
|
|
861
1132
|
self._channelColors = colors
|
|
862
1133
|
|
|
1134
|
+
@property
|
|
1135
|
+
def frameAxes(self):
|
|
1136
|
+
return self._frameAxes
|
|
1137
|
+
|
|
1138
|
+
@frameAxes.setter
|
|
1139
|
+
def frameAxes(self, axes):
|
|
1140
|
+
self._checkEditable()
|
|
1141
|
+
self._frameAxes = axes
|
|
1142
|
+
self._writeInternalMetadata()
|
|
1143
|
+
|
|
1144
|
+
@property
|
|
1145
|
+
def frameUnits(self):
|
|
1146
|
+
return self._frameUnits
|
|
1147
|
+
|
|
1148
|
+
@frameUnits.setter
|
|
1149
|
+
def frameUnits(self, units):
|
|
1150
|
+
self._checkEditable()
|
|
1151
|
+
if self.frameAxes is None:
|
|
1152
|
+
err = 'frameAxes must be set first with a list of frame axis names.'
|
|
1153
|
+
raise ValueError(err)
|
|
1154
|
+
if not isinstance(units, dict) or not all(
|
|
1155
|
+
k in self.frameAxes for k in units
|
|
1156
|
+
):
|
|
1157
|
+
err = 'frameUnits must be a dictionary with keys that exist in frameAxes.'
|
|
1158
|
+
raise ValueError(err)
|
|
1159
|
+
self._frameUnits = units
|
|
1160
|
+
|
|
1161
|
+
@property
|
|
1162
|
+
def frameValues(self):
|
|
1163
|
+
return self._frameValues
|
|
1164
|
+
|
|
1165
|
+
@frameValues.setter
|
|
1166
|
+
def frameValues(self, a):
|
|
1167
|
+
self._checkEditable()
|
|
1168
|
+
if self.frameAxes is None:
|
|
1169
|
+
err = 'frameAxes must be set first with a list of frame axis names.'
|
|
1170
|
+
raise ValueError(err)
|
|
1171
|
+
if len(a.shape) != len(self.frameAxes) + 1:
|
|
1172
|
+
err = f'frameValues must have {len(self.frameAxes) + 1} dimensions.'
|
|
1173
|
+
raise ValueError(err)
|
|
1174
|
+
self._frameValues = a
|
|
1175
|
+
self._writeInternalMetadata()
|
|
1176
|
+
|
|
1177
|
+
@property
|
|
1178
|
+
def projection(self):
|
|
1179
|
+
return self._projection
|
|
1180
|
+
|
|
1181
|
+
@projection.setter
|
|
1182
|
+
def projection(self, proj):
|
|
1183
|
+
import pyproj
|
|
1184
|
+
|
|
1185
|
+
self._checkEditable()
|
|
1186
|
+
try:
|
|
1187
|
+
pyproj.CRS.from_string(str(proj))
|
|
1188
|
+
except Exception as e:
|
|
1189
|
+
msg = f'Invalid projection value. Cannot be interpreted by pyproj: {str(e)}'
|
|
1190
|
+
raise TileSourceError(msg)
|
|
1191
|
+
self._projection = proj
|
|
1192
|
+
self._writeInternalMetadata()
|
|
1193
|
+
|
|
1194
|
+
@property
|
|
1195
|
+
def gcps(self):
|
|
1196
|
+
return self._gcps
|
|
1197
|
+
|
|
1198
|
+
@gcps.setter
|
|
1199
|
+
def gcps(self, gcps):
|
|
1200
|
+
self._checkEditable()
|
|
1201
|
+
if isinstance(gcps, str):
|
|
1202
|
+
gcps = list(zip(*[iter(gcps.split())] * 4, strict=False))
|
|
1203
|
+
if (isinstance(gcps, (list, tuple))):
|
|
1204
|
+
gcps = [
|
|
1205
|
+
[float(v) for v in gcp.split()]
|
|
1206
|
+
if isinstance(gcp, str) else gcp
|
|
1207
|
+
for gcp in gcps
|
|
1208
|
+
]
|
|
1209
|
+
if any(
|
|
1210
|
+
not isinstance(gcp, list) and not isinstance(gcp, tuple)
|
|
1211
|
+
for gcp in gcps
|
|
1212
|
+
):
|
|
1213
|
+
msg = 'Each GCP must be specified as a list, tuple, or space-separated string.'
|
|
1214
|
+
raise TileSourceError(msg)
|
|
1215
|
+
if any(len(gcp) != 4 for gcp in gcps):
|
|
1216
|
+
msg = (
|
|
1217
|
+
'Each GCP must contain four values: '
|
|
1218
|
+
'[projected_x, projected_y, pixel_x, pixel_y].'
|
|
1219
|
+
)
|
|
1220
|
+
raise TileSourceError(msg)
|
|
1221
|
+
unique_gcps = []
|
|
1222
|
+
dup_gcps = []
|
|
1223
|
+
for gcp in gcps:
|
|
1224
|
+
if not any(
|
|
1225
|
+
u[0:2] == gcp[0:2] or u[2:4] == gcp[2:4]
|
|
1226
|
+
for u in unique_gcps
|
|
1227
|
+
):
|
|
1228
|
+
unique_gcps.append(gcp)
|
|
1229
|
+
else:
|
|
1230
|
+
dup_gcps.append(gcp)
|
|
1231
|
+
gcps = unique_gcps
|
|
1232
|
+
if len(dup_gcps):
|
|
1233
|
+
msg = (
|
|
1234
|
+
f'Removed duplicate GCPs {dup_gcps} from list; shared projected '
|
|
1235
|
+
'coordinate or pixel coordinate with another GCP.'
|
|
1236
|
+
)
|
|
1237
|
+
warnings.warn(msg, stacklevel=2)
|
|
1238
|
+
if len(gcps) < 2:
|
|
1239
|
+
msg = 'Must specify at least 2 unique GCPs.'
|
|
1240
|
+
raise TileSourceError(msg)
|
|
1241
|
+
else:
|
|
1242
|
+
msg = 'GCPs must be specified as a list, tuple, or space-separated string.'
|
|
1243
|
+
raise TileSourceError(msg)
|
|
1244
|
+
self._gcps = gcps
|
|
1245
|
+
self._writeInternalMetadata()
|
|
1246
|
+
|
|
863
1247
|
def _generateDownsampledLevels(self, resample_method):
|
|
864
1248
|
self._checkEditable()
|
|
865
1249
|
current_arrays = dict(self._zarr.arrays())
|
|
@@ -903,7 +1287,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
903
1287
|
output=iterator_output,
|
|
904
1288
|
resample=False, # TODO: incorporate resampling in core
|
|
905
1289
|
):
|
|
906
|
-
|
|
1290
|
+
tile_data = tile['tile']
|
|
1291
|
+
if tile_data.shape[-1] == 1:
|
|
1292
|
+
tile_data = np.squeeze(tile['tile'], axis=(-1,))
|
|
1293
|
+
new_tile = downsampleTileHalfRes(tile_data, resample_method)
|
|
907
1294
|
overlap = {k: int(v / 2) for k, v in tile['tile_overlap'].items()}
|
|
908
1295
|
new_tile = new_tile[
|
|
909
1296
|
slice(overlap['top'], new_tile.shape[0] - overlap['bottom']),
|
|
@@ -923,6 +1310,23 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
923
1310
|
)
|
|
924
1311
|
self._validateZarr() # refresh self._levels before continuing
|
|
925
1312
|
|
|
1313
|
+
def _applyGeoReferencing(self, path):
|
|
1314
|
+
if self.projection and not self.gcps:
|
|
1315
|
+
msg = (
|
|
1316
|
+
'Projection was specified but GCPs were not specified. '
|
|
1317
|
+
'Cannot write georeferenced file.'
|
|
1318
|
+
)
|
|
1319
|
+
raise TileSourceError(msg)
|
|
1320
|
+
import tifftools
|
|
1321
|
+
|
|
1322
|
+
setlist = []
|
|
1323
|
+
if self.projection is not None:
|
|
1324
|
+
setlist.append(('projection', self.projection))
|
|
1325
|
+
if self.gcps is not None and len(self.gcps):
|
|
1326
|
+
setlist.append(('gcps', self.gcps))
|
|
1327
|
+
|
|
1328
|
+
tifftools.tiff_set(path, setlist=setlist, overwrite=True)
|
|
1329
|
+
|
|
926
1330
|
def write(
|
|
927
1331
|
self,
|
|
928
1332
|
path,
|
|
@@ -930,6 +1334,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
930
1334
|
alpha=True,
|
|
931
1335
|
overwriteAllowed=True,
|
|
932
1336
|
resample=None,
|
|
1337
|
+
**converterParams,
|
|
933
1338
|
):
|
|
934
1339
|
"""
|
|
935
1340
|
Output the current image to a file.
|
|
@@ -942,6 +1347,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
942
1347
|
:param resample: one of the ``ResampleMethod`` enum values. Defaults
|
|
943
1348
|
to ``NP_NEAREST`` for lossless and non-uint8 data and to
|
|
944
1349
|
``PIL_LANCZOS`` for lossy uint8 data.
|
|
1350
|
+
:param converterParams: options to pass to the large_image_converter if
|
|
1351
|
+
the output is not a zarr variant.
|
|
945
1352
|
"""
|
|
946
1353
|
if os.path.exists(path):
|
|
947
1354
|
if overwriteAllowed:
|
|
@@ -978,6 +1385,21 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
978
1385
|
**frame_position,
|
|
979
1386
|
)
|
|
980
1387
|
|
|
1388
|
+
if self.minWidth or self.minHeight:
|
|
1389
|
+
old_axes = self._axes if hasattr(self, '_axes') else {}
|
|
1390
|
+
current_arrays = dict(self._zarr.arrays())
|
|
1391
|
+
arr = current_arrays['0']
|
|
1392
|
+
new_shape = tuple(
|
|
1393
|
+
max(
|
|
1394
|
+
v,
|
|
1395
|
+
self.minWidth if self.minWidth is not None and k == 'x' else
|
|
1396
|
+
self.minHeight if self.minHeight is not None and k == 'y' else
|
|
1397
|
+
arr.shape[old_axes[k]],
|
|
1398
|
+
)
|
|
1399
|
+
for k, v in old_axes.items()
|
|
1400
|
+
)
|
|
1401
|
+
self._resizeImage(arr, new_shape, {}, None)
|
|
1402
|
+
|
|
981
1403
|
source._validateZarr()
|
|
982
1404
|
|
|
983
1405
|
if suffix in ['.zarr', '.db', '.sqlite', '.zip']:
|
|
@@ -1008,7 +1430,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
|
|
|
1008
1430
|
params = {}
|
|
1009
1431
|
if lossy and self.dtype == np.uint8:
|
|
1010
1432
|
params['compression'] = 'jpeg'
|
|
1011
|
-
|
|
1433
|
+
params['overwrite'] = overwriteAllowed
|
|
1434
|
+
params.update(converterParams)
|
|
1435
|
+
convert(str(attrs_path), path, **params)
|
|
1436
|
+
|
|
1437
|
+
self._applyGeoReferencing(path)
|
|
1012
1438
|
|
|
1013
1439
|
|
|
1014
1440
|
def open(*args, **kwargs):
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: large-image-source-zarr
|
|
3
|
+
Version: 1.33.6a188
|
|
4
|
+
Summary: A OME Zarr tilesource for large_image.
|
|
5
|
+
Home-page: https://github.com/girder/large_image
|
|
6
|
+
Author: Kitware, Inc.
|
|
7
|
+
Author-email: kitware@kitware.com
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: large_image,tile source
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/x-rst
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: large-image>=1.33.6.a188
|
|
21
|
+
Requires-Dist: zarr<3
|
|
22
|
+
Requires-Dist: imagecodecs
|
|
23
|
+
Requires-Dist: numcodecs<0.16
|
|
24
|
+
Requires-Dist: imagecodecs-numcodecs!=2024.9.22
|
|
25
|
+
Provides-Extra: girder
|
|
26
|
+
Requires-Dist: girder-large-image>=1.33.6.a188; extra == "girder"
|
|
27
|
+
Provides-Extra: all
|
|
28
|
+
Requires-Dist: large-image-converter; extra == "all"
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: classifier
|
|
32
|
+
Dynamic: description
|
|
33
|
+
Dynamic: description-content-type
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: keywords
|
|
36
|
+
Dynamic: license
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
Dynamic: provides-extra
|
|
39
|
+
Dynamic: requires-dist
|
|
40
|
+
Dynamic: requires-python
|
|
41
|
+
Dynamic: summary
|
|
42
|
+
|
|
43
|
+
A OME Zarr tilesource for large_image.
|
|
44
|
+
|
|
45
|
+
See the large-image package for more details.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
large_image_source_zarr/__init__.py,sha256=BJq47bX_ZR0M0laIJWlI_7RbmUIZgyHwq-A7zDzitH0,58921
|
|
2
|
+
large_image_source_zarr/girder_source.py,sha256=RljxQ49-2PyTPZ4xxTipyyaXiwLAC7npdwWVx7XEhLE,342
|
|
3
|
+
large_image_source_zarr-1.33.6a188.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
|
4
|
+
large_image_source_zarr-1.33.6a188.dist-info/METADATA,sha256=TKAqpuCo2XwIikJU-vyJWl7TcQGJ9npiLMhSMBCuXcA,1415
|
|
5
|
+
large_image_source_zarr-1.33.6a188.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
large_image_source_zarr-1.33.6a188.dist-info/entry_points.txt,sha256=VakBZBTOHxWMsYsOO8Egale1mPhLTlLruMfhSBUOwkk,166
|
|
7
|
+
large_image_source_zarr-1.33.6a188.dist-info/top_level.txt,sha256=cMYOehfHJ55_mkRAFS3h2lsO0Pe4jYvMrNKbEznImZI,24
|
|
8
|
+
large_image_source_zarr-1.33.6a188.dist-info/RECORD,,
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: large-image-source-zarr
|
|
3
|
-
Version: 1.29.3.dev39
|
|
4
|
-
Summary: A OME Zarr tilesource for large_image.
|
|
5
|
-
Home-page: https://github.com/girder/large_image
|
|
6
|
-
Author: Kitware, Inc.
|
|
7
|
-
Author-email: kitware@kitware.com
|
|
8
|
-
License: Apache Software License 2.0
|
|
9
|
-
Keywords: large_image,tile source
|
|
10
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Requires-Python: >=3.6
|
|
19
|
-
License-File: LICENSE
|
|
20
|
-
Requires-Dist: large-image >=1.29.3.dev39
|
|
21
|
-
Requires-Dist: zarr
|
|
22
|
-
Requires-Dist: imagecodecs-numcodecs
|
|
23
|
-
Provides-Extra: all
|
|
24
|
-
Requires-Dist: large-image-converter ; extra == 'all'
|
|
25
|
-
Provides-Extra: girder
|
|
26
|
-
Requires-Dist: girder-large-image >=1.29.3.dev39 ; extra == 'girder'
|
|
27
|
-
|
|
28
|
-
A OME Zarr tilesource for large_image.
|
|
29
|
-
|
|
30
|
-
See the large-image package for more details.
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
large_image_source_zarr/__init__.py,sha256=bXXS7sr6wTDrY_hLjRv9o4I-CWmqsTpWaFz8VG2PFjs,41105
|
|
2
|
-
large_image_source_zarr/girder_source.py,sha256=RljxQ49-2PyTPZ4xxTipyyaXiwLAC7npdwWVx7XEhLE,342
|
|
3
|
-
large_image_source_zarr-1.29.3.dev39.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
|
4
|
-
large_image_source_zarr-1.29.3.dev39.dist-info/METADATA,sha256=0ddv37nnqLKFK4u2fgDdBJH5CBfPMAei9nhk7hXXQqE,1115
|
|
5
|
-
large_image_source_zarr-1.29.3.dev39.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
|
|
6
|
-
large_image_source_zarr-1.29.3.dev39.dist-info/entry_points.txt,sha256=VakBZBTOHxWMsYsOO8Egale1mPhLTlLruMfhSBUOwkk,166
|
|
7
|
-
large_image_source_zarr-1.29.3.dev39.dist-info/top_level.txt,sha256=cMYOehfHJ55_mkRAFS3h2lsO0Pe4jYvMrNKbEznImZI,24
|
|
8
|
-
large_image_source_zarr-1.29.3.dev39.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|