NDK 開發實戰 - 實現相機美顏功能

《圖形圖像處理 - 實現圖片的美容效果》 一文中提到了圖片的美容,採用雙邊濾波算法來實現,具體的算法流程和實現思路,你們能夠在上篇文章中瞭解,這篇文章就在再也不反覆囉嗦了。這裏咱們再次來看下處理效果:android

處理前

處理後

上面的效果看似好像不錯,其實存在了大量的問題。從處理速度上來講,雙邊模糊算法是在二維的高斯函數上新增像素差值來實現的,使得算法的時間複雜度比較大(處理時間 > 1s),其次從處理效果上來講,用戶一眼就能看出來,這是一張通過加工處理過的圖片,眼睛很迷茫沒了深邃,效果看上去很模糊沒真實感。所以本文就從這兩個方面下手,第一優化美容算法,其次優化美顏效果,使其可以真正的用到咱們的手機移動端,實現實時美顏的功能。算法

1. 實現快速模糊

以前咱們在實現模糊時,採用的是作卷積操做,其算法的複雜度是 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);
複製代碼

那麼有沒有什麼辦法能夠優化呢?這裏給你們介紹一種新的算法 積分圖運算,咱們先來看下算法實現思路: 函數

積分圖計算.png

上圖的實現原理其實很簡單,處理的流程就是咱們根據原圖建立一張積分圖,經過積分圖就能夠求得原圖某一塊區域的像素大小總和。以前作卷積操做的複雜度是 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;
			}
		}
	}
}
複製代碼

快速模糊效果

2. 快速邊緣保留

實現了快速模糊算法後,咱們就得思考一下如何才能實現,快速的邊緣保留效果呢?咱們來看幾個公式:ui

快速邊緣保留算法.png

局部方差公式推導.png

具體的實現分析,你們能夠參考上面的實現思路,方差公式的推倒你們能夠參考這裏 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;
			}
		}
	}
}
複製代碼

處理前

處理後

3. 檢測與融合皮膚區域

實現了快速邊緣保留後,咱們有了兩方面的提高,第一個是算法時間上面的提高,第二個是效果上面的提高,臉上的水滴效果還在,眼睛區域基本沒有變化,圖片看上去比較真實。但咱們發現效果還不是很好,如脖子上面的頭髮與原圖相比有些模糊,所以咱們打算只對皮膚區域實現美顏,其餘區域採用其餘算法。那咱們怎麼去判斷皮膚區域呢?最簡單的一種方式就是根據 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;
		}
	}
}
複製代碼

處理前

處理後

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 移動端,你們感興趣能夠本身去試試,我將在後面的直播美顏部分來爲你們進行講解。code

視頻地址:pan.baidu.com/s/1Ax6qunmE…

視頻密碼:xzts

相關文章
相關標籤/搜索