diff --git a/geoutils/projtools.py b/geoutils/projtools.py index 77febf7a..cd8300b3 100644 --- a/geoutils/projtools.py +++ b/geoutils/projtools.py @@ -305,7 +305,7 @@ def reproject_shape(inshape: BaseGeometry, in_crs: CRS, out_crs: CRS) -> BaseGeo :returns: Reprojected geometry """ - reproj = pyproj.Transformer.from_crs(in_crs, out_crs, always_xy=True, skip_equivalent=True).transform + reproj = pyproj.Transformer.from_crs(in_crs, out_crs, always_xy=True).transform return shapely.ops.transform(reproj, inshape) diff --git a/geoutils/raster/raster.py b/geoutils/raster/raster.py index 5b1d4387..af0392b4 100644 --- a/geoutils/raster/raster.py +++ b/geoutils/raster/raster.py @@ -2140,17 +2140,30 @@ def reproject( return dst_r - def shift(self, xoff: float, yoff: float) -> None: + def shift( + self, xoff: float, yoff: float, distance_unit: Literal["georeferenced"] | Literal["pixel"] = "georeferenced" + ) -> None: """ Shift the raster by a (x,y) offset. + The shifting only updates the geotransform (no resampling is performed). + :param xoff: Translation x offset. :param yoff: Translation y offset. - - + :param distance_unit: Distance unit, either 'georeferenced' (default) or 'pixel'. """ + if distance_unit not in ["georeferenced", "pixel"]: + raise ValueError("Argument 'distance_unit' should be either 'pixel' or 'georeferenced'.") + + # Get transform dx, b, xmin, d, dy, ymax = list(self.transform)[:6] + # Convert pixel offsets to georeferenced units + if distance_unit == "pixel": + xoff *= self.res[0] + yoff *= self.res[1] + + # Overwrite transform by shifted transform self.transform = rio.transform.Affine(dx, b, xmin + xoff, d, dy, ymax + yoff) def save( diff --git a/tests/test_raster.py b/tests/test_raster.py index e0844b17..4e621b11 100644 --- a/tests/test_raster.py +++ b/tests/test_raster.py @@ -1102,8 +1102,11 @@ def test_shift(self, example: str) -> None: r = gu.Raster(example) + # Get original transform orig_transform = r.transform orig_bounds = r.bounds + + # Shift raster by georeferenced units (default) r.shift(xoff=1, yoff=1) # Only bounds should change @@ -1117,6 +1120,27 @@ def test_shift(self, example: str) -> None: assert orig_bounds.bottom + 1 == r.bounds.bottom assert orig_bounds.top + 1 == r.bounds.top + # Shift raster using pixel units + orig_transform = r.transform + orig_bounds = r.bounds + orig_res = r.res + r.shift(xoff=1, yoff=1, distance_unit="pixel") + + # Only bounds should change + assert orig_transform.c + 1 * orig_res[0] == r.transform.c + assert orig_transform.f + 1 * orig_res[1] == r.transform.f + for attr in ["a", "b", "d", "e"]: + assert getattr(orig_transform, attr) == getattr(r.transform, attr) + + assert orig_bounds.left + 1 * orig_res[0] == r.bounds.left + assert orig_bounds.right + 1 * orig_res[0] == r.bounds.right + assert orig_bounds.bottom + 1 * orig_res[1] == r.bounds.bottom + assert orig_bounds.top + 1 * orig_res[1] == r.bounds.top + + # Check that an error is raised for a wrong distance_unit + with pytest.raises(ValueError, match="Argument 'distance_unit' should be either 'pixel' or 'georeferenced'."): + r.shift(xoff=1, yoff=1, distance_unit="wrong_value") # type: ignore + @pytest.mark.parametrize("example", [landsat_b4_path, aster_dem_path]) # type: ignore def test_reproject(self, example: str) -> None: warnings.simplefilter("error")