在 《圖形圖像處理 - 實(shí)現(xiàn)圖片的美容效果》 一文中提到了圖片的美容,采用雙邊濾波算法來(lái)實(shí)現(xiàn),具體的算法流程和實(shí)現(xiàn)思路,大家可以在上篇文章中了解,這篇文章就在不再反復(fù)啰嗦了。這里我們?cè)俅蝸?lái)看下處理效果:


上面的效果看似好像不錯(cuò),其實(shí)存在了大量的問(wèn)題。從處理速度上來(lái)說(shuō),雙邊模糊算法是在二維的高斯函數(shù)上新增像素差值來(lái)實(shí)現(xiàn)的,使得算法的時(shí)間復(fù)雜度比較大(處理時(shí)間 > 1s),其次從處理效果上來(lái)說(shuō),用戶一眼就能看出來(lái),這是一張經(jīng)過(guò)加工處理過(guò)的圖片,眼睛很迷茫沒(méi)了深邃,效果看上去很模糊沒(méi)真實(shí)感。因此本文就從這兩個(gè)方面下手,第一優(yōu)化美容算法,其次優(yōu)化美顏效果,使其能夠真正的用到我們的手機(jī)移動(dòng)端,實(shí)現(xiàn)實(shí)時(shí)美顏的功能。
1. 實(shí)現(xiàn)快速模糊
之前我們?cè)趯?shí)現(xiàn)模糊時(shí),采用的是做卷積操作,其算法的復(fù)雜度是 image.rows * image.cols* kernel.rows * kernel.cols 且內(nèi)部采用的是 float 運(yùn)算,我們的卷積核 kernel 越大其算法的復(fù)雜度就越大。寫(xiě)法如下:
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);
那么有沒(méi)有什么辦法可以優(yōu)化呢?這里給大家介紹一種新的算法 積分圖運(yùn)算,我們先來(lái)看下算法實(shí)現(xiàn)思路:

上圖的實(shí)現(xiàn)原理其實(shí)很簡(jiǎn)單,處理的流程就是我們根據(jù)原圖創(chuàng)建一張積分圖,通過(guò)積分圖就可以求得原圖某一塊區(qū)域的像素大小總和。之前做卷積操作的復(fù)雜度是 kernel.rows * kernel.cols , 而通過(guò)積分圖來(lái)求就變成了 O(1) ,且不會(huì)隨著卷積核的增大而增加其算法的復(fù)雜度。我們來(lái)看下具體的代碼實(shí)現(xiàn):
// 積分圖的模糊算法 size 模糊的直徑
void meanBlur(Mat & src, Mat &dst, int size){
// size % 2 == 1
// 把原來(lái)進(jìn)行填充,方便運(yùn)算
Mat mat;
int radius = size / 2;
copyMakeBorder(src, mat, radius, radius, radius, radius, BORDER_DEFAULT);
// 求積分圖 (作業(yè)去手寫(xiě)積分圖的源碼)
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;
// 求四個(gè)點(diǎn),左上,左下,右上,右下
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++)
{
// 獲取四個(gè)點(diǎn)的值
lt = sum_mat.at<Vec3i>(y0, x0)[i];
lb = sum_mat.at<Vec3i>(y1, x0)[i];
rt = sum_mat.at<Vec3i>(y0, x1)[i];
rb = sum_mat.at<Vec3i>(y1, x1)[i];
// 區(qū)塊的合
int sum = rb - rt - lb + lt;
dst.at<Vec3b>(row, col)[i] = sum / area;
}
}
}
}

2. 快速邊緣保留
實(shí)現(xiàn)了快速模糊算法后,我們就得思考一下如何才能實(shí)現(xiàn),快速的邊緣保留效果呢?我們來(lái)看幾個(gè)公式:


具體的實(shí)現(xiàn)分析,大家可以參考上面的實(shí)現(xiàn)思路,方差公式的推倒大家可以參考這里 https://en.wikipedia.org/wiki/Variance 。剩下的就是直接開(kāi)始套公式了:
int getBlockSum(Mat &sum_mat, int x0, int y0, int x1, int y1, int ch){
// 獲取四個(gè)點(diǎn)的值
int lt = sum_mat.at<Vec3i>(y0, x0)[ch];
int lb = sum_mat.at<Vec3i>(y1, x0)[ch];
int rt = sum_mat.at<Vec3i>(y0, x1)[ch];
int rb = sum_mat.at<Vec3i>(y1, x1)[ch];
// 區(qū)塊的合
int sum = rb - rt - lb + lt;
return sum;
}
float getBlockSqSum(Mat &sqsum_mat, int x0, int y0, int x1, int y1, int ch){
// 獲取四個(gè)點(diǎn)的值
float lt = sqsum_mat.at<Vec3f>(y0, x0)[ch];
float lb = sqsum_mat.at<Vec3f>(y1, x0)[ch];
float rt = sqsum_mat.at<Vec3f>(y0, x1)[ch];
float rb = sqsum_mat.at<Vec3f>(y1, x1)[ch];
// 區(qū)塊的合
float sqsum = rb - rt - lb + lt;
return sqsum;
}
// 積分圖的模糊算法 size 模糊的直徑
void fatsBilateralBlur(Mat & src, Mat &dst, int size, int sigma){
// size % 2 == 1
// 把原來(lái)進(jìn)行填充,方便運(yùn)算
Mat mat;
int radius = size / 2;
copyMakeBorder(src, mat, radius, radius, radius, radius, BORDER_DEFAULT);
// 求積分圖 (作業(yè)去手寫(xiě)積分圖的源碼)
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;
// 求四個(gè)點(diǎn),左上,左下,右上,右下
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<Vec3b>(row, col)[i];
pixels = (1 - k)*(sum / area) + k * pixels;
dst.at<Vec3b>(row, col)[i] = pixels;
}
}
}
}


