☰
建筑墙壁损伤分割数据集:7820张labelme标注的格式转换与实战解析
2026/10/10 5:04:35 网站建设 项目流程

简介:这份建筑墙壁损伤缺陷分割数据集面向计算机视觉与机器学习研究者、建筑结构健康监测方向的开发者,用于训练和评估图像分割算法,实现剥落、锈蚀、裂缝、排水等损伤的自动识别与分类。资源包共1个docx文件,约2.1MB,文档内整理了数据集说明与标注信息,实际数据为7820张640×640像素的jpg图片及一一对应的json标注文件,采用labelme 5.5.0多边形框标注,覆盖涂鸦、支座、潮湿斑迹、锈蚀、剥落、泛碱、空洞区域、风化、残留模板、空腔、外露钢筋、伸缩缝、裂缝、排水、活动裂缝、接头胶带、岩窝、防护设备、水侵蚀混凝土和物体共20个类别,其中锈蚀、剥落、空腔等类别标注框数较多。已有102人学习关注。读者可据此构建语义分割或实例分割模型,自行将json转换为mask、YOLO或COCO格式,用于建筑安全检测与维护的自动化研究,减少人工巡检依赖。

1. 建筑墙壁损伤分割数据集:7820 张 labelme 标注到底能跑出什么

拿到一份标注好的建筑缺陷数据集,第一反应通常不是「太好了」,而是「这玩意儿能不能直接喂给模型」。这份建筑墙壁损伤缺陷分割数据集给的是 7820 张 640x640 的 jpg 图片,外加一一对应的 7820 个 labelme json 文件,20 个类别,多边形标注。它解决的不是「有没有数据」的问题,而是「有没有带精细轮廓标注的真实建筑场景数据」的问题。适合做语义分割、实例分割的算法验证,也适合做缺陷检测的预研。但注意,它只给 json,不给 mask,也不给 yolo 或 coco 格式,转换这一步得自己走。下面按「先看清数据长什么样、再动手转格式、最后避坑」的顺序拆开讲。

2. 先摸清 labelme json 的骨架:20 类标注怎么读、怎么统计

2.1 json 里到底存了什么

labelme 5.5.0 导出的 json,核心结构就三块:imageData(有些版本会塞 base64,这份数据集大概率是 null)、imagePath、shapes。shapes是个列表,每个元素包含label、points、shape_type、flags。这份数据集用的是 polygon,所以shape_type是polygon,points是一串[x, y]坐标。20 个类别名直接写在label字段里,大小写敏感,比如ExposedRebars不能写成exposedrebar。

先跑一段统计脚本,把每个类别的多边形数量和图片分布摸清楚。这一步不做,后面转格式时类别对不上会非常难受。

import json import os from collections import Counter, defaultdict json_dir = "./annotations" # 换成你的 json 所在目录 label_counter = Counter() img_per_label = defaultdict(set) for fname in os.listdir(json_dir): if not fname.endswith(".json"): continue with open(os.path.join(json_dir, fname), "r", encoding="utf-8") as f: data = json.load(f) for shape in data.get("shapes", []): label = shape["label"] label_counter[label] += 1 img_per_label[label].add(fname) for label, cnt in label_counter.most_common(): print(f"{label}: polygons={cnt}, images={len(img_per_label[label])}")

逻辑说明:遍历所有 json,累加每个 label 出现的次数,同时用 set 记录出现过该 label 的图片文件名,避免同一张图多个同类多边形被重复计图。参数上,json_dir指向你的标注目录;如果 json 和图片混在一起,脚本照样能跑,因为只筛.json。

跑完你会看到 Rust 有 14665 个多边形、Spalling 有 9849 个、Cavity 有 9222 个,而 object 只有 13 个。这个分布直接决定了后面训练时类别不平衡怎么处理。

2.2 类别分布对训练策略的影响

把 20 类的多边形数量拉出来看,Rust、Spalling、Cavity 三个类占了绝大多数,而 object、ACrack、EJoint、Rockpocket 这些都在几百甚至几十的量级。object 只有 13 个多边形,如果做实例分割,这个类基本训不动;如果做语义分割,它可能被背景淹没。

