手 Q 人臉識別動畫實現詳解


歡迎大家前往騰訊雲社區,獲取更多騰訊海量技術實踐干貨哦~

前言

開門見山,先來看下效果吧。

看到這么酷炫的效果圖,不得不贊嘆一下我們的設計師。然而,站在程序員的角度上看,除了酷炫之外更多的是復雜。但是,上面我們所看到的還只是最簡單的一種形態而已。更加復雜的情況是當存在多個人臉的時候進行主次臉動畫的切換,攝像頭移動的時候動畫的追蹤,多個動畫的之間的時序控制等問題,總之,UI展示加上各種業務邏輯使得這個動畫變得異常復雜。今天我們要講解的是剔除業務邏輯之外的單純UI上的實現。

為什么是SurfaceView

選擇一種方案的同時要給出為什么不選擇另一種的理由是什么。沒錯,為什么這里不用自定義Vew來完成繪圖呢?既然自定義View也可以實現一般的動畫效果,為什么還要引入SurfaceView呢?可以把View理解為一個經過系統優化的,可以用來高效執行一些幀數比較低動畫的對象,但是對於靈活性更高的動畫來說,View並不是最好的選擇。同時,對於普通的View它們都是在應用程序的主線程中進行繪制的,我們知道在Android系統上我們不能夠在主線程做一些耗時的操作,否則會引起ANR。對於一些游戲畫面,或者攝像頭預覽、視頻播放來說,它們的UI都比較復雜,而且要求能夠進行高效的繪制,因此,它們的UI就不適合在應用程序的主線程中進行繪制。這時候就必須要給那些需要復雜而高效UI的視圖生成一個獨立的繪圖表面,以及使用一個獨立的線程來繪制這些視圖的UI,所以SurfaceView華麗登場了。

SurfaceView,它擁有獨立的繪圖表面,即它不與其宿主窗口共享同一個繪圖表面。由於擁有獨立的繪圖表面,因此SurfaceView的UI就可以在一個獨立的線程中進行繪制。又由於不會占用主線程資源,SurfaceView一方面可以實現復雜而高效的UI,另一方面又不會導致用戶輸入得不到及時響應。SurfaceView 一般與SurfaceHolder.Callback配合使用,需要重寫以下三個方法:

@Override
public void surfaceCreated(SurfaceHolder holder) {
}    
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}    
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}

 

我們可以通過SurfaceView的getHolder()函數可以獲取SurfaceHolder對象,然后在子線程中通過 mHolder.lockCanvas()來獲得 canvas,繪制完畢之后調用mHolder.unlockCanvasAndPost(canvas)來釋放canvas,並把繪制的內容顯示出來。

你所知道的ObjectAnimator

所謂屬性動畫:改變一切能改變的對象的屬性值。ObjectAnimator內部的工作機制是通過尋找特定屬性的get和set方法,然后通過方法不斷地對值進行改變,從而實現動畫效果的。ObjectAnimator提供了ofInt、ofFloat、ofObject等幾個方法來對屬性進行插值操作,這幾個方法都是設置動畫作用的元素、作用的屬性、動畫開始、結束、以及中間的任意個屬性值。動畫更新的過程中,會不斷調用setPropName更新元素的屬性,所以使用ObjectAnimator更新某個屬性,必須得有setter方法。ObjectAnimator經常配合AnimatorSet進行使用:

if (null == mBCRotateAnimator1) {
    mBCRotateAnimator1 = ObjectAnimator.ofFloat(this, "bCRotate", 0f, 360f);
    mBCRotateAnimator1.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator1.setDuration(866);
}        
if (null == mBCRotateAnimator2) {
    mBCRotateAnimator2 = ObjectAnimator.ofFloat(this, "bCRotate", 360f, 720f);
    mBCRotateAnimator2.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator2.setDuration(334);
}        
if (null == mBCRotateAnimator3) {
    mBCRotateAnimator3 = ObjectAnimator.ofFloat(this, "bCRotate", 720f, 360f);
    mBCRotateAnimator3.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator3.setDuration(12000);
    mBCRotateAnimator3.setRepeatCount(ValueAnimator.INFINITE);
}        
if (null == mBCRotateAnimatorSet) {
    mBCRotateAnimatorSet = new AnimatorSet();
    ArrayList<Animator> animatorArrayList = new ArrayList<Animator>();
    animatorArrayList.add(mBCRotateAnimator1);
    animatorArrayList.add(mBCRotateAnimator2);
    animatorArrayList.add(mBCRotateAnimator3);
    mBCRotateAnimatorSet.playSequentially(animatorArrayList);
    mBCRotateAnimatorSet.setStartDelay(800);
}

 

實用又強大的三角函數

大家有沒有注意到轉圈動畫里面有一對小三角形呢?再細心點你會發現,這是個正三角形,並且其中的一個頂點正對着大圓的圓心,然后隨着大圓一起轉動,有木有?標題也說了,這里所有的元素都是自繪的,所以這兩個三角形它不是設計同學給的icon資源,而是在Canvas上面繪制出來的,那么這里問題就來了:

  • 如何確定三角形所在的位置?(總不能把它畫到圓外面去吧)

  • 如何確定三角形三個頂點的位置?(總不能把它畫歪了吧)

這里先拋一下數學思路,后面會進行更詳細的講解。

//繪制三角形/**
 * 數學問題:已知圓心(a, b),半徑r,和角度m,求圓周上點的坐標?
 * 解:假設圓心的坐標為(a, b),那么圓的方程是(x-a)^2+(y-b)^2=r^2
 * 根據方程可以求出圓上的各點坐標
 * 又已知角度m,則圓上點的坐標分別是(r*cos(m*Math.PI/180)+a, r*sin(m*Math.PI/180)+b)
 *
 * 拓展:已知正三角形中心點的坐標和其外接圓的半徑,求其他三個頂點的坐標?
 * 解:可畫出該三角形的外接圓,然后可把問題轉化為求圓上三個點的坐標,又因為是正三角形,所以每個點的角度已知。解法同上。
 */

 

人臉識別動畫完全解析

所有的動畫元素可以分解為以下幾種,這里我們主要講解第一種——掃描控件,因為這個是難度最大的

先來粗略看下掃描控件的設計稿(這還不是全部的,一共有好幾張,看不清的同學可以放大來看)

轉圈動畫是整個動畫的核心,我們先來看下,其實它是分了好幾層圓圈,圓弧,圓環,圓點,然后組合在一起根據各自不同的方向和速率進行轉動,同時還伴隨着alpha值的淡入淡出以及scale值的放大縮小,還有顏色的改變。so,幾乎平時接觸到的動畫里所有能改變的元素都摻和了進來。(alpha,scale,rotate,color,shadow,speed,direction)

重點來分析下那個大藍圈(暫且叫它為大藍圈):由兩個半圓弧組成,顏色為漸變藍(最后變為白色),中間夾着對稱的兩個三角形,然后在不同的時間段里以不同的速率進行旋轉和縮放。嘿嘿,畫圓弧是件很簡單的事情,調用canvas.drawArc()方法就可以了,圓弧的起始結束角度設計稿都有給了。這里的難點是在圓弧的不同部位繪制出漸變藍色以及陰影效果。對於漸變顏色的填充,我們可以使用API提供的 來實現,參數填入圓心的坐標,顏色數組,顏色的比例。最后調用Paint.setShader()方法即可。

public SweepGradient(float cx, float cy, int colors[], float positions[]) {}
int[] colors = new int[]{Color.argb(255, 0x1E, 0xFF, 0xEC), Color.argb(255, 0x00, 0xBF, 0xFF)}; //藍圈的漸變顏色float[] positions = new float[]{0f, 0.5f}; //漸變顏色占的位置Matrix matrix = new Matrix();
SweepGradient sweepGradient = new SweepGradient(scanningData.centerX, scanningData.centerY, colors, positions);
matrix.setRotate(scanningData.bCRightAngleStart+scanningData.bCRotate - 5, scanningData.centerX, scanningData.centerY);//這里居然需要減5°來矯正?sweepGradient.setLocalMatrix(matrix);
scanningData.mBCPaint.setShader(sweepGradient);

 

陰影效果可以調用API提供的 來實現,參數填入陰影半徑以及Blur type。最后調用Paint.setMaskFilter()方法即可。

public BlurMaskFilter(float radius, Blur style) {}

 

現在來看下大藍圈上的三角形如何繪制出來,我們只分析左上角那個,右下角那個處於對稱位置。以順時針X軸正方向為0°角,那么根據設計稿的初始狀態,可計算出左上角三角形的初始角度是位於225°左右,右下角三角形的初始角度是45°左右。這里我們的最終目標是求出三角形三個頂點的坐標,然后用線條連起來使其成為一個三角形,但是根據這些條件我們直接計算三角形的頂點坐標是做不到的。現在我們來分解下:先根據三角函數求出三角形的中心坐標,然后又由於這個是正三角形,再根據三角函數求出各個頂點的坐標。

