large-image-source-zarr 1.27.5.dev10__tar.gz → 1.27.5.dev55__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.dev10 → large-image-source-zarr-1.27.5.dev55}/PKG-INFO +1 -1
  2. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/large_image_source_zarr/__init__.py +293 -3
  3. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/large_image_source_zarr.egg-info/PKG-INFO +1 -1
  4. large-image-source-zarr-1.27.5.dev55/large_image_source_zarr.egg-info/requires.txt +6 -0
  5. large-image-source-zarr-1.27.5.dev10/large_image_source_zarr.egg-info/requires.txt +0 -6
  6. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/LICENSE +0 -0
  7. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/README.rst +0 -0
  8. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/large_image_source_zarr/girder_source.py +0 -0
  9. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/large_image_source_zarr.egg-info/SOURCES.txt +0 -0
  10. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/large_image_source_zarr.egg-info/dependency_links.txt +0 -0
  11. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/large_image_source_zarr.egg-info/entry_points.txt +0 -0
  12. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/large_image_source_zarr.egg-info/top_level.txt +0 -0
  13. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/pyproject.toml +0 -0
  14. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/setup.cfg +0 -0
  15. {large-image-source-zarr-1.27.5.dev10 → large-image-source-zarr-1.27.5.dev55}/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.dev10
3
+ Version: 1.27.5.dev55
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.
@@ -1,8 +1,12 @@
1
1
  import math
2
2
  import os
3
+ import shutil
4
+ import tempfile
3
5
  import threading
6
+ import uuid
4
7
  from importlib.metadata import PackageNotFoundError
5
8
  from importlib.metadata import version as _importlib_version
9
+ from pathlib import Path
6
10
 
7
11
  import numpy as np
8
12
  import packaging.version
@@ -10,10 +14,10 @@ import zarr
10
14
 
11
15
  import large_image
12
16
  from large_image.cache_util import LruCacheMetaclass, methodcache
13
- from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority
17
+ from large_image.constants import NEW_IMAGE_PATH_FLAG, TILE_FORMAT_NUMPY, SourcePriority
14
18
  from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
15
19
  from large_image.tilesource import FileTileSource
16
- from large_image.tilesource.utilities import nearPowerOfTwo
20
+ from large_image.tilesource.utilities import _imageToNumpy, nearPowerOfTwo
17
21
 
18
22
  try:
19
23
  __version__ = _importlib_version(__name__)
@@ -37,6 +41,7 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
37
41
  'zarray': SourcePriority.PREFERRED,
38
42
  'db': SourcePriority.MEDIUM,
39
43
  }
44
+ newPriority = SourcePriority.HIGH
40
45
 
41
46
  _tileSize = 512
42
47
  _minTileSize = 128
@@ -53,6 +58,13 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
53
58
  """
54
59
  super().__init__(path, **kwargs)
55
60
 
61
+ if str(path).startswith(NEW_IMAGE_PATH_FLAG):
62
+ self._initNew(path, **kwargs)
63
+ else:
64
+ self._initOpen(**kwargs)
65
+ self._tileLock = threading.RLock()
66
+
67
+ def _initOpen(self, **kwargs):
56
68
  self._largeImagePath = str(self._getLargeImagePath())
57
69
  self._zarr = None
58
70
  if not os.path.isfile(self._largeImagePath) and '//:' not in self._largeImagePath:
@@ -80,7 +92,48 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
80
92
  except Exception:
81
93
  msg = 'File cannot be opened -- not an OME NGFF file or understandable zarr file.'
82
94
  raise TileSourceError(msg)
83
- self._tileLock = threading.RLock()
95
+
96
+ def _initNew(self, path, **kwargs):
97
+ """
98
+ Initialize the tile class for creating a new image.
99
+ """
100
+ self._tempdir = tempfile.TemporaryDirectory(path)
101
+ self._zarr_store = zarr.DirectoryStore(self._tempdir.name)
102
+ self._zarr = zarr.open(self._zarr_store, mode='w')
103
+ # Make unpickleable
104
+ self._unpickleable = True
105
+ self._largeImagePath = None
106
+ self._dims = {}
107
+ self.sizeX = self.sizeY = self.levels = 0
108
+ self.tileWidth = self.tileHeight = self._tileSize
109
+ self._cacheValue = str(uuid.uuid4())
110
+ self._output = None
111
+ self._editable = True
112
+ self._bandRanges = None
113
+ self._addLock = threading.RLock()
114
+ self._framecount = 0
115
+ self._mm_x = 0
116
+ self._mm_y = 0
117
+ self._levels = []
118
+
119
+ def __del__(self):
120
+ if not hasattr(self, '_derivedSource'):
121
+ try:
122
+ self._zarr.close()
123
+ except Exception:
124
+ pass
125
+ try:
126
+ shutil.rmtree(self._tempdir)
127
+ except Exception:
128
+ pass
129
+
130
+ def _checkEditable(self):
131
+ """
132
+ Raise an exception if this is not an editable image.
133
+ """
134
+ if not self._editable:
135
+ msg = 'Not an editable image'
136
+ raise TileSourceError(msg)
84
137
 
85
138
  def _getGeneralAxes(self, arr):
86
139
  """
