☰
labelme格式牙齿分割数据集如何高效转COCO并训练语义分割模型
2026/10/11 18:10:01 网站建设 项目流程

简介:针对牙齿分割与病变检测的图像分割数据集说明文档,适用于医学影像分析、目标检测与分割等方向的开发者,也可作为口腔疾病辅助诊断项目的参考。文档基于labelme 5.5.0标注规范,介绍了一套包含2616张jpg图像及对应json标注的数据集(不包含mask文件),标注类别覆盖Tooth(牙齿)、Caries(龋齿)、Cavity(龋洞)、Crack(裂纹)、calculus(牙结石)、inflamation(炎症)六类,并给出每类框数:Tooth 1709、Caries 2913、Cavity 1099、Crack 139、calculus 1207、inflamation 620。文档特别说明,部分牙齿未标注属正常情况,因主要目的是病变检测,不影响模型训练;若需进行语义分割或实例分割,可借助labelme打开编辑,再将json自行转换为mask、YOLO或COCO格式。这份docx说明文档共1个文件,压缩包约2.22MB,能帮助你快速掌握数据集的类别分布、标注规则和转换思路。已有116人学习,建议在开展牙齿图像分割实验前先通读。

1. 2616张labelme格式牙齿分割数据集:先别急着训练,把三件事想清楚再动手

牙齿分割和牙齿病变分割是口腔AI落地的两个地基任务:前者告诉系统“牙在哪”,后者告诉系统“哪颗牙有问题”。标题里这套组合——labelme格式、2616张、6类别,是个分量相当足的开局:2616张全景牙片在医学影像数据集里算得上中大规模,6类别说明标注不只勾了牙齿轮廓,还把病变区域单独拎了出来,这让下游能做“病变位置定位”,而不是只能做“牙齿计数”。适合谁:做全景片、CBCT影像分析的个人开发者,或者想做牙科辅助诊断系统的团队。拿到数据集第一反应往往是解压后直接开训,但更该做的是三件事:验货、转格式、核对标签顺序,这三步走错任何一步,后面几十个小时训练基本白费。

2. 验货环节:labelme的JSON结构里有什么,直接决定你的转换脚本怎么写

2.1 labelme标注文件的每一段字段,背后都是一个处理决策

labelme是标注工具,不是训练框架;它产出的每个json文件就是一张图的全部标注信息。牙齿分割数据集如果是labelme格式,图片旁边通常躺着一个同名.json。先看一个典型的例子,再逐段解释。

