Spaces:
Running
Running
File size: 7,522 Bytes
d70361b | 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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | """Warp two neighboring ECW orthos onto the same overlap grid for DDA testing.
The app cannot read .ecw (rasterio has no ECW driver). Neighboring tiles
(e.g. 0-23 vs 0-24) also fail naive NCC because they cover different ground.
This script uses QGIS GDAL (ECW plugin) to:
1. read bounds
2. crop both to the geographic intersection (optional center window)
3. write matching GeoTIFFs (same CRS, pixel size, size)
Usage:
python scripts/prep_ecw_overlap_pair.py
python scripts/prep_ecw_overlap_pair.py --before PATH --after PATH --out DIR
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_BEFORE = Path(r"c:\Users\udayb\Downloads\0-24_ori _26-02-25.ecw")
DEFAULT_AFTER = Path(r"c:\Users\udayb\Downloads\0-23_ori_01-03-25.ecw")
DEFAULT_OUT = ROOT / "data" / "library_sources" / "central_delhi" / "Images"
QGIS_ROOT = Path(r"C:\Program Files\QGIS 4.0.2")
def _qgis_env() -> dict:
env = os.environ.copy()
bin_dir = str(QGIS_ROOT / "bin")
env["PATH"] = bin_dir + os.pathsep + env.get("PATH", "")
env["GDAL_DRIVER_PATH"] = str(QGIS_ROOT / "apps" / "gdal" / "lib" / "gdalplugins")
env["PROJ_LIB"] = str(QGIS_ROOT / "share" / "proj")
gdal_data = QGIS_ROOT / "apps" / "gdal" / "share" / "gdal"
if not gdal_data.is_dir():
gdal_data = QGIS_ROOT / "share" / "gdal"
if gdal_data.is_dir():
env["GDAL_DATA"] = str(gdal_data)
return env
def _gdalinfo(path: Path) -> dict:
exe = QGIS_ROOT / "bin" / "gdalinfo.exe"
raw = subprocess.check_output(
[str(exe), "-json", str(path)], env=_qgis_env(), text=True
)
return json.loads(raw)
def _extent(info: dict) -> tuple[float, float, float, float]:
c = info["cornerCoordinates"]
xs = [c["upperLeft"][0], c["lowerLeft"][0], c["upperRight"][0], c["lowerRight"][0]]
ys = [c["upperLeft"][1], c["lowerLeft"][1], c["upperRight"][1], c["lowerRight"][1]]
return min(xs), min(ys), max(xs), max(ys)
def _intersect(a, b):
xmin = max(a[0], b[0])
ymin = max(a[1], b[1])
xmax = min(a[2], b[2])
ymax = min(a[3], b[3])
if xmax <= xmin or ymax <= ymin:
raise SystemExit("No geographic overlap — these are not the same scene.")
return xmin, ymin, xmax, ymax
def _center_window(ext, height_m: float | None):
xmin, ymin, xmax, ymax = ext
if not height_m or height_m <= 0:
return ext
cy = 0.5 * (ymin + ymax)
half = height_m / 2.0
ymin2 = max(ymin, cy - half)
ymax2 = min(ymax, cy + half)
return xmin, ymin2, xmax, ymax2
def _warp(src: Path, dst: Path, te, tr: float) -> None:
exe = QGIS_ROOT / "bin" / "gdalwarp.exe"
xmin, ymin, xmax, ymax = te
cmd = [
str(exe), "-overwrite",
"-t_srs", "EPSG:32643",
"-te", str(xmin), str(ymin), str(xmax), str(ymax),
"-tr", str(tr), str(tr),
"-r", "bilinear",
"-of", "GTiff",
"-co", "TILED=YES",
"-co", "COMPRESS=LZW",
"-dstalpha",
str(src), str(dst),
]
subprocess.check_call(cmd, env=_qgis_env())
def crop_valid_overlap(before_path: Path, after_path: Path) -> None:
"""Drop nodata/alpha so the app's global NCC is measured on real overlap."""
import numpy as np
import rasterio
from rasterio.windows import Window
with rasterio.open(before_path) as db, rasterio.open(after_path) as da:
b = db.read()
a = da.read()
if b.shape[0] >= 4:
valid_b = b[3] > 0
rgb_b = b[:3]
else:
valid_b = np.any(b[:3] > 5, axis=0)
rgb_b = b[:3]
if a.shape[0] >= 4:
valid_a = a[3] > 0
rgb_a = a[:3]
else:
valid_a = np.any(a[:3] > 5, axis=0)
rgb_a = a[:3]
both = valid_b & valid_a & np.any(rgb_b > 5, axis=0) & np.any(rgb_a > 5, axis=0)
rows = np.any(both, axis=1)
cols = np.any(both, axis=0)
if not rows.any() or not cols.any():
raise SystemExit("No jointly valid pixels after warp")
r0, r1 = int(np.argmax(rows)), int(len(rows) - np.argmax(rows[::-1]))
c0, c1 = int(np.argmax(cols)), int(len(cols) - np.argmax(cols[::-1]))
window = Window(c0, r0, c1 - c0, r1 - r0)
transform = db.window_transform(window)
profile = db.profile.copy()
profile.update(
count=3,
height=r1 - r0,
width=c1 - c0,
transform=transform,
photometric="RGB",
)
profile.pop("nbits", None)
out_b = rgb_b[:, r0:r1, c0:c1]
out_a = rgb_a[:, r0:r1, c0:c1]
with rasterio.open(before_path, "w", **profile) as dst:
dst.write(out_b)
with rasterio.open(after_path, "w", **profile) as dst:
dst.write(out_a)
print(f"cropped valid overlap to {c1 - c0} x {r1 - r0} px")
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--before", type=Path, default=DEFAULT_BEFORE)
p.add_argument("--after", type=Path, default=DEFAULT_AFTER)
p.add_argument("--out", type=Path, default=DEFAULT_OUT)
p.add_argument("--gsd", type=float, default=0.03, help="Output metres/pixel")
p.add_argument(
"--center-height-m",
type=float,
default=120.0,
help="Keep this many metres of N-S overlap (0 = full strip)",
)
args = p.parse_args()
if not args.before.is_file() or not args.after.is_file():
raise SystemExit("ECW files not found")
if not (QGIS_ROOT / "bin" / "gdalwarp.exe").is_file():
raise SystemExit(f"QGIS GDAL not found at {QGIS_ROOT}")
info_b = _gdalinfo(args.before)
info_a = _gdalinfo(args.after)
ext = _center_window(_intersect(_extent(info_b), _extent(info_a)), args.center_height_m)
w = ext[2] - ext[0]
h = ext[3] - ext[1]
args.out.mkdir(parents=True, exist_ok=True)
before_out = args.out / "ecw_overlap_before_2025-02-26.tif"
after_out = args.out / "ecw_overlap_after_2025-03-01.tif"
print(f"overlap window {w:.2f} x {h:.2f} m @ {args.gsd} m/px -> {before_out.name}")
_warp(args.before, before_out, ext, args.gsd)
_warp(args.after, after_out, ext, args.gsd)
crop_valid_overlap(before_out, after_out)
sys.path.insert(0, str(ROOT))
import cv2
from app.detection_engine import _alignment_ncc
def load(path: Path):
im = cv2.imread(str(path), cv2.IMREAD_COLOR)
return cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
rb, ra = load(before_out), load(after_out)
ncc = float(_alignment_ncc(rb, ra))
meta = {
"before_src": str(args.before),
"after_src": str(args.after),
"extent_utm43n": list(ext),
"gsd_m": args.gsd,
"size_px": [int(rb.shape[1]), int(rb.shape[0])],
"ncc_full": round(ncc, 4),
"ncc_valid_pixels": round(ncc, 4),
"registration_gate": 0.55,
"fit_for_detection": bool(ncc >= 0.55),
"note": (
"Neighboring tiles cropped to shared ground. Dates are 3 days apart "
"so expect little real construction change; pair is for alignment/pipeline tests."
),
}
(args.out / "ecw_overlap_pair.json").write_text(
json.dumps(meta, indent=2), encoding="utf-8"
)
print(json.dumps(meta, indent=2))
if not meta["fit_for_detection"]:
raise SystemExit("NCC still below 0.55 — pair not fit")
if __name__ == "__main__":
main()
|