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.
- {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
- {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/README.rst +20 -20
- {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/large_image_source_multi/__init__.py +286 -64
- {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
- large_image_source_multi-1.33.6.dev6/large_image_source_multi.egg-info/requires.txt +9 -0
- {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/pyproject.toml +4 -0
- {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/setup.py +3 -24
- large_image_source_multi-1.30.1.dev16/large_image_source_multi.egg-info/requires.txt +0 -9
- {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/LICENSE +0 -0
- {large_image_source_multi-1.30.1.dev16 → large_image_source_multi-1.33.6.dev6}/docs/specification.rst +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: large-image-source-multi
|
|
3
|
-
Version: 1.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
..
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
..
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
..
|
|
15
|
-
|
|
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
|
-
..
|
|
19
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
+
- ``large-image-tasks``: A utility for running the converter via Girder Worker.
|
|
163
164
|
|
|
164
|
-
|
|
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': {
|
|
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
|
-
|
|
445
|
-
|
|
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
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
x
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: large-image-source-multi
|
|
3
|
-
Version: 1.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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='../..'
|
|
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
|
|
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',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|