常见做法是:先按大类跑一版 baseline,把 Rust、Spalling、Cavity、Weathering、Efflorescence 这几个量大的类训到收敛,再决定要不要为小类做重采样或加权重。不要一上来就 20 类全开,否则 loss 会被大类主导,小类的梯度信号几乎被吃掉。

另外注意,这份数据集的标注是 polygon 而不是 bbox,所以做目标检测需要先转 bbox,做分割则可以直接用 polygon。如果你只想做缺陷分类,那 json 里的 label 就够用,但会浪费掉位置信息。

3. 把 labelme json 转成 mask 和 yolo 格式:两条路线的具体命令

3.1 转语义分割 mask:用 labelme 自带工具还是自己写

labelme 提供了labelme_json_to_dataset命令行工具,能把单个 json 转成 mask 图。但这份数据集有 7820 个 json,逐个调用效率太低。常见做法是写一个批量脚本,直接解析 json 里的 polygon,用 PIL 的 ImageDraw 填充到单通道 mask 上。

import json import os import numpy as np from PIL import Image, ImageDraw # 20 类,按你的实际顺序建立索引,0 留给背景 CLASSES = ["Graffiti","Bearing","Wetspot","Rust","Spalling","Efflorescence", "Hollowareas","Weathering","Restformwork","Cavity","ExposedRebars", "EJoint","Crack","Drainage","ACrack","JTape","Rockpocket", "PEquipment","WConccor","object"] CLASS_TO_ID = {c: i + 1 for i, c in enumerate(CLASSES)} json_dir = "./annotations" img_dir = "./images" mask_dir = "./masks" os.makedirs(mask_dir, exist_ok=True) for fname in os.listdir(json_dir): if not fname.endswith(".json"): continue with open(os.path.join(json_dir, fname), "r", encoding="utf-8") as f: data = json.load(f) h, w = 640, 640 mask = Image.new("L", (w, h), 0) draw = ImageDraw.Draw(mask) for shape in data["shapes"]: label = shape["label"] if label not in CLASS_TO_ID: continue pts = [tuple(p) for p in shape["points"]] draw.polygon(pts, fill=CLASS_TO_ID[label]) mask.save(os.path.join(mask_dir, fname.replace(".json", ".png")))

逻辑说明:CLASS_TO_ID把类别名映射到 1 到 20,0 是背景。draw.polygon按 points 填充,后画的类别会覆盖先画的,所以如果同一区域有重叠标注,顺序会影响结果。参数上,h, w固定 640,因为数据集图片就是 640x640;如果你的图片尺寸不一致,要从 json 里读imageHeight和imageWidth。

这个脚本跑完,每个 json 对应一张 png mask,像素值就是类别 id。语义分割训练时直接读这对 (jpg, png) 即可。

3.2 转 yolo 实例分割格式:txt 里的归一化坐标

YOLO 的实例分割格式要求每个图片对应一个 txt,每行是class_id x1 y1 x2 y2 ...,坐标归一化到 0 到 1。注意 YOLO 的 class_id 从 0 开始,所以这里不要加 1。

import json import os CLASSES = ["Graffiti","Bearing","Wetspot","Rust","Spalling","Efflorescence", "Hollowareas","Weathering","Restformwork","Cavity","ExposedRebars", "EJoint","Crack","Drainage","ACrack","JTape","Rockpocket", "PEquipment","WConccor","object"] CLASS_TO_ID = {c: i for i, c in enumerate(CLASSES)} json_dir = "./annotations" out_dir = "./labels" os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(json_dir): if not fname.endswith(".json"): continue with open(os.path.join(json_dir, fname), "r", encoding="utf-8") as f: data = json.load(f) w = data.get("imageWidth", 640) h = data.get("imageHeight", 640) lines = [] for shape in data["shapes"]: label = shape["label"] if label not in CLASS_TO_ID: continue pts = shape["points"] norm = [] for x, y in pts: norm.append(f"{x / w:.6f}") norm.append(f"{y / h:.6f}") lines.append(f"{CLASS_TO_ID[label]} " + " ".join(norm)) with open(os.path.join(out_dir, fname.replace(".json", ".txt")), "w") as f: f.write("\n".join(lines))

