这几年在文保数字化的圈子里,图像分割的应用越来越多。今天想聊的这套数据,是一份敦煌壁画破损识别分割数据集,LabelMe 格式,总共14210张图像,只标了1个类别。乍一听“14210张、单类别”好像并不复杂,但真正做过分割项目的人都知道,文保类数据集的难点从来不在数量,而在于标注规范、类内差异,以及模型能不能适应真实的壁画表面。
这套数据集解决的是一个很具体的问题:在壁画数字化保护流程中,肉眼巡检只能发现明显的大面积破损,而裂隙、起甲、酥碱、霉变、划痕这些细节问题,需要像素级地标出来、量出来、追踪起来。有了足够干净的分割标注,训练出的模型才能自动从海量壁画数字影像里圈出破损区域,辅助文保人员进行病害普查、修复优先级评估和变化监测。对于做计算机视觉的朋友来说,它是非常典型的“少类别、强长尾、复杂纹理背景”分割任务;对于文保工作者来说,它又是一份可以直接落地使用的病害底图。这篇文章会从数据设计、标注规范、格式转换讲到模型训练和踩坑经验,尽量把后面要做的准备工作都讲透。
1. 项目整体设计与需求拆解
1.1 破损识别到底要解决什么问题
壁画破损识别不是普通的目标检测任务。检测框只能告诉你“这个区域可能有问题”,但它给不出具体的形状和面积。而在文保工作中,病害的分布范围、边缘轮廓、占据面积比例,直接关系到修复方案的制定和修复材料的用量估算。比如起甲这种病害,一块壁画上可能同时存在几十处大小不一的起甲区域,修复时需要逐片处理,如果没有像素级边界,工作人员只能靠手绘图纸估算,效率和精度都很差。
所以这个数据集选择了“分割”而不是“检测”,本质上是为了匹配下游的量化分析需求。模型输出的是和原图同尺寸的掩膜,每个像素要么属于“破损”,要么属于背景,这种输出天然支持后续的连通域分析,可以统计单处破损的面积、周长、分布密度,甚至按年度对比同一位置的变化趋势。实际上这也是很多文保单位在数字化项目里最想要的产出。
1.2 “1类别”背后的真实考量
看到“1类别”时,很多人第一反应是:破损不是有很多种吗?为什么只标一个类别?这里其实涉及两个层面的权衡。
第一是标注成本。14210张图像如果按多类别标注,每个标注员都要反复切换裂缝、起甲、酥碱、霉变等多个标签,在边缘模糊的壁画表面,类与类之间的边界很难判定,标注一致性会急速下降。而统一归为“破损”一个类别,标注员只需要判断“这里坏没坏”,而不是“坏成什么类型”,作业速度和一致性都能大幅提升,也更适合大规模外包或半自动化标注。
第二是算法落地阶段的现实需求。很多破损形态在视觉特征上相互交织,比如起甲区域的边缘往往伴随裂隙,酥碱区域又容易和霉斑重叠。模型如果一开始就做多分类,容易在某些交界处产生混乱;而先做二分类,把“有没有破损”这件事做扎实,后续再基于破损掩膜做形态学分析和子类细分,反而更稳。所以这份数据集把“1类别”作为第一版目标,是非常务实的选择。
2. 数据采集、图像预处理与标注规范
2.1 图像来源与基础清洗流程
做数据集的第一步是凑图,但绝不是把图像堆在一起就行。这批数据主要来自壁画数字化采集影像和高精度局部拍摄照片,覆盖了洞窟内不同光照、不同拍摄角度、不同年代壁画的情况。采集回来之后,我建议先做一轮基础清洗,规则非常明确:
- 剔除严重过曝、欠曝、失焦的图像;
- 剔除壁画表面被现代修复材料大面积覆盖、原有病害特征不可辨的图像;
- 剔除重复采集的同角度同区域图像,保留画质更优的一份;
- 对大尺寸原始影像统一做无损重采样,长边控制在2000像素左右,保留足够的标注细节,同时避免后续训练时内存爆掉。
这个环节看起来不起眼,但对最终数据集质量影响巨大。我见过不少项目就因为偷懒没做清洗,模型训练时频繁被同一场景的不同裁剪块干扰,验证指标虚高,换到新数据上立刻拉胯。
2.2 LabelMe标注流程与操作规范
标注工具选了LabelMe,原因是它在文保领域已经形成了事实标准,web版本和桌面版本都有,标注界面直观,支持多边形、矩形、圆形多种标注方式,并且导出的JSON结构简单,后面做格式转换非常方便。
实际标注时,我们制定了一套非常严格的规范,核心规则是:只标“肉眼可辨的破损区域”,统一使用多边形(polygon)框出,标签名固定为一个英文单词damage。
- 起甲、裂隙、酥碱、剥落、霉变等不同形态的病害,只要视觉上能明确判断为破损,都归入damage这一个类别;
- 多边形必须贴合物体的实际边缘,不允许为了省事用矩形或粗略轮廓代替;
- 每个损坏区域单独画一个多边形,即使两个区域相邻也不合并;
- 破损极小但肉眼可辨的区域(比如长度不足3厘米的细小裂隙),也要标注,不能因为小就忽略;
- 壁画表面的自然纹理、颜料氧化造成的颜色变化、拍摄光线形成的阴影,一律视为背景,不标。
这里需要特别说一句:裂隙和阴影的区分是标注中最容易出问题的地方。实际作业中,我们要求标注员必须放大到100%倍率下逐块检查,只有能明确看出“结构性的断裂或剥落”才允许标为damage,单纯的光影渐变不标。为了统一标准,标注团队在前500张图像上进行了两轮试标,每个样本都要经过交叉复核,逐渐把“什么算破损”的共识固化了下来。
2.3 质检与二次审查机制
单靠标注员自觉是不够的,必须做质检闭环。我们当时的流程分三步:
第一步,机器初检。编写脚本自动比对JSON文件和对应图像,检查文件能否正常解析、标注坐标是否越界、多边形是否自相交。这一步能把大部分低级错误筛掉。
第二步,人工抽检。从每日新增标注中按10%比例随机抽检,重点看多边形边缘与真实破损边缘的重合度。筛查出的问题样本退回给标注人员修改,并在当天例会上同步错误类型。
第三步,交叉复标。对于边缘极其模糊、争议率高的图像,由两名标注员分别独立标注,再用IoU计算一致性。如果IoU低于0.8,这张图就进入仲裁流程,由经验更丰富的复核人员重新划定边界。
这套机制跑下来,标注质量整体稳定。不过也要承认,壁画破损的边界本身就是渐变的,再严格的规范也无法做到像素级完美,只要整体IoU在可接受范围内,其实已经能满足训练需求。
3. 标注格式拆解与训练数据适配
3.1 LabelMe JSON格式到底长什么样
LabelMe的标注结果保存在一个JSON文件里,文件名通常和图像名一致。结构非常直观,核心字段如下:
{ "version": "4.5.6", "flags": {}, "shapes": [ { "label": "damage", "points": [ [320, 512], [342, 540], [366, 548], [382, 600] ], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "mural_001.jpg", "imageData": null, "imageHeight": 1920, "imageWidth": 2560 }其中shapes数组里的每个元素代表一个标注多边形,points是顶点坐标列表,shape_type决定了图形类型,最常见的就是polygon。注意这里坐标的单位是像素,且坐标值必须落在图像尺寸范围内。imageData字段在桌面版保存时通常是null,只有特制脚本批量导出的版本才会包含base64图像数据,训练时一般用不到。
理解这个结构是后面所有工作的基础。无论后续要转成COCO、VOC还是单纯的掩膜图片,本质上都是在解析shapes里的多边形,然后把坐标转换成对应的掩膜像素。
3.2 从JSON到训练掩膜的转换
绝大多数分割模型并不直接吃JSON,而是需要一张张掩膜图。掩膜通常是一张与原始图像同尺寸的单通道PNG,背景像素值为0,破损区域像素值为255。转换时用到的核心库是opencv和numpy,步骤很简单:
import cv2 import json import numpy as np with open("mural_001.json", "r", encoding="utf-8") as f: data = json.load(f) height = data["imageHeight"] width = data["imageWidth"] mask = np.zeros((height, width), dtype=np.uint8) for shape in data["shapes"]: if shape["label"] != "damage": continue points = np.array(shape["points"], dtype=np.int32) cv2.fillPoly(mask, [points], 255) cv2.imwrite("mural_001_mask.png", mask)这段代码虽然短,但里面有几个非常容易踩的坑。
第一,坐标类型。JSON里读出来的points默认是浮点数,直接传给fillPoly会报错,必须先用int32转一下。第二,掩膜的尺寸。有人图省事把mask直接写到小尺寸再resize回去,结果边缘全部变形,IoU直接缩水好几个点,所以我强烈建议标注阶段就保持掩膜和原图同尺寸。第三,多边形的封闭。fillPoly会自动闭合多边形,但如果原标注里手滑画了自相交的多边形,填充结果就会出现孔洞,这也是前面质检环节要求检查自相交的原因。
3.3 数据划分策略与基础统计
14210张图像不能一股脑全进训练集,必须划分出验证和测试。我们实际采用的划分比例是训练集:验证集:测试集 = 8:1:1,即11368张训练、1421张验证、1421张测试。划分时做了一个很重要的事:先按壁画区域分组,再按组划分,而不是直接随机打乱。原因是同一区域的壁画通常来自连续的采集序列,图像之间高度相似,如果随机划分,会让同一区域的图像同时出现在训练集和验证集里,验证分数虚高,换到没见过的洞窟图像立刻打回原形。
另外做了一个基础统计:全部14210张图平均分辨率为1528×1196,单张图中破损区域面积占比从0.3%到64%不等,中位数约为7.8%。这是一个典型的极度不平衡分布。后面训练模型时,所有策略都是围绕这个不均衡展开的。
4. 模型选型、训练策略与效果评估
4.1 分割模型的选择逻辑
基于这份单类别分割数据集,我尝试过三类主流方案:U-Net系列、DeepLabV3+、以及带Transformer结构的SegFormer和Mask2Former。说下实际感受。
U-Net是最稳妥的基线,训练速度快,显存占用小,对于形态规整、边缘相对清晰的病害(比如剥落、大片起甲)效果就已经够用。DeepLabV3+在感受野上更有优势,受益于ASPP模块,对细小裂隙的捕捉比U-Net稍好,但训练时对输入分辨率更敏感。SegFormer这类Transformer方案在理论上能拿到更好的全局上下文,实际测试中边界确实更平滑,但训练轮数和显存需求成倍上升,对于单类别任务收益有限。
如果只让我推荐一个实用组合,我的选择是:直接用带预训练权重的ResNet-50作为编码器,接U-Net解码器,配合小尺寸输入做快速迭代;确认方案有效后,再切到DeepLabV3+做最终版。这么做的好处是前期试错成本极低,后面才有精力去做增强和调参。
4.2 损失函数的踩坑与选择
单类别分割本质是二分类,最自然的损失是BCE,但直接用BCE在这个数据集上会翻车。原因很简单,破损区域平均占比不足8%,模型只要无脑预测全背景,BCE损失就已经很小,网络根本学不到东西。
解决思路有两个方向。第一是加权重,给正样本提高权重,让“分错一个破损像素”的惩罚远大于“分错一个背景像素”。第二是换损失函数,把Dice Loss或者Focal Loss加进来。
我最终使用的是Dice Loss + BCE的加权组合,权重比例约等于1:1:
class CombinedLoss(nn.Module): def __init__(self): super().__init__() def forward(self, pred, target): bce = nn.functional.binary_cross_entropy_with_logits(pred, target) pred_sigmoid = torch.sigmoid(pred).flatten() target_flat = target.flatten() intersection = (pred_sigmoid * target_flat).sum() dice = 1 - (2 * intersection + 1) / ( pred_sigmoid.sum() + target_flat.sum() + 1 ) return bce + diceDice Loss对正负样本不平衡天然免疫,因为它关注的是预测区域与真实区域的交并比,而不是逐像素的绝对误差。不过Dice Loss也不是万能的,如果输入图像里破损区域过小,Dice值会在训练早期震荡得很厉害,这时单纯增大BCE比重、或者用小学习率预热,能缓解不少。
4.3 数据增强与关键超参数
这份数据集的增强策略和自然图像分割不太一样,不能乱用翻转和旋转。壁画有明确的上下方向感,随机上下翻转会让模型学到错误的方向信息,给后续不同位置图像的分割带来干扰。我们实际启用的增强包括:
- 水平随机翻转,概率0.5;
- 多尺度随机缩放,范围0.8到1.2;
- 随机旋转,角度限制在±15度以内;
- 亮度和对比度扰动,范围±0.2;
- 随机裁剪到统一尺寸512×512。
输入尺寸选择512是因为壁画图像普遍较大,直接整图送入模型显存不够,而缩小图像又容易丢失裂隙细节。更好的做法是训练时随机裁剪,推理时用滑窗拼接,或者直接按长边缩放到704像素再整体推理。实测下来,512裁剪训练加704整图推理,对小目标裂隙的召回率最高。
超参数方面,初始学习率1e-4,使用poly学习率策略,批量大小为16,训练的轮数大约80轮。优化器用AdamW。一个值得注意的细节:Batch Normalization在批量大小为16时表现正常,但如果因为显存限制被迫降到批量4以下,建议改用GroupNorm,稳得多。
4.4 评估指标怎么用才不算自欺欺人
分割项目最常见的评估指标是IoU和Dice,这两个值高度相关,单看一个就行。我们以IoU为主指标,验证集上最终达到了约0.823,测试集上约0.807,对单类别破损分割来说算是合格线以上的水平。
但只报告一个IoU远不够。壁画破损这种长尾场景里,小目标的召回率才是最值得关注的。所以额外统计了小目标召回率——以单个连通域面积低于全图面积0.5%的破损区域为对象,统计被模型正确分割出来的比例。这个数在早期版本只有不到四成,后来通过多尺度增强和裁剪训练提到了约六成。如果指标体系里不放这个小目标指标,模型对细裂隙的盲区问题很容易被整体IoU掩盖。
5. 常见问题与排查技巧实录
5.1 标注边界不一致怎么办
数了几十轮训练后我发现一个规律:模型在验证集上的表现波动,很多时候不是模型问题,而是标注边界不一致导致的“假差”。同一个破损区域,标注员A画得紧贴边缘,标注员B习惯留一点余量,两版掩膜边界差出一两个像素,IoU对这类差异特别敏感。
排查方法是做标注一致性分析,随机抽100张图像让两个人重标一遍,计算两版掩膜的IoU。我们当时的平均一致性IoU在0.88左右,对边界天然模糊的壁画来说算正常。如果这个值低于0.8,说明标注规范执行不到位,先补规范再训练,否则模型学到的只是标注员的平均习惯。
5.2 背景误报集中在哪些区域
模型训练中期,我仔细看了很多分割失败的案例,发现误报高度集中在壁画边缘、颜料剥落处、以及带有现代修复补色的区域。这些地方有两个共同特点:色彩变化剧烈,或者局部纹理异常。
其中一个典型案例是壁画表面的“贴金”区域,金色颜料因为反射角度问题,会在图像上形成高亮斑块,模型容易把高亮斑块当成破损。后来我用形态学后处理,对预测掩膜里的孤立小块做开运算,把小面积连通域直接删除,同时结合光照条件做局部亮度归一化,误报率明显下降。这类问题不是单靠调损失函数能解决的,很多时候得回到数据层面补样本,或者在推理后处理里做文章。
5.3 裂隙这种小目标漏检严重
如果你发现模型把大片起甲分得很好,但细裂隙总是漏,大概率是训练时输入尺寸太小,裂隙在resize后变成了1到2个像素,信号几乎消失。
我们锁定的方案是把训练输入升级为512×512的随机裁剪,让模型有时间看到裂隙的局部形态。同时增加一个针对性增强:对小连通域破损区域做局部放大,模拟大图下看小目标的效果。这样做了20轮之后,小目标召回率从原来的41%提升到58%。
5.4 从LabelMe格式迁移时容易犯的错
LabelMe转COCO、转YOLO格式时,最常见的错误是类别ID错位或坐标归一整没弄对。比如COCO的category_id要从1开始,而有些转换脚本默认从0开始,两版数据混合训练时,类别全部错位,模型的损失曲线却很难看出异常。另一个坑是YOLO分割格式要求坐标归一化到0到1之间,很多人用全图尺寸归一化没问题,一旦图像块是裁剪出来的,忘记按裁剪后的尺寸归一,结果整批标注全部偏移。
我的建议很简单:转换格式后刻意选取5张图,把生成的掩膜或边界框直接可视化叠加在原图上检查一遍,再开始训练。这种一步不用一分钟的校验能避免很多莫名其妙的训练事故。
5.5 显存不足时的训练方案
单卡显存只有8GB或12GB是这类项目很常见的情况。我们实践出的组合是:输入尺寸512、批量大小4、启用梯度累积、次数设为4,相当于一个有效批量16。同时把Supervision等增强库的耗时控制在每轮10分钟以内。这样显存占用稳定在9GB左右,单卡也能撑起完整训练。如果还超,就把编码器从ResNet-50换到MobileNetV3,速度换精度,但不至于断供训练。
6. 从单类别出发的扩展思路
这份数据集的“1类别”设计方便了第一轮落地,但也意味着后续如果要细分子类,必须重新设计标注规范。如果你拿到了同样格式的数据,想从“破损”细分到“裂缝”“起甲”“霉变”,我的建议是不要直接在原掩膜上改标签,而是保留原始JSON,在LabelMe里按新的类别体系重新标注或做半自动修正。因为原始多边形边界比掩膜更精确,重新分类时只需要调整标签名,不用重新描边。
另外,如果后续目标是破损面积变化监测,可以引入同一壁画区域在不同年份的图像对,做成时序分割任务。这时单类别的掩膜反而成了优势,因为模型要关注的核心信号是“破损区域如何扩张”,而不是“哪种破损在扩张”,两期掩膜相减就能得到变化面积,非常直观。
我个人这几轮做下来,最深的体会是:文保分割数据集的价值,很大程度上藏在标注规范和数据清洗里。14210张图像并不算多,但如果标注质量稳定、划分方式合理,它能支撑起一套可靠的生产模型。反过来,哪怕数量翻倍,如果大家各标各的、边界乱飞,后面所有环节都是在给标注错误买单。如果你正准备用这份数据集,或者要照着做一套类似的文保分割数据,先花时间把标注规范和质检流程定下来,再碰模型。这一步做得越扎实,后面调模型时就越省心。