用 OpenGL 對視頻幀內容進行替換

在羣裏面有人提到了這麼一個實現:現有一段素材視頻,想要對視頻中的某個內容進行替換,換成本身的圖片,這個怎麼用 OpenGL 去實現呢?java

首先要明確的是,視頻是由一幀一幀圖像組成的,它利用了人眼的視覺暫留效應,一秒內播放足夠幀數的圖片纔會感受到是連續的。git

而想要對視頻的內容進行替換,也就是要將每一幀圖像的內容都進行替換了,通常來講這應該是屬於視頻後期處理了,用專業的 AE (Adobe After Effects)軟件來處理會比較好。github

處理思路

若是用 OpenGL 來處理,有這樣的一個思路:微信

首先經過 MediaCodec 對每一幀圖像內容進行解碼,而後再經過 OpenGL 對當前解碼的一幀圖像進行處理,在原圖像上加一個透明的遮罩層,遮罩層的要求就是對於要替換的內容區域是非透明的,其餘區域透明,將遮罩層和原圖像進行融合,最後獲得的就是一幀被替換過內容圖像了,再將處理過的一幀圖像進行編碼,從新編碼成新的視頻內容。學習

一直重複 解碼 -> 處理 -> 編碼這個過程,直到視頻的每一幀內容都處理完了,就實現了對視頻內容替換。ui

固然這僅僅是個思路,難點在於如何找到合適的遮罩層,若是視頻圖像內容是變更的,要替換的內容不是固定的,那麼對於遮罩層要求更高了,每一幀處理都得有個合適的遮罩。編碼

下面會針對視頻的一幀圖像內容進行處理,如何將一幀的圖像內容替換了。spa

直接效果

效果以下:線程

Sketch 設計圖

代碼實現的效果,左上方的內容被右上方內容替換了,最後成了右下角的圖片。設計

軟件實現圖

準備工做

不會作設計的開發不是好碼農

是時候掏出個人大寶石軟件 Sketch 切個圖了:

準備一張待替換內容:

待替換圖片

而後再切一張同等大小,並把中間圓形位置的圖片替換成想要的圖片,其餘周邊內容設置透明度爲 0 。

帶透明度的遮罩圖

接下來的事情就是將兩張圖片融合,分別介紹基於着色器和顏色混合來替換內容。

這兩個方案都有一個共同點,就是要將帶遮罩的圖片覆蓋在原圖上,不一樣的是如何處理兩個圖片之間的覆蓋,透明度就是一個比較好的切入點。

使用着色器進行替換

在 OpenGL 的渲染管線中,會先構建圖形,而後進行光柵化,光柵化後對每個片元着色,在這個着色過程當中能夠根據須要對片元進行處理,包括拋棄某些片元等,簡單說在 OpenGL 中就是先有形後有色,而在有形有色的過程當中能夠搞點小操做~~

對片元進行處理就是咱們的片元着色器腳本了。

precision mediump float;
varying vec2 vTextureCoord; //接收從頂點着色器過來的參數
uniform sampler2D sTexture;//紋理內容數據
void main() { 
   vec4 bcolor = texture2D(sTexture, vTextureCoord);//給此片元從紋理中採樣出顏色值 
   if(bcolor.a<0.6) {
   		discard;
   } else {
      gl_FragColor=bcolor;
}}
複製代碼

咱們的遮罩圖除了要替換的內容,其餘地方都是透明的,根據採樣出的透明度值小於閾值,就拋棄該片元,直接就不顯示了。

而透明度知足要求的就會顯示,而且在最後映射到視口上時,直接覆蓋了原有的顏色。

經過這種方式就實現了內容替換。

使用着色器進行替換

使用顏色混合進行替換

使用顏色混合的方式不像着色器那樣簡單粗暴,要麼拋棄某些片元,要麼直接覆蓋了。

它是根據必定的計算規則,來計算兩個顏色之間的融合。

在 OpenGL 中使用顏色混合要設置合理的混合因子。

glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        // 繪製
        glDisable(GL_BLEND)
複製代碼

混合因子的設置使得若是遮罩圖是透明的,使用被遮罩圖的顏色,若是不是透明的,使用遮罩圖的顏色,這樣就不是直接拋棄某些片元了。

使用顏色混合進行替換

代碼實現

在具體的代碼實現中,採用了 EGL 來實現離屏的渲染。

在非主線程中,初始化 EGL 環境,而後準備好繪製的必要工做,接着執行繪製,最後把繪製的結果經過 glReadPixels 讀取出來。

Observable.fromCallable {
        // 初始化 EGL 環境
            return@fromCallable initEgl()
        }.map {
        // 設置各類矩陣
            prepare(width, height)
            return@map it
        }.map {
        // 執行繪製
            replaceContent(isBlend)
            return@map it
        }.map {
        // 讀取像素
            val result = readPixel(width, height)
            it.destroy()
            return@map result
        }.subscribeOn(Schedulers.computation())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                // 設置效果
                    mResultImage.setImageBitmap(it)
                }, {
                    showToast("replace failed")
                })
複製代碼

具體的繪製過程比較簡單,若是採用了顏色混合就執行顏色混合的繪製,不然採用着色器的繪製,也體現了就是將遮罩圖直接覆蓋在原圖上的思想。

private fun replaceContent(isBlend: Boolean) {
        glClearColor(1f, 1f, 1f, 1f)
        glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)
        mOriginImage?.drawSelf(mOriginTextureId)
        if (isBlend) {
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
            mReplaceImage?.drawSelf(mReplaceTextureId)
            glDisable(GL_BLEND)
        } else {
            mAlphaTextureRect?.drawSelf(mReplaceTextureId)
        }
    }
複製代碼

在最後讀取像素內容時要注意,glReadPixels 讀取的內容是上下顛倒的,須要將它翻轉過來。

for (i in 0 until height) {
            for (j in 0 until width) {
                pixelMirroredArray[(height - i - 1) * width + j] = pixelArray[i * width + j]
            }
        }
複製代碼

具體的實現能夠參考個人 Github 項目,求一波 Star 。

github.com/glumes/Andr…

後續想法

對於視頻內容替換,這裏僅僅是給出了一幀圖像內容的替換,並且仍是基於透明度的。

看到好萊塢有些電影場景拍攝時,後面都會給出一塊純色的幕布,而後在後期處理時把幕布內容替換成背景,這種替換經過着色器比較顏色的範圍應該也是能夠實現的。

固然了,要是搭配圖像識別來替換內容玩法就更加豐富了。

最後

俗話說:

獨學而無友,則孤陋而寡聞

學習的路上是須要交流和分享的,這裏有個二維碼,若是你對 OpenGL ES 感興趣歡迎一塊兒來交流討論。

加羣討論

要是二維碼過時了,加微信 ezglumes 好友,備註 OpenGL ,拉你入羣~

這裏還有個二維碼,歡迎掃描關注,及時閱讀最新文章內容~~

掃描關注
相關文章
相關標籤/搜索