逻辑说明:imageWidth和imageHeight优先从 json 读,读不到才用 640 兜底。归一化保留 6 位小数,足够精度。每行一个多边形,YOLO 训练时直接读这个 txt 做实例分割。注意,YOLO 的 polygon 标签要求点数一致或者用变长处理,不同版本的 ultralytics 对变长支持不同,建议先确认你的 YOLO 版本。

3.3 转 coco 格式:一个 json 装下所有标注

COCO 格式适合做实例分割训练,尤其是用 detectron2 或 mmdetection 时。核心是把所有图片的images、annotations、categories三个列表拼到一个 json 里。annotations里的segmentation存 polygon 坐标,category_id从 1 开始。

import json import os CLASSES = ["Graffiti","Bearing","Wetspot","Rust","Spalling","Efflorescence", "Hollowareas","Weathering","Restformwork","Cavity","ExposedRebars", "EJoint","Crack","Drainage","ACrack","JTape","Rockpocket", "PEquipment","WConccor","object"] coco = {"images": [], "annotations": [], "categories": []} for i, name in enumerate(CLASSES): coco["categories"].append({"id": i + 1, "name": name, "supercategory": "defect"}) json_dir = "./annotations" ann_id = 1 for img_id, fname in enumerate(os.listdir(json_dir)): if not fname.endswith(".json"): continue with open(os.path.join(json_dir, fname), "r", encoding="utf-8") as f: data = json.load(f) w = data.get("imageWidth", 640) h = data.get("imageHeight", 640) coco["images"].append({ "id": img_id, "file_name": fname.replace(".json", ".jpg"), "width": w, "height": h }) for shape in data["shapes"]: label = shape["label"] if label not in CLASSES: continue pts = shape["points"] xs = [p[0] for p in pts] ys = [p[1] for p in pts] seg = [coord for p in pts for coord in p] coco["annotations"].append({ "id": ann_id, "image_id": img_id, "category_id": CLASSES.index(label) + 1, "segmentation": [seg], "area": (max(xs) - min(xs)) * (max(ys) - min(ys)), "bbox": [min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)], "iscrowd": 0 }) ann_id += 1 with open("coco_annotations.json", "w") as f: json.dump(coco, f)

逻辑说明:segmentation是列表套列表,每个子列表是一个多边形的扁平坐标。area这里用外接矩形面积近似,严格来说应该用多边形面积,但很多训练框架不依赖这个字段。bbox是外接矩形,做检测时用得上。跑完得到一个 coco_annotations.json,直接喂给 mmdetection 的 coco 格式数据集即可。

4. 避坑与排查:json 转 mask 时最容易翻车的五件事

4.1 现象:mask 全黑,一个类别都没画上

原因通常是points的坐标类型不对。labelme 导出的 points 是浮点数列表,但有些版本会存成字符串或者嵌套结构。另一个可能是shape_type不是polygon,而是rectangle或circle,这时points只有两个点,draw.polygon画不出东西。

解决:先打印一个 json 的shapes[0]看结构。如果是 rectangle,要用draw.rectangle;如果是 circle,要用draw.ellipse。这份数据集明确说是 polygon,但如果你混入了其他来源的 json,就要做类型判断。

4.2 现象:类别 id 对不上,训练时 loss 直接 NaN

原因多半是类别名大小写不一致,或者 json 里出现了 CLASSES 列表之外的 label。比如object和Object会被当成两个类,而你的映射表里只有一个。

解决:在转换脚本里加一个unknown_labels集合,把所有不在 CLASSES 里的 label 收集起来打印。跑一遍全量数据,确认没有漏网之鱼。如果有,要么补进 CLASSES,要么统一改名。

4.3 现象:mask 边缘有锯齿,小目标分割效果差

原因是用 PIL 的draw.polygon填充时,默认不做抗锯齿,而且 polygon 的顶点是整数坐标,浮点坐标被截断。对于裂缝这种细长目标,截断会导致标注区域偏移。

