From bf802478645928703dc34618b7aaf963f22eb6cc Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 16 Jul 2021 14:24:52 +0200 Subject: [PATCH 1/5] add POST crop endpoint using geojson feature --- docs/endpoints/cog.md | 34 +++++++++++- docs/endpoints/stac.md | 38 ++++++++++++- src/titiler/core/tests/test_factories.py | 47 ++++++++++++++-- src/titiler/core/titiler/core/factory.py | 68 +++++++++++++++++++++++- 4 files changed, 179 insertions(+), 8 deletions(-) diff --git a/docs/endpoints/cog.md b/docs/endpoints/cog.md index 307c768a0..2471612f0 100644 --- a/docs/endpoints/cog.md +++ b/docs/endpoints/cog.md @@ -19,6 +19,7 @@ Read Info/Metadata and create Web map Tiles from a **single** COG. The `cog` rou | `GET` | `/cog/point/{lon},{lat}` | JSON | return pixel value from a dataset | `GET` | `/cog/preview[.{format}]` | image/bin | create a preview image from a dataset | `GET` | `/cog/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset +| `POST` | `/cog/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering a dataset | `GET` | `/cog/validate` | JSON | validate a COG and return dataset info | `GET` | `/cog/viewer` | HTML | demo webpage @@ -106,13 +107,42 @@ Example: - **colormap**: JSON encoded custom Colormap. OPTIONAL - **resampling_method**: rasterio resampling method. Default is `nearest`. -Note: if `height` and `width` are provided `max_size` will be ignored. - Example: - `https://myendpoint/cog/crop/0,0,10,10.png?url=https://somewhere.com/mycog.tif` - `https://myendpoint/cog/crop/0,0,10,10.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` + +`:endpoint:/cog/crop[/{width}x{height}][].{format}] - [POST]` + +- Body: + - **feature**: A valida GeoJSON feature (Polygon or MultiPolygon) + +- PathParams: + - **height**: Force output image height. OPTIONAL + - **width**: Force output image width. OPTIONAL + - **format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. OPTIONAL + +- QueryParams: + - **url**: Cloud Optimized GeoTIFF URL. **REQUIRED** + - **bidx**: Comma (',') delimited band indexes. OPTIONAL + - **expression**: rio-tiler's band math expression (e.g B1/B2). OPTIONAL + - **nodata**: Overwrite internal Nodata value. OPTIONAL + - **max_size**: Max image size, default is 1024. OPTIONAL + - **rescale**: Comma (',') delimited Min,Max bounds. OPTIONAL + - **color_formula**: rio-color formula. OPTIONAL + - **colormap_name**: rio-tiler color map name. OPTIONAL + - **colormap**: JSON encoded custom Colormap. OPTIONAL + - **resampling_method**: rasterio resampling method. Default is `nearest`. + +Example: + +- `https://myendpoint/cog/crop?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/crop.png?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/crop/100x100.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` + +Note: if `height` and `width` are provided `max_size` will be ignored. + ### Point `:endpoint:/cog/point/{lon},{lat}` diff --git a/docs/endpoints/stac.md b/docs/endpoints/stac.md index 51c27a7e1..4e2a9a8cd 100644 --- a/docs/endpoints/stac.md +++ b/docs/endpoints/stac.md @@ -20,6 +20,7 @@ Read Info/Metadata and create Web map Tiles from a **single** STAC Item. The `s | `GET` | `/stac/point/{lon},{lat}` | JSON | return pixel value from a dataset | `GET` | `/stac/preview[.{format}]` | image/bin | create a preview image from a dataset | `GET` | `/stac/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset +| `POST` | `/stac/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering a STAC Item | `GET` | `/stac/viewer` | HTML | demo webpage (Not created by the factory) ## Description @@ -115,13 +116,46 @@ Example: ***assets** OR **expression** is required -Note: if `height` and `width` are provided `max_size` will be ignored. - Example: - `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01` - `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` + +`:endpoint:/stac/crop[/{width}x{height}][].{format}] - [POST]` + +- Body: + - **feature**: A valida GeoJSON feature (Polygon or MultiPolygon) + +- PathParams: + - **height**: Force output image height. OPTIONAL + - **width**: Force output image width. OPTIONAL + - **format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. OPTIONAL + +- QueryParams: + - **url**: STAC Item URL. **REQUIRED** + - **assets**: Comma (',') delimited asset names. OPTIONAL* + - **expression**: rio-tiler's band math expression (e.g B1/B2). OPTIONAL + - **bidx**: Comma (',') delimited band indexes. OPTIONAL + - **nodata**: Overwrite internal Nodata value. OPTIONAL + + - **max_size**: Max image size, default is 1024. OPTIONAL + - **rescale**: Comma (',') delimited Min,Max bounds. OPTIONAL + - **color_formula**: rio-color formula. OPTIONAL + - **colormap_name**: rio-tiler color map name. OPTIONAL + - **colormap**: JSON encoded custom Colormap. OPTIONAL + - **resampling_method**: rasterio resampling method. Default is `nearest`. + +***assets** OR **expression** is required + +Example: + +- `https://myendpoint/stac/crop?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/crop.png?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` + +Note: if `height` and `width` are provided `max_size` will be ignored. + ### Point `:endpoint:/cog/point/{lon},{lat}` diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 59bc7b3c3..f49f48b6c 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -31,7 +31,7 @@ def test_TilerFactory(): """Test TilerFactory class.""" cog = TilerFactory() - assert len(cog.router.routes) == 21 + assert len(cog.router.routes) == 24 assert cog.tms_dependency == TMSParams cog = TilerFactory(router_prefix="something", tms_dependency=WebMercatorTMSParams) @@ -272,6 +272,47 @@ def test_TilerFactory(): assert meta["dtype"] == "int16" assert meta["count"] == 1 + feature = json.dumps( + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-59.23828124999999, 74.16408546675687], + [-59.83154296874999, 73.15680773175981], + [-58.73291015624999, 72.88087095711504], + [-56.62353515625, 73.06104462497655], + [-55.17333984375, 73.41588526207096], + [-55.2392578125, 74.09799577518739], + [-56.88720703125, 74.2895142503942], + [-57.23876953124999, 74.30735341486248], + [-59.23828124999999, 74.16408546675687], + ] + ], + }, + } + ) + + response = client.post(f"/crop?url={DATA_DIR}/cog.tif", data=feature) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + + response = client.post(f"/crop.tif?url={DATA_DIR}/cog.tif", data=feature) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/tiff; application=geotiff" + meta = parse_img(response.content) + assert meta["dtype"] == "uint16" + assert meta["count"] == 2 + + response = client.post(f"/crop/100x100.jpeg?url={DATA_DIR}/cog.tif", data=feature) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/jpeg" + meta = parse_img(response.content) + assert meta["width"] == 100 + assert meta["height"] == 100 + @patch("rio_tiler.io.cogeo.rasterio") def test_MultiBaseTilerFactory(rio): @@ -279,7 +320,7 @@ def test_MultiBaseTilerFactory(rio): rio.open = mock_rasterio_open stac = MultiBaseTilerFactory(reader=STACReader) - assert len(stac.router.routes) == 22 + assert len(stac.router.routes) == 25 app = FastAPI() app.include_router(stac.router) @@ -359,7 +400,7 @@ def test_MultiBandTilerFactory(): """test MultiBandTilerFactory.""" bands = MultiBandTilerFactory(reader=BandFileReader) - assert len(bands.router.routes) == 22 + assert len(bands.router.routes) == 25 app = FastAPI() app.include_router(bands.router) diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 07da19838..37625169d 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -35,7 +35,7 @@ from titiler.core.resources.responses import GeoJSONResponse, XMLResponse from titiler.core.utils import Timer, bbox_to_feature -from fastapi import APIRouter, Depends, Path, Query +from fastapi import APIRouter, Body, Depends, Path, Query from starlette.requests import Request from starlette.responses import Response @@ -706,6 +706,72 @@ def part( return Response(content, media_type=format.mediatype, headers=headers) + @self.router.post( + r"/crop", **img_endpoint_params, + ) + @self.router.post( + r"/crop.{format}", **img_endpoint_params, + ) + @self.router.post( + r"/crop/{width}x{height}.{format}", **img_endpoint_params, + ) + def geojson_crop( + feature: Feature = Body(..., descriptiom="GeoJSON Feature."), + format: ImageType = Query( + None, description="Output image type. Default is auto." + ), + src_path=Depends(self.path_dependency), + layer_params=Depends(self.layer_dependency), + image_params=Depends(self.img_dependency), + dataset_params=Depends(self.dataset_dependency), + render_params=Depends(self.render_dependency), + colormap=Depends(self.colormap_dependency), + kwargs: Dict = Depends(self.additional_dependency), + ): + """Create image from a geojson feature.""" + timings = [] + headers: Dict[str, str] = {} + + with Timer() as t: + with rasterio.Env(**self.gdal_config): + with self.reader(src_path, **self.reader_options) as src_dst: + data = src_dst.feature( + feature.dict(exclude_none=True), + **layer_params.kwargs, + **image_params.kwargs, + **dataset_params.kwargs, + **kwargs, + ) + dst_colormap = getattr(src_dst, "colormap", None) + timings.append(("dataread", round(t.elapsed * 1000, 2))) + + with Timer() as t: + image = data.post_process( + in_range=render_params.rescale_range, + color_formula=render_params.color_formula, + ) + timings.append(("postprocess", round(t.elapsed * 1000, 2))) + + if not format: + format = ImageType.jpeg if data.mask.all() else ImageType.png + + with Timer() as t: + content = image.render( + add_mask=render_params.return_mask, + img_format=format.driver, + colormap=colormap or dst_colormap, + **format.profile, + **render_params.kwargs, + ) + timings.append(("format", round(t.elapsed * 1000, 2))) + + if OptionalHeader.server_timing in self.optional_headers: + headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in timings] + ) + + return Response(content, media_type=format.mediatype, headers=headers) + @dataclass class MultiBaseTilerFactory(TilerFactory): From 0329c77cf8c58dc4cf04dddc96e9d3b8babecf08 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 16 Jul 2021 14:28:51 +0200 Subject: [PATCH 2/5] update docs --- docs/advanced/tiler_factories.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/advanced/tiler_factories.md b/docs/advanced/tiler_factories.md index 19e03954b..01f40a225 100644 --- a/docs/advanced/tiler_factories.md +++ b/docs/advanced/tiler_factories.md @@ -23,8 +23,9 @@ app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) | `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document | `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/point/{lon},{lat}` | JSON | return pixel value from a dataset -| `GET` | `/preview[.{format}]` | image/bin | **Optional** - create a preview image from a dataset -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | **Optional** - create an image from part of a dataset +| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset +| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset +| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature ### `titiler.core.factory.MultiBaseTilerFactory` @@ -52,8 +53,9 @@ app.include_router(cog.router, tags=["STAC"]) | `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document | `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/point/{lon},{lat}` | JSON | return pixel value from a dataset -| `GET` | `/preview[.{format}]` | image/bin | **Optional** - create a preview image from a dataset -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | **Optional** - create an image from part of a dataset +| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset +| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset +| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature ### `titiler.core.factory.MultiBandTilerFactory` @@ -87,8 +89,9 @@ app.include_router(cog.router, tags=["Landsat"]) | `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document | `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/point/{lon},{lat}` | JSON | return pixel value from a dataset -| `GET` | `/preview[.{format}]` | image/bin | **Optional** - create a preview image from a dataset -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | **Optional** - create an image from part of a dataset +| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset +| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset +| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature ### `titiler.mosaic.factory.MosaicTilerFactory` From 00b7c5284201349f1f943947d09d02732cee3f79 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 16 Jul 2021 14:38:58 +0200 Subject: [PATCH 3/5] update changelog --- CHANGES.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 05ff72bde..eb6d19be8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Release Notes +## Unrelease + +### titiler.core + +* add `/crop` POST endpoint to return an image from a GeoJSON feature (https://github.com/developmentseed/titiler/pull/339) + + ## 0.3.3 (2021-06-29) ### titiler.core From 42a18b4037faa8e74df5a4dc96251d7391b0bec5 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Fri, 16 Jul 2021 14:59:16 +0200 Subject: [PATCH 4/5] fix failing tests --- src/titiler/application/tests/routes/test_demos.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/titiler/application/tests/routes/test_demos.py b/src/titiler/application/tests/routes/test_demos.py index 676cbb17b..5228a6858 100644 --- a/src/titiler/application/tests/routes/test_demos.py +++ b/src/titiler/application/tests/routes/test_demos.py @@ -3,7 +3,7 @@ def test_cog_viewer(app): """Test COG Viewer.""" - response = app.get("/cog/viewer") + response = app.get("/cog/viewer", headers={"accept-encoding": "gzip"}) assert response.status_code == 200 assert response.headers["content-type"] == "text/html; charset=utf-8" assert response.headers["content-encoding"] == "gzip" @@ -11,7 +11,7 @@ def test_cog_viewer(app): def test_stac_viewer(app): """Test STAC Viewer.""" - response = app.get("/stac/viewer") + response = app.get("/stac/viewer", headers={"accept-encoding": "gzip"}) assert response.status_code == 200 assert response.headers["content-type"] == "text/html; charset=utf-8" assert response.headers["content-encoding"] == "gzip" From 49509251df20821b3641c8c9176a6298a92f0f23 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Fri, 16 Jul 2021 15:04:14 +0200 Subject: [PATCH 5/5] Update docs/endpoints/stac.md --- docs/endpoints/stac.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/endpoints/stac.md b/docs/endpoints/stac.md index 4e2a9a8cd..d656be383 100644 --- a/docs/endpoints/stac.md +++ b/docs/endpoints/stac.md @@ -152,7 +152,7 @@ Example: - `https://myendpoint/stac/crop?url=https://somewhere.com/item.json&assets=B01` - `https://myendpoint/stac/crop.png?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/stac/crop/100x100.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` Note: if `height` and `width` are provided `max_size` will be ignored. @@ -275,4 +275,3 @@ Demonstration viewer added to the router created by the factory (https://github. Example: - `https://myendpoint/stac/viewer?url=https://somewhere.com/item.json` -