large-image-source-zarr 1.30.2.dev10__tar.gz → 1.30.2.dev42__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 (14) hide show
  1. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/PKG-INFO +3 -3
  2. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/large_image_source_zarr/__init__.py +189 -16
  3. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/large_image_source_zarr.egg-info/PKG-INFO +3 -3
  4. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/large_image_source_zarr.egg-info/requires.txt +2 -2
  5. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/LICENSE +0 -0
  6. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/README.rst +0 -0
  7. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/large_image_source_zarr/girder_source.py +0 -0
  8. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/large_image_source_zarr.egg-info/SOURCES.txt +0 -0
  9. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/large_image_source_zarr.egg-info/dependency_links.txt +0 -0
  10. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/large_image_source_zarr.egg-info/entry_points.txt +0 -0
  11. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/large_image_source_zarr.egg-info/top_level.txt +0 -0
  12. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/pyproject.toml +0 -0
  13. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/setup.cfg +0 -0
  14. {large_image_source_zarr-1.30.2.dev10 → large_image_source_zarr-1.30.2.dev42}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-zarr
3
- Version: 1.30.2.dev10
3
+ Version: 1.30.2.dev42
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.
@@ -19,12 +19,12 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Requires-Python: >=3.6
20
20
  Description-Content-Type: text/x-rst
21
21
  License-File: LICENSE
22
- Requires-Dist: large-image>=1.30.2.dev10
22
+ Requires-Dist: large-image>=1.30.2.dev42
23
23
  Requires-Dist: zarr
24
24
  Requires-Dist: numcodecs
25
25
  Requires-Dist: imagecodecs-numcodecs!=2024.9.22
26
26
  Provides-Extra: girder
27
- Requires-Dist: girder-large-image>=1.30.2.dev10; extra == "girder"
27
+ Requires-Dist: girder-large-image>=1.30.2.dev42; extra == "girder"
28
28
  Provides-Extra: all
29
29
  Requires-Dist: large-image-converter; extra == "all"
30
30
 
@@ -98,6 +98,9 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
98
98
  self._largeImagePath = str(self._getLargeImagePath())
99
99
  self._zarr = None
100
100
  self._editable = False
101
+ self._frameValues = None
102
+ self._frameAxes = None
103
+ self._frameUnits = None
101
104
  if not os.path.isfile(self._largeImagePath) and '//:' not in self._largeImagePath:
102
105
  raise TileSourceFileNotFoundError(self._largeImagePath) from None
103
106
  try:
@@ -152,6 +155,9 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
152
155
  self._imageDescription = None
153
156
  self._levels = []
154
157
  self._associatedImages = {}
158
+ self._frameValues = None
159
+ self._frameAxes = None
160
+ self._frameUnits = None
155
161
 
156
162
  def __del__(self):
157
163
  if not hasattr(self, '_derivedSource'):
@@ -218,10 +224,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
218
224
  tuple that is used to find the maximum size array, preferring ome
219
225
  arrays, then total pixels, then channels. 'is_ome' is a boolean.
220
226
  'series' is a list of the found groups and arrays that match the
