
简介面向计算机视觉开发者的 LaMa 图像修复 OpenVINO Demo 资源包提供基于 OpenVINO 工具套件的图像修复示例适合对图片编辑、老照片修复、遮挡物去除等场景感兴趣的中高级开发者。资源共 383 个文件压缩包约 831MB主要包含 149 个 dll 运行库、66 个 xml 配置、2 个 onnx 推理模型、9 个 C# 源码文件以及配套的解决方案与项目工程同时涵盖 28 个 txt 说明文档和 5 个图片素材用户可按需加载模型并直接体验 LaMa 在 OpenVINO 环境下的修复效果。目前已有 251 人学习下载适合作为快速搭建 OpenVINO 图像修复环境、理解模型转换与推理流程的参考范本。资源内目录结构完整从模型文件到可执行程序均有收录便于二次开发与调试尤其适合需要将 LaMa 部署到 Intel 平台的工程实践。1. 不是修图软件的橡皮擦LaMa 图像修复和 OpenVINO 这套组合到底解决什么第一次用 LaMa 修图时我以为这不过是又一个图像修复 demo 程序——在图上画个框模型把框里的内容补出来。真正完整跑一遍才发现LaMa 的关键优势在“大块缺损”你直接把照片里一个人框掉它能把背后的墙、地面、影子按透视结构续下去而不是模糊成一块色斑。这个表现来自它的快速傅里叶卷积FFC而不是普通 GAN 那种局部感受野硬推。标题里的 OpenVINO 则是另一层问题模型再能修也要能落到用户机器上跑。用 OpenVINO 做推理发布可以绕开对方机器上的 PyTorch 环境同时拿到 Intel CPU 和核显的加速收益。这篇文章按我实际搭 LaMa OpenVINO 修复 demo 的路径来写先讲为什么选 LaMa、OpenVINO 在中间扮演什么角色再给完整的模型导出流程随后是一个最小可跑的命令行修复 demo最后是换到 OpenVINO 之后我踩过的几个坑。适合两类读者一类是做去水印、旧照片修复、物体移除的工程师另一类是想在自己项目里集成图像修复模型、但不想给客户机器装整套深度学习环境的部署派。2. 为什么图修复选 LaMa又为什么把推理挪到 OpenVINO2.1 大 mask 攻坚LaMa 的三个核心设计图像修复模型很多从最老的 PatchMatch到后来基于 GAN 的 DeepFill、EdgeConnect、Free-Form 系列。LaMa 之所以在“物体移除”这个场景里成为首选是因为它把训练和推理都瞄准了“大 mask”这个硬骨头。第一是用大感受野的 FFC 替换普通卷积。普通卷积的感受野要靠层数堆堆到 50 层也只能看到局部。FFC 在频域做全局运算一张 512×512 的图卷积核只到 3×3却能把整张图的低频结构都纳入当前像素的计算范围。这样墙的走向、地面的透视、光源方向这类全局信息不会因为 mask 太大就断掉。第二是训练时 mask 的尺度和形状非常激进。LaMa 训练集的 mask 最大可以覆盖图像的多半个区域而且有多种多边形、笔触、矩形组合。模型被逼着在没有局部上下文的情况下靠全局结构和纹理先验来补训练出来的模型对“大洞”天然更稳。第三是输出是 UNet 结构配合感知损失而不是简单 L1/L2。L1 会让输出偏向保守细节发糊感知损失保证补出来的纹理在 VGG 特征空间里和周围一致视觉上“像”周围环境。推理时它的输入输出很简单输入一张 RGB 图和一个单通道 mask两者在通道维拼接成 4 通道输出一张等尺寸的修复图。这个“拼接”设计至关重要后面导出和推理都要保持和训练完全一致差一点都不行。2.2 OpenVINO 在 Demo 里的角色运行时正确而不是效果提升要明确一点OpenVINO 不改变模型效果它只是推理运行时。同样的 IR 模型和同样的输入OpenVINO 输出的浮点数结果和 PyTorch 会有微小浮点差异但肉眼不可见。那为什么不在 demo 里直接跑 PyTorch最常见的原因是环境。PyTorch 的部署依赖太重torch、torchvision、CUDA 或一堆编译库体积几个 GB版本一换就极容易 incompatible。OpenVINO 的运行时本体轻很多模型固化之后不需要 PyTorch也不需要网上下权重。用户机器上只要装一个 openvino 的 Python 包或者把 demo 和 OpenVINO 的 runtime 一起打包就能跑。第二个原因是 Intel 设备上有可用的性能抓手。CPU 上可以控制INFERENCE_NUM_THREADS有核显的机器还能直接deviceGPU走 OpenCL。我一般会先锁 CPU 跑通正确性再考虑要不要上核显。这里要提前打个预防针OpenVINO 的GPU指 Intel 核显或 Arc 独显不是 N 卡这是个非常容易误会的点。第三个原因其实和 demo 的定位有关。demo 程序和原型最本质的区别是原型证明“这个思路能出活”demo 要让人照着跑且结果稳定。PyTorch 推理里常见的torch.no_grad()、autocast、动态图随机性在 demo 里都是潜在翻车点。OpenVINO 的 IR 把网络结构固化下来运行时行为收敛得多适合作为 demo 程序的推理后端。2.3 一个可复用的 LaMa OpenVINO demo 通常包含什么我整理一个修复 demo 目录时习惯按“模型产物、交互工具、推理入口、文档”四块放。下面是文件清单同时也是你复现这个方案的目标产物文件/目录作用说明ov_model/lama_512.xml.binOpenVINO IR 模型Python 侧直接加载 .xml 即可make_mask.py交互式 mask 画笔用鼠标在图片上涂出待修复区inpaint_demo.py推理主程序加载 IR预处理推理回贴保存requirements.txt依赖清单opencv-python、openvino、numpytestdata/测试图集固定样本用于回归验证README.md参数与命令记录导出和调参步骤第 3 章到第 4 章就按这张表的内容走。先解决最硬的环节怎么把 LaMa 模型从 PyTorch 格式转成 OpenVINO 能加载的 IR。3. 把 LaMa 从 PyTorch 变成 OpenVINO IR导出脚本与模型优化3.1 固定推理入口导出前把模型包一层LaMa 的公开训练实现里推理时会有torch.no_grad()、可能还有 autocast、有后处理分支。直接拿整个推理函数去做 ONNX 导出通常会在一些无关的算子上报错。我一般会单独写一个导出用的 wrapper把“输入”和“输出”固定成最小集合。# lama_export_wrapper.py import torch import torch.nn as nn class LamaExportWrapper(nn.Module): 导出专用包装层把 image mask 拼成 4 通道后进生成器。 注意这里 mask 的语义255 表示待修复区域0 表示保留区域。 训练代码里的 mask 范围是 0~1所以这里统一除 255。 def __init__(self, generator): super().__init__() self.generator generator def forward(self, img, mask): # img: (1, 3, 512, 512) float32, 值域 0~1 # mask: (1, 1, 512, 512) float32, 值域 0~255 mask_norm mask / 255.0 x torch.cat([img, mask_norm], dim1) return self.generator(x)这步的核心逻辑是不要在导出脚本里再写一遍预处理。图片归一化、mask 归一化、通道拼接全部收进 forward这样导出后的 ONNX 模型输入就是最直观的img和mask两个张量之后用 OpenVINO 推理时也用同一个规则。常见错误是导出时输入是 0~255推理时却手滑除以 255 两遍——这类问题非常难一眼看出来所以统一到 wrapper 里最安全。参数说明输入尺寸我固定成(1, 3, 512, 512)和(1, 1, 512, 512)。512 是分辨率LaMa 官方在 512 上训练输入略大或略小都能修但固定 512 的导出会让 IR 的体积、内存和首次编译时间都可控适合 demo。如果你需要处理大图后面第 6 章会讲动态 shape 的取舍。3.2 导出 ONNX一次只导出不连推理wrapper 写完后再导出。下面这段脚本里的get_generator()指代你从训练权重里构造出生成器的方式不同来源的代码这个函数名不同但流程是一样的。# export_onnx.py import torch from lama_export_wrapper import LamaExportWrapper from your_lama_checkpoint import get_generator # 替换成你的模型加载函数 generator get_generator( model_pathbig-lama.pt, devicecpu, ).eval() wrapped LamaExportWrapper(generator) dummy_img torch.zeros((1, 3, 512, 512), dtypetorch.float32) dummy_mask torch.zeros((1, 1, 512, 512), dtypetorch.float32) torch.onnx.export( wrapped, (dummy_img, dummy_mask), lama_512.onnx, input_names[img, mask], output_names[inpainted], opset_version12, dynamic_axesNone, do_constant_foldingTrue, ) print(export done)这段代码的关键参数有三个。第一个是opset_version我这里写 12但如果你导出时报了和 FFT、aten::fft相关的不支持算子错误就把版本升到 17 或更高再试。LaMa 内部的快速傅里叶卷积涉及复数运算老 opset 对复数算子支持不全这是 LaMa 导出最常见的卡点。第二个是dynamic_axesNone这等于把模型的所有维度都固化成静态 shape换取发布环境的稳定性。第三个是input_names要和 wrapper 的 forward 参数名一一对应后面 OpenVINO 加载时按名字取输入名字对上才不会乱。导出成功后你会得到一个lama_512.onnx体积和原始权重差不多不要惊讶ONNX 只是换了格式没有压缩。3.3 mo 转 IROpenVINO 模型优化器的参数与验证ONNX 拿到之后用 OpenVINO 自带的mo模型优化器转 IR。如果你装的是 OpenVINO 工具套件先激活环境变量再跑下面命令source /opt/intel/openvino_2024/bin/setupvars.sh mo \ --input_model lama_512.onnx \ --input img,mask \ --input_shape [1,3,512,512],[1,1,512,512] \ --output_dir ov_model \ --data_type FP32--input里的img,mask顺序要和 ONNX 输入名一致--input_shape也用逗号分隔对应两组 shape。这一步很多人踩坑不写--inputmo 可能按自己的排序重排输入后面 OpenVINO 推理时用 dict 按名字传都救不回来所以一定要显式写。--data_type FP32是保推理质量的选择虽然 IR 可以转 FP16 减小体积但图像修复的输出对精度比分类任务敏感demo 阶段别贪这个。转换完成后检查一下python -c import openvino as ov; coreov.Core(); mcore.read_model(ov_model/lama_512.xml); print([i for i in m.inputs], [o for o in m.outputs])能正常打印出两个输入和一个输出IR 就没问题。到这一步模型侧的准备工作全部完成接下来写 demo 程序。4. 写一个最小可跑的 OpenVINO LaMa 修复 demo命令行版4.1 交互式 mask 画笔先在图上圈出要修的区域修复的输入除了原图之外必须有 mask。mask 是单通道图白色区域255表示“这里需要补”黑色区域0表示“原样保留”。做一个可用的 demo 至少要提供一个最原始的交互方式用鼠标在图上涂白。OpenCV 的setMouseCallback就够用。# make_mask.py import cv2 import numpy as np def paint_mask(src_path): 左键画白色遮罩中键清空Esc 保存并退出。 img cv2.imread(src_path) mask np.zeros(img.shape[:2], dtypenp.uint8) def on_mouse(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: # 圆形笔刷半径 60按住左键拖拽连续画 cv2.circle(mask, (x, y), 60, 255, -1) elif event cv2.EVENT_MBUTTONDOWN: mask[:] 0 # 中键清空重画 cv2.namedWindow(mark mask) cv2.setMouseCallback(mark mask, on_mouse) while True: preview img.copy() preview[mask 255] (0, 0, 255) # 用红色显示待修复区 cv2.imshow(mark mask, preview) key cv2.waitKey(30) if key 27: # Esc break cv2.destroyAllWindows() return img, mask if __name__ __main__: cv2.imwrite(mask.png, paint_mask(input.jpg)[1])这段代码里有几个细节值得说。第一mask 直接用单通道np.zeros创建不要创建成三通道再取第一通道后面拼接和 OpenVINO 输入都更省事。第二圆刷半径 60 是相对 1080P 级别图片的经验值窗口缩小后画起来偏粗但你可以在函数开头加一个radius参数按图片宽度比例计算比如radius img.shape[1] // 20。第三预览图把 mask 区域染红这能直观确认你画的区域是否覆盖完整mask 画小了修复区域边缘会有半截残留画大了则会吞掉不该动的背景。4.2 修复管线预处理、推理、回贴三步mask 画完后进入核心的推理类。这里尤其要注意所有预处理都要和导出时保持一致图片除 255、mask 除 255、通道直接拼接在模型内部完成。下面是完整可跑的推理类。# run_ov_lama.py import cv2 import numpy as np import openvino as ov class LamaOVInpainter: def __init__(self, xml_path, deviceCPU): core ov.Core() self.model core.read_model(xml_path) self.compiled core.compile_model(self.model, device) self.inputs list(self.model.inputs) def inpaint(self, img_bgr, mask, work_size512, dilate_kernel11): img_bgr: BGR 原图任意尺寸 mask: 单通道 0/255 掩码与原图同尺寸 work_size: 送入模型的短边长度 dilate_kernel: mask 向外膨胀的核大小控制边缘过渡 h, w img_bgr.shape[:2] # 1. 统一缩放 resized_img cv2.resize(img_bgr, (work_size, work_size)) resized_mask cv2.resize(mask, (work_size, work_size), interpolationcv2.INTER_NEAREST) # 2. mask 二值化和膨胀 _, resized_mask cv2.threshold(resized_mask, 127, 255, cv2.THRESH_BINARY) if dilate_kernel 0: kernel np.ones((dilate_kernel, dilate_kernel), np.uint8) resized_mask cv2.dilate(resized_mask, kernel) # 3. 转模型输入 inp resized_img.astype(np.float32) / 255.0 inp np.transpose(inp, (2, 0, 1))[None] # (1,3,H,W) msk resized_mask.astype(np.float32)[None, None] # (1,1,H,W) # 4. 推理按输入名传参 results self.compiled({self.inputs[0]: inp, self.inputs[1]: msk}) out_tensor list(results.values())[0][0] # (3,H,W) out_img np.transpose(out_tensor, (1, 2, 0)) out_img np.clip(out_img * 255.0, 0, 255).astype(np.uint8) # 5. 回贴只在原 mask 区域替换其余保留原图 out_full cv2.resize(out_img, (w, h)) mask_full cv2.resize(mask, (w, h), interpolationcv2.INTER_NEAREST) final img_bgr.copy() final[mask_full 0] out_full[mask_full 0] return final第 1 步的work_size是速度与质量的平衡点。512 是导出时的固定输入如果你用 768 甚至 1024对边缘纹理更友好但 CPU 上推理时间可能翻倍而且这个模型本身不是无限分辨率鲁棒喷到 1024 以上收益很小。第 2 步的dilate_kernel非常关键模型预测的修复结果在 mask 边缘往往有点生硬把 mask 向外扩 11 个像素让模型的输出区域覆盖到一部分原始背景回贴时边缘过渡会更自然。第 4 步用self.inputs[0]、self.inputs[1]而不是写死的字符串是为了避免不同导出方式下输入名的大小写或前缀差异。list(results.values())[0]是兼容 OpenVINO 新旧版本返回结构的写法。4.3 命令行参数和一次完整跑通光有类还不够 demo 用加个命令行入口把可调项全部暴露出来。# 接在上面的 inpaint_demo.py 末尾 if __name__ __main__: import argparse parser argparse.ArgumentParser() parser.add_argument(--xml, requiredTrue, helpIR 模型 .xml 路径) parser.add_argument(--input, requiredTrue, help输入图片) parser.add_argument(--mask, default, help已有 mask 图不传则用画笔交互) parser.add_argument(--output, defaultout.png) parser.add_argument(--device, defaultCPU) parser.add_argument(--size, typeint, default512) parser.add_argument(--dilate, typeint, default11) args parser.parse_args() img cv2.imread(args.input) if args.mask: mask cv2.imread(args.mask, cv2.IMREAD_GRAYSCALE) else: _, mask paint_mask(args.input) worker LamaOVInpainter(args.xml, deviceargs.device) result worker.inpaint(img, mask, work_sizeargs.size, dilate_kernelargs.dilate) cv2.imwrite(args.output, result)命令行跑法python inpaint_demo.py \ --xml ov_model/lama_512.xml \ --input testdata/room.jpg \ --mask mask.png \ --output result.png \ --device CPU这个跑法有一个隐藏优点--mask和交互模式分开了所以你可以提前把 mask 用程序生成比如自动检测水印区域然后批量修复。把“选 mask”和“修复”拆成两条命令是 demo 程序沉淀成批处理工具的必经一步。5. 换到 OpenVINO 之后踩过的坑现象、原因与修改方式5.1 修复区像被水彩笔涂过边缘发灰现象是模型输出的修复区域并不丑但和周围色差明显边缘处还有一圈灰边尤其浅色墙面上非常扎眼。百思不得其解。原因出在 mask 的数值语义。我第一次接的时候mask 读进来是 0/255但我在拼通道之前顺手写了一句mask mask.astype(np.float32)没除 255。模型训练时 mask 的分布是 0~1你把 0~255 直接喂进去等于告诉模型“这些区域半损坏”输出当然整体偏灰。后面的灰边则是 mask 用了cv2.resize默认的线性插值边缘变成半透明灰阶导致的。解决方式是两件事一起做统一走mask / 255.0并且 mask 的缩放强制用cv2.INTER_NEAREST。图像可以线性插值mask 不行mask 是语义标签插值出来的灰阶会让模型糊涂。5.2 推理时报输入 shape 对不上现象OpenVINO 报错类似Input shape [1,1,512,512] does not match [1,3,512,512]或者是 dims 数量差一。原因OpenVINO 的输入要求 NCHW而 opencv 读进来的图是 HWC。预处理里忘了np.transpose(inp, (2, 0, 1))就会出这个问题另一种情况是灰度 mask 忘了加[None, None]扩充成 4 维直接以 (512, 512) 传入。解决方式是在推理前加个形状断言出了问题第一时间暴露而不是等 OV 报晦涩错误assert inp.shape (1, 3, work_size, work_size), inp.shape assert msk.shape (1, 1, work_size, work_size), msk.shape这个断言看起来低级但在调试 OpenVINO 程序时真的很救命因为 OV 的 shape 报错经常不是发生在最直观的位置。5.3 导出 ONNX 报 unsupported FFT 算子现象torch.onnx报类似Exporting the operator fft to ONNX opset version ... is not supported位置定位到 LaMa 的 FFC 模块。原因LaMa 的快速傅里叶卷积用torch.fft.rfft做频谱变换ONNX 对复数运算的支持一直滞后老版本 opset 下根本没有对应的算子映射。新 torch 配新 opset 才有版本没对上就翻车。解决方式分两步走先把opset_version升到 17再更新 torch 到 2.x。如果还是不行不要急着改模型代码先隔离确认报错确实在 FFT 分支常见的误判是把 bicubic 插值算子的报错也当成 FFT 问题。一个便宜的替代方案如果你实在导出不了 ONNX可以先用 PyTorch 把权重算成一组输入输出对然后绕路校准——但这步会破坏端到端管线我不推荐在 demo 阶段花这个精力。5.4 同一张图两次跑结果耗时差几十倍现象demo 第一次跑一张图要 2 秒关掉重开同一张图只要 0.3 秒。换个目录或改个参数又回到 2 秒。原因OpenVINO 在 CPU 上首次推理会做运行时子图编译和缓存第二次直接命中缓存所以快。这是正常的不是 bug。另一个拖慢的因子是 OpenMP 线程抢占默认线程数等于物理核数如果你的 demo 同时开着窗口线程和推理线程反而互相抢核。解决方式在compile_model前限定线程数并记录首次编译开销到日志。core ov.Core() core.set_property(CPU, {INFERENCE_NUM_THREADS: 4}) compiled core.compile_model(model, CPU)同时如果你反复调模型文件建议给compile_model传一个cache_dir参数OpenVINO 会把编译缓存写到磁盘二次加载直接读缓存多进程部署下收益尤其明显。5.5 换成--device GPU之后更慢甚至直接报错现象按文档把device改成GPU有些环境直接报cannot find a kernel有些环境能跑但比 CPU 还慢耗时翻倍。原因OpenVINO 说的GPU是 Intel 核显或 Arc 独立显卡走 OpenCL 后端。如果你的 CPU 有核显但驱动没装好或者系统是服务器版根本没核显core.available_devices里就不会有GPU。而另一部分机器核显性能本就不强处理这种 CNN 模型不一定比 CPU 快。解决方式让设备名可配置代码里先侦察再选择。print(core.available_devices)如果期望设备不在列表里就把device回落到CPU并打一行 warning 而不是崩掉。这里有个现实经验demo 程序要面对不同用户机器设备自动回退是保命设计别在代码里写死加速设备。6. 让 demo 变成你能复用的工具回归验证与三处可调参数demo 程序能跑是一回事能长期复跑是另一回事。我通常会做一件看起来很笨的事建立固定测试集每次改代码或改模型都跑一遍并记录指标。准备三到五张代表性图片一张大面积墙面的、一张有复杂纹理植被的、一张人物照。对每张图固定 mask修复后与 ground truth 计算 PSNR 和 SSIM只统计 mask 区域内。没有 ground truth 的话哪怕只记录“修复区均值灰度”和“边缘最大梯度”也比没有强至少能发现代码改动手滑引入的全图发暗或发灰。回归脚本很简单核心几行# regress.py 片段 psnr cv2.PSNR(gt_roi, pred_roi) # 只裁剪 mask 区域比较 ssim compare_ssim(gt_roi, pred_roi, multichannelTrue)参数调整方面有三个旋钮我建议有限度开放。第一是dilate_kernel我默认 11面对建筑直线边缘调到 15 能让接缝藏得更深但植被这类无规则边缘超过 11 反而开始吞细节。第二是work_size不是越大越好512 到 768 之间收益明显超过 1024 反而模型没见过那么大的输入分布光影容易失真。第三是线程数固定INFERENCE_NUM_THREADS4在大多数机器上比默认值稳定但这在有两路 CPU 的机器上是最优值纯笔记本建议试一下再定。我最后落一个习惯正式发布 demo 前一定在“没装 PyTorch、只有 openvino 和 opencv 的干净 Python 环境”里完整跑一遍导出后的流程测的机器最好是集成显卡的办公本——那是大多数使用者手里机器的真实性能。这一步能过滤掉九成环境依赖问题。这个方向还很新随手拈来就能修图但真正值得投入的是把“固定导出流程 可调 mask 回归测试”沉淀成你自己团队可复用的工具链。希望帮到你。本文还有配套的精品资源点击获取