简介本资源为面向目标检测学习者的船只检测数据集从COCO2017数据集中筛选提取舰船类目标并整理而成适用于YOLO等主流检测算法的训练与验证可服务于舰船识别、海上目标监测等实验场景适合具备一定深度学习基础、正在做检测项目或课程实践的同学使用。压缩包共约2000个文件包含3146张jpg图像、3146个xml标注文件与3147个txt标签文件xml可直接对接Pascal VOC格式txt则适配YOLO系列训练流程包体整体约505.62MB目录组织清晰便于按需取用与格式转换。目标类别统一为boat标注数量为3146数据规模适中能较好支撑模型训练与效果对比。目前已有559人学习下载可作为舰船检测任务的现成数据基础帮助读者省去自行从COCO中筛选与格式转换的繁琐步骤快速投入模型搭建与调参验证。1. 船只检测数据集与3100张标注图从拿到压缩包到跑通第一条基线港口监控、航道巡检、无人机海面搜救只要涉及水面目标识别船只检测几乎是绕不开的第一道工程题。但真正动手时卡住大多数人的不是模型结构而是数据公开的船只检测数据集要么类别太粗、要么场景单一要么标注格式和手头框架对不上。一个 3100 张量级的船只检测数据集恰好落在“够用又不至于训不动”的甜点区——它足以让 YOLO 系列收敛出可用的权重又不会让单卡机器跑到天荒地老。这篇文章面向的是手里已经有一个船只检测数据集压缩包、想把它变成能跑通、能评估、能继续迭代的检测模型的工程师。我会按“先看清数据长什么样、再转格式、再配训练、最后排坑”的顺序讲中间所有参数和命令都是我自己在类似规模数据集上反复调过的你照着改路径就能复现。数据集本身不是终点能不能把它喂进训练管线、能不能判断标注质量、能不能在 mAP 掉下来时知道去哪找原因才是这条链路真正值钱的地方。2. 先摸清 3100 张船只数据的底细类别、分布与标注格式拿到一个船只检测数据集最忌讳的动作是直接train.py一把梭。3100 张这个量级如果类别定义混乱或者长尾严重训出来的模型会在某些场景下集体翻车而你连问题出在哪都不知道。所以第一步永远是做数据体检看目录结构、看标注格式、看类别分布、看图像尺寸分布。这一步花半小时能省掉后面几天的玄学调参。2.1 目录结构与标注格式的快速判定常见的船只检测数据集标注格式无非几种PASCAL VOC 的 XML、COCO 的 JSON、YOLO 的 txt每行class x_center y_center w h归一化到 0~1以及 LabelMe 的 JSON。先别急着写转换脚本用几条命令把结构看清楚。# 看顶层目录结构确认 images / labels / annotations 的分布 find ./ship_dataset -maxdepth 2 -type d | sort # 统计图片数量确认是否与“3100”这个量级吻合 find ./ship_dataset -type f \( -name *.jpg -o -name *.png -o -name *.jpeg \) | wc -l # 抽样看标注文件长什么样判断格式 find ./ship_dataset -type f -name *.xml | head -3 find ./ship_dataset -type f -name *.txt | head -3 find ./ship_dataset -type f -name *.json | head -3这几条命令的逻辑很直接find -maxdepth 2避免你被深层嵌套目录淹没wc -l给你一个真实图片数head -3让你在不打开编辑器的情况下判断标注格式。参数上唯一要注意的是-name后面的通配如果你的图片是.JPG大写后缀得单独再跑一次Linux 下文件名大小写敏感这个坑我踩过不止一次。如果抽样出来是 XML基本就是 VOC 格式每个 XML 里object节点下有name和bndbox如果是 txt 且每行 5 个数字大概率是 YOLO 格式如果是单个大 JSON那就是 COCO。判定清楚之后再决定要不要转换能直接用就别转转换本身就是引入错误的高发环节。2.2 类别分布与长尾问题的量化船只检测数据集最容易出现的问题是类别不均衡大船样本多、小渔船样本少或者“船”和“非船”混在一起。用一段 Python 把类别统计出来别靠感觉。import os import glob from collections import Counter import xml.etree.ElementTree as ET # 假设是 VOC 格式如果是 YOLO 格式改读 txt 第一列即可 anno_dir ./ship_dataset/annotations counter Counter() for xml_path in glob.glob(os.path.join(anno_dir, *.xml)): tree ET.parse(xml_path) root tree.getroot() for obj in root.findall(object): name obj.find(name).text.strip() counter[name] 1 # 打印类别与实例数按数量降序 for cls, cnt in counter.most_common(): print(f{cls}: {cnt}) # 计算长尾比最多类 / 最少类 if len(counter) 1: most counter.most_common(1)[0][1] least counter.most_common()[-1][1] print(f长尾比: {most / least:.1f})这段代码的核心是Counter累加每个 XML 里的object数量最后用most_common()排序。参数上anno_dir要换成你实际的标注目录如果是 YOLO 格式把解析 XML 的部分换成读 txt 第一列并映射到classes.txt里的类名。长尾比这个指标很实用如果超过 20说明最少类样本严重不足训练时要么做重采样要么在 loss 里给类别权重否则模型会直接“放弃”小类。提示如果长尾比超过 50先别急着训优先确认最少类是不是标注错误或类别定义重复很多所谓的长尾其实是标注不规范造成的。2.3 图像尺寸与目标尺度分布船只检测的另一个隐藏变量是目标尺度。3100 张图里如果既有近景大船又有远景小船锚框anchor设置不合理就会导致小目标召回率极低。用脚本统计一下 bbox 的相对面积占比。import glob import xml.etree.ElementTree as ET import numpy as np areas [] for xml_path in glob.glob(./ship_dataset/annotations/*.xml): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w float(size.find(width).text) img_h float(size.find(height).text) for obj in root.findall(object): bbox obj.find(bndbox) w float(bbox.find(xmax).text) - float(bbox.find(xmin).text) h float(bbox.find(ymax).text) - float(bbox.find(ymin).text) areas.append((w * h) / (img_w * img_h)) areas np.array(areas) print(f相对面积 中位数: {np.median(areas):.4f}) print(f相对面积 10分位: {np.percentile(areas, 10):.4f}) print(f相对面积 90分位: {np.percentile(areas, 90):.4f}) print(f小于1%面积的占比: {(areas 0.01).mean():.2%})这里用相对面积而不是绝对像素是为了消除图像分辨率差异。中位数告诉你典型目标多大10 分位告诉你小目标有多小如果小于 1% 面积的占比超过 30%那训练时输入分辨率就不能压得太低否则小目标在特征图上直接消失。这个统计结果直接决定你后面imgsz设 640 还是 960。3. 把船只数据转成 YOLO 格式脚本、划分与三个边界坑体检做完接下来是格式转换和数据集划分。这一步的目标是产出一个 YOLO 能直接吃的目录结构images/train、images/val、labels/train、labels/val外加一个data.yaml。听起来简单但转换脚本里坐标归一化、类别映射、空标注处理这三处最容易出问题。3.1 VOC 转 YOLO 的完整脚本与参数说明下面这个脚本处理 VOC 到 YOLO 的转换同时完成训练/验证集划分。我一般按 8:2 划分3100 张大概 2480 训练、620 验证。import os import glob import random import shutil import xml.etree.ElementTree as ET # 配置区按实际路径修改 VOC_IMG_DIR ./ship_dataset/images VOC_ANNO_DIR ./ship_dataset/annotations OUT_DIR ./ship_yolo CLASSES [ship] # 如果有多类按实际顺序写索引即类别 id VAL_RATIO 0.2 SEED 42 random.seed(SEED) # 建立输出目录 for split in [train, val]: os.makedirs(os.path.join(OUT_DIR, images, split), exist_okTrue) os.makedirs(os.path.join(OUT_DIR, labels, split), exist_okTrue) def voc_to_yolo(xml_path, img_w, img_h): 解析 VOC XML返回 YOLO 格式的行列表 tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASSES: continue # 跳过未定义类别避免类别 id 错乱 cls_id CLASSES.index(name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 归一化并转为中心点宽高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / 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{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines # 收集所有图片按文件名匹配 XML img_paths [] for ext in [*.jpg, *.jpeg, *.png]: img_paths.extend(glob.glob(os.path.join(VOC_IMG_DIR, ext))) img_paths.sort() random.shuffle(img_paths) val_count int(len(img_paths) * VAL_RATIO) val_set set(img_paths[:val_count]) for img_path in img_paths: base os.path.splitext(os.path.basename(img_path))[0] xml_path os.path.join(VOC_ANNO_DIR, base .xml) if not os.path.exists(xml_path): continue # 没有标注的图片直接跳过 # 读取图像尺寸这里用 PIL确保和标注一致 from PIL import Image with Image.open(img_path) as im: img_w, img_h im.size lines voc_to_yolo(xml_path, img_w, img_h) if not lines: continue # 空标注图片不写入避免训练时全背景样本干扰 split val if img_path in val_set else train shutil.copy(img_path, os.path.join(OUT_DIR, images, split, os.path.basename(img_path))) with open(os.path.join(OUT_DIR, labels, split, base .txt), w) as f: f.write(\n.join(lines)) # 生成 data.yaml with open(os.path.join(OUT_DIR, data.yaml), w) as f: f.write(fpath: {os.path.abspath(OUT_DIR)}\n) f.write(train: images/train\n) f.write(val: images/val\n) f.write(fnc: {len(CLASSES)}\n) f.write(fnames: {CLASSES}\n) print(转换完成)逻辑上分四步收集图片、随机打乱后切分验证集、逐张解析 XML 并归一化坐标、写出 YOLO txt 和 data.yaml。参数里CLASSES必须和你的数据集类别严格一致顺序就是类别 idVAL_RATIO控制验证集比例3100 张用 0.2 比较稳SEED固定随机种子保证划分可复现。坐标裁剪那几行别省VOC 标注里 xmax 超出图像宽度的脏数据很常见不裁剪会导致 YOLO 训练时 loss 直接 NaN。3.2 数据集划分的三个边界坑第一个坑是“同场景泄漏”。如果 3100 张图里有连续帧来自同一段视频随机划分会把高度相似的帧同时放进训练和验证验证 mAP 虚高实际部署一塌糊涂。判断方法很简单看文件名是否有连续编号。如果有按编号段划分而不是随机划分。第二个坑是“空标注图片”。有些图片里确实没有船VOC 里就没有object节点。这类图片在 YOLO 训练中如果保留但 label 为空文件部分版本会报错或当成全背景。我的做法是直接跳过不写入数据集因为船只检测里纯背景样本的边际收益很低。第三个坑是“类别名大小写与空格”。ship和Ship在CLASSES里是两个类XML 里如果混用转换后会变成两个类别 id训练时模型直接分裂。转换前先用set把所有name收集起来打印一遍确认没有隐藏字符。注意转换完成后务必抽查 5 张图的 txt 和原图用可视化脚本画框确认坐标没偏。归一化坐标一旦算错训练 loss 会正常下降但 mAP 极低属于典型的“黑匣子”故障。4. 用 YOLOv8 在 3100 张船只数据上跑通训练参数、显存与收敛判断数据准备好了接下来是训练。3100 张这个量级YOLOv8n 或 YOLOv8s 是比较务实的选择n 版本在单张 8G 显存卡上就能跑s 版本精度更高但显存吃紧。我一般先用 n 跑一个基线确认数据管线没问题再换 s 或 m 做精度提升。4.1 训练命令与关键参数逐项说明yolo detect train \ data./ship_yolo/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience20 \ workers8 \ project./runs/ship \ namebaseline_n逐项说data指向刚才生成的 yamlmodel用预训练权重3100 张从零训容易过拟合预训练能省一半 epochepochs100是上限配合patience20做早停验证集 20 轮不提升就停imgsz640是默认值如果前面统计小目标占比高改成 960但显存和耗时都会涨batch16在 8G 卡上跑 640 分辨率比较稳显存不够就降到 8 并开ampTrue默认开lr00.01是初始学习率lrf0.01是最终学习率系数余弦退火到初始的 1%workers8是数据加载线程按 CPU 核数调太高反而抢资源。训练启动后重点看三个信号box_loss是否稳定下降、mAP50是否在 30 轮后开始爬升、val/val_loss是否在训练 loss 下降时反而上升。如果 val_loss 提前上升就是过拟合要么减 epoch要么加数据增强。4.2 显存不够时的三档降级方案8G 卡跑 640 分辨率 batch16 一般没问题但如果你的数据集图像本身分辨率很高或者换成 YOLOv8m就会 OOM。降级顺序我一般这样排档位操作影响一档batch 降到 8训练变慢收敛略稳二档imgsz 降到 512小目标召回下降三档换 yolov8n精度下降但能跑通优先降 batch因为 imgsz 直接影响小目标检测能力船只检测里小目标往往是重点。如果三档都试了还 OOM检查是不是workers开太高导致内存爆了而不是显存。4.3 用验证集判断模型是否真的学到了船训练完不是看最后一行 mAP 就完事。用yolo detect val跑一遍验证集然后抽几张预测图看框。yolo detect val \ model./runs/ship/baseline_n/weights/best.pt \ data./ship_yolo/data.yaml \ imgsz640 \ batch16重点看mAP50-95和mAP50的差距。如果 mAP50 很高但 mAP50-95 很低说明框的位置不够准可能是标注框偏大或偏小如果两者都低检查类别是否混淆、小目标是否漏检。我一般会再跑一个predict模式把saveTrue打开肉眼过一遍预测结果这一步能发现很多指标看不出的问题比如模型把浪花当成船、或者把远处的桥墩误检。5. 船只检测训练避坑五条血泪排查记录这一章是我在类似规模数据集上踩过的坑每条按“现象 → 原因 → 解决”写你遇到问题时可以对照排查。5.1 现象loss 正常下降但 mAP 始终接近 0原因坐标归一化时用了绝对像素而不是相对比例或者 x_center/y_center 算反了。YOLO 的 txt 要求全部归一化到 0~1且顺序是class x_center y_center w h顺序错了 loss 照样降但框全在图像外。解决抽一张图用脚本把 txt 里的框还原画到原图上肉眼确认。还原公式是xmin (x_center - w/2) * img_w如果画出来框在图像外或全挤在角落就是归一化错了。5.2 现象训练到一半 loss 突然变 NaN原因标注里有宽高为 0 的框或者 xmax 小于 xmin。VOC 标注里这种脏数据不少转换时没裁剪就会在 loss 计算时除零。解决在转换脚本里加一行过滤if w 0 or h 0: continue同时把坐标裁剪到 0~1。已经生成的数据集可以写个脚本扫一遍所有 txt把含 0 宽高的行删掉或修正。5.3 现象验证集 mAP 很高但实际图片预测全是漏检原因训练集和验证集来自同一段视频的连续帧数据泄漏导致验证指标虚高。模型记住了背景而不是船的特征。解决按视频段或文件名编号段划分数据集确保验证集里的场景在训练集里没出现过。如果数据集本身场景单一那验证指标只能作为参考必须另找外部图片做真实测试。5.4 现象小目标船只几乎全部漏检原因imgsz设得太低小目标在 640 分辨率下只剩几个像素特征图上下采样后直接消失。或者 anchor 尺寸和实际目标尺度不匹配。解决先把imgsz提到 960 或 1280 试一轮看小目标召回是否改善。如果改善明显说明是分辨率问题如果没改善检查标注里小目标的框是否准确很多漏检其实是标注漏标。YOLOv8 是 anchor-free 的不需要调 anchor但输入分辨率的影响依然很大。5.5 现象模型把云、浪花、岸边建筑误检成船原因负样本不足或者数据集中“船”的背景过于单一模型没学会区分相似纹理。解决加入纯背景负样本图片或者在数据增强里开mosaic和mixup让模型见到更多背景组合。如果误检集中在特定场景比如码头针对性补一批该场景的无船图片作为负样本比盲目加数据更有效。6. 把 3100 张船只数据用出复利增量标注与模型迭代的实操习惯跑通基线只是开始真正决定这个数据集价值的是后续迭代。我的习惯是第一版模型训完后立刻用它去预测一批未标注的船只图片把高置信度的预测框转成预标注人工只做修正这样标注效率能提升三到五倍。具体做法是用yolo detect predict输出 txt再写脚本转回 VOC 或 LabelMe 格式给标注工具用。yolo detect predict \ model./runs/ship/baseline_n/weights/best.pt \ source./unlabeled_images \ save_txtTrue \ save_confTrue \ conf0.5 \ iou0.5conf0.5是只保留置信度高于 0.5 的框iou0.5控制 NMS 重叠阈值。输出的 txt 里每行多了置信度列写个脚本把置信度低于 0.8 的框标成“待确认”高于 0.8 的标成“预标注”人工只复核待确认部分。这个流程跑顺之后3100 张的基数可以持续滚大而且新增数据的标注质量比从零标更高因为模型已经帮你过滤了大部分明显区域。另一个习惯是每次迭代都保留一份data.yaml和权重文件的对应关系用版本号或日期命名别用best.pt覆盖。船只检测的场景漂移很常见今天在港口训的模型明天拿去内河可能就掉点保留历史版本能让你快速回滚对比。我一般会在runs/ship/下按20250101_baseline_n这种格式建目录每个目录里放data.yaml副本、训练命令记录和最终权重。这个习惯看起来笨但在需要复现某个历史结果时能省掉大量“后悔药”时间。最后说一个验证技巧别只看 mAP定期抽 20 张验证集图片做可视化和上一版模型并排对比。指标涨了但肉眼看着更差的情况在船只检测里并不少见尤其是小目标密集的场景。希望帮到你。本文还有配套的精品资源点击获取
