在 Android 端實現實時視頻的美顏

作者:聲網Agora 資深軟件開發工程師 戚敏明

如今越來越多的用戶開始對美顏/道具這一功能產生越來越大的需求,尤其是在泛娛樂場景下。而現如今市場上有許多第三方的美顏 SDK 可以供開發者選擇使用,那么這些第三方的美顏 SDK 是否可以與 Agora RTC SDK 進行結合從而實現實時視頻泛娛樂這一應用場景呢?答案當然是肯定的。

本文的目的就是要幫助大家快速了解如何使用。默認情況下,聲網Agora SDK 提供端到端的整體方案,聲網Agora SDK 負責采集音視頻,前處理,然后將數據發送到對端進行渲染,這種運行模式通常能滿足大多數開發者的需求。但如果開發者希望對采集到的數據進行二次處理(比如美顏等),建議通過 setVideoSource(IVideoSource videoSource) 調用自定義視頻數據源來實現。在這種情況下,整個過程的數據流如下圖所示:

  1. 從相機采集視頻數據
  2. 將采集到的數據傳遞給 FaceUnity SDK 進行二次處理,并進行渲染
  3. 將處理過的數據傳遞給 Agora RTC SDK
  4. Agora RTC SDK 將處理過的數據編碼通過 SD-RTN 傳輸到對端,對端進行解碼并渲染

本文將以 Android 平臺代碼 為例子來具體講解如何實現,其他平臺實現參考 FaceUnity

1. 設置 Agora RTC SDK 視頻源為自定義視頻源

// 一個通用的實現 IVideoSource 接口的類如下,本示例程序中未用到該類
public class MyVideoSource implements IVideoSource {
    @Override
    public int getBufferType() {
        // 返回當前幀數據緩沖區的類型,每種類型數據在 Agora RTC SDK 內部會經過不同的處理,所以必須與幀數據的類型保持一致
        // 有三種類型 BufferType.BYTE_ARRAY/BufferType.TEXTURE/BufferType.BYTE_BUFFER
        return BufferType.BYTE_ARRAY;
    }

    @Override
    public boolean onInitialize(IVideoFrameConsumer consumer) {
        // IVideoFrameConsumer 是由 Agora RTC SDK 創建的,在 MyVideoSource 生命周期中注意保存它的引用,因為后續將通過它將數據傳送給SDK
        mConsumer = consumer;
    }

    @Override
    public boolean onStart() {
        mHasStarted = true;
    }

    @Override
    public void onStop() {
        mHasStarted = false;
    }

    @Override
    public void onDispose() {
        // 釋放對 Consumer 的引用
        mConsumer = null;
    }
}

在本示例程序中,使用了 TextureSource 類,該類是 Agora RTC SDK 提供的適用于紋理類型(texture)視頻源的預定義實現。當實例化了該類后,可調用 setVideoSource 接口來設置視頻源,具體用法如下:

mRtcEngine.setVideoSource(mTextureSource);

2. 采集數據

本示例程序中,使用到的自定義視頻源為相機,視頻數據采集在示例程序中完成,具體做法如下:

private void openCamera(final int cameraType) {
    synchronized (mCameraLock) {
        Camera.CameraInfo info = new Camera.CameraInfo();
        ......
        ...... // 省略部分代碼
        mCameraOrientation = CameraUtils.getCameraOrientation(cameraId);
        CameraUtils.setCameraDisplayOrientation(mActivity, cameraId, mCamera); // 根據相機傳感器方向和手機當前方向設置相機預覽方向

        Camera.Parameters parameters = mCamera.getParameters(); 

        CameraUtils.setFocusModes(parameters);
        int[] size = CameraUtils.choosePreviewSize(parameters, mCameraWidth, mCameraHeight); // 選擇最佳預覽尺寸
        ......
        ...... // 省略部分代碼

        mCamera.setParameters(parameters);
    }

    cameraStartPreview();
}

private void cameraStartPreview() {
    ......
    ...... // 省略部分代碼  
    mCamera.setPreviewTexture(mSurfaceTexture = new SurfaceTexture(mCameraTextureId));
    mCamera.startPreview();
}

其中 openCamera 方法主要是對相機做了一些參數配置,例如預覽尺寸,顯示方向,對焦模式等,而 cameraStartPreview 方法則主要調用了 setPreviewTexture 方法來指定相機預覽數據所需要輸出到的 SurfaceTexture。另外本示例程序中還重載了 onPreviewFrame 回調接口,該接口主要用來接收相機返回的預覽數據,其中入參 byte[] data 就是相機所捕捉到的預覽數據,當得到該數據后本例會調用 mGLSurfaceView.requesetRender() 方法來請求繪制圖像。

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    mCameraNV21Byte = data;
    mCamera.addCallbackBuffer(data);
    mGLSurfaceView.requestRender();
}

如此一來,相機的預覽數據就保存在了 mCameraNV21Byte 數組和 mSurfaceTexture 中。

3. 初始化 FaceUnity SDK

在使用 FaceUnity 提供的 SDK 之前,必須進行初始化工作,具體做法如下:

public static void initFURenderer(Context context) {
    try {
        Log.e(TAG, "fu sdk version " + faceunity.fuGetVersion());

        /**
         * fuSetup faceunity 初始化
         * 其中 v3.bundle:人臉識別數據文件,缺少該文件會導致系統初始化失敗;
         *      authpack:用于鑒權證書內存數組。若沒有,請咨詢 support@faceunity.com
         * 首先調用完成后再調用其他FU API
         */
        InputStream v3 = context.getAssets().open(BUNDLE_v3);
        byte[] v3Data = new byte[v3.available()];
        v3.read(v3Data);
        v3.close();
        faceunity.fuSetup(v3Data, null, authpack.A());

        /**
         * 加載優化表情跟蹤功能所需要加載的動畫數據文件 anim_model.bundle;
         * 啟用該功能可以使表情系數及 avatar 驅動表情更加自然,減少異常表情、模型缺陷的出現。該功能對性能的影響較小。
         * 啟用該功能時,通過 fuLoadAnimModel 加載動畫模型數據,加載成功即可啟動。該功能會影響通過 fuGetFaceInfo 獲取的 expression 表情系數,以及通過表情驅動的 avatar 模型。
         * 適用于使用 Animoji 和 avatar 功能的用戶,如果不是,可不加載
         */
        InputStream animModel = context.getAssets().open(BUNDLE_anim_model);
        byte[] animModelData = new byte[animModel.available()];
        animModel.read(animModelData);
        animModel.close();
        faceunity.fuLoadAnimModel(animModelData);

        /**
         * 加載高精度模式的三維張量數據文件 ardata_ex.bundle。
         * 適用于換臉功能,如果沒用該功能可不加載;如果使用了換臉功能,必須加載,否則會報錯
         */
        InputStream ar = context.getAssets().open(BUNDLE_ardata_ex);
        byte[] arDate = new byte[ar.available()];
        ar.read(arDate);
        ar.close();
        faceunity.fuLoadExtendedARData(arDate);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

4. 對采集到的原始數據進行美顏處理

在第 2 步中,我們已經得到了相機的原始數據,那么下面我們就要調用相應的美顏 API 來對該數據進行二次處理,具體做法如下:

@Override
public void onDrawFrame(GL10 gl) {
    try {
        mSurfaceTexture.updateTexImage(); // 強制刷新生成新的紋理圖片
        mSurfaceTexture.getTransformMatrix(mtx);
    } catch (Exception e) {
        return;
    }

    if (mCameraNV21Byte == null) {
        mFullFrameRectTexture2D.drawFrame(mFuTextureId, mtx, mvp);
        return;
    }
    mFuTextureId = mOnCameraRendererStatusListener.onDrawFrame(mCameraNV21Byte, mCameraTextureId, mCameraWidth, mCameraHeight, mtx, mSurfaceTexture.getTimestamp());
    // 用于屏蔽切換調用 SDK 處理數據方法導致的綠屏(切換SDK處理數據方法是用于展示,實際使用中無需切換,故無需調用做這個判斷,直接使用 else 分支繪制即可)
    if (mFuTextureId <= 0) {
        mTextureOES.drawFrame(mCameraTextureId, mtx, mvp);
    } else {
        mFullFrameRectTexture2D.drawFrame(mFuTextureId, mtx, mvp); // 做顯示繪制到界面上
    }

    mFPSUtil.limit();
    mGLSurfaceView.requestRender();

    isDraw = true;
}

此處的 onDrawFrame 方法調用由第 2 步中 mGLSurfaceView.requesetRender() 調用觸發,其中的 mCameraNV21BytemCameraTextureId 就是我們得到的相機原始數據,在 onDrawFrame 中我們進行了 mOnCameraRendererStatusListener.onDrawFrame 的回調,而該回調接口的實現如下:

@Override
public int onDrawFrame(byte[] cameraNV21Byte, int cameraTextureId, int cameraWidth, int cameraHeight, float[] mtx, long timeStamp) {
    int fuTextureId;
    byte[] backImage = new byte[cameraNV21Byte.length];
    fuTextureId = mFURenderer.onDrawFrame(cameraNV21Byte, cameraTextureId,
            cameraWidth, cameraHeight, backImage, cameraWidth, cameraHeight); // FU 美顏操作
    if (mVideoFrameConsumerReady) {
        mIVideoFrameConsumer.consumeByteArrayFrame(backImage,
                MediaIO.PixelFormat.NV21.intValue(), cameraWidth,
                cameraHeight, mCameraOrientation, System.currentTimeMillis()); // 數據傳遞給 Agora RTC SDK
    }
    return fuTextureId;
}

可以看到,該回調接口又調用了 mFURenderer.onDrawFrame 方法,而該方法中主要調用了如下 FaceUnity 的 API 來對原始數據做美顏處理:

int fuTex = faceunity.fuDualInputToTexture(img, tex, flags, w, h, mFrameId++, mItemsArray, readBackW, readBackH, readBackImg);

其中 imgtex 是我們傳入的原始數據,mItemsArray 則是需要用到的美顏效果數組,當該方法返回時,得到的數據便是經過美顏處理的數據,該數據會寫回到我們傳入的 img 數組中,而返回的 fuTex 則是經過美顏處理的新的紋理標識。而相應的美顏效果可以通過如下方法進行調節(均在 faceunity 當中):

 // filter_level 濾鏡強度 范圍 0~1 SDK 默認為 1
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "filter_level", mFilterLevel);
 // filter_name 濾鏡
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "filter_name", mFilterName.filterName());

 // skin_detect 精準美膚 0:關閉 1:開啟 SDK 默認為 0
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "skin_detect", mSkinDetect);
 // heavy_blur 美膚類型 0:清晰美膚 1:朦朧美膚 SDK 默認為 0
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "heavy_blur", mHeavyBlur);
 // blur_level 磨皮 范圍 0~6 SDK 默認為 6
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "blur_level", 6 * mBlurLevel);
 // blur_blend_ratio 磨皮結果和原圖融合率 范圍 0~1 SDK 默認為 1
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "blur_blend_ratio", 1);

 // color_level 美白 范圍 0~1 SDK 默認為 1
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "color_level", mColorLevel);
 // red_level 紅潤 范圍 0~1 SDK 默認為 1
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "red_level", mRedLevel);
 // eye_bright 亮眼 范圍 0~1 SDK 默認為 0
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "eye_bright", mEyeBright);
 // tooth_whiten 美牙 范圍 0~1 SDK 默認為 0
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "tooth_whiten", mToothWhiten);
  // face_shape_level 美型程度 范圍 0~1 SDK 默認為 1
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "face_shape_level", mFaceShapeLevel);
 // face_shape 臉型 0:女神 1:網紅 2:自然 3:默認 4:自定義(新版美型) SDK 默認為 3
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "face_shape", mFaceShape);
 // eye_enlarging 大眼 范圍 0~1 SDK 默認為 0
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "eye_enlarging", mEyeEnlarging);
 // cheek_thinning 瘦臉 范圍 0~1 SDK 默認為 0
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "cheek_thinning", mCheekThinning);
 // intensity_chin 下巴 范圍 0~1 SDK 默認為 0.5 大于   0.5 變大,小于 0.5 變小
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "intensity_chin", mIntensityChin);
 // intensity_forehead 額頭 范圍 0~1 SDK 默認為 0.5   大于 0.5 變大,小于 0.5 變小
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "intensity_forehead", mIntensityForehead);
 // intensity_nose 鼻子 范圍 0~1 SDK 默認為 0
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "intensity_nose", mIntensityNose);
 // intensity_mouth 嘴型 范圍 0~1 SDK 默認為 0.5   大于 0.5 變大,小于 0.5 變小
 faceunity.fuItemSetParam(mItemsArray[ITEM_ARRAYS_FACE_BEAUTY_INDEX], "intensity_mouth", mIntensityMouth);

5. 本地對經過美顏處理的數據進行渲染顯示

如果本地需要對美顏效果進行預覽,則可以對進行過美顏處理的數據進行自渲染,具體做法如下:

mFullFrameRectTexture2D.drawFrame(mFuTextureId, mtx, mvp);

其中 mFuTextureId 便是第 4 步中經過美顏處理返回的新的紋理標識,我們通過調用 mFullFrameRectTexture2D.drawFrame 方法在本地 GLSurfaceView.Renderer 中的 onDrawFrame 方法中進行繪制。

6. 將經過美顏處理的數據發送給對端

當拿到已經經過美顏處理的數據后,下一步要做的就是通過調用 Agora RTC SDK 提供的接口將該數據傳送給對端,具體做法如下:

mIVideoFrameConsumer.consumeByteArrayFrame(backImage,
                    MediaIO.PixelFormat.NV21.intValue(), cameraWidth,
                    cameraHeight, mCameraOrientation,
                    System.currentTimeMillis());

其中 mIVideoFrameConsume 就是我們在第 1 步中保存的 IVideoFrameConsumer 對象,通過調用該對象的 consumeByteArrayFrame 方法,我們就可以將經過美顏處理的數據發送給 Agora RTC SDK,然后通過 SD-RTN 傳到對端,其中的入參 backImage 便是我們在第 4 步中得到的經過美顏處理的數據,MediaIO.PixelFormat.NV21.intValue() 為該視頻數據使用的格式, cameraWidthcameraHeight 為視頻圖像的寬與高,mCameraOrientation 為視頻圖像需要旋轉的角度,System.currentTimeMillis() 為當前單調遞增時間,Agora RTC SDK 以此來判斷每一幀數據的先后順序。

7. 對端對收到的經過美顏處理的數據進行渲染顯示

當對端收到發送過來的經過美顏處理的數據時,我們可以對其進行渲染顯示(這是默認的渲染方式,當然也可以類似于自定義的視頻源去實現自定義渲染,這里就不展開),具體做法如下:

private void setupRemoteView(int uid) {
    SurfaceView surfaceV = RtcEngine.CreateRendererView(getApplicationContext());
    surfaceV.setZOrderOnTop(true);
    surfaceV.setZOrderMediaOverlay(true);
    mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceV, VideoCanvas.RENDER_MODE_FIT, uid));
}

其中 uid 為發送端的用戶標識。

8. 更多參考

9. 問答

我們在 github 有相應的 Demo,大家可以嘗試。如果你在這個過程中遇到任何問題,可以在 RTC 開發者社區的 Agora 版塊 發帖提問,我們的工程師會來解答。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容