From fe3fd98e78f841babf06613e65c2a754780c6006 Mon Sep 17 00:00:00 2001 From: bstadlbauer <11799671+bstadlbauer@users.noreply.github.com> Date: Tue, 19 Oct 2021 08:15:32 +0200 Subject: [PATCH] Allow float tile_buffer (#405) * Add tests of the expected behaviour * Add `tile_buffer` `float` support in `COGReader.tile()` * :facepalm Co-authored-by: Bernhard Stadlbauer Co-authored-by: vincentsarago --- CHANGES.md | 1 + rio_tiler/errors.py | 4 ++++ rio_tiler/io/cogeo.py | 41 ++++++++++++++++++++++++++++------------- tests/test_io_cogeo.py | 26 ++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2f686bf0..0b251973 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -55,6 +55,7 @@ with COGReader( * compare dataset bounds and tile bounds in TMS crs in `rio_tiler.io.base.SpatialMixin.tile_exists` method to allow dataset and TMS not compatible with WGS84 crs (https://github.com/cogeotiff/rio-tiler/pull/429) * use `httpx` package instead of requests (author @rodrigoalmeida94, https://github.com/cogeotiff/rio-tiler/pull/431) +* allow **half pixel** `tile_buffer` around the tile (e.g 0.5 -> 257x257, 1.5 -> 259x259) (author @bstadlbauer, https://github.com/cogeotiff/rio-tiler/pull/405) **breaking changes** diff --git a/rio_tiler/errors.py b/rio_tiler/errors.py index 4fdd5b40..55126f12 100644 --- a/rio_tiler/errors.py +++ b/rio_tiler/errors.py @@ -13,6 +13,10 @@ class TileOutsideBounds(RioTilerError): """Z-X-Y Tile is outside image bounds.""" +class IncorrectTileBuffer(RioTilerError): + """Tile buffer is a float but not half of an integer""" + + class PointOutsideBounds(RioTilerError): """Point is outside image bounds.""" diff --git a/rio_tiler/io/cogeo.py b/rio_tiler/io/cogeo.py index fc03e6e3..c4e860a2 100644 --- a/rio_tiler/io/cogeo.py +++ b/rio_tiler/io/cogeo.py @@ -6,7 +6,7 @@ import attr import numpy import rasterio -from morecantile import Tile, TileMatrixSet +from morecantile import BoundingBox, Tile, TileMatrixSet from rasterio import transform from rasterio.crs import CRS from rasterio.enums import Resampling @@ -17,8 +17,13 @@ from rasterio.warp import calculate_default_transform, transform_bounds from .. import reader -from ..constants import WEB_MERCATOR_TMS, WGS84_CRS, BBox, Indexes, NoData -from ..errors import ExpressionMixingWarning, NoOverviewWarning, TileOutsideBounds +from ..constants import WEB_MERCATOR_TMS, WGS84_CRS, BBox, Indexes, NoData, NumType +from ..errors import ( + ExpressionMixingWarning, + IncorrectTileBuffer, + NoOverviewWarning, + TileOutsideBounds, +) from ..expression import apply_expression, parse_expression from ..models import BandStatistics, ImageData, ImageStatistics, Info from ..utils import ( @@ -325,7 +330,7 @@ def tile( tilesize: int = 256, indexes: Optional[Indexes] = None, expression: Optional[str] = None, - tile_buffer: Optional[int] = None, + tile_buffer: Optional[NumType] = None, **kwargs: Any, ) -> ImageData: """Read a Web Map tile from a COG. @@ -337,6 +342,7 @@ def tile( tilesize (int, optional): Output image size. Defaults to `256`. indexes (int or sequence of int, optional): Band indexes. expression (str, optional): rio-tiler expression (e.g. b1/b2+b3). + tile_buffer (int or float, optional): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * tile_buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). kwargs (optional): Options to forward to the `COGReader.part` method. Returns: @@ -349,16 +355,25 @@ def tile( ) tile_bounds = self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z)) - if tile_buffer: - x_res = (tile_bounds[2] - tile_bounds[0]) / tilesize - y_res = (tile_bounds[3] - tile_bounds[1]) / tilesize - tile_bounds = ( - tile_bounds[0] - x_res * tile_buffer, - tile_bounds[1] - y_res * tile_buffer, - tile_bounds[2] + x_res * tile_buffer, - tile_bounds[3] + y_res * tile_buffer, + if tile_buffer is not None: + if tile_buffer % 0.5: + raise IncorrectTileBuffer( + "`tile_buffer` must be a multiple of `0.5` (e.g: 0.5, 1, 1.5, ...)." + ) + + x_res = (tile_bounds.right - tile_bounds.left) / tilesize + y_res = (tile_bounds.top - tile_bounds.bottom) / tilesize + + # Buffered Tile Bounds + tile_bounds = BoundingBox( + tile_bounds.left - x_res * tile_buffer, + tile_bounds.bottom - y_res * tile_buffer, + tile_bounds.right + x_res * tile_buffer, + tile_bounds.top + y_res * tile_buffer, ) - tilesize += tile_buffer * 2 + + # Buffered Tile Size + tilesize += int(tile_buffer * 2) return self.part( tile_bounds, diff --git a/tests/test_io_cogeo.py b/tests/test_io_cogeo.py index 9e7314ee..06d392a1 100644 --- a/tests/test_io_cogeo.py +++ b/tests/test_io_cogeo.py @@ -17,6 +17,7 @@ from rio_tiler.errors import ( AlphaBandWarning, ExpressionMixingWarning, + IncorrectTileBuffer, NoOverviewWarning, TileOutsideBounds, ) @@ -257,6 +258,31 @@ def test_tile_invalid_bounds(): cog.tile(38, 24, 7) +def test_tile_with_incorrect_float_buffer(): + with pytest.raises(IncorrectTileBuffer): + with COGReader(COGEO) as cog: + cog.tile(43, 24, 7, tile_buffer=0.8) + + +def test_tile_with_int_buffer(): + with COGReader(COGEO) as cog: + data, mask = cog.tile(43, 24, 7, tile_buffer=1) + assert data.shape == (1, 258, 258) + assert mask.all() + + with COGReader(COGEO) as cog: + data, mask = cog.tile(43, 24, 7, tile_buffer=0) + assert data.shape == (1, 256, 256) + assert mask.all() + + +def test_tile_with_correct_float_buffer(): + with COGReader(COGEO) as cog: + data, mask = cog.tile(43, 24, 7, tile_buffer=0.5) + assert data.shape == (1, 257, 257) + assert mask.all() + + def test_point_valid(): """Read point.""" lon = -56.624124590533825