《Android 美顏類相機(jī)開發(fā)匯總》第五章 Android OpenGLES 美顏定制實(shí)現(xiàn)

在介紹美顏定制之前涡匀,我們先來復(fù)習(xí)一下OpenGL中圖像繪制原理。OpenGL的圖像繪制陨瘩,是由許許多多三角形構(gòu)成的腕够。OpenGL的繪制離不開三角形的繪制。通常對于不需要對圖像細(xì)節(jié)進(jìn)行處理的時候玫荣,我們一般會使用glDrawArrays方法將整張圖片繪制處理。但如果要對圖像的某一個部分進(jìn)行形變等微調(diào)崇决,這時候通常將圖像劃分為許許多多的三角形。比如MLS算法原理就是通過調(diào)整三角形的頂點(diǎn)位置實(shí)現(xiàn)圖像形變的。將一張圖像劃分為許許多多的三角形之后脸侥,使用glDrawArrays就不夠劃算了,由于glDrawArrays在圖像有多個連續(xù)的三角形構(gòu)成的時候睁枕,會出現(xiàn)許多重復(fù)的邊官边,這里面不僅僅產(chǎn)生比較大的內(nèi)存開銷外遇,也對CPU到GPU傳遞數(shù)據(jù)的帶寬造成一定的影響,對于移動端來說跳仿,內(nèi)存和帶寬都比較受限。這時候菲语,使用glDrawElements是一個比較好的方式。

人臉三角形索引構(gòu)建

本人將結(jié)合美顏類相機(jī)的美型處理用到的技術(shù)山上,詳細(xì)介紹glDrawElements的用法。
我用的是Face++免費(fèi)提供的人臉關(guān)鍵點(diǎn)檢測SDK佩憾,雖然免費(fèi)使用有設(shè)備和次數(shù)限制,但對于驗(yàn)證來說楞黄,足夠了,在此感謝Face++的幫助谅辣。根據(jù)Face++的SDK的文檔婶恼,106個關(guān)鍵點(diǎn)如下圖所示:


106關(guān)鍵點(diǎn)

我們在得到人臉關(guān)鍵點(diǎn)后桑阶,需要對關(guān)鍵點(diǎn)進(jìn)行三角劃分,三角剖分算法通常是Delaunay Triangulation蚣录,關(guān)于Delaunay Triangulation 算法可以參考官方資料。這里不做介紹萎河,這里并不是重點(diǎn)。由于人臉關(guān)鍵點(diǎn)的位置是相對固定的虐杯,人臉關(guān)鍵點(diǎn)內(nèi)部的關(guān)系是固定的,因此在實(shí)時預(yù)覽的時候擎椰,并不需要每次都用Delaunay算法來對圖像進(jìn)行三角剖分,并且對于移動端來說达舒,每次都用Delaunay進(jìn)行三角劃分是不實(shí)際的,不管是CPU負(fù)載還是手機(jī)發(fā)熱等方面都是行不通的巩搏。我們只需要提前建立一個索引,配合人臉檢測SDK得到的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)就可以做三角劃分以及圖像重建工作了贯底。

我們把上圖中122個關(guān)鍵點(diǎn)連起來,可以得到以下的圖像:


122點(diǎn)三角剖分

然后糯俗,我們逐個將三角形的索引連接起來,得到下面這樣一個索引數(shù)組:

// 臉外索引(人臉頂部中心逆時針數(shù)) 44個三角形
110, 114, 111,
111, 114, 115,
115, 111, 32,
32, 115, 116,
116, 32, 31,
31, 116, 30,
30, 116, 29,
29, 116, 28,
28, 116, 27,
27, 116, 26,
26, 116, 25,
25, 116, 117,
117, 25, 24,
24, 117, 23,
23, 117, 22,
22, 117, 21,
21, 117, 20,
20, 117, 19,
19, 117, 118,
118, 19, 18,
18, 118, 17,
17, 118, 16,
16, 118, 15,
15, 118, 14,
14, 118, 13,
13, 118, 119,
119, 13, 12,
12, 119, 11,
11, 119, 10,
10, 119, 9,
9, 119, 8,
8, 119, 7,
7, 119, 120,
120, 7, 6,
6, 120, 5,
5, 120, 4,
4, 120, 3,
3, 120, 2,
2, 120, 1,
1, 120, 0,
0, 120, 121,
121, 0, 109,
109, 121, 114,
114, 109, 110,
// 臉內(nèi)部索引
// 額頭 14個三角形
0, 33, 109,
109, 33, 34,
34, 109, 35,
35, 109, 36,
36, 109, 110,
36, 110, 37,
37, 110, 43,
43, 110, 38,
38, 110, 39,
39, 110, 111,
111, 39, 40,
40, 111, 41,
41, 111, 42,
42, 111, 32,
// 左眉毛  10個三角形
33, 34, 64,
64, 34, 65,
65, 34, 107,
107, 34, 35,
35, 36, 107,
107, 36, 66,
66, 107, 65,
66, 36, 67,
67, 36, 37,
37, 67, 43,
// 右眉毛 10個三角形
43, 38, 68,
68, 38, 39,
39, 68, 69,
39, 40, 108,
39, 108, 69,
69, 108, 70,
70, 108, 41,
41, 108, 40,
41, 70, 71,
71, 41, 42,
// 左眼 21個三角形
0, 33, 52,
33, 52, 64,
52, 64, 53,
64, 53, 65,
65, 53, 72,
65, 72, 66,
66, 72, 54,
66, 54, 67,
54, 67, 55,
67, 55, 78,
67, 78, 43,
52, 53, 57,
53, 72, 74,
53, 74, 57,
74, 57, 73,
72, 54, 104,
72, 104, 74,
74, 104, 73,
73, 104, 56,
104, 56, 54,
54, 56, 55,
// 右眼 21個三角形
68, 43, 79,
68, 79, 58,
68, 58, 59,
68, 59, 69,
69, 59, 75,
69, 75, 70,
70, 75, 60,
70, 60, 71,
71, 60, 61,
71, 61, 42,
42, 61, 32,
61, 60, 62,
60, 75, 77,
60, 77, 62,
77, 62, 76,
75, 77, 105,
77, 105, 76,
105, 76, 63,
105, 63, 59,
105, 59, 75,
59, 63, 58,
// 左臉頰 16個
0, 52, 1,
1, 52, 2,
2, 52, 57,
2, 57, 3,
3, 57, 4,
4, 57, 112,
57, 112, 74,
74, 112, 56,
56, 112, 80,
80, 112, 82,
82, 112, 7,
7, 112, 6,
6, 112, 5,
5, 112, 4,
56, 80, 55,
55, 80, 78,
// 右臉頰 16個
32, 61, 31,
31, 61, 30,
30, 61, 62,
30, 62, 29,
29, 62, 28,
28, 62, 113,
62, 113, 76,
76, 113, 63,
63, 113, 81,
81, 113, 83,
83, 113, 25,
25, 113, 26,
26, 113, 27,
27, 113, 28,
63, 81, 58,
58, 81, 79,
// 鼻子部分 16個
78, 43, 44,
43, 44, 79,
78, 44, 80,
79, 81, 44,
80, 44, 45,
44, 81, 45,
80, 45, 46,
45, 81, 46,
80, 46, 82,
81, 46, 83,
82, 46, 47,
47, 46, 48,
48, 46, 49,
49, 46, 50,
50, 46, 51,
51, 46, 83,
// 鼻子和嘴巴中間三角形 14個
7, 82, 84,
82, 84, 47,
84, 47, 85,
85, 47, 48,
48, 85, 86,
86, 48, 49,
49, 86, 87,
49, 87, 88,
88, 49, 50,
88, 50, 89,
89, 50, 51,
89, 51, 90,
51, 90, 83,
83, 90, 25,
// 上嘴唇部分 10個
84, 85, 96,
96, 85, 97,
97, 85, 86,
86, 97, 98,
86, 98, 87,
87, 98, 88,
88, 98, 99,
88, 99, 89,
89, 99, 100,
89, 100, 90,
// 下嘴唇部分 10個
90, 100, 91,
100, 91, 101,
101, 91, 92,
101, 92, 102,
102, 92, 93,
102, 93, 94,
102, 94, 103,
103, 94, 95,
103, 95, 96,
96, 95, 84,
// 唇間部分 8個
96, 97, 103,
97, 103, 106,
97, 106, 98,
106, 103, 102,
106, 102, 101,
106, 101, 99,
106, 98, 99,
99, 101, 100,
// 嘴巴與下巴之間的部分(關(guān)鍵點(diǎn)7 到25 與嘴巴鼻翼圍起來的區(qū)域) 24個
7, 84, 8,
8, 84, 9,
9, 84, 10,
10, 84, 95,
10, 95, 11,
11, 95, 12,
12, 95, 94,
12, 94, 13,
13, 94, 14,
14, 94, 93,
14, 93, 15,
15, 93, 16,
16, 93, 17,
17, 93, 18,
18, 93, 92,
18, 92, 19,
19, 92, 20,
20, 92, 91,
20, 91, 21,
21, 91, 22,
22, 91, 90,
22, 90, 23,
23, 90, 24,
24, 90, 25

在得到索引數(shù)組之后得湘,我們接下來對圖像進(jìn)行三角剖分和重建工作顿仇。

圖像三角形繪制與圖像重建

上一步淘正,我們根據(jù)圖像得到了三角剖分的索引臼闻,接下來我們需要使用glDrawElements方法將人臉識別得到的關(guān)鍵點(diǎn)的頂點(diǎn)和紋理坐標(biāo)給計算出來。為了方便處理述呐,在106個關(guān)鍵點(diǎn)基礎(chǔ)上,添加眉心思犁、臉頰代虾、額頭以及圖像四周8個頂點(diǎn)坐標(biāo)進(jìn)來激蹲,然后將得到的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)傳遞到Filter中進(jìn)行處理棉磨。然后在繪制前更新頂點(diǎn)坐標(biāo)和紋理左邊緩沖Buffer,將坐標(biāo)傳給shader進(jìn)行處理即可学辱。

美顏定制分類

美型定制分類主要可以分成三大類:

  • 美白、磨皮處理衙傀。主要是磨皮、膚色調(diào)節(jié)處理差油。
  • 利用遮罩紋理進(jìn)行顏色調(diào)節(jié)、顏色映射等處理。這個主要是用來調(diào)節(jié)某個部位的发侵,比如亮眼、美牙刃鳄、消除法令紋(鼻唇溝)盅弛、消除眼袋、調(diào)節(jié)臥蠶等某一部位的調(diào)節(jié)
  • 調(diào)節(jié)像素偏移叔锐。主要是臉部形態(tài)調(diào)節(jié)挪鹏,比如瘦臉大眼愉烙、下巴、嘴型等調(diào)節(jié)處理返顺。

美顏定制分類實(shí)現(xiàn)

一、美白遂鹊、磨皮處理

磨皮

