Yolo26 模型转换 onnx 再转换 om 模型到 Ascend310B4 运行经过

Yolo26 模型转换 onnx 再转换 om 模型到 Ascend310B4 运行经过

前言

原来以为说什么 yolo26 转换成 om 模型不能在昇腾上面运行,实际呢就是把训练好的模型转换好就行了,这里只要昇腾芯片对 onnx 模型版本的要求(一般 opset = 11 较为稳定),以及昇腾芯片能够适配的 CANN 版本就好了,其他就是一个运行匹配的问题,此次为什么能够花费这么久呢?完全是因为自己不理解 yolo26 模型的输出是什么意思,蒙起眼睛来打靶,完全打不中,后来参考了一个博主的,把需求弄清楚,就好了。

版本对应问题

这里再次强调一下版本

我的芯片是 Ascend310B4 对应的 HDK23.0.rc3

芯片适配的CANN版本是 8.1.RC1 查询:CANN版本兼容性-CANN社区版8.1.RC1-昇腾社区

适配的opset版本是 11 查询:通过查看昇腾 CANNONNX 的支持 链接====>https://www.hiascend.com/document/detail/zh/canncommercial/900/API/aolapi/operatorlist_00177.html 通过查看 ONNX 对应的 ONNX opset 链接 ====> https://runtime.onnx.org.cn/docs/reference/compatibility.html

代码

对于代码来说,主要是一个理解转化的模型需要进行 NMS 就好了,理解模型的输出,就能够很好的解决问题。

比如模型的输入是[batch,300,6]那么就要明白,这个数据代表的意思是什么?

加入 batch 是 1 ,那么如果输入一张图片的话,就会有 300 个框的结果,6 代表的是 x1, y1, x2, y2, score, class_id

参数 含义
x1 左上角 x 坐标
y1 左上角 y 坐标
x2 右下角 x 坐标
y2 右下角 y 坐标

score = 模型认为这个框里"确实有目标"的概率

class_id = 表示这个目标属于哪个类别

总结成一句话就是:在图像的某个位置,我认为那里有一个目标,它的置信度是多少,它属于哪个类别

结果很明确,一张图中不太可能有 300 个目标,那么我们就需要对结果做筛选,选出符合要求的目标,此时就需要做 NMS 非极大值抑制,非极大值抑制是关键,因为 yolo26 的模型导出成 onnx 并没有直接给你抑制

代码如下:

python 复制代码
from ais_bench.infer.interface import InferSession  # 导入Ascend推理接口
import cv2  # 导入OpenCV库,用于图像处理
import numpy as np  # 导入NumPy库,用于数值计算
from pathlib import Path  # 导入Path类,用于路径操作

# =========================
# 类别定义
# =========================
CLASS_NAMES = {
    0: "frogman",  # 类别ID 0: 蛙人
    1: "auv",      # 类别ID 1: 自主水下航行器
    2: "chain",    # 类别ID 2: 链条
    3: "boat",     # 类别ID 3: 船只
    4: "cage",     # 类别ID 4: 笼子
    5: "ball"      # 类别ID 5: 球
}

IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"}  # 支持的图像文件扩展名集合


# =========================
# Letterbox
# =========================
def letterbox(img, new_shape=640, color=(114, 114, 114)):
    """
    将图像通过letterbox方式缩放到指定大小,保持宽高比并填充边缘
    
    参数:
        img: 输入图像
        new_shape: 目标尺寸(宽高相同),默认640
        color: 填充颜色,默认灰色(114,114,114)
    
    返回:
        img: 处理后的图像
        r: 缩放比例
        (dw, dh): 填充的宽度和高度(未取整前的)
    """
    h, w = img.shape[:2]  # 获取原始图像高度和宽度
    r = min(new_shape / h, new_shape / w)  # 计算缩放比例,选择较小的缩放因子以确保图像能完整放入

    new_w, new_h = int(w * r), int(h * r)  # 计算缩放后的新宽度和新高度
    resized = cv2.resize(img, (new_w, new_h))  # 将图像缩放到新尺寸

    dw = new_shape - new_w  # 计算需要填充的宽度差值
    dh = new_shape - new_h  # 计算需要填充的高度差值
    dw /= 2  # 左右均分填充宽度
    dh /= 2  # 上下均分填充高度

    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))  # 计算上边和下边的填充像素数
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))  # 计算左边和右边的填充像素数

    img = cv2.copyMakeBorder(  # 添加边框填充
        resized,  # 输入图像
        top, bottom, left, right,  # 四边填充大小
        cv2.BORDER_CONSTANT,  # 边框类型为常数填充
        value=color  # 填充颜色值
    )

    return img, r, (dw, dh)  # 返回处理后的图像、缩放比例和填充值


