Fangming Guo commited on
Commit
103e15d
·
verified ·
1 Parent(s): 6839126

Upload 12 files

Browse files
.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

  • SHA256: 8c5ada657cf8110a9f8aaac954c1dd96cde0187315b581276c32b0d1863e756f
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB
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

  • SHA256: 08a335427ce666b98b42a7e791379dd85bd5cdd80e8dda85f105d71b2cfca41c
  • Pointer size: 131 Bytes
  • Size of remote file: 742 kB
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