在 《圖形圖像處理 - 實現圖片的美容效果》 一文中提到了圖片的美容,採用雙邊濾波算法來實現,具體的算法流程和實現思路,你們能夠在上篇文章中瞭解,這篇文章就在再也不反覆囉嗦了。這裏咱們再次來看下處理效果:android
上面的效果看似好像不錯,其實存在了大量的問題。從處理速度上來講,雙邊模糊算法是在二維的高斯函數上新增像素差值來實現的,使得算法的時間複雜度比較大(處理時間 > 1s),其次從處理效果上來講,用戶一眼就能看出來,這是一張通過加工處理過的圖片,眼睛很迷茫沒了深邃,效果看上去很模糊沒真實感。所以本文就從這兩個方面下手,第一優化美容算法,其次優化美顏效果,使其可以真正的用到咱們的手機移動端,實現實時美顏的功能。算法
以前咱們在實現模糊時,採用的是作卷積操做,其算法的複雜度是 image.rows * image.cols* kernel.rows * kernel.cols 且內部採用的是 float 運算,咱們的卷積核 kernel 越大其算法的複雜度就越大。寫法以下:bash
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<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];
// 區塊的合
int sum = rb - rt - lb + lt;
dst.at<Vec3b>(row, col)[i] = sum / area;
}
}
}
}
複製代碼
實現了快速模糊算法後,咱們就得思考一下如何才能實現,快速的邊緣保留效果呢?咱們來看幾個公式:ui
具體的實現分析,你們能夠參考上面的實現思路,方差公式的推倒你們能夠參考這裏 en.wikipedia.org/wiki/Varian… 。剩下的就是直接開始套公式了:spa
int getBlockSum(Mat &sum_mat, int x0, int y0, int x1, int y1, int ch){
// 獲取四個點的值
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];
// 區塊的合
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<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];
// 區塊的合
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<Vec3b>(row, col)[i];
pixels = (1 - k)*(sum / area) + k * pixels;
dst.at<Vec3b>(row, col)[i] = pixels;
}
}
}
}
複製代碼
實現了快速邊緣保留後,咱們有了兩方面的提高,第一個是算法時間上面的提高,第二個是效果上面的提高,臉上的水滴效果還在,眼睛區域基本沒有變化,圖片看上去比較真實。但咱們發現效果還不是很好,如脖子上面的頭髮與原圖相比有些模糊,所以咱們打算只對皮膚區域實現美顏,其餘區域採用其餘算法。那咱們怎麼去判斷皮膚區域呢?最簡單的一種方式就是根據 RGB 或者 YCrCb 的值來篩選,而後根據皮膚區域來進行融合。3d
// 皮膚區域檢測
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;
}
}
}
}
// 皮膚區域融合
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 ,經過指針去獲取, 指針 -> 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;
}
}
}
複製代碼
若是咱們對處理效果依舊不是很滿意的話,咱們能夠本身再作一些折騰,像邊緣增強或者模糊疊加等等。指針
// 邊緣的提高 (無關緊要)
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 移動端,你們感興趣能夠本身去試試,我將在後面的直播美顏部分來爲你們進行講解。code
視頻地址:pan.baidu.com/s/1Ax6qunmE…
視頻密碼:xzts