解决:把坐标四舍五入到整数后再画,或者用ImageDraw.Draw(mask, "L")配合更高分辨率的中间 mask 再降采样。另一个办法是直接用cv2.fillPoly,它对浮点坐标的处理更稳。

4.4 现象:YOLO 训练报错「label format invalid」

原因是 txt 里的坐标没有归一化,或者归一化后超出了 0 到 1 的范围。labelme 的 points 是像素坐标,如果图片实际尺寸不是 640x640,而 json 里的imageWidth又是错的,归一化就会出界。

解决:在写 txt 之前加一个 clamp,把坐标限制在 0 到 1 之间。同时检查 json 的imageWidth和imageHeight是否和 jpg 实际尺寸一致。不一致时以 jpg 实际尺寸为准。

4.5 现象:coco 格式加载后类别数不对

原因是categories的 id 从 0 开始,而 COCO 标准要求从 1 开始。另一个可能是annotations里的category_id和categories里的id没有对齐。

解决:统一用i + 1作为 category id,并且在生成annotations时用同一个映射。加载后打印len(dataset.categories)确认是 20 还是 21。

5. 进阶用法:用 20 类标注做缺陷严重度分级与主动学习

5.1 从分割结果反推缺陷面积占比

语义分割 mask 拿到后,除了算 mIoU,还可以直接统计每个类别的像素占比。这个占比在建筑检测里比单纯的类别存在更有意义,因为它能反映缺陷的严重程度。比如 Rust 的像素占比超过 15% 就可以标记为重度锈蚀,低于 5% 是轻度。

import numpy as np from PIL import Image mask = np.array(Image.open("masks/example.png")) total = mask.size for cls_id in range(1, 21): ratio = (mask == cls_id).sum() / total if ratio > 0: print(f"class {cls_id}: {ratio:.4f}")

逻辑说明:mask == cls_id生成布尔数组,sum()得到像素数,除以total得到占比。这个指标可以直接接到检测报告里,比单纯输出「有裂缝」更有决策价值。

5.2 用低置信度样本做主动学习

20 类里小样本类别多,与其盲目加数据,不如用模型跑一遍未标注图片,挑出置信度低的样本优先标注。具体做法是:先用现有 7820 张训一个 baseline,然后在新的建筑巡检图片上推理,把预测熵高的图片筛出来。这些图片往往包含模型没见过的缺陷形态或光照条件,标注价值最高。

我一般会设一个阈值,比如分类头最大 softmax 概率低于 0.6 就进候选池。然后人工过一遍,把真正有信息量的挑出来补标。这样一轮下来,小类别的召回率通常能提几个点,比随机加数据划算。

5.3 验证转换是否正确的一个笨办法

转完 mask 后,不要直接开训。先随机抽 20 张,把原图、mask、json 里的 polygon 画在一起做可视化。用 PIL 把 mask 转成彩色叠加到原图上,再用ImageDraw把 polygon 的边描出来。如果边和 mask 边界重合,说明转换没问题;如果偏移,就是坐标缩放或截断的问题。

import json from PIL import Image, ImageDraw img = Image.open("images/example.jpg").convert("RGB") mask = Image.open("masks/example.png") overlay = Image.new("RGB", img.size, (0, 0, 0)) overlay.paste(img, (0, 0), mask) draw = ImageDraw.Draw(overlay) with open("annotations/example.json") as f: data = json.load(f) for shape in data["shapes"]: pts = [tuple(p) for p in shape["points"]] draw.line(pts + [pts[0]], fill=(255, 0, 0), width=2) overlay.save("check_overlay.png")

逻辑说明:paste的第三个参数是 mask,这里用 mask 做透明度通道,但 mask 是单通道 L 模式,直接 paste 会有点问题。更稳的做法是把 mask 转成 RGBA,或者用Image.blend。这个脚本的目的是肉眼检查,不追求完美叠加。

从那以后我每次转完格式都强制走一遍可视化抽检,宁可花十分钟看图,也不愿意训到一半发现 mask 全错。希望帮到你。

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

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

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

立即咨询