From 14c8691a29a56614e3de6ef3717c4a5e1abc83ab Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 5 Dec 2024 13:39:35 -0500 Subject: [PATCH] Adjust multisource multi layer compositing. When using the multisource to composite multiple images with alpha channels, use nearest neighbor for upper tiles. This avoids edge effects from bicubic interpolation --- CHANGELOG.md | 4 ++++ .../multi/large_image_source_multi/__init__.py | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec3cc4d9..b41697399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 1.30.5 +### Improvements + +- When using the multisource to composite multiple images with alpha channels, use nearest neighbor for upper tiles ([#1736](../../pull/1736)) + ### Changes - Adjust how compositing is done on styled images by adjusting the expected full alpha value ([#1735](../../pull/1735)) diff --git a/sources/multi/large_image_source_multi/__init__.py b/sources/multi/large_image_source_multi/__init__.py index 458a2b8ae..d180c3052 100644 --- a/sources/multi/large_image_source_multi/__init__.py +++ b/sources/multi/large_image_source_multi/__init__.py @@ -1037,7 +1037,8 @@ def _mergeTiles(self, base, tile, x, y): base[y:y + tile.shape[0], x:x + tile.shape[1], :] = tile return base - def _getTransformedTile(self, ts, transform, corners, scale, frame, crop=None): + def _getTransformedTile(self, ts, transform, corners, scale, frame, + crop=None, firstMerge=False): """ Determine where the target tile's corners are located on the source. Fetch that so that we have at least sqrt(2) more resolution, then use @@ -1054,6 +1055,9 @@ def _getTransformedTile(self, ts, transform, corners, scale, frame, crop=None): :param crop: an optional dictionary to crop the source image in full resolution, untransformed coordinates. This may contain left, top, right, and bottom values in pixels. + :param firstMerge: if False and using an alpha channel, transform + with nearest neighbor rather than a higher order function to + avoid transparency effects. :returns: a numpy array tile or None, x, y coordinates within the target tile for the placement of the numpy tile array. """ @@ -1145,9 +1149,12 @@ def _getTransformedTile(self, ts, transform, corners, scale, frame, crop=None): destShape = [min(destShape[0], outh - y), min(destShape[1], outw - x)] if destShape[0] <= 0 or destShape[1] <= 0: return None, None, None - # Add an alpha band if needed + # Add an alpha band if needed. This has to be done before the + # transform if it isn't the first tile, since the unused transformed + # areas need to have a zero alpha value if srcImage.shape[2] in {1, 3}: _, srcImage = _makeSameChannelDepth(np.zeros((1, 1, srcImage.shape[2] + 1)), srcImage) + useNearest = srcImage.shape[2] in {2, 4} and not firstMerge # skimage.transform.warp is faster and has less artifacts than # scipy.ndimage.affine_transform. It is faster than using cupy's # version of scipy's affine_transform when the source and destination @@ -1157,7 +1164,7 @@ def _getTransformedTile(self, ts, transform, corners, scale, frame, crop=None): # provide any speed improvement srcImage.astype(float), skimage.transform.AffineTransform(np.linalg.inv(transform)), - order=3, + order=0 if useNearest else 3, output_shape=(destShape[0], destShape[1], srcImage.shape[2]), ).astype(srcImage.dtype) return destImage, x, y @@ -1234,7 +1241,8 @@ def _addSourceToTile(self, tile, sourceEntry, corners, scale): else: sourceTile, x, y = self._getTransformedTile( ts, transform, corners, scale, sourceEntry.get('frame', 0), - source.get('position', {}).get('crop')) + source.get('position', {}).get('crop'), + firstMerge=tile is None) if sourceTile is not None and all(dim > 0 for dim in sourceTile.shape): targetDtype = np.dtype(self._info.get('dtype', ts.dtype)) changeDtype = sourceTile.dtype != targetDtype