YOLO海洋目标检测数据集:5000张图、三种标签格式与训练落地路线
简介在计算机视觉工程中数据集的质量与格式直接决定目标检测模型的训练效率。VOC、COCO与YOLO是三种最常用的标注格式分别适用于人工审核、跨框架评测与模型训练。理解它们的坐标表示与转换逻辑能有效避免格式错位导致的无效训练。对于海洋遥感、港口监控、无人机海面巡查等应用场景一套包含5000张真实海面图像并提供三种标签格式的数据集可大幅降低标注成本帮助工程师快速跑通YOLO训练流程同时沉淀一套可复用的标签管理与数据划分习惯。1. 海洋目标检测别再自己标数据5000 张图、三种标签格式与一条落地路线YOLO海洋目标检测数据集含5000张图片这类资源是开始海面目标检测项目时最省时间的入口。我第一次做海上船只检测时光标注就花了两周后来拿到这种带VOC、COCO、YOLO三种标签格式的包直接省掉了格式转换和划分的重复劳动。它解决的核心问题是用现成数据把YOLO训练流程跑通同时留下一套可复用的标签管理习惯。适合做海洋遥感、港口监控、无人机海面巡查的算法工程师和研究生如果你手头完全没有标注数据它能让你当天看到训练曲线。不过资源包也不是万能药后面我会讲清楚哪些地方必须自己再动一次手。2. 三种标签格式的结构与选型VOC、COCO、YOLO 各自适合哪个环节这个包里带三种格式不是炫技而是目标检测工具链本来就是割裂的标注工具大多导出VOC训练框架有的吃COCO而YOLO训练又需要txt。很多教学代码只教一种格式真正做项目时才发现要来回转。为了避免拿到资源后两眼一抹黑先把三种格式的存储结构、坐标含义和适用场景说清楚。我会从VOC开始因为它是人工最容易理解的“中间格式”。2.1 VOC XML一张图一个文件人工核对时最好用VOC格式的标签是XML文件和图片同名通常放在Annotations目录下。每个XML里有filename、source然后一个object就是一个目标object下面的bndbox给出xmin、ymin、xmax、ymax。坐标是绝对像素值不做任何归一化。我的习惯是拿到数据集后先随机抽10个XML用文本编辑器直接看能迅速判断标注质量也能发现比如“类别名大小写不一致”这类问题。下面是一段解析VOC XML的Python代码用来快速预览一张图的标注import xml.etree.ElementTree as ET tree ET.parse(Annotations/0001.xml) root tree.getroot() print(图片名:, root.find(filename).text) for obj in root.iter(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) print(f{name}: ({xmin}, {ymin}) - ({xmax}, {ymax}))说明这段代码能把XML里的类别和边界框原样打印出来。VOC里坐标是像素绝对值所以一旦图片做了resizeXML里的坐标不会跟着变必须配合原图分辨率来用。这也是很多转换脚本出错的源头直接用VOC坐标除以resize后的宽高得到错误的归一化框。VOC还有一种常见字段是difficult如果某个目标特别模糊标注者会打上1表示这张框不参与评估处理数据时要注意保留而不是随手删掉。VOC格式最适合人工Review因为它可读性强、单文件独立用LabelImg导出的默认格式就是它。缺点是文件数量多类别是字符串而不是数字ID转YOLO时如果大小写或中英文不一致很容易让两个类别挤到同一个ID里。如果你只是在YOLO上训练我一般会把VOC当成“中间格式”而不是最终训练格式。2.2 COCO JSON一个文件搞定所有但细节让新人进黑匣子COCO格式把所有标注塞进一个JSON文件与VOC完全相反。顶层有三个关键字段images数组每项有id、file_name、width、heightannotations数组每项有id、image_id、category_id、bbox、area、iscrowdcategories数组每项有id、name。这里的bbox是[x, y, width, height]即左上角坐标加框宽高单位是像素。我见过不少新手把COCO的bbox当成VOC的xmin,ymin,xmax,ymax直接用结果训练出来预测框全是歪的。COCO比较反直觉的点还有两个一是category_id通常从1开始而YOLO的类别编号从0开始转换时一定要做减一操作二是iscrowd字段如果一张图里有密集的船群标注时被标成一个crowd对象COCO的评估工具会把crowd区域里的其他检测视为误检导致mAP看起来比实际低得离谱。另外annotations里还有area字段通常等于bbox宽乘高但有些转换工具生成时可能为0会让训练框架在采样时跳过这个目标。下面这段代码用于读取COCO文件并检查前三条图片记录顺便检查每个类别的目标数量import json from collections import Counter with open(annotations/instances.json) as f: coco json.load(f) print(categories:, coco[categories]) print(图片数量:, len(coco[images])) print(标注数量:, len(coco[annotations])) for img in coco[images][:3]: print(img[id], img[file_name], img[width], img[height]) cat_counter Counter(ann[category_id] for ann in coco[annotations]) print(每个类别的目标数:, dict(cat_counter))说明先打印categories可以确认类别ID和名称是否与业务对应。如果资源包里COCO的categories和你预期的类别顺序不一样后续做迁移评估时一定要小心。COCO格式的优势是评估指标标准统一MMDetection、Detectron2等框架都直接用劣势是单个JSON可能几十上百MB且新人很难直观地把标注映射回图片上一旦出错排查成本高。对于YOLO训练COCO只是“中间交换格式”不是最终格式。2.3 YOLO TXT归一化坐标直接喂给损失函数最容易出错的也是这里YOLO格式下每张图片对应一个同名txt文件通常放在labels目录里。每行代表一个目标格式是class x_center y_center width height。注意前四个值都是相对于图片宽高的归一化值范围0到1左右class是一个从0开始的整数。坐标是中心点格式不是左上角也不是右下角。很多VOC转YOLO的脚本都卡在xyxy转xywh这一步。下面这段代码读取一个YOLO txt并打印每个目标的类别和归一化框同时过滤出异常行with open(labels/0001.txt) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(异常行:, line) continue cls int(parts[0]) cx, cy, w, h map(float, parts[1:]) print(f类别{cls}: 中心({cx:.4f}, {cy:.4f}) 宽高({w:.4f}, {h:.4f}))说明如果发现某一行坐标值大于1很多基本是转换脚本忘了归一化如果类别值大于类总数减一说明类别ID从1开始了需要整体减一。YOLO txt是ultralytics框架直接读取的格式训练时不再需要额外解析所以最终进入训练流程的几乎都是它。这个包同时给出VOC、COCO、YOLO三种格式就是为了让你在不同阶段挑着用人工校对用VOC做评测和迁移到其他框架用COCO训练YOLO用txt。补充一个三个格式的对比表方便快速做选择格式存储方式坐标形式适合阶段VOC XML每图一个XML像素绝对值xyxy人工标注、审核、转格式COCO JSON单一大JSON像素值x,y,w,h评测、MMDetection等框架YOLO TXT每图一个txt归一化cx,cy,w,hultralytics训练与部署用表格列出来项目里谁负责哪一段就一清二楚了。如果还打算做目标检测的跨库评测COCO的JSON是唯一能直接进官方评估工具的形式所以保留它不会亏。3. 划分脚本怎么用从解压到生成 train/val/test 的完整流程与校验3.1 解压后的第一件事不是训练而是核对图片与标签的“账”拿到RAR解压后目录里通常有images、labels、xmls、annotations之类的文件夹还有划分脚本和训练教程。这时候不要急着把代码往训练代码里塞先跑一遍账目核对确认图片数和标签数的比值、每个类别的样本量、空标签文件的数量。这一步能让你在动手前就知道数据集有没有明显问题避免后面训练到一半才发现某类样本只有10张。我用这段Python脚本做最基础的统计import glob from collections import Counter img_files glob.glob(images/*.jpg) label_files glob.glob(labels/*.txt) print(f图片数: {len(img_files)}) print(fYOLO标签数: {len(label_files)}) cls_counter Counter() empty_labels [] for lbl_path in label_files: with open(lbl_path) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty_labels.append(lbl_path) for line in lines: cls_counter[int(line.split()[0])] 1 print(类别编号分布:, dict(cls_counter)) print(空标签文件数:, len(empty_labels))说明类别编号分布能立刻看出有没有失踪的类别。比如你期待类别是0到4但统计出来只有0、1、2、4说明3号类别为空训练时就会有一个类别永远学不到。空标签文件也要单独记下来因为这些图片属于“负样本”对减少误检有作用划分时不能直接丢弃。3.2 同步划分图片、YOLO txt、VOC XML 必须按同一套名字走训练集、验证集、测试集的划分看起来简单实际上最常翻车的点是只挪了图片忘了挪对应的txt或xml或者用不同随机数各划了一次导致验证集里出现训练图片。我一般按图片名不带后缀作为唯一主键先对所有图片名洗牌再按比例切片最后同时复制图片、txt、xml。这样才保证同一个目标在三个集合里只出现一次。下面是项目里常用的一份划分脚本比例默认是train/val/test7/2/1import os import random import shutil random.seed(42) img_dir images yolo_dir labels voc_dir xmls out_dir dataset_split # 以jpg为例主流海洋数据集大多是jpg或png names [f[:-4] for f in os.listdir(img_dir) if f.endswith(.jpg)] random.shuffle(names) n len(names) n_train int(n * 0.7) n_val int(n * 0.2) splits { train: names[:n_train], val: names[n_train:n_train n_val], test: names[n_train n_val:], } for split, file_names in splits.items(): os.makedirs(f{out_dir}/images/{split}, exist_okTrue) os.makedirs(f{out_dir}/labels/{split}, exist_okTrue) os.makedirs(f{out_dir}/voc/{split}, exist_okTrue) for name in file_names: shutil.copy(f{img_dir}/{name}.jpg, f{out_dir}/images/{split}/{name}.jpg) if os.path.exists(f{yolo_dir}/{name}.txt): shutil.copy(f{yolo_dir}/{name}.txt, f{out_dir}/labels/{split}/{name}.txt) if os.path.exists(f{voc_dir}/{name}.xml): shutil.copy(f{voc_dir}/{name}.xml, f{out_dir}/voc/{split}/{name}.xml)说明这里的核心是random.seed(42)固定随机种子保证每次划分结果可复现方便别人复现代码。第二个关键是os.path.exists判断如果某张图没有txt或xml脚本不会中断而是在文件系统里留下一个“缺标签”的样本。我建议第一次跑完后立刻做3.4的校验不要抱着“应该都有”的心态。3.3 COCO 划分不能直接切文件要按 image_id 重新生成子 JSON上面脚本处理了VOC和YOLOCOCO格式是一个大JSON不能简单复制文件。划分时先建立文件名到image_id的映射再筛出该集合内的images和annotations生成新的子JSON。很多旧脚本只复制图片和txt完全忘了同步COCO导致后面想用COCO格式评估时val JSON里混着train的标注。我用下面这段代码补上COCO子集生成import json with open(annotations/instances.json) as f: coco json.load(f) file_to_img_id {img[file_name]: img[id] for img in coco[images]} for split, file_names in splits.items(): image_ids {file_to_img_id[name .jpg] for name in file_names} sub_coco { images: [img for img in coco[images] if img[id] in image_ids], annotations: [ann for ann in coco[annotations] if ann[image_id] in image_ids], categories: coco[categories], } with open(f{out_dir}/coco_{split}.json, w) as f: json.dump(sub_coco, f, indent1)说明这里用集合推导式收集image_ids再筛选images和annotations。如果数据集里的file_name带有子目录前缀比如“images/ship/001.jpg”上面的name .jpg就会出错需要先用真实file_name做匹配。我建议先打印file_to_img_id的前几个键确认文件命名的边界条件。3.4 划分后的一致性校验这份“坏文件清单”比肉眼可靠划分完成后不能直接开训最后要做一次一致性校验。我的做法是分别统计每个split下图片stem集合和标签stem集合打印出差集。差集为空才继续有差集就回到划分脚本修而不是在训练报错时再去猜。from pathlib import Path def validate_split(split): img_root Path(fdataset_split/images/{split}) lbl_root Path(fdataset_split/labels/{split}) img_stems {p.stem for p in img_root.glob(*.jpg)} lbl_stems {p.stem for p in lbl_root.glob(*.txt)} missing_lbl img_stems - lbl_stems missing_img lbl_stems - img_stems if missing_lbl or missing_img: print(f[{split}] 图片缺标签: {len(missing_lbl)} 个, 标签缺图片: {len(missing_img)} 个) print(示例:, list(missing_lbl)[:5], list(missing_img)[:5]) else: print(f[{split}] 检查和图片一致共 {len(img_stems)} 张) for s in [train, val, test]: validate_split(s)说明这一步相当于给划分操作吃了颗后悔药能在损失几千块钱算力之前发现文件级错误。如果你之后用了别的增强脚本也建议把校验脚本留到管线里每次生成新数据集都跑一遍。4. 训练教程跑通ultralytics 环境配置、data.yaml 与最小训练命令4.1 环境配置用 ultralytics 一个包就能开始先说环境。ultralytics这个框架把数据加载、数据增强、训练、评估和导出都封装好了安装是最省心的一环。对于0基础纯小白我强烈建议先用conda建一个独立环境不要用系统的base环境因为YOLO的依赖和公司内其他项目经常互相冲突。这里给出最小安装命令conda create -n yolo python3.10 -y conda activate yolo pip install ultralytics说明python3.10是常见稳定版本3.11、3.12也能跑但遇到某些旧显卡驱动时3.10的兼容问题最少。装完之后跑一下python -c import torch; print(torch.cuda.is_available())如果输出False说明当前环境没有可用CUDA训练会沦落到CPU上速度慢到线性增长的心理准备。这个环境配置步骤是通用的和数据包无关。如果公司网速一般第一次运行训练命令时下载yolov8n.pt可能会卡住。我一般会先手动把预训练权重放到当前工作目录再运行命令避免终端悬挂在进度条上。这个手动下载和训练命令下载是同一件事区别只是先把文件放到位。4.2 data.yaml 怎么写nc、names 与路径映射ultralytics训练时读取一个data.yaml告诉框架图片和标签在哪里、类别有哪些、名称是什么。目录结构建议按官方约定images/train、images/val、labels/train、labels/val且images和labels同级。如果你的数据已经由前面划分脚本生成那么只需要写一个data.yamlpath: /data/ocean_dataset_split train: images/train val: images/val test: images/test nc: 4 names: 0: ship 1: boat 2: buoy 3: platform说明path是数据集根目录train和val是相对path的子目录。这些路径可以直接写绝对路径但为了配合多机训练我一般写成相对路径。nc是类别数量names列表的顺序必须和YOLO txt中class数字一一对应。如果你把VOC标签里的字符串名直接按字母序填进来但txt编号是按原数据集顺序生成的结果就是“ship”类被当成“boat”类训练mAP曲线还不会报警这是最难排查的一类问题。这里需要多说一句预训练权重ultralytics的预训练权重是在COCO80类数据集上训练的而我们自定义数据集只有4类。加载权重时检测头和分类头形状不一致ultralytics会自动丢弃原来的输出头并重新初始化。这也是为什么你不需要手动改权重文件但要在data.yaml里正确写nc和names。4.3 最小训练命令与三个必调参数数据准备就绪后一条最小训练命令长这样yolo train dataocean.yaml modelyolov8n.pt epochs100 batch16 imgsz640 patience20说明modelyolov8n.pt表示用YOLOv8n的预训练权重作为初始点它会从网络加载如果已有本地权重就在本地加载。epochs100是总轮数batch16是每批图片数imgsz640是将输入图片缩放到640x640。这三个参数直接决定训练时长和显存占用是每个项目都绕不开的必调项。imgsz在海洋目标检测里尤其关键。如果你的原图分辨率是1920x1080很多远处船只只有20x20像素直接缩到640小目标几乎消失。常见做法是把imgsz提高到1280代价是显存和训练时间成倍增加。我一般先跑一版640看整体mAP再跑1280对比否则容易把算力浪费在无效实验上。patience20是早停参数连续20轮mAP没有提升就停止训练。这个参数能省不少时间但如果你把epochs设成500而模型在300轮才真正开始收敛早停可能提前杀掉训练。所以初跑阶段我习惯设大一点比如50确认学习的曲线被完整记录后再决定。训练日志里还会输出box_loss、cls_loss、dfl_loss三项损失这三个共同组成YOLO的损失函数。我一般不看总loss而是分别看box_loss高说明边框回归还没学好cls_loss高说明类别混淆严重dfl_loss高通常和目标框大小分布有关。如果cls_loss连续不降优先去看类别样本数是否有极端不均衡而不是硬调学习率。4.4 第一次训练后看哪些指标训练结束后会在runs/detect/train目录下生成权重和曲线。我第一件事不是看mAP而是看results.png里的mAP50和mAP50-95曲线。如果mAP50能超过0.6这个数据集基本可以用了如果低于0.4建议先怀疑数据清洗再怀疑网络结构。另外一定要看confusion_matrix.png这张图能显示两个类别之间互相混淆的情况。海洋目标里“ship”和“boat”经常因为尺寸大小差异不明显而互相混如果这两个类在混淆矩阵里频繁互换可以考虑合并成同一个类或者重新优化标注边界。很多教程不会提醒你看这张图但它比mAP更能指出数据定义问题。在训练教程里ultralytics也内置了验证命令最简单的是yolo val modelruns/detect/train/weights/best.pt dataocean.yaml说明训练完成后用best.pt在测试集上验证比训练日志里最后一段更可靠因为它用的是test split而非val。资源包里如果有官方划分脚本大概率也是这样约定。5. 避坑指南海洋数据集训练最常见的 5 个翻车现场5.1 数据侧标签格式转换错位带来的无效训练第一个翻车现场训练了50轮loss还在3.0以上曲线完全下不来。现象是mAP50一直接近0训练loss也不减。原因几乎都是标签坐标格式错误。比如把COCO的x,y,w,h直接当成YOLO的cx,cy,w,h使用或者把VOC的xyxy转成xywh时忘记除以宽高。解决方法是写一个可视化脚本把训练数据集的ground truth画到原图上人为看10张图只要框的位置明显不对立刻停止训练重新检查转换代码。第二个翻车现场验证集mAP很高但实际跑一段海面视频发现模型拼命检测海浪。现象是在训练集JPG上效果完美一上视频全是误检。原因一般是负样本太少模型把背景和前景的边界学偏了。解决是在训练集里加入一批没有任何目标的海洋空景图片对应空标签文件模型才能学会“这里没有船”的置信度。这也是为什么前面划分脚本不能随意丢弃空标签的原因。5.2 训练侧参数和环境导致的无效训练第三个翻车现场训练过程中显存OOM直接中断。现象是跑到一半报CUDA out of memory。原因是batch16, imgsz1280同时开太大而你的显卡只有8G显存。解决方法是先小步试错batch4, imgsz960确认loss能下降后再加大同时把workers调低避免数据加载占用过多内存。第四个翻车现场训练能跑完但val mAP50为0。现象是训练日志一切正常损失在降模型也能推理但验证集mAP全是0。原因通常是data.yaml里的names顺序和txt标签类别ID对不上或者验证集里混合了没有标签的图片。解决方法是先打印一张验证集图片的预测结果如果类别颜色和业务对不上就修正data.yaml顺序再跑一次3.4的校验脚本确认val labels不是空的。第五个翻车现场训练曲线不收敛loss上下剧烈震荡。现象是box_loss忽高忽低mAP曲线像锯齿。原因可能是学习率太高但更常见的是batch太小样本随机性太大。解决是先用lr00.0001试跑50轮如果稳定了再逐步提高或者增大batch并开启ampTrue混合精度这样能缓解显存压力同时让梯度更平滑。最后补充一个通用排查步骤当训练结果诡异时永远先可视化数据再用小模型小imgsz跑10轮做冒烟测试。冒烟测试如果还是不稳十有八九是数据管线问题而不是模型问题。这是我一遍遍踩坑之后形成的习惯。6. 进阶技巧用小目标切图推理把召回率再提一档当模型在1920x1080海图上远处船只只有二三十像素时即便训练时用了imgsz1280推理时直接整图缩放仍会丢小目标。常见做法是SAHI这类切图推理库或者自己写一个滑动窗口。我倾向先用自带脚本因为SAHI的NMS逻辑可以省掉很多合并麻烦但如果你想快速验证可行性下面这段代码足够用import cv2 import numpy as np from ultralytics import YOLO model YOLO(best.pt) def slice_inference(img, slice_size640, overlap0.2, conf0.25): h, w img.shape[:2] step int(slice_size * (1 - overlap)) detections [] for y in range(0, h, step): for x in range(0, w, step): crop img[y:y slice_size, x:x slice_size] result model(crop, confconf) for box in result[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() detections.append((x x1, y y1, x x2, y y2, float(box.conf[0]), int(box.cls[0]))) return detections说明这个函数把大图切成一个个640x640的小块每块独立推理再把检测框坐标加回偏移量。overlap设为0.2可以防止目标正好卡在切缝上。缺点是同一个大目标可能被多个窗口重复检测所以后面必须接NMS如果目标本身不算小甚至可以直接用模型自身的NMS输出结果但对重叠窗口产生的重复框最好用cv2.dnn.NMSBoxes再压一次。我的个人习惯是先训练一个1280x1280高分辨率版本再切图推理做对比。如果切图比高分辨率mAP高出5个点以上说明小目标问题值得用切图方案解决如果提升不明显就直接用整图推理省得推理管线变复杂。还有一个小技巧切图时不要缩放直接按原像素切这样小目标不会因为缩放出二次模糊同时把conf调低到0.15因为切图后一个小目标在一个窗口里占比变大模型给出的置信度往往比整图时高所以可以接受更低的阈值。我现在已经习惯在训完之后先画10张预测图看一眼漏检的是不是远处小船。如果漏检对象集中在某个尺寸区间切图推理经常比改网络结构更管用。希望帮到你。本文还有配套的精品资源点击获取