建筑墙壁损伤分割数据集:7820张labelme格式与20类别实战解析
简介这份建筑墙壁损伤缺陷分割数据集面向计算机视觉与机器学习方向的研究者、算法工程师及建筑安全检测从业者用于训练和评估图像分割模型实现剥落、锈蚀、裂缝、排水等墙体病害的自动识别与分类。资源包共1个docx文件约2.1MB内含数据集说明与标注规范实际数据以labelme格式组织包含7820张640×640像素的jpg图片及一一对应的json标注文件采用多边形框精细描绘损伤区域覆盖涂鸦、支座、潮湿斑迹、锈蚀、剥落、泛碱、空洞区域、风化、残留模板、空腔、外露钢筋、伸缩缝、裂缝、排水、活动裂缝、接头胶带、岩窝、防护设备、水侵蚀混凝土和物体共20个类别其中锈蚀、剥落、空腔等类别标注框数较多。目前已有102人学习关注。读者可据此构建语义或实例分割流程自行将json转换为mask、YOLO或COCO格式用于建筑健康监测与自动化巡检研究减少人工检查依赖。1. 建筑墙壁损伤缺陷分割数据集7820 张 labelme 格式、20 类别的真实用法拿到一个标注好的建筑墙壁损伤数据集第一反应往往不是训练模型而是先搞清楚它到底能不能直接用。7820 张图像、20 个类别、labelme 的 json 标注格式这三个数字背后藏着不少门道。labelme 是标注工具不是训练框架它产出的 json 文件需要转成掩码或者 COCO 格式才能喂给分割网络。20 个类别意味着这不是简单的二分类裂缝检测而是要把剥落、渗水、霉斑、钢筋外露这些不同损伤分开。建筑墙壁损伤分割的落地场景很明确老旧小区巡检、桥梁隧道衬砌检测、历史建筑保护评估都需要从一张照片里自动圈出损伤区域并判断类型。适合谁做智慧工地、结构健康监测、无人机巡检的团队以及想入门语义分割但缺数据的研究生。这篇笔记不讲空话直接拆解从 labelme json 到可训练掩码的完整链路把参数、脚本和踩过的坑都摆出来。2. labelme 格式拆解与 20 类别标签体系设计2.1 labelme json 里到底存了什么labelme 标注一张图会生成一个同名 json 文件核心字段是shapes数组和imageData。shapes里每个对象包含label、points、shape_type和group_id。对于墙壁损伤分割shape_type通常是polygonpoints是一串[x, y]坐标。imageData是 base64 编码的原图有些版本会省略以减小文件体积。7820 张图对应 7820 个 json如果每张图平均 3 到 5 个损伤区域总多边形数量在 2 万到 4 万之间。这里第一个坑就来了labelme 的坐标是相对于原图尺寸的绝对像素值但不同标注员可能用了不同分辨率的原因如果原图被缩放后再标注坐标就对不上了。所以拿到数据集第一件事是校验 json 里的imageWidth和imageHeight是否和实际图片一致。import json import os from PIL import Image def validate_labelme_json(json_path, image_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_name data[imagePath] img_path os.path.join(image_dir, img_name) if not os.path.exists(img_path): return f图片缺失: {img_name} with Image.open(img_path) as img: w, h img.size if data[imageWidth] ! w or data[imageHeight] ! h: return f尺寸不匹配: {img_name}, json{data[imageWidth]}x{data[imageHeight]}, 实际{w}x{h} labels [s[label] for s in data[shapes]] return labels # 批量校验 json_dir ./annotations image_dir ./images for jf in os.listdir(json_dir): if jf.endswith(.json): result validate_labelme_json(os.path.join(json_dir, jf), image_dir) if isinstance(result, str): print(result)这段脚本遍历所有 json检查图片是否存在以及尺寸是否一致。参数说明json_dir是标注文件目录image_dir是原图目录。如果输出大量尺寸不匹配说明标注时图片被预处理过需要统一还原或重新标注。另一个常见问题是imageData字段占空间7820 张图的 json 加起来可能好几个 G建议用labelme_export_json时加--nodata参数去掉 base64 图像数据只保留标注坐标。2.2 20 个类别怎么映射成训练标签20 个类别如果直接做多通道二分类显存吃不消如果做多分类语义分割需要把每个 polygon 转成单通道索引图像素值 0 到 19 对应不同损伤背景为 255 或 0。常见做法是维护一个label_name_to_id字典按字母序或按损伤严重程度排序。比如类别名称建议 ID说明crack1裂缝spalling2剥落efflorescence3泛碱mold4霉斑rebar_exposure5钢筋外露.........background0背景排序时注意把出现频率高的类别放在前面方便后续做类别平衡。如果某些类别样本极少比如少于 50 个 polygon训练时要么过采样要么直接合并到相近类别。我一般会先统计每个类别的 polygon 数量和像素占比再决定是否合并。import json from collections import defaultdict label_counts defaultdict(int) label_pixels defaultdict(int) for jf in os.listdir(json_dir): if not jf.endswith(.json): continue with open(os.path.join(json_dir, jf), r, encodingutf-8) as f: data json.load(f) for shape in data[shapes]: label shape[label] label_counts[label] 1 # 粗略估算多边形面积 points shape[points] n len(points) area 0 for i in range(n): x1, y1 points[i] x2, y2 points[(i1) % n] area x1 * y2 - x2 * y1 label_pixels[label] abs(area) / 2 for label in sorted(label_counts, keylabel_counts.get, reverseTrue): print(f{label}: {label_counts[label]} 个多边形, 约 {label_pixels[label]:.0f} 像素面积)这段代码统计每个类别的多边形数量和近似面积。参数说明label_counts记录出现次数label_pixels用鞋带公式算多边形面积。如果某个类别面积占比低于 0.5%训练时容易被背景淹没需要加权重或者用 focal loss。注意鞋带公式对自相交多边形会算错但 labelme 标注一般不会自相交可以放心用。3. 从 labelme json 到分割掩码转换脚本与四个边界坑3.1 用 labelme 自带工具批量转换labelme 提供了labelme_json_to_dataset命令行工具能把单个 json 转成img.png、label.png、label_names.txt和label_viz.png。但 7820 张图一张张转太慢而且它默认输出的是 8 位调色板图不是索引图。批量转换的常见做法是写个循环调用 Python API# 先安装 labelme注意 conda 安装 labelme 和 pip 安装的版本差异 conda install -c conda-forge labelme # 或者 pip install labelme # 单个转换 labelme_json_to_dataset ./annotations/001.json -o ./output/001批量脚本import os import subprocess json_dir ./annotations out_dir ./masks os.makedirs(out_dir, exist_okTrue) for jf in os.listdir(json_dir): if not jf.endswith(.json): continue name jf[:-5] out_path os.path.join(out_dir, name) subprocess.run([labelme_json_to_dataset, os.path.join(json_dir, jf), -o, out_path], checkTrue)逻辑说明subprocess.run调用命令行工具-o指定输出目录。参数说明checkTrue表示转换失败会抛异常方便定位问题。但这个方法有个坑labelme 版本不同输出文件名可能不一样有的版本输出label.png有的输出label_names.txt里的顺序和实际 ID 不对应。所以更稳妥的方式是自己写转换脚本直接操作 json 生成索引掩码。3.2 自己写脚本生成单通道索引掩码自己写转换脚本的好处是可控能直接输出 0 到 19 的索引图还能处理多边形重叠和孔洞。核心逻辑是用PIL.ImageDraw在空白图上画多边形填充对应的类别 ID。import json import os import numpy as np from PIL import Image, ImageDraw def json_to_mask(json_path, image_dir, label_map, output_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_name data[imagePath] img_path os.path.join(image_dir, img_name) with Image.open(img_path) as img: w, h img.size mask Image.new(L, (w, h), 0) # 背景为 0 draw ImageDraw.Draw(mask) # 按面积从大到小排序避免小目标被覆盖 shapes sorted(data[shapes], keylambda s: len(s[points]), reverseTrue) for shape in shapes: label shape[label] if label not in label_map: continue points [tuple(p) for p in shape[points]] draw.polygon(points, filllabel_map[label]) mask.save(output_path) # 使用示例 label_map {crack: 1, spalling: 2, efflorescence: 3, mold: 4, rebar_exposure: 5} json_to_mask(./annotations/001.json, ./images, label_map, ./masks/001.png)逻辑说明Image.new(L, ...)创建 8 位灰度图draw.polygon填充多边形。参数说明label_map是类别名到 ID 的映射sorted按点数降序排列让大区域先画小区域后画减少遮挡。注意fill值不能超过 25520 个类别完全够用。如果多边形有孔洞labelme 的shape_type可能是circle或rectangle需要单独处理但墙壁损伤标注基本都是polygon。3.3 四个边界坑坐标越界、重叠覆盖、类别缺失、图像旋转第一个坑是坐标越界。标注员可能在图片边缘外画了多边形draw.polygon不会报错但会画出界导致掩码边缘出现异常值。解决办法是在画之前把坐标裁剪到[0, w-1]和[0, h-1]。第二个坑是重叠覆盖。两个损伤区域重叠时后画的会覆盖先画的。如果按面积降序画小目标会被大目标盖住。更合理的做法是按类别优先级画比如裂缝优先于剥落或者用group_id区分实例但语义分割不需要区分实例所以按面积降序加类别优先级就够了。第三个坑是类别缺失。20 个类别里有些类别在某个 json 里没出现这很正常但训练时如果某个类别在整个数据集里只有几十个像素模型根本学不到。建议在转换前先统计每个类别的总像素低于阈值的类别合并或剔除。第四个坑是图像旋转。手机拍摄的墙壁照片可能带 EXIF 旋转信息PIL.Image.open默认不旋转但标注时可能是在旋转后的图上标的。解决办法是用ImageOps.exif_transpose统一旋转方向。from PIL import ImageOps with Image.open(img_path) as img: img ImageOps.exif_transpose(img) w, h img.size这行代码放在读取图片之后、创建掩码之前确保坐标和图像方向一致。4. 训练前的数据检查与增强策略4.1 用可视化脚本抽查掩码质量转换完 7820 张掩码后不能直接开训得先抽查。最直观的方法是把原图和掩码叠加用不同颜色显示不同类别。import numpy as np from PIL import Image def visualize_mask(image_path, mask_path, label_map, output_path): img Image.open(image_path).convert(RGB) mask np.array(Image.open(mask_path)) overlay np.array(img).copy() colors { 1: [255, 0, 0], # 裂缝 红色 2: [0, 255, 0], # 剥落 绿色 3: [0, 0, 255], # 泛碱 蓝色 4: [255, 255, 0], # 霉斑 黄色 5: [255, 0, 255], # 钢筋外露 品红 } for label_id, color in colors.items(): overlay[mask label_id] color result Image.blend(img, Image.fromarray(overlay), alpha0.5) result.save(output_path) visualize_mask(./images/001.jpg, ./masks/001.png, label_map, ./vis/001_vis.png)逻辑说明Image.blend把原图和彩色掩码按 50% 透明度混合。参数说明colors字典只列了前 5 个类别实际使用时按 20 个类别补全。抽查时重点看边缘是否贴合、有没有漏标、有没有把非损伤区域标成损伤。如果发现某张图掩码全黑说明 json 里没有有效多边形需要检查标注文件。4.2 针对墙壁损伤的增强参数怎么设建筑墙壁损伤图像有几个特点光照不均、拍摄角度倾斜、损伤尺度差异大。增强策略要针对这些特点。常见做法是用 Albumentations 库参数设置如下增强类型参数说明RandomRotate90p0.5墙壁照片旋转 90 度不影响损伤语义HorizontalFlipp0.5水平翻转安全RandomBrightnessContrastlimit0.2, p0.5模拟光照变化GaussNoisevar_limit(10, 50), p0.3模拟传感器噪声ElasticTransformalpha1, sigma50, p0.2模拟透视变形RandomScalescale_limit0.2, p0.5适应不同拍摄距离注意不要用垂直翻转因为墙壁损伤有重力方向性垂直翻转后裂缝走向不符合物理规律。也不要用太强的色彩抖动损伤区域的颜色是重要特征色相偏移太大会让模型学到错误的颜色关联。import albumentations as A transform A.Compose([ A.RandomRotate90(p0.5), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(limit0.2, p0.5), A.GaussNoise(var_limit(10, 50), p0.3), A.ElasticTransform(alpha1, sigma50, p0.2), A.RandomScale(scale_limit0.2, p0.5), A.Resize(512, 512), ]) # 同时增强图像和掩码 augmented transform(imageimage, maskmask)参数说明A.Resize(512, 512)放在最后统一输入尺寸。如果显存够可以用 768 或 1024但 512 在多数分割网络上已经够用。ElasticTransform的alpha和sigma控制变形强度墙壁损伤不需要太夸张的变形alpha1比较温和。5. 避坑与排查labelme 数据集转分割掩码的 5 个血泪教训现象一转换后的掩码全是黑色没有任何类别像素。原因json 里的shapes为空或者label名称和label_map里的键不匹配比如大小写不一致、多了空格。 解决先打印 json 里所有label值和label_map做交集比对。建议在转换脚本里加一行assert label in label_map不匹配时直接报错。现象二掩码边缘出现锯齿或偏移。原因标注时图片被缩放json 里的坐标是缩放后的但转换时用了原图尺寸。 解决检查 json 里的imageWidth和imageHeight如果和实际图片不一致按比例缩放坐标。缩放公式x_new x * w_actual / w_json。现象三训练时 loss 不下降模型输出全背景。原因类别极度不平衡背景像素占比超过 95%模型学到全部预测背景就能拿到高准确率。 解决用加权交叉熵或 focal loss权重按类别像素占比的倒数设置。或者用RandomCrop增强时保证每个 batch 里至少有一个损伤区域。现象四验证集指标很高但实际推理效果差。原因训练集和验证集按随机划分同一面墙的不同角度照片被分到两边导致数据泄露。 解决按拍摄对象或拍摄日期划分数据集同一面墙的照片只出现在训练集或验证集之一。如果数据集没有提供分组信息可以用图像哈希做近似去重。现象五labelme 转换脚本在 Windows 上报编码错误。原因json 文件默认用 UTF-8 保存但 Windows 中文环境可能用 GBK 读取。 解决打开文件时显式指定encodingutf-8写文件时也指定。如果 json 里有中文类别名确保label_map的键也是 UTF-8 字符串。提示转换完 7820 张掩码后先跑一遍全量校验脚本统计每个类别的像素总数和图像数量。如果某个类别在超过 80% 的图像里都不存在考虑合并到相近类别否则训练时该类别几乎学不到。6. 用 20 类掩码跑通第一个分割模型从配置到验证的实操细节选模型时建筑墙壁损伤分割不需要太重的 backbone。常见做法是用 DeepLabV3 配 ResNet-50或者 U-Net 配 EfficientNet-B0。如果显存只有 8G建议用 512x512 输入、batch size 4、混合精度训练。损失函数用CrossEntropyLoss加类别权重优化器用 AdamW学习率 1e-4余弦退火。训练 50 个 epoch 后看验证集的 mIoU如果某个类别 IoU 低于 0.3单独把该类的样本抽出来做过采样。验证时不要只看 mIoU还要看每个类别的 IoU 和像素准确率。墙壁损伤分割里裂缝的 IoU 通常最低因为裂缝细长几个像素的偏差就会导致 IoU 大幅下降。解决办法是用 Dice Loss 和 CrossEntropy 组合Dice 对细长目标更友好。另外推理时可以用滑动窗口加重叠把大图切成 512x512 的小块分别预测再拼接边缘用高斯权重融合能明显减少拼接缝。import torch import torch.nn as nn class CombinedLoss(nn.Module): def __init__(self, weightNone): super().__init__() self.ce nn.CrossEntropyLoss(weightweight) self.dice DiceLoss() def forward(self, pred, target): return self.ce(pred, target) self.dice(pred, target) class DiceLoss(nn.Module): def forward(self, pred, target): pred torch.softmax(pred, dim1) target_one_hot torch.nn.functional.one_hot(target, num_classespred.shape[1]) target_one_hot target_one_hot.permute(0, 3, 1, 2).float() intersection (pred * target_one_hot).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target_one_hot.sum(dim(2, 3)) dice (2 * intersection 1e-6) / (union 1e-6) return 1 - dice.mean()逻辑说明CombinedLoss把交叉熵和 Dice 损失相加DiceLoss对每个类别算 Dice 系数再平均。参数说明weight是类别权重张量长度等于类别数按像素占比的倒数设置。1e-6是平滑项防止除零。训练时如果显存不够可以把pred和target转成float16但 Dice 计算建议用float32避免精度问题。我自己的习惯是每训练 5 个 epoch 就存一次 checkpoint同时用验证集算一次 mIoU如果连续 3 次 mIoU 不升就降低学习率。7820 张图在单卡 3090 上大概跑 6 到 8 小时能收敛如果用了预训练权重会更快。最后提醒一句建筑墙壁损伤的类别定义在不同项目里可能不一样拿到新数据集时先和标注方确认每个类别的边界别自己猜。希望帮到你。本文还有配套的精品资源点击获取