large-image-source-zarr 1.27.5.dev69__tar.gz → 1.27.5.dev99__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.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/PKG-INFO +1 -1
  2. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/large_image_source_zarr/__init__.py +184 -98
  3. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/large_image_source_zarr.egg-info/PKG-INFO +1 -1
  4. large-image-source-zarr-1.27.5.dev99/large_image_source_zarr.egg-info/requires.txt +6 -0
  5. large-image-source-zarr-1.27.5.dev69/large_image_source_zarr.egg-info/requires.txt +0 -6
  6. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/LICENSE +0 -0
  7. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/README.rst +0 -0
  8. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/large_image_source_zarr/girder_source.py +0 -0
  9. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/large_image_source_zarr.egg-info/SOURCES.txt +0 -0
  10. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/large_image_source_zarr.egg-info/dependency_links.txt +0 -0
  11. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/large_image_source_zarr.egg-info/entry_points.txt +0 -0
  12. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/large_image_source_zarr.egg-info/top_level.txt +0 -0
  13. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/pyproject.toml +0 -0
  14. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/setup.cfg +0 -0
  15. {large-image-source-zarr-1.27.5.dev69 → large-image-source-zarr-1.27.5.dev99}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-zarr
3
- Version: 1.27.5.dev69
3
+ Version: 1.27.5.dev99
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,6 +17,7 @@ from large_image.cache_util import LruCacheMetaclass, methodcache
17
17
  from large_image.constants import NEW_IMAGE_PATH_FLAG, TILE_FORMAT_NUMPY, SourcePriority
18
18
  from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
19
19
  from large_image.tilesource import FileTileSource
20
+ from large_image.tilesource.resample import ResampleMethod, downsampleTileHalfRes
20
21
  from large_image.tilesource.utilities import _imageToNumpy, nearPowerOfTwo
21
22
 
22
23
  try:
@@ -130,7 +131,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
130
131
  except Exception:
131
132
  pass
132
133
  try:
133
- shutil.rmtree(self._tempdir)
134
+ self._tempdir.cleanup()
134
135
  except Exception:
135
136
  pass
136
137
 
@@ -516,6 +517,31 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
516
517
  return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z,
517
518
  pilImageAllowed, numpyAllowed, **kwargs)
518
519
 
520
+ def _validateNewTile(self, tile, mask, placement, axes):
521
+ if not isinstance(tile, np.ndarray) or axes is None:
522
+ axes = 'yxs'
523
+ tile, mode = _imageToNumpy(tile)
524
+ elif not isinstance(axes, str) and not isinstance(axes, list):
525
+ err = 'Invalid type for axes. Must be str or list[str].'
526
+ raise ValueError(err)
527
+ axes = [x.lower() for x in axes]
528
+ if axes[-1] != 's':
529
+ axes.append('s')
530
+ if mask is not None and len(axes) - 1 == len(mask.shape):
531
+ mask = mask[:, :, np.newaxis]
532
+ if 'x' not in axes or 'y' not in axes:
533
+ err = 'Invalid value for axes. Must contain "y" and "x".'
534
+ raise ValueError(err)
535
+ for k in placement:
536
+ if k not in axes:
537
+ axes[0:0] = [k]
538
+ while len(tile.shape) < len(axes):
539
+ tile = np.expand_dims(tile, axis=0)
540
+ while mask is not None and len(mask.shape) < len(axes):
541
+ mask = np.expand_dims(mask, axis=0)
542
+
543
+ return tile, mask, placement, axes
544
+
519
545
  def addTile(self, tile, x=0, y=0, mask=None, axes=None, **kwargs):