{ "version": "5.0.1", "flags": {}, "shapes": [ { "label": "tooth", "points": [[112.3, 85.6], [118.0, 92.1], [130.5, 90.0]], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "IMG_0001.png", "imageData": null, "imageHeight": 1536, "imageWidth": 2048 }

这段结构在labelme导出的文件里非常典型,字段含义如下:

字段含义对后续处理的直接作用
shapes标注对象数组每个元素一个目标,牙齿和病变都在里面
label当前对象的类别名6类标签的字符串藏在这里,大小写中英文都可能出现
points多边形顶点坐标数组是浮点数不是整数,转COCO时千万别提前取整
shape_type形状类型牙科影像几乎全是polygon,偶尔有rectangle
imagePath图像相对路径json移动后最容易失效的字段
imageData内嵌图像数据若不为null则是base64编码,json会膨胀到几十兆
imageHeight/Width标注时的图像尺寸与实际图像尺寸不一致时,说明图被换过

这里最值得记的一点:labelme的坐标是float,不是int。标注者画牙冠时不会精确到像素整数,系统记录的是浮点坐标。很多团队转格式时图省事直接int(),等mask出来发现牙冠边缘全是锯齿,这是最常见的第一类翻车现场。

另一个常见坑是flags。labelme里flags通常是空对象,但有些工作流会用flags记录“这张图有没有病变”“是否难例”。处理时要保留但不参与类别判断。我一般会先写一个只读脚本把所有json的flags值拉一份,防止有标注者在里面塞了额外语义而不自知。

2.2 六个类别到底叫什么:让脚本统计一遍,别信文档猜

拿到数据集后第一个动作不是读docx说明,而是跑标签统计。标题自称“6类别”,但实际标签名可能五花八门:英文小写、首字母大写、中文、甚至拼写错误。“牙齿”“龋齿”“根尖病变”在多人标注时很容易出现同义不同名。

import json, glob, os, collections def collect_all_labels(json_dir): label_counter = collections.Counter() shape_type_counter = collections.Counter() for jf in glob.glob(os.path.join(json_dir, "*.json")): with open(jf, "r", encoding="utf-8") as f: data = json.load(f) for shape in data["shapes"]: lab = shape["label"].strip() label_counter[lab] += 1 shape_type_counter[(lab, shape["shape_type"])] += 1 return label_counter, shape_type_counter label_counter, shape_counter = collect_all_labels("annotations") print("label 统计:") for label, cnt in label_counter.most_common(): print(f" {label:20s} {cnt}") print("label + shape_type 组合:") for key, cnt in shape_counter.most_common(): print(f" {key[0]:20s} {key[1]:10s} {cnt}")

这段脚本按label分组统计所有标注对象的出现次数,同时统计label与shape_type的组合,为的是确认6个类别里有没有混进矩形标注或多边形标注。输出后建议人工核对一遍:正常牙齿数据集里,牙齿主类别占比最高,病变类占比往往只有个位数百分比。如果统计出来的类别不止6个,看是不是大小写混用或中英文混用,比如"tooth"和"Tooth"被统计成两个类别,这在多人标注的数据集里很常见。

另一个要留意的是空标注:如果统计发现某个json的shapes列表为空,也就是这张图没有任何标注。后续转COCO时它的annotations会是空的,训练脚本如果默认每张图都有mask就会直接报错。

2.3 配对与一致性检查:图片、json、尺寸三方对不齐,训练时全炸

labelme的imagePath记录的是标注时的相对路径。数据集经过下载、解压、重命名后,这个字段经常失效。还有一类问题是图像被重新导出过,分辨率变了,但json里的imageHeight/Width还是旧的。跑一次三方对账:

import os, json from PIL import Image IMG_DIR = "images" JSON_DIR = "annotations" missing_img = [] # json 引用了不存在的图 size_mismatch = [] # json 里的尺寸和实际图不一致 for fn in os.listdir(JSON_DIR): if not fn.endswith(".json"): continue with open(os.path.join(JSON_DIR, fn), "r", encoding="utf-8") as f: data = json.load(f) img_path = os.path.join(IMG_DIR, data["imagePath"]) if not os.path.exists(img_path): missing_img.append(fn) continue with Image.open(img_path) as im: real_w, real_h = im.size if (real_w, real_h) != (data["imageWidth"], data["imageHeight"]): size_mismatch.append((fn, data["imageWidth"], data["imageHeight"], real_w, real_h)) print("缺图 json:", len(missing_img)) for f in missing_img[:10]: print(" ", f) print("尺寸不一致:", len(size_mismatch)) for f, jw, jh, rw, rh in size_mismatch[:10]: print(f" {f}: json={jw}x{jh} 实际={rw}x{rw}")

逻辑说明:脚本对每个json先确认对应图片存在,再比对标注时记录的尺寸和实际尺寸。缺图比尺寸不一致更危险——缺图样本在训练时会让DataLoader崩掉;尺寸不一致则会造成mask和图像对不上,训练不报错但指标永远上不去。

参数说明:这里用PIL读图而不是OpenCV,是因为有些牙科影像可能是16bit灰度PNG,OpenCV默认按8bit读,会静默截断。PIL至少能忠实地返回尺寸。如果发现大量尺寸不一致,后续统一缩放到固定输入尺寸时,要按实际图像尺寸缩放标注坐标,而不是按json里记录的值缩放。

3. 把labelme转成COCO:多点归一化、坐标精度与可视验证

3.1 为什么宁可多写一次转换,也不直接用labelme json训练

labelme是标注环节的工具,不是训练环节的数据接口。每张图一个json,没有统一的类别表、没有图像清单、没有默认的数据集划分;而训练框架里无论是检测还是分割,都期望数据能一次性加载、按索引访问。COCO格式恰好补上这些:单个json聚合了images、annotations、categories三张主表,生态里的评估脚本、可视化工具、数据增强库都能直接消费。做牙齿分割时,如果后面要切到实例分割(每颗牙单独一个实例),COCO的polygon和RLE都有现成支持;如果只是语义分割,COCO也能在训前一次转成mask目录。

还有一个实际理由:团队协作。标注工作是多人分批做的,labelme的元数据字段(version、group_id)不可控;COCO经过一次标准转换后,类别映射、坐标格式、字段顺序全都固定了,后续谁接手都不用再猜。我一般会保留两套中间产物:原始labelme json作为标注底稿,转换后的COCO作为训练唯一入口,两边用脚本保证同步,绝不手工改COCO。

3.2 labelme2coco转换脚本:鞋带公式算面积,坐标保持float

转换的核心工作是三件事:把多边形的二维点阵打成一维浮点数组;按类别映射表填category_id;为每个标注补上bbox和area。下面是一个可直接落地的版本。

import json, os, glob import numpy as np def polygon_area(points): """鞋带公式计算多边形面积,返回平方像素数""" pts = np.asarray(points, dtype=np.float64) if pts.shape[0] < 3: return 0.0 x = pts[:, 0] y = pts[:, 1] return 0.5 * abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1))) def flatten_points(points): """COCO 的 polygon 需要一维扁平列表 [x1,y1,x2,y2,...]""" return np.asarray(points, dtype=np.float64).flatten().tolist() def labelme_to_coco(ann_dir, img_dir, category_names, out_path): coco = { "images": [], "annotations": [], "categories": [{"id": idx, "name": name} for idx, name in enumerate(category_names, start=1)], } ann_id = 1 for img_id, json_file in enumerate( sorted(glob.glob(os.path.join(ann_dir, "*.json")))): with open(json_file, "r", encoding="utf-8") as f: data = json.load(f) coco["images"].append({ "id": img_id, "file_name": os.path.basename(data["imagePath"]), "width": data["imageWidth"], "height": data["imageHeight"], }) for shape in data["shapes"]: label = shape["label"].strip() if label not in category_names: continue # 未知标签先跳过,避免把噪音带进训练 cat_id = category_names.index(label) + 1 pts = shape["points"] if len(pts) < 3: continue # 少于3个点无法构成多边形 xs = [p[0] for p in pts] ys = [p[1] for p in pts] bbox = [min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)] area = polygon_area(pts) coco["annotations"].append({ "id": ann_id, "image_id": img_id, "category_id": cat_id, "segmentation": [flatten_points(pts)], "area": area, "bbox": bbox, "iscrowd": 0, }) ann_id += 1 with open(out_path, "w", encoding="utf-8") as f: json.dump(coco, f, ensure_ascii=False) # 顺序即 category_id,跑之前先按第2章统计确认类别名 labelme_to_coco("annotations", "images", ["tooth", "caries", "gingiva", "root", "pulp", "periapical_lesion"], "train_coco.json")

