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.
This commit is contained in:
patrykorwat 2026-08-24 10:43:43 +00:00
parent 0d0581494f
commit 97705e7df2

View File

@ -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: