"""Implements the :stac-ext:`Raster Extension <raster>`."""
from __future__ import annotations
import warnings
from collections.abc import Iterable
from typing import (
Any,
Generic,
Literal,
TypeVar,
Union,
cast,
)
import pystac
from pystac.extensions import item_assets
from pystac.extensions.base import (
ExtensionManagementMixin,
PropertiesExtension,
SummariesExtension,
)
from pystac.extensions.hooks import ExtensionHooks
from pystac.utils import StringEnum, get_opt, get_required, map_opt
T = TypeVar("T", pystac.Asset, item_assets.AssetDefinition)
SCHEMA_URI = "https://stac-extensions.github.io/raster/v1.1.0/schema.json"
SCHEMA_URIS = [
"https://stac-extensions.github.io/raster/v1.0.0/schema.json",
SCHEMA_URI,
]
SCHEMA_STARTWITH = "https://stac-extensions.github.io/raster/"
BANDS_PROP = "raster:bands"
[docs]
class Sampling(StringEnum):
AREA = "area"
POINT = "point"
[docs]
class DataType(StringEnum):
INT8 = "int8"
INT16 = "int16"
INT32 = "int32"
INT64 = "int64"
UINT8 = "uint8"
UINT16 = "uint16"
UINT32 = "uint32"
UINT64 = "uint64"
FLOAT16 = "float16"
FLOAT32 = "float32"
FLOAT64 = "float64"
CINT16 = "cint16"
CINT32 = "cint32"
CFLOAT32 = "cfloat32"
CFLOAT64 = "cfloat64"
OTHER = "other"
[docs]
class NoDataStrings(StringEnum):
INF = "inf"
NINF = "-inf"
NAN = "nan"
[docs]
class Statistics:
"""Represents statistics information attached to a band in the raster extension.
Use Statistics.create to create a new Statistics instance.
"""
properties: dict[str, Any]
def __init__(self, properties: dict[str, float | None]) -> None:
self.properties = properties
[docs]
def apply(
self,
minimum: float | None = None,
maximum: float | None = None,
mean: float | None = None,
stddev: float | None = None,
valid_percent: float | None = None,
) -> None:
"""
Sets the properties for this raster Band.
Args:
minimum : Minimum value of all the pixels in the band.
maximum : Maximum value of all the pixels in the band.
mean : Mean value of all the pixels in the band.
stddev : Standard Deviation value of all the pixels in the band.
valid_percent : Percentage of valid (not nodata) pixel.
"""
self.minimum = minimum
self.maximum = maximum
self.mean = mean
self.stddev = stddev
self.valid_percent = valid_percent
[docs]
@classmethod
def create(
cls,
minimum: float | None = None,
maximum: float | None = None,
mean: float | None = None,
stddev: float | None = None,
valid_percent: float | None = None,
) -> Statistics:
"""
Creates a new band.
Args:
minimum : Minimum value of all the pixels in the band.
maximum : Maximum value of all the pixels in the band.
mean : Mean value of all the pixels in the band.
stddev : Standard Deviation value of all the pixels in the band.
valid_percent : Percentage of valid (not nodata) pixel.
"""
b = cls({})
b.apply(
minimum=minimum,
maximum=maximum,
mean=mean,
stddev=stddev,
valid_percent=valid_percent,
)
return b
@property
def minimum(self) -> float | None:
"""Get or sets the minimum pixel value
Returns:
Optional[float]
"""
return self.properties.get("minimum")
@minimum.setter
def minimum(self, v: float | None) -> None:
if v is not None:
self.properties["minimum"] = v
else:
self.properties.pop("minimum", None)
@property
def maximum(self) -> float | None:
"""Get or sets the maximum pixel value
Returns:
Optional[float]
"""
return self.properties.get("maximum")
@maximum.setter
def maximum(self, v: float | None) -> None:
if v is not None:
self.properties["maximum"] = v
else:
self.properties.pop("maximum", None)
@property
def mean(self) -> float | None:
"""Get or sets the mean pixel value
Returns:
Optional[float]
"""
return self.properties.get("mean")
@mean.setter
def mean(self, v: float | None) -> None:
if v is not None:
self.properties["mean"] = v
else:
self.properties.pop("mean", None)
@property
def stddev(self) -> float | None:
"""Get or sets the standard deviation pixel value
Returns:
Optional[float]
"""
return self.properties.get("stddev")
@stddev.setter
def stddev(self, v: float | None) -> None:
if v is not None:
self.properties["stddev"] = v
else:
self.properties.pop("stddev", None)
@property
def valid_percent(self) -> float | None:
"""Get or sets the Percentage of valid (not nodata) pixel
Returns:
Optional[float]
"""
return self.properties.get("valid_percent")
@valid_percent.setter
def valid_percent(self, v: float | None) -> None:
if v is not None:
self.properties["valid_percent"] = v
else:
self.properties.pop("valid_percent", None)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Returns these statistics as a dictionary.
Returns:
dict: The serialization of the Statistics.
"""
return self.properties
[docs]
@staticmethod
def from_dict(d: dict[str, Any]) -> Statistics:
"""Constructs an Statistics from a dict.
Returns:
Statistics: The Statistics deserialized from the JSON dict.
"""
return Statistics(properties=d)
[docs]
class Histogram:
"""Represents pixel distribution information attached to a band in the raster
extension.
Use Band.create to create a new Band.
"""
properties: dict[str, Any]
def __init__(self, properties: dict[str, Any]) -> None:
self.properties = properties
[docs]
def apply(
self,
count: int,
min: float,
max: float,
buckets: list[int],
) -> None:
"""
Sets the properties for this raster Band.
Args:
count : number of buckets of the distribution.
min : minimum value of the distribution.
Also the mean value of the first bucket.
max : maximum value of the distribution.
Also the mean value of the last bucket.
buckets : Array of integer indicating the number
of pixels included in the bucket.
"""
self.count = count
self.min = min
self.max = max
self.buckets = buckets
[docs]
@classmethod
def create(
cls,
count: int,
min: float,
max: float,
buckets: list[int],
) -> Histogram:
"""
Creates a new band.
Args:
count : number of buckets of the distribution.
min : minimum value of the distribution.
Also the mean value of the first bucket.
max : maximum value of the distribution.
Also the mean value of the last bucket.
buckets : Array of integer indicating the number
of pixels included in the bucket.
"""
b = cls({})
b.apply(
count=count,
min=min,
max=max,
buckets=buckets,
)
return b
@property
def count(self) -> int:
"""Get or sets the number of buckets of the distribution.
Returns:
int
"""
return get_required(self.properties.get("count"), self, "count")
@count.setter
def count(self, v: int) -> None:
self.properties["count"] = v
@property
def min(self) -> float:
"""Get or sets the minimum value of the distribution.
Returns:
float
"""
return get_required(self.properties.get("min"), self, "min")
@min.setter
def min(self, v: float) -> None:
self.properties["min"] = v
@property
def max(self) -> float:
"""Get or sets the maximum value of the distribution.
Returns:
float
"""
return get_required(self.properties.get("max"), self, "max")
@max.setter
def max(self, v: float) -> None:
self.properties["max"] = v
@property
def buckets(self) -> list[int]:
"""Get or sets the Array of integer indicating
the number of pixels included in the bucket.
Returns:
List[int]
"""
return get_required(self.properties.get("buckets"), self, "buckets")
@buckets.setter
def buckets(self, v: list[int]) -> None:
self.properties["buckets"] = v
[docs]
def to_dict(self) -> dict[str, Any]:
"""Returns this histogram as a dictionary.
Returns:
dict: The serialization of the Histogram.
"""
return self.properties
[docs]
@staticmethod
def from_dict(d: dict[str, Any]) -> Histogram:
"""Constructs an Histogram from a dict.
Returns:
Histogram: The Histogram deserialized from the JSON dict.
"""
return Histogram(properties=d)
[docs]
class RasterBand:
"""Represents a Raster Band information attached to an Item
that implements the raster extension.
Use Band.create to create a new Band.
"""
properties: dict[str, Any]
def __init__(self, properties: dict[str, Any]) -> None:
self.properties = properties
[docs]
def apply(
self,
nodata: float | NoDataStrings | None = None,
sampling: Sampling | None = None,
data_type: DataType | None = None,
bits_per_sample: float | None = None,
spatial_resolution: float | None = None,
statistics: Statistics | None = None,
unit: str | None = None,
scale: float | None = None,
offset: float | None = None,
histogram: Histogram | None = None,
) -> None:
"""
Sets the properties for this raster Band.
Args:
nodata : Pixel values used to identify pixels that are nodata in the assets.
sampling : One of area or point. Indicates whether a pixel value should be
assumed to represent a sampling over the region of the pixel or a point
sample at the center of the pixel.
data_type :The data type of the band.
One of the data types as described in the
:stac-ext:`Raster Data Types <raster/#data-types> docs`.
bits_per_sample : The actual number of bits used for this band.
Normally only present when the number of bits is non-standard for the
datatype, such as when a 1 bit TIFF is represented as byte
spatial_resolution : Average spatial resolution (in meters) of the pixels in
the band.
statistics: Statistics of all the pixels in the band
unit: unit denomination of the pixel value
scale: multiplicator factor of the pixel value to transform into the value
(i.e. translate digital number to reflectance).
offset: number to be added to the pixel value (after scaling) to transform
into the value (i.e. translate digital number to reflectance).
histogram: Histogram distribution information of the pixels values in the
band
"""
self.nodata = nodata
self.sampling = sampling
self.data_type = data_type
self.bits_per_sample = bits_per_sample
self.spatial_resolution = spatial_resolution
self.statistics = statistics
self.unit = unit
self.scale = scale
self.offset = offset
self.histogram = histogram
[docs]
@classmethod
def create(
cls,
nodata: float | NoDataStrings | None = None,
sampling: Sampling | None = None,
data_type: DataType | None = None,
bits_per_sample: float | None = None,
spatial_resolution: float | None = None,
statistics: Statistics | None = None,
unit: str | None = None,
scale: float | None = None,
offset: float | None = None,
histogram: Histogram | None = None,
) -> RasterBand:
"""
Creates a new band.
Args:
nodata : Pixel values used to identify pixels that are nodata in the assets.
sampling : One of area or point. Indicates whether a pixel value should be
assumed to represent a sampling over the region of the pixel or a point
sample at the center of the pixel.
data_type :The data type of the band.
One of the data types as described in the
:stac-ext:`Raster Data Types <raster/#data-types> docs`.
bits_per_sample : The actual number of bits used for this band.
Normally only present when the number of bits is non-standard for the
datatype, such as when a 1 bit TIFF is represented as byte
spatial_resolution : Average spatial resolution (in meters) of the pixels in
the band.
statistics: Statistics of all the pixels in the band
unit: unit denomination of the pixel value
scale: multiplicator factor of the pixel value to transform into the value
(i.e. translate digital number to reflectance).
offset: number to be added to the pixel value (after scaling) to transform
into the value (i.e. translate digital number to reflectance).
histogram: Histogram distribution information of the pixels values in the
band
"""
b = cls({})
b.apply(
nodata=nodata,
sampling=sampling,
data_type=data_type,
bits_per_sample=bits_per_sample,
spatial_resolution=spatial_resolution,
statistics=statistics,
unit=unit,
scale=scale,
offset=offset,
histogram=histogram,
)
return b
@property
def nodata(self) -> float | NoDataStrings | None:
"""Get or sets the nodata pixel value
Returns:
Optional[float]
"""
return self.properties.get("nodata")
@nodata.setter
def nodata(self, v: float | NoDataStrings | None) -> None:
if v is not None:
self.properties["nodata"] = v
else:
self.properties.pop("nodata", None)
@property
def sampling(self) -> Sampling | None:
"""Get or sets the property indicating whether a pixel value should be assumed
to represent a sampling over the region of the pixel or a point sample
at the center of the pixel.
Returns:
Optional[Sampling]
"""
return self.properties.get("sampling")
@sampling.setter
def sampling(self, v: Sampling | None) -> None:
if v is not None:
self.properties["sampling"] = v
else:
self.properties.pop("sampling", None)
@property
def data_type(self) -> DataType | None:
"""Get or sets the data type of the band.
Returns:
Optional[DataType]
"""
return self.properties.get("data_type")
@data_type.setter
def data_type(self, v: DataType | None) -> None:
if v is not None:
self.properties["data_type"] = v
else:
self.properties.pop("data_type", None)
@property
def bits_per_sample(self) -> float | None:
"""Get or sets the actual number of bits used for this band.
Returns:
float
"""
return self.properties.get("bits_per_sample")
@bits_per_sample.setter
def bits_per_sample(self, v: float | None) -> None:
if v is not None:
self.properties["bits_per_sample"] = v
else:
self.properties.pop("bits_per_sample", None)
@property
def spatial_resolution(self) -> float | None:
"""Get or sets the average spatial resolution (in meters) of the pixels in the
band.
Returns:
[float]
"""
return self.properties.get("spatial_resolution")
@spatial_resolution.setter
def spatial_resolution(self, v: float | None) -> None:
if v is not None:
self.properties["spatial_resolution"] = v
else:
self.properties.pop("spatial_resolution", None)
@property
def statistics(self) -> Statistics | None:
"""Get or sets the average spatial resolution (in meters) of the pixels in the
band.
Returns:
[Statistics]
"""
return Statistics.from_dict(get_opt(self.properties.get("statistics")))
@statistics.setter
def statistics(self, v: Statistics | None) -> None:
if v is not None:
self.properties["statistics"] = v.to_dict()
else:
self.properties.pop("statistics", None)
@property
def unit(self) -> str | None:
"""Get or sets the unit denomination of the pixel value
Returns:
[str]
"""
return self.properties.get("unit")
@unit.setter
def unit(self, v: str | None) -> None:
if v is not None:
self.properties["unit"] = v
else:
self.properties.pop("unit", None)
@property
def scale(self) -> float | None:
"""Get or sets the multiplicator factor of the pixel value to transform
into the value (i.e. translate digital number to reflectance).
Returns:
[float]
"""
return self.properties.get("scale")
@scale.setter
def scale(self, v: float | None) -> None:
if v is not None:
self.properties["scale"] = v
else:
self.properties.pop("scale", None)
@property
def offset(self) -> float | None:
"""Get or sets the number to be added to the pixel value (after scaling)
to transform into the value (i.e. translate digital number to reflectance).
Returns:
[float]
"""
return self.properties.get("offset")
@offset.setter
def offset(self, v: float | None) -> None:
if v is not None:
self.properties["offset"] = v
else:
self.properties.pop("offset", None)
@property
def histogram(self) -> Histogram | None:
"""Get or sets the histogram distribution information of the pixels values in
the band.
Returns:
[Histogram]
"""
return Histogram.from_dict(get_opt(self.properties.get("histogram")))
@histogram.setter
def histogram(self, v: Histogram | None) -> None:
if v is not None:
self.properties["histogram"] = v.to_dict()
else:
self.properties.pop("histogram", None)
def __repr__(self) -> str:
return "<Raster Band>"
[docs]
def to_dict(self) -> dict[str, Any]:
"""Returns this band as a dictionary.
Returns:
dict: The serialization of the Band.
"""
return self.properties
[docs]
class RasterExtension(
Generic[T],
PropertiesExtension,
ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]],
):
"""An abstract class that can be used to extend the properties of an
:class:`~pystac.Item`, :class:`~pystac.Asset`, or
:class:`~pystac.extension.item_assets.AssetDefinition` with properties from
the :stac-ext:`Raster Extension <raster>`. This class is generic over
the type of STAC Object to be extended (e.g. :class:`~pystac.Item`,
:class:`~pystac.Asset`).
This class will generally not be used directly. Instead, use the concrete
implementation associated with the STAC Object you want to extend (e.g.
:class:`~ItemRasterExtension` to extend an :class:`~pystac.Item`). You may
prefer to use the `ext` class method of this class to construct the correct
instance type for you.
"""
name: Literal["raster"] = "raster"
properties: dict[str, Any]
"""The :class:`~pystac.Asset` fields, including extension properties."""
[docs]
def apply(self, bands: list[RasterBand]) -> None:
"""Applies raster extension properties to the extended :class:`pystac.Item` or
:class:`pystac.Asset`.
Args:
bands : a list of :class:`~pystac.RasterBand` objects that represent
the available raster bands.
"""
self.bands = bands
@property
def bands(self) -> list[RasterBand] | None:
"""Gets or sets a list of available bands where each item is a
:class:`~RasterBand` object (or ``None`` if no bands have been set). If not
available the field should not be provided.
"""
return self._get_bands()
@bands.setter
def bands(self, v: list[RasterBand] | None) -> None:
self._set_property(
BANDS_PROP, map_opt(lambda bands: [b.to_dict() for b in bands], v)
)
def _get_bands(self) -> list[RasterBand] | None:
return map_opt(
lambda bands: [RasterBand(b) for b in bands],
self._get_property(BANDS_PROP, list[dict[str, Any]]),
)
[docs]
@classmethod
def get_schema_uri(cls) -> str:
return SCHEMA_URI
[docs]
@classmethod
def get_schema_uris(cls) -> list[str]:
warnings.warn(
"get_schema_uris is deprecated and will be removed in v2",
DeprecationWarning,
)
return SCHEMA_URIS
[docs]
@classmethod
def ext(cls, obj: T, add_if_missing: bool = False) -> RasterExtension[T]:
"""Extends the given STAC Object with properties from the :stac-ext:`Raster
Extension <raster>`.
This extension can be applied to instances of :class:`~pystac.Asset`.
Raises:
pystac.ExtensionTypeError : If an invalid object type is passed.
"""
if isinstance(obj, pystac.Asset):
cls.ensure_owner_has_extension(obj, add_if_missing)
return cast(RasterExtension[T], AssetRasterExtension(obj))
elif isinstance(obj, item_assets.AssetDefinition):
cls.ensure_owner_has_extension(obj, add_if_missing)
return cast(RasterExtension[T], ItemAssetsRasterExtension(obj))
else:
raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
[docs]
@classmethod
def summaries(
cls, obj: pystac.Collection, add_if_missing: bool = False
) -> SummariesRasterExtension:
cls.ensure_has_extension(obj, add_if_missing)
return SummariesRasterExtension(obj)
[docs]
class AssetRasterExtension(RasterExtension[pystac.Asset]):
asset_href: str
"""The ``href`` value of the :class:`~pystac.Asset` being extended."""
properties: dict[str, Any]
"""The :class:`~pystac.Asset` fields, including extension properties."""
additional_read_properties: Iterable[dict[str, Any]] | None = None
"""If present, this will be a list containing 1 dictionary representing the
properties of the owning :class:`~pystac.Item`."""
def __init__(self, asset: pystac.Asset):
self.asset_href = asset.href
self.properties = asset.extra_fields
if asset.owner and isinstance(asset.owner, pystac.Item):
self.additional_read_properties = [asset.owner.properties]
def __repr__(self) -> str:
return f"<AssetRasterExtension Asset href={self.asset_href}>"
[docs]
class ItemAssetsRasterExtension(RasterExtension[item_assets.AssetDefinition]):
asset_definition: item_assets.AssetDefinition
"""A reference to the :class:`~pystac.extensions.item_assets.AssetDefinition`
being extended."""
properties: dict[str, Any]
"""The :class:`~pystac.extensions.item_assets.AssetDefinition` fields, including
extension properties."""
def __init__(self, item_asset: item_assets.AssetDefinition):
self.properties = item_asset.properties
self.asset_definition = item_asset
def __repr__(self) -> str:
return "<ItemAssetsRasterExtension AssetDefinition={}>".format(
self.asset_definition
)
[docs]
class SummariesRasterExtension(SummariesExtension):
"""A concrete implementation of :class:`~SummariesExtension` that extends
the ``summaries`` field of a :class:`~pystac.Collection` to include properties
defined in the :stac-ext:`Raster Extension <raster>`.
"""
@property
def bands(self) -> list[RasterBand] | None:
"""Get or sets a list of :class:`~pystac.Band` objects that represent
the available bands.
"""
return map_opt(
lambda bands: [RasterBand(b) for b in bands],
self.summaries.get_list(BANDS_PROP),
)
@bands.setter
def bands(self, v: list[RasterBand] | None) -> None:
self._set_summary(BANDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v))
[docs]
class RasterExtensionHooks(ExtensionHooks):
schema_uri: str = SCHEMA_URI
prev_extension_ids: set[str] = {*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI]}
stac_object_types = {pystac.STACObjectType.ITEM, pystac.STACObjectType.COLLECTION}
RASTER_EXTENSION_HOOKS: ExtensionHooks = RasterExtensionHooks()