
简介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中中中只需要 RuntimeOpenVINO 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 IRLaMa 的常见导出方式是先把 PyTorch 权重转成 ONNX再用 OpenVINO 的 Model Optimizer 转成 IR。转换命令本身不复杂但输入形状必须和推理脚本保持一致。一般输入是四通道RGB 图像加一个掩膜我习惯在导出 ONNX 时就统一成1x4xHxW这样后面转 IR 不用再改动态轴。# 常见导出脚本长这样重点看 input_names 和输出张量 import torch model torch.load(lama.pt, map_locationcpu) model.eval() dummy torch.randn(1, 4, 512, 512) torch.onnx.export( model, dummy, lama.onnx, input_names[input], output_names[output], opset_version11, dynamic_axes{input: {0: batch}, output: {0: batch}} )这段导出脚本把模型接到 ONNX。dynamic_axes只留 batch 维度动态避免后续 OpenVINO 处理动态空间维度时容易出现算子不兼容。512x512是很多 LaMa 变体的默认输入尺寸如果你的包没有明确说明先按这个尺寸测通再调。导出完成后用 OpenVINO 命令行转 IRmo --input_model lama.onnx --input_shape [1,4,512,512] --output_dir ./irmo是 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, interpolationcv2.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 或 1LaMa 输入里掩膜必须是二进制不是 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, iterations2)iterations2对应大约 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), fshape 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, deviceCPU, batch_size1): 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), dtypenp.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_STREAMS1反而更快因为避免线程切换开销对于批量设置成物理核心数的一半是我尝试过的稳健起点。验证输出时不要只看肉眼看几张。把修复后结果和原图都存下来如果原图没有原始“干净版”那至少算一算修复区域周围的均值与方差看是否和整体纹理一致。更严格的做法是准备测试集用 PSNR 和 SSIM 作为回归指标每改一次预处理参数就重跑一遍防止“这周效果好了下周换数据全崩”。我现在做修复任务已经习惯把模型路径、输入尺寸、pad 策略、掩膜膨胀次数全部塞进一个 YAML 配置文件出问题直接翻配置而不是改代码。某次批量处理时没有备份掩膜统一膨胀参数导致一张低分辨率图标被修出毛边从那以后再也不敢把掩膜参数写死在推理脚本里。前期多花十分钟把校验脚本写好后期能避开很多看不出来的纹理失真。希望帮到你。本文还有配套的精品资源点击获取