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

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

《NDK 开发实战 - 实现相机美颜功能》 处理前
《NDK 开发实战 - 实现相机美颜功能》 处理后

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

那么有没有什么办法可以优化呢?这里给大家介绍一种新的算法 积分图运算,我们先来看下算法实现思路:

《NDK 开发实战 - 实现相机美颜功能》 积分图计算.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;
            }
        }
    }
}

《NDK 开发实战 - 实现相机美颜功能》 快速模糊效果

2. 快速边缘保留

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

《NDK 开发实战 - 实现相机美颜功能》 快速边缘保留算法.png
《NDK 开发实战 - 实现相机美颜功能》 局部方差公式推导.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;
            }
        }
    }
}

《NDK 开发实战 - 实现相机美颜功能》 处理前
《NDK 开发实战 - 实现相机美颜功能》 处理后

3. 检测与融合皮肤区域

实现了快速边缘保留后,我们有了两方面的提升,第一个是算法时间上面的提升,第二个是效果上面的提升,脸上的水滴效果还在,眼睛区域基本没有变化,图片看上去比较真实。但我们发现效果还不是很好,如脖子上面的头发与原图相比有些模糊,因此我们打算只对皮肤区域实现美颜,其他区域采用其他算法。那我们怎么去判断皮肤区域呢?最简单的一种方式就是根据 RGB 或者 YCrCb 的值来筛选,然后根据皮肤区域来进行融合。

《NDK 开发实战 - 实现相机美颜功能》 皮肤区域检测

// 皮肤区域检测
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;
        }
    }
}

《NDK 开发实战 - 实现相机美颜功能》 处理前
《NDK 开发实战 - 实现相机美颜功能》 处理后

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

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

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

    原文作者:红橙Darren
    原文地址: https://www.jianshu.com/p/82de03d33f2e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