Source code for cv2ext.bboxes._algorithms

# Copyright (c) 2024 Justin Davis (davisjustin302@gmail.com)
#
# MIT License
from __future__ import annotations

from typing import TYPE_CHECKING

from cv2ext._jit import register_jit

from ._iou import _iou_kernel

if TYPE_CHECKING:
    from collections.abc import Sequence


[docs] def filter_bboxes_by_region( bboxes: Sequence[tuple[int, int, int, int]], region: tuple[int, int, int, int], overlap: float = 0.6, ) -> list[tuple[int, int, int, int]]: """ Get the bounding boxes contained within a region. Parameters ---------- bboxes : Sequence[tuple[int, int, int, int]] The bounding boxes in form (x1, y1, x2, y2). region : tuple[int, int, int, int] The region by which to filter the bounding boxes. overlap : float The percentage of a bounding boxes area is inside the region to be included. Returns ------- list[tuple[int, int, int, int]] The bounding boxes in the region. """ filtered: list[tuple[int, int, int, int]] = [] r_x1, r_y1, r_x2, r_y2 = region for bbox in bboxes: b_x1, b_y1, b_x2, b_y2 = bbox area = (b_x2 - b_x1) * (b_y2 - b_y1) # Calculate the intersection area x1 = max(b_x1, r_x1) y1 = max(b_y1, r_y1) x2 = min(b_x2, r_x2) y2 = min(b_y2, r_y2) intersection_area = max(0, x2 - x1) * max(0, y2 - y1) if area == 0: continue # Calculate the percentage of the bbox area that is inside the region overlap_percentage = intersection_area / area if overlap_percentage >= overlap: filtered.append(bbox) return filtered
@register_jit() def _match_kernel( bboxes1: Sequence[ tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int] ], bboxes2: Sequence[ tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int] ], iou_threshold: float = 0.5, *, class_agnostic: bool = False, ) -> list[tuple[int, int]]: # if len(bboxes1) == 0 or len(bboxes2) == 0: # err_msg = "Each list of bboxes must have at least length of 1." # raise ValueError(err_msg) matches: list[tuple[int, int]] = [] used_idx: set[int] = set() for idx1, entry1 in enumerate(bboxes1): best_iou: float = 0.0 best_idx: int = -1 if len(entry1) == 3: bbox1, _, cid1 = entry1 else: bbox1 = entry1 cid1 = -1 for idx2, entry2 in enumerate(bboxes2): if idx2 in used_idx: continue if len(entry2) == 3: bbox2, _, cid2 = entry2 else: bbox2 = entry2 cid2 = -1 if not class_agnostic and cid1 != cid2: continue iou = _iou_kernel(bbox1, bbox2) if iou > best_iou: best_iou = iou best_idx = idx2 if best_iou >= iou_threshold: matches.append((idx1, best_idx)) used_idx.add(best_idx) return matches
[docs] def match( bboxes1: Sequence[ tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int] ], bboxes2: Sequence[ tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int] ], iou_threshold: float = 0.5, *, class_agnostic: bool = False, ) -> list[tuple[int, int]]: """ Match bounding boxes using a greedy algorithm. Parameters ---------- bboxes1 : Sequence[tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int]] The first Sequence of bounding boxes bboxes2 : Sequence[tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int]] The second Sequence of bounding boxes iou_threshold : float, optional The IOU threshold which determines whether two bounding boxes are a match. By default, 0.5 class_agnostic : bool, optional Whether or not to compare class ID (if present) By default, False Returns ------- list[tuple[int, int]] A list of the matching indices """ return _match_kernel(bboxes1, bboxes2, iou_threshold, class_agnostic=class_agnostic)
[docs] def calculate_metrics( bboxes1: Sequence[ tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int] ], bboxes2: Sequence[ tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int] ], iou_threshold: float = 0.5, epsilon: float = 1e-6, *, class_agnostic: bool = False, ) -> tuple[list[tuple[int, int]], dict[str, float]]: """ Compute accuracy metrics between two Sequences of bounding boxes/detections. Bounding boxes are matched using the greedy algolrithm from :func:`match`. Parameters ---------- bboxes1 : Sequence[tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int]] The first Sequence of bounding boxes bboxes2 : Sequence[tuple[int, int, int, int] | tuple[tuple[int, int, int, int], float, int]] The second Sequence of bounding boxes iou_threshold : float, optional The IOU threshold which determines whether two bounding boxes are a match. By default, 0.5 epsilon : float, optional The minimum/default value to prevent divide by zero errors. By default, 1e-6 class_agnostic : bool, optional Whether or not to compare class ID (if present) By default, False Returns ------- tuple[ list[tuple[int, int]], dict[str, float], ] The list of matching indices and a dict of the computer metrics Metrics are: tp, fp, fn, precision, recall, f1 """ matches = match(bboxes1, bboxes2, iou_threshold, class_agnostic=class_agnostic) true_positives = len(matches) false_positives = len(bboxes1) - true_positives false_negatives = len(bboxes2) - true_positives default_val = max(int(len(bboxes2) == len(bboxes1)), epsilon) precision = ( true_positives / (true_positives + false_positives) if true_positives + false_positives > 0 else default_val ) recall = ( true_positives / (true_positives + false_negatives) if true_positives + false_negatives > 0 else default_val ) f1_score = ( 2 * (precision * recall) / (precision + recall) if precision + recall > 0 else default_val ) metrics = { "tp": max(true_positives, 0), "fp": max(false_positives, 0), "fn": max(false_negatives, 0), "precision": max(precision, 1e-6), "recall": max(recall, 1e-6), "f1": max(f1_score, 1e-6), } return matches, metrics