Android OpenGL開發實踐 - 基於OpenGL ES 2.0的Android相機實時圖片塗鴉實現思路

2021-02-14 騰訊光影研究室

這篇文章將給大家講解如何在Android系統上基於OpenGL ES 2.0來實現相機實時圖片塗鴉效果,所塗內容跟隨人臉出現、消失、移動、旋轉及縮放,在這裡,我們假設您:

在開始講解之前,先簡要介紹一下OpenGL ES 2.0的一些必要的基礎知識,方便對文章的理解。

基礎知識一:OpenGL的坐標系

為方便講解,以下只講解二維的情況,在OpenGL使用中,我們主要會涉及到以下三個坐標系:

屏幕坐標系
屏幕坐標系就是我們手機屏幕的坐標系,以像素為單位,左上角是坐標系原點,即(0,0),x的取值範圍為0~屏幕寬度,y的取值範圍為0~屏幕高度,詳見下圖:

世界坐標系
它是OpenGL內部的繪圖區域的坐標系,x、y的取值範圍都是-1~1,坐標原點在繪圖區域的中心,見下圖,假設綠色區域是一個OpenGL的繪圖區域:

紋理坐標系
就是紋理本身的坐標系,坐標原點在紋理的左上角,s(x)、t(y)的取值範圍都是0~1,見下圖,假設
黃色區域是一個紋理貼圖:

基礎知識二:Shader

Shader就是OpenGL的著色器,分為頂點著色器(Vertex Shader)和片元著色器(Fragment Shader),這兩個著色器都由一段小程序來實現,用OpenGL Shading Language編寫,語法類似C語言,使用時將相應shader程序代碼載入OpenGL即可。

OpenGL在把點繪到屏幕上之前,點會依次經過頂點著色器和片元著色器的處理。頂點著色器是處理頂點的位置、大小、旋轉等操作,比如希望顯示一個經過順時針旋轉90度、並放大1倍的紋理,可以在頂點著色器中編寫相應的代碼;片元著色器主要處理顏色操作,比如希望將一個紋理中某個區域的顏色變成紅色,可以在片元著色器中編寫相應的代碼。

相機實時圖片塗鴉實現思路

下面開始循序漸進地講解塗鴉的實現,首先先來實現一個簡單的框架:在相機預覽的界面的中央畫一個貼圖。

Part1: 一個簡單的框架

先來定義一下Vertex Shader和Fragment Shader,這兩個Shader是必不可少的。

Vertex Shader:

簡要介紹一下這個Vertex Shader的含義,正如前文所說的,Vertex Shader的作用是對頂點進行一些位置、大小、旋轉等變換操作,但在現在這個shader裡,這些都沒有涉及,只是一個最簡單的Shader,各變量及其含義:

a_Position
頂點數據,代表了要畫的每個頂點,注意,這裡的a_Position只是一個點,那麼它如何能代表要畫的每個頂點?這是剛接觸Shader時很容易會產生的疑惑之一,實際上,Shader代碼會被OpenGL反覆調用多次,每畫一個點就會調用一次,a_Position就代表當前要畫的點,反覆不停地調用,a_Position就被賦上了不同頂點的值。

a_TextureCoordinates
紋理坐標數據,用於描述要畫的紋理頂點,在這裡,沒有對它作任何處理,直接賦給了v_TextureCoordinates。

v_TextureCoordinates
用於將Vertex Shader中接受到的紋理頂點數據傳遞到Fragment Shader中,等會兒會看到在Fragment Shader中也有一個名字相同的變量。

gl_Position
最終告訴OpenGL要畫的頂點位置,這裡直接將a_Position賦給了它,不作任何變換。

Fragment Shader:

同樣,如前文所提到的,Fragment Shader主要處理顏色操作,各變量含義:

u_TextureUnit
java層傳遞過來的紋理,例如一張待繪製的圖片

v_TextureCoordinates
這個就是剛才說的Vertex Shader中傳遞過來的,其值就是Vertex Shader中的a_TextureCoordinates

gl_FragColor
最終告訴OpenGL要畫的頂點顏色,這裡texture2D(u_TextureUnit, v_TextureCoordinates)是什麼意思呢?就是取u_TextureUnit紋理中的v_TextureCoordinates點,而v_TextureCoordinates點又是Vertex Shader中傳遞過來的紋理的點,所以相當於是在這個紋理中取對應的點,Fragment Shader和Vertex Shader一樣也是會反覆地調用,這樣gl_FragColor就取到了這個紋理每一個點的顏色,結果就是將這個紋理畫了出來。

java關鍵代碼

