C#实现人脸增强:基于GFPGAN的Facefusion第五步

在AI视觉应用日益普及的今天,换脸技术早已不再局限于影视特效或娱乐恶搞。从虚拟主播到数字人生成,再到个性化内容创作,高质量的人脸处理流水线正成为许多产品的核心支撑。其中,FaceFusion 作为一个结构清晰、模块化程度高的换脸流程框架,因其高精度与可扩展性受到广泛关注。

该流程包含五个关键步骤:

  1. 人脸检测
  2. 关键点定位
  3. 特征提取
  4. 人脸替换
  5. 人脸增强

前四步完成了“身份迁移”的任务——将源人脸的身份特征映射到目标图像中,但输出结果往往存在细节模糊、边缘不自然、皮肤质感塑料化等问题。真正决定最终观感是否“以假乱真”的,正是最后一步:人脸增强(Face Enhancement)

本文聚焦于使用 GFPGAN 模型,在 C# 环境下通过 ONNX Runtime 实现高性能推理,完成对换脸后图像的精细化修复与画质提升。我们将深入解析如何将 PyTorch 训练好的模型部署至 .NET 平台,并集成进完整的 FaceFusion 流水线中,实现端到端的自动化处理。


效果对比:从“像”到“真”的跨越

经过前序步骤处理后的图像虽然实现了身份替换,但在高频细节上仍有明显缺陷:

  • 皮肤纹理丢失,呈现“磨皮过度”感
  • 鼻翼、嘴角等轮廓区域轻微失真
  • 光照融合不一致导致局部色块断层

而引入 GFPGAN 后,这些问题得到了显著改善:

原始替换结果 GFPGAN增强后
存在轻微模糊、色彩断层 细节丰富、过渡平滑、肤色自然

具体表现为:
- ✅ 皮肤微结构重建:毛孔、细纹、胡茬等细节得以恢复
- ✅ 五官边界锐化:眼睑线条更清晰,唇形更立体
- ✅ 消除人工痕迹:缓解因仿射变换和压缩带来的“涂抹感”
- ✅ 分辨率稳定输出:统一输出为 512×512 高清图像

更重要的是,GFPGAN 基于“先验引导”的生成机制,在保留原始姿态与表情的同时,不会改变已替换的身份特征,确保了整个流程的一致性和可控性。


模型选型:为什么是 GFPGAN v1.4?

