Fangming Guo commited on
Upload 12 files
Browse files- .gitattributes +2 -0
- ax_infer.py +294 -0
- boats.jpg +3 -0
- config.json +32 -0
- dota_cali.tar +3 -0
- export_onnx.py +135 -0
- onnx_infer.py +308 -0
- result_yolo26_obb.jpg +3 -0
- yolo26l-obb_1024x1024.onnx +3 -0
- yolo26m-obb_1024x1024.onnx +3 -0
- yolo26n-obb_1024x1024.onnx +3 -0
- yolo26s-obb_1024x1024.onnx +3 -0
- yolo26x-obb_1024x1024.onnx +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
boats.jpg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
result_yolo26_obb.jpg filter=lfs diff=lfs merge=lfs -text
|
ax_infer.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# YOLO26-OBB Inference Script (AXERARuntime)
|
| 4 |
+
#
|
| 5 |
+
# Copyright (c) 2025, AXERA Semiconductor Co., Ltd. All rights reserved.
|
| 6 |
+
#
|
| 7 |
+
# Licensed under the BSD 3-Clause License (the "License"); you may not use
|
| 8 |
+
# this file except in compliance with the License. You may obtain a copy of
|
| 9 |
+
# the License at
|
| 10 |
+
#
|
| 11 |
+
# https://opensource.org/licenses/BSD-3-Clause
|
| 12 |
+
#
|
| 13 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 14 |
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
| 15 |
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
| 16 |
+
# License for the specific language governing permissions and limitations
|
| 17 |
+
# under the License.
|
| 18 |
+
# =============================================================================
|
| 19 |
+
#
|
| 20 |
+
# Author: GUOFANGMING
|
| 21 |
+
#
|
| 22 |
+
|
| 23 |
+
import os
|
| 24 |
+
import cv2
|
| 25 |
+
import numpy as np
|
| 26 |
+
from time import time
|
| 27 |
+
import argparse
|
| 28 |
+
import logging
|
| 29 |
+
import axengine as ort
|
| 30 |
+
|
| 31 |
+
logging.basicConfig(
|
| 32 |
+
level=logging.DEBUG,
|
| 33 |
+
format='[%(name)s] [%(asctime)s.%(msecs)03d] [%(levelname)s] %(message)s',
|
| 34 |
+
datefmt='%H:%M:%S'
|
| 35 |
+
)
|
| 36 |
+
logger = logging.getLogger("YOLO26-OBB")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# DOTAv1 class names (15 classes)
|
| 40 |
+
DOTA_CLASSES = [
|
| 41 |
+
"plane", "ship", "storage tank", "baseball diamond", "tennis court",
|
| 42 |
+
"basketball court", "ground track field", "harbor", "bridge",
|
| 43 |
+
"large vehicle", "small vehicle", "helicopter", "roundabout",
|
| 44 |
+
"soccer ball field", "swimming pool"
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
DOTA_COLORS = [
|
| 48 |
+
(255, 56, 56), (255, 159, 56), (255, 207, 56), (180, 255, 56),
|
| 49 |
+
(102, 255, 56), (56, 255, 122), (56, 255, 207), (56, 207, 255),
|
| 50 |
+
(56, 122, 255), (102, 56, 255), (180, 56, 255), (255, 56, 207),
|
| 51 |
+
(255, 56, 122), (200, 200, 200), (128, 128, 255),
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def infer_hw_layout(shape):
|
| 56 |
+
"""Infer input height, width and layout from model input shape."""
|
| 57 |
+
shape = list(shape)
|
| 58 |
+
if len(shape) == 4 and shape[-1] == 3:
|
| 59 |
+
h = int(shape[1] or 1024)
|
| 60 |
+
w = int(shape[2] or 1024)
|
| 61 |
+
return h, w, "NHWC"
|
| 62 |
+
if len(shape) == 4 and shape[1] == 3:
|
| 63 |
+
h = int(shape[2] or 1024)
|
| 64 |
+
w = int(shape[3] or 1024)
|
| 65 |
+
return h, w, "NCHW"
|
| 66 |
+
return 1024, 1024, "NCHW"
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def softmax(x, axis=-1):
|
| 70 |
+
e_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
|
| 71 |
+
return e_x / np.sum(e_x, axis=axis, keepdims=True)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def dfl_decode(box_pred, reg_max):
|
| 75 |
+
N = box_pred.shape[0]
|
| 76 |
+
box_pred = box_pred.reshape(N, 4, reg_max)
|
| 77 |
+
box_pred = softmax(box_pred, axis=-1)
|
| 78 |
+
proj = np.arange(reg_max, dtype=np.float32)
|
| 79 |
+
return np.sum(box_pred * proj, axis=-1)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def decode_obb(box_preds, angle_preds, anchors, stride, reg_max=None):
|
| 83 |
+
"""Decode oriented boxes (cx, cy, w, h, theta) in pixel space."""
|
| 84 |
+
if reg_max is not None and box_preds.shape[-1] == 4 * reg_max and reg_max > 1:
|
| 85 |
+
box_preds = dfl_decode(box_preds, reg_max)
|
| 86 |
+
|
| 87 |
+
angle = angle_preds.reshape(-1)
|
| 88 |
+
cos_a = np.cos(angle)
|
| 89 |
+
sin_a = np.sin(angle)
|
| 90 |
+
|
| 91 |
+
lt = box_preds[:, :2]
|
| 92 |
+
rb = box_preds[:, 2:]
|
| 93 |
+
xf = (rb[:, 0] - lt[:, 0]) * 0.5
|
| 94 |
+
yf = (rb[:, 1] - lt[:, 1]) * 0.5
|
| 95 |
+
|
| 96 |
+
cx = xf * cos_a - yf * sin_a + anchors[:, 0]
|
| 97 |
+
cy = xf * sin_a + yf * cos_a + anchors[:, 1]
|
| 98 |
+
w = lt[:, 0] + rb[:, 0]
|
| 99 |
+
h = lt[:, 1] + rb[:, 1]
|
| 100 |
+
return np.stack([cx * stride, cy * stride, w * stride, h * stride, angle], axis=1)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def regularize_rbox(rboxes):
|
| 104 |
+
"""Ensure w >= h, theta in [0, pi)."""
|
| 105 |
+
rboxes = rboxes.copy()
|
| 106 |
+
swap = rboxes[:, 2] < rboxes[:, 3]
|
| 107 |
+
if np.any(swap):
|
| 108 |
+
w_old = rboxes[swap, 2].copy()
|
| 109 |
+
rboxes[swap, 2] = rboxes[swap, 3]
|
| 110 |
+
rboxes[swap, 3] = w_old
|
| 111 |
+
rboxes[swap, 4] = rboxes[swap, 4] + np.pi / 2.0
|
| 112 |
+
rboxes[:, 4] = np.mod(rboxes[:, 4], np.pi)
|
| 113 |
+
return rboxes
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def scale_rboxes_lefttop(rboxes, scale, orig_shape):
|
| 117 |
+
rboxes = rboxes.copy()
|
| 118 |
+
rboxes[:, :4] /= scale
|
| 119 |
+
rboxes[:, 0] = np.clip(rboxes[:, 0], 0, orig_shape[1])
|
| 120 |
+
rboxes[:, 1] = np.clip(rboxes[:, 1], 0, orig_shape[0])
|
| 121 |
+
return rboxes
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def rbox_to_corners(rbox):
|
| 125 |
+
cx, cy, w, h, ag = rbox
|
| 126 |
+
cos_a, sin_a = np.cos(ag), np.sin(ag)
|
| 127 |
+
wx, wy = w / 2 * cos_a, w / 2 * sin_a
|
| 128 |
+
hx, hy = -h / 2 * sin_a, h / 2 * cos_a
|
| 129 |
+
return np.array([
|
| 130 |
+
[cx - wx - hx, cy - wy - hy],
|
| 131 |
+
[cx + wx - hx, cy + wy - hy],
|
| 132 |
+
[cx + wx + hx, cy + wy + hy],
|
| 133 |
+
[cx - wx + hx, cy - wy + hy],
|
| 134 |
+
], dtype=np.float32)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def main():
|
| 138 |
+
ap = argparse.ArgumentParser(description='YOLO26-OBB Inference (AXERARuntime)')
|
| 139 |
+
ap.add_argument('--model-path', type=str, default='yolo26n-obb_1024x1024.axmodel')
|
| 140 |
+
ap.add_argument('--test-img', type=str, default='boats.jpg')
|
| 141 |
+
ap.add_argument('--img-save-path', type=str, default='result_yolo26_obb.jpg')
|
| 142 |
+
ap.add_argument('--score-thres', type=float, default=0.25)
|
| 143 |
+
ap.add_argument('--nms-thres', type=float, default=0.45)
|
| 144 |
+
ap.add_argument('--num-classes', type=int, default=15)
|
| 145 |
+
ap.add_argument('--providers', type=str, default='AxEngineExecutionProvider')
|
| 146 |
+
opt = ap.parse_args()
|
| 147 |
+
|
| 148 |
+
if not os.path.exists(opt.model_path):
|
| 149 |
+
logger.error(f"Model not found: {opt.model_path}")
|
| 150 |
+
return
|
| 151 |
+
|
| 152 |
+
t0 = time()
|
| 153 |
+
providers = [p.strip() for p in opt.providers.split(",") if p.strip()] or None
|
| 154 |
+
sess = ort.InferenceSession(opt.model_path, providers=providers)
|
| 155 |
+
logger.debug(f"\033[1;31mLoad model time = {(time() - t0) * 1000:.2f} ms\033[0m")
|
| 156 |
+
|
| 157 |
+
inp = sess.get_inputs()[0]
|
| 158 |
+
input_name = inp.name
|
| 159 |
+
m_h, m_w, layout = infer_hw_layout(inp.shape)
|
| 160 |
+
|
| 161 |
+
img = cv2.imread(opt.test_img)
|
| 162 |
+
if img is None:
|
| 163 |
+
logger.error(f"Image not found or unreadable: {opt.test_img}")
|
| 164 |
+
return
|
| 165 |
+
|
| 166 |
+
# Preprocess (left-top letterbox, gray padding)
|
| 167 |
+
t0 = time()
|
| 168 |
+
orig_h, orig_w = img.shape[:2]
|
| 169 |
+
scale = min(m_h / orig_h, m_w / orig_w)
|
| 170 |
+
new_w, new_h = int(orig_w * scale), int(orig_h * scale)
|
| 171 |
+
resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
| 172 |
+
padded = cv2.copyMakeBorder(
|
| 173 |
+
resized, 0, m_h - new_h, 0, m_w - new_w,
|
| 174 |
+
cv2.BORDER_CONSTANT, value=(127, 127, 127)
|
| 175 |
+
)
|
| 176 |
+
rgb = cv2.cvtColor(padded, cv2.COLOR_BGR2RGB)
|
| 177 |
+
if layout == "NHWC":
|
| 178 |
+
input_tensor = rgb[None, ...].astype(np.uint8)
|
| 179 |
+
else:
|
| 180 |
+
input_tensor = np.transpose(rgb, (2, 0, 1))[None, ...].astype(np.uint8)
|
| 181 |
+
logger.debug(f"\033[1;31mPre-process time = {(time() - t0) * 1000:.2f} ms\033[0m")
|
| 182 |
+
|
| 183 |
+
# Inference
|
| 184 |
+
t0 = time()
|
| 185 |
+
ort_outputs = sess.run(None, {input_name: input_tensor})
|
| 186 |
+
out_metas = sess.get_outputs()
|
| 187 |
+
logger.debug(f"\033[1;31mForward time = {(time() - t0) * 1000:.2f} ms\033[0m")
|
| 188 |
+
|
| 189 |
+
# Post-process
|
| 190 |
+
t0 = time()
|
| 191 |
+
strides = (8, 16, 32)
|
| 192 |
+
conf_raw = -np.log(1 / opt.score_thres - 1)
|
| 193 |
+
detections = [] # [cx, cy, w, h, theta, conf, cls_id]
|
| 194 |
+
|
| 195 |
+
output_items = []
|
| 196 |
+
for meta, data in zip(out_metas, ort_outputs):
|
| 197 |
+
shape = list(meta.shape)
|
| 198 |
+
if any(s is None or isinstance(s, str) for s in shape):
|
| 199 |
+
shape = list(data.shape)
|
| 200 |
+
output_items.append((data, shape))
|
| 201 |
+
|
| 202 |
+
for scale_idx, stride in enumerate(strides):
|
| 203 |
+
box_idx = scale_idx * 3
|
| 204 |
+
cls_idx = scale_idx * 3 + 1
|
| 205 |
+
ang_idx = scale_idx * 3 + 2
|
| 206 |
+
if ang_idx >= len(output_items):
|
| 207 |
+
continue
|
| 208 |
+
|
| 209 |
+
box_data, box_shape = output_items[box_idx]
|
| 210 |
+
cls_data, cls_shape = output_items[cls_idx]
|
| 211 |
+
ang_data, ang_shape = output_items[ang_idx]
|
| 212 |
+
|
| 213 |
+
H, W = box_shape[1], box_shape[2]
|
| 214 |
+
box_channels = box_shape[-1]
|
| 215 |
+
reg_max = None
|
| 216 |
+
if box_channels > 4 and box_channels % 4 == 0:
|
| 217 |
+
reg_max = box_channels // 4
|
| 218 |
+
|
| 219 |
+
box_data = box_data[0].reshape(-1, box_channels)
|
| 220 |
+
cls_data = cls_data[0].reshape(-1, cls_shape[-1])
|
| 221 |
+
ang_data = ang_data[0].reshape(-1, ang_shape[-1])
|
| 222 |
+
|
| 223 |
+
if cls_data.shape[-1] == 1:
|
| 224 |
+
cls_scores = cls_data[:, 0]
|
| 225 |
+
cls_ids = np.zeros(len(cls_scores), dtype=np.int32)
|
| 226 |
+
else:
|
| 227 |
+
cls_scores = np.max(cls_data, axis=1)
|
| 228 |
+
cls_ids = np.argmax(cls_data, axis=1)
|
| 229 |
+
|
| 230 |
+
valid = cls_scores >= conf_raw
|
| 231 |
+
if not np.any(valid):
|
| 232 |
+
continue
|
| 233 |
+
|
| 234 |
+
v_box = box_data[valid]
|
| 235 |
+
v_ang = ang_data[valid]
|
| 236 |
+
v_score = 1.0 / (1.0 + np.exp(-cls_scores[valid]))
|
| 237 |
+
v_id = cls_ids[valid]
|
| 238 |
+
|
| 239 |
+
gy, gx = np.indices((H, W))
|
| 240 |
+
anchors = np.stack((gx.ravel(), gy.ravel()), axis=-1).astype(np.float32) + 0.5
|
| 241 |
+
anchors = anchors[valid]
|
| 242 |
+
|
| 243 |
+
rboxes = decode_obb(v_box, v_ang, anchors, stride, reg_max)
|
| 244 |
+
rboxes = regularize_rbox(rboxes)
|
| 245 |
+
for i in range(len(rboxes)):
|
| 246 |
+
detections.append([*rboxes[i], v_score[i], int(v_id[i])])
|
| 247 |
+
|
| 248 |
+
logger.debug(f"\033[1;31mPost-process time = {(time() - t0) * 1000:.2f} ms\033[0m")
|
| 249 |
+
|
| 250 |
+
if len(detections) == 0:
|
| 251 |
+
logger.info("No detections found.")
|
| 252 |
+
cv2.imwrite(opt.img_save_path, img)
|
| 253 |
+
return
|
| 254 |
+
|
| 255 |
+
detections = np.array(detections, dtype=np.float32)
|
| 256 |
+
|
| 257 |
+
# Rotated NMS
|
| 258 |
+
rotated_boxes = []
|
| 259 |
+
for det in detections:
|
| 260 |
+
cx, cy, w, h, theta = det[:5]
|
| 261 |
+
rotated_boxes.append(((float(cx), float(cy)), (float(w), float(h)), float(np.degrees(theta))))
|
| 262 |
+
scores = detections[:, 5].tolist()
|
| 263 |
+
keep = cv2.dnn.NMSBoxesRotated(rotated_boxes, scores, opt.score_thres, opt.nms_thres)
|
| 264 |
+
if len(keep) == 0:
|
| 265 |
+
logger.info("No detections after NMS.")
|
| 266 |
+
cv2.imwrite(opt.img_save_path, img)
|
| 267 |
+
return
|
| 268 |
+
|
| 269 |
+
keep = np.array(keep).flatten()
|
| 270 |
+
final = detections[keep]
|
| 271 |
+
final[:, :5] = scale_rboxes_lefttop(final[:, :5], scale, (orig_h, orig_w))
|
| 272 |
+
|
| 273 |
+
logger.info(f"\033[1;32mDraw Results ({len(final)} oriented objects):\033[0m")
|
| 274 |
+
for det in final:
|
| 275 |
+
cx, cy, w, h, theta, conf, cid = det
|
| 276 |
+
cid = int(cid)
|
| 277 |
+
name = DOTA_CLASSES[cid] if cid < len(DOTA_CLASSES) else f"cls{cid}"
|
| 278 |
+
color = DOTA_COLORS[cid % len(DOTA_COLORS)]
|
| 279 |
+
logger.info(f" {name:20s} conf={conf:.2f} cx={cx:.1f} cy={cy:.1f} w={w:.1f} h={h:.1f} theta={np.degrees(theta):+.1f}deg")
|
| 280 |
+
corners = rbox_to_corners(det[:5]).astype(np.int32)
|
| 281 |
+
cv2.polylines(img, [corners], isClosed=True, color=color, thickness=2, lineType=cv2.LINE_AA)
|
| 282 |
+
label = f"{name} {conf:.2f}"
|
| 283 |
+
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
|
| 284 |
+
x_text, y_text = int(corners[0][0]), max(0, int(corners[0][1]) - 5)
|
| 285 |
+
cv2.rectangle(img, (x_text, y_text - th - 2), (x_text + tw + 2, y_text + 2), color, -1)
|
| 286 |
+
cv2.putText(img, label, (x_text + 1, y_text - 1),
|
| 287 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
|
| 288 |
+
|
| 289 |
+
cv2.imwrite(opt.img_save_path, img)
|
| 290 |
+
logger.info(f"Saved to {opt.img_save_path}")
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
if __name__ == "__main__":
|
| 294 |
+
main()
|
boats.jpg
ADDED
|
Git LFS Details
|
config.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "ONNX",
|
| 3 |
+
"npu_mode": "NPU3",
|
| 4 |
+
"quant": {
|
| 5 |
+
"input_configs": [
|
| 6 |
+
{
|
| 7 |
+
"tensor_name": "images",
|
| 8 |
+
"calibration_dataset": "../dota_cali.tar",
|
| 9 |
+
"calibration_size": 15,
|
| 10 |
+
"calibration_mean": [0, 0, 0],
|
| 11 |
+
"calibration_std": [255.0, 255.0, 255.0]
|
| 12 |
+
}
|
| 13 |
+
],
|
| 14 |
+
"calibration_method": "MinMax",
|
| 15 |
+
"precision_analysis": true,
|
| 16 |
+
"precision_analysis_method":"EndToEnd"
|
| 17 |
+
},
|
| 18 |
+
"input_processors": [
|
| 19 |
+
{
|
| 20 |
+
"tensor_name": "images",
|
| 21 |
+
"tensor_format": "BGR",
|
| 22 |
+
"src_format": "BGR",
|
| 23 |
+
"src_dtype": "U8",
|
| 24 |
+
"src_layout": "NHWC"
|
| 25 |
+
}
|
| 26 |
+
],
|
| 27 |
+
"output_processors": [
|
| 28 |
+
],
|
| 29 |
+
"compiler": {
|
| 30 |
+
"check": 0
|
| 31 |
+
}
|
| 32 |
+
}
|
dota_cali.tar
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:af11ce2d4668a0e0cc08fa42968ab0d1a1fb0cbf57ac8109597853de104d31ff
|
| 3 |
+
size 2621440
|
export_onnx.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# YOLO26-OBB Export ONNX Model Script
|
| 4 |
+
#
|
| 5 |
+
# Copyright (c) 2025, AXERA Semiconductor Co., Ltd. All rights reserved.
|
| 6 |
+
#
|
| 7 |
+
# Licensed under the BSD 3-Clause License (the "License"); you may not use
|
| 8 |
+
# this file except in compliance with the License. You may obtain a copy of
|
| 9 |
+
# the License at
|
| 10 |
+
#
|
| 11 |
+
# https://opensource.org/licenses/BSD-3-Clause
|
| 12 |
+
#
|
| 13 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 14 |
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
| 15 |
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
| 16 |
+
# License for the specific language governing permissions and limitations
|
| 17 |
+
# under the License.
|
| 18 |
+
# =============================================================================
|
| 19 |
+
#
|
| 20 |
+
# Author: GUOFANGMING
|
| 21 |
+
#
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
from ultralytics import YOLO
|
| 25 |
+
from ultralytics.nn.modules.head import OBB26
|
| 26 |
+
import os
|
| 27 |
+
import shutil
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def npu_obb26_forward(self, x):
|
| 31 |
+
"""
|
| 32 |
+
YOLO26 OBB26 Head Modified for NPU.
|
| 33 |
+
|
| 34 |
+
OBB26 structure:
|
| 35 |
+
- cv2 / one2one_cv2: box regression layers (output 4*reg_max channels, reg_max=1 -> 4)
|
| 36 |
+
- cv3 / one2one_cv3: classification layers (output nc channels, nc=15 for DOTAv1)
|
| 37 |
+
- cv4 / one2one_cv4: angle regression layers (output ne channels, ne=1)
|
| 38 |
+
|
| 39 |
+
Note:
|
| 40 |
+
OBB26 outputs raw angle predictions (in radians) without sigmoid transformation,
|
| 41 |
+
which is different from the original OBB head where the angle is wrapped via
|
| 42 |
+
``(angle.sigmoid() - 0.25) * pi`` in ``forward_head``. The post-process therefore
|
| 43 |
+
consumes the raw angle directly via ``cos(angle)`` / ``sin(angle)``.
|
| 44 |
+
|
| 45 |
+
Output:
|
| 46 |
+
List of Tensors (9 items: 3 scales * 3 outputs), Layout: NHWC for all branches.
|
| 47 |
+
For each scale:
|
| 48 |
+
- Box_Raw (B, H, W, 4*reg_max), <-- bbox regression (ltrb distance, reg_max=1)
|
| 49 |
+
- Cls_Raw (B, H, W, nc), <-- class scores (raw logits)
|
| 50 |
+
- Angle_Raw (B, H, W, ne), <-- rotation angle (raw radians, ne=1)
|
| 51 |
+
"""
|
| 52 |
+
if not isinstance(x, (list, tuple)):
|
| 53 |
+
x = [x]
|
| 54 |
+
|
| 55 |
+
res = []
|
| 56 |
+
|
| 57 |
+
# Box / Cls use one2one branch (end2end mode) when available
|
| 58 |
+
if hasattr(self, 'one2one_cv2') and hasattr(self, 'one2one_cv3'):
|
| 59 |
+
box_layers = self.one2one_cv2
|
| 60 |
+
cls_layers = self.one2one_cv3
|
| 61 |
+
else:
|
| 62 |
+
box_layers = self.cv2
|
| 63 |
+
cls_layers = self.cv3
|
| 64 |
+
|
| 65 |
+
# Angle uses one2one_cv4 in end2end mode
|
| 66 |
+
if hasattr(self, 'one2one_cv4'):
|
| 67 |
+
angle_layers = self.one2one_cv4
|
| 68 |
+
else:
|
| 69 |
+
angle_layers = self.cv4
|
| 70 |
+
|
| 71 |
+
for i in range(self.nl):
|
| 72 |
+
# 1. Box branch - NHWC (4 * reg_max channels, reg_max=1 -> 4)
|
| 73 |
+
bboxes = box_layers[i](x[i]).permute(0, 2, 3, 1)
|
| 74 |
+
|
| 75 |
+
# 2. Cls branch - NHWC (nc channels, raw logits)
|
| 76 |
+
scores = cls_layers[i](x[i]).permute(0, 2, 3, 1)
|
| 77 |
+
|
| 78 |
+
# 3. Angle branch - NHWC (ne channels, raw radians)
|
| 79 |
+
angle = angle_layers[i](x[i]).permute(0, 2, 3, 1)
|
| 80 |
+
|
| 81 |
+
res.append(bboxes)
|
| 82 |
+
res.append(scores)
|
| 83 |
+
res.append(angle)
|
| 84 |
+
|
| 85 |
+
return res
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def batch_export_yolo26_obb():
|
| 89 |
+
variants = ['n', 's', 'm', 'l', 'x']
|
| 90 |
+
imgsz = 1024 # YOLO26-OBB officially uses 1024x1024 (DOTAv1 pretrained)
|
| 91 |
+
|
| 92 |
+
# Execute Monkey Patch
|
| 93 |
+
OBB26.forward = npu_obb26_forward
|
| 94 |
+
print("Monkey patch applied for OBB26: Output Layout forced to NHWC (Box, Cls, Angle for each scale).")
|
| 95 |
+
|
| 96 |
+
for v in variants:
|
| 97 |
+
model_name = f"yolo26{v}-obb"
|
| 98 |
+
pt_path = f"{model_name}.pt"
|
| 99 |
+
onnx_final_name = f"{model_name}_{imgsz}x{imgsz}.onnx"
|
| 100 |
+
print(f"\n--- Processing {model_name} ---")
|
| 101 |
+
try:
|
| 102 |
+
# Load model
|
| 103 |
+
model = YOLO(pt_path)
|
| 104 |
+
|
| 105 |
+
# Reapply monkey patch
|
| 106 |
+
OBB26.forward = npu_obb26_forward
|
| 107 |
+
|
| 108 |
+
# Ensure the model's head also uses the new forward
|
| 109 |
+
if hasattr(model.model, 'model') and len(model.model.model) > 0:
|
| 110 |
+
head = model.model.model[-1]
|
| 111 |
+
if isinstance(head, OBB26):
|
| 112 |
+
head.forward = lambda x: npu_obb26_forward(head, x)
|
| 113 |
+
|
| 114 |
+
# Execute export
|
| 115 |
+
exported_path = model.export(
|
| 116 |
+
format="onnx",
|
| 117 |
+
imgsz=imgsz,
|
| 118 |
+
dynamic=False,
|
| 119 |
+
opset=11,
|
| 120 |
+
simplify=True,
|
| 121 |
+
nms=False
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Move and rename
|
| 125 |
+
if exported_path:
|
| 126 |
+
shutil.move(exported_path, onnx_final_name)
|
| 127 |
+
print(f"Success: {onnx_final_name}")
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f"Failed to export {model_name}: {e}")
|
| 130 |
+
import traceback
|
| 131 |
+
traceback.print_exc()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
batch_export_yolo26_obb()
|
onnx_infer.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# YOLO26-OBB Inference Script (ONNXRuntime)
|
| 4 |
+
#
|
| 5 |
+
# Copyright (c) 2025, AXERA Semiconductor Co., Ltd. All rights reserved.
|
| 6 |
+
#
|
| 7 |
+
# Licensed under the BSD 3-Clause License (the "License"); you may not use
|
| 8 |
+
# this file except in compliance with the License. You may obtain a copy of
|
| 9 |
+
# the License at
|
| 10 |
+
#
|
| 11 |
+
# https://opensource.org/licenses/BSD-3-Clause
|
| 12 |
+
#
|
| 13 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 14 |
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
| 15 |
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
| 16 |
+
# License for the specific language governing permissions and limitations
|
| 17 |
+
# under the License.
|
| 18 |
+
# =============================================================================
|
| 19 |
+
#
|
| 20 |
+
# Author: GUOFANGMING
|
| 21 |
+
#
|
| 22 |
+
|
| 23 |
+
import onnxruntime as ort
|
| 24 |
+
import cv2
|
| 25 |
+
import numpy as np
|
| 26 |
+
import argparse
|
| 27 |
+
import os
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# DOTAv1 class names (15 classes), order matches Ultralytics DOTAv1.yaml
|
| 31 |
+
DOTA_CLASSES = [
|
| 32 |
+
"plane", "ship", "storage tank", "baseball diamond", "tennis court",
|
| 33 |
+
"basketball court", "ground track field", "harbor", "bridge",
|
| 34 |
+
"large vehicle", "small vehicle", "helicopter", "roundabout",
|
| 35 |
+
"soccer ball field", "swimming pool"
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
# Distinct BGR colors for the 15 DOTA categories
|
| 39 |
+
DOTA_COLORS = [
|
| 40 |
+
(255, 56, 56), (255, 159, 56), (255, 207, 56), (180, 255, 56),
|
| 41 |
+
(102, 255, 56), (56, 255, 122), (56, 255, 207), (56, 207, 255),
|
| 42 |
+
(56, 122, 255), (102, 56, 255), (180, 56, 255), (255, 56, 207),
|
| 43 |
+
(255, 56, 122), (200, 200, 200), (128, 128, 255),
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def preprocess_image(image, input_size=(1024, 1024)):
|
| 48 |
+
"""Letterbox preprocess (left-top aligned).
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
image: BGR image (H, W, C)
|
| 52 |
+
input_size: (height, width) target size (default 1024x1024 for OBB)
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
input_tensor: (1, 3, H, W) float32 normalized to [0, 1]
|
| 56 |
+
scale: scalar resize ratio
|
| 57 |
+
original_shape: (orig_h, orig_w)
|
| 58 |
+
"""
|
| 59 |
+
orig_h, orig_w = image.shape[:2]
|
| 60 |
+
m_h, m_w = input_size
|
| 61 |
+
scale = min(m_h / orig_h, m_w / orig_w)
|
| 62 |
+
new_w, new_h = int(orig_w * scale), int(orig_h * scale)
|
| 63 |
+
img_resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
| 64 |
+
input_bgr = cv2.copyMakeBorder(
|
| 65 |
+
img_resized, 0, m_h - new_h, 0, m_w - new_w,
|
| 66 |
+
cv2.BORDER_CONSTANT, value=(114, 114, 114)
|
| 67 |
+
)
|
| 68 |
+
input_rgb = cv2.cvtColor(input_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
|
| 69 |
+
input_tensor = np.transpose(input_rgb, (2, 0, 1))[None, ...]
|
| 70 |
+
return input_tensor, scale, (orig_h, orig_w)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def softmax(x, axis=-1):
|
| 74 |
+
e_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
|
| 75 |
+
return e_x / np.sum(e_x, axis=axis, keepdims=True)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def dfl_decode(box_pred, reg_max):
|
| 79 |
+
"""Decode DFL box predictions of shape (N, 4*reg_max) -> (N, 4)."""
|
| 80 |
+
N = box_pred.shape[0]
|
| 81 |
+
box_pred = box_pred.reshape(N, 4, reg_max)
|
| 82 |
+
box_pred = softmax(box_pred, axis=-1)
|
| 83 |
+
proj = np.arange(reg_max, dtype=np.float32)
|
| 84 |
+
return np.sum(box_pred * proj, axis=-1)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def decode_obb(box_preds, angle_preds, anchors, stride, reg_max=None):
|
| 88 |
+
"""Decode oriented bounding boxes from predictions.
|
| 89 |
+
|
| 90 |
+
Mirrors ``ultralytics.utils.tal.dist2rbox``:
|
| 91 |
+
lt, rb = box[:2], box[2:]
|
| 92 |
+
cos, sin = cos(angle), sin(angle)
|
| 93 |
+
xf, yf = (rb - lt) / 2
|
| 94 |
+
x = xf*cos - yf*sin + anchor_x
|
| 95 |
+
y = xf*sin + yf*cos + anchor_y
|
| 96 |
+
w = l + r, h = t + b
|
| 97 |
+
(xy, wh) are in feature units, then multiplied by stride to get pixels.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
box_preds: (N, 4) or (N, 4*reg_max) raw ltrb distances (in feature units).
|
| 101 |
+
angle_preds: (N, 1) or (N,) raw angle in radians (no sigmoid applied).
|
| 102 |
+
anchors: (N, 2) - feature-space anchor centers (gx+0.5, gy+0.5).
|
| 103 |
+
stride: scalar stride for the current scale.
|
| 104 |
+
reg_max: if not None, apply DFL to box_preds first.
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
rboxes: (N, 5) array in [cx, cy, w, h, theta] format (pixel units, theta in radians).
|
| 108 |
+
"""
|
| 109 |
+
if reg_max is not None and box_preds.shape[-1] == 4 * reg_max and reg_max > 1:
|
| 110 |
+
box_preds = dfl_decode(box_preds, reg_max)
|
| 111 |
+
|
| 112 |
+
angle = angle_preds.reshape(-1) # raw radians
|
| 113 |
+
cos_a = np.cos(angle)
|
| 114 |
+
sin_a = np.sin(angle)
|
| 115 |
+
|
| 116 |
+
lt = box_preds[:, :2]
|
| 117 |
+
rb = box_preds[:, 2:]
|
| 118 |
+
xf = (rb[:, 0] - lt[:, 0]) * 0.5
|
| 119 |
+
yf = (rb[:, 1] - lt[:, 1]) * 0.5
|
| 120 |
+
|
| 121 |
+
cx = xf * cos_a - yf * sin_a + anchors[:, 0]
|
| 122 |
+
cy = xf * sin_a + yf * cos_a + anchors[:, 1]
|
| 123 |
+
w = lt[:, 0] + rb[:, 0]
|
| 124 |
+
h = lt[:, 1] + rb[:, 1]
|
| 125 |
+
|
| 126 |
+
rboxes = np.stack([cx * stride, cy * stride, w * stride, h * stride, angle], axis=1)
|
| 127 |
+
return rboxes
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def regularize_rbox(rboxes):
|
| 131 |
+
"""Regularize rotated boxes: ensure w >= h and angle in [0, pi).
|
| 132 |
+
|
| 133 |
+
Matches Ultralytics' OBB convention: longer side is treated as width and
|
| 134 |
+
rotation is mapped into [0, pi). This makes IoU comparisons consistent.
|
| 135 |
+
"""
|
| 136 |
+
rboxes = rboxes.copy()
|
| 137 |
+
swap = rboxes[:, 2] < rboxes[:, 3]
|
| 138 |
+
if np.any(swap):
|
| 139 |
+
w_old = rboxes[swap, 2].copy()
|
| 140 |
+
rboxes[swap, 2] = rboxes[swap, 3]
|
| 141 |
+
rboxes[swap, 3] = w_old
|
| 142 |
+
rboxes[swap, 4] = rboxes[swap, 4] + np.pi / 2.0
|
| 143 |
+
rboxes[:, 4] = np.mod(rboxes[:, 4], np.pi)
|
| 144 |
+
return rboxes
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def scale_rboxes_lefttop(rboxes, scale, orig_shape):
|
| 148 |
+
"""Scale rotated boxes from network input back to original image."""
|
| 149 |
+
rboxes = rboxes.copy()
|
| 150 |
+
rboxes[:, :4] /= scale
|
| 151 |
+
rboxes[:, 0] = np.clip(rboxes[:, 0], 0, orig_shape[1])
|
| 152 |
+
rboxes[:, 1] = np.clip(rboxes[:, 1], 0, orig_shape[0])
|
| 153 |
+
return rboxes
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def rbox_to_corners(rbox):
|
| 157 |
+
"""Convert (cx, cy, w, h, theta) to 4 corner points (x, y) in image space."""
|
| 158 |
+
cx, cy, w, h, ag = rbox
|
| 159 |
+
cos_a, sin_a = np.cos(ag), np.sin(ag)
|
| 160 |
+
wx, wy = w / 2 * cos_a, w / 2 * sin_a
|
| 161 |
+
hx, hy = -h / 2 * sin_a, h / 2 * cos_a
|
| 162 |
+
p1 = (cx - wx - hx, cy - wy - hy)
|
| 163 |
+
p2 = (cx + wx - hx, cy + wy - hy)
|
| 164 |
+
p3 = (cx + wx + hx, cy + wy + hy)
|
| 165 |
+
p4 = (cx - wx + hx, cy - wy + hy)
|
| 166 |
+
return np.array([p1, p2, p3, p4], dtype=np.float32)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def main():
|
| 170 |
+
parser = argparse.ArgumentParser(description='YOLO26-OBB ONNX Inference')
|
| 171 |
+
parser.add_argument('-m', '--model', type=str, default='yolo26n-obb_1024x1024.onnx',
|
| 172 |
+
dest='model_path', help='Path to YOLO26-OBB *.onnx Model.')
|
| 173 |
+
parser.add_argument('-i', '--img', type=str, default='boats.jpg',
|
| 174 |
+
dest='test_img', help='Path to Test Image.')
|
| 175 |
+
parser.add_argument('-o', '--output', type=str, default='result_yolo26_obb.jpg',
|
| 176 |
+
dest='img_save_path', help='Path to Save Result Image.')
|
| 177 |
+
parser.add_argument('--score-thres', type=float, default=0.25,
|
| 178 |
+
help='Confidence threshold.')
|
| 179 |
+
parser.add_argument('--nms-thres', type=float, default=0.45,
|
| 180 |
+
help='IoU threshold for rotated NMS.')
|
| 181 |
+
parser.add_argument('--num-classes', type=int, default=15,
|
| 182 |
+
help='Number of classes (DOTAv1: 15).')
|
| 183 |
+
opt = parser.parse_args()
|
| 184 |
+
|
| 185 |
+
if not os.path.exists(opt.model_path):
|
| 186 |
+
print(f"Error: Model not found: {opt.model_path}")
|
| 187 |
+
return
|
| 188 |
+
if not os.path.exists(opt.test_img):
|
| 189 |
+
print(f"Error: Image not found: {opt.test_img}")
|
| 190 |
+
return
|
| 191 |
+
|
| 192 |
+
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
|
| 193 |
+
try:
|
| 194 |
+
session = ort.InferenceSession(opt.model_path, providers=providers)
|
| 195 |
+
except Exception:
|
| 196 |
+
session = ort.InferenceSession(opt.model_path, providers=['CPUExecutionProvider'])
|
| 197 |
+
|
| 198 |
+
input_name = session.get_inputs()[0].name
|
| 199 |
+
output_names = [o.name for o in session.get_outputs()]
|
| 200 |
+
input_shape = session.get_inputs()[0].shape
|
| 201 |
+
imgsz = (int(input_shape[2]), int(input_shape[3]))
|
| 202 |
+
|
| 203 |
+
img0 = cv2.imread(opt.test_img)
|
| 204 |
+
if img0 is None:
|
| 205 |
+
print(f"Error: Cannot read image: {opt.test_img}")
|
| 206 |
+
return
|
| 207 |
+
|
| 208 |
+
img, scale, orig_shape = preprocess_image(img0.copy(), imgsz)
|
| 209 |
+
|
| 210 |
+
outputs = session.run(output_names, {input_name: img})
|
| 211 |
+
|
| 212 |
+
# Post-process: 9 outputs (3 scales * (box, cls, angle))
|
| 213 |
+
strides = [8, 16, 32]
|
| 214 |
+
conf_raw = -np.log(1 / opt.score_thres - 1)
|
| 215 |
+
detections = [] # each: [cx, cy, w, h, theta, conf, cls_id]
|
| 216 |
+
|
| 217 |
+
for scale_idx, stride in enumerate(strides):
|
| 218 |
+
box_idx = scale_idx * 3
|
| 219 |
+
cls_idx = scale_idx * 3 + 1
|
| 220 |
+
ang_idx = scale_idx * 3 + 2
|
| 221 |
+
if ang_idx >= len(outputs):
|
| 222 |
+
continue
|
| 223 |
+
|
| 224 |
+
box_data = outputs[box_idx] # (1, H, W, 4*reg_max)
|
| 225 |
+
cls_data = outputs[cls_idx] # (1, H, W, nc)
|
| 226 |
+
ang_data = outputs[ang_idx] # (1, H, W, 1)
|
| 227 |
+
|
| 228 |
+
H, W = box_data.shape[1:3]
|
| 229 |
+
box_channels = box_data.shape[-1]
|
| 230 |
+
reg_max = None
|
| 231 |
+
if box_channels > 4 and box_channels % 4 == 0:
|
| 232 |
+
reg_max = box_channels // 4
|
| 233 |
+
|
| 234 |
+
box_data = box_data[0].reshape(-1, box_channels)
|
| 235 |
+
cls_data = cls_data[0].reshape(-1, cls_data.shape[-1])
|
| 236 |
+
ang_data = ang_data[0].reshape(-1, ang_data.shape[-1])
|
| 237 |
+
|
| 238 |
+
if cls_data.shape[-1] == 1:
|
| 239 |
+
cls_scores = cls_data[:, 0]
|
| 240 |
+
cls_ids = np.zeros(len(cls_scores), dtype=np.int32)
|
| 241 |
+
else:
|
| 242 |
+
cls_scores = np.max(cls_data, axis=1)
|
| 243 |
+
cls_ids = np.argmax(cls_data, axis=1)
|
| 244 |
+
|
| 245 |
+
valid = cls_scores >= conf_raw
|
| 246 |
+
if not np.any(valid):
|
| 247 |
+
continue
|
| 248 |
+
|
| 249 |
+
v_box = box_data[valid]
|
| 250 |
+
v_ang = ang_data[valid]
|
| 251 |
+
v_score = 1.0 / (1.0 + np.exp(-cls_scores[valid]))
|
| 252 |
+
v_id = cls_ids[valid]
|
| 253 |
+
|
| 254 |
+
gy, gx = np.indices((H, W))
|
| 255 |
+
anchors = np.stack((gx.ravel(), gy.ravel()), axis=-1).astype(np.float32) + 0.5
|
| 256 |
+
anchors = anchors[valid]
|
| 257 |
+
|
| 258 |
+
rboxes = decode_obb(v_box, v_ang, anchors, stride, reg_max)
|
| 259 |
+
rboxes = regularize_rbox(rboxes)
|
| 260 |
+
|
| 261 |
+
for i in range(len(rboxes)):
|
| 262 |
+
detections.append([*rboxes[i], v_score[i], int(v_id[i])])
|
| 263 |
+
|
| 264 |
+
if len(detections) == 0:
|
| 265 |
+
print("No detections found.")
|
| 266 |
+
cv2.imwrite(opt.img_save_path, img0)
|
| 267 |
+
return
|
| 268 |
+
|
| 269 |
+
detections = np.array(detections, dtype=np.float32)
|
| 270 |
+
|
| 271 |
+
# Rotated NMS: cv2.dnn.NMSBoxesRotated takes ((cx, cy), (w, h), angle_deg).
|
| 272 |
+
rotated_boxes = []
|
| 273 |
+
for det in detections:
|
| 274 |
+
cx, cy, w, h, theta = det[:5]
|
| 275 |
+
rotated_boxes.append(((float(cx), float(cy)), (float(w), float(h)), float(np.degrees(theta))))
|
| 276 |
+
scores = detections[:, 5].tolist()
|
| 277 |
+
keep = cv2.dnn.NMSBoxesRotated(rotated_boxes, scores, opt.score_thres, opt.nms_thres)
|
| 278 |
+
if len(keep) == 0:
|
| 279 |
+
print("No detections after NMS.")
|
| 280 |
+
cv2.imwrite(opt.img_save_path, img0)
|
| 281 |
+
return
|
| 282 |
+
|
| 283 |
+
keep = np.array(keep).flatten()
|
| 284 |
+
final = detections[keep]
|
| 285 |
+
final[:, :5] = scale_rboxes_lefttop(final[:, :5], scale, orig_shape)
|
| 286 |
+
|
| 287 |
+
print(f"Done! Found {len(final)} oriented objects.")
|
| 288 |
+
for det in final:
|
| 289 |
+
cx, cy, w, h, theta, conf, cid = det
|
| 290 |
+
cid = int(cid)
|
| 291 |
+
name = DOTA_CLASSES[cid] if cid < len(DOTA_CLASSES) else f"cls{cid}"
|
| 292 |
+
color = DOTA_COLORS[cid % len(DOTA_COLORS)]
|
| 293 |
+
print(f" {name:20s} conf={conf:.2f} cx={cx:.1f} cy={cy:.1f} w={w:.1f} h={h:.1f} theta={np.degrees(theta):+.1f} deg")
|
| 294 |
+
corners = rbox_to_corners(det[:5]).astype(np.int32)
|
| 295 |
+
cv2.polylines(img0, [corners], isClosed=True, color=color, thickness=2, lineType=cv2.LINE_AA)
|
| 296 |
+
label = f"{name} {conf:.2f}"
|
| 297 |
+
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
|
| 298 |
+
x_text, y_text = int(corners[0][0]), max(0, int(corners[0][1]) - 5)
|
| 299 |
+
cv2.rectangle(img0, (x_text, y_text - th - 2), (x_text + tw + 2, y_text + 2), color, -1)
|
| 300 |
+
cv2.putText(img0, label, (x_text + 1, y_text - 1),
|
| 301 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
|
| 302 |
+
|
| 303 |
+
cv2.imwrite(opt.img_save_path, img0)
|
| 304 |
+
print(f"Result saved to {opt.img_save_path}")
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
if __name__ == "__main__":
|
| 308 |
+
main()
|
result_yolo26_obb.jpg
ADDED
|
Git LFS Details
|
yolo26l-obb_1024x1024.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6f538f74fe43905154f3e737e7d08686fb85e448819b98d4f98e97250dc6f9fe
|
| 3 |
+
size 102621428
|
yolo26m-obb_1024x1024.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9b22bded2093750fee2a7ba460ab4eea95564e1064fb773b68b6d8ea085b2c5d
|
| 3 |
+
size 84971005
|
yolo26n-obb_1024x1024.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9540624d3d2a0c5094541bb5bf4b0ba79161b7b5828cd7ad982638a10bb8c425
|
| 3 |
+
size 9917917
|
yolo26s-obb_1024x1024.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2721ca7d52bd73ecea72ef6989397d684f781460a2aa8ba300f64764a9cd8563
|
| 3 |
+
size 39148849
|
yolo26x-obb_1024x1024.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2a941f8fadd3c1ebc081a41225c75ccd8f8401195762f5c689ca4013a633bfcf
|
| 3 |
+
size 230436254
|