From 3b46d9e2d3b22517c1ddbca06fe5a0d76c28beec Mon Sep 17 00:00:00 2001 From: Bane Sullivan Date: Sat, 10 Feb 2024 12:00:24 -0800 Subject: [PATCH] Fix vmin/vmax (#199) * Fix vmin/vmax * Fix typing * Remove broken pine gulch example * Fix typing * Remove debug statements --- doc/source/conf.py | 2 +- doc/source/user-guide/remote-cog.rst | 2 +- doc/source/user-guide/validate_cog.rst | 2 +- localtileserver/client.py | 16 ++-- localtileserver/examples.py | 7 -- localtileserver/tiler/__init__.py | 1 - localtileserver/tiler/data/__init__.py | 4 - localtileserver/tiler/handler.py | 79 +++++++++++++------ localtileserver/tiler/utilities.py | 4 +- .../tileserver/_include/examples.html | 1 - localtileserver/web/views.py | 1 - localtileserver/widgets.py | 8 +- tests/conftest.py | 2 +- tests/test_app.py | 4 +- tests/test_client.py | 19 +++++ tests/test_examples.py | 5 -- 16 files changed, 95 insertions(+), 62 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 8f6f67eb..ea715c07 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -118,7 +118,7 @@ attribution: 'Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL.' }).addTo(map); -L.tileLayer('https://tileserver.banesullivan.com/api/tiles/{z}/{x}/{y}.png?filename=https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-02-16/pine-gulch-fire20/1030010076004E00.tif', { +L.tileLayer('https://tileserver.banesullivan.com/api/tiles/{z}/{x}/{y}.png', { attribution: 'Raster file served by localtileserver.', subdomains: '', crossOrigin: false, diff --git a/doc/source/user-guide/remote-cog.rst b/doc/source/user-guide/remote-cog.rst index aec87bad..9cb38ee1 100644 --- a/doc/source/user-guide/remote-cog.rst +++ b/doc/source/user-guide/remote-cog.rst @@ -17,7 +17,7 @@ we can view tiles of the remote file very efficiently in a Jupyter notebook. from localtileserver import TileClient import folium, ipyleaflet - url = 'https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-02-16/pine-gulch-fire20/1030010076004E00.tif' + url = 'https://github.com/giswqs/data/raw/main/raster/landsat7.tif' # First, create a tile server from the URL raster file client = TileClient(url) diff --git a/doc/source/user-guide/validate_cog.rst b/doc/source/user-guide/validate_cog.rst index 950888a7..3dd292c8 100644 --- a/doc/source/user-guide/validate_cog.rst +++ b/doc/source/user-guide/validate_cog.rst @@ -15,7 +15,7 @@ You can use the script by: from localtileserver import validate_cog # Path to raster (URL or local path) - url = 'https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-02-16/pine-gulch-fire20/1030010076004E00.tif' + url = 'https://github.com/giswqs/data/raw/main/raster/landsat7.tif' # If invalid, returns False validate_cog(url) diff --git a/localtileserver/client.py b/localtileserver/client.py index 479759be..0669eb83 100644 --- a/localtileserver/client.py +++ b/localtileserver/client.py @@ -165,8 +165,8 @@ def tile( y: int, indexes: Optional[List[int]] = None, colormap: Optional[str] = None, - vmin: Optional[float] = None, - vmax: Optional[float] = None, + vmin: Optional[Union[float, List[float]]] = None, + vmax: Optional[Union[float, List[float]]] = None, nodata: Optional[Union[int, float]] = None, output_path: pathlib.Path = None, encoding: str = "PNG", @@ -220,8 +220,8 @@ def thumbnail( self, indexes: Optional[List[int]] = None, colormap: Optional[str] = None, - vmin: Optional[float] = None, - vmax: Optional[float] = None, + vmin: Optional[Union[float, List[float]]] = None, + vmax: Optional[Union[float, List[float]]] = None, nodata: Optional[Union[int, float]] = None, output_path: pathlib.Path = None, encoding: str = "PNG", @@ -417,8 +417,8 @@ def get_tile_url( self, indexes: Optional[List[int]] = None, colormap: Optional[str] = None, - vmin: Optional[float] = None, - vmax: Optional[float] = None, + vmin: Optional[Union[float, List[float]]] = None, + vmax: Optional[Union[float, List[float]]] = None, nodata: Optional[Union[int, float]] = None, client: bool = False, ): @@ -452,11 +452,11 @@ def get_tile_url( if vmin is not None: if isinstance(vmin, Iterable) and not isinstance(indexes, Iterable): raise ValueError("`indexes` must be explicitly set if `vmin` is an iterable.") - params["min"] = vmin + params["vmin"] = vmin if vmax is not None: if isinstance(vmax, Iterable) and not isinstance(indexes, Iterable): raise ValueError("`indexes` must be explicitly set if `vmax` is an iterable.") - params["max"] = vmax + params["vmax"] = vmax if nodata is not None: if isinstance(nodata, Iterable) and not isinstance(indexes, Iterable): raise ValueError("`indexes` must be explicitly set if `nodata` is an iterable.") diff --git a/localtileserver/examples.py b/localtileserver/examples.py index 17fbb8a6..d12f7db2 100644 --- a/localtileserver/examples.py +++ b/localtileserver/examples.py @@ -8,7 +8,6 @@ get_data_path, get_elevation_us_url, get_oam2_url, - get_pine_gulch_url, get_sf_bay_url, ) from localtileserver.tiler.data import DIRECTORY @@ -54,12 +53,6 @@ def get_bahamas(*args, **kwargs): return TileClient(path, *args, **kwargs) -@wraps(_get_example_client) -def get_pine_gulch(*args, **kwargs): - path = get_pine_gulch_url() - return TileClient(path, *args, **kwargs) - - @wraps(_get_example_client) def get_landsat(*args, **kwargs): path = get_data_path("landsat.tif") diff --git a/localtileserver/tiler/__init__.py b/localtileserver/tiler/__init__.py index f81e1ba7..f9294606 100644 --- a/localtileserver/tiler/__init__.py +++ b/localtileserver/tiler/__init__.py @@ -5,7 +5,6 @@ get_data_path, get_elevation_us_url, get_oam2_url, - get_pine_gulch_url, get_sf_bay_url, str_to_bool, ) diff --git a/localtileserver/tiler/data/__init__.py b/localtileserver/tiler/data/__init__.py index da871dc0..abbd5a29 100644 --- a/localtileserver/tiler/data/__init__.py +++ b/localtileserver/tiler/data/__init__.py @@ -23,10 +23,6 @@ def get_data_path(name): return DIRECTORY / name -def get_pine_gulch_url(): - return "https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-02-16/pine-gulch-fire20/1030010076004E00.tif" - - def get_sf_bay_url(): # Non-COG: https://data.kitware.com/#item/60747d792fa25629b9a79538 # COG: https://data.kitware.com/#item/626854a04acac99f42126a72 diff --git a/localtileserver/tiler/handler.py b/localtileserver/tiler/handler.py index 5ed3ca8a..d1949772 100644 --- a/localtileserver/tiler/handler.py +++ b/localtileserver/tiler/handler.py @@ -1,6 +1,6 @@ """Methods for working with images.""" import pathlib -from typing import List, Optional, Union +from typing import Dict, List, Optional, Tuple, Union import numpy as np import rasterio @@ -114,32 +114,65 @@ def _handle_nodata(tile_source: Reader, nodata: Optional[Union[int, float]] = No return nodata +def _handle_vmin_vmax( + indexes: List[int], + vmin: Optional[Union[float, List[float]]] = None, + vmax: Optional[Union[float, List[float]]] = None, +) -> Tuple[Dict[int, float], Dict[int, float]]: + # TODO: move these string checks to the rest api + if isinstance(vmin, (str, int)): + vmin = float(vmin) + if isinstance(vmax, (str, int)): + vmax = float(vmax) + if isinstance(vmin, list): + vmin = [float(v) for v in vmin] + if isinstance(vmax, list): + vmax = [float(v) for v in vmax] + if isinstance(vmin, float) or vmin is None: + vmin = [vmin] * len(indexes) + if isinstance(vmax, float) or vmax is None: + vmax = [vmax] * len(indexes) + # vmin/vmax must be list of values at this point + if len(vmin) != len(indexes): + raise ValueError("vmin must be same length as indexes") + if len(vmax) != len(indexes): + raise ValueError("vmax must be same length as indexes") + # Now map to the band indexes + return dict(zip(indexes, vmin)), dict(zip(indexes, vmax)) + + def _render_image( tile_source: Reader, img: ImageData, - indexes: Optional[List[int]] = None, + indexes: List[int], + vmin: Dict[int, Optional[float]], + vmax: Dict[int, Optional[float]], colormap: Optional[str] = None, - vmin: Optional[float] = None, - vmax: Optional[float] = None, img_format: str = "PNG", ): - if isinstance(vmin, str): - vmin = float(vmin) - if isinstance(vmax, str): - vmax = float(vmax) colormap = cmap.get(colormap) if colormap else None if ( not colormap and len(indexes) == 1 and tile_source.dataset.colorinterp[indexes[0] - 1] == ColorInterp.palette ): + # NOTE: vmin/vmax are not used for palette images colormap = tile_source.dataset.colormap(indexes[0]) - elif img.data.dtype != np.dtype("uint8") or vmin is not None or vmax is not None: - stats = tile_source.statistics() - in_range = [ - (s.min if vmin is None else vmin, s.max if vmax is None else vmax) - for s in stats.values() - ] + # TODO: change these to any checks for none in vmin/vmax + elif ( + img.data.dtype != np.dtype("uint8") + or any(v is not None for v in vmin) + or any(v is not None for v in vmax) + ): + stats = tile_source.statistics(indexes=indexes) + in_range = [] + for i in indexes: + in_range.append( + ( + stats[f"b{i}"].min if vmin[i] is None else vmin[i], + stats[f"b{i}"].max if vmax[i] is None else vmax[i], + ) + ) img.rescale( in_range=in_range, out_range=[(0, 255)], @@ -157,8 +190,8 @@ def get_tile( y: int, indexes: Optional[List[int]] = None, colormap: Optional[str] = None, - vmin: Optional[float] = None, - vmax: Optional[float] = None, + vmin: Optional[Union[float, List[float]]] = None, + vmax: Optional[Union[float, List[float]]] = None, nodata: Optional[Union[int, float]] = None, img_format: str = "PNG", ): @@ -166,15 +199,16 @@ def get_tile( indexes = [1] indexes = _handle_band_indexes(tile_source, indexes) nodata = _handle_nodata(tile_source, nodata) + vmin, vmax = _handle_vmin_vmax(indexes, vmin, vmax) img = tile_source.tile(x, y, z, indexes=indexes, nodata=nodata) return _render_image( tile_source, img, indexes=indexes, - colormap=colormap, - img_format=img_format, vmin=vmin, vmax=vmax, + colormap=colormap, + img_format=img_format, ) @@ -191,8 +225,8 @@ def get_preview( tile_source: Reader, indexes: Optional[List[int]] = None, colormap: Optional[str] = None, - vmin: Optional[float] = None, - vmax: Optional[float] = None, + vmin: Optional[Union[float, List[float]]] = None, + vmax: Optional[Union[float, List[float]]] = None, nodata: Optional[Union[int, float]] = None, img_format: str = "PNG", max_size: int = 512, @@ -201,13 +235,14 @@ def get_preview( indexes = [1] indexes = _handle_band_indexes(tile_source, indexes) nodata = _handle_nodata(tile_source, nodata) + vmin, vmax = _handle_vmin_vmax(indexes, vmin, vmax) img = tile_source.preview(max_size=max_size, indexes=indexes, nodata=nodata) return _render_image( tile_source, img, indexes=indexes, - colormap=colormap, - img_format=img_format, vmin=vmin, vmax=vmax, + colormap=colormap, + img_format=img_format, ) diff --git a/localtileserver/tiler/utilities.py b/localtileserver/tiler/utilities.py index 909037f0..b2e0ca8a 100644 --- a/localtileserver/tiler/utilities.py +++ b/localtileserver/tiler/utilities.py @@ -7,7 +7,7 @@ from rasterio import CRS -from localtileserver.tiler.data import clean_url, get_data_path, get_pine_gulch_url +from localtileserver.tiler.data import clean_url, get_data_path class ImageBytes(bytes): @@ -84,8 +84,6 @@ def get_clean_filename(filename: str): filename = get_data_path("aws_elevation_tiles_prod.xml") elif filename == "bahamas": filename = get_data_path("bahamas_rgb.tif") - elif filename == "pine_gulch": - filename = get_pine_gulch_url() if str(filename).startswith("/vsi"): return filename diff --git a/localtileserver/web/templates/tileserver/_include/examples.html b/localtileserver/web/templates/tileserver/_include/examples.html index 3db45854..7d667a78 100644 --- a/localtileserver/web/templates/tileserver/_include/examples.html +++ b/localtileserver/web/templates/tileserver/_include/examples.html @@ -7,7 +7,6 @@ - diff --git a/localtileserver/web/views.py b/localtileserver/web/views.py index eefad8b8..803a6d5a 100644 --- a/localtileserver/web/views.py +++ b/localtileserver/web/views.py @@ -74,7 +74,6 @@ def sample_data_context(): context["filename_dem"] = data.get_data_path("aws_elevation_tiles_prod.xml") context["filename_bluemarble"] = data.get_data_path("frmt_wms_bluemarble_s3_tms.xml") context["filename_virtualearth"] = data.get_data_path("frmt_wms_virtualearth.xml") - context["filename_pine_gulch"] = data.get_pine_gulch_url() context["filename_sf_bay"] = data.get_sf_bay_url() context["filename_landsat_salt_lake"] = data.get_data_path("landsat.tif") context["filename_oam2"] = data.get_oam2_url() diff --git a/localtileserver/widgets.py b/localtileserver/widgets.py index 97a07f04..68cda8bd 100644 --- a/localtileserver/widgets.py +++ b/localtileserver/widgets.py @@ -23,8 +23,8 @@ def get_leaflet_tile_layer( debug: bool = False, indexes: Optional[List[int]] = None, colormap: Optional[str] = None, - vmin: Optional[float] = None, - vmax: Optional[float] = None, + vmin: Optional[Union[float, List[float]]] = None, + vmax: Optional[Union[float, List[float]]] = None, nodata: Optional[Union[int, float]] = None, attribution: str = None, **kwargs, @@ -128,8 +128,8 @@ def get_folium_tile_layer( debug: bool = False, indexes: Optional[List[int]] = None, colormap: Optional[str] = None, - vmin: Optional[float] = None, - vmax: Optional[float] = None, + vmin: Optional[Union[float, List[float]]] = None, + vmax: Optional[Union[float, List[float]]] = None, nodata: Optional[Union[int, float]] = None, attr: str = None, **kwargs, diff --git a/tests/conftest.py b/tests/conftest.py index 21f2879f..1131faca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,7 +35,7 @@ def blue_marble(port="default", debug=True): @pytest.fixture def remote_file_url(): - return "https://opendata.digitalglobe.com/events/california-fire-2020/pre-event/2018-02-16/pine-gulch-fire20/1030010076004E00.tif" + return "https://github.com/giswqs/data/raw/main/raster/landsat7.tif" @pytest.fixture diff --git a/tests/test_app.py b/tests/test_app.py index 0e005c54..ae374ac0 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -16,8 +16,8 @@ def test_home_page(flask_client): def test_cesium_split_view(flask_client): - filenameA = "https%3A%2F%2Fopendata.digitalglobe.com%2Fmarshall-fire21%2Fpre%2F13%2F031131113123%2F2021-12-21%2F1050010028D5F600-visual.tif" - filenameB = "https%3A%2F%2Fopendata.digitalglobe.com%2Fmarshall-fire21%2Fpost-event%2F2021-12-30%2F10200100BCB1A500%2F10200100BCB1A500.tif" + filenameA = "https://github.com/giswqs/data/raw/main/raster/landsat7.tif" + filenameB = filenameA r = flask_client.get(f"/split/?filenameA={filenameA}&filenameB={filenameB}") assert r.status_code == 200 r = flask_client.get(f"/split/?filenameA={filenameA}") diff --git a/tests/test_client.py b/tests/test_client.py index 13a68989..54dcb01b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -145,6 +145,25 @@ def test_multiband_vmin_vmax(bahamas): ).format(z=8, x=72, y=110) +def test_vmin_vmax(bahamas): + url = bahamas.get_tile_url( + indexes=[1, 2, 3], + vmin=[100, 200, 250], + ).format(z=8, x=72, y=110) + assert get_content(url) # just make sure it doesn't fail + url = bahamas.get_tile_url( + indexes=[1, 2, 3], + vmax=[20, 50, 70], + ).format(z=8, x=72, y=110) + assert get_content(url) # just make sure it doesn't fail + url = bahamas.get_tile_url( + indexes=[1, 2, 3], + vmin=[20, 50, 70], + vmax=[100, 200, 250], + ).format(z=8, x=72, y=110) + assert get_content(url) # just make sure it doesn't fail + + def test_launch_non_default_server(bahamas_file): default = TileClient(bahamas_file) diff = TileClient(bahamas_file, port=0) diff --git a/tests/test_examples.py b/tests/test_examples.py index 521f9589..0695e09b 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -34,11 +34,6 @@ def test_get_bahamas(): assert client.metadata -def test_get_pine_gulch(): - client = examples.get_pine_gulch() - assert client.metadata - - def test_get_landsat(): client = examples.get_landsat() assert client.metadata