Spaces:
Paused
Paused
File size: 5,267 Bytes
a358495 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | """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
|