资讯动态

海洋目标检测数据集实战:格式转换、数据划分与YOLO训练指南

发布时间:2026/10/14 11:50:27 来源:尧图企业网站定制
简介YOLO海洋目标检测数据集面向计算机视觉学习者与目标检测开发者收集1000张真实海洋场景图片包含船只、海洋生物等丰富类别并同步整理VOC、COCO、YOLO三种格式标签可直接用于YOLO系列模型训练。压缩包共2000个文件大小约23.38MB文件以XML标注、TXT标签为主另有YAML训练配置与Python脚本覆盖数据标注、配置读取与划分测试全流程。已有415人学习下载特别适合需要练习海洋目标检测的入门与进阶用户。除数据本身外还附带Windows/Linux下的YOLO环境搭建教程和训练案例以及训练集/验证集/测试集划分脚本帮助减少数据整理时间让使用者快速进入模型训练与调优阶段。1. 海洋目标检测数据集值不值得用1000张图能覆盖哪些任务做海洋监测、智慧渔业和近岸搜救的工程师十有八九卡在同一个环节模型结构不缺、训练框架不缺缺的是干净可用的标注数据。这个压缩包的标题说得很直接1000张海洋目标检测图片同时给 VOC、COCO、YOLO 三种标签格式还带划分脚本和训练教程。它解决的问题不是“能不能训”而是“拿到手之后别在格式转换、数据划分上白耗两周”。这个规模的数据集属于“打基线和练手刚好直接上生产不够”的中间档。用它跑通迁移学习、验证模型选型、测通部署流程都没问题但如果你要部署到真实海域监控系统1000张图只是一个起点后面还要靠数据增强、二次标注和持续回灌把它滚大。这篇文章会拆清楚三件事三类标签格式怎么做换算和校验、划分脚本怎么写才不泄漏、YOLO 训练参数怎么设才稳。新手建议按章节顺序做熟手可以直接跳到第 5 章对照排查。2. VOC、COCO、YOLO 三种标签格式坐标哲学与初次校验2.1 三种格式的差异从像素坐标到归一化坐标VOC 格式的核心是每个图片对应一个同名 XML 文件目标框用xmin、ymin、xmax、ymax四个绝对像素值描述。比如一张 1920×1080 的图上有一个 boatXML 里长这样annotation filename001.jpg/filename size width1920/width height1080/height /size object nameboat/name bndbox xmin120/xmin ymin300/ymin xmax640/xmax ymax720/ymax /bndbox /object /annotationCOCO 格式则把所有标注汇总到一个 JSON 文件里bbox用[x, y, width, height]表达同样是绝对像素。class 不是字符串而是category_id整数需要搭配categories数组才能映射回类别名{ images: [ {id: 1, file_name: 001.jpg, width: 1920, height: 1080} ], annotations: [ {id: 1, image_id: 1, category_id: 1, bbox: [120, 300, 520, 420], area: 218400, iscrowd: 0} ], categories: [ {id: 1, name: boat} ] }YOLO 格式最朴素每张图对应一个同名 TXT每一行是一个目标五个数字依次是class_id、x_center、y_center、width、height全部做了归一化取值在 0 到 1 之间1 0.333 0.472 0.271 0.389三个格式放在一起对比差异就非常明显了格式文件组织坐标表达类别表达典型用途VOC每图一个 XML绝对像素(xmin, ymin, xmax, ymax)name 字符串可视化、标注软件生态COCO全局一个 JSON绝对像素(x, y, w, h)category_id 整数评估指标、跨框架协作YOLO每图一个 TXT归一化后(cx, cy, w, h)从 0 开始的索引YOLO 系列训练直接读取三种格式本身没有优劣差别在于场景。做迁移学习训练YOLO 格式最省事做可视化排查、和其他团队交换标注VOC 最直观做统一的评估和指标计算COCO 最标准。所以这个数据包把三种格式都给了真正的价值不是让你选一种而是让你在不同环节用不同格式不用自己做交叉换算。2.2 格式转换的三个易错点从 VOC 或 COCO 转到 YOLO计算并不复杂核心公式是把像素坐标换成比例坐标。以 VOC 为例x_center (xmin xmax) / 2 / image_width y_center (ymin ymax) / 2 / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height公式简单但真正翻车的地方从来不在公式本身。第一个坑是零宽高框。标注软件里偶尔会出现xmax xmin或ymax ymin的废框转成归一化坐标后宽或高直接变成 0YOLO 训练时很容易让 loss 变成 NaN。转换脚本里必须加一道过滤宽或高小于 1 像素的框直接丢弃同时记录到日志里方便回溯。第二个坑是归一化后的精度。归一化坐标经常是0.333333这种无限小数如果只保留三四位小数小目标框在图像上会有几个像素的偏移。海洋场景里远处的小船可能只占十几个像素这种量化误差足以让 label 完全错位。我一般保留至少 6 位小数宁可文件大一点。第三个坑出现在 COCO 的category_id上。COCO 的类别 ID 通常从 1 开始而且不一定是连续的 0、1、2转换成 YOLO 时必须拿着categories数组做一次映射让类别编号从 0 开始重新排序。否则你训练配置里的names和 TXT 里的class_id对不上训练跑完 mAP 永远是 0。这个问题排在所有转换事故的第一名后面第 5 章会再展开。2.3 拿到压缩包后先做的一次完整性校验我拿到任何数据集的第一件事不是解压后直接开训而是先跑一个完整性校验脚本。这个脚本能同时查出“标签缺失”“坐标越界”“类别索引越界”“图片读不出来”四类问题。1000 张图的包看起来不大但漏掉几条坏标签训练时排查代价远大于现在花几分钟检查import os import sys from pathlib import Path from PIL import Image def check_yolo_format(images_dir: str, labels_dir: str, num_classes: int) - None: img_exts {.jpg, .jpeg, .png, .bmp} bad_files [] empty_files [] for img_file in sorted(Path(images_dir).glob(*)): if img_file.suffix.lower() not in img_exts: continue label_file Path(labels_dir) / f{img_file.stem}.txt if not label_file.exists(): bad_files.append(f{img_file.name}: label missing) continue with open(label_file, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty_files.append(img_file.name) for line in lines: parts line.split() if len(parts) ! 5: bad_files.append(f{img_file.name}: bad line - {line}) continue cls_id, x_center, y_center, w, h parts cls_id int(cls_id) x_center, y_center, w, h float(x_center), float(y_center), float(w), float(h) if cls_id 0 or cls_id num_classes: bad_files.append(f{img_file.name}: class {cls_id} out of range) if not (0 x_center 1 and 0 y_center 1 and 0 w 1 and 0 h 1): bad_files.append(f{img_file.name}: bbox out of range - {line}) try: with Image.open(img_file) as im: im.verify() except Exception: bad_files.append(f{img_file.name}: image corrupt) print(fchecked {len(list(Path(images_dir).glob(*)))} files) print(fempty labels: {len(empty_files)} - {empty_files[:5]}) if bad_files: print(problem list:) for item in bad_files[:50]: print( , item) sys.exit(1) print(all labels look good) if __name__ __main__: check_yolo_format(images_dirdata/images, labels_dirdata/labels, num_classes4)这个脚本的逻辑分三步先图片找标签再逐行解析标签内容最后用 PIL 验证图片是否完整可读。注意num_classes必须和你训练配置里的nc一致这是检查类别索引越界的唯一依据。运行前把images_dir和labels_dir改成解压后的实际路径。如果脚本输出all labels look good说明格式层面没问题可以放心进下一步。3. 用划分脚本把数据集切成训练/验证/测试集三个原则和一个脚本3.1 为什么不要手动拖文件夹1000 张图听起来不多很多人图省事直接在文件管理器里手动分文件夹。这个做法在常规数据集上勉强能用在海洋目标检测数据集上大概率翻车原因是海洋监控数据通常来自视频抽帧。假设一段 10 分钟的航拍视频按每秒 1 帧抽帧得到 600 张图片前后相邻几十帧画面几乎是同一艘船、同一片海。手动划分时你看到的只是文件名根本认不出哪些是同一段视频里的连续帧于是训练集和验证集里都混进了画面高度相似的图。训练时模型相当于“提前见过”验证集的画面mAP 虚高。等模型换个水域上线真实性能暴跌几个点这就是典型的数据泄漏。划分要遵守三个原则。第一个是按源分组如果文件名里有视频 ID 或拍摄批次信息划分时必须以整个源为最小单位而不是单张图。第二个是固定随机种子每次划分结果可复现后续排查问题能知道每一张图到底去了哪个集合。第三个是分层抽样保证每个集合里各类别的样本比例和全量数据接近避免某个类别全跑到测试集里训练时根本没见过。比例上我一般用 7:2:1也就是 70% 训练、20% 验证、10% 测试。测试集只在最终评估时用一次调参过程只碰训练集和验证集。1000 张图按这个比例切测试集 100 张作为最终性能参考足够。3.2 一个可直接改用的划分脚本下面的脚本按“组”进行划分组内图片看作不可分割的整体从根本上杜绝同源帧泄漏import argparse import random import shutil from collections import defaultdict from pathlib import Path def parse_args(): parser argparse.ArgumentParser(descriptiongroup-aware dataset split) parser.add_argument(--src_dir, typestr, requiredTrue, helpdata dir containing images and labels) parser.add_argument(--out_dir, typestr, requiredTrue, helpoutput dir) parser.add_argument(--ratios, typefloat, nargs3, default[0.7, 0.2, 0.1], helptrain/val/test ratios) parser.add_argument(--seed, typeint, default42, helprandom seed) return parser.parse_args() def group_key(filename: str) - str: # 按文件名里的前缀分组例如 video01_frame001.jpg 属于组 video01 # 如果文件名没有明确分组前缀就按连续帧的帧号区间分桶 parts filename.split(_) if len(parts) 2 and parts[0].startswith(video): return parts[0] # fallback: 直接把整张图看作独立组只做纯随机划分 return fimg_{filename} def main(): args parse_args() random.seed(args.seed) src_images Path(args.src_dir) / images src_labels Path(args.src_dir) / labels assert src_images.exists() and src_labels.exists(), src dir must contain images/ and labels/ img_files sorted(src_images.glob(*)) groups defaultdict(list) for img_file in img_files: if img_file.suffix.lower() not in {.jpg, .jpeg, .png}: continue key group_key(img_file.name) groups[key].append(img_file.name) group_names list(groups.keys()) random.shuffle(group_names) total_groups len(group_names) n_train int(total_groups * args.ratios[0]) n_val int(total_groups * args.ratios[1]) split_map {} # group_name - split_name for i, group in enumerate(group_names): if i n_train: split_map[group] train elif i n_train n_val: split_map[group] val else: split_map[group] test for split_name in [train, val, test]: (Path(args.out_dir) / images / split_name).mkdir(parentsTrue, exist_okTrue) (Path(args.out_dir) / labels / split_name).mkdir(parentsTrue, exist_okTrue) for group, files in groups.items(): split_name split_map[group] for fname in files: src_img src_images / fname src_lbl src_labels / f{Path(fname).stem}.txt shutil.copy2(src_img, Path(args.out_dir) / images / split_name / fname) if src_lbl.exists(): shutil.copy2(src_lbl, Path(args.out_dir) / labels / split_name / f{Path(fname).stem}.txt) print(ftrain groups: {n_train}, val groups: {n_val}, test groups: {total_groups - n_train - n_val}) print(foutput written to {args.out_dir}) if __name__ __main__: main()运行方式很简单python split_dataset.py --src_dir ./ocean_data --out_dir ./ocean_data_split --ratios 0.7 0.2 0.1 --seed 42这段脚本的关键在group_key函数。它按文件名前缀把图片归组video01_frame001.jpg这种命名会被归到video01组于是同一条视频的帧永远进同一个集合。如果你的文件名不是这种结构把group_key换成 ID 命名规则即可。--seed 42保证任何一次运行都能复现同样的划分结果这是后续排错的基础。复制文件而不是移动文件原始压缩包和解压目录保持不动随时可以重新划分。3.3 划分后必做的三分钟验证划分完成不等于划分正确。我每次都会花几分钟做三件事数文件、看类别分布、查跨集相似样本。数文件最直接用命令行统计各集合图片数for split in train val test; do echo $split images: $(find ocean_data_split/images/$split -type f | wc -l) echo $split labels: $(find ocean_data_split/labels/$split -type f | wc -l) done如果某集合图片数和标签数对不齐大概率是源数据本身缺标签需要回到数据包重新做校验。类别分布我习惯统计每个集合里的标注框数量手头没现成脚本的话一个快速方法是用 Python 读一遍所有 TXT对class_id计数。重点看训练集和验证集的比例是否接近如果一个类在训练集有 80% 的框、验证集只有 5%后续训练会出偏向。最后查一下是否有同名但内容完全相同的图片跨集合出现。视频抽帧本来就会产生大量高度相似帧划分脚本按组切割已经规避了绝大多数泄漏。检查时可以用图片的 MD5 值做精确查重重复图片如果出现在不同集合说明分组逻辑里漏了某些命名规则必须修group_key后重新划分。4. 把训练教程跑起来从目录结构到盯 loss 曲线4.1 训练前准备目录、data.yaml、预训练权重用 YOLO 官方生态训练目录结构最常见的组织方式如下images放图片labels放 YOLO 格式 txt训练集和验证集各自一个子目录ocean_data/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/接着写data.yaml这是训练时要传入的核心配置文件path: /absolute/path/to/ocean_data train: images/train val: images/val test: images/test nc: 4 names: [boat, swimmer, buoy, fish]这里唯一不能出错的就是names的顺序。YOLO 训练时会直接把 TXT 里的class_id映射到names的索引位置names[0]对应 0 号类别。如果转换标签时用了另一套类别顺序这里必须完全一致否则训练过程毫无报错mAP 却始终上不去。预训练权重选择上1000 张图这个规模我建议优先用yolov8n或yolov8s。n 和 s 参数量小在小数据集上不容易过拟合训练速度快迭代试错成本低。这些权重通常是从大规模公开数据集上预训练得到的哪怕海洋目标和自然图像差异不小前几层的基础纹理、边缘特征依然能直接复用。4.2 训练命令和参数的取舍两类常见的入口都兼容上面的目录和配置。用 YOLO 命令行 API 是最省事的方式yolo detect train \ dataocean.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ projectocean_exp \ namebaseline如果用的是 YOLOv5 分支对应命令是python train.py \ --data ocean.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --epochs 100 \ --img 640 \ --patience 20没必要纠结用哪个分支两者训练出来的模型结构和推理方式很接近选你更熟悉的就行。几个关键参数值得仔细权衡参数建议值说明imgsz640海洋小目标多显存够用就上 1280先 640 跑通再调整batch16受显存限制用 n 系列模型可以尝试 32epochs1001000 张图不需要太长早停开启后通常 70 轮收敛patience20验证集指标连续 20 轮不提升就停止cacheTrue小数据集完全能放进内存明显加快训练workersCPU 核数的一半过高的 IO 线程反而拖慢训练不要想着一步到位。我一般先用imgsz416和epochs30跑一版确认数据链路、标签映射、loss 下降都没问题再上完整参数。这个方法能区分“代码配置问题”和“模型效果问题”省下的调试时间比多跑一次训练的时间划算得多。4.3 训练日志要盯的三个信号训练启动后不需要一直盯着终端但每轮日志里这三项必须关注。第一是各种 loss 值。box_loss和cls_loss在前 20 个 epoch 内应当稳步下降如果有某个 loss 降不下去甚至升高往往指向标签错误尤其是 0 宽高框或者坐标越界框。第二是mAP_0.5它应该从某个小值开始爬升如果一直停在 0 且 loss 看着正常回到第 2 章检查类别索引。第三是精确率和召回率的关系海洋搜救场景对召回率更敏感漏一个人远比误检一个浮标严重所以recall比precision更值得盯。训练结束或者中途停住后用验证集跑一次推理看具体效果yolo detect val \ dataocean.yaml \ modelocean_exp/baseline/weights/best.pt \ imgsz640这里best.pt是训练过程中验证集指标最好的权重。输出会包含每类别的 mAP 和 PR 曲线保存成图片可以直接看。如果单看曲线不好定位问题把几张典型图片的预测框画出来对照原图错到哪一目了然。如果训练中途断电或想接着跑不需要从头开始。YOLO 命令行 API 直接加resumeTruev5 分支加--resume会自动从上次保存的 checkpoint 继续。这个功能在长训练中非常实用。5. 海洋目标检测的典型翻车点从标签错位到小目标漏检5.1 训练没报错但 mAP 是 0先查标签不要查模型现象训练过程完全正常loss 在下降日志也没报错但每个类别的 mAP 全是 0验证集预测框全部偏到画面边缘或直接消失。原因九成是类别索引错位。数据集转成 YOLO 格式时TXT 里的class_id是按某个自定义顺序写的但data.yaml里的names用的是另一套顺序。比如转换脚本里 boat 是 1swimmer 是 2训练配置里names[1]却是 swimmer模型永远在用错误的映射学习训练目标自相矛盾。解决打开任意一个 TXT遍历所有标签确认class_id最大值小于nc然后手动检查data.yaml的names顺序和标签生成时的种类顺序做逐一比对。更稳的办法是写一个映射文件固定类别顺序转换脚本和训练配置都从同一个文件读取从源头杜绝两边不一致。5.2 验证集 loss 高位震荡同源帧污染了划分现象训练集 loss 降得很顺利验证集 loss 却大幅波动验证集 mAP 频繁异常跳动。即使增大训练集也没改善。原因第 3 章说过的同源帧泄漏。海洋目标检测数据大量来自视频抽帧相邻几十帧内容高度相似。如果划分时按单张图随机切分同一段视频的画面同时出现在训练集和验证集模型在验证集上表现虚高换个拍摄场景立刻原形毕露。解决按视频源分组划分而不是按单张图。如果文件名里没有标注视频 ID可以用时间戳或者帧号推测连续区间把连续帧序列整体分到同一个集合。已经按错误方式划分的重新跑一次划分脚本不要想着手工纠偏。5.3 远处的船和游泳者几乎全部漏检小目标问题现象大目标的检测效果尚可但远处的小船、露出水面的人头几乎全漏mAP_0.5勉强能看mAP_0.5:0.95低得离谱。原因海洋场景里小目标占比天然高远处的船在整张图里可能只占几十个像素。网络经过多次下采样后这些小目标在深层特征图里只剩几个像素的信息特征丢失严重。模型本身没问题是输入分辨率和目标尺度不匹配。解决首选直接加大imgsz到 1280显存不够时用 SAHI 一类的切片推理把大图切成 tile 分别检测再合并结果。训练时开启多尺度训练让模型在训练过程中看到不同分辨率的输入。不要指望盲目换大模型来解决大模型参数量更大小数据集上更容易过拟合小目标问题反而加重。5.4 模型只认船不认游泳者和浮标类别不平衡现象训练结束后 boat 类别的 mAP 很高swimmer 和 buoy 却几乎不输出推理时所有框都落在船上。检视数据发现 boat 的标注框数量远多于其他类别。原因类别样本数量差距过大时总 loss 被大类别主导小类别学不出来。检测框架默认对所有类别平等看待但实际分布并不平等。解决最简单的路径是给少数类增加副本对少数类样本做水平翻转、亮度扰动等增强后补进训练集进阶做法是修改cls_loss的类别权重让类别权重和样本数量成反比。推理阶段也可以对少数类别单独降低 confidence 阈值比如 boat 用 0.25、swimmer 用 0.1平衡采回率。这个办法不改训练立竿见影。6. 把 1000 张图的价值榨干迁移学习、数据增强与二次标注的取舍小数据集的第一铁律不要从零训练。我拿到这个包后的习惯是直接加载预训练权重前 30 个 epoch 冻结 backbone 前两阶段只训练检测头等验证集 loss 不再下降再解冻全部参数微调。海洋目标里的船、岸线、浮标其底层纹理和公开数据集中的物体特征有很强共性这部分不必重新学把训练资源集中在任务相关的特征上收益更高。数据增强要按类别精细调不能一把梭全开。对海洋场景我常用的配置是mosaic开、scale上下限收紧到 0.8 到 1.2防止目标被缩放得过了头hsv亮度扰动给 0.2 就够海天背景本身光照变化不大扰动过强反而让模型学到错误的颜色不变性。fliplr一定要开船的朝向在检测任务里没有语义信息水平翻转等于让有限样本直接翻倍。不要开那些会把小目标强行缩没的增强例如大幅的 random resize海洋场景的小目标本来就稀缺增强后目标消失等于白费样本。二次标注是扩大数据集性价比最高的手段。用当前模型对一批没有标注的海域截图做预测把置信度 0.1 到 0.5 之间的检测结果导出成候选框再用标注工具人工确认只挑模型最容易漏的硬样本补回训练集。100 张高质量硬样本的收益通常超过随机新标 500 张。这个流程可以滚动执行每补一批就重训一次再更新一次候选框几次迭代后模型在陌生水域的表现提升非常明显。最后保留一个必须坚持的验证习惯留出一组“陌生水域”截图从训练和验证环节里彻底隔离只在最终评估时使用。模型在训练集和验证集上的分数再漂亮这组铁律测试过不了就不部署。这个习惯帮我拦住了好几次“本地跑得好、上线就崩”的发布事故。我现在拿到任何数据包第一件事都是跑格式校验和划分检查确认无误再点火训练这习惯帮我省掉了太多白跑的 epoch。希望你能绕开这些坑更快把这 1000 张图变成能用的模型。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