@@ -284,6 +337,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
284
337
  baseArray.shape[self._axes.get('c')] in {1, 3, 4}):
285
338
  self._bandCount = baseArray.shape[self._axes['c']]
286
339
  self._axes['s'] = self._axes.pop('c')
340
+ elif 's' in self._axes:
341
+ self._bandCount = baseArray.shape[self._axes['s']]
287
342
  self._zarrFindLevels()
288
343
  self._getScale()
289
344
  stride = 1
@@ -326,6 +381,13 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
326
381
  'mm_y': mm_y,
327
382
  }
328
383
 
384
+ def getState(self):
385
+ # Use the _cacheValue to avoid caching the source and tiles if we are
386
+ # creating something new.
387
+ if not hasattr(self, '_cacheValue'):
388
+ return super().getState()
389
+ return super().getState() + ',%s' % (self._cacheValue, )
390
+
329
391
  def getMetadata(self):
330
392
  """
331
393
  Return a dictionary of metadata containing levels, sizeX, sizeY,
@@ -333,6 +395,8 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
333
395
 
334
396
  :returns: metadata dictionary.
335
397
  """
398
+ if self._levels is None:
399
+ self._validateZarr()
336
400
  result = super().getMetadata()
337
401
  if self._framecount > 1:
338
402
  result['frames'] = frames = []
@@ -397,6 +461,9 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
397
461
 
398
462
  @methodcache()
399
463
  def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs):
464
+ if self._levels is None:
465
+ self._validateZarr()
466
+
400
467
  frame = self._getFrame(**kwargs)
401
468
  self._xyzInRange(x, y, z, frame, self._framecount)
402
469
  x0, y0, x1, y1, step = self._xyzToCorners(x, y, z)
@@ -439,6 +506,221 @@ class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
439
506
  return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z,
440
507
  pilImageAllowed, numpyAllowed, **kwargs)
441
508
 
