news 2026/9/10 2:26:35

用onnxruntime部署LivePortrait人像动画:Python与C++双链路推理实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用onnxruntime部署LivePortrait人像动画:Python与C++双链路推理实践

简介:基于onnxruntime部署LivePortrait人像动画生成的程序包,包含C++与Python两种实现方式,适合需要将人脸驱动、表情迁移等能力集成到本地应用的开发者。压缩包内共14个文件,大小约459KB,以C++源文件(.cpp/.h)和Python脚本(.py)为主,辅以CMakeLists构建配置、README说明文档、示例图片及一段演示视频,文件类型覆盖源码、文档与素材,便于快速上手。目前已有204人学习下载。压缩包内按照liveportrait-onnxrun-main组织目录,python与cpp两个分支可对照阅读,其中人脸分析、图像裁剪、主推理等核心模块均单独拆分,代码结构清晰。读者可获得可直接编译运行的工程脚本、完整的onnxruntime调用示例,以及用于效果验证的静态图和驱动视频,适合有一定深度学习部署基础、希望参考C++/Python双语言实现细节的开发者。

1. 用onnxruntime部署LivePortrait人像动画生成:一个部署包里的两条推理链路

LivePortrait人像动画生成,常见形态是用一张静态人脸照片去跟拍一段驱动视频,把视频里的表情、头部姿态和眼神迁移到照片上,输出一段口型同步、头部微微转动的动态人像。官方实现跑在PyTorch上,模型拆成多个子网络,环境依赖一多,交付时经常被CUDA版本、torch版本和一堆wheel包卡住。onnxruntime介入之后,模型被固化成onnx文件,C++和Python共用同一套推理产物,服务端、边缘盒子、离线批处理都能跑同一个模型。这个zip包的名称已经把交付结构说清楚了:一条Python链路负责快速调试和批量生成,一条C++链路负责生产环境和低延迟调用。适合做数字人、直播特效、短视频模板的开发者。下面按两条链路分别展开,先理清LivePortrait在onnxruntime里到底是什么。

2. LivePortrait在onnxruntime里的模型构成与选型逻辑

2.1 别指望一个model.onnx搞定:LivePortrait是多段推理管线

把LivePortrait导出成onnx之后,它不是一个端到端大模型,而是一组分工明确的子模型。常见做法是拆成四个部分:人脸检测模型,负责从输入图像中定位人脸框;关键点模型,负责提取面部特征点和姿态;retargeting模型,负责从驱动视频帧中抽取表情系数;stitching模型,负责把源人脸和驱动系数合成为最终帧。严格来说,官方仓库里还可能涉及eye和lip的单独处理分支,但部署包通常会把眼睛和嘴巴的系数回归合并进retargeting流程,让调用方少一次IO。

这样拆的好处是每个子模型都可以被替换或单独升级。比如人脸检测可以换成更轻量的版本,在不影响表情迁移质量的前提下降低CPU负载。代价是C++和Python两端都要按顺序调用多个session,中间的张量格式、归一化方式必须保持一致。实际调试时,先确认每个onnx文件的输入输出名和shape,再写pipeline。这一步跳过,后面shape mismatch会反复出现。

2.2 为什么选onnxruntime而不是直接在C++里调libtorch

Libtorch是PyTorch的C++前端,能跑原版模型,但动态库体积大,部署环境要跟着CUDA和C++ ABI版本走,稍有不慎就链接失败。onnxruntime的核心设计是固定计算图、统一运行时,Python和C++调用的是同一个底层实现。对LivePortrait这种多模型编排的推理任务,onnxruntime的图优化能自动做算子融合,比如把LayerNorm和矩阵乘合并,减少kernel启动开销。对于Jetson这类边缘设备,还能用TensorRT EP来加速,而Python和C++的调用方式完全不变。

选择onnxruntime还有一个现实原因:更新节奏稳定,CPU、GPU、TensorRT三种ExecutionProvider的API长期保持兼容。这意味着用Python调通的模型,C++端改几行初始化代码就能跑出同样结果,不需要为两种语言维护两套权重转换逻辑。模型文件是同一份onnx,两端只是换了个壳。

2.3 C++与Python两种部署形态的选择边界

Python入口的优势是迭代快,适合效果调试和离线批量生成。C++入口适合嵌入到现有服务进程里,比如推流服务、视频处理管线,以及要求首帧延迟小于100毫秒的场景。两者不是替代关系,而是同一份onnx在不同生命周期里的两种形态。

