large-image-source-multi 1.27.5.dev4__tar.gz → 1.30.7.dev10__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 (16) hide show
  1. {large-image-source-multi-1.27.5.dev4/large_image_source_multi.egg-info → large_image_source_multi-1.30.7.dev10}/PKG-INFO +10 -3
  2. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/README.rst +20 -40
  3. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/large_image_source_multi/__init__.py +150 -46
  4. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10/large_image_source_multi.egg-info}/PKG-INFO +10 -3
  5. large_image_source_multi-1.30.7.dev10/large_image_source_multi.egg-info/requires.txt +9 -0
  6. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/setup.py +4 -3
  7. large-image-source-multi-1.27.5.dev4/large_image_source_multi.egg-info/requires.txt +0 -9
  8. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/LICENSE +0 -0
  9. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/docs/specification.rst +0 -0
  10. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/large_image_source_multi/girder_source.py +0 -0
  11. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/large_image_source_multi.egg-info/SOURCES.txt +0 -0
  12. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/large_image_source_multi.egg-info/dependency_links.txt +0 -0
  13. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/large_image_source_multi.egg-info/entry_points.txt +0 -0
  14. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/large_image_source_multi.egg-info/top_level.txt +0 -0
  15. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/pyproject.toml +0 -0
  16. {large-image-source-multi-1.27.5.dev4 → large_image_source_multi-1.30.7.dev10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-multi
3
- Version: 1.27.5.dev4
3
+ Version: 1.30.7.dev10
4
4
  Summary: A tilesource for large_image to composite other tile sources
5
5
  Home-page: https://github.com/girder/large_image
6
6
  Author: Kitware, Inc.
@@ -15,10 +15,17 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
- Requires-Python: >=3.6
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/x-rst
21
+ License-File: LICENSE
22
+ Requires-Dist: jsonschema
23
+ Requires-Dist: large-image>=1.30.7.dev10
24
+ Requires-Dist: pyyaml
19
25
  Provides-Extra: all
26
+ Requires-Dist: scikit-image; extra == "all"
20
27
  Provides-Extra: girder
21
- License-File: LICENSE
28
+ Requires-Dist: girder-large-image>=1.30.7.dev10; extra == "girder"
22
29
 
23
30
  A tilesource for large_image to composite other tile sources
24
31
 
@@ -15,7 +15,7 @@ Large Image
15
15
  :target: https://codecov.io/github/girder/large_image?branch=master
16
16
  :alt: codecov.io
17
17
 
18
- .. |doi-badge| image:: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.4723355-blue
18
+ .. |doi-badge| image:: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.4723355-blue.svg
19
19
  :target: https://zenodo.org/badge/latestdoi/45569214
20
20
 
21
21
  .. |pypi-badge| image:: https://img.shields.io/pypi/v/large-image.svg?logo=python&logoColor=white
@@ -39,7 +39,7 @@ Highlights
39
39
  Installation
40
40
  ------------
41
41
 
42
- In addition to installing the ``large-image`` package, you'll need at least one tile source (a ``large-image-source-xxx`` package). You can install everything from the main project with one of these commands:
42
+ In addition to installing the base ``large-image`` package, you'll need at least one tile source which corresponds to your target file format(s) (a ``large-image-source-xxx`` package). You can install everything from the main project with one of these commands:
43
43
 
44
44
  Pip
45
45
  ~~~
@@ -52,7 +52,7 @@ Install all tile sources on linux::
52
52
 
53
53
  pip install large-image[all] --find-links https://girder.github.io/large_image_wheels
54
54
 
55
- Install all tile sources and all Girder plugins on linux::
55
+ When using large-image with an instance of `Girder`_, install all tile sources and all Girder plugins on linux::
56
56
 
57
57
  pip install large-image[all] girder-large-image-annotation[tasks] --find-links https://girder.github.io/large_image_wheels
58
58
 
@@ -60,8 +60,9 @@ Install all tile sources and all Girder plugins on linux::
60
60
  Conda
61
61
  ~~~~~
62
62
 
63
- Conda makes dependency management a bit easier if not on Linux. Some of the source modules are available on conda-forge. You can install the following::
63
+ Conda makes dependency management a bit easier if not on Linux. The base module, converter module, and two of the source modules are available on conda-forge. You can install the following::
64
64
 
65
+ conda install -c conda-forge large-image
65
66
  conda install -c conda-forge large-image-source-gdal
66
67
  conda install -c conda-forge large-image-source-tiff
67
68
  conda install -c conda-forge large-image-converter
@@ -118,7 +119,7 @@ Large Image consists of several Python modules designed to work together. These
118
119
 
119
120
  - ``large-image-source-deepzoom``: A tile source for reading Deepzoom tiles.
120
121
 
121
- - ``large-image-source-dicom``: A tile source for reading DICOM WSI images.
122
+ - ``large-image-source-dicom``: A tile source for reading DICOM Whole Slide Images (WSI).
122
123
 
123
124
  - ``large-image-source-gdal``: A tile source for reading geotiff files via GDAL. This handles source data with more complex transforms than the mapnik tile source.
124
125
 
@@ -128,62 +129,41 @@ Large Image consists of several Python modules designed to work together. These
128
129
 
129
130
  - ``large-image-source-nd2``: A tile source for reading nd2 (NIS Element) images.
130
131
 
131
- - ``large-image-source-ometiff``: A tile source using the tiff library that can handle some multi-frame OMETiff files.
132
+ - ``large-image-source-ometiff``: A tile source using the tiff library that can handle most multi-frame OMETiff files that are compliant with the specification.
132
133
 
133
134
  - ``large-image-source-openjpeg``: A tile source using the Glymur library to read jp2 (JPEG 2000) files.
134
135
 
135
136
  - ``large-image-source-openslide``: A tile source using the OpenSlide library. This works with svs, ndpi, Mirax, tiff, vms, and other file formats.
136
137
 
137
- - ``large-image-source-pil``: A tile source for small images via the Python Imaging Library (Pillow).
138
+ - ``large-image-source-pil``: A tile source for small images via the Python Imaging Library (Pillow). By default, the maximum size is 4096, but the maximum size can be configured.
138
139
 
139
140
  - ``large-image-source-tiff``: A tile source for reading pyramidal tiff files in common compression formats.
140
141
 
141
142
  - ``large-image-source-tifffile``: A tile source using the tifffile library that can handle a wide variety of tiff-like files.
142
143
 
143
- - ``large-image-source-vips``: A tile source for reading any files handled by libvips. This also can be used for writing tiled images from numpy arrays.
144
+ - ``large-image-source-vips``: A tile source for reading any files handled by libvips. This also can be used for writing tiled images from numpy arrays (up to 4 dimensions).
144
145
 
145
- - ``large-image-source-zarr``: A tile source using the zarr library that can handle OME-Zarr (OME-NGFF) files as well as some other zarr files.
146
+ - ``large-image-source-zarr``: A tile source using the zarr library that can handle OME-Zarr (OME-NGFF) files as well as some other zarr files. This can also be used for writing N-dimensional tiled images from numpy arrays. Written images can be saved as any supported format.
146
147
 
147
148
  - ``large-image-source-test``: A tile source that generates test tiles, including a simple fractal pattern. Useful for testing extreme zoom levels.
148
149
 
149
- - ``large-image-source-dummy``: A tile source that does nothing.
150
+ - ``large-image-source-dummy``: A tile source that does nothing. This is an absolutely minimal implementation of a tile source used for testing. If you want to create a custom tile source, start with this implementation.
150
151
 
151
- Most tile sources can be used with girder-large-image. You can specific an extras_require of ``girder`` to include ``girder-large-image`` with the source.
152
152
 
153
- - As a Girder plugin:
153
+ As a `Girder`_ plugin, ``large-image`` adds end points to access all of the image formats it can read both to get metadata and to act as a tile server.
154
+ In the Girder UI, ``large-image`` shows images on item pages, and can show thumbnails in item lists when browsing folders.
155
+ There is also cache management to balance memory use and speed of response in Girder when ``large-image`` is used as a tile server.
154
156
 
155
- - ``girder-large-image``: Large Image as a Girder_ 3.x plugin.
156
- You can specify extras_require of ``tasks`` to install a Girder Worker task that can convert otherwise unreadable images to pyramidal tiff files.
157
+ Most tile sources can be used with Girder Large Image. You can specify an extras_require of ``girder`` to install the following packages:
157
158
 
158
- - ``girder-large-image-annotation``: Annotations for large images as a Girder_ 3.x plugin.
159
+ - ``girder-large-image``: Large Image as a Girder 3.x plugin.
160
+ You can install ``large-image[tasks]`` to install a Girder Worker task that can convert otherwise unreadable images to pyramidal tiff files.
161
+
162
+ - ``girder-large-image-annotation``: Adds models to the Girder database for supporting annotating large images. These annotations can be rendered on images. Annotations can include polygons, points, image overlays, and other types. Each annotation can have a label and metadata.
159
163
 
160
164
  - ``large-image-tasks``: A utility for running the converter via Girder Worker.
161
165
  You can specify an extras_require of ``girder`` to include modules needed to work with the Girder remote worker or ``worker`` to include modules needed on the remote side of the Girder remote worker. If neither is specified, some conversion tasks can be run using Girder local jobs.
162
166
 
163
167
 
164
- Developer Installation
165
- ----------------------
166
-
167
- To install all packages from source, clone the repository::
168
-
169
- git clone https://github.com/girder/large_image.git
170
- cd large_image
171
-
172
- Install all packages and dependencies::
173
-
174
- pip install -e . -r requirements-dev.txt
175
-
176
- If you aren't developing with Girder 3, you can skip installing those components. Use ``requirements-dev-core.txt`` instead of ``requirements-dev.txt``::
177
-
178
- pip install -e . -r requirements-dev-core.txt
179
-
180
-
181
- Tile source prerequisites
182
- =========================
183
-
184
- Many tile sources have complex prerequisites. These can be installed directly using your system's package manager or from some prebuilt Python wheels for Linux. The prebuilt wheels are not official packages, but they can be used by instructing pip to use them by preference::
185
-
186
- pip install -e . -r requirements-dev.txt --find-links https://girder.github.io/large_image_wheels
187
-
188
168
 
189
- .. _Girder: https://github.com/girder/girder
169
+ .. _Girder: https://girder.readthedocs.io/en/latest/
@@ -10,7 +10,6 @@ from importlib.metadata import PackageNotFoundError
10
10
  from importlib.metadata import version as _importlib_version
11
11
  from pathlib import Path
12
12
 
13
- import jsonschema
14
13
  import numpy as np
15
14
  import yaml
16
15
 
@@ -27,6 +26,26 @@ except PackageNotFoundError:
27
26
  # package is not installed
28
27
  pass
29
28
 
29
+ jsonschema = None
30
+ _validator = None
31
+
32
+
33
+ def _lazyImport():
34
+ """
35
+ Import the jsonschema module. This is done when needed rather than in the
36
+ module initialization because it is slow.
37
+ """
38
+ global jsonschema, _validator
39
+
40
+ if jsonschema is None:
41
+ try:
42
+ import jsonschema
43
+
44
+ _validator = jsonschema.Draft6Validator(MultiSourceSchema)
45
+ except ImportError:
46
+ msg = 'jsonschema module not found.'
47
+ raise TileSourceError(msg)
48
+
30
49
 
31
50
  SourceEntrySchema = {
32
51
  'type': 'object',
@@ -269,10 +288,24 @@ SourceEntrySchema = {
269
288
  'type': 'array',
270
289
  'items': {'type': 'integer'},
271
290
  },
272
- 'style': {'type': 'object'},
273
- 'params': {
291
+ 'sampleScale': {
274
292
  'description':
275
- 'Additional parameters to pass to the base tile source',
293
+ 'Each pixel sample values is divided by this scale after any '
294
+ 'sampleOffset has been applied',
295
+ 'type': 'number',
296
+ },
297
+ 'sampleOffset': {
298
+ 'description':
299
+ 'This is added to each pixel sample value before any '
300
+ 'sampleScale is applied',
301
+ 'type': 'number',
302
+ },
303
+ 'style': {
304
+ 'description': 'A style specification to pass to the base tile source',
305
+ 'type': 'object',
306
+ },
307
+ 'params': {
308
+ 'description': 'Additional parameters to pass to the base tile source',
276
309
  'type': 'object',
277
310
  },
278
311
  },
@@ -333,6 +366,10 @@ MultiSourceSchema = {
333
366
  'layout and size, assume all sources are so similar',
334
367
  'type': 'boolean',
335
368
  },
369
+ 'dtype': {
370
+ 'description': 'If present, a numpy dtype name to use for the data.',
371
+ 'type': 'string',
372
+ },
336
373
  'singleBand': {
337
374
  'description':
338
375
  'If true, output only the first band of compositied results',
@@ -376,6 +413,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
376
413
  None: SourcePriority.FALLBACK,
377
414
  'application/json': SourcePriority.PREFERRED,
378
415
  'application/yaml': SourcePriority.PREFERRED,
416
+ 'text/yaml': SourcePriority.PREFERRED,
379
417
  }
380
418
 
381
419
  _minTileSize = 64
@@ -383,9 +421,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
383
421
  _defaultTileSize = 256
384
422
  _maxOpenHandles = 6
385
423
 
386
- _validator = jsonschema.Draft6Validator(MultiSourceSchema)
387
-
388
- def __init__(self, path, **kwargs):
424
+ def __init__(self, path, **kwargs): # noqa
389
425
  """
390
426
  Initialize the tile class. See the base class for other available
391
427
  parameters.
@@ -394,11 +430,22 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
394
430
  """
395
431
  super().__init__(path, **kwargs)
396
432
 
433
+ _lazyImport()
434
+ self._validator = _validator
397
435
  self._largeImagePath = self._getLargeImagePath()
398
436
  self._lastOpenSourceLock = threading.RLock()
399
437
  # 'c' must be first as channels are special because they can have names
400
438
  self._axesList = ['c', 'z', 't', 'xy']
401
- if not os.path.isfile(self._largeImagePath):
439
+ if isinstance(path, dict) and 'sources' in path:
440
+ self._info = path.copy()
441
+ self._basePath = '.'
442
+ self._largeImagePath = '.'
443
+ try:
444
+ self._validator.validate(self._info)
445
+ except jsonschema.ValidationError:
446
+ msg = 'File cannot be validated via multi-source reader.'
447
+ raise TileSourceError(msg)
448
+ elif not os.path.isfile(self._largeImagePath):
402
449
  try:
403
450
  possibleYaml = self._largeImagePath.split('multi://', 1)[-1]
404
451
  self._info = yaml.safe_load(possibleYaml)
@@ -410,7 +457,8 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
410
457
  try:
411
458
  with builtins.open(self._largeImagePath) as fptr:
412
459
  start = fptr.read(1024).strip()
413
- if start[:1] not in ('{', '#', '-') and (start[:1] < 'a' or start[:1] > 'z'):
460
+ if (start[:1] not in ('{', '#', '-') and
461
+ (start[:1] < 'a' or start[:1] > 'z')) or 'FeatureCollection' in start:
414
462
  msg = 'File cannot be opened via multi-source reader.'
415
463
  raise TileSourceError(msg)
416
464
  fptr.seek(0)
@@ -661,9 +709,15 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
661
709
  aKey = tuple(self._axisKey(source, frame.get(f'Index{axis.upper()}') or 0, axis)
662
710
  for axis in self._axesList)
663
711
  channel = channels[cIdx] if cIdx < len(channels) else None
712
+ # We add the channel name to our channel list if the individual
713
+ # source lists the channel name or a set of channels OR the source
714
+ # appends channels to an existing set.
664
715
  if channel and channel not in self._channels and (
665
- 'channel' in source or 'channels' in source):
716
+ 'channel' in source or 'channels' in source or
717
+ len(self._channels) == aKey[0]):
666
718
  self._channels.append(channel)
719
+ # Adjust the channel number if the source named the channel; do not
720
+ # do so in other cases
667
721
  if (channel and channel in self._channels and
668
722
  'c' not in source and 'cValues' not in source):
669
723
  aKey = tuple([self._channels.index(channel)] + list(aKey[1:]))
@@ -759,7 +813,11 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
759
813
  self.tileWidth = self.tileWidth or ts.tileWidth
760
814
  self.tileHeight = self.tileHeight or ts.tileHeight
761
815
  if not hasattr(self, '_firstdtype'):
762
- self._firstdtype = ts.dtype
816
+ self._firstdtype = (
817
+ ts.dtype if not self._info.get('dtype') else
818
+ np.dtype(self._info['dtype']))
819
+ if self._info.get('dtype'):
820
+ self._dtype = np.dtype(self._info['dtype'])
763
821
  if not numChecked:
764
822
  tsMag = ts.getNativeMagnification()
765
823
  for key in self._nativeMagnification:
@@ -773,7 +831,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
773
831
  self._bands = {}
774
832
  self._bands.update(tsMeta['bands'])
775
833
  lastSource = source
776
- self._bandcount = 1 if self._info.get('singleBand') else (
834
+ self._bandCount = 1 if self._info.get('singleBand') else (
777
835
  len(self._bands) if hasattr(self, '_bands') else (bandCount or None))
778
836
  bbox = self._sourceBoundingBox(source, tsMeta['sizeX'], tsMeta['sizeY'])
779
837
  computedWidth = max(computedWidth, int(math.ceil(bbox['right'])))
@@ -828,7 +886,8 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
828
886
  with self._lastOpenSourceLock:
829
887
  if (hasattr(self, '_lastOpenSource') and
830
888
  self._lastOpenSource['source'] == source and
831
- self._lastOpenSource['params'] == params):
889
+ (self._lastOpenSource['params'] == params or (
890
+ params == {} and self._lastOpenSource['params'] is None))):
832
891
  return self._lastOpenSource['ts']
833
892
  if not len(large_image.tilesource.AvailableTileSources):
834
893
  large_image.tilesource.loadTileSources()
@@ -841,6 +900,22 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
841
900
  if params is None:
842
901
  params = source.get('params', {})
843
902
  ts = openFunc(source['path'], **params)
903
+ if (self._dtype and np.dtype(ts.dtype).kind == 'f' and
904
+ (self._dtype == 'check' or np.dtype(self._dtype).kind != 'f') and
905
+ 'sampleScale' not in source and 'sampleOffset' not in source):
906
+ minval = maxval = 0
907
+ for f in range(ts.frames):
908
+ ftile = ts.getTile(x=0, y=0, z=0, frame=f, numpyAllowed='always')
909
+ minval = min(minval, np.amin(ftile))
910
+ maxval = max(maxval, np.amax(ftile))
911
+ if minval >= 0 and maxval <= 1:
912
+ source['sampleScale'] = None
913
+ elif minval >= 0:
914
+ source['sampleScale'] = 2 ** math.ceil(math.log2(maxval))
915
+ else:
916
+ source['sampleScale'] = 2 ** math.ceil(math.log2(max(-minval, maxval)) + 1)
917
+ source['sampleOffset'] = source['sampleScale'] / 2
918
+ source['sourceName'] = ts.name
844
919
  with self._lastOpenSourceLock:
845
920
  self._lastOpenSource = {
846
921
  'source': source,
@@ -860,7 +935,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
860
935
  None if the associated image doesn't exist.
861
936
  """
862
937
  if imageKey not in self._associatedImages:
863
- return
938
+ return None
864
939
  source = self._sources[self._associatedImages[imageKey]['sourcenum']]
865
940
  ts = self._openSource(source)
866
941
  return ts.getAssociatedImage(self._associatedImages[imageKey]['key'], *args, **kwargs)
@@ -894,7 +969,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
894
969
  """
895
970
  Return additional known metadata about the tile source. Data returned
896
971
  from this method is not guaranteed to be in any particular format or
897
- have specific values.
972
+ have specific values. Also, only the first 100 sources are used.
898
973
 
899
974
  :returns: a dictionary of data or None.
900
975
  """
@@ -903,7 +978,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
903
978
  'sources': copy.deepcopy(self._sources),
904
979
  'sourceFiles': [],
905
980
  }
906
- for path in self._sourcePaths.values():
981
+ for path in list(self._sourcePaths.values())[:100]:
907
982
  source = self._sources[min(path['sourcenum'])]
908
983
  ts = self._openSource(source)
909
984
  result['sourceFiles'].append({
@@ -937,38 +1012,39 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
937
1012
  (tile.shape[0] + y - base.shape[0], base.shape[1], base.shape[2]),
938
1013
  dtype=base.dtype)
939
1014
  if base.shape[2] in {2, 4}:
940
- vfill[:, :, -1] = fullAlphaValue(self.dtype)
1015
+ vfill[:, :, -1] = fullAlphaValue(base.dtype)
941
1016
  base = np.vstack((base, vfill))
942
1017
  if base.shape[1] < tile.shape[1] + x:
943
1018
  hfill = np.zeros(
944
1019
  (base.shape[0], tile.shape[1] + x - base.shape[1], base.shape[2]),
945
1020
  dtype=base.dtype)
946
1021
  if base.shape[2] in {2, 4}:
947
- hfill[:, :, -1] = fullAlphaValue(self.dtype)
1022
+ hfill[:, :, -1] = fullAlphaValue(base.dtype)
948
1023
  base = np.hstack((base, hfill))
949
1024
  if base.flags.writeable is False:
950
1025
  base = base.copy()
951
1026
  if base.shape[2] in {2, 4}:
952
1027
  baseA = base[y:y + tile.shape[0], x:x + tile.shape[1], -1].astype(
953
- float) / fullAlphaValue(self.dtype)
954
- tileA = tile[:, :, -1].astype(float) / fullAlphaValue(self.dtype)
1028
+ float) / fullAlphaValue(base.dtype)
1029
+ tileA = tile[:, :, -1].astype(float) / fullAlphaValue(tile.dtype)
955
1030
  outA = tileA + baseA * (1 - tileA)
956
1031
  base[y:y + tile.shape[0], x:x + tile.shape[1], :-1] = (
957
- np.where(tileA[..., np.newaxis], tile[:, :, :-1], 0) +
1032
+ np.where(tileA[..., np.newaxis], tile[:, :, :-1] * tileA[..., np.newaxis], 0) +
958
1033
  base[y:y + tile.shape[0], x:x + tile.shape[1], :-1] * baseA[..., np.newaxis] *
959
1034
  (1 - tileA[..., np.newaxis])
960
1035
  ) / np.where(outA[..., np.newaxis], outA[..., np.newaxis], 1)
961
- base[y:y + tile.shape[0], x:x + tile.shape[1], -1] = outA * fullAlphaValue(self.dtype)
1036
+ base[y:y + tile.shape[0], x:x + tile.shape[1], -1] = outA * fullAlphaValue(base.dtype)
962
1037
  else:
963
1038
  base[y:y + tile.shape[0], x:x + tile.shape[1], :] = tile
964
1039
  return base
965
1040
 
966
- def _getTransformedTile(self, ts, transform, corners, scale, frame, crop=None):
1041
+ def _getTransformedTile(self, ts, transform, corners, scale, frame,
1042
+ crop=None, firstMerge=False):
967
1043
  """
968
1044
  Determine where the target tile's corners are located on the source.
969
1045
  Fetch that so that we have at least sqrt(2) more resolution, then use
970
- scikit-image warp to transform it. scikit-image does a better job than
971
- scipy.ndimage.affine_transform.
1046
+ scikit-image warp to transform it. scikit-image does a better and
1047
+ faster job than scipy.ndimage.affine_transform.
972
1048
 
973
1049
  :param ts: the source of the image to transform.
974
1050
  :param transform: a 3x3 affine 2d matrix for transforming the source
@@ -980,6 +1056,9 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
980
1056
  :param crop: an optional dictionary to crop the source image in full
981
1057
  resolution, untransformed coordinates. This may contain left, top,
982
1058
  right, and bottom values in pixels.
1059
+ :param firstMerge: if False and using an alpha channel, transform
1060
+ with nearest neighbor rather than a higher order function to
1061
+ avoid transparency effects.
983
1062
  :returns: a numpy array tile or None, x, y coordinates within the
984
1063
  target tile for the placement of the numpy tile array.
985
1064
  """
@@ -1010,7 +1089,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1010
1089
  # we only need every 1/srcscale pixel.
1011
1090
  srcscale = int(2 ** math.log2(max(1, srcscale)))
1012
1091
  # Pad to reduce edge effects at tile boundaries
1013
- border = int(math.ceil(2 * srcscale))
1092
+ border = int(math.ceil(4 * srcscale))
1014
1093
  region = {
1015
1094
  'left': int(max(0, minx - border) // srcscale) * srcscale,
1016
1095
  'top': int(max(0, miny - border) // srcscale) * srcscale,
@@ -1057,28 +1136,38 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1057
1136
  x, y = int(math.floor(x)), int(math.floor(y))
1058
1137
  # Recompute where the source corners will land
1059
1138
  destcorners = (np.dot(transform, regioncorners.T).T).tolist()
1060
- destsize = (max(math.ceil(c[0]) for c in destcorners),
1061
- max(math.ceil(c[1]) for c in destcorners))
1062
- # Add an alpha band if needed
1139
+ destShape = [
1140
+ max(max(math.ceil(c[1]) for c in destcorners), srcImage.shape[0]),
1141
+ max(max(math.ceil(c[0]) for c in destcorners), srcImage.shape[1]),
1142
+ ]
1143
+ if max(0, -x) or max(0, -y):
1144
+ transform[0][2] -= max(0, -x)
1145
+ transform[1][2] -= max(0, -y)
1146
+ destShape[0] -= max(0, -y)
1147
+ destShape[1] -= max(0, -x)
1148
+ x += max(0, -x)
1149
+ y += max(0, -y)
1150
+ destShape = [min(destShape[0], outh - y), min(destShape[1], outw - x)]
1151
+ if destShape[0] <= 0 or destShape[1] <= 0:
1152
+ return None, None, None
1153
+ # Add an alpha band if needed. This has to be done before the
1154
+ # transform if it isn't the first tile, since the unused transformed
1155
+ # areas need to have a zero alpha value
1063
1156
  if srcImage.shape[2] in {1, 3}:
1064
1157
  _, srcImage = _makeSameChannelDepth(np.zeros((1, 1, srcImage.shape[2] + 1)), srcImage)
1065
- # Add enough space to warp the source image in place
1066
- srcImage = np.pad(srcImage, (
1067
- (0, max(0, destsize[1] - srcImage.shape[0])),
1068
- (0, max(0, destsize[0] - srcImage.shape[1])),
1069
- (0, 0)), mode='constant')
1158
+ useNearest = srcImage.shape[2] in {2, 4} and not firstMerge
1159
+ # skimage.transform.warp is faster and has less artifacts than
1160
+ # scipy.ndimage.affine_transform. It is faster than using cupy's
1161
+ # version of scipy's affine_transform when the source and destination
1162
+ # images are converted from numpy to cupy and back in this method.
1070
1163
  destImage = skimage.transform.warp(
1164
+ # Although using np.float32 could reduce memory use, it doesn't
1165
+ # provide any speed improvement
1071
1166
  srcImage.astype(float),
1072
- skimage.transform.AffineTransform(np.linalg.inv(
1073
- transform))).astype(srcImage.dtype)
1074
- # Trim to target size and location
1075
- destImage = destImage[max(0, -y):, max(0, -x):, :]
1076
- x += max(0, -x)
1077
- y += max(0, -y)
1078
- if x >= outw or y >= outh:
1079
- return None, None, None
1080
- destImage = destImage[:min(destImage.shape[0], outh - y),
1081
- :min(destImage.shape[1], outw - x), :]
1167
+ skimage.transform.AffineTransform(np.linalg.inv(transform)),
1168
+ order=0 if useNearest else 3,
1169
+ output_shape=(destShape[0], destShape[1], srcImage.shape[2]),
1170
+ ).astype(srcImage.dtype)
1082
1171
  return destImage, x, y
1083
1172
 
1084
1173
  def _addSourceToTile(self, tile, sourceEntry, corners, scale):
@@ -1098,12 +1187,12 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1098
1187
  :returns: a numpy array of the tile.
1099
1188
  """
1100
1189
  source = self._sources[sourceEntry['sourcenum']]
1101
- ts = self._openSource(source, sourceEntry['kwargs'])
1102
1190
  # If tile is outside of bounding box, skip it
1103
1191
  bbox = source['bbox']
1104
1192
  if (corners[2][0] <= bbox['left'] or corners[0][0] >= bbox['right'] or
1105
1193
  corners[2][1] <= bbox['top'] or corners[0][1] >= bbox['bottom']):
1106
1194
  return tile
1195
+ ts = self._openSource(source, sourceEntry['kwargs'])
1107
1196
  transform = bbox.get('transform')
1108
1197
  x = y = 0
1109
1198
  # If there is no transform or the diagonals are positive and there is
@@ -1153,8 +1242,23 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1153
1242
  else:
1154
1243
  sourceTile, x, y = self._getTransformedTile(
1155
1244
  ts, transform, corners, scale, sourceEntry.get('frame', 0),
1156
- source.get('position', {}).get('crop'))
1245
+ source.get('position', {}).get('crop'),
1246
+ firstMerge=tile is None)
1157
1247
  if sourceTile is not None and all(dim > 0 for dim in sourceTile.shape):
1248
+ targetDtype = np.dtype(self._info.get('dtype', ts.dtype))
1249
+ changeDtype = sourceTile.dtype != targetDtype
1250
+ if source.get('sampleScale') or source.get('sampleOffset'):
1251
+ sourceTile = sourceTile.astype(float)
1252
+ if source.get('sampleOffset'):
1253
+ sourceTile[:, :, :-1] += source['sampleOffset']
1254
+ if source.get('sampleScale') and source.get('sampleScale') != 1:
1255
+ sourceTile[:, :, :-1] /= source['sampleScale']
1256
+ if sourceTile.dtype != targetDtype:
1257
+ if changeDtype:
1258
+ sourceTile = (
1259
+ sourceTile.astype(float) * fullAlphaValue(targetDtype) /
1260
+ fullAlphaValue(sourceTile))
1261
+ sourceTile = sourceTile.astype(targetDtype)
1158
1262
  tile = self._mergeTiles(tile, sourceTile, x, y)
1159
1263
  return tile
1160
1264
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: large-image-source-multi
3
- Version: 1.27.5.dev4
3
+ Version: 1.30.7.dev10
4
4
  Summary: A tilesource for large_image to composite other tile sources
5
5
  Home-page: https://github.com/girder/large_image
6
6
  Author: Kitware, Inc.
@@ -15,10 +15,17 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
- Requires-Python: >=3.6
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/x-rst
21
+ License-File: LICENSE
22
+ Requires-Dist: jsonschema
23
+ Requires-Dist: large-image>=1.30.7.dev10
24
+ Requires-Dist: pyyaml
19
25
  Provides-Extra: all
26
+ Requires-Dist: scikit-image; extra == "all"
20
27
  Provides-Extra: girder
21
- License-File: LICENSE
28
+ Requires-Dist: girder-large-image>=1.30.7.dev10; extra == "girder"
22
29
 
23
30
  A tilesource for large_image to composite other tile sources
24
31
 
@@ -0,0 +1,9 @@
1
+ jsonschema
2
+ large-image>=1.30.7.dev10
3
+ pyyaml
4
+
5
+ [all]
6
+ scikit-image
7
+
8
+ [girder]
9
+ girder-large-image>=1.30.7.dev10
@@ -19,8 +19,7 @@ def prerelease_local_scheme(version):
19
19
 
20
20
  if os.getenv('CIRCLE_BRANCH') in ('master', ):
21
21
  return ''
22
- else:
23
- return get_local_node_and_date(version)
22
+ return get_local_node_and_date(version)
24
23
 
25
24
 
26
25
  try:
@@ -37,6 +36,7 @@ setup(
37
36
  'fallback_version': '0.0.0'},
38
37
  description=description,
39
38
  long_description=long_description,
39
+ long_description_content_type='text/x-rst',
40
40
  license='Apache Software License 2.0',
41
41
  author='Kitware, Inc.',
42
42
  author_email='kitware@kitware.com',
@@ -49,6 +49,7 @@ setup(
49
49
  'Programming Language :: Python :: 3.10',
50
50
  'Programming Language :: Python :: 3.11',
51
51
  'Programming Language :: Python :: 3.12',
52
+ 'Programming Language :: Python :: 3.13',
52
53
  ],
53
54
  install_requires=[
54
55
  'jsonschema',
@@ -64,7 +65,7 @@ setup(
64
65
  keywords='large_image, tile source',
65
66
  packages=find_packages(exclude=['test', 'test.*']),
66
67
  url='https://github.com/girder/large_image',
67
- python_requires='>=3.6',
68
+ python_requires='>=3.8',
68
69
  entry_points={
69
70
  'large_image.source': [
70
71
  'multi = large_image_source_multi:MultiFileTileSource',
@@ -1,9 +0,0 @@
1
- jsonschema
2
- large-image>=1.27.5.dev4
3
- pyyaml
4
-
5
- [all]
6
- scikit-image
7
-
8
- [girder]
9
- girder-large-image>=1.27.5.dev4