SM737's picture
Upload folder using huggingface_hub (part 4)
a358495 verified
Raw History Blame Contribute Delete
5.27 kB
"""SAR radiometry contract with improved speckle filtering and adaptive thresholding."""
from dataclasses import dataclass
import numpy as np
import rasterio
from scipy import ndimage as ndi
from satquery_engine.services.bands import detect_band_map, normalize
@dataclass
class SARData:
db: np.ndarray
valid: np.ndarray
configuration: dict
class SARPreprocessor:
"""Enhanced SAR preprocessing with multi-stage speckle filtering and dual-pol support."""
def process(self, path, polarizations=("vv", "vh")):
with rasterio.open(path) as src:
bands = detect_band_map(src)
# Gracefully downgrade to single-pol if VH is unavailable
available_pols = tuple(p for p in polarizations if p in bands.indices)
if not available_pols:
raise ValueError("SAR preprocessing requires confirmed polarization bands.")
tags = {normalize(k): normalize(v) for k, v in src.tags().items()}
units = tags.get("units", tags.get("unit", tags.get("sarunits", "")))
representation = tags.get("representation", tags.get("sampletype", ""))
if units in {"db", "decibel", "decibels"}:
method = "db"
elif units in {"linear", "intensity", "power", "amplitude"}:
method = "amplitude" if representation == "amplitude" or units == "amplitude" else "intensity" if representation in {"intensity", "power", "sigma0", "gamma0", "beta0"} or units in {"intensity", "power"} else None
else:
method = None
if method is None:
raise ValueError("SAR units/representation are unknown. Confirm dB, linear intensity, or linear amplitude before analysis.")
indexes = [bands.indices[p] for p in available_pols]
values = src.read(indexes, masked=True).astype("float32").filled(np.nan)
for i, index in enumerate(indexes):
values[i] = values[i] * src.scales[index - 1] + src.offsets[index - 1]
valid = np.all(np.isfinite(values), axis=0)
if method != "db":
valid &= np.all(values > 0, axis=0)
values = (20 if method == "amplitude" else 10) * np.log10(np.where(values > 0, values, np.nan))
values[:, ~valid] = np.nan
if not valid.any():
raise ValueError("SAR image has no valid backscatter pixels.")
# Enhanced speckle filtering: multi-stage approach
# Stage 1: 5x5 median filter for impulse noise removal
filtered = np.empty_like(values)
for i in range(values.shape[0]):
filtered[i] = ndi.median_filter(np.where(valid, values[i], 0.0), size=5)
# Stage 2: 3x3 mean filter for residual speckle smoothing
for i in range(filtered.shape[0]):
filtered[i] = ndi.uniform_filter(filtered[i], size=3)
# Compute dual-pol ratio if both VV and VH available
dual_pol_ratio = None
if len(available_pols) >= 2 and "vv" in available_pols and "vh" in available_pols:
vv_idx = list(available_pols).index("vv")
vh_idx = list(available_pols).index("vh")
# VH/VV ratio in dB: water has low ratio, vegetation has high ratio
dual_pol_ratio = filtered[vh_idx] - filtered[vv_idx]
return SARData(filtered, valid, {
"version": "sar_units_v2",
"input_representation": method,
"output_units": "dB",
"polarizations": list(available_pols),
"band_indices": indexes,
"valid_fraction": float(valid.mean()),
"range_db": [float(np.nanmin(filtered)), float(np.nanmax(filtered))],
"speckle_filter": "median5x5_then_mean3x3",
"dual_pol_ratio_available": dual_pol_ratio is not None,
"dual_pol_ratio": dual_pol_ratio,
})
def croma_compatibility(assets):
sar = next((a for a in assets if a.modality == "sar"), None)
optical = next((a for a in assets if a.modality in {"optical", "multispectral"}), None)
if sar is None or optical is None:
return ["CROMA requires identifiable optical and SAR assets."]
errors = []
if "sentinel1" not in normalize(sar.sensor or "") or "sentinel2" not in normalize(optical.sensor or ""):
errors.append("CROMA is unavailable for these sensors: its released representation requires Sentinel-1 and Sentinel-2.")
if sar.bands != 2 or optical.bands != 12:
errors.append("CROMA is unavailable for this channel representation: confirmed 2-channel SAR and 12-channel optical data are required.")
if not {"vv", "vh"} <= set(sar.band_map.get("indices", {})):
errors.append("CROMA needs confirmed VV and VH channels.")
# Matching channel counts alone cannot verify order/normalization.
expected = {"b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b8a", "b9", "b11", "b12"}
names = {normalize(n).replace("b0", "b") for n in optical.band_names}
if names != expected:
errors.append("CROMA optical band order cannot be verified against its Sentinel-2 contract.")
return errors