磨皮可以參考本人關(guān)于實(shí)時美顏處理的優(yōu)化文章:
Android OpenGLES 實(shí)時美顏(磨皮)的優(yōu)化
Android OpenGLES 實(shí)時美顏(磨皮)的優(yōu)化(二)

本項目主要基于第二篇文章的處理實(shí)現(xiàn)蔗包,具體的過程這里就不做重復(fù)介紹了秉扑。

膚色(美白)調(diào)節(jié)

膚色調(diào)節(jié)主要通過LUT將皮膚的色彩映射成白色的调限。我們需要限定皮膚的灰度范圍误澳,根據(jù)灰度范圍進(jìn)行映射調(diào)節(jié)即可吨娜。灰度紋理是一個256 x 1大小的問題宦赠,也就是通常說的一階顏色查找表。另外勾扭,由于皮膚的顏色比較單一,我們可以將通用的512 x 512大小的Lookup Table縮放成64x64像素的桅滋,提高映射效率。由于實(shí)現(xiàn)起來比較簡單丐谋,也不知該怎么說比較好煌珊,可以參考本人的開源相機(jī)CainCamera 中的GLImageBeautyComplexionFilter的實(shí)現(xiàn),整個fragment shader 如下:

// 美膚濾鏡
precision mediump float;
varying highp vec2 textureCoordinate;

uniform sampler2D inputTexture; // 圖像texture
uniform sampler2D grayTexture;  // 灰度查找表
uniform sampler2D lookupTexture; // LUT

uniform highp float levelRangeInv; // 范圍
uniform lowp float levelBlack; // 灰度level 
uniform lowp float alpha; // 膚色程度

void main() {
    lowp vec3 textureColor = texture2D(inputTexture, textureCoordinate).rgb;

    textureColor = clamp((textureColor - vec3(levelBlack, levelBlack, levelBlack)) * levelRangeInv, 0.0, 1.0);
    textureColor.r = texture2D(grayTexture, vec2(textureColor.r, 0.5)).r;
    textureColor.g = texture2D(grayTexture, vec2(textureColor.g, 0.5)).g;
    textureColor.b = texture2D(grayTexture, vec2(textureColor.b, 0.5)).b;

    mediump float blueColor = textureColor.b * 15.0;

    mediump vec2 quad1;
    quad1.y = floor(blueColor / 4.0);
    quad1.x = floor(blueColor) - (quad1.y * 4.0);

    mediump vec2 quad2;
    quad2.y = floor(ceil(blueColor) / 4.0);
    quad2.x = ceil(blueColor) - (quad2.y * 4.0);

    highp vec2 texPos1;
    texPos1.x = (quad1.x * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.r);
    texPos1.y = (quad1.y * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.g);

    highp vec2 texPos2;
    texPos2.x = (quad2.x * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.r);
    texPos2.y = (quad2.y * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.g);

    lowp vec4 newColor1 = texture2D(lookupTexture, texPos1);
    lowp vec4 newColor2 = texture2D(lookupTexture, texPos2);

    lowp vec3 newColor = mix(newColor1.rgb, newColor2.rgb, fract(blueColor));

    textureColor = mix(textureColor, newColor, alpha);

    gl_FragColor = vec4(textureColor, 1.0); 
}

二定庵、通過遮罩紋理進(jìn)行顏色變換/映射處理的

亮眼

亮眼的實(shí)現(xiàn)并不算什么難度,一句話概括就是—— 把眼睛白色部分變得更白猪落,黑色部分變得更黑畴博。為了得到顏色差值笨忌,我們需要得到一張比較平滑的圖像俱病,再與原圖比較才能得到明顯的顏色差。因此庶艾,實(shí)現(xiàn)思路如下:
1、我們需要對圖像進(jìn)行高斯模糊颖榜,這里需要不同的kernel進(jìn)行高斯模糊處理,將眼睛部分與輸入原圖的顏色差值更加明顯
2掩完、將得到的高斯模糊圖像與輸入圖像進(jìn)行比較,得到顏色差且蓬,再將顏色差值放大幾倍。最后做線性混合處理即可诈胜。
實(shí)現(xiàn)代碼如下:

// 高斯模糊的圖像顏色值
vec4 blurColor = texture2D(blurTexture, textureCoordinate);
// 統(tǒng)計顏色
vec3 sumColor = vec3(0.0, 0.0, 0.0);
// 將RGB顏色差值放大。突出眼睛明亮部分
sumColor = clamp((sourceColor.rgb - blurColor.rgb) * 6.0, 0.0, 1.0);
sumColor = max(sourceColor.rgb, sumColor);
// 用原圖和最終得到的明亮部分進(jìn)行線性混合處理
color = mix(sourceColor, vec4(sumColor, 1.0), strength);

如果整張圖片做這個處理焦匈,將會是這樣的:


整圖做顏色差放大處理

可以看到顏色差比較明顯昵仅。你可以將6.0改小一點(diǎn)就沒那么明顯過爆的感覺了,這里可以根據(jù)實(shí)際需要進(jìn)行調(diào)節(jié)摔笤。
這里有個問題,由于是整圖處理的吕世,所以圖像其他地方也出現(xiàn)了明顯的顏色差變化,我們不需要除了眼睛外的其他地方顏色差發(fā)生變化,因此我們需要添加眼睛部分的遮罩紋理進(jìn)來做處理晚伙。
眼睛遮罩圖如下:


眼睛遮罩圖

我們將遮罩圖與處理后的圖像進(jìn)行比較,保留白色的眼睛部分漓帚,其他部分則用原始圖像的顏色值即可。

為了實(shí)現(xiàn)遮罩尝抖,我們對遮罩進(jìn)行三角剖分迅皇,遮罩圖的三角剖分如下:


遮罩三角剖分

對應(yīng)人臉關(guān)鍵點(diǎn)三角剖分的地方,如圖所示:


眼睛的三角剖分

我們將眼睛周圍的點(diǎn)標(biāo)記為0 ~ 15登颓。由此可以得到遮罩的紋理坐標(biāo)如下:
    /**
     * 眼睛遮罩紋理坐標(biāo)
     */
    private static final float[] mEyeMaskTextureVertices = new float[] {
            0.102757f, 0.465517f,
            0.175439f, 0.301724f,
            0.370927f, 0.310345f,
            0.446115f, 0.603448f,
            0.353383f, 0.732759f,
            0.197995f, 0.689655f,

            0.566416f, 0.629310f,
            0.659148f, 0.336207f,
            0.802005f, 0.318966f,
            0.884712f, 0.465517f,
            0.812030f, 0.681034f,
            0.681704f, 0.750023f,

            0.273183f, 0.241379f,
            0.275689f, 0.758620f,

            0.721805f, 0.275862f,
            0.739348f, 0.758621f,
    };

眼睛部分的三角剖分索引如下:

/**
     * 眼睛部分索引
     */
    private static final short[] mEyeIndices = new short[] {
            0, 5, 1,
            1, 5, 12,
            12, 5, 13,
            12, 13, 4,
            12, 4, 2,
            2, 4, 3,

            6, 7, 11,
            7, 11, 14,
            14, 11, 15,
            14, 15, 10,
            14, 10, 8,
            8, 10, 9
    };

同時,我們要取得某個人臉的眼睛的頂點(diǎn)坐標(biāo):

