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);

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

积分图计算.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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容