221
- best criteria. 'axes' and 'channels' are from the best array.
222
- 'associated' is a list of all groups and arrays that might be
223
- associated images. These have to be culled for the actual groups
224
- used in the series.
227
+ best criteria. 'axes', 'axes_values', 'axes_units', and 'channels'
228
+ are from the best array. 'associated' is a list of all groups and
229
+ arrays that might be associated images. These have to be culled
230
+ for the actual groups used in the series.
225
231
  """
226
232
  attrs = group.attrs.asdict() if group is not None else {}
227
233
  min_version = packaging.version.Version('0.4')
@@ -233,9 +239,19 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
233
239
  all(packaging.version.Version(m['version']) >= min_version
234
240
  for m in attrs['multiscales'] if 'version' in m))
235
241
  channels = None
242
+ axes_values = None
243
+ axes_units = None
236
244
  if is_ome:
237
245
  axes = {axis['name']: idx for idx, axis in enumerate(
238
246
  attrs['multiscales'][0]['axes'])}
247
+ axes_values = {
248
+ axis['name']: axis.get('values')
249
+ for axis in attrs['multiscales'][0]['axes']
250
+ }
251
+ axes_units = {
252
+ axis['name']: axis.get('unit')
253
+ for axis in attrs['multiscales'][0]['axes']
254
+ }
239
255
  if isinstance(attrs['omero'].get('channels'), list):
240
256
  channels = [channel['label'] for channel in attrs['omero']['channels']]
241
257
  if all(channel.startswith('Channel ') for channel in channels):
@@ -254,6 +270,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
254
270
  results['series'] = [(group, arr)]
255
271
  results['is_ome'] = is_ome
256
272
  results['axes'] = axes
273
+ results['axes_values'] = axes_values
274
+ results['axes_units'] = axes_units
257
275
  results['channels'] = channels
258
276
  elif check == results['best']:
259
277
  results['series'].append((group, arr))
@@ -333,6 +351,44 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
333
351
  except Exception:
334
352
  pass
335
353
 
354
+ def _readFrameValues(self, found, baseArray):
355
+ """
356
+ Populate frame values and frame units from image metadata.
357
+ """
358
+ axes_values = found.get('axes_values')
359
+ axes_units = found.get('axes_units')
360
+ if isinstance(axes_values, dict):
361
+ self._frameAxes = [
362
+ a for a, i in sorted(self._axes.items(), key=lambda x: x[1])
363
+ if axes_values.get(a) is not None
364
+ ]
365
+ self._frameUnits = {k: axes_units.get(k) for k in self.frameAxes if k in axes_units}
366
+ frame_values_shape = [baseArray.shape[self._axes[a]] for a in self.frameAxes]
367
+ frame_values_shape.append(len(frame_values_shape))
368
+ frame_values = np.empty(frame_values_shape, dtype=object)
369
+ all_frame_specs = self.getMetadata().get('frames')
370
+ for axis, values in axes_values.items():
371
+ if axis in self.frameAxes:
372
+ slicing = [slice(None) for i in range(len(frame_values_shape))]
373
+ axis_index = self.frameAxes.index(axis)
374
+ slicing[-1] = axis_index
375
+ if len(values) == frame_values_shape[axis_index]:
376
+ # uniform values have same length as axis
377
+ for i, value in enumerate(values):
378
+ slicing[axis_index] = i
379
+ frame_values[tuple(slicing)] = value
380
+ elif len(values) == self._framecount:
381
+ # non-uniform values have a value for every frame
382
+ for i, value in enumerate(values):
383
+ if all_frame_specs and len(all_frame_specs) > i:
384
+ for k, j in all_frame_specs[i].items():
385
+ if 'Index' in k:
386
+ name = k.replace('Index', '').lower()
387
+ if name:
388
+ slicing[self._frameAxes.index(name)] = j
389
+ frame_values[tuple(slicing)] = value
390
+ self._frameValues = frame_values
391
+
336
392
  def _validateZarr(self):
337
393
  """
338
394
  Validate that we can read tiles from the zarr parent group in
@@ -347,7 +403,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
347
403
  self._series = found['series']
348
404
  baseGroup, baseArray = self._series[0]
349
405
  self._is_ome = found['is_ome']
350
- self._axes = {k.lower(): v for k, v in found['axes'].items() if baseArray.shape[v] > 1}
406
+ self._axes = {
407
+ k.lower(): v
408
+ for k, v in found['axes'].items()
409
+ if baseArray.shape[v] > 1 or k in found.get('axes_values', {})
410
+ }
351
411
  if len(self._series) > 1 and 'xy' in self._axes:
352
412
  msg = 'Conflicting xy axis data.'
353
413
  raise TileSourceError(msg)
@@ -386,8 +446,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
386
446
  stride = 1
387
447
  self._strides = {}
388
448
  self._axisCounts = {}
389
- for _, k in sorted((-'tzc'.index(k) if k in 'tzc' else 1, k)
390
- for k in self._axes if k not in 'xys'):
449
+ for _, k in sorted(
450
+ (-self._axes.get(k, 'tzc'.index(k) if k in 'tzc' else -1), k)
451
+ for k in self._axes if k not in 'xys'
452
+ ):
391
453
  self._strides[k] = stride
392
454
  self._axisCounts[k] = baseArray.shape[self._axes[k]]
393
455
  stride *= baseArray.shape[self._axes[k]]
@@ -396,6 +458,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
396
458
  self._axisCounts['xy'] = len(self._series)
397
459
  stride *= len(self._series)
398
460
  self._framecount = stride
461
+ self._readFrameValues(found, baseArray)
399
462
 
400
463
  def _nonemptyLevelsList(self, frame=0):
401
464
  """
@@ -447,6 +510,16 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
447
510
  for axis in self._strides:
448
511
  frame['Index' + axis.upper()] = (
449
512
  idx // self._strides[axis]) % self._axisCounts[axis]
513
+ if self.frameValues is not None and self.frameAxes is not None:
514
+ current_frame_slice = tuple(
515
+ frame['Index' + axis.upper()] for axis in self.frameAxes
516
+ )
517
+ current_frame_values = self.frameValues[current_frame_slice]
518
+ for i, axis in enumerate(self.frameAxes):
519
+ value = current_frame_values[i]
520
+ # ensure that values are returned as native python types
521
+ native_value = getattr(value, 'tolist', lambda v=value: v)()
522
+ frame['Value' + axis.upper()] = native_value
450
523
  frames.append(frame)
451
524
  self._addMetadataFrameInformation(result, getattr(self, '_channels', None))
452
525
  return result
@@ -596,7 +669,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
596
669
  placement = {
597
670
  'x': x,
598
671
  'y': y,
599
- **kwargs,
672
+ **{k: v for k, v in kwargs.items() if not k.endswith('_value')},
673
+ }
674
+ frame_values = {
675
+ k.replace('_value', ''): v for k, v in kwargs.items()
676
+ if k not in placement
600
677
  }
601
678
  tile, mask, placement, axes = self._validateNewTile(tile, mask, placement, axes)
602
679
 
@@ -615,6 +692,26 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
615
692
  for i, a in enumerate(axes)
616
693
  ])
617
694
 
695
+ if len(frame_values.keys()) > 0:
696
+ # update self.frameValues
697
+ self.frameAxes = [
698
+ a for a in axes
699
+ if a in frame_values or
700
+ (self.frameAxes is not None and a in self.frameAxes)
701
+ ]
702
+ frames_shape = [new_dims[a] for a in self.frameAxes]
703
+ frames_shape.append(len(frames_shape))
704
+ if self.frameValues is None:
705
+ self.frameValues = np.empty(frames_shape, dtype=object)
706
+ elif self.frameValues.shape != frames_shape:
707
+ self.frameValues = np.pad(
708
+ self.frameValues,
709
+ [(0, s - self.frameValues.shape[i]) for i, s in enumerate(frames_shape)],
710
+ )
711
+ current_frame_slice = tuple(placement.get(a) for a in self.frameAxes)
712
+ for i, k in enumerate(self.frameAxes):
713
+ self.frameValues[(*current_frame_slice, i)] = frame_values.get(k)
714
+
618
715
  current_arrays = dict(self._zarr.arrays())
619
716
  if store_path == '0':
620
717
  # if writing to base data, invalidate generated levels
@@ -692,7 +789,48 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
692
789
  )
693
790
  self._associatedImages[imageKey] = (group, arr)
694
791
 
792
+ def _getAxisInternalMetadata(self, axis_name):
793
+ """
794
+ Get the metadata structure describing an axis in the image.
795
+ This will be written to the image metadata.
796
+
797
+ :param axis_name: a string corresponding to an axis in the image
798
+ :returns: a dictionary describing the target axis
799
+ """
800
+ axis_metadata = {'name': axis_name}
801
+ if axis_name in ['x', 'y']:
802
+ axis_metadata['type'] = 'space'
803
+ axis_metadata['unit'] = 'millimeter'
804
+ elif axis_name in ['s', 'c']:
805
+ axis_metadata['type'] = 'channel'
806
+ if self.frameAxes is not None:
807
+ axis_index = self.frameAxes.index(axis_name) if axis_name in self.frameAxes else None
808
+ if axis_index is not None and self.frameValues is not None:
809
+ all_frame_values = self.frameValues[..., axis_index]
810
+ split = np.split(
811
+ all_frame_values,
812
+ all_frame_values.shape[axis_index],
813
+ axis=axis_index,
814
+ )
815
+ uniform = all(len(np.unique(
816
+ # convert all values to strings for mixed type comparison with np.unique
817
+ (a[np.not_equal(a, None)]).astype(str),
818
+ )) == 1 for a in split)
819
+ if uniform:
820
+ values = [a.flat[0] for a in split]
821
+ else:
822
+ values = all_frame_values.flatten().tolist()
823
+ axis_metadata['values'] = values
824
+ unit = self.frameUnits.get(axis_name) if self.frameUnits is not None else None
825
+ if unit is not None:
826
+ axis_metadata['unit'] = unit
827
+ return axis_metadata
828
+
695
829
  def _writeInternalMetadata(self):
830
+ """
831
+ Write metadata to Zarr attributes.
832
+ Metadata structure adheres to OME schema from https://ngff.openmicroscopy.org/latest/
833
+ """
696
834
  self._checkEditable()
697
835
  with self._threadLock and self._processLock:
698
836
  name = str(self._tempdir.name).split('/')[-1]
@@ -717,17 +855,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
717
855
  }
718
856
  datasets.append(dataset_metadata)
719
857
  for a in sorted_axes:
720
- axis_metadata = {'name': a}
721
- if a in ['x', 'y']:
722
- axis_metadata['type'] = 'space'
723
- axis_metadata['unit'] = 'millimeter'
724
- elif a in ['s', 'c']:
725
- axis_metadata['type'] = 'channel'
726
- elif a == 't':
858
+ if a == 't':
727
859
  rdefs['defaultT'] = 0
728
860
  elif a == 'z':
729
861
  rdefs['defaultZ'] = 0
730
- axes.append(axis_metadata)
862
+ axes.append(self._getAxisInternalMetadata(a))
731
863
  if channel_axis is not None and len(arrays) > 0:
732
864
  base_array = list(arrays.values())[0]
733
865
  base_shape = base_array.shape
@@ -862,6 +994,47 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
862
994
  self._checkEditable()
863
995
  self._channelColors = colors
864
996
 
997
+ @property
998
+ def frameAxes(self):
999
+ return self._frameAxes
1000
+
1001
+ @frameAxes.setter
1002
+ def frameAxes(self, axes):
1003
+ self._checkEditable()
1004
+ self._frameAxes = axes
1005
+
1006
+ @property
1007
+ def frameUnits(self):
1008
+ return self._frameUnits
1009
+
1010
+ @frameUnits.setter
1011
+ def frameUnits(self, units):
1012
+ self._checkEditable()
1013
+ if self.frameAxes is None:
1014
+ err = 'frameAxes must be set first with a list of frame axis names.'
1015
+ raise ValueError(err)
1016
+ if not isinstance(units, dict) or not all(
1017
+ k in self.frameAxes for k in units.keys()
1018
+ ):
1019
+ err = 'frameUnits must be a dictionary with keys that exist in frameAxes.'
1020
+ raise ValueError(err)
1021
+ self._frameUnits = units
1022
+
1023
+ @property
1024
+ def frameValues(self):
1025
+ return self._frameValues
1026
+
1027
+ @frameValues.setter
1028
+ def frameValues(self, a):
1029
+ self._checkEditable()
1030
+ if self.frameAxes is None:
1031
+ err = 'frameAxes must be set first with a list of frame axis names.'
1032
+ raise ValueError(err)
1033
+ if len(a.shape) != len(self.frameAxes) + 1:
1034
+ err = f'frameValues must have {len(self.frameAxes) + 1} dimensions.'
1035
+ raise ValueError(err)
1036
+ self._frameValues = a
1037
+
865
1038
  def _generateDownsampledLevels(self, resample_method):
866
1039
  self._checkEditable()
867
1040
  current_arrays = dict(self._zarr.arrays())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-zarr
3
- Version: 1.30.2.dev10
3
+ Version: 1.30.2.dev42
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.
@@ -19,12 +19,12 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Requires-Python: >=3.6
20
20
  Description-Content-Type: text/x-rst
21
21
  License-File: LICENSE
22
- Requires-Dist: large-image>=1.30.2.dev10
22
+ Requires-Dist: large-image>=1.30.2.dev42
23
23
  Requires-Dist: zarr
24
24
  Requires-Dist: numcodecs
25
25
  Requires-Dist: imagecodecs-numcodecs!=2024.9.22
26
26
  Provides-Extra: girder
27
- Requires-Dist: girder-large-image>=1.30.2.dev10; extra == "girder"
27
+ Requires-Dist: girder-large-image>=1.30.2.dev42; extra == "girder"
28
28
  Provides-Extra: all
29
29
  Requires-Dist: large-image-converter; extra == "all"
30
30
 
@@ -1,4 +1,4 @@
1
- large-image>=1.30.2.dev10
1
+ large-image>=1.30.2.dev42
2
2
  zarr
3
3
  numcodecs
4
4
  imagecodecs-numcodecs!=2024.9.22
@@ -7,4 +7,4 @@ imagecodecs-numcodecs!=2024.9.22
7
7
  large-image-converter
8
8
 
9
9
  [girder]
10
- girder-large-image>=1.30.2.dev10
10
+ girder-large-image>=1.30.2.dev42