简介:针对船舶目标检测场景的YOLO系列数据集已封装为zip包,面向目标检测算法学习者和开发者,覆盖充气船、独木舟等类别的1088张带标签图像,适合海事监控、水上交通等场景的模型训练与实验。包内共有2000个文件:1063个xml文件提供VOC格式标注,936个txt文件提供YOLO格式标注,另含data.yaml配置文件,可直接衔接YOLOv5、v7、v8、v9、v10及yolo11等主流版本。数据集已经按训练集、验证集、测试集划分完毕,无需手动拆分,下载后修改配置即可开始训练;两种标注格式并存,方便在YOLO与VOC之间切换,且YOLO格式txt已包含归一化的中心点、宽度与高度信息,可直接作为模型输入,也便于与公开模型对比验证。压缩包整体约88.34MB,已有86人学习,适合快速上手小规模船舶识别项目,或作为目标检测教学与练手的入门数据集。
1. 这个船舶数据集值不值得用:带标签的1088张图像,先别急着开训练
做水面目标检测的人,最烦的往往不是模型,而是数据。自己飞无人机采集,一天能留下几百张可用的航拍画面就算运气好,标注又要一帧帧抠框。所以看到“船舶数据集-1088张图像带标签-充气船-独木舟-船舶.zip”这类资源,第一反应就是下载解压,赶紧把yolo算法跑起来。我的建议是先忍一忍。1088张图像对三分类检测来说不算充裕,但充气船、独木舟、船舶这三类的外观差异足够大,船体外形又比行人规整,模型学起来并不吃力。这个数据集适合做河道监管、水上搜救、港口监控等场景的首版验证,也适合刚接触yolo算法的人完整走一遍训练流程。但它能不能发挥价值,取决于你拆包后的第一个小时:类别分布是否均衡、标签格式是什么、标注框有没有越界。这几步不查清楚,后面训练大概率翻车。
2. 拆包先做数据体检:类别分布、标注框和图像分辨率的三个检查点
“带标签”这三个字只能说明压缩包里存在标注文件,不能说明标注质量。最常见的压缩包结构有两种:一种是VOC格式的XML标签,一种是YOLO格式的txt标签。后者可以直接训练,前者必须先转换。不管哪种,先做数据体检都值得,因为标注质量问题会在训练中变成诡异的损失曲线,让你误以为是模型调参问题,实际上根源在数据。
2.1 用Python脚本核对XML标注与类别名
如果解压后看到的是XML文件,第一件事是统计类别名和标注框数量,而不是急着找训练脚本。很多公开数据集的类别名并不规范,比如同一类目标可能同时出现“Inflatable Boat”“inflatable_boat”“boat_inflatable”三种写法,统计脚本能把这些差异一次性暴露出来。
# check_voc.py import os import xml.etree.ElementTree as ET from collections import Counter xml_dir = "voc_annotations" # 改成你解压后的XML目录 cls_counter = Counter() # 类别名 -> 出现次数 box_counter = Counter() # (类别, 大小档位) -> 框数量 for name in os.listdir(xml_dir): if not name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, name)) for obj in tree.findall("object"): cls = obj.find("name").text.strip().lower() cls_counter[cls] += 1 box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) w, h = xmax - xmin, ymax - ymin if w > 0 and h > 0: area = w * h degree = "large" if area > 1000 else "small" box_counter[(cls, degree)] += 1 print("类别统计:", cls_counter) print("框大小分布:", box_counter)这段代码的逻辑很直接:遍历全部XML,解析每个object节点的类别名和bndbox坐标,最后打印统计结果。值得注意的两点:一是lower()会把大小写差异统一,如果打印结果里出现两种相似但不同的名字,说明标注阶段没有遵循统一命名规范;二是area > 1000这个阈值是按普通航拍图设定的,如果你的图像分辨率是1920x1080,小目标阈值可以改成32 * 32。跑完脚本后,你立刻就知道三类目标的数量差距有多大。如果充气船只有几十个框而船舶有近千个框,后续训练必须做类别均衡处理,否则模型会严重偏向样本多的类。
2.2 标注框异常检测:面积过滤与边界溢出
标注框的几何质量同样需要检查。坐标溢出在VOC转YOLO时不会报错,但归一化后会出现负数或大于1的值,训练时这些框会污染损失计算,让模型在无意义区域反复试错。更隐蔽的是图像缺失:训练框架加载数据集时会静默跳过没有图像文件的标注,你以为是1088张图参与训练,实际可能只有900张。
# sanity_check.py import os from PIL import Image import xml.etree.ElementTree as ET xml_dir = "voc_annotations" img_dir = "images" issues = [] for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue img_name = xml_name[:-4] + ".jpg" img_path = os.path.join(img_dir, img_name) if not os.path.exists(img_path): issues.append(f"缺失图像: {img_name}") continue with Image.open(img_path) as im: W, H = im.size tree = ET.parse(os.path.join(xml_dir, xml_name)) for i, obj in enumerate(tree.findall("object")): box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) if xmin < 0 or ymin < 0 or xmax > W or ymax > H: issues.append(f"{img_name} 第{i}个框越界") if xmax <= xmin or ymax <= ymin: issues.append(f"{img_name} 第{i}个框面积为0") print("问题数量:", len(issues)) for line in issues[:30]: print(line)这里的判断逻辑按两个标准:坐标是否落在图像范围内、宽高是否为正。issues列表里记录了所有异常。如果缺失图像的数量很多,先别急着转换格式,去检查解压目录层级,通常是压缩包里多套了一层文件夹导致的。如果越界框占比超过5%,我一般会写个批量修正脚本,把越界坐标clip回图像边界,而不是在转换时硬截断,因为硬截断会改变中心点位置,影响更大。这个体检过程大约十分钟,却能省下后面几天无效训练的时间。
| 检查项 | 检查方法 | 通过标准 |
|---|---|---|
| 类别名统一 | 统计脚本输出 | 类别集合与预期完全一致,无近义重复 |
| 标注框坐标 | 边界检查脚本 | 越界框数量为0 |
| 图像与标注对应 | 文件缺失检查 | images与XML目录一一对应 |
| 小目标占比 | 框面积分布 | 明确小目标数量,决定后续是否切片训练 |
3. 把VOC转成YOLO格式:归一化脚本与四个边界坑
这一章讲转换。YOLO系列训练框架要求的标签格式是每张图对应一个txt文件,每行一个目标,格式为“类别id cx cy w h”,其中cx、cy是中心点坐标,w、h是框宽高,四个值都用图像宽高做了归一化。VOC的XML则是绝对像素坐标,所以必须先转换。
3.1 归一化坐标换算公式与转换脚本
归一化公式本身不复杂:cx = (xmin + xmax) / 2 / 图像宽度,cy = (ymin + ymax) / 2 / 图像高度,w = (xmax - xmin) / 图像宽度,h = (ymax - ymin) / 图像高度。真正容易出错的地方是“图像宽度”取哪里的值。XML里通常带一个<size>节点记录宽高,但它经常和图片实际分辨率不一致,尤其是经过压缩、旋转后的二次导出数据。所以我的习惯是转换时用PIL实际读取图片尺寸。
# voc2yolo.py import os from PIL import Image import xml.etree.ElementTree as ET voc_dir = "xml_annotations" img_dir = "images" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) # 类别映射表:顺序必须和后续data.yaml中names完全一致 class_map = { "inflatable_boat": 0, "canoe": 1, "ship": 2, } def convert(xml_path, img_path, out_path): with Image.open(img_path) as im: W, H = im.size tree = ET.parse(xml_path) lines = [] for obj in tree.findall("object"): cls = obj.find("name").text.strip().lower().replace(" ", "_") if cls not in class_map: print(f"跳过未知类别: {cls}") continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) cx = (xmin + xmax) / 2.0 / W cy = (ymin + ymax) / 2.0 / H bw = (xmax - xmin) / W bh = (ymax - ymin) / H if cx < 0 or cy < 0 or bw <= 0 or bh <= 0: print(f"异常框跳过: {img_path} {cls}") continue lines.append(f"{class_map[cls]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") if lines: with open(out_path, "w") as f: f.write("\n".join(lines) + "\n") for xml_name in os.listdir(voc_dir): if not xml_name.endswith(".xml"): continue stem = xml_name[:-4] convert( os.path.join(voc_dir, xml_name), os.path.join(img_dir, stem + ".jpg"), os.path.join(out_dir, stem + ".txt"), )这个脚本里有几个细节值得说明。class_map是整套训练流程的“契约”,它的顺序必须和后面data.yaml里的names顺序完全一致,否则模型会把充气船当成独木舟来学。lower().replace(" ", "_")是为了把“Inflatable Boat”这类带空格、大小写混合的类别名统一成inflatable_boat,因为YOLO的txt按空格切分字段,类别名里带空格会导致解析错位。坐标计算全部保留float,不要为了省空间转成int,一个像素的误差对1088张这样的小数据集影响会被放大。
3.2 类别映射表与txt标签生成:四个边界坑
转换这一步看起来简单,实际踩坑的人非常多,而且坑位都集中在这四个地方。
坑1:类别名大小写和空格。XML里写成“Canoe”或“canoe”,代码能处理,但如果数据处理流水线里某个环节忘了lower(),就会产生两个不同的类。统一办法是转换脚本里强制strip().lower().replace(" ", "_"),并在映射表里缺失时打印警告。
坑2:整数坐标被四舍五入。有些标注工具导出XML时会把坐标存成整数,比如<xmin>126</xmin>而不是126.4。如果转换时再int()一次,框就缩小了。正确做法是从XML读float、计算时保留float,只在写入txt时用{:.6f}保留六位小数。
坑3:误信XML里的<size>节点。这个字段在二次压缩或格式转换后经常和真实图片分辨率不一致,尤其是JPEG重编码后。转换脚本里用PIL读一次im.size,用真实宽高做分母,是最稳的做法。
坑4:归一化后越界框被clamp。很多人发现某个框算出来cx=1.02,就直接max(0.0, min(1.0, cx))截断。这个操作会把中心点强行拉回图像边缘,导致框位置整体偏移,比越界本身更糟。正确做法是在转换前就修正XML坐标,或者直接从异常检测阶段就把这类框找出来重新标。转换阶段做clamp只是把问题掩盖了。
如果资源包里给的本来就是YOLO格式的txt,可以跳过整个转换流程。但还是要检查txt第一列数字是否都在0到2之间,以及五个字段是否都是合法浮点数。干过这行的人都知道,不是所有“带标签”的txt都能直接用。
4. 用YOLOv8在本地跑通船舶检测:目录结构、data.yaml与训练命令
标签转好之后,接下来是搭建训练工程。常见做法是用ultralytics的YOLOv8,命令风格清晰,适合小数据集快速验证。网上关于yolo算法讲解ppt很多,框架图画得很漂亮,但真到自己的船舶数据集上,最耗费时间的反而是目录结构这类琐碎问题。
4.1 数据集目录结构与data.yaml配置
YOLO训练框架对数据目录有硬性要求:图像和标签必须在同级目录下,各自分train和val子目录。以detect任务为例,标准结构如下:
dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/图像放images/train,对应的txt必须放labels/train,文件名必须完全一致,只是扩展名不同。这个结构没有商量余地,因为ultralytics加载数据时是根据图像路径自动到labels目录找同名txt的。如果找不到,它不报错,只是悄悄跳过这张图,训练结束你都不知道实际用了多少张。
目录建好之后,写一个data.yaml:
path: /path/to/dataset train: images/train val: images/val names: 0: inflatable_boat 1: canoe 2: ship这里的names顺序必须和第3章转换脚本里的class_map一致,不能用“船、皮划艇”这类自由命名,也不要改成“class1/class2/class3”。类别名的意义在于方便你读混淆矩阵和PR曲线,名不副实会干扰排查。
4.2 训练启动命令与关键参数调整
结构和配置就绪后,可以直接用命令行启动训练:
yolo detect train \ model=yolov8n.pt \ data=boat.yaml \ imgsz=640 \ batch=16 \ epochs=100 \ patience=15 \ project=runs/boat \ name=exp1这个命令有几个参数需要按场景调整。model=yolov8n.pt是最轻量的版本,适合8GB显存以下的显卡。如果显存够,换yolov8s.pt精度会好一点,但训练时间接近翻倍。batch不是越大越好,普通消费级显卡建议先设8,能跑起来再加到16;如果爆显存,降到4比换模型更实际。epochs=100配合patience=15意思是连续15个epoch验证集指标没提升就早停,这个组合对1088张的小数据集很实用,能有效防止过拟合。
这里补充一个重要调参项:对于航拍类船舶图像,长宽比差异很大,默认训练会先把所有图resize成方形,造成大量无效填充和显存浪费。可以在命令里加rect=True启用矩形训练,batch内图像按相似长宽比分组,能明显降低显存占用。如果你想追求小目标召回,可以先把imgsz提到1280训练一轮,再用imgsz=640微调,这种两阶段策略在小数据集上比单纯调大imgsz更稳。
命令行训练只是入口,实际工作中很多人用Python脚本调用:
from ultralytics import YOLO model = YOLO("yolov8n.pt") model.train(data="boat.yaml", epochs=100, imgsz=640, batch=16)这种写法的好处是可以把整个训练流程封装成脚本,便于复现和调参。但命令行的参数含义完全一致,看个人习惯。初始权重选yolov8n.pt这类预训练权重,因为COCO预训练模型已经学过船这个类别的大致特征,迁移到自己的船舶数据集上收敛明显更快。
5. 训练避坑指南与排查:1088张图常见的五个翻车现场
小数据集的训练多少有些玄学成分,但大部分翻车都能从标签、数据划分和参数配置上找到明确原因。下面五条是这类船舶数据集中最常见的踩坑记录,按“现象、原因、解决”写清楚,你可以直接对号入座。
5.1 训练中三个最常见的翻车现场
现场一:训练了20个epoch,train/box_loss在下降,val/box_loss和mAP50纹丝不动,mAP50一直停在0.05附近。第一个怀疑对象是类别映射错位。检查方式很简单:打开labels/train里任意几个txt,看第一列数字是否都在0到2之间,再对照data.yaml的names顺序看数字0对应的是不是inflatable_boat。另一个原因是空标签文件太多,如果部分图像没有标注框,生成的txt是空文件,训练框架会把它们当作背景样本。少量空标签没问题,但如果空标签占了三成以上,模型会倾向于“什么都不检”。解决:检查空txt比例,超过20%就考虑删除这些无目标图像,或者重新审视标注完整性。
现场二:训练集mAP50到0.98,验证集只有0.35,典型的过拟合。原因首先是1088张图太少,模型把训练集里的背景纹理也背下来了;其次是数据划分方式不合理。很多人用random_split直接随机分,但无人机航拍数据里同一艘船往往连续出现在多张相邻帧中,随机划分会把同一目标同时分进训练集和验证集,造成验证指标虚高,实际部署时又打回原形。解决:按采集批次或视频片段划分数据,保证同一目标不会跨集合出现;同时把epochs降到60到80,增强hsv_h、hsv_s这类颜色增强参数,让模型对光影变化更鲁棒。
现场三:batch=16训练到第三个epoch直接CUDA out of memory被杀。这种现象在8GB显存的卡上最常见,尤其是在anchors数量和输入分辨率都较大的情况下。原因有两个层面:一是batch太大,二是默认矩形训练导致显存浪费。解决:先降batch到4或8,确认能跑通之后再逐步调大;如果还想保住batch,就加rect=True减少填充区域,或者把imgsz降到512。这里的关键是不要一上来就追求大批量,1088张图的小数据集,batch=8和batch=16的收敛效果差异不大。
5.2 小目标漏检与背景误检的排查
现象四:几十米外的充气船在画面里只有十几个像素,模型在推理时完全漏检;对比之下,近处的船舶能被稳框住。这属于小目标漏检,原因是目标尺寸相对输入分辨率太小。YOLO在imgsz=640下会把原图压缩,小目标很可能缩到几个像素以内,特征层上根本留不下信息。解决有两个方向:一是统计标注框面积分布,如果大量框的宽高小于32像素,直接把训练imgsz提到1280;二是对大图做切片推理,常见做法是用SAHI这类工具把航拍图切成若干小图,分别推理再合并结果。切片会增加推理时间,但无人机和监控场景通常可以接受。
现象五:推理时把岸边的浮标、白色浪花甚至树根误检成充气船。原因是训练集里几乎没有无目标的纯背景图像。YOLO对“画面里没有目标”的建模依赖背景样本,如果每张图都至少有一个标注框,模型只能学会“哪些区域像船”,学不会“哪些区域一定不是船”。解决:从原视频素材里抽取一批无船帧加入训练集,作为纯背景负样本;或者降低推理时的conf阈值同时提高iou阈值,减少低置信度误检。这个问题的根源在数据分布,调参只能缓解,补背景图才治本。
6. 验证与进阶:用mAP、混淆矩阵和TTA决定下一步投入
训练结束后,第一步不是看测试集视频,而是跑一次标准的验证评估:
yolo detect val \ model=runs/boat/exp1/weights/best.pt \ data=boat.yaml \ split=valultralytics会在runs/detect/val目录下生成confusion_matrix.png和PR曲线图。混淆矩阵要重点看对角线之外的非零值:如果inflatable_boat这一列里出现了canoe的响应,说明两类存在系统性误检,需要补标这两类的边界样本;如果background列比例很高,说明负样本不足的问题还没解决。
用验证指标决定下一步投入,我一般按下面的经验划分:
| val mAP50 | 建议动作 |
|---|---|
| 0.90以上 | 当前数据已经够用,重心放到NMS参数和后处理优化 |
| 0.70~0.90 | 补标困难时段、遮挡、逆光样本,重点提升难例 |
| 0.50~0.70 | 回查标签质量和类别映射,先修数据再谈标注 |
| 0.50以下 | 大概率数据或配置存在问题,不要继续标数据 |
如果验证指标在0.85附近但想再压榨一点,可以试TTA推理:
yolo detect predict \ model=runs/boat/exp1/weights/best.pt \ source=test_images \ tta=TrueTTA会对输入做多尺度翻转推理再融合结果,对漏检有一定改善,代价是推理时间成倍增加,适合离线分析,不适合实时视频流。
我做这类小数据集项目养成了一个习惯:每次转换完标签,先随机挑五张图把标注框画出来看一眼,再启动训练。这个动作看起来多余,却能拦下一大半格式错位、类别错乱的问题。数据和代码之间出问题的时候,数据往往是更该被怀疑的那个。希望这个思路能帮到你,至少帮你少跑几轮无效训练。
本文还有配套的精品资源,点击获取