ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

小辣椒小彩椒检测数据集处理与YOLOv8训练部署全攻略

小辣椒小彩椒检测数据集处理与YOLOv8训练部署全攻略 简介小辣椒小彩椒检测数据集共有2292张实地拍摄的辣椒作物图像聚焦农业目标检测、果实计数和成熟度分布分析适合计算机视觉研究者、农业智能化开发人员以及需要训练检测模型的学生与工程师。数据采用Pascal VOC与YOLO双格式标注每张图片配有对应的XML标注文件和TXT标注文件免去格式转换步骤可接入主流检测框架标注类别为果实、树木、背景三类果实目标框达19293个背景框6240个树木框2627个总计28160个标注框信息完整方便划分训练集、验证集以及开展模型性能对比。压缩包采用7z格式共包含2000个文件以1999个XML标注文件为主体并附带1个说明文档用于辅助理解目录结构和标注统计口径整体大小约272.99MB下载后可直接解压使用。目前已有175人学习下载该数据集尤其适合小辣椒、小彩椒这类小目标密集、枝叶遮挡场景的检测算法调优与评测也可作为目标检测教学实验的练习数据帮助快速上手数据准备与模型迭代流程。1. 小辣椒小彩椒检测数据集2292张3类别真正值钱的不只是图片数量小辣椒小彩椒检测数据集2292张图片、3个类别这规模放在目标检测里不算大但对农产品分拣和田间巡检来说属于“够跑通一个靠谱模型”的典型量级。做这类数据集处理时真正值钱的不是那张图片总数而是类别定义是否清晰、标注有没有错漏、以及目标尺寸分布适不适合你手里的部署场景。小辣椒簇生且互相遮挡小彩椒颜色饱和度高、反光严重这两类与背景的边界经常比想象中模糊。这篇文章按“先摸格式、再转标注、训练调参、最后上分拣线”的顺序把一条能直接照做的落地路径写清楚适合正在做目标检测数据集处理、或准备用 YOLOv8 跑农产品识别的工程师参考。2. 先摸清2292张数据的底细目录、标注格式与抽样检查拿到数据集的第一件事不是开训练而是搞清楚标注文件到底是什么格式、有没有缺图、类别编号是否连续。很多翻车现场都始于这一步被跳过等训练跑到一半发现 mAP 是 0再回头查 label白白浪费十几个小时。2.1 打开压缩包先别跑用10分钟做目录与标注格式摸底常见的检测数据集标注有三种VOC XML、YOLO TXT、COCO JSON。这2292张数据包打开后先别急着写训练脚本用两三条命令就能把底摸清楚find /path/to/chili_dataset -type f | sed s/.*\.// | sort | uniq -c find /path/to/chili_dataset -name *.xml | wc -l find /path/to/chili_dataset -name *.txt | wc -l第一条命令统计所有文件的扩展名分布第二条、第三条分别确认 XML 和 TXT 标注的数量。如果 XML 数量接近图片数量说明大概率是 VOC 格式如果 TXT 数量接近说明已经是 YOLO 格式如果只有 JSON那就是 COCO。这个判断决定了后面的转换脚本怎么写。拿到标注格式后还要确认一件事有没有 class 列表文件。常见压缩包里的文件结构大致如下文件/目录作用images/ 或 JPEGImages/原始图片annotations/ 或 labels/标注文件classes.txt / class_names.txt类别顺序定义README.txt采集说明、类别含义classes.txt 非常重要YOLO 训练时类别编号完全依赖这个顺序。如果压缩包里没有就从 XML 里提取所有name标签看看一共有几种写法。类名大小写不一致比如Red_Chili和red_chili也经常出现后面转换时统一处理。2.2 标注质量抽检把3类框画到图上才能看到错误统计完格式马上做标注抽检。我的习惯是写一个可视化脚本把每张图的标注框直接画到图上随机抽 50 张翻一遍。框错位、类别标反、重复框、漏框肉眼扫一遍比看任何统计数字都直观。import xml.etree.ElementTree as ET import cv2 import glob import os def draw_voc_boxes(image_dir, xml_dir, out_dir, color_mapNone): os.makedirs(out_dir, exist_okTrue) xml_list sorted(glob.glob(os.path.join(xml_dir, *.xml))) for idx, xml_path in enumerate(xml_list): tree ET.parse(xml_path) root tree.getroot() img_name os.path.basename(root.find(filename).text) img_path os.path.join(image_dir, img_name) img cv2.imread(img_path) if img is None: print(f[skip] {img_name} 找不到对应图片) continue for obj in root.iter(object): name obj.find(name).text.strip() box obj.find(bndbox) x1 int(float(box.find(xmin).text)) y1 int(float(box.find(ymin).text)) x2 int(float(box.find(xmax).text)) y2 int(float(box.find(ymax).text)) color color_map.get(name, (0, 255, 0)) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, name, (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) out_path os.path.join(out_dir, fcheck_{idx:04d}_{img_name}) cv2.imwrite(out_path, img) if __name__ __main__: draw_voc_boxes( image_dir./images, xml_dir./annotations, out_dir./check_output, color_map{ red_chili: (0, 0, 255), # OpenCV 是 BGR这里是红色 green_chili: (0, 255, 0), # 绿色 colored_pepper: (255, 0, 255) # 紫色 } )这段脚本的逻辑很直白遍历所有 XML解析 filename 找到图片把每个 object 的 bndbox 画上去类别名写在框上方。输出到 check_output 目录翻两遍就能看出大多数问题。两个参数要留意一是color_map里 OpenCV 用的是 BGR 顺序红是(0,0,255)反了会颜色错乱二是 XML 里的 filename 有时带子目录前缀最好用os.path.basename处理一下否则图片路径拼接会失败。如果标注是 YOLO TXT 格式这段脚本不能直接用需要先从 TXT 解析出坐标再画框。建议先按 3.1 的脚本把格式统一再回来做可视化。2.3 类别比例与目标尺寸分布决定你后面要不要重采样可视化抽查之后做一次量化统计。小彩椒和小辣椒在视觉上相似度不低如果某一类标注框数量明显少于另外两类训练时模型很容易偏向多数类。目标尺寸分布也直接决定训练分辨率怎么设。import xml.etree.ElementTree as ET import glob import os def analyze_voc_boxes(xml_dir, image_dir): class_counter {} size_counter {small: 0, mid: 0, large: 0} total_boxes 0 for xml_path in sorted(glob.glob(os.path.join(xml_dir, *.xml))): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) for obj in root.iter(object): name obj.find(name).text.strip() 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) w x2 - x1 h y2 - y1 class_counter[name] class_counter.get(name, 0) 1 total_boxes 1 if w 32 and h 32: size_counter[small] 1 elif w 96 and h 96: size_counter[mid] 1 else: size_counter[large] 1 print(类别统计:, class_counter, 总框数:, total_boxes) print(尺寸分布:, size_counter) if __name__ __main__: analyze_voc_boxes(./annotations, ./images)这段脚本统计两个东西每个类别的标注框数量以及小、中、大目标的占比以 32x32 和 96x96 为分界这是目标检测里比较常用的划分口径。小目标占比高训练时 imgsz 就要往上提某类框数量过少后面就要考虑重采样或 copy-paste 增强。2292 张图按 3 类均分每类约 700 多张但每张图里可能有多个辣椒实例所以总框数会远大于 2292。如果统计出来某类只有 300 个框那这类大概率会拖后腿。3. 把标注转成YOLO能吃的格式VOC转YOLO脚本与训练集划分YOLO 训练需要的是 TXT 格式的标注每行一个目标类别编号、归一化后的中心点 x、中心点 y、宽、高。如果拿到的数据包是 VOC XML就必须先转换。这一步不复杂但边界情况很多转换前先想清楚类别顺序转换后一定要抽检。3.1 从VOC XML到YOLO TXT一个能直接跑的转换脚本转换的核心是把像素坐标归一化到 [0,1] 区间。这个脚本可以处理多类、未闭合边框、图片尺寸读取三种常见情况。import glob import os import xml.etree.ElementTree as ET # 这个顺序必须和后面的 dataset.yaml 保持一致 CLASSES [red_chili, green_chili, colored_pepper] def voc_to_yolo(xml_dir, image_dir, out_label_dir): os.makedirs(out_label_dir, exist_okTrue) for xml_path in sorted(glob.glob(os.path.join(xml_dir, *.xml))): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) if img_w 0 or img_h 0: print(f[skip] {xml_path} 图片尺寸无效) continue out_txt os.path.join( out_label_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt ) lines [] for obj in root.iter(object): name obj.find(name).text.strip() if name not in CLASSES: print(f[skip] 未知类别 {name} in {xml_path}) 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) if x2 x1 or y2 y1: continue x_center ((x1 x2) / 2.0) / img_w y_center ((y1 y2) / 2.0) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{CLASSES.index(name)} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(lines)) print(f[ok] {os.path.basename(xml_path)} - {len(lines)} boxes) if __name__ __main__: voc_to_yolo(./annotations, ./images, ./labels)几个关键点尺寸用 XML 里的 size 节点而不是cv2.imread去读图因为读大图会浪费时间而且 XML 里记录的尺寸通常就是标注时的原图尺寸类别名 strip 掉首尾空格避免red_chili和red_chili被当成两个类if x2 x1 or y2 y1直接过滤非法框这种框在人工标注里不少见。所有坐标统一做 clip 到 [0,1]是因为少数标注会把框的中心或宽高写到图像外不 clip 会让 YOLO 训练报 NaN。转换完成后随便打开一个生成的 TXT 看一眼格式应该类似0 0.523412 0.441203 0.152301 0.233442 1 0.612345 0.521334 0.101234 0.181234每行五个数字类别编号必须在[0, nc-1]之间。注意编号从 0 开始不是从 1 开始。这一步出错后面训练 mAP 永远为 0。3.2 按图片划分训练/验证集脚本与三种划分策略划分数据集的原则只有一条同一张图片不能同时出现在训练集和验证集里。听起来是废话但很多人直接按标注文件划分一张图有两个标注文件前面一半进训练后面一半进验证结果就是数据泄露验证指标虚高到不敢信。import os import random import shutil IMG_DIR ./images LBL_DIR ./labels OUT_DIR ./split RATIO (0.85, 0.10, 0.05) SEED 2024 def split_dataset(img_dir, lbl_dir, out_dir, ratio, seed): names [ os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg, .png)) ] random.Random(seed).shuffle(names) n len(names) n_train int(n * ratio[0]) n_val int(n * ratio[1]) n_test n - n_train - n_val groups { train: names[:n_train], val: names[n_train:n_train n_val], test: names[n_train n_val:], } for split_name, name_list in groups.items(): img_out os.path.join(out_dir, split_name, images) lbl_out os.path.join(out_dir, split_name, labels) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for name in name_list: img_src os.path.join(img_dir, name .jpg) lbl_src os.path.join(lbl_dir, name .txt) if os.path.exists(img_src): shutil.copy2(img_src, img_out) if os.path.exists(lbl_src): shutil.copy2(lbl_src, lbl_out) if __name__ __main__: split_dataset(IMG_DIR, LBL_DIR, OUT_DIR, RATIO, SEED)这里用的是复制而不是移动保留原始数据作为备份。数据集的划分比例2292 张图我的常用配置是 85% 训练、10% 验证、5% 测试也就是约 1948 / 229 / 115 张。对这个小数据集测试集留 5% 只做最终评估平时调参只看验证集。SEED固定成 2024保证每次复现结果一样后面想对比增强策略时这个固定种子就是唯一的控制变量。三种划分策略里随机划分最简单但如果图片是按地块或按天采集的同一株辣椒的多个角度可能同时出现在训练和验证里指标会虚高。更严谨的做法是按采集批次分组划分把相同时间、相同地块的图片归到一个包里再分。不过这需要数据包里带场景信息拿到数据后先看 README 有没有这类字段有就按组划分没有就用随机划分加固定种子。3.3 写dataset.yaml与数据增强参数2292张的增强尺度怎么控划分完目录写 YOLO 训练用的 dataset.yaml。这个文件是所有训练命令的入口路径写错或类别顺序不对训练直接崩。# chili.yaml train: ./split/train/images val: ./split/val/images test: ./split/test/images nc: 3 names: 0: red_chili 1: green_chili 2: colored_pepper两个坑提醒一下路径建议写绝对路径相对路径依赖你执行 yolo 命令时的工作目录换个终端就找不到数据Windows 下不要直接写C:\chili\images这种带反斜杠和冒号的路径YAML 会把C:解析成奇怪的键值对最好换成 Linux 容器或转成正斜杠写法。数据增强这块2292 张属于中小型数据集增强要做但不能做过头。我自己在这个数据集上的常用参数如下参数建议值说明hsv_h0.005 ~ 0.01红色系对色相偏移极为敏感默认 0.015 会把红辣椒变成黄辣椒hsv_s0.5 ~ 0.7饱和度增强对辣椒这种高饱和目标有效但别拉满hsv_v0.3 ~ 0.4亮度扰动模拟田间光照变化比色相安全mosaic1.0前中期开最后 10 个 epoch 关掉让小目标稳定收敛copy_paste0.1 ~ 0.3框多时很有用能缓解遮挡和实例不平衡flipud0.0 ~ 0.1辣椒不会倒着长垂直翻转少开degrees0 ~ 10小角度旋转模拟摄像头安装偏差别超过 15 度这里的逻辑是农业场景下相机角度变化有限旋转增强过了会让模型学到错误的姿态先验。色相增强尤其要克制因为红色小辣椒、黄色彩椒、绿色辣椒在 HSV 空间里距离很近色相扰动过强会直接把类间边界搅乱。如果你用的是 Ultralytics YOLOv8这些参数可以直接写进训练命令也可以写到 yaml 里统一管理。4. 训练参数怎么设YOLOv8上的模型选型、超参与评估指标数据准备好了接下来是训练。很多人拿到模型就默认 YOLOv8n 起步但 2292 张图、3 个类、目标多为中等大小n 往往欠拟合s 才是性价比较高的起步点。4.1 为什么我从YOLOv8s起步而不是n、m或l在 2292 张的数据规模下模型体量选择的本质是平衡拟合能力和过拟合风险。YOLOv8n 参数量最少训练快但对辣椒这种形状多变、相互遮挡的目标容易欠拟合YOLOv8m 和 l 表达能力更强可数据量不够训练后期验证 mAP 会原地踏步甚至回退。模型相对计算量显存占用batch16, imgsz640适用性判断YOLOv8n最轻约 4GB边缘设备优先但小辣椒这种密集小目标容易表现不足YOLOv8s适中约 6~8GB这个数据规模最稳的起步点推荐YOLOv8m较重约 10~12GB迁移学习基础好、数据量大再考虑否则容易过拟合我的习惯是从预训练权重yolov8s.pt开始练而不是从空的yolov8s.yaml开始。预训练权重在 COCO 上学到的通用特征尤其边缘、纹理和颜色表征对辣椒这种视觉目标帮助很大能明显加快收敛也能缓解数据量不足的问题。4.2 训练命令与关键参数epochs、batch、imgsz、mosaic怎么选拿 Ultralytics YOLOv8 举例训练命令我一般长这样yolo detect train \ data./chili.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ device0 \ workers8 \ patience30 \ close_mosaic10 \ project./runs/chili \ nameexp1逐个说参数epochs200对 2292 张属于够用且不过量的值配合早期停止patience30验证集指标连续 30 个 epoch 不涨就自动停imgsz640是 3 类中等目标的标准配置如果你的尺寸分布统计显示小目标占比很高再提到 768batch16取决于显存12GB 显卡建议从 16 起步workers8是数据加载线程Linux 下开满没问题Windows 下容易报错降到 2~4close_mosaic10表示最后 10 个 epoch 关闭 mosaic 增强这是稳定收敛的小技巧因为 mosaic 拼图里的目标尺度变化太剧烈后期需要回归到真实尺度分布。训练中断了不要慌续训命令yolo detect train resumeTrue project./runs/chili nameexp1这个resume是后悔药前提是项目目录里还有上次的 weights 和超参数记录。训练过程中如果发现 loss 是 NaN大概率是数据里有空标注或坐标异常回到 2.2 的抽检脚本重新查不要继续调参数。4.3 数值指标怎么读mAP50和PR曲线能告诉你什么训练完看 results.csv里面每一行对应一个 epoch 的指标。关注三列metrics/mAP50(B)、metrics/mAP50-95(B)、metrics/precision(B)和metrics/recall(B)。mAP50 是 IoU 阈值 0.5 下的平均精度mAP50-95 是 0.5 到 0.95 多个阈值下的平均后者对预测框的位置精度更敏感。对分拣线场景我的判断习惯是先用 mAP50 确认“能不能找到辣椒”再用 PR 曲线确认“误检能不能接受”。PR 曲线可以从验证集预测结果里画出来固定一个置信度阈值比如 0.25看 precision 和 recall 的平衡点。在runs/chili/exp1/下会生成confusion_matrix.png这个图直接展示每个真实类别被预测成了什么。小辣椒和小彩椒如果互相误检图谱上这两格的数字会很扎眼这比单纯看 mAP 数字更直观。如果某一类 recall 明显低说明该类漏检严重回到第 2.3 节检查类别框数是不是太少而不是盲目增加训练轮数。5. 辣椒检测的避坑清单从标签编号到HSV增强的五个翻车点这块内容全部来自我在类似农产品检测数据集上的血泪经验。每一条都是“现象 → 原因 → 解决”的结构遇到类似问题可以直接对照排查。5.1 训练loss一直掉mAP却是0现象训练日志里 loss 稳步下降但验证集 mAP 一直是 0模型预测输出里全是空框。原因最常见的是标签类别编号从 1 开始写比如1 0.52 0.44 0.15 0.23但nc: 3的模型只认[0, 1, 2]。编号 3 超出类别范围YOLO 在计算损失时把这些框全部忽略。另一个常见原因是数据增强后的某一类标签全为空验证集里所有标注都没被正确读取。解决先打印任意一张验证图片对应的标签文件确认编号范围在[0, nc-1]然后跑一次验证集预测yolo detect predict modelruns/chili/exp1/weights/best.pt source./split/val/images save_txtTrue如果生成的预测 txt 全部为空基本可以确定是标签编号错位。回看 3.1 的转换脚本确认CLASSES.index(name)得到的确实是 0 开头。5.2 红色小辣椒和红色彩椒互相误检现象验证集上红色小辣椒的预测框经常套在红色彩椒身上反之亦然单看 AP 值两个类都一般。原因本质上这两类在颜色特征上几乎重合。小辣椒和彩椒都有红色品种如果采集时类别定义只按颜色区分模型学不到可分的形状特征。小辣椒细长、彩椒接近圆球形但训练数据里如果类内形状差异大模型就容易只抓颜色这个最强特征。解决先看 confusion_matrix.png确认是这两个类互混。最简单的方案是训练后处理在推理端加一个长宽比过滤规则细长框判定为小辣椒接近正方形的框判定为彩椒这比修改模型便宜得多。更根治的做法是重新定义类别比如按“红色细长椒/红色圆椒”而不是“小辣椒/小彩椒”或者干脆合并成一类再用第二个分类器处理等级划分。5.3 显存爆掉你以为只是batch太大现象batch16、imgsz640 在 12GB 显卡上训练到一半 OOM甚至刚启动就报错。原因除了模型和图片本身mosaic 增强会把 4 张图拼成一张大图参与前向传播显存消耗远高于单张 640x640 的推理。另外 Ultralytics 默认开启 AMP但某些显卡驱动组合下 AMP 不一定生效OOM 就会来得更早。验证集的前向推理也会占用显存如果验证集图片分辨率很高同样会爆。解决先把 batch 减半到 8如果还爆就把 imgsz 降到 608再不行就加cacheFalse显式关闭缓存因为缓存的是内存不是显存但会间接影响加载效率。最后还可以把训练和验证分开训练完单独跑一次yolo detect val避免训练过程中的验证阶段抢显存。OOM 的排查顺序是 batch - imgsz - AMP - cache。5.4 训练图片近景多部署后远处小辣椒全漏现象测试集 mAP50 不低但把模型放到分拣线或田间巡检摄像头上距离稍远的小目标几乎全部漏检。原因训练集里近景大目标占比过高损失函数被大目标主导模型在小目标特征上的能力没有被充分优化。尤其辣椒这类数据采集时离果实半米内拍标注框可能占整张图的一半部署相机装在 2 米高处一个辣椒只有 30x30 像素尺度差异太大。解决先用第 2.3 节的统计脚本看目标尺寸分布。如果小目标占比低于 20%两个办法一是把 imgsz 从 640 提到 768 甚至 896小目标在特征图上的像素点变多模型有机会学到细节二是推理时用切图检测把大图切成 640x640 的小块分别推理再合并结果这是做检测数据集处理时解决小目标漏检最稳的手段。当然最根本的还是补一批远距离拍摄的数据标注几十张都比增强有效。5.5 HSV增强把红辣椒变成了黄辣椒现象训练日志里的训练集预览图里红色小辣椒有的是橙色有的接近黄色和彩椒类混在一起。训练结束后红辣椒类的 precision 明显低于其他类。原因Ultralytics 默认hsv_h0.015这个值对大多数目标没问题但红色在 HSV 色相空间里紧挨着橙色和黄色0.015 的随机偏移已经足够让红的变橙、变黄。辣椒不是交通标志颜色是它的核心辨识特征色相一旦被增强破坏模型就会丢失最重要的线索。解决把hsv_h降到0.005甚至0.0用hsv_s和hsv_v的增强来替代色相扰动。饱和度增强可以模拟不同光照下的颜色浓郁程度亮度增强可以模拟顺光和逆光这两个维度对辣椒识别是安全的。如果你用 Albumentations 做外部增强优先选随机亮度和对比度少动色相。这个参数配置我在 3.3 的增强表格里已经给了具体值直接抄即可。6. 把模型送上分拣线导出、量化与产线验证训练告一段落模型如果只在测试集上指标好看那离上线还差一半。分拣线部署要过两关推理速度够不够、真实场景里有没有误检漏检。6.1 导出ONNX把dynamic batch打开yolo export modelruns/chili/exp1/weights/best.pt formatonnx dynamicTrue opset12 simplifyTruedynamicTrue让 batch 维度可变现场推理时一次处理几张图都灵活opset12是边缘设备兼容性比较好的版本opset 太高老 GPU 或端侧 NPU 可能不认simplifyTrue会去掉一些冗余计算节点体积更小、推理略快。导出后在 Python 里确认输入输出import onnxruntime as ort sess ort.InferenceSession(best.onnx) print(sess.get_inputs()[0].shape) print(sess.get_inputs()[0].type)打印出来输入 shape 应该带None的 batch 维度。这里要留意导出后最好用同一张图分别跑 PyTorch 模型和 ONNX 模型对比输出框的置信度差异。两边输出如果差超过 0.01大概率是导出过程出了问题最常见的是预处理比如归一化参数没有对齐。6.2 用10分钟产线视频抽帧回测别只信mAP我见不少人拿测试集 mAP 交代模型效果结果一上产线就翻车。测试集再怎么分都是离线数据没有现场的光照变化、黑色塑料薄膜背景和运动模糊。我的习惯是拉一段 10 分钟的产线视频按帧抽块先跑一遍模型看看真实表现。import cv2 cap cv2.VideoCapture(field_day.mp4) frame_id 0 while True: ret, frame cap.read() if not ret: break if frame_id % 10 0: cv2.imwrite(fframes/{frame_id:06d}.jpg, frame) frame_id 1 cap.release()抽帧保存后用固定置信度阈值对抽出的每一帧推理统计两个数字漏检率标注过的辣椒没被框出来和误检率框出来的不是辣椒。分拣线这种场景漏检率比误检率更致命漏掉一个辣椒意味着分拣错误误检一次可能只是多一次复核。我自己的验收标准是单帧推理延迟低于部署设备预算的一半漏检率低于 1%误检率低于 5%才敢说模型真的能上线。这是我第一次做农业检测时踩过的坑。当时只看测试集 mAP50 有 0.93觉得稳了结果现场一跑远处小辣椒漏了一半FPS 也达不到分拣节拍。后来把验收指标改成产线视频抽帧回测加单帧耗时统计才真正让模型在分拣线上跑起来。希望这些弯路能帮你省几个通宵。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进