ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLO海洋目标检测数据集:从标注格式转换到训练验证全指南

YOLO海洋目标检测数据集:从标注格式转换到训练验证全指南 简介面向海洋目标检测需求的数据集资源包适合正在学习YOLO或需要真实场景标注数据的算法工程师与研究者。压缩包共2000个文件核心是1986个xml标注文件同时包含6个html教程、5个txt划分列表和3个py脚本整体约140.33MB。数据采用LabelImg标注框选质量较高标签同步提供VOC、COCO、YOLO三种格式可直接用于YOLO系列模型训练三个划分脚本可灵活生成训练集、验证集和测试集配套的Linux与Windows环境搭建及训练教程能引导用户从零完成环境配置并基于案例修改后训练自己的数据。整套资源将图片数据、多格式标签、脚本与文档组织成一套可直接上手的目录结构省去单独搜集和格式转换的环节鉴于海洋目标检测在生态监测、渔业管理等场景中的应用这套资料对模型验证与教学演示也有实用价值。目前已有303人学习下载适合作为海洋目标检测项目起步和教学实践的参考资料。1. 海洋目标检测数据集拿到手先确认三样东西再开工拿到“YOLO海洋目标检测数据集(含10000张图片)对应voc、coco和yolo三种格式标签划分脚本训练教程”这样的压缩包我的第一反应不是马上解压跑训练而是先做三件小事看图片分辨率分布、看类别数量、看标签文件覆盖情况。这套材料对入门目标检测很友好因为已经帮你标好框、转好格式、写好划分脚本你只需要把训练流程跑通但越是现成的数据越容易忽视标签里的脏数据而脏数据会在验证阶段毫无意外地翻车。我一般会先展开目录统计 images 下有多少 jpg、多少 png随手抽查几张尾号的图片是否与对应标签同名。确认这三件事再决定用哪套标签格式进训练。否则训练跑到一半报“label shape error”回头找问题才是最花时间的。2. VOC/COCO/YOLO 三种标注格式换算关系与选型理由如果打开压缩包看到三个标注目录第一反应别急着删。VOC 和 COCO 是更通用的数据交换格式YOLO 是训练链最喜欢的快捷格式。多数训练教程直接吃 YOLO但跨框架复现、可视化调试、做实例分割时保留 COCO json 能省很多事。下面先把坐标换算讲清楚再解释为什么 ultralytics 这一套默认读 YOLO txt。2.1 三种格式的坐标表达从 xml、json 到 txt三种格式本质上都在表达“目标在第几类、框在哪”只是写法不一样。表格里列一下格式载体框的写法是否归一化VOCxmlxmin, ymin, xmax, ymax 四个像素值否COCOjsonbbox: [x, y, width, height] 四个像素值否YOLOtxtclass_id center_x center_y width height 五个数是VOC 的 xml 里bndbox给出左上角和右下角坐标。假设图片宽度 W1280、高度 H720一个框从 (100, 80) 到 (180, 200)那么 xmin100, ymin80, xmax180, ymax200。要转成 YOLO 的归一化中心点需要算center_x (100 180) / 2 / 1280 0.109375center_y (80 200) / 2 / 720 0.194444box_w (180 - 100) / 1280 0.0625box_h (200 - 80) / 720 0.166667对应 YOLO txt 里的一行就是0 0.109375 0.194444 0.0625 0.166667。COCO 的 bbox 给的是左上角 x100、y80、宽 80、高 120换算时就是中心点x w/2再除以图片宽公式完全一样。这里有个容易搞混的点VOC 的框坐标是左上角右下角COCO 是左上角加宽高而且 COCO json 里同一个目标的bbox和segmentation可以同时存在。如果你只是用检测模型只读bbox就够如果你想后续跑分割不要丢掉segmentation。还有少数标注工具会输出中心点格式转 YOLO 前先打印两行检查不要直接套公式。2.2 为什么 yolov5/yolov8 这条训练链默认吃 YOLO 格式Ultralytics 系列的训练代码在数据加载阶段会给每张图片找同名 txt然后一行一行解析出类别和坐标。相比每轮训练去解析 xml/json这种纯文本方案更轻而且 label 和 image 分目录放路径规则一眼能看懂。另一个原因是 YOLO 的 txt 天然做了归一化。YOLO 系列损失函数坐标部分经常用 CIoU 等需要的是相对于图片尺寸的坐标而不是绝对像素归一化之后无论输入 640 还是 1280同一个目标用同一份 txt 都能直接训练。这对海洋目标检测很关键因为航拍图和近岸摄像头图分辨率经常不一致标签只要不越界就能适配各种输入尺寸。那为什么还要保留 VOC/COCO常见做法是把它们当作“母版”转换脚本只跑一次把 VOC/COCO 转成 YOLO然后固定使用 YOLO 训练但以后如果有人要复现论文或对接 MMDetection、Detectron2直接从母版重新导出即可。所以我不建议收到数据集后立刻把 xml/json 删了宁可留着占一点磁盘空间。2.3 类别映射COCO 80 类怎么读进 YOLO 的目标检测配置里不少人的训练是从yolov8s.pt这种 COCO 预训练权重开始的这不是问题但要注意类别对齐。预训练模型的检测头是 80 类你的海洋数据集可能是 8 类或 10 类写数据配置文件时nc和names必须和转出来的 txt 完全一致。我一般会在 yaml 里这样写path: /your_dataset_path train: images/train val: images/val test: images/test nc: 8 names: [ship, boat, fisherman, warship, container, sailboat, dock, floating_object]关键在于names的顺序。如果你在转换脚本里把boat放在下标 1那么所有 boat 的标签行必须写成1 cx cy w h。一旦顺序改过没有重新转 txt训练时模型会以为标签 1 是另一种目标结果就是混淆矩阵里全是“错位”。如果一开始用的 VOC xml 里name是字符串建议先固定一个CLASSES列表再写一个字典或列表让 id 稳定。特别是从 COCO json 转 YOLO 时json 里categories的 id 可能不是连续从 0 开始的比如原 id 有 3、7、9转换脚本需要把它们映射成 0、1、2而不是直接把原 id 写进 txt。常见错误是看到category_id是 5 就直接写 5但你的 yaml 只有 8 类训练时就会出现 label 越界。2.4 什么场景必须保留 COCO/VOC不要只留 YOLO虽然训练吃 YOLO但有两个场景需要保留母版。第一你想用同一个数据集跑实例分割或旋转框检测YOLO txt 只有轴对齐框没有多边形和旋转角COCO json 里的 segmentation 如果带多边形还能救回来。第二你想在 MMDetection 或 Detectron2 上复现对比模型这些框架多数以 COCO 为准重新导入反而要再写一遍反向转换。我的习惯是压缩包里保留“annotations_original”目录转换脚本生成“labels_yolo”两套互不覆盖。这样 10000 张图也就多占几十 MB这点成本可以接受但能省很多重新标注的时间。3. 把 VOC 和 COCO 转成 YOLO最小脚本与四个边界坑如果压缩包里已经有 yolo 标签理论上不用转。但你会碰到一种情况想用自己的类别顺序重新训练或者想新增一批图片并用 VOC/COCO 标注这时候必须写转换脚本。标题虽然给了三种格式但训练教程往往只吃 YOLO所以这一步几乎是每次做海洋数据集都要经历的。下面给两个可以直接改着用的脚本再讲四个翻车点。3.1 VOC 批量转 YOLOxml 目录到 txt 目录先看 VOC 转 YOLO 的完整脚本。假设输入是一个放 xml 的目录输出是放 txt 的目录import os import xml.etree.ElementTree as ET CLASSES [ship, boat, fisherman, container] def voc_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) width float(size.find(width).text) height float(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASSES: continue cls_id CLASSES.index(name) 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 / width cy (ymin ymax) / 2.0 / height bw (xmax - xmin) / width bh (ymax - ymin) / height cx min(max(cx, 0), 1) cy min(max(cy, 0), 1) bw min(max(bw, 0), 1) bh min(max(bh, 0), 1) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines) (\n if lines else )) # 使用示例 voc_dir data/annotations/voc txt_dir data/labels os.makedirs(txt_dir, exist_okTrue) for xml_file in os.listdir(voc_dir): if xml_file.endswith(.xml): voc_to_yolo(os.path.join(voc_dir, xml_file), txt_dir)脚本逻辑拆开看先解析size拿到图片宽高再遍历每个object用CLASSES把字符串类别名映射成整数 id然后按公式算中心点和宽高最后 clamp 到 0 到 1 之间。这里 clamp 是为了防止标注框本身超出图片边界但注意如果框在图片外很多clamp 会造成中心和尺寸失真最好还是回到原图确认而不是全部交给 clamp。参数说明如果有的 xml 里size缺失或者写的是 0需要从对应图片读取真实宽高。实际做法是换成from PIL import Image读图或者用cv2.imread拿到 shape。真实项目里我建议优先相信图片文件而不是 xml 里的 size。xml 是标注工具生成的偶尔会把宽高写反只信它容易出玄学错误。3.2 COCO 批量转 YOLO从 json 中提取 bbox 并重映射类别COCO json 相比 xml 更规范但坑也不少。下面是一个最小可用的转换脚本import json from pathlib import Path def coco_to_yolo(ann_file, out_dir): with open(ann_file, r, encodingutf-8) as f: data json.load(f) images {img[id]: img for img in data[images]} # 把原 category_id 映射成连续 id这一步很重要 cat_id_to_new {cat[id]: idx for idx, cat in enumerate(data[categories])} anns_by_image {} for ann in data[annotations]: img_id ann[image_id] anns_by_image.setdefault(img_id, []).append(ann) out_dir Path(out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) for img_id, anns in anns_by_image.items(): img images[img_id] width img[width] height img[height] lines [] for ann in anns: x, y, bw, bh ann[bbox] cx (x bw / 2) / width cy (y bh / 2) / height nw bw / width nh bh / height new_id cat_id_to_new[ann[category_id]] lines.append(f{new_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) txt_name Path(img[file_name]).stem .txt (out_dir / txt_name).write_text(\n.join(lines), encodingutf-8)这段代码比 VOC 版本短是因为 COCO 已经把类别 id 和 bbox 都结构化好了。要注意几件事第一ann[bbox]的 x、y 是左上角坐标不是中心点第二category_id在 COCO json 里是原始 id例如原数据集有 90 类你的海洋数据集只用其中 10 类那么一定要先做过滤再映射否则新 id 会出现空洞第三如果 json 里图片名带子目录file_name可能是images/train/001.jpg用Path(...).stem取的时候别把目录漏掉。过滤类别我一般会加一个keep_cats {ship, boat, ...}条件先把不需要的 annotation 排除然后再 enumeratecategories。顺序不定的话建议人工写下类别清单不要依赖 JSON 里的顺序因为不同标注工具导出的顺序可能不是你想的。3.3 四个边界坑空标签、坐标越界、文件名错位、类别偏移第一个坑是空标签。有些图片没有任何目标VOC xml 里没有objectCOCO json 里没有对应 annotation。转换时要么生成空 txt要么干脆把它们移出训练目录。Ultralytics 对空 txt 能容忍但会打一堆“WARNING: ignoring empty label”日志我习惯保留空 txt因为图片本身还有背景价值减少漏检误检。不过如果你的空标签比例超过了 20%先看看是不是漏标了。第二个坑是坐标越界。标注框的 xmax 可能比图片宽度还大转换后 YOLO 坐标会大于 1。脚本里 clamp 能防住一部分但真正源头是原标注框有问题。不要只在转换时修建议把越界样本的 xml/json 打印出来看一眼是标注框画大了还是图片本来就有黑边。海洋场景里船到画面边缘时边界框常压着边线压边 1-2 像素没问题压 50 像素就要处理。第三个坑是文件名错位。转换脚本基于 xml 文件名找输出 txt 名如果图片叫IMG_001.JPGxml 叫IMG_001.xml大小写不一致Windows 下没问题Linux 下就会找不到对应的图片或标签。先统一成小写或统一用 stem 对比。我一般会写一行检查找所有图片有多少在 labels 目录下找不到配对 txt打印缺失列表。第四个坑是类别 id 偏移。前面提过的CLASSES顺序一旦变化所有 txt 的 class id 都要重转。这个坑最隐蔽的地方在于VOC 格式里的name是字符串转出来的 id 只依赖脚本里的列表COCO 格式里原 category_id 可能是乱序如果直接映射没重排模型训完看到的 names 全是错的。任何一步动了类别清单建议重转一遍不要手动改 txt 的第一列。4. 划分训练/验证/测试集脚本逻辑与场景分组压缩包里的划分脚本通常帮你分好了但我一般会重新理解它的划分逻辑。随机划分是最简单的但海洋目标检测里频繁出现“同一航次的相邻帧”如果不做场景分组验证集指标会虚高。所以这一章先讲基础随机划分再讲分组划分最后给出划分后的统计检查。4.1 随机划分脚本train/val/test 按 8:1:1 生成目录先看一个最常用的随机划分脚本。它假设所有图片在images/下标签在labels/下脚本会随机打乱后把文件分别移动到对应的子目录import random from pathlib import Path random.seed(42) src_img_dir Path(images) src_lbl_dir Path(labels) img_exts [.jpg, .jpeg, .png, .bmp] images [] for ext in img_exts: images.extend(src_img_dir.glob(f*{ext})) images.sort() random.shuffle(images) total len(images) train_end int(total * 0.8) val_end int(total * 0.9) splits { train: images[:train_end], val: images[train_end:val_end], test: images[val_end:], } for split_name, imgs in splits.items(): img_out src_img_dir / split_name lbl_out src_lbl_dir / split_name img_out.mkdir(exist_okTrue) lbl_out.mkdir(exist_okTrue) for img in imgs: target_img img_out / img.name if target_img.exists(): continue img.rename(target_img) lbl_path src_lbl_dir / (img.stem .txt) if lbl_path.exists(): lbl_path.rename(lbl_out / lbl_path.name) else: print(fWARNING: {lbl_path} not found)这段脚本的核心是先用固定随机种子打乱图片顺序再按索引切成三段最后移动文件和对应标签。参数说明random.seed(42)保证每次运行划分结果一致否则重现实验会很难train_end和val_end用整数索引切分比例可以自由调我通常留 10% 做 test保证最后有一个没见过的集合做终评。移动文件的写法有一个隐含风险如果两次运行脚本图片可能已经被移动过再运行就会重复移动出现“找不到源文件”的报错。更稳妥的方案是不移动文件只生成train.txt、val.txt、test.txt三份列表文件Ultralytics 的 yaml 里也能读取 txt 列表路径。不过对新手来说目录结构更直观大家普遍愿意用移动的方式。只要记住“只运行一次或者每次运行前恢复原目录”。另外如果图片和标签原本就在同一个目录但没有 labels 子目录脚本里的src_lbl_dir / split_name要改成src_img_dir / split_name并在移动完图片后自行创建标签子目录。不同数据集打包风格差挺多拿到手先tree看一眼。4.2 更稳的做法按航次/视频片段分组划分海洋数据很多时候是按视频抽帧生成的。同一段视频的前 50 帧和后 50 帧环境几乎一样船的位置只有细微变化。如果随机划分训练集和验证集很可能同时包含同一段视频的相邻帧模型在验证时等于看见过“几乎一样”的图mAP 会很好看但一到新航线、新水域就露馅。解决办法是按片段划分。假定文件名里带拍摄批次信息比如20240315_0810_001.jpg其中前 14 位可以看作一个片段编号。脚本可以是from collections import defaultdict def get_seq_id(filename): # 按你的命名规则取片段号这里取前14位 return filename[:14] seq_groups defaultdict(list) for ext in img_exts: for img in src_img_dir.glob(f*{ext}): seq_groups[get_seq_id(img.name)].append(img) seq_ids list(seq_groups.keys()) random.seed(7) random.shuffle(seq_ids) seq_train seq_ids[:int(len(seq_ids) * 0.8)] seq_val seq_ids[int(len(seq_ids) * 0.8):int(len(seq_ids) * 0.9)] seq_test seq_ids[int(len(seq_ids) * 0.9):]逻辑说明先把同一个片段的所有图片放进一个桶然后按片段 ID 打乱而不是按单张图片打乱。这样验证集里不会出现训练集同一片段的相邻帧。参数get_seq_id要按实际文件名改比如无人机的文件名往往包含架次编号如果数据集没有明显的批次字段可以用采集时间戳的小时级别组合或者干脆按“每连续 100 帧归为同一个片段”处理。这样做的一个代价是样本均衡性变差。有的片段有 500 张有的片段只有 10 张按片段切分后某个类别可能全部落进测试集。所以切完必须做类别统计如果测试集的“漂浮物”类别极少宁可手动把几个该类别集中的片段换进测试集。4.3 划分后必须做的三项统计第一项是图片数和标签数。命令行三行就能看for split in train val test; do echo $split images: $(ls images/$split | wc -l) echo $split labels: $(ls labels/$split | wc -l) done第二项是类别分布。用一段小脚本统计每个 split 中每类框的数量from collections import Counter import os for split in [train, val, test]: counter Counter() label_dir flabels/{split} for name in os.listdir(label_dir): with open(os.path.join(label_dir, name)) as f: for line in f.read().splitlines(): if line.strip(): counter[int(line.split()[0])] 1 print(split, counter)第三项是检查标签缺失。图片数多于标签数说明有的图没有 label。如果这些图恰好是空场景没问题如果它们其实有船那就是漏标注要去掉或补标。我一般还会统计一下图片尺寸分布因为后面定imgsz时要用如果大部分图是 4K 的用 640 也不是不行但小目标会很难受。from PIL import Image import os widths [] for name in os.listdir(images/train)[:500]: with Image.open(os.path.join(images/train, name)) as im: widths.append(im.size) print(sorted(set(widths), keylambda x: x[0]*x[1])[:10])这步检查看起来琐碎但能避免你把验证集指标当成“能上线”的唯一信号。后面训练时如果发现小目标一直漏检回来翻尺寸分布比调参数更有效。5. 训练阶段常见问题与排查从环境配置到指标异常数据集、转换脚本、划分脚本都齐了接下来就是训练。Ultralytics 这条路线对新手很友好一条命令能跑完但实际训练中你会碰到环境、数据、显存等一堆问题。这一章按时间顺序把最常见的问题列出每个都按现象、原因、解决来写。5.1 ultralytics 环境配置与最小训练命令先从零搭环境。常见做法是用 conda 建一个独立环境然后安装 ultralyticsconda create -n yolo python3.10 -y conda activate yolo pip install ultralytics很多新手的第一个坑是装了 CPU 版 PyTorch训练速度慢到怀疑人生。如果机器有 NVIDIA GPU建议先确认驱动然后安装带 CUDA 的 PyTorch。Ultralytics 在 pip 装上后会自动带 torch但它可能不会帮你选 GPU 版本。用python -c import torch; print(torch.cuda.is_available())检查输出True才说明 GPU 可用。环境没问题后写一个数据配置文件path: /home/me/data/ocean train: images/train val: images/val test: images/test nc: 8 names: [ship, boat, fisherman, warship, container, sailboat, dock, floating_object]然后跑训练yolo detect train dataocean.yaml modelyolov8s.pt epochs100 imgsz640 batch16 patience15 device0这个命令的参数需要说清楚data指向 yamlmodel可以给一个预训练权重路径也可以给 YAML 结构文件如yolov8s.yaml表示从零训练epochs是训练轮数10000 张图的情况 100 轮足够batch取决于显存16 比较稳不够就降到 8imgsz是训练输入尺寸海洋航拍里小目标多如果显存允许可以提到 960patience是早停耐心值指标连续 15 个 epoch 不涨就停。5.2 数据加载阶段的三类常见报错第一类No labels found in /path/to/labels/train。现象是训练一开始就退出。原因通常是 yaml 里train路径写的是images/train而 Ultralytics 会自动到这个路径的上级目录找 labels或者标签目录和图片目录没对齐。解决方法是把 labels 放在图片目录的平行目录并确保 yaml 里只写images/train框架会自动找labels/train。如果标签目录名字不同需要改目录名或写绝对路径。第二类Invalid label或Label shape error。现象是跑到某个 epoch 就中断。原因多半是 txt 里类别 id 大于等于nc或者坐标里有 nan。用前文的统计脚本检查一下打印出所有标签行的最大第一列值必须小于nc。如果发现问题回到转换脚本检查类别映射是否漏了。第三类是 OOM。现象是 CUDA out of memory。原因是 batch 或 imgsz 太大。先降 batch 到 8再不行降 imgsz也可以打开 AMP。不需要换模型8G 显存用yolov8n加 imgsz640 也能跑起来只是精度差一点。5.3 训练指标异常的排查思路第一种情况训练集 mAP 很高验证集 mAP 低。这是过拟合信号。验证集与训练集分布差异大可能是划分随机性不够也可能是模型容量太大。优先检查验证集里是否有某些类别的样本特别少如果类别均衡但依然过拟合把 mosaic 增强概率调低或增加数据增强以外的正则手段。第二种情况mAP0.5 不低但 mAP0.5:0.95 很低。海洋目标常常是小目标0.5:0.95 要求框和真值交叠更严格小目标天然劣势。可以考虑把输入分辨率从 640 提到 1280或者换用带更大特征图的检测头。这个数据集如果大多数目标面积小于 32x32单靠调参很难翻天最好做切片推理。第三种情况某个类别 AP 一直是 0。原因通常是该类别在训练集里样本太少或者标签从未被正确转换。先Counter看类别计数如果该类样本不到几十个按经验基本训不好。可以在 yaml 里给该类提高权重也可以增加该类图片。有时候原因是该类别 id 错位比如船全被标成 boatship 类一个正样本都没有这就要检查转换脚本的类别顺序。第四种情况早停触发太早训练刚 10 个 epoch 就停了。原因是学习率策略在预热阶段没给足或者数据量太大模型还没收敛。可以调大patience到 30或者调低学习率。观察results.csv里的val/box_loss如果还在稳定下降就不要让早停打断它。6. 训练完别急着上线三个针对海洋目标的验证技巧训练输出里的mAP0.5只是开始真正判断模型能不能用要看混淆矩阵、PR 曲线和小目标指标。我把自己的三个习惯放在这里第一个是每次训练完一定导出confusion_matrix.png看渔船和商船是不是互相混第二个是单独统计小目标 AP第三个是导出 ONNX 后用 TensorRT 做一次速度与阈值验证。T4 上用 640 分辨率大概跑 25 帧每秒左右能支持多少路取决于你的业务容忍度这类吞吐验证在上线前必须做。6.1 用混淆矩阵和 PR 曲线找漏检训练结束后执行yolo detect val dataocean.yaml modelruns/detect/train/weights/best.pt把 val 预测图和 label 可视化一起看。混淆矩阵里如果“boat”误判成“floating_object”说明这两类的外观太像需要补负样本如果某一行几乎全被分到背景就是漏检要降低 confidence 阈值再看一次 PR 曲线。6.2 用小目标指标和实际帧率做上线判断统计验证集里真值框面积小于 32x32 的比例再看模型在这些目标上的 recall。海洋场景里这类目标占比经常过半只看整体 mAP 会高估。上线时 confidence 阈值不要直接用默认 0.25我一般先在验证集上扫 0.2 到 0.5挑误报和漏检平衡点。早期我的教训是只信验证集 mAP结果在真实视频里几十米外的小艇全漏了后来把切片推理加进去漏检少了一半。这个方向值不值得投入答案也藏在这些验证指标里。希望帮到你。本文还有配套的精品资源点击获取
返回列表