简介这是一套面向YOLO系列算法研究与工程落地的室内人头检测数据集适合目标检测初学者快速上手也适合算法工程师做模型调优与验证。资源包含927张已标注图像提供YOLO格式txt与VOC格式xml双套标签分别存放并附带已划分好的配置文件data.yaml可直接用于yolov5至yolo11等主流版本的训练、验证与测试流程。压缩包共2000个文件以927个xml与927个txt标注文件为核心另含145张jpg图和1个yaml配置整体约170.38MBtxt采用类别、中心点坐标、宽高的归一化格式xml则方便可视化检查与二次标注。已有120人学习。拿到后无需额外整理目录即可开始实验也可通过对比两种标签格式理解标注规范这套数据尤其适合室内监控、人员计数等场景的模型训练与效果验证是高性价比的数据基础。1. 室内人头检测数据集为什么 927 张带标签就够跑通 YOLO 检测门店晚上闭店后我盯着监控回放想统计这个时段走道里到底有没有人、会议室使用率怎么样。这时候如果做人脸识别既涉及隐私又容易被帽子、口罩和低头动作干扰反而是“只检测头顶到下巴这坨区域”的人头检测更稳。你手上这个压缩包名写得很直白yolo算法-室内人头检测数据集-927张图像带标签-头部.zip。它是室内场景的 RGB 图像和对应的 YOLO 格式标注拿来训 YOLOv5、YOLOv8 这类目标检测网络几乎不需要额外清洗。927 张不大但足够验证一条完整链路解压、体检、标签可视化、训练、评估、导出部署。对刚开始接触 yolo 的师弟师妹或者想快速评估人头检测可行性的一线工程师这个规模比啃上万张的大数据集友好得多。有一点容易被忽略室内人头检测的难点不在“有没有人”而在头的尺度小、姿态多、遮挡多所以它逼着你同时调数据划分和训练参数而不是只跑一遍默认命令。2. 拿到 zip 之后先做数据集体检再谈训练2.1 解压与目录结构images 和 labels 的约定压缩包叫“带标签”但到手第一件事不是直接训练而是把结构摸清楚。常见 YOLO 数据集目录是 images/train、images/val、labels/train、labels/val。不过很多被人分享的小数据集是扁平的解压后只有 images 和 labels 两个平层甚至有些导出的 zip 带 train/ 和 valid/ 子目录。我先用一段命令看层级避免后面路径配错。unzip yolo算法-室内人头检测数据集-927张图像带标签-头部.zip -d head_dataset cd head_dataset find . -maxdepth 3 -type d | sort find images -type f | head -20第一条命令把压缩包解压到 head_dataset 目录避免文件散落一地。find . -maxdepth 3 -type d只列出三层以内的目录立刻就能看出有没有 train/val 分层。最后再看几张图片文件名确认扩展名是 jpg 还是 png顺便检查文件名里有没有空格或中文。这里有个容易翻车的点如果图片叫店门口cam01 123.jpg而标签叫店门口cam01 123.txtLinux 系统下空格和全角字符会干扰后续 glob 匹配训练时会出现大量找不到标签的 warning。解压后可能会遇到三种典型情况。第一种已经分好 images/train、images/val这种情况最省事直接跳到 yaml 编写。第二种只有 images 和 labels 两个平层后面需要自己按 8:2 划分。第三种train/val 目录在顶层但标签按分类器目录排列比如 labels/train/head/xxx.txt这种 YOLO 不认必须先归并成平铺的 labels 目录。无论哪种我的建议是先把整个数据集读一遍而不是只抽查一两个文件。2.2 用 Python 脚本检查标注格式与类别一致性YOLO 格式的标签是每张图一个同名 txt 文件每行五个值类别 id、归一化中心点 x、归一化中心点 y、归一化宽、归一化高。所有坐标必须落在 0 到 1 之间。这个规则一旦被破坏训练时模型学到的框就会整体漂移。我会先跑一个体检脚本统计图片和标签数量是否对得上、有没有空标签、坐标是否越界、类别 id 到底是什么。import os from collections import Counter from pathlib import Path img_dir Path(images) label_dir Path(labels) imgs sorted(img_dir.glob(*.jpg)) sorted(img_dir.glob(*.png)) labels sorted(label_dir.glob(*.txt)) print(fimages: {len(imgs)}, labels: {len(labels)}) missing [p.stem for p in imgs if not (label_dir / f{p.stem}.txt).exists()] print(missing labels:, missing[:10]) cls_count Counter() bad_lines [] empty [] for txt in labels: lines txt.read_text().strip().splitlines() if not lines: empty.append(txt.name) continue for line in lines: parts line.split() if len(parts) ! 5: bad_lines.append((txt.name, line)) continue cls parts[0] x, y, w, h map(float, parts[1:]) if not (0 x 1 and 0 y 1 and 0 w 1 and 0 h 1): bad_lines.append((txt.name, fout of range: {line})) cls_count[cls] 1 print(classes:, cls_count) print(empty labels:, empty[:10]) print(bad lines:, bad_lines[:10])这段脚本做了四件事第一统计图片和标签数量数量对不上说明有漏标第二找出缺标签的图片这些图在训练中会被自动跳过但会造成有效样本减少第三检查每一行是否恰好五个字段防止把 VOC 格式或 CSV 格式混进来第四打印类别 id 分布。如果打印出来的 classes 只有 0那后面 yaml 里 names 就写[head]如果出现 0 和 1 两种说明数据里混了 person 类需要想清楚是保留还是过滤。我最常遇到的问题是空标签文件有些打包工具会把失败的标注写成 0 字节 txt虽然不影响训练但会让验证集 mAP 变虚因为这些图其实没有正样本。2.3 标签可视化确认“人头”不是“人脸”脚本只能保证格式正确不能保证语义正确。拿到这种数据集我至少抽出十张图把标注框画出来看看。如果框只框住了脸部而不是整个头或者大量框的长宽比极度偏长那这套标签就和项目需求对不上。import cv2 from pathlib import Path def draw_yolo_boxes(image_path, label_path): img cv2.imread(str(image_path)) h, w img.shape[:2] if not label_path.exists(): return img for line in label_path.read_text().strip().splitlines(): cls, x, y, bw, bh line.split() x, y, bw, bh float(x) * w, float(y) * h, float(bw) * w, float(bh) * h x1, y1, x2, y2 int(x - bw / 2), int(y - bh / 2), int(x bw / 2), int(y bh / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, cls, (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) return img out_dir Path(visual) out_dir.mkdir(exist_okTrue) for i, img_p in enumerate(sorted(Path(images).glob(*.jpg))[:20]): vis draw_yolo_boxes(img_p, Path(labels) / f{img_p.stem}.txt) cv2.imwrite(str(out_dir / fcheck_{i:03d}.jpg), vis)这段代码把所有标注框转换成像素坐标画到原图上并保存到 visual 目录。为什么强调“保存而不是 cv2.imshow”因为很多服务器或远程开发环境没有显示器imshow 会直接报错。参数上要注意labels 里的坐标是归一化的所以必须先乘上原图宽高得到像素坐标后再算出左上角和右下角。int()截断不会影响训练因为 YOLO 在训练时会重新按网络输入尺寸缩放。可视化主要看三件事第一框是否贴合头部轮廓有没有把肩膀和上半身也框进去这决定了检测框是“头”还是“人”第二有没有漏标比如图像角落里的头没被框住漏标比错标更致命因为模型会错误地把那个位置当成背景第三有没有重复框或两个框交叠严重。室内人头数据集里人头遮挡是常态两个框交叠超过 50% 也不能直接删因为你删掉一个等于把一个可见目标强行抹除。比较合理的做法是把重叠严重的样本单独挑出来看看是标注歧义还是的确两个头离得太近。这个步骤做完再谈训练才有底气。3. 用 YOLOv8 在本地把室内人头检测跑起来3.1 环境配置Anaconda 建虚拟环境的推荐版本组合yolo v8 现在最常用的落地环境就是 Anaconda 加虚拟环境。别在 base 里直接装因为项目之间依赖容易打架。我一般建一个独立环境Python 用 3.10然后装 ultralytics 和 opencv。conda create -n head_yolo python3.10 -y conda activate head_yolo pip install ultralytics opencv-python创建完成后检查 torch 和 GPU 是否可用这一步能避免你训练到一半才发现 CUDA 没配对。python -c import torch; print(torch.__version__, torch.cuda.is_available())如果torch.cuda.is_available()返回 False最常见原因是 PyTorch 装的是 CPU 版。你可以按自己机器上的 CUDA 版本重装。CUDA 12.x 的典型安装命令是pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121。注意这里不硬指哪个版本要以你机器的驱动为准。如果你只是想验证流程CPU 也能训927 张图配 640 分辨率一百轮下来大概几个小时能跑通但不适合反复调参。3.2 数据格式转换与 yaml 编写虽然压缩包名叫“带标签”不代表里面一定是 YOLO txt。我见过太多打包者把 VOC XML 直接塞进“YOLO 数据集”里。所以训练前先检查标签扩展名如果发现 XML要写脚本转成 txt。常见转换逻辑是解析 XML 里的 bndbox 坐标除以图像宽高得到归一化中心点。import xml.etree.ElementTree as ET from pathlib import Path anno_dir Path(xml_annots) out_dir Path(labels) out_dir.mkdir(exist_okTrue) CLASS_MAP {head: 0} for xml_path in anno_dir.glob(*.xml): tree ET.parse(xml_path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) out_lines [] for obj in root.findall(object): cls obj.find(name).text if cls not in CLASS_MAP: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) cx ((x1 x2) / 2) / w cy ((y1 y2) / 2) / h bw (x2 - x1) / w bh (y2 - y1) / h out_lines.append(f{CLASS_MAP[cls]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_path out_dir / f{xml_path.stem}.txt out_path.write_text(\n.join(out_lines))这段脚本的关键点有三个。第一size/width和size/height必须来自原图不是来自 XML 里某个缩略图字段否则所有框都会偏移。第二CLASS_MAP里没有的类会被跳过如果你不想要 person 类这里直接过滤掉。第三坐标精度保留六位小数足够训练时 YOLO 内部会再归一化到网格尺度过于截断会让相邻框信息丢失。转换完再跑一遍第二章的体检脚本确认没有空行和越界。标签格式没问题后写 head.yaml。训练数据路径我习惯用绝对路径避免在别的目录启动训练时找不到相对路径。train: /home/user/head_dataset/images/train val: /home/user/head_dataset/images/val nc: 1 names: [head]这里names的顺序必须和标签里的 class id 一一对应。如果标签里只有类别 0names 里只有 head 就够。如果你把 names 写成了[person, head]模型会把所有标签的类别 0 当成 person损失函数直接乱套。3.3 最小训练命令与训练参数说明用 ultralytics 官方 CLI最小训练命令是这样yolo detect train datahead.yaml modelyolov8n.pt epochs100 batch16 imgsz640 device0这条命令会把 yolov8n 预训练权重下载下来然后在 head.yaml 指定的训练集和验证集上训练。yolov8n.pt是从 COCO 预训练来的包含 person 类别的高层语义特征迁移到 head 检测上能少跑很多轮。如果不加这个权重参数而是写modelyolov8n.yaml相当于从随机初始化开始训收敛慢小数据集还容易过拟合。batch16需要在 8GB 左右显存下才安全如果你是 4GB 老卡先降到 8。imgsz640是训练输入尺寸后面会单独说它是这个人头数据集最值得调的超参数之一。训练界面会输出每个 epoch 的 box loss、cls loss、dfl loss以及验证集指标。第一次跑不用追求 mAP 多高重点看 loss 曲线box loss 前 50 轮有没有明显下降。如果 loss 一路震荡不降先回到第 2 章检查数据和标签。训练结束后best.pt 会存在当前目录下的 runs/detect/train 里后面验证和导出都用它。3.4 验证集比例别把所有 image 都拿去训练927 张图看着不多但你不能全训。如果只做训练不看验证模型在训练集上 mAP 可能很高放到监控视频里就乱框。我一般留 20% 做验证集约 185 张。验证集必须和训练集没有任何重叠这里有个坑很多人随机划分单张图片但同一个房间连续采集的相似帧很可能被拆到两边导致验证集严重泄漏。后面避坑章会展开这里先给一个最基础的划分脚本。import random, shutil from pathlib import Path random.seed(42) img_dir Path(images) label_dir Path(labels) train_dir Path(images/train) val_dir Path(images/val) train_dir.mkdir(parentsTrue, exist_okTrue) val_dir.mkdir(parentsTrue, exist_okTrue) all_images list(img_dir.glob(*.jpg)) list(img_dir.glob(*.png)) all_images [p for p in all_images if (label_dir / f{p.stem}.txt).exists()] random.shuffle(all_images) val_count int(len(all_images) * 0.2) val_images all_images[:val_count] train_images all_images[val_count:] for img in val_images: shutil.copy2(img, val_dir / img.name) shutil.copy2(label_dir / f{img.stem}.txt, val_dir / f{img.stem}.txt) for img in train_images: shutil.copy2(img, train_dir / img.name) shutil.copy2(label_dir / f{img.stem}.txt, train_dir / f{img.stem}.txt)复制而不是移动是因为后面调参时你可能还要重新划分原数据不丢才有后悔药。脚本里把 seed 固定为 42保证每次划分结果一致。复制完以后建议打印 train 和 val 的平均人头数如果 val 平均人头数比 train 低一截说明这次划分恰好把复杂场景全留在训练集里评估结果会偏乐观。更稳的做法是按场景分组后面再讲。4. 训练参数与数据增强怎么把 927 张图的潜力压出来4.1 关键超参数imgsz、batch、epochs、patience训练命令里的几个参数对室内人头检测的影响排序大概是 imgsz batch epochspatience 只是用来防止过拟合的刹车。IMGSZ 是最容易被忽略的室内摄像头往往装在两三米高的地方人离镜头远一点头部在原图里可能只有 20 到 30 个像素。如果训练输入用 416这些头在下采样后基本消失用 640 是底线我推荐直接试 960。yolo detect train datahead.yaml modelyolov8n.pt epochs150 batch12 imgsz960 patience15 device0imgsz960会让每一轮训练耗时显著变长显存占用也变大所以你看到我把 batch 降到了 12。这里的原则是在显存不爆的前提下优先保 imgszbatch 只要大于 8 就不至于让 BN 统计失真。epochs150对 927 张图来说足够因为数据量小训练 60 轮以后基本就饱和了。patience15表示验证集 mAP 连续 15 轮不涨就提前停止省下来的时间可以多做几轮实验。如果你用 Python API还可以多传optimizerSGD数据量小时 SGD 比 AdamW 更稳不容易在最后阶段震荡。4.2 数据增强坑室内人头尺度小别乱开 mosaicYOLOv8 默认开启 mosaic把四张图拼成一张。这个增强对常规物体非常有效但对小目标有点残忍每张子图缩到四分之一本来 20 像素的小头直接变成 5 像素标签边界都快糊了。我在做密集型人头检测时吃过这个亏训练 loss 降得不错但验证集上远处的小头几乎全漏。解决方法是把 mosaic 调低或者彻底关掉。ultralytics 允许在数据 yaml 里覆盖增强参数在 head.yaml 里加一段mosaic: 0.3 mixup: 0.1 perspective: 0.0001 scale: 0.6mosaic: 0.3表示只有 30% 的概率做拼接既保留一部分多尺度信息又不至于把所有小目标都毁掉。mixup: 0.1我开得很保守因为把两个人头混合成半透明图上标注框语义会变弱。perspective是透视畸变室内监控相机视角固定没必要开大。scale控制随机缩放幅度我建议保持接近默认的 0.5 附近但不要把随机缩小开过头。调这些参数不需要重写训练代码CLI 训练时会自动读取 yaml 里的增强字段。4.3 预训练模型与迁移学习要不要用 COCO 权重数据集只有 927 张从零训练绝对不是首选。COCO 预训练权重里虽然没有 head 这个类但 backbone 已经学会识别边缘、纹理、人体部件这些通用特征。你用yolov8n.pt而不是yolov8n.yaml相当于让一个见过世面的模型来适应你的数据损失函数一开始就有了合理的初始梯度。我习惯分两阶段。第一阶段冻结前 10 层只训练后面检测头跑 30 到 50 轮让模型先学会把“头”和背景分开。第二阶段解冻全部层用较小学习率微调把场景特异的特征学进来。Python API 里这样写from ultralytics import YOLO model YOLO(yolov8n.pt) model.train(datahead.yaml, epochs50, freeze10, imgsz640, batch16) model.train(datahead.yaml, epochs100, resumeTrue, lr00.001, imgsz640, batch16)freeze10是冻结网络前 10 层见名知义。第二阶段用resumeTrue从第一阶段的 best.pt 继续lr00.001比默认小一个量级避免把已经学好的特征破坏掉。这个做法的核心逻辑是预训练权重是低层特征的金矿只有当你确认新的数据分布和 COCO 差异极大时才值得全部解冻重训。室内人头场景算差异不算极大因为 COCO 里有大量 person 样本。4.4 损失函数CIoU 与分类损失对遮挡的影响YOLOv8 的边框损失主要用 CIoU并配上 DFL 分布聚焦损失。CIoU 会同时考虑预测框和真实框的重叠面积、中心点距离、宽高比。对室内人头来说最大的敌人是遮挡两个人近距离交叠标注框可能只标了可见部分CIoU 会强制模型把框的中心落在可见区域的中心这其实是合理的。真正会让模型“漏检”的不是损失函数本身而是损失权重失衡。你在 ultralytics 的配置里能看到box_loss_gain、cls_loss_gain、dfl_loss_gain这三个值。如果发现验证集上 Recall 特别低漏检一堆人头可以先把box_loss_gain稍微调高比如从默认的 7.5 调到 8.5让模型更在意框的完整程度。但我的经验是先别急着调损失系数先回数据层面把遮挡严重的样本可视化看看标注框是不是只标了光洁的部分导致回归目标本身就存在歧义。把歧义样本修掉比调参数收益大得多。分类损失同理数据集只有 head 一个类别分类压力很小如果 cls loss 占比过高反而会让模型倾向于保守把困难的头判成背景。5. 避坑与常见问题927 张带标签数据集的落地清单5.1 标注格式混用TXT 与 XML 的转换坑现象训练刚开始终端刷出一大片WARNING: image ... cannot load label file或者 loss 一路不变。原因压缩包解压后标签实际是 XML 格式文件名虽然以 txt 结尾但内容不是五段式也可能是标签文本里混了回车之外的不可见字符Python 读取后拆出来的字段数量超过五个。解决先用head -5 任意标签.txt看内容。如果看到annotation就用第 3.2 节的转换脚本统一转成 YOLO txt。如果看到多行里出现 6 个以上字段八成是 CSV 转出来的按空格分割后需要截断多余列。另外注意类别名大小写Linux 下head和Head是两个不同字符串转脚本里要么统一小写要么用字典做映射否则类别 id 会错位。5.2 图像尺寸不一致导致矩形框偏移现象可视化阶段画出框都在人头的斜上方或侧面训练哪怕跑了 100 轮mAP 也低得奇怪。原因YOLO 标签是归一化坐标但打包者可能在不同阶段对图片做了 resize。比如一部分图像先被统一缩放到 1280×720而标签是在原始 1920×1080 下生成的两者坐标比例对不上。还有一种情况是同一张图存在 .jpg 和 .png 两份训练读的是 png标签坐标却是按 jpg 尺寸算的。解决不要只信压缩包逐张读图实际尺寸重新生成标签。写一个修正脚本先对所有图片跑一遍cv2.imread拿到真实高宽再根据原始宽高比例把标签里的中心点还原。这一步很枯燥但值得做因为坐标漂移的数据集无论换什么模型都不会收敛。我发现这类坑经常出现在“二手转发的数据集”里原始分享者自己可能也不知道坐标已经失效。5.3 小目标漏检人头只有十几个像素怎么办现象模型能检测画面中央的大人头但角落里两三个头完全漏检验证集 mAP0.5 不低可是 mAP0.5:0.95 很低或者 AP_small 几乎是 0。原因网络最后一层特征图下采样倍数高18 像素的小头在 stride 32 的网格上连一个格子都占不满模型根本拿不到足够信息。再加上 mosaic 增强把图像缩小问题雪上加霜。解决第一把 imgsz 从 640 提到 960让输入分辨率接近原图小目标占据更多像素第二按 4.2 节把 mosaic 关小避免目标进一步缩小第三如果显存允许可以换成 yolov8s 或 yolov8m更大的模型有更多通道能捕捉细节。注意不要在验证时乱改 NMS 阈值降低 confidence 到 0.1 确实能召回更多小目标但也会引入大量背景误检。最好先通过训练数据火力覆盖解决而不是靠后处理兜底。5.4 训练中断、显存不够与随机种子现象训练到第 40 轮GPU 直接 OOM 退出重新训练时每次结果都不一样两次实验之间很难比较。原因imgsz 和 batch 配得太激进8GB 卡硬跑 960 分辨率加 batch 16显存必然爆。随机种子没固定时数据增强和数据加载顺序每次都变模型收敛路径也不同导致你分不清是参数改动带来的提升还是随机性造成的波动。解决遇到 OOM 时优先降 batch 而不是降 imgsz因为小目标检测受分辨率影响更大。如果降 batch 到 4 还是爆再降 imgsz。固定随机种子在 ultralytics 里可以直接在命令行加seed42。中断后别从头再来用model.train(resumeTrue)接着跑代码会从 runs/detect/train 里的上次权重继续。养成固定 seed 的习惯后每次实验的置信度才有可比性。5.5 评估指标虚高val split 与训练集重叠现象训练 loss 降到 0.3验证集 mAP 冲到 0.99但把 best.pt 拿到另一个房间的监控视频里测试连续几十帧都在乱出框。原因划分验证集时用了纯随机抽样而数据集里同一个场景的连续帧被同时分到了 train 和 val。模型实际上已经“见过”验证帧里几乎相同的人头位置评估指标自然虚高。这是小数据集最常见的水分来源。解决划分前按场景分组。如果文件名里有场景前缀比如floor1_cam1_001.jpg就取前两段作为分组 key同一个 key 的图全部进 train 或全部进 val。脚本里做个映射即可from collections import defaultdict def scene_key(name): # 按实际命名约定调整例如返回 floor1_cam1 parts name.split(_) return _.join(parts[:2]) groups defaultdict(list) for img in all_images: groups[scene_key(img.stem)].append(img)这样做虽然会损失一部分训练样本同一场景的验证图不参与训练但评估结果才是真实水平。如果数据集没有命名分组信息只能按相似度聚类后拆分工作量会大很多。我个人的经验是一个诚实但低一点的 mAP比虚高的指标更能帮你判断这套数据集到底能不能用。6. 从 927 张到 927×N验证模型泛化性的三种做法6.1 K 折交叉验证小数据集的诚实评估927 张只分一次 train/val结论容易受那次随机划分影响。更可靠的做法是 5 折交叉验证把数据按场景分成五份每轮拿一份做验证其余四份训练最后看五轮 mAP 均值和方差。代码上可以直接用 sklearn 的 GroupKFoldgroup 参数传场景分组 key确保同一个房间不会被拆到不同折里。五轮训练会让总耗时翻五倍但考虑到每个 epoch 才 100 步左右这个成本是值得的。from sklearn.model_selection import GroupKFold for fold, (train_idx, val_idx) in enumerate(split): print(ffold {fold}, train {len(train_idx)}, val {len(val_idx)})每折的验证结果取平均能过滤掉“这轮验证集恰好简单”带来的幻觉。我做完 K 折以后通常还会选 mAP 最高的一折权重做部署而不是非要平均权重。6.2 用历史视频帧补充难例927 张图训练出的模型大概率会在某一个具体门店场景里表现尚可换到另一个楼层马上退化。要把它用到 927×N就得从现场视频里抽帧做半自动标注。先跑一个现有模型在连续帧上预测把置信度低于 0.5 但画面里确有头部的帧挑出来人工修正后再加入训练集。抽帧不要每帧都抽同一场景每秒抽一帧就够否则会把大量高度相似的难例塞进训练集反而造成过拟合。6.3 导出 ONNX 后用部署端验证训练出来的 best.pt 是 PyTorch 权重实际部署往往要转成 ONNX。用 ultralytics 导出非常直接yolo export modelbest.pt formatonnx opset12 imgsz640导出后用 onnxruntime 在几段真实监控视频上跑推理统计误检和漏检。这一步能暴露训练时看不出的问题比如某些框架对动态尺寸支持不好框会在边缘抖动或者某个摄像头图像偏暗模型在低对比度下召回崩溃。先验证再铺开比直接上线省心得多。我自己的习惯是任何小数据集项目都要留三样东西划分脚本、体检脚本、可视化输出。没有这三样半年后回头连自己都说不清当初哪些图进过验证集。这套流程从 927 张开始慢慢滚成几千张泛化性才有保障。希望帮到你。本文还有配套的精品资源点击获取