GMCNN 这个模型,玩图像修复的人应该不陌生,OpenVINO 系列的实战也做了不少,但把这两者真正串起来跑一遍的完整记录其实不算多。我之前在接手一个老照片修复 Demo 的时候,正好完整走过一遍从模型转换到推理上线的流程,踩了不少坑,也沉淀下来一套可以直接照抄的步骤。这篇就把整个思路、实操代码和排查记录一并整理出来,给后面要搞图像修复项目的人做个参考。
1. 理解GMCNN图像修复的核心逻辑
1.1 这个模型到底解决什么问题
图像修复(inpainting)说白了就是给图像“补洞”。最常见的两个场景:一是老照片上有划痕、折痕、污渍,要恢复成干净的原貌;二是拍摄时画面里有不想要的人或物体,比如游客入境、垃圾袋入镜,需要把这些物体“抠掉”并让背景自然衔接。
传统修图软件里的内容感知填充,遇到小面积缺损效果还行,但一旦缺损区域大一点、周围的纹理和结构稍微复杂一些,出来的结果就容易发糊、发虚。这里面最大的难点是:被遮挡的上下文是未知的,模型不能只靠边缘插值,而要“凭空生成”一段合理的内容,同时还要和周边光影、色彩、纹理保持连贯。
GMCNN 的全称是 Generative Multi-column Convolutional Neural Networks,核心思路就是通过多列并行卷积网络,从不同尺度的感受野去理解图像上下文,再融合多列特征生成修复结果。说得直白一点:普通模型只用一条路径看图和重建,GMCNN 用好几条并行的路径,每条路径管不同粗细的信息,细的负责纹理,粗的负责结构,最后把这些信息揉在一起输出,修出来的区域就比单路径方法更自然。
我实际测试下来,GMCNN 在移除中等面积物体、修复纯色背景和有一定规律纹理的区域时,效果都挺稳的。尤其适合那种“缺失区域周围有大量可参考上下文”的场景,这也是它后来能成为很多修复项目首选模型的原因。
1.2 GMCNN区别于其他修复模型的关键设计
很多人会把 GMCNN 和基于注意力机制的 Contextual Attention 修复方法放在一起比较。两者目标一样,但路径完全不同。Contextual Attention 的思路是“从已知区域里找相似补丁,通过不相邻区域的匹配来贴补到缺失区”,也就是“复制粘贴 + 优化”,而 GMCNN 更强调“学习如何生成”。
GMCNN 的几个核心设计点:
多列并行结构:若干条并行的卷积分支,每条分支的感受野尺寸不同。感受野小的分支能抓到高频细节,比如发丝、布料的纹理;感受野大的分支能感知全局结构,比如建筑外轮廓、人体姿态。最后通过融合层把这些粗细粒度特征合并,既保住整体结构又具备局部细节。
粗略生成 + 细节补全的两阶段模式:网络先执行一次整体结构的粗生成,再基于粗生成结果去细化纹理,这种 coarse-to-fine 的路径在图像生成领域是比较成熟的套路,GMCNN 把它做得比较扎实。
结构约束与损失函数设计:修复结果要和原始图像已知区域保持一致性,GMCNN 在训练中会计算多个损失,包括像素重建误差、感知损失以及基于对抗生成思想的判别器损失。判别器的存在迫使生成结果在概率分布上更像真实图像,而不是仅仅平滑过渡。
这些设计组合起来,让 GMCNN 在中等复杂度图像的修复上有明显的优势。缺点是模型参数和计算量都不小,直接跑在开发机上可以,要部署到边缘设备或普通 CPU 机器上,不做转换和优化会很吃力——这正是我想聊 OpenVINO 的原因。
2. 为什么偏偏选OpenVINO来跑GMCNN
2.1 模型训练环境与部署环境之间的落差
其实 GMCNN 有公开的 PyTorch 实现,训练和推理都能直接在 PyTorch 环境里跑。那为什么还要多此一举转一遍 OpenVINO?我在真实项目里遇到过几个很现实的问题。
第一,模型跑在 Python + PyTorch 环境里,光是依赖就有几十个包,部署到客户机器上非常痛苦。客户不可能为了看一个修复效果去装一整套深度学习框架。第二,纯 PyTorch 推理在 CPU 上的效率很一般,OpenVINO 团队针对 Intel 的 CPU 做了底层指令集优化,同样的模型,在推理速度上实测能有几倍差距。第三,PyTorch 的动态图机制,即使设置了 eval 模式,推理过程中仍有不少额外开销,对于追求低延迟的服务化场景不够友好。
OpenVINO 这个工具链,其实做的就是“训练框架到可部署格式”的翻译工作。它把模型从 PyTorch/TensorFlow 等形式转换成中间表示格式 IR,再由内部多个硬件插件去执行推理。转换之后,模型依赖大幅减少,还能在 CPU、GPU、NPU 之间灵活切换硬件,这在工程化部署里头是非常实用的能力。
2.2 OpenVINO能给修复任务带来什么具体好处
就我的实践经验来看,改到 OpenVINO 之后,最直观的变化有四个:
- 部署包体量大幅缩小:不再需要捆绑 PyTorch 和一堆 CUDA 依赖,只需要安装 openvino runtime,体积小一个数量级。
- CPU 推理性能提升明显:OpenVINO 会充分利用 CPU 的并行指令集,在无独立显卡的机器上,处理一张中等尺寸的修复图,速度接近实时要求,这在纯 PyTorch CPU 环境下是做不到的。
- 支持量化压缩:可以把 FP32 模型量化到 INT8,尺寸和延迟进一步降低,虽然修复任务对精度敏感,但有些环节的量化实测下来对效果影响不大。
- 硬件切换灵活:同一个 IR 模型,代码里切换一行 device_name 就能从 CPU 换到 GPU 或 NPU,不用为不同硬件维护多套推理代码。
当然,OpenVINO 也不是没有缺点。转换过程中如果模型里面有某些自定义算子,OpenVINO 不一定认识,需要处理兼容性。好在 GMCNN 的主体结构是常规卷积、反卷积和激活函数,基本都在支持范围内,属于比较顺畅的转换对象。
3. 完整实操:从模型转换到推理输出
3.1 环境准备与模型文件获取
我建议直接在干净的虚拟环境里操作,避免和已有项目产生依赖冲突。用 conda 创建环境:
conda create -n gmcnn_openvino python=3.9 conda activate gmcnn_openvino pip install openvino==2023.1.0 torch onnx onnxruntime opencv-python numpy这里有一个细节:OpenVINO 版本迭代很快,不同版本的 API 接口略有差别,我下面给的代码基于 2023.1 之后的 Runtime API,也就是用openvino.runtime.Core这套写法。老版本还在用inference_engine,建议新项目直接上新版。
GMCNN 模型的权重文件,可以通过公开实现仓库的训练脚本自己训练,也可以用一些社区开源的预训练权重。不管从哪里拿到的权重,关键是先确认模型的输入输出结构。我看过几个不同版本的 GMCNN 实现,大致输入都是“原始图像 + 掩膜”。掩膜是单通道图,缺失区域对应的像素值一般为 1,需要保留的区域为 0;输出是修复后的图像,通道数和原图一致。
在我用的版本里,输入尺寸要求是 256x256 或 512x512,模型内部有固定下采样倍数,所以宽高最好能被 8 整除,否则最终输出尺寸和输入对不上。
3.2 用ONNX作为中间格式转换到IR
OpenVINO 本身不直接吃 PyTorch 的.pth权重,最通用的做法是先导出成 ONNX,再用 OpenVINO 的模型转换工具转成 IR 格式(.xml+.bin两个文件)。
导出 ONNX 这一步比较容易踩坑。主要注意两点:一是模型要切换到 eval 模式,关闭 dropout 和 batchnorm 的训练行为;二是输入要给一个实际的示例张量,PyTorch 的 tracing 机制会根据实际输入推导出计算图。
下面是导出 ONNX 的参考代码:
import torch import torch.onnx # 假设已经创建好模型实例并加载权重 # model = GMCNN() # model.load_state_dict(torch.load("gmcnn.pth", map_location="cpu")) model.eval() # 根据模型要求的输入尺寸构造示例输入 batch_size = 1 height, width = 256, 256 dummy_image = torch.randn(batch_size, 3, height, width) dummy_mask = torch.randn(batch_size, 1, height, width) torch.onnx.export( model, (dummy_image, dummy_mask), "gmcnn.onnx", input_names=["image", "mask"], output_names=["output"], dynamic_axes={ "image": {0: "batch", 2: "height", 3: "width"}, "mask": {0: "batch", 2: "height", 3: "width"}, "output": {0: "batch", 2: "height", 3: "width"} }, opset_version=11 )导出成功后,再用 OpenVINO 的 Model Optimizer 工具转换成 IR:
mo --input_model gmcnn.onnx \ --input "image[1,3,256,256],mask[1,1,256,256]" \ --output_dir ./openvino_model这一步如果顺利,会得到gmcnn.xml和gmcnn.bin。如果运气好,连转换命令都不用额外加参数。转换完我建议先用 ONNX Runtime 跑一遍原始 ONNX,确认导出的 ONNX 输出没问题,再做 OpenVINO 转换,这样排查问题的时候能快速定位是转换问题还是后续推理问题。
3.3 OpenVINO推理代码的编写与解释
转换完成后,推理代码其实很短。核心流程包括读取模型、加载输入图片和掩膜、执行预处理、推理、后处理输出。
先看一下完整示例:
import cv2 import numpy as np from openvino.runtime import Core # 初始化 OpenVINO 推理引擎 core = Core() model = core.read_model("./openvino_model/gmcnn.xml") compiled_model = core.compile_model(model, device_name="CPU") # 输入输出节点信息 input_image = compiled_model.inputs[0] input_mask = compiled_model.inputs[1] output_node = compiled_model.outputs[0] # 读取图片和掩膜 image_bgr = cv2.imread("input.jpg") image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) mask_gray = cv2.imread("mask.png", cv2.IMREAD_GRAYSCALE) # 预处理:统一尺寸、归一化 height, width = 256, 256 image_resized = cv2.resize(image_rgb, (width, height)) mask_resized = cv2.resize(mask_gray, (width, height)) image_normalized = image_resized.astype(np.float32) / 255.0 mask_normalized = mask_resized.astype(np.float32) / 255.0 # 增加 batch 维度并调整通道布局为 NCHW input_image_np = np.transpose(image_normalized, (2, 0, 1))[None, ...] input_mask_np = mask_normalized[None, None, ...] # 执行推理 result = compiled_model([input_image_np, input_mask_np])[output_node] # 后处理 output_np = np.transpose(result[0], (1, 2, 0)) output_np = np.clip(output_np * 255.0, 0, 255).astype(np.uint8) output_bgr = cv2.cvtColor(output_np, cv2.COLOR_RGB2BGR) cv2.imwrite("output.jpg", output_bgr)这段代码看起来简单,但有三个地方非常值得展开说说。
第一个是device_name 的选择。CPU是最通用的,但如果你有 Intel 核显或独立显卡,改成GPU往往能获得更快的推理速度。OpenVINO 在 GPU 上跑卷积算子有不错的优化。还可以用VPU或NPU跑在边缘 AI 盒子上,不过需要磁盘空间和驱动支持,一般开发阶段先别折腾。
第二个是输入数据的通道顺序。PyTorch 模型内部按照 NCHW 布局处理张量,而 OpenCV 读图默认是 HWC 布局,而且颜色通道是 BGR。这两层不一致,必须通过np.transpose调成(2, 0, 1)把通道提到第二维,同时把 BGR 转成 RGB。哪怕转错一个,修复结果都会出现严重色偏或者结构错乱。这个问题排除了好久才找到根源。
第三个是掩膜值的含义。不同 GmCNN 代码库对掩膜的定义可能不一样。有的用 1 表示缺失区域,有的用 0 表示缺失区域。在转换和推理前,最好翻一下模型源码里的数据加载逻辑,确认掩膜值描述,不然很可能修复出来的区域黑乎乎一片,完全不是预期效果。我在下面排查部分会专门讲这个。
3.4 关键预处理与后处理细节
预处理部分除了归一化和通道调整,还有两个容易忽略的细节值得单独说。
一是尺寸对齐。GMCNN 内部有多个下采样层,输出尺寸和输入尺寸不一定是简单的对应关系。为了保证最终输出能完整复用原图尺寸,我建议把输入图像等比缩放到模型期望尺寸,推理完成后再把输出结果等比 resize 回原始尺寸。如果原始图是 800x600,强制拉伸到 256x256 会让画面比例失真,修复出来的内容也会变形。
二是掩膜的膨胀与羽化。很多时候外界给的是硬掩膜,边缘非常锐利,直接输入模型,修复结果容易带有明显的“补丁感”。一个简单的缓解手段是在预处理阶段对掩膜做轻度高斯模糊或膨胀,让掩膜边缘有一个渐变过渡。这个技巧虽然不是原论文标准流程,但实测对视觉效果有正面帮助。
后处理部分,常见操作是把输出转回 BGR、截断像素范围,然后针对不确定的修复区域做一个“只替换掩膜区域”的融合操作:把原始图的非掩膜区域和模型输出的修复区域拼在一起,避免模型把未损坏区域也改得面目全非。实现方法其实就是一个np.where:
# 只把掩膜区域的内容替换为模型输出 result_final = original_image.copy() mask_bool = mask_gray_resized > 0.5 result_final[mask_bool] = output_bgr[mask_bool]这样能最大程度保证原始干净区域的细节不丢失,修复区域又获得生成结果。
4. 常见问题与排查技巧实录
4.1 修复区域输出一片黑色或一片模糊
这是最常遇到的问题。第一次跑通时,我也被这个现象搞得头大。黑色输出大概率是掩膜语义反了,模型认为缺失区是已知区,所以把信息填充到了不该填的地方。解决方式很直接:检查掩膜逻辑,将掩膜里 0 和 1 的含义对调试试,再看看输出是否正常。
如果掩膜方向没问题,却依然模糊,那就要怀疑是不是归一化方式不一致。有些模型训练时不是简单x / 255.0,而是使用了带均值和方差的标准化,例如(x - 0.5) / 0.5。预处理必须和训练保持一致,否则输入分布偏离了训练分布,输出必然素质下滑。我习惯先把模型训练代码里的 transform 部分完整读一遍,再反向实现到推理预处理里。
4.2 动态shape导致的推理报错
我在用dynamic_axes导出 ONNX 后,直接转 OpenVINO 会遇到 shape 相关的报错。原因是 OpenVINO 虽然支持动态维度,但在某些 CPU 设备上动态维度会显著降低推理性能,或者干脆不支持某些算子组合。
最省心的方案是转换时固定输入分辨率:
mo --input_model gmcnn.onnx \ --input "image[1,3,256,256],mask[1,1,256,256]"进一步还可以用--data_type FP16或FP32来指定精度。如果项目确实需要支持变长输入,那么建议在推理代码里做一个 auto-resize 环节:先把输入图统一缩放到模型固定尺寸,推理后再缩回去。OpenVINO 里去处理动态 shape 不是不行,但工程复杂度会上升很多,除非真有很强的必要性,否则固定 shape 是更稳妥的选择。
4.3 CPU上推理速度太慢怎么办
如果模型在 CPU 上的推理延迟还是偏高,可以从几个方向优化。
第一,确认 OpenVINO 使用了足够的线程数:
compiled_model = core.compile_model( model, device_name="CPU", config={"NUM_STREAMS": "1", "INFERENCE_NUM_THREADS": "8"} )线程数可以根据实际 CPU 核心数调整,不需要盲目开满,因为线程切换本身也有开销。
第二,把模型导出为 FP16 甚至 INT8。FP16 在 Intel CPU 上的收益不如 GPU 明显,但模型文件体积能缩小一半。INT8 需要用 OpenVINO 的 Post-Training Optimization 工具做校准,耗时多一些,但在 CPU 上的推理加速是实打实的。修复任务中,如果只是个人工具场景,不苛求像素级精度,INT8 的表现完全可以接受。
第三,使用预处理 API。OpenVINO 支持直接在建图阶段绑定输入的缩放、布局转换等操作,这样可以在推理内部完成部分预处理,但需要注意这并不会显著减少总体耗时,只是让代码更整洁。
4.4 转换时遇到不支持的算子
GMCNN 大多数算子都是常规算子,但有些实现里含有自定义 attention 模块或某种特殊上采样方式,用新版 OpenVINO 转换时偶尔会报 “Unsupported operation”。这种时候我的处理流程是:
- 先升级 OpenVINO 到最新版本,新版本支持的算子覆盖面越来越广。
- 如果依然不支持,考虑替换模型实现,或者把自定义模块在 Python 里拆出去,只用 OpenVINO 跑主干部分。
不过说实话,经过这么多轮 OpenVINO 迭代,GMCNN 这种结构基本都能被新版本识别,遇到算子不支持的概率已经很低了。真要遇到了,大概率是模型实现本身不是标准的 GMCNN,而是加了很多花式改造的变种,这时候优先想想是否非得用这个变种。
5. 实践中的一些经验
这个项目跑完,我最大的感受是:模型本身的算法设计固然重要,但从“能出效果”到“能稳定部署”之间,有一条很长的路要走。OpenVINO 在这条路上帮我省掉了很多基础设施层面的工作,但该做的预处理、掩膜逻辑确认、尺寸策略,一样都不能少。
有几个值得记住的小经验:
- 拿到权重后,不要急着写推理代码,先把模型源码里的输入输出定义、预处理逻辑、掩膜语义读清楚。这一步花 30 分钟,能省后面三天排查时间。
- 任何模型转换过程,都建议保留一个 “原始框架推理结果”作为基准。我在做 OpenVINO 转换的时候,习惯性地先跑一遍 PyTorch 原模型,记录输出图像,再用 OpenVINO 跑同一个输入,对比两张图的误差。如果误差很小,说明转换没问题,后面调试可以聚焦在业务逻辑上。
- 如果只是做一个小工具或个人 Demo,固定输入尺寸完全够用;如果要做服务端接口,建议把图像缩放、掩膜预处理都封装成独立函数,接口只负责传入原图和掩膜,内部完成所有转换和融合,这样代码结构清晰,也方便后续接入异步处理。
GMCNN 本身的能力边界也很明显:它对大面积的纯空白区域、复杂透视场景的修复效果不算特别理想。想进一步压榨效果,可以把 GMCNN 和边缘检测、语义分割等前置信息结合,给生成过程提供更强引导,或者用更大分辨率训练版本。OpenVINO 这块做好了准备,后面换更强的模型,也只是换一下中间表示和推理配置的事。