Base classes and custom readers
rio-tiler
provides multiple abstract base
classes from which it derives its
main readers: Reader
and
STACReader
. You can also use these classes to build
custom readers.
Abstract Base Classes¶
BaseReader¶
Main rio_tiler.io
Abstract Base Class.
Minimal Arguments¶
- input: Input
-
tms: The TileMatrixSet define which default projection and map grid the reader uses. Defaults to WebMercatorQuad.
-
bounds: Dataset's bounding box. Not in the
__init__
method. - crs: dataset's crs. Not in the
__init__
method. - transform: dataset's Affine transform. Not in the
__init__
method. - height: dataset's height. Not in the
__init__
method. - width: dataset's width. Not in the
__init__
method.
Important
BaseClass Arguments outside the __init__
method and without default value HAVE TO be set in the __attrs_post_init__
step.
Methods¶
- tile_exists(tile_x: int, tile_y: int, tile_z: int): Check if a given tile (for the input TMS) intersect the dataset bounds.
- get_geographic_bounds(crs: rasterio.crs.CRS): dataset's bounds in Geographic CRS (calculated from
self.bounds
andself.crs
).
Abstract Methods¶
Abstract methods, are method that HAVE TO be implemented in the child class.
- info: returns dataset info (
rio_tiler.models.Info
) - statistics: returns dataset band statistics (
Dict[str, rio_tiler.models.BandStatistics]
) - tile: reads data for a specific XYZ slippy map indexes (
rio_tiler.models.ImageData
) - part: reads specific part of a dataset (
rio_tiler.models.ImageData
) - preview: creates an overview of a dataset (
rio_tiler.models.ImageData
) - point: reads pixel value for a specific point (
List
) - feature: reads data for a geojson feature (
rio_tiler.models.ImageData
)
Example: Reader
MultiBaseReader¶
The goal of the MultiBaseReader
is to enable joining results from multiple files (e.g STAC).
The MultiBaseReader
has the same attributes/properties/methods as the BaseReader
.
Example: STACReader
import os
import pathlib
from typing import Dict, Type
import attr
from morecantile import TileMatrixSet
from rio_tiler.io.base import MultiBaseReader
from rio_tiler.io import Reader, BaseReader
from rio_tiler.constants import WEB_MERCATOR_TMS
from rio_tiler.models import Info
from rio_tiler.types import AssetInfo
from rio_tiler.errors import InvalidAssetName
@attr.s
class AssetFileReader(MultiBaseReader):
input: str = attr.ib()
prefix: str = attr.ib() # we add a custom attribute
# because we add another attribute (prefix) we need to
# re-specify the other attribute for the class
reader: Type[BaseReader] = attr.ib(default=Reader)
reader_options: Dict = attr.ib(factory=dict)
tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
# we place min/max zoom in __init__
minzoom: int = attr.ib(default=None)
maxzoom: int = attr.ib(default=None)
def __attrs_post_init__(self):
"""Parse Sceneid and get grid bounds."""
self.assets = sorted(
[p.stem.split("_")[1] for p in pathlib.Path(self.input).glob(f"*{self.prefix}*.tif")]
)
with self.reader(self._get_asset_info(self.assets[0])["url"]) as cog:
self.bounds = cog.bounds
self.crs = cog.crs
self.transform = cog.transform
self.height = cog.height
self.width = cog.width
if self.minzoom is None:
self.minzoom = cog.minzoom
if self.maxzoom is None:
self.maxzoom = cog.maxzoom
def _get_asset_info(self, asset: str) -> AssetInfo:
"""Validate band's name and return band's url."""
if asset not in self.assets:
raise InvalidAssetName(
f"'{asset}' is not valid, should be one of {self.assets}"
)
return AssetInfo(url=os.path.join(self.input, f"{self.prefix}{asset}.tif"))
# we have a directoty with "scene_b1.tif", "scene_b2.tif"
with AssetFileReader(input="my_dir/", prefix="scene_") as cr:
print(cr.assets)
>>> ['band1', 'band2']
info = cr.info(assets=("band1", "band2"))
# MultiBaseReader returns a Dict
assert isinstance(info, dict)
print(list(info))
>>> ['band1', 'band2']
assert isinstance(info["band1"], Info)
print(info["band1"].model_dump_json(exclude_none=True))
>>> {
"bounds": [
199980,
2690220,
309780,
2800020
],
"crs": "http://www.opengis.net/def/crs/EPSG/0/32629",
"band_metadata": [
[
"b1",
{}
]
],
"band_descriptions": [
[
"b1",
""
]
],
"dtype": "uint16",
"nodata_type": "Nodata",
"colorinterp": [
"gray"
],
"scales": [
1
],
"offsets": [
0
],
"driver": "GTiff",
"count": 1,
"width": 549,
"height": 549,
"overviews": [
2
],
"nodata_value": 0
}
img = cr.tile(238, 218, 9, assets=("band1", "band2"))
print(img.assets)
>>> ['my_dir/scene_band1.tif', 'my_dir/scene_band2.tif']
# Each assets have 1 bands, so when combining each img we get a (2, 256, 256) array.
print(img.data.shape)
>>> (2, 256, 256)
MultiBandsReader¶
Almost as the previous MultiBaseReader
, the MultiBandsReader
children will merge results extracted from different file but taking each file as individual bands.
The MultiBaseReader
has the same attributes/properties/methods as the BaseReader
.
Example
import os
import pathlib
from typing import Dict, Type
import attr
from morecantile import TileMatrixSet
from rio_tiler.io.base import MultiBandReader
from rio_tiler.io import COGReader, BaseReader
from rio_tiler.constants import WEB_MERCATOR_TMS
@attr.s
class BandFileReader(MultiBandReader):
input: str = attr.ib()
prefix: str = attr.ib() # we add a custom attribute
# because we add another attribute (prefix) we need to
# re-specify the other attribute for the class
reader: Type[BaseReader] = attr.ib(default=COGReader)
reader_options: Dict = attr.ib(factory=dict)
tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
# we place min/max zoom in __init__
minzoom: int = attr.ib(default=None)
maxzoom: int = attr.ib(default=None)
def __attrs_post_init__(self):
"""Parse Sceneid and get grid bounds."""
self.bands = sorted(
[p.stem.split("_")[1] for p in pathlib.Path(self.input).glob(f"*{self.prefix}*.tif")]
)
with self.reader(self._get_band_url(self.bands[0])) as cog:
self.bounds = cog.bounds
self.crs = cog.crs
self.transform = cog.transform
self.height = cog.height
self.width = cog.width
if self.minzoom is None:
self.minzoom = cog.minzoom
if self.maxzoom is None:
self.maxzoom = cog.maxzoom
def _get_band_url(self, band: str) -> str:
"""Validate band's name and return band's url."""
return os.path.join(self.input, f"{self.prefix}{band}.tif")
# we have a directoty with "scene_b1.tif", "scene_b2.tif"
with BandFileReader(input="my_dir/", prefix="scene_") as cr:
print(cr.bands)
>>> ['band1', 'band2']
print(cr.info(bands=("band1", "band2")).model_dump_json(exclude_none=True))
>>> {
"bounds": [
199980,
2690220,
309780,
2800020
],
"crs": "http://www.opengis.net/def/crs/EPSG/0/32629",
"band_metadata": [
[
"band1",
{}
],
[
"band2",
{}
]
],
"band_descriptions": [
[
"band1",
""
],
[
"band2",
""
]
],
"dtype": "uint16",
"nodata_type": "Nodata",
"colorinterp": [
"gray",
"gray"
]
}
img = cr.tile(238, 218, 9, bands=("band1", "band2"))
print(img.assets)
>>> ['my_dir/scene_band1.tif', 'my_dir/scene_band2.tif']
print(img.data.shape)
>>> (2, 256, 256)
Note: rio-tiler-pds
readers are built using the MultiBandReader
base class.
Custom Reader subclass¶
The example was created as a response to developmentseed/titiler?235. In short, the user needed a way to keep metadata information from an asset within a STAC item.
Sadly when we are using the STAC Reader we only keep the metadata about the item but not the assets metadata (because we built the STAC Reader with the idea that user might first want to merge assets together).
But rio-tiler has been designed to be easily customizable.
import attr
from rasterio.io import DatasetReader
from rio_tiler.io.stac import fetch, _to_pystac_item
from rio_tiler.io import Reader
import pystac
@attr.s
class CustomSTACReader(Reader):
"""Custom Reader support."""
# This will keep the STAC item info within the instance
item: pystac.Item = attr.ib(default=None, init=False)
def __attrs_post_init__(self):
"""Define _kwargs, open dataset and get info."""
# get STAC item URL and asset name
asset = self.input.split(":")[-1]
stac_url = self.input.replace(f":{asset}", "")
# Fetch the STAC item
self.item = pystac.Item.from_dict(fetch(stac_url), stac_url)
# Get asset url from the STAC Item
self.input = self.item.assets[asset].get_absolute_href()
super().__attrs_post_init__()
with CustomSTACReader("https://canada-spot-ortho.s3.amazonaws.com/canada_spot_orthoimages/canada_spot5_orthoimages/S5_2007/S5_11055_6057_20070622/S5_11055_6057_20070622.json:pan") as cog:
print(type(cog.dataset))
print(cog.input)
print(cog.nodata)
print(cog.bounds)
>>> rasterio.io.DatasetReader
>>> "https://canada-spot-ortho.s3.amazonaws.com/canada_spot_orthoimages/canada_spot5_orthoimages/S5_2007/S5_11055_6057_20070622/s5_11055_6057_20070622_p10_1_lcc00_cog.tif"
>>> 0
>>> (-869900.0, 1370200.0, -786360.0, 1453180.0)
In this CustomSTACReader
, we are using a custom path schema
in form of {item-url}:{asset-name}
. When creating an instance of CustomSTACReader
, we will do the following:
- Parse the input path to get the STAC url and asset name
- Fetch and parse the STAC item
- Construct a new
input
using the asset full url. - Fall back to the regular
Reader
initialization (usingsuper().__attrs_post_init__()
)
Simple Reader¶
from typing import Any, Dict, List
import attr
import rasterio
from rasterio.io import DatasetReader
from rio_tiler.io import BaseReader
from rio_tiler.models import BandStatistics, Info, ImageData
from morecantile import TileMatrixSet
from rio_tiler.constants import BBox, WEB_MERCATOR_TMS
@attr.s
class SimpleReader(BaseReader):
input: DatasetReader = attr.ib()
# We force tms to be outside the class __init__
tms: TileMatrixSet = attr.ib(init=False, default=WEB_MERCATOR_TMS)
def __attrs_post_init__(self):
# Set bounds and crs variable
self.bounds = self.input.bounds
self.crs = self.input.crs
self.transform = self.input.transform
self.height = self.input.height
self.width = self.input.width
@property
def minzoom(self):
"""Return dataset minzoom."""
return self._minzoom
@property
def maxzoom(self):
"""Return dataset maxzoom."""
return self._maxzoom
# implement all mandatory methods
def info(self) -> Info:
raise NotImplemented
def statistics(self, **kwargs: Any) -> Dict[str, BandStatistics]:
raise NotImplemented
def part(self, bbox: BBox, **kwargs: Any) -> ImageData:
raise NotImplemented
def preview(self, **kwargs: Any) -> ImageData:
raise NotImplemented
def point(self, lon: float, lat: float, **kwargs: Any) -> List:
raise NotImplemented
def feature(self, shape: Dict, **kwargs: Any) -> ImageData:
raise NotImplemented
def tile(self, tile_x: int, tile_y: int, tile_z: int, **kwargs: Any) -> ImageData:
if not self.tile_exists(tile_x, tile_y, tile_z):
raise TileOutsideBounds(
f"Tile {tile_z}/{tile_x}/{tile_y} is outside bounds"
)
tile_bounds = self.tms.xy_bounds(Tile(x=tile_x, y=tile_y, z=tile_z))
return reader.part(
self.input,
tile_bounds,
width=256,
height=256,
bounds_crs=tms.rasterio_crs,
dst_crs=tms.rasterio_crs,
**kwargs,
)
with rasterio.open("file.tif") as src:
with SimpleReader(src) as cog:
img = cog.tile(1, 1, 1)