首先創建兩個類,CameraView繼承GLSurfaceView並實現SurfaceTexture.OnFrameAvailableListener接口,MyRenderer實現GLSurfaceView.Renderer接口,在CameraView的構造函數裡做一些OpenGL必要的初始化:

值得一提的是setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY),OpenGL可以將渲染設置為每幀都自動渲染或者是你要求它渲染它才渲染,這裡的GLSurfaceView.RENDERMODE_WHEN_DIRTY屬於後者,在onFrameAvailable()回調裡調用GLSurfaceView的requestRender()方法觸發渲染,也就是觸發onDrawFrame()的調用。

下面在MyRenderer類中,我們先將剛才的那兩個Shader給Load進來:

然後在onSurfaceCreated中做一些變量的初始化:

其中IMAGE_POSITION_VERTEX是紋理圖片的位置坐標數組,它的作用是確定要把紋理圖片畫在屏幕的什麼地方,它裡面的坐標值是對應世界坐標系中的坐標值,IMAGE_TEXTURE_VERTEX是紋理圖片本身的頂點坐標數組,它的作用是確定要畫這個紋理圖片的什麼部分,如下圖所示:

IMAGE_POSITION_VERTEX所指定的位置即相當於上圖中「繪製位置」,IMAGE_TEXTURE_VERTEX指定的紋理繪製部分即相當於上圖中的「繪製部分」。

如果想把一個紋理圖片的全部部分畫在屏幕中央,可以將IMAGE_POSITION_VERTEX及IMAGE_TEXTURE_VERTEX取值如下:

這裡注意一點,vertex點的取法是和畫法有關的,這裡採用的畫法是GLES20.GL_TRIANGLE_FAN,如果採用其它畫法,vertex點數組要作相應地調整,否則畫面會錯亂。

然後在onDrawFrame中繪製圖片:

至此,我們有了一個簡單的框架,可以在相機預覽界面繪製一個圖片了。

Part2: 塗鴉畫布

簡介

下面來介紹塗鴉畫布的創建以及將手指在屏幕上觸摸的位置繪製貼圖。
塗鴉畫布是一個獨立於相機預覽幀的繪圖區域,它的作用是可以將已繪製好的塗鴉暫存起來,否則因為相機預覽幀每一幀都是新的,需要把之前繪製過的東西再重新繪製一次,即就算塗鴉結束了,每幀也都需要調用多次OpenGL繪製方法將之前塗鴉的內容繪製到相機預覽幀上,否則在新的幀上就看不見之前塗的內容,示意圖如下:

有了塗鴉畫布後,就可以將塗鴉內容畫到塗鴉畫布上,然後對每一個新的相機預覽幀,直接將整個畫布畫上去,將畫布畫上去只需要調用一次OpenGL繪圖方法:

這裡的畫布實際上就是一個空的texture,創建方法和創建一個普通的texture是一個樣的,即用GLES20.glGenTextures()來創建,然後進行一些初始化等操作:

為什麼需要framebuffer?因為OpenGL默認是渲染到屏幕的,我們往畫布上畫東西並不希望馬上顯示出來,因為畫布還要貼到臉上,之後再顯示出來。

坐標變換

有了塗鴉畫布之後,下一步就是如何將塗鴉的內容畫到畫布上。首先討論坐標系的轉換,引入畫布之後,現在相關的坐標系又多了一個畫布的坐標系,手指在屏幕上觸摸之後,如何讓圖案最終在觸摸的位置畫出來呢?

手指在屏幕上觸摸之後,onTouchEvent()中所得到的坐標是屏幕坐標系中的坐標,而相機有一個預覽寬高的設置,這個寬高可以和屏幕寬高不一樣,比如1080*1920的屏幕,相機的預覽寬高可以設置為720*960,因此第一個坐標系的轉換就是將屏幕坐標系中的觸摸點坐標轉換成與相機預覽寬高相對應的坐標,相機預覽的坐標系原點及x、y軸方向與屏幕坐標系相同:

得到了觸摸點在相機預覽畫面中的坐標之後,下一步是轉換成它在畫布中的坐標,因為畫布是跟隨人臉移動、旋轉及縮放的,因此這一步稍微有一點複雜,這裡畫布貼到人臉上採用的方案是將畫布中心對準人臉的鼻尖位置(鼻尖坐標由人臉檢測SDK得到),示意圖如下:

可能有人會問,從圖中看,屏幕中有些部分超出了畫布,這部分是否能塗上去?是塗不上去的,只能塗在塗鴉畫布上,因此實際使用的時候,會把塗鴉畫布設置成比屏幕大一些,一般可以自己試一下,比如把手機放遠,看看人臉縮小後畫布要設置能多大還能覆蓋屏幕,一般不用設置得太大,因為人臉縮得太小後,人臉通常也識別不出來了,這時候也不用擔心畫布被縮得太小了。

