壁画图像修复实战:损伤分类、掩码提取与OpenCV实现

📅 发布时间:2026/10/11 10:40:50
壁画图像修复实战:损伤分类、掩码提取与OpenCV实现
简介这是一份围绕壁画图像修复与损伤修复方法研究的个人学习资源包面向数字图像处理、文化遗产数字化保护方向的学生与研究者可用于理解壁画破损诊断、数字图像辅助复原、颜料层修复、线条辅助重构等技术流程。压缩包共41个文件约7.36MB主体为Python代码14个py覆盖网络模型、损失函数、数据加载、训练与评估模块另有10张jpg结果展示图、3张png示意图、多个zbak备份、txt说明、yml/cfg配置与md文档等便于对照实验和二次开发。目前已有180人浏览学习适合有一定Python基础、希望快速上手壁画修复模型并查看中间结果的读者。资源按功能拆分目录既包含训练入口与推理脚本也保留了备份文件和样例输出可作为个人项目复现、课题开题或算法对比的参考。1. 为什么最先“修”的是图像而不是墙面走进莫高窟第 428 窟能看到不少壁画裂隙与起甲病害但在物理触碰之前数字化影像已经先被修复过一轮。所谓壁画图像修复技术就是把壁画裂隙、剥落、褪色等损伤映射为图像中的掩码区域再用传统图像处理或深度学习模型填补并做色彩一致性校正。整套流程能解决两个问题不触碰文物前提下复原视觉完整性以及为修前修后对比提供量化依据。适合文物保护从业者、图像算法工程师和对文化遗产数字化感兴趣的学生入门实操。2. 损伤分类与数字化采集修复算法要先看懂伤情很多同学拿到壁画照片就直接跑cv2.inpaint结果裂隙没补好反而把原本完好的纹理糊成一片。问题出在跳过了伤情分析这一步。壁画的损伤不是一种病裂隙是线性窄条起甲是片状剥落霉变是斑块伴随色彩偏移它们的图像特征完全不同修复算法的选型也应该不同。2.1 五类典型损伤与算法选型的映射关系我一般先把壁画损伤分成五类裂隙、起甲、酥碱、霉变、褪色。裂隙在图像上表现为暗色线性区域适合用形态学先连接断裂处再分割起甲是边缘翘起的片状区域边缘信息强修复时要保留边缘不能一味平滑酥碱导致颜料颗粒脱落分布散、面积小适合用小尺度样本块修复霉变是带颜色的斑块必须和颜色校正一起做否则补出来的区域色温不对褪色则是整体色彩偏移属于全局变换不需要局部重绘。把损伤类型和算法选型映射起来之后代码操作才会有的放矢。下表是我在实际项目里的选型对照供参考。损伤类型图像特征推荐处理方式备选算法裂隙暗色线性窄条形态学闭运算 区域修复FMM / NS起甲片状剥落边缘突起掩码生成后小半径修复双调和修复酥碱散点颗粒脱落小样本块纹理合成PatchMatch霉变斑块且色偏去色 修复 色彩迁移直方图匹配褪色全局色彩偏移白平衡 / 灰色世界算法色彩映射表判断损伤类型时有个快方法把图像转到 HSV 空间看 V 通道的局部方差。裂隙区域方差小且呈线性分布起甲区域边缘处方差突变明显霉变区域的 H 通道和周围差距很大。这些特征比肉眼扫图更客观。2.2 采集端的色彩管理与格式选择修复效果的上限取决于采集质量。采集壁画数字影像时我坚持用 16 位 RAW 或 DNG 格式存储光源色温稳定在 5000K 上下并在画面边缘放色卡用于后期校色。有人图省事用手机直出 JPGJPG 的 8 位量化在暗部只有不到 30 个灰度级裂隙边缘的过渡信息已经被压缩没了修复时会出现难看的色块。多光谱成像在大型壁画项目里越来越常见但个人学习不必上那么重的设备。手持基准反射板配合一台支持 RAW 输出的相机就够用先用反射板做白平衡基准再拍壁画后期在 raw 处理软件里同步色温。如果连 RAW 都没有至少要用无损 TIFF 格式存档采集原图。色彩管理还涉及一个容易忽略的点显示器色域。修壁画至少要保证显示器能覆盖 sRGB 全色域否则你校好的颜色在自己屏幕上正常换台设备偏色严重。我会在系统里加载显示器的 ICC 配置文件并把修图软件的色彩管理设为“保留嵌入配置文件”。3. 从掩码到重建图像修复的三种可落地实现拿到高质量原图并完成损伤分类之后才进入真正的修复流程。这一章先讲怎么从壁画图像里准确提取损伤区域生成掩码因为掩码质量直接决定修复效果。然后给两套可运行的修复实现方案一套用 OpenCV 内置算法一套用双调和修复做边界过渡。3.1 裂缝检测与掩码生成形态学到底在提取什么壁画裂隙在图像里是暗色线性结构但噪点、颜料颗粒的阴影也会形成暗色小点需要形态学操作来区分。核心思路是先用闭运算把短线裂隙连接成完整路径再用面积和长宽比过滤掉噪点。import cv2 import numpy as np # 读取灰度图使用 16 位读取避免量化损失 img cv2.imread(wall_painting.dng, cv2.IMREAD_UNCHANGED) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray_8u cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # 闭运算kernel 沿水平方向拉长优先连接横向缝 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 3)) closed cv2.morphologyEx(gray_8u, cv2.MORPH_CLOSE, kernel, iterations2) # 阈值分割取暗部 _, thresh cv2.threshold(closed, 50, 255, cv2.THRESH_BINARY_INV) # 连通域分析按面积过滤小噪点 num_labels, labels, stats, _ cv2.connectedComponentsWithStats(thresh, 8) mask np.zeros_like(thresh) for i in range(1, num_labels): area stats[i, cv2.CC_STAT_AREA] w, h stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT] if area 200 and max(w, h) / min(w, h) 3: mask[labels i] 255这段代码的核心是把“像是裂缝的东西”和“只是噪点/颗粒阴影的东西”分开。闭运算的 kernel 尺寸值得说明(15, 3)意味着在水平方向优先连接断裂的裂隙垂直方向的敏感性降低适合横向为主的裂隙。如果壁画里的裂隙走向偏垂直把 kernel 转成(3, 15)即可。阈值 50 不是固定的。它表示灰度值低于 50 的像素判为暗部这个值应该依据采集时测得的壁画背景亮度调整。连通域过滤条件area 200且长宽比大于 3能滤掉大多数颜料颗粒阴影。颗粒阴影通常是圆形斑块长宽比接近 1不会通过筛选。3.2 区域填补FMM、NS 与双调和修复的取舍OpenCV 的cv2.inpaint内置两种算法INPAINT_TELEA 基于快速行进法FMM按距离从掩码边缘向内推进INPAINT_NS 基于偏微分方程通过等照度线方向传播边缘信息。我通常用 TELEA 处理裂隙它的计算速度快且线性结构的保持效果好。NS 算法更适合大面积块状剥落但它的等照度线计算不稳定修复结果容易产生波纹。import cv2 import numpy as np # 掩码已经是 8 位单通道0 表示完好255 表示待修复区 mask_uint8 mask.astype(np.uint8) # TELEA 算法修复半径 3 像素 restored_telea cv2.inpaint(gray_8u, mask_uint8, inpaintRadius3, flagscv2.INPAINT_TELEA) # NS 算法修复半径 5 像素 restored_ns cv2.inpaint(gray_8u, mask_uint8, inpaintRadius5, flagscv2.INPAINT_NS)inpaintRadius参数非常敏感。它表示从掩码边界向内部推进时每一步的邻域半径。半径太小修复区域中间会残留暗斑半径太大修复区与周围纹理的连接变得模糊。我处理壁画裂隙时半径一般不超过 5 像素除非裂隙宽度本身超过 10 像素。如果修复区域紧贴着壁画边缘或纹路复杂的部位OpenCV 的两种算法都不够平滑可以换用 skimage 的双调和修复。双调和方程会生成一个平滑的解在掩码边界上保证颜色连续适合起甲区域那种有轻微弧度的大块剥落。from skimage.restoration import inpaint_biharmonic # 把掩码调整为布尔型True 表示待修复区域 mask_bool mask_uint8 0 restored_biharmonic inpaint_biharmonic( gray_8u.astype(float) / 255.0, mask_bool, channel_axisNone, max_iter200 ) restored_biharmonic (restored_biharmonic * 255).astype(np.uint8)inpaint_biharmonic返回的是浮点归一化结果所以我在代码里做了乘法还原。max_iter200是迭代上限对一般壁画图足够了如果掩码区域特别大迭代次数不足时修复区域会呈现梯度不明显的大片灰块这时可以适当上调到 500。参数选型上我给自己定过一个基准掩码面积小于图像总面积的 5% 时用 TELEA5% 到 20% 时用 NS 或双调和超过 20% 时需要先用深度学习模型粗修一遍再做边缘修复单靠传统算法在超大掩码下效果不佳。3.3 颜色校正白平衡与直方图匹配的参数要点修复完的结构纹理常常混入不协调的色调尤其是修复区边缘发灰发青。原因有两层一是原始图像本身有色偏修复算法忠实填补后把偏色“固化”了二是修复算法产生的新像素在颜色分布上与邻域不一致。我习惯按顺序做两步先对整图做白平衡校正消除固有偏色再对修复区域做局部直方图匹配。import cv2 import numpy as np # 灰色世界算法假设整幅图平均色是灰色据此校准 RGB 增益 img_float img.astype(np.float32) mean_b np.mean(img_float[:, :, 0]) mean_g np.mean(img_float[:, :, 1]) mean_r np.mean(img_float[:, :, 2]) avg_gray (mean_b mean_g mean_r) / 3.0 gain_b avg_gray / (mean_b 1e-6) gain_g avg_gray / (mean_g 1e-6) gain_r avg_gray / (mean_r 1e-6) img_balanced img_float.copy() img_balanced[:, :, 0] * gain_b img_balanced[:, :, 1] * gain_g img_balanced[:, :, 2] * gain_r img_balanced np.clip(img_balanced, 0, 65535).astype(np.uint16)灰色世界算法把整幅图都当作中性灰来反推光源色温对壁画这类大面积暖色为主的画面是有风险的。如果壁画本身以红褐色为主灰色世界算法会把红色通道的增益压得过低整个画面变青。所以我在应用前先做一步判断如果彩色直方图的峰值集中在暖色调区域就把灰色世界改成分段校准只用采集时放在画面边缘的色卡来算增益。修复区域的局部直方图匹配是另一个关键步骤。操作是把修复区域的彩色直方图向它的外围邻域直方图对齐让补丁区域的色彩分布和周围环境尽量一致。from skimage.exposure import match_histograms # 取掩码扩大 20 像素后的矩形区域作为邻域用于直方图匹配 kernel_dilate cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (41, 41)) neighbor_mask cv2.dilate(mask_uint8, kernel_dilate, iterations2) neighbor_area neighbor_mask 0 # 只对修复区做直方图匹配 restored_color img_balanced.copy() for c in range(3): channel img_balanced[:, :, c] neighbor_hist channel[neighbor_area] target_channel match_histograms(channel, neighbor_hist) restored_color[:, :, c] np.where(mask_bool, target_channel, channel)kernel_dilate的尺寸决定了邻域范围。取(41, 41)意味着掩码向外扩约 20 像素这个范围能覆盖修复区周边的真实色彩分布。如果扩得太大远处的颜色会混进来修复区色彩被拉偏扩得太小邻域样本太少直方图匹配会出现过度拟合。这一步是整个传统修复流程里最耗参数调优的地方但也是效果最直观的一步。修完如果发现修复区“很平、没有纹理感”基本可以确定是直方图匹配做过了头它只调整了颜色分布没有重建纹理如果修复区颜色突兀需要回头检查一下掩码是否把原本完好的区域误判成了损伤区。4. 壁画修复避坑指南五个翻车现场与排查路径这个章节的内容来自我多次的翻车血泪经验。传统图像修复的参数组合非常多而且每张壁画的亮度、对比度、纹理密度都不一样参数照搬必踩坑。4.1 修复区泛白像毛玻璃掩码膨胀与修复半径的博弈现象修复后的裂隙区域发白边缘模糊像隔了一层毛玻璃。原因一是掩码膨胀得太宽把裂隙周围的完好纹理也标记成了待修复区二是修复半径比裂隙宽度大太多算法在空白区域推不出纹理。解决先把掩码做一次腐蚀让掩码窄回裂隙本体同时把inpaintRadius调到裂隙宽度的 1/3 左右。我通常把腐蚀 kernel 设为(3, 1)或(3, 3)然后重新观察修复效果。4.2 色彩断层成带状条纹8 位深度在说谎现象修复后的壁画有明暗不等的横向色带导致色彩过渡不自然。原因全程用 8 位深度处理而 8 位图像在梯度平缓区域只有有限的灰度级一旦做直方图匹配极小的灰度差被放大成肉眼可见的断层。解决从读图开始就用cv2.imread配合IMREAD_UNCHANGED保持 16 位深度所有中间计算全部用浮点数最后再转回 8 位输出显示。注意 OpenCV 中inpaint支持 16 位输入但掩码必须是 8 位单通道。4.3 误检把纹理当成裂缝光照不均下的分割翻车现象修完发现很多原本完好的纹路被当成裂隙抹掉了画面失去细节。原因阈值分割对光照不均很敏感——同一张壁画上阴影区域的暗纹理可能比亮区域的真实裂缝更“暗”固定阈值无法兼顾。解决先用分块阈值替代全局阈值把图切成小方块分别计算阈值更稳定的做法是用形态学重建去除光照背景再做阈值分割。分割之前务必先做亮度归一化或顶帽变换。4.4 补丁纹理被反复复制样本块尺寸设错现象修复区域里出现肉眼可辨的重复纹理单元像地砖一样规则排列。原因基于样本块的修复算法里样本块尺寸设太大时匹配窗口能容纳更多纹理结构也更容易重复采样同一个块。解决将样本块尺寸减小到至少小于最小可辨纹理周期的 1/2。如果壁画纹路是约 10 像素一个周期样本块边长设为 58 像素比较安全避免一次复制一个大区域。4.5 指标很高但视觉很假评估口径的偏差现象修复后 PSNR 达到 34dB 以上但人眼一看就是假的。原因评估时把修复区域和“修复后的参考图”做了对比而参考图本身就是修复算法的产物比出来的数字当然好看。解决评估时只取掩码外扩一定距离后的环状邻域用这个邻域上的像素一致性来判断修复边界是否自然PSNR/SSIM 只作为参考最终要以放大巡检修复区边缘的纹理连接质量为准。5. 往评估深一步梯度图、16 位输出与可复现记录5.1 用梯度图看边界缝隙修复是否成功肉眼在高分辨率屏幕上不容易同时看到全局和细节。我习惯在最后加一步用 Sobel 算子计算修复前后图像的梯度图然后只看掩码边界附近的梯度变化。如果修复图像在边界上的梯度与原图差异超过阈值说明修复区域还是“悬浮”在画面上没有融入背景。import cv2 import numpy as np gray_final cv2.cvtColor(img_final, cv2.COLOR_BGR2GRAY) grad_x cv2.Sobel(gray_final, cv2.CV_64F, 1, 0, ksize3) grad_y cv2.Sobel(gray_final, cv2.CV_64F, 0, 1, ksize3) grad_mag np.sqrt(grad_x ** 2 grad_y ** 2) # 取掩码外扩 10 像素的环形区域检查梯度突变 ring_mask cv2.dilate(mask_uint8, np.ones((21, 21), np.uint8), iterations1) ring_mask ring_mask.astype(bool) ~mask_bool boundary_grad grad_mag[ring_mask] print(np.percentile(boundary_grad, [5, 50, 95]))如果第 95 百分位的梯度值比邻域正常区域高出 20% 以上说明边界还有锐利的不连续需要回去检查修复半径或掩码收缩程度。这一步是纯量化手段比肉眼盯着屏幕十分钟有效得多。5.2 规则记录修复参数输出文件必须是无损 16 位 TIFF命名里带上阈值、掩码面积、修复半径和白平衡增益四项关键参数。保存修复过程记录比保存结果重要——壁画修复项目里一张报告可能要几周后回溯原始参数一致才能复现结果。我会在输出目录里放一个短的参数快照例如img_radius5_maskarea2334_gain_r1.03_gain_b0.97.tif加上一份包含版本、工具链、每个步骤消耗时间的简短 README。5.3 小图起手个人学习路径建议我的学习习惯是从小图开始把壁画裁成 512×512 的小块在单条裂隙上跑通全流程再加入起甲和霉变做混合场景。小图的好处有三点参数迭代快到秒级直方图匹配看得见肉眼变化处理失败时佐证来源明确。这套流程适合做个人学习的实验记录原始图、掩码、中间修正结果、最终成品全部存档再把参数记录和最终效果对齐形成自己的修复样本库。如果是个人学习阶段不必追求大而全的深度学习方案先把传统算法吃透再换模型对比同一组掩码下的效果差异会更有价值。从那以后我每修完一张壁画图都会强制走一遍梯度检查加参数记录这两步哪怕只是改了个阈值也要留下痕迹。这种习惯让我在半年后回溯项目时还能准确说清楚每一版结果的生成条件。希望帮到你。本文还有配套的精品资源点击获取