video_monitor/app/analysis/engines/yolo_postprocess.py

359 lines
13 KiB
Python
Raw Normal View History

2026-08-30 22:22:11 +08:00
"""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