由於三角形是在圓周上,假設圓心(a, b),半徑r,和三角形所在的角度m,其實這幾個變量都是知道的,圓心坐標(a,b)則是人臉的中心點,可以通過人臉識別后的矩形坐標返回,半徑r則是設計稿給的初始半徑,角度m就是剛剛我們計算出來的225°,那么根據三角函數可得該三角形的圓心坐標則是 x = rcos(mMath.PI/180)+a, y = rsin(mMath.PI/180)+b。下一步計算三角形三個頂點的坐標。思路還是一樣的:畫出該三角形的外接圓,三角形的中心坐標即是外接圓的圓心,問題可轉化為求外接圓上三個點的坐標,是不是又回到了上面的求解過程?是的。我們已知了這個外接圓的半徑(設計稿給出),圓心坐標,現在要知道的是三角形三個頂點的角度,然后我們就可以分別算出它們的坐標了。我們知道這是個正三角形,而且其中一個頂點指向大圓圈的圓心,暫且把這個頂點命名為P。畫出三角形的外接圓,即頂點P相對於外接圓的位置就是右下角那個三角形相對於大藍圈的位置,因為這兩個三角形是對稱的,頂點是相對的。也就是說頂點P相對於外接圓的角度是45°。如圖:

那么這樣就好辦了,第一個頂點的角度為45°,第二個頂點的角度為45°+120° = 165°,第三個頂點的坐標為165°+120°=285°,再次運用三角函數可求得三個頂點的坐標,然后調用Path.moveTo(),Path.lineTo(),Path.close()等方法把三角形的路徑描述出來,最后調用Canvas.drawPath()把三角形繪制出來,三角形的繪制過程就到此結束了。

//繪制三角形/**
 * 數學問題:已知圓心(a, b),半徑r,和角度m,求圓周上點的坐標?
 * 解:假設圓心的坐標為(a, b),那么圓的方程是(x-a)^2+(y-b)^2=r^2
 * 根據方程可以求出圓上的各點坐標
 * 又已知角度m,則圓上點的坐標分別是(r*cos(m*Math.PI/180)+a, r*sin(m*Math.PI/180)+b)
 *
 * 拓展:已知正三角形中心點的坐標和其外接圓的半徑,求其他三個頂點的坐標?
 * 解:可畫出該三角形的外接圓,然后可把問題轉化為求圓上三個點的坐標,又因為是正三角形,所以每個點的角度已知。解法同上。
 */
scanningData.bTLeftTopX = (float)((scanningData.bCRadius * scanningData.bCScale) * Math.cos((scanningData.bTLeftTopAngle + scanningData.bCRotate) * Math.PI / 180) + scanningData.centerX);
scanningData.bTLeftTopY = (float)((scanningData.bCRadius * scanningData.bCScale) * Math.sin((scanningData.bTLeftTopAngle + scanningData.bCRotate) * Math.PI / 180) + scanningData.centerY);
float tempAngle1 = scanningData.bTRightBottomAngle + scanningData.bCRotate; //取對稱的角度,因為這是在三角形小圓里面的角度,不是大圓的角度
float tempAngle2 = tempAngle1 + 120f; //正三角形,所以加120°
float tempAngle3 = tempAngle2 + 120f;
if (tempAngle1 >= 360) {
    tempAngle1 = tempAngle1 - 360;}  
if (tempAngle2 >= 360) {
    tempAngle2 = tempAngle2 - 360;}  
if (tempAngle3 >= 360) {
    tempAngle3 = tempAngle3 - 360;}  
//問題轉化,求三角形三個頂點的坐標(看上面的數學解析)
float vertex1_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle1 * Math.PI / 180) + scanningData.bTLeftTopX); 
float vertex1_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle1 * Math.PI / 180) + scanningData.bTLeftTopY);  
float vertex2_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle2 * Math.PI / 180) + scanningData.bTLeftTopX);  
float vertex2_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle2 * Math.PI / 180) + scanningData.bTLeftTopY);  
float vertex3_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle3 * Math.PI / 180) + scanningData.bTLeftTopX);  
float vertex3_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle3 * Math.PI / 180) + scanningData.bTLeftTopY);  
Path path = new Path();
path.moveTo(vertex1_x, vertex1_y);
path.lineTo(vertex2_x, vertex2_y);
path.lineTo(vertex3_x, vertex3_y);
path.close();
scanningData.mBTPaint.setColor(scanningData.bTLeftTopColor);  
scanningData.mBTPaint.setAlpha((int) (scanningData.bTAlpha * 255));
canvas.drawPath(path, scanningData.mBTPaint);

 