我们采用的是 GFPGAN v1.4 的 ONNX 格式导出版本(gfpgan_1.4.onnx,这是目前在轻量化与性能之间平衡较好的一个公开可用模型。

输入张量说明

name: input
shape: Float[1, 3, 512, 512]
range: [-1.0, 1.0] (归一化后的 RGB 图像)
format: NCHW (Batch, Channel, Height, Width)

输出张量说明

name: output
shape: Float[1, 3, 512, 512]
range: [-1.0, 1.0] → 后处理映射至 [0, 255]
format: NCHW

⚠️ 注意事项:
- 输入图像必须是 已经对齐裁剪至 512×512 尺寸的人脸区域
- 推荐由前序模块提供标准五点关键点(双眼、鼻尖、双嘴角),并完成仿射校正
- 若输入未对齐,模型仍能推理,但增强效果会大打折扣

相比其他超分模型(如 ESRGAN),GFPGAN 的优势在于其引入了 面部结构先验信息(Facial Prior),利用预训练的 StyleGAN2 作为生成器骨干,结合退化感知判别器,在低质量图像修复任务中表现尤为出色。


项目架构设计:WinForms + ONNX Runtime

为了便于调试与演示,本项目采用 WinForms 构建可视化界面,整体结构简洁明了:

/FaceFusionSharp/
│
├── Form5.cs                  // 主窗体:人脸增强界面
├── FaceEnhance.cs            // GFPGAN 推理封装类
├── Common.cs                 // 工具函数库(仿射变换、掩码生成、图像粘贴等)
├── model/gfpgan_1.4.onnx     // GFPGAN ONNX 模型文件
├── images/                   // 示例图片存放目录
│   ├── swapimg.jpg           // 替换后待增强图像
│   └── enhanced.jpg          // 增强结果保存路径
└── packages.config           // NuGet 包依赖配置

核心依赖项

库名 用途
OpenCvSharp4 & OpenCvSharp4.Windows 图像读取、处理与矩阵运算
Microsoft.ML.OnnxRuntime ONNX 模型推理引擎(CPU 版)
Newtonsoft.Json JSON 字符串反序列化(用于测试数据加载)

可通过 NuGet 安装:

Install-Package Microsoft.ML.OnnxRuntime
Install-Package OpenCvSharp4
Install-Package OpenCvSharp4.Windows
Install-Package Newtonsoft.Json

建议开发环境为 x64 平台,避免因 DLL 加载失败导致运行异常。


核心代码实现详解

1. 用户交互层:Form5.cs

using Newtonsoft.Json;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace FaceFusionSharp
{
    public partial class Form5 : Form
    {
        public Form5()
        {
            InitializeComponent();
        }

        string source_path = "";
        FaceEnhance enhance_face;

        private void button1_Click(object sender, EventArgs e)
        {
            if (pictureBox1.Image == null)
            {
                MessageBox.Show("请先加载待增强图像!");
                return;
            }

            pictureBox3.Image = null;
            button1.Enabled = false;
            Application.DoEvents();

            Mat source_img = Cv2.ImRead(source_path);
            List<Point2f> target_landmark_5 = new List<Point2f>();

            // 【调试用】硬编码目标人脸五点坐标(实际应从前一步获取)
            string landmarkJson = "[{\"X\":485.6,\"Y\":247.8},{\"X\":704.2,\"Y\":247.4},{\"X\":527.5,\"Y\":360.2},{\"X\":485.4,\"Y\":495.8},{\"X\":647.7,\"Y\":505.1}]";
            target_landmark_5 = JsonConvert.DeserializeObject<List<Point2f>>(landmarkJson);

            Mat resultImg = enhance_face.Process(source_img, target_landmark_5);
            pictureBox3.Image = resultImg.ToBitmap();

            // 可选:保存结果
            Cv2.ImWrite("images/enhanced.jpg", resultImg);

            button1.Enabled = true;
        }

        private void Form5_Load(object sender, EventArgs e)
        {
            enhance_face = new FaceEnhance("model/gfpgan_1.4.onnx");

            source_path = "images/swapimg.jpg";
            if (System.IO.File.Exists(source_path))
            {
                pictureBox1.Image = new Bitmap(source_path);
            }
            else
            {
                MessageBox.Show("未找到测试图像,请检查路径:images/swapimg.jpg");
            }
        }
    }
}

🔍 要点说明:
- 使用 Application.DoEvents() 防止 UI 在长时间推理期间卡死
- target_landmark_5 当前为固定值,真实场景中应从前序模块动态传入
- 支持实时预览与结果保存,便于调试验证


2. 推理核心类:FaceEnhance.cs

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Linq;

namespace FaceFusionSharp
{
    internal class FaceEnhance
    {
        private readonly int input_height = 512;
        private readonly int input_width = 512;

        private readonly List<Point2f> normed_template; 
        private readonly float FACE_MASK_BLUR = 0.3f;
        private readonly int[] FACE_MASK_PADDING = { 0, 0, 0, 0 };

        private readonly InferenceSession onnx_session;

        public FaceEnhance(string modelPath)
        {
            var options = new SessionOptions();
            options.LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_WARNING;
            options.AppendExecutionProvider_CPU(0); 

            onnx_session = new InferenceSession(modelPath, options);

            // 定义标准五点模板(GFPGAN 训练时使用的人脸对齐基准)
            normed_template = new List<Point2f>
            {
                new Point2f(192.98f, 239.95f),  // 左眼
                new Point2f(318.90f, 240.19f),  // 右眼
                new Point2f(256.63f, 314.02f),  // 鼻尖
                new Point2f(201.26f, 371.41f),  // 左嘴角
                new Point2f(313.09f, 371.15f)   // 右嘴角
            };
        }

        /// <summary>
        /// 预处理:根据关键点进行人脸对齐 + 归一化
        /// </summary>
        private void Preprocess(
            Mat srcImg,
            List<Point2f> faceLandmark5,
            out Mat cropImg,
            out Mat affineMatrix,
            out Mat boxMask)
        {
            cropImg = new Mat();
            affineMatrix = Common.WarpFaceByFivePoints(srcImg, cropImg, faceLandmark5, normed_template, new Size(512, 512));
            int[] cropSize = { cropImg.Cols, cropImg.Rows };
            boxMask = Common.CreateStaticBoxMask(cropSize, FACE_MASK_BLUR, FACE_MASK_PADDING);

            // BGR to RGB 并归一化至 [-1, 1]
            Mat rgb = new Mat();
            Cv2.CvtColor(cropImg, rgb, ColorConversionCodes.BGR2RGB);

            Mat[] channels = Cv2.Split(rgb);
            for (int i = 0; i < 3; i++)
            {
                channels[i].ConvertTo(channels[i], MatType.CV_32FC1, 1.0 / (255.0 * 0.5), -1.0); 
            }

            Cv2.Merge(channels, rgb);

            foreach (var ch in channels) ch.Dispose();
            rgb.CopyTo(cropImg);
            rgb.Dispose();
        }

        /// <summary>
        /// 主处理函数:执行人脸增强
        /// </summary>
        public Mat Process(Mat targetImg, List<Point2f> targetLandmark5)
        {
            Mat cropImg, affineMatrix, boxMask;
            Preprocess(targetImg, targetLandmark5, out cropImg, out affineMatrix, out boxMask);

            // 构建输入张量
            float[] inputData = Common.ExtractMatData(cropImg); 
            var inputTensor = new DenseTensor<float>(inputData, new[] { 1, 3, 512, 512 });
            var inputs = new List<NamedOnnxValue>
            {
                NamedOnnxValue.CreateFromTensor("input", inputTensor)
            };

            // 执行推理
            using var results = onnx_session.Run(inputs);
            var outputTensor = results[0].AsTensor<float>().ToArray();

            // 后处理:[-1,1] → [0,255] 并转为 U8 图像
            int length = 512 * 512;
            var r = new float[length];
            var g = new float[length];
            var b = new float[length];

            Array.Copy(outputTensor, 0,       b, 0, length); 
            Array.Copy(outputTensor, length,   g, 0, length);
            Array.Copy(outputTensor, 2*length, r, 0, length);

            for (int i = 0; i < length; i++)
            {
                r[i] = Math.Min(Math.Max((r[i] + 1) * 0.5f * 255.0f, 0), 255);
                g[i] = Math.Min(Math.Max((g[i] + 1) * 0.5f * 255.0f, 0), 255);
                b[i] = Math.Min(Math.Max((b[i] + 1) * 0.5f * 255.0f, 0), 255);
            }

            Mat rMat = new Mat(512, 512, MatType.CV_32FC1, r);
            Mat gMat = new Mat(512, 512, MatType.CV_32FC1, g);
            Mat bMat = new Mat(512, 512, MatType.CV_32FC1, b);
            Mat enhancedRgb = new Mat();
            Cv2.Merge(new[] { bMat, gMat, rMat }, enhancedRgb);

            Mat enhancedU8 = new Mat();
            enhancedRgb.ConvertTo(enhancedU8, MatType.CV_8UC3);

            rMat.Dispose(); gMat.Dispose(); bMat.Dispose();
            enhancedRgb.Dispose();

            // 获取掩码数据并规范化 [0,1]
            float[] maskData;
            boxMask.GetArray(out maskData);
            for (int i = 0; i < maskData.Length; i++)
            {
                maskData[i] = Math.Max(0, Math.Min(1, maskData[i]));
            }
            Mat refinedMask = new Mat(512, 512, MatType.CV_32FC1, maskData);

            // 将增强后的人脸粘贴回原图
            Mat pasteFrame = Common.PasteBack(targetImg, enhancedU8, refinedMask, affineMatrix);
            Mat finalImage = Common.Blend(targetImg, pasteFrame);

            // 释放资源
            cropImg.Dispose();
            affineMatrix.Dispose();
            boxMask.Dispose();
            refinedMask.Dispose();
            enhancedU8.Dispose();
            pasteFrame.Dispose();

            return finalImage;
        }
    }
}

📌 关键设计点:
- 使用 DenseTensor<float> 构造符合 ONNX 要求的输入张量
- 手动拆解通道顺序(ONNX 输出为 RGB,OpenCV 使用 BGR)
- 引入软边掩码防止拼接处出现明显边界
- 严格管理 Mat 对象生命周期,避免内存泄漏


3. 公共工具方法:Common.cs(节选)

public static class Common
{
    /// <summary>
    /// 使用五点关键点进行仿射对齐
    /// </summary>
    public static Mat WarpFaceByFivePoints(Mat src, Mat dst, IEnumerable<Point2f> kps, IEnumerable<Point2f> reference, Size size)
    {
        var pts1 = kps.Take(5).ToArray();
        var pts2 = reference.Take(5).ToArray();
        Mat M = Cv2.GetAffineTransform(pts1, pts2);
        Cv2.WarpAffine(src, dst, M, size, InterpolationFlags.Linear, BorderTypes.Constant, new Scalar(0, 0, 0));
        return M;
    }

    /// <summary>
    /// 创建矩形软边掩码
    /// </summary>
    public static Mat CreateStaticBoxMask(int[] size, float blur, int[] padding)
    {
        int h = size[0], w = size[1];
        int ph, pw, pb, pr;
        ph = padding[0]; pw = padding[1]; pb = padding[2]; pr = padding[3];

        Mat mask = Mat.Zeros(h, w, MatType.CV_32FC1);
        Cv2.Rectangle(mask, new Rect(pw, ph, w - pw - pr, h - ph - pb), new Scalar(1.0), -1);

        int blurSize = (int)(Math.Min(h, w) * blur);
        if (blurSize > 0 && blurSize % 2 == 0) blurSize++;
        Cv2.GaussianBlur(mask, mask, new Size(blurSize, blurSize), 0);

        return mask;
    }

    /// <summary>
    /// 将增强后的小脸图粘贴回原图
    /// </summary>
    public static Mat PasteBack(Mat targetImg, Mat faceImg, Mat mask, Mat affineMatrix)
    {
        Mat invM = affineMatrix.Invert();
        Mat warpedFace = new Mat();
        Cv2.WarpAffine(faceImg, warpedFace, invM, new Size(targetImg.Cols, targetImg.Rows), InterpolationFlags.Linear, BorderTypes.Constant);

        Mat expandedMask = new Mat();
        Cv2.WarpAffine(mask, expandedMask, invM, new Size(targetImg.Cols, targetImg.Rows), InterpolationFlags.Linear, BorderTypes.Constant);

        return warpedFace;
    }

    /// <summary>
    /// 融合原图与粘贴图(加权叠加)
    /// </summary>
    public static Mat Blend(Mat src, Mat paste)
    {
        Mat result = new Mat();
        Cv2.AddWeighted(src, 0.5, paste, 0.5, 0, result);
        return result;
    }

    /// <summary>
    /// 提取 Mat 数据为 float 数组(HWC → CHW)
    /// </summary>
    public static float[] ExtractMatData(Mat mat)
    {
        int height = mat.Rows;
        int width = mat.Cols;
        int channels = 3;
        float[] data = new float[height * width * channels];

        unsafe
        {
            byte* ptr = (byte*)mat.DataPointer;
            for (int c = 0; c < channels; c++)
            {
                for (int i = 0; i < height * width; i++)
                {
                    data[c * height * width + i] = ptr[i * channels + (2 - c)]; // BGR → RGB
                }
            }
        }

        return data;
    }
}

💡 工程经验提示:
- ExtractMatData 中使用指针直接访问像素数据,效率远高于逐像素读取
- CreateStaticBoxMask 控制融合区域范围,避免影响背景
- PasteBack 利用逆变换将增强后图像精确还原至原位置


运行指南与常见问题排查

📦 源码获取

👉 点击此处下载完整 Visual Studio 项目源码

包含:
- .sln 解决方案文件
- FaceFusionSharp 工程代码
- 示例图像集
- gfpgan_1.4.onnx 模型文件(需自行准备或联系作者)

▶️ 快速运行步骤

  1. 安装 .NET Framework 4.8
  2. 打开 FaceFusionSharp.sln
  3. 确保 model/gfpgan_1.4.onnx 文件存在
  4. 设置启动项为 Form5
  5. 编译并运行程序
  6. 点击 “开始增强” 查看效果

❗ 常见问题及解决方案

问题 原因分析 解决方案
报错“无法加载 DLL ‘onnxruntime’” 平台不匹配或缺少本地依赖 确认项目平台为 x64,安装 Microsoft.ML.OnnxRuntime
图像黑屏或颜色异常 通道顺序错误或归一化参数不对 检查是否正确执行 BGR→RGB 转换,确认归一化公式 (x/127.5 - 1)
性能缓慢(>2s/帧) 使用 CPU 推理 更换为 Microsoft.ML.OnnxRuntime.Gpu 包,启用 CUDA 支持
输出图像有黑边 仿射变换填充方式不当 修改 BorderTypesReplicateReflect 减少边缘畸变

展望未来:不止于单张图像增强

当前实现虽已完成基本功能闭环,但仍有不少优化空间:

支持多张人脸同时增强
可通过检测多个关键点组,循环调用 Process 方法实现批处理。

添加性别/年龄引导控制
结合属性分类器,选择不同风格的 GFPGAN 子模型进行差异化增强。

联合 Real-ESRGAN 实现超分+去噪一体化
先用 ESRGAN 提升分辨率,再用 GFPGAN 修复面部细节,形成两级增强流水线。

移植至视频流处理
结合 FFmpeg 或 Media Foundation,实现实时摄像头换脸+增强直播。

更重要的是,这套基于 ONNX 的推理架构具备良好的跨平台潜力,未来可轻松迁移到 Linux 服务器、移动端甚至 WebAssembly 环境中。


这种将前沿 AI 模型与传统桌面开发深度融合的方式,不仅降低了深度学习落地的技术门槛,也为更多开发者提供了参与 AIGC 内容创作的可能性。GFPGAN 的加入,让换脸不再是简单的“贴图”,而是真正走向“重生”。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