水域实例分割数据集处理全流程:从解压校验到YOLOv8训练

发布时间:2026/9/28 1:44:24
水域实例分割数据集处理全流程:从解压校验到YOLOv8训练
简介一套聚焦水域场景的实例分割数据集覆盖训练集1340个、验证集333个共1673个标注样本包含河流、湖泊、海洋、湿地、池塘及其他水域六大类别适合计算机视觉开发者、遥感影像分析人员和GIS研究者使用。压缩包共2000个文件大小103.31MB以1673个YOLO格式的txt标注文件为核心配套jpg图像、yaml配置和docx说明文档标注采用多边形掩码形式可直接接入YOLO系列模型进行训练。图片采集自卫星影像、航拍和地面拍摄涵盖多种自然与人工水域形态可用于水域监测、富营养化预警、水体提取和洪水风险识别等应用为水资源管理与智能环保提供数据支撑。目前已有179人学习下载数据划分明确、标注完整统一能显著减少AI项目的数据采集与标注成本适合科研实验与小型工程项目直接使用。1. 拿到「水域实例分割数据集.zip」第一件事不是解压做水域目标识别的项目时「水域实例分割数据集.zip」这类压缩包几乎是绕不开的起点里面一般是遥感影像或岸基相机拍到的水面图配上 COCO 或 YOLO 格式的实例分割标注用来训练船、浮标、养殖网箱、漂浮物这类目标的检测与分割模型。它要回答的问题从「这片区域是不是水」升级成「水面上有哪些独立目标、各自占哪些像素」。适合谁准备做 yolo 实例分割、拿 YOLOv8 训练自己水域模型的工程师以及需要把这份数据集转成自有训练格式的算法同学。但如果你拿到手就双击解压丢进训练脚本大概率会先在前处理和格式转换上耗掉整个白天。下面按解压前该检查什么开始把转换、训练、踩坑到验证的完整路径讲一遍。2. 拆包之前水域实例分割到底在分割什么2.1 实例分割与语义分割的边界水域任务为什么非实例不可实例分割和语义分割的区别很多新手一开始会混。语义分割做的是像素级分类每一类像素给一个标签实例分割除了分类还要把同类目标区分成不同的个体。一句话概括语义分割只回答「像素是什么类别」实例分割还要回答「这个类别里哪个像素属于哪个物体」。放在水域场景里区别立刻变得具体。如果你想分割出整片水面判断哪里有水、哪里是岸语义分割足够。但你要数清楚港口里停了几条船、跟踪一条船的运动轨迹、评估一片藻华覆盖了几块分离的区域语义分割把同类像素全部标成同一个标签就没法用了。yolo 的 segment 系列从 YOLOv8、YOLO11 一路到社区讨论度很高的新版本处理的就是这种带实例编号的分割输出一个目标对应一个 mask、一个包围框、一个类别。输出结构决定了它天然适合「数目标」和「量目标」的任务这正是水域管理、渔业监管、环保巡查这类场景的核心诉求。还有一层容易忽略全景分割是实例分割加语义分割的并集把背景区域也纳入建模。但绝大多数水域数据集只做实例分割因为水面、岸线这类背景对项目本身价值不大真正要的是船、网箱、浮标这些具体目标。做格式转换时务必先确认标注是实例级而不是语义级。如果拿到的标注把同类目标合成了一个大 mask后续训练的模型对重叠、靠近的目标完全没有区分能力这类问题在转换阶段发现成本最低。2.2 水面上的「实例」有哪些形态从船只到漂浮物的目标分布这类数据集里最常见的类别大体可以归成几类目标形态直接决定你后面的训练策略。第一类是船只。船在水域实例分割里是「正样本担当」轮廓清晰、类别明确不管是渔船、货船还是快艇标注边界都比较好勾。第二类是养殖网箱或浮标这类目标小、排列密在遥感影像上往往只有几十个像素是典型的小目标难点。第三类是漂浮物、藻华或垃圾带边界模糊跟水面的灰度差可能只有几个像素标注员自己都容易画歪。目标形态直接影响使用方式。船这类大目标整图训练没问题网箱、浮标这类小目标就得考虑切图或提高输入分辨率藻华这类半透明目标mask 边缘天然不可靠训练时要谨慎使用 mosaic 增强——mosaic 把四张图拼在一起藻华区域容易被拼缝截断产生假的轮廓边缘。标注质量也要留意小目标如果只画了个大概方框而不是精确轮廓模型学出来的 mask 就是糊的这是数据集本身的质量问题调参救不回来。拿到 zip 之后先解压看一遍类别清单别急着训。类别数量和类别顺序会影响后面所有步骤因为转 YOLO 文本格式时类别索引一旦错位整个训练集都在学错误的标签训练完才发现就晚了。2.3 数据集内部的两种常见组织COCO JSON 与 YOLO 文本水域实例分割数据集的 zip 里常见两种组织方式我建议拿到手先识别是哪一种再决定要不要写转换脚本。一种是 COCO 风格参照 COCO2017 那套标准结构一个 JSON 文件统一管理 images、annotations、categories 三张表。images 记录每张图的宽高、文件名、idannotations 记录每个目标的 image_id、category_id、bbox、segmentationsegmentation 字段可能是多边形坐标列表也可能是 RLE 压缩编码。另一种是 YOLO 风格每张图对应一个同名 txt 文件每行是一条标注开头是类别索引后面是一串归一化的多边形坐标比如0 0.5 0.6 0.51 0.55 ...。两种组织的差别决定了处理路径对比项COCO JSON 组织YOLO 文本组织标注载体单一 JSONannotations 数组每张图一个 txt坐标体系像素绝对坐标归一化相对坐标类别 id任意正整数可跳号0 起连续索引mask 形式polygon 或 RLE 二选一统一 polygon主要缺点转换成本高、依赖解析库无元数据难做全局校验COCO 结构的优势是信息全、方便校验坏处是训练框架不直接吃YOLO 结构则是省事但缺少中央元数据类别分布、总目标数都要自己扫。实际情况下这份 zip 里两种都可能存在最常见的是 COCO JSON 配原图因为学术数据集发布方普遍按这个来。无论哪种组织解压后第一件事都是核对关联关系图片 id、annotation 里的 image_id、文件名三者要能对上。这个校验不做的后果通常要等训练跑到一半、loss 异常波动或 mAP 极低时才暴露排查成本远高于一开始花十分钟做匹配检查。3. 把 zip 变成 YOLOv8 能吃的格式解压校验与转换脚本3.1 解压前三查完整性问题、目录结构、文件名编码拿到 zip 先别双击我一般先在命令行做三个检查每项都有明确目的。第一步看内容清单确认解压后不会把一堆散文件直接怼到当前目录unzip -l 水域实例分割数据集.zip | head -50-l只列内容不解压能同时看到文件数和目录层级。良性的数据集 zip顶层应该有一个根目录里面是 images/ 和 labels/ 或 annotations/ 这种标准布局如果看到几百张 jpg 散落在顶层解压时就要先建好目录再接出来否则后面路径全乱。第二步测试压缩包完整性。传输过程导致 zip 截断是常见事故尤其从网盘或邮箱中转过的文件unzip -t 水域实例分割数据集.zip-t会逐个条目录校验 crc32。如果报出bad CRC或unexpected end of file别犹豫重新获取完整文件不要试图用解压软件「修复」——缺了标注文件的 zip 修出来也是废的。如果这是唯一来源就直接放弃这个包zip 不像部分压缩格式带恢复记录普通 zip 坏了基本没救。第三步处理文件名编码。Windows 下压缩的中文文件名默认是 GBKLinux 或 macOS 上解压会变成乱码最常见的表现是文件名变成一串下划线或问号unzip -O UTF-8 水域实例分割数据集.zip -d ./water_seg如果当前 unzip 版本不支持-OmacOS 自带的版本就没有可以改用 Python 的 zipfile 配纯 Python 解压或者换带编码转换的解压工具。这一步不是强迫症文件名乱码会让后续图片与标注的关联检查直接失败因为脚本按文件名匹配时全部对不上。3.2 COCO 转 YOLO 实例分割格式一份能改改就用的脚本解压并确认目录结构后最常见的工作是把 COCO JSON 转成 YOLO 文本标注。训练框架读 YOLO 标签几乎零成本而且很多数据增强的实现也围绕 YOLO 文本展开。下面这份脚本核心思路是遍历 COCO 的 annotations把 segmentation 统一归一化后写入同名 txtimport json import os import numpy as np import cv2 from pathlib import Path from pycocotools import mask as coco_mask def polygon_to_line(poly, w, h, cls_id, simplifyTrue): pts np.array(poly, dtypenp.float64).reshape(-1, 2) pts[:, 0] / w pts[:, 1] / h # COCO 多边形首尾点重复YOLO 不要求闭合点去掉冗余 if len(pts) 3 and np.allclose(pts[0], pts[-1]): pts pts[:-1] if simplify: # Douglas-Peucker 压掉共线点减少边框点数 pts cv2.approxPolyDP(pts, epsilon0.002, closedTrue).reshape(-1, 2) flat [f{x:.6f} for p in pts for x in p] return f{cls_id} .join(flat) \n def rle_to_lines(rle, w, h, cls_id): mask_arr coco_mask.decode(rle) # RLE 先解码成 0/1 掩码 contours, _ cv2.findContours( mask_arr.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) lines [] for cnt in contours: cnt cnt.reshape(-1, 2).astype(np.float64) if len(cnt) 3: continue cnt[:, 0] / w cnt[:, 1] / h flat [f{x:.6f} for p in cnt for x in p] lines.append(f{cls_id} .join(flat) \n) return lines def convert(coco_path, out_dir): with open(coco_path, r, encodingutf-8) as f: coco json.load(f) img_map {im[id]: im for im in coco[images]} # 类别 id 可能不连续统一映射到 0..N-1保证和训练配置一致 cat_map {cat[id]: i for i, cat in enumerate(coco[categories])} os.makedirs(out_dir, exist_okTrue) for ann in coco[annotations]: img img_map[ann[image_id]] w, h img[width], img[height] cls cat_map[ann[category_id]] txt_path Path(out_dir) / (Path(img[file_name]).stem .txt) seg ann[segmentation] with open(txt_path, a, encodingutf-8) as f: if isinstance(seg, list): # 多边形标注 for poly in seg: f.write(polygon_to_line(poly, w, h, cls)) elif isinstance(seg, dict): # RLE 标注解码后提轮廓 for line in rle_to_lines(seg, w, h, cls): f.write(line) print(f转换完成类别映射: {cat_map}) if __name__ __main__: convert(annotations/instances_train.json, labels/train)脚本逻辑分三段说明。第一段 poly 归一化COCO 的 polygon 是未归一化的像素坐标YOLO 要求归一化到 [0,1]同时 COCO 多边形首尾点相同YOLO 不需要这个冗余闭合点保留会让点数虚高、训练时轮廓点参与 loss 计算增加开销。第二段 RLE 处理部分数据集对 crowd 类或复杂掩码用 RLE 存储必须先经 pycocotools 解码成二值图再用 cv2 提取外轮廓不能直接当坐标写。第三段类别映射COCO 的 category_id 往往从 1 开始还可能跳号YOLO 标签必须是 0 起的连续索引所以一定要建立 cat_map否则类别错位是静默发生的训练时看不出明显报错mAP 却始终上不去。approxPolyDP的 epsilon 参数控制压点阈值。水面目标边缘噪声大epsilon 设 0.002归一化坐标基本能压掉 30% 到 50% 的点而不破坏轮廓。如果目标是网箱这类要精测面积的建议把 simplify 设为 False保留原始点数面积精度优先。提示如果 zip 里已经带了 YOLO 格式的 labels 目录可以跳过转换直接做 3.3 的自查。格式转换不是必选项校验才是。3.3 转换后的自查命令类别分布、掩码面积与坏样本转换脚本跑完不等于转换正确。我每次都会做三个自查任何一个异常就回查转换逻辑。第一个是类别分布统计。YOLO 标签没有中央元数据只能扫 txt# 统计每个类别出现的目标个数awk 按第一列类别索引分组 awk {print $1} labels/train/*.txt | sort | uniq -c这个结果应该和 COCO JSON 里 annotations 按 category_id 的统计对得上。对不上说明有标注被丢弃常见于多边形点少于 3 个被跳过或者类别映射写错。第二个是坏样本扫描。有些标注坐标越界YOLO 训练框架一般会报错或裁剪但先扫一遍能定位是哪张图import glob for txt in glob.glob(labels/train/*.txt): for line in open(txt): nums list(map(float, line.strip().split()[1:])) if any(x 0 or x 1 for x in nums): print(f{txt} 越界: {line.strip()})第三个是掩码面积对比。转换过程最容易悄悄改变面积因为 COCO 的 RLE 和多边形表示天然有误差。写个小脚本统计每个 txt 的多边形面积用鞋带公式对比原 JSON 里的 bbox 面积如果超出合理范围检查是不是归一化写错、坐标除反了宽高。面积意义重大实例分割里 mask 面积直接参与损失计算面积系统性偏小会让模型学出「瘦一圈」的轮廓。三个自查走完转换这一步才算收尾。很多人跳过这步直接训练最后 AP 上不去又回头查数据来回浪费的时间远超这几分钟。4. 喂给 YOLO 之前数据集划分、配置与水域适配的训练参数4.1 数据集划分按场景不按单张图避免同源泄漏把这份数据集用于 YOLOv8 训练时第一个常被忽略的问题是划分方式。很多数据集 zip 里的图是按视频帧导出的同一个场景的连续帧彼此高度相似。如果 train/val 只是随机按文件名切极可能同一场景的不同帧同时出现在训练集和验证集里验证指标虚高一到真实场景就崩。我一般的做法是先按场景分组。文件名里通常有场景前缀比如site1_001.jpg、site1_002.jpg这种按前缀划分如果没有前缀就按拍摄时间聚类或按序列窗口分组。划分以「场景组」为单位from pathlib import Path import random random.seed(0) files sorted(Path(images).glob(*.jpg)) groups {} for f in files: scene f.name.split(_)[0] # 按前缀分场景site1_001.jpg - site1 groups.setdefault(scene, []).append(f.name) items list(groups.items()) random.shuffle(items) split int(len(items) * 0.8) train [n for _, names in items[:split] for n in names] val [n for _, names in items[split:] for n in names] print(ftrain {len(train)} 张 / val {len(val)} 张覆盖 {len(items)} 个场景)划分比例上水域任务我通常用 80/20但要求 val 里每个类别至少出现一次。如果 val 里缺了网箱这一类模型对这类目标的 AP 在验证阶段就是虚的等于没验证。4.2 dataset.yaml 与训练命令几个必须适配水域的参数YOLO 的训练配置集中在 dataset.yaml 里# water_seg.yaml path: ./water_seg train: images/train val: images/val # 类别名要与 COCO categories 一一对应顺序就是标签索引 names: 0: ship 1: fish_cage 2: algae 3: floating_debrisnames 的顺序必须和 3.2 节转换脚本输出的 cls_id 严格一致这是整个流程里最容易出错又最隐蔽的环节。类别名可以按数据集实际字段改顺序不能动。训练命令里值得调整的参数我整理成一张对照表参数默认值水域任务建议原因imgsz6401280水面小目标占像素比例低分辨率影响远大于 batch sizeclose_mosaic010最后 10 个 epoch 关 mosaic消除拼缝假轮廓mosaic1.00.5小目标被拼图边界截断的风险大降半用scale0.50.3尺度增强太猛会把网箱这类目标缩没hsv_s0.70.5水面色调单一过度调色制造假纹理对应的训练命令yolo segment train \ modelyolov8n-seg.pt \ datawater_seg.yaml \ epochs200 imgsz1280 batch8 \ mosaic0.5 close_mosaic10 \ scale0.5 hsv_h0.015 hsv_s0.5 hsv_v0.3如果换用 YOLO11 或更新的 segment 模型这套参数格式基本通用数据集处理和 yaml 不用动换权重文件就行。显存不够时优先减 batch 而不是降 imgszimgsz 对水域小目标的影响是决定性的。4.3 水域小目标的三个处理手段切图、升分辨率、关 mosaic水域实例分割最典型的问题是目标太小。网箱在 1080p 图像里往往只有 30×30 像素标注的多边形点也只有三四个模型很难学到有效特征。第一招是切图。把原图切成 1024×1024 或 640×640 的块再按块训练。切图要注意给每个块重新生成标签多边形要跟着平移裁剪、切掉的点要去掉不能只切图片不切标注。推理阶段也要做同样尺度的切片社区里常见的 SAHI 方案就是训练推理尺度一致的切片方案比只在训练时切图稳定。第二招是提高 imgsz。直接把输入分辨率提到 1280 或 1536小目标占的像素比例变大模型更容易学到边缘细节。代价是显存和训练时间翻倍但水域数据集通常只有几千张图这两项开销在可接受范围。第三招是降低或关闭 mosaic。mosaic 对小目标有一个隐性伤害目标被拼图边界裁掉一半模型学到的是「半截船的 mask」。如果类别以网箱、浮标这类小目标为主把 mosaic 关到 0.3 以下配合温和的尺度增强。这三招不是互斥的实际项目里我经常组合切图加 imgsz 1280 加 close_mosaic。但规模小的数据集不要同时全部上先跑一版 baseline看验证集里小目标类别的 AP再决定加哪一招否则参数叠加后说不清是谁起的作用。5. 水域实例分割数据集的避坑指南5 条上过当的坑5.1 图片全黑16 位 PNG 与 8 位标注的错位现象解压后用看图软件打开图片全是黑的但文件大小正常标注也有内容。原因一些遥感来源的水域数据集采用 16 位 PNG 存储亮度范围在 0–65535标准看图器和 OpenCV 默认按 8 位读动态范围被压到 0–255 里偏暗的一侧肉眼看就是纯黑。图片和标注本身没坏只是位深问题。解决先用 PIL 确认模式再转 8 位归一化python -c from PIL import Image; imImage.open(site1_001.png); print(im.mode, im.size)如果模式是I;16训练前统一用 OpenCV 的 normalize 转 8 位按 2% 到 98% 分位拉伸对比度并把结果另存到新目录不要覆盖原图。覆盖原图会让后续所有脚本的路径假设乱掉。注意还有一类水域图是带 alpha 通道的 RGBA 的 PNGcv2.imread 默认丢弃透明通道如果原图有半透明的水面标注会出问题读取时用cv2.IMREAD_UNCHANGED。5.2 zip 伪加密导致标注解不开现象解压「水域实例分割数据集.zip」时提示文件已加密、要求输入密码但发布方从未提供密码试任何密码都报错。原因zip 的本地文件头里有一个 general purpose bit flag最低位是加密标志。有些数据集在打包时误置了这个标志文件本身并没有真正加密这就是常说的「zip 伪加密」——数据流是明文的只是标志位骗过了解压工具。解决先试空密码伪加密的包经常空密码能直接解开unzip -P 水域实例分割数据集.zip不行就用脚本清除加密标志再解压。脚本只改标志位不动数据区import struct def strip_fake_encrypt(src, dst): data bytearray(open(src, rb).read()) idx, cnt 0, 0 while idx len(data) - 4: if data[idx:idx4] bPK\x03\x04: # 本地文件头 flag struct.unpack_from(H, data, idx 6)[0] if flag 0x0001: struct.pack_into(H, data, idx 6, flag ~0x0001) cnt 1 name_len struct.unpack_from(H, data, idx 26)[0] extra_len struct.unpack_from(H, data, idx 28)[0] idx 30 name_len extra_len elif data[idx:idx4] bPK\x01\x02: # 中央目录头同样处理 flag struct.unpack_from(H, data, idx 8)[0] if flag 0x0001: struct.pack_into(H, data, idx 8, flag ~0x0001) cnt 1 name_len struct.unpack_from(H, data, idx 42)[0] extra_len struct.unpack_from(H, data, idx 44)[0] comment_len struct.unpack_from(H, data, idx 46)[0] idx 46 name_len extra_len comment_len else: idx 1 open(dst, wb).write(data) print(fcleared {cnt} entries) strip_fake_encrypt(水域实例分割数据集.zip, 水域实例分割数据集_fixed.zip)处理完再用unzip -t验证一次能完整列出内容说明已经是明文状态。这个脚本对真正加密的 zip 无效那种情况只能找正确的密码渠道。顺便提醒zip 密码找回没有通用解法真加密靠暴力破解完全不现实别信来路不明的「移除密码」工具那类工具本身经常是恶意程序载体。5.3 转换后掩码偏移一个像素COCO 闭包规则与 resize 插值现象把 COCO 转成 YOLO 后把标签画回原图检查发现 mask 边缘整体向右下偏移 1 像素或者边缘出现锯齿。原因两个常见来源。一是 COCO 多边形是未闭合的转换时没有正确处理闭包轮廓会少一个点画出来就差一截二是某些处理流程先把图 resize 了却没有同步处理多边形坐标或者 mask 用双线性插值做缩放0/1 的边界被插值糊掉。像素级偏移在检测任务里无所谓但在实例分割里 mask 边界直接决定面积精度水面目标面积统计错 1 像素按平方公里算就差出可观的误差。解决转换脚本里统一坐标空间。所有几何变换resize、crop、padding都对标注坐标用同一套矩阵mask 相关的图像操作统一用最近邻插值cv2.INTER_NEAREST避免双线性把 0/1 掩码变成 0.3 的模糊值。转换完成后抽 10 到 20 张图做叠加可视化人眼先过一遍再进训练。这一步是性价比最高的检查视觉上 1 像素的偏移一眼就能看出来。5.4 小目标水域几乎不学习尺度增强与标注的矛盾现象训练 100 个 epoch船的 AP 到 0.85网箱的 AP 只有 0.05而且一直上不去。原因网箱这类目标在原图上占比太小默认尺度增强 scale0.5 会随机把图缩小到 50%小目标直接缩到几个像素梯度被大目标淹没。同时数据集里小目标标注本身可能很粗糙只画了个大概轮廓模型学到的 mask 自然也是糊的。解决把 scale 降到 0.2–0.3或者直接关闭尺度增强只留轻微平移用 4.3 节的切图方案把小目标放大后再训练。检查标注里面积小于 32×32 像素的目标占比如果这类目标多且标注精度差与其调参不如先做一轮标注复核。另一个实用技巧是调高 mask 分支的损失权重ultralytics 配置里的 mask_loss_weight 从默认 1.0 提到 2.0 以上对小目标 mask 收敛有明显帮助。但权重调高会加剧大目标对小目标的淹没配合切图用才安全。5.5 改文件名导致标注关联断裂现象训练时报一堆image not found或 loss 异常波动检查发现有的 txt 内容对不上图。原因数据集 zip 里的文件名可能含中文、空格或特殊字符有人习惯先批量改名成数字编号结果图片改名后YOLO 的 txt 标签按原图 basename 关联改名不同步关联就断了。COCO 转 YOLO 的流程里这种问题尤其隐蔽因为图片改了名但 JSON 里的 image_id 仍指向旧文件名。解决拿到 zip 后先建立「文件映射表」记录原文件名与 id 的对应关系任何改名操作都通过映射表同步更新。更好的做法是不改名保留 zip 内部原始目录结构只在训练配置里指定 path。如果一定要规范化文件名就用脚本统一改图片和对应 txt并跑一次配对校验# 检查每个图片是否有对应标签缺失即报告 for img in images/train/*.jpg; do stem$(basename $img .jpg) [ -f labels/train/$stem.txt ] || echo missing label: $img done这个检查十秒钟跑完却能避免训练跑几个小时后才发现一半标签没加载的惨剧。6. 验证产出把标签画回原图做一眼检查再用 IoU 抽样复核6.1 可视化核查最便宜的后悔药格式转换和训练都完成之后我最后一关永远是人工可视化。把 YOLO 标签画回原图叠加显示人眼扫一遍import cv2 import numpy as np from pathlib import Path def draw_labels(img_path, label_path, out_path): img cv2.imread(str(img_path)) h, w img.shape[:2] for line in Path(label_path).read_text().strip().splitlines(): parts line.strip().split() cls parts[0] pts np.array(parts[1:], dtypenp.float32).reshape(-1, 2) pts[:, 0] * w pts[:, 1] * h cv2.polylines(img, [pts.astype(np.int32)], True, (0, 255, 0), 2) cv2.putText(img, cls, tuple(pts[0].astype(int)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2) cv2.imwrite(out_path, img) # 从 train 和 val 各抽 10 张检查 for img_path in sorted(Path(images/train).glob(*.jpg))[:10]: label Path(labels/train) / (img_path.stem .txt) if label.exists(): draw_labels(img_path, label, fcheck_{img_path.stem}.jpg)这段代码让「接口正确性」变得肉眼可见mask 是否贴合目标边界、类别是不是张冠李戴、多边形有没有自相交或凹陷异常。任何转换脚本都可以声称自己正确但画出来骗不了人。6.2 IoU 抽检把转换误差量化出来可视化解决「对不上」但解决不了「差一点」。要量化转换误差拿原始 COCO JSON 和生成的 YOLO 标签做 IoU 抽检import json import numpy as np from pathlib import Path from pycocotools import mask as coco_mask import cv2 def yolo_txt_to_mask(txt_path, w, h): mask np.zeros((h, w), dtypenp.uint8) for line in Path(txt_path).read_text().strip().splitlines(): pts np.array(line.split()[1:], dtypenp.float32).reshape(-1, 2) pts[:, 0] * w pts[:, 1] * h cv2.fillPoly(mask, [pts.astype(np.int32)], 1) return mask with open(annotations/instances_train.json) as f: coco json.load(f) img_map {im[id]: im for im in coco[images]} ious [] for ann in coco[annotations][::20]: # 每 20 个抽 1 个 img img_map[ann[image_id]] seg ann[segmentation] gt_mask coco_mask.decode( coco_mask.frPyObjects([seg], img[height], img[width]) ) if isinstance(seg, list) else coco_mask.decode(seg) txt_path Path(labels/train) / (Path(img[file_name]).stem .txt) if not txt_path.exists(): continue pred_mask yolo_txt_to_mask(txt_path, img[width], img[height]) inter np.logical_and(gt_mask, pred_mask).sum() union np.logical_or(gt_mask, pred_mask).sum() ious.append(inter / max(union, 1)) if ious[-1] 0.95: print(f{img[file_name]} 目标 {ann[id]} IoU{ious[-1]:.3f}) print(f抽检 {len(ious)} 个目标平均 IoU {np.mean(ious):.4f})逻辑很直接把同一份标注分别从 JSON 和 txt 渲染成二值掩码算交并比。平均 IoU 低于 0.95说明转换过程丢了点或走了样回头查 epsilon 和闭包处理抽检全在 0.97 以上转换这一步基本可以放心。我自己每次处理这类实例分割数据集固定流程都是「解压三查 → 格式转换 → 三项自查 → 训练 → 可视化抽检」中间任何一步不过就返回上一步修绝不带着疑问往下走。这个习惯是从一次掩码偏移 1 像素导致面积精度全线崩盘的教训里逼出来的——那次之后我彻底明白实例分割项目里八成的问题出在数据链路而不是模型上。数据链路通了模型参数怎么调都顺数据链路有暗伤再好的模型结构也白搭。希望帮到你。本文还有配套的精品资源点击获取