OpenCV 实战 —— 人像美容(二) 美容实战

 

美容思路

人像美容即去除面部杂质的过程, 这一过程通常会使用像素平滑处理, 也就是模糊效果

一. 使用双边模糊实现

双边模糊是基于高斯模糊, 不过其可以指定处理像素的范围, 因此它可以保留更多的细节

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;

void main() {
	Mat src = imread("F:/VisualStudioSpace/OpenCV/src/timg.jpg");
	imshow("src", src);
	Mat bilateral_blur;
	// 对 [0, 100] 之内的像素, 进行模糊处理
	bilateralFilter(src, bilateral_blur, 0, 100, 15);
	imshow("bilateral_blur", bilateral_blur);
}

效果分析

双边模糊实现

  • 处理速度较慢
  • 模糊较严重, 发丝细节没有得到很好的保留
  • 眼睛无神

二. 效果优化

图像美容的难点主要在于, 去除面部杂质的同时, 较好的保留人物的细节

  • 检测面部区域: 通过像素阈值, 只对皮肤部分进行处理(亚洲皮肤)
  • 速度优化: 使用积分图, 提升均值模糊算法速度
  • 保留细节: 边缘保留算法

一) 边缘保留算法

快速边缘保留算法, 是根据积分图通过常熟级的计算, 得到局部的均值与方差, 让一个滤波操作变成一个常量时间可以完成的操作

1. 计算局部均值

\(M_i,_j = 通过积分图求区块内的和\)

2. 计算局部方差

\(DiffSq(x) = \frac{1}{n}(\sum_{i=1}^n{x_i^2} - \frac{1}{n}(\sum_{i=1}^n{x_i})^2)\)

3. 计算边缘系数

\(K_i,_j = \frac{DiffSq_i,_j}{DiffSq_i,_j + sigma^2}\)

4. 局部方差滤波公式

\(Pixel_i,_j = (1 - K_i,_j)*M_i,_j + K_i,_j*Z_i,_j\)

其中 Z 代表原像素值

  • 当边缘系数 K 趋近于 0 时, 会趋近于平均像素值
  • 当边缘系数 K 趋近于 1 时, 会趋近于真实像素, 即较好的保留边缘效果

二) 面部区域检测

1. YCbCr 色彩空间

Y > 80 && 85 < Cb < 135 && 135 < Cr < 180

2. RGB 色彩空间

min = min(R, G, B), max = max(R, G, B)
R > 95 && G > 40 && B > 20 && (R - G) > 15 && max - min > 15 && R > G && R > B

三) 算法实现

#include<opencv2/opencv.hpp>
#include<iostream>

using namespace cv;

// 找寻皮肤区域
void skinDetect(const Mat & src, Mat & mask) {
	mask.create(src.size(), CV_8UC1);
	Mat ycrcb;
	cvtColor(src, ycrcb, COLOR_BGR2YCrCb);
	for (int row = 0; row < src.rows; row++) {
		for (int col = 0; col < src.cols; col++) {
			Vec3b pixel = ycrcb.at<Vec3b>(row, col);
			uchar y = pixel[0];
			uchar cr = pixel[1];
			uchar cb = pixel[2];
			// 找寻皮肤区域
			if (y > 80 && 85 < cb < 135 && 135 < cr < 180) {
				mask.at<uchar>(row, col) = 255;
			}
			else {
				mask.at<uchar>(row, col) = 0;
			}
		}
	}
	// 对找到的皮肤区域做一次平滑处理, 注意: (平滑处理之后, 值就不仅仅为 0 和 255 了)
	blur(mask, mask, Size(5, 5));
}

// 获取区块内的和
int getBlockSum(Mat & mat, int x0, int y0, int x1, int y1, int channel) {
	int lt = mat.at<Vec3i>(y0, x0)[channel];
	int lb = mat.at<Vec3i>(y1, x0)[channel];
	int rt = mat.at<Vec3i>(y0, x1)[channel];
	int rb = mat.at<Vec3i>(y1, x1)[channel];
	return rb - rt - lb + lt;
}

// 获取区块内的平方和
float geBlockSqrtSum(Mat & mat, int x0, int y0, int x1, int y1, int channel) {
	float lt = mat.at<Vec3f>(y0, x0)[channel];
	float lb = mat.at<Vec3f>(y1, x0)[channel];
	float rt = mat.at<Vec3f>(y0, x1)[channel];
	float rb = mat.at<Vec3f>(y1, x1)[channel];
	return rb - rt - lb + lt;
}

// 使用积分图, 实现的均值模糊效果
void fastSkinBlur(const Mat & src, Mat & dst, const Mat & skin_mask, int size, float sigma) {
	// ...... 验证 size 是否为基数
	// 填充边框, 方便运算
	Mat mat;
	int border_size = size >> 1;
	copyMakeBorder(src, mat, border_size, border_size, border_size, border_size, BORDER_DEFAULT);
	// 根据 mat 求其对应的积分图
	Mat sum_mat, sqsum_mat;
	integral(mat, sum_mat, sqsum_mat, CV_32S, CV_32F);
	// 根据积分图进行区间
	dst.create(src.size(), src.type());
	int width = src.cols;
	int height = src.rows;
	int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
	int lt = 0, lb = 0, rt = 0, rb = 0;
	int area = size * size;
	int channels = sum_mat.channels();
	for (int row = 0; row < height; row++)
	{
		y0 = row;
		y1 = row + size;
		for (int col = 0; col < width; col++) {
			// 若不在皮肤区域, 使用源像素值即可
			if (skin_mask.at<uchar>(row, col) < 255 >> 1) {
				dst.at<Vec3b>(row, col) = src.at<Vec3b>(row, col);
				continue;
			}
			x0 = col;
			x1 = col + size;
			for (int i = 0; i < channels; i++) {
				// 1. 计算局部均值
				int sum = getBlockSum(sum_mat, x0, y0, x1, y1, i);
				// 2. 计算局部方差
				float sq_sum = geBlockSqrtSum(sqsum_mat, x0, y0, x1, y1, i);
				float diff_sq = (sq_sum - (sum * sum) / area) / area;
				// 3. 计算边缘系数
				float k = diff_sq / (diff_sq + sigma);
				// 4. 通过局部方差滤波公式, 计算像素值
				int pixel = src.at<Vec3b>(row, col)[i];
				pixel = (1 - k) * (sum / area) + k * pixel;
				// 5. 给 dst 赋值
				dst.at<Vec3b>(row, col)[i] = saturate_cast<uchar>(pixel);
			}
		}
	}
}

void main() {
	Mat src = imread("F:/VisualStudioSpace/OpenCV/src/timg.jpg");
	imshow("src", src);
	// 找寻皮肤区域
	Mat skin_mask;
	skinDetect(src, skin_mask);
	// 图像美容函数
	Mat dst;
	int size = 15;
	fastSkinBlur(src, dst, skin_mask, size, size * size);
	// 稍微提升一些亮度
	add(dst, Scalar(10, 10, 10), dst);
	// 轮廓提升(高性能设备上使用)
	Mat candy;
	Canny(src, candy, 100, 200, 3, false);
	bitwise_and(src, src, dst, candy);
	// 展示最终图像
	imshow("dst", dst);
	cvWaitKey(0);
}

效果展示

image