yolo26-obb / ax_infer.py
Fangming Guo
Upload 3 files
fbbeea1 verified
Raw
History Blame Contribute Delete
12.4 kB
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()