☰
LaMa图像修复结合OpenVINO:CPU上高效实现高质量图像修复
2026/10/10 5:02:19 网站建设 项目流程

简介:LaMa 图像修复模型在视觉补全领域表现突出,此项资源将其部署到 OpenVINO 推理框架内,构成一套能够直接运行的演示工程。面向需要在常见 Intel 硬件环境中开展图片内容擦除、水印去除、缺损区域重建工作的开发者与学习者,也适合作为人工智能应用部署方向的项目演练素材。压缩包共包含 383 个文件,主要有动态链接库、XML 配置、说明文档、ONNX 模型、C# 工程源码等多类内容,覆盖程序运行所必需的库文件、模型文件与配套图片,整体体积约 831.62MB,目录组织较规范。已有 252 人学习下载。读者可以获得完整的工程参考,包括模型加载方式、推理接口调用逻辑与结果展示流程,能够减少自行完成环境适配、模型转换等工作带来的时间成本,较快地理解并复现基于 OpenVINO 的图像修复方案。

1. LaMa Image Inpainting 图像修复 OpenVINO Demo.rar 到底是什么,值得动手吗

把一张老照片里的路灯、反光或者莫名其妙的路人去掉,最烦的不是圈选,而是补出来的纹理跟周围完全不是一个材质。LaMa Image Inpainting 图像修复 OpenVINO Demo 给出了一条不用高端 GPU 的思路:模型负责对被掩膜遮住的区域重新生成结构,OpenVINO 负责把这个模型压成普通 CPU 也能跑的推理。很多人第一反应是“修复模型不都得上 GPU 吗”,实测下来 CPU 也可以,只是 Demo 里把很多参数都埋起来了。这篇笔记把这个演示包拆开来讲,适合从零验证修复效果,也想把流程接进自己管线的工程师。

2. LaMa 为何能高质量修复,以及 OpenVINO 部署选型的理由

2.1 LaMa 的核心思路:用傅里叶卷积扩大感受野,而不是堆层数

LaMa 全称是 Large Mask Inpainting,它在意的不是“补小块”,而是“补大块还看不出痕迹”。传统图像修复方法走的是像素扩散,比如逐行推进纹理贴片,遇到复杂背景就露馅;更早的深度学习方法则是堆 U-Net 层数,靠卷积一层层把信息传到中心,空洞一大,远处信息传到中心早衰减没了。LaMa 的做法是引入快速傅里叶卷积(FFC),把图片从空间域转换到频域去操作。频域里的一个点是全局信息的汇总,模型一次前向就能把整张图的上下文“看”到,所以它对不规则掩膜、半透明水印、镜面反光这类任务很友好。

LaMa 另一个关键点是训练时用了对抗损失和感知损失一起约束。对抗损失逼着生成器把纹理做得接近真实分布,感知损失保结构不变形。所以它输出的纹理不是平滑糊掉的,而是能跟着周围墙砖、草地、皮肤毛孔走的质感纹理。这也是为什么它能在几类公开修复基准上稳定排在前面,而不是靠单张图碰运气。

从部署角度,LaMa 前向推理只有一次,不需要多次迭代优化,对实际接入来说是很大的优势。你不需要在服务器上维护优化循环,拿到输入图加掩膜,跑一次网络,再做一次后处理就能出结果。下面先把运行依赖搭起来。

# 用 venv 隔离,避免在系统 Python 里装一堆没有用的依赖 python -m venv lama_venv source lama_venv/bin/activate pip install --upgrade pip pip install numpy opencv-python pip install openvino onnx

这段命令先把 Python 虚拟环境建好,再把核心依赖装进去。numpy 和 opencv-python 负责图像读写、矩阵操作;onnx 用来导出和验证模型;openvino 是最终推理引擎。注意这里不需要装完整的 GPU 加速套件,后面你会发现 OpenVINO 在 CPU 上的表现已经足够应付演示和中小批量生产场景。

2.2 用 OpenVINO 还是 ONNX Runtime?先看部署约束

选型是多数人第一步就卡住的地方。同是开源推理引擎,ONNX Runtime 和 OpenVINO 都能加载 ONNX 模型,区别主要在两点:一是 CPU 指令集优化,OpenVINO 会针对常见 CPU 指令集做更激进的算子融合,尤其卷积这类修复模型主力算子;二是 OpenVINO 有模型转换工具,能把模型压成中间表示(IR)格式再运行,首次编译后的执行计划更干净。很多人在 GPU 服务器上试通一个模型,最后要交付到现场的一台普通办公机上,才发现显卡那边完全没环境,这种血泪经验我遇到过不止一次。

