From 8155acf969e548cb70bc419bd65fdd06d7856c14 Mon Sep 17 00:00:00 2001 From: Jens Date: Mon, 27 Jul 2026 09:13:28 +0200 Subject: [PATCH] Evaluate regional detector ensembles --- .../evaluate_belgium_building_candidate.py | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/scripts/evaluate_belgium_building_candidate.py b/scripts/evaluate_belgium_building_candidate.py index 31a4dc57..d4a91790 100644 --- a/scripts/evaluate_belgium_building_candidate.py +++ b/scripts/evaluate_belgium_building_candidate.py @@ -52,6 +52,31 @@ def metrics(tp: int, fp: int, fn: int) -> dict[str, float | int]: } +def ensemble_predictions( + primary: list[tuple[tuple[float, float, float, float], float]], + secondary: list[tuple[tuple[float, float, float, float], float]], + *, + match_iou: float, + mode: str, +) -> list[tuple[tuple[float, float, float, float], float]]: + unmatched = set(range(len(secondary))) + combined = [] + unmatched_primary = [] + for box, score in primary: + candidates = [(iou(box, secondary[index][0]), index) for index in unmatched] + overlap, index = max(candidates, default=(0.0, -1)) + if overlap < match_iou: + unmatched_primary.append((box, score)) + continue + other_box, other_score = secondary[index] + unmatched.remove(index) + combined.append((tuple((left + right) / 2 for left, right in zip(box, other_box)), (score + other_score) / 2)) + if mode == "union": + combined.extend(unmatched_primary) + combined.extend(secondary[index] for index in unmatched) + return combined + + def scale_box( box: tuple[float, float, float, float], factor: float, offset_x: float = 0.0, offset_y: float = 0.0 ) -> tuple[float, float, float, float]: @@ -106,6 +131,9 @@ def main() -> int: parser.add_argument("--box-scale", type=float, default=1.0) parser.add_argument("--box-offset-x", type=float, default=0.0) parser.add_argument("--box-offset-y", type=float, default=0.0) + parser.add_argument("--additional-model", type=Path) + parser.add_argument("--ensemble-mode", choices=("consensus", "union"), default="consensus") + parser.add_argument("--ensemble-match-iou", type=float, default=0.3) parser.add_argument("--proposal-classifier", type=Path) parser.add_argument("--proposal-classifier-threshold", type=float, default=0.5) parser.add_argument("--proposal-crop-scale", type=float, default=1.4) @@ -147,8 +175,14 @@ def main() -> int: max_det=args.max_det, verbose=False, ) + additional_results = None + if args.additional_model: + additional_results = YOLO(str(args.additional_model)).predict( + image_paths, conf=min(args.thresholds), device=args.device, augment=args.augment, + imgsz=args.imgsz, max_det=args.max_det, verbose=False, + ) observations: list[dict[str, Any]] = [] - for tile, result in zip(tiles, results, strict=True): + for result_index, (tile, result) in enumerate(zip(tiles, results, strict=True)): height, width = result.orig_shape predictions = [ ( @@ -159,6 +193,20 @@ def main() -> int: ) for box, score in zip(result.boxes.xyxy.cpu().tolist(), result.boxes.conf.cpu().tolist(), strict=True) ] + if additional_results is not None: + secondary = [ + ( + scale_box(tuple(map(float, box)), args.box_scale, args.box_offset_x, args.box_offset_y), + float(score), + ) + for box, score in zip( + additional_results[result_index].boxes.xyxy.cpu().tolist(), + additional_results[result_index].boxes.conf.cpu().tolist(), strict=True, + ) + ] + predictions = ensemble_predictions( + predictions, secondary, match_iou=args.ensemble_match_iou, mode=args.ensemble_mode + ) if proposal_classifier is not None: from PIL import Image import torch @@ -229,6 +277,9 @@ def main() -> int: "box_scale": args.box_scale, "box_offset_x": args.box_offset_x, "box_offset_y": args.box_offset_y, + "additional_model": str(args.additional_model) if args.additional_model else None, + "ensemble_mode": args.ensemble_mode if args.additional_model else None, + "ensemble_match_iou": args.ensemble_match_iou if args.additional_model else None, "proposal_classifier": str(args.proposal_classifier) if args.proposal_classifier else None, "proposal_classifier_threshold": args.proposal_classifier_threshold if args.proposal_classifier else None, "proposal_crop_scale": args.proposal_crop_scale if args.proposal_classifier else None,