Files
Bird_detect_cascade_service_V2/app/inference/yolo_onnx.py
T
2026-08-21 14:19:14 +08:00

217 lines
6.6 KiB
Python

from typing import List, Dict
import cv2
import numpy as np
import onnxruntime as ort
def letterbox(
img: np.ndarray,
new_shape=(960, 960),
color=(114, 114, 114),
auto=False,
scale_fill=False,
scale_up=True,
stride=32,
):
shape = img.shape[:2] # h, w
if isinstance(new_shape, int):
new_shape = (new_shape, new_shape)
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
if not scale_up:
r = min(r, 1.0)
ratio = (r, r)
new_unpad = (int(round(shape[1] * r)), int(round(shape[0] * r)))
dw = new_shape[1] - new_unpad[0]
dh = new_shape[0] - new_unpad[1]
if auto:
dw, dh = np.mod(dw, stride), np.mod(dh, stride)
elif scale_fill:
dw, dh = 0.0, 0.0
new_unpad = (new_shape[1], new_shape[0])
ratio = (new_shape[1] / shape[1], new_shape[0] / shape[0])
dw /= 2
dh /= 2
if shape[::-1] != new_unpad:
img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
top = int(round(dh - 0.1))
bottom = int(round(dh + 0.1))
left = int(round(dw - 0.1))
right = int(round(dw + 0.1))
img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
return img, ratio, (dw, dh)
def xywh2xyxy(x: np.ndarray) -> np.ndarray:
y = np.copy(x)
y[:, 0] = x[:, 0] - x[:, 2] / 2
y[:, 1] = x[:, 1] - x[:, 3] / 2
y[:, 2] = x[:, 0] + x[:, 2] / 2
y[:, 3] = x[:, 1] + x[:, 3] / 2
return y
def box_iou(box: np.ndarray, boxes: np.ndarray) -> np.ndarray:
x1 = np.maximum(box[0], boxes[:, 0])
y1 = np.maximum(box[1], boxes[:, 1])
x2 = np.minimum(box[2], boxes[:, 2])
y2 = np.minimum(box[3], boxes[:, 3])
inter = np.maximum(0, x2 - x1) * np.maximum(0, y2 - y1)
area1 = max(0, box[2] - box[0]) * max(0, box[3] - box[1])
area2 = np.maximum(0, boxes[:, 2] - boxes[:, 0]) * np.maximum(0, boxes[:, 3] - boxes[:, 1])
union = area1 + area2 - inter + 1e-7
return inter / union
def nms_numpy(boxes: np.ndarray, scores: np.ndarray, iou_thres: float) -> List[int]:
if len(boxes) == 0:
return []
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(int(i))
if order.size == 1:
break
ious = box_iou(boxes[i], boxes[order[1:]])
inds = np.where(ious <= iou_thres)[0]
order = order[inds + 1]
return keep
def scale_boxes_from_letterbox(boxes: np.ndarray, ratio, pad, original_shape) -> np.ndarray:
# boxes: xyxy in letterboxed image
gain = ratio[0]
pad_x, pad_y = pad
boxes[:, [0, 2]] -= pad_x
boxes[:, [1, 3]] -= pad_y
boxes[:, :4] /= gain
h, w = original_shape[:2]
boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, w - 1)
boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, h - 1)
return boxes
class YOLOv5ONNXDetector:
def __init__(
self,
model_path: str,
class_names: List[str],
img_size: int = 960,
device: str = "cpu",
):
self.model_path = model_path
self.class_names = class_names
self.img_size = img_size
providers = ["CPUExecutionProvider"]
if device.lower() in ["cuda", "gpu"]:
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
self.session = ort.InferenceSession(model_path, providers=providers)
active_providers = self.session.get_providers()
print("[YOLO] providers:", active_providers)
if (
device.lower() in ["cuda", "gpu"]
and "CUDAExecutionProvider" not in active_providers
):
raise RuntimeError(
"请求使用 CUDA 推理,但 YOLO 会话未启用 "
"CUDAExecutionProvider。请安装匹配 CUDA/cuDNN 的 "
"onnxruntime-gpu。"
)
self.input_name = self.session.get_inputs()[0].name
self.output_names = [o.name for o in self.session.get_outputs()]
def preprocess(self, img_bgr: np.ndarray):
img_lb, ratio, pad = letterbox(img_bgr, new_shape=(self.img_size, self.img_size), auto=False)
img_rgb = cv2.cvtColor(img_lb, cv2.COLOR_BGR2RGB)
img = img_rgb.transpose(2, 0, 1).astype(np.float32) / 255.0
img = np.expand_dims(img, axis=0)
return img, ratio, pad
def infer(
self,
img_bgr: np.ndarray,
conf_thres: float = 0.25,
iou_thres: float = 0.45,
max_det: int = 50,
) -> List[Dict]:
input_tensor, ratio, pad = self.preprocess(img_bgr)
outputs = self.session.run(self.output_names, {self.input_name: input_tensor})
pred = outputs[0]
# 常见 YOLOv5 ONNX 输出: [1, num_boxes, 5 + num_classes]
if isinstance(pred, list):
pred = pred[0]
pred = np.asarray(pred)
if pred.ndim == 3:
pred = pred[0]
elif pred.ndim == 2:
pass
else:
raise ValueError(f"不支持的 YOLO 输出维度: {pred.shape}")
if pred.shape[1] < 6:
raise ValueError(f"YOLO 输出最后一维应至少为 6,当前为: {pred.shape}")
boxes_xywh = pred[:, :4]
obj_conf = pred[:, 4]
if pred.shape[1] == 6:
# 某些导出格式可能已经是 x,y,w,h,conf,class_id
cls_ids = pred[:, 5].astype(np.int32)
scores = obj_conf
else:
cls_scores = pred[:, 5:]
cls_ids = np.argmax(cls_scores, axis=1)
cls_conf = cls_scores[np.arange(len(cls_scores)), cls_ids]
scores = obj_conf * cls_conf
mask = scores >= conf_thres
if not np.any(mask):
return []
boxes_xywh = boxes_xywh[mask]
scores = scores[mask]
cls_ids = cls_ids[mask]
boxes = xywh2xyxy(boxes_xywh)
boxes = scale_boxes_from_letterbox(boxes, ratio, pad, img_bgr.shape)
final_results = []
for class_id in np.unique(cls_ids):
idxs = np.where(cls_ids == class_id)[0]
keep = nms_numpy(boxes[idxs], scores[idxs], iou_thres)
for k in keep:
original_idx = idxs[k]
cid = int(class_id)
cname = self.class_names[cid] if 0 <= cid < len(self.class_names) else str(cid)
final_results.append({
"bbox_xyxy": boxes[original_idx].astype(float).tolist(),
"det_confidence": float(scores[original_idx]),
"det_class_id": cid,
"det_class_name": cname,
})
final_results.sort(key=lambda x: x["det_confidence"], reverse=True)
return final_results[:max_det]