From 5936d4e1c6d9e700e3c9b1c1eee4bd1e90d993c5 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 7 Dec 2023 14:12:58 -0500 Subject: [PATCH] Support negative x, y in addTile --- CHANGELOG.md | 1 + .../vips/large_image_source_vips/__init__.py | 20 ++++++++++--- test/test_source_vips.py | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 710043c5a..e4242de4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Reduce stderr noise in PIL and rasterio sources ([#1397](../../pull/1397)) - Harden OME tiff reader ([#1398](../../pull/1398)) - Improve checks for formats we shouldn't read ([#1399](../../pull/1399)) +- Support negative x, y in addTile ([#1401](../../pull/1401)) ### Changes - Use an enum for priority constants ([#1400](../../pull/1400)) diff --git a/sources/vips/large_image_source_vips/__init__.py b/sources/vips/large_image_source_vips/__init__.py index 4fdf8ec42..1283582a6 100644 --- a/sources/vips/large_image_source_vips/__init__.py +++ b/sources/vips/large_image_source_vips/__init__.py @@ -299,7 +299,8 @@ def _invalidateImage(self): """ if self._output is not None: self._image = None - w, h = self._output['width'], self._output['height'] + w = self._output['width'] - min(0, self._output['minx']) + h = self._output['height'] - min(0, self._output['miny']) w = max(self.minWidth or w, w) h = max(self.minHeight or h, h) self.sizeX = w @@ -311,7 +312,10 @@ def _invalidateImage(self): def addTile(self, tile, x=0, y=0, mask=None, interpretation=None): """ Add a numpy or image tile to the image, expanding the image as needed - to accommodate it. + to accommodate it. Note that x and y can be negative. If so, the + output image (and internal memory access of the image) will act as if + the 0, 0 point is the most negative position. Cropping is applied + after this offset. :param tile: a numpy array, PIL Image, vips image, or a binary string with an image. The numpy array can have 2 or 3 dimensions. @@ -432,7 +436,9 @@ def _outputToImage(self): if img.format == 'float' and entry['image'].format == 'double': entryimage = entryimage.cast(img.format) branch = branch.composite( - entryimage, pyvips.BlendMode.OVER, x=entry['x'], y=entry['y']) + entryimage, pyvips.BlendMode.OVER, + x=entry['x'] - min(0, self._output['minx']), + y=entry['y'] - min(0, self._output['miny'])) if not ((idx + 1) % leaves) or idx + 1 == len(self._output['images']): trunk = trunk.composite(branch, pyvips.BlendMode.OVER, x=0, y=0) branch = baseimg.copy() @@ -594,9 +600,15 @@ def bandFormat(self): if not self._editable: return self._image.format return self._getVipsFormat() - # TODO: specify bit depth / bandFormat explicitly + @property + def origin(self): + if not self._editable: + return {'x': 0, 'y': 0} + return {'x': min(0, self._output['minx'] or 0), + 'y': min(0, self._output['miny'] or 0)} + def open(*args, **kwargs): """Create an instance of the module class.""" diff --git a/test/test_source_vips.py b/test/test_source_vips.py index 74a02aa93..1d3e57a95 100644 --- a/test/test_source_vips.py +++ b/test/test_source_vips.py @@ -3,6 +3,7 @@ import tempfile import large_image_source_vips +import numpy as np import pytest import pyvips @@ -210,3 +211,30 @@ def testNewAndWriteWithMask(): assert resultMetadata['sizeX'] == 4000 finally: shutil.rmtree(tmpdir) + + +def testNewAndWriteNegative(): + out = large_image_source_vips.new() + out.addTile(np.full((4, 4, 3), 1, dtype=np.uint8), x=0, y=0) + out.addTile(np.full((5, 4, 3), 2, dtype=np.uint8), x=-2, y=1) + with tempfile.TemporaryDirectory() as tmpdir: + outputPath = os.path.join(tmpdir, 'temp.tiff') + out.write(outputPath, lossy=False) + region = out.getRegion(format=large_image.constants.TILE_FORMAT_NUMPY)[0] + assert (region[:, :, 0] == np.array([ + [0, 0, 1, 1, 1, 1], + [2, 2, 2, 2, 1, 1], + [2, 2, 2, 2, 1, 1], + [2, 2, 2, 2, 1, 1], + [2, 2, 2, 2, 0, 0], + [2, 2, 2, 2, 0, 0]])).all() + + ts = large_image.open(outputPath) + region = ts.getRegion(format=large_image.constants.TILE_FORMAT_NUMPY)[0] + assert (region[:, :, 0] == np.array([ + [0, 0, 1, 1, 1, 1], + [2, 2, 2, 2, 1, 1], + [2, 2, 2, 2, 1, 1], + [2, 2, 2, 2, 1, 1], + [2, 2, 2, 2, 0, 0], + [2, 2, 2, 2, 0, 0]])).all()