简介:这份文档面向具备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 python=3.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, device="cuda") y = torch.nn.Conv2d(3, 16, 3, padding=1).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), encoding="utf-8") if __name__ == "__main__": img_dir = Path("raw/images") xml_dir = Path("raw/annotations") out_dir = Path("raw/labels") out_dir.mkdir(parents=True, exist_ok=True) 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(encoding="utf-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加imgsz=640、batch=16试一把,看显存峰值。如果显存还剩很多,优先提分辨率而不是提 batch,因为小目标检测对分辨率更敏感。如果显存紧张,先用batch=-1让训练脚本自动探测能塞下的最大批大小,拿到这个值再手动往下调一档留余量,因为训练过程中验证阶段和保存 checkpoint 都会额外占一点。
| 场景 | 模型 | imgsz | batch 建议 | 说明 |
|---|---|---|---|---|
| 显存 8G 左右 | yolov10n | 640 | 8 或-1自动 | 先跑通,别追精度 |
| 显存 12G 左右 | yolov10s | 640 | 16 | 兼顾速度和精度 |
| 显存 16G 以上 | yolov10m | 640 到 960 | 16 到 32 | 小目标多时提 imgsz |
| 纯 CPU 调试 | yolov10n | 320 | 4 | 只验证流程,不看指标 |
这张表是起步参考值,不是最优解。同一个显存容量,驱动版本、cuDNN 版本、有没有开混合精度都会影响实际能塞多少,所以第一次训练一定要盯一眼显存峰值再定。
4.2 训练命令与配置项逐条解释
训练入口有命令行和 YAML 配置两种写法。这个案例里给的是 YAML 配置文件,实际用起来我更倾向命令行直接写参数,改起来直观,也方便记在笔记里。
yolo detect train \ data=/home/user/datasets/mydata/data.yaml \ model=yolov10n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ workers=4 \ patience=30 \ project=runs/train \ name=exp1data指向 3.3 里那份 YAML,这个路径写错会直接报找不到数据集。model写yolov10n.pt表示从预训练权重开始微调,如果是从零训练就写对应的结构 YAML。epochs是最大轮数,patience=30表示验证指标连续 30 轮没提升就提前停,这个参数在小数据集上很有用,能省掉大量无效训练时间。device=0指定第一块显卡,多卡就用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"], label="box_loss") axes[0].plot(df["epoch"], df["train/cls_loss"], label="cls_loss") axes[0].set_xlabel("epoch"); axes[0].set_ylabel("loss"); axes[0].legend() # 右图:验证指标,看是否已经平台期 axes[1].plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") axes[1].plot(df["epoch"], df["metrics/mAP50-95(B)"], label="mAP50-95") axes[1].set_xlabel("epoch"); axes[1].set_ylabel("mAP"); axes[1].legend() plt.tight_layout() plt.savefig("runs/train/exp1/curves.png", dpi=150)读这张图有三个固定判断。训练损失持续下降但验证 mAP 走平甚至回落,是过拟合,处理方式是加数据增强、减模型规模或者提前停。训练损失一开始就下不去,基本可以确定是数据问题,回去跑 3.4 的自检。mAP50 很高但 mAP50-95 很低,说明框找得到但定位不精细,这种情况通常是标注框本身不够紧,或者训练分辨率偏低,提高imgsz往往比调学习率有效。
4.4 评估命令与 mAP 的实际含义
训练完的指标是训练过程中顺手算的,正式评估建议单独跑一次,参数也能控制得更细。
yolo detect val \ model=runs/train/exp1/weights/best.pt \ data=/home/user/datasets/mydata/data.yaml \ imgsz=640 \ conf=0.001 \ iou=0.6 \ split=valconf=0.001这个看似极低的阈值是评估时的标准做法:mAP 计算需要对所有预测按置信度排序画 PR 曲线,阈值设高了会把低置信度的正确预测提前砍掉,算出来的 mAP 偏低。iou=0.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,输出就会偏。另一个常见原因是导出时用了half=True但推理端输入还是 float32。
解决:把预处理和导出配置对齐。用 ultralytics 自带的导出命令,并在推理端复用它的预处理逻辑,或者严格按 letterbox 的规则自己实现:等比缩放、灰边填充、坐标在还原时按同样的比例和偏移反算。导出后先拿同一张图跑一遍 PyTorch 和 ONNX,比对框坐标,差在几个像素内才算对。
注意:导出的模型只固化前向计算,预处理和后处理永远在模型外面。这两段代码必须自己保证一致,这不是导出工具能替你解决的事。
6. 实时推理与导出:摄像头链路、后处理与延迟验证
实时检测的代码骨架不长,坑集中在两处:取帧方式和结果解析。下面这段用stream=True让推理返回生成器,避免一次性把视频读进内存,同时把帧率算在循环里,方便一眼看出性能瓶颈在哪。
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 # stream=True 返回生成器,逐帧处理,适合视频源 r = model.predict(frame, imgsz=640, conf=0.25, iou=0.45, device=0, verbose=False, stream=True) 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, f"FPS {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()conf=0.25和iou=0.45是部署侧的起点,前者控制漏检和误检的平衡,后者控制密集场景下框被合并的程度,这两个值都要在自己的数据上扫一遍再定,不能直接抄评估参数。stream=True的意义在于把读帧和推理错开,不会因为视频源慢而把整段加载进内存。
跑通之后再谈优化,顺序一般是:先测纯推理延迟,用固定图片循环跑一百次取平均,排除采集和渲染的干扰;再导出 ONNX 或 TensorRT 引擎对比同一张图的延迟和输出一致性。
# 导出 ONNX,简化计算图,半精度适合支持 fp16 的显卡 yolo export model=runs/train/exp1/weights/best.pt format=onnx imgsz=640 simplify=True half=True导出后务必拿同一张图跑 PyTorch 和导出产物两条路径,比对框坐标和置信度,差在可接受范围内再替换线上模型。从那以后我每次导出都强制走一遍这个比对,不管多急,因为一旦预处理没对齐,线上表现会变得毫无规律,比直接报错更难查。希望帮到你。
本文还有配套的精品资源,点击获取