# =========================
# NMS
# =========================
def nms(boxes, scores, iou_thres=0.45):
    """
    非极大值抑制算法,去除重叠度高的冗余检测框
    
    参数:
        boxes: 边界框坐标列表 [[x1,y1,x2,y2], ...]
        scores: 对应的置信度分数
        iou_thres: IoU阈值,高于此阈值的框会被抑制
    
    返回:
        保留的边界框索引列表
    """
    if len(boxes) == 0:  # 如果没有边界框,直接返回空列表
        return []

    xywh = []  # 存储转换为[x,y,width,height]格式的边界框
    for x1, y1, x2, y2 in boxes:  # 遍历所有边界框
        xywh.append([x1, y1, x2 - x1, y2 - y1])  # 转换为[x,y,宽度,高度]格式

    idxs = cv2.dnn.NMSBoxes(  # 调用OpenCV的NMS函数
        xywh,  # 边界框列表
        scores.tolist(),  # 置信度分数列表
        0.0,  # 置信度阈值(此处设为0,因为之前已经过滤过)
        iou_thres  # IoU阈值
    )

    if len(idxs) == 0:  # 如果没有保留的框
        return []

    return idxs.flatten()  # 将索引展平为一维数组返回


# =========================
# 后处理(YOLOv26: 1,300,6)
# =========================
def postprocess(pred, orig_shape, ratio, pad, conf_thres=0.25, iou_thres=0.45):
    """
    模型输出后处理:置信度过滤、NMS、坐标还原
    
    参数:
        pred: 模型预测输出,形状为(300,6),每行格式[x1,y1,x2,y2,score,class_id]
        orig_shape: 原始图像尺寸(高度,宽度)
        ratio: 缩放比例
        pad: 填充值(dw,dh)
        conf_thres: 置信度阈值
        iou_thres: NMS的IoU阈值
    
    返回:
        boxes: 过滤并还原后的边界框
        scores: 对应的置信度分数
        cls_ids: 对应的类别ID
    """
    orig_h, orig_w = orig_shape  # 获取原始图像高度和宽度
    dw, dh = pad  # 获取填充值

    pred = np.squeeze(pred)  # 去除批次维度,从(1,300,6)变为(300,6)

    boxes = pred[:, :4]  # 提取边界框坐标 [x1,y1,x2,y2]
    scores = pred[:, 4]  # 提取置信度分数
    cls_ids = pred[:, 5].astype(int)  # 提取类别ID并转换为整数类型

    # 置信度过滤:保留置信度大于阈值的检测结果
    mask = scores > conf_thres
    boxes, scores, cls_ids = boxes[mask], scores[mask], cls_ids[mask]

    if len(boxes) == 0:  # 如果没有检测结果
        return [], [], []

    # 非极大值抑制(NMS)
    keep = nms(boxes, scores, iou_thres)

    boxes = boxes[keep]  # 保留NMS后的边界框
    scores = scores[keep]  # 保留对应的置信度
    cls_ids = cls_ids[keep]  # 保留对应的类别ID

    # 还原坐标:将letterbox后的坐标映射回原始图像坐标
    boxes[:, [0, 2]] = (boxes[:, [0, 2]] - dw) / ratio  # 还原x1和x2坐标
    boxes[:, [1, 3]] = (boxes[:, [1, 3]] - dh) / ratio  # 还原y1和y2坐标

    # 坐标裁剪:确保边界框不超出原始图像范围
    boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, orig_w)  # 裁剪x坐标到[0,宽度]
    boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, orig_h)  # 裁剪y坐标到[0,高度]

    return boxes, scores, cls_ids  # 返回处理后的结果


