简介面向实例分割数据集制作场景的图像标注小工具参考VIA与labelme的交互思路采用C#开发并附完整工程源码。它支持矩形框与多边形轮廓标注适合Mask R-CNN等算法训练前的样本标定也便于需要二次开发标注流程的算法工程师与研究生使用。整包约4.07MB共65个文件以C#源码、DLL依赖、可执行程序、图标与配置为主另有解决方案文件、SQLite数据库及docx使用说明兼顾直接运行和编译改造。目前已有2340人学习下载。读者可获得可运行的标注程序与完整源码理解标注数据落库、图形绘制与对象管理等模块组织并据说明文档快速上手对需要定制快捷键、导出格式或标注类别体系的人也能在现有工程上继续扩展。1. 一份能直接喂给 Mask R-CNN 的标注样本卡住的从来不是点几下去年帮某实验室收拾过一个图像标注的烂尾工程两千张图三个人标了半个月导出 JSON 之后转 COCO 报了十七个错训练跑起来 mask 全是噪点。回头查才发现问题不在标注的人手慢而在工具选错了、格式没对齐、转换脚本没做边界处理。图像标注这件事真正的成本从来不在鼠标点得多快而在样本制作这条链路上每一环的格式约定。这份工具包围绕 mask_rcnn 的实例分割需求把 VIA 和 labelme 两条常见路线串起来什么时候用 VIA 做纯框选、什么时候必须换 labelme 描多边形、导出的 JSON 怎么变成 COCO 实例标注和二值掩码、哪些参数会让 mAP 莫名其妙掉一截。适合已经跑通过检测模型、准备往实例分割走的人也适合手上有一批标注数据但不敢直接拿去训的团队。2. 标注工具选型VIA 与 labelme 在 Mask R-CNN 流程里的分工2.1 Mask R-CNN 到底吃什么格式先把这个说透后面所有选型和脚本都围绕它展开。Mask R-CNN 在训练阶段对每条样本的诉求是三件东西同时对齐一张原图、一组实例的边界框、每个实例一张二值掩码。这三者必须共享同一套坐标系和同一份类别映射任何一处错位模型不会报错只会安静地学歪。主流实现里无论是基于 COCO 格式还是自定义 Dataset最终喂进网络的结构大致是这样图像张量归一化到 0~1boxes 是[x1, y1, x2, y2]的浮点绝对坐标labels 是从 1 开始的整数0 保留给背景masks 是与原图同尺寸的布尔或 0/1 矩阵每个实例一张。注意 labels 从 1 开始这一条后面会专门讲它带来的坑。这意味着标注工具需要提供的不只是一个框或一条线而是能被还原成闭合多边形的几何描述。纯矩形框工具在检测任务里够用但到了实例分割框只能告诉你目标在哪告不了你像素边界在哪所以 VIA 和 labelme 的分工本质上是任务粒度的分工。2.2 VIA 的适用边界只框不描的时候最省事VIA 是浏览器里跑的标注工具单 HTML 文件双击打开就能用不需要装 Python 环境。这个特性在两种场景下价值极大一是临时拉几个非技术同事帮忙做粗标二是需要在不同机器之间来回搬标注工程。它的工作方式是围绕一个 project 文件展开的所有图像路径、标注结果都存在一个 JSON 里图像本体可以外链。标注类型支持矩形、多边形、圆形、点、折线导出格式也有多种包括 COCO 风格。但我一般只让 VIA 干两件事给检测模型做预标注以及给 labelme 做第一轮粗筛。原因是 VIA 的多边形标注体验在实例数量多的时候比较吃力——顶点吸附不如 labelme 顺手缩放和拖拽的精度也不够标一个 30 顶点的轮廓要来回微调很久。如果你的数据里目标边界比较规整比如矩形零件、文档区域VIA 的矩形模式完全够用如果是人、动物、车辆这类轮廓复杂的目标VIA 只适合出第一版粗框精修还是得换工具。另一个需要提前知道的点VIA 的 JSON 结构是按文件名 → 区域列表组织的和 COCO 的图片列表 标注列表扁平结构差别较大中间要写一层转换。转换时最容易丢的信息是坐标系——VIA 支持相对坐标0~1存储如果你的工程里开了这个选项转出来必须先乘以图像宽高否则框会全部缩到左上角。2.3 labelme 的多边形为什么更适合实例分割labelme 是 Python 生态里的桌面标注工具标注结果一张图对应一个同名 JSON几何信息存在shapes数组里每个 shape 有label、points、group_id、shape_type四个关键字段。这个结构对上文说的实例概念非常贴合一个 shape 就是一个多边形一组 shape 可以靠group_id归并成同一个实例。它相对 VIA 的优势集中在三点。第一是几何精度labelme 支持滚轮缩放、按住拖动画布、顶点级编辑标轮廓复杂的实例时效率差距很明显。第二是shape_type的语义清晰矩形、多边形、圆形、线段分开存转换脚本可以按类型分支处理不会出现看着像矩形其实是两个点的模糊情况。第三是和 Python 训练代码同语言转换脚本里可以直接 import labelme 的工具函数也可以自己用 OpenCV 画掩码不用跨语言倒腾。代价是它需要图形界面环境批量处理得靠命令行参数而且默认会把图像内容以 base64 编码塞进 JSON 的imageData字段。这个字段在几百张图的时候能把 JSON 撑到几十兆版本管理直接废掉——后面第 3 章会讲怎么关掉它。2.4 两条路线的选型对照把上面的判断压成一张表实际项目里按这个走基本不会选错维度VIAlabelme部署方式单 HTML浏览器打开Python 包需图形界面几何精度中等缩放受限高支持顶点级编辑实例归并无原生 group 概念group_id显式归并输出结构单 JSON 汇总一图一 JSON图像内嵌默认外链默认 base64 内嵌适合任务检测框、粗略预标实例分割、精修轮廓转换成本需自写一层映射生态内转换脚本成熟我的常规做法是先用 VIA 把两千张图快速过一遍只标矩形框和类别一天能出一千五百张然后把带框的图导进 labelme框的位置作为定位参考人工只做加多边形这一件事效率比从零开始描高不少。这条流水线在实例分割任务上比纯用任何一个工具都快。3. labelme 落地从安装到标完第一批 200 张3.1 环境准备与版本锁定labelme 的安装本身不复杂但版本这个事得提一句。不同大版本之间 JSON 结构有过调整主要是flags和shape_type相关协同标注的几个人如果版本不一致导出的文件混在一起会让转换脚本出现有的有字段有的没有的情况。所以第一步是把版本钉死。# 建议在独立虚拟环境里装避免和训练环境的 PyQt 冲突 python -m venv venv_label source venv_label/bin/activate # Windows 用 venv_label\Scripts\activate # 指定版本安装四个人用同一个版本号 pip install labelme5.4.1 # 验证 labelme --versionvenv这一步不是形式主义。labelme 依赖 PyQt5而训练环境里经常已经装了 OpenCV 的 GUI 版本两者在 Linux 上抢 Qt 插件的情况我见过不止一次表现是启动直接段错误。分环境装能省掉这类玄学问题。版本号按团队实际情况定关键是所有人一致装完把pip freeze的结果发到群里对齐。3.2 标注前的目录约定目录结构要在动手标之前定死因为 labelme 的 JSON 里存的是imagePath相对路径中途挪目录会导致转换脚本找不到图。我一般用这套dataset/ ├── images/ # 原图全部平铺不放子目录 │ ├── 0001.jpg │ └── 0002.jpg ├── annotations/ # labelme 输出的 json和图片同名 │ ├── 0001.json │ └── 0002.json └── labels.txt # 类别清单一行一个labels.txt是重点。labelme 启动时用--labels参数加载它标注界面右侧会出现预设类别列表点选即可不用手打。这一步能挡掉至少一半的类别名不一致问题——手打必然出现person、Person、person尾随空格三种写法。# 启动命令几个参数都要带上 labelme ./images \ --labels ./labels.txt \ --nodata \ --autosave \ --output ./annotations \ --nodata 之外还可以加 --keep-prev 参考上一张的标注逐个说这几个参数。--labels指定预设类别文件--nodata让 JSON 里不存 base64 图像数据文件体积能从几兆降到几 KB这个必须开--autosave在切换图片时自动保存防手滑--output把 JSON 统一写到 annotations 目录避免和原图混在一起。--keep-prev是可选的相邻帧相似比如视频抽帧时能直接继承上一张的标注改两笔就能用。3.3 多边形标注的实际操作节奏工具装好、目录建好剩下的就是手感问题。这里把最高频的几个操作列一下新手照着练十分钟基本就顺了Ctrl N新建标注或点左侧 Create Polygons进入多边形模式沿目标轮廓依次点击闭合时点回第一个点弹出类别选择框Ctrl Z撤销上一个顶点标歪了不用整个重来Ctrl 滚轮缩放画布标细节前先放大到能看清边缘A/D切换上一张 / 下一张Ctrl S手动保存开了 autosave 也需要切图才触发Delete删除选中的整个 shape实操中有两个习惯值得从第一天就养成。一是标注顺序统一比如固定按从左到右、从上到下标实例这样后面抽检的时候容易对账。二是轮廓点不要贴着边缘描稍微往里收一两个像素。原因是标签边缘和真实边缘的过渡带最容易让模型学糊往里收一点反而让 mask 更干净这个在第 6 章讲验证时会再提。3.4 JSON 结构逐字段拆解标完一张打开 JSON 看一眼心里要有数。一个典型文件长这样{ version: 5.4.1, flags: {}, shapes: [ { label: person, points: [[312.4, 108.7], [355.2, 112.1], [361.0, 240.5]], group_id: null, shape_type: polygon, flags: {} } ], imagePath: 0001.jpg, imageData: null, imageHeight: 480, imageWidth: 640 }shapes是核心一个元素对应一个几何区域。points是绝对像素坐标不是归一化的顺序就是点击顺序label必须和labels.txt里的字符串完全一致group_id默认 null把同一实例拆成多块时手工填入相同整数shape_type决定转换脚本怎么解释points——polygon是任意多点rectangle只有两个点左上、右下circle是两个点圆心、圆周上一点linestrip是折线不闭合。imageData在加了--nodata之后是 null脚本里要靠imagePath去 images 目录捞原图。imageHeight和imageWidth是 labelme 打开图片时读到的尺寸转换时拿它和 PIL 读出的尺寸对一下能抓出 EXIF 旋转那类问题。4. 把 labelme JSON 转成 COCO 与二值 mask脚本与四个边界坑4.1 转换目标COCO instances 的三块内容转换不是把 json 拼一起这么简单得先明确目标结构。COCO 的实例标注文件由三块组成images记录每张图的 id、文件名、宽高annotations记录每个实例的 id、所属图片 id、类别 id、bbox、area、segmentation、iscrowdcategories记录类别 id 和名称。三者靠 id 互相引用任何一处 id 不唯一或者引用不到pycocotools 加载时就会报错。segmentation有两种表达多边形一串扁平坐标和 RLE游程编码的掩码。实例分割一般用多边形就够训练框架会在 Dataset 里自己栅格化成 mask。bbox用[x, y, width, height]的绝对像素格式注意是宽高不是右下角坐标这个和很多检测框架的xyxy不一样写错不会报错只会让框整体偏移。area我建议直接用栅格化后的 mask 像素数算而不是用外接框面积因为后面做小目标过滤时用得上。4.2 逐实例转换脚本下面这个脚本把 labelme 目录转成 COCO instances JSON处理了group_id归并、shape_type分支和类别过滤import json import os import glob from collections import defaultdict import cv2 import numpy as np # 类别清单必须和 labels.txt 顺序一致id 从 1 开始0 留给背景 CLASSES [person, car, dog] NAME2ID {name: i 1 for i, name in enumerate(CLASSES)} def shape_to_polygon(shape): 把不同 shape_type 统一成多边形点集返回 float32 的 Nx2 数组 pts np.array(shape[points], dtypenp.float32) st shape.get(shape_type, polygon) if st polygon: return pts if st rectangle: # rectangle 只存了左上和右下两个点扩成四个角 (x1, y1), (x2, y2) pts return np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtypenp.float32) if st circle: # circle 存圆心和圆周上一点用外接正方形近似成 32 边形 (cx, cy), (px, py) pts r float(np.hypot(px - cx, py - cy)) ang np.linspace(0, 2 * np.pi, 32, endpointFalse) return np.stack([cx r * np.cos(ang), cy r * np.sin(ang)], axis1) return None # point / linestrip 这类不参与实例分割直接丢弃 def convert(json_dir, image_dir, out_path): images, annotations [], [] categories [{id: i 1, name: n, supercategory: none} for i, n in enumerate(CLASSES)] ann_id, skipped 1, 0 for img_id, jf in enumerate(sorted(glob.glob(os.path.join(json_dir, *.json))), start1): data json.load(open(jf, encodingutf-8)) img_name data[imagePath] img cv2.imread(os.path.join(image_dir, img_name)) if img is None: print(f[skip] 读不到图{img_name}) continue h, w img.shape[:2] # 尺寸一致性校验抓 EXIF 旋转、图片被替换等问题 if (h, w) ! (data[imageHeight], data[imageWidth]): print(f[warn] 尺寸不一致 {img_name}: json({data[imageHeight]},{data[imageWidth]}) f实际({h},{w})) images.append({id: img_id, file_name: img_name, width: w, height: h}) # 按 group_id 归并同一实例的多个多边形合并成一条标注 groups defaultdict(list) for shape in data[shapes]: if shape[label] not in NAME2ID: skipped 1 continue gid shape.get(group_id) key gid if gid is not None else f_s{len(groups)} groups[key].append(shape) for _, shapes in groups.items(): mask np.zeros((h, w), dtypenp.uint8) segs [] for s in shapes: poly shape_to_polygon(s) if poly is None: continue cv2.fillPoly(mask, [poly.astype(np.int32)], color1) segs.append(poly.flatten().tolist()) if not segs: continue ys, xs np.where(mask 0) if len(xs) 0: continue x1, x2, y1, y2 xs.min(), xs.max(), ys.min(), ys.max() annotations.append({ id: ann_id, image_id: img_id, category_id: NAME2ID[shapes[0][label]], segmentation: segs, bbox: [int(x1), int(y1), int(x2 - x1 1), int(y2 - y1 1)], area: int(mask.sum()), iscrowd: 0, }) ann_id 1 coco {images: images, annotations: annotations, categories: categories} json.dump(coco, open(out_path, w, encodingutf-8), ensure_asciiFalse) print(f图片 {len(images)} 张实例 {len(annotations)} 个跳过未知类别 {skipped} 处) if __name__ __main__: convert(./annotations, ./images, ./instances_train.json)逻辑上有三个决策点值得说清楚。第一是CLASSES的顺序它直接决定category_id训练时 Dataset 的类别映射必须和它完全一致否则模型学到的类别会整体错位而这个错在 loss 曲线上一丁点异常都看不出来。第二是group_id归并defaultdict的 key 用 group_id为 null 时用序号兜底保证没编组的 shape 各自成实例。第三是bbox用 mask 的包围盒而不是多边形顶点的包围盒因为多边形可能有凹处后者会算大。参数方面iscrowd固定 0表示每个实例都是独立可分的如果你的数据里有大量重叠目标并且打算用 crowd 标注这里要改成 1 并在计算 mAP 时做特殊处理一般小规模数据集用不上。area用mask.sum()得到的是像素数如果要做面积归一化再除以w*h。4.3 类别映射表与标签清洗CLASSES列表看着简单实际上是转换环节最容易出事的地方。标注过程中只要有人绕过预设列表手打了标签就会出现同名不同写或者多一个字的情况。稳妥的做法是在转换前先跑一次全量统计把所有出现过的label值列出来对账import json, glob from collections import Counter counter Counter() for jf in glob.glob(./annotations/*.json): for s in json.load(open(jf, encodingutf-8))[shapes]: counter[s[label]] 1 for name, cnt in counter.most_common(): print(f{repr(name):24} {cnt})用repr打印是刻意的能显示person 和person的区别也能看出全角空格。对完账把异常项在 JSON 里批量改掉比在转换脚本里写一堆容错分支干净得多。常见的脏标签有这么几种尾随空格、首字母大写、中英文混用car和汽车同时出现、带序号后缀person1、person2多标记协同时的典型产物。最后一种要特别小心它们语义上属于同一类但如果直接进CLASSES模型会被迫学两个高度相似的类mAP 会明显掉。4.4 划分数据集按图分不按实例分转换完之后要划分训练集和验证集。这里有个几乎人人都踩过一次的坑按标注条目随机分。如果一张图上有 20 个实例按实例分会让这 20 个中的一部分进训练集、一部分进验证集验证指标会虚高好几个点因为模型在训练时已经见过这张图的背景、光照、构图了。正确的做法是先按图片划分再按图片 id 把标注分下去import json, random coco json.load(open(./instances_train.json, encodingutf-8)) ids [img[id] for img in coco[images]] random.seed(42) # 固定种子保证每次划分结果可复现 random.shuffle(ids) split int(len(ids) * 0.85) val_ids set(ids[split:]) val { images: [i for i in coco[images] if i[id] in val_ids], annotations: [a for a in coco[annotations] if a[image_id] in val_ids], categories: coco[categories], } train { images: [i for i in coco[images] if i[id] not in val_ids], annotations: [a for a in coco[annotations] if a[image_id] not in val_ids], categories: coco[categories], } json.dump(train, open(./train.json, w), ensure_asciiFalse) json.dump(val, open(./val.json, w), ensure_asciiFalse) print(ftrain {len(train[images])} 图 / {len(train[annotations])} 实例) print(fval {len(val[images])} 图 / {len(val[annotations])} 实例)random.seed(42)不是可选项。没有固定种子的划分两次实验的结果根本不可比调参调到最后会发现参差不齐的指标其实来自数据切分。85/15 这个比例适合千张量级数据量上万之后可以往 95/5 走验证集保证每类至少 50 个实例即可。划分完打印两边的实例数如果某一类在验证集里是 0说明这批数据本身就不平衡得回去补样本。5. 避坑与排查标注数据翻车的五条血泪记录5.1 掩码全是空白矩形的两点坑现象转换脚本跑完没有报错但把 mask 可视化出来全是黑的或者只有零星几个像素。原因标注时用了矩形工具shape_type为rectanglepoints里只有左上和右下两个点。直接丢给cv2.fillPoly时两个点构不成多边形OpenCV 会静默返回不填充不抛异常。解决在转换脚本里按shape_type分支处理矩形扩成四角圆形用外接多边形近似见 4.2 的shape_to_polygon。更省事的办法是标注规范里直接禁用矩形和圆形全部用多边形标代价是标注速度慢一点但省掉一整类转换问题。我现在的项目里就是全多边形标之前跟标注的人讲清楚比事后修脚本划算。5.2 同一目标被学成两个实例现象训练完的模型在一辆车上预测出两个重叠的 mask两个框几乎重合类别相同。看训练数据也没发现异常。原因目标被遮挡标注的人把它拆成两个 shape 分别描了两块但group_id都是 null。转换时脚本把它们当成两个独立实例模型就学到了一个目标可以有两个实例这个错误模式。解决拆分标注时必须编组。在 labelme 里把同一实例的多个 shape 选中后合并成一组group_id会被写成同一个整数转换脚本据此合并成一条标注。已经标完的数据可以写脚本补救如果两个 shape 类别相同、外接框 IoU 超过 0.6、且其中一个的 mask 与另一个的凸包有较大重叠就自动把 group_id 设成相同值。这类启发式规则有误伤改完必须过一遍可视化抽检。5.3 训练 loss 正常但 mAP 极低现象训练集 loss 一路降得很好验证集 mAP 停在 0.1 左右上不去分类分支的准确率看着还行。原因JSON 里存在大小写或空格差异的标签比如Person和person同时出现。转换脚本的NAME2ID只覆盖了一种写法另一种全部进了未知类别被跳过——而跳过是静默的只在最后打印一个计数很容易被忽略。结果就是训练集里有一部分实例凭空消失模型在验证时对这类目标完全没学会。解决转换前跑一次全量 label 统计4.3 的代码并对所有标签做统一归一化去首尾空格、转小写、中文全角转半角。归一化之后再核对CLASSES。同时在转换脚本里把跳过未知类别从计数改成打印具体文件名和标签别让它安静地发生。5.4 框的位置整体偏移现象可视化时发现所有框都比目标往左上偏了一截mask 却基本对得上。原因points是绝对像素坐标但图片被外部工具做过裁剪或缩放JSON 里的imageWidth/imageHeight和实际图片已经不一致。labelme 保存时记的是当时的尺寸之后换图不会更新。解决转换时做尺寸断言4.2 里那段[warn]就是这个用途发现不一致直接中断而不是继续跑。另外原图目录要设为只读禁止任何人做二次处理需要裁剪或缩放时先把 JSON 一起同步转换再进流程。还有一种情况是 JPEG 的 EXIF 旋转标记文件系统里看着是竖图PIL 和 OpenCV 解码出来可能是横的两边宽高对调。统一用cv2.imread读图并在转换前把所有图片的 EXIF 方向标记清掉。5.5 类别 id 从 0 开始导致背景错乱现象模型把所有背景都预测成某一类或者某一类完全预测不出来混淆矩阵看着很怪。原因自定义CLASSES列表时用了enumerate(CLASSES)从 0 开始编号而 Mask R-CNN 系的实现普遍约定 0 是背景真实类别从 1 开始。类别 id 和背景冲突后网络输出的第 0 通道既是背景又是某个类梯度互相打架。解决NAME2ID一律用i 1转换完检查categories里最小 id 是不是 1。如果你的训练框架用 COCO 预训练权重还要确认categories的顺序和预训练权重的输出通道顺序对得上否则加载权重时会整体错位这种情况建议先冻结检测头训一轮再解冻。6. 标注质量自检把 500 张样本过一遍可视化复核转换脚本跑通不代表数据能用最后这道复核关卡住的问题比前面所有环节加起来都多。我一般分三层过统计层、可视化层、断言层。统计层看分布。把 COCO JSON 读进来按类别统计实例数、按图片统计每图实例数、按面积分档统计小中大目标占比。重点看两个数单类实例数低于 100 的类基本不用指望模型能学好得回去补标面积小于图片总面积 0.1% 的实例绝大多数是误点的噪点建议加一条过滤规则直接剔除。import json from collections import Counter coco json.load(open(./train.json, encodingutf-8)) cat {c[id]: c[name] for c in coco[categories]} per_cls Counter(cat[a[category_id]] for a in coco[annotations]) per_img Counter(a[image_id] for a in coco[annotations]) for name, cnt in per_cls.most_common(): print(f{name:12} 实例 {cnt:5}) # 小目标筛查面积占比低于阈值的大概率是误点 tiny [] for a in coco[annotations]: img next(i for i in coco[images] if i[id] a[image_id]) ratio a[area] / (img[width] * img[height]) if ratio 0.001: tiny.append((a[id], a[image_id], round(ratio, 5))) print(f面积占比 0.1% 的实例{len(tiny)} 个)这段代码的next()查找在万级图片上会慢实际项目里先把images转成字典再做 O(1) 查询。0.001这个阈值不是固定的取决于你的目标尺度——标注单据、零件缺陷这类小目标可以放到 0.0005行人车辆这类可以收到 0.002。可视化层看质量。随机抽 50 张把每个实例的 mask 用半透明色叠到原图上同时画出 bbox 和类别名存成 png 一张张翻。这一步没有脚本能替代很多问题只能靠眼睛发现轮廓明显切进目标内部的、两个实例 mask 互相压住的、框画得比 mask 大一圈的。抽检比例我习惯按 10% 起数据量小时直接全看。import random import cv2 import numpy as np random.seed(7) picks random.sample(coco[images], 50) for img_info in picks: img cv2.imread(f./images/{img_info[file_name]}) overlay img.copy() for a in coco[annotations]: if a[image_id] ! img_info[id]: continue for seg in a[segmentation]: poly np.array(seg, dtypenp.int32).reshape(-1, 2) cv2.fillPoly(overlay, [poly], (0, 255, 0)) x, y, w, h a[bbox] cv2.rectangle(img, (x, y), (x w, y h), (0, 0, 255), 2) out cv2.addWeighted(overlay, 0.4, img, 0.6, 0) cv2.imwrite(f./check/{img_info[file_name]}, out)addWeighted的 0.4/0.6 是让 mask 半透明能同时看到轮廓和目标本身的纹理判断贴合度更准。绿色是 mask红色是框两者不一致的地方一眼就能看出来。断言层兜底。新数据进来时跑一次pycocotools的加载和annToMask能自动抓出segmentation点数不合法、bbox越界、area与 mask 对不上这类结构性问题。把它写成 CI 里的一步比人肉检查可靠得多。血泪经验是我曾经因为省掉可视化抽检把一个group_id全为 null 的数据集直接训了两天最后发现验证集里同一类目标的 AP 比预期低了一倍回头查是遮挡实例被拆成了两个。从那以后我每次转完格式都强制走一遍统计 → 抽检 10% → pycocotools 断言这套流程哪怕数据是别人给的、哪怕只改了一个标签。这套标注工具和转换脚本的完整流程按第 2 章的选型、第 3 章的目录约定、第 4 章的脚本一路走下来基本能覆盖从零到可训练样本的全过程。希望帮到你。本文还有配套的精品资源点击获取