脚本逻辑分四层:外层遍历json并建立image表;内层遍历每个shapes生成annotation;未知标签直接跳过;最后整体落盘。area用鞋带公式算真实多边形面积,而不是bbox面积,COCO评估工具在算mask AP时会按这个字段做归一化,填错会拿到一个自己骗自己的高AP。

参数说明里有一个必须强调的坑:category_names的顺序就是最终category_id。同一个数据集,如果有人把顺序从["tooth","caries"]改成["caries","tooth"],训练结果会完全错位。我的习惯是先用第2章统计脚本拿到全部label集合,人工确认一次语序,再把这个列表写进转换脚本;任何新增或改名都要重新走统计流程,不直接在脚本里加一行。

另一个值得注意的点:bbox用min/max直接计算,COCO要求格式为[x, y, width, height],左上角坐标加宽高,别把右下角坐标填进去。

3.3 转完必须可视化:没赋值的多边形是看不出来的

转换脚本不报错不等于结果正确。牙科影像的多边形数量多、形态细碎,一颗牙一个轮廓,病变区可能只有几十个像素,最有效的验证方式是把mask叠加到原图上人工扫一遍。

import json, cv2, numpy as np, os def render_masks(coco_path, img_dir, out_dir, colors=None): with open(coco_path, "r", encoding="utf-8") as f: coco = json.load(f) for img_info in coco["images"][:20]: # 先抽查前20张 img = cv2.imread(os.path.join(img_dir, img_info["file_name"])) anns = [a for a in coco["annotations"] if a["image_id"] == img_info["id"]] for ann in anns: poly = np.array(ann["segmentation"][0], dtype=np.float64) poly = poly.reshape(-1, 2).astype(np.int32) color = colors.get(ann["category_id"], (0, 255, 0)) cv2.fillPoly(img, [poly], color=color) out = os.path.join(out_dir, img_info["file_name"]) cv2.imwrite(out, img) print("written:", out) render_masks("train_coco.json", "images", "overlay_check", {1: (0, 255, 0), 2: (0, 0, 255), 3: (255, 200, 0)})

