建筑墙壁损伤分割数据集:7820张labelme标注的毕设全流程方案
简介:面向建筑视觉检测与图像分割方向研究者、算法工程师及高校学生的建筑墙壁损伤缺陷分割数据集,以labelme标注格式提供,可用于训练和评估裂缝、剥落、锈蚀等损伤的自动识别模型,服务于建筑健康监测与智能巡检场景。资源包共1个docx文件,约2.1MB,内含数据集说明与使用指引,7820张640×640像素jpg图片均配有同名json标注文件,采用多边形框精细描绘损伤区域,覆盖涂鸦、支座、潮湿斑迹、锈蚀、剥落、泛碱、空洞区域、风化、残留模板、空腔、外露钢筋、伸缩缝、裂缝、排水、活动裂缝、接头胶带、岩窝、防护设备、水侵蚀混凝土及物体共20个类别,其中锈蚀、剥落、空腔等类别标注量较大。已有103人学习关注。读者可据此了解数据组织方式与类别分布,借助labelme 5.5.0查看编辑标注,并自行转换为mask、YOLO或COCO格式用于语义分割或实例分割实验,为建筑损伤检测研究提供准确合理的图像基础。
1. 建筑墙壁损伤分割数据集:7820 张 labelme 标注到底能撑起什么样的毕设
做建筑缺陷检测方向的同学,十有八九卡在第一步:数据从哪来。自己扛着相机去拍,拍回来还要一张张画多边形,7820 张图光标注就能耗掉一整个学期。这份建筑墙壁损伤缺陷分割数据集正好卡在这个痛点上——7820 张 640×640 的 jpg,配 7820 个 labelme 5.5.0 的 json,20 个损伤类别,全部是多边形 polygon 标注。它适合三类人:做语义分割/实例分割毕设的学生、要给建筑巡检做算法原型的从业者、以及需要一份结构完整的分割数据集方案做课程设计的读者。下面按「需求怎么立、数据怎么摸清、格式怎么转、模型怎么训、最后怎么验收」这条线,把整套方案拆开讲。
2. 需求与数据摸底:20 类损伤的分布决定了你的方案上限
拿到一份数据集,最忌讳上来就train.py一把梭。血泪经验是:先花半天把类别分布、标注质量、长尾情况摸清楚,后面能省掉一周的返工。这一章先把「这份数据能做什么、不能做什么」讲透,再谈模型。
2.1 从建筑巡检场景反推任务定义
建筑外墙和结构面的病害巡检,人工方式是搭脚手架或者无人机拍完人工看图,效率低且漏检率高。自动化方案的核心任务就是像素级分割:给定一张墙面图,输出每个像素属于哪类损伤。这份数据集的 20 个类别恰好覆盖了巡检报告里最常见的病害项——Crack(裂缝)、Spalling(剥落)、Rust(锈蚀)、Efflorescence(泛碱)、ExposedRebars(外露钢筋)、Hollowareas(空洞区域)这些是结构安全相关的重点,Graffiti(涂鸦)、Drainage(排水)、PEquipment(防护设备)则偏外观与附属设施。
任务定义上要先做一个决策:走语义分割还是实例分割。语义分割只关心「这个像素是不是裂缝」,实例分割还要区分「这是第几条裂缝」。从标注形式看,labelme 的 polygon 天然支持实例级,因为每个 shape 是独立对象;但从工程落地看,巡检报告通常只需要「有没有、在哪、多大面积」,语义分割足够,且训练成本低一半。我一般建议毕设走语义分割,把实例分割作为加分项在答辩时提一句「数据支持扩展」。
提示:
object类只有 13 个标注,基本是标注过程中的杂项兜底类,训练时建议直接过滤掉,否则会引入噪声梯度。
2.2 类别分布的长尾问题必须先量化
把每个类别的标注数拉出来看,问题立刻暴露:
| 类别 | 标注数 | 占比定位 |
|---|---|---|
| Rust 锈蚀 | 14665 | 头部 |
| Spalling 剥落 | 9849 | 头部 |
| Cavity 空腔 | 9222 | 头部 |
| Weathering 风化 | 4621 | 中段 |
| Efflorescence 泛碱 | 3956 | 中段 |
| Crack 裂缝 | 3612 | 中段 |
| Graffiti 涂鸦 | 2197 | 中段 |
| ExposedRebars 外露钢筋 | 1993 | 中段 |
| PEquipment 防护设备 | 1892 | 中段 |
| Wetspot 潮湿点 | 1657 | 中段 |
| Drainage 排水 | 1649 | 尾部 |
| Hollowareas 空洞区域 | 1538 | 尾部 |
| Bearing 支座 | 1201 | 尾部 |
| JTape 接头胶带 | 1076 | 尾部 |
| Restformwork 残留模板 | 1019 | 尾部 |
| Spalling 之外的 Cavity 已列 | — | — |
| Rockpocket 岩窝 | 616 | 长尾 |
| EJoint 伸缩缝 | 506 | 长尾 |
| ACrack 活动裂缝 | 423 | 长尾 |
| WConccor 水侵蚀混凝土 | 407 | 长尾 |
| object 物体 | 13 | 噪声 |
头部 Rust 是尾部 ACrack 的 34 倍,这个比例意味着:如果直接用交叉熵训练,模型会倾向于把所有像素预测成 Rust 和背景,尾部类别的 IoU 会趋近于 0。方案上必须提前设计应对——要么在损失函数里加权,要么在采样时做类别平衡,要么在评估时单独报告尾部类别。这一步不做,后面调参全是玄学。
2.3 标注质量抽检与清洗规则
labelme 5.5.0 的 json 结构里,每个 shape 有label、points、shape_type。抽检时重点看三件事:一是shape_type是否全是polygon(如果有rectangle混入,转 mask 时会出问题);二是 points 是否闭合合理(少于 3 个点的多边形要丢弃);三是同一区域是否被重复标注成两个类别(比如裂缝和活动裂缝 ACrack 容易混)。
清洗脚本可以这样写,跑一遍全量 json,输出问题清单:
import json, os, glob def audit_labelme(json_dir): issues = [] for fp in glob.glob(os.path.join(json_dir, "*.json")): with open(fp, "r", encoding="utf-8") as f: data = json.load(f) for i, shape in enumerate(data.get("shapes", [])): pts = shape.get("points", []) # 规则1:多边形至少3个点 if shape.get("shape_type") != "polygon" or len(pts) < 3: issues.append((fp, i, "bad_shape", shape.get("label"))) # 规则2:坐标越界检查(图片640x640) for x, y in pts: if not (0 <= x <= 640 and 0 <= y <= 640): issues.append((fp, i, "out_of_bound", shape.get("label"))) break return issues if __name__ == "__main__": problems = audit_labelme("./jsons") print(f"问题标注数: {len(problems)}") for p in problems[:20]: print(p)这段脚本做的是「体检」而不是「治疗」:bad_shape说明该 shape 无法转成有效 mask,训练前要剔除;out_of_bound说明坐标超出 640×640,可能是标注时图片被缩放导致的,需要人工复核。跑完拿到问题清单,再决定是批量修复还是直接丢弃对应图片。通常 7820 张里问题样本在几十张量级,丢弃对总量影响可忽略。
3. 格式转换方案:从 labelme json 到 mask / YOLO / COCO 的三条路径
数据集只给 jpg + json,不含 mask,这是最容易被低估的工作量。转换做错,后面训练全白搭。这一章把三条主流路径讲清楚,并给出可直接复用的转换代码。
3.1 语义分割路径:json 转单通道 mask
语义分割需要的是每张图对应一张 mask,像素值等于类别 id。转换核心是用PIL.ImageDraw.polygon把每个 shape 填充到对应类别 id 上。注意两个坑:一是填充顺序,后填的会覆盖先填的,重叠区域要以「更具体的类别优先」为原则排序;二是背景设为 0,类别 id 从 1 开始,避免和背景混淆。
import json, os import numpy as np from PIL import Image, ImageDraw # 20类映射,背景0,object类不参与训练可跳过 CLASS_MAP = { "Graffiti": 1, "Bearing": 2, "Wetspot": 3, "Rust": 4, "Spalling": 5, "Efflorescence": 6, "Hollowareas": 7, "Weathering": 8, "Restformwork": 9, "Cavity": 10, "ExposedRebars": 11, "EJoint": 12, "Crack": 13, "Drainage": 14, "ACrack": 15, "JTape": 16, "Rockpocket": 17, "PEquipment": 18, "WConccor": 19, # object 不映射,视为背景 } def json_to_mask(json_path, out_path, size=(640, 640)): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) mask = Image.new("L", size, 0) # 0 = 背景 draw = ImageDraw.Draw(mask) # 按类别id升序填充,保证重叠时高id覆盖低id(可按业务调整) shapes = sorted(data["shapes"], key=lambda s: CLASS_MAP.get(s["label"], 0)) for shape in shapes: cid = CLASS_MAP.get(shape["label"]) if cid is None: continue pts = [tuple(p) for p in shape["points"]] if len(pts) >= 3: draw.polygon(pts, fill=cid) mask.save(out_path) if __name__ == "__main__": os.makedirs("./masks", exist_ok=True) for fp in os.listdir("./jsons"): if fp.endswith(".json"): json_to_mask(f"./jsons/{fp}", f"./masks/{fp.replace('.json', '.png')}")CLASS_MAP是整条流水线的「字典」,训练、评估、可视化都必须引用同一份,建议单独存成classes.json避免多处硬编码不一致。size固定 640×640 与原始图片一致,不要在这里做缩放,缩放留给训练时的 data augmentation。填充顺序按类别 id 排序是一种简化策略,如果业务上「裂缝」应该覆盖「风化」,就把 Crack 的 id 调大或者单独写优先级字段。
3.2 实例分割路径:转 COCO 格式
如果要做实例分割,COCO 是事实标准。转换要点:每个 shape 变成一个 annotation,segmentation存多边形点列表,bbox由多边形外接矩形算出,category_id与 CLASS_MAP 对齐。COCO 的iscrowd设为 0,area用多边形面积。
import json, os from shapely.geometry import Polygon def build_coco(json_dir, out_file): coco = {"images": [], "annotations": [], "categories": []} for name, cid in CLASS_MAP.items(): coco["categories"].append({"id": cid, "name": name, "supercategory": "defect"}) ann_id = 1 for img_id, fp in enumerate(sorted(os.listdir(json_dir)), start=1): if not fp.endswith(".json"): continue with open(os.path.join(json_dir, fp), "r", encoding="utf-8") as f: data = json.load(f) coco["images"].append({ "id": img_id, "file_name": fp.replace(".json", ".jpg"), "width": 640, "height": 640 }) for shape in data["shapes"]: cid = CLASS_MAP.get(shape["label"]) pts = shape["points"] if cid is None or len(pts) < 3: continue poly = Polygon(pts) if not poly.is_valid: poly = poly.buffer(0) # 自相交多边形修复 xs = [p[0] for p in pts]; ys = [p[1] for p in pts] coco["annotations"].append({ "id": ann_id, "image_id": img_id, "category_id": cid, "segmentation": [[c for p in pts for c in p]], "bbox": [min(xs), min(ys), max(xs)-min(xs), max(ys)-min(ys)], "area": poly.area, "iscrowd": 0 }) ann_id += 1 with open(out_file, "w", encoding="utf-8") as f: json.dump(coco, f, ensure_ascii=False)poly.buffer(0)是处理自相交多边形的常用手段,labelme 手画的多边形偶尔会出现「8 字形」,直接算面积会报错或得到负值。segmentation的扁平化写法[x1,y1,x2,y2,...]是 COCO 对单多边形的标准格式,别写成嵌套列表。
3.3 YOLO 路径与三条路径的取舍
YOLO 分割格式(YOLOv8-seg)要求每张图一个 txt,每行是class_id x1 y1 x2 y2 ...,坐标归一化到 0~1。转换逻辑与 COCO 类似,只是输出形态不同。三条路径怎么选,给一张对照表:
| 路径 | 适用任务 | 训练框架 | 转换成本 | 毕设推荐度 |
|---|---|---|---|---|
| mask 单通道 | 语义分割 | U-Net / DeepLab / SegFormer | 低 | 高,最稳 |
| COCO | 实例分割 | Mask R-CNN / YOLOv8-seg | 中 | 中,加分项 |
| YOLO txt | 实例分割 | YOLOv8-seg / YOLOv11-seg | 中 | 中,部署友好 |
我的建议是:主线走 mask + 语义分割,保证能出结果;如果时间充裕,再补一条 COCO 做实例分割对比。两条线共用同一份 CLASS_MAP,评估指标口径一致,答辩时能讲出「同一数据两种任务范式」的深度。
4. 训练方案设计:类别不平衡下的分割模型落地
数据准备好了,接下来是模型。这一章不讲模型原理,讲的是「在这份 20 类、长尾严重、640×640 的数据上,怎么配出一套能跑出合理结果的训练方案」。
4.1 基线模型选型与输入尺寸策略
640×640 的输入对分割模型是友好的——不算大,单卡 8G 显存能跑 batch size 8 左右。基线我一般选两个:U-Net(ResNet34 或 EfficientNet-B0 编码器)作为「能跑通、好解释」的保底方案,SegFormer-B0 作为「指标更好看」的进阶方案。两者都在 640 输入下工作良好,不需要额外 resize。
为什么不建议一上来就上大模型?7820 张图对分割任务属于中等偏小规模,大模型(如 SegFormer-B5)容易过拟合,且训练时间长,毕设周期内调参空间被压缩。先用 B0 跑出 baseline IoU,再决定要不要升级,这是更稳的节奏。
数据划分建议 8:1:1,即训练 6256、验证 782、测试 782。划分时按图片随机分,但要检查每个类别的标注在三个子集里的分布是否大致一致——尾部类别(ACrack 423 个标注)如果测试集里一个都没有,测试指标就没意义。可以写个脚本统计划分后各类别标注数,偏差超过 20% 就重新分。
4.2 损失函数与类别加权的具体配置
针对长尾,方案是「交叉熵 + Dice」组合损失,并对交叉熵做类别加权。权重按1 / sqrt(freq)计算,比1/freq温和,避免尾部类别权重爆炸导致训练不稳定。
import numpy as np import torch import torch.nn as nn # 各类别标注数(来自数据集统计,object已排除) counts = np.array([14665, 9849, 9222, 4621, 3956, 3612, 2197, 1993, 1892, 1657, 1649, 1538, 1201, 1076, 1019, 616, 506, 423, 407], dtype=np.float32) # 1/sqrt 加权,再归一化到均值1 weights = 1.0 / np.sqrt(counts) weights = weights / weights.mean() class_weights = torch.tensor(weights, dtype=torch.float32) ce_loss = nn.CrossEntropyLoss(weight=class_weights, ignore_index=255) def dice_loss(pred, target, smooth=1.0): # pred: [B, C, H, W] logits; target: [B, H, W] long pred = torch.softmax(pred, dim=1) target_onehot = torch.nn.functional.one_hot(target, pred.shape[1]).permute(0, 3, 1, 2).float() inter = (pred * target_onehot).sum(dim=(2, 3)) union = pred.sum(dim=(2, 3)) + target_onehot.sum(dim=(2, 3)) dice = (2 * inter + smooth) / (union + smooth) return 1 - dice.mean() def total_loss(pred, target): return ce_loss(pred, target) + dice_loss(pred, target)ignore_index=255是为边界像素预留的——如果后续做边界忽略训练,把边界像素标成 255 即可。weights归一化到均值 1 是为了让学习率不用跟着权重整体缩放重新调。Dice 损失负责拉高重叠度,CE 负责像素级分类,两者相加是分割任务里最稳的组合之一。
4.3 训练配置与显存控制
优化器用 AdamW,lr 初始 1e-4,weight decay 1e-4,cosine 退火到 1e-6,训练 60~80 epoch。batch size 8,配合 AMP 混合精度,8G 显存能稳住。数据增强用水平翻转、随机裁剪(裁到 512 再 resize 回 640)、亮度对比度扰动、轻微旋转(±10°)。注意:建筑病害有方向语义(比如裂缝走向),旋转幅度不要太大,否则会破坏「竖向裂缝」这类先验。
验证指标用 mIoU 和每类 IoU,每 5 个 epoch 存一次 checkpoint,保留验证 mIoU 最高的那个。训练日志里单独记录尾部 5 类(Rockpocket、EJoint、ACrack、WConccor、Restformwork)的 IoU,这是判断加权策略是否生效的关键信号。
5. 避坑与排查:这份数据集最容易翻车的五个点
这一章是踩坑记录,每条按「现象 → 原因 → 解决」写,都是这类 labelme 数据集转分割任务时高频出现的问题。
5.1 转出来的 mask 全是背景
现象:训练 loss 不降,预测图全黑,mIoU 接近 0。原因:json 里shapes的label字段与 CLASS_MAP 的 key 大小写或空格不一致,get全部返回 None,所有 shape 被跳过。解决:转换前先跑一遍set(shape["label"] for ...)打印实际标签集合,与 CLASS_MAP 做差集比对。这份数据集的标签是驼峰命名(如ExposedRebars、WConccor),手敲映射表极易写错,建议直接从 json 里程序化提取标签生成映射表初稿。
5.2 训练到一半 loss 变 NaN
现象:前 10 个 epoch 正常,之后 loss 突然 NaN。原因:Dice 损失在某个 batch 里某类完全没出现,union为 0,虽然加了 smooth 但混合精度下仍可能数值溢出;或者学习率偏高导致梯度爆炸。解决:Dice 的 smooth 从 1.0 提到 1e-5 量级不够,建议保持 1.0 但把 AMP 的 GradScaler 初始 scale 调低,同时把 lr 降到 5e-5 重跑。另一个排查点是检查 mask 里是否有超出类别数的像素值(比如转换脚本 bug 写入了 20 以上的 id),用np.unique(mask)抽查几张。
5.3 验证集指标虚高但测试集崩盘
现象:验证 mIoU 0.6+,换测试集掉到 0.3。原因:划分时没做分层,验证集恰好集中了头部类别(Rust、Spalling),尾部类别几乎没进验证集。解决:按类别做分层划分,保证每个类别在验证/测试集里至少有 10% 的标注量;尾部类别样本太少时,用「按图片分组、组内含尾部类别则整组进验证」的策略。划分完必须打印三个子集的类别分布表核对。
5.4 可视化时颜色对不上类别
现象:预测图和 mask 叠在一起,颜色和类别图例完全错位。原因:可视化脚本用的调色板顺序与 CLASS_MAP 的 id 顺序不一致,或者 mask 存的是 0~19 而训练用的是 1~20。解决:把「类别名 → id → RGB」做成唯一一份配置,训练、转换、可视化全部 import 同一份,禁止各写各的。这类问题不报错,但会让人在答辩现场被问住。
5.5 显存够但训练速度异常慢
现象:batch size 8 能跑,但每个 epoch 要两三个小时。原因:数据加载是瓶颈——每张图实时读 json 转 mask,IO 和 CPU 解码占满。解决:转换阶段就把 mask 落盘成 png,训练时直接读 png;DataLoader 的num_workers设到 CPU 核数的 70% 左右,pin_memory=True,persistent_workers=True。这一步做完,同样配置下 epoch 时间通常能砍掉一半以上。
6. 验收与答辩:用一张指标表把方案讲圆
最后落到验收。毕设或交付方案的验收,不是「模型跑起来了」就算完,而是要有一套可复现、可解释的评估口径。我一般会准备一张「总表 + 分表」:总表给 mIoU、像素准确率、各类平均 Dice;分表按头部/中段/尾部三档分别报告 IoU,并附上混淆矩阵里最容易混的三对类别。
评估脚本的核心是逐类累计混淆矩阵,最后一次性算 IoU,避免逐 batch 平均带来的偏差:
import numpy as np def compute_iou(conf_matrix): # conf_matrix: [C, C],行是真值,列是预测 ious = {} for c in range(conf_matrix.shape[0]): tp = conf_matrix[c, c] fp = conf_matrix[:, c].sum() - tp fn = conf_matrix[c, :].sum() - tp ious[c] = tp / (tp + fp + fn + 1e-6) miou = np.mean(list(ious.values())) return miou, ious答辩时怎么讲这份方案,给几个具体建议。第一,开场不要讲模型,先讲「建筑巡检人工成本高、漏检率高」这个需求,再用类别分布表说明「这份数据的难点在长尾」,让评委先认可问题定义。第二,讲转换流水线时,把「json → mask → 训练 → 评估」画成一条线,强调 CLASS_MAP 单一来源这个工程细节,这是区分「跑通」和「做对」的地方。第三,指标汇报一定要带尾部类别,主动说「ACrack 只有 423 个标注,IoU 0.2 是数据量限制而非模型问题」,比被问出来强。第四,准备一个失败案例的可视化——比如某张图把 Wetspot 误判成 Rust,分析原因是两者颜色接近、标注边界模糊,这体现的是分析能力。
验收标准可以量化成这样:整体 mIoU ≥ 0.45(20 类长尾数据下这是合理线),头部 5 类 IoU ≥ 0.6,尾部 5 类 IoU ≥ 0.15,推理单张 640×640 在单卡上 ≤ 50ms。达不到就回到第 4 章调损失权重和增强策略,而不是换模型。
从那以后我每次接分割数据集的项目,都强制先跑一遍类别分布统计和标注抽检,再动一行训练代码——这份 7820 张、20 类的数据集尤其如此,长尾和格式转换这两关过了,后面的模型调参才有意义。希望帮到你。
本文还有配套的精品资源,点击获取