最近项目需要用到美颜的一些效果,因此开始接触opencv 计算机视觉库,在腾讯课堂上找到一个简单且免费的入门视频《Opencv4 快速入门视频30讲》,看完视频后,初步才对opencv 有一个比较清晰的概念和基本用法。 接下来就是开始对美颜的一些初步接触,下面写的一个简单的测试 效果,具体功能包括亮度、对比度、瘦脸、大眼、美白磨皮等,但实际上用于项目使用还是问题很多,需要更多的优化。
1.图像创建各个功能滑动条
void BeautyCam::initMainImgUI()
{
namedWindow("BeautyCam", WINDOW_AUTOSIZE);
string path = "6.jpg";
m_MainImg =imread(path);
imshow("src", m_MainImg);
//检测人脸数据68点
m_vecFaceData = dectectFace68(path);
int max_value = 100;
int con_value = 100;
int lignhtnesss = 50;
int contrast = 2;
int bigeyeval = 0;
int faceval = 0;
int beautyval = 0;
createTrackbar("亮度", "BeautyCam", &lignhtnesss, max_value, on_lightness, (void*)(&m_MainImg));
createTrackbar("对比度", "BeautyCam", &contrast, max_value, on_contrast, (void*)(&m_MainImg));
createTrackbar("大眼", "BeautyCam", &bigeyeval, 60, on_BigEye, (void*)(&m_MainImg));
createTrackbar("瘦脸", "BeautyCam", &faceval, 70, on_thinFace, (void*)(&m_MainImg));
createTrackbar("美颜", "BeautyCam", &beautyval, 200, on_beautyFace, (void*)(&m_MainImg));
on_lightness(50, (void*)(&m_MainImg));
imshow("BeautyCam", m_MainImg);
}
此代码就是创建滚动条的初始化程序,主要就是createTrackbar函数的使用:
原型:
CV_EXPORTS int createTrackbar(const String& trackbarname, //滚动条名称
const String& winname, //滚动条作用在哪一个窗口上 窗口名称
int* value, int count, //滑块初始值 和 滚动条最大值
TrackbarCallback onChange = 0, // 回调函数 滑条值变化
void* userdata = 0); //用户创给回调函数的用户数据
这里就是需要定义相关的回调函数,原型为:void (*TrackbarCallback)(int pos, void* userdata);
因此只需要创建相同的函数就行,滑条的变化就会导致回调函数触发,传递形参pos值的变化。
例如:
//对比度调节
static void on_contrast(int b, void*userdata);
//亮度调节
static void on_lightness(int b, void*userdata);
//眼睛调节
static void on_BigEye(int b, void*userdata);
//瘦脸效果
static void on_thinFace(int b, void*userdata);
//美颜效果
static void on_beautyFace(int b, void*userdata);
2.回调函数 对比度调节的实现
void BeautyCam::on_contrast(int b, void*userdata)
{
Mat img = *((Mat *)userdata);
Mat m = Mat::zeros(img.size(), img.type());
Mat dst = Mat::zeros(img.size(), img.type());
m = Scalar(b, b, b);
double con = b / 100.0;
addWeighted(img, con, m, 0, 0, dst);
imshow("BeautyCam", dst);
}
addWeighted()函数是将两张相同大小,相同类型的图片融合的函数;
原型:CV_EXPORTS_W void addWeighted(InputArray src1, //第一个输入图像
double alpha, //第一个元素权重
InputArray src2, //第二个输入图像
double beta, // 第二个元素权重
double gamma, //图1和图2 作和后添加的数值
OutputArray dst,//输出图像
int dtype = -1);
3. 回调函数 亮度调节的实现
void BeautyCam::on_lightness(int b, void*userdata)
{
Mat img = *((Mat *)userdata);
Mat m = Mat::zeros(img.size(), img.type());
Mat dst = Mat::zeros(img.size(), img.type());
m = Scalar(b, b, b);
addWeighted(img, 1.0, m, 0, b, dst);
imshow("BeautyCam", dst);
}
同上,对比度和亮度其实可以直接用一个addWeighted 实现,同时设置亮度和对比度(差值在扩大),主要是就是对addWeighed 的beta 和alpha参数去调节。
4.人脸数据检测
需要实现大眼或者瘦脸的效果,首先是需要检测人脸并提取特征点,一般常用的就是68个点,如下:
在此处是通过调用三方库dlib来获取68点特性点数据的:
std::vector> BeautyCam::dectectFace68(const string &path)
{
std::vector> rets;
//加载图片路径
array2d img;
load_image(img, path.c_str());
//定义人脸检测器
frontal_face_detector detector = get_frontal_face_detector();
std::vector dets = detector(img);
for (auto var : dets)
{
//关键点检测器
shape_predictor sp;
deserialize("shape_predictor_68_face_landmarks.dat") >> sp;
//定义shape对象保存检测的68个关键点
full_object_detection shape = sp(img, var);
//存储文件
ofstream out("face_detector.txt");
//读取关键点到容器中
std::vector points_vec;
for (int i = 0; i < shape.num_parts(); ++i)
{
auto a = shape.part(i);
out << a.x() << " " << a.y() << " ";
Point2f ff(a.x(), a.y());
points_vec.push_back(ff);
}
rets.push_back(points_vec);
}
cout << "人脸检测结束:" <
5.图像平移变形算法
接下来的难题就是图像局部变形算法,具体原理就是瘦脸是使用图像局部平移变形,放大眼睛是图像局部缩放变形。
图像局部平移变形公式:
说实话我看着这个公式看不懂,好像是结合《Interactive Image Warping》交互式图像变形算法而来的,去查看此文章过程中,发现全英文的,只能说一句,干瞪眼,完全看不懂,更别说公式是怎么推导出来的了,放弃了,直接根据大佬的代码去看懂这个公式。
参考:https://blog.csdn.net/grafx/article/details/70232797 图像处理算法之瘦脸及放大眼睛
图像局部缩放公式(大眼):
参考:https://www.freesion.com/article/40151105562/ 瘦脸大眼算法
百度过程中并没有发现c++编写的一些瘦脸大眼测试例子,有些贴的代码比较简单参数有点不好理解,但是python却有相关的例子(原理一样 ),因此通过python代码去封装成c++ 能使用的接口。
瘦脸和大眼主要是参考如下两篇博客来封装的函数接口:
参考:https://www.cnblogs.com/ckAng/p/10978078.html python+opencv+dlib瘦脸效果
参考:https://www.freesion.com/article/40151105562/ 瘦脸大眼算法
6.回调函数 大眼效果调节
void BeautyCam::on_BigEye(int b, void*userdata)
{
Mat src = *((Mat *)userdata);
Mat dst = src.clone();
for (auto points_vec : m_pIntance->m_vecFaceData)
{
Point2f left_landmark = points_vec[38];
Point2f left_landmark_down = points_vec[27];
Point2f right_landmark = points_vec[44];
Point2f right_landmark_down = points_vec[27];
Point2f endPt = points_vec[30];
//# 计算第4个点到第6个点的距离作为距离
/*float r_left = sqrt(
(left_landmark.x - left_landmark_down.x) * (left_landmark.x - left_landmark_down.x) +
(left_landmark.y - left_landmark_down.y) * (left_landmark.y - left_landmark_down.y));
cout << "左眼距离:" << r_left;*/
float r_left = b;
// # 计算第14个点到第16个点的距离作为距离
//float r_right = sqrt(
// (right_landmark.x - right_landmark_down.x) * (right_landmark.x - right_landmark_down.x) +
// (right_landmark.y - right_landmark_down.y) * (right_landmark.y - right_landmark_down.y));
//cout << "右眼距离:" << r_right;
float r_right = b;
// # 瘦左
m_pIntance->LocalTranslationWarp_Eye(src, dst, left_landmark.x, left_landmark.y, endPt.x, endPt.y, r_left);
// # 瘦右
m_pIntance->LocalTranslationWarp_Eye(src, dst, right_landmark.x, right_landmark.y, endPt.x, endPt.y, r_right);
}
imshow("BeautyCam", dst);
}
对于眼睛的放大,主要是对特征点38 27 44 30 四个点来进行图像局部缩放,也可以自己找适当的点尝试,点并不是唯一的。
此处用作滑条调节,因此把左眼距离和右眼距离都设置成滑条值,但这样是有问题的,因为不是每对眼睛拍照的眼珠都是一样大的。因此可以直接根据计算的距离(代码注释的地方)来进行加减滑条值。
例如:r_right = (系数)*r_right + b r_left= (系数)*r_left+ b 需要自己去调节适当的系数
图像局部缩放算法代码实现:
void BeautyCam::LocalTranslationWarp_Eye(Mat &img, Mat &dst, int warpX, int warpY, int endX, int endY, float radius)
{
//平移距离
float ddradius = radius * radius;
//计算|m-c|^2
size_t mc = (endX - warpX)*(endX - warpX) + (endY - warpY)*(endY - warpY);
//计算 图像的高 宽 通道数量
int height = img.rows;
int width = img.cols;
int chan = img.channels();
auto Abs = [&](float f) {
return f > 0 ? f : -f;
};
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
// # 计算该点是否在形变圆的范围之内
//# 优化,第一步,直接判断是会在(startX, startY)的矩阵框中
if ((Abs(i - warpX) > radius) && (Abs(j - warpY) > radius))
continue;
float distance = (i - warpX)*(i - warpX) + (j - warpY)*(j - warpY);
if (distance < ddradius)
{
float rnorm = sqrt(distance) / radius;
float ratio = 1 - (rnorm - 1)*(rnorm - 1)*0.5;
//映射原位置
float UX = warpX + ratio * (i - warpX);
float UY = warpY + ratio * (j - warpY);
//根据双线性插值得到UX UY的值
BilinearInsert(img, dst, UX, UY, i, j);
}
}
}
}
这其中使用到了双线性插值算法,因此也去查看了双线性插值算法原理,这里我只是对每一个像素点单独进行双线性插值如下:
void BeautyCam::BilinearInsert(Mat &src, Mat &dst, float ux, float uy, int i, int j)
{
auto Abs = [&](float f) {
return f > 0 ? f : -f;
};
int c = src.channels();
if (c == 3)
{
//存储图像得浮点坐标
CvPoint2D32f uv;
CvPoint3D32f f1;
CvPoint3D32f f2;
//取整数
int iu = (int)ux;
int iv = (int)uy;
uv.x = iu + 1;
uv.y = iv + 1;
//step图象像素行的实际宽度 三个通道进行计算(0 , 1 2 三通道)
f1.x = ((uchar*)(src.data + src.step*iv))[iu * 3] * (1 - Abs(uv.x - iu)) + \
((uchar*)(src.data + src.step*iv))[(iu + 1) * 3] * (uv.x - iu);
f1.y = ((uchar*)(src.data + src.step*iv))[iu * 3 + 1] * (1 - Abs(uv.x - iu)) + \
((uchar*)(src.data + src.step*iv))[(iu + 1) * 3 + 1] * (uv.x - iu);
f1.z = ((uchar*)(src.data + src.step*iv))[iu * 3 + 2] * (1 - Abs(uv.x - iu)) + \
((uchar*)(src.data + src.step*iv))[(iu + 1) * 3 + 2] * (uv.x - iu);
f2.x = ((uchar*)(src.data + src.step*(iv + 1)))[iu * 3] * (1 - Abs(uv.x - iu)) + \
((uchar*)(src.data + src.step*(iv + 1)))[(iu + 1) * 3] * (uv.x - iu);
f2.y = ((uchar*)(src.data + src.step*(iv + 1)))[iu * 3 + 1] * (1 - Abs(uv.x - iu)) + \
((uchar*)(src.data + src.step*(iv + 1)))[(iu + 1) * 3 + 1] * (uv.x - iu);
f2.z = ((uchar*)(src.data + src.step*(iv + 1)))[iu * 3 + 2] * (1 - Abs(uv.x - iu)) + \
((uchar*)(src.data + src.step*(iv + 1)))[(iu + 1) * 3 + 2] * (uv.x - iu);
((uchar*)(dst.data + dst.step*j))[i * 3] = f1.x*(1 - Abs(uv.y - iv)) + f2.x*(Abs(uv.y - iv)); //三个通道进行赋值
((uchar*)(dst.data + dst.step*j))[i * 3 + 1] = f1.y*(1 - Abs(uv.y - iv)) + f2.y*(Abs(uv.y - iv));
((uchar*)(dst.data + dst.step*j))[i * 3 + 2] = f1.z*(1 - Abs(uv.y - iv)) + f2.z*(Abs(uv.y - iv));
}
}
整体的一个大眼效果就完成了。
7.回调函数 瘦脸效果调节
void BeautyCam::on_thinFace(int b, void*userdata)
{
Mat src = *((Mat *)userdata);
Mat dst = src.clone();
for (auto points_vec : m_pIntance->m_vecFaceData)
{
Point2f endPt = points_vec[34];
for (int i = 3; i < 15; i = i + 2)
{
Point2f start_landmark = points_vec[i];
Point2f end_landmark = points_vec[i + 2];
//计算瘦脸距离(相邻两个点算距离)
/*float dis = sqrt(
(start_landmark.x - end_landmark.x) * (start_landmark.x - end_landmark.x) +
(start_landmark.y - end_landmark.y) * (start_landmark.y - end_landmark.y));*/
float dis = b;
dst = m_pIntance->LocalTranslationWarp_Face(dst, start_landmark.x, start_landmark.y, endPt.x, endPt.y, dis);
}
}
imshow("BeautyCam", dst);
}
在这里就没有选择进行选定指定特征点进行计算距离,我直接使用滑条值来作为瘦脸距离进行操作的。具体可以根据上面68特征点采取几个点来计算瘦脸距离。
图像局部平移算法代码实现:
Mat dst = img.clone();
//平移距离
float ddradius = radius * radius;
//计算|m-c|^2
size_t mc = (endX - warpX)*(endX - warpX) + (endY - warpY)*(endY - warpY);
//计算 图像的高 宽 通道数量
int height = img.rows;
int width = img.cols;
int chan = img.channels();
auto Abs = [&](float f) {
return f > 0 ? f : -f;
};
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
// # 计算该点是否在形变圆的范围之内
//# 优化,第一步,直接判断是会在(startX, startY)的矩阵框中
if ((Abs(i - warpX) > radius) && (Abs(j - warpY) > radius))
continue;
float distance = (i - warpX)*(i - warpX) + (j - warpY)*(j - warpY);
if (distance < ddradius)
{
//# 计算出(i, j)坐标的原坐标
//# 计算公式中右边平方号里的部分
float ratio = (ddradius - distance) / (ddradius - distance + mc);
ratio *= ratio;
//映射原位置
float UX = i - ratio * (endX - warpX);
float UY = j - ratio * (endY - warpY);
//根据双线性插值得到UX UY的值
BilinearInsert(img, dst, UX, UY, i, j);
//改变当前的值
}
}
}
return dst;
}
以上两个平移和缩放算法主要就是根据python代码来进行封装的,整体效果还行,但还是需要不断的优化和更改。
8.回调函数 美颜磨皮算法
原理是使用opencv自带的人脸训练数据来获取人脸矩阵数据,进行双边滤波和高斯模糊来实现的;
美颜磨皮算法公式:
代码实现如下:
void BeautyCam::on_beautyFace(int b, void*userdata)
{
Mat src = *((Mat *)userdata);
Mat img = src.clone();
double scale = 1.3;
CascadeClassifier cascade = m_pIntance->loadCascadeClassifier("./haarcascade_frontalface_alt.xml");//人脸的训练数据
CascadeClassifier netcascade = m_pIntance->loadCascadeClassifier("./haarcascade_eye_tree_eyeglasses.xml");//人眼的训练数据
if (cascade.empty() || netcascade.empty())
return;
m_pIntance->detectAndDraw(img, cascade, scale,b);
if (m_pIntance->isDetected == false)
{
cout << "enter" << endl;
Mat dst;
int value1 = 3, value2 = 1;
int dx = value1 * 5; //双边滤波参数之一
//double fc = value1 * 12.5; //双边滤波参数之一
double fc = b;
int p = 50;//透明度
Mat temp1, temp2, temp3, temp4;
//对原图层image进行双边滤波,结果存入temp1图层中
bilateralFilter(img, temp1, dx, fc, fc);
//将temp1图层减去原图层image,将结果存入temp2图层中
temp2 = (temp1 - img + 128);
//高斯模糊
GaussianBlur(temp2, temp3, Size(2 * value2 - 1, 2 * value2 - 1), 0, 0);
//以原图层image为基色,以temp3图层为混合色,将两个图层进行线性光混合得到图层temp4
temp4 = img + 2 * temp3 - 255;
//考虑不透明度,修正上一步的结果,得到最终图像dst
dst = (img*(100 - p) + temp4 * p) / 100;
dst.copyTo(img);
}
imshow("BeautyCam", img);
}
美颜效果主要是通过双边滤波参数来调节的。
void BeautyCam::detectAndDraw(Mat& img, CascadeClassifier& cascade, double scale, int val)
{
std::vector faces;
const static Scalar colors[] = { CV_RGB(0,0,255),
CV_RGB(0,128,255),
CV_RGB(0,255,255),
CV_RGB(0,255,0),
CV_RGB(255,128,0),
CV_RGB(255,255,0),
CV_RGB(255,0,0),
CV_RGB(255,0,255) };//用不同的颜色表示不同的人脸
//将图片缩小,加快检测速度
Mat gray, smallImg(cvRound(img.rows / scale), cvRound(img.cols / scale), CV_8UC1);
//因为用的是类haar特征,所以都是基于灰度图像的,这里要转换成灰度图像
cvtColor(img, gray, CV_BGR2GRAY);
resize(gray, smallImg, smallImg.size(), 0, 0, INTER_LINEAR);//将尺寸缩小到1/scale,用线性插值
equalizeHist(smallImg, smallImg);//直方图均衡
cascade.detectMultiScale(smallImg, //image表示的是要检测的输入图像
faces,//objects表示检测到的人脸目标序列
1.1, //caleFactor表示每次图像尺寸减小的比例
2, //minNeighbors表示每一个目标至少要被检测到3次才算是真的目标(因为周围的像素和不同的窗口大小都可以检测到人脸),
0 | CASCADE_SCALE_IMAGE ,//minSize为目标的最小尺寸
Size(30, 30)); //minSize为目标的最大尺寸
int i = 0;
//遍历检测的矩形框
for (std::vector::const_iterator r = faces.begin(); r != faces.end(); r++, i++)
{
isDetected = true;
Mat smallImgROI;
std::vector nestedObjects;
Point center, left, right;
Scalar color = colors[i % 8];
int radius;
center.x = cvRound((r->x + r->width*0.5)*scale);//还原成原来的大小
center.y = cvRound((r->y + r->height*0.5)*scale);
radius = cvRound((r->width + r->height)*0.25*scale);
left.x = center.x - radius;
left.y = cvRound(center.y - radius * 1.3);
if (left.y < 0)
{
left.y = 0;
}
right.x = center.x + radius;
right.y = cvRound(center.y + radius * 1.3);
if (right.y > img.rows)
{
right.y = img.rows;
}
/*原理算法
美肤-磨皮算法
Dest =(Src * (100 - Opacity) + (Src + 2 * GuassBlur(EPFFilter(Src) - Src + 128) - 256) * Opacity) /100 ;
*/
//绘画识别的人脸框
//rectangle(img, left, right, Scalar(255, 0, 0));
Mat roi = img(Range(left.y, right.y), Range(left.x, right.x));
Mat dst;
int value1 = 3, value2 = 1;
int dx = value1 * 5; //双边滤波参数之一
//double fc = value1 * 12.5; //双边滤波参数之一
double fc = val;//变化值
int p = 50;//透明度
Mat temp1, temp2, temp3, temp4;
//双边滤波 输入图像 输出图像 每像素领域的直径范围颜色空间过滤器的sigma 坐标空间滤波器的sigma
bilateralFilter(roi, temp1, dx, fc, fc);
temp2 = (temp1 - roi + 128);
//高斯模糊
GaussianBlur(temp2, temp3, Size(2 * value2 - 1, 2 * value2 - 1), 0, 0);
temp4 = roi + 2 * temp3 - 255;
dst = (roi*(100 - p) + temp4 * p) / 100;
dst.copyTo(roi);
}
}
到此,美白磨皮的简单功能就实现了。
参考:https://blog.csdn.net/zhangqipu000/article/details/53260647 opencv 美白磨皮人脸检测
整体效果只是简单的实现,如果要运用到项目中,问题还是很多的,需要不断的优化和算法的更改呀。作为最近学习opencv的一个简单demo,来巩固知识点。