C# OnnxRuntime部署LivePortrait:人像驱动视频生成实战
发布时间:2026/10/2 1:01:35来源:尧图网络
简介本资源面向具备一定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 DenseTensorfloat BitmapToTensor(Bitmap bmp, int width, int height, float mean, float std) { var resized new Bitmap(bmp, new Size(width, height)); var tensor new DenseTensorfloat(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或Spanbyte直接读内存。3. 从单帧到视频驱动帧循环、关键点迁移与编码输出3.1 驱动视频抽帧与帧率对齐人像驱动的输入是一段驱动视频你得先把它拆成帧序列。C# 里没有内置的视频解码常见做法是用 FFmpeg 命令行抽帧或者用 OpenCvSharp 的 VideoCapture。我一般用后者因为能直接在内存里拿 Mat省一次磁盘 IOusing OpenCvSharp; public static ListMat ExtractFrames(string videoPath, int targetFps 25) { var frames new ListMat(); 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, DenseTensorfloat drivingMotion) { // 1. 源人像外观编码只需算一次循环外缓存 var sourceTensor BitmapToTensor(sourceFace, 256, 256, 0.5f, 0.5f); var appearanceInput new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(input, sourceTensor) }; using var appearanceResult _appearanceSession.Run(appearanceInput); var appearanceFeature appearanceResult.First().AsTensorfloat(); // 2. 驱动运动特征与外观特征拼接送入形变模块 var warpInput new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(appearance, appearanceFeature), NamedOnnxValue.CreateFromTensor(motion, drivingMotion) }; using var warpResult _warpSession.Run(warpInput); var warpedFeature warpResult.First().AsTensorfloat(); // 3. 解码回图像 var decodeInput new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(feature, warpedFeature) }; using var decodeResult _decodeSession.Run(decodeInput); return TensorToBitmap(decodeResult.First().AsTensorfloat()); }参数说明输入节点名input、appearance、motion、feature必须和导出时的名字一致用 Netron 打开 ONNX 文件就能看到。外观编码只算一次放在循环外缓存这是性能优化的关键——每帧都重算外观速度直接砍半。3.3 输出帧合成视频解码出来的是一张张 Bitmap最后要合成视频。用 OpenCvSharp 的 VideoWriter 最省事public static void WriteVideo(ListBitmap 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) * previousalpha 取 0.6 左右。4.5 现象内存持续增长直到崩溃原因NamedOnnxValue、DenseTensor、Bitmap这些对象没有及时释放。C# 有 GC但非托管内存部分 GC 管不到。解决所有IDisposable对象用using包起来。Bitmap 在每帧处理完后手动Dispose()。如果帧数多考虑分批处理别一次性把所有帧都留在内存里。5. 进阶技巧把推理耗时压到实时线以下5.1 外观特征缓存与批处理前面提过外观编码只算一次但很多人还是会把它放进帧循环里。正确的做法是在进入驱动帧循环之前先把源人像的外观特征算出来存成DenseTensorfloat循环里直接复用。这一项优化在 25fps 场景下能省掉将近 40% 的总耗时。批处理是另一个方向。运动提取模块可以一次喂多帧把 batch size 从 1 提到 4 或 8GPU 利用率会明显上升。但要注意显存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 推理单帧约 180msGPU 约 35ms。加上预处理和后处理GPU 链路大概能到 20fps 左右离实时 25fps 还差一点。把 batch size 提到 4 之后GPU 单帧均摊降到 22ms勉强够到实时线。但这里有个边界batch 太大反而会因为显存拷贝变慢。我的经验是 3060 这个级别batch 4 是甜点batch 8 开始收益递减。另外IntraOpNumThreads在 GPU 模式下设成 1 就行CPU 线程抢的是预处理的时间。从那以后我每次拿到新的 ONNX 模型包都强制先用 Netron 过一遍输入输出再写一行最小推理代码验证数值范围最后才往工程里集成。这个习惯帮我省掉了至少三次通宵排查。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网