下面这张表是几个常见部署方式的粗略对比,具体数字会因为模型输入分辨率、机器型号浮动,趋势是稳定的。

部署方式启动耗时CPU 优势依赖复杂度
PyTorch 直接跑高一般高,需要完整训练框架
ONNX Runtime中中中,只需要 Runtime
OpenVINO Runtime中高中,带模型优化工具链

选 OpenVINO 还有一个现实原因:这个 Demo 既然叫 OpenVINO Demo,包里大概率已经准备好 IR 格式的权重或转换脚本。如果你是拿着一个 .pt 文件过来的,同样可以走 ONNX 再转 IR 的路径,后面第三章会给完整操作。一体两面去看,OpenVINO 并不是说推理精度一定比 PyTorch 高,而是它在 CPU 生态里更容易跑起来,对没有独显的实验室、办公网环境特别实用。

3. 跑通 OpenVINO Demo:从解压到第一次修复成功的全链路

3.1 解压后先别急着跑:按目录结构确认权重格式

拿到 .rar 压缩包,第一件事不是双击运行脚本,而是先看压缩包内布局。常见做法是用 unrar 解压到独立目录,再按目录结构确认权重格式。

mkdir lama_demo && cd lama_demo unrar x ../LaMa*.rar find . -maxdepth 2 -type f | head -20

解压命令走的是 unrar,如果系统没装,用 7z 也可以。find列出前 20 个文件,重点找三类东西:权重文件、示例图片和 README。我在真实项目里遇到的包结构通常长这样:models/目录放.xml和.bin文件,表示 OpenVINO IR 已经转换好;或者放.onnx文件,需要你后续自行转换;input/和masks/对应测试原图与掩膜图;src/里放推理脚本。

如果包里只有.bin和.xml,那可以直接跳到 3.3。如果只有.onnx,按 3.2 先转 IR。如果连 ONNX 都没有,只有一个.pt检查点,那你需要先问清楚来源训练脚本按什么输入格式保存的,因为 LaMa 有不同变体,输入通道排列和归一化手段不一致,后面推理时掩膜很容易搞错。

3.2 把 PyTorch/ONNX 权重转成 OpenVINO IR

LaMa 的常见导出方式是先把 PyTorch 权重转成 ONNX,再用 OpenVINO 的 Model Optimizer 转成 IR。转换命令本身不复杂,但输入形状必须和推理脚本保持一致。一般输入是四通道(RGB 图像加一个掩膜),我习惯在导出 ONNX 时就统一成1x4xHxW,这样后面转 IR 不用再改动态轴。

# 常见导出脚本长这样,重点看 input_names 和输出张量 import torch model = torch.load("lama.pt", map_location="cpu") model.eval() dummy = torch.randn(1, 4, 512, 512) torch.onnx.export( model, dummy, "lama.onnx", input_names=["input"], output_names=["output"], opset_version=11, dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}} )

这段导出脚本把模型接到 ONNX。dynamic_axes只留 batch 维度动态,避免后续 OpenVINO 处理动态空间维度时容易出现算子不兼容。512x512是很多 LaMa 变体的默认输入尺寸,如果你的包没有明确说明,先按这个尺寸测通再调。导出完成后用 OpenVINO 命令行转 IR:

mo --input_model lama.onnx --input_shape [1,4,512,512] --output_dir ./ir

mo是 OpenVINO 自带的模型转换工具。--input_shape写死的原因是把模型编译期的静态信息定下来,减少运行时的额外开销。转完后ir/目录里会出现lama.xml和lama.bin,前者描述计算图,后者存权重。实际踩坑点在于:如果 Demo 期望的掩膜是单通道,但导出时把掩膜当成了三通道,转换后模型输入就不是 4 而是 6,这时要么回去改导出,要么在推理时手动扩展通道数。

3.3 第一个能跑通的推理脚本:读取图像、组合掩膜、执行推理

权重和输入就绪后,写一个最小推理脚本。把图像读进来,把掩膜组合成模型期望的输入长度,调用 OpenVINO 执行,最后把输出叠回背景图。

