feat: embed gf3 water extraction workflow
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""Raster transforms, thresholding, and morphology."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from scipy import ndimage as ndi
|
||||
|
||||
|
||||
def slope_degrees(dem: np.ndarray, dx_deg: float, dy_deg: float, center_lat: float) -> np.ndarray:
|
||||
meters_per_deg_lat = 111_320.0
|
||||
meters_per_deg_lon = 111_320.0 * math.cos(math.radians(center_lat))
|
||||
dz_dy, dz_dx = np.gradient(dem.astype(np.float32), dy_deg * meters_per_deg_lat, dx_deg * meters_per_deg_lon)
|
||||
slope = np.degrees(np.arctan(np.sqrt(dz_dx * dz_dx + dz_dy * dz_dy)))
|
||||
slope[~np.isfinite(slope)] = np.nan
|
||||
return slope.astype(np.float32)
|
||||
|
||||
|
||||
def to_db(arr: np.ndarray, eps: float = 1e-8) -> np.ndarray:
|
||||
out = np.full(arr.shape, np.nan, dtype=np.float32)
|
||||
valid = np.isfinite(arr) & (arr > 0)
|
||||
out[valid] = 10.0 * np.log10(arr[valid] + eps)
|
||||
return out
|
||||
|
||||
|
||||
def robust_normalize(arr: np.ndarray, valid: np.ndarray, q_low: float = 2.0, q_high: float = 98.0) -> tuple[np.ndarray, float, float]:
|
||||
values = arr[valid]
|
||||
lo, hi = np.nanpercentile(values, [q_low, q_high])
|
||||
if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo:
|
||||
lo, hi = float(np.nanmin(values)), float(np.nanmax(values))
|
||||
norm = np.clip((arr - lo) / max(hi - lo, 1e-6), 0.0, 1.0)
|
||||
norm[~valid] = np.nan
|
||||
return norm.astype(np.float32), float(lo), float(hi)
|
||||
|
||||
|
||||
def otsu_threshold(values: np.ndarray, bins: int = 512) -> float:
|
||||
values = values[np.isfinite(values)]
|
||||
if values.size == 0:
|
||||
raise ValueError("No finite values for thresholding")
|
||||
hist, edges = np.histogram(values, bins=bins)
|
||||
centers = (edges[:-1] + edges[1:]) * 0.5
|
||||
weight1 = np.cumsum(hist).astype(np.float64)
|
||||
weight2 = np.cumsum(hist[::-1]).astype(np.float64)[::-1]
|
||||
mean1 = np.cumsum(hist * centers) / np.maximum(weight1, 1.0)
|
||||
mean2 = (np.cumsum((hist * centers)[::-1]) / np.maximum(weight2[::-1], 1.0))[::-1]
|
||||
variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2
|
||||
return float(centers[:-1][np.argmax(variance12)])
|
||||
|
||||
|
||||
def remove_small_components(mask: np.ndarray, min_pixels: int) -> np.ndarray:
|
||||
if min_pixels <= 1:
|
||||
return mask
|
||||
labels, count = ndi.label(mask)
|
||||
if count == 0:
|
||||
return mask
|
||||
sizes = np.bincount(labels.ravel())
|
||||
keep = sizes >= min_pixels
|
||||
keep[0] = False
|
||||
return keep[labels]
|
||||
|
||||
|
||||
def fill_small_holes(mask: np.ndarray, max_pixels: int) -> np.ndarray:
|
||||
if max_pixels <= 0:
|
||||
return mask
|
||||
inv = ~mask
|
||||
labels, count = ndi.label(inv)
|
||||
if count == 0:
|
||||
return mask
|
||||
border = np.unique(np.concatenate([labels[0, :], labels[-1, :], labels[:, 0], labels[:, -1]]))
|
||||
sizes = np.bincount(labels.ravel())
|
||||
fill = sizes <= max_pixels
|
||||
fill[border] = False
|
||||
out = mask.copy()
|
||||
out[fill[labels]] = True
|
||||
return out
|
||||
|
||||
|
||||
def disk_structure(radius: int) -> np.ndarray:
|
||||
if radius <= 0:
|
||||
return np.ones((1, 1), dtype=bool)
|
||||
y, x = np.ogrid[-radius : radius + 1, -radius : radius + 1]
|
||||
return (x * x + y * y) <= radius * radius
|
||||
|
||||
|
||||
def close_mask(mask: np.ndarray, radius: int, valid: np.ndarray) -> np.ndarray:
|
||||
if radius <= 0:
|
||||
return mask
|
||||
closed = ndi.binary_closing(mask & valid, structure=disk_structure(radius))
|
||||
return closed & valid
|
||||
|
||||
|
||||
def open_mask(mask: np.ndarray, radius: int, valid: np.ndarray) -> np.ndarray:
|
||||
if radius <= 0:
|
||||
return mask
|
||||
opened = ndi.binary_opening(mask & valid, structure=disk_structure(radius))
|
||||
return opened & valid
|
||||
|
||||
Reference in New Issue
Block a user