diff --git a/CHANGELOG.md b/CHANGELOG.md index 1981b1555..68eec27b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - `keep_parent` to Catalog `add_item` and `add_child` to avoid overriding existing parents ([#1117](https://github.com/stac-utils/pystac/pull/1117)) - `owner` attribute to `AssetDefinition` in the item-assets extension ([#1110](https://github.com/stac-utils/pystac/pull/1110)) - Windows `\\` path delimiters are converted to POSIX style `/` delimiters ([#1125](https://github.com/stac-utils/pystac/pull/1125)) +- Updated raster extension to work with the item_assets extension's AssetDefinition objects ([#1110](https://github.com/stac-utils/pystac/pull/1110)) +- Classification extension ([#1093](https://github.com/stac-utils/pystac/pull/1093)), with support for adding classification information to item_assets' `AssetDefinition`s and raster's `RasterBand` objects. ### Changed diff --git a/pystac/__init__.py b/pystac/__init__.py index d2a0286a3..6e96a83d3 100644 --- a/pystac/__init__.py +++ b/pystac/__init__.py @@ -85,6 +85,7 @@ import pystac.validation import pystac.extensions.hooks +import pystac.extensions.classification import pystac.extensions.datacube import pystac.extensions.eo import pystac.extensions.file @@ -106,6 +107,7 @@ EXTENSION_HOOKS = pystac.extensions.hooks.RegisteredExtensionHooks( [ + pystac.extensions.classification.CLASSIFICATION_EXTENSION_HOOKS, pystac.extensions.datacube.DATACUBE_EXTENSION_HOOKS, pystac.extensions.eo.EO_EXTENSION_HOOKS, pystac.extensions.file.FILE_EXTENSION_HOOKS, diff --git a/pystac/extensions/classification.py b/pystac/extensions/classification.py new file mode 100644 index 000000000..27bacdb2c --- /dev/null +++ b/pystac/extensions/classification.py @@ -0,0 +1,655 @@ +"""Implements the :stac-ext:`Classification `.""" + +from __future__ import annotations + +import re +from typing import ( + Any, + Dict, + Generic, + Iterable, + List, + Optional, + Pattern, + TypeVar, + Union, + cast, +) + +import pystac +import pystac.extensions.item_assets as item_assets +from pystac.extensions.base import ( + ExtensionManagementMixin, + PropertiesExtension, + SummariesExtension, +) +from pystac.extensions.hooks import ExtensionHooks +from pystac.extensions.raster import RasterBand +from pystac.serialization.identify import STACJSONDescription, STACVersionID +from pystac.utils import get_required, map_opt + +T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition, RasterBand) + +SCHEMA_URI_PATTERN: str = ( + "https://stac-extensions.github.io/classification/v{version}/schema.json" +) +DEFAULT_VERSION: str = "1.1.0" +SUPPORTED_VERSIONS: List[str] = ["1.1.0", "1.0.0"] + +# Field names +PREFIX: str = "classification:" +BITFIELDS_PROP: str = PREFIX + "bitfields" +CLASSES_PROP: str = PREFIX + "classes" +RASTER_BANDS_PROP: str = "raster:bands" + +COLOR_HINT_PATTERN: Pattern[str] = re.compile("^([0-9A-Fa-f]{6})$") + + +class Classification: + """Represents a single category of a classification. + + Use Classification.create to create a new Classification. + """ + + properties: Dict[str, Any] + + def __init__(self, properties: Dict[str, Any]) -> None: + self.properties = properties + + def apply( + self, + value: int, + description: str, + name: Optional[str] = None, + color_hint: Optional[str] = None, + ) -> None: + """ + Set the properties for a new Classification. + + Args: + value: The integer value corresponding to this class + description: The description of this class + name: The optional human-readable short name for this class + color_hint: An optional hexadecimal string-encoded representation of the + RGB color that is suggested to represent this class (six hexadecimal + characters, all capitalized) + """ + self.value = value + self.name = name + self.description = description + self.color_hint = color_hint + + if color_hint is not None: + match = COLOR_HINT_PATTERN.match(color_hint) + assert ( + color_hint is None or match is not None and match.group() == color_hint + ), "Must format color hints as '^([0-9A-F]{6})$'" + + if color_hint is not None: + match = COLOR_HINT_PATTERN.match(color_hint) + assert ( + color_hint is None or match is not None and match.group() == color_hint + ), "Must format color hints as '^([0-9A-F]{6})$'" + + @classmethod + def create( + cls, + value: int, + description: str, + name: Optional[str] = None, + color_hint: Optional[str] = None, + ) -> Classification: + """ + Create a new Classification. + + Args: + value: The integer value corresponding to this class + name: The human-readable short name for this class + description: The optional long-form description of this class + color_hint: An optional hexadecimal string-encoded representation of the + RGB color that is suggested to represent this class (six hexadecimal + characters, all capitalized) + """ + c = cls({}) + c.apply( + value=value, + name=name, + description=description, + color_hint=color_hint, + ) + return c + + @property + def value(self) -> int: + """Get or set the class value + + Returns: + int + """ + return get_required(self.properties.get("value"), self, "value") + + @value.setter + def value(self, v: int) -> None: + self.properties["value"] = v + + @property + def description(self) -> str: + """Get or set the description of the class + + Returns: + str + """ + return get_required(self.properties.get("description"), self, "description") + + @description.setter + def description(self, v: str) -> None: + self.properties["description"] = v + + @property + def name(self) -> Optional[str]: + """Get or set the name of the class + + Returns: + Optional[str] + """ + return self.properties.get("name") + + @name.setter + def name(self, v: Optional[str]) -> None: + if v is not None: + self.properties["name"] = v + else: + self.properties.pop("name", None) + + @property + def color_hint(self) -> Optional[str]: + """Get or set the optional color hint for this class. + + The color hint must be a six-character string of capitalized hexadecimal + characters ([0-9A-F]). + + Returns: + Optional[str] + """ + return self.properties.get("color-hint") + + @color_hint.setter + def color_hint(self, v: Optional[str]) -> None: + if v is not None: + match = COLOR_HINT_PATTERN.match(v) + assert ( + v is None or match is not None and match.group() == v + ), "Must format color hints as '^([0-9A-F]{6})$'" + self.properties["color-hint"] = v + else: + self.properties.pop("color-hint", None) + + def to_dict(self) -> Dict[str, Any]: + """Returns the dictionary encoding of this class + + Returns: + dict: The serialization of the Classification + """ + return self.properties + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Classification): + raise NotImplementedError + return ( + self.value == other.value + and self.description == other.description + and self.name == other.name + and self.color_hint == other.color_hint + ) + + def __repr__(self) -> str: + return f"" + + +class Bitfield: + """Encodes the representation of values as bits in an integer. + + Use Bitfield.create to create a new Bitfield. + """ + + properties: Dict[str, Any] + + def __init__(self, properties: Dict[str, Any]): + self.properties = properties + + def apply( + self, + offset: int, + length: int, + classes: List[Classification], + roles: Optional[List[str]] = None, + description: Optional[str] = None, + name: Optional[str] = None, + ) -> None: + """Sets the properties for this Bitfield. + + Args: + offset: describes the position of the least significant bit captured + by this bitfield, with zero indicating the least significant binary + digit + length: the number of bits described by this bitfield + classes: a list of Classification objects describing the various levels + captured by this bitfield + roles: the optional role of this bitfield (see `Asset Roles + `) + description: an optional short description of the classification + name: the optional name of the class for machine readability + """ + self.offset = offset + self.length = length + self.classes = classes + self.roles = roles + self.description = description + self.name = name + + assert offset >= 0, "Non-negative offsets only" + assert length >= 1, "Positive field lengths only" + assert len(classes) > 0, "Must specify at least one class" + assert ( + roles is None or len(roles) > 0 + ), "When set, roles must contain at least one item" + + @classmethod + def create( + cls, + offset: int, + length: int, + classes: List[Classification], + roles: Optional[List[str]] = None, + description: Optional[str] = None, + name: Optional[str] = None, + ) -> Bitfield: + """Sets the properties for this Bitfield. + + Args: + offset: describes the position of the least significant bit captured + by this bitfield, with zero indicating the least significant binary + digit + length: the number of bits described by this bitfield + classes: a list of Classification objects describing the various levels + captured by this bitfield + roles: the optional role of this bitfield (see `Asset Roles + `) + description: an optional short description of the classification + name: the optional name of the class for machine readability + """ + b = cls({}) + b.apply( + offset=offset, + length=length, + classes=classes, + roles=roles, + description=description, + name=name, + ) + return b + + @property + def offset(self) -> int: + """Get or set the offset of the bitfield. + + Describes the position of the least significant bit captured by this + bitfield, with zero indicating the least significant binary digit + + Returns: + int + """ + return get_required(self.properties.get("offset"), self, "offset") + + @offset.setter + def offset(self, v: int) -> None: + self.properties["offset"] = v + + @property + def length(self) -> int: + """Get or set the length (number of bits) of the bitfield + + Returns: + int + """ + return get_required(self.properties.get("length"), self, "length") + + @length.setter + def length(self, v: int) -> None: + self.properties["length"] = v + + @property + def classes(self) -> List[Classification]: + """Get or set the class definitions for the bitfield + + Returns: + List[Classification] + """ + + return [ + Classification(d) + for d in cast( + List[Dict[str, Any]], + get_required( + self.properties.get("classes"), + self, + "classes", + ), + ) + ] + + @classes.setter + def classes(self, v: List[Classification]) -> None: + self.properties["classes"] = [c.to_dict() for c in v] + + @property + def roles(self) -> Optional[List[str]]: + """Get or set the role of the bitfield. + + See `Asset Roles + ` + + Returns: + Optional[List[str]] + """ + return self.properties.get("roles") + + @roles.setter + def roles(self, v: Optional[List[str]]) -> None: + if v is not None: + self.properties["roles"] = v + else: + self.properties.pop("roles", None) + + @property + def description(self) -> Optional[str]: + """Get or set the optional description of a bitfield. + + Returns: + Optional[str] + """ + return self.properties.get("description") + + @description.setter + def description(self, v: Optional[str]) -> None: + if v is not None: + self.properties["description"] = v + else: + self.properties.pop("description", None) + + @property + def name(self) -> Optional[str]: + """Get or set the optional name of the bitfield. + + Returns: + Optional[str] + """ + return self.properties.get("name") + + @name.setter + def name(self, v: Optional[str]) -> None: + if v is not None: + self.properties["name"] = v + else: + self.properties.pop("name", None) + + def __repr__(self) -> str: + return ( + f"" + ) + + def to_dict(self) -> Dict[str, Any]: + """Returns the dictionary encoding of this bitfield + + Returns: + dict: The serialization of the Bitfield + """ + return self.properties + + +class ClassificationExtension( + Generic[T], + PropertiesExtension, + ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]], +): + """An abstract class that can be used to extend the properties of + :class:`~pystac.Item`, :class:`~pystac.Asset`, + :class:`~pystac.extension.raster.RasterBand`, or + :class:`~pystac.extension.item_assets.AssetDefinition` with properties from the + :stac-ext:`Classification Extension `. This class is generic + over the type of STAC object being extended. + + This class is not to be instantiated directly. One can either directly use the + subclass corresponding to the object you are extending, or the `ext` class + method can be used to construct the proper class for you. + """ + + properties: Dict[str, Any] + """The :class:`~pystac.Asset` fields, including extension properties.""" + + def apply( + self, + classes: Optional[List[Classification]] = None, + bitfields: Optional[List[Bitfield]] = None, + ) -> None: + """Applies the classifiation extension fields to the extended object. + + Note: one may set either the classes or bitfields objects, but not both. + + Args: + classes: a list of + :class:`~pystac.extension.classification.Classification` objects + describing the various classes in the classification + """ + assert ( + classes is None + and bitfields is not None + or bitfields is None + and classes is not None + ), "Must set exactly one of `classes` or `bitfields`" + self.classes = classes + self.bitfields = bitfields + + @property + def classes(self) -> Optional[List[Classification]]: + """Get or set the classes for the base object + + Note: Setting the classes will clear the object's bitfields if they are + not None + + Returns: + Optional[List[Classification]] + """ + return self._get_classes() + + @classes.setter + def classes(self, v: Optional[List[Classification]]) -> None: + if self._get_bitfields() is not None: + self.bitfields = None + self._set_property( + CLASSES_PROP, map_opt(lambda classes: [c.to_dict() for c in classes], v) + ) + + def _get_classes(self) -> Optional[List[Classification]]: + return map_opt( + lambda classes: [Classification(c) for c in classes], + self._get_property(CLASSES_PROP, List[Dict[str, Any]]), + ) + + @property + def bitfields(self) -> Optional[List[Bitfield]]: + """Get or set the bitfields for the base object + + Note: Setting the bitfields will clear the object's classes if they are + not None + + Returns: + Optional[List[Bitfield]] + """ + return self._get_bitfields() + + @bitfields.setter + def bitfields(self, v: Optional[List[Bitfield]]) -> None: + if self._get_classes() is not None: + self.classes = None + self._set_property( + BITFIELDS_PROP, + map_opt(lambda bitfields: [b.to_dict() for b in bitfields], v), + ) + + def _get_bitfields(self) -> Optional[List[Bitfield]]: + return map_opt( + lambda bitfields: [Bitfield(b) for b in bitfields], + self._get_property(BITFIELDS_PROP, List[Dict[str, Any]]), + ) + + @classmethod + def get_schema_uri(cls) -> str: + return SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) + + @classmethod + def get_schema_uris(cls) -> List[str]: + return [SCHEMA_URI_PATTERN.format(version=v) for v in SUPPORTED_VERSIONS] + + @classmethod + def ext(cls, obj: T, add_if_missing: bool = False) -> ClassificationExtension[T]: + """Extends the given STAC object with propertied from the + :stac-ext:`Classification Extension ` + + This extension can be applied to instances of :class:`~pystac.Item`, + :class:`~pystac.Asset`, + :class:`~pystac.extensions.item_assets.AssetDefinition`, or + :class:`~pystac.extension.raster.RasterBand`. + + Raises: + pystac.ExtensionTypeError : If an invalid object type is passed + """ + if isinstance(obj, pystac.Item): + cls.validate_has_extension(obj, add_if_missing) + return cast(ClassificationExtension[T], ItemClassificationExtension(obj)) + elif isinstance(obj, pystac.Asset): + cls.validate_owner_has_extension(obj, add_if_missing) + return cast(ClassificationExtension[T], AssetClassificationExtension(obj)) + elif isinstance(obj, item_assets.AssetDefinition): + cls.validate_has_extension( + cast(Union[pystac.Item, pystac.Collection], obj.owner), add_if_missing + ) + return cast( + ClassificationExtension[T], ItemAssetsClassificationExtension(obj) + ) + elif isinstance(obj, RasterBand): + return cast( + ClassificationExtension[T], RasterBandClassificationExtension(obj) + ) + + else: + raise pystac.ExtensionTypeError( + "Classification extension does not apply to type " + f"'{type(obj).__name__}'" + ) + + @classmethod + def summaries( + cls, obj: pystac.Collection, add_if_missing: bool = False + ) -> SummariesClassificationExtension: + cls.validate_has_extension(obj, add_if_missing) + return SummariesClassificationExtension(obj) + + +class ItemClassificationExtension(ClassificationExtension[pystac.Item]): + item: pystac.Item + + properties: Dict[str, Any] + + def __init__(self, item: pystac.Item): + self.item = item + self.properties = item.properties + + def __repr__(self) -> str: + return f"" + + +class AssetClassificationExtension(ClassificationExtension[pystac.Asset]): + asset: pystac.Asset + asset_href: str + properties: Dict[str, Any] + additional_read_properties: Optional[Iterable[Dict[str, Any]]] + + def __init__(self, asset: pystac.Asset): + self.asset = 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"" + + +class ItemAssetsClassificationExtension( + ClassificationExtension[item_assets.AssetDefinition] +): + properties: Dict[str, Any] + asset_defn: item_assets.AssetDefinition + + def __init__(self, item_asset: item_assets.AssetDefinition): + self.asset_defn = item_asset + self.properties = item_asset.properties + + def __repr__(self) -> str: + return f" str: + return f"" + + +class SummariesClassificationExtension(SummariesExtension): + @property + def classes(self) -> Optional[List[Classification]]: + return map_opt( + lambda classes: [Classification(c) for c in classes], + self.summaries.get_list(CLASSES_PROP), + ) + + @classes.setter + def classes(self, v: Optional[List[Classification]]) -> None: + self._set_summary(CLASSES_PROP, map_opt(lambda x: [c.to_dict() for c in x], v)) + + @property + def bitfields(self) -> Optional[List[Bitfield]]: + return map_opt( + lambda bitfields: [Bitfield(b) for b in bitfields], + self.summaries.get_list(BITFIELDS_PROP), + ) + + @bitfields.setter + def bitfields(self, v: Optional[List[Bitfield]]) -> None: + self._set_summary( + BITFIELDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v) + ) + + +class ClassificationExtensionHooks(ExtensionHooks): + schema_uri: str = SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) + prev_extension_ids = set(ClassificationExtension.get_schema_uris()) - set( + [ClassificationExtension.get_schema_uri()] + ) + stac_object_types = {pystac.STACObjectType.ITEM} + + def migrate( + self, obj: Dict[str, Any], version: STACVersionID, info: STACJSONDescription + ) -> None: + super().migrate(obj, version, info) + + +CLASSIFICATION_EXTENSION_HOOKS: ExtensionHooks = ClassificationExtensionHooks() diff --git a/tests/data-files/classification/classification-landsat-example.json b/tests/data-files/classification/classification-landsat-example.json new file mode 100644 index 000000000..84e1b1705 --- /dev/null +++ b/tests/data-files/classification/classification-landsat-example.json @@ -0,0 +1,1221 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "id": "LC08_L2SP_047027_20201204_02_T1", + "properties": { + "platform": "landsat-8", + "instruments": [ + "oli", + "tirs" + ], + "created": "2022-03-18T14:15:28.445062Z", + "gsd": 30, + "description": "Landsat Collection 2 Level-2", + "eo:cloud_cover": 1.55, + "view:off_nadir": 0, + "view:sun_elevation": 18.80722985, + "view:sun_azimuth": 164.91405951, + "proj:epsg": 32610, + "proj:shape": [ + 7971, + 7861 + ], + "proj:transform": [ + 30, + 0, + 353685, + 0, + -30, + 5374215 + ], + "landsat:cloud_cover_land": 1.9, + "landsat:wrs_type": "2", + "landsat:wrs_path": "047", + "landsat:wrs_row": "027", + "landsat:collection_category": "T1", + "landsat:collection_number": "02", + "landsat:correction": "L2SP", + "landsat:scene_id": "LC80470272020339LGN00", + "sci:doi": "10.5066/P9OGBGM6", + "datetime": "2020-12-04T19:02:11.194486Z" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -124.27384594041735, + 48.508330109565485 + ], + [ + -121.83965465293758, + 48.078217267779486 + ], + [ + -122.53780648794415, + 46.376775468741904 + ], + [ + -124.89627102658746, + 46.80206928347854 + ], + [ + -124.27384594041735, + 48.508330109565485 + ] + ] + ] + }, + "links": [ + { + "rel": "cite-as", + "href": "https://doi.org/10.5066/P9OGBGM6", + "title": "Landsat 8-9 OLI/TIRS Collection 2 Level-2" + }, + { + "rel": "via", + "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l2-sr/items/LC08_L2SP_047027_20201204_20210313_02_T1_SR", + "type": "application/json", + "title": "USGS STAC Item" + }, + { + "rel": "via", + "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l2-st/items/LC08_L2SP_047027_20201204_20210313_02_T1_ST", + "type": "application/json", + "title": "USGS STAC Item" + } + ], + "assets": { + "thumbnail": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_thumb_small.jpeg", + "type": "image/jpeg", + "title": "Thumbnail image", + "roles": [ + "thumbnail" + ] + }, + "reduced_resolution_browse": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_thumb_large.jpeg", + "type": "image/jpeg", + "title": "Reduced resolution browse image", + "roles": [ + "overview" + ] + }, + "mtl.json": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_MTL.json", + "type": "application/json", + "title": "Product Metadata File (json)", + "description": "Collection 2 Level-2 Product Metadata File (json)", + "roles": [ + "metadata" + ] + }, + "mtl.txt": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_MTL.txt", + "type": "text/plain", + "title": "Product Metadata File (txt)", + "description": "Collection 2 Level-2 Product Metadata File (txt)", + "roles": [ + "metadata" + ] + }, + "mtl.xml": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_MTL.xml", + "type": "application/xml", + "title": "Product Metadata File (xml)", + "description": "Collection 2 Level-2 Product Metadata File (xml)", + "roles": [ + "metadata" + ] + }, + "ang": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ANG.txt", + "type": "text/plain", + "title": "Angle Coefficients File", + "description": "Collection 2 Level-1 Angle Coefficients File", + "roles": [ + "metadata" + ] + }, + "qa_pixel": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_QA_PIXEL.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Pixel Quality Assessment Band", + "description": "Collection 2 Level-1 Pixel Quality Assessment Band (QA_PIXEL)", + "raster:bands": [ + { + "nodata": 1, + "data_type": "uint16", + "spatial_resolution": 30, + "unit": "bit index", + "classification:bitfields": [ + { + "name": "fill", + "description": "Corresponding pixels in L1 image bands are fill", + "offset": 0, + "length": 1, + "classes": [ + { + "name": "not_fill", + "description": "L1 image band pixels are not fill", + "value": 0 + }, + { + "name": "fill", + "description": "L1 image band pixels are fill", + "value": 1 + } + ] + }, + { + "name": "dilated", + "description": "Dilated cloud", + "offset": 1, + "length": 1, + "classes": [ + { + "name": "not_dilated", + "description": "Cloud is not dilated or no cloud", + "value": 0 + }, + { + "name": "dilated", + "description": "Cloud dilation", + "value": 1 + } + ] + }, + { + "name": "cirrus", + "description": "Cirrus mask", + "offset": 2, + "length": 1, + "classes": [ + { + "name": "not_cirrus", + "description": "No confidence level set or low confidence cirrus", + "value": 0 + }, + { + "name": "cirrus", + "description": "High confidence cirrus", + "value": 1 + } + ] + }, + { + "name": "cloud", + "description": "Cloud mask", + "offset": 3, + "length": 1, + "classes": [ + { + "name": "not_cloud", + "description": "Cloud confidence is not high", + "value": 0 + }, + { + "name": "cloud", + "description": "High confidence cloud", + "value": 1 + } + ] + }, + { + "name": "shadow", + "description": "Cloud shadow mask", + "offset": 4, + "length": 1, + "classes": [ + { + "name": "not_shadow", + "description": "Cloud shadow confidence is not high", + "value": 0 + }, + { + "name": "shadow", + "description": "High confidence cloud shadow", + "value": 1 + } + ] + }, + { + "name": "snow", + "description": "Snow/Ice mask", + "offset": 5, + "length": 1, + "classes": [ + { + "name": "not_snow", + "description": "Snow/Ice confidence is not high", + "value": 0 + }, + { + "name": "snow", + "description": "High confidence snow cover", + "value": 1 + } + ] + }, + { + "name": "clear", + "description": "Cloud or dilated cloud bits set", + "offset": 6, + "length": 1, + "classes": [ + { + "name": "not_clear", + "description": "Cloud or dilated cloud bits are set", + "value": 0 + }, + { + "name": "clear", + "description": "Cloud and dilated cloud bits are not set", + "value": 1 + } + ] + }, + { + "name": "water", + "description": "Water mask", + "offset": 7, + "length": 1, + "classes": [ + { + "name": "not_water", + "description": "Land or cloud", + "value": 0 + }, + { + "name": "water", + "description": "Water", + "value": 1 + } + ] + }, + { + "name": "cloud_confidence", + "description": "Cloud confidence levels", + "offset": 8, + "length": 2, + "classes": [ + { + "name": "not_set", + "description": "No confidence level set", + "value": 0 + }, + { + "name": "low", + "description": "Low confidence cloud", + "value": 1 + }, + { + "name": "medium", + "description": "Medium confidence cloud", + "value": 2 + }, + { + "name": "high", + "description": "High confidence cloud", + "value": 3 + } + ] + }, + { + "name": "shadow_confidence", + "description": "Cloud shadow confidence levels", + "offset": 10, + "length": 2, + "classes": [ + { + "name": "not_set", + "description": "No confidence level set", + "value": 0 + }, + { + "name": "low", + "description": "Low confidence cloud shadow", + "value": 1 + }, + { + "name": "reserved", + "description": "Reserved - value not used", + "value": 2 + }, + { + "name": "high", + "description": "High confidence cloud shadow", + "value": 3 + } + ] + }, + { + "name": "snow_confidence", + "description": "Snow/Ice confidence levels", + "offset": 12, + "length": 2, + "classes": [ + { + "name": "not_set", + "description": "No confidence level set", + "value": 0 + }, + { + "name": "low", + "description": "Low confidence snow/ice", + "value": 1 + }, + { + "name": "reserved", + "description": "Reserved - value not used", + "value": 2 + }, + { + "name": "high", + "description": "High confidence snow/ice", + "value": 3 + } + ] + }, + { + "name": "cirrus_confidence", + "description": "Cirrus confidence levels", + "offset": 14, + "length": 2, + "classes": [ + { + "name": "not_set", + "description": "No confidence level set", + "value": 0 + }, + { + "name": "low", + "description": "Low confidence cirrus", + "value": 1 + }, + { + "name": "reserved", + "description": "Reserved - value not used", + "value": 2 + }, + { + "name": "high", + "description": "High confidence cirrus", + "value": 3 + } + ] + } + ] + } + ], + "roles": [ + "cloud", + "cloud-shadow", + "snow-ice", + "water-mask" + ] + }, + "qa_radsat": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_QA_RADSAT.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Radiometric Saturation and Terrain Occlusion Quality Assessment Band", + "description": "Collection 2 Level-1 Radiometric Saturation and Terrain Occlusion Quality Assessment Band (QA_RADSAT)", + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 30, + "unit": "bit index", + "classification:bitfields": [ + { + "name": "band1", + "description": "Band 1 radiometric saturation", + "offset": 0, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 1 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 1 is saturated", + "value": 1 + } + ] + }, + { + "name": "band2", + "description": "Band 2 radiometric saturation", + "offset": 1, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 2 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 2 is saturated", + "value": 1 + } + ] + }, + { + "name": "band3", + "description": "Band 3 radiometric saturation", + "offset": 2, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 3 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 3 is saturated", + "value": 1 + } + ] + }, + { + "name": "band4", + "description": "Band 4 radiometric saturation", + "offset": 3, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 4 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 4 is saturated", + "value": 1 + } + ] + }, + { + "name": "band5", + "description": "Band 5 radiometric saturation", + "offset": 4, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 5 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 5 is saturated", + "value": 1 + } + ] + }, + { + "name": "band6", + "description": "Band 6 radiometric saturation", + "offset": 5, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 6 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 6 is saturated", + "value": 1 + } + ] + }, + { + "name": "band7", + "description": "Band 7 radiometric saturation", + "offset": 6, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 7 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 7 is saturated", + "value": 1 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 7, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "band9", + "description": "Band 9 radiometric saturation", + "offset": 8, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 9 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 9 is saturated", + "value": 1 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 9, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 10, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "occlusion", + "description": "Terrain not visible from sensor due to intervening terrain", + "offset": 11, + "length": 1, + "classes": [ + { + "name": "not_occluded", + "description": "Terrain is not occluded", + "value": 0 + }, + { + "name": "occluded", + "description": "Terrain is occluded", + "value": 1 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 12, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 13, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 14, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 15, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + } + ] + } + ], + "roles": [ + "saturation" + ] + }, + "coastal": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B1.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Coastal/Aerosol Band", + "description": "Collection 2 Level-2 Coastal/Aerosol Band (SR_B1) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B1", + "common_name": "coastal", + "description": "Coastal/Aerosol", + "center_wavelength": 0.44, + "full_width_half_max": 0.02 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "blue": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B2.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Blue Band", + "description": "Collection 2 Level-2 Blue Band (SR_B2) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B2", + "common_name": "blue", + "description": "Visible blue", + "center_wavelength": 0.48, + "full_width_half_max": 0.06 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "green": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B3.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Green Band", + "description": "Collection 2 Level-2 Green Band (SR_B3) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B3", + "common_name": "green", + "description": "Visible green", + "center_wavelength": 0.56, + "full_width_half_max": 0.06 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "red": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B4.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red Band", + "description": "Collection 2 Level-2 Red Band (SR_B4) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B4", + "common_name": "red", + "description": "Visible red", + "center_wavelength": 0.65, + "full_width_half_max": 0.04 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "nir08": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B5.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Near Infrared Band 0.8", + "description": "Collection 2 Level-2 Near Infrared Band 0.8 (SR_B5) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B5", + "common_name": "nir08", + "description": "Near infrared", + "center_wavelength": 0.87, + "full_width_half_max": 0.03 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "swir16": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B6.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Short-wave Infrared Band 1.6", + "description": "Collection 2 Level-2 Short-wave Infrared Band 1.6 (SR_B6) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B6", + "common_name": "swir16", + "description": "Short-wave infrared", + "center_wavelength": 1.61, + "full_width_half_max": 0.09 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "swir22": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B7.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Short-wave Infrared Band 2.2", + "description": "Collection 2 Level-2 Short-wave Infrared Band 2.2 (SR_B7) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B7", + "common_name": "swir22", + "description": "Short-wave infrared", + "center_wavelength": 2.2, + "full_width_half_max": 0.19 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "qa_aerosol": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_QA_AEROSOL.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Aerosol Quality Assessment Band", + "description": "Collection 2 Level-2 Aerosol Quality Assessment Band (SR_QA_AEROSOL) Surface Reflectance Product", + "raster:bands": [ + { + "nodata": 1, + "data_type": "uint8", + "spatial_resolution": 30, + "unit": "bit index", + "classification:bitfields": [ + { + "name": "fill", + "description": "Corresponding pixels in L1 image bands are fill", + "offset": 0, + "length": 1, + "classes": [ + { + "name": "not_fill", + "description": "L1 image band pixels are not fill", + "value": 0 + }, + { + "name": "fill", + "description": "L1 image band pixels are fill", + "value": 1 + } + ] + }, + { + "name": "retrieval", + "description": "Valid aerosol retrieval", + "offset": 1, + "length": 1, + "classes": [ + { + "name": "not_valid", + "description": "Aerosol retrieval is not valid", + "value": 0 + }, + { + "name": "valid", + "description": "Aerosol retrieval is valid", + "value": 1 + } + ] + }, + { + "name": "water", + "description": "Water mask", + "offset": 2, + "length": 1, + "classes": [ + { + "name": "not_water", + "description": "Not water", + "value": 0 + }, + { + "name": "water", + "description": "Water", + "value": 1 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 3, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 4, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "interpolated", + "description": "Aerosol is interpolated", + "offset": 5, + "length": 1, + "classes": [ + { + "name": "not_interpolated", + "description": "Aerosol is not interpolated", + "value": 0 + }, + { + "name": "interpolated", + "description": "Aerosol is interpolated", + "value": 1 + } + ] + }, + { + "name": "level", + "description": "Aerosol level", + "offset": 6, + "length": 2, + "classes": [ + { + "name": "climatology", + "description": "No aerosol correction applied", + "value": 0 + }, + { + "name": "low", + "description": "Low aerosol level", + "value": 1 + }, + { + "name": "medium", + "description": "Medium aerosol level", + "value": 2 + }, + { + "name": "high", + "description": "High aerosol level", + "value": 3 + } + ] + } + ] + } + ], + "roles": [ + "data-mask", + "water-mask" + ] + }, + "lwir11": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_B10.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Surface Temperature Band", + "description": "Collection 2 Level-2 Thermal Infrared Band (ST_B10) Surface Temperature", + "eo:bands": [ + { + "name": "TIRS_B10", + "common_name": "lwir11", + "description": "Long-wave infrared", + "center_wavelength": 10.9, + "full_width_half_max": 0.59 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "unit": "kelvin", + "scale": 0.00341802, + "offset": 149 + } + ], + "gsd": 100, + "roles": [ + "data", + "temperature" + ] + }, + "atran": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_ATRAN.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Atmospheric Transmittance Band", + "description": "Collection 2 Level-2 Atmospheric Transmittance Band (ST_ATRAN) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "scale": 0.0001 + } + ], + "roles": [ + "data" + ] + }, + "cdist": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_CDIST.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Cloud Distance Band", + "description": "Collection 2 Level-2 Cloud Distance Band (ST_CDIST) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "kilometer", + "scale": 0.01 + } + ], + "roles": [ + "data" + ] + }, + "drad": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_DRAD.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Downwelled Radiance Band", + "description": "Collection 2 Level-2 Downwelled Radiance Band (ST_DRAD) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "watt/steradian/square_meter/micrometer", + "scale": 0.001 + } + ], + "roles": [ + "data" + ] + }, + "urad": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_URAD.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Upwelled Radiance Band", + "description": "Collection 2 Level-2 Upwelled Radiance Band (ST_URAD) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "watt/steradian/square_meter/micrometer", + "scale": 0.001 + } + ], + "roles": [ + "data" + ] + }, + "trad": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_TRAD.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Thermal Radiance Band", + "description": "Collection 2 Level-2 Thermal Radiance Band (ST_TRAD) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "watt/steradian/square_meter/micrometer", + "scale": 0.001 + } + ], + "roles": [ + "data" + ] + }, + "emis": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_EMIS.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Emissivity Band", + "description": "Collection 2 Level-2 Emissivity Band (ST_EMIS) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "emissivity coefficient", + "scale": 0.0001 + } + ], + "roles": [ + "data" + ] + }, + "emsd": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_EMSD.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Emissivity Standard Deviation Band", + "description": "Collection 2 Level-2 Emissivity Standard Deviation Band (ST_EMSD) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "emissivity coefficient", + "scale": 0.0001 + } + ], + "roles": [ + "data" + ] + }, + "qa": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_QA.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Surface Temperature Quality Assessment Band", + "description": "Collection 2 Level-2 Quality Assessment Band (ST_QA) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "kelvin", + "scale": 0.01 + } + ], + "roles": [ + "data" + ] + } + }, + "bbox": [ + -124.98085491310867, + 46.35352512466258, + -121.78788697796408, + 48.51466487533742 + ], + "stac_extensions": [ + "https://stac-extensions.github.io/raster/v1.0.0/schema.json", + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://landsat.usgs.gov/stac/landsat-extension/v1.1.1/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + "https://stac-extensions.github.io/classification/v1.0.0/schema.json" + ] +} diff --git a/tests/data-files/classification/classification_landsat_example.json b/tests/data-files/classification/classification_landsat_example.json new file mode 100644 index 000000000..2a5edef7d --- /dev/null +++ b/tests/data-files/classification/classification_landsat_example.json @@ -0,0 +1,1221 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "id": "LC08_L2SP_047027_20201204_02_T1", + "properties": { + "platform": "landsat-8", + "instruments": [ + "oli", + "tirs" + ], + "created": "2022-03-18T14:15:28.445062Z", + "gsd": 30, + "description": "Landsat Collection 2 Level-2", + "eo:cloud_cover": 1.55, + "view:off_nadir": 0, + "view:sun_elevation": 18.80722985, + "view:sun_azimuth": 164.91405951, + "proj:epsg": 32610, + "proj:shape": [ + 7971, + 7861 + ], + "proj:transform": [ + 30, + 0, + 353685, + 0, + -30, + 5374215 + ], + "landsat:cloud_cover_land": 1.9, + "landsat:wrs_type": "2", + "landsat:wrs_path": "047", + "landsat:wrs_row": "027", + "landsat:collection_category": "T1", + "landsat:collection_number": "02", + "landsat:correction": "L2SP", + "landsat:scene_id": "LC80470272020339LGN00", + "sci:doi": "10.5066/P9OGBGM6", + "datetime": "2020-12-04T19:02:11.194486Z" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -124.27384594041735, + 48.508330109565485 + ], + [ + -121.83965465293758, + 48.078217267779486 + ], + [ + -122.53780648794415, + 46.376775468741904 + ], + [ + -124.89627102658746, + 46.80206928347854 + ], + [ + -124.27384594041735, + 48.508330109565485 + ] + ] + ] + }, + "links": [ + { + "rel": "cite-as", + "href": "https://doi.org/10.5066/P9OGBGM6", + "title": "Landsat 8-9 OLI/TIRS Collection 2 Level-2" + }, + { + "rel": "via", + "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l2-sr/items/LC08_L2SP_047027_20201204_20210313_02_T1_SR", + "type": "application/json", + "title": "USGS STAC Item" + }, + { + "rel": "via", + "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l2-st/items/LC08_L2SP_047027_20201204_20210313_02_T1_ST", + "type": "application/json", + "title": "USGS STAC Item" + } + ], + "assets": { + "thumbnail": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_thumb_small.jpeg", + "type": "image/jpeg", + "title": "Thumbnail image", + "roles": [ + "thumbnail" + ] + }, + "reduced_resolution_browse": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_thumb_large.jpeg", + "type": "image/jpeg", + "title": "Reduced resolution browse image", + "roles": [ + "overview" + ] + }, + "mtl.json": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_MTL.json", + "type": "application/json", + "title": "Product Metadata File (json)", + "description": "Collection 2 Level-2 Product Metadata File (json)", + "roles": [ + "metadata" + ] + }, + "mtl.txt": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_MTL.txt", + "type": "text/plain", + "title": "Product Metadata File (txt)", + "description": "Collection 2 Level-2 Product Metadata File (txt)", + "roles": [ + "metadata" + ] + }, + "mtl.xml": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_MTL.xml", + "type": "application/xml", + "title": "Product Metadata File (xml)", + "description": "Collection 2 Level-2 Product Metadata File (xml)", + "roles": [ + "metadata" + ] + }, + "ang": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ANG.txt", + "type": "text/plain", + "title": "Angle Coefficients File", + "description": "Collection 2 Level-1 Angle Coefficients File", + "roles": [ + "metadata" + ] + }, + "qa_pixel": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_QA_PIXEL.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Pixel Quality Assessment Band", + "description": "Collection 2 Level-1 Pixel Quality Assessment Band (QA_PIXEL)", + "raster:bands": [ + { + "nodata": 1, + "data_type": "uint16", + "spatial_resolution": 30, + "unit": "bit index", + "classification:bitfields": [ + { + "name": "fill", + "description": "Corresponding pixels in L1 image bands are fill", + "offset": 0, + "length": 1, + "classes": [ + { + "name": "not_fill", + "description": "L1 image band pixels are not fill", + "value": 0 + }, + { + "name": "fill", + "description": "L1 image band pixels are fill", + "value": 1 + } + ] + }, + { + "name": "dilated", + "description": "Dilated cloud", + "offset": 1, + "length": 1, + "classes": [ + { + "name": "not_dilated", + "description": "Cloud is not dilated or no cloud", + "value": 0 + }, + { + "name": "dilated", + "description": "Cloud dilation", + "value": 1 + } + ] + }, + { + "name": "cirrus", + "description": "Cirrus mask", + "offset": 2, + "length": 1, + "classes": [ + { + "name": "not_cirrus", + "description": "No confidence level set or low confidence cirrus", + "value": 0 + }, + { + "name": "cirrus", + "description": "High confidence cirrus", + "value": 1 + } + ] + }, + { + "name": "cloud", + "description": "Cloud mask", + "offset": 3, + "length": 1, + "classes": [ + { + "name": "not_cloud", + "description": "Cloud confidence is not high", + "value": 0 + }, + { + "name": "cloud", + "description": "High confidence cloud", + "value": 1 + } + ] + }, + { + "name": "shadow", + "description": "Cloud shadow mask", + "offset": 4, + "length": 1, + "classes": [ + { + "name": "not_shadow", + "description": "Cloud shadow confidence is not high", + "value": 0 + }, + { + "name": "shadow", + "description": "High confidence cloud shadow", + "value": 1 + } + ] + }, + { + "name": "snow", + "description": "Snow/Ice mask", + "offset": 5, + "length": 1, + "classes": [ + { + "name": "not_snow", + "description": "Snow/Ice confidence is not high", + "value": 0 + }, + { + "name": "snow", + "description": "High confidence snow cover", + "value": 1 + } + ] + }, + { + "name": "clear", + "description": "Cloud or dilated cloud bits set", + "offset": 6, + "length": 1, + "classes": [ + { + "name": "not_clear", + "description": "Cloud or dilated cloud bits are set", + "value": 0 + }, + { + "name": "clear", + "description": "Cloud and dilated cloud bits are not set", + "value": 1 + } + ] + }, + { + "name": "water", + "description": "Water mask", + "offset": 7, + "length": 1, + "classes": [ + { + "name": "not_water", + "description": "Land or cloud", + "value": 0 + }, + { + "name": "water", + "description": "Water", + "value": 1 + } + ] + }, + { + "name": "cloud_confidence", + "description": "Cloud confidence levels", + "offset": 8, + "length": 2, + "classes": [ + { + "name": "not_set", + "description": "No confidence level set", + "value": 0 + }, + { + "name": "low", + "description": "Low confidence cloud", + "value": 1 + }, + { + "name": "medium", + "description": "Medium confidence cloud", + "value": 2 + }, + { + "name": "high", + "description": "High confidence cloud", + "value": 3 + } + ] + }, + { + "name": "shadow_confidence", + "description": "Cloud shadow confidence levels", + "offset": 10, + "length": 2, + "classes": [ + { + "name": "not_set", + "description": "No confidence level set", + "value": 0 + }, + { + "name": "low", + "description": "Low confidence cloud shadow", + "value": 1 + }, + { + "name": "reserved", + "description": "Reserved - value not used", + "value": 2 + }, + { + "name": "high", + "description": "High confidence cloud shadow", + "value": 3 + } + ] + }, + { + "name": "snow_confidence", + "description": "Snow/Ice confidence levels", + "offset": 12, + "length": 2, + "classes": [ + { + "name": "not_set", + "description": "No confidence level set", + "value": 0 + }, + { + "name": "low", + "description": "Low confidence snow/ice", + "value": 1 + }, + { + "name": "reserved", + "description": "Reserved - value not used", + "value": 2 + }, + { + "name": "high", + "description": "High confidence snow/ice", + "value": 3 + } + ] + }, + { + "name": "cirrus_confidence", + "description": "Cirrus confidence levels", + "offset": 14, + "length": 2, + "classes": [ + { + "name": "not_set", + "description": "No confidence level set", + "value": 0 + }, + { + "name": "low", + "description": "Low confidence cirrus", + "value": 1 + }, + { + "name": "reserved", + "description": "Reserved - value not used", + "value": 2 + }, + { + "name": "high", + "description": "High confidence cirrus", + "value": 3 + } + ] + } + ] + } + ], + "roles": [ + "cloud", + "cloud-shadow", + "snow-ice", + "water-mask" + ] + }, + "qa_radsat": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_QA_RADSAT.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Radiometric Saturation and Terrain Occlusion Quality Assessment Band", + "description": "Collection 2 Level-1 Radiometric Saturation and Terrain Occlusion Quality Assessment Band (QA_RADSAT)", + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 30, + "unit": "bit index", + "classification:bitfields": [ + { + "name": "band1", + "description": "Band 1 radiometric saturation", + "offset": 0, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 1 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 1 is saturated", + "value": 1 + } + ] + }, + { + "name": "band2", + "description": "Band 2 radiometric saturation", + "offset": 1, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 2 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 2 is saturated", + "value": 1 + } + ] + }, + { + "name": "band3", + "description": "Band 3 radiometric saturation", + "offset": 2, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 3 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 3 is saturated", + "value": 1 + } + ] + }, + { + "name": "band4", + "description": "Band 4 radiometric saturation", + "offset": 3, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 4 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 4 is saturated", + "value": 1 + } + ] + }, + { + "name": "band5", + "description": "Band 5 radiometric saturation", + "offset": 4, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 5 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 5 is saturated", + "value": 1 + } + ] + }, + { + "name": "band6", + "description": "Band 6 radiometric saturation", + "offset": 5, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 6 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 6 is saturated", + "value": 1 + } + ] + }, + { + "name": "band7", + "description": "Band 7 radiometric saturation", + "offset": 6, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 7 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 7 is saturated", + "value": 1 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 7, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "band9", + "description": "Band 9 radiometric saturation", + "offset": 8, + "length": 1, + "classes": [ + { + "name": "not_saturated", + "description": "Band 9 is not saturated", + "value": 0 + }, + { + "name": "saturated", + "description": "Band 9 is saturated", + "value": 1 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 9, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 10, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "occlusion", + "description": "Terrain not visible from sensor due to intervening terrain", + "offset": 11, + "length": 1, + "classes": [ + { + "name": "not_occluded", + "description": "Terrain is not occluded", + "value": 0 + }, + { + "name": "occluded", + "description": "Terrain is occluded", + "value": 1 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 12, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 13, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 14, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 15, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + } + ] + } + ], + "roles": [ + "saturation" + ] + }, + "coastal": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B1.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Coastal/Aerosol Band", + "description": "Collection 2 Level-2 Coastal/Aerosol Band (SR_B1) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B1", + "common_name": "coastal", + "description": "Coastal/Aerosol", + "center_wavelength": 0.44, + "full_width_half_max": 0.02 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "blue": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B2.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Blue Band", + "description": "Collection 2 Level-2 Blue Band (SR_B2) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B2", + "common_name": "blue", + "description": "Visible blue", + "center_wavelength": 0.48, + "full_width_half_max": 0.06 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "green": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B3.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Green Band", + "description": "Collection 2 Level-2 Green Band (SR_B3) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B3", + "common_name": "green", + "description": "Visible green", + "center_wavelength": 0.56, + "full_width_half_max": 0.06 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "red": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B4.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Red Band", + "description": "Collection 2 Level-2 Red Band (SR_B4) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B4", + "common_name": "red", + "description": "Visible red", + "center_wavelength": 0.65, + "full_width_half_max": 0.04 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "nir08": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B5.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Near Infrared Band 0.8", + "description": "Collection 2 Level-2 Near Infrared Band 0.8 (SR_B5) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B5", + "common_name": "nir08", + "description": "Near infrared", + "center_wavelength": 0.87, + "full_width_half_max": 0.03 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "swir16": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B6.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Short-wave Infrared Band 1.6", + "description": "Collection 2 Level-2 Short-wave Infrared Band 1.6 (SR_B6) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B6", + "common_name": "swir16", + "description": "Short-wave infrared", + "center_wavelength": 1.61, + "full_width_half_max": 0.09 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "swir22": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_B7.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Short-wave Infrared Band 2.2", + "description": "Collection 2 Level-2 Short-wave Infrared Band 2.2 (SR_B7) Surface Reflectance", + "eo:bands": [ + { + "name": "OLI_B7", + "common_name": "swir22", + "description": "Short-wave infrared", + "center_wavelength": 2.2, + "full_width_half_max": 0.19 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "scale": 0.0000275, + "offset": -0.2 + } + ], + "roles": [ + "data", + "reflectance" + ] + }, + "qa_aerosol": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_SR_QA_AEROSOL.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Aerosol Quality Assessment Band", + "description": "Collection 2 Level-2 Aerosol Quality Assessment Band (SR_QA_AEROSOL) Surface Reflectance Product", + "raster:bands": [ + { + "nodata": 1, + "data_type": "uint8", + "spatial_resolution": 30, + "unit": "bit index", + "classification:bitfields": [ + { + "name": "fill", + "description": "Corresponding pixels in L1 image bands are fill", + "offset": 0, + "length": 1, + "classes": [ + { + "name": "not_fill", + "description": "L1 image band pixels are not fill", + "value": 0 + }, + { + "name": "fill", + "description": "L1 image band pixels are fill", + "value": 1 + } + ] + }, + { + "name": "retrieval", + "description": "Valid aerosol retrieval", + "offset": 1, + "length": 1, + "classes": [ + { + "name": "not_valid", + "description": "Aerosol retrieval is not valid", + "value": 0 + }, + { + "name": "valid", + "description": "Aerosol retrieval is valid", + "value": 1 + } + ] + }, + { + "name": "water", + "description": "Water mask", + "offset": 2, + "length": 1, + "classes": [ + { + "name": "not_water", + "description": "Not water", + "value": 0 + }, + { + "name": "water", + "description": "Water", + "value": 1 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 3, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "unused", + "description": "Unused bit", + "offset": 4, + "length": 1, + "classes": [ + { + "name": "unused", + "description": "Unused bit", + "value": 0 + } + ] + }, + { + "name": "interpolated", + "description": "Aerosol is interpolated", + "offset": 5, + "length": 1, + "classes": [ + { + "name": "not_interpolated", + "description": "Aerosol is not interpolated", + "value": 0 + }, + { + "name": "interpolated", + "description": "Aerosol is interpolated", + "value": 1 + } + ] + }, + { + "name": "level", + "description": "Aerosol level", + "offset": 6, + "length": 2, + "classes": [ + { + "name": "climatology", + "description": "No aerosol correction applied", + "value": 0 + }, + { + "name": "low", + "description": "Low aerosol level", + "value": 1 + }, + { + "name": "medium", + "description": "Medium aerosol level", + "value": 2 + }, + { + "name": "high", + "description": "High aerosol level", + "value": 3 + } + ] + } + ] + } + ], + "roles": [ + "data-mask", + "water-mask" + ] + }, + "lwir11": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_B10.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Surface Temperature Band", + "description": "Collection 2 Level-2 Thermal Infrared Band (ST_B10) Surface Temperature", + "eo:bands": [ + { + "name": "TIRS_B10", + "common_name": "lwir11", + "description": "Long-wave infrared", + "center_wavelength": 10.9, + "full_width_half_max": 0.59 + } + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "spatial_resolution": 30, + "unit": "kelvin", + "scale": 0.00341802, + "offset": 149 + } + ], + "gsd": 100, + "roles": [ + "data", + "temperature" + ] + }, + "atran": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_ATRAN.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Atmospheric Transmittance Band", + "description": "Collection 2 Level-2 Atmospheric Transmittance Band (ST_ATRAN) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "scale": 0.0001 + } + ], + "roles": [ + "data" + ] + }, + "cdist": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_CDIST.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Cloud Distance Band", + "description": "Collection 2 Level-2 Cloud Distance Band (ST_CDIST) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "kilometer", + "scale": 0.01 + } + ], + "roles": [ + "data" + ] + }, + "drad": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_DRAD.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Downwelled Radiance Band", + "description": "Collection 2 Level-2 Downwelled Radiance Band (ST_DRAD) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "watt/steradian/square_meter/micrometer", + "scale": 0.001 + } + ], + "roles": [ + "data" + ] + }, + "urad": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_URAD.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Upwelled Radiance Band", + "description": "Collection 2 Level-2 Upwelled Radiance Band (ST_URAD) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "watt/steradian/square_meter/micrometer", + "scale": 0.001 + } + ], + "roles": [ + "data" + ] + }, + "trad": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_TRAD.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Thermal Radiance Band", + "description": "Collection 2 Level-2 Thermal Radiance Band (ST_TRAD) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "watt/steradian/square_meter/micrometer", + "scale": 0.001 + } + ], + "roles": [ + "data" + ] + }, + "emis": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_EMIS.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Emissivity Band", + "description": "Collection 2 Level-2 Emissivity Band (ST_EMIS) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "emissivity coefficient", + "scale": 0.0001 + } + ], + "roles": [ + "data" + ] + }, + "emsd": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_EMSD.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Emissivity Standard Deviation Band", + "description": "Collection 2 Level-2 Emissivity Standard Deviation Band (ST_EMSD) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "emissivity coefficient", + "scale": 0.0001 + } + ], + "roles": [ + "data" + ] + }, + "qa": { + "href": "LC08_L2SP_047027_20201204_20210313_02_T1_ST_QA.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Surface Temperature Quality Assessment Band", + "description": "Collection 2 Level-2 Quality Assessment Band (ST_QA) Surface Temperature Product", + "raster:bands": [ + { + "nodata": -9999, + "data_type": "int16", + "spatial_resolution": 30, + "unit": "kelvin", + "scale": 0.01 + } + ], + "roles": [ + "data" + ] + } + }, + "bbox": [ + -124.98085491310867, + 46.35352512466258, + -121.78788697796408, + 48.51466487533742 + ], + "stac_extensions": [ + "https://stac-extensions.github.io/raster/v1.0.0/schema.json", + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://landsat.usgs.gov/stac/landsat-extension/v1.1.1/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + "https://stac-extensions.github.io/classification/v1.1.0/schema.json" + ] +} diff --git a/tests/data-files/classification/collection-item-assets-raster-bands.json b/tests/data-files/classification/collection-item-assets-raster-bands.json new file mode 100644 index 000000000..5503c73e7 --- /dev/null +++ b/tests/data-files/classification/collection-item-assets-raster-bands.json @@ -0,0 +1,120 @@ +{ + "type": "Collection", + "id": "103001005D31F500", + "stac_version": "1.0.0", + "description": "103001005D31F500 ARD Tiles", + "links": [ + { + "rel": "root", + "href": "../order_collections/5867496013686833273_root_collection.json", + "type": "application/json" + }, + { + "rel": "parent", + "href": "../order_collections/5867496013686833273_root_collection.json", + "type": "application/json" + }, + { + "rel": "item", + "href": "../10/120020223032/2016-10-08/103001005D31F500.json", + "type": "application/json" + } + ], + "extent": { + "spatial": { + "bbox": [ + [ + -122.43409303549794, + 37.76425505192271, + -122.37337588954041, + 37.812440528660765 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2016-10-08 19:25:32Z", + "2016-10-08 19:25:32Z" + ] + ] + } + }, + "item_assets": { + "cloud-mask-raster": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Cloud/Cloud Shadow Coverage Raster", + "description": "thematic raster, 0 -> no data, 1 -> clear, 2 -> cloud, 3 -> cloud shadow", + "proj:shape": [ + 2176, + 2176 + ], + "eo:bands": [ + { + "name": "BAND_CM", + "description": "Clouds/Cloud Shadows Mask" + } + ], + "raster:bands": [ + { + "classification:classes": [ + { + "value": 0, + "name": "nodata", + "description": "NoData" + }, + { + "value": 1, + "name": "clear", + "description": "Clear of clouds or shadows" + }, + { + "value": 2, + "name": "cloud", + "description": "Clouds", + "color-hint": "B8D0EC" + }, + { + "value": 3, + "name": "cloud_shadow", + "description": "Cloud shadows", + "color-hint": "9C9EA0" + } + ] + } + ] + }, + "visual": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Visual (RGB) Image", + "proj:shape": [ + 17408, + 17408 + ], + "eo:bands": [ + { + "name": "BAND_R", + "common_name": "red", + "description": "Red" + }, + { + "name": "BAND_G", + "common_name": "green", + "description": "Green" + }, + { + "name": "BAND_B", + "common_name": "blue", + "description": "Blue" + } + ] + } + }, + "license": "proprietary", + "stac_extensions": [ + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/raster/v1.0.0/schema.json", + "https://stac-extensions.github.io/classification/v1.0.0/schema.json" + ] +} \ No newline at end of file diff --git a/tests/data-files/classification/collection-item-assets.json b/tests/data-files/classification/collection-item-assets.json new file mode 100644 index 000000000..1e13f3315 --- /dev/null +++ b/tests/data-files/classification/collection-item-assets.json @@ -0,0 +1,120 @@ +{ + "type": "Collection", + "id": "103001005D31F500", + "stac_version": "1.0.0", + "description": "103001005D31F500 ARD Tiles", + "links": [ + { + "rel": "root", + "href": "../order_collections/5867496013686833273_root_collection.json", + "type": "application/json" + }, + { + "rel": "parent", + "href": "../order_collections/5867496013686833273_root_collection.json", + "type": "application/json" + }, + { + "rel": "item", + "href": "../10/120020223032/2016-10-08/103001005D31F500.json", + "type": "application/json" + } + ], + "extent": { + "spatial": { + "bbox": [ + [ + -122.43409303549794, + 37.76425505192271, + -122.37337588954041, + 37.812440528660765 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2016-10-08 19:25:32Z", + "2016-10-08 19:25:32Z" + ] + ] + } + }, + "item_assets": { + "cloud-mask-raster": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Cloud/Cloud Shadow Coverage Raster", + "description": "thematic raster, 0 -> no data, 1 -> clear, 2 -> cloud, 3 -> cloud shadow", + "proj:shape": [ + 2176, + 2176 + ], + "eo:bands": [ + { + "name": "BAND_CM", + "description": "Clouds/Cloud Shadows Mask" + } + ], + "raster:bands": [ + { + "classification:classes": [ + { + "value": 0, + "name": "nodata", + "description": "NoData" + }, + { + "value": 1, + "name": "clear", + "description": "Clear of clouds or shadows" + }, + { + "value": 2, + "name": "cloud", + "description": "Clouds", + "color-hint": "B8D0EC" + }, + { + "value": 3, + "name": "cloud_shadow", + "description": "Cloud shadows", + "color-hint": "9C9EA0" + } + ] + } + ] + }, + "visual": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Visual (RGB) Image", + "proj:shape": [ + 17408, + 17408 + ], + "eo:bands": [ + { + "name": "BAND_R", + "common_name": "red", + "description": "Red" + }, + { + "name": "BAND_G", + "common_name": "green", + "description": "Green" + }, + { + "name": "BAND_B", + "common_name": "blue", + "description": "Blue" + } + ] + } + }, + "license": "proprietary", + "stac_extensions": [ + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/raster/v1.0.0/schema.json", + "https://stac-extensions.github.io/classification/v1.0.0/schema.json" + ] +} diff --git a/tests/extensions/test_classification.py b/tests/extensions/test_classification.py new file mode 100644 index 000000000..bd004a290 --- /dev/null +++ b/tests/extensions/test_classification.py @@ -0,0 +1,317 @@ +import json +import logging +from copy import deepcopy +from datetime import datetime +from typing import Any, Dict, List, cast + +import pytest +from dateutil.parser import parse + +import pystac +from pystac import Collection, Item +from pystac.extensions.classification import ( + CLASSES_PROP, + DEFAULT_VERSION, + SCHEMA_URI_PATTERN, + SUPPORTED_VERSIONS, + Bitfield, + Classification, + ClassificationExtension, +) +from pystac.extensions.item_assets import ItemAssetsExtension +from pystac.extensions.raster import RasterBand, RasterExtension +from tests.utils import TestCases + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger() + +LANDSAT_EXAMPLE_URI = TestCases.get_path( + "data-files/classification/classification_landsat_example.json" +) +CLASSIFICATION_COLLECTION_RASTER_URI = TestCases.get_path( + "data-files/classification/collection-item-assets-raster-bands.json" +) +PLAIN_ITEM = TestCases.get_path("data-files/item/sample-item.json") + + +@pytest.fixture +def item_dict() -> Dict[str, Any]: + with open(LANDSAT_EXAMPLE_URI) as f: + return cast(Dict[str, Any], json.load(f)) + + +@pytest.fixture +def landsat_item() -> Item: + return Item.from_file(LANDSAT_EXAMPLE_URI) + + +@pytest.fixture +def plain_item() -> Item: + return Item.from_file(PLAIN_ITEM) + + +@pytest.fixture +def collection() -> Collection: + return Collection.from_file(CLASSIFICATION_COLLECTION_RASTER_URI) + + +def test_stac_extensions(landsat_item: Item) -> None: + assert ClassificationExtension.has_extension(landsat_item) + + +def test_classification_object() -> None: + c = Classification.create( + name="dummy", description="empty class", value=0, color_hint="FF00AB" + ) + assert c.name == "dummy" + assert c.description == "empty class" + assert c.color_hint == "FF00AB" + assert c.value == 0 + + assert Classification(c.to_dict()) == c + with pytest.raises(NotImplementedError): + c == "blah" + + +def test_bitfield_object() -> None: + b = Bitfield.create( + offset=0, + length=1, + classes=[ + Classification.create(description="no", value=0), + Classification.create(description="yes", value=1), + ], + roles=["data"], + description="dummy description", + name="dummy", + ) + assert b.offset == 0 + assert b.length == 1 + assert len(b.classes) == 2 + assert b.roles == ["data"] + assert b.description == "dummy description" + assert b.name == "dummy" + + +def test_get_schema_uri(landsat_item: Item) -> None: + assert any( + [ + uri in landsat_item.stac_extensions + for uri in ClassificationExtension.get_schema_uris() + ] + ) + + +def test_ext_raises_if_item_does_not_conform(plain_item: Item) -> None: + with pytest.raises(pystac.errors.ExtensionNotImplemented): + ClassificationExtension.ext(plain_item) + + +def test_apply_bitfields(plain_item: Item) -> None: + ClassificationExtension.add_to(plain_item) + ClassificationExtension.ext(plain_item).apply( + bitfields=[ + Bitfield.create( + offset=0, + length=1, + classes=[ + Classification.create(description="no", value=0), + Classification.create(description="yes", value=1), + ], + ) + ] + ) + + plain_item.validate() + assert ( + ClassificationExtension.ext(plain_item).bitfields is not None + and len(cast(List[Bitfield], ClassificationExtension.ext(plain_item).bitfields)) + == 1 + ) + assert ( + cast(List[Bitfield], ClassificationExtension.ext(plain_item).bitfields)[ + 0 + ].offset + == 0 + ) + assert ( + cast(List[Bitfield], ClassificationExtension.ext(plain_item).bitfields)[ + 0 + ].length + == 1 + ) + assert ( + ClassificationExtension.ext(plain_item).bitfields is not None + and len( + cast(List[Bitfield], ClassificationExtension.ext(plain_item).bitfields)[ + 0 + ].classes + ) + == 2 + ) + + +def test_create_classes(plain_item: Item) -> None: + ClassificationExtension.add_to(plain_item) + ext = ClassificationExtension.ext(plain_item) + ext.apply( + bitfields=[ + Bitfield.create( + offset=0, + length=1, + classes=[ + Classification.create(description="no", value=0), + Classification.create(description="yes", value=1), + ], + ) + ] + ) + ext.classes = [ + Classification.create(description="no", value=0), + Classification.create(description="yes", value=1), + ] + assert ext.bitfields is None + ext.bitfields = [ + Bitfield.create( + offset=0, + length=1, + classes=[ + Classification.create(description="no", value=0), + Classification.create(description="yes", value=1), + ], + ) + ] + assert ext.classes is None + + +def test_create() -> None: + field = Bitfield.create( + name="cloud_confidence", + description="Cloud confidence levels", + offset=8, + length=2, + classes=[ + Classification.create( + name="not_set", description="No confidence level set", value=0 + ), + Classification.create( + name="low", description="Low confidence cloud", value=1 + ), + Classification.create( + name="medium", description="Medium confidence cloud", value=2 + ), + Classification.create( + name="high", description="High confidence cloud", value=3 + ), + ], + ) + + logger.info(field) + + +def test_color_hint_formatting() -> None: + with pytest.raises(Exception): + Classification.create(value=0, description="water", color_hint="#0000ff") + Classification.create(value=0, description="water", color_hint="0000FF") + + +def test_to_from_dict(item_dict: Dict[str, Any]) -> None: + def _parse_times(a_dict: Dict[str, Any]) -> None: + for k, v in a_dict.items(): + if isinstance(v, dict): + _parse_times(v) + elif isinstance(v, (tuple, list, set)): + for vv in v: + if isinstance(vv, dict): + _parse_times(vv) + else: + if k == "datetime": + if not isinstance(v, datetime): + a_dict[k] = parse(v) + a_dict[k] = a_dict[k].replace(microsecond=0) + + d1 = deepcopy(item_dict) + d2 = Item.from_dict(item_dict).to_dict() + _parse_times(d1) + _parse_times(d2) + assert d1 == d2, f"Mismatch between dictionaries: \n{d1}\n{d2}" + + +def test_add_to(plain_item: Item) -> None: + assert ClassificationExtension.get_schema_uri() not in plain_item.stac_extensions + + # Check that the URI gets added to stac_extensions + ClassificationExtension.add_to(plain_item) + assert ClassificationExtension.get_schema_uri() in plain_item.stac_extensions + + # Check that the URI only gets added once, regardless of how many times add_to + # is called. + ClassificationExtension.add_to(plain_item) + ClassificationExtension.add_to(plain_item) + + classification_uris = [ + uri + for uri in plain_item.stac_extensions + if uri == ClassificationExtension.get_schema_uri() + ] + assert len(classification_uris) == 1 + + +def test_validate_classification(landsat_item: Item) -> None: + landsat_item.validate() + + +def test_add_item_classes(plain_item: Item) -> None: + item_ext = ClassificationExtension.ext(plain_item, add_if_missing=True) + item_ext.__repr__() + assert item_ext.classes is None + item_ext.classes = [Classification.create(description="dummy", value=0)] + assert item_ext.properties[CLASSES_PROP] == [{"value": 0, "description": "dummy"}] + + +def test_add_asset_classes(plain_item: Item) -> None: + ClassificationExtension.ext(plain_item, add_if_missing=True) + asset = plain_item.assets["analytic"] + assert CLASSES_PROP not in asset.extra_fields.keys() + asset_ext = ClassificationExtension.ext(asset) + asset_ext.__repr__() + asset_ext.classes = [Classification.create(value=0, description="dummy")] + assert CLASSES_PROP in asset.extra_fields.keys() + assert asset.extra_fields[CLASSES_PROP] == [{"value": 0, "description": "dummy"}] + + +def test_item_asset_raster_classes(collection: Collection) -> None: + item_asset = ItemAssetsExtension.ext(collection, add_if_missing=True).item_assets[ + "cloud-mask-raster" + ] + raster_bands = cast(List[RasterBand], RasterExtension.ext(item_asset).bands) + raster_bands_ext = ClassificationExtension.ext(raster_bands[0]) + raster_bands_ext.__repr__() + assert raster_bands_ext.classes is not None + + +def test_item_assets_extension(collection: Collection) -> None: + item_asset = ItemAssetsExtension.ext(collection, add_if_missing=True).item_assets[ + "cloud-mask-raster" + ] + ext = ClassificationExtension.ext(item_asset) + ext.__repr__() + + +def test_older_extension_version(landsat_item: Item) -> None: + OLD_VERSION = list(set(SUPPORTED_VERSIONS) - set([DEFAULT_VERSION]))[0] + new = SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) + old = SCHEMA_URI_PATTERN.format(version=OLD_VERSION) + + stac_extensions = set(landsat_item.stac_extensions) + stac_extensions.remove(new) + stac_extensions.add(old) + item_as_dict = landsat_item.to_dict(include_self_link=False, transform_hrefs=False) + item_as_dict["stac_extensions"] = list(stac_extensions) + item = Item.from_dict(item_as_dict) + assert ClassificationExtension.has_extension(item) + assert old in item.stac_extensions + + migrated_item = pystac.Item.from_dict(item_as_dict, migrate=True) + assert ClassificationExtension.has_extension(migrated_item) + assert new in migrated_item.stac_extensions