脚本抽查前20张,遍历每张图的annotation,把多边形填色画到原图上再写回磁盘。检查重点不是整体效果,而是三类细节:牙冠轮廓是否贴合牙釉质边界;病变类别是否独立成块而不是糊成一片;相邻牙的重叠区域是否被重复标注。一条经验阈值:如果抽查里看到超过三处轮廓明显漂移,说明原始json在标注时就有系统性偏移,先回到标注端修数据,不要硬调增强参数去掩盖。

提示:可视化时不要把GT和预测画在同一张图里,那样会影响对原始标注质量的判断。这一步只画GT,不叠加任何模型输出。

4. 训练设置与参数选择:牙齿主体分割和病变分割,是两套难度

4.1 模型选型:2616张的体量,UNet家族依然是首选

牙齿分割数据和自然图像不一样:全景片结构相对固定,牙齿在图像里位置规律、边界清晰,用不着非常深的网络。我一般首选UNet或UNet++,配一个带预训练权重的encoder,比如resnet34或efficientnet-b0。

为什么不直接上SegFormer或Swin这类结构:它们参数多,在2616张规模下容易过拟合;牙科影像没有ImageNet那么强的结构先验,预训练收益打折扣。DeepLabV3+可以做第二梯队尝试,它的空洞卷积对牙冠大边界有优势,但对病变小目标不友好。关键区分是任务:牙齿主体分割是大目标、边界清晰,常规Dice Loss就能收敛;病变分割(龋齿、根尖周病变)目标小、占比低,更像小目标检测,网络需要保留较高分辨率特征,UNet的跳跃连接在这里比深层全局注意力更管用。

4.2 划分数据集时按影像来源分组,不能随机切

牙科场景里,一个患者往往有多个视角或多次拍摄的片子,同一患者的多张影像高度相似。如果随机打乱并按比例划分,训练集里出现某位患者的部分片子、验证集里出现这位患者的另一张片子,模型等于提前“见过”了验证数据,验证指标虚高得离谱,上线后会原形毕露。常见做法是按文件名前缀推断患者ID,或者按数据集文档里带的患者编号字段分组。

import os, random def group_split(ann_dir, val_ratio=0.15, test_ratio=0.1, seed=42): json_files = [f for f in os.listdir(ann_dir) if f.endswith(".json")] groups = {} # 常见命名规则: patient_001_left.png -> patient_001 for f in json_files: pid = "_".join(f.split("_")[:2]) groups.setdefault(pid, []).append(f) pids = list(groups.keys()) rng = random.Random(seed) rng.shuffle(pids) n_val = max(1, int(len(pids) * val_ratio)) n_test = max(1, int(len(pids) * test_ratio)) val_pids = set(pids[:n_val]) test_pids = set(pids[n_val:n_val + n_test]) train_pids = set(pids[n_val + n_test:]) split = {"train": [], "val": [], "test": []} for pid, files in groups.items(): if pid in train_pids: split["train"].extend(files) elif pid in val_pids: split["val"].extend(files) else: split["test"].extend(files) for k, v in split.items(): print(k, len(v)) return split group_split("annotations")

分组脚本先把文件按患者ID聚合,再以患者为单位划分训练、验证、测试。关键参数是文件名切分规则:如果命名是IMG_0001.png这种没有患者信息的,说明数据集没预留分组字段,只能退回随机划分,但在报告里要写明这一局限。val_ratio和test_ratio按0.15/0.1是全景片数据集的合理默认值,测试集只做最终评估,不参与调参。

提示:检查分组合理性时,打印每个患者下的样本数与影像视角数。如果某个患者有20张以上,而数据集平均每人只有2张,优先怀疑文件名前缀规则没切准。

4.3 训练参数:类别权重、损失函数、增强三板斧

牙科数据集的特殊性在于类别不平衡:牙齿主体占面积大,病变区域可能只占整张图的0.5%甚至更少,模型会用“把所有像素预测为背景”来拿高分。缓解手段有两个:给loss加类别权重,或改用Dice/Tversky系列损失。我常用的组合是Dice Loss加Cross Entropy,权重按各类别面积统计的倒数设置。

