美容思路
人像美容即去除面部杂质的过程, 这一过程通常会使用像素平滑处理, 也就是模糊效果
一. 使用双边模糊实现
双边模糊是基于高斯模糊, 不过其可以指定处理像素的范围, 因此它可以保留更多的细节
#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);
}