3. 檢測(cè)與融合皮膚區(qū)域
實(shí)現(xiàn)了快速邊緣保留后,我們有了兩方面的提升,第一個(gè)是算法時(shí)間上面的提升,第二個(gè)是效果上面的提升,臉上的水滴效果還在,眼睛區(qū)域基本沒(méi)有變化,圖片看上去比較真實(shí)。但我們發(fā)現(xiàn)效果還不是很好,如脖子上面的頭發(fā)與原圖相比有些模糊,因此我們打算只對(duì)皮膚區(qū)域?qū)崿F(xiàn)美顏,其他區(qū)域采用其他算法。那我們?cè)趺慈ヅ袛嗥つw區(qū)域呢?最簡(jiǎn)單的一種方式就是根據(jù) RGB 或者 YCrCb 的值來(lái)篩選,然后根據(jù)皮膚區(qū)域來(lái)進(jìn)行融合。

// 皮膚區(qū)域檢測(cè)
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<Vec3b>(row, col);
uchar y = pixels[0];
uchar cr = pixels[1];
uchar cb = pixels[2];
if (y>80 && 85<cb<135 && 135<cr<180){
skinMask.at<uchar>(row, col) = 255;
}
else{
skinMask.at<uchar>(row, col) = 0;
}
}
}
}
// 皮膚區(qū)域融合
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<uchar>(row,col);
// 人臉位置
if (mask_pixels == 255){
dst.at<Vec3b>(row, col) = blur_mat.at<Vec3b>(row, col);
}
else{
dst.at<Vec3b>(row, col) = src.at<Vec3b>(row, col);
}
*/
// src ,通過(guò)指針去獲取, 指針 -> Vec3b -> 獲取
uchar b1 = src.at<Vec3b>(row, col)[0];
uchar g1 = src.at<Vec3b>(row, col)[1];
uchar r1 = src.at<Vec3b>(row, col)[2];
// blur_mat
uchar b2 = blur_mat.at<Vec3b>(row, col)[0];
uchar g2 = blur_mat.at<Vec3b>(row, col)[1];
uchar r2 = blur_mat.at<Vec3b>(row, col)[2];
// dst 254 1
float k = mask_f.at<float>(row,col);
dst.at<Vec3b>(row, col)[0] = b2*k + (1 - k)*b1;
dst.at<Vec3b>(row, col)[1] = g2*k + (1 - k)*g1;
dst.at<Vec3b>(row, col)[2] = r2*k + (1 - k)*r1;
}
}
}


4. 最后總結(jié)
如果我們對(duì)處理效果依舊不是很滿意的話,我們可以自己再做一些折騰,像邊緣加強(qiáng)或者模糊疊加等等。
// 邊緣的提升 (可有可無(wú))
Mat cannyMask;
Canny(src, cannyMask, 150, 300, 3, false);
imshow("Canny", cannyMask);
// & 運(yùn)算 0 ,255
bitwise_and(src, src, fuseDst, cannyMask);
imshow("bitwise_and", fuseDst);
// 稍微提升一下對(duì)比度(亮度)
add(fuseDst, Scalar(10, 10, 10), fuseDst);
最后總結(jié)一下:無(wú)論我們?cè)趺刺幚硪WC兩個(gè)方面,第一個(gè)是速度方面,因?yàn)槿绻傻揭苿?dòng)端手機(jī)上必須得考慮實(shí)時(shí)性,第二個(gè)是效果方面,要讓用戶看上去自然,盡量不要讓用戶感知這是處理過(guò)的特效。至于怎么集成到 android 移動(dòng)端,大家感興趣可以自己去試試,我將在后面的直播美顏部分來(lái)為大家進(jìn)行講解。

視頻地址:https://pan.baidu.com/s/1Ax6qunmEbabtVteYaza3VQ
視頻密碼:xzts