large-image-source-zarr 1.29.1.dev6__tar.gz → 1.29.1.dev16__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.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/PKG-INFO +3 -3
  2. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/large_image_source_zarr/__init__.py +182 -27
  3. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/large_image_source_zarr.egg-info/PKG-INFO +3 -3
  4. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/large_image_source_zarr.egg-info/requires.txt +2 -2
  5. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/LICENSE +0 -0
  6. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/README.rst +0 -0
  7. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/large_image_source_zarr/girder_source.py +0 -0
  8. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/large_image_source_zarr.egg-info/SOURCES.txt +0 -0
  9. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/large_image_source_zarr.egg-info/dependency_links.txt +0 -0
  10. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/large_image_source_zarr.egg-info/entry_points.txt +0 -0
  11. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/large_image_source_zarr.egg-info/top_level.txt +0 -0
  12. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/pyproject.toml +0 -0
  13. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/setup.cfg +0 -0
  14. {large-image-source-zarr-1.29.1.dev6 → large-image-source-zarr-1.29.1.dev16}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-zarr
3
- Version: 1.29.1.dev6
3
+ Version: 1.29.1.dev16
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.
@@ -17,11 +17,11 @@ Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Requires-Python: >=3.6
19
19
  License-File: LICENSE
20
- Requires-Dist: large-image>=1.29.1.dev6
20
+ Requires-Dist: large-image>=1.29.1.dev16
21
21
  Requires-Dist: zarr
22
22
  Requires-Dist: imagecodecs-numcodecs
23
23
  Provides-Extra: girder
24
- Requires-Dist: girder-large-image>=1.29.1.dev6; extra == "girder"
24
+ Requires-Dist: girder-large-image>=1.29.1.dev16; extra == "girder"
25
25
  Provides-Extra: all
26
26
  Requires-Dist: large-image-converter; extra == "all"
27
27
 
@@ -79,6 +79,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
79
79
  def _initOpen(self, **kwargs):
80
80
  self._largeImagePath = str(self._getLargeImagePath())
81
81
  self._zarr = None
82
+ self._editable = False
82
83
  if not os.path.isfile(self._largeImagePath) and '//:' not in self._largeImagePath:
83
84
  raise TileSourceFileNotFoundError(self._largeImagePath) from None
84
85
  try:
@@ -126,7 +127,11 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
126
127
  self._framecount = 0
127
128
  self._mm_x = 0
128
129
  self._mm_y = 0
130
+ self._channelNames = []
131
+ self._channelColors = []
132
+ self._imageDescription = None
129
133
  self._levels = []
134
+ self._associatedImages = {}
130
135
 
131
136
  def __del__(self):
132
137
  if not hasattr(self, '_derivedSource'):
@@ -312,6 +317,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
312
317
  Validate that we can read tiles from the zarr parent group in
313
318
  self._zarr. Set up the appropriate class variables.
314
319
  """
320
+ if self._editable:
321
+ self._writeInternalMetadata()
315
322
  found = self._scanZarrGroup(self._zarr)
316
323
  if found['best'] is None:
317
324
  msg = 'No data array that can be used.'
@@ -324,8 +331,10 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
324
331
  msg = 'Conflicting xy axis data.'
325
332
  raise TileSourceError(msg)
326
333
  self._channels = found['channels']
327
- self._associatedImages = [
328
- (g, a) for g, a in found['associated'] if not any(g is gb for gb, _ in self._series)]
334
+ self._associatedImages = {
335
+ g.name.replace('/', ''): (g, a)
336
+ for g, a in found['associated'] if not any(g is gb for gb, _ in self._series)
337
+ }
329
338
  self.sizeX = baseArray.shape[self._axes['x']]
330
339
  self.sizeY = baseArray.shape[self._axes['y']]
331
340
  self.tileWidth = (
@@ -429,6 +438,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
429
438
 
430
439
  :returns: a dictionary of data or None.
431
440
  """
