359 lines
13 KiB
Python
359 lines
13 KiB
Python
"""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 + nm,nm 一般 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
|