509
+ def addTile(self, tile, x=0, y=0, mask=None, axes=None, **kwargs):
510
+ """
511
+ Add a numpy or image tile to the image, expanding the image as needed
512
+ to accommodate it. Note that x and y can be negative. If so, the
513
+ output image (and internal memory access of the image) will act as if
514
+ the 0, 0 point is the most negative position. Cropping is applied
515
+ after this offset.
516
+
517
+ :param tile: a numpy array, PIL Image, or a binary string
518
+ with an image. The numpy array can have 2 or 3 dimensions.
519
+ :param x: location in destination for upper-left corner.
520
+ :param y: location in destination for upper-left corner.
521
+ :param mask: a 2-d numpy array (or 3-d if the last dimension is 1).
522
+ If specified, areas where the mask is false will not be altered.
523
+ :param axes: a string or list of strings specifying the names of axes
524
+ in the same order as the tile dimensions
525
+ :param kwargs: start locations for any additional axes
526
+ """
527
+ # TODO: improve band bookkeeping
528
+
529
+ self._checkEditable()
530
+ placement = {
531
+ 'x': x,
532
+ 'y': y,
533
+ **kwargs,
534
+ }
535
+ if not isinstance(tile, np.ndarray) or axes is None:
536
+ axes = 'yxs'
537
+ tile, mode = _imageToNumpy(tile)
538
+ elif not isinstance(axes, str) and not isinstance(axes, list):
539
+ err = 'Invalid type for axes. Must be str or list[str].'
540
+ raise ValueError(err)
541
+ axes = [x.lower() for x in axes]
542
+ if axes[-1] != 's':
543
+ axes.append('s')
544
+ if mask is not None and len(axes) - 1 == len(mask.shape):
545
+ mask = mask[:, :, np.newaxis]
546
+ if 'x' not in axes or 'y' not in axes:
547
+ err = 'Invalid value for axes. Must contain "y" and "x".'
548
+ raise ValueError(err)
549
+ for k in placement:
550
+ if k not in axes:
551
+ axes[0:0] = [k]
552
+ with self._addLock:
553
+ self._axes = {k: i for i, k in enumerate(axes)}
554
+ while len(tile.shape) < len(axes):
555
+ tile = np.expand_dims(tile, axis=0)
556
+ while mask is not None and len(mask.shape) < len(axes):
557
+ mask = np.expand_dims(mask, axis=0)
558
+
559
+ new_dims = {
560
+ a: max(
561
+ self._dims.get(a, 0),
562
+ placement.get(a, 0) + tile.shape[i],
563
+ )
564
+ for a, i in self._axes.items()
565
+ }
566
+ placement_slices = tuple([
567
+ slice(placement.get(a, 0), placement.get(a, 0) + tile.shape[i], 1)
568
+ for i, a in enumerate(axes)
569
+ ])
570
+
571
+ current_arrays = dict(self._zarr.arrays())
572
+ chunking = None
573
+ if 'root' not in current_arrays:
574
+ root = np.empty(tuple(new_dims.values()), dtype=tile.dtype)
575
+ chunking = tuple([
576
+ self._tileSize if a in ['x', 'y'] else
577
+ new_dims.get('s') if a == 's' else 1
578
+ for a in axes
579
+ ])
580
+ else:
581
+ root = current_arrays['root']
582
+ root.resize(*tuple(new_dims.values()))
583
+ if root.chunks[-1] != new_dims.get('s'):
584
+ # rechunk if length of samples axis changes
585
+ chunking = tuple([
586
+ self._tileSize if a in ['x', 'y'] else
587
+ new_dims.get('s') if a == 's' else 1
588
+ for a in axes
589
+ ])
590
+
591
+ if mask is not None:
592
+ root[placement_slices] = np.where(mask, tile, root[placement_slices])
593
+ else:
594
+ root[placement_slices] = tile
595
+ if chunking:
596
+ self._zarr.create_dataset('root', data=root[:], chunks=chunking, overwrite=True)
597
+
598
+ # Edit OME metadata
599
+ self._zarr.attrs.update({
600
+ 'multiscales': [{
601
+ 'version': '0.5-dev',
602
+ 'axes': [{
603
+ 'name': a,
604
+ 'type': 'space' if a in ['x', 'y'] else 'other',
605
+ } for a in axes],
606
+ 'datasets': [{'path': 0}],
607
+ }],
608
+ 'omero': {'version': '0.5-dev'},
609
+ })
610
+
611
+ # Edit large_image attributes
612
+ self._dims = new_dims
613
+ self._dtype = tile.dtype
614
+ self._bandCount = new_dims.get(axes[-1]) # last axis is assumed to be bands
615
+ self.sizeX = new_dims.get('x')
616
+ self.sizeY = new_dims.get('y')
617
+ self._framecount = np.prod([
618
+ length
619
+ for axis, length in new_dims.items()
620
+ if axis in axes[:-3]
621
+ ])
622
+ self._cacheValue = str(uuid.uuid4())
623
+ self._levels = None
624
+ self.levels = int(max(1, math.ceil(math.log(max(
625
+ self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
626
+
627
+ @property
628
+ def crop(self):
629
+ """
630
+ Crop only applies to the output file, not the internal data access.
631
+
632
+ It consists of x, y, w, h in pixels.
633
+ """
634
+ return getattr(self, '_crop', None)
635
+
636
+ @crop.setter
637
+ def crop(self, value):
638
+ self._checkEditable()
639
+ if value is None:
640
+ self._crop = None
641
+ return
642
+ x, y, w, h = value
643
+ x = int(x)
644
+ y = int(y)
645
+ w = int(w)
646
+ h = int(h)
647
+ if x < 0 or y < 0 or w <= 0 or h <= 0:
648
+ msg = 'Crop must have non-negative x, y and positive w, h'
649
+ raise TileSourceError(msg)
650
+ self._crop = (x, y, w, h)
651
+
652
+ def write(
653
+ self,
654
+ path,
655
+ lossy=True,
656
+ alpha=True,
657
+ overwriteAllowed=True,
658
+ ):
659
+ """
660
+ Output the current image to a file.
661
+
662
+ :param path: output path.
663
+ :param lossy: if false, emit a lossless file.
664
+ :param alpha: True if an alpha channel is allowed.
665
+ :param overwriteAllowed: if False, raise an exception if the output
666
+ path exists.
667
+ """
668
+ if os.path.exists(path):
669
+ if overwriteAllowed:
670
+ if os.path.isdir(path):
671
+ shutil.rmtree(path)
672
+ else:
673
+ os.remove(path)
674
+ else:
675
+ raise TileSourceError('Output path exists (%s).' % str(path))
676
+
677
+ # TODO: compute half, quarter, etc. resolutions
678
+ self._validateZarr()
679
+ suffix = Path(path).suffix
680
+ data_dir = self._tempdir
681
+ data_store = self._zarr_store
682
+
683
+ if self.crop:
684
+ x, y, w, h = self.crop
685
+ current_arrays = dict(self._zarr.arrays())
686
+ # create new temp storage for cropped data
687
+ data_dir = tempfile.TemporaryDirectory()
688
+ data_store = zarr.DirectoryStore(data_dir.name)
689
+ cropped_zarr = zarr.open(data_store, mode='w')
690
+ for arr_name in current_arrays:
691
+ arr = np.array(current_arrays[arr_name])
692
+ cropped_arr = arr.take(
693
+ indices=range(x, x + w),
694
+ axis=self._axes.get('x'),
695
+ ).take(
696
+ indices=range(y, y + h),
697
+ axis=self._axes.get('y'),
698
+ )
699
+ cropped_zarr.create_dataset(arr_name, data=cropped_arr, overwrite=True)
700
+ cropped_zarr.attrs.update(self._zarr.attrs)
701
+
702
+ if suffix == '.zarr':
703
+ shutil.copytree(data_dir.name, path)
704
+
705
+ elif suffix in ['.db', '.sqlite']:
706
+ sqlite_store = zarr.SQLiteStore(path)
707
+ zarr.copy_store(data_store, sqlite_store, if_exists='replace')
708
+ sqlite_store.close()
709
+
710
+ elif suffix == '.zip':
711
+ zip_store = zarr.ZipStore(path)
712
+ zarr.copy_store(data_store, zip_store, if_exists='replace')
713
+ zip_store.close()
714
+
715
+ else:
716
+ from large_image_converter import convert
717
+
718
+ attrs_path = Path(data_dir.name) / '.zattrs'
719
+ convert(str(attrs_path), path, overwrite=overwriteAllowed)
720
+
721
+ if self.crop:
722
+ shutil.rmtree(data_dir.name)
723
+
442
724
 
443
725
  def open(*args, **kwargs):
444
726
  """
@@ -452,3 +734,11 @@ def canRead(*args, **kwargs):
452
734
  Check if an input can be read by the module class.
453
735
  """
454
736
  return ZarrFileTileSource.canRead(*args, **kwargs)
737
+
738
+
739
+ def new(*args, **kwargs):
740
+ """
741
+ Create a new image, collecting the results from patches of numpy arrays or
742
+ smaller images.
743
+ """
744
+ return ZarrFileTileSource(NEW_IMAGE_PATH_FLAG + str(uuid.uuid4()), *args, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-zarr
3
- Version: 1.27.5.dev10
3
+ Version: 1.27.5.dev55
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.dev55
2
+ zarr
3
+ imagecodecs-numcodecs
4
+
5
+ [girder]
6
+ girder-large-image>=1.27.5.dev55
@@ -1,6 +0,0 @@
1
- large-image>=1.27.5.dev10
2
- zarr
3
- imagecodecs-numcodecs
4
-
5
- [girder]
6
- girder-large-image>=1.27.5.dev10