LivePortrait ONNX Runtime 部署:C++与Python双语言推理实战 简介这份资源面向希望在人像动画生成方向落地的开发者与算法工程师提供基于onnxruntime推理引擎部署LivePortrait的完整程序同时给出C与Python两套实现路径解决从模型推理到工程集成的问题。压缩包共14个文件约459KB以4个cpp源文件、3个h头文件与2个py脚本为核心配合CMakeLists.txt构建配置、README说明文档以及示例图片、模板与演示视频等素材覆盖人脸检测、裁剪对齐、动画驱动等模块便于对照理解整体流程。目前已有204人学习下载。读者可据此获得可直接编译运行的工程骨架、Python快速验证脚本与C部署参考掌握onnxruntime加载模型、前后处理与跨语言调用的排错思路适合具备一定深度学习与C基础、需要将人像动画能力集成到自有项目中的读者参考。1. 从一张照片到一段视频LivePortrait 的 ONNX Runtime 部署到底难在哪你手里有一张人像照片想让她的眼睛眨起来、嘴角动起来甚至跟着一段驱动视频做出同步表情——LivePortrait 就是干这个的。它把源人像的关键点、表情系数、头部姿态解耦成几组隐式表示再用一个轻量生成器逐帧渲染效果比早期的一阶运动模型自然得多。但真正落到工程里问题不在模型本身而在推理引擎PyTorch 权重动辄几百 MB依赖 CUDA 版本、torch 版本、Python 版本三者严格对齐换一台机器就翻车。ONNX Runtime 的价值就在这里——把模型导出成.onnx后C 和 Python 都能用同一份权重CPU 上也能跑出可接受的帧率部署体积和依赖复杂度直接降一个量级。这篇笔记面向两类人一是想把 LivePortrait 集成进自己桌面端或服务端的 C 工程师二是用 Python 快速验证效果、再决定要不要转 C 的算法同学。下面从模型拆解、环境搭建、双语言推理到踩坑排查一步步走完。2. 拆解 LivePortrait 的 ONNX 模型图哪些子模型必须导出哪些可以省2.1 五个核心子模型与它们的输入输出LivePortrait 的推理链路不是单个模型而是一组分工明确的子网络。常见做法是把它拆成五个 ONNX 文件分别对应不同阶段子模型作用典型输入典型输出appearance_extractor提取源人像的外观特征源图 256x2563x64x64 特征图motion_extractor提取驱动视频的隐式关键点驱动帧 256x256关键点 Jacobianwarping_network根据运动变形外观特征外观特征 运动变形后特征spade_generator生成最终人脸变形特征512x512 RGBstitching_network把生成人脸贴回原图原图 生成脸完整帧导出时最容易犯的错是把 motion_extractor 的输入尺寸写死。LivePortrait 原版支持动态分辨率但 ONNX 导出如果不开 dynamic axes驱动视频换分辨率就会报 shape mismatch。我一般会在导出脚本里显式指定dynamic_axes把 batch 和 spatial 维度都放开。2.2 用 Python 导出 ONNX 的最小脚本导出是整个流程的地基这一步错了后面 C 怎么调都是白费。下面这段脚本假设你已经拿到官方 PyTorch 权重放在./checkpoints下import torch import torch.onnx from liveportrait import AppearanceExtractor, MotionExtractor # 加载 PyTorch 权重注意 map_location 用 cpu避免导出时依赖 GPU appearance AppearanceExtractor().eval() appearance.load_state_dict(torch.load(./checkpoints/appearance.pth, map_locationcpu)) # 构造 dummy input尺寸必须和推理时一致 dummy torch.randn(1, 3, 256, 256) torch.onnx.export( appearance, dummy, ./onnx/appearance_extractor.onnx, input_names[input], output_names[feature], dynamic_axes{ input: {0: batch, 2: height, 3: width}, feature: {0: batch} }, opset_version17, # 17 对 LayerNorm 和 GELU 支持更稳 do_constant_foldingTrue ) print(export done)逻辑说明eval()必须调用否则 BatchNorm 和 Dropout 会引入随机性导出的 ONNX 每次输出都不一样。dynamic_axes里 batch 维度放开是为了后面 C 批量推理留余地spatial 维度放开是因为 warping 阶段特征图尺寸会变。opset_version17是我踩过坑之后固定的值——低于 15 时某些插值算子会退化成自定义 opONNX Runtime 加载直接报NOT_IMPLEMENTED。参数说明do_constant_foldingTrue会把能提前算的常量折叠掉减小模型体积但如果你后续要做量化建议先关掉等量化完再重新导出。2.3 验证 ONNX 模型是否真的可用导出完别急着写 C先用 ONNX Runtime 的 Python 包跑一遍确认输出和 PyTorch 对齐import onnxruntime as ort import numpy as np import torch sess ort.InferenceSession(./onnx/appearance_extractor.onnx, providers[CPUExecutionProvider]) dummy np.random.randn(1, 3, 256, 256).astype(np.float32) onnx_out sess.run(None, {input: dummy})[0] # 和 PyTorch 对比 with torch.no_grad(): torch_out appearance(torch.from_numpy(dummy)).numpy() diff np.abs(onnx_out - torch_out).max() print(max diff:, diff) # 正常应小于 1e-4如果 diff 超过 1e-3说明导出有问题常见原因是 opset 版本不匹配或某个算子被降级。这时候别硬着头皮往下走回头改导出脚本比在 C 里 debug 快十倍。3. 环境搭建ONNX Runtime 动态库、VC 运行库和 Python 依赖怎么配不打架3.1 C 侧ONNX Runtime 动态库的获取与链接ONNX Runtime 官方提供预编译包Windows 下直接下载onnxruntime-win-x64-1.x.x.zip解压后得到include、lib、bin三个目录。我一般会把它们放到项目根目录的third_party/onnxruntime下保持路径可控。Visual Studio 工程里需要配三处附加包含目录third_party\onnxruntime\include附加库目录third_party\onnxruntime\lib附加依赖项onnxruntime.libDebug 和 Release 各一份别混用运行时把onnxruntime.dll和onnxruntime_providers_shared.dll拷到 exe 同目录。这里有个血泪经验如果你用了 GPU 版还要额外拷onnxruntime_providers_cuda.dll而且 CUDA 版本必须和编译 ONNX Runtime 时用的版本一致否则加载 provider 时直接崩连错误信息都不给。另外Windows 上跑任何 C 程序都绕不开Microsoft Visual C 2015-2022 Redistributable (x64)。很多机器上没装或者装的是旧版表现是程序启动时报VCRUNTIME140.dll 缺失。直接去微软官网下最新的 x64 运行库装上别去第三方站点下版本混乱反而更难排查。3.2 Python 侧onnxruntime 和 opencv 的版本对齐Python 这边简单得多但版本坑一样存在pip install onnxruntime1.17.0 pip install opencv-python4.9.0.80 pip install numpy1.26.4为什么锁版本onnxruntime 1.17对numpy 2.x支持不完整如果你环境里 numpy 是 2.0import 的时候会报_ARRAY_API not found。opencv-python锁 4.9 是因为 4.10 之后某些视频解码后端变了读驱动视频时帧率会异常。这些不是理论推测是我在三个不同机器上复现过的。如果你要用 GPU 推理把第一行换成onnxruntime-gpu1.17.0并且确认nvidia-smi里的 CUDA 版本是 11.8 或 12.x和 onnxruntime-gpu 的编译版本对应。3.3 一个最小可跑的 C 推理骨架环境配好后先用一段最短的 C 代码验证 ONNX Runtime 能不能加载模型#include onnxruntime_cxx_api.h #include iostream int main() { Ort::Env env(ORT_LOGGING_LEVEL_WARNING, liveportrait); Ort::SessionOptions opts; opts.SetIntraOpNumThreads(4); // CPU 推理线程数按核数调 // 模型路径用宽字符Windows 下中文路径不会翻车 Ort::Session session(env, L./onnx/appearance_extractor.onnx, opts); Ort::AllocatorWithDefaultOptions allocator; auto input_name session.GetInputNameAllocated(0, allocator); auto output_name session.GetOutputNameAllocated(0, allocator); std::cout input: input_name.get() , output: output_name.get() std::endl; return 0; }逻辑说明Ort::Env全局一个就够不要每个模型都建。SetIntraOpNumThreads控制单次推理内部的并行度LivePortrait 这种多子模型串联的场景设成物理核数的一半通常比拉满更稳因为子模型之间还有调度开销。GetInputNameAllocated返回的是智能指针别手动 delete。参数说明ORT_LOGGING_LEVEL_WARNING在生产环境够用调试阶段可以开到VERBOSE但日志量很大记得重定向到文件。4. C 与 Python 双语言推理从单帧到视频流的完整链路4.1 C 侧五个子模型串联的推理流程单模型跑通后要把五个子模型串起来。核心是管理好每个 session 的输入输出张量避免频繁分配内存。下面是一个简化的串联骨架// 假设五个 session 已经初始化好 std::vectorfloat run_pipeline(const std::vectorfloat src_image, const std::vectorfloat drv_frame) { // 1. 提取源外观特征 auto appearance run_session(appearance_sess, src_image, {1,3,256,256}); // 2. 提取驱动运动 auto motion run_session(motion_sess, drv_frame, {1,3,256,256}); // 3. 变形 auto warped run_session(warp_sess, concat(appearance, motion), {1,64,64,64}); // 4. 生成人脸 auto face run_session(spade_sess, warped, {1,64,64,64}); // 5. 贴回原图 auto result run_session(stitch_sess, concat(src_image, face), {1,3,512,512}); return result; }逻辑说明run_session是一个封装函数内部负责创建Ort::Value、绑定输入输出、调用session.Run。每个子模型的输入 shape 必须和导出时一致尤其是 warping 阶段特征图尺寸写错会直接抛异常。concat是自定义的拼接函数把两组 float 数据按通道维拼起来。参数说明如果要做视频流不要每帧都重新创建Ort::Value而是预分配好输入输出 buffer用Ort::Value::CreateTensor绑定同一块内存。这样能省掉大量 malloc/free实测帧率能提升 20% 以上。4.2 Python 侧用 cv2 读视频、逐帧推理、写回视频Python 这边更适合快速验证和调参。下面是一个完整的视频驱动脚本import cv2 import numpy as np import onnxruntime as ort # 初始化五个 sessionproviders 按需切换 providers [CPUExecutionProvider] sessions { name: ort.InferenceSession(f./onnx/{name}.onnx, providersproviders) for name in [appearance_extractor, motion_extractor, warping_network, spade_generator, stitching_network] } def preprocess(frame, size(256, 256)): img cv2.resize(frame, size) img img[:, :, ::-1].astype(np.float32) / 255.0 # BGR-RGB, 归一化 return img.transpose(2, 0, 1)[None, ...] # NCHW src cv2.imread(source.jpg) src_tensor preprocess(src) appearance sessions[appearance_extractor].run(None, {input: src_tensor})[0] cap cv2.VideoCapture(driving.mp4) fps cap.get(cv2.CAP_PROP_FPS) w int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer cv2.VideoWriter(output.mp4, cv2.VideoWriter_fourcc(*mp4v), fps, (w, h)) while True: ret, frame cap.read() if not ret: break drv_tensor preprocess(frame) motion sessions[motion_extractor].run(None, {input: drv_tensor})[0] warped sessions[warping_network].run(None, {appearance: appearance, motion: motion})[0] face sessions[spade_generator].run(None, {input: warped})[0] result sessions[stitching_network].run(None, {source: src_tensor, face: face})[0] out_frame (result[0].transpose(1, 2, 0)[:, :, ::-1] * 255).astype(np.uint8) writer.write(out_frame) cap.release() writer.release()逻辑说明preprocess里做了三件事——resize、BGR 转 RGB、归一化到 [0,1]顺序不能乱。appearance只算一次因为源人像不变这是 LivePortrait 相比逐帧重算方案最大的性能优势。motion_extractor每帧都要跑它是整个链路里最耗时的部分。参数说明VideoWriter_fourcc(*mp4v)在 Windows 上兼容性最好Linux 上如果报错可以换成avc1。输出帧率直接沿用输入视频的 fps不要手动改否则音画不同步。4.3 双语言性能对比与选型建议同一台机器i7-1270032GB无独显上实测Python 和 C 的差距主要在启动开销和内存管理上指标Python (onnxruntime)C (onnxruntime)单帧推理耗时约 85ms约 62ms内存峰值1.8GB1.1GB启动时间2.5s0.4s部署依赖Python 多个包一个 exe 两个 dllC 的优势在长时运行和内存敏感场景Python 的优势在快速迭代和调试。我的建议是先用 Python 把参数调好、确认效果再把推理逻辑平移到 C。不要一上来就写 C调参阶段会痛苦十倍。5. 避坑与排查ONNX Runtime 部署 LivePortrait 最常见的五个翻车点5.1 模型加载报 NOT_IMPLEMENTED现象C 里Ort::Session构造时抛异常提示某个算子NOT_IMPLEMENTED。原因导出 ONNX 时 opset 版本太低或者用了 ONNX Runtime 尚未实现的算子。LivePortrait 里的GridSample和LayerNormalization是重灾区。解决把导出脚本的opset_version提到 17重新导出。如果还不行用onnxsim简化模型或者检查 ONNX Runtime 版本是否太旧——1.14 以下对 opset 17 支持不完整。5.2 输出全黑或者花屏现象推理跑通了但输出图像是全黑或者彩色噪点。原因输入预处理和导出时的预处理不一致。LivePortrait 原版对输入做了(x - 0.5) / 0.5的归一化如果你只做了/255模型输出就会完全错乱。解决对照原版推理代码把归一化参数抄准。RGB 和 BGR 的顺序也要确认OpenCV 读进来是 BGR送进模型前必须转。5.3 视频输出帧率异常或音画不同步现象输出视频播放速度明显不对或者画面和声音对不上。原因VideoWriter的 fps 参数和输入不一致或者某些帧推理失败被跳过但时间戳没补。解决用cap.get(cv2.CAP_PROP_FPS)读输入帧率原样传给VideoWriter。如果某帧推理失败不要跳过用上一帧结果填充保持帧数一致。5.4 C 程序在别人机器上启动就崩现象自己机器上跑得好好的拷到同事电脑上双击就闪退。原因缺Microsoft Visual C 2015-2022 Redistributable或者onnxruntime.dll没拷全。解决把 VC 运行库和所有 onnxruntime 相关 dll 一起打包。可以用dumpbin /dependents your.exe查看依赖了哪些 dll逐个确认。5.5 GPU 推理比 CPU 还慢现象切到CUDAExecutionProvider后单帧耗时反而增加了。原因模型太小数据在 CPU 和 GPU 之间来回拷贝的开销超过了计算本身。LivePortrait 的 appearance_extractor 只有几层卷积GPU 优势发挥不出来。解决只把 spade_generator 和 warping_network 放到 GPU其余留在 CPU。ONNX Runtime 支持按 session 指定 provider不要一刀切。6. 进阶技巧用 IO Binding 和模型量化把 C 推理再压榨 30%6.1 IO Binding 减少内存拷贝默认情况下ONNX Runtime 每次Run都会把输入从 CPU 内存拷到推理设备输出再拷回来。对于 LivePortrait 这种五段串联的链路拷贝开销累积起来很可观。IO Binding 允许你预先绑定输入输出内存跳过这部分拷贝Ort::IoBinding binding(session); binding.BindInput(input, input_tensor); binding.BindOutput(output, output_tensor); session.Run(Ort::RunOptions{nullptr}, binding);逻辑说明input_tensor和output_tensor需要提前用Ort::Value::CreateTensor创建好并且保证生命周期覆盖整个Run调用。绑定之后如果输入数据变了直接改内存内容即可不用重新绑定。参数说明IO Binding 在 GPU 场景下收益最大CPU 场景下也能省掉一次内部拷贝实测五段链路整体耗时下降约 12%。6.2 动态量化把模型体积和内存砍一半ONNX Runtime 提供了训练后量化工具可以把 FP32 模型转成 INT8体积缩小约 75%CPU 推理速度提升 20% 到 40%。代价是画质可能有轻微下降需要自己权衡from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_input./onnx/spade_generator.onnx, model_output./onnx/spade_generator_int8.onnx, weight_typeQuantType.QInt8 )逻辑说明quantize_dynamic只量化权重激活值在推理时动态量化不需要校准数据集适合快速验证。如果对画质要求高改用quantize_static但需要准备一批校准图片。参数说明QuantType.QInt8比QUInt8在 CPU 上通常更快因为 Intel 的 VNNI 指令对 signed int8 优化更好。量化后一定要用第 2 章的对齐脚本重新验证输出 diff超过 1e-2 就说明量化太激进考虑只量化部分子模型。6.3 一个我常用的验证习惯每次改完导出脚本或者量化参数我都会跑一个固定的小测试集三张源图 × 三段驱动视频共九组记录每组的第一帧和最后一帧的 PSNR。PSNR 低于 30dB 就说明这次改动引入了可见的画质损失需要回退。这个习惯帮我省掉了无数次「上线后才发现画质崩了」的后悔药。部署这件事快不是第一位的稳才是。希望帮到你。本文还有配套的精品资源点击获取