| |
| import onnxruntime as ort |
| import cv2 |
| import numpy as np |
| import argparse |
| import os |
|
|
|
|
| def preprocess_image(image, input_size=(640, 640)): |
| """ |
| Preprocess image with left-top aligned letterbox (same as official YOLO). |
| |
| Args: |
| image: BGR image (H, W, C) |
| input_size: (height, width) target size |
| |
| Returns: |
| input_tensor: (1, 3, H, W) float32 tensor normalized to [0, 1] |
| scale: scale ratio used for resizing |
| original_shape: (orig_h, orig_w) original image shape |
| """ |
| orig_h, orig_w = image.shape[:2] |
| m_h, m_w = input_size |
|
|
| |
| scale = min(m_h / orig_h, m_w / orig_w) |
|
|
| |
| new_w, new_h = int(orig_w * scale), int(orig_h * scale) |
| img_resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR) |
|
|
| |
| input_bgr = cv2.copyMakeBorder( |
| img_resized, 0, m_h - new_h, 0, m_w - new_w, |
| cv2.BORDER_CONSTANT, value=(114, 114, 114) |
| ) |
|
|
| |
| input_rgb = cv2.cvtColor(input_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 |
|
|
| |
| input_tensor = np.transpose(input_rgb, (2, 0, 1))[None, ...] |
|
|
| return input_tensor, scale, (orig_h, orig_w) |
|
|
|
|
| def softmax(x, axis=-1): |
| """Compute softmax along axis.""" |
| e_x = np.exp(x - np.max(x, axis=axis, keepdims=True)) |
| return e_x / np.sum(e_x, axis=axis, keepdims=True) |
|
|
|
|
| def dfl_decode(box_pred, reg_max=16): |
| """ |
| Decode DFL (Distribution Focal Loss) box predictions. |
| |
| Args: |
| box_pred: (N, 4 * reg_max) raw box predictions |
| reg_max: number of DFL bins |
| |
| Returns: |
| (N, 4) decoded ltrb distances |
| """ |
| N = box_pred.shape[0] |
| box_pred = box_pred.reshape(N, 4, reg_max) |
| box_pred = softmax(box_pred, axis=-1) |
| proj = np.arange(reg_max, dtype=np.float32) |
| return np.sum(box_pred * proj, axis=-1) |
|
|
|
|
| def decode_bboxes(bbox_preds, anchors, stride, reg_max=None): |
| """ |
| Decode bounding boxes from predictions. |
| |
| Args: |
| bbox_preds: (N, 4) or (N, 4*reg_max) - distance predictions in ltrb format |
| anchors: (N, 2) - anchor points (x, y) with offset 0.5 |
| stride: scalar - stride value |
| reg_max: if not None, apply DFL decoding first |
| |
| Returns: |
| boxes: (N, 4) in xyxy format |
| """ |
| if reg_max is not None and bbox_preds.shape[-1] == 4 * reg_max: |
| bbox_preds = dfl_decode(bbox_preds, reg_max) |
|
|
| |
| lt = bbox_preds[:, :2] |
| rb = bbox_preds[:, 2:] |
|
|
| x1y1 = anchors - lt |
| x2y2 = anchors + rb |
|
|
| boxes = np.hstack([x1y1, x2y2]) * stride |
| return boxes |
|
|
|
|
| def scale_boxes_lefttop(boxes, scale, orig_shape): |
| """Scale boxes from model output to original image coordinates.""" |
| boxes = boxes.copy() |
| boxes[..., :4] /= scale |
| boxes[..., [0, 2]] = np.clip(boxes[..., [0, 2]], 0, orig_shape[1]) |
| boxes[..., [1, 3]] = np.clip(boxes[..., [1, 3]], 0, orig_shape[0]) |
| return boxes |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description='YOLOv8-Det ONNX Inference') |
| parser.add_argument('-m', '--model', type=str, default='yolov8n_640x640.onnx', |
| dest='model_path', help='Path to YOLOv8 Detection *.onnx Model.') |
| parser.add_argument('-i', '--img', type=str, default='bus.jpg', |
| dest='test_img', help='Path to Test Image.') |
| parser.add_argument('-o', '--output', type=str, default='result_yolov8_det.jpg', |
| dest='img_save_path', help='Path to Save Result Image.') |
| parser.add_argument('--score-thres', type=float, default=0.25, |
| help='Confidence threshold.') |
| parser.add_argument('--nms-thres', type=float, default=0.7, |
| help='IoU threshold for NMS.') |
| opt = parser.parse_args() |
|
|
| if not os.path.exists(opt.model_path): |
| print(f"Error: Model not found: {opt.model_path}") |
| return |
|
|
| if not os.path.exists(opt.test_img): |
| print(f"Error: Image not found: {opt.test_img}") |
| return |
|
|
| |
| providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] |
| try: |
| session = ort.InferenceSession(opt.model_path, providers=providers) |
| except: |
| session = ort.InferenceSession(opt.model_path, providers=['CPUExecutionProvider']) |
|
|
| input_name = session.get_inputs()[0].name |
| output_names = [o.name for o in session.get_outputs()] |
| input_shape = session.get_inputs()[0].shape |
| imgsz = (input_shape[2], input_shape[3]) |
|
|
| |
| img0 = cv2.imread(opt.test_img) |
| if img0 is None: |
| print(f"Error: Cannot read image: {opt.test_img}") |
| return |
|
|
| |
| img, scale, orig_shape = preprocess_image(img0.copy(), imgsz) |
|
|
| |
| outputs = session.run(output_names, {input_name: img}) |
|
|
| |
| strides = [8, 16, 32] |
| conf_raw = -np.log(1 / opt.score_thres - 1) |
| detections = [] |
|
|
| |
| for scale_idx, stride in enumerate(strides): |
| box_idx = scale_idx * 2 |
| cls_idx = scale_idx * 2 + 1 |
|
|
| if box_idx >= len(outputs) or cls_idx >= len(outputs): |
| continue |
|
|
| box_data = outputs[box_idx] |
| cls_data = outputs[cls_idx] |
|
|
| H, W = box_data.shape[1:3] |
| box_channels = box_data.shape[-1] |
|
|
| |
| reg_max = None |
| if box_channels > 4 and box_channels % 4 == 0: |
| reg_max = box_channels // 4 |
|
|
| |
| box_data = box_data[0].reshape(-1, box_channels) |
| cls_data = cls_data[0].reshape(-1, cls_data.shape[-1]) |
|
|
| |
| cls_scores = np.max(cls_data, axis=1) |
| cls_ids = np.argmax(cls_data, axis=1) |
|
|
| |
| valid_mask = cls_scores >= conf_raw |
| if not np.any(valid_mask): |
| continue |
|
|
| v_box = box_data[valid_mask] |
| v_score = 1 / (1 + np.exp(-cls_scores[valid_mask])) |
| v_id = cls_ids[valid_mask] |
|
|
| |
| gy, gx = np.indices((H, W)) |
| anchors = np.stack((gx.ravel(), gy.ravel()), axis=-1).astype(np.float32) + 0.5 |
| anchors = anchors[valid_mask] |
|
|
| |
| boxes = decode_bboxes(v_box, anchors, stride, reg_max) |
|
|
| |
| for i in range(len(boxes)): |
| detections.append([*boxes[i], v_score[i], v_id[i]]) |
|
|
| if len(detections) == 0: |
| print("No detections found.") |
| cv2.imwrite(opt.img_save_path, img0) |
| return |
|
|
| detections = np.array(detections) |
|
|
| |
| xywh = detections[:, :4].copy() |
| xywh[:, 2] = xywh[:, 2] - xywh[:, 0] |
| xywh[:, 3] = xywh[:, 3] - xywh[:, 1] |
|
|
| indices = cv2.dnn.NMSBoxes(xywh.tolist(), detections[:, 4].tolist(), opt.score_thres, opt.nms_thres) |
|
|
| if len(indices) == 0: |
| print("No detections after NMS.") |
| cv2.imwrite(opt.img_save_path, img0) |
| return |
|
|
| indices = indices.flatten() |
| detections = detections[indices] |
|
|
| |
| detections[:, :4] = scale_boxes_lefttop(detections[:, :4], scale, orig_shape) |
|
|
| |
| coco_names = [ |
| "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", |
| "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", |
| "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", |
| "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", |
| "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", |
| "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", |
| "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", |
| "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", |
| "scissors", "teddy bear", "hair drier", "toothbrush" |
| ] |
|
|
| |
| base_colors = [ |
| (255, 42, 4), (235, 219, 11), (243, 243, 243), (183, 223, 0), (104, 31, 17), |
| (221, 111, 255), (79, 68, 255), (0, 237, 204), (68, 243, 0), (255, 0, 189), |
| (255, 180, 0), (186, 0, 221), (255, 255, 0), (0, 192, 38), (179, 255, 1), |
| (255, 36, 125), (104, 0, 123), (108, 27, 255), (47, 109, 252), (11, 255, 162), |
| ] |
|
|
| for det in detections: |
| box = det[:4].astype(int) |
| conf = det[4] |
| cls_id = int(det[5]) |
| color = [int(c) for c in base_colors[cls_id % len(base_colors)]] |
| cls_name = coco_names[cls_id] if cls_id < len(coco_names) else str(cls_id) |
|
|
| cv2.rectangle(img0, (box[0], box[1]), (box[2], box[3]), color, 2) |
| label = f"{cls_name} {conf:.2f}" |
| cv2.putText(img0, label, (box[0], box[1] - 10), |
| cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) |
|
|
| cv2.imwrite(opt.img_save_path, img0) |
| print(f"Done! Found {len(detections)} objects. Result saved to {opt.img_save_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|