import cv2 import numpy as np from openvino.runtime import Core # 读取原图和掩膜,统一到模型输入尺度 image = cv2.imread("input/photo.jpg") mask = cv2.imread("masks/mask.png", cv2.IMREAD_GRAYSCALE) H, W = image.shape[:2] input_size = (512, 512) image_resized = cv2.cvtColor(cv2.resize(image, input_size), cv2.COLOR_BGR2RGB) mask_resized = cv2.resize(mask, input_size, interpolation=cv2.INTER_NEAREST) # 转成网络输入要求的 [B, C, H, W] 布局 image_norm = image_resized.astype(np.float32) / 255.0 mask_norm = (mask_resized > 127).astype(np.float32) input_data = np.concatenate([image_norm, mask_norm[..., None]], axis=-1) input_data = input_data.transpose((2, 0, 1))[None, ...] # OpenVINO 推理 ie = Core() model = ie.read_model("ir/lama.xml") compiled = ie.compile_model(model, "CPU") output = compiled([input_data])[compiled.output(0)] # 后处理:拿模型输出的 RGB 结果替换掩膜区域 out_img = output[0].transpose((1, 2, 0)) out_img = np.clip(out_img, 0.0, 1.0) * 255.0 out_img = cv2.cvtColor(out_img.astype(np.uint8), cv2.COLOR_RGB2BGR) result = image.copy() mask_resized_bool = mask_resized > 0 result[mask_resized_bool] = out_img[mask_resized_bool] cv2.imwrite("output/result.jpg", result)

这里几个参数容易出错。mask_norm把掩膜强制转成 0 或 1,LaMa 输入里掩膜必须是二进制,不是 0 到 255。transpose((2, 0, 1))是把 OpenCV 的 HWC 布局换成网络的 CHW。compiled.output(0)是为了兼容 OpenVINO 不同版本对输出节点的命名差异,直接用索引最稳。最后一步把模型输出贴回原图,而不是直接用整个输出做结果,目的是保留原图像素,只在掩膜区域用网络补充,这样模型边界误差不会污染全图。

4. 掩膜与预处理细节:修复质量的隐形开关

4.1 掩膜要不要膨胀?边缘假象的来源

很多人第一次跑通 Demo 都会看到同一个现象:掩膜边界处有一条细缝,或者周围纹理像被涂了一笔。原因大多是掩膜没有做膨胀。模型训练时,掩膜和图像是在同一个尺度对齐的,但实际标注时鼠标勾画的边缘往往比目标区域窄了几个像素,模型拿到的“待修复区域”边界是锐利的,而它输出时会在边界两侧做某种过渡,如果遮罩没有提前覆盖足,边界就会被原图的颜色硬生生切开。

我的习惯是先对掩膜做一次膨胀,再做轻微的腐蚀回缩。膨胀的目的是让修复区域略微超过真实需要涂掉的目标边缘,让模型输出的过渡带落在掩膜内部,这样贴回原图时,边缘那个像素仍然由模型负责。

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask_dilated = cv2.dilate(mask, kernel, iterations=2)

iterations=2对应大约 4 个像素的扩张,具体数值取决于你输入的分辨率。在 512 分辨率上,2 到 4 次足够;如果原图是 2K 以上,建议把 mask 缩放到模型输入分辨率后再做膨胀,因为小尺寸里 1 像素对应的原图区域更大,盲目标配很容易造成溢色。衡量标准也很简单:修复结果里如果还能看到被涂掉物体的残影,说明膨胀不够;如果看到周围真实纹理也被糊掉,说明膨胀过头。

4.2 缩放策略:能直接 pad 就不要硬拉伸

LaMa 训练时通常把图像缩放到固定尺寸,但现实中你拿到的图很少正好是 512x512。常见错误是用 Opencv 的 resize 强行把非方图压成正方形,结果人物变形,修复完换回原图尺寸时几何结构对不上。我一般会先用短边等比缩放,再把长边方向做对称填充,这样模型看到的物体比例没有变。

scale = min(input_size[0] / image.shape[0], input_size[1] / image.shape[1]) resized = cv2.resize(image, (int(image.shape[1] * scale), int(image.shape[0] * scale))) pad_left = (input_size[1] - resized.shape[1]) // 2 pad_top = (input_size[0] - resized.shape[0]) // 2 image_padded = cv2.copyMakeBorder( resized, pad_top, input_size[0] - pad_top - resized.shape[0], pad_left, input_size[1] - pad_left - resized.shape[1], cv2.BORDER_REFLECT )

