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