繼續沿用之前的例子,前面是得到了觸摸點在相機預覽畫面中的坐標是(200,400),它如何對應到塗鴉畫面上面呢?這裡的方法是先計算觸摸點相對於人臉鼻尖的位置,因為塗鴉畫布是將畫布中心對準了人臉鼻尖位置,所以再通過算出來的相對位置轉換成塗鴉畫布上的對應位置,以保證它在塗鴉畫布上還是手指觸摸的那個地方。假設畫布的實際尺寸設置為600*600,畫布中心點坐標是(300,300),人臉鼻尖坐標是(360,320)先從簡單的情況看起,假設畫布貼上去之前,沒有進行移動、旋轉和縮放,那麼將是:

以上是一種簡單的情況,那麼如果人臉先旋轉了一下呢?這時畫布也是跟著旋轉了,這時的坐標如何轉換?其實思路很簡單,就是畫的時候,計算點坐標時把它當作還沒轉的情況來計算,算出來後再轉相應的角度就行了:

如何計算點(x,y)的值呢?有個神奇的公式,它可以計算一個點繞某個點逆時針旋轉後的點坐標:

其中x、y是旋轉前的點坐標,x0、y0是繞著旋轉的點坐標,x』、y』是旋轉後的點坐標,α是旋轉角度。

下面來看看,如果人臉縮放了,如何計算正確的坐標,這裡採取的方法是,當第一次把塗鴉畫布貼到人臉上的時候,先記錄人臉的初始寬度,之後的幀裡再用當前人臉的寬度和記錄的初始人臉寬度就行對比,從而得知人臉縮放的比例。人臉寬度的計算要依賴於人臉檢測SDK,只需要用人臉檢測出的人臉兩邊邊的對應點相減就行了:

人臉縮放後,要保持觸摸點轉換成塗鴉畫布上的正確位置,只需要把觸摸點與人臉鼻尖點之間的差值相應地縮放就可以了:

這裡有一點需要注意的是,假設塗鴉畫布的實際尺寸是600*600,它隨人臉進行縮放後,它的實際尺寸仍然是600*600,只不過顯示的時候被縮放了,因此在將觸摸點轉換成塗鴉畫布上的對應點時,仍要按塗鴉畫布是600*600來計算。

另外,還可以給畫布設置一個顯示的縮放比例,這個是什麼意思呢?之前說過,塗鴉畫布在實際使用的時候,會設置成比屏幕大一些,以確保在人臉縮小後,畫布不至於被跟著縮小至比屏幕還小,不然有些地方就塗不上去了,將塗鴉畫布設大,可以把它的實際尺寸設大,也可以是把它進行顯示放大,為什麼需要進行顯示放大?因為如果塗鴉畫布實際尺寸設置得很大,相當於畫布的解析度很高,這樣畫出的東西就比較精細,從而耗時也會增加,而進行顯示放大不會增加塗鴉畫布的實際尺寸,只相當於把一個小的東西在顯示時扯大了,會稍微變模糊一些。因此,可以將塗鴉畫布的實際大小設置得適中一些,再進行適當地顯示放大,來使得畫布不至於被跟著縮小至比屏幕還小,同時又讓畫布的分辨不會過高而增加繪製耗時。

加上了塗鴉畫布顯示縮放比例後,坐標換轉的計算邏輯也要相應地作修改,假設display_scale是設置的畫布顯示縮放比例,沿用之前的例子,如果畫布被放大顯示了,算出的點會有相應的偏移,調整示意圖如下:

現在可以將手指在屏幕上觸摸時在onTouchEvent()回調中所得到的觸摸坐標正確地轉換成塗鴉畫布中的坐標了,那麼如何在對應的坐標點畫塗鴉圖案呢?前面已經講解了一個簡單的繪圖框架,現在實際就是確定一下前文所說的IMAGE_POSITION_VERTEX以及IMAGE_TEXTURE_VERTEX該如何取值。將一個貼圖畫到一個位置上,那麼這張圖的哪個部分對準到這個點上呢?為了解決這個問題,這裡引入一個概念叫「錨點」,所謂錨點就是紋理圖片上用於對準的點,如下圖所示:

實際上,錨點的設置並不是OpenGL本身的功能,不過我們可以對IMAGE_POSITION_VERTEX稍作修改便可以指定自己想要的錨點,例如我們指定錨點為紋理貼圖的中心:

