简介:一套面向OpenVINO平台开发者的LaMa图像修复演示工程,适合需要快速上手图像修复模型部署的算法工程师与学习者,也可作为企业级图像修复应用的参考基线。压缩包共383个文件,大小约832MB,核心文件包含149个dll运行库、66个xml配置、12个onnx模型文件以及多个可执行exe与项目源码,其中onnx模型承担LaMa的推理主干,dll提供OpenVINO推理及第三方依赖支持,xml与config则完成推理参数与运行环境配置。通过该Demo,读者可系统理解LaMa图像修复模型在OpenVINO上的部署链路,包括模型加载、预处理、推理及后处理全流程;项目还附带了Visual Studio解决方案文件与工程构建配置,便于直接编译调试。目前已有251人学习下载,适合希望结合OpenVINO实现高效图像修复、或进一步改进修复算法的开发人员。
1. LaMa Image Inpainting + OpenVINO 的 Demo 包:先跑通再谈原理
一个带.rar后缀的“LaMa Image Inpainting 图像修复 OpenVINO Demo”,承载着两类期待:一类想拿它去水印、去杂物、抹掉画面里的路人和招牌;另一类想把 LaMa 的高质量修复能力搬进只有 CPU 或核显的机器上。我的建议是,别急着双击运行,先花半小时把“PyTorch 权重转 ONNX、再转 OpenVINO IR、最后写一个最小推理脚本”这条链路亲手跑一遍。Demo 包只是交付形式,底层这套流程才是真正能复用的东西。
能动手复现的人,才能判断单张修复的耗时能不能接受,也才知道把 mask 调宽一点会不会让墙面纹理彻底糊掉。这篇文章把模型转换、本地部署参数、批处理脚本和常见翻车点一次讲完。新手可以按步骤走,熟手正好核对那些容易出错的边界细节;至于.rar里有没有现成脚本,不影响你按这套方法把它跑通。
2. LaMa 修复与 OpenVINO 的适配逻辑:跑 Demo 前先弄清三件事
2.1 为什么 LaMa 能修大空洞:FFC、膨胀掩码与感受野
LaMa(Large Mask Inpainting)在图像修复圈子里站稳脚跟,靠的是对大面积遮挡的处理能力。传统扩散类算法从洞边缘向内推,空洞一大,纹理和结构信息就在传播途中衰减,最后墙不像墙、草不像草。LaMa 的做法是让模型“看得更远”:骨干网络里嵌入了 FFC(Fast Fourier Convolution),特征被拆成局部通道和全局通道,全局通道经过傅里叶变换后能感知整张图的低频结构,再与局部纹理分支融合。墙面走向、桌面透视这类全局信息可以跨越大半个画面传进洞里,修复出的结果才像是“原本长出来的”。
这里有一个容易被 Demo 使用者忽略的细节:LaMa 的训练数据里,mask 大多是大块、高比例的随机遮挡,模型天然偏向“大开大合”的填充。推理时如果画的是 1~2 像素的细线,或者 mask 边界非常锐利,模型会表现得犹豫,修复结果容易出现模糊残留。常见做法是用 OpenCV 的dilate把 mask 膨胀几个像素再送入模型,这样更接近训练分布,也让覆盖区域比实际水印略大,防止水印边缘的浅色残影留在线条两侧。
另一个关键点是输入组织方式。LaMa 网络输入不是单张 RGB,而是把 image 和 mask 在通道维度拼接成 4 通道张量,mask 的取值是 0/1,不是 0/255。很多人直接在预处理里把 mask 除以 255 当成 0~1 输入,结果模型对“半透明白色区域”的理解出错,修复区会带上原图残影。这个问题在多数 demo 包的说明里都不会写,但它必须在预处理代码里被显式处理。
2.2 为什么用 OpenVINO 而不是原版 PyTorch 跑 Demo
原版 LaMa 是 PyTorch 项目,效果验证和论文复现都在 Python 生态里做。但如果目标是给运维机器、内网服务器、无独显笔记本做本地部署,PyTorch 依赖就成了麻烦:CUDA 不一定有、Python 版本可能冲突、包体积又大。OpenVINO 的路线是把模型编译成 IR(.xml+.bin),运行时只需一个推理库,CPU、核显 GPU、NPU 都能跑同一种 IR。它在 x86 CPU 上针对 CNN 做了指令级优化,老机器上的表现通常比直接跑 ONNXRuntime 更稳,修复这类单张大图任务时,差距主要体现为内存占用和编译期开销更小。
常见做法是“PyTorch → ONNX → OpenVINO IR”两步走。为什么要中间隔一层 ONNX?因为 LaMa 的 PyTorch 图里包含 FFC 这类自定义结构,直接导到 OpenVINO 容易撞上一堆解析问题;ONNX 是更中立的交换格式,把问题切成两段,哪一段出故障就缩小到哪一段排查。.rar里的 demo 如果开箱能跑,大概率已经帮你走完了这两步;你手里只有.pt/.pth权重,那就必须自己补上第三章的转换流程,这一步躲不掉。
2.3 拿到.rar后应当先确认的目录清单与运行环境
解压之后先别急着执行入口脚本,把文件列出来看一遍更稳妥。常见做法是直接跑一条命令:
find . -maxdepth 2 -type f | sort这一步的价值在于:确认模型文件是真实存在而不是空壳,确认入口脚本是.py还是.bat,确认示例图与 mask 是否齐全。如果是 Linux 环境,还要留意文件名里有没有空格,路径乱七八糟的 demo 包大半问题都出在这里。
| 项目 | 建议值 | 原因 |
|---|---|---|
| 模型权重 | 优先.xml + .bin,其次.onnx,最后.pt / .pth | 前两类可直接用 OpenVINO 或 ONNXRuntime 推理,.pt需要补转换步骤 |
| Python 环境 | 3.8~3.11,OpenVINO 用新版本 | 新 APIopenvino.runtime.Core更稳定,旧版本接口差异很大 |
| 依赖库 | opencv-python、numpy、openvino | 推理本身只需要这三个,不必装 PyTorch 和 CUDA |
| 输入输出 | 图片路径 + mask 路径 → 输出图片 | 命令行 demo 一般就是这么封装的 |
确认完目录结构后,打开入口脚本看它是调Core().read_model还是onnxruntime,据此判断缺什么依赖。再把示例 mask 用cv2.imread(path, cv2.IMREAD_GRAYSCALE)读出来看一眼,彩色图必须先转灰度、再二值化。这三件事十分钟内能完成,能省掉后面两小时的排错时间。
3. 把 LaMa 权重转成 OpenVINO IR:ONNX 转换与 reshape 实录
如果.rar里只带了.pt或.pth,转换这一步绕不开。好消息是整个转换不需要 GPU,一台纯 CPU 机器就能完成,耗时通常在一两分钟,前提是 PyTorch 环境能正常加载权重。这里有一个先决条件:模型结构代码要能跑通。Demo 包里如果没有附带模型定义文件,你需要从模型发布方找到对应的网络结构脚本,torch.onnx.export需要结构加权重才能工作。
3.1 从 PyTorch 导出 ONNX:四通道输入与动态轴必须一起定
import torch # 假设你已经按 LaMa 仓库方式加载好模型权重 model = ... # 来自模型的网络结构实例,已经 load_state_dict model.eval() # LaMa 输入是 [B, 4, H, W]: # 前 3 通道是归一化到 [-1, 1] 的 RGB,最后 1 通道是 0/1 mask dummy = torch.randn(1, 4, 512, 512) torch.onnx.export( model, dummy, "big-lama.onnx", input_names=["input"], output_names=["output"], dynamic_axes={ "input": {0: "batch", 2: "height", 3: "width"}, "output": {0: "batch", 2: "height", 3: "width"}, }, opset_version=13, do_constant_folding=True, ) print("onnx 已导出")dynamic_axes是最容易忽略的参数。如果不加,转出来的是固定 512×512,以后想修更大的图就必须重新导出。加了第 0、2、3 维动态后,导入 OpenVINO 时才能通过 reshape 指定任意 batch 和分辨率。opset_version我一般固定为 13,OpenVINO 对这个版本的算子支持最稳,太高可能在旧版本上撞到未实现算子。do_constant_folding可开可关,开了之后图更小,但偶尔会把 BatchNorm 折叠出问题,如果转换报错就关掉再试。
输入通道顺序不能反。如果模型定义里把 mask 作为第二个输入而不是拼接,就把input_names改成两个名字、传入两个 dummy。判断依据是网络 forward 的第一行:如果是torch.cat([x, mask], dim=1),就是单输入四通道;如果是self.encoder(image, mask),就是双输入。这一步搞错,后面 OpenVINO 的输入布局全对不上。
注意:导出前务必把
model.eval()放在前面,否则 BatchNorm 和 Dropout 的状态不确定,导出的图推理结果会随机漂移。
3.2 用 mo 转 IR:FP32、FP16 与压缩选项怎么选
# FP32,CPU 上精度最稳 mo --input_model big-lama.onnx \ --input_shape [1,4,512,512] \ --data_type FP32 \ --output_dir ir_model # 压缩为 FP16,减小体积;新版本中也可以用 ovc 替代 mo mo --input_model big-lama.onnx \ --input_shape [1,4,512,512] \ --compress_to_fp16 \ --output_dir ir_model_fp16--input_shape的作用是固定初始形状。虽然 ONNX 里有动态轴,建议转换时先固定成 512×512,部署阶段再按需 reshape;直接带全动态转会让 mo 生成的 IR 在部分设备上退化成低效图。--data_type FP32是兼容性最好的选择,--compress_to_fp16则把权重压成半精度,体积约小一半,内存占用也更低。LaMa 是像素级生成模型,FP16 的舍入误差在肉眼层面几乎不可见;但如果你后续要做边缘像素级验收,还是留 FP32。
| 选项 | 适用场景 | 注意点 |
|---|---|---|
| FP32 | 验证、调试、追求最大兼容 | CPU 老设备上最稳,不会出现奇怪的半边黑 |
| FP16 | 部署到较新 CPU 或核显 | 部分老 CPU 不支持 fp16 指令,会偷偷转回 fp32 反而更慢 |
如果 mo 命令在旧版本里报参数名不对,检查当前发布版本的转换工具是否已改名为ovc。新版命令格式是ovc big-lama.onnx --compress_to_fp16,转换逻辑一致。转换成功后,ir_model目录下会得到一对.xml和.bin文件,这才是 OpenVINO 真正运行时读取的模型。
3.3 转换产物检查:先用一张小图验证 IR 会不会“翻车”
from openvino.runtime import Core import numpy as np core = Core() model = core.read_model("ir_model/big-lama.xml") compiled = core.compile_model(model, "CPU") # 造一个 512x512 的随机输入,只验证管线是否通 fake_input = np.random.rand(1, 4, 512, 512).astype(np.float32) result = compiled([fake_input]) out = result[compiled.output(0)] print(out.shape, out.dtype, float(out.min()), float(out.max()))这一步的目的不是看效果,是验证 IR 里的算子是否全部被当前设备支持。输出 shape 是[1, 4, 512, 512]、数值范围在[-1, 1]附近,就说明链路是通的。如果报错,看日志里是哪个算子未实现,然后回到导出步骤调整opset_version或do_constant_folding。随机噪声输入下模型输出当然是杂乱图像,这是正常的,不要拿这一步的结果判断 LaMa 本身的修复效果。
core.compile_model(model, "CPU")可以换成"GPU"或"AUTO",但初期不建议开 AUTO,因为设备间切换会掩盖真正的报错。输入端的compiled.input(0)对应导出时的input,输出端的compiled.output(0)对应output,名称如果有多义性,建议在read_model前通过model.inputs打印名字确认。
4. 本地部署 LaMa OpenVINO 推理:CPU/GPU 参数与批量修图脚本
4.1 加载 IR 模型与输入预处理:归一化、mask 尺寸对齐
import cv2 import numpy as np from openvino.runtime import Core class LamaInpaint: def __init__(self, xml_path, device="CPU"): self.core = Core() self.model = self.core.read_model(xml_path) self.compiled = self.core.compile_model(self.model, device) self.input_blob = self.compiled.input(0) self.output_blob = self.compiled.output(0) def preprocess(self, image, mask, size=512): # image: BGR ndarray; mask: 0/255 单通道 image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w = image.shape[:2] image = cv2.resize(image, (size, size), interpolation=cv2.INTER_LINEAR) mask = cv2.resize(mask, (size, size), interpolation=cv2.INTER_NEAREST) # LaMa 预训练使用 [-1,1] 分布 image = image.astype(np.float32) / 127.5 - 1.0 mask = (mask > 127).astype(np.float32) # 通道拼接为 4 通道 NCHW tensor = np.concatenate([image, mask[..., None]], axis=-1) tensor = tensor.transpose(2, 0, 1)[None].astype(np.float32) return tensor, (h, w)中间用COLOR_BGR2RGB是因为 OpenCV 读图默认 BGR,而 LaMa 训练时用的是 RGB 分布。mask 用INTER_NEAREST是必须的,线性插值会产生 0.5 的小数,模型会把半透明边界当作“半遮挡”处理,修复区边缘发虚。等比缩放到 512 时如果原图是 16:9 会压扁人脸,更稳的方案是中心裁剪到方形再缩放,我上面为展示最小链路直接 resize,实际部署时建议先裁剪。
归一化用127.5而不是255,这样才能映射到[-1, 1]。如果导出模型时用的是mean=[0.5,0.5,0.5], std=[0.5,0.5,0.5],那么预处理必须一致;常见翻车是把图像归一化到[0,1],而模型权重是按[-1,1]训练的,结果修复区域整体色调偏灰。
4.2 推理与后处理:只把 mask 区域的像素写回
def inpaint(self, image_path, mask_path, out_path): image = cv2.imread(image_path) mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: raise ValueError("mask 读取失败,检查路径与通道数") tensor, (h, w) = self.preprocess(image, mask) result = self.compiled([tensor])[self.output_blob] pred = result[0].transpose(1, 2, 0) # CHW -> HWC pred = (pred + 1.0) * 127.5 # 从 [-1,1] 恢复到 [0,255] pred = pred[..., ::-1] # RGB -> BGR pred = cv2.resize(pred, (w, h)) pred = np.clip(pred, 0, 255).astype(np.uint8) # 只替换 mask 覆盖区域,保留原图其余部分 mask_up = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST) mask_bool = (mask_up > 127)[..., None] out = np.where(mask_bool, pred, image) cv2.imwrite(out_path, out)输出矩阵第一维是 batch,所以取result[0];通道顺序是 RGB,写入文件前要转回 BGR。为什么要“只替换 mask 区域”?因为 LaMa 只保证被 mask 覆盖区域的重建质量,非 mask 区域经过模型重新生成一遍后,纹理会有细微改变,直接保留原图能避免整张图被修出塑料感。这个写回操作是所有后处理里最关键的策略。
mask_up放大回原尺寸时仍然用 NEAREST,并做>127阈值,避免边缘出现半像素。这里如果偷懒用线性插值,交界处就会留下一圈黑影,也就是大家常说的“修复完边缘发黑”。后处理顺序应该是:先放大 mask,再二值化,最后np.where写回;反过来的话,放大后的灰度值会污染判断结果。
4.3 小批量修复目录里的图片:线程数与流式推理
from pathlib import Path import concurrent.futures def process_one(args): img_path, mask_path, out_path = args LamaInpaint("ir_model/big-lama.xml", "CPU").inpaint( str(img_path), str(mask_path), str(out_path) ) src = Path("imgs") mask_src = Path("masks") out_dir = Path("outputs") out_dir.mkdir(exist_ok=True) tasks = [] for p in src.glob("*.jpg"): mask_p = mask_src / (p.stem + ".png") if mask_p.exists(): tasks.append((p, mask_p, out_dir / p.name)) with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool: list(pool.map(process_one, tasks))每次调用都新建LamaInpaint实例其实很浪费,read_model和compile_model的开销远大于推理本身。更好的做法是把 compiled 模型作为进程内单例,所有 worker 共享;上面这种写法清晰但慢,正式批量脚本里不建议。max_workers=4在 CPU 上不是越多越好,OpenVINO 的 CPU 插件自己会开线程,Python 层再并发,实际上是在抢核心。
推荐的做法是先max_workers=1跑一遍记录单张耗时,然后逐个往上加,找到吞吐不再增长的拐点。GPU 上多 worker 共享一个 compiled 实例反而最稳,多实例并行通常不增吞吐。批处理前务必确认 mask 文件与图片一一对应,不要在流程里把 A 图的 mask 传给 B 图,这种错位在批量模式里很难靠肉眼发现。
4.4 CPU 与 GPU 上的设备和线程参数调节
# CPU 上限制线程数,避免 OpenVINO 内部线程与 Python 并发打架 config = {"CPU_THREADS_NUM": "4", "CPU_PIN_DYNAMIC": "YES"} compiled = core.compile_model(model, "CPU", config)| 场景 | 设备 | 注意点 |
|---|---|---|
| 无独显的服务器或运维机 | CPU | 先设CPU_THREADS_NUM,再试AUTO;大图建议缩到 512 再推理 |
| 笔记本核显 | GPU | 第一次编译有冷启动时间,跑性能测试时要把第一次推理排除 |
| 不确定目标设备 | AUTO | 适合 demo 验证,不适合生产,因为线程参数难控制 |
OpenVINO 的 CPU 插件在 Windows 和 Linux 上的默认线程调度策略不一样,Windows 上偶尔会因超线程调度导致变慢,先固定CPU_THREADS_NUM是最直接的干预手段。GPU 上可以设置GPU_NUM_STREAMS来切换吞吐模式,但 LaMa 这种单张大图任务,吞吐模式带来的收益有限,反而可能增加内存峰值。
这里有个现实经验:真正决定性能的大头永远是图像尺寸。512×512 到 1024×1024,计算量翻四倍,调线程参数的收益远不如把输入尺寸降下来。如果产品需求能接受输出图小于原图,就在预处理里把最长边限制到 640,耗时和内存都会友好很多。
5. 避坑:LaMa + OpenVINO 修复最常翻车的五个细节
5.1 mask 边缘发黑,修复区和原图之间有一圈暗线
现象:输出图里,修复区域的边缘明显比两侧都暗,细看是一条 1~2 像素的黑边。
原因:最常见的是 mask 缩放时用了线性插值,或者在后处理写回时没对放大后的 mask 做二值化。LaMa 训练时只见过硬边界的 0/1 mask,推理时遇到 0.5 之类的小数,会认为这块区域是半遮挡,输出过渡带偏保守,校色后就成了暗边。另一个常见原因是写回时cv2.resize(mask, ...)之后直接参与np.where,没有先做>127的阈值。
解决:预处理和后处理各做一次二值化,两条路径都别省。如果边缘还是发黑,就把 mask 用 3×3 的膨胀核扩大两三个像素,让修复区略微覆盖到原图上,交界处的暗线会被新的修复像素吃掉。这个方法也顺带缓解了手绘 mask 太细导致修复不彻底的问题。
5.2 转 IR 或推理时报 Unsupported operator
现象:用mo转换时直接中断,报某个 onnx 算子不支持;或者编译好的模型放到推理阶段时报Unsupported model。
原因:LaMa 不同分支的模型封装方式差异很大,有的把 mask 和图像 concat 后直接进网络,有的在 forward 里先 upsampling 再拼接。导出的 ONNX 里会出现Resize-11、Upsample的变体,老版本 OpenVINO 对这些算子的支持不全。
解决:先把手上的 OpenVINO 升级到新发布版,再把opset_version降到 12 重新导出。如果依旧报错,用 ONNXRuntime 先跑一遍导出的.onnx,能跑通就说明模型本身没问题,问题一定在算子映射层。不要一上来就怀疑权重损坏,先缩小问题域再动手。
5.3 CPU 推理慢到不可用,一张 512×512 要四十秒
现象:同事机器几秒出图,老服务器要四十秒,完全没法用。
原因:一类是设备确实不支持 AVX512,FP32 算力不够;另一类是每次请求都在新建read_model和compile_model,把编译时间也计入了推理耗时。还有一种隐藏情况:模型初始编译形状是 512,你输入却切成 1024,实际算力需求涨了 4 倍,耗时根本不是线性增长。
解决:把 compiled 模型做成进程单例,初始化一次,后续只调推理。再用 benchmark_app 单独测一次,区分编译时间和推理时间。如果确认是设备算力问题,就把输入降到 384×384,或者用--compress_to_fp16重新转一份 IR。注意 FP16 在老 CPU 上未必更快,硬件不支持时反而会转为 FP32 多绕一层,一定要做对比实验,不要凭感觉选。
5.4 大图修复内存暴涨,4K 图直接把进程吃掉
现象:一张 4000×3000 的图,处理到一半内存涨到 8GB 以上,进程被杀。
原因:LaMa 的 FFC 里傅里叶分支对空间分辨率异常敏感,图像一大,中间特征图体积非常夸张,OpenVINO 的 CPU 推理会把整张特征图留在内存里。加上批量任务同时读入多张 4K 图,每张转 float32 就有几十 MB,堆积起来很容易触顶。
解决:限制同时处理的图像数,用信号量控制并发。真正的高分辨率需求,采用切块修复再拼回的方式;切块时 mask 必须跟着图一起切,每块留 8% 左右的重叠,否则拼缝明显。我一般默认把最长边限制在 768 以内,超过就走切块通道。别指望用 resize 硬压到 512 解决,细节纹理损失可能让修复结果变得不可接受。
5.5 换一台机器解压就报 FileNotFoundError
现象:同事发来的.rar在自己电脑上一切正常,换一台解压后运行直接报找不到big-lama.xml。
原因:demo 脚本里写死了绝对路径,或者模型和脚本不在同一目录;也有可能是.rar解压时保留了带空格的目录名,而启动脚本没做处理。这在 demo 交付里是最常见的问题。
解决:入口脚本里永远用Path(__file__).parent定位文件,而不是os.getcwd()。解压后先跑一次find . -maxdepth 2 -type f,看清楚模型文件的实际位置再运行。这个坑虽小,但我见过太多 demo 死在路径上,顺手就能规避。
6. 进阶:把 Demo 改造成批量修图工具时的验证与量化经验
6.1 交付前先跑一轮固定回归样本
不要拿一张图调通就宣布部署成功。修复模型对 mask 形态非常敏感,同一张图换三种笔刷画 mask,结果可能完全不同。我会准备一个固定样本集:10 张不同场景的图,覆盖室内、室外、人脸、文字、纹理墙,每张配三种 mask:粗块、细线、半透明水印。修复后重点看三条:边缘是否发暗、纹理是否糊成塑料、原图区域有没有被顺带修改。这三条做成检查单,每次调参数后统一重跑,能省掉大量线上反馈回来再返工的时间。
6.2 值得继续投入的三个方向:切块、INT8 量化与自动评估
如果回归样本全部通过,下一步值得投入的方向有三个。第一是切块修复,把大图按 512×512 滑窗切块,每块带重叠,拼合时只在 mask 边缘做羽化,能把 4K 图的内存压到 2GB 以内。第二是 INT8 量化,OpenVINO 的 NNCF 可以对修复类模型做后训练量化,但必须盯住边缘区域,量化最容易丢的是高频纹理;墙面或草地出现色块就退回 FP16。第三是自动评估,预生成 mask 后用原图遮盖再修复,算 PSNR 和 SSIM,用脚本批量对比,而不是天天靠肉眼判断。
这套流程我已经沉淀成了固定习惯:拿到任何修复模型,先转 ONNX,再转 IR,写一个不依赖原框架的推理脚本,最后用统一回归集验收。这个习惯能保证换项目时不被“Demo 能跑,生产不能用”反复折腾,也让我在面对一张陌生图片时,总能快速判断是模型问题、预处理问题还是设备适配问题。希望帮到你。
本文还有配套的精品资源,点击获取