441
+ if self._editable:
442
+ self._writeInternalMetadata()
432
443
  result = {}
433
444
  result['zarr'] = {
434
445
  'base': self._zarr.attrs.asdict(),
@@ -445,7 +456,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
445
456
 
446
457
  :return: the list of image keys.
447
458
  """
448
- return [f'image_{idx}' for idx in range(len(self._associatedImages))]
459
+ return list(self._associatedImages.keys())
449
460
 
450
461
  def _getAssociatedImage(self, imageKey):
451
462
  """
@@ -454,15 +465,9 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
454
465
  :param imageKey: the key of the associated image.
455
466
  :return: the image in PIL format or None.
456
467
  """
457
- if not imageKey.startswith('image_'):
458
- return
459
- try:
460
- idx = int(imageKey[6:])
461
- except Exception:
462
- return
463
- if idx < 0 or idx >= len(self._associatedImages):
468
+ if imageKey not in self._associatedImages:
464
469
  return
465
- group, arr = self._associatedImages[idx]
470
+ group, arr = self._associatedImages[imageKey]
466
471
  axes = self._getGeneralAxes(arr)
467
472
  trans = [idx for idx in range(len(arr.shape))
468
473
  if idx not in axes.values()] + [axes['y'], axes['x']]
@@ -565,8 +570,6 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
565
570
  :param kwargs: start locations for any additional axes. Note that
566
571
  ``level`` is a reserved word and not permitted for an axis name.
567
572
  """
568
- # TODO: improve band bookkeeping
569
-
570
573
  self._checkEditable()
571
574
  store_path = str(kwargs.pop('level', 0))
572
575
  placement = {
@@ -629,20 +632,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
629
632
  path=store_path,
630
633
  )
631
634
 
632
- # If base data changed, update large_image attributes and OME metadata
635
+ # If base data changed, update large_image attributes
633
636
  if store_path == '0':
634
- self._zarr.attrs.update({
635
- 'multiscales': [{
636
- 'version': '0.5-dev',
637
- 'axes': [{
638
- 'name': a,
639
- 'type': 'space' if a in ['x', 'y'] else 'other',
640
- } for a in axes],
641
- 'datasets': [{'path': 0}],
642
- }],
643
- 'omero': {'version': '0.5-dev'},
644
- })
645
-
646
637
  self._dtype = tile.dtype
647
638
  self._bandCount = new_dims.get(axes[-1]) # last axis is assumed to be bands
648
639
  self.sizeX = new_dims.get('x')
@@ -657,6 +648,113 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
657
648
  self.levels = int(max(1, math.ceil(math.log(max(
658
649
  self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
659
650
 
651
+ def addAssociatedImage(self, image, imageKey=None):
652
+ """
653
+ Add an associated image to this source.
654
+
655
+ :param image: a numpy array, PIL Image, or a binary string
656
+ with an image. The numpy array can have 2 or 3 dimensions.
657
+ """
658
+ data, _ = _imageToNumpy(image)
659
+ with self._addLock:
660
+ if imageKey is None:
661
+ # Each associated image should be in its own group
662
+ num_existing = len(self.getAssociatedImagesList())
663
+ imageKey = f'image_{num_existing + 1}'
664
+ group = self._zarr.require_group(imageKey)
665
+ arr = zarr.array(
666
+ data,
667
+ store=self._zarr_store,
668
+ path=f'{imageKey}/image',
669
+ )
670
+ self._associatedImages[imageKey] = (group, arr)
671
+
672
+ def _writeInternalMetadata(self):
673
+ self._checkEditable()
674
+ with self._addLock:
675
+ name = str(self._tempdir.name).split('/')[-1]
676
+ arrays = dict(self._zarr.arrays())
677
+ channel_axis = self._axes.get('s') or self._axes.get('c')
678
+ datasets = []
679
+ axes = []
680
+ channels = []
681
+ rdefs = {'model': 'color' if len(self._channelColors) else 'greyscale'}
682
+ sorted_axes = [a[0] for a in sorted(self._axes.items(), key=lambda item: item[1])]
683
+ for arr_name in arrays:
684
+ level = int(arr_name)
685
+ scale = [1.0 for a in sorted_axes]
686
+ scale[self._axes.get('x')] = self._mm_x * (2 ** level)
687
+ scale[self._axes.get('y')] = self._mm_y * (2 ** level)
688
+ dataset_metadata = {
689
+ 'path': arr_name,
690
+ 'coordinateTransformations': [{
691
+ 'type': 'scale',
692
+ 'scale': scale,
693
+ }],
694
+ }
695
+ datasets.append(dataset_metadata)
696
+ for a in sorted_axes:
697
+ axis_metadata = {'name': a}
698
+ if a in ['x', 'y']:
699
+ axis_metadata['type'] = 'space'
700
+ axis_metadata['unit'] = 'millimeter'
701
+ elif a in ['s', 'c']:
702
+ axis_metadata['type'] = 'channel'
703
+ elif a == 't':
704
+ rdefs['defaultT'] = 0
705
+ elif a == 'z':
706
+ rdefs['defaultZ'] = 0
707
+ axes.append(axis_metadata)
708
+ if channel_axis and len(arrays) > 0:
709
+ base_array = list(arrays.values())[0]
710
+ base_shape = base_array.shape
711
+ for c in range(base_shape[channel_axis]):
712
+ channel_metadata = {
713
+ 'active': True,
714
+ 'coefficient': 1,
715
+ 'color': 'FFFFFF',
716
+ 'family': 'linear',
717
+ 'inverted': False,
718
+ 'label': f'Band {c + 1}',
719
+ }
720
+ channel_data = base_array[..., c]
721
+ channel_min = np.min(channel_data)
722
+ channel_max = np.max(channel_data)
723
+ channel_metadata['window'] = {
724
+ 'end': channel_max,
725
+ 'max': channel_max,
726
+ 'min': channel_min,
727
+ 'start': channel_min,
728
+ }
729
+ if len(self._channelNames) > c:
730
+ channel_metadata['label'] = self._channelNames[c]
731
+ if len(self._channelColors) > c:
732
+ channel_metadata['color'] = self._channelColors[c]
733
+ channels.append(channel_metadata)
734
+ # Guidelines from https://ngff.openmicroscopy.org/latest/
735
+ self._zarr.attrs.update({
736
+ 'multiscales': [{
737
+ 'version': '0.5',
738
+ 'name': name,
739
+ 'axes': axes,
740
+ 'datasets': datasets,
741
+ 'metadata': {
742
+ 'description': self._imageDescription or '',
743
+ 'kwargs': {
744
+ 'multichannel': (channel_axis is not None),
745
+ },
746
+ },
747
+ }],
748
+ 'omero': {
749
+ 'id': 1,
750
+ 'version': '0.5',
751
+ 'name': name,
752
+ 'channels': channels,
753
+ 'rdefs': rdefs,
754
+ },
755
+ 'bioformats2raw.layout': 3,
756
+ })
757
+
660
758
  @property
661
759
  def crop(self):
662
760
  """
@@ -682,6 +780,59 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
682
780
  raise TileSourceError(msg)
683
781
  self._crop = (x, y, w, h)
684
782
 
783
+ @property
784
+ def mm_x(self):
785
+ return self._mm_x
786
+
787
+ @mm_x.setter
788
+ def mm_x(self, value):
789
+ self._checkEditable()
790
+ value = float(value) if value is not None else None
791
+ if value is not None and value <= 0:
792
+ msg = 'mm_x must be positive or None'
793
+ raise TileSourceError(msg)
794
+ self._mm_x = value
795
+
796
+ @property
797
+ def mm_y(self):
798
+ return self._mm_y
799
+
800
+ @mm_y.setter
801
+ def mm_y(self, value):
802
+ self._checkEditable()
803
+ value = float(value) if value is not None else None
804
+ if value is not None and value <= 0:
805
+ msg = 'mm_y must be positive or None'
806
+ raise TileSourceError(msg)
807
+ self._mm_y = value
808
+
809
+ @property
810
+ def imageDescription(self):
811
+ return self._imageDescription
812
+
813
+ @imageDescription.setter
814
+ def imageDescription(self, description):
815
+ self._checkEditable()
816
+ self._imageDescription = description
817
+
818
+ @property
819
+ def channelNames(self):
820
+ return self._channelNames
821
+
822
+ @channelNames.setter
823
+ def channelNames(self, names):
824
+ self._checkEditable()
825
+ self._channelNames = names
826
+
827
+ @property
828
+ def channelColors(self):
829
+ return self._channelColors
830
+
831
+ @channelColors.setter
832
+ def channelColors(self, colors):
833
+ self._checkEditable()
834
+ self._channelColors = colors
835
+
685
836
  def _generateDownsampledLevels(self, resample_method):
686
837
  self._checkEditable()
687
838
  current_arrays = dict(self._zarr.arrays())
@@ -705,6 +856,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
705
856
  width=4096 + tile_overlap['x'],
706
857
  height=4096 + tile_overlap['y'],
707
858
  )
859
+ sorted_axes = [a[0] for a in sorted(self._axes.items(), key=lambda item: item[1])]
708
860
  for level in range(1, self.levels):
709
861
  scale_factor = 2 ** level
710
862
  iterator_output = dict(
@@ -739,7 +891,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
739
891
  x=x,
740
892
  y=y,
741
893
  **frame_position,
742
- axes=list(self._axes.keys()),
894
+ axes=sorted_axes,
743
895
  level=level,
744
896
  )
745
897
  self._validateZarr() # refresh self._levels before continuing
@@ -799,6 +951,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
799
951
  **frame_position,
800
952
  )
801
953
 
954
+ source._writeInternalMetadata()
955
+
802
956
  if suffix in ['.zarr', '.db', '.sqlite', '.zip']:
803
957
  if resample is None:
804
958
  resample = (
@@ -807,6 +961,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
807
961
  else ResampleMethod.NP_NEAREST
808
962
  )
809
963
  source._generateDownsampledLevels(resample)
964
+ source._writeInternalMetadata() # rewrite with new level datasets
810
965
 
811
966
  if suffix == '.zarr':
812
967
  shutil.copytree(source._tempdir.name, path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-zarr
3
- Version: 1.29.1.dev6
3
+ Version: 1.29.1.dev16
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.
@@ -17,11 +17,11 @@ Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Requires-Python: >=3.6
19
19
  License-File: LICENSE
20
- Requires-Dist: large-image>=1.29.1.dev6
20
+ Requires-Dist: large-image>=1.29.1.dev16
21
21
  Requires-Dist: zarr
22
22
  Requires-Dist: imagecodecs-numcodecs
23
23
  Provides-Extra: girder
24
- Requires-Dist: girder-large-image>=1.29.1.dev6; extra == "girder"
24
+ Requires-Dist: girder-large-image>=1.29.1.dev16; extra == "girder"
25
25
  Provides-Extra: all
26
26
  Requires-Dist: large-image-converter; extra == "all"
27
27
 
@@ -1,4 +1,4 @@
1
- large-image>=1.29.1.dev6
1
+ large-image>=1.29.1.dev16
2
2
  zarr
3
3
  imagecodecs-numcodecs
4
4
 
@@ -6,4 +6,4 @@ imagecodecs-numcodecs
6
6
  large-image-converter
7
7
 
8
8
  [girder]
9
- girder-large-image>=1.29.1.dev6
9
+ girder-large-image>=1.29.1.dev16