这里pad_left用整除,右侧填充量用总宽减掉左边,保证偶数对称。BORDER_REFLECT比BORDER_CONSTANT对自然图像的边缘纹理更友好,因为它用图像自身的镜像内容填充,不会出现一条明显色带。掩膜也要按同样的缩放比例和 padding 处理,最好直接用cv2.warpAffine或cv2.resize后在同一偏移量上 pad,否则掩膜和图像错位,修复区域就会偏移。

硬拉伸唯一能接受的场景是 Demo 自带的测试图本身已经是正方形。如果在生产管线里,面对手机屏幕截图、摄像头抓拍这类纵横比差异大的图,pad 的结果会明显优于拉伸。输出时再去掉 padding 区域,只把掩膜覆盖的真实目标区域贴回原图坐标系。

4.3 通道顺序、归一化与后处理:黑匣子的真实边界

LaMa 输入排列不是固定不变的。有的导出版本接受 RGB 在前、mask 在后,也就是1x4xHxW;也有的版本把 mask 作为第四通道但接受 CHW 与 HWC 混着来。打开模型后用model.input(0).shape直接查询,比自己猜要快得多。如果查到是(1, 4, H, W),那就按第 3 章的代码把通道拼起来。如果查到是(1, 3, H, W),说明这个变体内部把 mask 量化到了 RGB 的某个通道里,你要根据包的 README 决定怎么塞。

归一化是我见过最多翻车的地方。某开发者把图像除以 255,再把 mask 除以 255,结果模型输出整体偏灰,所有纹理都像蒙了一层雾。原因在于有的导出脚本本身已经在模型内部做过归一化,外部就不再需要除;有的一开始就是用 0 到 255 训练。这个信息完全无法从推理结果反推。解决办法是:找到 Demo 源码里读图部分,看它是否在进模型前有astype(np.float32) / 255.0或ImageNet的 mean/std 处理。以下是我通常采用的参数组合。

元素常见数值什么时候用
图像归一化除以 255,映射到 0-1模型在导出时已完成内部归一化
掩膜归一化非 0 即 1LaMa 常规变体一致
图像通道RGB与模型输入保持一致,防止 BGR 反转
输出处理clip 到 0-1 再乘 255模型直接输出归一化张量

如果实在找不到归一化依据,我的教训是对照包里的示例输出。示例图跑出青色,就把图像换成除以 255 再减均值试一轮;跑出像黑白底片,就把通道从 BGR 换成 RGB。如果两个方向都不对,再检查 mask 数值范围,经验里面 90% 是 mask 没做二值化。

5. 常见坑与排查:五个让修复翻车的隐蔽问题

5.1 模型输出整体发青:通道顺序或归一化不当

现象:修复结果里被填的区域发青发紫,整块颜色脱离现实。

原因:绝大多数网络训练时用的是 RGB 顺序,而 OpenCV 默认读图是 BGR。如果推理脚本没做cv2.COLOR_BGR2RGB转换,模型看到的颜色通道完全错位,输出自然偏色。归一化方式错乱也会出现类似但更轻微的色偏。

解决:在把图像送入模型前,统一先cv2.cvtColor(im, cv2.COLOR_BGR2RGB),输出再转回 BGR 给 OpenCV 显示。写一个后处理函数,把输出np.clip(..., 0, 1) * 255乘以系数确认一下。如果模型本身就按 BGR 训练,那这个转换就不要做,两种可能都测一次,看哪边输出正常。

5.2 掩膜区域没被修复,反而出现复制描边

现象:掩膜边缘有很粗的一条线,看起来像原图被平移复制了一部分,中间还是原样。

原因:掩膜和图像没有对齐。常见路径是原图缩放到512x512,掩膜却在原图尺寸直接读,没有经过 resize;或者读掩膜时用了cv2.INTER_AREA,把二值掩膜插值成半透明,导致模型那里到底是修还是不修变得模糊。

解决:掩膜缩放必须用cv2.INTER_NEAREST,保证只取 0 和 255 两种值。缩放后检查np.unique(mask),如果出现非 0/255 之外的数值,重新二值化。同时保证原图和掩膜用完全相同的缩放比例与 pad 偏移,我习惯写一个函数同时输出 image_padded 和 mask_padded,避免手算坐标出错。

5.3 CPU 首次推理特别慢,重复第二次变快:没做预热

现象:第一次跑一张图花了十几秒,第二次再跑同一张图却只要一两秒。

