Harden SAM roof refinement geometry gates
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user