318 lines
10 KiB
Python
318 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
|
|
|
|
GAMMA_FLOAT32 = np.dtype(">f4")
|
|
ZERO_ERROR_RE = re.compile(r"number of zero values\s+(\d+)\s+in MLI1 image patch exceeds threshold:\s+(\d+)", re.IGNORECASE)
|
|
|
|
|
|
@dataclass
|
|
class GammaShape:
|
|
width: int
|
|
lines: int
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Scan init_offsetm patch positions for LT-1 DEM-assisted coreg.")
|
|
parser.add_argument("run_root", help="Experiment run root, e.g. /mnt/d/.../run_20260420T093322Z")
|
|
parser.add_argument("--case", dest="cases", action="append", default=[], help="Case name to scan. Can be repeated.")
|
|
parser.add_argument("--date", dest="dates", action="append", default=[], help="Slave date to scan. Can be repeated.")
|
|
parser.add_argument("--project", default="pyint_stage")
|
|
parser.add_argument("--patch-size", type=int, default=512)
|
|
parser.add_argument("--stride", type=int, default=256)
|
|
parser.add_argument("--max-candidates", type=int, default=20)
|
|
return parser.parse_args()
|
|
|
|
|
|
def parse_gamma_par_value(path: Path, key: str) -> str:
|
|
prefix = key.strip() + ":"
|
|
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith(prefix):
|
|
_, _, tail = stripped.partition(":")
|
|
return tail.strip().split()[0]
|
|
raise ValueError(f"Missing key '{key}' in {path}")
|
|
|
|
|
|
def parse_gamma_shape(path: Path) -> GammaShape:
|
|
width = int(float(parse_gamma_par_value(path, "range_samples")))
|
|
lines = int(float(parse_gamma_par_value(path, "azimuth_lines")))
|
|
return GammaShape(width=width, lines=lines)
|
|
|
|
|
|
def build_valid_mask(path: Path, shape: GammaShape) -> np.ndarray:
|
|
arr = np.memmap(path, dtype=GAMMA_FLOAT32, mode="r", shape=(shape.lines, shape.width))
|
|
valid = np.isfinite(arr) & (arr != 0)
|
|
return np.asarray(valid, dtype=np.uint8)
|
|
|
|
|
|
def patch_sum(integral: np.ndarray, top: int, left: int, bottom: int, right: int) -> int:
|
|
br = int(integral[bottom, right])
|
|
tr = int(integral[top, right])
|
|
bl = int(integral[bottom, left])
|
|
tl = int(integral[top, left])
|
|
return br - tr - bl + tl
|
|
|
|
|
|
def candidate_positions(mask: np.ndarray, *, patch_size: int, stride: int, max_candidates: int) -> list[dict[str, Any]]:
|
|
lines, width = mask.shape
|
|
patch_size = max(1, min(patch_size, lines, width))
|
|
half = patch_size // 2
|
|
valid_y = range(half, lines - (patch_size - half) + 1, max(1, stride))
|
|
valid_x = range(half, width - (patch_size - half) + 1, max(1, stride))
|
|
|
|
integral = np.pad(mask.astype(np.int64), ((1, 0), (1, 0)), mode="constant")
|
|
integral = integral.cumsum(axis=0).cumsum(axis=1)
|
|
patch_area = patch_size * patch_size
|
|
|
|
center_y = lines // 2
|
|
center_x = width // 2
|
|
center_key = (center_x, center_y)
|
|
|
|
candidates: list[dict[str, Any]] = []
|
|
seen: set[tuple[int, int]] = set()
|
|
|
|
for y in valid_y:
|
|
for x in valid_x:
|
|
top = y - half
|
|
left = x - half
|
|
bottom = top + patch_size
|
|
right = left + patch_size
|
|
nonzero = patch_sum(integral, top, left, bottom, right)
|
|
ratio = float(nonzero / patch_area)
|
|
entry = {
|
|
"rpos": int(x),
|
|
"azpos": int(y),
|
|
"patch_nonzero_count": int(nonzero),
|
|
"patch_nonzero_ratio": ratio,
|
|
"is_center": bool((x, y) == center_key),
|
|
}
|
|
candidates.append(entry)
|
|
|
|
candidates.sort(key=lambda item: item["patch_nonzero_ratio"], reverse=True)
|
|
|
|
selected: list[dict[str, Any]] = []
|
|
for entry in candidates:
|
|
key = (entry["rpos"], entry["azpos"])
|
|
if key in seen:
|
|
continue
|
|
selected.append(entry)
|
|
seen.add(key)
|
|
if len(selected) >= max_candidates:
|
|
break
|
|
|
|
if center_key not in seen:
|
|
top = center_y - half
|
|
left = center_x - half
|
|
top = max(0, min(lines - patch_size, top))
|
|
left = max(0, min(width - patch_size, left))
|
|
bottom = top + patch_size
|
|
right = left + patch_size
|
|
nonzero = patch_sum(integral, top, left, bottom, right)
|
|
selected.append(
|
|
{
|
|
"rpos": int(left + half),
|
|
"azpos": int(top + half),
|
|
"patch_nonzero_count": int(nonzero),
|
|
"patch_nonzero_ratio": float(nonzero / patch_area),
|
|
"is_center": True,
|
|
}
|
|
)
|
|
|
|
for index, entry in enumerate(selected, start=1):
|
|
entry["rank"] = index
|
|
return selected
|
|
|
|
|
|
def parse_zero_error(text: str) -> dict[str, Any]:
|
|
match = ZERO_ERROR_RE.search(text or "")
|
|
if not match:
|
|
return {}
|
|
return {
|
|
"zero_count": int(match.group(1)),
|
|
"zero_threshold": int(match.group(2)),
|
|
}
|
|
|
|
|
|
def run_init_offsetm(
|
|
*,
|
|
mli0: Path,
|
|
samp: Path,
|
|
diff0: Path,
|
|
output_dir: Path,
|
|
patch_size: int,
|
|
rpos: int,
|
|
azpos: int,
|
|
) -> dict[str, Any]:
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
diff_copy = output_dir / f"r{rpos}_a{azpos}.diff_par"
|
|
shutil.copy2(diff0, diff_copy)
|
|
|
|
cmd = [
|
|
"init_offsetm",
|
|
str(mli0),
|
|
str(samp),
|
|
str(diff_copy),
|
|
"1",
|
|
"1",
|
|
str(int(rpos)),
|
|
str(int(azpos)),
|
|
"-",
|
|
"-",
|
|
"-",
|
|
str(int(patch_size)),
|
|
"0",
|
|
]
|
|
result = subprocess.run(cmd, text=True, capture_output=True, check=False)
|
|
combined = ((result.stdout or "") + "\n" + (result.stderr or "")).strip()
|
|
payload = {
|
|
"command": " ".join(cmd),
|
|
"returncode": int(result.returncode),
|
|
"ok": result.returncode == 0,
|
|
"stdout_tail": (result.stdout or "")[-4000:],
|
|
"stderr_tail": (result.stderr or "")[-4000:],
|
|
"diff_par_copy": str(diff_copy),
|
|
}
|
|
payload.update(parse_zero_error(combined))
|
|
return payload
|
|
|
|
|
|
def scan_case_date(
|
|
*,
|
|
run_root: Path,
|
|
project: str,
|
|
case_name: str,
|
|
slave_date: str,
|
|
patch_size: int,
|
|
stride: int,
|
|
max_candidates: int,
|
|
output_root: Path,
|
|
) -> dict[str, Any]:
|
|
case_root = run_root / case_name / project
|
|
slc_dir = case_root / "SLC" / slave_date
|
|
rslc_dir = case_root / "RSLC" / slave_date
|
|
amp_par = slc_dir / f"{slave_date}_2rlks.amp.par"
|
|
shape = parse_gamma_shape(amp_par)
|
|
mask = build_valid_mask(rslc_dir / "mli0", shape)
|
|
|
|
candidates = candidate_positions(mask, patch_size=patch_size, stride=stride, max_candidates=max_candidates)
|
|
output_dir = output_root / case_name / slave_date
|
|
results = []
|
|
for candidate in candidates:
|
|
command_result = run_init_offsetm(
|
|
mli0=rslc_dir / "mli0",
|
|
samp=slc_dir / f"{slave_date}_2rlks.amp",
|
|
diff0=rslc_dir / "diff0",
|
|
output_dir=output_dir / "diff_par",
|
|
patch_size=patch_size,
|
|
rpos=int(candidate["rpos"]),
|
|
azpos=int(candidate["azpos"]),
|
|
)
|
|
row = dict(candidate)
|
|
row.update(command_result)
|
|
results.append(row)
|
|
|
|
success_count = sum(1 for item in results if item["ok"])
|
|
best_ratio = max((float(item["patch_nonzero_ratio"]) for item in results), default=0.0)
|
|
best_zero = min((int(item.get("zero_count", 10**18)) for item in results if "zero_count" in item), default=None)
|
|
payload = {
|
|
"case": case_name,
|
|
"slave_date": slave_date,
|
|
"shape": {"width": shape.width, "lines": shape.lines},
|
|
"patch_size": int(patch_size),
|
|
"stride": int(stride),
|
|
"candidate_count": len(results),
|
|
"success_count": int(success_count),
|
|
"best_patch_nonzero_ratio": best_ratio,
|
|
"best_zero_count": best_zero,
|
|
"results": results,
|
|
}
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
(output_dir / "scan_summary.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
return payload
|
|
|
|
|
|
def write_summary_tsv(path: Path, summaries: list[dict[str, Any]]) -> None:
|
|
header = [
|
|
"case",
|
|
"slave_date",
|
|
"rank",
|
|
"is_center",
|
|
"rpos",
|
|
"azpos",
|
|
"patch_nonzero_ratio",
|
|
"returncode",
|
|
"ok",
|
|
"zero_count",
|
|
"zero_threshold",
|
|
]
|
|
lines = ["\t".join(header)]
|
|
for summary in summaries:
|
|
for row in summary["results"]:
|
|
values: list[str] = []
|
|
for key in header:
|
|
if key in {"case", "slave_date"}:
|
|
value = summary[key]
|
|
else:
|
|
value = row.get(key)
|
|
if isinstance(value, float):
|
|
values.append(f"{value:.6f}")
|
|
elif value is None:
|
|
values.append("")
|
|
else:
|
|
values.append(str(value))
|
|
lines.append("\t".join(values))
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
run_root = Path(args.run_root).resolve()
|
|
case_names = tuple(args.cases) if args.cases else ("case_A_baseline", "case_C_precise_orbit_rewrite")
|
|
slave_dates = tuple(args.dates) if args.dates else ("20230624", "20230920")
|
|
output_root = run_root / "scan_init_offsetm_patch"
|
|
output_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
if not shutil.which("init_offsetm"):
|
|
raise RuntimeError("init_offsetm is not available in PATH")
|
|
|
|
summaries = []
|
|
for case_name in case_names:
|
|
for slave_date in slave_dates:
|
|
summaries.append(
|
|
scan_case_date(
|
|
run_root=run_root,
|
|
project=args.project,
|
|
case_name=case_name,
|
|
slave_date=slave_date,
|
|
patch_size=int(args.patch_size),
|
|
stride=int(args.stride),
|
|
max_candidates=int(args.max_candidates),
|
|
output_root=output_root,
|
|
)
|
|
)
|
|
|
|
(output_root / "scan_summary.json").write_text(
|
|
json.dumps({"run_root": str(run_root), "summaries": summaries}, ensure_ascii=False, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
write_summary_tsv(output_root / "scan_summary.tsv", summaries)
|
|
print(json.dumps({"run_root": str(run_root), "output_root": str(output_root), "scan_count": len(summaries)}, ensure_ascii=False))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|