Harden SAM roof refinement geometry gates
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-30 04:00:24 +02:00
parent 5ac3238c27
commit 29378bba42
4 changed files with 74 additions and 6 deletions
@@ -13,9 +13,19 @@ SPEC.loader.exec_module(MODULE)
def test_plausible_refinement_is_fail_closed() -> None:
source = (10.0, 10.0, 30.0, 30.0)
assert MODULE.plausible_refinement(source, (8.0, 9.0, 31.0, 32.0), min_iou=0.15, min_area_ratio=0.25, max_area_ratio=4.0)
assert not MODULE.plausible_refinement(source, (100.0, 100.0, 120.0, 120.0), min_iou=0.15, min_area_ratio=0.25, max_area_ratio=4.0)
assert not MODULE.plausible_refinement(source, (0.0, 0.0, 100.0, 100.0), min_iou=0.15, min_area_ratio=0.25, max_area_ratio=4.0)
limits = dict(
min_iou=0.15,
min_area_ratio=0.25,
max_area_ratio=4.0,
max_center_shift_ratio=0.75,
min_dimension_ratio=0.5,
max_dimension_ratio=2.0,
)
assert MODULE.plausible_refinement(source, (8.0, 9.0, 31.0, 32.0), **limits)
assert not MODULE.plausible_refinement(source, (100.0, 100.0, 120.0, 120.0), **limits)
assert not MODULE.plausible_refinement(source, (0.0, 0.0, 100.0, 100.0), **limits)
assert not MODULE.plausible_refinement(source, (10.0, 10.0, 51.0, 20.0), **limits)
assert not MODULE.plausible_refinement(source, (24.0, 24.0, 44.0, 44.0), **limits)
def test_yolo_round_trip_shape() -> None:
+28
View File
@@ -3746,6 +3746,34 @@ Validation:
- `cd backend && python -m alembic heads && python -m alembic upgrade head --sql`
# Codex Execution Log
## 2026-07-30 - Fail-closed SAM roof-refinement gate hardening
### What changed
- Reproduced the V64 Flemish calibration-candidate export for Aarschot, Beveren and Oostkamp from the immutable temporal-cutoff manifest.
- Ran the checksummed `/app/sam2_t.pt` model on the server RTX 4080 SUPER and rendered deterministic before/after label contact sheets.
- Rejected both the legacy refinement settings and a stricter trial because dense or temporally absent footprint prompts still produced visually invalid roof boxes; neither result was promoted into a training corpus.
- Hardened `scripts/refine_yolo_labels_with_sam.py` with configurable centre-shift and per-dimension ratio gates. The selected values and model checksum are persisted in `sam-refinement.json`.
### What was tested
- `python -m pytest backend/tests/test_sam_roof_label_refinement.py -q` passed (`2 passed`).
- `python -m py_compile scripts/refine_yolo_labels_with_sam.py` passed.
- CUDA refinement completed on 706 candidate labels; the strictest trial refined 659 and retained 47 explicit fallbacks. Visual QA still failed, so the run remains evidence only.
### What remains open
- Replace or align the rejected Flemish calibration labels with image-visible, independently validated roof boxes.
- Rebuild and retrain only after that label gate passes; protected test and pure-background evaluation remain closed.
### Known limitations
- Geometry plausibility alone cannot prove that a SAM mask represents the intended roof in dense scenes.
### Next recommended pass
- Add an auditable image/footprint alignment stage or select lower-parallax independent Flemish AOIs, then repeat visual QA before corpus composition.
## Post-V1 national coverage completion: Wallonia (2026-07-22)
- Located and live-validated the stable official WALOUS 2018 GeoTIFF
+2
View File
@@ -8,6 +8,8 @@ Uitvoeringsbord: `docs/PYTORCH_TRAINING_ROADMAP_BELGIUM.md`.
- [x] Generaliseer de single-class tile-export op klasse, referentiebron en referentielaag.
- [x] Laat productietraining fail-closed stoppen wanneer vereiste CUDA ontbreekt.
- [ ] Materialiseer geografisch gescheiden GRB/PICC/URBIS-gebouwcorpora met temporeel passende orthofoto's.
- [x] Verwerp de drie V64 Vlaamse kalibratiekandidaten fail-closed na gereproduceerde ruwe en SAM2-contact-sheets; verscherp de geometrische SAM2-gates.
- [ ] Lever onafhankelijke Vlaamse kalibratie-AOI's met aantoonbaar beeldzichtbare daklabels vóór een volgende training.
- [ ] Train en evalueer een Belgische gebouwchallenger onafhankelijk; promoveer alleen zonder achtergrondregressie.
- [ ] Houd zonnepanelen en segmentatie `not_configured` tot gereviewde taaklabels en hold-outs bestaan.
+31 -3
View File
@@ -7,6 +7,7 @@ import argparse
import gc
import hashlib
import json
import math
import os
import shutil
from pathlib import Path
@@ -38,11 +39,29 @@ def plausible_refinement(
min_iou: float,
min_area_ratio: float,
max_area_ratio: float,
max_center_shift_ratio: float,
min_dimension_ratio: float,
max_dimension_ratio: float,
) -> bool:
source_area = (source[2] - source[0]) * (source[3] - source[1])
refined_area = (refined[2] - refined[0]) * (refined[3] - refined[1])
source_width, source_height = source[2] - source[0], source[3] - source[1]
refined_width, refined_height = refined[2] - refined[0], refined[3] - refined[1]
source_area = source_width * source_height
refined_area = refined_width * refined_height
ratio = refined_area / source_area if source_area > 0 else 0.0
return min_area_ratio <= ratio <= max_area_ratio and iou(source, refined) >= min_iou
if source_width <= 0 or source_height <= 0:
return False
width_ratio = refined_width / source_width
height_ratio = refined_height / source_height
source_centre = ((source[0] + source[2]) / 2, (source[1] + source[3]) / 2)
refined_centre = ((refined[0] + refined[2]) / 2, (refined[1] + refined[3]) / 2)
centre_shift_ratio = math.dist(source_centre, refined_centre) / math.hypot(source_width, source_height)
return (
min_area_ratio <= ratio <= max_area_ratio
and min_dimension_ratio <= width_ratio <= max_dimension_ratio
and min_dimension_ratio <= height_ratio <= max_dimension_ratio
and centre_shift_ratio <= max_center_shift_ratio
and iou(source, refined) >= min_iou
)
def read_boxes(path: Path, width: int, height: int) -> list[tuple[float, float, float, float]]:
@@ -85,6 +104,9 @@ def main() -> int:
parser.add_argument("--min-source-iou", type=float, default=0.15)
parser.add_argument("--min-area-ratio", type=float, default=0.25)
parser.add_argument("--max-area-ratio", type=float, default=4.0)
parser.add_argument("--max-center-shift-ratio", type=float, default=0.75)
parser.add_argument("--min-dimension-ratio", type=float, default=0.5)
parser.add_argument("--max-dimension-ratio", type=float, default=2.0)
parser.add_argument("--max-prompts-per-pass", type=int, default=96)
parser.add_argument("--force", action="store_true")
args = parser.parse_args()
@@ -139,6 +161,9 @@ def main() -> int:
min_iou=args.min_source_iou,
min_area_ratio=args.min_area_ratio,
max_area_ratio=args.max_area_ratio,
max_center_shift_ratio=args.max_center_shift_ratio,
min_dimension_ratio=args.min_dimension_ratio,
max_dimension_ratio=args.max_dimension_ratio,
):
output_boxes[start + local_index] = candidate
unmatched.remove(candidate_index)
@@ -184,6 +209,9 @@ def main() -> int:
"min_source_iou": args.min_source_iou,
"min_area_ratio": args.min_area_ratio,
"max_area_ratio": args.max_area_ratio,
"max_center_shift_ratio": args.max_center_shift_ratio,
"min_dimension_ratio": args.min_dimension_ratio,
"max_dimension_ratio": args.max_dimension_ratio,
"max_prompts_per_pass": args.max_prompts_per_pass,
"refined_label_count": refined_count,
"fallback_label_count": fallback_count,