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