/**
     * 取得亮眼需要的頂點(diǎn)坐標(biāo)
     * @param vertexPoints
     * @param faceIndex
     */
    public synchronized void getBrightEyeVertices(float[] vertexPoints, int faceIndex) {
        if (vertexPoints == null || vertexPoints.length < 32
                || faceIndex >= mFaceArrays.size() || mFaceArrays.get(faceIndex) == null) {
            return;
        }
        // 眼睛邊沿部分 index = 0 ~ 11
        for (int i = 52; i < 64; i++) {
            vertexPoints[(i - 52) * 2] = mFaceArrays.get(faceIndex).vertexPoints[i * 2];
            vertexPoints[(i - 52) * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[i * 2 + 1];
        }

        vertexPoints[12 * 2] = mFaceArrays.get(faceIndex).vertexPoints[72 * 2];
        vertexPoints[12 * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[72 * 2 + 1];

        vertexPoints[13 * 2] = mFaceArrays.get(faceIndex).vertexPoints[73 * 2];
        vertexPoints[13 * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[73 * 2 + 1];

        vertexPoints[14 * 2] = mFaceArrays.get(faceIndex).vertexPoints[75 * 2];
        vertexPoints[14 * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[75 * 2 + 1];

        vertexPoints[15 * 2] = mFaceArrays.get(faceIndex).vertexPoints[76 * 2];
        vertexPoints[15 * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[76 * 2 + 1];

    }

至此咕痛,我們準(zhǔn)備好了需要繪制的遮罩紋理、遮罩紋理坐標(biāo)茉贡、圖像頂點(diǎn)坐標(biāo)、眼睛部分的索引放椰。所有數(shù)據(jù)都準(zhǔn)備好了。我們就可以用這些數(shù)據(jù)進(jìn)行亮眼處理了庄敛。繪制階段科汗,我們只需要將對應(yīng)的遮罩紋理坐標(biāo)傳給shader藻烤,使用glDrawElements繪制三角形即可头滔。
單一亮眼處理的完整shader如下:

// 亮眼處理
precision highp float;

varying vec2 textureCoordinate;
varying vec2 maskCoordinate;

uniform sampler2D inputTexture; // 輸入圖像紋理
uniform sampler2D blurTexture;  // 經(jīng)過高斯模糊處理的圖像紋理
uniform sampler2D maskTexture;  // 眼睛遮罩圖像紋理

uniform float strength;         // 明亮程度
uniform int enableProcess;      // 是否允許亮眼,沒有人臉時不需要亮眼處理

void main()
{
    vec4 sourceColor = texture2D(inputTexture, textureCoordinate);
    vec4 maskColor = texture2D(maskTexture, maskCoordinate);
    vec4 color = sourceColor;
    if (enableProcess == 1) {
        // 如果遮罩紋理存在
        if (maskColor.r > 0.01) {
            // 高斯模糊的圖像顏色值
            vec4 blurColor = texture2D(blurTexture, textureCoordinate);
            // 統(tǒng)計顏色
            vec3 sumColor = vec3(0.0, 0.0, 0.0);
            // 將RGB顏色差值放大兴猩。突出眼睛明亮部分
            sumColor = clamp((sourceColor.rgb - blurColor.rgb) * 3.0, 0.0, 1.0);
            sumColor = max(sourceColor.rgb, sumColor);

            // 用原圖和最終得到的明亮部分進(jìn)行線性混合處理
            color = mix(sourceColor, vec4(sumColor, 1.0), strength * maskColor.r);
        }
    }

    gl_FragColor = color;
}
美牙

跟前面的亮眼處理類似早歇,都需要遮罩只對嘴巴部分進(jìn)行映射處理倾芝。牙齒美白過程則跟膚色調(diào)節(jié)處理一樣箭跳,通過顏色查找表進(jìn)行映射處理。我們首先要得到嘴巴的遮罩紋理谱姓,并對其進(jìn)行三角剖分,入下圖所示:


美牙遮罩三角剖分

對應(yīng)的人臉關(guān)鍵點(diǎn)三角剖分以及索引標(biāo)號0~11:


嘴巴三角剖分

由此路翻,我們可以得到嘴唇遮罩紋理坐標(biāo):
    /**
     * 美牙遮罩紋理坐標(biāo)
     */
    private static final float[] mTeethMaskTextureVertices = new float[] {
            0.154639f, 0.378788f,
            0.295533f, 0.287879f,
            0.398625f, 0.196970f,
            0.512027f, 0.287879f,
            0.611684f, 0.212121f,
            0.728523f, 0.287879f,
            0.872852f, 0.378788f,
            0.742268f, 0.704546f,
            0.639176f, 0.848485f,
            0.522337f, 0.636364f,
            0.398625f, 0.833333f,
            0.240550f, 0.651515f,
    };

美牙所需要的索引:

/**
     * 美牙索引
     */
    private static final short[] mTeethIndices = new short[] {
            0, 11, 1,
            1, 11, 10,
            1, 10, 2,
            2, 10, 3,
            3, 10, 9,
            3, 9, 8,
            3, 8, 4,
            4, 8, 5,
            5, 8, 7,
            5, 7, 6,
    };

以及某個人臉上嘴巴頂點(diǎn)的坐標(biāo):

    /**
     * 取得美牙需要的頂點(diǎn)坐標(biāo)茄靠,嘴巴周圍12個頂點(diǎn)
     * @param vertexPoints
     * @param faceIndex
     */
    public synchronized void getBeautyTeethVertices(float[] vertexPoints, int faceIndex) {
        if (vertexPoints == null || vertexPoints.length < 24
                || faceIndex >= mFaceArrays.size() || mFaceArrays.get(faceIndex) == null) {
            return;
        }
        for (int i = 84; i < 96; i++) {
            vertexPoints[(i - 84) * 2] = mFaceArrays.get(faceIndex).vertexPoints[i * 2];
            vertexPoints[(i - 84) * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[i * 2 + 1];
        }
    }

在得到了遮罩紋理坐標(biāo)、繪制需要的索引账嚎、人臉嘴巴頂點(diǎn)坐標(biāo)后莫瞬,我們就可以做美牙處理了郭蕉。由于牙齒的顏色比較單一,整體都是白偏黃召锈。因此顏色查找表可以用 64 x 64大小。shader代碼如下:

        vec4 maskColor = texture2D(maskTexture, maskCoordinate.xy);
        if (maskColor.r > 0.001) {
            mediump float blueColor = sourceColor.b * 15.0;

            vec2 quad1;
            vec2 quad2;

            quad1.y = floor(floor(blueColor) * 0.25);
            quad1.x = floor(blueColor) - (quad1.y * 4.0);

            quad2.y = floor(ceil(blueColor) * 0.25);
            quad2.x = ceil(blueColor) - (quad2.y * 4.0);

            vec2 texPos1;
            vec2 texPos2;

            texPos1.x = (quad1.x * 0.25) + 0.0078125 + (0.234375 * sourceColor.r);
            texPos1.y = (quad1.y * 0.25) + 0.0078125 + (0.234375 * sourceColor.g);

            texPos2.x = (quad2.x * 0.25) + 0.0078125 + (0.234375 * sourceColor.r);
            texPos2.y = (quad2.y * 0.25) + 0.0078125 + (0.234375 * sourceColor.g);

            lowp vec3 newColor1 = texture2D(teethLookupTexture, texPos1).rgb;
            lowp vec3 newColor2 = texture2D(teethLookupTexture, texPos2).rgb;
            lowp vec3 newColor = mix(newColor1, newColor2, fract(blueColor));

            color = vec4(mix(sourceColor.rgb, newColor, teethStrength * maskColor.r), 1.0);
        }

法令紋理拐袜、臥蠶、眼袋處理這些由于沒有遮罩紋理蹬铺,這里就不再介紹了秉撇。處理思路跟亮眼差不多甜攀,都是找到顏色差之后進(jìn)行顏色差消除琐馆、突出顏色差等處理實(shí)現(xiàn)。
這部分本人將其統(tǒng)稱為人臉美化濾鏡瘦麸,可以參考本項目中的GLImageBeautyFaceFilter的實(shí)現(xiàn)。這里為了方便厉碟,將整個shader集中起來,利用不同的processType進(jìn)行分類箍鼓,shader如下:
vertex_beauty_face.glsl:

attribute vec4 aPosition;       // 圖像頂點(diǎn)坐標(biāo)
attribute vec4 aTextureCoord;   // 遮罩紋理坐標(biāo)勿她,這里是復(fù)用了原來的圖像紋理坐標(biāo)

varying vec2 textureCoordinate;
varying vec2 maskCoordinate;

void main() {
    gl_Position = aPosition;
    maskCoordinate = aTextureCoord.xy;
    // 用頂點(diǎn)坐標(biāo)來處理紋理坐標(biāo)
    textureCoordinate = aPosition.xy * 0.5 + 0.5;
}

fragment_beauty_face.glsl:

// 人臉美化處理
precision highp float;

varying vec2 textureCoordinate;
varying vec2 maskCoordinate;

uniform sampler2D inputTexture;         // 輸入圖像紋理
uniform sampler2D blurTexture;          // 經(jīng)過高斯模糊處理的圖像紋理
uniform sampler2D blurTexture2;         // 經(jīng)過高斯模糊處理的圖像紋理2
uniform sampler2D maskTexture;          // 遮罩圖像紋理
uniform sampler2D teethLookupTexture;   // 美牙的lookup table 紋理

uniform float brightEyeStrength;        // 亮眼程度
uniform float teethStrength;            // 美牙程度
uniform float nasolabialStrength;       // 法令紋處理程度
uniform float furrowStrength;           // 臥蠶處理程度
uniform float eyeBagStrength;           // 眼袋處理程度

uniform int processType;                // 處理類型, 1表示亮眼處理阵翎,2表示美牙處理逢并,3表示消除法令紋郭卫,4表示消除臥蠶眼袋,其他類型則直接繪制原圖

void main()
{
    vec4 sourceColor = texture2D(inputTexture, textureCoordinate);
    vec4 color = sourceColor;
    if (processType == 1) { // 亮眼處理
        // 如果遮罩紋理存在
        vec4 maskColor = texture2D(maskTexture, maskCoordinate);
        if (maskColor.r > 0.01) {
            // 高斯模糊的圖像顏色值
            vec4 blurColor = texture2D(blurTexture2, textureCoordinate);
            // 統(tǒng)計顏色
            vec3 sumColor = vec3(0.0, 0.0, 0.0);
            // 將RGB顏色差值放大贰军。突出眼睛明亮部分
            sumColor = clamp((sourceColor.rgb - blurColor.rgb) * 3.3, 0.0, 1.0);
            sumColor = max(sourceColor.rgb, sumColor);
            // 用原圖和最終得到的明亮部分進(jìn)行線性混合處理
            color = mix(sourceColor, vec4(sumColor, 1.0), brightEyeStrength * maskColor.r);
        }
    } else if (processType == 2) { // 美牙處理
        vec4 maskColor = texture2D(maskTexture, maskCoordinate.xy);
        if (maskColor.r > 0.001) {
            mediump float blueColor = sourceColor.b * 15.0;

            vec2 quad1;
            vec2 quad2;

            quad1.y = floor(floor(blueColor) * 0.25);
            quad1.x = floor(blueColor) - (quad1.y * 4.0);

            quad2.y = floor(ceil(blueColor) * 0.25);
            quad2.x = ceil(blueColor) - (quad2.y * 4.0);

            vec2 texPos1;
            vec2 texPos2;

            texPos1.x = (quad1.x * 0.25) + 0.0078125 + (0.234375 * sourceColor.r);
            texPos1.y = (quad1.y * 0.25) + 0.0078125 + (0.234375 * sourceColor.g);

            texPos2.x = (quad2.x * 0.25) + 0.0078125 + (0.234375 * sourceColor.r);
            texPos2.y = (quad2.y * 0.25) + 0.0078125 + (0.234375 * sourceColor.g);

            lowp vec3 newColor1 = texture2D(teethLookupTexture, texPos1).rgb;
            lowp vec3 newColor2 = texture2D(teethLookupTexture, texPos2).rgb;
            lowp vec3 newColor = mix(newColor1, newColor2, fract(blueColor));

            color = vec4(mix(sourceColor.rgb, newColor, teethStrength * maskColor.r), 1.0);
        }
    } else if (processType == 3) { // 消除法令紋
        vec3 maskColor = texture2D(maskTexture, maskCoordinate.xy).rgb;
        // 去除法令紋原理,用兩張不同程度的高斯模糊圖像差值比較俯树,得到鼻唇溝附近的顏色差值比較,配合法令紋遮罩圖像去除法令紋
        if (maskColor.r > 0.01) {
            vec3 blurColor1 = texture2D(blurTexture, textureCoordinate.xy).rgb;
            vec3 blurColor2 = texture2D(blurTexture2, textureCoordinate.xy).rgb;
            vec3 diffColor = clamp((blurColor2 - blurColor1) * 1.8 + 0.1 * blurColor2, 0.0, 0.5);
            color = vec4(min(sourceColor.rgb + diffColor, 1.0), 1.0) * nasolabialStrength * maskColor.r;
        }
    } else if (processType == 4) {
        vec4 maskColor = texture2D(maskTexture, maskCoordinate.xy);
        if (maskColor.r > 0.005) {  // 消除眼袋许饿,用紅色表示
            vec3 blurColor1 = texture2D(blurTexture, textureCoordinate.xy).rgb;
            vec3 blurColor2 = texture2D(blurTexture2, textureCoordinate.xy).rgb;
            // 放大差值,用輸入的圖像加上差值球化,消除差值所帶來的影響
            vec3 diffColor = clamp((blurColor2 - blurColor1) * 2.0 + 0.05 * blurColor2, 0.0, 0.3);
            color.rgb = mix(color.rgb, min(color.rgb + diffColor, 1.0), eyeBagStrength * maskColor.r);
        } else if (maskColor.g > 0.005) { // 消除臥蠶,藍(lán)色部分為臥蠶遮罩
            color.rgb = mix(color.rgb, pow(color.rgb, vec3(0.5, 0.5, 0.5)), furrowStrength * maskColor.g);
        }
    }
    gl_FragColor = color;
}

亮眼筒愚、美牙的效果如下:


亮眼美牙效果

備注:這有由于遮罩用的是白色的菩浙,導(dǎo)致了嘴唇部分也變白了一點(diǎn),這里你可以改成你需要遮罩紋理素材芍耘。

三、調(diào)節(jié)像素偏移

調(diào)節(jié)像素偏移主要是用來處理瘦臉大眼斋竞、調(diào)節(jié)下巴等處理。前面我們做了圖像三角剖分和重建工作坝初,得到了整張人臉的索引。接下來我們只需要得到對應(yīng)的頂點(diǎn)坐標(biāo)鳄袍、紋理坐標(biāo),更新頂點(diǎn)坐標(biāo)緩沖重罪、紋理坐標(biāo)緩沖即可。這里需要根據(jù)是否有人臉來做頂點(diǎn)判斷剿配。其中updateCartesianVertices()方法是用來傳遞人臉關(guān)鍵點(diǎn)在圖像中的實(shí)際笛卡爾坐標(biāo)的,這里主要是為了方便做像素偏移計算:

if (LandmarkEngine.getInstance().hasFace()) {
            LandmarkEngine.getInstance().updateFaceAdjustPoints(mVertices, mTextureVertices, 0);
            mVertexBuffer.clear();
            mVertexBuffer.put(mVertices);
            mVertexBuffer.position(0);

            mTextureBuffer.clear();
            mTextureBuffer.put(mTextureVertices);
            mTextureBuffer.position(0);

            updateCartesianVertices();

            mIndexBuffer.clear();
            mIndexBuffer.put(FaceImageIndices);
            mIndexBuffer.position(0);
            mIndexLength = mIndexBuffer.capacity();

            setInteger(mEnableReshapeHandle, 1);
        } else { // 沒有人臉時索引變回默認(rèn)的6個
            mIndexBuffer.clear();
            mIndexBuffer.put(TextureRotationUtils.Indices);
            mIndexBuffer.position(0);
            mIndexLength = 6;
            setInteger(mEnableReshapeHandle, 0);
        }

接下來呼胚,我們就可以在fragment shader 中進(jìn)行像素調(diào)節(jié)處理了息裸。關(guān)于像素偏移調(diào)節(jié)沪编,可以參考下面這篇文章,寫得比較詳細(xì):
在OpenGL中利用shader進(jìn)行實(shí)時瘦臉大眼等臉型微調(diào)

臉型調(diào)節(jié)的fragment shader 如下所示(截止本文章位置蚁廓,部分臉型調(diào)節(jié)功能還沒完成):

precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D inputTexture;

// 圖像笛卡爾坐標(biāo)系的關(guān)鍵點(diǎn)厨幻,也就是紋理坐標(biāo)乘以寬高得到
uniform vec2 cartesianPoints[106];

#define INDEX_FACE_LIFT     0   // 瘦臉
#define INDEX_FACE_SHAVE    1   // 削臉
#define INDEX_FACE_NARROW   2   // 小臉
#define INDEX_CHIN          3   // 下巴
#define INDEX_FOREHEAD      4   // 額頭
#define INDEX_EYE_ENLARGE   5   // 大眼
#define INDEX_EYE_DISTANCE  6   // 眼距
#define INDEX_EYE_CORNER    7   // 眼角
#define INDEX_NOSE_THIN     8   // 瘦鼻
#define INDEX_ALAE          9   // 鼻翼
#define INDEX_PROBOSCIS    10   // 長鼻
#define INDEX_MOUTH        11   // 嘴型
#define INDEX_SIZE 12           // 索引大小

// 美型程度參數(shù)列表
uniform float reshapeIntensity[INDEX_SIZE];

// 紋理寬度
uniform int textureWidth;
// 紋理高度
uniform int textureHeight;

// 是否允許美型處理,存在人臉時為1克胳,沒有人臉時為0
uniform int enableReshape;

// 曲線形變處理
vec2 curveWarp(vec2 textureCoord, vec2 originPosition, vec2 targetPosition, float radius)
{
    vec2 offset = vec2(0.0);
    vec2 result = vec2(0.0);

    vec2 direction = targetPosition - originPosition;

    float infect = distance(textureCoord, originPosition)/radius;

    infect = 1.0 - infect;
    infect = clamp(infect, 0.0, 1.0);
    offset = direction * infect;

    result = textureCoord - offset;

    return result;
}

// 大眼處理
vec2 enlargeEye(vec2 currentCoordinate, vec2 circleCenter, float radius, float intensity)
{
    float currentDistance = distance(currentCoordinate, circleCenter);
    float weight = currentDistance / radius;
    weight = 1.0 - intensity * (1.0 - weight * weight);
    weight = clamp(weight, 0.0, 1.0);
    currentCoordinate = circleCenter + (currentCoordinate - circleCenter) * weight;

    return currentCoordinate;
}

// 瘦臉
vec2 faceLift(vec2 currentCoordinate, float faceLength)
{
    vec2 coordinate = currentCoordinate;
    vec2 currentPoint = vec2(0.0);
    vec2 destPoint = vec2(0.0);
    float faceLiftScale = reshapeIntensity[INDEX_FACE_LIFT] * 0.05;
    float radius = faceLength;

    currentPoint = cartesianPoints[3];
    destPoint = currentPoint + (cartesianPoints[44] - currentPoint) * faceLiftScale;
    coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);

    currentPoint = cartesianPoints[29];
    destPoint = currentPoint + (cartesianPoints[44] - currentPoint) * faceLiftScale;
    coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);

    radius = faceLength * 0.8;
    currentPoint = cartesianPoints[10];
    destPoint = currentPoint + (cartesianPoints[46] - currentPoint) * (faceLiftScale * 0.6);
    coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);

    currentPoint = cartesianPoints[22];
    destPoint = currentPoint + (cartesianPoints[46] - currentPoint) * (faceLiftScale * 0.6);
    coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);

    return coordinate;
}

// 削臉
vec2 faceShave(vec2 currentCoordinate, float faceLength)
{
    vec2 coordinate = currentCoordinate;
    vec2 currentPoint = vec2(0.0);
    vec2 destPoint = vec2(0.0);
    float faceShaveScale = reshapeIntensity[INDEX_FACE_SHAVE] * 0.12;
    float radius = faceLength * 1.0;

    // 下巴中心
    vec2 chinCenter = (cartesianPoints[16] + cartesianPoints[93]) * 0.5;
    currentPoint = cartesianPoints[13];
    destPoint = currentPoint + (chinCenter - currentPoint) * faceShaveScale;
    coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);

    currentPoint = cartesianPoints[19];
    destPoint = currentPoint + (chinCenter - currentPoint) * faceShaveScale;
    coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);

    return coordinate;
}

