large-image-source-multi 1.32.12.dev13__tar.gz → 1.32.12.dev14__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.32.12.dev13/large_image_source_multi.egg-info → large_image_source_multi-1.32.12.dev14}/PKG-INFO +3 -3
  2. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/large_image_source_multi/__init__.py +254 -50
  3. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14/large_image_source_multi.egg-info}/PKG-INFO +3 -3
  4. large_image_source_multi-1.32.12.dev14/large_image_source_multi.egg-info/requires.txt +9 -0
  5. large_image_source_multi-1.32.12.dev13/large_image_source_multi.egg-info/requires.txt +0 -9
  6. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/LICENSE +0 -0
  7. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/README.rst +0 -0
  8. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/docs/specification.rst +0 -0
  9. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/large_image_source_multi/girder_source.py +0 -0
  10. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/large_image_source_multi.egg-info/SOURCES.txt +0 -0
  11. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/large_image_source_multi.egg-info/dependency_links.txt +0 -0
  12. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/large_image_source_multi.egg-info/entry_points.txt +0 -0
  13. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/large_image_source_multi.egg-info/top_level.txt +0 -0
  14. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/pyproject.toml +0 -0
  15. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/setup.cfg +0 -0
  16. {large_image_source_multi-1.32.12.dev13 → large_image_source_multi-1.32.12.dev14}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: large-image-source-multi
3
- Version: 1.32.12.dev13
3
+ Version: 1.32.12.dev14
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.
@@ -17,12 +17,12 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: LICENSE
19
19
  Requires-Dist: jsonschema
20
- Requires-Dist: large-image>=1.32.12.dev13
20
+ Requires-Dist: large-image>=1.32.12.dev14
21
21
  Requires-Dist: pyyaml
22
22
  Provides-Extra: all
23
23
  Requires-Dist: scikit-image; extra == "all"
24
24
  Provides-Extra: girder
25
- Requires-Dist: girder-large-image>=1.32.12.dev13; extra == "girder"
25
+ Requires-Dist: girder-large-image>=1.32.12.dev14; extra == "girder"
26
26
  Dynamic: author
27
27
  Dynamic: author-email
28
28
  Dynamic: classifier
@@ -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. '
@@ -580,6 +644,62 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
580
644
  source['path'] = source['path'].resolve(False)
581
645
  return sources
582
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
+
583
703
  def _sourceBoundingBox(self, source, width, height):
584
704
  """
585
705
  Given a source with a possible transform and an image width and height,
@@ -597,6 +717,8 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
597
717
  bbox = {'left': 0, 'top': 0, 'right': width, 'bottom': height}
598
718
  if not pos:
599
719
  return bbox
720
+ if 'warp' in pos:
721
+ _lazyImportSkimageTransform()
600
722
  x0, y0, x1, y1 = 0, 0, width, height
601
723
  if 'crop' in pos:
602
724
  x0 = min(max(pos['crop'].get('left', x0), 0), width)
@@ -612,6 +734,8 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
612
734
  m[1][0] = pos.get('s21', 0) * pos.get('scale', 1)
613
735
  m[1][1] = pos.get('s22', 1) * pos.get('scale', 1)
614
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)
615
739
  if not np.array_equal(m, np.identity(3)):
616
740
  bbox['transform'] = m
617
741
  try:
@@ -619,11 +743,23 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
619
743
  except np.linalg.LinAlgError:
620
744
  msg = 'The position for a source is not invertable (%r)'
621
745
  raise TileSourceError(msg, pos)
622
- 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
623
756
  bbox['left'] = min(transcorners[0])
624
757
  bbox['top'] = min(transcorners[1])
625
758
  bbox['right'] = max(transcorners[0])
626
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
627
763
  return bbox
628
764
 
629
765
  def _axisKey(self, source, value, key):
@@ -1044,8 +1180,34 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1044
1180
  base[y:y + tile.shape[0], x:x + tile.shape[1], :] = tile
1045
1181
  return base
1046
1182
 
1047
- def _getTransformedTile(self, ts, transform, corners, scale, frame,
1048
- crop=None, firstMerge=False):
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):
1049
1211
  """
1050
1212
  Determine where the target tile's corners are located on the source.
1051
1213
  Fetch that so that we have at least sqrt(2) more resolution, then use