520
546
  """
521
547
  Add a numpy or image tile to the image, expanding the image as needed
@@ -537,60 +563,47 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
537
563
  # TODO: improve band bookkeeping
538
564
 
539
565
  self._checkEditable()
566
+ store_path = str(kwargs.pop('level', 0))
540
567
  placement = {
541
568
  'x': x,
542
569
  'y': y,
543
570
  **kwargs,
544
571
  }
545
- if not isinstance(tile, np.ndarray) or axes is None:
546
- axes = 'yxs'
547
- tile, mode = _imageToNumpy(tile)
548
- elif not isinstance(axes, str) and not isinstance(axes, list):
549
- err = 'Invalid type for axes. Must be str or list[str].'
550
- raise ValueError(err)
551
- axes = [x.lower() for x in axes]
552
- if axes[-1] != 's':
553
- axes.append('s')
554
- if mask is not None and len(axes) - 1 == len(mask.shape):
555
- mask = mask[:, :, np.newaxis]
556
- if 'x' not in axes or 'y' not in axes:
557
- err = 'Invalid value for axes. Must contain "y" and "x".'
558
- raise ValueError(err)
559
- for k in placement:
560
- if k not in axes:
561
- axes[0:0] = [k]
572
+ tile, mask, placement, axes = self._validateNewTile(tile, mask, placement, axes)
573
+
562
574
  with self._addLock:
563
575
  self._axes = {k: i for i, k in enumerate(axes)}
564
- while len(tile.shape) < len(axes):
565
- tile = np.expand_dims(tile, axis=0)
566
- while mask is not None and len(mask.shape) < len(axes):
567
- mask = np.expand_dims(mask, axis=0)
568
-
569
576
  new_dims = {
570
577
  a: max(
571
- self._dims.get(a, 0),
578
+ self._dims.get(store_path, {}).get(a, 0),
572
579
  placement.get(a, 0) + tile.shape[i],
573
580
  )
574
581
  for a, i in self._axes.items()
575
582
  }
583
+ self._dims[store_path] = new_dims
576
584
  placement_slices = tuple([
577
585
  slice(placement.get(a, 0), placement.get(a, 0) + tile.shape[i], 1)
578
586
  for i, a in enumerate(axes)
579
587
  ])
580
588
 
581
589
  current_arrays = dict(self._zarr.arrays())
590
+ if store_path == '0':
591
+ # if writing to base data, invalidate generated levels
592
+ for path in current_arrays:
593
+ if path != store_path:
594
+ self._zarr_store.rmdir(path)
582
595
  chunking = None
583
- if 'root' not in current_arrays:
584
- root = np.empty(tuple(new_dims.values()), dtype=tile.dtype)
596
+ if store_path not in current_arrays:
597
+ arr = np.empty(tuple(new_dims.values()), dtype=tile.dtype)
585
598
  chunking = tuple([
586
599
  self._tileSize if a in ['x', 'y'] else
587
600
  new_dims.get('s') if a == 's' else 1
588
601
  for a in axes
589
602
  ])
590
603
  else:
591
- root = current_arrays['root']
592
- root.resize(*tuple(new_dims.values()))
593
- if root.chunks[-1] != new_dims.get('s'):
604
+ arr = current_arrays[store_path]
605
+ arr.resize(*tuple(new_dims.values()))
606
+ if arr.chunks[-1] != new_dims.get('s'):
594
607
  # rechunk if length of samples axis changes
595
608
  chunking = tuple([
596
609
  self._tileSize if a in ['x', 'y'] else
@@ -599,40 +612,45 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
599
612
  ])
600
613
 
601
614
  if mask is not None:
602
- root[placement_slices] = np.where(mask, tile, root[placement_slices])
615
+ arr[placement_slices] = np.where(mask, tile, arr[placement_slices])
603
616
  else:
604
- root[placement_slices] = tile
617
+ arr[placement_slices] = tile
605
618
  if chunking:
606
- self._zarr.create_dataset('root', data=root[:], chunks=chunking, overwrite=True)
607
-
608
- # Edit OME metadata
609
- self._zarr.attrs.update({
610
- 'multiscales': [{
611
- 'version': '0.5-dev',
612
- 'axes': [{
613
- 'name': a,
614
- 'type': 'space' if a in ['x', 'y'] else 'other',
615
- } for a in axes],
616
- 'datasets': [{'path': 0}],
617
- }],
618
- 'omero': {'version': '0.5-dev'},
619
- })
620
-
621
- # Edit large_image attributes
622
- self._dims = new_dims
623
- self._dtype = tile.dtype
624
- self._bandCount = new_dims.get(axes[-1]) # last axis is assumed to be bands
625
- self.sizeX = new_dims.get('x')
626
- self.sizeY = new_dims.get('y')
627
- self._framecount = np.prod([
628
- length
629
- for axis, length in new_dims.items()
630
- if axis in axes[:-3]
631
- ])
632
- self._cacheValue = str(uuid.uuid4())
633
- self._levels = None
634
- self.levels = int(max(1, math.ceil(math.log(max(
635
- self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
619
+ zarr.array(
620
+ arr,
621
+ chunks=chunking,
622
+ overwrite=True,
623
+ store=self._zarr_store,
624
+ path=store_path,
625
+ )
626
+
627
+ # If base data changed, update large_image attributes and OME metadata
628
+ if store_path == '0':
629
+ self._zarr.attrs.update({
630
+ 'multiscales': [{
631
+ 'version': '0.5-dev',
632
+ 'axes': [{
633
+ 'name': a,
634
+ 'type': 'space' if a in ['x', 'y'] else 'other',
635
+ } for a in axes],
636
+ 'datasets': [{'path': 0}],
637
+ }],
638
+ 'omero': {'version': '0.5-dev'},
639
+ })
640
+
641
+ self._dtype = tile.dtype
642
+ self._bandCount = new_dims.get(axes[-1]) # last axis is assumed to be bands
643
+ self.sizeX = new_dims.get('x')
644
+ self.sizeY = new_dims.get('y')
645
+ self._framecount = np.prod([
646
+ length
647
+ for axis, length in new_dims.items()
648
+ if axis in axes[:-3]
649
+ ])
650
+ self._cacheValue = str(uuid.uuid4())
651
+ self._levels = None
652
+ self.levels = int(max(1, math.ceil(math.log(max(
653
+ self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
636
654
 
637
655
  @property
638
656
  def crop(self):
@@ -659,12 +677,75 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
659
677
  raise TileSourceError(msg)
660
678
  self._crop = (x, y, w, h)
661
679
 
680
+ def _generateDownsampledLevels(self, resample_method):
681
+ self._checkEditable()
682
+ current_arrays = dict(self._zarr.arrays())
683
+ if '0' not in current_arrays:
684
+ msg = 'No root data found, cannot generate lower resolution levels.'
685
+ raise TileSourceError(msg)
686
+ if 'x' not in self._axes or 'y' not in self._axes:
687
+ msg = 'Data must have an X axis and Y axis to generate lower resolution levels.'
688
+ raise TileSourceError(msg)
689
+
690
+ metadata = self.getMetadata()
691
+
692
+ if (
693
+ resample_method.value < ResampleMethod.PIL_MAX_ENUM.value and
694
+ resample_method != ResampleMethod.PIL_NEAREST
695
+ ):
696
+ tile_overlap = dict(x=4, y=4, edges=True)
697
+ else:
698
+ tile_overlap = dict(x=0, y=0)
699
+ tile_size = dict(
700
+ width=4096 + tile_overlap['x'],
701
+ height=4096 + tile_overlap['y'],
702
+ )
703
+ for level in range(1, self.levels):
704
+ scale_factor = 2 ** level
705
+ iterator_output = dict(
706
+ maxWidth=self.sizeX // scale_factor,
707
+ maxHeight=self.sizeY // scale_factor,
708
+ )
709
+ for frame in metadata.get('frames', [{'Index': 0}]):
710
+ frame_position = {
711
+ k.replace('Index', '').lower(): v
712
+ for k, v in frame.items()
713
+ if k.replace('Index', '').lower() in self._axes
714
+ }
715
+ for tile in self.tileIterator(
716
+ tile_size=tile_size,
717
+ tile_overlap=tile_overlap,
718
+ frame=frame['Index'],
719
+ output=iterator_output,
720
+ resample=False, # TODO: incorporate resampling in core
721
+ ):
722
+ new_tile = downsampleTileHalfRes(tile['tile'], resample_method)
723
+ overlap = {k: int(v / 2) for k, v in tile['tile_overlap'].items()}
724
+ new_tile = new_tile[
725
+ slice(overlap['top'], new_tile.shape[0] - overlap['bottom']),
726
+ slice(overlap['left'], new_tile.shape[1] - overlap['right']),
727
+ ]
728
+
729
+ x = int(tile['x'] / 2 + overlap['left'])
730
+ y = int(tile['y'] / 2 + overlap['top'])
731
+
732
+ self.addTile(
733
+ new_tile,
734
+ x=x,
735
+ y=y,
736
+ **frame_position,
737
+ axes=list(self._axes.keys()),
738
+ level=level,
739
+ )
740
+ self._validateZarr() # refresh self._levels before continuing
741
+
662
742
  def write(
663
743
  self,
664
744
  path,
665
745
  lossy=True,
666
746
  alpha=True,
667
747
  overwriteAllowed=True,
748
+ resample=None,
668
749
  ):
669
750
  """
