large-image-source-multi 1.30.1.dev16__tar.gz → 1.33.6.dev6__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.30.1.dev16/large_image_source_multi.egg-info → large_image_source_multi-1.33.6.dev6}/PKG-INFO +19 -8
  2. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/README.rst +20 -20
  3. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/large_image_source_multi/__init__.py +286 -64
  4. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6/large_image_source_multi.egg-info}/PKG-INFO +19 -8
  5. large_image_source_multi-1.33.6.dev6/large_image_source_multi.egg-info/requires.txt +9 -0
  6. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/pyproject.toml +4 -0
  7. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/setup.py +3 -24
  8. large_image_source_multi-1.30.1.dev16/large_image_source_multi.egg-info/requires.txt +0 -9
  9. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/LICENSE +0 -0
  10. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/docs/specification.rst +0 -0
  11. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/large_image_source_multi/girder_source.py +0 -0
  12. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/large_image_source_multi.egg-info/SOURCES.txt +0 -0
  13. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/large_image_source_multi.egg-info/dependency_links.txt +0 -0
  14. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/large_image_source_multi.egg-info/entry_points.txt +0 -0
  15. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/large_image_source_multi.egg-info/top_level.txt +0 -0
  16. {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/setup.cfg +0 -0
@@ -1,31 +1,42 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: large-image-source-multi
3
- Version: 1.30.1.dev16
3
+ Version: 1.33.6.dev6
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.
7
7
  Author-email: kitware@kitware.com
8
- License: Apache Software License 2.0
8
+ License: Apache-2.0
9
9
  Keywords: large_image,tile source
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
- Classifier: License :: OSI Approved :: Apache Software License
12
11
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
12
  Classifier: Programming Language :: Python :: 3.9
15
13
  Classifier: Programming Language :: Python :: 3.10
16
14
  Classifier: Programming Language :: Python :: 3.11
17
15
  Classifier: Programming Language :: Python :: 3.12
18
16
  Classifier: Programming Language :: Python :: 3.13
19
- Requires-Python: >=3.6
17
+ Requires-Python: >=3.9
20
18
  Description-Content-Type: text/x-rst
21
19
  License-File: LICENSE
22
20
  Requires-Dist: jsonschema
23
- Requires-Dist: large-image>=1.30.1.dev16
21
+ Requires-Dist: large-image>=1.33.6.dev6
24
22
  Requires-Dist: pyyaml
25
23
  Provides-Extra: all
26
24
  Requires-Dist: scikit-image; extra == "all"
27
25
  Provides-Extra: girder
28
- Requires-Dist: girder-large-image>=1.30.1.dev16; extra == "girder"
26
+ Requires-Dist: girder-large-image>=1.33.6.dev6; extra == "girder"
27
+ Dynamic: author
28
+ Dynamic: author-email
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: keywords
34
+ Dynamic: license
35
+ Dynamic: license-file
36
+ Dynamic: provides-extra
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
29
40
 
30
41
  A tilesource for large_image to composite other tile sources
31
42
 
@@ -1,25 +1,23 @@
1
1
  Large Image
2
2
  ===========
3
3
 
4
- |build-status| |codecov-io| |license-badge| |doi-badge| |pypi-badge|
4
+ .. image:: https://img.shields.io/circleci/build/github/girder/large_image.svg
5
+ :target: https://circleci.com/gh/girder/large_image
6
+ :alt: Build Status
5
7
 
6
- .. |build-status| image:: https://img.shields.io/circleci/build/github/girder/large_image.svg
7
- :target: https://circleci.com/gh/girder/large_image
8
- :alt: Build Status
8
+ .. image:: https://img.shields.io/badge/license-Apache%202-blue.svg
9
+ :target: https://raw.githubusercontent.com/girder/large_image/master/LICENSE
10
+ :alt: License
9
11
 
10
- .. |license-badge| image:: https://img.shields.io/badge/license-Apache%202-blue.svg
11
- :target: https://raw.githubusercontent.com/girder/large_image/master/LICENSE
12
- :alt: License
12
+ .. image:: https://img.shields.io/codecov/c/github/girder/large_image.svg
13
+ :target: https://codecov.io/github/girder/large_image?branch=master
14
+ :alt: codecov.io
13
15
 
14
- .. |codecov-io| image:: https://img.shields.io/codecov/c/github/girder/large_image.svg
15
- :target: https://codecov.io/github/girder/large_image?branch=master
16
- :alt: codecov.io
16
+ .. image:: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.4562625-blue.svg
17
+ :target: https://doi.org/10.5281/zenodo.4562625
17
18
 
18
- .. |doi-badge| image:: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.4723355-blue.svg
19
- :target: https://zenodo.org/badge/latestdoi/45569214
20
-
21
- .. |pypi-badge| image:: https://img.shields.io/pypi/v/large-image.svg?logo=python&logoColor=white
22
- :target: https://pypi.org/project/large-image/
19
+ .. image:: https://img.shields.io/pypi/v/large-image.svg?logo=python&logoColor=white
20
+ :target: https://pypi.org/project/large-image/
23
21
 
24
22
  *Python modules to work with large, multiresolution images.*
25
23
 
@@ -156,13 +154,15 @@ There is also cache management to balance memory use and speed of response in Gi
156
154
 
157
155
  Most tile sources can be used with Girder Large Image. You can specify an extras_require of ``girder`` to install the following packages:
158
156
 
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.
157
+ - ``girder-large-image``: Large Image as a Girder 3.x plugin.
158
+
159
+ You can install ``large-image[tasks]`` to install a Girder Worker task that can convert otherwise unreadable images to pyramidal tiff files.
160
+
161
+ - ``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.
161
162
 
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.
163
+ - ``large-image-tasks``: A utility for running the converter via Girder Worker.
163
164
 
164
- - ``large-image-tasks``: A utility for running the converter via Girder Worker.
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.
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.
166
166
 
167
167
 
168
168
 
@@ -6,6 +6,7 @@ import math
6
6
  import os
7
7
  import re
8
8
  import threading
9
+ import warnings
9
10
  from importlib.metadata import PackageNotFoundError
10
11
  from importlib.metadata import version as _importlib_version
11
12
  from pathlib import Path
@@ -28,6 +29,7 @@ except PackageNotFoundError:
28
29
 
29
30
  jsonschema = None
30
31
  _validator = None
32
+ skimage_transform = None
31
33
 
32
34
 
33
35
  def _lazyImport():
@@ -47,6 +49,20 @@ def _lazyImport():
47
49
  raise TileSourceError(msg)
48
50
 
49
51
 
52
+ def _lazyImportSkimageTransform():
53
+ """
54
+ Import the skimage.transform module. This is only needed when a TPS warp is used.
55
+ """
56
+ global skimage_transform
57
+
58
+ if skimage_transform is None:
59
+ try:
60
+ import skimage.transform as skimage_transform
61
+ except ImportError:
62
+ msg = 'scikit-image transform module not found.'
63
+ raise TileSourceError(msg)
64
+
65
+
50
66
  SourceEntrySchema = {
51
67
  'type': 'object',
52
68
  'additionalProperties': True,
@@ -270,6 +286,54 @@ SourceEntrySchema = {
270
286
  # TODO: Add polygon option
271
287
  # TODO: Add postTransform option
272
288
  },
289
+ 'warp': {
290
+ 'description':
291
+ 'An object describing a series of landmarks which have both '
292
+ 'a source location and a destination location. These sets of points '
293
+ 'define a warp (thin plate spline or affine transform) that will '
294
+ 'be applied to the source image.',
295
+ 'type': 'object',
296
+ 'properties': {
297
+ 'src': {
298
+ 'description':
299
+ 'The set of source locations for landmarks defining a warp. '
300
+ 'This can be described by a list of [x, y] points or a mapping of '
301
+ 'unique marker IDs to [x, y] points.',
302
+ 'type': ['array', 'object'],
303
+ 'items': {
304
+ 'type': 'array',
305
+ 'items': {'type': 'number'},
306
+ 'minItems': 2,
307
+ 'maxItems': 2,
308
+ },
309
+ 'additionalProperties': {
310
+ 'type': 'array',
311
+ 'items': {'type': 'number'},
312
+ 'minItems': 2,
313
+ 'maxItems': 2,
314
+ },
315
+ },
316
+ 'dst': {
317
+ 'description':
318
+ 'The set of destination locations for landmarks defining a warp. '
319
+ 'This can be described by a list of [x, y] points or a mapping of '
320
+ 'unique marker IDs to [x, y] points.',
321
+ 'type': ['array', 'object'],
322
+ 'items': {
323
+ 'type': 'array',
324
+ 'items': {'type': 'number'},
325
+ 'minItems': 2,
326
+ 'maxItems': 2,
327
+ },
328
+ 'additionalProperties': {
329
+ 'type': 'array',
330
+ 'items': {'type': 'number'},
331
+ 'minItems': 2,
332
+ 'maxItems': 2,
333
+ },
334
+ },
335
+ },
336
+ },
273
337
  'scale': {
274
338
  'description':
275
339
  'Values less than 1 will downsample the source. '
@@ -300,10 +364,12 @@ SourceEntrySchema = {
300
364
  'sampleScale is applied',
301
365
  'type': 'number',
302
366
  },
303
- 'style': {'type': 'object'},
367
+ 'style': {
368
+ 'description': 'A style specification to pass to the base tile source',
369
+ 'type': 'object',
370
+ },
304
371
  'params': {
305
- 'description':
306
- 'Additional parameters to pass to the base tile source',
372
+ 'description': 'Additional parameters to pass to the base tile source',
307
373
  'type': 'object',
308
374
  },
309
375
  },
@@ -440,9 +506,12 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
440
506
  self._largeImagePath = '.'
441
507
  try:
442
508
  self._validator.validate(self._info)
443
- except jsonschema.ValidationError:
444
- msg = 'File cannot be validated via multi-source reader.'
445
- raise TileSourceError(msg)
509
+ except jsonschema.ValidationError as exp:
510
+ from large_image.exceptions import _improveJsonschemaValidationError
511
+
512
+ _improveJsonschemaValidationError(exp)
513
+ msg = f'File cannot be validated via multi-source reader {str(exp)}.'
514
+ raise TileSourceError(msg) from None
446
515
  elif not os.path.isfile(self._largeImagePath):
447
516
  try:
448
517
  possibleYaml = self._largeImagePath.split('multi://', 1)[-1]
@@ -455,7 +524,8 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
455
524
  try:
456
525
  with builtins.open(self._largeImagePath) as fptr:
457
526
  start = fptr.read(1024).strip()
458
- if start[:1] not in ('{', '#', '-') and (start[:1] < 'a' or start[:1] > 'z'):
527
+ if (start[:1] not in ('{', '#', '-') and
528
+ (start[:1] < 'a' or start[:1] > 'z')) or 'FeatureCollection' in start:
459
529
  msg = 'File cannot be opened via multi-source reader.'
460
530
  raise TileSourceError(msg)
461
531
  fptr.seek(0)
@@ -470,9 +540,12 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
470
540
  raise TileSourceError(msg)
471
541
  try:
472
542
  self._validator.validate(self._info)
473
- except jsonschema.ValidationError:
474
- msg = 'File cannot be validated via multi-source reader.'
475
- raise TileSourceError(msg)
543
+ except jsonschema.ValidationError as exp:
544
+ from large_image.exceptions import _improveJsonschemaValidationError
545
+
546
+ _improveJsonschemaValidationError(exp)
547
+ msg = f'File cannot be validated via multi-source reader {str(exp)}.'
548
+ raise TileSourceError(msg) from None
476
549
  self._basePath = Path(self._largeImagePath).parent
477
550
  self._basePath /= Path(self._info.get('basePath', '.'))
478
551
  for axis in self._info.get('axes', []):
@@ -571,6 +644,62 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
571
644
  source['path'] = source['path'].resolve(False)
572
645
  return sources
573
646
 
647
+ def _getWarp(self, warp, m):
648
+ """
649
+ Preprocess a warp specification and transformation matrix prior to applying the warp.
650
+
651
+ :param warp: A dictionary with a warp specification,
652
+ adhering to the schema for ``MultiSource.SourceEntry.position.warp``.
653
+ :param m: An ndarray representing a transformation matrix,
654
+ that may be modified according to the warp specification.
655
+ """
656
+ ret = None
657
+ warp_src = warp.get('src')
658
+ warp_dst = warp.get('dst')
659
+ if isinstance(warp_src, dict) and isinstance(warp_dst, dict):
660
+ src_key_set = set(warp_src.keys())
661
+ dst_key_set = set(warp_dst.keys())
662
+ keys = list(src_key_set & dst_key_set)
663
+ warp_src = [warp_src[key] for key in keys]
664
+ warp_dst = [warp_dst[key] for key in keys]
665
+ unused = (src_key_set | dst_key_set) - set(keys)
666
+ if len(unused):
667
+ msg = (
668
+ 'The following keys did not have a value in both src and dst, '
669
+ f'so they were dropped: {unused}.'
670
+ )
671
+ warnings.warn(msg, stacklevel=2)
672
+ elif isinstance(warp_src, list) and isinstance(warp_dst, list):
673
+ pass
674
+ else:
675
+ msg = 'warp src and warp dst must either be both dicts or both lists.'
676
+ raise TileSourceError(msg)
677
+ if len(warp_src) != len(warp_dst):
678
+ msg = 'warp src and warp dst must have the same number of points.'
679
+ raise TileSourceError(msg)
680
+ if len(warp_src) < 1:
681
+ msg = 'warp src and warp dst must have at least one point.'
682
+ raise TileSourceError(msg)
683
+
684
+ warp_src = np.array(warp_src or []).astype(float)
685
+ warp_dst = np.array(warp_dst or []).astype(float)
686
+ warp_src = warp_src[:min(warp_src.shape[0], warp_dst.shape[0]), :]
687
+ warp_dst = warp_dst[:warp_src.shape[0], :]
688
+ if warp_src.shape[0] < 1:
689
+ pass
690
+ elif warp_src.shape[0] == 1:
691
+ m[0][2] += warp_dst[0][0] - warp_src[0][0]
692
+ m[1][2] += warp_dst[0][1] - warp_src[0][1]
693
+ elif warp_src.shape[0] <= 3:
694
+ transformer = skimage_transform.AffineTransform()
695
+ transformer.estimate(warp_src, warp_dst)
696
+ m = np.dot(transformer.params, m)
697
+ else:
698
+ warp_dst = np.dot(m, np.hstack([warp_dst, np.ones((len(warp_dst), 1))]).T).T[:, :2]
699
+ ret = {'src': warp_src, 'dst': warp_dst}
700
+ m = np.identity(3)
701
+ return ret, m
702
+
574
703
  def _sourceBoundingBox(self, source, width, height):
575
704
  """
576
705
  Given a source with a possible transform and an image width and height,
@@ -588,6 +717,8 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
588
717
  bbox = {'left': 0, 'top': 0, 'right': width, 'bottom': height}
589
718
  if not pos:
590
719
  return bbox
720
+ if 'warp' in pos:
721
+ _lazyImportSkimageTransform()
591
722
  x0, y0, x1, y1 = 0, 0, width, height
592
723
  if 'crop' in pos:
593
724
  x0 = min(max(pos['crop'].get('left', x0), 0), width)
@@ -603,6 +734,8 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
603
734
  m[1][0] = pos.get('s21', 0) * pos.get('scale', 1)
604
735
  m[1][1] = pos.get('s22', 1) * pos.get('scale', 1)
605
736
  m[1][2] = pos.get('y', 0)
737
+ if 'warp' in pos and skimage_transform is not None:
738
+ bbox['warp'], m = self._getWarp(pos.get('warp'), m)
606
739
  if not np.array_equal(m, np.identity(3)):
607
740
  bbox['transform'] = m
608
741
  try:
@@ -610,11 +743,23 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
610
743
  except np.linalg.LinAlgError:
611
744
  msg = 'The position for a source is not invertable (%r)'
612
745
  raise TileSourceError(msg, pos)
613
- transcorners = np.dot(m, corners.T)
746
+ warp = bbox.get('warp')
747
+ if warp is None:
748
+ transcorners = np.dot(m, corners.T)
749
+ elif skimage_transform is not None:
750
+ transformer = skimage_transform.ThinPlateSplineTransform()
751
+ transformer.estimate(warp.get('src'), warp.get('dst'))
752
+ # We might want to adjust the number of points based on some
753
+ # criteria such as source image size or number of warp points
754
+ corners = self._perimeterPoints(x0, y0, x1, y1, 8)
755
+ transcorners = transformer(corners).T
614
756
  bbox['left'] = min(transcorners[0])
615
757
  bbox['top'] = min(transcorners[1])
616
758
  bbox['right'] = max(transcorners[0])
617
759
  bbox['bottom'] = max(transcorners[1])
760
+ # TODO: Maybe inflate this a bit for warp because of edge effects?
761
+ # That is, it can warp outside of the specified box because we don't
762
+ # have infinite perimeter sampling
618
763
  return bbox
619
764
 
620
765
  def _axisKey(self, source, value, key):
@@ -897,7 +1042,8 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
897
1042
  if params is None:
898
1043
  params = source.get('params', {})
899
1044
  ts = openFunc(source['path'], **params)
900
- if (self._dtype and np.dtype(ts.dtype).kind == 'f' and self._dtype.kind != 'f' and
1045
+ if (self._dtype and np.dtype(ts.dtype).kind == 'f' and
1046
+ (self._dtype == 'check' or np.dtype(self._dtype).kind != 'f') and
901
1047
  'sampleScale' not in source and 'sampleOffset' not in source):
902
1048
  minval = maxval = 0
903
1049
  for f in range(ts.frames):
@@ -931,7 +1077,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
931
1077
  None if the associated image doesn't exist.
932
1078
  """
933
1079
  if imageKey not in self._associatedImages:
934
- return
1080
+ return None
935
1081
  source = self._sources[self._associatedImages[imageKey]['sourcenum']]
936
1082
  ts = self._openSource(source)
937
1083
  return ts.getAssociatedImage(self._associatedImages[imageKey]['key'], *args, **kwargs)
@@ -1034,7 +1180,34 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1034
1180
  base[y:y + tile.shape[0], x:x + tile.shape[1], :] = tile
1035
1181
  return base
1036
1182
 
1037
- def _getTransformedTile(self, ts, transform, corners, scale, frame, crop=None):
1183
+ def _perimeterPoints(self, x0, y0, x1, y1, sample):
1184
+ return np.vstack([
1185
+ np.column_stack([np.linspace(x0, x1, sample, endpoint=False), np.full(sample, y0)]),
1186
+ np.column_stack([np.full(sample, x1), np.linspace(y0, y1, sample, endpoint=False)]),
1187
+ np.column_stack([np.linspace(x1, x0, sample, endpoint=False), np.full(sample, y1)]),
1188
+ np.column_stack([np.full(sample, x0), np.linspace(y1, y0, sample, endpoint=False)]),
1189
+ ])
1190
+
1191
+ def _smallestSpacingRatio(self, srcpts, destpts):
1192
+ """
1193
+ Find the smallest ratio of the distance between two adjacent source
1194
+ perimeter points (srccorners) and two destination perimeter points
1195
+
1196
+ :param srcpts: an ndarray of points that correspond with the
1197
+ destination points.
1198
+ :param dstpts: an ndarray of corresponding points.
1199
+ :returns: the minimum ratio of the distance between a source section
1200
+ and a destination section.
1201
+ """
1202
+ srcdist = np.linalg.norm(srcpts - np.roll(srcpts, 1, axis=0), axis=1)
1203
+ destdist = np.linalg.norm(destpts - np.roll(destpts, 1, axis=0), axis=1)
1204
+ if np.all(destdist == 0):
1205
+ return 1
1206
+ minratio = np.min(srcdist[destdist != 0] / destdist[destdist != 0])
1207
+ return minratio or 1
1208
+
1209
+ def _getTransformedTile(self, ts, transform, corners, scale, frame, # noqa
1210
+ warp=None, crop=None, firstMerge=False):
1038
1211
  """
1039
1212
  Determine where the target tile's corners are located on the source.
1040
1213
  Fetch that so that we have at least sqrt(2) more resolution, then use
@@ -1048,36 +1221,66 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1048
1221
  corner 0 must be the upper left, 2 must be the lower right.
1049
1222
  :param scale: scaling factor from full res to the target resolution.
1050
1223
  :param frame: frame number of the source image.
1224
+ :param warp: an optional dictionary to specify a thin plate spline
1225
+ transformation to apply to the source image. This dictionary must
1226
+ contain keys 'src' and 'dst', each with a list of [x, y] points.
1227
+ The length of the 'src' list of points must equal the length of the
1228
+ 'dst' list of points (both must have at least 3 points).
1051
1229
  :param crop: an optional dictionary to crop the source image in full
1052
1230
  resolution, untransformed coordinates. This may contain left, top,
1053
1231
  right, and bottom values in pixels.
1232
+ :param firstMerge: if False and using an alpha channel, transform
1233
+ with nearest neighbor rather than a higher order function to
1234
+ avoid transparency effects.
1054
1235
  :returns: a numpy array tile or None, x, y coordinates within the
1055
1236
  target tile for the placement of the numpy tile array.
1056
1237
  """
1057
- try:
1058
- import skimage.transform
1059
- except ImportError:
1060
- msg = 'scikit-image is required for affine transforms.'
1061
- raise TileSourceError(msg)
1062
1238
  # From full res source to full res destination
1063
1239
  transform = transform.copy() if transform is not None else np.identity(3)
1240
+ warp_src = warp_dst = None
1241
+ _lazyImportSkimageTransform()
1242
+ if warp is not None:
1243
+ warp_src = warp['src'].copy()
1244
+ warp_dst = warp['dst'].copy()
1064
1245
  # Scale dest corners to actual size; adjust transform for the same
1065
1246
  corners = np.array(corners)
1066
1247
  corners[:, :2] //= scale
1067
- transform[:2, :] /= scale
1068
1248
  # Offset so our target is the actual destination array we use
1069
- transform[0][2] -= corners[0][0]
1070
- transform[1][2] -= corners[0][1]
1249
+ if warp is None:
1250
+ transform[:2, :] /= scale
1251
+ transform[0][2] -= corners[0][0]
1252
+ transform[1][2] -= corners[0][1]
1253
+ else:
1254
+ warp_dst /= scale
1255
+ warp_dst[:, 0] -= corners[0][0]
1256
+ warp_dst[:, 1] -= corners[0][1]
1071
1257
  corners[:, :2] -= corners[0, :2]
1072
1258
  outw, outh = corners[2][0], corners[2][1]
1073
1259
  if not outh or not outw:
1074
1260
  return None, 0, 0
1075
- srccorners = np.dot(np.linalg.inv(transform), np.array(corners).T).T.tolist()
1261
+ if warp is None:
1262
+ dstcorners = corners
1263
+ srccorners = np.dot(np.linalg.inv(transform), np.array(corners).T).T.tolist()
1264
+ else:
1265
+ transformer = skimage_transform.ThinPlateSplineTransform()
1266
+ transformer.estimate(warp_dst, warp_src)
1267
+ # Is there any way to be smarter about the sampling?
1268
+ dstcorners = self._perimeterPoints(0, 0, outw, outh, 8)
1269
+ srccorners = transformer(dstcorners)
1076
1270
  minx = min(c[0] for c in srccorners)
1077
1271
  maxx = max(c[0] for c in srccorners)
1078
1272
  miny = min(c[1] for c in srccorners)
1079
1273
  maxy = max(c[1] for c in srccorners)
1080
- srcscale = max((maxx - minx) / outw, (maxy - miny) / outh)
1274
+ if warp is None:
1275
+ srcscale = max((maxx - minx) / outw, (maxy - miny) / outh)
1276
+ else:
1277
+ # Use the half spacing for better interpolation. If the warped
1278
+ # image were uniform, this could be a factor of sqrt(2), since the
1279
+ # image could be rotated and we might need to interpolate from
1280
+ # that; using a bigger factor can ensure we have enough pixels for
1281
+ # warps that are no more than another factor of sqrt(2) in
1282
+ # variation from the sampled perimeter.
1283
+ srcscale = self._smallestSpacingRatio(srccorners, dstcorners) / 2
1081
1284
  # we only need every 1/srcscale pixel.
1082
1285
  srcscale = int(2 ** math.log2(max(1, srcscale)))
1083
1286
  # Pad to reduce edge effects at tile boundaries
@@ -1109,52 +1312,69 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1109
1312
  [region['right'] // srcscale, region['top'] // srcscale, 1],
1110
1313
  [region['right'] // srcscale, region['bottom'] // srcscale, 1],
1111
1314
  [region['left'] // srcscale, region['bottom'] // srcscale, 1]], dtype=float)
1112
- # adjust our transform if we took a low res version of the source
1113
- transform[:2, :2] *= srcscale
1114
- # Find where the source corners land on the destination.
1115
- preshiftcorners = (np.dot(transform, regioncorners.T).T).tolist()
1116
- regioncorners[:, :2] -= regioncorners[0, :2]
1117
- destcorners = (np.dot(transform, regioncorners.T).T).tolist()
1118
- offsetx, offsety = None, None
1119
- for idx in range(4):
1120
- if offsetx is None or destcorners[idx][0] < offsetx:
1121
- x = preshiftcorners[idx][0]
1122
- offsetx = destcorners[idx][0] - (x - math.floor(x))
1123
- if offsety is None or destcorners[idx][1] < offsety:
1124
- y = preshiftcorners[idx][1]
1125
- offsety = destcorners[idx][1] - (y - math.floor(y))
1126
- transform[0][2] -= offsetx
1127
- transform[1][2] -= offsety
1128
- x, y = int(math.floor(x)), int(math.floor(y))
1129
- # Recompute where the source corners will land
1130
- destcorners = (np.dot(transform, regioncorners.T).T).tolist()
1131
- destShape = [
1132
- max(max(math.ceil(c[1]) for c in destcorners), srcImage.shape[0]),
1133
- max(max(math.ceil(c[0]) for c in destcorners), srcImage.shape[1]),
1134
- ]
1135
- if max(0, -x) or max(0, -y):
1136
- transform[0][2] -= max(0, -x)
1137
- transform[1][2] -= max(0, -y)
1138
- destShape[0] -= max(0, -y)
1139
- destShape[1] -= max(0, -x)
1140
- x += max(0, -x)
1141
- y += max(0, -y)
1142
- destShape = [min(destShape[0], outh - y), min(destShape[1], outw - x)]
1143
- if destShape[0] <= 0 or destShape[1] <= 0:
1144
- return None, None, None
1145
- # Add an alpha band if needed
1315
+ if warp is None:
1316
+ # adjust our transform if we took a low res version of the source
1317
+ transform[:2, :2] *= srcscale
1318
+ else:
1319
+ warp_src /= srcscale
1320
+ warp_src -= regioncorners[0, :2]
1321
+ if warp is None:
1322
+ # Find where the source corners land on the destination.
1323
+ preshiftcorners = (np.dot(transform, regioncorners.T).T).tolist()
1324
+ regioncorners[:, :2] -= regioncorners[0, :2]
1325
+ destcorners = (np.dot(transform, regioncorners.T).T).tolist()
1326
+ offsetx, offsety = None, None
1327
+ for idx in range(4):
1328
+ if offsetx is None or destcorners[idx][0] < offsetx:
1329
+ x = preshiftcorners[idx][0]
1330
+ offsetx = destcorners[idx][0] - (x - math.floor(x))
1331
+ if offsety is None or destcorners[idx][1] < offsety:
1332
+ y = preshiftcorners[idx][1]
1333
+ offsety = destcorners[idx][1] - (y - math.floor(y))
1334
+ transform[0][2] -= offsetx
1335
+ transform[1][2] -= offsety
1336
+ x, y = int(math.floor(x)), int(math.floor(y))
1337
+ # Recompute where the source corners will land
1338
+ destcorners = (np.dot(transform, regioncorners.T).T).tolist()
1339
+ destShape = [
1340
+ max(max(math.ceil(c[1]) for c in destcorners), srcImage.shape[0]),
1341
+ max(max(math.ceil(c[0]) for c in destcorners), srcImage.shape[1]),
1342
+ ]
1343
+ if max(0, -x) or max(0, -y):
1344
+ transform[0][2] -= max(0, -x)
1345
+ transform[1][2] -= max(0, -y)
1346
+ destShape[0] -= max(0, -y)
1347
+ destShape[1] -= max(0, -x)
1348
+ x += max(0, -x)
1349
+ y += max(0, -y)
1350
+ destShape = [min(destShape[0], outh - y), min(destShape[1], outw - x)]
1351
+ if destShape[0] <= 0 or destShape[1] <= 0:
1352
+ return None, None, None
1353
+ else:
1354
+ x = y = 0
1355
+ destShape = [outh, outw]
1356
+ # Add an alpha band if needed. This has to be done before the
1357
+ # transform if it isn't the first tile, since the unused transformed
1358
+ # areas need to have a zero alpha value
1146
1359
  if srcImage.shape[2] in {1, 3}:
1147
1360
  _, srcImage = _makeSameChannelDepth(np.zeros((1, 1, srcImage.shape[2] + 1)), srcImage)
1361
+ useNearest = srcImage.shape[2] in {2, 4} and not firstMerge
1362
+
1363
+ if warp is None:
1364
+ transformer = skimage_transform.AffineTransform(np.linalg.inv(transform))
1365
+ else:
1366
+ transformer = skimage_transform.ThinPlateSplineTransform()
1367
+ transformer.estimate(warp_dst, warp_src)
1148
1368
  # skimage.transform.warp is faster and has less artifacts than
1149
1369
  # scipy.ndimage.affine_transform. It is faster than using cupy's
1150
1370
  # version of scipy's affine_transform when the source and destination
1151
1371
  # images are converted from numpy to cupy and back in this method.
1152
- destImage = skimage.transform.warp(
1372
+ destImage = skimage_transform.warp(
1153
1373
  # Although using np.float32 could reduce memory use, it doesn't
1154
1374
  # provide any speed improvement
1155
1375
  srcImage.astype(float),
1156
- skimage.transform.AffineTransform(np.linalg.inv(transform)),
1157
- order=3,
1376
+ transformer,
1377
+ order=0 if useNearest else 3,
1158
1378
  output_shape=(destShape[0], destShape[1], srcImage.shape[2]),
1159
1379
  ).astype(srcImage.dtype)
1160
1380
  return destImage, x, y
@@ -1183,13 +1403,14 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1183
1403
  return tile
1184
1404
  ts = self._openSource(source, sourceEntry['kwargs'])
1185
1405
  transform = bbox.get('transform')
1406
+ warp = bbox.get('warp')
1186
1407
  x = y = 0
1187
1408
  # If there is no transform or the diagonals are positive and there is
1188
1409
  # no sheer and integer pixel alignment, use getRegion with an
1189
1410
  # appropriate size
1190
1411
  scaleX = transform[0][0] if transform is not None else 1
1191
1412
  scaleY = transform[1][1] if transform is not None else 1
1192
- if ((transform is None or (
1413
+ if (warp is None and (transform is None or (
1193
1414
  transform[0][0] > 0 and transform[0][1] == 0 and
1194
1415
  transform[1][0] == 0 and transform[1][1] > 0 and
1195
1416
  transform[0][2] % scaleX == 0 and transform[1][2] % scaleY == 0)) and
@@ -1230,8 +1451,9 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1230
1451
  resample=None, format=TILE_FORMAT_NUMPY)
1231
1452
  else:
1232
1453
  sourceTile, x, y = self._getTransformedTile(
1233
- ts, transform, corners, scale, sourceEntry.get('frame', 0),
1234
- source.get('position', {}).get('crop'))
1454
+ ts, transform, corners, scale, sourceEntry.get('frame', 0), warp,
1455
+ source.get('position', {}).get('crop'),
1456
+ firstMerge=tile is None)
1235
1457
  if sourceTile is not None and all(dim > 0 for dim in sourceTile.shape):
1236
1458
  targetDtype = np.dtype(self._info.get('dtype', ts.dtype))
1237
1459
  changeDtype = sourceTile.dtype != targetDtype
@@ -1,31 +1,42 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: large-image-source-multi
3
- Version: 1.30.1.dev16
3
+ Version: 1.33.6.dev6
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.
7
7
  Author-email: kitware@kitware.com
8
- License: Apache Software License 2.0
8
+ License: Apache-2.0
9
9
  Keywords: large_image,tile source
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
- Classifier: License :: OSI Approved :: Apache Software License
12
11
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.8
14
12
  Classifier: Programming Language :: Python :: 3.9
15
13
  Classifier: Programming Language :: Python :: 3.10
16
14
  Classifier: Programming Language :: Python :: 3.11
17
15
  Classifier: Programming Language :: Python :: 3.12
18
16
  Classifier: Programming Language :: Python :: 3.13
19
- Requires-Python: >=3.6
17
+ Requires-Python: >=3.9
20
18
  Description-Content-Type: text/x-rst
21
19
  License-File: LICENSE
22
20
  Requires-Dist: jsonschema
23
- Requires-Dist: large-image>=1.30.1.dev16
21
+ Requires-Dist: large-image>=1.33.6.dev6
24
22
  Requires-Dist: pyyaml
25
23
  Provides-Extra: all
26
24
  Requires-Dist: scikit-image; extra == "all"
27
25
  Provides-Extra: girder
28
- Requires-Dist: girder-large-image>=1.30.1.dev16; extra == "girder"
26
+ Requires-Dist: girder-large-image>=1.33.6.dev6; extra == "girder"
27
+ Dynamic: author
28
+ Dynamic: author-email
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: keywords
34
+ Dynamic: license
35
+ Dynamic: license-file
36
+ Dynamic: provides-extra
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
29
40
 
30
41
  A tilesource for large_image to composite other tile sources
31
42
 
@@ -0,0 +1,9 @@
1
+ jsonschema
2
+ large-image>=1.33.6.dev6
3
+ pyyaml
4
+
5
+ [all]
6
+ scikit-image
7
+
8
+ [girder]
9
+ girder-large-image>=1.33.6.dev6
@@ -1,3 +1,7 @@
1
1
  [build-system]
2
2
  requires = ["setuptools", "setuptools-scm"]
3
3
  build-backend = "setuptools.build_meta"
4
+
5
+ [tool.setuptools_scm]
6
+ fallback_version = "0.0.0"
7
+ root = "../.."
@@ -6,52 +6,32 @@ description = 'A tilesource for large_image to composite other tile sources'
6
6
  long_description = description + '\n\nSee the large-image package for more details.'
7
7
 
8
8
 
9
- def prerelease_local_scheme(version):
10
- """
11
- Return local scheme version unless building on master in CircleCI.
12
-
13
- This function returns the local scheme version number
14
- (e.g. 0.0.0.dev<N>+g<HASH>) unless building on CircleCI for a
15
- pre-release in which case it ignores the hash and produces a
16
- PEP440 compliant pre-release version number (e.g. 0.0.0.dev<N>).
17
- """
18
- from setuptools_scm.version import get_local_node_and_date
19
-
20
- if os.getenv('CIRCLE_BRANCH') in ('master', ):
21
- return ''
22
- else:
23
- return get_local_node_and_date(version)
24
-
25
-
26
9
  try:
27
10
  from setuptools_scm import get_version
28
11
 
29
- version = get_version(root='../..', local_scheme=prerelease_local_scheme)
12
+ version = get_version(root='../..')
30
13
  limit_version = f'>={version}' if '+' not in version and not os.getenv('TOX_ENV_NAME') else ''
31
14
  except (ImportError, LookupError):
32
15
  limit_version = ''
33
16
 
34
17
  setup(
35
18
  name='large-image-source-multi',
36
- use_scm_version={'root': '../..', 'local_scheme': prerelease_local_scheme,
37
- 'fallback_version': '0.0.0'},
38
19
  description=description,
39
20
  long_description=long_description,
40
21
  long_description_content_type='text/x-rst',
41
- license='Apache Software License 2.0',
22
+ license='Apache-2.0',
42
23
  author='Kitware, Inc.',
43
24
  author_email='kitware@kitware.com',
44
25
  classifiers=[
45
26
  'Development Status :: 5 - Production/Stable',
46
- 'License :: OSI Approved :: Apache Software License',
47
27
  'Programming Language :: Python :: 3',
48
- 'Programming Language :: Python :: 3.8',
49
28
  'Programming Language :: Python :: 3.9',
50
29
  'Programming Language :: Python :: 3.10',
51
30
  'Programming Language :: Python :: 3.11',
52
31
  'Programming Language :: Python :: 3.12',
53
32
  'Programming Language :: Python :: 3.13',
54
33
  ],
34
+ python_requires='>=3.9',
55
35
  install_requires=[
56
36
  'jsonschema',
57
37
  f'large-image{limit_version}',
@@ -66,7 +46,6 @@ setup(
66
46
  keywords='large_image, tile source',
67
47
  packages=find_packages(exclude=['test', 'test.*']),
68
48
  url='https://github.com/girder/large_image',
69
- python_requires='>=3.6',
70
49
  entry_points={
71
50
  'large_image.source': [
72
51
  'multi = large_image_source_multi:MultiFileTileSource',
@@ -1,9 +0,0 @@
1
- jsonschema
2
- large-image>=1.30.1.dev16
3
- pyyaml
4
-
5
- [all]
6
- scikit-image
7
-
8
- [girder]
9
- girder-large-image>=1.30.1.dev16