至此,塗鴉畫布的坐標系轉換就講完了

塗鴉畫布的平移、旋轉及縮放

下面這部分講解如何實現塗鴉畫布隨人臉平移、旋轉及縮放,前面提到過,Vertex Shader會對每個要畫的點都調一次,因此對每個點做對應的變換,也就實現了對塗鴉畫布的變換,平移、旋轉及縮放都有對應地矩陣操作可以方便地實現,將這些操作寫在Vertex Shader中對傳進Vertex Shader中的點進行變換就行了。

以下均假設變換前的點為x0、y0,變換後的點為x、y。

平移變換:

其中Δx、Δy分別表示在x、y軸上的平移量。

旋轉變換:

其中θ表示繞原點逆時針旋轉的角度。
tips:如果希望繞某個特定點旋轉,可以先作平移操作,讓特定點在平衡後處於原點的位置,再進行旋轉操作,旋轉結束後再按原路平移回去,如下圖所示:

縮放變換:

其中k1、k2分別表示x、y坐標的縮放比例。

至此,本文已接近尾聲,總結一下幾個關鍵點:

塗鴉畫布的創建,本質上是創建一個空的texture當作畫板

坐標轉換,關係著塗鴉位置是否正確,涉及到多個坐標系的轉換,一旦某步出錯,可能導致最後結果存在很大偏差

Vertext Shader中平移、旋轉及縮放代碼的編寫,本質上是套用變換矩陣

作者簡介:kenneyqin(覃華崢),天天P圖Android工程師

