简介这份文档面向具备Python基础、希望入门计算机视觉目标检测的开发者以YOLOv10为核心构建实时目标检测系统内容贯通环境准备、模型获取、数据标注、参数配置、训练评估与视频流推理等环节。包内共1个doc文件约28KB采用章节化教程组织硬件软件要求、深度学习框架与图像处理库安装、常用数据集选择与标注工具使用、学习率与批量大小设置以及平均精度曲线和检测框可视化示例。已有207人学习适合作为课程设计、毕业项目或视觉应用原型的参考资料。读者可据此掌握从标签格式整理到摄像头实时检测的流程框架理解训练命令、评估指标与结果分析方法并借鉴常见配置与排错思路快速搭建可运行的实时目标检测案例。1. 从一段掉帧的摄像头画面说起某开发者把 YOLOv10 接到摄像头上做实时目标检测系统画面出来第一秒还正常十几秒后帧率掉到个位数检测框还往右下角偏了半格。这类问题往往不是模型能力不够而是从环境版本、数据格式到取帧方式中间任何一环没对齐最后都以“帧率低”“框不准”“mAP 归零”的形式暴露出来。这套 YOLOv10 开发案例把环境准备、权重获取、数据标注与格式转换、训练评估、实时检测应用串成一条能真正跑通的链路适合两类人刚接触目标检测、想从零搭出一个能看的效果的已经跑通、但卡在显存、帧率或者指标不涨的。下面按实际动手顺序拆。2. 环境与权重把 CUDA、PyTorch、ultralytics 的版本咬合钉死拿到一个目标检测案例绝大多数人第一反应是复制训练命令先跑起来环境随便装装。我自己前几次也是这么干的结果十次里有六次卡在“装完了但 GPU 用不上”。YOLOv10 这套东西对版本的敏感度比一般脚本高因为它同时依赖三样东西显卡驱动、PyTorch 编译时绑定的 CUDA 运行时、以及 ultralytics 这个上层封装对 torch 版本的兼容区间。这三者的关系不是“装了就行”而是必须咬合。2.1 版本错位为什么比代码写错更致命PyTorch 的 pip 安装包是分 CUDA 版本编译的cu121、cu118这些后缀代表这份 wheel 里打包的 CUDA 运行时版本。装成 CPU 版本torch.cuda.is_available()直接返回 False装成比你驱动支持的版本更新的 CUDA运行时会在初始化阶段报驱动版本不足装成比 ultralytics 要求更老的 torch会在 import 阶段抛出一堆看似莫名其妙的方法缺失错误。这三种情况报出来的错各不相同但根因是同一个。更隐蔽的是 cuDNN。卷积算子的实现依赖 cuDNN 的版本版本不匹配时程序不报错但某些尺寸的输入会走一条更慢的回退路径表现出来就是“模型能跑、单帧推理却要几百毫秒”。这种问题在代码层面几乎查不出来只能靠版本对齐规避。我一般的顺序是先看显卡驱动支持到哪个 CUDA 大版本再倒推装哪份 torch wheel最后装 ultralytics让它自己带依赖。反过来先装 ultralytics 再补 torch很容易把 torch 覆盖成 CPU 版。这是一个值得记住的前后顺序。2.2 一套可复现的安装顺序下面这段命令适合干净机器虚拟环境名和 Python 版本可以按自己习惯改但顺序别动。# 1. 建独立环境避免和系统里的 torch 打架 conda create -n yolov10 python3.10 -y conda activate yolov10 # 2. 先装 torch。cu121 换成与你驱动匹配的索引 # 不要写 pip install torch 就完事那样大概率拿到 CPU 版 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 3. 验证 torch 这一层已经通了再往上装 python -c import torch; print(torch.__version__, torch.cuda.is_available()) # 4. 装上层封装和图像处理库 pip install ultralytics opencv-python matplotlib pandas # 5. 最后确认版本没有互相覆盖 pip list | grep -Ei torch|ultralytics|opencv第 2 步的--index-url是关键它决定你拿到的是 GPU 版还是 CPU 版。第 3 步单独插一条验证命令目的是把问题隔离开如果这里is_available()就已经是 False后面所有训练报错都不用看了直接回头查驱动和 CUDA 版本。第 5 步的pip list是我养成的习惯因为装 opencv 时偶尔会把某个依赖顺带降级看一眼版本表能省掉后面半小时的排查。如果你在公司内网或者离线机器上pip走不通那就先在能联网的机器上pip download把整个依赖树拉下来再拷进内网做本地安装别指望运行时临时下包。权重文件同理yolov10n.pt、yolov10s.pt这类文件名是官方命名第一次调用时会自动下载到工作目录离线环境要提前放好并确保路径能对上。2.3 五分钟验证清单环境装完不要急着训练先跑一遍下面这段。它做四件事确认 GPU 可见、确认 cuDNN 版本、跑一次真实卷积触发算子编译、加载一次模型确认权重路径正确。import torch # 1. 基础信息版本 是否可见 GPU print(torch:, torch.__version__, | cuda:, torch.version.cuda) print(cuda available:, torch.cuda.is_available()) print(device:, torch.cuda.get_device_name(0)) # 2. cuDNN 版本空值说明后端没挂上 print(cudnn:, torch.backends.cudnn.version()) # 3. 跑一次真实算子触发 kernel 编译 # 只打印 is_available 不够有些环境要等第一次前向才暴露问题 x torch.randn(1, 3, 640, 640, devicecuda) y torch.nn.Conv2d(3, 16, 3, padding1).cuda()(x) print(conv out:, y.shape, | sum:, float(y.sum())) # 4. 加载一次模型确认权重可读、类别名可解析 from ultralytics import YOLO model YOLO(yolov10n.pt) print(classes:, len(model.names), model.names)第 3 步的意义在于torch.cuda.is_available()只检查驱动和运行时是否握手成功不代表算子能正常执行。有些环境在这一步返回 True真正跑卷积时才报no kernel image is available那是显卡算力和编译目标不匹配。第 4 步的model.names打印出来是一串类别名能正常输出说明权重文件没损坏、路径也找对了。如果这里卡住很久多半是在下载权重看一眼当前目录有没有.pt文件就能判断。这套验证跑通之后再动数据后面出问题才能确定不是环境导致的。很多人跳这一步等到训练 loss 变成 NaN 才回头查那时候变量太多反而不好定位。3. 数据从标注到 YOLO 格式目录树、归一化坐标与 data.yaml环境通了接下来最容易翻车的地方是数据。目标检测的输入不是一张图加一个类别而是一张图加若干个框每个框还要绑定类别和坐标。标注工具导出的格式五花八门YOLO 只认一种每张图配一个同名.txt每行一个目标格式是类别id 中心x 中心y 宽 高后四个值全部归一化到 0 到 1 之间。这个格式差异是所有“训练不收敛”问题里占比最高的一类。3.1 标签文件和常见标注工具的差异Pascal VOC 那套 XML 标注用的是左上角和右下角的绝对像素坐标也就是xmin, ymin, xmax, ymax而且类别是字符串。COCO 的 JSON 用的是[x, y, width, height]绝对坐标类别是独立的 id 映射表。YOLO 要的是中心点加宽高、再除以图像宽高做归一化类别直接用从 0 开始的整数索引。转换过程里有三个点必须自己盯着一是坐标系原点。VOC 和 COCO 都是左上角YOLO 归一化后也是左上角为原点这一点一致但中心点要自己算cx (xmin xmax) / 2 / img_w。二是边界裁剪。标注时手抖拖出图像边界的框很常见归一化后会得到大于 1 或小于 0 的值训练时这类值会让损失函数直接失控必须 clamp 到[0, 1]。三是类别 id 的起始值。有些标注工具从 1 开始编号而 YOLO 要求从 0 开始错一位的后果是所有目标都被归到错误的类别上模型照样能训练但 mAP 永远上不去这种问题靠看 loss 曲线是发现不了的。3.2 一个能直接改的 VOC 转 YOLO 脚本下面这段处理的是 VOC XML 转 YOLO txt改一下CLASSES和输入输出目录就能用。import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image # 类别顺序即最终标签里的 id必须和 data.yaml 的 names 完全一致 CLASSES [person, car, bicycle] def convert_one(xml_path: Path, img_dir: Path, out_dir: Path): root ET.parse(xml_path).getroot() # 优先从 XML 的 size 节点取宽高没有就回退到读原图 size root.find(size) if size is not None: img_w int(size.find(width).text) img_h int(size.find(height).text) else: with Image.open(img_dir / f{xml_path.stem}.jpg) as im: img_w, img_h im.size lines [] for obj in root.iter(object): name (obj.find(name).text or ).strip() if name not in CLASSES: # 不在类别表里的目标直接跳过 continue cls_id CLASSES.index(name) b obj.find(bndbox) x1 float(b.find(xmin).text) y1 float(b.find(ymin).text) x2 float(b.find(xmax).text) y2 float(b.find(ymax).text) # 坐标裁剪防止标注越界 x1, x2 max(0.0, min(x1, img_w)), max(0.0, min(x2, img_w)) y1, y2 max(0.0, min(y1, img_h)), max(0.0, min(y2, img_h)) if x2 x1 or y2 y1: # 宽度或高度为 0 的无效框 continue cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) (out_dir / f{xml_path.stem}.txt).write_text(\n.join(lines), encodingutf-8) if __name__ __main__: img_dir Path(raw/images) xml_dir Path(raw/annotations) out_dir Path(raw/labels) out_dir.mkdir(parentsTrue, exist_okTrue) for xml in xml_dir.glob(*.xml): convert_one(xml, img_dir, out_dir)脚本里保留六位小数是个小细节。YOLO 的标签文件是纯文本保留太少会引入肉眼看不出来的定位误差保留太多写起来也没意义六位对 640 尺寸的图来说精度已经远超像素级。跳过不在CLASSES里的目标这一条实际用的时候要看情况如果你的标注集里混了不想要的类别静默跳过是对的如果是类别名拼写不一致静默跳过就会让你以为数据集里没有这个类所以转换完必须做一次统计看看每个类别的框数量对不对。3.3 data.yaml 的四个字段和路径坑转换完只是有了标签训练脚本还得知道去哪找图和标签。YOLO 用一份 YAML 描述这件事字段不多但路径写法是最常见的踩坑点。# data.yaml path: /home/user/datasets/mydata # 数据集根目录建议写绝对路径 train: images/train # 相对 path 的训练图目录 val: images/val # 相对 path 的验证图目录 names: 0: person 1: car 2: bicyclepath写绝对路径能避开九成的“找不到图片”报错。train和val写的是相对path的路径指向的是图片目录不是标签目录。YOLO 的约定是标签目录和图片目录同级名字由图片路径里的images替换成labels得到。也就是说图片在mydata/images/train/a.jpg标签就必须在mydata/labels/train/a.txt文件名主干要一模一样。这个映射关系是写死在数据加载逻辑里的改目录名之前先确认这套规则。names的键不能有跳号0、1、2要连续。中间缺一个会导致加载时报索引越界。另外names的顺序必须和 3.2 里CLASSES的顺序完全一致这两处任何一处改了没同步标签和类别名就对不上训练照样跑但预测出来全是错的类。3.4 训练前把标签画回图上自检转换脚本写对了不代表结果对了最省时间的验证方式是把标签读回来画到原图上用眼睛看。import cv2 from pathlib import Path CLASSES [person, car, bicycle] def draw_one(img_path: Path, txt_path: Path, out_path: Path): img cv2.imread(str(img_path)) h, w img.shape[:2] for line in txt_path.read_text(encodingutf-8).splitlines(): if not line.strip(): continue c, cx, cy, bw, bh line.split() cx, cy, bw, bh float(cx) * w, float(cy) * h, float(bw) * w, float(bh) * h x1, y1 int(cx - bw / 2), int(cy - bh / 2) x2, y2 int(cx bw / 2), int(cy bh / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, CLASSES[int(c)], (x1, max(y1 - 6, 12)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(str(out_path), img) # 抽 20 张看看 imgs sorted(Path(raw/images).glob(*.jpg))[:20] for p in imgs: draw_one(p, Path(raw/labels) / f{p.stem}.txt, Path(check) / p.name)这段代码把归一化值乘回图像宽高还原成像素框。抽 20 张看一遍能发现框整体偏移宽高取错源、框大小不对没归一化或归一化了两次、类别名错位id 起始值不对这三类问题。框整体偏移半个物体是很典型的“用 XML 里的 size 但图被 resize 过”的表现这种问题在 loss 曲线上只会表现为收敛慢看不出原因。提示切换数据集或者重新标注之后这三步自检一定要重跑一遍。数据问题在训练阶段暴露出来排查成本会高一个数量级。4. 训练到评估显存预算、参数配置与 mAP 曲线读法数据理顺了训练本身反而是最确定的一步。YOLO 系列把训练流程封装得比较好真正需要动脑的是两个地方一是显存够不够、batch 和输入尺寸怎么配二是指标出来之后怎么判断模型是欠拟合、过拟合还是数据有问题。4.1 先算显存预算再定 batch 和 imgsz显存占用跟三个量直接相关模型规模、输入分辨率、批大小。经验上输入尺寸从 640 提到 1280显存占用大约翻四倍因为中间特征图的面积是平方增长batch 从 8 提到 16显存大约翻倍这是线性的。模型规模从n换到s或m参数量和中间激活都会涨但比改分辨率温和。我一般的做法是先拿yolov10n.pt加imgsz640、batch16试一把看显存峰值。如果显存还剩很多优先提分辨率而不是提 batch因为小目标检测对分辨率更敏感。如果显存紧张先用batch-1让训练脚本自动探测能塞下的最大批大小拿到这个值再手动往下调一档留余量因为训练过程中验证阶段和保存 checkpoint 都会额外占一点。场景模型imgszbatch 建议说明显存 8G 左右yolov10n6408 或-1自动先跑通别追精度显存 12G 左右yolov10s64016兼顾速度和精度显存 16G 以上yolov10m640 到 96016 到 32小目标多时提 imgsz纯 CPU 调试yolov10n3204只验证流程不看指标这张表是起步参考值不是最优解。同一个显存容量驱动版本、cuDNN 版本、有没有开混合精度都会影响实际能塞多少所以第一次训练一定要盯一眼显存峰值再定。4.2 训练命令与配置项逐条解释训练入口有命令行和 YAML 配置两种写法。这个案例里给的是 YAML 配置文件实际用起来我更倾向命令行直接写参数改起来直观也方便记在笔记里。yolo detect train \ data/home/user/datasets/mydata/data.yaml \ modelyolov10n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ workers4 \ patience30 \ projectruns/train \ nameexp1data指向 3.3 里那份 YAML这个路径写错会直接报找不到数据集。model写yolov10n.pt表示从预训练权重开始微调如果是从零训练就写对应的结构 YAML。epochs是最大轮数patience30表示验证指标连续 30 轮没提升就提前停这个参数在小数据集上很有用能省掉大量无效训练时间。device0指定第一块显卡多卡就用0,1。workers是数据加载进程数Linux 下 4 到 8 比较合适Windows 下遇到卡死就设成 0。project和name决定输出目录最终权重会落在runs/train/exp1/weights/下里面best.pt是验证指标最好的一轮last.pt是最后一轮。这两个文件后面推理和评估都要用别只拿last.pt训练后期过拟合的情况下last.pt的指标经常比best.pt差一截。训练日志里会实时打印每轮的 box loss、cls loss、dfl loss 和验证集的 mAP。刚开始几轮 loss 下降快是正常的如果前五轮 loss 基本不动先回去查数据格式别急着调学习率。4.3 损失曲线和 mAP 曲线怎么读训练结束后目录里会有一份results.csv里面是逐轮的指标。把它读出来画图比盯日志高效得多。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/train/exp1/results.csv) df.columns [c.strip() for c in df.columns] # 列名可能带空格 fig, axes plt.subplots(1, 2, figsize(12, 4)) # 左图训练损失看是否还在下降 axes[0].plot(df[epoch], df[train/box_loss], labelbox_loss) axes[0].plot(df[epoch], df[train/cls_loss], labelcls_loss) axes[0].set_xlabel(epoch); axes[0].set_ylabel(loss); axes[0].legend() # 右图验证指标看是否已经平台期 axes[1].plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) axes[1].plot(df[epoch], df[metrics/mAP50-95(B)], labelmAP50-95) axes[1].set_xlabel(epoch); axes[1].set_ylabel(mAP); axes[1].legend() plt.tight_layout() plt.savefig(runs/train/exp1/curves.png, dpi150)读这张图有三个固定判断。训练损失持续下降但验证 mAP 走平甚至回落是过拟合处理方式是加数据增强、减模型规模或者提前停。训练损失一开始就下不去基本可以确定是数据问题回去跑 3.4 的自检。mAP50 很高但 mAP50-95 很低说明框找得到但定位不精细这种情况通常是标注框本身不够紧或者训练分辨率偏低提高imgsz往往比调学习率有效。4.4 评估命令与 mAP 的实际含义训练完的指标是训练过程中顺手算的正式评估建议单独跑一次参数也能控制得更细。yolo detect val \ modelruns/train/exp1/weights/best.pt \ data/home/user/datasets/mydata/data.yaml \ imgsz640 \ conf0.001 \ iou0.6 \ splitvalconf0.001这个看似极低的阈值是评估时的标准做法mAP 计算需要对所有预测按置信度排序画 PR 曲线阈值设高了会把低置信度的正确预测提前砍掉算出来的 mAP 偏低。iou0.6是 NMS 的重叠阈值评估时通常比推理时宽松一些。这两个参数和实际部署时的参数不是一个概念别把评估用的conf直接搬到推理代码里那样会漏掉大量目标。mAP50 表示 IoU 阈值取 0.5 时的平均精度比较宽松日常看趋势够用。mAP50-95 是把 IoU 从 0.5 到 0.95 每隔 0.05 取一次再平均对定位精度要求高得多这个指标更接近实际部署时的体验。两个指标一起看能区分“找得到”和“框得准”这两件事。5. 避坑与排查五条现象、原因和处理这部分是我在类似项目里反复撞到的几堵墙按“看到什么、为什么、怎么改”写遇到对应现象可以直接对号入座。5.1 报错Results object has no attribute xyxy现象照着一份老教程写推理代码跑到解析结果那行直接抛 AttributeError提示 Results 对象没有xyxy属性。原因YOLO 的推理返回结构在不同版本之间改过。早期版本返回的是一个可以直接下标取xyxy的张量后来改成了Results对象框信息收在boxes里boxes下面才有xyxy、conf、cls这些字段。网上大量教程还停留在旧写法复制过来必然报错。解决改成遍历results[0].boxes从每个 box 上取box.xyxy[0]、box.conf[0]、box.cls[0]。更省事的做法是直接用results[0].plot()拿到已经画好框的图只在需要自己定制渲染时才手动解析。5.2 训练 loss 不降mAP 一直是 0现象训练正常启动日志也正常输出但 box loss 在高位震荡mAP 从头到尾接近 0验证阶段偶尔报“no labels found”。原因这类现象九成出在数据侧。常见的有三种标签坐标没归一化值还是几百的像素数类别 id 从 1 开始编号而names从 0 开始图片目录和标签目录的文件名主干对不上比如图片叫001.jpg标签叫001_person.txt。解决先跑 3.4 的可视化自检框画得出来且贴合物体说明坐标没问题再统计每个类别的框数量某个类别数量为 0 就说明 id 或者类别名映射错了最后检查图片和标签的数量是否一致不一致的那个文件名就是问题所在。5.3 训练中途报 CUDA out of memory现象训练跑了几十轮之后突然 OOM或者一开始就 OOM 但显存监控看着还有余量。原因一开始就 OOM 通常是batch或imgsz设大了也可能是显卡上还挂着别的进程。跑到中途才 OOM多数是验证阶段或 checkpoint 保存时的峰值超过了训练阶段或者数据里有尺寸特别大的图letterbox 之后仍然偏大。解决先用nvidia-smi看一眼有没有残留进程再用batch-1让脚本自动探测。如果自动探测出来的值很小说明模型或者分辨率本身就不适合当前显存降imgsz到 512 或换更小的模型比硬扛更实际。另外把workers调小一点也能缓解数据加载进程本身也占内存。5.4 摄像头画面越跑越慢延迟不断累积现象刚开摄像头时画面流畅跑一两分钟后画面明显滞后于真实动作越跑越严重但单帧推理耗时测出来并没有变。原因视频采集是有缓冲队列的。推理速度跟不上采集速度时帧会堆在队列里cap.read()每次读到的都是队列头部的旧帧于是延迟一直累加。这个问题很隐蔽因为它看起来像“模型变慢了”实际模型速度根本没变。解决把采集缓冲设成 1并且在推理跟不上时主动丢帧。cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)在多数后端上有效。更彻底的做法是采集和推理拆成两个线程采集线程只保留最新一帧推理线程取最新的那帧处理中间用一把锁保护。只靠单线程循环读帧迟早会遇到这个问题。5.5 导出 ONNX 之后结果和 PyTorch 对不上现象PyTorch 权重推理框的位置挺准导出 ONNX 用同一张图跑框整体偏移或者置信度明显不同。原因预处理没对齐。PyTorch 推理路径里包含了 letterbox 缩放、BGR 转 RGB、归一化、通道转置这一整套预处理手写 ONNX 推理时如果只做了 resize 没做 letterbox或者忘了 BGR 转 RGB输出就会偏。另一个常见原因是导出时用了halfTrue但推理端输入还是 float32。解决把预处理和导出配置对齐。用 ultralytics 自带的导出命令并在推理端复用它的预处理逻辑或者严格按 letterbox 的规则自己实现等比缩放、灰边填充、坐标在还原时按同样的比例和偏移反算。导出后先拿同一张图跑一遍 PyTorch 和 ONNX比对框坐标差在几个像素内才算对。注意导出的模型只固化前向计算预处理和后处理永远在模型外面。这两段代码必须自己保证一致这不是导出工具能替你解决的事。6. 实时推理与导出摄像头链路、后处理与延迟验证实时检测的代码骨架不长坑集中在两处取帧方式和结果解析。下面这段用streamTrue让推理返回生成器避免一次性把视频读进内存同时把帧率算在循环里方便一眼看出性能瓶颈在哪。import time import cv2 from ultralytics import YOLO model YOLO(runs/train/exp1/weights/best.pt) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 缓冲设 1防止延迟累积 prev time.time() while True: ok, frame cap.read() if not ok: break # streamTrue 返回生成器逐帧处理适合视频源 r model.predict(frame, imgsz640, conf0.25, iou0.45, device0, verboseFalse, streamTrue) res next(iter(r)) for box in res.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) cls_id, score int(box.cls[0]), float(box.conf[0]) label f{res.names[cls_id]} {score:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, max(y1 - 8, 12)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) now time.time() fps 1.0 / max(now - prev, 1e-6) prev now cv2.putText(frame, fFPS {fps:.1f}, (12, 28), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imshow(YOLOv10, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()conf0.25和iou0.45是部署侧的起点前者控制漏检和误检的平衡后者控制密集场景下框被合并的程度这两个值都要在自己的数据上扫一遍再定不能直接抄评估参数。streamTrue的意义在于把读帧和推理错开不会因为视频源慢而把整段加载进内存。跑通之后再谈优化顺序一般是先测纯推理延迟用固定图片循环跑一百次取平均排除采集和渲染的干扰再导出 ONNX 或 TensorRT 引擎对比同一张图的延迟和输出一致性。# 导出 ONNX简化计算图半精度适合支持 fp16 的显卡 yolo export modelruns/train/exp1/weights/best.pt formatonnx imgsz640 simplifyTrue halfTrue导出后务必拿同一张图跑 PyTorch 和导出产物两条路径比对框坐标和置信度差在可接受范围内再替换线上模型。从那以后我每次导出都强制走一遍这个比对不管多急因为一旦预处理没对齐线上表现会变得毫无规律比直接报错更难查。希望帮到你。本文还有配套的精品资源点击获取