一. 人脸美颜
人脸美颜技术涵盖了多个领域和内容,结合人工智能、计算机视觉等技术,形成了多样化的应用场景和功能模块。
例如:
- 基础美颜:磨皮、美白、祛痘、祛黑眼圈等。五官重塑:瘦脸、大眼、提眉、缩鼻翼等基于黄金比例的动态调整。光影与色彩优化:自动调节亮度、对比度、饱和度,增强面部立体感
在这里只讨论磨皮、美白这些的实现。
二. 算法流程
2.1 基于双边滤波实现的磨皮
我之前写过一篇文章《OpenCV 笔记(29):图像降噪算法——高斯双边滤波、均值迁移滤波》(juejin.cn/post/735492…) 曾介绍过双边滤波的原理,在早期双边滤波还经常用于人脸美颜。
双边滤波器可以用如下的公式表示:
2.2 基于深度学习实现的美颜
本文实现的方式有别于之前介绍的传统图像处理方式,使用多种模型组合的形式实现整个流程。
大致步骤如下:
- 人脸检测与裁剪:使用 YOLOv8-Face 检测到人脸并裁剪。美颜模型处理:先用 BeautyGAN 对人脸区域进行美颜,可以得到局部增强的效果。细节优化:再以 CodeFormer 对人脸区域进行细节重建,可有效提升纹理与自然度。人脸解析与掩码:利用 Face Parsing 获得美颜后精准的面部掩码,便于后续与原图进行融合。无缝融合:通过掩码将处理后的人脸融回原图,理论上可维持整体风格与背景一致性。
美颜增强流水线: 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 模型,这两步是美颜的关键所在。
好了,下面的代码给出了完整的算法流程和相关注释:
各个模型文件和各个模型调用相关的代码可以在 github.com/fengzhizi71… 可以找到。
#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;}
下面两张图分别是原图和最终的效果图。
下面三张图表示:使用 YOLOv8-Face 检测到人脸的区域、用 BeautyGAN 对人脸区域进行美颜、用 CodeFormer 对人脸区域细节重建。
用 BeautyGAN 对该图进行美颜看上去效果不是特别好,可能是因为 makeup 图像也就是参考妆容图像相对于原图太小的缘故。
下面两张图表示:用 Face Parsing 获得美颜后的面部Mask、通过 Mask 将处理后的人脸融回原图中的人脸区域。
为了看上去更自然,融回原图后,我还用了 GFPGAN 模型对人脸进行增强,才生成最后的效果图。
再跑一些图看看效果:
四. 总结
上述代码大致完成了一个简单的美颜功能,不足之处还是有很多,后续可能会逐步完善。
例如:
- BeautyGAN 模型相对比较老,需要尝试使用新的模型/技术来替换。融合算法的改进,目前在在掩码边缘使用高斯模糊,后续尝试使用贝塞尔曲线平滑,确保边缘自然。逐步完善各种美颜的功能。
参考资料: