From c0f8d549a866737a3c276f9838fa0a019e7975be Mon Sep 17 00:00:00 2001 From: Pratichhya Sharma Date: Tue, 11 Aug 2026 14:59:16 +0200 Subject: [PATCH 1/5] first version udp --- .../s2_sam/benchmark_scenarios/s2_sam.json | 38 ++++ .../terravision/s2_sam/notebook/s2_sam.ipynb | 53 +++++ .../terravision/s2_sam/openeo_udp/README.md | 50 +++++ .../terravision/s2_sam/openeo_udp/generate.py | 52 +++++ .../terravision/s2_sam/openeo_udp/s2_sam.json | 196 ++++++++++++++++++ .../terravision/s2_sam/openeo_udp/sam_udf.py | 132 ++++++++++++ .../terravision/s2_sam/records/s2_sam.json | 127 ++++++++++++ 7 files changed, 648 insertions(+) create mode 100644 algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json create mode 100644 algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb create mode 100644 algorithm_catalog/terravision/s2_sam/openeo_udp/README.md create mode 100644 algorithm_catalog/terravision/s2_sam/openeo_udp/generate.py create mode 100644 algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json create mode 100644 algorithm_catalog/terravision/s2_sam/openeo_udp/sam_udf.py create mode 100644 algorithm_catalog/terravision/s2_sam/records/s2_sam.json diff --git a/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json b/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json new file mode 100644 index 00000000..4caa74ef --- /dev/null +++ b/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json @@ -0,0 +1,38 @@ +[ + { + "id": "s2_sam", + "type": "openeo", + "description": "Run Sentinel-2 based Spectral Angle Mapping (S2-SAM) on a monthly BAP composite and return the best matching USGS spectral library class index per pixel.", + "backend": "openeofed.dataspace.copernicus.eu", + "process_graph": { + "s2sam1": { + "process_id": "s2_sam", + "namespace": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json", + "arguments": { + "spatial_extent": { + "west": 5.05, + "south": 51.21, + "east": 5.1, + "north": 51.23 + }, + "temporal_extent": [ + "2025-05-01", + "2025-05-31" + ] + } + }, + "saveresult1": { + "process_id": "save_result", + "arguments": { + "data": { + "from_node": "s2sam1" + }, + "format": "NetCDF", + "options": {} + }, + "result": true + } + }, + "reference_data": {} + } +] diff --git a/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb b/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb new file mode 100644 index 00000000..4bc613d9 --- /dev/null +++ b/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb @@ -0,0 +1,53 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "31162cad", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Authenticated using refresh token.\n" + ] + } + ], + "source": [ + "import openeo\n", + "\n", + "connection = openeo.connect(\"openeofed.dataspace.copernicus.eu\").authenticate_oidc()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26bc37db", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cdse", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/algorithm_catalog/terravision/s2_sam/openeo_udp/README.md b/algorithm_catalog/terravision/s2_sam/openeo_udp/README.md new file mode 100644 index 00000000..dda28d1a --- /dev/null +++ b/algorithm_catalog/terravision/s2_sam/openeo_udp/README.md @@ -0,0 +1,50 @@ +# Sentinel-2 based Spectral Angle Mapping (S2-SAM) + +This service applies Spectral Angle Mapping (SAM) to Sentinel-2 Level-2A reflectance data. +For each pixel, the algorithm compares the pixel spectrum to a spectral reference library and selects the closest match based on the smallest spectral angle. + +## What this service does + +SAM measures similarity in spectral *shape* (not absolute brightness): + +$$ +θ = \cos^{-1}\left(\frac{x \cdot y}{\|x\|\|y\|}\right) +$$ + +where: +- $x$ is the pixel spectrum. +- $y$ is a reference spectrum from the library. +- smaller $\theta$ means a better match. + +The output is a raster of class indices, where each pixel value is the index of the best-matching reference signature. + +## Sentinel-2 bands used + +The service uses only the Sentinel-2 L2A bands provided by the BAP composite input, not all L2A layers. +The band subset is (B01, B02, B03, B04, B05, B06, B07, B08, B8A, B09, B11, B12). + +Input compositing is based on the BAP process: +[https://algorithm-catalogue.apex.esa.int/apps/bap_composite](https://algorithm-catalogue.apex.esa.int/apps/bap_composite) + +## Reference spectral library + +The reference library is based on a Sentinel-2-resampled USGS mineral/material collection. +It includes a broad set of geology and mining-related targets (e.g. iron oxides, clays, sulfates, silicates, carbonates, and related materials). + +The CSV used by the service contains: +- 498 named reference signatures (material columns). +- 1 `cwvl` column with central wavelength values. + +Examples of included targets are minerals such as Acmite, Actinolite, Alunite, Jarosite, Kaolinite, Hematite, Goethite, Pyrite, Chalcopyrite, and many more variants. + +## Notes on interpretation + +- The product reports the **closest spectral match** in the selected library, not a definitive mineral identification. +- Similar materials can produce similar spectra at Sentinel-2 spectral resolution. +- Results are most useful when combined with geological context, ancillary data, and expert validation. + +## Missing data handling + +If some bands are missing (`NaN`) at a pixel, the implementation computes SAM using only the available bands, with a minimum valid-band threshold. +Pixels without enough valid information are marked as no match. + diff --git a/algorithm_catalog/terravision/s2_sam/openeo_udp/generate.py b/algorithm_catalog/terravision/s2_sam/openeo_udp/generate.py new file mode 100644 index 00000000..479b8390 --- /dev/null +++ b/algorithm_catalog/terravision/s2_sam/openeo_udp/generate.py @@ -0,0 +1,52 @@ +from datetime import date +import json +from pathlib import Path + +import openeo +from openeo.api.process import Parameter +from openeo.rest.udp import build_process_dict + +def generate(): + connection = openeo.connect("openeofed.dataspace.copernicus.eu").authenticate_oidc() + + spatial_extent = Parameter.spatial_extent( + name="spatial_extent", + description="Limits the data to process to the specified bounding box or polygons.\\n\\nFor raster data, the process loads the pixel into the data cube if the point at the pixel center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC).\\nFor vector data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been provided.\\n\\nEmpty geometries are ignored.\\nSet this parameter to null to set no limit for the spatial extent." + ) + + temporal_extent = Parameter.temporal_interval( + name="temporal_extent", + description="Temporal extent specified as two-element array with start and end date/date-time." + ) + + + bap_cube = connection.datacube_from_process( + "bap_composite", + namespace = "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/vito/bap_composite/openeo_udp/bap_composite.json", + temporal_extent = temporal_extent, + geometry= spatial_extent, + bands = ['B01','B02','B03','B04','B05','B06','B07','B08','B8A','B09','B11','B12'], + max_cloud_cover = 80 + ) + composite = bap_cube.aggregate_temporal_period("month","first") + + udf = openeo.UDF.from_file( + Path(__file__).parent / "sam_udf.py", + ) + processed_cube = composite.apply(process=udf) + + + return build_process_dict( + process_graph=processed_cube, + process_id="s2_sam", + description=(Path(__file__).parent / "README.md").read_text(), + parameters=[ + spatial_extent, + temporal_extent, + ] + ) + + +if __name__ == "__main__": + with open("s2_sam.json", "w") as f: + json.dump(generate(), f, indent=2) \ No newline at end of file diff --git a/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json b/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json new file mode 100644 index 00000000..1702820b --- /dev/null +++ b/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json @@ -0,0 +1,196 @@ +{ + "process_graph": { + "bapcomposite1": { + "process_id": "bap_composite", + "arguments": { + "bands": [ + "B01", + "B02", + "B03", + "B04", + "B05", + "B06", + "B07", + "B08", + "B8A", + "B09", + "B11", + "B12" + ], + "geometry": { + "from_parameter": "spatial_extent" + }, + "max_cloud_cover": 80, + "temporal_extent": { + "from_parameter": "temporal_extent" + } + }, + "namespace": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/vito/bap_composite/openeo_udp/bap_composite.json" + }, + "aggregatetemporalperiod1": { + "process_id": "aggregate_temporal_period", + "arguments": { + "data": { + "from_node": "bapcomposite1" + }, + "period": "month", + "reducer": { + "process_graph": { + "first1": { + "process_id": "first", + "arguments": { + "data": { + "from_parameter": "data" + } + }, + "result": true + } + } + } + } + }, + "apply1": { + "process_id": "apply", + "arguments": { + "data": { + "from_node": "aggregatetemporalperiod1" + }, + "process": { + "process_graph": { + "runudf1": { + "process_id": "run_udf", + "arguments": { + "data": { + "from_parameter": "x" + }, + "runtime": "Python", + "udf": "import xarray\nimport numpy as np\nimport pandas as pd\n\nfrom openeo.udf import inspect\n\n\"\"\"Spectral Angle Mapping UDF for Sentinel-2 based material matching.\n\nThis UDF compares each pixel spectrum against a USGS-derived reference library\nand returns the index of the best matching reference signature.\n\"\"\"\n\n\ndef compute_sam_w_nan(\n img: np.ndarray,\n usgs_df: pd.DataFrame,\n min_bands: int=12,\n) -> np.ndarray:\n \"\"\"Compute SAM\n\n Only overlapping finite bands are used for each pixel-reference pair.\n Pixels with fewer than min_bands valid overlaps are marked as no match.\n \"\"\"\n bands, height, width = img.shape\n # Flatten spatial dimensions so SAM can be computed in matrix form.\n pixel_spectra = img.reshape(bands, -1).T\n # Transpose library so each row corresponds to one reference signature.\n usgs_matrix = usgs_df.T.values\n\n n_pix = pixel_spectra.shape[0]\n n_mat = usgs_matrix.shape[0]\n\n # Pre-fill with +inf so non-computable matches remain easy to detect.\n angles = np.full((n_pix, n_mat), np.inf, dtype=np.float32)\n\n # Compute pixel finite mask once and reuse it for all reference signatures.\n Xfinite = np.isfinite(pixel_spectra)\n\n for j in range(n_mat):\n y = usgs_matrix[j]\n yfinite = np.isfinite(y)\n\n valid = Xfinite & yfinite[None, :]\n n_valid = valid.sum(axis=1)\n\n # Enforce a minimum number of overlapping bands per pixel-match pair.\n ok = n_valid >= min_bands\n if not np.any(ok):\n continue\n\n # Compute dot product and norms only over valid overlap.\n Xv = np.where(valid, pixel_spectra, 0.0)\n yv = np.where(yfinite, y, 0.0)\n\n dot = (Xv * yv[None, :]).sum(axis=1)\n nx = np.sqrt((Xv * Xv).sum(axis=1))\n ny = np.sqrt((yv * yv).sum())\n\n # Add a small epsilon to keep the denominator numerically safe.\n denom = nx * ny + 1e-12\n cosang = np.clip(dot / denom, -1.0, 1.0)\n\n # Convert cosine similarity to spectral angle in radians.\n ang = np.arccos(cosang).astype(np.float32)\n angles[ok, j] = ang[ok]\n\n # Select the reference with minimum angle for each pixel.\n best = np.argmin(angles, axis=1).reshape(height, width)\n # Pixels that never reached the min_bands threshold remain as no match.\n best[np.isinf(angles).all(axis=1).reshape(height, width)] = -1\n return best\n\ndef compute_sam_wo_nan(img: np.ndarray, usgs_df: pd.DataFrame) -> np.array:\n \"\"\"Compute SAM for fully valid arrays without missing values.\"\"\"\n bands, height, width = img.shape\n inspect(data=img.shape, message=\"SAM no-NaN path: input image shape\", level='debug')\n # Flatten to [pixels, bands] to vectorize pairwise comparisons.\n pixel_spectra = img.reshape(bands, -1).T\n\n # Normalize each pixel vector so comparison focuses on spectral shape.\n pixel_norms = np.linalg.norm(pixel_spectra, axis=1, keepdims=True)\n inspect(data=pixel_norms.shape, message=\"SAM no-NaN path: pixel norms shape\", level='debug')\n pixel_spectra_norm = pixel_spectra / (pixel_norms + 1e-10)\n inspect(data=pixel_spectra_norm.shape, message=\"SAM no-NaN path: normalized pixel spectra shape\", level='debug')\n\n # Normalize each reference signature with the same convention.\n inspect(data=usgs_df.shape, message=\"SAM no-NaN path: USGS dataframe shape\", level='debug')\n usgs_matrix = usgs_df.T.values\n inspect(data=usgs_matrix.shape, message=\"SAM no-NaN path: USGS matrix shape\", level='debug')\n usgs_norms = np.linalg.norm(usgs_matrix, axis=1, keepdims=True)\n inspect(data=usgs_norms.shape, message=\"SAM no-NaN path: USGS norms shape\", level='debug')\n usgs_matrix_norm = usgs_matrix / (usgs_norms + 1e-10)\n inspect(data=usgs_matrix_norm.shape, message=\"SAM no-NaN path: normalized USGS matrix shape\", level='debug')\n # Compute cosine similarity between each pixel and each reference spectrum.\n dot_product = np.dot(\n pixel_spectra_norm, usgs_matrix_norm.T\n )\n inspect(data=dot_product.shape, message=\"SAM no-NaN path: dot product shape\", level='debug')\n # Convert similarities to angles for SAM-based ranking.\n angles = np.arccos(np.clip(dot_product, -1.0, 1.0))\n inspect(data=angles.shape, message=\"SAM no-NaN path: angle matrix shape\", level='debug')\n # Lower angle means closer spectral match.\n best_match_indices = np.argmin(angles, axis=1)\n inspect(data=best_match_indices.shape, message=\"SAM no-NaN path: best match index vector shape\", level='debug')\n # Restore the original 2D spatial layout.\n match_index_map = best_match_indices.reshape(height, width)\n inspect(data=match_index_map.shape, message=\"SAM no-NaN path: output map shape\", level='debug')\n return match_index_map\n\n\n\n# UDF entry point\ndef apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray:\n \"\"\"openEO UDF entry point.\n\n Expects an input cube with band-first layout and returns a 2D class-index map.\n \"\"\"\n inspect(data=cube.dims, message=\"SAM UDF: input cube dimensions\", level='debug')\n inspect(data=cube.shape, message=\"SAM UDF: input cube shape\", level='debug')\n # External reference library used for spectral matching.\n csv_url = \"https://artifactory.vgt.vito.be/artifactory/auxdata-public/openEO_terravision/S2_USGS_library498.csv\"\n usgs_df = pd.read_csv(csv_url, sep=\";\")\n inspect(data=usgs_df.shape, message=\"SAM UDF: loaded USGS dataframe shape\", level='debug')\n\n # Use the NaN-aware path only when needed.\n if np.isnan(cube.values).any():\n result_cube = compute_sam_w_nan(cube.values, usgs_df, 12)\n else:\n result_cube = compute_sam_wo_nan(cube.values, usgs_df)\n\n # Return only spatial dimensions, with class index per pixel.\n return xarray.DataArray(result_cube, dims=cube.dims[1:], coords={dim: cube.coords[dim] for dim in cube.dims[1:]})" + }, + "result": true + } + } + } + }, + "result": true + } + }, + "id": "s2_sam", + "description": "# Sentinel-2 based Spectral Angle Mapping (S2-SAM)\n\nThis service applies Spectral Angle Mapping (SAM) to Sentinel-2 Level-2A reflectance data.\nFor each pixel, the algorithm compares the pixel spectrum to a spectral reference library and selects the closest match based on the smallest spectral angle.\n\n## What this service does\n\nSAM measures similarity in spectral *shape* (not absolute brightness):\n\n$$\nθ = \\cos^{-1}\\left(\\frac{x \\cdot y}{\\|x\\|\\|y\\|}\\right)\n$$\n\nwhere:\n- $x$ is the pixel spectrum.\n- $y$ is a reference spectrum from the library.\n- smaller $\\theta$ means a better match.\n\nThe output is a raster of class indices, where each pixel value is the index of the best-matching reference signature.\n\n## Sentinel-2 bands used\n\nThe service uses only the Sentinel-2 L2A bands provided by the BAP composite input, not all L2A layers.\nThe band subset is (B01, B02, B03, B04, B05, B06, B07, B08, B8A, B09, B11, B12).\n\nInput compositing is based on the BAP process:\n[https://algorithm-catalogue.apex.esa.int/apps/bap_composite](https://algorithm-catalogue.apex.esa.int/apps/bap_composite)\n\n## Reference spectral library\n\nThe reference library is based on a Sentinel-2-resampled USGS mineral/material collection.\nIt includes a broad set of geology and mining-related targets (e.g. iron oxides, clays, sulfates, silicates, carbonates, and related materials).\n\nThe CSV used by the service contains:\n- 498 named reference signatures (material columns).\n- 1 `cwvl` column with central wavelength values.\n\nExamples of included targets are minerals such as Acmite, Actinolite, Alunite, Jarosite, Kaolinite, Hematite, Goethite, Pyrite, Chalcopyrite, and many more variants.\n\n## Notes on interpretation\n\n- The product reports the **closest spectral match** in the selected library, not a definitive mineral identification.\n- Similar materials can produce similar spectra at Sentinel-2 spectral resolution.\n- Results are most useful when combined with geological context, ancillary data, and expert validation.\n\n## Missing data handling\n\nIf some bands are missing (`NaN`) at a pixel, the implementation computes SAM using only the available bands, with a minimum valid-band threshold.\nPixels without enough valid information are marked as no match.\n\n", + "parameters": [ + { + "name": "spatial_extent", + "description": "Limits the data to process to the specified bounding box or polygons.\\n\\nFor raster data, the process loads the pixel into the data cube if the point at the pixel center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC).\\nFor vector data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been provided.\\n\\nEmpty geometries are ignored.\\nSet this parameter to null to set no limit for the spatial extent.", + "schema": [ + { + "title": "Bounding Box", + "type": "object", + "subtype": "bounding-box", + "required": [ + "west", + "south", + "east", + "north" + ], + "properties": { + "west": { + "description": "West (lower left corner, coordinate axis 1).", + "type": "number" + }, + "south": { + "description": "South (lower left corner, coordinate axis 2).", + "type": "number" + }, + "east": { + "description": "East (upper right corner, coordinate axis 1).", + "type": "number" + }, + "north": { + "description": "North (upper right corner, coordinate axis 2).", + "type": "number" + }, + "base": { + "description": "Base (optional, lower left corner, coordinate axis 3).", + "type": [ + "number", + "null" + ], + "default": null + }, + "height": { + "description": "Height (optional, upper right corner, coordinate axis 3).", + "type": [ + "number", + "null" + ], + "default": null + }, + "crs": { + "description": "Coordinate reference system of the extent, specified as as [EPSG code](http://www.epsg-registry.org/) or [WKT2 CRS string](http://docs.opengeospatial.org/is/18-010r7/18-010r7.html). Defaults to `4326` (EPSG code 4326) unless the client explicitly requests a different coordinate reference system.", + "anyOf": [ + { + "title": "EPSG Code", + "type": "integer", + "subtype": "epsg-code", + "minimum": 1000, + "examples": [ + 3857 + ] + }, + { + "title": "WKT2", + "type": "string", + "subtype": "wkt2-definition" + } + ], + "default": 4326 + } + } + }, + { + "title": "Vector data cube", + "description": "Limits the data cube to the bounding box of the given geometries in the vector data cube. For raster data, all pixels inside the bounding box that do not intersect with any of the polygons will be set to no data (`null`). Empty geometries are ignored.", + "type": "object", + "subtype": "datacube", + "dimensions": [ + { + "type": "geometry" + } + ] + }, + { + "title": "No filter", + "description": "Don't filter spatially. All data is included in the data cube.", + "type": "null" + } + ] + }, + { + "name": "temporal_extent", + "description": "Temporal extent specified as two-element array with start and end date/date-time.", + "schema": { + "type": "array", + "subtype": "temporal-interval", + "uniqueItems": true, + "minItems": 2, + "maxItems": 2, + "items": { + "anyOf": [ + { + "type": "string", + "subtype": "date-time", + "format": "date-time" + }, + { + "type": "string", + "subtype": "date", + "format": "date" + }, + { + "type": "null" + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/algorithm_catalog/terravision/s2_sam/openeo_udp/sam_udf.py b/algorithm_catalog/terravision/s2_sam/openeo_udp/sam_udf.py new file mode 100644 index 00000000..131e4525 --- /dev/null +++ b/algorithm_catalog/terravision/s2_sam/openeo_udp/sam_udf.py @@ -0,0 +1,132 @@ +import xarray +import numpy as np +import pandas as pd + +from openeo.udf import inspect + +"""Spectral Angle Mapping UDF for Sentinel-2 based material matching. + +This UDF compares each pixel spectrum against a USGS-derived reference library +and returns the index of the best matching reference signature. +""" + + +def compute_sam_w_nan( + img: np.ndarray, + usgs_df: pd.DataFrame, + min_bands: int=12, +) -> np.ndarray: + """Compute SAM + + Only overlapping finite bands are used for each pixel-reference pair. + Pixels with fewer than min_bands valid overlaps are marked as no match. + """ + bands, height, width = img.shape + # Flatten spatial dimensions so SAM can be computed in matrix form. + pixel_spectra = img.reshape(bands, -1).T + # Transpose library so each row corresponds to one reference signature. + usgs_matrix = usgs_df.T.values + + n_pix = pixel_spectra.shape[0] + n_mat = usgs_matrix.shape[0] + + # Pre-fill with +inf so non-computable matches remain easy to detect. + angles = np.full((n_pix, n_mat), np.inf, dtype=np.float32) + + # Compute pixel finite mask once and reuse it for all reference signatures. + Xfinite = np.isfinite(pixel_spectra) + + for j in range(n_mat): + y = usgs_matrix[j] + yfinite = np.isfinite(y) + + valid = Xfinite & yfinite[None, :] + n_valid = valid.sum(axis=1) + + # Enforce a minimum number of overlapping bands per pixel-match pair. + ok = n_valid >= min_bands + if not np.any(ok): + continue + + # Compute dot product and norms only over valid overlap. + Xv = np.where(valid, pixel_spectra, 0.0) + yv = np.where(yfinite, y, 0.0) + + dot = (Xv * yv[None, :]).sum(axis=1) + nx = np.sqrt((Xv * Xv).sum(axis=1)) + ny = np.sqrt((yv * yv).sum()) + + # Add a small epsilon to keep the denominator numerically safe. + denom = nx * ny + 1e-12 + cosang = np.clip(dot / denom, -1.0, 1.0) + + # Convert cosine similarity to spectral angle in radians. + ang = np.arccos(cosang).astype(np.float32) + angles[ok, j] = ang[ok] + + # Select the reference with minimum angle for each pixel. + best = np.argmin(angles, axis=1).reshape(height, width) + # Pixels that never reached the min_bands threshold remain as no match. + best[np.isinf(angles).all(axis=1).reshape(height, width)] = -1 + return best + +def compute_sam_wo_nan(img: np.ndarray, usgs_df: pd.DataFrame) -> np.array: + """Compute SAM for fully valid arrays without missing values.""" + bands, height, width = img.shape + inspect(data=img.shape, message="SAM no-NaN path: input image shape", level='debug') + # Flatten to [pixels, bands] to vectorize pairwise comparisons. + pixel_spectra = img.reshape(bands, -1).T + + # Normalize each pixel vector so comparison focuses on spectral shape. + pixel_norms = np.linalg.norm(pixel_spectra, axis=1, keepdims=True) + inspect(data=pixel_norms.shape, message="SAM no-NaN path: pixel norms shape", level='debug') + pixel_spectra_norm = pixel_spectra / (pixel_norms + 1e-10) + inspect(data=pixel_spectra_norm.shape, message="SAM no-NaN path: normalized pixel spectra shape", level='debug') + + # Normalize each reference signature with the same convention. + inspect(data=usgs_df.shape, message="SAM no-NaN path: USGS dataframe shape", level='debug') + usgs_matrix = usgs_df.T.values + inspect(data=usgs_matrix.shape, message="SAM no-NaN path: USGS matrix shape", level='debug') + usgs_norms = np.linalg.norm(usgs_matrix, axis=1, keepdims=True) + inspect(data=usgs_norms.shape, message="SAM no-NaN path: USGS norms shape", level='debug') + usgs_matrix_norm = usgs_matrix / (usgs_norms + 1e-10) + inspect(data=usgs_matrix_norm.shape, message="SAM no-NaN path: normalized USGS matrix shape", level='debug') + # Compute cosine similarity between each pixel and each reference spectrum. + dot_product = np.dot( + pixel_spectra_norm, usgs_matrix_norm.T + ) + inspect(data=dot_product.shape, message="SAM no-NaN path: dot product shape", level='debug') + # Convert similarities to angles for SAM-based ranking. + angles = np.arccos(np.clip(dot_product, -1.0, 1.0)) + inspect(data=angles.shape, message="SAM no-NaN path: angle matrix shape", level='debug') + # Lower angle means closer spectral match. + best_match_indices = np.argmin(angles, axis=1) + inspect(data=best_match_indices.shape, message="SAM no-NaN path: best match index vector shape", level='debug') + # Restore the original 2D spatial layout. + match_index_map = best_match_indices.reshape(height, width) + inspect(data=match_index_map.shape, message="SAM no-NaN path: output map shape", level='debug') + return match_index_map + + + +# UDF entry point +def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray: + """openEO UDF entry point. + + Expects an input cube with band-first layout and returns a 2D class-index map. + """ + inspect(data=cube.dims, message="SAM UDF: input cube dimensions", level='debug') + inspect(data=cube.shape, message="SAM UDF: input cube shape", level='debug') + # External reference library used for spectral matching. + csv_url = "https://artifactory.vgt.vito.be/artifactory/auxdata-public/openEO_terravision/S2_USGS_library498.csv" + usgs_df = pd.read_csv(csv_url, sep=";") + inspect(data=usgs_df.shape, message="SAM UDF: loaded USGS dataframe shape", level='debug') + + # Use the NaN-aware path only when needed. + if np.isnan(cube.values).any(): + result_cube = compute_sam_w_nan(cube.values, usgs_df, 12) + else: + result_cube = compute_sam_wo_nan(cube.values, usgs_df) + + # Return only spatial dimensions, with class index per pixel. + return xarray.DataArray(result_cube, dims=cube.dims[1:], coords={dim: cube.coords[dim] for dim in cube.dims[1:]}) \ No newline at end of file diff --git a/algorithm_catalog/terravision/s2_sam/records/s2_sam.json b/algorithm_catalog/terravision/s2_sam/records/s2_sam.json new file mode 100644 index 00000000..766195c7 --- /dev/null +++ b/algorithm_catalog/terravision/s2_sam/records/s2_sam.json @@ -0,0 +1,127 @@ +{ + "id": "s2_sam", + "type": "Feature", + "conformsTo": [ + "https://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core", + "https://apex.esa.int/core/openeo-udp" + ], + "geometry": null, + "properties": { + "created": "2026-07-13T00:00:00Z", + "updated": "2026-08-11T00:00:00Z", + "type": "service", + "title": "Sentinel-2 based Spectral Angle Mapping (S2-SAM)", + "description": "Computes Spectral Angle Mapping (SAM) from a Sentinel-2 L2A BAP composite to identify the closest matching signature from a Sentinel-2-resampled USGS spectral library for geology and mining related targets.", + "keywords": [ + "Geology", + "Mining", + "Mineral Mapping", + "Sentinel-2", + "Spectral Angle Mapping (SAM)", + "Remote Sensing" + ], + "language": { + "code": "en-US", + "name": "English (United States)" + }, + "languages": [ + { + "code": "en-US", + "name": "English (United States)" + } + ], + "contacts": [ + { + "name": "Christoph Becker", + "position": "Researcher", + "organization": "VITO", + "links": [ + { + "href": "https://www.vito.be/", + "title": "VITO Website", + "rel": "about", + "type": "text/html" + } + ], + "contactInstructions": "Contact via VITO", + "roles": [ + "principal investigator" + ] + } + ], + "themes": [ + { + "concepts": [ + { + "id": "GEOLOGY" + }, + { + "id": "MINING" + }, + { + "id": "REMOTE SENSING" + }, + { + "id": "Sentinel-2 MSI" + } + ], + "scheme": "https://gcmd.earthdata.nasa.gov/kms/concepts/concept_scheme/sciencekeywords" + } + ], + "formats": [ + { + "name": "NetCDF" + } + ], + "license": "Proprietary", + "version": "1.0.0", + "publisher": { + "name": "TERRAVISION" + } + }, + "linkTemplates": [], + "links": [ + { + "rel": "application", + "type": "application/vnd.openeo+json;type=process", + "title": "openEO Process Definition", + "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json" + }, + { + "rel": "code", + "type": "text/html", + "title": "S2-SAM source code", + "href": "https://github.com/ESA-APEx/apex_algorithms" + }, + { + "rel": "service", + "type": "application/json", + "title": "CDSE openEO Federation", + "href": "https://openeofed.dataspace.copernicus.eu" + }, + { + "rel": "platform", + "type": "application/json", + "title": "CDSE openEO Federation processing backend", + "href": "../../../../platform_catalog/cdse_openeo_federation.json" + }, + { + "rel": "provider", + "type": "application/json", + "title": "TERRAVISION", + "href": "../../record.json" + }, + { + "rel": "notebook", + "type": "application/x-ipynb+json", + "title": "S2-SAM example notebook", + "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb" + }, + { + "rel": "webapp", + "type": "text/html", + "title": "OpenEO Web Editor", + "href": "https://editor.openeo.org/?wizard=UDP&wizard%7Eprocess=s2_sam&wizard%7EprocessUrl=https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json&server=https%3A%2F%2Fopeneofed.dataspace.copernicus.eu" + } + ] +} \ No newline at end of file From 8877d82fcebb16d6c40aa99b1421881ce660e49b Mon Sep 17 00:00:00 2001 From: Pratichhya Sharma Date: Tue, 11 Aug 2026 15:48:16 +0200 Subject: [PATCH 2/5] updated commit num --- .../s2_sam/benchmark_scenarios/s2_sam.json | 119 ++++++++++++------ .../terravision/s2_sam/records/s2_sam.json | 6 +- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json b/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json index 4caa74ef..1dcaadc6 100644 --- a/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json +++ b/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json @@ -1,38 +1,85 @@ [ - { - "id": "s2_sam", - "type": "openeo", - "description": "Run Sentinel-2 based Spectral Angle Mapping (S2-SAM) on a monthly BAP composite and return the best matching USGS spectral library class index per pixel.", - "backend": "openeofed.dataspace.copernicus.eu", - "process_graph": { - "s2sam1": { - "process_id": "s2_sam", - "namespace": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json", - "arguments": { - "spatial_extent": { - "west": 5.05, - "south": 51.21, - "east": 5.1, - "north": 51.23 - }, - "temporal_extent": [ - "2025-05-01", - "2025-05-31" - ] - } - }, - "saveresult1": { - "process_id": "save_result", - "arguments": { - "data": { - "from_node": "s2sam1" - }, - "format": "NetCDF", - "options": {} + { + "id": "s2_sam", + "type": "openeo", + "description": "Run Sentinel-2 based Spectral Angle Mapping (S2-SAM) on a monthly BAP composite and return the best matching USGS spectral library class index per pixel.", + "backend": "openeofed.dataspace.copernicus.eu", + "process_graph": { + "s2sam1": { + "arguments": { + "spatial_extent": { + "geometry": { + "coordinates": [ + [ + [ + -15.487987, + 27.813076 + ], + [ + -15.489753, + 27.814047 + ], + [ + -15.493856, + 27.809741 + ], + [ + -15.492555, + 27.808545 + ], + [ + -15.487809, + 27.806311 + ], + [ + -15.485954, + 27.809176 + ], + [ + -15.484348, + 27.812444 + ], + [ + -15.48635, + 27.81229 + ], + [ + -15.487987, + 27.813076 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "CNTR_ID": "ES", + "ID": "598" + }, + "type": "Feature" + }, + "temporal_extent": [ + "2024-05-01", + "2024-05-31" + ] + }, + "namespace": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json", + "process_id": "s2_sam" + }, + "saveresult1": { + "arguments": { + "data": { + "from_node": "s2sam1" + }, + "format": "NetCDF", + "options": {} + }, + "process_id": "save_result", + "result": true + } }, - "result": true - } - }, - "reference_data": {} - } -] + "reference_data": { + "job-results.json": "https://s3.waw3-1.cloudferro.com/apex-benchmarks-internal/HP96!tests_test_benchmarks.py__test_run_benchmark_bsi_!actual/job-results.json", + "openEO.nc": "https://s3.waw3-1.cloudferro.com/apex-benchmarks-internal/HP96!tests_test_benchmarks.py__test_run_benchmark_bsi_!actual/openEO.nc" + } + } +] \ No newline at end of file diff --git a/algorithm_catalog/terravision/s2_sam/records/s2_sam.json b/algorithm_catalog/terravision/s2_sam/records/s2_sam.json index 766195c7..9720bd0c 100644 --- a/algorithm_catalog/terravision/s2_sam/records/s2_sam.json +++ b/algorithm_catalog/terravision/s2_sam/records/s2_sam.json @@ -85,7 +85,7 @@ "rel": "application", "type": "application/vnd.openeo+json;type=process", "title": "openEO Process Definition", - "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json" + "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json" }, { "rel": "code", @@ -115,13 +115,13 @@ "rel": "notebook", "type": "application/x-ipynb+json", "title": "S2-SAM example notebook", - "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb" + "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb" }, { "rel": "webapp", "type": "text/html", "title": "OpenEO Web Editor", - "href": "https://editor.openeo.org/?wizard=UDP&wizard%7Eprocess=s2_sam&wizard%7EprocessUrl=https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json&server=https%3A%2F%2Fopeneofed.dataspace.copernicus.eu" + "href": "https://editor.openeo.org/?wizard=UDP&wizard%7Eprocess=s2_sam&wizard%7EprocessUrl=https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json&server=https%3A%2F%2Fopeneofed.dataspace.copernicus.eu" } ] } \ No newline at end of file From cbd0e2ea82478d9ae4b83da60063ad0623067476 Mon Sep 17 00:00:00 2001 From: Pratichhya Sharma Date: Wed, 12 Aug 2026 12:18:49 +0200 Subject: [PATCH 3/5] updated reference data --- .../s2_sam/benchmark_scenarios/s2_sam.json | 4 +- .../terravision/s2_sam/notebook/s2_sam.ipynb | 107 +++++++++++++++++- .../terravision/s2_sam/records/s2_sam.json | 6 + 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json b/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json index 1dcaadc6..856a4cc6 100644 --- a/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json +++ b/algorithm_catalog/terravision/s2_sam/benchmark_scenarios/s2_sam.json @@ -78,8 +78,8 @@ } }, "reference_data": { - "job-results.json": "https://s3.waw3-1.cloudferro.com/apex-benchmarks-internal/HP96!tests_test_benchmarks.py__test_run_benchmark_bsi_!actual/job-results.json", - "openEO.nc": "https://s3.waw3-1.cloudferro.com/apex-benchmarks-internal/HP96!tests_test_benchmarks.py__test_run_benchmark_bsi_!actual/openEO.nc" + "job-results.json": "https://s3.waw3-1.cloudferro.com/apex-benchmarks-internal/HP120!tests_test_benchmarks.py__test_run_benchmark_s2_sam_!actual/job-results.json", + "openEO.nc": "https://s3.waw3-1.cloudferro.com/apex-benchmarks-internal/HP120!tests_test_benchmarks.py__test_run_benchmark_s2_sam_!actual/openEO.nc" } } ] \ No newline at end of file diff --git a/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb b/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb index 4bc613d9..6344366f 100644 --- a/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb +++ b/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb @@ -22,11 +22,114 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "26bc37db", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "spatial_extent = {\n", + " \"type\": \"Feature\",\n", + " \"properties\": {\n", + " \"ID\": \"598\",\n", + " \"CNTR_ID\": \"ES\"\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [[\n", + " [-15.487987, 27.813076],\n", + " [-15.489753, 27.814047],\n", + " [-15.493856, 27.809741],\n", + " [-15.492555, 27.808545],\n", + " [-15.487809, 27.806311],\n", + " [-15.485954, 27.809176],\n", + " [-15.484348, 27.812444],\n", + " [-15.486350, 27.812290],\n", + " [-15.487987, 27.813076]\n", + " ]]\n", + " }\n", + "}\n", + "temporal_extent = [\"2024-05-01\", \"2024-05-31\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9fb09937", + "metadata": {}, + "outputs": [], + "source": [ + "public_url = \"https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json\"\n", + "sam_cube = connection.datacube_from_process(\n", + " \"s2_sam\", \n", + " namespace = public_url,\n", + " temporal_extent = temporal_extent,\n", + " spatial_extent= spatial_extent\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4918618a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0:00:00 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': send 'start'\n", + "0:00:05 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n", + "0:00:10 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n", + "0:00:17 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n", + "0:00:26 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n", + "0:00:36 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n", + "0:00:48 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n", + "0:01:04 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n", + "0:01:23 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n", + "0:01:47 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n", + "0:02:17 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n", + "0:02:54 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n", + "0:03:41 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': finished (progress 100%)\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job = sam_cube.create_job(\n", + " title=f'S2 SAM processing for {temporal_extent[0]} to {temporal_extent[1]}',\n", + " out_format=\"NetCDF\")\n", + "job.start_and_wait()" + ] } ], "metadata": { diff --git a/algorithm_catalog/terravision/s2_sam/records/s2_sam.json b/algorithm_catalog/terravision/s2_sam/records/s2_sam.json index 9720bd0c..fa6014f5 100644 --- a/algorithm_catalog/terravision/s2_sam/records/s2_sam.json +++ b/algorithm_catalog/terravision/s2_sam/records/s2_sam.json @@ -122,6 +122,12 @@ "type": "text/html", "title": "OpenEO Web Editor", "href": "https://editor.openeo.org/?wizard=UDP&wizard%7Eprocess=s2_sam&wizard%7EprocessUrl=https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json&server=https%3A%2F%2Fopeneofed.dataspace.copernicus.eu" + }, + { + "rel": "thumbnail", + "type": "image/png", + "title": "Thumbnail image", + "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/records/thumbnail.png" } ] } \ No newline at end of file From dc4fe8715fc01539896db75f751278fe6189a59d Mon Sep 17 00:00:00 2001 From: Pratichhya Sharma Date: Wed, 12 Aug 2026 12:19:07 +0200 Subject: [PATCH 4/5] thumbnail --- .../terravision/s2_sam/records/thumbnail.png | Bin 0 -> 22414 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 algorithm_catalog/terravision/s2_sam/records/thumbnail.png diff --git a/algorithm_catalog/terravision/s2_sam/records/thumbnail.png b/algorithm_catalog/terravision/s2_sam/records/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..2b028e83c720548fc0b054bc1e246e4d3690e520 GIT binary patch literal 22414 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 eY6~;FrgqJsonlk;bPBGrO-IvE Date: Wed, 12 Aug 2026 12:24:38 +0200 Subject: [PATCH 5/5] updated the commit --- algorithm_catalog/terravision/s2_sam/records/s2_sam.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algorithm_catalog/terravision/s2_sam/records/s2_sam.json b/algorithm_catalog/terravision/s2_sam/records/s2_sam.json index fa6014f5..ea511c97 100644 --- a/algorithm_catalog/terravision/s2_sam/records/s2_sam.json +++ b/algorithm_catalog/terravision/s2_sam/records/s2_sam.json @@ -115,7 +115,7 @@ "rel": "notebook", "type": "application/x-ipynb+json", "title": "S2-SAM example notebook", - "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb" + "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/dc4fe8715fc01539896db75f751278fe6189a59d/algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb" }, { "rel": "webapp", @@ -127,7 +127,7 @@ "rel": "thumbnail", "type": "image/png", "title": "Thumbnail image", - "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/records/thumbnail.png" + "href": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/dc4fe8715fc01539896db75f751278fe6189a59d/algorithm_catalog/terravision/s2_sam/records/thumbnail.png" } ] } \ No newline at end of file