简介:鸵鸟目标检测数据集是一份面向目标检测任务与模型训练的高质量标注资源,适合计算机视觉学习者、算法工程师及无人机巡检、毕业设计、科研实验等相关项目使用。压缩包采用rar格式打包,内部共1258个文件,包含419张jpg原图、419个xml标注文件以及420个txt标注文件,覆盖VOC与YOLO两种主流格式,整体大小约43.15MB,解压后可直接被LabelImg、YOLO系列脚本等工具读取,且无需解压密码。目前已有74人学习下载,数据经过整理,拿到后即可用于模型训练与测试。数据由LabelImg手工标注,类别统一为ostrich,标注过程强调目标边界准确、图像内目标无遗漏,并经过一致性检查,因此能有效减少训练时的标注噪声。此外,文件目录明确区分图片、xml和txt三个文件夹,便于按格式筛选使用,适合直接用于目标检测模型的训练、验证与对比实验。
1. 419张鸵鸟目标标注数据集,到底能用来做什么
拿到一份“鸵鸟数据集 VOC和YOLO格式目标标注419张左右”的项目材料,最该先想清楚的不是怎么训练,而是这 419 张图够干什么。很多做检测的同行拿到小数据集第一反应是直接开训,结果 loss 震荡、mAP 上不去,最后把锅甩给网络结构。实际上,这类单一物种、双格式标注的小数据集,最适合的用途是:把“数据怎么采集、怎么标注、怎么转格式、怎么喂给 YOLO 训练”这条路完整跑通,顺便验证迁移学习在垂直小目标场景里的下限。鸵鸟这种目标体型大、背景相对可控,比标注行人、车辆这种多类别数据集省心得多,对做鸟类识别系统、养殖场监测、毕设演示的开发者都友好。接下来就把这套数据的制作、转换和训练链路拆开讲清楚。
2. 从原始图片到VOC标注:采集清洗、LabelImg画框与XML结构校验
2.1 采集和清洗:决定后面所有工作量的一步
数据集里标注质量差,九成是原始图片没筛干净。419 张左右这个规模,常见做法是准备 600 张以上的原始图,清洗掉模糊、重复、严重过曝的,最后留下 400 张出头。清洗标准就三条:目标主体清晰可辨、光照不过暗、图片不是同一场景的连续重复帧。鸵鸟的警戒姿态和行走姿态差别很大,采集时要有意保留远景、近景、侧身、背对镜头这几种构图。如果是用监控视频抽帧,每 5 到 10 秒抽一帧,避免连续帧带来的数据冗余——这一点直接影响后面训练集和验证集划分的合理性。
图片格式也要统一。我一般会先把所有图转成 .jpg,分辨率不低于 640x640,文件名改成纯英文小写加数字,比如 ostrich_001.jpg。这个步骤看起来多余,但在 Windows 上训练时,中文路径和空格会引发一堆玄学错误,后面第 4 章会专门讲。做完清洗,把图片统一放进 JPEGImages 目录,就可以开始标注。
2.2 用LabelImg产出VOC标注:老牌工具的操作要点
目标检测常用标注工具里,LabelImg 最老牌也最省事,原生支持输出 VOC 格式的 XML,对做中小型数据集的人来说是效率最高的选择。安装就一条命令:
pip install labelimg装完后启动前,先在项目目录建一个 predefined_classes.txt,里面写一行 ostrich,这样画框时类别就是写死的,不用每次下拉选择。启动命令:
labelimg ./JPEGImages ./Annotations predefined_classes.txt左侧目录选 JPEGImages,保存目录选 Annotations,右上角格式切到 PascalVOC。标注快捷键要记牢:W 开始画框,A/D 翻页,Ctrl+S 保存。画鸵鸟这种大目标,框的边界要尽量贴住目标外沿,把脖子和腿都包进去,但不要把大片草地背景框进来。我的习惯是开启界面右上角的自动保存,每画完一框就落盘,避免软件崩了白干。
LabelImg 有个要注意的细节:它允许你把框拖出图片边界,这样产生的越界框在转 YOLO 时会算出大于 1 的归一化坐标,训练直接翻车。后面章节的转换脚本里会做钳制处理,但标注时能避免就避免。一个经验是标注完一张图后,看一眼右侧 XML 里的 bndbox 数值是否都在图片宽高范围内。多人协作标注时,还要统一规则:遮挡超过 50% 的鸵鸟不标,只标可见部分完整的目标。
2.3 VOC的XML结构:读懂一个标签文件就等于读懂了格式
VOC 格式的每个标注文件对应一张图片,同名 XML 里记录了图片信息、目标类别和矩形框坐标。下面这张是典型的鸵鸟标注 XML:
<annotation> <folder>JPEGImages</folder> <filename>ostrich_001.jpg</filename> <source> <database>Unknown</database> </source> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>ostrich</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>312</xmin> <ymin>98</ymin> <xmax>874</xmax> <ymax>612</ymax> </bndbox> </object> </annotation>理解这个结构的关键是:size 节点里的宽高是这张图真正的像素尺寸,bndbox 里的四个坐标是绝对像素坐标,单位是像素。后面转 YOLO 格式时,归一化要用的就是这两个数据。很多人在转换脚本里直接读 XML 里的 size,但 XML 里的 size 可能和实际图片不一致(比如图片被后期压缩、旋转过),最稳的做法是转换时用 OpenCV 重新读图的宽高,而不是信任 XML。这条是转换过程最容易被忽略的坑,后面转换脚本就是这么写的。
2.4 标注自检:用脚本批量排查空标注和越界框
419 张图人工抽查不现实,得写小脚本批量过。下面这段代码检查三类问题:图片缺失、空标注、坐标越界或退化框。
# check_voc.py import xml.etree.ElementTree as ET import os from pathlib import Path def check_xml(xml_path, img_dir): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.findtext('filename') img_path = os.path.join(img_dir, img_name) if not os.path.exists(img_path): return f'[缺失图片] {img_name} 找不到对应图片' size = root.find('size') W = int(size.findtext('width')) H = int(size.findtext('height')) objs = root.findall('object') if len(objs) == 0: return f'[空标注] {img_name} 没有任何object' for obj in objs: box = obj.find('bndbox') xmin = float(box.findtext('xmin')) ymin = float(box.findtext('ymin')) xmax = float(box.findtext('xmax')) ymax = float(box.findtext('ymax')) if xmin < 0 or ymin < 0 or xmax > W or ymax > H: return f'[越界] {img_name} 框 {xmin},{ymin},{xmax},{ymax} 超出图片' if xmax - xmin <= 1 or ymax - ymin <= 1: return f'[退化框] {img_name} 存在宽或高<=1像素的框' return None xml_dir = 'Annotations' img_dir = 'JPEGImages' issues = 0 for xml_file in sorted(Path(xml_dir).glob('*.xml')): err = check_xml(str(xml_file), img_dir) if err: issues += 1 print(err) print(f'检查完成,共发现 {issues} 个问题')这段脚本为什么值得跑一遍:越界框会在转 YOLO 时产生大于 1 或小于 0 的归一化坐标,退化框会让后续计算目标宽高比出现除以零。实际跑下来 419 张图里出现三五个越界框很常见,尤其是用手柄画框划出图片边缘时。发现问题后回到 LabelImg 手动重画,或者直接改 XML 里的坐标值,不要偷懒跳过。
3. VOC转YOLO格式:归一化坐标换算、类别映射与训练集划分脚本
3.1 两种格式的差异:为什么YOLO要用归一化坐标
VOC 和 YOLO 格式是目标检测领域流传最广的两种标注格式,现在大部分公开数据集也都愿意双格式发布,比如电力红外检测数据集、CUB 鸟类数据集,基本都是 VOC 和 YOLO 各给一份。两者核心差异在于坐标体系:VOC 用绝对像素坐标,YOLO 用相对图片宽高的归一化坐标。
VOC 的 xmin、ymin、xmax、ymax 是像素坐标,换个分辨率图坐标就失效了;YOLO 的每一行是“类别ID 中心点x 中心点y 框宽 框高”,五个数全部在 0 到 1 之间。归一化的好处是训练时不管输入图片被 resize 成 640 还是 960,标注都跟着等比缩放,不用重新算坐标。YOLO 格式的 txt 文件和图片同名,一张图一个 txt,每行一个目标。转换的核心公式:
x_center = ((xmin + xmax) / 2) / width
y_center = ((ymin + ymax) / 2) / height
box_w = (xmax - xmin) / width
box_h = (ymax - ymin) / height
公式本身不复杂,常见翻车点在类别 ID 的映射。VOC 的 XML 里写的是字符串类名,YOLO 只认数字 ID。你的 data.yaml 里 names 列表的顺序就是 ID 顺序,data.yaml 里 ostrich 排在第一个,那 ID 就是 0。转换脚本里的类别列表必须和 data.yaml 里的顺序完全一致,这个一致性是数据能正确训练的前提。
3.2 VOC转YOLO的参考脚本:钳制越界、重读尺寸、归一化
下面这段脚本是从 VOC 批量转 YOLO 的常规做法,可以直接复制到项目里按需改:
# voc2yolo.py import xml.etree.ElementTree as ET import os import cv2 # 类别顺序必须和 data.yaml 里的 names 完全一致 class_list = ['ostrich'] def convert(xml_path, img_dir, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.findtext('filename') # 用真实图片尺寸,不要用 XML 里记录的 size img = cv2.imread(os.path.join(img_dir, img_name)) if img is None: print('打开图片失败,可能文件名不匹配:', img_name) return H, W = img.shape[:2] lines = [] for obj in root.findall('object'): cls = obj.findtext('name') if cls not in class_list: print('发现未知类别:', cls, '在', img_name) continue cls_id = class_list.index(cls) b = obj.find('bndbox') xmin = float(b.findtext('xmin')) ymin = float(b.findtext('ymin')) xmax = float(b.findtext('xmax')) ymax = float(b.findtext('ymax')) # 越界框钳制到合法范围 xmin = max(0, min(xmin, W - 1)) xmax = max(xmin + 1, min(xmax, W)) ymin = max(0, min(ymin, H - 1)) ymax = max(ymin + 1, min(ymax, H)) cx = ((xmin + xmax) / 2) / W cy = ((ymin + ymax) / 2) / H w = (xmax - xmin) / W h = (ymax - ymin) / H lines.append(f'{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') if lines: out_path = os.path.join(out_dir, img_name.rsplit('.', 1)[0] + '.txt') with open(out_path, 'w') as f: f.write('\n'.join(lines)) xml_dir = 'Annotations' img_dir = 'JPEGImages' out_dir = 'labels' os.makedirs(out_dir, exist_ok=True) for xml_file in sorted(os.listdir(xml_dir)): if xml_file.endswith('.xml'): convert(os.path.join(xml_dir, xml_file), img_dir, out_dir)这段代码里最关键的参数和逻辑有三个。一是第 8 行的 class_list,这个列表的顺序决定所有类别 ID,后面 YOLO 训练配置里的 names 必须照抄,否则类别标签全部错位。二是用 cv2.imread 重新读图拿 H 和 W,而不是直接用 XML 里的 size 节点:一旦图片被压缩或旋转过,XML 里的尺寸就是错的,拿错尺寸做归一化,框坐标会系统性偏移。三是越界钳制逻辑,xmin 被限制不小于 0、不大于 W-1,xmax 至少比 xmin 大 1,这是防止手工标注越界框导致归一化坐标变成负数或大于 1 的最后一道防线。归一化结果保留 6 位小数足够,不需要更多精度。
3.3 按 8:2 划分训练集和验证集:别让同源图片两边串门
419 张左右的图转完 YOLO 格式后,下一步是划分。常规做法是 8:2 划分,也就是约 335 张训练、84 张验证。如果数据是从视频抽帧来的,千万不能直接随机打乱划分——连续帧画面几乎一样,同一只鸵鸟的相邻帧会同时出现在训练集和验证集,验证指标虚高,部署时原形毕露。
# split.py import os import random import shutil from pathlib import Path random.seed(42) source = Path('labels') images = Path('JPEGImages') train_img = Path('images/train') val_img = Path('images/val') train_lbl = Path('labels/train') val_lbl = Path('labels/val') for d in [train_img, val_img, train_lbl, val_lbl]: d.mkdir(parents=True, exist_ok=True) all_txt = list(source.glob('*.txt')) random.shuffle(all_txt) split = int(len(all_txt) * 0.8) def move_file(txt_path, dst_img_dir, dst_lbl_dir): # 优先按 .jpg 找图,找不到再试 .png img_path = images / (txt_path.stem + '.jpg') if not img_path.exists(): img_path = images / (txt_path.stem + '.png') if not img_path.exists(): print('图片缺失:', txt_path.stem) return shutil.copy(img_path, dst_img_dir / img_path.name) shutil.copy(txt_path, dst_lbl_dir / txt_path.name) for t in all_txt[:split]: move_file(t, train_img, train_lbl) for t in all_txt[split:]: move_file(t, val_img, val_lbl) print(f'train: {split}, val: {len(all_txt) - split}')这个脚本有两个值得注意的点。第一,random.seed(42) 固定随机种子,保证每次运行划分结果一致,方便复现训练效果。第二,划分逻辑是先打乱后切片,如果你希望验证集覆盖更多场景,可以改成按图片前缀分组——比如 ostrich_field_xxx 属于场地A,ostrich_zoo_xxx 属于场地B,先把前缀分组再抽组进验证集。对 419 张这种规模的数据,我更推荐按场景分组划分,因为单一类别的模型最怕遇到没见过的背景环境,只有验证集场景足够陌生,评估结果才有参考价值。划分完成后,YOLO 训练需要的目录结构就是 images/train、images/val、labels/train、labels/val 四个目录,里面图片和 txt 同名一一对应。
3.4 转换后的自检清单:一眼看穿txt里的坐标是否合理
转换完别急着训练,先用眼睛抽查。cat 打开一个 txt 文件,内容应该是这样的:
0 0.463281 0.479167 0.439844 0.713889 0 0.797656 0.229861 0.161719 0.251389五列分别对应:类别ID、中心点x、中心点y、框宽、框高。单类数据集里第一列全是 0 是对的,如果出现 1,说明 class_list 或 data.yaml 有漏项。后面四列全是 0 到 1 之间的小数,一旦看到大于 1 的数,说明越界钳制没生效,回头检查 XML 和脚本。
再跑一个统计命令,看看每张图平均几个框:
for f in labels/*.txt; do wc -l < "$f"; done | sort -n | uniq -c输出结果里如果出现大量的 0 行数,说明有不少空 txt 文件。空 txt 在 YOLO 训练里是允许的(表示这张图没有目标),但 419 张图里空标注比例超过 5%,就要反思标注策略是不是把太多遮挡目标漏掉了。我一般要求单张图平均不少于 1 个目标,因为鸵鸟虽然是群居动物,但画面里目标数量波动很大,如果平均框数过低,训练时正样本太少,模型学不到特征。
4. 419张数据训练前的避坑排查:越界框、类别ID错位、BN崩溃与路径编码
4.1 越界框:归一化坐标算出来大于1,训练直接废掉
现象:运行 YOLO 训练时日志里出现 nan 的 loss,或者框画出来像乱码,一张图上的预测框飘到图片外面去。用脚本检查 txt 时发现坐标有 1.032、-0.008 这种数字。
原因:标注阶段框拖出了图片边界,或图片被 resize 后 XML 里的坐标没有同步更新。归一化坐标一旦超出 0 到 1 区间,YOLO 在计算 anchor 和损失函数时就会产生无效梯度,轻则损失异常,重则整个训练过程崩掉。
解决:转换脚本里已经写了钳制逻辑,但如果你拿到的是别人的转换脚本,一定要确认有没有这行钳制。没有的话,用下面这段快速扫一遍所有 txt,把越界文件抓出来:
import os for root, dirs, files in os.walk('labels'): for name in files: if not name.endswith('.txt'): continue path = os.path.join(root, name) with open(path) as f: for line in f: vals = line.split() if len(vals) == 0: continue for v in vals[1:]: if float(v) < 0 or float(v) > 1: print(f'{name}: 越界 {line.strip()}') break扫出来后重新标注或修正 XML 再转一次,不要手动改 YOLO txt 里的数字,那样容易引入新的不一致。
4.2 XML里记录的尺寸和实际图片不一致:框的位置集体漂移
现象:用可视化脚本把 yolo txt 画回原图,发现所有框都往左上角偏移了几十个像素,或者框和鸵鸟身体错位明显,但 XML 里检查坐标又看不出问题。
原因:这是处理数据集用于 YOLOv8 训练时最容易踩的暗坑。图片从网上采集时经常被压缩软件重写尺寸,比如原图是 2000x1500,压缩后变成 1280x960,但 XML 里的 width 和 height 还是 2000 和 1500。转换时如果直接用 XML 里的 size 做分母,归一化坐标整体出错。
解决:转换脚本一律用 cv2.imread 读取实际图片尺寸,注释里也写了不要信 XML 的 size。如果你的图片目录里混着多种分辨率,建议统一 resize 到固定尺寸再标注,否则模型输入尺寸和标注尺寸的对应关系会很乱。另外,手机上拍的图经常带 EXIF 旋转信息,OpenCV 的 imread 不会自动矫正旋转,导致画框和实际目标差 90 度。处理办法是先批量把 EXIF 旋转烧进像素里(用 PIL 的 ImageOps.exif_transpose),再统一转成 jpg。
4.3 类别ID错位:类名对不上,训练时标签张冠李戴
现象:单类数据集训练出的模型,在验证集上 mAP 有 0.9,但部署时把其他鸟类(比如鸭子)也识别成鸵鸟,或者预测框位置准确但置信度忽高忽低。
原因:类别 ID 映射混乱。YOLO 的 data.yaml 里 names 顺序和转换时 class_list 顺序不一致。比如转换脚本里 class_list = ['ostrich'],但 data.yaml 写成了 names: ['bird'],训练时虽然只有一个类别,但内部的类别名映射出现分歧,模型学到的特征表征就会出现偏差。
解决:全流程只维护一份类别清单。我惯用的做法是在项目根目录放一个 classes.txt,转换脚本读它,data.yaml 也根据它生成,不要人工在多处维护。对 419 张这种单类数据集,类别错位产生的后果可能不明显,因为只有一个类别,但一旦你后续扩充类别,错误会被放大。另一个相关经验:类名一律用英文字母,不要用中文。LabelImg 虽然能写中文类名,但转出来的 txt、训练时的日志、可视化显示都可能出现编码问题,调试起来很痛苦。
4.4 训练中BN崩溃:batch size小、数据分布差,loss突然飙成NaN
现象:yolo 训练中 BN 崩溃的典型表现是前几十轮 loss 正常下降,某个 epoch 之后 loss 突然变成 nan,或者 val 集的 mAP 从 0.8 掉到 0.1 再也涨不回来。日志里经常伴随出现 "NaN" 或 "RuntimeError: expected scalar type" 之类的报错。
原因:两个常见诱因。一是 batch size 太小(比如显卡显存不够只设了 batch=2 或 4),BN 层在这么小的样本上统计均值和方差极不稳定,某个批次的极端值就能把统计量带崩。二是输入数据里混进了损坏图片——全黑图、全白图、不完整的 jpg,这些图会让模型学到极端分布。
解决:先把 batch 提到 8 或 16 试一下,这是性价比最高的修复手段。然后排查数据:用 OpenCV 读取所有训练图,计算每张图的标准差,把标准差接近 0 的图(也就是纯色图)列出来删掉。最后再考虑把学习率从默认 0.01 降到 0.001,让训练更稳定。还有一个经验:用 COCO 预训练权重而不是随机初始化,预训练模型下载后直接加载,BN 统计量继承自大规模数据,比从头训稳定得多。对小数据集来说,迁移学习不是可选优化,而是基本盘。
4.5 中文路径与文件名不匹配:训练日志里的图片地址找不到
现象:数据集放到 Windows 桌面上的中文文件夹里,YOLO 训练启动时报 FileNotFoundError,或者提示 "Image not found"。检查目录结构、文件命名都正常,但程序就是读不到。这个问题在 Linux 服务器上少见,在 Windows 本机上训练非常常见。
原因:OpenCV 和部分深度学习框架对中文路径和空格支持不好,路径里的中文字符编码在传递过程中被破坏。更隐蔽的一个问题是图片扩展名混用,有些图是 .jpg、有些是 .png,但转换脚本只按 .jpg 找图,对不上。
解决:整个数据集从根目录开始只用英文、数字和下划线,图片名统一成 ostrich_001.jpg 这种格式;训练脚本和配置文件里不要出现绝对路径,写相对路径指向数据集根目录。图片扩展名不统一时,在 3.3 节划分脚本里已经做了 .jpg 和 .png 的兼容查找,但最干净的做法还是采集后立刻统一转成 jpg,一劳永逸。遇到 FileNotFoundError 先别查网络结构,先查路径。
5. 用转换好的鸵鸟数据跑通YOLOv8:最小训练命令与画框验证
5.1 最小训练命令:从预训练权重和data.yaml开始
数据和标签就位后,YOLOv8 训练自己的数据集只需要两个文件:一个 data.yaml,一条训练命令。data.yaml 是最容易出错的地方,先贴标准内容:
path: ./ostrich_dataset train: images/train val: images/val nc: 1 names: ['ostrich']注意 path 写相对路径,train 和 val 不要写成绝对路径,否则换机器跑就废了。nc 是类别数,单类就是 1,names 顺序必须和之前转换脚本里的 class_list 完全一致。训练命令:
yolo detect train data=ostrich.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16model 参数填 yolov8n.pt 时,第一次运行会自动把 yolo 预训练模型下载下来,这个过程不用干预,等它下完就行。epochs 设 150 对 419 张数据是合理的,再多就容易过拟合。batch 16 是一个稳妥值,如果你的显卡显存只有 8G,降到 8 但不要更低,原因在第 4 章 BN 崩溃那条讲过。训练结束后,重点看 runs/detect/train/confusion_matrix.png 里的混淆矩阵,单类模型的混淆矩阵只有 2 行 2 列:真实鸵鸟、背景。如果矩阵里背景被预测成鸵鸟的比例偏高,说明负样本不够,需要在数据里补一些没有鸵鸟的背景图。矩阵里的数值是按行归一化的,别纠结总和不是 100,那不是 bug。
5.2 画框预览:验证txt有没有转错的后悔药
训练之前花 10 分钟做一次可视化回读,能省掉几小时查错时间。所谓回读,就是把 yolo txt 的归一化坐标乘回图片宽高,画框存成新图,肉眼看框和鸵鸟是否贴合:
import cv2 import os img_dir = 'images/train' label_dir = 'labels/train' for name in os.listdir(label_dir)[:30]: img_path = os.path.join(img_dir, name.replace('.txt', '.jpg')) img = cv2.imread(img_path) if img is None: continue h, w = img.shape[:2] with open(os.path.join(label_dir, name)) as f: for line in f: vals = line.strip().split() if len(vals) != 5: continue cx, cy, bw, bh = map(float, vals[1:]) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(f'preview_{name.replace(".txt", ".jpg")}', img)这段脚本就是第 5 章最重要的验证手段。浮点坐标乘宽高后转 int 时注意边界,框超出画面边缘的情况会在这时现出原形。预览 30 张就够,重点看两类图:有多只鸵鸟的群像图和鸵鸟在画面边缘的图。前者看框是否都覆盖到,后者看钳制逻辑是否把边缘框切得过分。这张图存下来留着,训练完再对比预测结果,能直观看出数据标注和模型预测的差异。
5.3 419张还不够?先统计目标尺寸,再谈增强
最后聊一下小数据集的进阶思路。419 张图训练一个单类检测器,效果好坏不取决于总张数,而取决于目标尺寸分布和场景覆盖。用一个简单统计就能判断该不该升级方案:遍历所有 txt,计算每个框的面积占整张图面积的比例。如果大量框占比小于 5%,属于小目标场景,imgsz 可以从 640 提到 960,或者用 SAHI 切片推理,而不是盲目堆训练轮数。如果目标占比普遍在 15% 以上,419 张配合预训练权重已经够用,重点是保证验证集的场景和你实际部署场景一致。
关于数据增强,YOLOv8 默认开 mosaic。对鸵鸟这种大目标,mosaic 的随机缩放有时会把目标切得太碎,反而干扰训练。如果训练曲线显示收敛慢,尝试在 data.yaml 里配置 hsv_h、hsv_s 这类轻微颜色增强,关闭或降低 mosaic 概率,比粗暴叠加增强更有效。我的习惯是拿到任何数据先画框预览、统计尺寸分布,最后才碰训练参数——数据集决定 80% 的效果,参数是最后的玄学。这套流程帮你把 419 张的鸵鸟数据转成 YOLO、跑通训练、排查掉绝大多数坑,希望帮到你。
本文还有配套的精品资源,点击获取