diff --git a/CHANGES.md b/CHANGES.md index ab8c7049..2f686bf0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -61,11 +61,84 @@ with COGReader( * update morecantile requirement to version >=3.0 (https://github.com/cogeotiff/rio-tiler/pull/418) * remove python 3.6 support (https://github.com/cogeotiff/rio-tiler/pull/418) * remove `max_size` defaults for `COGReader.part` and `COGReader.feature`, which will now default to full resolution reading. -* deprecate `.metadata` methods (https://github.com/cogeotiff/rio-tiler/pull/425) + +```python +# before +with COGReader("my.tif") as cog: + img = cog.part(*cog.dataset.bounds, dst_crs=cog.dataset.crs, bounds_crs=cog.dataset.crs) + # by default image should be max 1024x1024 + assert max(img.width, 1024) # by default image should be max 1024x1024 + assert max(img.height, 1024) + +# now (there is no more max_size default) +with COGReader("my.tif") as cog: + img = cog.part(*cog.dataset.bounds, dst_crs=cog.dataset.crs, bounds_crs=cog.dataset.crs) + assert img.width == cog.dataset.width + assert img.height == cog.dataset.height +``` + +* deprecate `.metadata` and `.stats` methods (https://github.com/cogeotiff/rio-tiler/pull/425) +* add `.statistics` method in base classes (https://github.com/cogeotiff/rio-tiler/pull/427) + * remove `rio_tiler.io.base.SpatialMixin.spatial_info` and `rio_tiler.io.base.SpatialMixin.center` properties (https://github.com/cogeotiff/rio-tiler/pull/429) -* `rio_tiler.io.base.SpatialMixin.bounds` should now be in dataset's CRS (not in `WGS84`) (https://github.com/cogeotiff/rio-tiler/pull/429) + +* Reader's `.bounds` property should now be in dataset's CRS, not in `WGS84` (https://github.com/cogeotiff/rio-tiler/pull/429) + +```python +# before +with COGReader("my.tif") as cog: + print(cog.bounds) + >>> (-61.287001876638215, 15.537756794450583, -61.27877967704677, 15.542486503997608) + +# now +with COGReader("my.tif") as cog: + print(cog.bounds) + >>> (683715.3266400001, 1718548.5702, 684593.2680000002, 1719064.90736) + + print(cog.crs) + >>> EPSG:32620 + + print(cog.geographic_bounds) + >>> (-61.287001876638215, 15.537756794450583, -61.27877967704677, 15.542486503997608) +``` + * Use `RIO_TILER_MAX_THREADS` environment variable instead of `MAX_THREADS` (author @rodrigoalmeida94, https://github.com/cogeotiff/rio-tiler/pull/432) * remove `band_expression` in `rio_tiler.io.base.MultiBandReader` (https://github.com/cogeotiff/rio-tiler/pull/433) +* change `asset_expression` input type from `str` to `Dict[str, str]` in `rio_tiler.io.base.MultiBaseReader` (https://github.com/cogeotiff/rio-tiler/pull/434) + +```python +# before +with STACReader("mystac.json") as stac: + img = stac.preview( + assets=("data1", "data2"), + asset_expression="b1*2", # expression was applied to each asset + ) + +# now +with STACReader("mystac.json") as stac: + img = stac.preview( + assets=("data1", "data2"), + asset_expression={"data1": "b1*2", "data2": "b2*100"}, # we can now pass per asset expression + ) +``` + +* add `asset_indexes` in `rio_tiler.io.base.MultiBaseReader`, which replaces `indexes`. (https://github.com/cogeotiff/rio-tiler/pull/434) + +```python +# before +with STACReader("mystac.json") as stac: + img = stac.preview( + assets=("data1", "data2"), + indexes=(1,), # indexes was applied to each asset + ) + +# now +with STACReader("mystac.json") as stac: + img = stac.preview( + assets=("data1", "data2"), + asset_indexes={"data1": 1, "data2": 2}, # we can now pass per asset indexes + ) +``` # 2.1.3 (2021-09-14) diff --git a/README.md b/README.md index f110b09f..1d542e3a 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,21 @@ At the low level, `rio-tiler` is *just* a wrapper around the [rasterio.vrt.Warpe with STACReader("item.json") as stac: print(stac.assets) # available asset - img = stac.tile(x, y, z, assets="asset1", indexes=(1, 2, 3)) # read tile for asset1 and indexes 1,2,3 - img = stac.tile(x, y, z, assets=("asset1", "asset2", "asset3",), indexes=(1,)) # create an image from assets 1,2,3 using their first band + img = stac.tile( # read tile for asset1 and indexes 1,2,3 + x, + y, + z, + assets="asset1", + indexes=(1, 2, 3), + ) + + img = stac.tile( # create an image from assets 1,2,3 using their first band + x, + y, + z, + assets=("asset1", "asset2", "asset3",), + asset_indexes={"asset1": 1, "asset2": 1, "asset3": 1}, + ) ``` - [Mosaic](https://cogeotiff.github.io/rio-tiler/mosaic/) (merging or stacking) diff --git a/docs/readers.md b/docs/readers.md index 35c94668..03295fb7 100644 --- a/docs/readers.md +++ b/docs/readers.md @@ -1,5 +1,20 @@ -## COGReader +`rio-tiler`'s COGReader and STACReader are built from its abstract base classes (`AsyncBaseReader`, `BaseReader`, `MultiBandReader`, `MultiBaseReader`). Thoses Classes implements defaults interfaces which helps the integration in broder application. To learn more about `rio-tiler`'s base classes see [Base classes and custom readers](advanced/custom_readers.md) + +## rio_tiler.io.COGReader + +The `COGReader` is designed to work with simple raster datasets (e.g COG, GeoTIFF, ...). + +The class is derieved from the `rio_tiler.io.base.BaseReader` base class. +```python +from rio_tiler.io import COGReader + +COGReader.__mro__ +>>> (rio_tiler.io.cogeo.COGReader, + rio_tiler.io.base.BaseReader, + rio_tiler.io.base.SpatialMixin, + object) +``` #### Properties @@ -12,6 +27,30 @@ - **geographic_bounds**: dataset's bounds in WGS84 - **colormap**: dataset's internal colormap + +```python +from rio_tiler.io import COGReader + +with COGReader("myfile.tif") as cog: + print(cog.dataset) + print(cog.tms.identifier) + print(cog.minzoom) + print(cog.maxzoom) + print(cog.bounds) + print(cog.crs) + print(cog.geographic_bounds) + print(cog.colormap) + +>> +WebMercatorQuad +16 +22 +(683715.3266400001, 1718548.5702, 684593.2680000002, 1719064.90736) +EPSG:32620 +(-61.287001876638215, 15.537756794450583, -61.27877967704677, 15.542486503997608) +{} +``` + #### Methods - **read()**: Read the entire dataset @@ -32,12 +71,12 @@ with COGReader("myfile.tif") as cog: # With indexes with COGReader("myfile.tif") as cog: img = cog.read(indexes=1) # or cog.read(indexes=(1,)) - assert img.data.count == 1 + assert img.count == 1 # With expression with COGReader("myfile.tif") as cog: img = cog.read(expression="B1/B2") - assert img.data.count == 1 + assert img.count == 1 ``` - **tile()**: Read map tile from a raster @@ -57,12 +96,12 @@ with COGReader("myfile.tif") as cog: # With indexes with COGReader("myfile.tif") as cog: img = cog.tile(1, 2, 3, tilesize=256, indexes=1) - assert img.data.count == 1 + assert img.count == 1 # With expression with COGReader("myfile.tif"s) as cog: img = cog.tile(1, 2, 3, tilesize=256, expression="B1/B2") - assert img.data.count == 1 + assert img.count == 1 ``` - **part()**: Read a raster for a given bounding box (`bbox`). By default the bbox is considered to be in WGS84. @@ -233,6 +272,7 @@ with COGReader("myfile.tif") as cog: stats = cog.statistics() assert isinstance(stats, dict) +# stats will be in form or {"band": BandStatistics(), ...} print(stats) >>> { '1': BandStatistics(...), @@ -304,59 +344,261 @@ with COGReader("my_cog.tif") as cog: cog.tile(1, 1, 1, nodata=0) ``` -## STACReader +## rio_tiler.io.STACReader -In `rio-tiler` v2, we added a `rio_tiler.io.STACReader` to allow tile/metadata fetching of assets withing a STAC item. The STACReader objects has the same properties/methods as the COGReader. +In `rio-tiler` v2, we added a `rio_tiler.io.STACReader` to allow tile/metadata fetching of assets withing a STAC item. + +The class is derieved from the `rio_tiler.io.base.MultiBaseReader` base class which help handling responses from multiple `BaseReader` (each asset will be read with a `BaseReader`). +```python +from rio_tiler.io import STACReader + +STACReader.__mro__ +>>> (rio_tiler.io.stac.STACReader, + rio_tiler.io.base.MultiBaseReader, + rio_tiler.io.base.BaseReader, + rio_tiler.io.base.SpatialMixin, + object) +``` + +#### Properties + +- **filepath**: STAC Item path, URL or S3 URL +- **item**: PySTAC item +- **assets**: Asset list. +- **tms**: morecantile TileMatrixSet used for tile reading +- **minzoom**: dataset's minimum zoom level (for input tms) +- **maxzoom**: dataset's maximum zoom level (for input tms) +- **bounds**: dataset's bounds (in dataset crs) +- **crs**: dataset's crs +- **geographic_bounds**: dataset's bounds in WGS84 ```python -from typing import Dict from rio_tiler.io import STACReader with STACReader( "https://1tqdbvsut9.execute-api.us-west-2.amazonaws.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_34SGA_20200318_0_L2A", exclude_assets={"thumbnail"} ) as stac: + print(stac.filepath) + print(stac.item) + print(stac.assets) + print(stac.tms.identifier) + print(stac.minzoom) + print(stac.maxzoom) print(stac.bounds) + print(stac.crs) print(stac.geographic_bounds) - print(stac.assets) ->>> [23.293255090449595, 31.505183020453355, 24.296453548295318, 32.51147809805106] ->>> [23.293255090449595, 31.505183020453355, 24.296453548295318, 32.51147809805106] ->>> ['overview', 'visual', 'B01', 'B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B08', 'B8A', 'B09', 'B11', 'B12', 'AOT', 'WVP', 'SCL'] +>>> https://1tqdbvsut9.execute-api.us-west-2.amazonaws.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_34SGA_20200318_0_L2A + +['overview', 'visual', 'B01', 'B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B08', 'B8A', 'B09', 'B11', 'B12', 'AOT', 'WVP', 'SCL'] +WebMercatorQuad +0 +24 +[23.293255090449595, 31.505183020453355, 24.296453548295318, 32.51147809805106] +EPSG:4326 +(23.293255090449595, 31.505183020453355, 24.296453548295318, 32.51147809805106) +``` -# Name of assets to read -assets = ["B01", "B02"] +#### Methods -with STACReader( - "https://1tqdbvsut9.execute-api.us-west-2.amazonaws.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_34SGA_20200318_0_L2A", - exclude_assets={"thumbnail"} -) as stac: - img = stac.tile(145, 103, 8, tilesize=256, assets=assets) +The `STACReader` as the same methods as the `COGReader` (defined by the BaseReader/MultiBaseReader classes). + +!!! important + `STACReader` methods require to set either `assets=` or `expression=` option. + +- **tile()**: Read map tile from a STAC Item + +```python +from rio_tiler.io import STACReader + +stac_url = "https://1tqdbvsut9.execute-api.us-west-2.amazonaws.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_34SGA_20200318_0_L2A" + +# Using `assets=` +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + # stac.tile(x, y, z, assets=?, expression=?, asset_expression=?, asset_indexes=?, **kwargs) + img = stac.tile( + 145, + 103, + 8, + tilesize=256, + assets=["B01", "B02"], + ) + assert img.count == 2 # each assets have one band print(img.assets) >>> [ 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/34/S/GA/2020/3/S2A_34SGA_20200318_0_L2A/B01.tif', - 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/34/S/GA/2020/3/S2A_34SGA_20200318_0_L2A/B02.tif' + 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/34/S/GA/2020/3/S2A_34SGA_20200318_0_L2A/B02.tif', ] +print(img.band_names) +>>> ['B01_1', 'B02_1'] + +# Using `expression=` +with STACReader(stac_url, exclude_assets={"thumbnail"}) as stac: + img = stac.tile( + 145, + 103, + 8, + tilesize=256, + expression="B01/B02", + ) + assert img.count == 1 + + +# Using `assets=` + `asset_expression` (apply band math in an asset) +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + img = stac.tile( + 145, + 103, + 8, + tilesize=256, + assets=["B01", "B02"], + asset_expression={ + "B01": "b1+500", # add 500 to the first band + "B02": "b1-100", # substract 100 to the first band + } + ) + assert img.count == 2 + +# Using `assets=` + `asset_indexes` (select a specific index in an asset) +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + img = stac.tile( + 145, + 103, + 8, + tilesize=256, + assets=["B01"], + asset_indexes={ + "B01": (1, 1, 1), # return the first band 3 times + } + ) + assert img.count == 3 +``` -print(img.data.shape) ->>> (2, 256, 256) +`asset_indexes` and `asset_expression` are available for all STACReader methods expect `info`. -# With expression -with STACReader( - "https://1tqdbvsut9.execute-api.us-west-2.amazonaws.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_34SGA_20200318_0_L2A", - exclude_assets={"thumbnail"} -) as stac: - img = stac.tile(145, 103, 8, tilesize=256, expression="B01/B02") -print(img.assets) +- **part()**: Read a STAC item for a given bounding box (`bbox`). By default the bbox is considered to be in WGS84. + +```python +bbox = (23.8, 31.9, 24.1, 32.2) +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + # stac.part((minx, miny, maxx, maxy), assets=?, expression=?, asset_expression=?, asset_indexes=?, **kwargs) + img = stac.part(bbox, assets=["B01", "B02"], max_size=128) + assert img.count == 2 # each assets have one band +``` + +- **feature()**: Read a STAC item for a geojson feature. By default the feature is considered to be in WGS84. + +```python +feat = { + "type": "Feature", + "geometry": { + "coordinates": [ + [ + [23.8, 32.2], + [23.8, 31.9], + [24.1, 31.9], + [24.1, 32.2], + [23.8, 32.2] + ] + ], + "type": "Polygon" + } +} +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + # stac.feature(feature, assets=?, expression=?, asset_expression=?, asset_indexes=?, **kwargs) + img = stac.feature(feat, assets=["B01", "B02"], max_size=128) + assert img.count == 2 # each assets have one band +``` + +- **preview()**: Read a preview of STAC Item + +```python +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + # stac.preview(assets=?, expression=?, asset_expression=?, asset_indexes=?, **kwargs) + img = stac.preview(assets=["B01", "B02"], max_size=128) + assert img.count == 2 # each assets have one band +``` + +- **point()**: Read the pixel values for assets for a given `lon, lat` coordinates. By default the coordinates are considered to be in WGS84. + +```python +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + # stac.point(lon, lat, assets=?, expression=?, asset_expression=?, asset_indexes=?, **kwargs) + data = stac.point(24.1, 31.9, assets=["B01", "B02"]) + +print(data) >>> [ - 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/34/S/GA/2020/3/S2A_34SGA_20200318_0_L2A/B01.tif', - 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/34/S/GA/2020/3/S2A_34SGA_20200318_0_L2A/B02.tif' + [3595], # values for B01 + [3198] # values for B02 ] +``` + +- **info()**: Return simple metadata about the assets -print(img.data.shape) ->>> (1, 256, 256) +```python +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + # stac.info(assets=?, **kwargs) + info = stac.info(assets=["B01", "B02"]) + +print(list(info)) +>>> ["B01", "B02"] + +print(info["B01"].json(exclude_none=True)) +>>> { + "bounds": [23.106076243528157, 31.505173744374172, 24.296464503939948, 32.519334871696195], + "minzoom": 8, + "maxzoom": 11, + "band_metadata": [["1", {}]], + "band_descriptions": [["1", ""]], + "dtype": "uint16", + "nodata_type": "Nodata", + "colorinterp": ["gray"], + "nodata_value": 0.0, + "width": 1830, + "driver": "GTiff", + "height": 1830, + "overviews": [2, 4, 8], + "count": 1 +} ``` -Note: `STACReader` is based on `rio_tiler.io.base.MultiBaseReader` class. +- **statistics()**: Return image statistics (Min/Max/Stdev) + +```python +with STACReader(stac_url, exclude_assets={"thumbnail"},) as stac: + # stac.statistics(assets=?, asset_expression=?, asset_indexes=?, **kwargs) + stats = stac.statistics(assets=["B01", "B02"], max_size=128) + +# stats will be in form or {"asset": {"band": BandStatistics(), ...}, ...} +print(list(info)) +>>> ["B01", "B02"] + +print(list(info["B01"])) +>>> ["1"] # B01 has only one band entry "1" + +print(info["B01"]["1"].json(exclude_none=True)) +{ + "min": 283.0, + "max": 7734.0, + "mean": 1996.959687371452, + "count": 12155.0, + "sum": 24273045.0, + "std": 1218.4455268717047, + "median": 1866.0, + "majority": 322.0, + "minority": 283.0, + "unique": 4015.0, + "histogram": [ + [3257.0, 2410.0, 2804.0, 1877.0, 1050.0, 423.0, 199.0, 93.0, 31.0, 11.0], + [283.0, 1028.1, 1773.2, 2518.3, 3263.4, 4008.5, 4753.6, 5498.7, 6243.8, 6988.900000000001, 7734.0] + ], + "valid_percent": 74.19, + "masked_pixels": 4229.0, + "valid_pixels": 12155.0, + "percentile_2": 326.08000000000004, + "percentile_98": 5026.76 +} +``` diff --git a/docs/v3_migration.md b/docs/v3_migration.md index 68bee893..fe700d07 100644 --- a/docs/v3_migration.md +++ b/docs/v3_migration.md @@ -2,7 +2,6 @@ `rio-tiler` version 3.0 introduced [many breaking changes](release-notes.md). This document aims to help with migrating your code to use `rio-tiler` 3.0. - ## Morecantile 2.0 -> 3.0 Morecantil 3.0 switched from rasterio to pyproj for the coordinates transformation processes (https://github.com/developmentseed/morecantile/blob/master/CHANGES.md#300a0-2021-09-09). @@ -69,6 +68,44 @@ with COGReader("my_tif.tif") as cog: >>> {'1': BandStatistics(min=1.0, max=7872.0, mean=2107.524612053134, count=1045504.0, sum=2203425412.0, std=2271.0065537857326, median=2800.0, majority=1.0, minority=7072.0, unique=15.0, histogram=[[503460.0, 0.0, 0.0, 161792.0, 283094.0, 0.0, 0.0, 0.0, 87727.0, 9431.0], [1.0, 788.1, 1575.2, 2362.3, 3149.4, 3936.5, 4723.6, 5510.7, 6297.8, 7084.900000000001, 7872.0]], valid_percent=100.0, masked_pixels=0.0, valid_pixels=1045504.0, percentile_2=1.0, percentile_98=6896.0)} ``` +## `asset_expression` and `asset_indexes` + +In 3.0, we changed how `asset_expression` was defined in `rio_tiler.io.MultiBaseReader` (the base class of STAC like datasets). In 2.0, it was defined as a `string` (e.g `b1+100`) and would be applied to all `assets` and in 3.0 it's now a `dict` in form of `{"asset 1": "expression for asset 1", ...}`. + +```python +# v2 +with STACReader("mystac.json") as stac: + img = stac.preview( + assets=("data1", "data2"), + asset_expression="b1*2", # expression was applied to each asset + ) + +# v3 +with STACReader("mystac.json") as stac: + img = stac.preview( + assets=("data1", "data2"), + asset_expression={"data1": "b1*2", "data2": "b2*100"}, # we can now pass per asset expression + ) +``` + +We also added `asset_indexes` to return specific indexes per asset. + + +```python +# v2 +with STACReader("mystac.json") as stac: + img = stac.preview( + assets=("data1", "data2"), + indexes=1, # first band of each asset would be returned + ) + +# v3 +with STACReader("mystac.json") as stac: + img = stac.preview( + assets=("data1", "data2"), + asset_indexes={"data1": (1, 2), "data2": (3,)}, # we can now pass per asset Indexes + ) +``` ## Deprecation diff --git a/rio_tiler/io/base.py b/rio_tiler/io/base.py index 62e97811..750e23e5 100644 --- a/rio_tiler/io/base.py +++ b/rio_tiler/io/base.py @@ -383,7 +383,7 @@ class MultiBaseReader(BaseReader, metaclass=abc.ABCMeta): reader (rio_tiler.io.BaseReader): reader. reader_options (dict, option): options to forward to the reader. Defaults to `{}`. tms (morecantile.TileMatrixSet, optional): TileMatrixSet grid definition. Defaults to `WebMercatorQuad`. - assets (sequence): Asset list. **READ ONLY attribute**. + assets (sequence): Asset list. **Not in __init__**. """ @@ -506,18 +506,16 @@ def _reader(asset: str, *args, **kwargs) -> Dict: def statistics( # type: ignore self, assets: Union[Sequence[str], str] = None, - indexes: Optional[Indexes] = None, - asset_expression: Optional[ - str - ] = None, # Expression for each asset based on band indexes + asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset + asset_expression: Optional[Dict[str, str]] = None, # Expression for each asset **kwargs: Any, ) -> Dict[str, Dict[str, BandStatistics]]: """Return array statistics for multiple assets. Args: assets (sequence of str or str): assets to fetch info from. - indexes (int or sequence of int, optional): Band indexes. - asset_expression (str, optional): rio-tiler expression for each asset (e.g. b1/b2+b3). + asset_indexes (dict, optional): Band indexes for each asset (e.g {"asset1": 1, "asset2": (1, 2,)}). + asset_expression (dict, optional): rio-tiler expression for each asset (e.g. {"asset1": "b1/b2+b3", "asset2": ...}). kwargs (optional): Options to forward to the `self.reader.statistics` method. Returns: @@ -530,14 +528,20 @@ def statistics( # type: ignore if isinstance(assets, str): assets = (assets,) + asset_indexes = asset_indexes or {} + asset_expression = asset_expression or {} + def _reader(asset: str, *args, **kwargs) -> Dict: url = self._get_asset_url(asset) with self.reader(url, tms=self.tms, **self.reader_options) as cog: # type: ignore - return cog.statistics(*args, **kwargs) + return cog.statistics( + *args, + indexes=asset_indexes.get(asset), + expression=asset_expression.get(asset), + **kwargs, + ) - return multi_values( - assets, _reader, indexes=indexes, expression=asset_expression, **kwargs, - ) + return multi_values(assets, _reader, **kwargs) def tile( self, @@ -546,9 +550,8 @@ def tile( tile_z: int, assets: Union[Sequence[str], str] = None, expression: Optional[str] = None, - asset_expression: Optional[ - str - ] = None, # Expression for each asset based on band indexes + asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset + asset_expression: Optional[Dict[str, str]] = None, # Expression for each asset **kwargs: Any, ) -> ImageData: """Read and merge Wep Map tiles from multiple assets. @@ -559,7 +562,8 @@ def tile( tile_z (int): Tile's zoom level index. assets (sequence of str or str, optional): assets to fetch info from. expression (str, optional): rio-tiler expression for the asset list (e.g. asset1/asset2+asset3). - asset_expression (str, optional): rio-tiler expression for each asset (e.g. b1/b2+b3). + asset_indexes (dict, optional): Band indexes for each asset (e.g {"asset1": 1, "asset2": (1, 2,)}). + asset_expression (dict, optional): rio-tiler expression for each asset (e.g. {"asset1": "b1/b2+b3", "asset2": ...}). kwargs (optional): Options to forward to the `self.reader.tile` method. Returns: @@ -588,22 +592,22 @@ def tile( "assets must be passed either via expression or assets options." ) + asset_indexes = asset_indexes or {} + asset_expression = asset_expression or {} + def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData: url = self._get_asset_url(asset) with self.reader(url, tms=self.tms, **self.reader_options) as cog: # type: ignore - data = cog.tile(*args, **kwargs) + data = cog.tile( + *args, + indexes=asset_indexes.get(asset), + expression=asset_expression.get(asset), + **kwargs, + ) data.band_names = [f"{asset}_{n}" for n in data.band_names] return data - output = multi_arrays( - assets, - _reader, - tile_x, - tile_y, - tile_z, - expression=asset_expression, - **kwargs, - ) + output = multi_arrays(assets, _reader, tile_x, tile_y, tile_z, **kwargs,) if expression: blocks = expression.split(",") @@ -617,9 +621,8 @@ def part( bbox: BBox, assets: Union[Sequence[str], str] = None, expression: Optional[str] = None, - asset_expression: Optional[ - str - ] = None, # Expression for each asset based on band indexes + asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset + asset_expression: Optional[Dict[str, str]] = None, # Expression for each asset **kwargs: Any, ) -> ImageData: """Read and merge parts from multiple assets. @@ -628,7 +631,8 @@ def part( bbox (tuple): Output bounds (left, bottom, right, top) in target crs. assets (sequence of str or str, optional): assets to fetch info from. expression (str, optional): rio-tiler expression for the asset list (e.g. asset1/asset2+asset3). - asset_expression (str, optional): rio-tiler expression for each asset (e.g. b1/b2+b3). + asset_indexes (dict, optional): Band indexes for each asset (e.g {"asset1": 1, "asset2": (1, 2,)}). + asset_expression (dict, optional): rio-tiler expression for each asset (e.g. {"asset1": "b1/b2+b3", "asset2": ...}). kwargs (optional): Options to forward to the `self.reader.part` method. Returns: @@ -652,16 +656,22 @@ def part( "assets must be passed either via expression or assets options." ) + asset_indexes = asset_indexes or {} + asset_expression = asset_expression or {} + def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData: url = self._get_asset_url(asset) with self.reader(url, tms=self.tms, **self.reader_options) as cog: # type: ignore - data = cog.part(*args, **kwargs) + data = cog.part( + *args, + indexes=asset_indexes.get(asset), + expression=asset_expression.get(asset), + **kwargs, + ) data.band_names = [f"{asset}_{n}" for n in data.band_names] return data - output = multi_arrays( - assets, _reader, bbox, expression=asset_expression, **kwargs, - ) + output = multi_arrays(assets, _reader, bbox, **kwargs) if expression: blocks = expression.split(",") @@ -674,9 +684,8 @@ def preview( self, assets: Union[Sequence[str], str] = None, expression: Optional[str] = None, - asset_expression: Optional[ - str - ] = None, # Expression for each asset based on band indexes + asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset + asset_expression: Optional[Dict[str, str]] = None, # Expression for each asset **kwargs: Any, ) -> ImageData: """Read and merge previews from multiple assets. @@ -684,7 +693,8 @@ def preview( Args: assets (sequence of str or str, optional): assets to fetch info from. expression (str, optional): rio-tiler expression for the asset list (e.g. asset1/asset2+asset3). - asset_expression (str, optional): rio-tiler expression for each asset (e.g. b1/b2+b3). + asset_indexes (dict, optional): Band indexes for each asset (e.g {"asset1": 1, "asset2": (1, 2,)}). + asset_expression (dict, optional): rio-tiler expression for each asset (e.g. {"asset1": "b1/b2+b3", "asset2": ...}). kwargs (optional): Options to forward to the `self.reader.preview` method. Returns: @@ -708,14 +718,21 @@ def preview( "assets must be passed either via expression or assets options." ) + asset_indexes = asset_indexes or {} + asset_expression = asset_expression or {} + def _reader(asset: str, **kwargs: Any) -> ImageData: url = self._get_asset_url(asset) with self.reader(url, tms=self.tms, **self.reader_options) as cog: # type: ignore - data = cog.preview(**kwargs) + data = cog.preview( + indexes=asset_indexes.get(asset), + expression=asset_expression.get(asset), + **kwargs, + ) data.band_names = [f"{asset}_{n}" for n in data.band_names] return data - output = multi_arrays(assets, _reader, expression=asset_expression, **kwargs) + output = multi_arrays(assets, _reader, **kwargs) if expression: blocks = expression.split(",") @@ -730,9 +747,8 @@ def point( lat: float, assets: Union[Sequence[str], str] = None, expression: Optional[str] = None, - asset_expression: Optional[ - str - ] = None, # Expression for each asset based on band indexes + asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset + asset_expression: Optional[Dict[str, str]] = None, # Expression for each asset **kwargs: Any, ) -> List: """Read pixel value from multiple assets. @@ -742,7 +758,8 @@ def point( lat (float): Latittude. assets (sequence of str or str, optional): assets to fetch info from. expression (str, optional): rio-tiler expression for the asset list (e.g. asset1/asset2+asset3). - asset_expression (str, optional): rio-tiler expression for each asset (e.g. b1/b2+b3). + asset_indexes (dict, optional): Band indexes for each asset (e.g {"asset1": 1, "asset2": (1, 2,)}). + asset_expression (dict, optional): rio-tiler expression for each asset (e.g. {"asset1": "b1/b2+b3", "asset2": ...}). kwargs (optional): Options to forward to the `self.reader.point` method. Returns: @@ -766,14 +783,20 @@ def point( "assets must be passed either via expression or assets options." ) + asset_indexes = asset_indexes or {} + asset_expression = asset_expression or {} + def _reader(asset: str, *args, **kwargs: Any) -> Dict: url = self._get_asset_url(asset) with self.reader(url, tms=self.tms, **self.reader_options) as cog: # type: ignore - return cog.point(*args, **kwargs) + return cog.point( + *args, + indexes=asset_indexes.get(asset), + expression=asset_expression.get(asset), + **kwargs, + ) - data = multi_values( - assets, _reader, lon, lat, expression=asset_expression, **kwargs, - ) + data = multi_values(assets, _reader, lon, lat, **kwargs) values = [d for _, d in data.items()] if expression: @@ -787,9 +810,8 @@ def feature( shape: Dict, assets: Union[Sequence[str], str] = None, expression: Optional[str] = None, - asset_expression: Optional[ - str - ] = None, # Expression for each asset based on band indexes + asset_indexes: Optional[Dict[str, Indexes]] = None, # Indexes for each asset + asset_expression: Optional[Dict[str, str]] = None, # Expression for each asset **kwargs: Any, ) -> ImageData: """Read and merge parts defined by geojson feature from multiple assets. @@ -798,7 +820,8 @@ def feature( shape (dict): Valid GeoJSON feature. assets (sequence of str or str, optional): assets to fetch info from. expression (str, optional): rio-tiler expression for the asset list (e.g. asset1/asset2+asset3). - asset_expression (str, optional): rio-tiler expression for each asset (e.g. b1/b2+b3). + asset_indexes (dict, optional): Band indexes for each asset (e.g {"asset1": 1, "asset2": (1, 2,)}). + asset_expression (dict, optional): rio-tiler expression for each asset (e.g. {"asset1": "b1/b2+b3", "asset2": ...}). kwargs (optional): Options to forward to the `self.reader.feature` method. Returns: @@ -822,16 +845,22 @@ def feature( "assets must be passed either via expression or assets options." ) + asset_indexes = asset_indexes or {} + asset_expression = asset_expression or {} + def _reader(asset: str, *args: Any, **kwargs: Any) -> ImageData: url = self._get_asset_url(asset) with self.reader(url, tms=self.tms, **self.reader_options) as cog: # type: ignore - data = cog.feature(*args, **kwargs) + data = cog.feature( + *args, + indexes=asset_indexes.get(asset), + expression=asset_expression.get(asset), + **kwargs, + ) data.band_names = [f"{asset}_{n}" for n in data.band_names] return data - output = multi_arrays( - assets, _reader, shape, expression=asset_expression, **kwargs, - ) + output = multi_arrays(assets, _reader, shape, **kwargs) if expression: blocks = expression.split(",") @@ -851,7 +880,7 @@ class MultiBandReader(BaseReader, metaclass=abc.ABCMeta): reader (rio_tiler.io.BaseReader): reader. reader_options (dict, option): options to forward to the reader. Defaults to `{}`. tms (morecantile.TileMatrixSet, optional): TileMatrixSet grid definition. Defaults to `WebMercatorQuad`. - bands (sequence): Band list. **READ ONLY attribute**. + bands (sequence): Band list. **Not in __init__**. """ diff --git a/tests/test_io_stac.py b/tests/test_io_stac.py index 81ecb6b5..01cd93a6 100644 --- a/tests/test_io_stac.py +++ b/tests/test_io_stac.py @@ -137,11 +137,6 @@ def test_tile_valid(rio): assert img.mask.shape == (256, 256) assert img.band_names == ["green_1"] - img = stac.tile(71, 102, 8, assets="green", indexes=(1, 1, 1)) - assert img.data.shape == (3, 256, 256) - assert img.mask.shape == (256, 256) - assert img.band_names == ["green_1", "green_1", "green_1"] - data, mask = stac.tile(71, 102, 8, assets=("green",)) assert data.shape == (1, 256, 256) assert mask.shape == (256, 256) @@ -157,10 +152,27 @@ def test_tile_valid(rio): assert img.data.shape == (1, 256, 256) assert img.band_names == ["green/red"] - img = stac.tile(71, 102, 8, assets=("green", "red"), asset_expression="b1*2,b1") - assert img.data.shape == (4, 256, 256) + img = stac.tile( + 71, + 102, + 8, + assets=("green", "red"), + asset_indexes={"green": (1, 1,), "red": 1}, + ) + assert img.data.shape == (3, 256, 256) + assert img.mask.shape == (256, 256) + assert img.band_names == ["green_1", "green_1", "red_1"] + + img = stac.tile( + 71, + 102, + 8, + assets=("green", "red"), + asset_expression={"green": "b1*2,b1", "red": "b1*2"}, + ) + assert img.data.shape == (3, 256, 256) assert img.mask.shape == (256, 256) - assert img.band_names == ["green_b1*2", "green_b1", "red_b1*2", "red_b1"] + assert img.band_names == ["green_b1*2", "green_b1", "red_b1*2"] @patch("rio_tiler.io.cogeo.rasterio") @@ -201,10 +213,21 @@ def test_part_valid(rio): assert img.data.shape == (1, 73, 83) assert img.band_names == ["green/red"] - img = stac.part(bbox, assets=("green", "red"), asset_expression="b1*2,b1") - assert img.data.shape == (4, 73, 83) + img = stac.part( + bbox, assets=("green", "red"), asset_indexes={"green": (1, 1,), "red": 1} + ) + assert img.data.shape == (3, 73, 83) assert img.mask.shape == (73, 83) - assert img.band_names == ["green_b1*2", "green_b1", "red_b1*2", "red_b1"] + assert img.band_names == ["green_1", "green_1", "red_1"] + + img = stac.part( + bbox, + assets=("green", "red"), + asset_expression={"green": "b1*2,b1", "red": "b1*2"}, + ) + assert img.data.shape == (3, 73, 83) + assert img.mask.shape == (73, 83) + assert img.band_names == ["green_b1*2", "green_b1", "red_b1*2"] @patch("rio_tiler.io.cogeo.rasterio") @@ -239,10 +262,20 @@ def test_preview_valid(rio): assert img.data.shape == (1, 259, 255) assert img.band_names == ["green/red"] - img = stac.preview(assets=("green", "red"), asset_expression="b1*2,b1") - assert img.data.shape == (4, 259, 255) + img = stac.preview( + assets=("green", "red"), asset_indexes={"green": (1, 1,), "red": 1} + ) + assert img.data.shape == (3, 259, 255) + assert img.mask.shape == (259, 255) + assert img.band_names == ["green_1", "green_1", "red_1"] + + img = stac.preview( + assets=("green", "red"), + asset_expression={"green": "b1*2,b1", "red": "b1*2"}, + ) + assert img.data.shape == (3, 259, 255) assert img.mask.shape == (259, 255) - assert img.band_names == ["green_b1*2", "green_b1", "red_b1*2", "red_b1"] + assert img.band_names == ["green_b1*2", "green_b1", "red_b1*2"] @patch("rio_tiler.io.cogeo.rasterio") @@ -273,6 +306,26 @@ def test_point_valid(rio): ) assert len(data) == 1 + data = stac.point( + -80.477, + 33.4453, + assets=("green", "red"), + asset_indexes={"green": (1, 1), "red": 1}, + ) + assert len(data) == 2 + assert len(data[0]) == 2 + assert len(data[1]) == 1 + + data = stac.point( + -80.477, + 33.4453, + assets=("green", "red"), + asset_expression={"green": "b1*2,b1", "red": "b1*2"}, + ) + assert len(data) == 2 + assert len(data[0]) == 2 + assert len(data[1]) == 1 + @patch("rio_tiler.io.cogeo.rasterio") def test_stats_valid(rio): @@ -312,14 +365,25 @@ def test_statistics_valid(rio): assert stats["green"] assert isinstance(stats["green"]["1"], BandStatistics) + stats = stac.statistics(assets=("green", "red"), hist_options={"bins": 20}) + assert len(stats) == 2 + assert len(stats["green"]["1"]["histogram"][0]) == 20 + # Check that asset_expression is passed - stats = stac.statistics(assets="green", asset_expression="b1*2") + stats = stac.statistics( + assets=("green", "red"), asset_expression={"green": "b1*2", "red": "b1+100"} + ) assert stats["green"] assert isinstance(stats["green"]["b1*2"], BandStatistics) + assert isinstance(stats["red"]["b1+100"], BandStatistics) - stats = stac.statistics(assets=("green", "red"), hist_options={"bins": 20}) - assert len(stats) == 2 - assert len(stats["green"]["1"]["histogram"][0]) == 20 + # Check that asset_indexes is passed + stats = stac.statistics( + assets=("green", "red"), asset_indexes={"green": 1, "red": 1} + ) + assert stats["green"] + assert isinstance(stats["green"]["1"], BandStatistics) + assert isinstance(stats["red"]["1"], BandStatistics) @patch("rio_tiler.io.cogeo.rasterio") @@ -427,6 +491,22 @@ def test_feature_valid(rio): assert img.data.shape == (1, 118, 96) assert img.band_names == ["green/red"] + img = stac.feature( + feat, assets=("green", "red"), asset_indexes={"green": (1, 1), "red": 1} + ) + assert img.data.shape == (3, 118, 96) + assert img.mask.shape == (118, 96) + assert img.band_names == ["green_1", "green_1", "red_1"] + + img = stac.feature( + feat, + assets=("green", "red"), + asset_expression={"green": "b1*2,b1", "red": "b1*2"}, + ) + assert img.data.shape == (3, 118, 96) + assert img.mask.shape == (118, 96) + assert img.band_names == ["green_b1*2", "green_b1", "red_b1*2"] + def test_relative_assets(): """Should return absolute href for assets"""