尧图精选

瓷砖缺陷检测:COCO JSON清洗、转换YOLO与评估全流程

🕒 发布时间:2026/9/11 16:07:56 📁 来源:尧图网络
简介瓷砖缺陷检测数据集面向目标检测与工业质检场景服务需要训练外观缺陷识别模型的开发者、质检工程师及科研人员覆盖边缘崩裂、破洞、裂缝等典型缺陷整体规模包含7992张原始图像。压缩包采用zip格式共2000个文件以1997张jpg图片和3个json标注文件为主打包体积约579.25MBjson标注文件保存目标类别与位置信息标注格式支持YOLO、COCO JSON、Pascal VOC XML等主流工具链可衔接常见深度学习框架便于按需转换训练格式。该数据可支撑瓷砖产线自动质检、零售到货抽检、施工安装前筛查、建筑检测服务以及机器学习算法研究等落地场景帮助减少人工复检成本。目前已有646人浏览学习下载后可直接用于模型训练、标注格式对比与缺陷识别效果验证降低算法落地的前期数据准备门槛。1. 7992张瓷砖缺陷图配COCO JSON为什么值得自己再清洗一遍工业视觉里瓷砖质检的难点不是能不能检测而是缺陷形态差异巨大边缘崩裂是缺块破洞是局部穿孔裂缝却可能是从边缘延伸出的细线。拿到7992张原始图像附带COCO JSON标注看起来可以直接训练但COCO格式本身只规定数据结构不保证标注质量和样本分布。边缘崩裂在图像里往往占很大面积裂缝却可能只有几个像素宽这两类缺陷在同一个JSON里共存时直接跑模型会头疼。这篇博客我会带你从COCO JSON的字段结构开始讲清楚三种缺陷应该怎么存储、怎么验证、怎么转成YOLO格式以及最后用什么指标评判模型——不需要额外数据你手头这份数据集就够。2. COCO JSON格式里边缘崩裂、破洞、裂缝各自的标注形态2.1 COCO五项字段与瓷砖场景对照COCO JSON是一个根对象顶层固定五个键info、licenses、images、annotations、categories。images是一个列表每个元素记录一张图的id、file_name、width、heightannotations是另一个列表每个元素代表一个缺陷实例字段包括image_id、category_id、bbox、area、segmentation和iscrowdcategories则把类别id映射到名称。瓷砖缺陷检测里categories通常只写三类edge_chip边缘崩裂、hole破洞、crack裂缝。images里的宽高就是拍摄原图的分辨率比如1920x1080所有坐标值包括bbox和polygon顶点都以这个分辨率的像素绝对值为单位。这里最关键的是annotations里的segmentation字段。COCO支持两种写法polygon即一个二维数组每个内层数组是一串x1,y1,x2,y2,...顶点RLE即把掩码编码成游程长度。对于边缘崩裂和裂缝polygon远比RLE友好边缘崩裂的轮廓是一条从瓷砖边缘凹进去的折线polygon只需要标那些能表达凹缺的顶点裂缝又细又长用矩形框会框进大量背景用polygon才能贴着裂缝走向。破洞如果是圆润的孔洞polygon同样合适如果图像里是喷墨打印的碎点才需要考虑RLE。人工标注时多数是polygon因为labelme和CVAT默认导出polygon。提示拿到JSON后先确认segmentation是polygon形式还是RLE形式。RLE一般来自预测结果后处理自建数据集里出现RLE多半是转格式导出的坐标校验方式会完全不一样。为了快速看一个COCO JSON是否能用可以对着下面的表格逐项核对字段作用在瓷砖缺陷数据集里怎么核对info数据集版本、描述、日期看是否有自定义数据没有也不影响使用licenses版权信息商用前必须看但很多自建数据集是空数组images每张图id、file_name、width、height检查file_name是否与image文件一一对应宽高是否与真实图像一致categoriesid到类别名的映射确认edge_chip/hole/crack的id是否连续annotations里category_id是否都在其中annotations每个实例的bbox、area、segmentation是后续所有校验的核心看完表格再去看annotations实例就能区分这个JSON是不是COCO和这个COCO能不能直接用两件事。很多自建数据集的COCO JSON其实是脚本转出来的字段齐全但语义不对比如area仍然是bbox面积而不是多边形面积这种只能算长得像COCO。2.2 三种缺陷的segmentation写出方式polygon的存储格式是[x1, y1, x2, y2, ...]偶数位是x奇数位是y一个封闭多边形至少要3个顶点但实际标注中顶点越多轮廓越准。边缘崩裂的物理特征是瓷砖边缘缺了一个角标注时要把崩掉区域的边界圈成一圈起点和终点不必显式闭合COCO解析时会自动连接。破洞的轮廓是闭合曲线内部是深色孔洞如果孔洞中还有碎屑一个实例可能需要多个polygon来分别描述每个连通块COCO允许segmentation内层是一个二维数组每个子数组表示一个单独的多边形。裂缝是最容易标坏的一类。很多标注员会把裂缝标成几个断开的小段每个小段作为一个新实例但实际一条长裂缝应当尽量在一个实例里表达。出现断开通常是因为裂缝被纹理或釉面反光截断标注员开启了描绘接续功能后依然会有断点。训练时断成多个实例也不是不能用但会导致模型把一条连续裂纹预测成多个离散目标后续按根数计数时错得离谱。如果这份JSON里crack的实例数明显多于有裂缝的图像数大概率就是断段问题。area字段也有讲究。COCO官方要求area是真实掩码面积由多边形或RLE计算得出而不是bbox面积的近似值。在缺陷检测场景里边缘崩裂的bbox往往比掩码大得多因为凹缺的包围盒会包进整个缺角矩形区域直接用bbox面积去度量缺陷大小会在后面做小目标筛选时选错样本。校验时就重算polygon面积和JSON里的area对一下数量级。2.3 一张缺陷图的完整JSON片段从7992张原始图里任意取一张边缘崩裂样本它在COCO JSON里大致长这样{ images: [ { id: 1, file_name: IMG_0001.jpg, width: 1920, height: 1080 } ], annotations: [ { id: 101, image_id: 1, category_id: 1, bbox: [1520, 340, 210, 45], area: 5231.5, segmentation: [ [1530, 352, 1560, 341, 1602, 350, 1650, 362, 1700, 358, 1728, 371, 1722, 395, 1695, 380, 1640, 372, 1601, 385, 1562, 370] ], iscrowd: 0 } ], categories: [ {id: 1, name: edge_chip}, {id: 2, name: hole}, {id: 3, name: crack} ] }这个片段里image_id1指向images里的id1说明这张图有一个标注。bbox的[x, y, width, height]分别是1520, 340, 210, 45表示崩缺区域在图像右半部分宽度210像素、高度45像素。segmentation里的坐标是绝对像素值注意第一个点的x、y是1530、352正好在bbox内部这是正常的。area5231.5对比bbox面积 210x459450小了不少说明这是凹陷形状的精确面积不是矩形面积。iscrowd0表示普通实例。拿到这段JSON先拿任意一个JSON viewer打开确认file_name对应的图片存在。如果ID是0开头而不是1不要慌COCO标准没有强制从1开始但你在转换时要保持映射一致。如果发现segmentation里出现了没有偶数点数量的数组比如坐标点数不是偶数那这个JSON在pycocotools解析时会直接报错属于断裂数据需要找标注软件重新导出。3. 用Python校验和统计COCO JSON先过一遍再动手训练3.1 读取COCO JSON并输出每类数量我一般拿到这个数据集先做文本层面的统计看annotations数量、每个类别有多少实例、有多少图完全没标注。这一步用裸的json和collections就可以不需要额外依赖。代码import json from collections import Counter with open(annotations/instances_tile.json, r, encodingutf-8) as f: coco json.load(f) cats {c[id]: c[name] for c in coco[categories]} print(categories:, cats) annos coco[annotations] print(total annotations:, len(annos)) cat_counter Counter(a[category_id] for a in annos) for cid, cnt in cat_counter.items(): print(f{cats[cid]}: {cnt} instances) img_with_anno set(a[image_id] for a in annos) print(images with annotations:, len(img_with_anno)) print(total images:, len(coco[images]))这段代码先读整个JSON然后从categories构建id到名称的映射。Counter统计的是每个category_id的出现次数也就是实例数。注意这里不是把同名category合并而是按id分桶因为有些JSON里存在id不连续但名称重复的情况。最后用集合去重出有标注的image_id数量如果它远小于总图数7992说明有一批图像没有缺陷可能是背景样本训练时需要注意类别不平衡或加入负样本采样。常见问题annotations为空列表或者categories列表缺项都会让后续转换直接报错。可以先跑这段代码确认三个类别都存在再往下走。如果是用labelme导出后转COCO常见一个坑是images和annotations里有大量字段缺失例如images里没有file_name而只有path所以打印出来的categories的key也要检查最好随手加一个断言。3.2 检查bbox与segmentation一致性COCO官方定义里bbox是实例最小外接矩形。实际标注工具对bbox有两种产生路径一种是从segmentation计算另一种是标注人先画框再画多边形两种路径下框和掩码可能对不上。我用下面这段代码遍历全部annotations找出bbox和第一个polygon的外接矩形之间超过5像素的实例for a in annos: seg a[segmentation][0] xs seg[0::2] ys seg[1::2] min_x, max_x min(xs), max(xs) min_y, max_y min(ys), max(ys) bbox a[bbox] if abs(bbox[0] - min_x) 5 or abs(bbox[1] - min_y) 5: print(a[id], bbox left/top mismatch) if abs((bbox[0] bbox[2]) - max_x) 5 or abs((bbox[1] bbox[3]) - max_y) 5: print(a[id], bbox right/bottom mismatch)代码里seg是第一个polygon因为有的实例有多个多边形这里先只检查第一个xs通过偶数切片拿所有x坐标ys拿所有y坐标。min和max算出polygon外接矩形的左右上下边界再与bbox的左上角和右下角比较。允许5像素容差是为了抵消手工标注和导出精度带来的微小误差。如果跑出来大量实例都触发打印就说明bbox是标注软件自动生成的宽松框而不是最小外接矩形。这种不一致在转YOLO时尤其危险。YOLO只读bbox不读segmentation如果bbox把大片背景并进来模型学到的目标框会比真实缺陷大训练初期loss虽然低但实际应用时容易出现看起来框住了其实只框住了一半崩缺的错觉。修正思路是转换时用polygon重新计算最小外接矩形我在后面4.1会给出具体公式。3.3 抽查缺陷面积分布发现小目标裂缝的视觉特征和破洞完全不同裂缝可能只有1-2像素宽但一条裂缝能拉出上千像素长度破洞通常成点状面积小但轮廓闭合。直接用segmentation算实例像素面积能直观反映这种尺度差异。用pycocotools计算面积比较稳妥因为pycocotools实现了RLE和多边形的高效计算import numpy as np from pycocotools import mask as mask_util cat_area {cid: [] for cid in cats} for a in annos: rle mask_util.frPolyObjects(a[segmentation], a[bbox] is None) if isinstance(rle, list): rle mask_util.merge(rle) m mask_util.decode(rle) cat_area[a[category_id]].append(m.sum()) for cid, areas in cat_area.items(): arr np.array(areas) print(cats[cid], median:, int(np.median(arr)), min:, int(arr.min()), max:, int(arr.max()))frPolyObjects可以把多边形列表转成RLE返回值可能是单个RLE也可能是RLE列表所以用isinstance判断并merge。decode之后得到一个布尔矩阵sum得到像素面积。这个面积是真实的mask面积不是polygon面积函数返回的浮点数两者理论上一致但性能上decode后直接sum更直观。运行结果大致长这样下表仅为示意不是该数据集真实统计类别实例数示意中位面积 px最小面积 px最大面积 pxedge_chip3120486211218500hole226028905409900crack2610145482200如果真实输出和中位数差距大比如crack的中位面积不到edge_chip的3%那么小目标漏检风险就很高。后续模型设计时考虑增加P2层检测头、使用多尺度训练或专门对crack做复制粘贴增强都比在同样输入上盲目增大模型容量更有效。看到面积分布这一步才能决定是不是需要单独处理某一类缺陷。4. 把COCO JSON转成YOLO格式并划分数据集4.1 坐标从polygon到归一化矩形YOLO系列常规训练用的标签文本每一行由五个数字组成class_id cx cy w h。cx和cy是目标框中心点相对于图像宽高的归一化坐标w和h是归一化的宽高。COCO的bbox是绝对像素的左上角x、y和宽w、高h所以转换公式是cx (x w / 2) / image_width cy (y h / 2) / image_height w_norm w / image_width h_norm h / image_height如果直接使用COCO的bbox这一步就结束了。但我在3.2里提到bbox可能不是最小外接矩形这里有一个常见决策点如果bbox来自第一个多边形的最小外接矩形那么它的值应该在合理范围内如果明显偏大建议先根据segmentation重新计算严格外接矩形再转YOLO避免引入背景噪声。反过来如果bbox比多边形还小就容易漏掉一侧的缺陷区域这其实是另一种错误同样需要在转换前发现。归一化后坐标理论上在[0,1]区间内因为标注越界、图像resize等原因可能出现负值或超过1的值所以转换脚本里要做裁剪。裁剪后类别标签不做变化YOLO的class_id直接从COCO的category_id映射到从0开始的连续整数。4.2 转换脚本与数据集划分下面是把这份COCO JSON一次性转成YOLO文本并划分90/10 train/val的完整脚本import json, os, random from collections import defaultdict with open(annotations/instances_tile.json, r, encodingutf-8) as f: coco json.load(f) img_info {im[id]: im for im in coco[images]} annos_by_img defaultdict(list) for a in coco[annotations]: annos_by_img[a[image_id]].append(a) # 如果categories的id不是从0开始这里做映射 cat_id_map {c[id]: idx for idx, c in enumerate(coco[categories])} os.makedirs(labels, exist_okTrue) for img_id, im in img_info.items(): label_lines [] for a in annos_by_img.get(img_id, []): x, y, w, h a[bbox] cx (x w / 2) / im[width] cy (y h / 2) / im[height] nw w / im[width] nh h / im[height] cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) nw min(nw, 1.0) nh min(nh, 1.0) label_lines.append( f{cat_id_map[a[category_id]]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f} ) img_stem im[file_name].rsplit(., 1)[0] with open(flabels/{img_stem}.txt, w) as f: f.write(\n.join(label_lines)) ids list(img_info.keys()) random.shuffle(ids) split int(len(ids) * 0.9) for split_name, split_ids in [(train, ids[:split]), (val, ids[split:])]: with open(f{split_name}.txt, w) as f: for img_id in split_ids: f.write(img_info[img_id][file_name] \n)转换逻辑分成两部分。前半段遍历所有image对每个实例把COCO bbox的绝对坐标转成归一化中心点和宽高再写到以图像名命名的txt文件里。后半段随机打乱图像id按90/10比例写入train.txt和val.txt这两个文件里每一行是图像的相对路径YOLO训练脚本会读取它们去找到对应图片和labels目录下的同名txt。一点关键细节rsplit(., 1)[0]处理的是图像文件名带扩展名的场景确保IMG_0001.jpg生成IMG_0001.txt如果文件名里还有目录前缀需要先os.path.basename去掉路径否则YOLO的标签路径会找不到。另外有的框架要求标签文件放在与图像同名的目录下比如labels/和images/同级具体以你使用的训练脚本为准。4.3 把COCO转YOLO前必须做的三个检查转换前最好再核对三类东西都在代码里体现过。第一category id需要从原值映射到0开始不能直接把category_id写到标签里。COCO categories的id可以是任意整数经常出现从1或0开始的不连续情况YOLO的class_id要求是0..num_classes-1的整数所以enumerate之后的index才是对的值。第二bbox坐标需要裁剪。原始标注如果跨出图像边界比如segmentation有一部分在图像外bbox的右下角可能超过width或height。裁剪后虽然目标框变小了一点但至少不会让loss在归一化坐标大于1时出现数值问题。第三空标签文件是合法的。7992张原图里如果有一部分完全没有缺陷转换后labels目录下会出现空txt文件PyTorch版本的YOLO通常能处理但会把这个样本当作纯背景参与负样本采样。如果你想要更严格地控制背景比例可以额外记录空图像id列表在datasets配置里手动过滤。下面这个表格把COCO字段和YOLO行对应起来方便你在调试阶段快速检查某一行是否写对了COCO字段YOLO行位置转换方式category_id第1个数用cat_id_map映射到0..2bbox[0], bbox[1]只参与计算计算中心点坐标bbox[2], bbox[3]第3、4个数除图像宽高得到归一化宽高segmentation不使用丢弃若bbox不紧则重新计算5. 用pycocotools在COCO格式上直接评估缺陷检测模型顺带排掉两个坑5.1 用COCOeval快速算mAP训练完的目标检测模型如果输出COCO格式的prediction JSON就能直接用pycocotools计算mAP不用自己写IoU判断。prediction每行是{image_id: 1, category_id: 8, bbox: [x,y,w,h], score: 0.87}其中bbox同样是绝对像素坐标。评估脚本from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval gt COCO(annotations/instances_tile.json) dt gt.loadRes(predictions.json) ev COCOeval(gt, dt, bbox) ev.evaluate() ev.accumulate() ev.summarize() mAP_50_95 ev.stats[0] mAP_50 ev.stats[1] print(fmAP0.5:0.95{mAP_50_95:.3f}, mAP0.5{mAP_50:.3f})gt是COCO真值对象loadRes把预测JSON加载成检测结果集合。COCOeval的第三个参数传bbox表示用矩形框评测如果你的网络输出是多边形也可以用segm。summarize会打印包括AP50、AP75、AR100在内的12项指标。这类评估有个前提gt和dt里的image_id必须落在同样的图片集合上如果验证集从7992张里随机抽取预测JSON就只包含这部分image_id两者要一致否则COCOeval会报错或结果错乱。5.2 两个影响mAP的标注问题第一个问题是类别id错位。训练时如果COCO的category_id没有映射到0..2而评估时又用原始的id模型输出的category_id和真值对不上mAP会断崖式下降。排查时直接打印dt[0][category_id]和对应真值看是否同属一个类别。第二个问题是重复标注。前面我们只检查了bbox重复但更隐蔽的是同一个缺陷被标成两个相邻多边形它们IoU超过0.7评估时模型预测一个目标真值里却有两个导致precision下降。这种问题在裂缝数据集里常见因为断开线段被标成多个实例。最后一个技巧如果模型在val上的mAP比train低很多但不震荡优先怀疑是数据划分时总图片没有严格分离导致同一块瓷砖的不同照片同时出现在train和val里。判断方法是在划分前先统计图像间的直方图相似度用opencv算感知哈希阈值50%以下视为同一场景。这个检查虽然简单但在小批量瓷砖数据上经常能救回5-10个点的mAP。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →