NDK 開(kāi)發(fā)實(shí)戰(zhàn) - 實(shí)現(xiàn)相機(jī)美顏功能

《圖形圖像處理 - 實(shí)現(xiàn)圖片的美容效果》 一文中提到了圖片的美容,采用雙邊濾波算法來(lái)實(shí)現(xiàn),具體的算法流程和實(shí)現(xiàn)思路,大家可以在上篇文章中了解,這篇文章就在不再反復(fù)啰嗦了。這里我們?cè)俅蝸?lái)看下處理效果:

處理前
處理后

上面的效果看似好像不錯(cuò),其實(shí)存在了大量的問(wèn)題。從處理速度上來(lái)說(shuō),雙邊模糊算法是在二維的高斯函數(shù)上新增像素差值來(lái)實(shí)現(xiàn)的,使得算法的時(shí)間復(fù)雜度比較大(處理時(shí)間 > 1s),其次從處理效果上來(lái)說(shuō),用戶一眼就能看出來(lái),這是一張經(jīng)過(guò)加工處理過(guò)的圖片,眼睛很迷茫沒(méi)了深邃,效果看上去很模糊沒(méi)真實(shí)感。因此本文就從這兩個(gè)方面下手,第一優(yōu)化美容算法,其次優(yōu)化美顏效果,使其能夠真正的用到我們的手機(jī)移動(dòng)端,實(shí)現(xiàn)實(shí)時(shí)美顏的功能。

1. 實(shí)現(xiàn)快速模糊

之前我們?cè)趯?shí)現(xiàn)模糊時(shí),采用的是做卷積操作,其算法的復(fù)雜度是 image.rows * image.cols* kernel.rows * kernel.cols 且內(nèi)部采用的是 float 運(yùn)算,我們的卷積核 kernel 越大其算法的復(fù)雜度就越大。寫(xiě)法如下:

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

那么有沒(méi)有什么辦法可以優(yōu)化呢?這里給大家介紹一種新的算法 積分圖運(yùn)算,我們先來(lái)看下算法實(shí)現(xiàn)思路:

積分圖計(jì)算.png

上圖的實(shí)現(xiàn)原理其實(shí)很簡(jiǎn)單,處理的流程就是我們根據(jù)原圖創(chuàng)建一張積分圖,通過(guò)積分圖就可以求得原圖某一塊區(qū)域的像素大小總和。之前做卷積操作的復(fù)雜度是 kernel.rows * kernel.cols , 而通過(guò)積分圖來(lái)求就變成了 O(1) ,且不會(huì)隨著卷積核的增大而增加其算法的復(fù)雜度。我們來(lái)看下具體的代碼實(shí)現(xiàn):

// 積分圖的模糊算法 size 模糊的直徑
void meanBlur(Mat & src, Mat &dst, int size){
    // size % 2 == 1
    // 把原來(lái)進(jìn)行填充,方便運(yùn)算
    Mat mat;
    int radius = size / 2;
    copyMakeBorder(src, mat, radius, radius, radius, radius, BORDER_DEFAULT);
    // 求積分圖 (作業(yè)去手寫(xiě)積分圖的源碼) 
    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;
    // 求四個(gè)點(diǎn),左上,左下,右上,右下
    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++)
            {
                // 獲取四個(gè)點(diǎn)的值
                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];

                // 區(qū)塊的合
                int sum = rb - rt - lb + lt;
                dst.at<Vec3b>(row, col)[i] = sum / area;
            }
        }
    }
}
快速模糊效果
2. 快速邊緣保留

實(shí)現(xiàn)了快速模糊算法后,我們就得思考一下如何才能實(shí)現(xiàn),快速的邊緣保留效果呢?我們來(lái)看幾個(gè)公式:

快速邊緣保留算法.png
局部方差公式推導(dǎo).png

具體的實(shí)現(xiàn)分析,大家可以參考上面的實(shí)現(xiàn)思路,方差公式的推倒大家可以參考這里 https://en.wikipedia.org/wiki/Variance 。剩下的就是直接開(kāi)始套公式了:

int getBlockSum(Mat &sum_mat, int x0, int y0, int x1, int y1, int ch){
    // 獲取四個(gè)點(diǎn)的值
    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];

    // 區(qū)塊的合
    int sum = rb - rt - lb + lt;
    return sum;
}

float getBlockSqSum(Mat &sqsum_mat, int x0, int y0, int x1, int y1, int ch){
    // 獲取四個(gè)點(diǎn)的值
    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];

    // 區(qū)塊的合
    float sqsum = rb - rt - lb + lt;
    return sqsum;
}


// 積分圖的模糊算法 size 模糊的直徑
void fatsBilateralBlur(Mat & src, Mat &dst, int size, int sigma){
    // size % 2 == 1
    // 把原來(lái)進(jìn)行填充,方便運(yùn)算
    Mat mat;
    int radius = size / 2;
    copyMakeBorder(src, mat, radius, radius, radius, radius, BORDER_DEFAULT);
    // 求積分圖 (作業(yè)去手寫(xiě)積分圖的源碼) 
    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;
    // 求四個(gè)點(diǎn),左上,左下,右上,右下
    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. 檢測(cè)與融合皮膚區(qū)域

實(shí)現(xiàn)了快速邊緣保留后,我們有了兩方面的提升,第一個(gè)是算法時(shí)間上面的提升,第二個(gè)是效果上面的提升,臉上的水滴效果還在,眼睛區(qū)域基本沒(méi)有變化,圖片看上去比較真實(shí)。但我們發(fā)現(xiàn)效果還不是很好,如脖子上面的頭發(fā)與原圖相比有些模糊,因此我們打算只對(duì)皮膚區(qū)域?qū)崿F(xiàn)美顏,其他區(qū)域采用其他算法。那我們?cè)趺慈ヅ袛嗥つw區(qū)域呢?最簡(jiǎn)單的一種方式就是根據(jù) RGB 或者 YCrCb 的值來(lái)篩選,然后根據(jù)皮膚區(qū)域來(lái)進(jìn)行融合。

皮膚區(qū)域檢測(cè)
// 皮膚區(qū)域檢測(cè)
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;
            }
        }
    }
}

// 皮膚區(qū)域融合
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 ,通過(guò)指針去獲取, 指針 -> 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. 最后總結(jié)

如果我們對(duì)處理效果依舊不是很滿意的話,我們可以自己再做一些折騰,像邊緣加強(qiáng)或者模糊疊加等等。

// 邊緣的提升 (可有可無(wú))
Mat cannyMask;
Canny(src, cannyMask, 150, 300, 3, false);
imshow("Canny", cannyMask);
// & 運(yùn)算  0 ,255 
bitwise_and(src, src, fuseDst, cannyMask);
imshow("bitwise_and", fuseDst);
// 稍微提升一下對(duì)比度(亮度)
add(fuseDst, Scalar(10, 10, 10), fuseDst);

最后總結(jié)一下:無(wú)論我們?cè)趺刺幚硪WC兩個(gè)方面,第一個(gè)是速度方面,因?yàn)槿绻傻揭苿?dòng)端手機(jī)上必須得考慮實(shí)時(shí)性,第二個(gè)是效果方面,要讓用戶看上去自然,盡量不要讓用戶感知這是處理過(guò)的特效。至于怎么集成到 android 移動(dòng)端,大家感興趣可以自己去試試,我將在后面的直播美顏部分來(lái)為大家進(jìn)行講解。

視頻地址:https://pan.baidu.com/s/1Ax6qunmEbabtVteYaza3VQ
視頻密碼:xzts

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容