好了,最難的一塊繪制完了,其他的外圈,內圈,透明圈等等也都問題不大了。嗯,接下來,我們應該讓它動起來。為了讓動畫旋轉起來,我們可以用一個變量rotate運用到ObjectAnimator上面,然后在繪制的時候加上rotate的變化值即可。scale,alpha,等等都一樣,需要在繪制的時候把這些值給加上去。

if (null == mBCScaleAnimator1) {
    mBCScaleAnimator1 = ObjectAnimator.ofFloat(this, "bCScale", 0f, 1.04f);
    mBCScaleAnimator1.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator1.setDuration(200);
}
if (null == mBCScaleAnimator2) {
    mBCScaleAnimator2 = ObjectAnimator.ofFloat(this, "bCScale", 1.04f, 1f);
    mBCScaleAnimator2.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator2.setDuration(66);
}
if (null == mBCScaleAnimator3) {
    mBCScaleAnimator3 = ObjectAnimator.ofFloat(this, "bCScale", 1f, 1.02f);
    mBCScaleAnimator3.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator3.setDuration(66);
}

 

scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle1 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle1 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle2 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle2 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle3 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle3 * Math.PI / 180)

 

人物名稱的繪制以及指示線條以及下面的資料浮窗就不說了,調用Canvas.drawLine(),Canvas.drawText(),Canvas.drawCircle()等等就可以實現(其實線條的位置以及角度也需要運用三角函數進行計算),下面簡單分析下右邊小圖片的波紋效果。

其實這里又有一個需要自繪的三角形,有沒有感覺到崩潰?沒關系,還是按照我們上面的套路,改變下初始角度就可以了,算法在手,三角形我有!其實波紋效果的繪制也比較簡單,調用Canvas.drawCircle(),然后通過ObjectAnimator不斷地去改變圓圈的alpha值和scale值,從而達到波紋的效果。見如下代碼

if (null == mFPOutCircleAlphaAnimator) {
    mFPOutCircleAlphaAnimator = ObjectAnimator.ofFloat(this, "fPOutCircleAlpha", 1f, 0f, 0f);
    mFPOutCircleAlphaAnimator.setInterpolator(new LinearInterpolator());
    mFPOutCircleAlphaAnimator.setDuration(1500);
    mFPOutCircleAlphaAnimator.setRepeatCount(ValueAnimator.INFINITE);
}
if (null == mFPOutCircleAlphaAnimatorSet) {
    mFPOutCircleAlphaAnimatorSet = new AnimatorSet();
    mFPOutCircleAlphaAnimatorSet.play(mFPOutCircleAlphaAnimator);
    mFPOutCircleAlphaAnimatorSet.setStartDelay(2200);
}
if (null == mFPOutCircleScaleAnimator) {
     mFPOutCircleScaleAnimator = ObjectAnimator.ofFloat(this, "fPOutCircleScale", 1f, 1.3f, 1.3f);
     mFPOutCircleScaleAnimator.setInterpolator(new LinearInterpolator());
     mFPOutCircleScaleAnimator.setRepeatCount(ValueAnimator.INFINITE);
     mFPOutCircleScaleAnimator.setDuration(1500);
}
if (null == mFPOutCircleScaleAnimatorSet) {
      mFPOutCircleScaleAnimatorSet = new AnimatorSet();
      mFPOutCircleScaleAnimatorSet.play(mFPOutCircleScaleAnimator);
      mFPOutCircleScaleAnimatorSet.setStartDelay(2200);
}

scanningData.mFPOutCirclePaint.setAlpha((int)(scanningData.fPOutCircleAlpha * 255 * 0.5));
canvas.drawCircle(scanningData.fPCenterX, scanningData.fPCenterY, scanningData.fPOutCircleRadius * scanningData.fPOutCircleScale, scanningData.mFPOutCirclePaint);

 

到此,整個動畫繪制完畢。

相關閱讀

爸爸去哪兒玩轉黑科技:快來測測自己和老爸有多像?

研究 AI 識別同性戀竟受到死亡威脅!論文作者回應如下

人臉對齊:ASM (主動形狀模型)算法

此文已由作者授權騰訊雲技術社區發布,轉載請注明原文出處

原文鏈接:https://cloud.tencent.com/community/article/784358

海量技術實踐經驗,盡在騰訊雲社區! https://cloud.tencent.com/community


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM