☰
C# OnnxRuntime部署LivePortrait:人像驱动视频生成实战
2026/10/2 1:01:34 网站建设 项目流程

简介:本资源面向具备一定C#与深度学习基础的开发者,提供在.NET环境下通过OnnxRuntime部署LivePortrait模型的完整工程,用于实现快速、高质量的人像驱动视频生成。包内共384个文件,涵盖61个dll动态库、7个onnx模型文件、12个cs源码、40个xml配置、33张jpg示例图及19个mp4演示视频,另含onnxruntime运行时、lib与so等跨平台依赖,压缩包约882.12MB,结构完整可直接编译运行。已有182人学习下载,适合研究人像动画、虚拟主播或视频驱动方向的开发者参考。借助该工程,读者可掌握模型加载、推理会话配置与图像预处理流程,理解C#调用OnnxRuntime的接口封装方式,并对照示例素材快速验证驱动效果,为二次开发与性能调优提供可复用的实践基础。

1. 拆开这个 C# 资源包:LivePortrait 人像驱动到底能跑出什么效果

上周有个做虚拟主播上位机的朋友甩给我一个压缩包,标题写着「C# OnnxRuntime部署LivePortrait实现快速、高质量的人像驱动视频生成.rar」。他问得很直接:这玩意儿能不能在纯 C# 环境里跑起来,不装 Python、不碰 CUDA 那套环境,直接喂一张人像图加一段驱动视频,输出一段嘴型和表情跟着动的视频?我拆完之后的结论是:能,而且推理链路比想象中干净,但前提是你得搞清楚它到底封装了 LivePortrait 的哪几个模块。

LivePortrait 本身是快手开源的隐式关键点驱动方案,核心思路是把源人像和驱动视频分别编码成一组隐式关键点,在关键点空间做形变迁移,再解码回图像。原版是 PyTorch 训练加推理,而这个资源包做的事,是把推理阶段的模型导出成 ONNX,然后用 C# 的 OnnxRuntime 做前向计算,配合图像预处理和后处理,拼出一条完整的视频生成流水线。适合谁?做 C# 上位机、桌面端工具、Unity 插件、或者不想在产线机器上装 Python 运行时的开发者。你要的是能嵌进现有 .NET 工程的人像驱动能力,而不是再维护一套 Python 服务。

2. 模型拆解与 OnnxRuntime 会话初始化:五个 ONNX 文件各干什么

2.1 LivePortrait 推理链路的模块划分

拆开资源包里的模型目录,你会看到不止一个 .onnx 文件。LivePortrait 的推理不是单模型端到端,而是拆成了几个功能模块,常见做法是分成外观提取、运动提取、形变生成、解码生成这几段。具体到文件命名,不同导出脚本会有差异,但功能上跑不出这几类:

  • 外观编码器:输入源人像,输出三维外观特征,决定「长得像谁」。
  • 运动提取器:输入驱动视频的每一帧,输出隐式关键点,决定「怎么动」。
  • 关键点形变模块:把源人像的关键点和驱动关键点做映射,生成形变后的特征。
  • 解码器:把形变特征还原成 RGB 图像帧。
  • 可选的表情系数或头部姿态模块:部分导出会单独拆出来,方便你做参数控制。

这里有个容易翻车的点:很多人以为一个 ONNX 就能搞定,结果初始化会话时发现输入张量对不上。你得先确认每个模型的输入输出 shape,再决定 C# 里怎么串。

2.2 C# 里创建 InferenceSession 的正确姿势

OnnxRuntime 的 C# API 用起来不复杂,但会话选项配不好,性能差一倍。下面是我一般会用的初始化代码:

using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; // 按模型角色分别创建会话,不要共用一个 SessionOptions public class LivePortraitEngine : IDisposable { private readonly InferenceSession _appearanceSession; private readonly InferenceSession _motionSession; private readonly InferenceSession _warpSession; private readonly InferenceSession _decodeSession; public LivePortraitEngine(string modelDir, bool useGpu = false) { var options = new SessionOptions(); // 关掉图优化里的常量折叠,LivePortrait 的动态 shape 容易被折叠出问题 options.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_EXTENDED; options.EnableMemoryPattern = false; // 动态输入下开内存模式反而拖慢 options.IntraOpNumThreads = Environment.ProcessorCount / 2; if (useGpu) { // 需要引用 Microsoft.ML.OnnxRuntime.Gpu 包 options.AppendExecutionProvider_CUDA(0); } _appearanceSession = new InferenceSession(Path.Combine(modelDir, "appearance.onnx"), options); _motionSession = new InferenceSession(Path.Combine(modelDir, "motion.onnx"), options); _warpSession = new InferenceSession(Path.Combine(modelDir, "warp.onnx"), options); _decodeSession = new InferenceSession(Path.Combine(modelDir, "decoder.onnx"), options); } public void Dispose() { _appearanceSession?.Dispose(); _motionSession?.Dispose(); _warpSession?.Dispose(); _decodeSession?.Dispose(); } }

逻辑说明:每个模型单独建会话,是因为它们的输入 shape 和优化策略不一样,混在一起容易触发 shape 推断异常。参数上,EnableMemoryPattern在输入尺寸固定的场景下能提速,但 LivePortrait 的驱动帧尺寸经常变,关掉更稳。IntraOpNumThreads设成核数一半,是因为图像预处理本身也吃 CPU,全给推理线程反而整体变慢。

2.3 输入张量的预处理对齐

ONNX 模型不认 Bitmap,你得把图像转成 float 张量。LivePortrait 的输入一般是 NCHW 格式,归一化到 [0,1] 或 [-1,1] 取决于导出时的配置。我踩过的坑是归一化系数搞反,输出人脸发灰。下面这段是标准做法:

public static DenseTensor<float> BitmapToTensor(Bitmap bmp, int width, int height, float mean, float std) { var resized = new Bitmap(bmp, new Size(width, height)); var tensor = new DenseTensor<float>(new[] { 1, 3, height, width }); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { var pixel = resized.GetPixel(x, y); // 注意通道顺序:ONNX 导出时多为 RGB,而 Bitmap 是 BGR 排列 tensor[0, 0, y, x] = (pixel.R / 255f - mean) / std; tensor[0, 1, y, x] = (pixel.G / 255f - mean) / std; tensor[0, 2, y, x] = (pixel.B / 255f - mean) / std; } } return tensor; }

参数说明:mean和std必须和导出模型时的配置一致,常见是 0.5/0.5 或 ImageNet 的 0.485/0.456/0.406。通道顺序写错,人脸会偏色,这个用肉眼就能看出来。GetPixel在批量处理时性能很差,生产环境建议用LockBits或Span<byte>直接读内存。

3. 从单帧到视频:驱动帧循环、关键点迁移与编码输出

3.1 驱动视频抽帧与帧率对齐

人像驱动的输入是一段驱动视频,你得先把它拆成帧序列。C# 里没有内置的视频解码,常见做法是用 FFmpeg 命令行抽帧,或者用 OpenCvSharp 的 VideoCapture。我一般用后者,因为能直接在内存里拿 Mat,省一次磁盘 IO:

using OpenCvSharp; public static List<Mat> ExtractFrames(string videoPath, int targetFps = 25) { var frames = new List<Mat>(); using var capture = new VideoCapture(videoPath); if (!capture.IsOpened()) throw new IOException($"无法打开驱动视频: {videoPath}"); double srcFps = capture.Fps; int frameIndex = 0; int step = (int)Math.Round(srcFps / targetFps); // 抽帧间隔 while (true) { var frame = new Mat(); if (!capture.Read(frame) || frame.Empty()) break; if (frameIndex % step == 0) frames.Add(frame.Clone()); frameIndex++; } return frames; }

逻辑说明:驱动视频帧率往往高于输出需求,直接全量推理会浪费算力。step控制抽帧间隔,把驱动帧率对齐到目标输出帧率。注意frame.Clone()不能省,OpenCV 的 Mat 是复用缓冲区的,不克隆的话你拿到的全是最后一帧。

3.2 关键点迁移的核心调用顺序

LivePortrait 的推理顺序不能乱:先编码源人像的外观,再逐帧提取驱动运动,然后做形变,最后解码。乱序会导致关键点空间对不上,输出直接糊成一团。下面是一次完整前向的调用骨架:

public Bitmap GenerateFrame(Bitmap sourceFace, DenseTensor<float> drivingMotion) { // 1. 源人像外观编码,只需算一次,循环外缓存 var sourceTensor = BitmapToTensor(sourceFace, 256, 256, 0.5f, 0.5f); var appearanceInput = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", sourceTensor) }; using var appearanceResult = _appearanceSession.Run(appearanceInput); var appearanceFeature = appearanceResult.First().AsTensor<float>(); // 2. 驱动运动特征与外观特征拼接,送入形变模块 var warpInput = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("appearance", appearanceFeature), NamedOnnxValue.CreateFromTensor("motion", drivingMotion) }; using var warpResult = _warpSession.Run(warpInput); var warpedFeature = warpResult.First().AsTensor<float>(); // 3. 解码回图像 var decodeInput = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("feature", warpedFeature) }; using var decodeResult = _decodeSession.Run(decodeInput); return TensorToBitmap(decodeResult.First().AsTensor<float>()); }

参数说明:输入节点名input、appearance、motion、feature必须和导出时的名字一致,用 Netron 打开 ONNX 文件就能看到。外观编码只算一次,放在循环外缓存,这是性能优化的关键——每帧都重算外观,速度直接砍半。

3.3 输出帧合成视频

解码出来的是一张张 Bitmap,最后要合成视频。用 OpenCvSharp 的 VideoWriter 最省事:

public static void WriteVideo(List<Bitmap> frames, string outputPath, int fps = 25) { int width = frames[0].Width; int height = frames[0].Height; using var writer = new VideoWriter(outputPath, FourCC.MP4V, fps, new Size(width, height)); if (!writer.IsOpened()) throw new IOException("VideoWriter 初始化失败,检查编码器"); foreach (var bmp in frames) { using var mat = OpenCvSharp.Extensions.BitmapConverter.ToMat(bmp); writer.Write(mat); } }

逻辑说明:FourCC.MP4V在部分 Windows 环境下没有对应编码器,会静默失败。稳妥做法是先用FourCC.XVID输出 avi,再用 FFmpeg 转 mp4。fps要和抽帧时的目标帧率一致,否则视频会变速。

4. 避坑与排查:C# 部署 LivePortrait 最常见的五类翻车

4.1 现象:推理结果全黑或全灰

原因:归一化参数和模型导出配置不匹配,或者通道顺序 BGR/RGB 搞反。LivePortrait 的导出脚本通常按 RGB 处理,而System.Drawing.Bitmap的GetPixel返回的是 BGR 排列。

解决:先用一张纯色图跑一遍,打印输出张量的数值范围。正常应该在 [0,1] 或 [-1,1] 之间。如果全是 0 或 255,检查归一化;如果颜色偏蓝或偏红,交换 R 和 B 通道。

4.2 现象:OnnxRuntime 报「Input shape mismatch」

原因:驱动帧尺寸和模型期望的输入尺寸不一致。LivePortrait 的运动提取模块对输入分辨率有固定要求,常见是 256x256,你直接喂原始 1080p 帧就会炸。

解决:在预处理阶段统一 resize 到模型要求的尺寸。用 Netron 看每个模型的输入维度,动态维度会标成dynamic或-1,固定维度必须严格对齐。

4.3 现象:GPU 推理比 CPU 还慢

原因:每次推理都新建 InferenceSession,或者没释放IDisposable资源。CUDA 上下文初始化本身有开销,频繁创建会话会把开销放大。

解决:会话在应用启动时创建一次,全局复用。另外确认引用的 NuGet 包是Microsoft.ML.OnnxRuntime.Gpu而不是 CPU 版,两者 API 一样但运行时不同。

4.4 现象:输出视频人脸抖动严重

原因:驱动帧之间的关键点没有做时序平滑,逐帧独立推理会放大抖动。LivePortrait 原版在 Python 里有平滑处理,C# 移植时容易被漏掉。

解决:对运动特征做滑动窗口平均,窗口大小 3 到 5 帧。或者用一阶低通滤波:smoothed = alpha * current + (1 - alpha) * previous,alpha 取 0.6 左右。

4.5 现象:内存持续增长直到崩溃

原因:NamedOnnxValue、DenseTensor、Bitmap这些对象没有及时释放。C# 有 GC,但非托管内存部分 GC 管不到。

解决:所有IDisposable对象用using包起来。Bitmap 在每帧处理完后手动Dispose()。如果帧数多,考虑分批处理,别一次性把所有帧都留在内存里。

5. 进阶技巧:把推理耗时压到实时线以下

5.1 外观特征缓存与批处理

前面提过外观编码只算一次,但很多人还是会把它放进帧循环里。正确的做法是在进入驱动帧循环之前,先把源人像的外观特征算出来存成DenseTensor<float>,循环里直接复用。这一项优化在 25fps 场景下能省掉将近 40% 的总耗时。

批处理是另一个方向。运动提取模块可以一次喂多帧,把 batch size 从 1 提到 4 或 8,GPU 利用率会明显上升。但要注意显存,256x256 输入下 batch 8 大概吃 2GB 左右。CPU 推理就别批处理了,收益不明显。

5.2 用 Netron 确认模型输入输出

Netron 是看 ONNX 模型结构的免费工具,直接拖进去就能看到每个节点的输入输出名字、shape、数据类型。我每次拿到新的 ONNX 文件,第一件事就是用它确认三件事:输入节点名、输入 shape、输出节点名。这三样对不上,C# 代码写得再漂亮也跑不起来。

检查项常见值对不上的后果
输入节点名input / appearance / motionRun 时抛异常
输入 shape[1,3,256,256]shape mismatch
归一化范围[0,1] 或 [-1,1]输出全黑或过曝
通道顺序RGB人脸偏色
输出节点名output / feature取错张量

5.3 性能实测与调参边界

我在一台 i7-12700 + RTX 3060 的机器上做过粗略测试:CPU 推理单帧约 180ms,GPU 约 35ms。加上预处理和后处理,GPU 链路大概能到 20fps 左右,离实时 25fps 还差一点。把 batch size 提到 4 之后,GPU 单帧均摊降到 22ms,勉强够到实时线。

但这里有个边界:batch 太大反而会因为显存拷贝变慢。我的经验是 3060 这个级别,batch 4 是甜点,batch 8 开始收益递减。另外IntraOpNumThreads在 GPU 模式下设成 1 就行,CPU 线程抢的是预处理的时间。

从那以后我每次拿到新的 ONNX 模型包,都强制先用 Netron 过一遍输入输出,再写一行最小推理代码验证数值范围,最后才往工程里集成。这个习惯帮我省掉了至少三次通宵排查。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询