import os import cv2 import math import numpy as np from time import time import argparse import logging import axengine as ort logging.basicConfig( level=logging.DEBUG, format='[%(name)s] [%(asctime)s.%(msecs)03d] [%(levelname)s] %(message)s', datefmt='%H:%M:%S' ) logger = logging.getLogger("YOLO26-OBB") DOTA_CLASSES = [ "plane", "ship", "storage tank", "baseball diamond", "tennis court", "basketball court", "ground track field", "harbor", "bridge", "large vehicle", "small vehicle", "helicopter", "roundabout", "soccer ball field", "swimming pool" ] DOTA_COLORS = [ (255, 56, 56), (255, 159, 56), (255, 207, 56), (180, 255, 56), (102, 255, 56), (56, 255, 122), (56, 255, 207), (56, 207, 255), (56, 122, 255), (102, 56, 255), (180, 56, 255), (255, 56, 207), (255, 56, 122), (200, 200, 200), (128, 128, 255), ] def infer_hw_layout(shape): """Model input (NCHW or NHWC) -> H, W, layout name.""" shape = list(shape) if len(shape) == 4 and shape[-1] == 3: h = int(shape[1] or 1024) w = int(shape[2] or 1024) return h, w, "NHWC" if len(shape) == 4 and shape[1] == 3: h = int(shape[2] or 1024) w = int(shape[3] or 1024) return h, w, "NCHW" return 1024, 1024, "NCHW" def preprocess_image(image, input_size=(1024, 1024), layout="NCHW", padding_value=114): """LetterBox + BGR→RGB; uint8 NHWC or NCHW (same geometry as onnx_infer letterbox).""" orig_h, orig_w = image.shape[:2] new_h, new_w = input_size r = min(new_h / orig_h, new_w / orig_w) new_unpad_w = round(orig_w * r) new_unpad_h = round(orig_h * r) dw = new_w - new_unpad_w dh = new_h - new_unpad_h dw /= 2.0 dh /= 2.0 if (orig_w, orig_h) != (new_unpad_w, new_unpad_h): image = cv2.resize(image, (new_unpad_w, new_unpad_h), interpolation=cv2.INTER_LINEAR) top = round(dh - 0.1) bottom = round(dh + 0.1) left = round(dw - 0.1) right = round(dw + 0.1) padded = cv2.copyMakeBorder( image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(padding_value,) * 3, ) ratio_pad = (r, (left, top)) rgb = cv2.cvtColor(padded, cv2.COLOR_BGR2RGB) if layout == "NHWC": tensor = rgb[None, ...].astype(np.uint8) else: tensor = np.transpose(rgb, (2, 0, 1))[None, ...].astype(np.uint8) return tensor, ratio_pad, (orig_h, orig_w) def softmax(x, axis=-1): 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): 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_obb(box_preds, angle_preds, anchors, stride, reg_max=None): """dist2rbox -> xywhr in pixels.""" if reg_max is not None and box_preds.shape[-1] == 4 * reg_max and reg_max > 1: box_preds = dfl_decode(box_preds, reg_max) angle = angle_preds.reshape(-1) cos_a = np.cos(angle) sin_a = np.sin(angle) lt = box_preds[:, :2] rb = box_preds[:, 2:] xf = (rb[:, 0] - lt[:, 0]) * 0.5 yf = (rb[:, 1] - lt[:, 1]) * 0.5 cx = xf * cos_a - yf * sin_a + anchors[:, 0] cy = xf * sin_a + yf * cos_a + anchors[:, 1] w = lt[:, 0] + rb[:, 0] h = lt[:, 1] + rb[:, 1] return np.stack([cx * stride, cy * stride, w * stride, h * stride, angle], axis=1) def _get_covariance_matrix(boxes): a = (boxes[:, 2] ** 2) / 12.0 b = (boxes[:, 3] ** 2) / 12.0 c = boxes[:, 4] cos = np.cos(c) sin = np.sin(c) cos2 = cos * cos sin2 = sin * sin return a * cos2 + b * sin2, a * sin2 + b * cos2, (a - b) * cos * sin def batch_probiou(obb1, obb2, eps=1e-7): x1 = obb1[:, 0:1] y1 = obb1[:, 1:2] x2 = obb2[:, 0][None, :] y2 = obb2[:, 1][None, :] a1, b1, c1 = (v[:, None] for v in _get_covariance_matrix(obb1)) a2_full, b2_full, c2_full = _get_covariance_matrix(obb2) a2 = a2_full[None, :] b2 = b2_full[None, :] c2 = c2_full[None, :] sum_ab = (a1 + a2) * (b1 + b2) - (c1 + c2) ** 2 t1 = ((a1 + a2) * (y1 - y2) ** 2 + (b1 + b2) * (x1 - x2) ** 2) / (sum_ab + eps) * 0.25 t2 = ((c1 + c2) * (x2 - x1) * (y1 - y2)) / (sum_ab + eps) * 0.5 inner = ((a1 * b1 - c1 ** 2).clip(min=0.0) * (a2 * b2 - c2 ** 2).clip(min=0.0)) t3 = np.log(sum_ab / (4.0 * np.sqrt(inner) + eps) + eps) * 0.5 bd = np.clip(t1 + t2 + t3, eps, 100.0) hd = np.sqrt(1.0 - np.exp(-bd) + eps) return 1.0 - hd def nms_rotated_probiou(rboxes, scores, classes, iou_thres, max_wh=7680.0, agnostic=False): """Fast NMS with probiou.""" if rboxes.size == 0: return np.empty((0,), dtype=np.int64) boxes = rboxes.copy() if not agnostic: offset = classes.astype(np.float32) * float(max_wh) boxes[:, 0] = boxes[:, 0] + offset boxes[:, 1] = boxes[:, 1] + offset order = np.argsort(-scores) sorted_boxes = boxes[order] ious = batch_probiou(sorted_boxes, sorted_boxes) n = sorted_boxes.shape[0] triu = np.triu(np.ones((n, n), dtype=bool), k=1) ious = ious * triu keep_mask = (ious >= iou_thres).sum(axis=0) <= 0 return order[keep_mask] def scale_rboxes_lefttop(rboxes, ratio_pad, orig_shape): """Letterbox inverse for xywhr centers.""" rboxes = rboxes.copy() gain, (pad_x, pad_y) = ratio_pad rboxes[:, 0] -= pad_x rboxes[:, 1] -= pad_y rboxes[:, :4] /= gain rboxes[:, 0] = np.clip(rboxes[:, 0], 0, orig_shape[1]) rboxes[:, 1] = np.clip(rboxes[:, 1], 0, orig_shape[0]) return rboxes def regularize_rbox(rboxes): """Theta in [0, pi/2); swap w/h when needed.""" rboxes = rboxes.copy() t_mod = np.mod(rboxes[:, 4], np.pi) swap = t_mod >= (np.pi / 2.0) if np.any(swap): w_old = rboxes[swap, 2].copy() rboxes[swap, 2] = rboxes[swap, 3] rboxes[swap, 3] = w_old rboxes[:, 4] = np.mod(rboxes[:, 4], np.pi / 2.0) return rboxes def rbox_to_corners(rbox): cx, cy, w, h, ag = rbox cos_a, sin_a = math.cos(ag), math.sin(ag) wx, wy = w / 2.0 * cos_a, w / 2.0 * sin_a hx, hy = -h / 2.0 * sin_a, h / 2.0 * cos_a return np.array([ [cx - wx - hx, cy - wy - hy], [cx + wx - hx, cy + wy - hy], [cx + wx + hx, cy + wy + hy], [cx - wx + hx, cy - wy + hy], ], dtype=np.float32) def main(): ap = argparse.ArgumentParser(description='YOLO26-OBB Inference (AXERARuntime)') ap.add_argument('--model-path', type=str, default='yolo26n-obb_1024x1024.axmodel') ap.add_argument('--test-img', type=str, default='boats.jpg') ap.add_argument('--img-save-path', type=str, default='result_yolo26_obb.jpg') ap.add_argument('--score-thres', type=float, default=0.25) ap.add_argument('--nms-thres', type=float, default=0.45) ap.add_argument('--num-classes', type=int, default=15) ap.add_argument('--max-det', type=int, default=300) ap.add_argument('--agnostic-nms', action='store_true', help='Class-agnostic NMS.') ap.add_argument('--providers', type=str, default='AxEngineExecutionProvider') opt = ap.parse_args() if not os.path.exists(opt.model_path): logger.error(f"Model not found: {opt.model_path}") return if not os.path.exists(opt.test_img): logger.error(f"Image not found: {opt.test_img}") return t0 = time() providers = [p.strip() for p in opt.providers.split(",") if p.strip()] or None sess = ort.InferenceSession(opt.model_path, providers=providers) logger.debug(f"\033[1;31mLoad model time = {(time() - t0) * 1000:.2f} ms\033[0m") inp = sess.get_inputs()[0] input_name = inp.name m_h, m_w, layout = infer_hw_layout(inp.shape) img0 = cv2.imread(opt.test_img) if img0 is None: logger.error(f"Cannot read image: {opt.test_img}") return t0 = time() input_tensor, ratio_pad, orig_shape = preprocess_image( img0.copy(), input_size=(m_h, m_w), layout=layout ) logger.debug(f"\033[1;31mPre-process time = {(time() - t0) * 1000:.2f} ms\033[0m") t0 = time() output_names = [o.name for o in sess.get_outputs()] outputs = sess.run(output_names, {input_name: input_tensor}) logger.debug(f"\033[1;31mForward time = {(time() - t0) * 1000:.2f} ms\033[0m") t0 = time() strides = [8, 16, 32] conf_raw = -math.log(1.0 / opt.score_thres - 1.0) rboxes_all = [] scores_all = [] classes_all = [] for scale_idx, stride in enumerate(strides): box_idx = scale_idx * 3 cls_idx = scale_idx * 3 + 1 ang_idx = scale_idx * 3 + 2 if ang_idx >= len(outputs): continue box_data = outputs[box_idx] cls_data = outputs[cls_idx] ang_data = outputs[ang_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]) ang_data = ang_data[0].reshape(-1, ang_data.shape[-1]) if cls_data.shape[-1] == 1: cls_logits = cls_data[:, 0] cls_ids = np.zeros(len(cls_logits), dtype=np.int32) else: cls_logits = np.max(cls_data, axis=1) cls_ids = np.argmax(cls_data, axis=1) valid = cls_logits >= conf_raw if not np.any(valid): continue v_box = box_data[valid] v_ang = ang_data[valid] v_score = 1.0 / (1.0 + np.exp(-cls_logits[valid])) v_id = cls_ids[valid] gy, gx = np.indices((H, W)) anchors = np.stack((gx.ravel(), gy.ravel()), axis=-1).astype(np.float32) + 0.5 anchors = anchors[valid] rboxes = decode_obb(v_box, v_ang, anchors, stride, reg_max) rboxes_all.append(rboxes) scores_all.append(v_score) classes_all.append(v_id) if len(rboxes_all) == 0: logger.info("No detections found.") cv2.imwrite(opt.img_save_path, img0) return rboxes_all = np.concatenate(rboxes_all, axis=0).astype(np.float32) scores_all = np.concatenate(scores_all, axis=0).astype(np.float32) classes_all = np.concatenate(classes_all, axis=0).astype(np.int32) keep = nms_rotated_probiou( rboxes_all, scores_all, classes_all, iou_thres=opt.nms_thres, agnostic=opt.agnostic_nms, ) keep = keep[: opt.max_det] logger.debug(f"\033[1;31mPost-process time = {(time() - t0) * 1000:.2f} ms\033[0m") if len(keep) == 0: logger.info("No detections after NMS.") cv2.imwrite(opt.img_save_path, img0) return final_rboxes = rboxes_all[keep] final_scores = scores_all[keep] final_classes = classes_all[keep] final_rboxes = scale_rboxes_lefttop(final_rboxes, ratio_pad, orig_shape) final_rboxes = regularize_rbox(final_rboxes) logger.info(f"\033[1;32mDraw Results ({len(final_rboxes)} oriented objects):\033[0m") for i in range(len(final_rboxes)): cx, cy, w, h, theta = final_rboxes[i] conf = float(final_scores[i]) cid = int(final_classes[i]) name = DOTA_CLASSES[cid] if cid < len(DOTA_CLASSES) else f"cls{cid}" color = DOTA_COLORS[cid % len(DOTA_COLORS)] logger.info( f" {name:20s} conf={conf:.2f} cx={cx:.1f} cy={cy:.1f} " f"w={w:.1f} h={h:.1f} theta={math.degrees(theta):+.1f}deg" ) corners = rbox_to_corners((cx, cy, w, h, theta)).astype(np.int32) cv2.polylines(img0, [corners], isClosed=True, color=color, thickness=2, lineType=cv2.LINE_AA) label = f"{name} {conf:.2f}" (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) x_text, y_text = int(corners[0][0]), max(0, int(corners[0][1]) - 5) cv2.rectangle(img0, (x_text, y_text - th - 2), (x_text + tw + 2, y_text + 2), color, -1) cv2.putText(img0, label, (x_text + 1, y_text - 1), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA) cv2.imwrite(opt.img_save_path, img0) logger.info(f"Saved to {opt.img_save_path}") if __name__ == "__main__": main()