相關焦點

  • 基於OpenGL ES的深度學習框架編寫
    可實時跟PC或伺服器不同,行動裝置上的GPU可不一定有CPU強悍(多線程+neon/vfp),但在需要實時計算的場景(主要是相機預覽和視頻播放),往往都是基於OpenGL渲染環境的。 實時的情況下,深度學習框架的輸入和輸出都在GPU端,使用CPU進行計算往往需要拷貝圖像出來,算好後再傳到GPU端,因此基於GPU實現的深度學習的庫能持平CPU版本的效率就有足夠優勢了。
  • opengl實踐-從零開發遊戲
    作者:愛幹球的RDlearnopengl是一套很棒的opengl教程,深入淺出、有源碼且免費,除了說良心,不知道還能用什麼詞彙來形容這種高水平且善良的公益行為
  • Android相機開發詳解
    本篇來自 Glumes 的投稿,分享了Android相機開發的相關知識,希望對大家有所幫助。其中尺寸指的是:相機顯示預覽幀的尺寸相機拍攝幀的尺寸Android 顯示相機預覽內容的控制項尺寸而方向指的是相機顯示預覽幀的方向相機拍攝幀的方向Android 手機自身的方向在開發中要處理好這三個方向和三個尺寸各自的關係才行,這裡以 Camera 1.0 版本的 API 作為示例
  • 原創 | 學好opengl走遍天下都不怕系列《基礎篇》
    前言最近本來是想認真學習下《opengl es第三版》這本書,無奈內容過於生澀,有點看不下去,偶遇opengl-tutorial.org
  • Android OpenGL ES 從入門到精通系統性學習教程
    開發(15):立方體貼圖(天空盒)OpenGL ES 3.0 開發(16):相機預覽OpenGL ES 3.0 開發(17):相機基礎濾鏡OpenGL ES 3.0 開發(18):相機 LUT 濾鏡OpenGL ES 3.0 開發(19):相機抖音濾鏡OpenGL ES 3.0 開發(20
  • OpenGL ES 學習資源分享
    《OpenGL ES 應用開發實踐指南》這本書比較通俗易懂,直接上手使用 OpenGL ES,可以說是手把手教學了。作為初學者,最重要的是啥?環境配置、Demo 運行呀~~~在 《OpenGL ES 應用開發實踐指南》裡面,跟著書中的章節順序走,每一章都會有代碼示例,也算是一步步引導了。
  • OpenGL glfw學習(一)初識,環境,窗口
    而對於實現其接口的程序庫,則由不同企業或個人來實現,甚至你自己也可以來寫OpenGL接口的具體實現。咱們使用時只要求具體功能,不用關注其底層實現。目前常用的OpenGL庫有GLUT、GLFW、GLEW、GLEE等。
  • Android OpenGl ES 基礎入門知識
    相信大家都聽過大名鼎鼎的 OpenGL,但可能大多數人沒有實踐使用過,本文就來介紹一下 Android OpenGl ES 的基礎入門知識。或許你在工作中不會用到,但為你個人成長著想一下,擴展自己的知識廣度,總歸是有利無弊的,你說對吧?
  • OpenGLES2.0(二)實戰之繪製三角形
    選擇繪製三角形作為OpenGL ES 2.0的第一個實例,是因為前文中提到的,點、線、三角形是OpenGL ES世界的圖形基礎。無論多麼複雜的幾何物體,在OpenGL ES的世界裡都可以用三角形拼成。依照官方文檔中的說明,Android中利用OpenGL ES 2.0繪製三角形的步驟為:在AndroidManifest.xml文件中設置使用的OpenGL ES的版本:<uses-feature android:glEsVersion="0x00020000" android
  • 經驗丨Android開發最佳實踐
    +'compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.不過,請記住, Jsonkson庫比起GSON更大,所以根據你的情況選擇,你可能選擇GSON來避免APP 65k個方法限制。其它選擇: Json-smart and Boon JSON網絡請求,緩存,圖片 執行請求後端伺服器,有幾種交互的解決方案,你應該考慮實現你自己的網絡客戶端。使用 Volley或Retrofit。Volley 同時提供圖片緩存類。
  • Android 相機,音視頻開發入門篇
    這裡寫圖片描述關於EGL的知識內容很多,不想增加本文篇幅,重新寫一篇博客專門介紹EGL,有興趣點這裡Android 自定義相機開發(三) —— 了解下EGL。3.這裡順便說下OpenGl ES繪製相機數據的時候紋理坐標的變換問題,下次如果使用OpenGl 處理相機數據遇到鏡像或者上下顛倒可以對照下圖片上所說的規則:
  • CSharpGL(0)一個易學易用的C#版OpenGL
    你可以:繪製模型你可以用legacy opengl(glVertex)或modern opengl(VBO+Shader)繪製模型。當然這是最基本的功能。CSharpGL提供一個GLCanvas控制項供你進行繪製。使用紋理(貼圖)你可以用legacy opengl(glVertex)或modern opengl(VBO+Shader)為模型貼上貼圖。
  • Android實現導航欄添加消息數目提示功能
    開發中時常會出現信息提醒,新內容提示等等一堆問題。其實就是在各種控制項或者是item上面加「小圓點」。網上一搜一大堆。。。但是感覺說的好多。我們只需要基本功能2333.一、解需求思路在 RadioGroup 的 RadioButton 上面直接加小圓點,對於我來說實現有點困難,因為我下面還有文字。搞不好,文字就擠沒了。
  • Android Jetpack CameraX 庫 Beta 版正式發布!
    CameraX是一個Jetpack支持庫,旨在幫助您簡化相機應用的開發工作。它提供一致且易於使用的API界面,適用於大多數Android設備,並可向後兼容至Android5.0(API級別21)。CameraX的Beta版本正式發布,我們向為此作出貢獻的全體開發者社區成員致謝,這是我們共同努力的結果。
  • 如何基於Flutter和Paddle Lite實現實時目標檢測
    這款引擎允許我們在很多硬體平臺上實現輕量化的高效預測,進行一次預測耗時較短,也不需要太多的計算資源。那麼如果我們想開發一款既能在本地進行預測又能在Android和iOS上面有一致體驗的App的話,Flutter無疑是一個好選擇。
  • 開發總結:Android反編譯方法的總結
    【IT168技術】對於軟體開發人員來說,保護代碼安全也是比較重要的因素之一,不過目前來說Google Android平臺選擇了Java Dalvik VM的方式使其程序很容易破解和被修改,首先APK文件其實就是一個MIME為ZIP的壓縮包,我們修改ZIP後綴名方式可以看到內部的文件結構,類似Sun JavaMe的Jar
  • Android新手入門-Android中文SDK
    tutorial document will lead you through constructing a real Android Application: A notepad which can create, edit and delete notes, and covers many of the basic concepts with practical examples)開發工具
  • Android實現快遞時間軸功能
    前言具體實現1.最終效果如下:
  • Android開發必備的「80」個開源庫
    wiki 周刊https://github.com/bboyfeiyu/android-tech-frontier/wiki值得閱讀的 Android 技術文章https://github.com/bboyfeiyu/Worth-Reading-the-Android-technical-articles整理一些比較好的 Android 開發教程
  • 來開發一個wanandroid快應用吧
    程式設計師還是有必要了解如何開發一個快應用的。快應用官網不過快應用的語法類似於小程序,都是基於前段js來開發的。這裡通過wanandroid開發api來開發wanandroid應用修改manifest.json配置信息{  "package": "cn.codebear.wanandroid",  "name": "wanandroid",  "versionName": "1.0.0",  "versionCode": "1",  "minPlatformVersion