在 《图形图像处理 - 实现图片的美容效果》 一文中提到了图片的美容,采用双边滤波算法来实现,具体的算法流程和实现思路,大家可以在上篇文章中了解,这篇文章就在不再反复啰嗦了。这里我们再次来看下处理效果:
上面的效果看似好像不错,其实存在了大量的问题。从处理速度上来说,双边模糊算法是在二维的高斯函数上新增像素差值来实现的,使得算法的时间复杂度比较大(处理时间 > 1s),其次从处理效果上来说,用户一眼就能看出来,这是一张经过加工处理过的图片,眼睛很迷茫没了深邃,效果看上去很模糊没真实感。因此本文就从这两个方面下手,第一优化美容算法,其次优化美颜效果,使其能够真正的用到我们的手机移动端,实现实时美颜的功能。
1. 实现快速模糊
之前我们在实现模糊时,采用的是做卷积操作,其算法的复杂度是 image.rows * image.cols* kernel.rows * kernel.cols 且内部采用的是 float 运算,我们的卷积核 kernel 越大其算法的复杂度就越大。写法如下:
Mat src = imread("C:/Users/hcDarren/Desktop/android/example.png");
if (!src.data){
printf("imread error!");
return -1;
}
imshow("src", src);
Mat dst;
int size = 13;
Mat kernel = Mat::ones(Size(size,size),CV_32FC1)/(size*size);
filter2D(src,dst,src.depth(),kernel);
imshow("dst", dst);
那么有没有什么办法可以优化呢?这里给大家介绍一种新的算法 积分图运算,我们先来看下算法实现思路:
上图的实现原理其实很简单,处理的流程就是我们根据原图创建一张积分图,通过积分图就可以求得原图某一块区域的像素大小总和。之前做卷积操作的复杂度是 kernel.rows * kernel.cols , 而通过积分图来求就变成了 O(1) ,且不会随着卷积核的增大而增加其算法的复杂度。我们来看下具体的代码实现:
// 积分图的模糊算法 size 模糊的直径
void meanBlur(Mat & src, Mat &dst, int size){
// size % 2 == 1
// 把原来进行填充,方便运算
Mat mat;
int radius = size / 2;
copyMakeBorder(src, mat, radius, radius, radius, radius, BORDER_DEFAULT);
// 求积分图 (作业去手写积分图的源码)
Mat sum_mat, sqsum_mat;
integral(mat, sum_mat, sqsum_mat, CV_32S, CV_32S);
dst.create(src.size(), src.type());
int imageH = src.rows;
int imageW = src.cols;
int area = size*size;
// 求四个点,左上,左下,右上,右下
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
int lt = 0, lb = 0, rt = 0, rb = 0;
int channels = src.channels();
for (int row = 0; row < imageH; row++)
{
// 思考,x0,y0 , x1 , y1 sum_mat
// 思考,row, col, dst
y0 = row;
y1 = y0 + size;
for (int col = 0; col < imageW; col++)
{
x0 = col;
x1 = x0 + size;
for (int i = 0; i < channels; i++)
{
// 获取四个点的值
lt = sum_mat.at(y0, x0)[i];
lb = sum_mat.at(y1, x0)[i];
rt = sum_mat.at(y0, x1)[i];
rb = sum_mat.at(y1, x1)[i];
// 区块的合
int sum = rb - rt - lb + lt;
dst.at(row, col)[i] = sum / area;
}
}
}
}
2. 快速边缘保留
实现了快速模糊算法后,我们就得思考一下如何才能实现,快速的边缘保留效果呢?我们来看几个公式:
具体的实现分析,大家可以参考上面的实现思路,方差公式的推倒大家可以参考这里 https://en.wikipedia.org/wiki/Variance 。剩下的就是直接开始套公式了:
int getBlockSum(Mat &sum_mat, int x0, int y0, int x1, int y1, int ch){
// 获取四个点的值
int lt = sum_mat.at(y0, x0)[ch];
int lb = sum_mat.at(y1, x0)[ch];
int rt = sum_mat.at(y0, x1)[ch];
int rb = sum_mat.at(y1, x1)[ch];
// 区块的合
int sum = rb - rt - lb + lt;
return sum;
}
float getBlockSqSum(Mat &sqsum_mat, int x0, int y0, int x1, int y1, int ch){
// 获取四个点的值
float lt = sqsum_mat.at(y0, x0)[ch];
float lb = sqsum_mat.at(y1, x0)[ch];
float rt = sqsum_mat.at(y0, x1)[ch];
float rb = sqsum_mat.at(y1, x1)[ch];
// 区块的合
float sqsum = rb - rt - lb + lt;
return sqsum;
}
// 积分图的模糊算法 size 模糊的直径
void fatsBilateralBlur(Mat & src, Mat &dst, int size, int sigma){
// size % 2 == 1
// 把原来进行填充,方便运算
Mat mat;
int radius = size / 2;
copyMakeBorder(src, mat, radius, radius, radius, radius, BORDER_DEFAULT);
// 求积分图 (作业去手写积分图的源码)
Mat sum_mat, sqsum_mat;
integral(mat, sum_mat, sqsum_mat, CV_32S, CV_32F);
dst.create(src.size(), src.type());
int imageH = src.rows;
int imageW = src.cols;
int area = size*size;
// 求四个点,左上,左下,右上,右下
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
int lt = 0, lb = 0, rt = 0, rb = 0;
int channels = src.channels();
for (int row = 0; row < imageH; row++)
{
// 思考,x0,y0 , x1 , y1 sum_mat
// 思考,row, col, dst
y0 = row;
y1 = y0 + size;
for (int col = 0; col < imageW; col++)
{
x0 = col;
x1 = x0 + size;
for (int i = 0; i < channels; i++)
{
int sum = getBlockSum(sum_mat, x0, y0, x1, y1, i);
float sqsum = getBlockSqSum(sqsum_mat, x0, y0, x1, y1, i);
float diff_sq = (sqsum - (sum * sum) / area) / area;
float k = diff_sq / (diff_sq + sigma);
int pixels = src.at(row, col)[i];
pixels = (1 - k)*(sum / area) + k * pixels;
dst.at(row, col)[i] = pixels;
}
}
}
}
3. 检测与融合皮肤区域
实现了快速边缘保留后,我们有了两方面的提升,第一个是算法时间上面的提升,第二个是效果上面的提升,脸上的水滴效果还在,眼睛区域基本没有变化,图片看上去比较真实。但我们发现效果还不是很好,如脖子上面的头发与原图相比有些模糊,因此我们打算只对皮肤区域实现美颜,其他区域采用其他算法。那我们怎么去判断皮肤区域呢?最简单的一种方式就是根据 RGB 或者 YCrCb 的值来筛选,然后根据皮肤区域来进行融合。
// 皮肤区域检测
void skinDetect(const Mat &src, Mat &skinMask){
skinMask.create(src.size(), CV_8UC1);
int rows = src.rows;
int cols = src.cols;
Mat ycrcb;
cvtColor(src, ycrcb, COLOR_BGR2YCrCb);
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
Vec3b pixels = ycrcb.at(row, col);
uchar y = pixels[0];
uchar cr = pixels[1];
uchar cb = pixels[2];
if (y>80 && 85(row, col) = 255;
}
else{
skinMask.at(row, col) = 0;
}
}
}
}
// 皮肤区域融合
void fuseSkin(const Mat &src, const Mat &blur_mat, Mat &dst, const Mat &mask){
// 融合?
dst.create(src.size(),src.type());
GaussianBlur(mask, mask, Size(3, 3), 0.0);
Mat mask_f;
mask.convertTo(mask_f, CV_32F);
normalize(mask_f, mask_f, 1.0, 0.0, NORM_MINMAX);
int rows = src.rows;
int cols = src.cols;
int ch = src.channels();
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
// mask_f (1-k)
/*
uchar mask_pixels = mask.at(row,col);
// 人脸位置
if (mask_pixels == 255){
dst.at(row, col) = blur_mat.at(row, col);
}
else{
dst.at(row, col) = src.at(row, col);
}
*/
// src ,通过指针去获取, 指针 -> Vec3b -> 获取
uchar b1 = src.at(row, col)[0];
uchar g1 = src.at(row, col)[1];
uchar r1 = src.at(row, col)[2];
// blur_mat
uchar b2 = blur_mat.at(row, col)[0];
uchar g2 = blur_mat.at(row, col)[1];
uchar r2 = blur_mat.at(row, col)[2];
// dst 254 1
float k = mask_f.at(row,col);
dst.at(row, col)[0] = b2*k + (1 - k)*b1;
dst.at(row, col)[1] = g2*k + (1 - k)*g1;
dst.at(row, col)[2] = r2*k + (1 - k)*r1;
}
}
}
4. 最后总结
如果我们对处理效果依旧不是很满意的话,我们可以自己再做一些折腾,像边缘加强或者模糊叠加等等。
// 边缘的提升 (可有可无)
Mat cannyMask;
Canny(src, cannyMask, 150, 300, 3, false);
imshow("Canny", cannyMask);
// & 运算 0 ,255
bitwise_and(src, src, fuseDst, cannyMask);
imshow("bitwise_and", fuseDst);
// 稍微提升一下对比度(亮度)
add(fuseDst, Scalar(10, 10, 10), fuseDst);
最后总结一下:无论我们怎么处理要保证两个方面,第一个是速度方面,因为如果集成到移动端手机上必须得考虑实时性,第二个是效果方面,要让用户看上去自然,尽量不要让用户感知这是处理过的特效。至于怎么集成到 android 移动端,大家感兴趣可以自己去试试,我将在后面的直播美颜部分来为大家进行讲解。
视频地址:https://pan.baidu.com/s/1Ax6qunmEbabtVteYaza3VQ
视频密码:xzts