670
751
  Output the current image to a file.
@@ -684,53 +765,58 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
684
765
  else:
685
766
  raise TileSourceError('Output path exists (%s).' % str(path))
686
767
 
687
- # TODO: compute half, quarter, etc. resolutions
688
- self._validateZarr()
689
768
  suffix = Path(path).suffix
690
- data_dir = self._tempdir
691
- data_store = self._zarr_store
769
+ source = self
692
770
 
693
771
  if self.crop:
694
- x, y, w, h = self.crop
695
- current_arrays = dict(self._zarr.arrays())
696
- # create new temp storage for cropped data
697
- data_dir = tempfile.TemporaryDirectory()
698
- data_store = zarr.DirectoryStore(data_dir.name)
699
- cropped_zarr = zarr.open(data_store, mode='w')
700
- for arr_name in current_arrays:
701
- arr = np.array(current_arrays[arr_name])
702
- cropped_arr = arr.take(
703
- indices=range(x, x + w),
704
- axis=self._axes.get('x'),
705
- ).take(
706
- indices=range(y, y + h),
707
- axis=self._axes.get('y'),
772
+ top, left, height, width = self.crop
773
+ source = new()
774
+ source._zarr.attrs.update(self._zarr.attrs)
775
+ for frame in self.getMetadata().get('frames', [{'Index': 0}]):
776
+ frame_position = {
777
+ k.replace('Index', '').lower(): v
778
+ for k, v in frame.items()
779
+ if k.replace('Index', '').lower() in self._axes
780
+ }
781
+ for tile in self.tileIterator(
782
+ frame=frame['Index'],
783
+ region=dict(top=top, left=left, width=width, height=height),
784
+ resample=False,
785
+ ):
786
+ source.addTile(
787
+ tile['tile'],
788
+ x=tile['x'] - left,
789
+ y=tile['y'] - top,
790
+ axes=list(self._axes.keys()),
791
+ **frame_position,
792
+ )
793
+
794
+ if suffix in ['.zarr', '.db', '.sqlite', '.zip']:
795
+ if resample is None:
796
+ resample = (
797
+ ResampleMethod.PIL_LANCZOS
798
+ if lossy and source.dtype == np.uint8
799
+ else ResampleMethod.NP_NEAREST
708
800
  )
709
- cropped_zarr.create_dataset(arr_name, data=cropped_arr, overwrite=True)
710
- cropped_zarr.attrs.update(self._zarr.attrs)
711
-
712
- if suffix == '.zarr':
713
- shutil.copytree(data_dir.name, path)
714
-
715
- elif suffix in ['.db', '.sqlite']:
716
- sqlite_store = zarr.SQLiteStore(path)
717
- zarr.copy_store(data_store, sqlite_store, if_exists='replace')
718
- sqlite_store.close()
719
-
720
- elif suffix == '.zip':
721
- zip_store = zarr.ZipStore(path)
722
- zarr.copy_store(data_store, zip_store, if_exists='replace')
723
- zip_store.close()
801
+ source._generateDownsampledLevels(resample)
802
+
803
+ if suffix == '.zarr':
804
+ shutil.copytree(source._tempdir.name, path)
805
+ elif suffix in ['.db', '.sqlite']:
806
+ sqlite_store = zarr.SQLiteStore(path)
807
+ zarr.copy_store(source._zarr_store, sqlite_store, if_exists='replace')
808
+ sqlite_store.close()
809
+ elif suffix == '.zip':
810
+ zip_store = zarr.ZipStore(path)
811
+ zarr.copy_store(source._zarr_store, zip_store, if_exists='replace')
812
+ zip_store.close()
724
813
 
725
814
  else:
726
815
  from large_image_converter import convert
727
816
 
728
- attrs_path = Path(data_dir.name) / '.zattrs'
817
+ attrs_path = Path(source._tempdir.name) / '.zattrs'
729
818
  convert(str(attrs_path), path, overwrite=overwriteAllowed)
730
819
 
731
- if self.crop:
732
- shutil.rmtree(data_dir.name)
733
-
734
820
 
735
821
  def open(*args, **kwargs):
736
822
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-zarr
3
- Version: 1.27.5.dev69
3
+ Version: 1.27.5.dev99
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.
@@ -0,0 +1,6 @@
1
+ large-image>=1.27.5.dev99
2
+ zarr
3
+ imagecodecs-numcodecs
4
+
5
+ [girder]
6
+ girder-large-image>=1.27.5.dev99
@@ -1,6 +0,0 @@
1
- large-image>=1.27.5.dev69
2
- zarr
3
- imagecodecs-numcodecs
4
-
5
- [girder]
6
- girder-large-image>=1.27.5.dev69