☰
壁画图像修复实战:损伤分类、掩码提取与OpenCV实现
2026/10/11 10:40:49 网站建设 项目流程

简介:这是一份围绕壁画图像修复与损伤修复方法研究的个人学习资源包,面向数字图像处理、文化遗产数字化保护方向的学生与研究者,可用于理解壁画破损诊断、数字图像辅助复原、颜料层修复、线条辅助重构等技术流程。压缩包共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 上下,并在画面边缘放色卡用于后期校色。有人图省事用手机直出 JPG,JPG 的 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, iterations=2) # 阈值分割:取暗部 _, 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, inpaintRadius=3, flags=cv2.INPAINT_TELEA) # NS 算法,修复半径 5 像素 restored_ns = cv2.inpaint(gray_8u, mask_uint8, inpaintRadius=5, flags=cv2.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_axis=None, max_iter=200 ) restored_biharmonic = (restored_biharmonic * 255).astype(np.uint8)

inpaint_biharmonic返回的是浮点归一化结果,所以我在代码里做了乘法还原。max_iter=200是迭代上限,对一般壁画图足够了;如果掩码区域特别大,迭代次数不足时修复区域会呈现梯度不明显的大片灰块,这时可以适当上调到 500。

参数选型上我给自己定过一个基准:掩码面积小于图像总面积的 5% 时用 TELEA,5% 到 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, iterations=2) 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 像素一个周期,样本块边长设为 5~8 像素比较安全,避免一次复制一个大区域。

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, ksize=3) grad_y = cv2.Sobel(gray_final, cv2.CV_64F, 0, 1, ksize=3) grad_mag = np.sqrt(grad_x ** 2 + grad_y ** 2) # 取掩码外扩 10 像素的环形区域,检查梯度突变 ring_mask = cv2.dilate(mask_uint8, np.ones((21, 21), np.uint8), iterations=1) 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 的小块,在单条裂隙上跑通全流程,再加入起甲和霉变做混合场景。小图的好处有三点:参数迭代快到秒级,直方图匹配看得见肉眼变化,处理失败时佐证来源明确。这套流程适合做个人学习的实验记录:原始图、掩码、中间修正结果、最终成品全部存档,再把参数记录和最终效果对齐,形成自己的修复样本库。如果是个人学习阶段,不必追求大而全的深度学习方案,先把传统算法吃透,再换模型对比同一组掩码下的效果差异会更有价值。

从那以后,我每修完一张壁画图,都会强制走一遍梯度检查加参数记录这两步,哪怕只是改了个阈值也要留下痕迹。这种习惯让我在半年后回溯项目时还能准确说清楚每一版结果的生成条件。希望帮到你。

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

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

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

立即咨询