Repository: developmentseed/titiler (python)

Issue:
Add more Algorithms (min/max/mean/median.....)
ref https://github.com/sentinel-hub/titiler-openeo/blob/5452bd2d5026c761526d0ad0a715af28dbef352a/titiler/openeo/processes/implementations/math.py#L175-L202

Pull request description:
add `min`, `max`, `mean`, `median`, `std` and `var` algorithms
closes #1137 

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index b956b63..f725e7e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -12,6 +12,8 @@
 
 ### titiler.core
 
+* add `min`, `max`, `mean`, `median`, `std` and `var` algorithms
+
 * Fix TerrainRGB algorithm and param user-controlled nodata-height (@jo-chemla, https://github.com/developmentseed/titiler/pull/1116)
 
 * add `output_min` and `output_max` metadata attributes to `slope` algorithm (@tayden, https://github.com/developmentseed/titiler/pull/1089)
diff --git a/src/titiler/core/titiler/core/algorithm/__init__.py b/src/titiler/core/titiler/core/algorithm/__init__.py
index 1915dba..883cf85 100644
--- a/src/titiler/core/titiler/core/algorithm/__init__.py
+++ b/src/titiler/core/titiler/core/algorithm/__init__.py
@@ -13,6 +13,7 @@ from titiler.core.algorithm.base import AlgorithmMetadata  # noqa
 from titiler.core.algorithm.base import BaseAlgorithm
 from titiler.core.algorithm.dem import Contours, HillShade, Slope, TerrainRGB, Terrarium
 from titiler.core.algorithm.index import NormalizedIndex
+from titiler.core.algorithm.math import _Max, _Mean, _Median, _Min, _Std, _Var
 from titiler.core.algorithm.ops import CastToInt, Ceil, Floor
 
 default_algorithms: Dict[str, Type[BaseAlgorithm]] = {
@@ -25,6 +26,12 @@ default_algorithms: Dict[str, Type[BaseAlgorithm]] = {
     "cast": CastToInt,
     "ceil": Ceil,
     "floor": Floor,
+    "min": _Min,
+    "max": _Max,
+    "median": _Median,
+    "mean": _Mean,
+    "std": _Std,
+    "var": _Var,
 }
 
 
diff --git a/src/titiler/core/titiler/core/algorithm/math.py b/src/titiler/core/titiler/core/algorithm/math.py
new file mode 100644
index 0000000..4252425
--- /dev/null
+++ b/src/titiler/core/titiler/core/algorithm/math.py
@@ -0,0 +1,138 @@
+"""titiler.core.algorithm Math."""
+
+import numpy
+from rio_tiler.models import ImageData
+
+from titiler.core.algorithm.base import BaseAlgorithm
+
+__all__ = ["_Min", "_Max", "_Median", "_Mean", "_Std", "_Var"]
+
+
+class _Min(BaseAlgorithm):
+    """Return Min values along the `bands` axis."""
+
+    title: str = "Min"
+    description: str = "Return Min values along the `bands` axis."
+
+    output_nbands: int = 1
+
+    def __call__(self, img: ImageData) -> ImageData:
+        """Return Min."""
+        return ImageData(
+            numpy.ma.min(img.array, axis=0, keepdims=True),
+            assets=img.assets,
+            crs=img.crs,
+            bounds=img.bounds,
+            band_names=["min"],
+            metadata=img.metadata,
+            cutline_mask=img.cutline_mask,
+        )
+
+
+class _Max(BaseAlgorithm):
+    """Return Max values along the `bands` axis."""
+
+    title: str = "Max"
+    description: str = "Return Max values along the `bands` axis."
+
+    output_nbands: int = 1
+
+    def __call__(self, img: ImageData) -> ImageData:
+        """Return Max."""
+        return ImageData(
+            numpy.ma.max(img.array, axis=0, keepdims=True),
+            assets=img.assets,
+            crs=img.crs,
+            bounds=img.bounds,
+            band_names=["max"],
+            metadata=img.metadata,
+            cutline_mask=img.cutline_mask,
+        )
+
+
+class _Median(BaseAlgorithm):
+    """Return Median values along the `bands` axis."""
+
+    title: str = "Median"
+    description: str = "Return Median values along the `bands` axis."
+
+    output_nbands: int = 1
+    output_dtype: str = "float64"
+
+    def __call__(self, img: ImageData) -> ImageData:
+        """Return Median."""
+        return ImageData(
+            numpy.ma.median(img.array, axis=0, keepdims=True),
+            assets=img.assets,
+            crs=img.crs,
+            bounds=img.bounds,
+            band_names=["median"],
+            metadata=img.metadata,
+            cutline_mask=img.cutline_mask,
+        )
+
+
+class _Mean(BaseAlgorithm):
+    """Return Mean values along the `bands` axis."""
+
+    title: str = "Mean"
+    description: str = "Return Mean values."
+
+    output_nbands: int = 1
+    output_dtype: str = "float64"
+
+    def __call__(self, img: ImageData) -> ImageData:
+        """Return Mean."""
+        return ImageData(
+            numpy.ma.mean(img.array, axis=0, keepdims=True),
+            assets=img.assets,
+            crs=img.crs,
+            bounds=img.bounds,
+            band_names=["mean"],
+            metadata=img.metadata,
+            cutline_mask=img.cutline_mask,
+        )
+
+
+class _Std(BaseAlgorithm):
+    """Return the standard deviation along the `bands` axis."""
+
+    title: str = "Standard deviation"
+    description: str = "Return the Standard Deviation along the `bands` axis."
+
+    output_nbands: int = 1
+    output_dtype: str = "float64"
+
+    def __call__(self, img: ImageData) -> ImageData:
+        """Return Stddev."""
+        return ImageData(
+            numpy.ma.std(img.array, axis=0, keepdims=True, ddof=1),
+            assets=img.assets,
+            crs=img.crs,
+            bounds=img.bounds,
+            band_names=["std"],
+            metadata=img.metadata,
+            cutline_mask=img.cutline_mask,
+        )
+
+
+class _Var(BaseAlgorithm):
+    """Return Variance values along the `bands` axis."""
+
+    title: str = "Variance"
+    description: str = "Return Variance along the `bands` axis."
+
+    output_nbands: int = 1
+    output_dtype: str = "float64"
+
+    def __call__(self, img: ImageData) -> ImageData:
+        """Return Variance."""
+        return ImageData(
+            numpy.ma.var(img.array, axis=0, keepdims=True, ddof=1),
+            assets=img.assets,
+            crs=img.crs,
+            bounds=img.bounds,
+            band_names=["std"],
+            metadata=img.metadata,
+            cutline_mask=img.cutline_mask,
+        )


Test patch:
diff --git a/src/titiler/core/tests/test_algorithms.py b/src/titiler/core/tests/test_algorithms.py
index 4be542c..c48f404 100644
--- a/src/titiler/core/tests/test_algorithms.py
+++ b/src/titiler/core/tests/test_algorithms.py
@@ -3,6 +3,7 @@
 import json
 
 import numpy
+import pytest
 from fastapi import Depends, FastAPI
 from rasterio.io import MemoryFile
 from rio_tiler.models import ImageData
@@ -316,3 +317,35 @@ def test_ops():
     assert out.array.dtype == "uint8"
     assert out.array[0, 0, 0] == 2
     assert out.array[0, 1, 1] is numpy.ma.masked
+
+
+@pytest.mark.parametrize(
+    "name,numpy_method,options",
+    [
+        ("min", numpy.ma.min, {}),
+        ("max", numpy.ma.max, {}),
+        ("median", numpy.ma.median, {}),
+        ("mean", numpy.ma.mean, {}),
+        ("std", numpy.ma.std, {"ddof": 1}),
+        ("var", numpy.ma.var, {"ddof": 1}),
+    ],
+)
+def test_math_algorithm(name, numpy_method, options):
+    """test math algos."""
+    arr = numpy.ma.MaskedArray(
+        numpy.random.randint(0, 5000, (1, 256, 256)).astype("float32"),
+        mask=numpy.zeros((1, 256, 256), dtype="bool"),
+    )
+    arr.data[0, 0, 0] = 1.6
+    arr.mask[0, 1:100, 1:100] = True
+
+    img = ImageData(arr)
+    assert img.array.dtype == numpy.float32
+
+    algo = default_algorithms.get(name)()
+    out = algo(img)
+
+    numpy.testing.assert_array_equal(
+        out.array, numpy_method(img.array, axis=0, keepdims=True, **options)
+    )
+    assert out.array[0, 1, 1] is numpy.ma.masked