
简介基于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里调libtorchLibtorch是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.8onnxruntime-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, providersproviders) lmk_session ort.InferenceSession(models/landmark.onnx, sess_options, providersproviders) stitch_session ort.InferenceSession(models/stitching.onnx, sess_options, providersproviders) retarget_session ort.InferenceSession(models/retargeting.onnx, sess_options, providersproviders)这里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, Lmodels/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-gpu1.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, h512, w512): # 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, margin0.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::arrayint64_t, 4 input_shape{1, 3, 512, 512}; std::arrayint64_t, 3 output_shape{1, 512, 512}; // 具体以onnx为准 auto memory_info Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeDefault); std::vectorfloat input_data(1 * 3 * 512 * 512); std::vectorfloat output_data(1 * 512 * 512); // 预分配输入/输出tensor整个生命周期复用 Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); Ort::Value output_tensor Ort::Value::CreateTensorfloat( 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个模型时显存占用主要来自stitching和retargeting两个模型。如果显存仍然吃紧可以考虑让face_det和landmark这两个模型走CPU EP只让retargeting和stitching走CUDA用CPU/GPU混合的方式降低峰值显存。代价是CPU上的两段推理会增加约20到40毫秒延迟但对离线批处理影响不大。5. 让动画更自然的参数调试与四个常见坑5.1 对关键点序列做时域平滑防止脸部抖动的实用做法LivePortrait生成的视频出现不自然的抖动常见原因不是模型质量而是相邻帧的关键点坐标跳变。驱动视频本身如果有轻微晃动retargeting输出的系数会被放大。常见做法是对关键点序列做一阶低通滤波也就是EMA平滑def ema_smooth(points, alpha0.3): smoothed [] prev points[0] for p in points: prev alpha * p (1 - alpha) * prev smoothed.append(prev) return np.array(smoothed)alpha越大跟踪越快但抖动抑制能力弱alpha越小视频越稳定但人物动作会显得黏滞。嘴部区域建议alpha0.3头部姿态建议alpha0.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_convertstd::codecvt_utf8_utf16wchar_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 常见坑三动态库缺失导致程序启动报0xc000007bC部署包在换了一台电脑后双击运行报0xc000007b十有八九是缺onnxruntime.dll或依赖的VC运行库。程序运行时依赖msvcp140.dll、vcomp140.dll等运行库目标机器需要安装Microsoft Visual C Redistributable对应版本。但更微妙的是onnxruntime.dll本身也依赖这些运行库。交付时把onnxruntime.dll放在exe同目录并确认运行库已装到位是最省事的规避方案。不要指望把所有dll塞进system32不同版本的运行库会互相覆盖问题更难排查。5.5 常见坑四动态输入维度导致的shape mismatchLivePortrait导出的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输出视频的帧率要与驱动视频帧率一致。若驱动视频是30fpswriter也要设成30fps否则播放时口型速度对不上。编写批处理时顺便把失败帧的索引记录下来对应驱动视频丢帧的位置会显示为黑帧或静止帧需要在后处理里用前一帧填充。本文还有配套的精品资源点击获取