ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

图像修复算法实战:OpenCV与深度学习Python源码解析

图像修复算法实战:OpenCV与深度学习Python源码解析 简介这是一份面向计算机相关专业毕业设计与课程项目的深度学习图像修复算法Python源码包用于解决图像缺损区域的智能补全与修复问题适合正在准备毕设或需要项目实战练习的学生。资源提供完整的可运行代码包含基础版与进阶版图像修复实现并配有项目说明文档通过对比输入图片与多组修复结果图能直观理解算法效果与调参逻辑帮助快速上手深度学习视觉项目。压缩包共14个文件以Python脚本、Markdown说明文档及图片素材为主另有.gitignore等工程配置文件整体仅5.33MB结构紧凑下载后即可按需查阅。目前已有177人学习项目经导师指导并获高分代码完整、注释清晰可作为毕业设计、课程设计或期末大作业的参考范例也可作为入门深度学习图像修复的练手项目。1. 图像修复算法实战从 OpenCV 到深度学习的 Python 源码包能修复什么图像修复这个词听起来有点玄学但落到工程上就是把图片里缺损、遮挡、划痕的区域重建出来。传统 OpenCV 的cv2.inpaint对细线划痕挺好用可一旦遮挡面积变大修复结果就开始发虚。这套基于深度学习的图像修复算法 Python 源码包把两种方案放进了同一份工程inpaint_simple.py对应传统扩散修复inpaint_complex.py对应生成式深度学习修复。对正在做毕业设计、课程设计或期末大作业的同学来说它最大的价值是完整跑通了两套流程读图、生成掩码、修复、输出对比而不是只丢给你一个黑匣子。它能解决的不只是“把图修干净”更是帮你搞明白什么场景该用哪一种修复算法。2. 修复算法怎么选扩散式修复与生成式修复的分界线2.1 先用文件清单理解源码结构拿到压缩包先别急着跑把目录结构过一遍。这份源码的核心文件不多但每个都对应一个明确环节。路径作用data/1.jpg、data/2.jpg两张测试图用来验证修复流程是否走通inpaint_simple.py传统 OpenCV 修复脚本基于扩散式算法inpaint_complex.py深度学习修复脚本基于生成式模型result/s_*.pngsimple 脚本输出的修复结果result/c_*.pngcomplex 脚本输出的修复结果README.md运行说明与依赖说明.gitignoreGit 忽略配置工程规范用按常见命名习惯s_系列对应 simplec_系列对应 complex。result目录里已经放了四组对比图你在跑完自己的实验后可以把新结果覆盖进去做前后对比这也是毕设里最常用的展示方式。这里有个容易被忽略的细节整个压缩包里没有直接看到训练好的权重文件.pth或.h5。所以复杂版第一次跑之前优先去README.md里确认权重路径是怎么写的——是自动下载、还是需要先训练、还是脚本里留了本地路径。这决定了你能不能直接进入推理环节。2.2 传统修复基于等照度线和流体方程的两种思路inpaint_simple.py底层调用的就是 OpenCV 的cv2.inpaint这个函数提供两种算法INPAINT_TELEA和INPAINT_NS。TELEA 算法基于快速行进法FMM思路是从掩码边界开始由外向内一层层插值。每个待修复像素的取值由邻域已知像素加权决定权重跟距离、方向相关。NS 算法则把灰度变化看作流体场用 Navier-Stokes 方程把等照度线延续进缺失区域对结构走向的把控比 TELEA 更好但计算量也更大。这两种算法的共同点是它们只做边缘平滑和纹理延伸不做语义理解。也就是说算法并不知道缺失区域里原本是眼睛、车牌还是屋顶它只会根据周围像素把颜色“匀”过去。对几像素宽的划痕、折痕效果很干净但如果你用一块大矩形盖住人脸的左半边传统算法大概率会把皮肤颜色填进去五官完全对不上。我一般把传统修复定位成“预处理工具”而不是最终答案。很多抠图、老照片修复流程里先用它把细小瑕疵清掉再进下一步深度模型处理大面积遮挡这是比较务实的组合。2.3 深度学习修复把掩码当条件做生成复杂版的核心思路跟传统算法完全不同。深度学习修复不是插值而是生成——模型根据上下文“想象”缺失区域应该长什么样。典型做法是把图像和掩码拼接在一起作为输入图像给 RGB 三通道掩码给一通道合起来是四通道输入。生成器输出完整的 RGB 图然后通过如下规则合成最终结果掩码区域保留生成内容非掩码区域保留原图。生成器结构一般用 UNet。UNet 的编码器一层层下采样把语义信息压缩到深层特征解码器再一步步上采样还原分辨率。关键是跳跃连接skip connection会把编码器各层的浅层特征拼到解码器对应层这样边缘、纹理信息不会在下采样过程中丢光。修复质量高不高很大程度取决于跳跃连接做得好不好。判别器一般用 PatchGAN而不是输出单个真伪分数的普通判别器。PatchGAN 把图像划分成若干 patch对每个局部区域单独判断真伪。这样做的好处是局部判真伪能强迫生成器把纹理细节做扎实如果用整图一个分数生成器很容易学会“整体颜色对但细节糊”的偷懒策略。损失函数通常包含三部分。重建损失用 L1 或 L2但只对掩码区域计算这样模型不会偷懒直接复制原图感知损失用预训练 VGG 网络提取特征在特征空间比较生成结果和真实图让结构更接近对抗损失来自判别器让生成结果在数据分布上更像真实图像。三者的权重比例是个经验值后面第 4 章会具体讲怎么调。3. 简单版快速跑通inpaint_simple.py 的参数与两种掩码生成3.1 掩码是修复的第一个坑阈值法与矩形法先明确一个概念掩码mask是单通道图像白色255表示要修的缺损区域黑色0表示保留区域。OpenCV 的修复函数只看掩码不看其他标注。所以跑修复脚本之前第一步一定是生成一张和原图等尺寸的掩码。如果你要修的是深色划痕、折痕这类缺陷常见做法是用灰度阈值自动抠出来import cv2 import numpy as np # 读取测试图cv2 默认读成 BGR 三通道后面保存时保持通道顺序一致 img cv2.imread(data/1.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 深色缺陷灰度值偏低用阈值把它们单独分离出来 _, mask cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY) # 膨胀让掩码边缘覆盖到缺陷的过渡区避免修复后留下边界痕迹 kernel np.ones((5, 5), np.uint8) mask cv2.dilate(mask, kernel, iterations2) cv2.imwrite(mask_auto.png, mask) print(mask 非零像素数:, cv2.countNonZero(mask))这段代码里值得解释的是两个参数。threshold的阈值 100 是个经验值纯白背景的灰度在 200 以上深色划痕通常在 100 以下所以用 100 能筛掉大部分背景。如果图片光照不均匀可以改用cv2.THRESH_OTSU让它自动算阈值。iterations2控制膨胀次数每膨胀一次掩码边界向外扩一圈迭代太多次会把正常区域也划进修复范围后面修复结果会显得发糊。如果你要模拟的是大面积遮挡——比如贴纸、水印、路人——阈值法就不适用了。这时候直接暴力生成矩形掩码h, w img.shape[:2] mask np.zeros((h, w), dtypenp.uint8) # 行区间和列区间按照实际遮挡位置手动指定 mask[120:320, 140:360] 255 cv2.imwrite(mask_rect.png, mask)注意mask必须是np.uint8类型取值只能是 0 和 255不能是 1 或者浮点数 255.0。很多第一次跑修复代码的人在这里翻车拿三通道彩色图当掩码传进cv2.inpaintOpenCV 直接抛出 mask 类型或尺寸不匹配的异常。3.2 调用 cv2.inpaintradius 和算法的取舍掩码就绪后修复本身只有一行核心调用result cv2.inpaint(img, mask, inpaintRadius5, flagscv2.INPAINT_TELEA) cv2.imwrite(result/s_1.png, result)inpaintRadius是每个待修复点的搜索半径直接决定影响范围。半径太小缺损区域补不干净残留暗色斑块半径太大修复结果会“匀”过头把周围正常纹理一起抹平。给一个保守的调参建议细划痕用 3中等划痕用 5粗折痕用 9再大就不太建议了——传统算法不支持大半径找补。flags是算法选择INPAINT_TELEA速度快、适合纹理区域INPAINT_NS对结构边缘的延续更好但耗时大约是 TELEA 的两到三倍。我的习惯是先跑 TELEA 看效果如果缺损区域有明显走向性结构比如墙面直线、人物轮廓再换 NS 对比。跑完之后把result和原图拼在一张画布里做对比是毕设展示最直接的形式。用np.hstack横向拼接三张图原图、掩码可视化、修复结果。mask_show cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) compare np.hstack([img, mask_show, result]) cv2.imwrite(result/s_compare.png, compare)3.3 换自己的图时最容易踩的三个地方把脚本里的测试图换成自己的照片时至少有三个地方要检查。第一掩码尺寸必须和图像尺寸完全一致。很多人从别的项目里复制了一个掩码生成函数没注意到自己的图是1280x720掩码却是960x540。修复前加一行断言可以快速暴露问题assert mask.shape[:2] img.shape[:2], mask 和图像尺寸不一致第二通道顺序。cv2.imread读进来是 BGRcv2.imwrite写出去也是 BGR中间如果经过matplotlib显示它默认是 RGB直接imshow会看到红蓝互换的怪异配色。遇到偏色问题时先怀疑通道顺序别急着怪算法。第三掩码不是非黑即白也可能出问题。比如用 PIL 读入掩码并做了缩放插值会让边缘出现灰色数值OpenCV 会把 127 这类中间值当成“半修复区域”处理结果不可控。统一在掩码上做一次二值化_, mask cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)4. 复杂版深度修复inpaint_complex.py 的结构、损失与调参4.1 生成器结构的通用模板复杂版脚本里的模型结构通常不会绕开 UNet 这个基本盘。这里按工程里最常见的写法给一个生成器模板输入是拼接后的四通道RGB mask输出是三通道修复结果import torch import torch.nn as nn def conv_block(in_channels, out_channels): return nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size3, padding1), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), ) class UNetGenerator(nn.Module): def __init__(self, in_channels4, out_channels3): super().__init__() # 编码器逐层下采样通道数翻倍 self.down1 conv_block(in_channels, 64) self.down2 conv_block(64, 128) self.down3 conv_block(128, 256) # 解码器与对应编码器层拼接通道数相加 self.up1 conv_block(256 128, 128) self.up2 conv_block(128 64, 64) self.out_conv nn.Conv2d(64, out_channels, kernel_size3, padding1) def forward(self, img, mask): # 拼接掩码作为第四通道 x torch.cat([img, mask], dim1) d1 self.down1(x) # 保留浅层纹理 d2 self.down2(d1) d3 self.down3(d2) # 压缩后的语义特征 u1 self.up1(torch.cat([d3, d2], dim1)) u2 self.up2(torch.cat([u1, d1], dim1)) return torch.sigmoid(self.out_conv(u2))这个模板可以对照inpaint_complex.py里的实际类看。三个编码器层分别把空间分辨率降到 1/2、1/4、1/8通道数从 4 涨到 64、128、256。解码器上采样时把同尺度的浅层特征拼过来这就是跳跃连接。深层的 256 通道特征负责“想象”物体结构浅层的 64 通道特征负责保住边缘细节两者缺一不可。要注意的是输入输出都做了归一化图通常缩放到[-1, 1]或[0, 1]掩码缩放到[0, 1]。如果推理结果出现全黑或全白先检查反归一化是不是写反了这类问题占了复杂版报错的一大半。4.2 训练循环masked L1 与对抗损失的配合复杂版如果带训练代码核心训练循环一般长这样。这里给出一个带关键注释的精简版本def train_step(model, disc, opt_g, opt_d, img, mask, lambda_recon1.0, lambda_adv0.05): model.train() pred model(img, mask) # 非掩码区域用原图掩码区域用生成结果 completed img * (1 - mask) pred * mask # 判别器训练真实图期望判别为 1修复图期望判别为 0 d_real disc(img) d_fake disc(completed.detach()) loss_d torch.mean((d_real - 1) ** 2) torch.mean(d_fake ** 2) opt_d.zero_grad() loss_d.backward() opt_d.step() # 生成器训练只在掩码区域算 L1 重建损失 recon_loss torch.mean(torch.abs(pred - img) * mask) d_fake disc(completed) adv_loss torch.mean((d_fake - 1) ** 2) loss_g lambda_recon * recon_loss lambda_adv * adv_loss opt_g.zero_grad() loss_g.backward() opt_g.step() return loss_g.item(), loss_d.item()两个对比明显的设计细节。一是recon_loss乘了mask这意味着非掩码区域完全不参与生成器重建损失计算。如果去掉这个 mask模型只要把原图区域复制好就能拿到低 loss掩码区域反而没人管最后表现就是训练 loss 一路下降、修复区域依旧空洞。这是深度修复里最常见的隐性错误。二是判别器用的损失形式是 LSGAN 风格的(d_real - 1)^2 (d_fake)^2而不是传统 GAN 的交叉熵。LSGAN 收敛更稳生成质量对参数的敏感度低一些。lambda_recon和lambda_adv的默认比例大约 20:1也就是说像素级重建占主导对抗损失只做边缘锐化。如果你发现生成结果纹理模糊试着把lambda_adv从 0.05 提到 0.1如果发现结果出现奇怪的伪纹理就往回调。优化器一般用 Adam学习率1e-4到3e-4之间。生成器和判别器要分别建优化器不能共用一个否则梯度互相干扰训练基本稳定不下来。4.3 推理加载权重、归一化与后处理复杂版的推理部分重点不在模型调用而在数据前后处理的一致性。训练时怎么归一化推理时必须完全一致否则输出就是噪声。import cv2 import numpy as np import torch def preprocess(img_path, mask_path, size(256, 256)): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, size) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask cv2.resize(mask, size) _, mask cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) # 图像缩放到 [-1, 1]掩码缩放到 [0, 1] img_tensor torch.from_numpy(img.astype(np.float32) / 127.5 - 1.0) mask_tensor torch.from_numpy(mask.astype(np.float32) / 255.0) # 格式转成 (B, C, H, W) 并拼接 img_tensor img_tensor.permute(2, 0, 1).unsqueeze(0) mask_tensor mask_tensor.unsqueeze(0).unsqueeze(0) return img_tensor, mask_tensor/127.5 - 1.0的写法等价于把[0, 255]映射到[-1, 1]很多脚本写成(x / 255 - 0.5) * 2结果一样但前后必须用同一个公式。推理完成后要把输出从[-1, 1]反归一化回[0, 255]再转成 BGR 用cv2.imwrite保存。权重文件的加载方式取决于训练时用什么框架保存的。PyTorch 常见做法是torch.load后state_dict脚本里一般会有一段load_state_dict的逻辑。如果加载就报尺寸不匹配检查是不是把生成器的权重误加载到了判别器上——这个错误在毕设阶段我见过不下五次。5. 避坑图像修复源码从运行到调优的四个翻车现场5.1 运行阶段的高频报错记录一跑完没有任何报错但输出图和原图一模一样。现象是修复脚本执行完毕对比图里修复结果和输入完全一致缺损区域原封不动。原因几乎只有一个掩码全黑OpenCV 找不到任何待修复像素直接把原图复制返回。我在自己项目里也踩过当时是阈值取反写反了把白色背景当成了掩码区域黑色划痕反而被当作保留区。解决方法是修复前统计掩码有效像素非零数量为 0 时直接终止并提示if cv2.countNonZero(mask) 0: raise ValueError(mask 全黑请检查掩码生成逻辑)记录二cv2.inpaint报mask.size ! img.size或掩码类型错误。现象很直接代码跑到cv2.inpaint这行抛异常。原因分两类一是掩码尺寸和图像不是同一个分辨率二是掩码是三通道彩色图或浮点类型。OpenCV 的修复函数严格要求掩码是单通道uint8其他类型一律拒绝。解决思路是在调用前做归一化断言assert mask.ndim 2, 掩码必须是单通道 assert mask.dtype np.uint8, 掩码必须是 uint8 类型 assert mask.shape[:2] img.shape[:2], 掩码与图像尺寸不一致早失败早解决这三行断言能挡掉大量低级问题。记录三复杂版推理输出全黑或全白。现象是模型跑完保存的图片要么整张黑要么整张白偶尔会有颜色但对比度完全不对。原因几乎都出在归一化/反归一化上训练时输入范围是[-1, 1]推理时却把[0, 1]的数据直接塞给模型或者反归一化时用了乘法而不是乘加把数值压缩到了错误区间。解决方法是把预处理和后处理写进同一个函数组不要散落两处。我一般会在推理脚本里保留一组常量来强制统一NORM_MEAN 0.5 NORM_STD 0.5然后所有预处理都走(x / 255 - NORM_MEAN) / NORM_STD后处理走x * NORM_STD NORM_MEAN再乘 255。这样即使后面改了归一化策略也只有一个地方要动。5.2 效果层面的脏数据和调参问题记录四修复结果边界有明显的方框痕迹。现象是修复区域周围有一圈颜色过渡不自然的边界像给缺损区套了个半透明框。原因是掩码边缘太生硬算法只知道掩码内外分界不知道边界附近该做多宽的过渡。传统修复里inpaintRadius偏大时这个现象尤其明显。解决手段有两个并进一是对掩码做膨胀让修复边界向外多覆盖几个像素二是适当调小inpaintRadius。膨胀的迭代次数和半径之间有个经验配对iterations2时半径用 3 到 5iterations1时半径可以到 7。效果不满意就两个参数一起动不要只调一个。记录五深度模型训练时 loss 在降但修复区域一直是空洞。这个坑隐蔽在损失函数里。现象是训练了上百个 epochloss 降到了很低的水平但把生成结果可视化出来缺损区域还是一团模糊。原因是重建损失没有按掩码加权模型学会了复制非掩码区域来骗 loss根本没有动力去生成缺失内容。解决方法是回到训练循环里确认重建损失一定乘了mask。更严格一点的做法是计算一个独立的评估指标专门统计掩码区域的像素误差比如我在下一章要讲的 masked PSNR。训练阶段的 loss 和评估阶段的指标分开看模型是否真的学会了修复一眼就能判断出来。6. 验证与进阶用 masked PSNR 量化修复质量修复效果光靠肉眼看不靠谱尤其毕设答辩时老师会追问“好”是怎么定义的。最实用的量化指标是 PSNR峰值信噪比但全图 PSNR 有个陷阱修复区域占比小非掩码区域几乎没变化哪怕掩码区域修得很差全图 PSNR 也居高不下。正确做法是只算掩码区域内的误差也就是 masked PSNR。import numpy as np def masked_psnr(pred_bgr, gt_bgr, mask, peak255.0): # 三通道都要参与计算掩码按像素位置加权 mask mask.astype(np.float32) / 255.0 mask np.expand_dims(mask, axis-1) # (H, W, 1) diff (pred_bgr.astype(np.float32) - gt_bgr.astype(np.float32)) ** 2 diff diff * mask mse diff.sum() / (mask.sum() * gt_bgr.shape[-1] 1e-8) psnr 10 * np.log10(peak * peak / mse) return psnr这段代码的关键在分母mask.sum()是掩码有效像素总数乘以通道数 3得到掩码区域内的总像素数。加上1e-8防止 mask 全黑时除零。PSNR 的数值只要超过 30视觉上通常已经比较干净低于 25 时缺损区域大概率还有明显块状感。除了 masked PSNR进阶方向还可以在架构上做加法。一个低成本但有效的做法是先跑传统修复把细小划痕清掉再只对大面积缺损区域跑深度模型这样既省显存又减少模型的工作量。另一个方向是把修复和超分结合先低分辨率修复再上采样细化比直接修大图稳定得多。这套源码包里的 simple 和 complex 两条线其实已经把图像修复的两个技术分支都覆盖到了。建议你先按第 3 章把 simple 版本跑熟再去读 complex 版本的模型结构对照本文第 4 章的模板逐行理解。从那以后我每次跑修复模型都会强制走一遍固定流程先可视化掩码和原图叠加确认修复区域准确再跑推理最后用 masked PSNR 打分。这个习惯帮我少踩了很多坑希望帮到你。本文还有配套的精品资源点击获取
返回列表