video_monitor/app/analysis/engines/yolo_postprocess.py
2026-08-30 22:23:12 +08:00

359 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""YOLO 共享后处理 —— 按 (algorithm_type, task_type) 分发
支持版本yolo5 / yolo8 / yolo11 / yolo26
支持任务detect / segment / classify / pose / obb
输出结构差异:
- yolo5 detect: output [1, N, 5+nc] 行格式 [cx, cy, w, h, obj, cls_scores...]
- yolo8/11/26 detect: output [1, nc+4, N] 列格式 [cx, cy, w, h, cls_score_0, cls_score_1, ...] 无 obj
- yolo5 segment: outputs[0] [1, N, 5+nc+nm], outputs[1] [1, nm, mh, mw] (protos)
- yolo8/11/26 segment: outputs[0] [1, nc+4+nm, N], outputs[1] [1, nm, mh, mw]
- classify (all): output [1, nc] top-1 logits
- yolo8/11/26 pose: output [1, nc+4+nk*3, N] (nk 通常 17)
- yolo8/11/26 obb: output [1, nc+4+1, N] 末列是角度 (radians)
本模块仅依赖 numpy + opencv被 OnnxEngine / OpenVinoEngine 复用。
YoloPytorchEngine 直接用 ultralytics 的 Results 对象,不走本模块。
"""
import logging
import numpy as np
logger = logging.getLogger("analysis.engines.yolo_post")
try:
import cv2
_CV2 = True
except Exception:
cv2 = None
_CV2 = False
def decode_outputs(outputs, algorithm_type, task_type, labels, input_size,
conf_threshold, iou_threshold, orig_size, num_classes=None):
"""主入口:返回 list[dict],每条至少含 box/label/score按任务附加上下文。
outputs: list[np.ndarray] 原始模型输出(顺序与 onnx session.get_outputs() 一致)
"""
try:
if task_type == "classify":
return _decode_classify(outputs, labels)
if task_type == "detect":
return _decode_detect(outputs, algorithm_type, labels, input_size,
conf_threshold, iou_threshold, orig_size, num_classes)
if task_type == "segment":
return _decode_segment(outputs, algorithm_type, labels, input_size,
conf_threshold, iou_threshold, orig_size, num_classes)
if task_type == "pose":
return _decode_pose(outputs, algorithm_type, labels, input_size,
conf_threshold, iou_threshold, orig_size, num_classes)
if task_type == "obb":
return _decode_obb(outputs, algorithm_type, labels, input_size,
conf_threshold, iou_threshold, orig_size, num_classes)
logger.warning("yolo_post: 未知任务类型 %s", task_type)
return []
except Exception as e:
logger.warning("yolo_post decode err: %s", e)
return []
def _is_v5(algorithm_type):
return algorithm_type in ("yolo5", "yolov5", "v5")
def _xywh2xyxy(xywh):
x, y, w, h = xywh[..., 0], xywh[..., 1], xywh[..., 2], xywh[..., 3]
return np.stack([x - w / 2, y - h / 2, x + w / 2, y + h / 2], axis=-1)
def _nms(boxes, scores, iou_threshold):
if len(boxes) == 0:
return []
boxes_list = boxes.tolist() if hasattr(boxes, "tolist") else [list(b) for b in boxes]
scores_list = scores.tolist() if hasattr(scores, "tolist") else list(scores)
if _CV2:
try:
idx = cv2.dnn.NMSBoxes(boxes_list, scores_list, 0.0, float(iou_threshold))
if idx is None or len(idx) == 0:
return []
if hasattr(idx, "flatten"):
idx = idx.flatten()
return [int(i) for i in idx]
except Exception as e:
logger.debug("cv2 NMS fallback: %s", e)
# OpenCV 不可用或 NMS 失败时的朴素实现
order = np.argsort(-np.asarray(scores_list, dtype=np.float32))
suppressed = np.zeros(len(scores_list), dtype=bool)
keep = []
boxes_arr = np.asarray(boxes_list, dtype=np.float32)
for i in order:
if suppressed[i]:
continue
keep.append(int(i))
for j in order:
if j == i or suppressed[j]:
continue
if _iou(boxes_arr[i], boxes_arr[j]) > float(iou_threshold):
suppressed[j] = True
return keep
def _iou(a, b):
xa, ya = max(a[0], b[0]), max(a[1], b[1])
xb, yb = min(a[2], b[2]), min(a[3], b[3])
iw, ih = max(0.0, xb - xa), max(0.0, yb - ya)
inter = iw * ih
area_a = max(0.0, a[2] - a[0]) * max(0.0, a[3] - a[1])
area_b = max(0.0, b[2] - b[0]) * max(0.0, b[3] - b[1])
union = area_a + area_b - inter
return inter / union if union > 0 else 0.0
def _scale_box(box, sx, sy, orig_w, orig_h):
x1 = max(0, min(orig_w - 1, box[0] * sx))
y1 = max(0, min(orig_h - 1, box[1] * sy))
x2 = max(0, min(orig_w - 1, box[2] * sx))
y2 = max(0, min(orig_h - 1, box[3] * sy))
return [int(x1), int(y1), int(x2), int(y2)]
# ===================== classify =====================
def _decode_classify(outputs, labels):
out = np.asarray(outputs[0])
if out.ndim == 2 and out.shape[0] == 1:
out = out[0]
if out.ndim != 1:
# 某些导出会带 batch 维 + 多余维度
out = out.reshape(-1)
cid = int(np.argmax(out))
score = float(out[cid])
label = labels[cid] if 0 <= cid < len(labels) else str(cid)
return [{"box": [0, 0, 0, 0], "label": str(label), "score": score, "task": "classify"}]
# ===================== detect =====================
def _decode_detect(outputs, algorithm_type, labels, input_size, conf, iou, orig_size, nc=None):
arr = np.asarray(outputs[0])
iw, ih = input_size
orig_w, orig_h = orig_size
sx, sy = float(orig_w) / iw, float(orig_h) / ih
if _is_v5(algorithm_type):
# [1, N, 5+nc] 或 [N, 5+nc]
if arr.ndim == 3 and arr.shape[0] == 1:
arr = arr[0]
if arr.ndim != 2 or arr.shape[1] < 6:
return []
nc = arr.shape[1] - 5 if nc is None else nc
if nc < 1:
nc = 1
objs = arr[:, 4]
cls_scores = arr[:, 5:5 + nc]
# score = obj * cls
scores_all = objs[:, None] * cls_scores # [N, nc]
best_cls = np.argmax(scores_all, axis=1)
best_score = scores_all[np.arange(len(arr)), best_cls]
mask = best_score >= conf
if not np.any(mask):
return []
xywh = arr[mask, :4]
boxes_raw = _xywh2xyxy(xywh)
scores = best_score[mask]
clses = best_cls[mask]
else:
# yolo8/11/26: [1, nc+4, N] 需转置
if arr.ndim == 3 and arr.shape[0] == 1:
arr = arr[0]
if arr.ndim != 2:
return []
# 行格式可能是 [4+nc, N] —— 转成 [N, 4+nc]
if arr.shape[0] < arr.shape[1] and arr.shape[0] >= 4:
arr = arr.T
if arr.ndim != 2 or arr.shape[1] < 5:
return []
nc = arr.shape[1] - 4 if nc is None else nc
if nc < 1:
nc = 1
boxes_raw = _xywh2xyxy(arr[:, :4])
cls_scores = arr[:, 4:4 + nc]
best_cls = np.argmax(cls_scores, axis=1)
best_score = cls_scores[np.arange(len(arr)), best_cls]
mask = best_score >= conf
if not np.any(mask):
return []
boxes_raw = boxes_raw[mask]
scores = best_score[mask]
clses = best_cls[mask]
boxes = [_scale_box(b, sx, sy, orig_w, orig_h) for b in boxes_raw]
keep = _nms(boxes, scores, iou)
out = []
for i in keep:
label = labels[int(clses[i])] if 0 <= int(clses[i]) < len(labels) else str(int(clses[i]))
out.append({"box": boxes[i], "label": str(label), "score": float(scores[i]), "task": "detect"})
return out
# ===================== segment =====================
def _decode_segment(outputs, algorithm_type, labels, input_size, conf, iou, orig_size, nc=None):
arr = np.asarray(outputs[0])
proto = np.asarray(outputs[1]) if len(outputs) > 1 else None
iw, ih = input_size
orig_w, orig_h = orig_size
sx, sy = float(orig_w) / iw, float(orig_h) / ih
if _is_v5(algorithm_type):
if arr.ndim == 3 and arr.shape[0] == 1:
arr = arr[0]
if arr.ndim != 2 or arr.shape[1] < 6:
return []
nm = arr.shape[1] - 5 - (nc or (arr.shape[1] - 5))
# 推断 nm若 nc 已知
if nc is None:
# 退化:用 arr.shape[1]-5 / 2 估计(不可靠),优先按 nc 推断
nm = 32
nc = arr.shape[1] - 5 - nm
else:
nm = arr.shape[1] - 5 - nc
if nm < 1:
nm = 32
objs = arr[:, 4]
cls_scores = arr[:, 5:5 + nc]
scores_all = objs[:, None] * cls_scores
best_cls = np.argmax(scores_all, axis=1)
best_score = scores_all[np.arange(len(arr)), best_cls]
mask = best_score >= conf
if not np.any(mask):
return []
xywh = arr[mask, :4]
boxes_raw = _xywh2xyxy(xywh)
coeffs = arr[mask, 5 + nc:5 + nc + nm]
scores = best_score[mask]
clses = best_cls[mask]
else:
if arr.ndim == 3 and arr.shape[0] == 1:
arr = arr[0]
if arr.ndim != 2:
return []
if arr.shape[0] < arr.shape[1] and arr.shape[0] >= 4:
arr = arr.T
if arr.ndim != 2 or arr.shape[1] < 5:
return []
if nc is None:
# 含 mask 系数:列数 = 4 + nc + nmnm 一般 32
nm = 32
nc = arr.shape[1] - 4 - nm
else:
nm = arr.shape[1] - 4 - nc
if nm < 1:
nm = 32
nc = arr.shape[1] - 4 - nm
boxes_raw = _xywh2xyxy(arr[:, :4])
cls_scores = arr[:, 4:4 + nc]
coeffs = arr[:, 4 + nc:4 + nc + nm]
best_cls = np.argmax(cls_scores, axis=1)
best_score = cls_scores[np.arange(len(arr)), best_cls]
mask = best_score >= conf
if not np.any(mask):
return []
boxes_raw = boxes_raw[mask]
coeffs = coeffs[mask]
scores = best_score[mask]
clses = best_cls[mask]
boxes = [_scale_box(b, sx, sy, orig_w, orig_h) for b in boxes_raw]
keep = _nms(boxes, scores, iou)
out = []
for i in keep:
label = labels[int(clses[i])] if 0 <= int(clses[i]) < len(labels) else str(int(clses[i]))
item = {"box": boxes[i], "label": str(label), "score": float(scores[i]), "task": "segment"}
# mask 系数(简化:仅返回系数数组,由上层按需合成;不强制合成完整 mask 避免性能开销)
try:
item["mask_coeffs"] = [float(x) for x in coeffs[i]]
except Exception:
pass
out.append(item)
return out
# ===================== pose =====================
def _decode_pose(outputs, algorithm_type, labels, input_size, conf, iou, orig_size, nc=None):
arr = np.asarray(outputs[0])
iw, ih = input_size
orig_w, orig_h = orig_size
sx, sy = float(orig_w) / iw, float(orig_h) / ih
# 仅 yolo8/11/26 pose 普遍为 [1, 4+1+nk*3, N]4 box + 1 conf + nk*3 kpt
if arr.ndim == 3 and arr.shape[0] == 1:
arr = arr[0]
if arr.ndim != 2:
return []
if arr.shape[0] < arr.shape[1] and arr.shape[0] >= 5:
arr = arr.T
if arr.ndim != 2 or arr.shape[1] < 6:
return []
# 列布局:[cx, cy, w, h, conf, kpt_x, kpt_y, kpt_conf, ...] —— 标准 ultralytics pose 导出
confs = arr[:, 4]
mask = confs >= conf
if not np.any(mask):
return []
arr = arr[mask]
boxes_raw = _xywh2xyxy(arr[:, :4])
confs = arr[:, 4]
# 关键点:剩余列按 [x, y, conf] 三元组
kpt_cols = arr.shape[1] - 5
nk = kpt_cols // 3
kpts = arr[:, 5:5 + nk * 3].reshape(-1, nk, 3) if nk > 0 else None
boxes = [_scale_box(b, sx, sy, orig_w, orig_h) for b in boxes_raw]
keep = _nms(boxes, confs, iou)
out = []
for i in keep:
label = labels[0] if labels else "person"
item = {"box": boxes[i], "label": str(label), "score": float(confs[i]), "task": "pose"}
if kpts is not None:
kp = kpts[i]
# 缩放关键点 xy
item["keypoints"] = [
[float(kp[j, 0] * sx), float(kp[j, 1] * sy), float(kp[j, 2])]
for j in range(nk)
]
out.append(item)
return out
# ===================== obb =====================
def _decode_obb(outputs, algorithm_type, labels, input_size, conf, iou, orig_size, nc=None):
arr = np.asarray(outputs[0])
iw, ih = input_size
orig_w, orig_h = orig_size
sx, sy = float(orig_w) / iw, float(orig_h) / ih
# yolo8/11/26 obb: [1, 4+nc+1, N] 末列角度
if arr.ndim == 3 and arr.shape[0] == 1:
arr = arr[0]
if arr.ndim != 2:
return []
if arr.shape[0] < arr.shape[1] and arr.shape[0] >= 5:
arr = arr.T
if arr.ndim != 2 or arr.shape[1] < 6:
return []
nc = arr.shape[1] - 5 if nc is None else nc
if nc < 1:
nc = 1
boxes_raw = _xywh2xyxy(arr[:, :4])
cls_scores = arr[:, 4:4 + nc]
angles = arr[:, 4 + nc]
best_cls = np.argmax(cls_scores, axis=1)
best_score = cls_scores[np.arange(len(arr)), best_cls]
mask = best_score >= conf
if not np.any(mask):
return []
boxes_raw = boxes_raw[mask]
scores = best_score[mask]
clses = best_cls[mask]
angles = angles[mask]
boxes = [_scale_box(b, sx, sy, orig_w, orig_h) for b in boxes_raw]
keep = _nms(boxes, scores, iou)
out = []
for i in keep:
label = labels[int(clses[i])] if 0 <= int(clses[i]) < len(labels) else str(int(clses[i]))
out.append({"box": boxes[i], "label": str(label), "score": float(scores[i]),
"task": "obb", "angle": float(angles[i])})
return out