# =========================
# 画框(已修复OpenCV报错)
# =========================
def draw_boxes(img, boxes, scores, cls_ids):
    """
    在图像上绘制检测框、标签和置信度
    
    参数:
        img: 输入图像
        boxes: 边界框坐标列表
        scores: 置信度分数列表
        cls_ids: 类别ID列表
    
    返回:
        绘制了检测结果的图像
    """
    for box, score, cid in zip(boxes, scores, cls_ids):  # 遍历每个检测结果
        x1, y1, x2, y2 = map(int, box)  # 将坐标转换为整数类型

        label = f"{CLASS_NAMES.get(int(cid), cid)} {score:.2f}"  # 生成标签文本:类别名+置信度

        # ⚠️ 关键修复:必须强制 int(避免 OpenCV 报错)
        # 根据类别ID动态生成颜色(确保值在0-255范围内且为整数)
        color = (
            int((37 * int(cid)) % 255),  # 红色分量
            int((17 * int(cid)) % 255),  # 绿色分量
            int((29 * int(cid)) % 255)   # 蓝色分量
        )

        # 绘制边界框矩形
        cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)  # 线条粗细为2像素

        # 获取标签文本的尺寸
        (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)

        # 计算标签背景矩形的上边界(避免超出图像顶部)
        y_top = max(0, y1 - th - 6)
        # 绘制标签背景矩形
        cv2.rectangle(img, (x1, y_top), (x1 + tw, y1), color, -1)  # -1表示填充矩形

        # 绘制标签文本
        cv2.putText(
            img,  # 目标图像
            label,  # 文本内容
            (x1, max(0, y1 - 4)),  # 文本位置
            cv2.FONT_HERSHEY_SIMPLEX,  # 字体类型
            0.6,  # 字体大小
            (255, 255, 255),  # 文本颜色(白色)
            2,  # 文本粗细
            cv2.LINE_AA  # 抗锯齿线条类型
        )

    return img  # 返回绘制后的图像


# =========================
# 获取图片
# =========================
def get_images(folder: Path):
    """
    递归获取文件夹下所有支持的图像文件
    
    参数:
        folder: 文件夹路径(Path对象)
    
    返回:
        图像文件路径列表
    """
    imgs = []  # 存储图像路径的列表
    for p in sorted(folder.rglob("*")):  # 递归遍历文件夹下所有文件(排序后)
        if p.suffix.lower() in IMG_EXTS:  # 如果文件扩展名在支持的图像格式集合中
            imgs.append(p)  # 添加到列表
    return imgs


