fix(isce2): restore strict managed export workflow
- keep managed ISCE2 displacement export on the strict default pipeline path\n- retain reference normalization only as an optional debug/export capability\n- document the workflow-boundary change in the ISCE2 update log\n- keep WSL runner and engine metadata aligned with the export options
This commit is contained in:
@@ -11,6 +11,9 @@ gdal.UseExceptions()
|
||||
|
||||
DEFAULT_WAVELENGTH = 0.23793052222222222
|
||||
DEFAULT_NODATA = -9999.0
|
||||
DEFAULT_REFERENCE_MODE = "none"
|
||||
DEFAULT_REFERENCE_COH_THRESHOLD = 0.30
|
||||
REFERENCE_MODE_CHOICES = ("none", "coh_median")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -46,10 +49,23 @@ def parse_args() -> argparse.Namespace:
|
||||
default=0.05,
|
||||
help="Mask pixels with coherence below this threshold in *_disp.tif",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference-mode",
|
||||
type=str,
|
||||
choices=REFERENCE_MODE_CHOICES,
|
||||
default=DEFAULT_REFERENCE_MODE,
|
||||
help="Optional reference normalization mode for debug exports",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference-coh-threshold",
|
||||
type=float,
|
||||
default=DEFAULT_REFERENCE_COH_THRESHOLD,
|
||||
help="Minimum coherence used to select reference pixels for normalization",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-disp-full",
|
||||
action="store_true",
|
||||
help="Also export the unmasked displacement GeoTIFF for debugging",
|
||||
help="Also export the coherence-unmasked displacement GeoTIFF for debugging",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -77,12 +93,69 @@ def write_geotiff(array: np.ndarray, ref_ds: gdal.Dataset, out_path: Path, nodat
|
||||
ds = None
|
||||
|
||||
|
||||
def compute_reference_offset(
|
||||
disp_m_raw: np.ndarray,
|
||||
amp: np.ndarray,
|
||||
coh: np.ndarray,
|
||||
coh_threshold: float,
|
||||
reference_mode: str,
|
||||
reference_coh_threshold: float,
|
||||
) -> tuple[float, dict[str, float | int | str]]:
|
||||
amp_valid = np.isfinite(amp) & (amp != 0)
|
||||
coh_finite = np.isfinite(coh)
|
||||
disp_valid = np.isfinite(disp_m_raw)
|
||||
base_mask = amp_valid & coh_finite & disp_valid
|
||||
if not base_mask.any():
|
||||
raise RuntimeError("No valid displacement pixels available for ISCE2 export.")
|
||||
|
||||
normalized_mode = str(reference_mode or DEFAULT_REFERENCE_MODE).strip().lower()
|
||||
if normalized_mode not in REFERENCE_MODE_CHOICES:
|
||||
raise ValueError(f"Unsupported reference mode: {reference_mode}")
|
||||
|
||||
stats: dict[str, float | int | str] = {
|
||||
"mode": normalized_mode,
|
||||
"reference_count": 0,
|
||||
"reference_ratio": 0.0,
|
||||
"support_ratio": float(base_mask.mean()),
|
||||
"selection_threshold": 0.0,
|
||||
"fallback": "",
|
||||
}
|
||||
if normalized_mode == "none":
|
||||
return 0.0, stats
|
||||
|
||||
selection_threshold = min(1.0, max(0.0, max(float(coh_threshold), float(reference_coh_threshold))))
|
||||
reference_mask = base_mask & (coh >= selection_threshold)
|
||||
fallback = ""
|
||||
if not reference_mask.any():
|
||||
reference_mask = base_mask & (coh > 0)
|
||||
fallback = "coh>0"
|
||||
if not reference_mask.any():
|
||||
reference_mask = amp_valid & disp_valid
|
||||
fallback = "amp_only"
|
||||
|
||||
reference_count = int(reference_mask.sum())
|
||||
if reference_count <= 0:
|
||||
raise RuntimeError("Failed to select any reference pixels for displacement normalization.")
|
||||
|
||||
stats.update(
|
||||
{
|
||||
"reference_count": reference_count,
|
||||
"reference_ratio": float(reference_mask.mean()),
|
||||
"selection_threshold": float(selection_threshold),
|
||||
"fallback": fallback,
|
||||
}
|
||||
)
|
||||
return float(np.median(disp_m_raw[reference_mask])), stats
|
||||
|
||||
|
||||
def export_products(
|
||||
work_dir: Path,
|
||||
output_dir: Path,
|
||||
prefix: str,
|
||||
wavelength: float,
|
||||
coh_threshold: float,
|
||||
reference_mode: str = DEFAULT_REFERENCE_MODE,
|
||||
reference_coh_threshold: float = DEFAULT_REFERENCE_COH_THRESHOLD,
|
||||
include_disp_full: bool = False,
|
||||
nodata: float = DEFAULT_NODATA,
|
||||
) -> dict[str, Path]:
|
||||
@@ -104,14 +177,25 @@ def export_products(
|
||||
|
||||
coh_band = 2 if cor_ds.RasterCount >= 2 else 1
|
||||
coh = cor_ds.GetRasterBand(coh_band).ReadAsArray().astype(np.float32)
|
||||
coh_valid = coh > 0
|
||||
coh_valid = np.isfinite(coh) & (coh > 0)
|
||||
amp_valid = np.isfinite(amp) & (amp != 0)
|
||||
|
||||
disp_m = phase * wavelength / (4.0 * np.pi)
|
||||
disp_m_raw = phase * wavelength / (4.0 * np.pi)
|
||||
reference_offset_m, reference_stats = compute_reference_offset(
|
||||
disp_m_raw=disp_m_raw,
|
||||
amp=amp,
|
||||
coh=coh,
|
||||
coh_threshold=coh_threshold,
|
||||
reference_mode=reference_mode,
|
||||
reference_coh_threshold=reference_coh_threshold,
|
||||
)
|
||||
disp_m = disp_m_raw - reference_offset_m
|
||||
disp_m_full = disp_m.copy()
|
||||
|
||||
mask = (coh < coh_threshold) | (amp == 0)
|
||||
disp_m[mask] = nodata
|
||||
disp_m_full[amp == 0] = nodata
|
||||
mask = (~amp_valid) | (~np.isfinite(disp_m)) | (~np.isfinite(coh)) | (coh < coh_threshold)
|
||||
disp_m_masked = disp_m.copy()
|
||||
disp_m_masked[mask] = nodata
|
||||
disp_m_full[(~amp_valid) | (~np.isfinite(disp_m_full))] = nodata
|
||||
|
||||
coh_out = coh.copy()
|
||||
coh_out[~coh_valid] = nodata
|
||||
@@ -120,27 +204,46 @@ def export_products(
|
||||
out_disp = output_dir / f"{prefix}_disp.tif"
|
||||
out_coh = output_dir / f"{prefix}_coh.tif"
|
||||
|
||||
write_geotiff(disp_m, unw_ds, out_disp, nodata)
|
||||
write_geotiff(disp_m_masked, unw_ds, out_disp, nodata)
|
||||
write_geotiff(coh_out, cor_ds, out_coh, nodata)
|
||||
out_disp_full = None
|
||||
if include_disp_full:
|
||||
out_disp_full = output_dir / f"{prefix}_disp_full.tif"
|
||||
write_geotiff(disp_m_full, unw_ds, out_disp_full, nodata)
|
||||
|
||||
valid_disp = disp_m[disp_m != nodata]
|
||||
valid_disp = disp_m_masked[disp_m_masked != nodata]
|
||||
valid_coh = coh_out[coh_out != nodata]
|
||||
valid_full = disp_m_full[disp_m_full != nodata] if include_disp_full else np.array([], dtype=np.float32)
|
||||
valid_raw = disp_m_raw[amp_valid & np.isfinite(disp_m_raw)]
|
||||
using_reference = str(reference_stats["mode"]) != "none"
|
||||
|
||||
print(f"Work dir: {work_dir}")
|
||||
print(f"Output prefix: {prefix}")
|
||||
print(f"Coherence threshold: {coh_threshold}")
|
||||
print(f"Unwrap support ratio: {(amp != 0).mean()*100:.2f}%")
|
||||
print(f"Reference mode: {reference_stats['mode']}")
|
||||
if using_reference:
|
||||
print(
|
||||
"Reference coh floor: "
|
||||
f"{float(reference_stats['selection_threshold']):.2f}"
|
||||
)
|
||||
print(
|
||||
"Reference pixel ratio: "
|
||||
f"{float(reference_stats['reference_ratio'])*100:.2f}%"
|
||||
)
|
||||
print(f"Reference offset: {reference_offset_m:.4f} m")
|
||||
if reference_stats["fallback"]:
|
||||
print(f"Reference fallback: {reference_stats['fallback']}")
|
||||
print(f"Unwrap support ratio: {amp_valid.mean()*100:.2f}%")
|
||||
print(f"Coherence support ratio: {coh_valid.mean()*100:.2f}%")
|
||||
print(f"Masked disp ratio: {(disp_m != nodata).mean()*100:.2f}%")
|
||||
print(f"Masked disp ratio: {(disp_m_masked != nodata).mean()*100:.2f}%")
|
||||
if valid_raw.size:
|
||||
print(f"Raw disp range: [{valid_raw.min():.4f}, {valid_raw.max():.4f}] m")
|
||||
if valid_disp.size:
|
||||
print(f"Masked disp range: [{valid_disp.min():.4f}, {valid_disp.max():.4f}] m")
|
||||
label = "Norm disp range" if using_reference else "Disp range"
|
||||
print(f"{label + ':':24}[{valid_disp.min():.4f}, {valid_disp.max():.4f}] m")
|
||||
if include_disp_full and valid_full.size:
|
||||
print(f"Full disp range: [{valid_full.min():.4f}, {valid_full.max():.4f}] m")
|
||||
label = "Norm full disp range" if using_reference else "Full disp range"
|
||||
print(f"{label + ':':24}[{valid_full.min():.4f}, {valid_full.max():.4f}] m")
|
||||
if valid_coh.size:
|
||||
print(f"Coherence range: [{valid_coh.min():.4f}, {valid_coh.max():.4f}]")
|
||||
print(f"Wrote: {out_disp}")
|
||||
@@ -171,6 +274,8 @@ def main() -> int:
|
||||
prefix=prefix,
|
||||
wavelength=args.wavelength,
|
||||
coh_threshold=args.coh_threshold,
|
||||
reference_mode=args.reference_mode,
|
||||
reference_coh_threshold=args.reference_coh_threshold,
|
||||
include_disp_full=args.include_disp_full,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -13,7 +13,13 @@ import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from export_isce_geotiff import DEFAULT_WAVELENGTH, export_products
|
||||
from export_isce_geotiff import (
|
||||
DEFAULT_REFERENCE_COH_THRESHOLD,
|
||||
DEFAULT_REFERENCE_MODE,
|
||||
DEFAULT_WAVELENGTH,
|
||||
REFERENCE_MODE_CHOICES,
|
||||
export_products,
|
||||
)
|
||||
from lt1_input_resolver import (
|
||||
DEFAULT_WSL_DEM_CANDIDATES,
|
||||
ensure_lt1_orbit_xml,
|
||||
@@ -146,6 +152,18 @@ def parse_args() -> argparse.Namespace:
|
||||
default=0.05,
|
||||
help="Coherence threshold for *_disp.tif export",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference-mode",
|
||||
choices=REFERENCE_MODE_CHOICES,
|
||||
default=DEFAULT_REFERENCE_MODE,
|
||||
help="Optional reference normalization mode used only for debug exports",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference-coh-threshold",
|
||||
type=float,
|
||||
default=DEFAULT_REFERENCE_COH_THRESHOLD,
|
||||
help="Minimum coherence used when selecting reference pixels for export normalization",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-grid-size-m",
|
||||
type=int,
|
||||
@@ -199,6 +217,8 @@ def parse_args() -> argparse.Namespace:
|
||||
raise ValueError("--orbit-margin-sec must be between 60 and 120 seconds")
|
||||
if args.target_grid_size_m <= 0:
|
||||
raise ValueError("--target-grid-size-m must be greater than 0")
|
||||
if args.reference_coh_threshold < 0 or args.reference_coh_threshold > 1:
|
||||
raise ValueError("--reference-coh-threshold must be between 0 and 1")
|
||||
if args.force and args.resume_from:
|
||||
raise ValueError("--force cannot be used together with --resume-from")
|
||||
return args
|
||||
@@ -914,6 +934,8 @@ def main() -> int:
|
||||
prefix=output_prefix,
|
||||
wavelength=args.wavelength,
|
||||
coh_threshold=args.coh_threshold,
|
||||
reference_mode=args.reference_mode,
|
||||
reference_coh_threshold=args.reference_coh_threshold,
|
||||
include_disp_full=args.include_disp_full,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user