Skip to content

Commit 63822d8

Browse files
revert to merged statistics for MultiBaseTilerFactory (#437)
1 parent 0e7de05 commit 63822d8

File tree

7 files changed

+172
-15
lines changed

7 files changed

+172
-15
lines changed

CHANGES.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 0.5.0 (TBD)
44

55
* update rio-tiler/morecantile/rio-cogeo/cogeo-mosaic versions
6+
* add MultiBaseTilerFactory `/asset_statistics` which will return *per asset* statistics. Returns response in form of `Dict[{asset name}, Dict[{band name}, BandStatistics]]`
67

78
**breaking change**
89

@@ -16,6 +17,44 @@ expression = "b1+b2,b2"
1617
expression = "b1+b2;b2"
1718
```
1819

20+
* MultiBaseTilerFactory `/statistics` now returns *merged* statistics in form of `Dict[{asset_band or expression}, BandStatistics]` (instead of `Dict[{asset name}, Dict[{band name}, BandStatistics]]`)
21+
22+
```python
23+
# before
24+
response = httpx.get(f"/stac/statistics?url=item.json").json()
25+
print(response)
26+
>>> {
27+
"asset1": {
28+
"1": {
29+
"min": ...,
30+
"max": ...,
31+
...
32+
},
33+
"2": {
34+
"min": ...,
35+
"max": ...,
36+
...
37+
}
38+
}
39+
}
40+
41+
# now
42+
response = httpx.get(f"/stac/statistics?url=item.json").json()
43+
print(response)
44+
>>> {
45+
"asset1_1": {
46+
"min": ...,
47+
"max": ...,
48+
...
49+
},
50+
"asset1_2": {
51+
"min": ...,
52+
"max": ...,
53+
...
54+
},
55+
}
56+
```
57+
1958
## 0.4.3 (2022-02-08)
2059

2160
* add tile `buffer` option to match rio-tiler tile options (https://github.com/developmentseed/titiler/pull/427)

docs/advanced/tiler_factories.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ app.include_router(cog.router, tags=["STAC"])
4949
| `GET` | `/assets` | JSON | return the list of available assets
5050
| `GET` | `/info` | JSON ([Info][multiinfo_model]) | return assets basic info
5151
| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][multiinfo_geojson_model]) | return assets basic info as a GeoJSON feature
52-
| `GET` | `/statistics` | JSON ([Statistics][multistats_model]) | return assets statistics
53-
| `POST` | `/statistics` | GeoJSON ([Statistics][multistats_geojson_model]) | return assets statistics for a GeoJSON
52+
| `GET` | `/asset_statistics` | JSON ([Statistics][multistats_model]) | return per asset statistics
53+
| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return assets statistics (merged)
54+
| `POST` | `/statistics` | GeoJSON ([Statistics][multistats_geojson_model]) | return assets statistics for a GeoJSON (merged)
5455
| `GET` | `/tiles/[{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets
5556
| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document
5657
| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities

docs/endpoints/stac.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ app.include_router(stac.router, prefix="/stac", tags=["SpatioTemporal Asset Cata
2121
| `GET` | `/stac/bounds` | JSON | return STAC item bounds
2222
| `GET` | `/stac/info` | JSON | return asset's basic info
2323
| `GET` | `/stac/info.geojson` | GeoJSON | return asset's basic info as a GeoJSON feature
24+
| `GET` | `/stac/asset_statistics` | JSON | return per asset statistics
2425
| `GET` | `/stac/statistics` | JSON | return asset's statistics
2526
| `POST` | `/stac/statistics` | GeoJSON | return asset's statistics for a GeoJSON
2627
| `GET` | `/stac/tiles/[{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets
@@ -285,11 +286,36 @@ Example:
285286

286287
### Statistics
287288

289+
`:endpoint:/stac/asset_statistics - [GET]`
290+
291+
- QueryParams:
292+
- **url** (str): STAC Item URL. **Required**
293+
- **assets** (array[str]): asset names. Default to all available assets.
294+
- **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1;2;3`).
295+
- **asset_expression** (array[str]): Per asset band math expression (e.g `Asset1|b1\*b2`).
296+
- **max_size** (int): Max image size from which to calculate statistics, default is 1024.
297+
- **height** (int): Force image height from which to calculate statistics.
298+
- **width** (int): Force image width from which to calculate statistics.
299+
- **nodata** (str, int, float): Overwrite internal Nodata value.
300+
- **unscale** (bool): Apply dataset internal Scale/Offset.
301+
- **resampling** (str): rasterio resampling method. Default is `nearest`.
302+
- **categorical** (bool): Return statistics for categorical dataset, default is false.
303+
- **c** (array[float]): Pixels values for categories.
304+
- **p** (array[int]): Percentile values.
305+
- **histogram_bins** (str): Histogram bins.
306+
- **histogram_range** (str): Comma (',') delimited Min,Max histogram bounds
307+
308+
Example:
309+
310+
- `https://myendpoint/stac/statistics?url=https://somewhere.com/item.json&assets=B01&categorical=true&c=1&c=2&c=3&p=2&p98`
311+
312+
288313
`:endpoint:/stac/statistics - [GET]`
289314

290315
- QueryParams:
291316
- **url** (str): STAC Item URL. **Required**
292317
- **assets** (array[str]): asset names. Default to all available assets.
318+
- **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1/Asset2`).
293319
- **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1;2;3`).
294320
- **asset_expression** (array[str]): Per asset band math expression (e.g `Asset1|b1\*b2`).
295321
- **max_size** (int): Max image size from which to calculate statistics, default is 1024.

src/titiler/application/titiler/application/routers/stac.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def stac_demo(request: Request):
2525
"request": request,
2626
"tilejson_endpoint": stac.url_for(request, "tilejson"),
2727
"info_endpoint": stac.url_for(request, "info"),
28-
"statistics_endpoint": stac.url_for(request, "statistics"),
28+
"statistics_endpoint": stac.url_for(request, "asset_statistics"),
2929
},
3030
media_type="text/html",
3131
)

src/titiler/core/tests/test_factories.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ def test_MultiBaseTilerFactory(rio):
609609
rio.open = mock_rasterio_open
610610

611611
stac = MultiBaseTilerFactory(reader=STACReader)
612-
assert len(stac.router.routes) == 26
612+
assert len(stac.router.routes) == 27
613613

614614
app = FastAPI()
615615
app.include_router(stac.router)
@@ -691,7 +691,9 @@ def test_MultiBaseTilerFactory(rio):
691691
assert meta["count"] == 3
692692

693693
# GET - statistics
694-
response = client.get(f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09")
694+
response = client.get(
695+
f"/asset_statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09"
696+
)
695697
assert response.status_code == 200
696698
assert response.headers["content-type"] == "application/json"
697699
resp = response.json()
@@ -714,9 +716,8 @@ def test_MultiBaseTilerFactory(rio):
714716
"percentile_2",
715717
"percentile_98",
716718
}
717-
718719
response = client.get(
719-
f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&asset_bidx=B01|1&asset_bidx=B09|1"
720+
f"/asset_statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&asset_bidx=B01|1&asset_bidx=B09|1"
720721
)
721722
assert response.status_code == 200
722723
assert response.headers["content-type"] == "application/json"
@@ -725,6 +726,40 @@ def test_MultiBaseTilerFactory(rio):
725726
assert resp["B01"]["1"]
726727
assert resp["B09"]["1"]
727728

729+
response = client.get(f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09")
730+
assert response.status_code == 200
731+
assert response.headers["content-type"] == "application/json"
732+
resp = response.json()
733+
assert list(resp) == ["B01_1", "B09_1"]
734+
assert set(resp["B01_1"].keys()) == {
735+
"min",
736+
"max",
737+
"mean",
738+
"count",
739+
"sum",
740+
"std",
741+
"median",
742+
"majority",
743+
"minority",
744+
"unique",
745+
"histogram",
746+
"valid_percent",
747+
"masked_pixels",
748+
"valid_pixels",
749+
"percentile_2",
750+
"percentile_98",
751+
}
752+
753+
response = client.get(
754+
f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&asset_bidx=B01|1&asset_bidx=B09|1"
755+
)
756+
assert response.status_code == 200
757+
assert response.headers["content-type"] == "application/json"
758+
resp = response.json()
759+
assert len(resp) == 2
760+
assert resp["B01_1"]
761+
assert resp["B09_1"]
762+
728763
stac_feature = {
729764
"type": "FeatureCollection",
730765
"features": [

src/titiler/core/titiler/core/dependencies.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,24 @@ def __post_init__(self):
237237
}
238238

239239

240+
@dataclass
241+
class AssetsBidxExprParamsOptional(AssetsBidxExprParams):
242+
"""Assets, Band Indexes and Expression parameters but with no requirement."""
243+
244+
def __post_init__(self):
245+
"""Post Init."""
246+
if self.asset_indexes:
247+
self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore
248+
idx.split("|")[0]: list(map(int, idx.split("|")[1].split(",")))
249+
for idx in self.asset_indexes
250+
}
251+
252+
if self.asset_expression:
253+
self.asset_expression: Dict[str, str] = { # type: ignore
254+
idx.split("|")[0]: idx.split("|")[1] for idx in self.asset_expression
255+
}
256+
257+
240258
@dataclass
241259
class AssetsBidxParams(AssetsParams):
242260
"""asset and extra."""

src/titiler/core/titiler/core/factory.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from titiler.core.dependencies import (
1818
AssetsBidxExprParams,
19+
AssetsBidxExprParamsOptional,
1920
AssetsBidxParams,
2021
AssetsParams,
2122
BandsExprParams,
@@ -1055,7 +1056,7 @@ def statistics(self): # noqa: C901
10551056

10561057
# GET endpoint
10571058
@self.router.get(
1058-
"/statistics",
1059+
"/asset_statistics",
10591060
response_class=JSONResponse,
10601061
response_model=MultiBaseStatistics,
10611062
responses={
@@ -1065,15 +1066,15 @@ def statistics(self): # noqa: C901
10651066
}
10661067
},
10671068
)
1068-
def statistics(
1069+
def asset_statistics(
10691070
src_path=Depends(self.path_dependency),
10701071
asset_params=Depends(AssetsBidxParams),
10711072
dataset_params=Depends(self.dataset_dependency),
10721073
image_params=Depends(self.img_dependency),
10731074
stats_params=Depends(self.stats_dependency),
10741075
histogram_params=Depends(self.histogram_dependency),
10751076
):
1076-
"""Create image from a geojson feature."""
1077+
"""Per Asset statistics"""
10771078
with rasterio.Env(**self.gdal_config):
10781079
with self.reader(src_path) as src_dst:
10791080
return src_dst.statistics(
@@ -1084,6 +1085,43 @@ def statistics(
10841085
hist_options={**histogram_params},
10851086
)
10861087

1088+
# MultiBaseReader merged statistics
1089+
# https://github.com/cogeotiff/rio-tiler/blob/master/rio_tiler/io/base.py#L455-L468
1090+
# GET endpoint
1091+
@self.router.get(
1092+
"/statistics",
1093+
response_class=JSONResponse,
1094+
response_model=Statistics,
1095+
responses={
1096+
200: {
1097+
"content": {"application/json": {}},
1098+
"description": "Return dataset's statistics.",
1099+
}
1100+
},
1101+
)
1102+
def statistics(
1103+
src_path=Depends(self.path_dependency),
1104+
layer_params=Depends(AssetsBidxExprParamsOptional),
1105+
dataset_params=Depends(self.dataset_dependency),
1106+
image_params=Depends(self.img_dependency),
1107+
stats_params=Depends(self.stats_dependency),
1108+
histogram_params=Depends(self.histogram_dependency),
1109+
):
1110+
"""Merged assets statistics."""
1111+
with rasterio.Env(**self.gdal_config):
1112+
with self.reader(src_path) as src_dst:
1113+
# Default to all available assets
1114+
if not layer_params.assets and not layer_params.expression:
1115+
layer_params.assets = src_dst.assets
1116+
1117+
return src_dst.merged_statistics(
1118+
**layer_params,
1119+
**image_params,
1120+
**dataset_params,
1121+
**stats_params,
1122+
hist_options={**histogram_params},
1123+
)
1124+
10871125
# POST endpoint
10881126
@self.router.post(
10891127
"/statistics",
@@ -1102,7 +1140,7 @@ def geojson_statistics(
11021140
..., description="GeoJSON Feature or FeatureCollection."
11031141
),
11041142
src_path=Depends(self.path_dependency),
1105-
asset_params=Depends(AssetsBidxParams),
1143+
layer_params=Depends(AssetsBidxExprParamsOptional),
11061144
dataset_params=Depends(self.dataset_dependency),
11071145
image_params=Depends(self.img_dependency),
11081146
stats_params=Depends(self.stats_dependency),
@@ -1112,15 +1150,15 @@ def geojson_statistics(
11121150
with rasterio.Env(**self.gdal_config):
11131151
with self.reader(src_path) as src_dst:
11141152
# Default to all available assets
1115-
if not asset_params.assets:
1116-
asset_params.assets = src_dst.assets
1153+
if not layer_params.assets and not layer_params.expression:
1154+
layer_params.assets = src_dst.assets
11171155

11181156
# TODO: stream features for FeatureCollection
11191157
if isinstance(geojson, FeatureCollection):
11201158
for feature in geojson:
11211159
data = src_dst.feature(
11221160
feature.dict(exclude_none=True),
1123-
**asset_params,
1161+
**layer_params,
11241162
**image_params,
11251163
**dataset_params,
11261164
)
@@ -1148,7 +1186,7 @@ def geojson_statistics(
11481186
else: # simple feature
11491187
data = src_dst.feature(
11501188
geojson.dict(exclude_none=True),
1151-
**asset_params,
1189+
**layer_params,
11521190
**image_params,
11531191
**dataset_params,
11541192
)

0 commit comments

Comments
 (0)