@@ -1059,6 +1221,11 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1059
1221
  corner 0 must be the upper left, 2 must be the lower right.
1060
1222
  :param scale: scaling factor from full res to the target resolution.
1061
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).
1062
1229
  :param crop: an optional dictionary to crop the source image in full
1063
1230
  resolution, untransformed coordinates. This may contain left, top,
1064
1231
  right, and bottom values in pixels.
@@ -1068,30 +1235,52 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1068
1235
  :returns: a numpy array tile or None, x, y coordinates within the
1069
1236
  target tile for the placement of the numpy tile array.
1070
1237
  """
1071
- try:
1072
- import skimage.transform
1073
- except ImportError:
1074
- msg = 'scikit-image is required for affine transforms.'
1075
- raise TileSourceError(msg)
1076
1238
  # From full res source to full res destination
1077
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()
1078
1245
  # Scale dest corners to actual size; adjust transform for the same
1079
1246
  corners = np.array(corners)
1080
1247
  corners[:, :2] //= scale
1081
- transform[:2, :] /= scale
1082
1248
  # Offset so our target is the actual destination array we use
1083
- transform[0][2] -= corners[0][0]
1084
- 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]
1085
1257
  corners[:, :2] -= corners[0, :2]
1086
1258
  outw, outh = corners[2][0], corners[2][1]
1087
1259
  if not outh or not outw:
1088
1260
  return None, 0, 0
1089
- 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)
1090
1270
  minx = min(c[0] for c in srccorners)
1091
1271
  maxx = max(c[0] for c in srccorners)
1092
1272
  miny = min(c[1] for c in srccorners)
1093
1273
  maxy = max(c[1] for c in srccorners)
1094
- 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
1095
1284
  # we only need every 1/srcscale pixel.
1096
1285
  srcscale = int(2 ** math.log2(max(1, srcscale)))
1097
1286
  # Pad to reduce edge effects at tile boundaries
@@ -1123,54 +1312,68 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1123
1312
  [region['right'] // srcscale, region['top'] // srcscale, 1],
1124
1313
  [region['right'] // srcscale, region['bottom'] // srcscale, 1],
1125
1314
  [region['left'] // srcscale, region['bottom'] // srcscale, 1]], dtype=float)
1126
- # adjust our transform if we took a low res version of the source
1127
- transform[:2, :2] *= srcscale
1128
- # Find where the source corners land on the destination.
1129
- preshiftcorners = (np.dot(transform, regioncorners.T).T).tolist()
1130
- regioncorners[:, :2] -= regioncorners[0, :2]
1131
- destcorners = (np.dot(transform, regioncorners.T).T).tolist()
1132
- offsetx, offsety = None, None
1133
- for idx in range(4):
1134
- if offsetx is None or destcorners[idx][0] < offsetx:
1135
- x = preshiftcorners[idx][0]
1136
- offsetx = destcorners[idx][0] - (x - math.floor(x))
1137
- if offsety is None or destcorners[idx][1] < offsety:
1138
- y = preshiftcorners[idx][1]
1139
- offsety = destcorners[idx][1] - (y - math.floor(y))
1140
- transform[0][2] -= offsetx
1141
- transform[1][2] -= offsety
1142
- x, y = int(math.floor(x)), int(math.floor(y))
1143
- # Recompute where the source corners will land
1144
- destcorners = (np.dot(transform, regioncorners.T).T).tolist()
1145
- destShape = [
1146
- max(max(math.ceil(c[1]) for c in destcorners), srcImage.shape[0]),
1147
- max(max(math.ceil(c[0]) for c in destcorners), srcImage.shape[1]),
1148
- ]
1149
- if max(0, -x) or max(0, -y):
1150
- transform[0][2] -= max(0, -x)
1151
- transform[1][2] -= max(0, -y)
1152
- destShape[0] -= max(0, -y)
1153
- destShape[1] -= max(0, -x)
1154
- x += max(0, -x)
1155
- y += max(0, -y)
1156
- destShape = [min(destShape[0], outh - y), min(destShape[1], outw - x)]
1157
- if destShape[0] <= 0 or destShape[1] <= 0:
1158
- return None, None, None
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]
1159
1356
  # Add an alpha band if needed. This has to be done before the
1160
1357
  # transform if it isn't the first tile, since the unused transformed
1161
1358
  # areas need to have a zero alpha value
1162
1359
  if srcImage.shape[2] in {1, 3}:
1163
1360
  _, srcImage = _makeSameChannelDepth(np.zeros((1, 1, srcImage.shape[2] + 1)), srcImage)
1164
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)
1165
1368
  # skimage.transform.warp is faster and has less artifacts than
1166
1369
  # scipy.ndimage.affine_transform. It is faster than using cupy's
1167
1370
  # version of scipy's affine_transform when the source and destination
1168
1371
  # images are converted from numpy to cupy and back in this method.
1169
- destImage = skimage.transform.warp(
1372
+ destImage = skimage_transform.warp(
1170
1373
  # Although using np.float32 could reduce memory use, it doesn't
1171
1374
  # provide any speed improvement
1172
1375
  srcImage.astype(float),
1173
- skimage.transform.AffineTransform(np.linalg.inv(transform)),
1376
+ transformer,
1174
1377
  order=0 if useNearest else 3,
1175
1378
  output_shape=(destShape[0], destShape[1], srcImage.shape[2]),
1176
1379
  ).astype(srcImage.dtype)
@@ -1200,13 +1403,14 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1200
1403
  return tile
1201
1404
  ts = self._openSource(source, sourceEntry['kwargs'])
1202
1405
  transform = bbox.get('transform')
1406
+ warp = bbox.get('warp')
1203
1407
  x = y = 0
1204
1408
  # If there is no transform or the diagonals are positive and there is
1205
1409
  # no sheer and integer pixel alignment, use getRegion with an
1206
1410
  # appropriate size
1207
1411
  scaleX = transform[0][0] if transform is not None else 1
1208
1412
  scaleY = transform[1][1] if transform is not None else 1
1209
- if ((transform is None or (
1413
+ if (warp is None and (transform is None or (
1210
1414
  transform[0][0] > 0 and transform[0][1] == 0 and
1211
1415
  transform[1][0] == 0 and transform[1][1] > 0 and
1212
1416
  transform[0][2] % scaleX == 0 and transform[1][2] % scaleY == 0)) and
@@ -1247,7 +1451,7 @@ class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass):
1247
1451
  resample=None, format=TILE_FORMAT_NUMPY)
1248
1452
  else:
1249
1453
  sourceTile, x, y = self._getTransformedTile(
1250
- ts, transform, corners, scale, sourceEntry.get('frame', 0),
1454
+ ts, transform, corners, scale, sourceEntry.get('frame', 0), warp,
1251
1455
  source.get('position', {}).get('crop'),
1252
1456
  firstMerge=tile is None)
1253
1457
  if sourceTile is not None and all(dim > 0 for dim in sourceTile.shape):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: large-image-source-multi
3
- Version: 1.32.12.dev13
3
+ Version: 1.32.12.dev14
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.
@@ -17,12 +17,12 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Description-Content-Type: text/x-rst
18
18
  License-File: LICENSE
19
19
  Requires-Dist: jsonschema
20
- Requires-Dist: large-image>=1.32.12.dev13
20
+ Requires-Dist: large-image>=1.32.12.dev14
21
21
  Requires-Dist: pyyaml
22
22
  Provides-Extra: all
23
23
  Requires-Dist: scikit-image; extra == "all"
24
24
  Provides-Extra: girder
25
- Requires-Dist: girder-large-image>=1.32.12.dev13; extra == "girder"
25
+ Requires-Dist: girder-large-image>=1.32.12.dev14; extra == "girder"
26
26
  Dynamic: author
27
27
  Dynamic: author-email
28
28
  Dynamic: classifier
@@ -0,0 +1,9 @@
1
+ jsonschema
2
+ large-image>=1.32.12.dev14
3
+ pyyaml
4
+
5
+ [all]
6
+ scikit-image
7
+
8
+ [girder]
9
+ girder-large-image>=1.32.12.dev14
@@ -1,9 +0,0 @@
1
- jsonschema
2
- large-image>=1.32.12.dev13
3
- pyyaml
4
-
5
- [all]
6
- scikit-image
7
-
8
- [girder]
9
- girder-large-image>=1.32.12.dev13