对比维度Python入口C++入口
典型场景效果调试、批量离线生成、算法验证在线服务、嵌入式平台、低延迟调用
环境依赖Python 3.8+,onnxruntime-gpu、opencv-pythononnxruntime动态库、OpenCV、VS2019/2022或GCC
单帧延迟偏高,数据搬运与GIL有开销更低,GIL不存在,内存可复用
开发成本低,改完脚本立即看效果高,编译错误和内存管理需要时间
模型文件一组onnx文件与Python完全同一组onnx文件

实际项目中,我会先用Python把各段模型的输入输出形状和数值范围调通,确认效果满意后再把同样的调用序列平移成C++代码。这样排错范围从“算法问题+环境问题”缩小成纯粹的环境和内存问题。

2.4 初始化onnxruntime Session的最小代码(Python和C++对照)

Python端初始化一组session的代码很短。重点是设置图优化级别和线程数,并明确指定CUDA优先、CPU兜底:

import onnxruntime as ort # 图优化全开,让onnxruntime自动做算子融合 sess_options = ort.SessionOptions() sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL # 限制intra-op线程数,避免多模型并发时互相抢占CPU sess_options.intra_op_num_threads = 4 providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] det_session = ort.InferenceSession("models/face_det.onnx", sess_options, providers=providers) lmk_session = ort.InferenceSession("models/landmark.onnx", sess_options, providers=providers) stitch_session = ort.InferenceSession("models/stitching.onnx", sess_options, providers=providers) retarget_session = ort.InferenceSession("models/retargeting.onnx", sess_options, providers=providers)

这里providers列表的顺序决定了执行优先级,onnxruntime会依次检查每个provider是否支持当前算子。CUDA EP不支持某些算子时,会回退到CPU,这一机制保证了兼容性,但也会带来CPU/GPU混用导致的额外拷贝,性能敏感时要通过profiler确认哪些段落到走了CPU。

C++端初始化逻辑一模一样,只是API风格不同:

#include <onnxruntime_cxx_api.h> Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "liveportrait"); Ort::SessionOptions opts; opts.SetIntraOpNumThreads(4); opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); // 方式一:直接追加CUDA EP OrtCUDAProviderOptions cuda_options{}; cuda_options.device_id = 0; opts.AppendExecutionProvider_CUDA(cuda_options); Ort::Session det_session(env, L"models/face_det.onnx", opts);

注意Windows下路径要使用宽字符,否则中文路径或带空格的目录会导致打开模型失败。C++端还有一个容易被忽略的点:Ort::Session对象构造时会加载并解析整个模型,耗时几十到几百毫秒不等,所以session应该作为长生命周期对象复用,绝不能放在每帧推理的函数里反复创建。

3. Python端从静态图到动画视频的最小推理流程

3.1 环境准备:先确认python环境,再装onnxruntime-gpu

很多人拿到的LivePortrait整合包,Python入口就是onnxruntime加OpenCV加NumPy的组合。手动搭建时,先确认python版本在3.8到3.11之间,然后用pip安装。GPU环境的包名是onnxruntime-gpu,不要和CPU版onnxruntime装混,否则会出现在同一环境里两个包互相覆盖的情况。

pip install onnxruntime-gpu==1.17.0 opencv-python numpy

装完后用一行命令验证CUDA ExecutionProvider是否可用:

python -c "import onnxruntime as ort; print(ort.get_available_providers())"

输出列表里必须包含CUDAExecutionProvider。如果只有CPUExecutionProvider,大概率是onnxruntime-gpu版本与CUDA、cuDNN版本不匹配。此时不需要急着换包版本,先在onnxruntime官方兼容表里确认当前CUDA版本对应的runtime版本,再重新安装。

3.2 完整推理脚本:检测、关键点、retargeting、stitching的顺序不能乱

LivePortrait的推理pipeline一定按这个顺序执行:先检测人脸,再提取关键点,然后对驱动视频的每一帧抽取表情系数,最后用stitching把源人脸与驱动系数合成新帧。下面给出可直接运行的简化脚本结构:

