ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LaMa图像修复与OpenVINO实战:从模型转换到CPU部署避坑指南

LaMa图像修复与OpenVINO实战:从模型转换到CPU部署避坑指南 简介一套面向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、膨胀掩码与感受野LaMaLarge Mask Inpainting在图像修复圈子里站稳脚跟靠的是对大面积遮挡的处理能力。传统扩散类算法从洞边缘向内推空洞一大纹理和结构信息就在传播途中衰减最后墙不像墙、草不像草。LaMa 的做法是让模型“看得更远”骨干网络里嵌入了 FFCFast 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.11OpenVINO 用新版本新 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 IRONNX 转换与 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_version13, do_constant_foldingTrue, ) print(onnx 已导出)dynamic_axes是最容易忽略的参数。如果不加转出来的是固定 512×512以后想修更大的图就必须重新导出。加了第 0、2、3 维动态后导入 OpenVINO 时才能通过 reshape 指定任意 batch 和分辨率。opset_version我一般固定为 13OpenVINO 对这个版本的算子支持最稳太高可能在旧版本上撞到未实现算子。do_constant_folding可开可关开了之后图更小但偶尔会把 BatchNorm 折叠出问题如果转换报错就关掉再试。输入通道顺序不能反。如果模型定义里把 mask 作为第二个输入而不是拼接就把input_names改成两个名字、传入两个 dummy。判断依据是网络 forward 的第一行如果是torch.cat([x, mask], dim1)就是单输入四通道如果是self.encoder(image, mask)就是双输入。这一步搞错后面 OpenVINO 的输入布局全对不上。注意导出前务必把model.eval()放在前面否则 BatchNorm 和 Dropout 的状态不确定导出的图推理结果会随机漂移。3.2 用 mo 转 IRFP32、FP16 与压缩选项怎么选# FP32CPU 上精度最稳 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, deviceCPU): 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, size512): # image: BGR ndarray; mask: 0/255 单通道 image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w image.shape[:2] image cv2.resize(image, (size, size), interpolationcv2.INTER_LINEAR) mask cv2.resize(mask, (size, size), interpolationcv2.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), interpolationcv2.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_okTrue) 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_workers4) as pool: list(pool.map(process_one, tasks))每次调用都新建LamaInpaint实例其实很浪费read_model和compile_model的开销远大于推理本身。更好的做法是把 compiled 模型作为进程内单例所有 worker 共享上面这种写法清晰但慢正式批量脚本里不建议。max_workers4在 CPU 上不是越多越好OpenVINO 的 CPU 插件自己会开线程Python 层再并发实际上是在抢核心。推荐的做法是先max_workers1跑一遍记录单张耗时然后逐个往上加找到吞吐不再增长的拐点。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 要四十秒现象同事机器几秒出图老服务器要四十秒完全没法用。原因一类是设备确实不支持 AVX512FP32 算力不够另一类是每次请求都在新建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 能跑生产不能用”反复折腾也让我在面对一张陌生图片时总能快速判断是模型问题、预处理问题还是设备适配问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表