尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LivePortrait 人像动画 ONNX 部署:C++ 与 Python 双语言实战

LivePortrait 人像动画 ONNX 部署:C++ 与 Python 双语言实战 简介这份资源面向希望将LivePortrait人像动画生成能力落地到实际工程中的开发者提供基于onnxruntime推理的完整部署程序同时覆盖C与Python两种实现路径适合具备一定深度学习推理基础、需要跨语言集成或做性能对比的中高级开发者参考。压缩包共14个文件约459KB包含4个cpp与3个h源文件构成C推理主流程2个py脚本负责Python侧调用与裁剪工具另有txt、md说明文档及mp4、jpg、png示例素材便于快速理解输入输出格式。内容围绕人脸检测、特征裁剪与动画驱动等模块展开C部分以CMake组织工程Python部分提供可直接运行的入口脚本读者可据此搭建本地推理环境、对照两种语言实现差异并借鉴其模型加载与前后处理思路。目前已有204人学习下载适合作为人像动画部署的入门与迁移参考。1. 从一张照片到一段表情LivePortrait 在 onnxruntime 上的落地路径手里只有一张人像照片想让它的眼睛眨起来、嘴角动起来甚至跟着一段驱动视频做出同步表情——这是很多做数字人、虚拟主播、在线教育的团队都会碰到的需求。LivePortrait 这类人像动画生成方案核心思路是用一段驱动视频的表情和姿态去驱动一张静态人像输出一段自然的口型与头部动作视频。但真正把它推到生产环境绕不开两个现实问题一是原始实现往往依赖 PyTorch 生态部署到没有 GPU 或只有国产 CPU 的机器上很吃力二是 C 和 Python 两套调用方式各有各的适用场景选错了后面全是返工。这篇笔记锁定的就是这条路径用 onnxruntime 作为推理后端把 LivePortrait 的人像动画生成能力部署起来同时给出 C 和 Python 两种调用方式。适合已经跑通过 PyTorch 版本、想往工程化落地的同学也适合需要在鲲鹏 920 这类 ARM 服务器上做推理的团队。下面从模型拆解、环境搭建、两种语言实现一路讲到参数调优和踩坑记录尽量让每一步都能照着复现。2. 拆开 LivePortrait哪些模块要转 ONNX哪些可以留在外面2.1 模型结构里真正需要 ONNX 化的三个部分LivePortrait 的推理流程大致可以拆成外观提取、运动提取、形变与生成三段。外观提取网络负责从源图像里抽出身份和纹理特征这部分输入输出形状固定最适合转成 ONNX。运动提取网络处理驱动视频的每一帧输出表情和姿态系数它的输入是连续帧转 ONNX 时要注意动态轴设置。生成网络通常是 warping 加解码器把外观特征和运动系数合成最终画面这部分计算量最大也是 onnxruntime 加速收益最明显的地方。常见的做法是只把外观提取和生成网络转 ONNX运动提取如果帧数不多留在 Python 侧用原始实现跑或者单独转一个小模型。这样做的原因是运动提取对时序敏感ONNX 的动态 shape 支持虽然够用但调试成本高。我一般会先把生成网络转出来跑通端到端再回头补其他模块。转 ONNX 时用 torch.onnx.export重点设置 opset_version 和 dynamic_axes。opset 建议不低于 14否则一些插值算子会退化成不支持的版本。dynamic_axes 要把 batch 维和序列长度维标出来否则驱动视频换长度就要重新导出。import torch import torch.onnx # 假设 generator 是已经加载好权重的生成网络 dummy_appearance torch.randn(1, 256, 64, 64) dummy_motion torch.randn(1, 21, 3) torch.onnx.export( generator, (dummy_appearance, dummy_motion), liveportrait_generator.onnx, opset_version14, input_names[appearance, motion], output_names[output_image], dynamic_axes{ appearance: {0: batch}, motion: {0: batch, 1: seq_len}, output_image: {0: batch} }, do_constant_foldingTrue )这段代码里 do_constant_folding 打开后会把能提前算的常量折叠掉减小模型体积。dynamic_axes 里 motion 的第二维标成 seq_len是因为驱动视频长度不固定。导出后建议用 onnxruntime 的 Python 接口先加载一次确认没有算子报错再往下走。2.2 onnxruntime 和 onnx 的区别以及为什么选 onnxruntime 做推理很多人第一次接触会混淆 onnx 和 onnxruntime。onnx 是一种模型格式标准定义的是计算图的表示方式onnxruntime 是微软开源的推理引擎负责把 ONNX 模型加载进来并在 CPU、GPU 或特定加速器上执行。你可以把 onnx 理解成 PDF 文件格式onnxruntime 理解成 PDF 阅读器。导出模型用 onnx 相关工具跑推理用 onnxruntime。选 onnxruntime 的理由很直接跨平台、依赖少、对 ARM 架构支持成熟。在鲲鹏 920 上部署时onnxruntime 有预编译的 aarch64 版本装完就能用不需要额外编译 PyTorch。相比之下直接部署 PyTorch 模型在 ARM 服务器上要么编译一整天要么找不到匹配的 wheel 包。onnxruntime 还支持通过 execution provider 切换后端CPU 上用默认的 MLAS有 GPU 时切 CUDA 或 TensorRT代码几乎不用改。提示onnxruntime 的版本要和导出模型时的 opset 匹配。opset 14 的模型用 onnxruntime 1.12 以上版本加载比较稳版本太低会报不支持的算子。2.3 环境搭建Python 侧和 C 侧各装什么Python 侧相对简单pip 装 onnxruntime 和 onnx 就行。如果要在鲲鹏 920 上跑注意选 aarch64 的 wheel不要装成 x86 版本。C 侧需要下载 onnxruntime 的预编译库或者从源码编译然后配置头文件路径和链接库。# Python 侧 pip install onnxruntime onnx numpy opencv-python # 验证安装 python -c import onnxruntime as ort; print(ort.get_available_providers())C 侧在 Linux 上一般下载 onnxruntime-linux-x64 或 aarch64 的压缩包解压后得到 include 和 lib 两个目录。编译时用 -I 指定 include 路径-L 指定 lib 路径链接 onnxruntime 库。Windows 上则用 Visual C 的工程配置把附加包含目录和附加库目录指过去。注意 Microsoft Visual C Redistributable 要装好否则运行时会缺 DLL。# C 编译示例Linux g -stdc17 main.cpp -o liveportrait_demo \ -I./onnxruntime/include \ -L./onnxruntime/lib \ -lonnxruntime \ -lopencv_core -lopencv_imgproc -lopencv_imgcodecs编译参数里 -stdc17 是因为 onnxruntime 的 C API 用了一些 C17 特性。OpenCV 用来做图像预处理和后处理如果不想引入 OpenCV也可以自己写简单的图像读写但会麻烦不少。3. Python 侧跑通 LivePortrait从加载 ONNX 到输出第一帧动画3.1 用 onnxruntime 加载模型并做一次前向推理Python 侧的优势是调试快适合先把整个流程跑通再移植到 C。加载模型用 InferenceSession指定 providers 为 CPUExecutionProvider 或 CUDAExecutionProvider。输入数据要转成 numpy 数组注意数据类型和形状要和导出时一致。import onnxruntime as ort import numpy as np import cv2 # 创建推理会话 session ort.InferenceSession( liveportrait_generator.onnx, providers[CPUExecutionProvider] ) # 查看输入输出信息 for inp in session.get_inputs(): print(f输入名: {inp.name}, 形状: {inp.shape}, 类型: {inp.type}) # 准备输入数据 appearance np.random.randn(1, 256, 64, 64).astype(np.float32) motion np.random.randn(1, 21, 3).astype(np.float32) # 执行推理 outputs session.run( [output_image], {appearance: appearance, motion: motion} ) result outputs[0] print(f输出形状: {result.shape})session.run 的第一个参数是输出名列表第二个是输入字典。输入名要和导出时设置的 input_names 一致否则会报找不到输入。输出结果是一个列表顺序和输出名列表对应。拿到结果后一般要做反归一化再转成图像。3.2 图像预处理把人脸对齐到模型需要的输入尺寸LivePortrait 的输入不是随便一张照片就能用需要先做人脸检测和对齐裁出人脸区域再缩放到模型要求的尺寸。常见做法是用人脸关键点检测器找到眼睛、鼻子、嘴角的位置然后做仿射变换把脸摆正。def preprocess_face(image_path, target_size(256, 256)): img cv2.imread(image_path) # 这里假设已经用关键点检测拿到了人脸框和关键点 # 实际项目中可以用 mediapipe 或 insightface face_region detect_and_align(img) face_resized cv2.resize(face_region, target_size) face_normalized face_resized.astype(np.float32) / 255.0 # 转成 NCHW 格式 face_input np.transpose(face_normalized, (2, 0, 1)) face_input np.expand_dims(face_input, axis0) return face_input预处理里最容易翻车的是归一化方式。有的模型要求减均值除方差有的只要求除以 255。这个必须和训练时保持一致否则输出画面会偏色或者糊掉。如果不确定可以先用一张已知正确的图片跑一遍对比输出和预期。3.3 驱动视频逐帧推理与结果拼接驱动视频要逐帧提取运动系数然后和源图像的外观特征一起送进生成网络。每一帧输出一张图最后用视频编码器拼成 mp4。def animate_portrait(source_img, driving_video_path, session): appearance preprocess_face(source_img) cap cv2.VideoCapture(driving_video_path) frames [] while True: ret, frame cap.read() if not ret: break # 提取当前帧的运动系数 motion extract_motion(frame) motion np.expand_dims(motion, axis0).astype(np.float32) # 推理 output session.run( [output_image], {appearance: appearance, motion: motion} )[0] # 后处理 out_frame postprocess(output) frames.append(out_frame) cap.release() save_video(frames, output.mp4, fps25)extract_motion 这一步如果也转成了 ONNX就再开一个 session 跑。如果没转就用原始 PyTorch 实现。逐帧推理时注意 batch 维保持为 1不要一次塞多帧除非模型导出时支持了动态 batch。4. C 侧部署用 onnxruntime C API 做高性能推理4.1 创建会话和配置线程数C 侧的 API 和 Python 侧思路一致但写法更啰嗦。创建环境、会话选项、会话三步走。线程数通过 SessionOptions 设置默认会用满所有核心在服务器上跑多实例时建议限制一下。#include onnxruntime_cxx_api.h #include opencv2/opencv.hpp Ort::Env env(ORT_LOGGING_LEVEL_WARNING, liveportrait); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); Ort::Session session(env, liveportrait_generator.onnx, session_options);SetIntraOpNumThreads 控制单个算子内部的并行度SetInterOpNumThreads 控制算子之间的并行度。在鲲鹏 920 这种多核 ARM 上IntraOp 设成 4 到 8 比较合适设太大反而会因为线程切换开销导致性能下降。GraphOptimizationLevel 开到 ENABLE_ALL 会做算子融合和常量折叠推理速度能提升一截。4.2 构造输入张量并执行推理C 里输入数据要用 Ort::Value 包装内存布局和 Python 侧一致。注意数据生命周期Ort::Value 创建时如果用的是外部内存要保证推理期间内存不被释放。std::vectorint64_t appearance_shape {1, 256, 64, 64}; std::vectorint64_t motion_shape {1, 21, 3}; std::vectorfloat appearance_data(1 * 256 * 64 * 64, 0.5f); std::vectorfloat motion_data(1 * 21 * 3, 0.0f); auto memory_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value appearance_tensor Ort::Value::CreateTensorfloat( memory_info, appearance_data.data(), appearance_data.size(), appearance_shape.data(), appearance_shape.size()); Ort::Value motion_tensor Ort::Value::CreateTensorfloat( memory_info, motion_data.data(), motion_data.size(), motion_shape.data(), motion_shape.size()); const char* input_names[] {appearance, motion}; const char* output_names[] {output_image}; Ort::Value input_tensors[] { std::move(appearance_tensor), std::move(motion_tensor)}; auto output_tensors session.Run( Ort::RunOptions{nullptr}, input_names, input_tensors, 2, output_names, 1);CreateTensor 的模板参数是数据类型要和模型输入类型匹配。如果模型输入是 float16这里就要写 CreateTensor Ort::Float16_t 。Run 的返回值是 vector Ort::Value 取第一个就是输出。输出数据的指针用 GetTensorMutableData () 拿。4.3 把输出张量转回图像并保存输出张量一般是 NCHW 格式的 float 数组要转成 HWC 的 uint8 才能存成图片。转换时注意通道顺序OpenCV 默认是 BGR。float* output_data output_tensors[0].GetTensorMutableDatafloat(); auto output_shape output_tensors[0].GetTensorTypeAndShapeInfo().GetShape(); int out_h static_castint(output_shape[2]); int out_w static_castint(output_shape[3]); cv::Mat result(out_h, out_w, CV_32FC3); for (int h 0; h out_h; h) { for (int w 0; w out_w; w) { for (int c 0; c 3; c) { float val output_data[c * out_h * out_w h * out_w w]; result.atcv::Vec3f(h, w)[c] val; } } } result.convertTo(result, CV_8UC3, 255.0); cv::imwrite(output_frame.png, result);这段循环是逐像素拷贝数据量大时可以用 memcpy 按通道拷贝再转置但要注意内存布局。如果输出是 RGB 而 OpenCV 要 BGR最后还要做一次通道交换。5. 避坑与排查部署 LivePortrait 时最容易翻车的五个地方5.1 模型加载报错「Unsupported model IR version」现象是 session 创建时直接抛异常提示 IR 版本不支持。原因是导出模型用的 onnx 版本比 onnxruntime 支持的 IR 版本高。解决方法是降低导出时的 opset或者升级 onnxruntime。如果升级 onnxruntime 不方便就在导出时把 opset 设成 13 或 14别用太新的。5.2 推理结果全黑或全白现象是输出图像没有任何内容要么全黑要么全白。原因通常是输入归一化方式不对或者输入数据布局搞错了。检查两点一是输入值范围是不是和训练时一致二是 NCHW 的维度顺序有没有写反。我遇到过把 HWC 直接当 NCHW 塞进去的情况输出就是一片噪声。5.3 C 侧编译通过但运行时报缺少 DLL 或 so现象是编译没问题一运行就提示找不到 onnxruntime.dll 或 libonnxruntime.so。原因是运行时链接器找不到库文件。Linux 下用 LD_LIBRARY_PATH 把库目录加进去Windows 下把 DLL 放到 exe 同目录或者加到 PATH。另外 Microsoft Visual C Redistributable 没装也会导致类似报错装一下就好。5.4 驱动视频帧率变化导致输出抖动现象是输出视频里人脸动作一顿一顿的不流畅。原因是驱动视频帧率不稳定或者运动系数在帧间跳变太大。解决方法是先对驱动视频做固定帧率重采样再在运动系数上做一点平滑滤波。简单做法是用滑动平均窗口设 3 到 5 帧。5.5 鲲鹏 920 上推理速度远低于预期现象是在 x86 上跑得好好的模型搬到鲲鹏 920 上慢了好几倍。原因可能是 onnxruntime 装成了 x86 版本或者没有启用 ARM 的 NEON 加速。确认方法是打印 ort.get_available_providers()看是不是只有 CPUExecutionProvider。另外检查 onnxruntime 的版本是不是 aarch64 专用包通用包在 ARM 上性能会打折。6. 进阶技巧用动态 batch 和缓存把吞吐量拉上去单帧推理跑通之后下一步要考虑的是吞吐量。如果驱动视频很长逐帧推理会非常慢。一个实用的优化是把运动系数提取和图像生成解耦运动系数提取可以批量做图像生成再逐帧或小批量跑。另一个技巧是缓存源图像的外观特征因为同一张源图像在整个驱动过程中外观特征不变没必要每帧都重新提取。# 缓存外观特征 appearance_cache None def get_appearance(source_img, session): global appearance_cache if appearance_cache is None: appearance_cache preprocess_face(source_img) return appearance_cache这个缓存看起来简单但在实际项目里能省掉将近一半的计算量。外观提取网络虽然比生成网络小但每帧都跑一遍累积起来也很可观。注意缓存的生命周期要和源图像绑定换源图像时要清掉。验证优化效果的方法是用同一段驱动视频跑两次一次开缓存一次不开对比总耗时。我一般会在代码里加一个简单的计时器输出每帧平均耗时和总耗时。如果开了缓存之后耗时没有明显下降说明瓶颈在生成网络那就考虑把生成网络也做批量推理。批量推理的做法是把多帧的运动系数拼成一个 batch 送进去输出再拆开。这要求导出模型时 batch 维是动态的前面 dynamic_axes 里已经设了。批量大小根据显存或内存来定CPU 上一般 4 到 8 比较合适太大反而会因为内存带宽瓶颈变慢。# 批量推理示例 batch_motions np.stack(motion_list, axis0).astype(np.float32) batch_appearance np.repeat(appearance, len(motion_list), axis0) outputs session.run( [output_image], {appearance: batch_appearance, motion: batch_motions} )这里 appearance 用 np.repeat 复制了多份其实如果模型支持广播可以只传一份但 ONNX 导出时不一定保留了广播语义保险起见还是复制。批量推理的收益在 CPU 上通常有 1.5 到 2 倍在 GPU 上更明显。最后说一个我自己的习惯每次改完模型导出参数或者推理代码都会先用一张固定图片和一段固定驱动视频跑一遍把输出保存下来作为基准。后面任何改动都跟这个基准对比肉眼能看出差异就说明有问题。这个习惯帮我省了很多次重新排查的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表