動態(tài)貼紙簡介
動態(tài)貼紙是基于人臉識別SDK的一種應(yīng)用总滩。動態(tài)貼紙最常用的是二維圖像纲堵,也有使用3D 圖像的動態(tài)貼紙,而隨著AR和三維點云技術(shù)的發(fā)展咳秉,目前的AR貼紙也流行了起來婉支。比如抖音鸯隅、快手等短視頻應(yīng)用澜建,或者美顏相機、FaceU激萌等相機類應(yīng)用蝌以。只要涉及圖像音視頻的APP基本上都會涉及炕舵。可見跟畅,動態(tài)貼紙是一種常用的功能咽筋。那么接下來我們來介紹,如何在Android APP中實現(xiàn)動態(tài)貼紙功能徊件,這里僅介紹使用二維圖像構(gòu)建的動態(tài)貼紙奸攻,基于3D圖像和AR技術(shù)構(gòu)建的這兩種動態(tài)貼紙,這里不做介紹虱痕。
動態(tài)貼紙分類
由于動態(tài)貼紙是基于人臉識別SDK構(gòu)建的功能睹耐,那么動態(tài)貼紙又會涉及到人臉的各個器官。對此部翘,我們需要對動態(tài)貼紙進行分類硝训,分類如下:
頭頂、耳朵新思、眼睛窖梁、臉頰、鼻子夹囚、下巴纵刘、脖子、前景等:
頭頂 —— 一般是指頭頂中心荸哟,頭頂中心有可能會放一些帽子之類的貼紙
耳朵 —— 耳朵也放在額頭上方彰导,就跟動漫中娘化動物的耳朵一樣
眼睛 —— 一般用于眨眼等總眼角等地方噴出花朵、貼合眼淚等功能的實現(xiàn)
臉頰 —— 一般會用來處理貼紙的腮紅等功能
鼻子 —— 通常會貼合胡須等
脖子 —— 用來處理圍脖之類的裝飾
前景 —— 一般會用來模擬相框敲茄,就跟2005年前后的中學(xué)流行拍大頭貼那樣
總之位谋,這些是二維圖像構(gòu)建的動態(tài)貼紙的常用的器官。我們知道作用之后堰燎,接下來我們需要對各個器官部分進行實現(xiàn)掏父。
由于貼紙有很多種,這里我們只介紹最簡單的貼紙實現(xiàn)秆剪,還有帶彩妝赊淑、瘦臉等的貼紙這里不介紹爵政。為了方便做成動態(tài)下載,我們需要知道貼紙的參數(shù)陶缺。下面來介紹一下如何實現(xiàn)整個貼紙的功能吧
動態(tài)貼紙的實現(xiàn)
動態(tài)貼紙參數(shù)Json構(gòu)建
貼紙要做成動態(tài)下載的钾挟,我們首先需要知道貼紙的類型、名稱饱岸、寬高掺出、偏移量、相對于人臉的縮放比例苫费、人臉的寬度汤锨、貼紙相對于人臉中心點、貼紙幀數(shù)百框、貼紙一幀渲染的時長闲礼、是否帶音樂、是否循環(huán)铐维、貼紙支持的最大人臉數(shù)等基本參數(shù)柬泽。
我們來構(gòu)建這么一個Json,用來記錄動態(tài)貼紙嫁蛇,各個參數(shù)的意義可以參考下面的注釋:
{
"stickerList": [{
"type": "sticker", // 貼紙類型锨并,sticker表示普通貼紙
"centerIndexList": [43],// 貼紙中心點列表
"offsetX": 0, // 貼紙x軸偏移量
"offsetY": 0.03984, // 貼紙y軸偏移量
"baseScale": 1.7602, // 貼紙縮放倍數(shù)(相對于人臉)
"startIndex": 6, // 人臉起始位置
"endIndex": 26, // 人臉結(jié)束位置,起始位置和結(jié)束位置用于求人臉寬度的
"width": 345, // 貼紙寬度
"height": 251, // 貼紙高度
"frames": 12, // 貼紙幀數(shù)
"action": 0, // 貼紙動作
"stickerName": "face", // 貼紙名稱
"duration": 50, // 貼紙一幀的時間間隔
"stickerLooping": 1, // 是否循環(huán)播放
"audioPath": "", // 音樂路徑
"audioLooping": 1, // 音樂是否循環(huán)播放
"maxcount": 5 // 貼紙最大支持人臉數(shù)
}, {
"type": "frame", // 貼紙類型,frame表示前景
"alignMode":1, // 對齊方式
"width": 360, // 貼紙寬度
"height": 549, // 貼紙高度
"frames": 56, // 貼紙幀數(shù)
"action": 0, // 貼紙動作
"stickerName": "frame", // 貼紙名稱
"duration": 50, // 貼紙一幀的時間間隔
"stickerLooping": 1, // 貼紙是否循環(huán)播放
"audioPath": "", // 音樂路徑
"audioLooping": 1, // 音樂是否循環(huán)播放
"maxcount": 5 // 貼紙支持最大人臉數(shù)
}]
}
有了json棠众,我們接下來就解析json琳疏,代碼如下:
/**
* 讀取默認(rèn)動態(tài)貼紙數(shù)據(jù)
* @param folderPath json文件所在文件夾路徑
* @return
* @throws IOException
* @throws JSONException
*/
public static DynamicSticker decodeStickerData(String folderPath)
throws IOException, JSONException {
File file = new File(folderPath, "json");
String stickerJson = FileUtils.convertToString(new FileInputStream(file));
JSONObject jsonObject = new JSONObject(stickerJson);
DynamicSticker dynamicSticker = new DynamicSticker();
dynamicSticker.unzipPath = folderPath;
if (dynamicSticker.dataList == null) {
dynamicSticker.dataList = new ArrayList<>();
}
JSONArray stickerList = jsonObject.getJSONArray("stickerList");
for (int i = 0; i < stickerList.length(); i++) {
JSONObject jsonData = stickerList.getJSONObject(i);
String type = jsonData.getString("type");
DynamicStickerData data;
if ("sticker".equals(type)) {
data = new DynamicStickerNormalData();
JSONArray centerIndexList = jsonData.getJSONArray("centerIndexList");
((DynamicStickerNormalData) data).centerIndexList = new int[centerIndexList.length()];
for (int j = 0; j < centerIndexList.length(); j++) {
((DynamicStickerNormalData) data).centerIndexList[j] = centerIndexList.getInt(j);
}
((DynamicStickerNormalData) data).offsetX = (float) jsonData.getDouble("offsetX");
((DynamicStickerNormalData) data).offsetY = (float) jsonData.getDouble("offsetY");
((DynamicStickerNormalData) data).baseScale = (float) jsonData.getDouble("baseScale");
((DynamicStickerNormalData) data).startIndex = jsonData.getInt("startIndex");
((DynamicStickerNormalData) data).endIndex = jsonData.getInt("endIndex");
} else {
// 如果不是貼紙又不是前景的話,則直接跳過
if (!"frame".equals(type)) {
continue;
}
data = new DynamicStickerFrameData();
((DynamicStickerFrameData) data).alignMode = jsonData.getInt("alignMode");
}
DynamicStickerData stickerData = data;
stickerData.width = jsonData.getInt("width");
stickerData.height = jsonData.getInt("height");
stickerData.frames = jsonData.getInt("frames");
stickerData.action = jsonData.getInt("action");
stickerData.stickerName = jsonData.getString("stickerName");
stickerData.duration = jsonData.getInt("duration");
stickerData.stickerLooping = (jsonData.getInt("stickerLooping") == 1);
stickerData.audioPath = jsonData.optString("audioPath");
stickerData.audioLooping = (jsonData.optInt("audioLooping", 0) == 1);
stickerData.maxCount = jsonData.optInt("maxCount", 5);
dynamicSticker.dataList.add(stickerData);
}
渲染動態(tài)貼紙
前面一步闸拿,我們構(gòu)建了動態(tài)貼紙的json空盼,解析得到了動態(tài)貼紙的參數(shù)對象,接下來我們就可以構(gòu)建動態(tài)貼紙的渲染過程了新荤。貼紙的渲染過程無非就是逐個人臉揽趾、逐個貼紙渲染而已,并沒有什么難度苛骨。為了支持偽3D效果篱瞎,模擬遠(yuǎn)小近大的貼紙效果。我們需要從人臉關(guān)鍵點SDK中引入姿態(tài)角來計算貼紙痒芝,結(jié)合前面的貼紙參數(shù)對象俐筋,我們需要構(gòu)建一個視椎體并計算出每一幀貼紙的頂點坐標(biāo),計算過程過程如下:
1严衬、構(gòu)建視椎體:
@Override
public void onInputSizeChanged(int width, int height) {
super.onInputSizeChanged(width, height);
mRatio = (float) width / height;
Matrix.frustumM(mProjectionMatrix, 0, -mRatio, mRatio, -1.0f, 1.0f, 3.0f, 9.0f);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 6.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
}
這里構(gòu)建的視椎體加入的長寬比澄者,主要是為了方便后續(xù)的計算,并且視點(0.0, 0.0, 6.0) 到中心點(0.0, 0.0, 0.0)的距離為視點到近平面(0.0,0.0,3.0f)的兩倍,兩倍主要是為了方便后續(xù)的計算粱挡,你也可以設(shè)置成其他倍數(shù)赠幕,甚至正中心不在z軸上,只不過這樣會導(dǎo)致計算變得非常復(fù)雜询筏。
2榕堰、計算貼紙頂點和總變換矩陣
經(jīng)過前面的視椎體構(gòu)建,我們得到了貼紙在三維空間中的假想位置嫌套,接下來我們需要在這基礎(chǔ)上構(gòu)建貼紙的頂點以及根據(jù)人臉關(guān)鍵點SDK給過來的姿態(tài)角做矩陣變換逆屡。頂點坐標(biāo)的計算需要結(jié)合前面的貼紙參數(shù)對象進行計算。整個計算過程如下:
/**
* 更新貼紙頂點
* TODO 待優(yōu)化的點:消除姿態(tài)角誤差灌危、姿態(tài)角給貼紙偏移量造成的誤差
* @param stickerData
*/
private void calculateStickerVertices(DynamicStickerNormalData stickerData, OneFace oneFace) {
if (oneFace == null || oneFace.vertexPoints == null) {
return;
}
// 步驟一康二、計算貼紙的中心點和頂點坐標(biāo)
// 備注:由于frustumM設(shè)置的bottom 和top 為 -1.0 和 1.0碳胳,這里為了方便計算勇蝙,直接用高度作為基準(zhǔn)值來計算
// 1.1、計算貼紙相對于人臉的寬高
float stickerWidth = (float) FacePointsUtils.getDistance(
(oneFace.vertexPoints[stickerData.startIndex * 2] * 0.5f + 0.5f) * mImageWidth,
(oneFace.vertexPoints[stickerData.startIndex * 2 + 1] * 0.5f + 0.5f) * mImageHeight,
(oneFace.vertexPoints[stickerData.endIndex * 2] * 0.5f + 0.5f) * mImageWidth,
(oneFace.vertexPoints[stickerData.endIndex * 2 + 1] * 0.5f + 0.5f) * mImageHeight) * stickerData.baseScale;
float stickerHeight = stickerWidth * (float) stickerData.height / (float) stickerData.width;
// 1.2挨约、根據(jù)貼紙的參數(shù)計算出中心點的坐標(biāo)
float centerX = 0.0f;
float centerY = 0.0f;
for (int i = 0; i < stickerData.centerIndexList.length; i++) {
centerX += (oneFace.vertexPoints[stickerData.centerIndexList[i] * 2] * 0.5f + 0.5f) * mImageWidth;
centerY += (oneFace.vertexPoints[stickerData.centerIndexList[i] * 2 + 1] * 0.5f + 0.5f) * mImageHeight;
}
centerX /= (float) stickerData.centerIndexList.length;
centerY /= (float) stickerData.centerIndexList.length;
centerX = centerX / mImageHeight * ProjectionScale;
centerY = centerY / mImageHeight * ProjectionScale;
// 1.3味混、求出真正的中心點頂點坐標(biāo),這里由于frustumM設(shè)置了長寬比诫惭,因此ndc坐標(biāo)計算時需要變成mRatio:1翁锡,這里需要轉(zhuǎn)換一下
float ndcCenterX = (centerX - mRatio) * ProjectionScale;
float ndcCenterY = (centerY - 1.0f) * ProjectionScale;
// 1.4、貼紙的寬高在ndc坐標(biāo)系中的長度
float ndcStickerWidth = stickerWidth / mImageHeight * ProjectionScale;
float ndcStickerHeight = ndcStickerWidth * (float) stickerData.height / (float) stickerData.width;
// 1.5夕土、根據(jù)貼紙參數(shù)求偏移的ndc坐標(biāo)
float offsetX = (stickerWidth * stickerData.offsetX) / mImageHeight * ProjectionScale;
float offsetY = (stickerHeight * stickerData.offsetY) / mImageHeight * ProjectionScale;
// 1.6馆衔、貼紙帶偏移量的錨點的ndc坐標(biāo),即實際貼紙的中心點在OpenGL的頂點坐標(biāo)系中的位置
float anchorX = ndcCenterX + offsetX * ProjectionScale;
float anchorY = ndcCenterY + offsetY * ProjectionScale;
// 1.7怨绣、根據(jù)前面的錨點角溃,計算出貼紙實際的頂點坐標(biāo)
mStickerVertices[0] = anchorX - ndcStickerWidth; mStickerVertices[1] = anchorY - ndcStickerHeight;
mStickerVertices[2] = anchorX + ndcStickerWidth; mStickerVertices[3] = anchorY - ndcStickerHeight;
mStickerVertices[4] = anchorX - ndcStickerWidth; mStickerVertices[5] = anchorY + ndcStickerHeight;
mStickerVertices[6] = anchorX + ndcStickerWidth; mStickerVertices[7] = anchorY + ndcStickerHeight;
mVertexBuffer.clear();
mVertexBuffer.position(0);
mVertexBuffer.put(mStickerVertices);
// 步驟二、根據(jù)人臉姿態(tài)角計算透視變換的總變換矩陣
// 2.1篮撑、將Z軸平移到貼紙中心點减细,因為貼紙模型矩陣需要做姿態(tài)角變換
// 平移主要是防止貼紙變形
Matrix.setIdentityM(mModelMatrix, 0);
Matrix.translateM(mModelMatrix, 0, ndcCenterX, ndcCenterY, 0);
// 2.2、貼紙姿態(tài)角旋轉(zhuǎn)
// TODO 人臉關(guān)鍵點給回來的pitch角度似乎不太對赢笨?未蝌?SDK給過來的pitch角度值太小了,比如抬頭低頭pitch的實際角度30度了茧妒,SDK返回的結(jié)果才十幾度萧吠,后續(xù)再看看如何優(yōu)化
float pitchAngle = -(float) (oneFace.pitch * 180f / Math.PI);
float yawAngle = (float) (oneFace.yaw * 180f / Math.PI);
float rollAngle = (float) (oneFace.roll * 180f / Math.PI);
// 限定左右扭頭幅度不超過50°,銷毀人臉關(guān)鍵點SDK帶來的偏差
if (Math.abs(yawAngle) > 50) {
yawAngle = (yawAngle / Math.abs(yawAngle)) * 50;
}
// 限定抬頭低頭最大角度桐筏,消除人臉關(guān)鍵點SDK帶來的偏差
if (Math.abs(pitchAngle) > 30) {
pitchAngle = (pitchAngle / Math.abs(pitchAngle)) * 30;
}
// 貼紙姿態(tài)角變換纸型,優(yōu)先z軸變換,消除手機旋轉(zhuǎn)的角度影響,否則會導(dǎo)致扭頭绊袋、抬頭毕匀、低頭時貼紙變形的情況
Matrix.rotateM(mModelMatrix, 0, rollAngle, 0, 0, 1);
Matrix.rotateM(mModelMatrix, 0, yawAngle, 0, 1, 0);
Matrix.rotateM(mModelMatrix, 0, pitchAngle, 1, 0, 0);
// 2.4、將Z軸平移回到原來構(gòu)建的視椎體的位置癌别,即需要將坐標(biāo)z軸平移回到屏幕中心皂岔,此時才是貼紙的實際模型矩陣
Matrix.translateM(mModelMatrix, 0, -ndcCenterX, -ndcCenterY, 0);
// 2.5、計算總變換矩陣展姐。MVPMatrix 的矩陣計算是 MVPMatrix = ProjectionMatrix * ViewMatrix * ModelMatrix
// 備注:矩陣相乘的順序不同得到的結(jié)果是不一樣的躁垛,不同的順序會導(dǎo)致前面計算過程不一致,這點希望大家要注意
Matrix.setIdentityM(mMVPMatrix, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
Matrix.multiplyMM(mMVPMatrix, 0, mMVPMatrix, 0, mModelMatrix, 0);
}
整個shader就很簡單圾笨,如下:
vertex shader 如下:
uniform mat4 uMVPMatrix; // 變換矩陣
attribute vec4 aPosition; // 圖像頂點坐標(biāo)
attribute vec4 aTextureCoord; // 圖像紋理坐標(biāo)
varying vec2 textureCoordinate; // 圖像紋理坐標(biāo)
void main() {
gl_Position = uMVPMatrix * aPosition;
textureCoordinate = aTextureCoord.xy;
}
fragment shader 如下:
precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D inputTexture;
void main() {
gl_FragColor = texture2D(inputTexture, textureCoordinate);
}
經(jīng)過前面計算得到的mMVPMatrix教馆,就是需要傳遞到shader中總變換矩陣。然后inputTexture就是我們需要繪制的貼紙紋理擂达。至此土铺,貼紙的頂點和變換矩陣我們都算出來了,接下來就是逐個渲染了板鬓。這個沒啥好說的悲敷,就是一張一張紋理渲染上去就好。詳細(xì)過程請看項目中的代碼進行理解俭令。
實現(xiàn)的效果如下:
備注:該動態(tài)貼紙是通過asset目錄下的壓縮包資源解壓后后德,再從解壓目錄動態(tài)加載得到的。你只需要提供貼紙抄腔、json的壓縮包資源即可瓢湃。這樣我們就可以通過服務(wù)器下載貼紙的壓縮包,解壓后赫蛇,通過選中即可切換動態(tài)貼紙绵患。
動態(tài)貼紙音樂播放功能
經(jīng)過前面一步,我們實現(xiàn)了動態(tài)貼紙的渲染棍掐,接下來我們實現(xiàn)動態(tài)貼紙的音樂播放功能藏雏。有些動態(tài)貼紙會伴隨著音樂的播放。這個也沒啥好說的作煌,比較簡單掘殴,就是用MediaPlayer播放出來就好。
詳細(xì)實現(xiàn)可以參考本人的開源項目:
CainCamera