Spaces:
Paused
Paused
Download satquery_engine/services/sar.py from SM737/satquery-api: direct link, hf CLI and curl.
- Browser
- Download file 5.27 kB
-
https://huggingface.co/spaces/SM737/satquery-api/resolve/main/satquery_engine/services/sar.py
- Command line
-
hf download hf://spaces/SM737/satquery-api/satquery_engine/services/sar.py
-
curl -L -o sar.py https://huggingface.co/spaces/SM737/satquery-api/resolve/main/satquery_engine/services/sar.py
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 | |
| 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 | |