From 97705e7df225ccf96a38648620ac7ba8815f4e3d Mon Sep 17 00:00:00 2001 From: patrykorwat Date: Mon, 24 Aug 2026 10:43:43 +0000 Subject: [PATCH] perf(axelera): greedy NMS via cv2.dnn.NMSBoxes Replace the pure-Python O(n^2) _nms_xyxy loop with the framework's own C++ NMS (xyxy -> xywh first, same as frigate.util.model). Same contract: kept indices in descending score order; empty-safe. Removes a per-frame CPU hot spot on dense daytime frames without changing decode results. --- frigate/detectors/plugins/axelera.py | 53 +++++++++++++++++----------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/frigate/detectors/plugins/axelera.py b/frigate/detectors/plugins/axelera.py index a56cb55d93..ba302325a9 100644 --- a/frigate/detectors/plugins/axelera.py +++ b/frigate/detectors/plugins/axelera.py @@ -174,28 +174,39 @@ def _find_model_json(where: str) -> str: ) -def _nms_xyxy(boxes, scores, iou_threshold: float) -> list: - """Classic greedy NMS on xyxy boxes. Returns kept indices (desc score).""" +def _nms_xyxy(boxes, scores, iou_threshold: float) -> np.ndarray: + """Greedy NMS on xyxy boxes via cv2.dnn.NMSBoxes (C++, GIL-free). + + Returns kept indices in descending score order, same contract as the + previous pure-Python loop but without the O(n^2) worst case on dense + frames (daytime, multiple motion regions at 48 fps offered). + """ if len(boxes) == 0: - return [] - x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] - areas = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1) - order = scores.argsort()[::-1] - keep = [] - while order.size > 0: - i = order[0] - keep.append(int(i)) - if order.size == 1: - break - xx1 = np.maximum(x1[i], x1[order[1:]]) - yy1 = np.maximum(y1[i], y1[order[1:]]) - xx2 = np.minimum(x2[i], x2[order[1:]]) - yy2 = np.minimum(y2[i], y2[order[1:]]) - inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1) - union = areas[i] + areas[order[1:]] - inter - ovr = np.where(union > 0, inter / np.maximum(union, 1e-9), 0) - order = order[1:][ovr <= iou_threshold] - return keep + return np.array([], dtype=np.int64) + x1 = boxes[:, 0] + y1 = boxes[:, 1] + xywh = np.stack( + [ + x1, + y1, + np.maximum(0, boxes[:, 2] - x1), + np.maximum(0, boxes[:, 3] - y1), + ], + axis=1, + ) + indices = cv2.dnn.NMSBoxes( + xywh.tolist(), + [float(s) for s in scores], + # inputs are already filtered above CONF_THRESHOLD by the caller + score_threshold=0.0, + nms_threshold=float(iou_threshold), + ) + if len(indices) == 0: + return np.array([], dtype=np.int64) + if isinstance(indices, np.ndarray): + return indices.reshape(-1).astype(np.int64) + # OpenCV versions that return a (N, 1) list of nested lists + return np.asarray(indices, dtype=np.int64).reshape(-1) def _read_model_manifest(model_dir: str) -> dict: