NDK 开发实战 - 实现相机美颜功能

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/z240336124/article/details/88621737

《图形图像处理 - 实现图片的美容效果》 一文中提到了图片的美容,采用双边滤波算法来实现,具体的算法流程和实现思路,大家可以在上篇文章中了解,这篇文章就在不再反复啰嗦了。这里我们再次来看下处理效果:
处理前

处理后

上面的效果看似好像不错,其实存在了大量的问题。从处理速度上来说,双边模糊算法是在二维的高斯函数上新增像素差值来实现的,使得算法的时间复杂度比较大(处理时间 > 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);

那么有没有什么办法可以优化呢?这里给大家介绍一种新的算法 积分图运算,我们先来看下算法实现思路:
积分图计算.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. 快速边缘保留

实现了快速模糊算法后,我们就得思考一下如何才能实现,快速的边缘保留效果呢?我们来看几个公式:

快速边缘保留算法.png

局部方差公式推导.png

具体的实现分析,大家可以参考上面的实现思路,方差公式的推倒大家可以参考这里 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<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 的值来筛选,然后根据皮肤区域来进行融合。

皮肤区域检测

// 皮肤区域检测
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 移动端,大家感兴趣可以自己去试试,我将在后面的直播美颜部分来为大家进行讲解。

视频地址:https://pan.baidu.com/s/1Ax6qunmEbabtVteYaza3VQ
视频密码:xzts

猜你喜欢

转载自blog.csdn.net/z240336124/article/details/88621737