diff --git a/apps/drs_spot67_stac_example.py b/examples/drs_spot67_stac_example.py similarity index 87% rename from apps/drs_spot67_stac_example.py rename to examples/drs_spot67_stac_example.py index 4f724739cd5538c4b76a53480775abcf16a8dd80..b54f3f46fc493786189149f7f8b3e6f73672b524 100755 --- a/apps/drs_spot67_stac_example.py +++ b/examples/drs_spot67_stac_example.py @@ -6,7 +6,7 @@ def main(): bbox = scenes.BoundingBox(xmin=2, ymin=45.23, xmax=2.1, ymax=45.26) provider = scenes.stac.DinamisSpot67Provider() - scs = provider.search(bbox_wgs84=bbox) + scs = provider.scenes_search(bbox_wgs84=bbox) for i, sc in enumerate(scs): print(sc) pyotb.HaralickTextureExtraction({"in": sc.get_xs(), "out": f"/data/tex_{i}.img"}) diff --git a/apps/mpc_s2_stac_example.py b/examples/mpc_s2_stac_example.py similarity index 64% rename from apps/mpc_s2_stac_example.py rename to examples/mpc_s2_stac_example.py index 51434c1946c742b41f77a40f3af912741056eb01..96601edd1258015af4f42c4bcfa651a3a63af403 100755 --- a/apps/mpc_s2_stac_example.py +++ b/examples/mpc_s2_stac_example.py @@ -6,9 +6,10 @@ def main(): bbox = scenes.BoundingBox(xmin=2, ymin=45.23, xmax=2.1, ymax=45.26) provider = scenes.stac.MPCProvider() - scs = provider.search(bbox_wgs84=bbox) - for i, sc in enumerate(scs): - print(sc) + results = provider.stac_search(bbox_wgs84=bbox) + for i, result in enumerate(results): + print(result) + sc = provider.stac_item_to_scene(result) dyn = pyotb.DynamicConvert(sc.get_10m_bands()[0:256, 0:256, :]) dyn.write(f"/tmp/{i}.png") diff --git a/scenes/stac.py b/scenes/stac.py index e63a92a1e3fc4f75dd005c2d597ae076278f7ad8..b1a60245b5ffe4684e36dd437931bf26222cc1c4 100644 --- a/scenes/stac.py +++ b/scenes/stac.py @@ -23,6 +23,7 @@ import tempfile import datetime import pystac from pystac_client import Client +from scenes.core import Scene from scenes.spatial import BoundingBox from scenes.dates import any2datetime from scenes.spot import Spot67DRSScene @@ -31,16 +32,20 @@ from scenes.auth import OAuth2KeepAlive import requests from tqdm.autonotebook import tqdm import threading +from abc import abstractmethod + class ProviderBase: - def __init__(self, url): + def __init__(self, url, default_collections): """ Args: url: STAC endpoint + default_collections: default collections """ assert url self.url = url + self.default_collections = default_collections self.vsicurl_media_types = [pystac.MediaType.GEOPACKAGE, pystac.MediaType.GEOJSON, pystac.MediaType.COG, @@ -48,15 +53,16 @@ class ProviderBase: pystac.MediaType.TIFF, pystac.MediaType.JPEG2000] - def stac_search(self, collections: list[str], bbox_wgs84: BoundingBox, date_min: datetime.datetime | str = None, - date_max: datetime.datetime | str = None, filt: dict = None, query: dict = None): + def stac_search(self, bbox_wgs84: BoundingBox, collections: list[str] = None, + date_min: datetime.datetime | str = None, date_max: datetime.datetime | str = None, + filt: dict = None, query: dict = None): """ Search an item in a STAC catalog. see https://pystac-client.readthedocs.io/en/latest/api.html#pystac_client.Client.search Args: - collections: names of the collections to search bbox_wgs84: The bounding box in WGS84 (BoundingBox instance) + collections: names of the collections to search date_min: date min (datetime.datetime or str) date_max: date max (datetime.datetime or str) filt: JSON of query parameters as per the STAC API filter extension @@ -67,10 +73,28 @@ class ProviderBase: """ dt = [any2datetime(date) for date in [date_min, date_max] if date] if date_min or date_max else None api = Client.open(self.url) - results = api.search(max_items=None, bbox=bbox_wgs84.to_list(), datetime=dt, collections=collections, - filter=filt, query=query) + results = api.search(max_items=None, bbox=bbox_wgs84.to_list(), datetime=dt, filter=filt, query=query, + collections=self.default_collections if not collections else collections) return results.items() + def scenes_search(self, *args, as_generator=False, **kwargs) -> list[Scene]: + """ + Perform a STAC search then converts the resulting items into `Scene` objects + + Args: + *args: same args as stac_search() + as_generator: return scenes as generator, or not + **kwargs: same kwargs as stac_search() + + Returns: a list of `Scenes` + + """ + items = self.stac_search(*args, **kwargs) + gen = (self.stac_item_to_scene(item) for item in tqdm(items)) + if as_generator: + return gen + return list(gen) + def get_asset_path(self, asset: pystac.asset) -> str: """ Return the URI suited for GDAL if the asset is some geospatial data. @@ -89,10 +113,24 @@ class ProviderBase: return f"/vsicurl/{url}" return url + @abstractmethod + def stac_item_to_scene(self, item: pystac.item) -> Scene: + """ + Convert a STAC item into a `Scene` + + Args: + item: STAC item + + Returns: scene + + """ + raise NotImplementedError("") + class DinamisSpot67Provider(ProviderBase): - def __init__(self, url="https://stacapi.147.100.200.143.nip.io", auth=None): - super().__init__(url=url) + def __init__(self, auth=None): + super().__init__(url="https://stacapi.147.100.200.143.nip.io", + default_collections=["spot-6-7-drs"]) self.temp_dir = tempfile.TemporaryDirectory() self.headers_file = os.path.join(self.temp_dir.name, 'headers.txt') self.__auth_headers = None @@ -111,7 +149,6 @@ class DinamisSpot67Provider(ProviderBase): with self.__auth_headers_lock: self.__auth_headers = {"Authorization": f"Bearer {access_token}"} - def get_auth_headers(self): with self.__auth_headers_lock: return self.__auth_headers @@ -132,7 +169,6 @@ class DinamisSpot67Provider(ProviderBase): return url def update_headers(self, token): - print("update headers") self.set_auth_headers(token=token) tmp_headers_file = f"{self.headers_file}.tmp" old_headers_file = f"{self.headers_file}.old" @@ -143,38 +179,24 @@ class DinamisSpot67Provider(ProviderBase): os.rename(self.headers_file, old_headers_file) os.rename(tmp_headers_file, self.headers_file) - def search(self, bbox_wgs84: BoundingBox, date_min: datetime.datetime | str = None, - date_max: datetime.datetime | str = None, collections: list[str] = None, - as_generator=False) -> list[Spot67DRSScene]: + def stac_item_to_scene(self, item: pystac.item) -> Scene: """ - Search and instantiate S2 products from Microsoft + Convert a STAC item into a `Spot67DRSScene` Args: - bbox_wgs84: The bounding box in WGS84 (BoundingBox instance) - date_min: date min (datetime.datetime or str) - date_max: date max (datetime.datetime or str) - collections: list of collections - as_generator: return the scenes as generator + item: STAC item - Returns: - `Spot67DRSScene` instances in a list + Returns: scene """ - if not collections: - collections = ["spot-6-7-drs"] - items = self.stac_search(collections=collections, bbox_wgs84=bbox_wgs84, date_min=date_min, date_max=date_max) - gen = (Spot67DRSScene( - assets_paths={key: self.get_asset_path(asset) for key, asset in item.assets.items()}, - assets_headers=self.get_auth_headers() - ) for item in tqdm(items)) - if as_generator: - return gen - return list(gen) + return Spot67DRSScene(assets_paths={key: self.get_asset_path(asset) for key, asset in item.assets.items()}, + assets_headers=self.get_auth_headers()) class MPCProvider(ProviderBase): - def __init__(self, url="https://planetarycomputer.microsoft.com/api/stac/v1"): - super().__init__(url=url) + def __init__(self): + super().__init__(url="https://planetarycomputer.microsoft.com/api/stac/v1", + default_collections="sentinel-2-l2a") def get_asset_path(self, asset: pystac.asset) -> str: """ @@ -198,29 +220,14 @@ class MPCProvider(ProviderBase): return f"/vsicurl/{new_url}" return url - def search(self, bbox_wgs84: BoundingBox, date_min: datetime.datetime | str = None, - date_max: datetime.datetime | str = None, collections: list[str] = None, - as_generator=True) -> list[Sentinel2MPCScene]: + def stac_item_to_scene(self, item: pystac.item) -> Scene: """ - Search and instantiate Spot-6/7 DRS products from Dinamis + Convert a STAC item into a `Sentinel2MPCScene` Args: - bbox_wgs84: The bounding box in WGS84 (BoundingBox instance) - date_min: date min (datetime.datetime or str) - date_max: date max (datetime.datetime or str) - collections: list of collections - as_generator: return the scenes as generator + item: STAC item - Returns: - `Sentinel2MPCScene` instances in a list + Returns: scene """ - if not collections: - collections = ["sentinel-2-l2a"] - items = self.stac_search(collections=collections, bbox_wgs84=bbox_wgs84, date_min=date_min, date_max=date_max) - gen = (Sentinel2MPCScene( - assets_paths={key: self.get_asset_path(asset) for key, asset in item.assets.items()}, - ) for item in tqdm(items)) - if as_generator: - return gen - return list(gen) + return Sentinel2MPCScene(assets_paths={key: self.get_asset_path(asset) for key, asset in item.assets.items()})