# =========================
# 主函数
# =========================
def main():
    """主函数:解析命令行参数、加载模型、执行推理并保存结果"""
    import argparse  # 导入命令行参数解析模块

    parser = argparse.ArgumentParser()  # 创建参数解析器
    parser.add_argument("--model", type=str, required=True)  # 模型文件路径(必需)
    parser.add_argument("--source", type=str, required=True)  # 输入图像源文件夹路径(必需)
    parser.add_argument("--save", type=str, default="./output")  # 输出保存目录,默认为./output
    parser.add_argument("--device", type=int, default=0)  # 昇腾设备ID,默认为0
    parser.add_argument("--conf", type=float, default=0.20)  # 置信度阈值,默认0.20
    parser.add_argument("--iou", type=float, default=0.40)  # NMS的IoU阈值,默认0.40

    args = parser.parse_args()  # 解析命令行参数

    # =========================
    # load model
    # =========================
    session = InferSession(args.device, args.model)  # 创建推理会话,加载模型到指定设备

    save_dir = Path(args.save)  # 创建输出保存目录的Path对象
    img_dir = save_dir / "images"  # 保存绘制后图像的子目录
    label_dir = save_dir / "labels"  # 保存YOLO格式标签的子目录

    img_dir.mkdir(parents=True, exist_ok=True)  # 创建图像保存目录(如果父目录不存在则递归创建)
    label_dir.mkdir(parents=True, exist_ok=True)  # 创建标签保存目录

    # =========================
    # images
    # =========================
    images = get_images(Path(args.source))  # 获取所有待处理的图像文件
    print(f"[INFO] Found {len(images)} images")  # 打印找到的图像数量

    # =========================
    # inference loop
    # =========================
    for img_path in images:  # 遍历每个图像文件
        img = cv2.imread(str(img_path))  # 读取图像(BGR格式)
        if img is None:  # 如果图像读取失败
            continue  # 跳过该图像

        h, w = img.shape[:2]  # 获取原始图像的高度和宽度

        # preprocess(预处理)
        img_lb, ratio, pad = letterbox(img, 640)  # 对图像进行letterbox处理,缩放到640x640
        img_rgb = cv2.cvtColor(img_lb, cv2.COLOR_BGR2RGB)  # 将BGR格式转换为RGB格式

        inp = img_rgb.astype(np.float32) / 255.0  # 归一化到[0,1]范围并转换为float32类型
        inp = np.transpose(inp, (2, 0, 1))  # 调整维度顺序:从(H,W,C)转换为(C,H,W)
        inp = np.expand_dims(inp, 0)  # 添加批次维度,变为(1,C,H,W)
        inp = np.ascontiguousarray(inp)  # 确保数组在内存中是连续的

        # inference(推理)
        out = session.infer([inp])[0]  # 执行推理,获取输出(假设第一个输出是检测结果)
        pred = out[0]   # 获取预测结果,形状为(300,6)

        # postprocess(后处理)
        boxes, scores, cls_ids = postprocess(
            pred,  # 预测结果
            (h, w),  # 原始图像尺寸
            ratio,  # 缩放比例
            pad,  # 填充值
            args.conf,  # 置信度阈值
            args.iou  # IoU阈值
        )

        # =========================
        # save YOLO txt(保存YOLO格式标签)
        # =========================
        txt_path = label_dir / f"{img_path.stem}.txt"  # 生成标签文件路径(与图像同名,扩展名为.txt)
        lines = []  # 存储标签行的列表

        for box, score, cid in zip(boxes, scores, cls_ids):  # 遍历每个检测结果
            x1, y1, x2, y2 = box  # 获取边界框坐标

            # 转换为YOLO格式:中心点坐标和宽高(均归一化到[0,1])
            cx = (x1 + x2) / 2 / w  # 中心点x坐标(归一化)
            cy = (y1 + y2) / 2 / h  # 中心点y坐标(归一化)
            bw = (x2 - x1) / w  # 边界框宽度(归一化)
            bh = (y2 - y1) / h  # 边界框高度(归一化)

            # 格式:类别ID 中心x 中心y 宽度 高度 置信度
            lines.append(
                f"{int(cid)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f} {score:.6f}"
            )

        txt_path.write_text("\n".join(lines))  # 将标签写入文件(每行一个检测结果)

        # =========================
        # save image(保存绘制了检测框的图像)
        # =========================
        vis = draw_boxes(img.copy(), boxes, scores, cls_ids)  # 在图像副本上绘制检测结果

        save_img_path = img_dir / f"{img_path.stem}.jpg"  # 生成保存图像路径(转换为jpg格式)
        cv2.imwrite(str(save_img_path), vis)  # 保存绘制后的图像

        print(f"{img_path.name}: {len(boxes)} objects")  # 打印检测到的目标数量


if __name__ == "__main__":  # 如果作为主程序运行
    main()  # 调用主函数
相关推荐
机 _ 长7 天前
YOLO26-Mamba:融合MambaVision思想的目标检测创新实践
yolo·mamba·yolo26
ujainu小15 天前
CANN cann-recipes-train:训练配方仓库的使用场景
ascend
ujainu16 天前
CANN pto-isa:虚拟指令集如何连接编译与执行
android·ascend
ujainu16 天前
CANN pto-isa:PTO 虚拟指令集里的 90+ Tile 操作怎么设计的
ascend
ujainu17 天前
CANN pto-isa:PTO 性能优化的指令调度与硬件特化
性能优化·ascend
ujainu17 天前
CANN pto-isa:Transformer 推理编译链路:从 PyTorch 到昇腾 NPU 执行
pytorch·深度学习·transformer·ascend
ujainu17 天前
CANN pto-isa:跨平台算子开发为什么需要虚拟指令集?
ascend
ujainu17 天前
CANN pto-isa:为什么 AI 编译需要一层虚拟指令集
人工智能·ascend
ujainu17 天前
CANN pto-isa:PTO到机器码的映射
ascend