config = { "model": "unet", "encoder": "resnet34", "pretrained": True, "image_size": (512, 512), "batch_size": 8, "epochs": 80, "optimizer": "adamw", "lr": 1e-4, "weight_decay": 1e-4, "loss": { "name": "dice_ce", "dice_weight": 0.7, "ce_weight": 0.3, "class_weights": [1.0, 2.5, 4.0, 3.0, 4.5, 5.0] }, "augmentation": { "random_rotate": 30, "hflip": True, "scale": [0.8, 1.2], "brightness_contrast": 0.2, "clahe": True, "elastic": False }, "metrics": ["dice", "iou"] }

逐项说明为什么这么调。image_size 512是全景片的折中:原始尺寸常为1500~3000像素,直接训练显存不够,缩到512能保留牙冠边界和病变区的相对细节;再往下到256,根尖病变这种小区域会被抹成几个像素。batch_size 8在12GB显存附近是UNet加resnet34的合理值,显存小就降到4,并同步把epochs从80提到120。lr 1e-4配adamw是稳定起点,配合cosine衰减比step衰减省心。

dice_weight和ce_weight的比值决定网络早期收敛速度:CE提供稳定梯度方向,Dice保证前景区域被压实。class_weights要从第2章统计的面积占比回推——面积占比最小的类别权重最大,权重总和归一化到1即可。clahe增强对口腔影像价值很大,因为不同医院设备输出的对比度差异明显,clahe能抹平一部分这种差异;elastic弹性形变在牙科解剖结构上要谨慎,牙的位置有解剖约束,过度形变会让模型学到不可能的几何形态。

验证时不能只看整体Dice。我的习惯是分两个维度看:牙齿主体类别的Dice是否稳定在0.88以上;病变类别单独统计Dice和Recall,因为漏检一颗病变更致命。如果病变类Dice上不去但Recall还行,通常是边界分割过细导致的评估波动;如果Recall也不行,问题多半在标注或类别权重,而不是模型结构。

5. 牙齿分割数据集避坑指南:5个高频踩坑点与解决方案

5.1 json和图片数量对不上:先做双向核对

现象:数据集说明写着2616张,解压后json有2616个,但images目录里只有2590张图,或者反过来多出30个json。

原因:标注阶段有图被误删、同步工具没传全、重命名时文件被覆盖。这类问题不影响转换脚本,但会在训练时变成DataLoader的随机报错,而且报错信息经常指向一个毫不相关的文件,极难排查。

解决:进入任何后续流程前,先跑一次双向文件清单比对,把只在images和只在json里的文件都列出来。常见处理是缺json的图直接排除,缺图的json也排除——除非能找到原始文件,否则不要手工补json,补出来的标注质量无法验证,反而污染训练。

import os img_dir = "images" ann_dir = "annotations" img_names = {f for f in os.listdir(img_dir) if os.path.splitext(f)[1].lower() in {".png", ".jpg", ".jpeg", ".bmp"}} json_names = {f.replace(".json", "") for f in os.listdir(ann_dir) if f.endswith(".json")} only_img = img_names - json_names only_json = json_names - img_names print("只有图没有json:", len(only_img)) for f in sorted(only_img)[:10]: print(" ", f) print("只有json没有图:", len(only_json)) for f in sorted(only_json)[:10]: print(" ", f)

脚本用集合差集做比对,一步得出两类缺失清单。注意它对比的是去掉扩展名后的文件名,因为labelme有时把imagePath写错扩展名,比如图是.PNG大写、json里记录.png小写,在Linux上就是两个文件。

5.2 Mask边缘锯齿严重:坐标精度被int吃掉了

现象:可视化检查时牙齿轮廓呈明显阶梯状,像马赛克拼出来的。

原因:转换脚本里把points先做了int(),或者有人为了省存储把坐标量化到整数。牙冠轮廓本身是平滑曲线,量化后一个像素的误差在牙釉质高对比边界上会放大成肉眼可见的锯齿。

解决:从labelme到COCO全程保持浮点坐标,只允许在“画mask后输出为uint8图像”这一步才做类型转换。另一个隐含点:训练时对mask做resize用最近邻插值,对原图做resize用双线性插值。两件事分开做,不要图省事把原图和mask合并成一条数据流resize。

5.3 类别错位:label名不统一,训练集里出现第七个类别

现象:统计出来的label集合里有"tooth"、"Tooth"、"牙",还有拼写错误"carie"。文档里说6类,实际统计出7、8种字符串。

原因:多人协同标注时没有统一标签字典;标注工具下拉框可手动输入,标错一天也没人发现。