import cv2 import numpy as np import onnxruntime as ort from tqdm import tqdm def load_session(path, so): return ort.InferenceSession(path, so, providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) so = ort.SessionOptions() so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL det = load_session("models/face_det.onnx", so) lmk = load_session("models/landmark.onnx", so) retarget = load_session("models/retargeting.onnx", so) stitch = load_session("models/stitching.onnx", so) def preprocess(img, h=512, w=512): # BGR转RGB,缩放并归一化到[0,1],最后转成NCHW img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (w, h)) img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1))[None] return img def crop_face(img, bbox, margin=0.2): x1, y1, x2, y2 = bbox w, h = x2 - x1, y2 - y1 x1 = max(0, int(x1 - margin * w)) y1 = max(0, int(y1 - margin * h)) x2 = min(img.shape[1], int(x2 + margin * w)) y2 = min(img.shape[0], int(y2 + margin * h)) return img[y1:y2, x1:x2] def infer_video(source_img, driving_frames, video_writer): # 1) 检测人脸 bbox = det.run(None, {"input": preprocess(source_img)})[0][0] face = crop_face(source_img, bbox[:4]) face_tensor = preprocess(face) # 2) 提取源图关键点 src_lmk = lmk.run(None, {"input": face_tensor})[0] # 3) 对每一帧驱动图提取系数并合成 for frame in tqdm(driving_frames): kp_drv = retarget.run(None, {"input": preprocess(frame)})[0] out = stitch.run(None, {"src": src_lmk, "drv": kp_drv})[0] out = np.transpose(out[0], (1, 2, 0)) out = np.clip(out * 255, 0, 255).astype(np.uint8) video_writer.write(cv2.cvtColor(out, cv2.COLOR_RGB2BGR))

这段脚本的关键点在于输入张量的组织方式。每一个run调用的第一个参数是输出名列表,传None表示取全部输出,第二个参数是输入字典,键名必须和onnx模型导出时定义的输入名一致。常见错误是拿PyTorch源码里的变量名去当输入名,而导出的onnx往往做了简化。正确做法是先打印session的输入元数据:

for inp in det.get_inputs(): print(inp.name, inp.shape, inp.type)

输出示例可能是input [1,3,640,640] float32,这就把输入尺寸和通道顺序都固定下来了。归一化方式也要跟导出时的预处理对齐,常见的做法是除以255或ImageNet均值方差,两种差异会导致最终视频饱和度完全不同。

3.3 驱动系数与blending参数的调节范围

LivePortrait动画效果不佳,很多时候不是模型问题,而是系数参数取值范围不对。下面列出我在实际调试中会用到的参数区间和它们各自的效果:

参数取值范围效果说明
driving口型放大系数0.8~1.2控制嘴巴张开的幅度,超过1.5会显得嘴部僵硬
眨眼强度系数0.5~1.5控制眼神闭合程度,过高会出现闪烁感
blending融合系数0.5~1.0源图与驱动帧的混合比例,越高越接近真人皮肤细节
时域平滑窗口5~15帧抑制关键点抖动,太小视频闪,太大会有明显延迟感

这些参数的暴露方式每个部署包不太一样,有些直接写在配置文件的infer_params里,有些是要在调用stitching.run之前对kp_drv做乘法。注意区分:口型系数乘的是retargeting输出的嘴部相关维度,而不是整个系数向量整体缩放。整体缩放会把头部姿态也放大,结果就是人脸乱晃。

4. C++端动态库加载与GPU推理落地

4.1 在CMake工程里链上onnxruntime动态库的配置方法

C++端不推荐用vcpkg去装onnxruntime,版本滞后且控制不细。我一般直接把官方发布的onnxruntime压缩包放进third_party/onnxruntime目录,目录下包含include和lib两个子目录。CMakeLists.txt里用绝对路径指定,避免给同事的电脑带来环境差异。

cmake_minimum_required(VERSION 3.20) project(liveportrait_cpp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(ORT_DIR "${CMAKE_SOURCE_DIR}/third_party/onnxruntime") include_directories(${ORT_DIR}/include) link_directories(${ORT_DIR}/lib) find_package(OpenCV REQUIRED) add_executable(lp_main src/main.cpp) target_link_libraries(lp_main PRIVATE onnxruntime ${OpenCV_LIBS})

链接库名在Windows上是onnxruntime.lib对应的动态库onnxruntime.dll,在Linux上是libonnxruntime.so。编译前确认架构是x64还是arm64,两者不能混用。用VSCode配置C/C++环境时,只要把includePath指向${ORT_DIR}/includec_cpp_properties.json里的compilerPath选对,代码补全和编译就都能跑通。

4.2 C++推理循环与内存复用

C++推理代码要特别注意内存复用。每帧都创建Ort::Value并重新分配输出buffer,不仅慢,还会让内存峰值暴涨。下面的代码展示了在循环外预分配tensor、循环内只更新数据的方式:

std::array<int64_t, 4> input_shape{1, 3, 512, 512}; std::array<int64_t, 3> output_shape{1, 512, 512}; // 具体以onnx为准 auto memory_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeDefault); std::vector<float> input_data(1 * 3 * 512 * 512); std::vector<float> output_data(1 * 512 * 512); // 预分配输入/输出tensor,整个生命周期复用 Ort::Value input_tensor = Ort::Value::CreateTensor<float>( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); Ort::Value output_tensor = Ort::Value::CreateTensor<float>( memory_info, output_data.data(), output_data.size(), output_shape.data(), output_shape.size()); const char* input_names[] = {"input"}; const char* output_names[] = {"output"}; for (const auto& frame : driving_frames) { // 填充input_data preprocess(frame, input_data); // 推理,输出直接写入output_data session.Run(Ort::RunOptions{nullptr}, input_names, &input_tensor, 1, output_names, &output_tensor, 1); // 从output_data中取结果 postprocess(output_data); }

注意output_shape不能凭感觉写。onnx模型输出是动态shape时,CreateTensor时指定的shape必须与模型实际输出一致,否则Run会报错。最稳妥的方式是先调用session.GetOutputTypeInfo(0).GetTensorTypeAndShapeInfo().GetShape()拿到实际输出维度,再据此分配buffer。

4.3 在边缘设备上配置CUDA EP和显存策略

Jetson Orin NX这类设备上跑LivePortrait是常见需求,显存有限,需要调整onnxruntime的CUDA内存策略。arena_extend_strategy控制缓存分配器扩展方式,默认策略会预留较大显存,在Orin上容易导致CUDAGraph或后续任务无显存可用。显存不足时,优先调整这个参数:

OrtCUDAProviderOptions cuda_options{}; cuda_options.device_id = 0; // kSameAsRequested:按请求大小分配,显存占用更保守 cuda_options.arena_extend_strategy = 1; // 在默认流上做H2D拷贝,减少多流同步开销 cuda_options.do_copy_in_default_stream = 1; opts.AppendExecutionProvider_CUDA(cuda_options);

Orin NX上跑4个模型时,显存占用主要来自stitchingretargeting两个模型。如果显存仍然吃紧,可以考虑让face_detlandmark这两个模型走CPU EP,只让retargeting和stitching走CUDA,用CPU/GPU混合的方式降低峰值显存。代价是CPU上的两段推理会增加约20到40毫秒延迟,但对离线批处理影响不大。

5. 让动画更自然的参数调试与四个常见坑

5.1 对关键点序列做时域平滑,防止脸部抖动的实用做法

LivePortrait生成的视频出现不自然的抖动,常见原因不是模型质量,而是相邻帧的关键点坐标跳变。驱动视频本身如果有轻微晃动,retargeting输出的系数会被放大。常见做法是对关键点序列做一阶低通滤波,也就是EMA平滑:

def ema_smooth(points, alpha=0.3): smoothed = [] prev = points[0] for p in points: prev = alpha * p + (1 - alpha) * prev smoothed.append(prev) return np.array(smoothed)

alpha越大,跟踪越快,但抖动抑制能力弱;alpha越小,视频越稳定,但人物动作会显得黏滞。嘴部区域建议alpha=0.3,头部姿态建议alpha=0.2,因为头部大幅转动时平滑过度会产生明显的滞后感。可以先对整段视频做一次平滑预览,再按五官区域分开调。

5.2 常见坑一:Session跨线程调用导致崩溃

onnxruntime的Session对象不是完全无锁的,多个线程同时调用同一个Session的Run,轻则性能下降,重则直接崩溃。C++服务里常见错误是开一个线程池,每个请求共用同一个全局Session。正确做法有两种:一是每个线程独立创建Session,内存开销大但无锁争用;二是外部加互斥锁,吞吐要求不高时足够。Python端用concurrent.futures.ThreadPoolExecutor时也要注意,如果是同一Session并发推理,线程数超过1后速度不升反降。

5.3 常见坑二:C++字符串与路径编码引发的模型加载失败

Windows下C++读模型路径,如果路径含中文或空格,Ort::Session构造会失败,错误信息却不直观。原因在于onnxruntime会按UTF-8解析路径,而std::string从命令行拿到的是本地代码页编码。解决办法是使用宽字符重载:

std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter; std::wstring wide_path = converter.from_bytes(model_path_utf8); Ort::Session session(env, wide_path.c_str(), opts);

图片路径同理,cv::imread在Windows下对中文路径支持不好,先转成std::wstring再调用cv::imdecode读取文件内容,能绕开大部分编码问题。

5.4 常见坑三:动态库缺失导致程序启动报0xc000007b

C++部署包在换了一台电脑后双击运行报0xc000007b,十有八九是缺onnxruntime.dll或依赖的VC++运行库。程序运行时依赖msvcp140.dllvcomp140.dll等运行库,目标机器需要安装Microsoft Visual C++ Redistributable对应版本。但更微妙的是onnxruntime.dll本身也依赖这些运行库。交付时把onnxruntime.dll放在exe同目录,并确认运行库已装到位,是最省事的规避方案。不要指望把所有dll塞进system32,不同版本的运行库会互相覆盖,问题更难排查。

5.5 常见坑四:动态输入维度导致的shape mismatch

LivePortrait导出的onnx模型,输入shape常是[1,3,-1,-1][batch,3,512,512],动态维度让同一个模型能处理不同分辨率输入。但C++端如果用固定shape创建Ort::Value,而输入图像尺寸不是512的整数倍,就会触发shape mismatch。处理方式是推理前读一次输入shape,动态计算tensor维度:

错误写法正确写法
硬编码{1,3,512,512}直接创建tensor读取session.GetInputTypeInfo再创建tensor
所有模型用同一组shape每个模型独立读取输入输出shape
在循环内部创建新tensor循环外预分配,循环内改写数据

这个坑的隐蔽之处在于onnxruntime在CPU EP下可能帮你做了隐式resize,但换上CUDA EP后shape检查变严格,问题才暴露出来。

6. 用视频平滑技巧验证部署效果,顺手处理批处理

6.1 用EMA双系数把“像”变成“稳”

上一节提到的EMA平滑适用于关键点坐标,但对stitching输出的图像帧,用像素级混合更直接。做法是把当前帧与上一帧输出按比例混合:

prev_frame = None alpha = 0.25 for idx, frame in enumerate(driving_frames): out = infer_one_frame(frame) if prev_frame is None: prev_frame = out else: out = cv2.addWeighted(out, 1 - alpha, prev_frame, alpha, 0) prev_frame = out video_writer.write(out)

这个做法的代价是快速转头时会有轻微拖影,但能抹平大部分细小抖动。如果动作幅度大,可以把alpha降为0.1再做一次对比,选择视觉上更自然的版本。

6.2 一份用于验收的批处理脚本

部署完成后的验收不能只靠肉眼。先让Python和C++两条链路跑同一段驱动视频,对输出的逐帧像素做差值计算,差异超过1%的地方要检查预处理是否完全一致。再统计每帧耗时,Python端用time.perf_counter(),C++端用std::chrono::steady_clock,循环执行100次取均值。

一个实用的批处理做法是让C++入口支持命令行参数指定输入目录和输出目录,这样Shell脚本或Python脚本都能调用它:

./lp_main --source ./images/zhang.png \ --driving ./data/drive.mp4 \ --out ./result/zhang_drive.mp4 \ --alpha 0.25

输出视频的帧率要与驱动视频帧率一致。若驱动视频是30fps,writer也要设成30fps,否则播放时口型速度对不上。编写批处理时顺便把失败帧的索引记录下来,对应驱动视频丢帧的位置会显示为黑帧或静止帧,需要在后处理里用前一帧填充。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 2:24:13

电商智能分仓与需求预测实战:从数据集到闭环落地

简介&#xff1a;本资源是一份面向物流算法工程师、运筹优化与机器学习从业者及高校相关专业研究生的实战型数据集与建模方案包&#xff0c;聚焦电商场景下“单未下、货先行”的前置分仓策略&#xff0c;解决区域需求精准预测、RDC/FDC库存调拨协同与全局成本优化等核心问题。资…

作者头像 李华
网站建设 2026/9/10 2:22:20

Python面试三大件:迭代器、生成器与装饰器原理与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华