在介紹美顏定制之前涡匀,我們先來復(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)如下圖所示:
我們在得到人臉關(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)連起來,可以得到以下的圖像:
然后糯俗,我們逐個將三角形的索引連接起來,得到下面這樣一個索引數(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é)之后算墨,離關(guān)鍵點(diǎn)的為有一定的偏移宵荒。這就是瘦臉大眼等臉型調(diào)節(jié)處理過程净嘀。實(shí)現(xiàn)起來也沒什么難度。
詳細(xì)過程挖藏,可以參考本人的開源項目:
CainCamera