"""Read-only Delhi evaluation loader — Priyanka's manifest schema. See ``docs/delhi_eval/README.md``. Paths in the manifest are relative to the repo root (not the eval folder). Uday scripts only read; never write eval data. """ from __future__ import annotations import json from pathlib import Path from typing import Iterator import numpy as np from PIL import Image ROOT = Path(__file__).resolve().parent.parent.parent DEFAULT_MANIFEST = ROOT / "docs" / "delhi_eval" / "manifest.json" LABELS_DIR = ROOT / "docs" / "delhi_eval" / "labels" class DelhiEvalNotReady(Exception): """Manifest missing or unreadable.""" def manifest_path(path: str | Path | None = None) -> Path: return Path(path).resolve() if path else DEFAULT_MANIFEST def load_manifest(path: str | Path | None = None, *, required: bool = True) -> dict: mpath = manifest_path(path) if not mpath.is_file(): if required: raise DelhiEvalNotReady( f"Delhi manifest not found at {mpath}. " "Merge New/Priyanka or run scripts/build_delhi_manifest.py --init." ) return {"pairs": []} return json.loads(mpath.read_text(encoding="utf-8")) def _resolve_gt_path(pair: dict) -> Path | None: gt_rel = pair.get("gt_mask") if gt_rel: p = ROOT / gt_rel return p if p.is_file() else None pair_id = pair.get("pair_id") or pair.get("id") if pair_id: auto = LABELS_DIR / f"{pair_id}.png" if auto.is_file(): return auto return None def _load_rgb(path: Path) -> np.ndarray: if path.suffix.lower() in (".tif", ".tiff"): from app.dda.geotiff_io import load_rgb_pil return np.array(load_rgb_pil(path)) return np.array(Image.open(path).convert("RGB")) def _load_label(path: Path) -> np.ndarray: return np.array(Image.open(path).convert("L")) def iter_delhi_pairs( manifest: str | Path | None = None, *, require_gt: bool = False, ) -> Iterator[tuple[np.ndarray, np.ndarray, np.ndarray | None, str, str | None, str | None]]: """Yield (before, after, gt_or_none, pair_id, before_path, after_path).""" data = load_manifest(manifest, required=True) missing: list[str] = [] for pair in data.get("pairs", []): pair_id = pair.get("pair_id") or pair.get("id") or "unknown" before_rel = pair.get("before_path") or pair.get("before") after_rel = pair.get("after_path") or pair.get("after") if not (before_rel and after_rel): missing.append(f"{pair_id}: missing before/after paths") continue before_p = ROOT / before_rel after_p = ROOT / after_rel if not before_p.is_file() or not after_p.is_file(): missing.append(f"{pair_id}: image missing on disk") continue try: before = _load_rgb(before_p) after = _load_rgb(after_p) except Exception as exc: missing.append(f"{pair_id}: load failed ({exc})") continue gt = None gt_path = _resolve_gt_path(pair) if gt_path is not None: gt = _load_label(gt_path) elif require_gt: continue is_tif = before_p.suffix.lower() in (".tif", ".tiff") bp = str(before_p) if is_tif else None ap = str(after_p) if is_tif else None yield before, after, gt, pair_id, bp, ap if missing: print(f" WARNING: skipped {len(missing)} manifest entries:") for msg in missing[:8]: print(f" - {msg}") if len(missing) > 8: print(f" ... and {len(missing) - 8} more") def count_manifest_pairs(manifest: str | Path | None = None) -> dict: data = load_manifest(manifest, required=True) pairs = data.get("pairs", []) on_disk = sum( 1 for p in pairs if (ROOT / p["before_path"]).is_file() and (ROOT / p["after_path"]).is_file() ) labeled = sum(1 for p in pairs if _resolve_gt_path(p) is not None) return {"total": len(pairs), "on_disk": on_disk, "labeled": labeled} def dummy_delhi_pairs(n: int = 2, size: int = 384) -> list[tuple]: """In-memory synthetic pairs for Uday scaffold runs.""" rng = np.random.default_rng(42) out = [] for i in range(n): before = rng.integers(40, 200, (size, size, 3), dtype=np.uint8) before[:, size // 3: size // 3 + 6] = [90, 90, 90] after = before.copy() gt = np.zeros((size, size), dtype=np.uint8) x, y, w, h = 60 + i * 40, 70 + i * 20, 50, 40 after[y:y + h, x:x + w] = [205, 200, 190] gt[y:y + h, x:x + w] = 255 out.append((before, after, gt, f"dummy_{i:02d}", None, None)) return out