解决:在转换脚本前加一道“标签归一化”。先收集全量label字符串,人工确认哪些是同义词,建映射表{'Tooth': 'tooth', '牙': 'tooth', 'carie': 'caries'},然后再进转换。归一化脚本要独立保存,这样重新训练时能复现同一个映射。更彻底的做法是跑一次期望类别清单校验:shapes里出现期望列表之外的字符串就中断并打印文件名,而不是默默跳过——静默跳过会把漏标伪装成正常。

5.4 图像尺寸不统一导致shape mismatch和标注漂移

现象:训练时DataLoader偶发shape mismatch;或者可视化时发现mask和牙冠错开几个像素。

原因:牙科设备品牌多,全景片输出尺寸跨度大,从1500x2000到3000x1500都有;加上json里的imageHeight/Width可能是标注时旧图的尺寸,换图后没同步更新。

解决:训练管线统一到固定尺寸512或1024,但缩放逻辑必须正确:先把labelme的浮点坐标乘上scale因子再画mask;不要先画mask再整体resize mask。如果json里记录尺寸和实际图像不一致,以实际图像为准计算scale因子。这类问题排第四是因为它不报错、不影响启动,只影响精度,最容易骗过人眼。

5.5 重复标注与自相交多边形:面积算负值、Dice高不起来

现象:某张图里同一颗牙被标了两遍;转换时面积计算出现异常值;或COCO评估时某个类别的AP低得反常。

原因:标注者画到一半发现画错了,没删旧多边形,直接画了第二个;或多边形最后闭合点处理不规范,首尾交叉形成自相交。

解决:转换脚本对每个label的多个polygon做合并判断:同label且中心点距离小于阈值时只保留面积大的那个;自相交多边形用鞋带公式算出的面积可能偏离真实值,处理方式是做一次几何修复,或者直接把这类样本挑出来人工复查。面积字段在COCO评估里是归一化权重,填错不会报错,但会拿到自我欺骗的指标。

6. 训练前再加一道工序:用mask质量报告揪出最后10%漏网问题

训练前把每张图的标注质量量化成一张报告,是我每次拿到新数据集必做的一步。具体做法:把掩码按类别统计像素占比、连通域数量,输出离群样本的缩略图,人只需要扫离群样本,不用看完全部2616张。一张全景片里,牙齿主体通常是几个大片连通域,病变区域是零星小斑块;如果一个类别在某些图里突然出现几十个细小连通域,多半是标注时多点了一堆锚点,或者在噪点上画了形状。

import json, numpy as np, cv2, os def quality_report(coco_path, img_dir, out_dir): with open(coco_path, "r", encoding="utf-8") as f: coco = json.load(f) records = [] for img in coco["images"]: anns = [a for a in coco["annotations"] if a["image_id"] == img["id"]] img_arr = cv2.imread(os.path.join(img_dir, img["file_name"])) h, w = img_arr.shape[:2] for ann in anns: mask = np.zeros((h, w), dtype=np.uint8) poly = np.array(ann["segmentation"][0]).reshape(-1, 2).astype(np.int32) cv2.fillPoly(mask, [poly], 1) area = int(mask.sum()) n_components = cv2.connectedComponents(mask)[0] - 1 records.append({ "image": img["file_name"], "category_id": ann["category_id"], "area_px": area, "components": n_components, }) for cat_id in range(1, 7): cat_recs = [r for r in records if r["category_id"] == cat_id] if not cat_recs: continue areas = sorted(r["area_px"] for r in cat_recs) q1, q3 = np.percentile(areas, [25, 75]) iqr = q3 - q1 outliers = [r for r in cat_recs if r["area_px"] < q1 - 1.5 * iqr or r["area_px"] > q3 + 1.5 * iqr] print(f"类 {cat_id}: 中位面积 {np.median(areas):.0f}, 离群样本 {len(outliers)}")

这段脚本按类别输出面积分布和四分位离群样本,把“我觉得哪里有问题”变成“算法告诉我哪里有问题”。参数q1/q3用的是经典箱线图离群阈值,在标注质量检查里够用;如果离群比例超过10%,说明该类别标注风格差异大,先看标注规范而不是直接删样本。

我现在的流程已经固化成:解压后先按第2章脚本验货,再按第3章脚本转COCO,用第5章检查单逐条过五类已知坑,最后跑一遍质量报告再进训练。这一圈下来,训练时遇到的意外会少很多,省下的时间远超过检查花掉的一小时。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询