// 處理下巴
vec2 chinChange(vec2 currentCoordinate, float faceLength)
{
    vec2 coordinate = currentCoordinate;
    vec2 currentPoint = vec2(0.0);
    vec2 destPoint = vec2(0.0);
    float chinScale = reshapeIntensity[INDEX_CHIN] * 0.08;
    float radius = faceLength * 1.25;
    currentPoint = cartesianPoints[16];
    destPoint = currentPoint + (cartesianPoints[46] - currentPoint) * chinScale;
    coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);

    return coordinate;
}

void main()
{
    vec2 coordinate = textureCoordinate.xy;
    // 禁用美型處理或者鼻子不在圖像中漠另,則直接繪制
    if (enableReshape == 0 || (cartesianPoints[46].x / float(textureWidth) <= 0.03)
        || (cartesianPoints[46].y / float(textureHeight)) <= 0.03) {
        gl_FragColor = texture2D(inputTexture, coordinate);
        return;
    }

    // 將坐標(biāo)轉(zhuǎn)成圖像大小捏雌,這里是為了方便計算
    coordinate = textureCoordinate * vec2(float(textureWidth), float(textureHeight));

    float eyeDistance = distance(cartesianPoints[74], cartesianPoints[77]); // 兩個瞳孔的距離

    // 瘦臉
    coordinate = faceLift(coordinate, eyeDistance);

    // 削臉
    coordinate = faceShave(coordinate, eyeDistance);

    // 小臉 TODO 眼睛到下巴圖像線性縮小

    // 下巴
    coordinate = chinChange(coordinate, eyeDistance);

    // 額頭

    // 大眼
    float eyeEnlarge = reshapeIntensity[INDEX_EYE_ENLARGE] * 0.12; // 放大倍數(shù)
    if (eyeEnlarge > 0.0) {
        float radius = eyeDistance * 0.33; // 眼睛放大半徑
        coordinate = enlargeEye(coordinate, cartesianPoints[74] + (cartesianPoints[77] - cartesianPoints[74]) * 0.05, radius, eyeEnlarge);
        coordinate = enlargeEye(coordinate, cartesianPoints[77] + (cartesianPoints[74] - cartesianPoints[77]) * 0.05, radius, eyeEnlarge);
    }

    // 眼距

    // 眼角

    // 瘦鼻

    // 鼻翼

    // 長鼻

    // 嘴型

    // 轉(zhuǎn)變回原來的紋理坐標(biāo)系
    coordinate = coordinate / vec2(float(textureWidth), float(textureHeight));
    // 輸出圖像
    gl_FragColor = texture2D(inputTexture, coordinate);
}