原因:OpenVINO 的compile_model是在第一次推理时完成内部权重布局优化和线程池初始化的,这部分开销没有体现在编译阶段,而是在首帧推理里。

解决:正式使用前先喂一张随机噪声或者同一张图跑一次,完成预热。如果服务常驻,可以把编译对象保存起来,不要在函数内部每次都Core()和compile_model()。比较奇怪的是很多人用了 OpenVINO 还执着于“热对象”,我用下来的习惯是进程启动后立刻跑一次 dummy 输入,然后把 compiled_model 作为全局单例,后续调用只做infer,性能立刻稳定。

5.4 显式设置输入尺寸却报错:解析输入通道数

现象:read_model成功,compile_model也成功,但一执行就报Node number ... inconsistent,错误信息里提示输入节点 size 不对。

原因:模型输入是(1, 4, 512, 512),你按(1, 3, 512, 512)传了数据。也可能反过来,模型期望三通道,你把 mask 并入后给了四通道。这类问题大多出在导出时动态轴上。

解决:打印compiled.input(0).shape和compiled.output(0).shape,用实际 shape 来构造input_data。我通常在最开始加一个断言:

assert input_data.shape == tuple(compiled.input(0).shape), f"shape mismatch: {input_data.shape} vs {compiled.input(0).shape}"

这样问题在初始化阶段暴露,而不是在推理中间突然炸掉,排查成本一下子降下来。

5.5 集成到服务后内存持续增长:缓存策略没做

现象:服务跑几十张图后,进程 RSS 内存持续上升,最后接近系统上限。

原因:每次推理都在函数内部创建Core()并compile_model(),新模型实例占用的内存没有被及时释放。Python 的垃圾回收对大对象通常还可以,但 OpenVINO 的 runtime 对象遵循底层引用计数,局部变量退出后如果不显式del,可能会延迟留在内存里。

解决:把Core和compiled_model提升为模块级单例。更彻底的做法是利用 OpenVINO 的模型缓存目录,把编译后的模型缓存到磁盘,下次启动直接从缓存加载,省掉模型优化时间,也避免重复实例化。同时如果要并发处理,使用AsyncInferQueue而不是每次新建 request。

6. 把这套 Demo 做成日常批处理工具:封装、验证与性能调优

绕过所有坑之后,你会发现真正值得做的是把推理封装成类,批量目录处理。一个稳定的类至少包含三件事:模型加载与预热、图像预处理、后处理贴回。以下是我常写的结构。

class LamaInpainter: def __init__(self, xml_path, device="CPU", batch_size=1): self.core = Core() self.model = self.core.read_model(xml_path) self.compiled = self.core.compile_model(self.model, device) self._warmup() def _warmup(self): dummy = np.zeros(tuple(self.compiled.input(0).shape), dtype=np.float32) self.compiled([dummy]) def inpaint(self, image, mask): # 处理与推理逻辑 prepared = self._preprocess(image, mask) output = self.compiled([prepared])[self.compiled.output(0)] return self._postprocess(image, mask, output)

加载一次,批量调用inpaint。批量处理时不要傻傻地把所有图片一次性堆进内存,而是用生成器逐个读、逐个推理。性能调优上,如果机器有多个 CPU 物理核心,可以设置NUM_STREAMS参数,让 OpenVINO 在不同线程上重叠预处理和推理。对于单张图修复,NUM_STREAMS=1反而更快,因为避免线程切换开销;对于批量,设置成物理核心数的一半是我尝试过的稳健起点。

验证输出时不要只看肉眼看几张。把修复后结果和原图都存下来,如果原图没有原始“干净版”,那至少算一算修复区域周围的均值与方差,看是否和整体纹理一致。更严格的做法是准备测试集,用 PSNR 和 SSIM 作为回归指标,每改一次预处理参数就重跑一遍,防止“这周效果好了,下周换数据全崩”。

我现在做修复任务已经习惯把模型路径、输入尺寸、pad 策略、掩膜膨胀次数全部塞进一个 YAML 配置文件,出问题直接翻配置而不是改代码。某次批量处理时没有备份掩膜,统一膨胀参数导致一张低分辨率图标被修出毛边,从那以后再也不敢把掩膜参数写死在推理脚本里。前期多花十分钟把校验脚本写好,后期能避开很多看不出来的纹理失真。希望帮到你。

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

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

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

立即咨询