ARTICLE DETAIL

资讯详情

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

ONNX Runtime部署LivePortrait人像动画:C++与Python实战

ONNX Runtime部署LivePortrait人像动画:C++与Python实战 简介这份资源面向希望在人像动画生成方向落地的开发者与算法工程师提供基于onnxruntime推理引擎部署LivePortrait的完整程序同时给出C与Python两套实现路径便于在桌面端或工程环境中集成。压缩包共14个文件约459KB包含4个cpp与3个h源文件构成C推理主流程2个py脚本负责Python侧调用与裁剪处理另有txt、md说明文档及mp4、jpg、png示例素材方便对照输入输出效果。内容覆盖模型加载、人脸分析与动画生成等模块目录按cpp与python分开放置结构清晰适合具备一定推理部署基础、想快速跑通人像动画流程的读者参考。目前已有204人学习下载可作为从零搭建LivePortrait推理管线的起点帮助理解C与Python双端调用差异并在此基础上做二次开发与性能调优。1. 从一份 zip 说起LivePortrait 人像动画为什么值得用 onnxruntime 重写一遍拿到「使用onnxruntime部署LivePortrait人像动画生成C和Python程序.zip」这个标题时我第一反应不是去看它里面有什么而是想清楚它想解决什么。LivePortrait 这类人像动画方案核心是把一张静态人像加上一段驱动视频的表情、姿态合成出一段会动的人脸视频。原始实现通常跑在 PyTorch 上推理链路依赖 Python 运行时和一堆深度学习框架的动态库部署到没有 CUDA、没有完整 Python 环境的机器上就很别扭。onnxruntime 的价值就在这里把训练好的模型导出成 ONNX 格式用一套跨平台的推理引擎去跑C 侧可以直接链接动态库Python 侧也能用同一份模型文件两边结果对齐。这份 zip 面向的是想把人像动画真正落地到产品里的人——可能是做直播虚拟形象、短视频工具、在线证件照动化也可能是做端侧推理的嵌入式方向。它同时给 C 和 Python 两套程序说明作者想让读者既能快速用 Python 验证效果又能用 C 做高性能集成。热搜里「用onnxruntime动态库」「鲲鹏920适配onnxruntime框架」这些词恰好说明大家关心的不是模型本身而是怎么把它塞进真实工程环境。这篇就按「模型怎么来 → Python 怎么跑通 → C 怎么集成 → 坑在哪」的顺序把这条路走一遍。2. 把 LivePortrait 拆成 ONNX模型导出与输入输出对齐2.1 为什么不能直接拿 PyTorch 权重喂给 onnxruntimeonnxruntime 和 ONNX 的关系热搜里有人问「onnxruntime 和 onnx 区别 概念」这里一句话说清ONNX 是模型格式标准onnxruntime 是执行这个标准的推理引擎。LivePortrait 原始仓库给的是 PyTorch 的.pth权重onnxruntime 读不了必须先导出成.onnx。导出不是简单调个torch.onnx.export就完事LivePortrait 的推理链路里包含多个子模块外观特征提取、运动提取、形变网络、生成器、以及可选的眼部/嘴部重定向模块。每个子模块的输入输出张量形状、动态轴设置都不一样导出时要逐个确认。常见做法是先把整个 pipeline 在 PyTorch 里跑通一遍用torch.onnx.export对每个子模块单独导出而不是试图把整个 pipeline 塞进一个 ONNX 图。原因是 LivePortrait 里有不少控制流和动态 shape 操作整图导出容易在If、Loop节点上翻车。分模块导出后在 C 或 Python 侧用代码把模块串起来反而更可控。2.2 导出脚本与动态轴设置下面是一段我常用的导出骨架针对 LivePortrait 里典型的生成器模块。注意dynamic_axes的设置人像动画的输入分辨率可能变化batch 也可能不是 1。import torch import torch.onnx # 假设 model 是已经加载好权重的 LivePortrait 子模块 # 例如 appearance encoder 或 warping network model.eval() # 构造一个符合实际推理的 dummy input # 外观特征提取常见输入: [B, 3, 256, 256] dummy_input torch.randn(1, 3, 256, 256) # 动态轴: batch 维和空间维都设为动态方便后续换分辨率 dynamic_axes { input: {0: batch, 2: height, 3: width}, output: {0: batch, 2: height, 3: width}, } torch.onnx.export( model, dummy_input, liveportrait_appearance.onnx, input_names[input], output_names[output], dynamic_axesdynamic_axes, opset_version17, # 17 对动态 shape 支持较稳 do_constant_foldingTrue, # 常量折叠减小图体积 )这段代码的逻辑是把模型切到 eval 模式关掉 dropout 和 batchnorm 的训练行为构造一个形状正确的假输入让导出器能追踪计算图dynamic_axes告诉 onnxruntime 哪些维度在推理时可以变。参数上opset_version建议不低于 1617 更稳因为 LivePortrait 里有些插值和 reshape 操作在低 opset 下会报不支持。do_constant_folding打开后推理时不会重复计算常量节点对速度有好处。导出完成后别急着写 C先用 Python 的 onnxruntime 验证一遍数值对齐。这一步能省掉后面大量「C 结果不对」的排查时间。import onnxruntime as ort import numpy as np import torch # PyTorch 侧结果 with torch.no_grad(): torch_out model(dummy_input).numpy() # onnxruntime 侧结果 sess ort.InferenceSession(liveportrait_appearance.onnx, providers[CPUExecutionProvider]) onnx_out sess.run(None, {input: dummy_input.numpy()})[0] # 对齐检查最大绝对误差应小于 1e-4 diff np.abs(torch_out - onnx_out).max() print(max diff:, diff) assert diff 1e-4, 导出数值偏差过大检查 opset 和算子如果这里 diff 超过 1e-3常见原因是某些算子在不同后端实现有差异比如GridSample、Resize的插值模式。解决办法是回到导出脚本把这些算子替换成 onnxruntime 支持更好的等价实现或者调整opset_version。2.3 输入输出张量的命名与顺序LivePortrait 的多个子模块串起来时前一个模块的输出名要对应后一个模块的输入名。我一般会在导出时统一命名规范比如外观模块输出叫appearance_feat运动模块输出叫motion_feat生成器输入叫warped_feat。这样在 C 侧拼接时不容易搞混。另外要注意 onnxruntime 的run方法返回的是一个列表顺序和导出时的output_names一致C 侧拿到的Ort::Value也是按这个顺序别靠猜。3. Python 侧跑通从 onnxruntime 会话到一段会动的脸3.1 环境准备与 onnxruntime 安装的版本选择Python 侧相对简单但版本坑不少。onnxruntime 分 CPU 版和 GPU 版包名不同onnxruntime是 CPUonnxruntime-gpu是 CUDA。如果你机器上没有 NVIDIA 显卡装 GPU 版会在InferenceSession初始化时报找不到 CUDA provider。热搜里「python安装教程」「python安装」这些词说明不少读者还在环境阶段这里给一个最小可用的安装命令。# 建议 Python 3.8 - 3.11太新的版本可能没有对应 wheel python -m pip install onnxruntime1.16.3 python -m pip install opencv-python numpy版本上1.16.x 对 opset 17 支持完整且 CPU 推理性能比早期版本好。如果你在鲲鹏 920 这类 ARM 平台上要确认 pip 源里有 aarch64 的 wheel没有的话就得从源码编译那是另一条路。装完后用一行命令验证import onnxruntime as ort print(ort.get_available_providers()) # CPU 版应输出 [CPUExecutionProvider]3.2 用 Python 把多个 ONNX 模块串成完整 pipelineLivePortrait 的推理不是单个模型而是「提取源人脸特征 → 提取驱动视频每帧运动 → 形变 → 生成」。下面这段代码展示怎么用 onnxruntime 的多个 session 串起来输入是一张源图和一帧驱动图输出是动画帧。import cv2 import numpy as np import onnxruntime as ort # 加载各子模块providers 按优先级排列 providers [CPUExecutionProvider] appearance_sess ort.InferenceSession(appearance.onnx, providersproviders) motion_sess ort.InferenceSession(motion.onnx, providersproviders) warp_sess ort.InferenceSession(warping.onnx, providersproviders) gen_sess ort.InferenceSession(generator.onnx, providersproviders) def preprocess(img_bgr, size256): BGR 转 RGB归一化到 [0,1]加 batch 维 img cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img cv2.resize(img, (size, size)) img img.astype(np.float32) / 255.0 img np.transpose(img, (2, 0, 1))[None, ...] # NCHW return img source cv2.imread(source.jpg) driving cv2.imread(driving_frame.jpg) src_tensor preprocess(source) drv_tensor preprocess(driving) # 1. 源人脸外观特征 appearance_feat appearance_sess.run(None, {input: src_tensor})[0] # 2. 驱动帧运动特征 motion_feat motion_sess.run(None, {input: drv_tensor})[0] # 3. 形变把源特征按驱动运动扭曲 warped warp_sess.run(None, { appearance: appearance_feat, motion: motion_feat, })[0] # 4. 生成最终动画帧 output gen_sess.run(None, {warped: warped})[0] # 后处理CHW - HWC反归一化 out_img np.transpose(output[0], (1, 2, 0)) out_img np.clip(out_img * 255, 0, 255).astype(np.uint8) cv2.imwrite(animated_frame.jpg, cv2.cvtColor(out_img, cv2.COLOR_RGB2BGR))逻辑说明每个run调用就是一次 onnxruntime 推理输入字典的 key 必须和导出时的input_names完全一致大小写敏感。参数上providers列表决定执行后端CPU 版只有CPUExecutionProvider如果你装了 GPU 版可以写[CUDAExecutionProvider, CPUExecutionProvider]onnxruntime 会按顺序尝试。后处理里的反归一化要和训练时的预处理对齐LivePortrait 一般用[0,1]归一化但有些版本用[-1,1]这个必须看导出时的预处理代码搞错了输出就是一片灰。3.3 驱动视频逐帧处理与结果拼接单帧跑通后处理整段驱动视频就是循环加写视频。这里有个性能点不要每帧都重新创建InferenceSessionsession 创建开销很大要在循环外建好。另外可以用io.Binding做零拷贝但 Python 侧收益不如 C 明显先用简单方式跑通。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)) # 源图特征只算一次这是 LivePortrait 的关键优化点 src_tensor preprocess(source) appearance_feat appearance_sess.run(None, {input: src_tensor})[0] while True: ret, frame cap.read() if not ret: break drv_tensor preprocess(frame) motion_feat motion_sess.run(None, {input: drv_tensor})[0] warped warp_sess.run(None, { appearance: appearance_feat, motion: motion_feat, })[0] output gen_sess.run(None, {warped: warped})[0] out_img np.transpose(output[0], (1, 2, 0)) out_img np.clip(out_img * 255, 0, 255).astype(np.uint8) out_img cv2.resize(out_img, (w, h)) writer.write(cv2.cvtColor(out_img, cv2.COLOR_RGB2BGR)) cap.release() writer.release()这里把源图的外观特征提到循环外是因为它和驱动帧无关每帧重算纯属浪费。这个优化在 C 侧同样适用而且收益更大。注意输出分辨率如果和驱动视频不一致要 resize 回去否则写出的视频会变形。4. C 侧集成用 onnxruntime 动态库把推理塞进原生程序4.1 拿到 onnxruntime 动态库与头文件C 侧第一步不是写代码是准备依赖。onnxruntime 官方发布包里有include/和lib/两个目录Windows 下是onnxruntime.dllonnxruntime.libLinux 下是libonnxruntime.so。热搜里「用onnxruntime动态库」说的就是这个。下载对应平台的 release 包解压后把include加到编译器的头文件搜索路径lib加到链接路径。Windows 上如果你用 Visual Studio还要注意运行时的位数匹配onnxruntime 的 x64 库只能配 x64 的工程配成 Win32 会在链接时报LNK1112模块计算机类型冲突。另外microsoft visual c redistributable这个热搜词也相关onnxruntime 的 Windows 动态库依赖 VC 运行时目标机器没装的话会提示缺vcruntime140.dll发布时要么让用户装 redistributable要么静态链接运行时。4.2 一个最小的 C 推理程序下面这段代码展示怎么用 C API 加载 ONNX 模型并跑一次推理。这是所有 C 集成的基础骨架。#include onnxruntime_cxx_api.h #include vector #include iostream int main() { // 1. 创建环境日志级别设为 WARNING 避免刷屏 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, liveportrait); // 2. 会话选项线程数按 CPU 核心数设别默认 Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); // 3. 加载模型宽字符路径在 Windows 上更稳 Ort::Session session(env, appearance.onnx, session_options); // 4. 查询输入输出信息 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; // 5. 构造输入张量 [1,3,256,256] std::vectorint64_t input_shape {1, 3, 256, 256}; std::vectorfloat input_data(1 * 3 * 256 * 256, 0.5f); auto memory_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); // 6. 跑推理 const char* input_names[] {input_name.get()}; const char* output_names[] {output_name.get()}; auto outputs session.Run(Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 1); // 7. 取输出 float* out outputs[0].GetTensorMutableDatafloat(); auto out_shape outputs[0].GetTensorTypeAndShapeInfo().GetShape(); std::cout output dim: out_shape.size() first val: out[0] std::endl; return 0; }逻辑说明Ort::Env是全局环境一个进程一个就够SessionOptions里SetIntraOpNumThreads控制算子内并行线程数设成 CPU 物理核心数通常最优设太大反而因上下文切换变慢GetInputNameAllocated返回的是智能指针管理的字符串别用裸char*接否则内存泄漏。输入张量的CreateTensor不拷贝数据input_data的生命周期要覆盖整个Run调用这是新手最容易翻车的地方——局部 vector 提前析构推理结果就是随机值。编译命令在 Linux 下大概是这样g -stdc17 main.cpp -o liveportrait_demo \ -I./onnxruntime/include \ -L./onnxruntime/lib \ -lonnxruntime \ -Wl,-rpath,./onnxruntime/lib-Wl,-rpath把库搜索路径写进可执行文件运行时不用再设LD_LIBRARY_PATH。Windows 下用 MSVC 则是把onnxruntime.lib加到附加依赖项onnxruntime.dll放到 exe 同目录。4.3 多模块串联与图像数据在 C 里的流转C 侧把多个 ONNX 模块串起来核心是管理好每个中间张量的内存。和 Python 不同C 没有垃圾回收Ort::Value持有张量数据但如果你用外部 buffer 创建张量buffer 得自己管。我一般会封装一个LivePortraitPipeline类成员里放各个Ort::Session每个Run的输出用Ort::Value接住再作为下一个模块的输入。图像预处理在 C 里通常用 OpenCVcv::Mat转std::vectorfloat时注意通道顺序。OpenCV 默认 BGR模型要 RGB得用cv::cvtColor转。归一化除 255 这一步别用循环写用cv::Mat::convertTo配合缩放因子更快。后处理把float*转回cv::Mat时如果输出是 CHW 布局要手动按通道拷贝不能直接memcpy成 HWC。5. 避坑与排查那些让结果全黑或全灰的瞬间5.1 输出全灰或全黑现象Python 或 C 跑完保存的图片是一片均匀灰色或黑色。原因通常是预处理归一化范围不对。LivePortrait 不同版本有的用[0,1]有的用[-1,1]导出 ONNX 时如果没把归一化写进图里推理侧就得自己对齐。解决回到导出脚本确认训练时的mean和std在推理侧用同样的公式。一个快速验证方法是把输入图直接当输出保存看预处理后的张量反归一化能不能还原原图。5.2 C 推理结果和 Python 不一致现象同一张图Python 输出正常C 输出偏移或噪声。原因多半是输入张量的内存布局或数值类型不对。Python 的 numpy 默认float32C 里如果用了double或者intonnxruntime 会报类型不匹配或静默转换出错。解决用GetTensorTypeAndShapeInfo().GetElementType()打印模型期望的类型确保 C 侧CreateTensorfloat的模板参数一致。另外检查 NCHW 顺序C 里手动填数据时最容易把 H 和 W 搞反。5.3 动态 shape 模型在 C 里报维度错误现象导出时设了动态轴Python 换分辨率没事C 换分辨率就报Invalid input shape。原因是 C 侧构造输入张量时input_shape写死了没跟着实际图像尺寸变。解决把input_shape改成从cv::Mat的rows、cols动态计算并且确保导出时的dynamic_axes确实覆盖了这些维度。如果模型对最小尺寸有限制比如生成器要求不小于 64那输入前要 resize 到合法范围。5.4 onnxruntime 动态库加载失败现象程序启动时报找不到onnxruntime.dll或libonnxruntime.so。原因分两种库文件不在搜索路径或者位数/架构不匹配。解决Linux 下用ldd看依赖缺哪个补哪个-Wl,-rpath是最省事的做法Windows 下把 dll 放 exe 同目录或者用SetDllDirectory指定路径。如果是鲲鹏 920 这类 ARM 平台要确认下载的是 aarch64 版本的库x86 的库在 ARM 上根本加载不了。5.5 多线程推理时结果错乱现象单线程正常开多线程后偶发结果错乱或崩溃。原因是多个线程共用了同一个Ort::Session的输入输出 buffer或者Ort::Env的线程安全没处理好。解决Ort::Session本身是线程安全的可以多线程共享但每个线程要创建自己的Ort::Value输入输出。如果用了io.Binding每个线程要有独立的 binding 对象。另外SetIntraOpNumThreads和外部线程池别叠加太多总线程数超过物理核心数会拖慢整体吞吐。6. 进阶技巧用 io.Binding 和自定义算子把延迟压下来跑通之后下一步就是压延迟。LivePortrait 的推理链路里数据在 CPU 和推理引擎之间来回拷贝是主要开销之一。onnxruntime 提供了io.Binding机制允许你把预分配的设备内存直接绑定给输入输出省掉每次Run时的拷贝。C 侧用session.GetInputNameAllocated拿到名字后创建Ort::IoBinding用BindInput和BindOutput把Ort::Value绑上去之后调session.Run(Ort::RunOptions{nullptr}, binding)就行。这个改动在 256x256 输入下大概能省 5% 到 10% 的延迟分辨率越大收益越明显。另一个技巧是算子融合。导出 ONNX 时do_constant_folding只折叠常量不融合相邻算子。onnxruntime 在ORT_ENABLE_ALL优化级别下会自动做一部分融合但如果你发现某个子图特别慢可以用session_options.AddConfigEntry(session.disable_fusion, 0)确认融合是否生效或者用onnxruntime_perf_test工具单独测每个模块的耗时找出瓶颈模块再针对性优化。验证优化效果不能只看总时间要分段计时。我一般会在 C 里用std::chrono::high_resolution_clock给每个Run打点输出各模块耗时占比。如果发现生成器占了 70% 以上那优化重点就放在生成器的输入分辨率和 opset 选择上如果预处理占了 20%那就该换更快的 resize 实现或者用 GPU 做预处理。最后说个我自己的习惯每次改完导出脚本或推理代码先跑一个固定输入的回归测试把输出和基准的 max diff 打出来超过阈值就停下来查。这个习惯帮我省了无数次「改完 A 模块B 模块悄悄坏了」的后悔药。人像动画这种链路长的项目没有回归测试就是在裸奔。希望帮到你。本文还有配套的精品资源点击获取
返回列表