具體的實(shí)現(xiàn)性湿,可參考GLImageFaceReshapeFilter的實(shí)現(xiàn)。部分臉型調(diào)節(jié)效果如下:

臉型調(diào)節(jié)

圖中可以看到肤频,調(diào)節(jié)之后算墨,離關(guān)鍵點(diǎn)的為有一定的偏移宵荒。這就是瘦臉大眼等臉型調(diào)節(jié)處理過程净嘀。實(shí)現(xiàn)起來也沒什么難度。
詳細(xì)過程挖藏,可以參考本人的開源項目:
CainCamera

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市膜眠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宵膨,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狐树,死亡現(xiàn)場離奇詭異焙压,居然都是意外死亡抑钟,警方通過查閱死者的電腦和手機(jī)野哭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拨黔,“玉大人,你說我怎么就攤上這事篱蝇。” “怎么了零截?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長涧衙。 經(jīng)常有香客問我,道長雁比,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任偎捎,我火速辦了婚禮,結(jié)果婚禮上鸭限,老公的妹妹穿的比我還像新娘。我一直安慰自己败京,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布赡麦。 她就那樣靜靜地躺著帕识,像睡著了一般泛粹。 火紅的嫁衣襯著肌膚如雪肮疗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天伪货,我揣著相機(jī)與錄音钾怔,去河邊找鬼。 笑死宗侦,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的矾利。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼男旗,長吁一口氣:“原來是場噩夢啊……” “哼欣鳖!你這毒婦竟也來了无午?” 一聲冷哼從身側(cè)響起昔汉,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蛤签,失蹤者是張志新(化名)和其女友劉穎师痕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胰坟,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年竞滓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片商佑。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖茶没,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情抓半,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布笛求,位于F島的核電站,受9級特大地震影響探入,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜新症,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一响禽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芋类,春花似錦、人聲如沸侯繁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至咕别,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惰拱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工偿短, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昔逗。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像勾怒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子控硼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容