基于 YOLOv8 + BeautyGAN + CodeFormer + Face Parsing 实现简单的人脸美颜的功能

一. 人脸美颜

人脸美颜技术涵盖了多个领域和内容,结合人工智能、计算机视觉等技术,形成了多样化的应用场景和功能模块。

例如:

  • 基础美颜:磨皮、美白、祛痘、祛黑眼圈等。
  • 五官重塑:瘦脸、大眼、提眉、缩鼻翼等基于黄金比例的动态调整。
  • 光影与色彩优化:自动调节亮度、对比度、饱和度,增强面部立体感

在这里只讨论磨皮、美白这些的实现。

二. 算法流程

2.1 基于双边滤波实现的磨皮

我之前写过一篇文章《OpenCV 笔记(29):图像降噪算法——高斯双边滤波、均值迁移滤波》(//www.greatytc.com/p/d5a473c8af48) 曾介绍过双边滤波的原理,在早期双边滤波还经常用于人脸美颜。

高斯双边滤波.png

双边滤波器可以用如下的公式表示:

bilateralfilter(i,j)= \frac{\sum_{k,l} f(k,l)*w(i,j,k,l)}{\sum_{k,l}w(i,j,k,l)}

使用双边滤波进行美颜磨皮.png

2.2 基于深度学习实现的美颜

本文实现的方式有别于之前介绍的传统图像处理方式,使用多种模型组合的形式实现整个流程。

算法流程.png

大致步骤如下:

  1. 人脸检测与裁剪:使用 YOLOv8-Face 检测到人脸并裁剪。
  2. 美颜模型处理:先用 BeautyGAN 对人脸区域进行美颜,可以得到局部增强的效果。
  3. 细节优化:再以 CodeFormer 对人脸区域进行细节重建,可有效提升纹理与自然度。
  4. 人脸解析与掩码:利用 Face Parsing 获得美颜后精准的面部掩码,便于后续与原图进行融合。
  5. 无缝融合:通过掩码将处理后的人脸融回原图,理论上可维持整体风格与背景一致性。

美颜增强流水线: YOLOv8 -> BeautyGAN -> CodeFormer -> Face Parsing -> Mask 融合回原图

三. 整体的实现

整个流程涉及到多个模型,各个模型的部署和加速都使用 ONNXRuntime,每个模型的调用我都封装好了。

下面以 BeautyGan 模型的调用为例:

#include "../onnxruntime/OnnxRuntimeBase.h"

using namespace cv;
using namespace std;
using namespace Ort;

class BeautyGan: public OnnxRuntimeBase {
public:
    BeautyGan(std::string modelPath, const char* logId, const char* provider);

    void inferImage(Mat& src, Mat makeup, Mat& dst);

private:
    vector<float> preprocess(Mat image);
    Mat postprocess(float* output_data);

    vector<float> input_image_1;
    vector<float> input_image_2;

    int inpWidth;
    int inpHeight;
    int outWidth;
    int outHeight;
};
#include "../../include/faceBeauty/BeautyGan.h"

BeautyGan::BeautyGan(std::string modelPath, const char* logId, const char* provider): OnnxRuntimeBase(modelPath, logId, provider)
{
    this->inpHeight = input_node_dims[0][2];
    this->inpWidth = input_node_dims[0][3];
    this->outHeight = output_node_dims[0][2];
    this->outWidth = output_node_dims[0][3];
}

vector<float> BeautyGan::preprocess(Mat image)
{
    cv::resize(image, image, cv::Size(this->inpWidth, this->inpHeight));
    image.convertTo(image, CV_32F, 1.0 / 255.0);

    std::vector<cv::Mat> channels(3);
    cv::split(image, channels);

    std::vector<float> result(this->inpWidth * this->inpHeight * image.channels());
    const unsigned int channel_step = inpHeight * inpWidth;
    for (int i = 0; i < 3; ++i) {
        std::memcpy(result.data() + i * channel_step, channels[i].data, channel_step * sizeof(float));
    }

    return result;
}

cv::Mat BeautyGan::postprocess(float* output_data) {
    std::vector<cv::Mat> output_channels;
    const unsigned int channel_step = outHeight * outWidth;
    for (int i = 0; i < 3; ++i) {
        output_channels.emplace_back(outHeight, outWidth, CV_32F, output_data + i * channel_step);
    }

    cv::Mat output_img;
    cv::merge(output_channels, output_img);
    output_img = output_img * 255.0;
    output_img.convertTo(output_img, CV_8U);
    return output_img;
}


void BeautyGan::inferImage(Mat& src, Mat makeup, Mat& dst) {

    // 图像预处理
    this->input_image_1 = this->preprocess(src);        // 原始人脸图像
    this->input_image_2 = this->preprocess(makeup);     // 参考妆容图像

    std::array<int64_t,4> input_shape {1,3,this->inpHeight, this->inpWidth};

    auto allocator_info = MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
    vector<Value> ort_inputs;

    ort_inputs.push_back(Value::CreateTensor<float>(allocator_info, input_image_1.data(), input_image_1.size(), input_shape.data(), input_shape.size()));
    ort_inputs.push_back(Value::CreateTensor<float>(allocator_info, input_image_2.data(), input_image_2.size(), input_shape.data(), input_shape.size()));
    vector<Value> ort_outputs = this -> forward(ort_inputs);

    // 后处理
    float* output_data = ort_outputs.front().GetTensorMutableData<float>();
    cv::Mat beautygan_crop = this->postprocess(output_data);

    cv::resize(beautygan_crop, dst, src.size());
}

BeautyGan 模型的调用需要输入两张图,一张是原始人脸图像(通过 YOLOv8 获取的人脸区域),一张是参考妆容图像。

跑完 BeautyGan 模型后,将生成的图像再跑 CodeFormer 模型,这两步是美颜的关键所在。

CodeFormer.png

好了,下面的代码给出了完整的算法流程和相关注释:

各个模型文件和各个模型调用相关的代码可以在 https://github.com/fengzhizi715/MonicaImageProcessHttpServer 可以找到。

#include <iostream>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <vector>
#include "include/faceSwap/Yolov8Face.h"
#include "include/faceSwap/Face68Landmarks.h"
#include "include/faceSwap/FaceEnhance.h"
#include "include/faceBeauty/CodeFormer.h"
#include "include/faceBeauty/FaceParsing.h"
#include "include/faceBeauty/BeautyGan.h"
#include "include/onnxruntime/Constants.h"

using namespace cv;
using namespace std;

cv::Mat blend_face_skin_region(const cv::Mat& codeformed_face,
                                      const cv::Mat& original_face,
                                      const cv::Mat& skin_mask,
                                      int feather_size = 15, double feather_sigma = 5.0) {
    CV_Assert(codeformed_face.size() == original_face.size());
    CV_Assert(skin_mask.size() == original_face.size());
    CV_Assert(codeformed_face.type() == original_face.type());
    CV_Assert(skin_mask.type() == CV_8UC1);

    // feather mask 边缘
    cv::Mat blurred_mask;
    if (feather_size > 0) {
        cv::GaussianBlur(skin_mask, blurred_mask, cv::Size(feather_size, feather_size), feather_sigma);
    } else {
        blurred_mask = skin_mask.clone();
    }

    // 创建一个输出图像,初始为 original
    cv::Mat blended = original_face.clone();

    // 使用 mask 将 codeformed_face 拷贝到 blended 中
    codeformed_face.copyTo(blended, blurred_mask);  // 只复制 mask!=0 的区域

    return blended;
}

int main()
{
    string image_path = ".../girl.jpg";

    cv::Mat src = cv::imread(image_path);
    imshow("src",src);

    // 各种模型的加载
    const string& onnx_provider = OnnxProviders::CPU;
    const char* provider = onnx_provider.c_str();
    string modelPath = "/Users/Tony/CLionProjects/MonicaImageProcessHttpServer/models";

    string yolov8FaceModelPath = modelPath + "/yoloface_8n.onnx";
    string face68LandmarksModePath = modelPath + "/2dfan4.onnx";
    string faceEnhanceModePath = modelPath + "/gfpgan_1.4.onnx";
    string beautyGanModePath = modelPath + "/beautygan.onnx";
    string codeFormerModePath = modelPath + "/codeformer.onnx";
    string faceParsingModePath = modelPath + "/face_parsing_resnet34.onnx";

    const std::string& yolov8FaceLogId = "yolov8Face";
    const std::string& face68LandmarksLogId = "face68Landmarks";
    const std::string& faceEnhanceLogId = "faceEnhance";
    const std::string& beautyGanLogId = "beautyGan";
    const std::string& codeFormerLogId = "codeFormer";
    const std::string& faceParsingLogId = "faceParsing";

    Yolov8Face yolov8Face(yolov8FaceModelPath, yolov8FaceLogId.c_str(), provider);
    Face68Landmarks face68Landmarks(face68LandmarksModePath, face68LandmarksLogId.c_str(), provider);
    FaceEnhance faceEnhance(faceEnhanceModePath, faceEnhanceLogId.c_str(), provider);
    BeautyGan beautyGan(beautyGanModePath,beautyGanLogId.c_str(), provider);
    CodeFormer codeFormer(codeFormerModePath,codeFormerLogId.c_str(), provider);
    FaceParsing faceParsing(faceParsingModePath,faceParsingLogId.c_str(), provider);

    // 人脸检测与裁剪
    Mat original_face;
    Bbox box;
    try {
        vector<Bbox> boxes;
        yolov8Face.detect(src, boxes);
        box = boxes[0];

        original_face = src(Rect(cv::Point(box.xmin,box.ymin), cv::Point(box.xmax,box.ymax)));
    } catch(...) {
    }
    imshow("original_face",original_face);

    // 查找人脸的关键点
    vector<Point2f> face_landmark_5of68;
    face68Landmarks.detect(src, box, face_landmark_5of68);

    // 美颜模型处理
    Mat beautygan_crop;
    Mat makeup = cv::imread("/Users/Tony/BeautyGAN/imgs/makeup/vFG112.png"); // 参考妆容图像
    beautyGan.inferImage(original_face, makeup, beautygan_crop);
    imshow("beautygan",beautygan_crop);

    // 使用 CodeFormer 对人脸优化细节
    Mat codeformed_face; // CodeFormer 输出的人脸
    codeFormer.inferImage(beautygan_crop, codeformed_face);
    resize(codeformed_face, codeformed_face, original_face.size());
    imshow("codeformer", codeformed_face);

    // 人脸解析与获取掩码
    cv::Mat skin_mask;
    faceParsing.inferImage(codeformed_face, skin_mask);
    imshow("skin_mask", skin_mask);

    // 通过掩码将处理后人脸融回原图
    cv::resize(skin_mask, skin_mask, codeformed_face.size());
    cv::Mat blended_face = blend_face_skin_region(codeformed_face, original_face, skin_mask);
    imshow("blended_face", blended_face);
    blended_face.copyTo(src(Rect(cv::Point(box.xmin,box.ymin), cv::Point(box.xmax,box.ymax))));

    // 最后再用 GFPGAN 模型对人脸进行增强
    Mat result = faceEnhance.process(src, face_landmark_5of68);
    imshow("result", result);

    waitKey(0);
    return 0;
}

下面两张图分别是原图和最终的效果图。


原图和最终效果图.png

下面三张图表示:使用 YOLOv8-Face 检测到人脸的区域、用 BeautyGAN 对人脸区域进行美颜、用 CodeFormer 对人脸区域细节重建。

用 BeautyGAN 对该图进行美颜看上去效果不是特别好,可能是因为 makeup 图像也就是参考妆容图像相对于原图太小的缘故。

人脸区域.png

下面两张图表示:用 Face Parsing 获得美颜后的面部Mask、通过 Mask 将处理后的人脸融回原图中的人脸区域。


image.png

为了看上去更自然,融回原图后,我还用了 GFPGAN 模型对人脸进行增强,才生成最后的效果图。

再跑一些图看看效果:


image.png
image.png

四. 总结

上述代码大致完成了一个简单的美颜功能,不足之处还是有很多,后续可能会逐步完善。

例如:

  • BeautyGAN 模型相对比较老,需要尝试使用新的模型/技术来替换。
  • 融合算法的改进,目前在在掩码边缘使用高斯模糊,后续尝试使用贝塞尔曲线平滑,确保边缘自然。
  • 逐步完善各种美颜的功能。

参考资料:

  1. https://github.com/Honlan/BeautyGAN
  2. https://github.com/sczhou/CodeFormer
  3. https://github.com/yakhyo/face-parsing
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容