Audit and suppress nested detector proposals
This commit is contained in:
@@ -52,6 +52,29 @@ def metrics(tp: int, fp: int, fn: int) -> dict[str, float | int]:
|
||||
}
|
||||
|
||||
|
||||
def containment_overlap(
|
||||
left: tuple[float, float, float, float], right: tuple[float, float, float, float]
|
||||
) -> float:
|
||||
x1, y1 = max(left[0], right[0]), max(left[1], right[1])
|
||||
x2, y2 = min(left[2], right[2]), min(left[3], right[3])
|
||||
intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1)
|
||||
left_area = max(0.0, left[2] - left[0]) * max(0.0, left[3] - left[1])
|
||||
right_area = max(0.0, right[2] - right[0]) * max(0.0, right[3] - right[1])
|
||||
denominator = min(left_area, right_area)
|
||||
return intersection / denominator if denominator > 0 else 0.0
|
||||
|
||||
|
||||
def suppress_contained_predictions(
|
||||
predictions: list[tuple[tuple[float, float, float, float], float]], threshold: float
|
||||
) -> list[tuple[tuple[float, float, float, float], float]]:
|
||||
kept: list[tuple[tuple[float, float, float, float], float]] = []
|
||||
for candidate in sorted(predictions, key=lambda item: -item[1]):
|
||||
if any(containment_overlap(candidate[0], existing[0]) >= threshold for existing in kept):
|
||||
continue
|
||||
kept.append(candidate)
|
||||
return kept
|
||||
|
||||
|
||||
def ensemble_predictions(
|
||||
primary: list[tuple[tuple[float, float, float, float], float]],
|
||||
secondary: list[tuple[tuple[float, float, float, float], float]],
|
||||
@@ -128,6 +151,18 @@ def main() -> int:
|
||||
default=1000,
|
||||
help="Maximum detections retained per tile; dense Belgian urban tiles exceed YOLO's default 300.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nms-iou",
|
||||
type=float,
|
||||
default=0.7,
|
||||
help="Inference NMS IoU; freeze calibration-selected values before protected test evaluation.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--containment-nms",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Suppress lower-score nested boxes at this intersection-over-minimum-area threshold.",
|
||||
)
|
||||
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)
|
||||
@@ -141,6 +176,10 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
if args.proposal_classifier_batch < 1:
|
||||
parser.error("--proposal-classifier-batch must be positive")
|
||||
if not 0.0 < args.nms_iou < 1.0:
|
||||
parser.error("--nms-iou must be between zero and one")
|
||||
if not 0.0 < args.containment_nms <= 1.0:
|
||||
parser.error("--containment-nms must be above zero and at most one")
|
||||
|
||||
from ultralytics import YOLO
|
||||
proposal_classifier = None
|
||||
@@ -173,13 +212,14 @@ def main() -> int:
|
||||
augment=args.augment,
|
||||
imgsz=args.imgsz,
|
||||
max_det=args.max_det,
|
||||
iou=args.nms_iou,
|
||||
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,
|
||||
imgsz=args.imgsz, max_det=args.max_det, iou=args.nms_iou, verbose=False,
|
||||
)
|
||||
observations: list[dict[str, Any]] = []
|
||||
for result_index, (tile, result) in enumerate(zip(tiles, results, strict=True)):
|
||||
@@ -193,6 +233,7 @@ def main() -> int:
|
||||
)
|
||||
for box, score in zip(result.boxes.xyxy.cpu().tolist(), result.boxes.conf.cpu().tolist(), strict=True)
|
||||
]
|
||||
predictions = suppress_contained_predictions(predictions, args.containment_nms)
|
||||
if additional_results is not None:
|
||||
secondary = [
|
||||
(
|
||||
@@ -204,6 +245,7 @@ def main() -> int:
|
||||
additional_results[result_index].boxes.conf.cpu().tolist(), strict=True,
|
||||
)
|
||||
]
|
||||
secondary = suppress_contained_predictions(secondary, args.containment_nms)
|
||||
predictions = ensemble_predictions(
|
||||
predictions, secondary, match_iou=args.ensemble_match_iou, mode=args.ensemble_mode
|
||||
)
|
||||
@@ -274,6 +316,8 @@ def main() -> int:
|
||||
"test_time_augmentation": args.augment,
|
||||
"inference_imgsz": args.imgsz,
|
||||
"max_detections_per_tile": args.max_det,
|
||||
"nms_iou": args.nms_iou,
|
||||
"containment_nms": args.containment_nms,
|
||||
"box_scale": args.box_scale,
|
||||
"box_offset_x": args.box_offset_x,
|
||||
"box_offset_y": args.box_offset_y,
|
||||
|
||||
Reference in New Issue
Block a user