海洋目标检测数据集处理:YOLO格式转换与划分实战

发布时间:2026/9/23 8:09:50
海洋目标检测数据集处理:YOLO格式转换与划分实战 简介YOLO海洋目标检测数据集涵盖真实海洋场景高质量图片1000张面向目标检测初学者及需要标准VOC/COCO/YOLO格式数据的算法工程师可直接投入YOLO系列模型训练与评估。包体共2000个文件以xml与txt标签文件为主另有少量Python脚本、yaml配置和html说明文档标注由LabelImg人工精标场景丰富适合水下生物、漂浮物、船只等目标识别任务的验证与微调。压缩包整体仅23.38MB。资源另含环境搭建与训练教程分Windows/Linux版本以及训练集/验证集/测试集划分脚本可帮助用户快速完成数据准备并跑通训练流程适用于课程设计、毕业设计或工程预研。目前已有413人学习实用性较强。1. 为什么说1000张海洋图片的数据集真正的坑在格式和划分1000张图训YOLO最常见的翻车点不是模型不收敛而是labels和图片对不上。海洋目标检测的数据尤其如此:船载相机的图带着海面反光和抖动打标员又不止一个有人按VOC习惯导出xml有人按COCO习惯写json也有人用labelimg直接存成YOLO的txt。拿到这种RAR第一反应不该是装环境而是先把三种格式的坐标统一再把train和val划分清楚。这份含1000张图片、三套标签、划分脚本和训练教程的物料主要服务两类人:课题组做海洋生态监测、渔政巡航的yolo项目拿它当实验数据工业侧做海面目标检测的工程师拿它快速验证pipeline。两类人卡住的位置几乎都一样:格式转换、划分脚本、训练参数。接下来的章节按解压到训完的顺序来写卡在哪个环节直接拉对应章节。2. 解压后先理清目录:VOC、COCO、YOLO三种标签格式怎么统一2.1 先对齐images、labels、annotations三层的文件一一对应这类RAR解压后最常见的是下面这种结构不管压缩包内层怎么摆最终都得归成“一个图片文件名对应一份标注”的三段式。ocean-ds/ ├── images/ # 1000张 jpg/png文件名如 001.jpg ├── labels/ # YOLO格式 txt一行一个目标 ├── annotations/ # VOC的xml COCO的json 往往都在这里 ├── classes.txt # 类别清单一行为一个类别名 └── data.yaml # 部分发布者会预置没有就自己写我一般会先数一遍文件数量再用一个短的Python脚本检查“图片有标注、标注有图片”。不是因为不信任发布者而是因为不同打标员导出xml时偶发丢失空标签文件或者某个jpg带了非ASCII文件名导致同步漏掉这类问题越早发现越省事。import os from pathlib import Path img_dir Path(images) lab_dir Path(labels) imgs {p.stem: p for p in img_dir.iterdir() if p.suffix.lower() in (.jpg, .jpeg, .png)} labs {p.stem: p for p in lab_dir.iterdir() if p.suffix .txt} only_img set(imgs) - set(labs) only_lab set(labs) - set(imgs) print(有图无标签:, len(only_img), list(only_img)[:10]) print(有标签无图:, len(only_lab), list(only_lab)[:10])这段代码用文件名前缀做键把images和labels两个目录各自建成字典再做集合差。only_img和only_lab分别列出两边对不上的文件名。跑完以后对不上的文件单独挪出数据集目录不要直接删因为可能是标注漏导出而不是图片损坏。2.2 三种格式的坐标基准与换算:XML、JSON、TXT之间的转换VOC的xml是最直观的绝对像素坐标一个目标对应一组xmin、ymin、xmax、ymax且带上图片的真实宽高。COCO的json则把目标存成bbox: [x, y, width, height]同样是绝对像素但格式是左上角宽高。YOLO的txt最特殊它只存归一化中心点坐标和归一化宽高必须依赖图片的原始宽度和高度才能换算回去。annotation size width1920/width height1080/height /size object nameship/name bndbox xmin100/xmin ymin80/ymin xmax600/xmax ymax400/ymax /bndbox /object /annotation把上面的xml转成YOLO txt核心算法是先读图片尺寸再把四角坐标换算成中心点和宽高的比例。下面的函数可以直接复用:import os def voc_to_yolo(xml_path, img_w, img_h): import xml.etree.ElementTree as ET root ET.parse(xml_path).getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() cls_id classes.index(name) # classes 是读取 classes.txt 得到的列表 bnd obj.find(bndbox) xmin int(bnd.find(xmin).text) ymin int(bnd.find(ymin).text) xmax int(bnd.find(xmax).text) ymax int(bnd.find(ymax).text) center_x (xmin xmax) / 2.0 / img_w center_y (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {center_x:.6f} {center_y:.6f} {w:.6f} {h:.6f}) return linesimg_w和img_h不能从xml里的size取必须用PIL.Image.open实际读图得到因为打标软件偶尔会写错而YOLO在训练时是按真实图片尺寸解码txt的两处不一致会直接导致框偏移。同时注意所有坐标都要被图片宽高归一化写成6位小数足够不需要更多精度。三种格式的对比如下做转换前先确认自己手里的标注属于哪一列:格式存储位置坐标基准一个目标怎么写VOCxml文件左上右下绝对像素bndbox里4个数COCOjson文件左上宽高绝对像素bbox: [x, y, w, h]YOLOtxt文件中心宽高归一化一行5个数首列是类别IDCOCO转YOLO时注意:json里的bbox是左上角x、y加宽度、高度换算成YOLO需要先算中心点cx x w / 2再分别除以图片宽高。如果json里的坐标是浮点直接用;如果是int表明打标员导出时做过取整转换前要检查是否出现零宽度的框。2.3 格式互转里最容易被忽略的三个细节第一个坑是类别ID不一致。同样一个类别VOC里叫shipCOCO的categories里排在第5位YOLO的classes.txt里又排在第1位。很多人在转格式时只转了坐标没重新映射类别ID训练出来的混淆矩阵完全没法看。正确做法是先读classes.txt建立name - id的映射再从VOC或COCO里按name找到ID而不是沿用json里自带的ID顺序。第二个坑是空标签文件。1000张海洋图片里大概率有几张纯海面背景图打标员没标任何框导出YOLO格式时可能生成0字节的txt也可能根本不生成txt。YOLO训练允许空txt但划分脚本必须把这类图片也纳入统计否则训练集的类别分布和验证集不一致。第三个坑是归一化坐标越界。海面目标被截断在图片边缘时打标员容易把框拖出边界txt里出现小于0或大于1的值。Ultralytics在训练时会警告这类框但更稳妥的做法是在转换脚本里对坐标做clip把越界值拉回[0, 1]区间同时过滤掉clip后宽度或高度小于0.000001的退化框。这一步做完再进训练能省掉大量排查时间。3. 用划分脚本把1000张图片拆成train/val/test:分层抽样与防泄漏3.1 为什么小数据集的划分比模型结构更先影响结果1000张图本身不足以支撑特别深的网络它更考验的是验证集能不能代表真实海面场景。如果只是按随机数硬切最容易出现的问题是某个稀有的类别(比如特定船型或浮标)全被分到train里val里一个都没有训练过程看起来正常但mAP直接给出高得离谱或低得离谱的指标。另一类问题是数据泄漏:海洋目标检测经常把一段视频抽帧成图片相邻帧几乎一样随机划分会把同一段视频的连续帧同时分进train和val验证结果虚高部署到新场景立刻露馅。我一般会在划分前先把文件名按分隔符拆出“场景号”把同一个场景的所有帧当成一个整体参与分配。场景号通常是文件名里第一个下划线之前的部分比如video01_0001.jpg和video01_0002.jpg都归video01。这样能避免连续帧互相泄漏。没有下划线的文件名就按单张独立处理不再强分组。3.2 一个可跑的划分脚本:固定随机种子、按类别比例抽取下面的脚本用Python标准库加os实现不需要额外安装依赖。它先按场景分组再按组的大小降序做贪心分配每次把当前组放入“最缺”那个类别样本的split里保证train/val/test的类别比例尽量接近设定的比例。import os import random from collections import Counter, defaultdict random.seed(42) # 固定随机种子重跑结果一致 IMAGES_ROOT images LABELS_ROOT labels RATIO {train: 0.8, val: 0.1, test: 0.1} def group_of(stem): for sep in [_, -, ]: if sep in stem: return stem.split(sep)[0] return stem def read_cats(txt_path): cats [] if os.path.exists(txt_path): with open(txt_path, r) as f: for line in f: parts line.split() if parts: cats.append(int(parts[0])) return cats groups defaultdict(list) exts (.jpg, .jpeg, .png) for img_name in sorted(os.listdir(IMAGES_ROOT)): if not img_name.lower().endswith(exts): continue stem os.path.splitext(img_name)[0] txt os.path.join(LABELS_ROOT, f{stem}.txt) cats read_cats(txt) groups[group_of(stem)].append((stem, Counter(cats))) group_items list(groups.items()) random.shuffle(group_items) group_items.sort(keylambda x: -len(x[1])) # 大场景先分配 assigned {} split_stat {sp: Counter() for sp in RATIO} for gid, items in group_items: best_split, best_score None, float(inf) for sp in RATIO: total sum(split_stat[sp].values()) score 0.0 for _, cnt in items: for cls, num in cnt.items(): cur split_stat[sp][cls] / max(1, total) score cur - RATIO[sp] if score best_score: best_split, best_score sp, score for stem, cnt in items: assigned[stem] best_split split_stat[best_split].update(cnt) output {sp: [] for sp in RATIO} for stem, sp in assigned.items(): output[sp].append(os.path.abspath(os.path.join(IMAGES_ROOT, f{stem}.jpg))) for sp, paths in output.items(): with open(f{sp}.txt, w) as f: f.write(\n.join(paths)) print(sp, len(paths), dict(split_stat[sp]))脚本前半段负责读标签并统计每个场景的类别计数Counter(cats)记录每个类别在当前图片里出现几次。分配阶段的score计算了当前split里每个类别的占比与目标占比的差距分数越小代表这个split越缺这类样本就把当前场景整组放进去。这样得到的train.txt、val.txt、test.txt是图片路径列表YOLO训练时会按文件列表去读取。3.3 划分完必做的三项检查:数量、类别覆盖、路径第一项是数量校验。train、val、test三份txt的行数加起来必须等于1000多一行少一行都说明分组逻辑漏了图片。第二项是类别覆盖校验我通常直接在上面的脚本打印结果里看每个split的Counter如果一个类别在val里为0就需要把包含该类别的场景手动拨几帧进val。第三项是路径校验txt里存的是绝对路径换机器后路径前缀会变所以训练前要单独跑一个探活脚本:import os for split in [train.txt, val.txt, test.txt]: if not os.path.exists(split): print(缺少, split) continue missing 0 with open(split, r) as f: for line in f: img_path line.strip() if not os.path.exists(img_path): missing 1 print(图片不存在:, img_path) label_path img_path.replace(/images/, /labels/).rsplit(., 1)[0] .txt if not os.path.exists(label_path) and os.path.getsize(label_path if os.path.exists(label_path) else __file__) 0: missing 1 print(split, 缺失文件数:, missing)这段脚本把图片路径替换成对应的label路径检查同名txt是否存在。注意允许0字节空txt存在但文件本身必须得有否则训练时Ultralytics会跳过该图像造成数据量和预期不一致。4. YOLO训练教程:从环境配置到第一个epoch跑通4.1 用Anaconda创建YOLO环境并装好ultralytics训练环境直接用Anaconda隔离不要装在base环境里。以Ultralytics的YOLOv8为示例创建一个独立的Python 3.10环境然后安装ultralytics。GPU机器需要先确认CUDA驱动版本再匹配PyTorch;CPU机器可以直接装CPU版。conda create -n yolo python3.10 -y conda activate yolo # CPU 或先跑通流程 pip install ultralytics # GPU 机器建议先按 pytorch.org 给出的命令装 torch再装 ultralytics # 例如 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 yolo versionconda create -n yolo创建独立环境避免把系统Python搞乱;pip install ultralytics会把依赖的torch、torchvision、opencv-python一并拉进来但默认装的是CPU版torch。想用GPU加速必须先装GPU版PyTorch再装ultralytics顺序反过来会被CPU版覆盖。yolo version能确认命令行入口正常看到版本号后继续下一步。4.2 写data.yaml并启动训练:这六个参数决定收敛速度在数据集根目录写一个data.yaml告诉YOLO图片去哪儿找、类别有几类、名字分别是什么。这里的train和val可以直接指向第3章划分脚本生成的txt路径:path: /home/user/ocean-ds # 换成你的数据集绝对路径 train: train.txt val: val.txt test: test.txt nc: 3 names: 0: ship 1: dolphin 2: buoy写好后运行训练命令。第一次跑会自动下载yolov8n.pt预训练权重网络受限时需要手动下载后放到~/.config/Ultralytics/缓存目录里。cd /home/user/ocean-ds yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ batch16 \ imgsz640 \ device0 \ patience20modelyolov8n.pt表示从COCO预训练权重继续训练1000张图规模下用nano版本最稳直接上yolov8m会严重过拟合。epochs100给足训练轮次但实际跑不跑得到100取决于patience这个参数控制连续多少轮验证集指标不提升就提前停止。batch16是显存允许范围内的常见值8GB显存跑imgsz640至少需要8的batch。imgsz640是速度和精度的平衡点海洋小目标如果占比高可以尝试imgsz1280但训练时间会翻倍。4.3 训练日志里看什么:三个loss和mAP曲线的填报依据训练启动后终端会实时打印box_loss、cls_loss、dfl_loss和验证集mAP。box_loss衡量预测框和真实框的回归误差海面目标被截断时这个值容易偏高cls_loss是分类损失类间外观相似时降得慢dfl_loss是分布焦点损失它负责让框的边界更精确小目标尤其依赖这一项。只看最后数值没意义要看趋势:三个loss都下降mAP50缓慢爬升属于正常曲线;box_loss降而cls_loss震荡说明类别样本不均衡。下面这张参数表是影响收敛速度最直接的几个旋钮:参数建议值什么情况要改epochs1003轮过后mAP还在快速涨可以加到200batch16显存不足改8显存够大可以试32imgsz640小目标密集时改1280lr00.01loss震荡时降到0.005patience20数据集小收敛快可以降到10workers8Windows下报DataLoader错误时改0modelyolov8n.pt过拟合明显时换更小的yolov8n.yaml训练结束后权重保存在runs/detect/train/weights/best.pt比赛和项目里最终用的是best.pt而不是最后一次epoch的last.pt。训练过程中如果发现val loss在某个epoch后开始抬头train loss还在降就是过拟合信号此时回退到best.pt即可不需要从头重跑。5. 训练完先别急着部署:用验证集把模型推回重练至少一次5.1 用yolo detect val看best.pt的mAP与混淆矩阵训练完的第一件事不是找测试图而是先把验证集整体跑一遍:yolo detect val \ modelruns/detect/train/weights/best.pt \ datadata.yaml \ batch16输出会打印mAP50和mAP50-95同时生成runs/detect/val/目录里面有confusion_matrix.png和PR_curve.png。混淆矩阵要重点看对角线以外的格子:如果ship被大量误判成buoy说明这两类在验证集里长得太像要么加类别特定数据要么把混淆严重的图片挑出来重新标注。PR_curve是precision和recall在不同置信度下的折线这张图决定了后面部署时的置信度门限怎么选。5.2 调整置信度门限找回海上小目标的漏检海面目标检测最常见的部署问题是漏检尤其逆光条件下船舶颜色与背景接近时模型召回率骤降。Ultralytics推理默认conf0.25这个值在陆地区域可能合适但在海上大概率偏高。yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcetest/ \ conf0.15 \ iou0.45conf0.15把置信度门限从0.25拉低模型会把更多低分候选框输出为预测结果召回率上升但误检也会变多。正确做法是打开PR_curve.png找到precision和recall曲线的拐点把conf设在拐点附近。一般海洋目标检测场景在0.15到0.2之间扫一遍yolo detect val对比不同conf下的mAP和误检数量就能找到合适的值。边缘部署到低算力设备时门限还要再上浮一档因为设备算力导致的前处理损失会间接降低分数。5.3 给数据再做一遍低光增强池化新难例回训练集跑完验证集后把那些漏检最高的图片集中到一起按场景归组用albumentations做低光增强可以显著提升极端海况下的表现。这里推荐两种增强手法:一种是RandomBrightnessContrast把亮度对比度同时压低模拟阴天和黄昏;另一种是CLAHE它在低对比度海面上能把目标纹理拉出来。难例池化的做法是:每次验证结束后把conf在0.2到0.5之间的框对应的图片挑出来人工筛掉误检剩下的重新标注后合并进train集再跑一轮训练。这样每轮训练都只针对上一轮的薄弱环节比盲目扩数据更省时间。跑完这轮先看PR_curve里高置信度区间的recall有没有回升——有回升说明难例池化那步加对了。本文还有配套的精品资源点击获取