相機之大眼

相機之使用OpenGL預覽
相機之使用OpenGL拍照
相機之使用OpenGL錄像
相機之為錄像添加音頻

大眼效果

要實現(xiàn)大眼效果,就一定要識別人臉數(shù)據(jù)掠拳,因為需要知道眼睛在哪里癞揉,才能處理眼睛部分的數(shù)據(jù),可以使用Face++的人臉識別庫

獲取到眼睛的位置后碳想,需要定義一個大眼的最大范圍烧董,在片段著色器中,判斷當前位置是否在大眼的最大范圍內(nèi)胧奔,如果在逊移,就需要按照比例,采樣眼睛中的數(shù)據(jù)顯示到該位置


大眼示意圖-16407882303691.jpg

識別人臉數(shù)據(jù)

使用Face++的人臉識別庫

獲取攝像頭的每一幀byte數(shù)據(jù)

在打開攝像頭的時候可以添加一個 ImageReader龙填,并為 ImageReader 設置監(jiān)聽胳泉,就可以在onImageAvailable(ImageReader reader) 回調函數(shù)中,從參數(shù) reader 獲取每一幀的數(shù)據(jù)了

創(chuàng)建 ImageReader 時會傳入一個格式參數(shù) YUV_420_888岩遗, YUV_420_888 又分為好幾種具體格式扇商,如: YUV420P(I420、YV12) 宿礁、YUV420SP(NV12案铺、NV21)。 因此梆靖,在 ImageReader 的 onImageAvailable 回調中控汉,需要注意處理,不能直接復制出 planes[i].buffer 的數(shù)據(jù)返吻,要結合 RowStride 和 PixelStride 來排列每一幀 byte 數(shù)據(jù)

方法 說明
Image.Plane#getRowStride() 該分量在圖像中連續(xù)兩行像素的起點之間分量的距離姑子,并不一定等于圖像寬度,因為有的設備會在分量數(shù)據(jù)后面補充 0测僵,但最后一行又不補充 0 街佑,因此,要做特殊處理
Image.Plane#getPixelStride() 兩個該分量數(shù)據(jù)間隔的距離捍靠,如果 pixelStride 為 1沐旨,說明該分量是緊密相連的

代碼

/**
 * 將Image類型轉換為 YUV 的Byte數(shù)組
 * @param image Image
 * @param data ByteArray
 */
private fun image2ByteArray(image: Image, data: ByteArray) {
    val w = image.width
    val h = image.height
    // 會有三個plane,分別對應y榨婆、u希俩、v
    val planes = image.planes
    // 向data數(shù)組寫入數(shù)據(jù)的偏移值
    var offset = 0
    for (i in planes.indices) {
        val buffer = planes[i].buffer
        // 該分量在圖像中連續(xù)兩行像素的起點之間分量的距離
        val rowStride = planes[i].rowStride
        // 該分量相鄰的相同分量數(shù)據(jù)間隔的距離,如果 pixelStride 為 1纲辽,說明該分量是緊密相連的
        val pixelStride = planes[i].pixelStride
        // 代表該分量占據(jù)的寬高颜武,如果是第一個plane,其中存儲的是Y分量拖吼,會全部存儲鳞上,等于圖像寬高
        // 否則就是U或者V分量,4個Y分量共享一個UV分量吊档,寬高減一半
        val planeWidth = if (i == 0) w else w / 2
        val planeHeight = if (i == 0) h else h / 2
        // 該分量是緊密相連的篙议,且該分量在圖像中連續(xù)兩行像素的起點之間分量的距離等于圖像的寬度,
        // 說明是Y分量怠硼,直接全部拷貝到data
        if (pixelStride == 1 && rowStride == planeWidth) {
            buffer.get(data, offset, planeWidth * planeHeight)
            offset += planeWidth * planeHeight
        } else {
            // U或者V分量鬼贱,需要一行一行的拷貝
            val rowData = ByteArray(rowStride)
            // 遍歷除了最后一行的所有行,因為最后一行在有些設備上不會寫滿 rowStride 個數(shù)據(jù)香璃,要做特殊處理
            for (row in 0 until planeHeight - 1) {
                // 獲取這一行这难,該分量的數(shù)據(jù)
                buffer.get(rowData, 0, rowStride)
                // 將獲取的分量數(shù)據(jù)寫入data中,獲取分量數(shù)據(jù)時乘以pixelStride葡秒,是因為U和V分量可能需要交錯排布
                for (col in 0 until planeWidth) {
                    data[offset++] = rowData[col * pixelStride]
                }
            }
            // 最后一行姻乓,特殊處理
            buffer.get(rowData, 0, min(rowStride, buffer.remaining()))
            for (col in 0 until planeWidth) {
                data[offset++] = rowData[col * pixelStride]
            }
        }
    }
}

使用第三方人臉識別

我這里使用的Face++的人臉識別庫

class MainActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "MainActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //無標題、全屏
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
        window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
        //使用了navigation的框架眯牧,設置layout后蹋岩,會根據(jù)nav_graph自動轉到startDestination屬性設置的fragment中,這個項目是PermissionsFragment
        setContentView(R.layout.activity_main)

        // 聯(lián)?授權
        lifecycleScope.launch {
            val hasLicense = FaceUtil.checkLicense(this@MainActivity)
            // 初始化人臉檢測
            if (hasLicense) {
                initFaceDetect(this@MainActivity)
            }
        }
    }

    fun initFaceDetect(context: Context) {
        // 初始化facepp sdk学少,加載模型
        val modelBytes = context.assets.open("megviifacepp_model").readBytes()
        FaceppApi.getInstance().initHandle(modelBytes)

        // 初始化?臉檢測
        var retCode = FaceDetectApi.getInstance().initFaceDetect()
        if (retCode == FaceppApi.MG_RETCODE_OK) {
            //初始化稠密點檢測
            retCode = DLmkDetectApi.getInstance().initDLmkDetect()
        }

        val config = FaceDetectApi.FaceppConfig()
        config.face_confidence_filter = 0.6f
        config.detectionMode = FaceDetectApi.FaceppConfig.DETECTION_MODE_DETECT
        FaceDetectApi.getInstance().faceppConfig = config
    }

    override fun onDestroy() {
        super.onDestroy()
        FaceDetectApi.getInstance().releaseFaceDetect() //釋放?臉檢測
        DLmkDetectApi.getInstance().releaseDlmDetect() //釋放稠密點檢測
        FaceppApi.getInstance().ReleaseHandle() //釋放facepp sdk
    }
}

在 ImageReader.onImageAvailable(ImageReader reader) 回調函數(shù)中剪个,檢測人臉

setOnImageAvailableListener({
    val image: Image = it.acquireNextImage()
    ImageUtil.image2ByteArray(image, imageByteArray)
    val w = image.width
    val h = image.height
    image.close()
    // 檢測人臉數(shù)據(jù)
    val detectFace = FaceUtil.detectFace(imageByteArray, w, h)
    glSurfaceView.queueEvent {
        bigEyeFilter.setFacePosition(detectFace)
    }
}, imageReaderHandler)
fun detectFace(data: ByteArray, width: Int, height: Int): FloatArray {
    if (hasLicense) {

        //            val bitmap = BitmapFactory.decodeResource(context.resources, R.raw.test2)
        //            val imageData = bitmap2BGR(bitmap)
        val facePPImage = FacePPImage.Builder()
        .setData(data)
        .setWidth(width)
        .setHeight(height)
        .setMode(FacePPImage.IMAGE_MODE_NV21)
        .setRotation(FacePPImage.FACE_UP).build()

        try {
            val faces = FaceDetectApi.getInstance().detectFace(facePPImage)
            for (face in faces) {
                FaceDetectApi.getInstance().getRect(face, true) //獲取?臉框
                //獲取?臉關鍵點
                FaceDetectApi.getInstance().getLandmark(face, FaceDetectApi.LMK_84, true)
                for (i in face.points.indices) {
                    var x: Float = face.points[i].x / width * 2 - 1
                    val y: Float = face.points[i].y / height * 2 - 1
                    facePosition[i * 2] = x
                    facePosition[i * 2 + 1] = y
                }
            }
        } catch (e: Exception) {
            Log.d(TAG, "onCreate: ${e.message}")
        }
    }
    return facePosition
}

大眼效果著色器

頂點著色器

attribute vec4 a_Position;
attribute vec2 a_TextureCoord;
varying vec2 v_TextureCoord;

void main() {
    gl_Position=a_Position;
    v_TextureCoord=a_TextureCoord;
}

片段著色器

precision mediump float;

varying vec2 v_TextureCoord;
uniform sampler2D vTexture;
// 縮放系數(shù),取值[0,1]版确,0 表示不放大
uniform float scaleRatio;
// 放大圓半徑
uniform float radius;
// 左眼中心點
uniform vec2 leftEyeCenter;
// 右眼中心點
uniform vec2 rightEyeCenter;
// 圖像寬高比
uniform float aspectRatio;

// circleCenter:放大圓的中心點扣囊;textureCoord:原本采樣的點;radius:放大圓的半徑阀坏;scaleRatio:放大強度如暖;exponent:放大指數(shù),一般都是2忌堂;aspectRatio:圖像寬高比
vec2 scaledCoord(vec2 circleCenter, vec2 textureCoord, float radius, float scaleRatio, float exponent, float aspectRatio){
    vec2 scaledCoord = textureCoord;

    // 原本的采樣點到放大圓中心點距離盒至,x乘以寬高比,是因為寬高可能不同士修,導致放大區(qū)域變成橢圓形
    // 當寬高不同時枷遂,顯示會進行拉伸,“x*aspectRatio” 就代表棋嘲,把未進行拉伸的圖像固定高度為1時酒唉,此點的x值應該為多少
    // 也可以將未進行拉伸的圖像固定寬度定為1,使用 “y/aspectRatio” 計算此點的y值是多少
    float distance = distance(vec2(textureCoord.x * aspectRatio, textureCoord.y), vec2(circleCenter.x * aspectRatio, circleCenter.y));

    // 如果距離小于圓半徑
    if (distance < radius){
        // 原本采樣點到放大圓中心點距離 與 放大圓半徑 的比例
        float distanceRatio = distance / radius;
        // 利用指數(shù)函數(shù)沸移,實現(xiàn)放大的平滑過渡
        distanceRatio = 1.0 - scaleRatio * (1.0 - pow(distanceRatio, exponent));
        // 從放大圓的中心點痪伦,按指定方向移動相應比例侄榴,就得到縮放后應該采樣的坐標了,放大是乘以 distanceRatio网沾,縮小除以 distanceRatio
        scaledCoord = circleCenter + (textureCoord - circleCenter) * distanceRatio;
    }

    // 返回縮放后的采樣坐標
    return scaledCoord;
}

void main(){
    // 處理左眼
    vec2 newCoord = scaledCoord(leftEyeCenter, v_TextureCoord, radius, scaleRatio, 2.0, aspectRatio);
    // 處理右眼
    newCoord = scaledCoord(rightEyeCenter, newCoord, radius, scaleRatio, 2.0, aspectRatio);
    gl_FragColor = texture2D(vTexture, newCoord);
}

封裝著色器程序

class BigEyeFilter(context: Context, width: Int, height: Int) :
    FboFilter(context, R.raw.big_eye_vertex, R.raw.big_eye_frag, width, height) {
    private var bigEyeRatio: Float = 0f
    private lateinit var matrix: FloatArray
    private var facePosition: FloatArray = FloatArray(84 * 2)
    private val leftEyeCenter = GLES20.glGetUniformLocation(mProgram, "leftEyeCenter")
    private val rightEyeCenter = GLES20.glGetUniformLocation(mProgram, "rightEyeCenter")
    private val radius = GLES20.glGetUniformLocation(mProgram, "radius")
    private val scaleRatio = GLES20.glGetUniformLocation(mProgram, "scaleRatio")
    private val aspectRatio = GLES20.glGetUniformLocation(mProgram, "aspectRatio")

    // 左眼中心點索引
    private val leftIndex = 9

    // 右眼中心點索引
    private val rightIndex = 0

    override fun onDrawInFBO(textureId: Int) {

        // 先將textureId的圖像畫到這一個FBO中
        //激活紋理單元0
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        //將textureId紋理綁定到紋理單元0
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
        //將紋理單元0傳給vTexture癞蚕,告訴vTexture采樣器從紋理單元0讀取數(shù)據(jù)
        GLES20.glUniform1i(vTexture, 0)
        //在textureId紋理上畫出圖像
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

        // 右眼中心點
        val rightResult = getEyePos(rightIndex)
        GLES20.glUniform2f(rightEyeCenter, rightResult[0], rightResult[1])
        // 左眼中心點
        val leftResult = getEyePos(leftIndex)
        GLES20.glUniform2f(leftEyeCenter, leftResult[0], leftResult[1])

        // 放大圓的半徑,我定為兩眼中心點距離的四分之一
        var maxR = sqrt(
            pow(leftResult[0] - rightResult[0], 2f) + pow(leftResult[1] - rightResult[1], 2f)
        ) / 4f
        GLES20.glUniform1f(radius, maxR)

        // 傳入放大系數(shù)
        GLES20.glUniform1f(scaleRatio, bigEyeRatio)

        // 傳入寬高比
        GLES20.glUniform1f(aspectRatio, width.toFloat() / height.toFloat())

        //解除綁定
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
    }

    /**
     * 獲取紋理坐標系中辉哥,眼睛中心的位置
     * @param index Int
     * @return FloatArray
     */
    private fun getEyePos(index: Int): FloatArray {
        val eye =
            floatArrayOf(facePosition[index * 2], facePosition[index * 2 + 1], 0f, 1f)
        val eyeResult = FloatArray(4)
        // 因為facePosition中的人臉數(shù)據(jù)是向左側著的桦山,因此位置信息需要旋轉90度
        Matrix.multiplyMV(eyeResult, 0, matrix, 0, eye, 0)
        // 現(xiàn)在坐標是在歸一化坐標系中的值,而OpenGL程序中是在texture2D函數(shù)中使用醋旦,需要轉換為紋理坐標
        eyeResult[0] = (eyeResult[0] + 1f) / 2f
        eyeResult[1] = (eyeResult[1] + 1f) / 2f
        return eyeResult
    }

    /**
     * 更新人臉頂點位置
     * @param facePosition FloatArray
     */
    fun setFacePosition(facePosition: FloatArray) {
        this.facePosition = facePosition
    }

    fun setBigEyeRatio(ratio: Float) {
        bigEyeRatio = ratio
    }

    /**
     * 設置變換矩陣恒水,否則人臉位置是旋轉90的
     * @param matrix FloatArray
     */
    fun setUniforms(matrix: FloatArray) {
        this.matrix = matrix
    }
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市饲齐,隨后出現(xiàn)的幾起案子钉凌,更是在濱河造成了極大的恐慌,老刑警劉巖箩张,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甩骏,死亡現(xiàn)場離奇詭異,居然都是意外死亡先慷,警方通過查閱死者的電腦和手機饮笛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來论熙,“玉大人福青,你說我怎么就攤上這事∨Ч睿” “怎么了无午?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長祝谚。 經(jīng)常有香客問我宪迟,道長,這世上最難降的妖魔是什么交惯? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任次泽,我火速辦了婚禮,結果婚禮上席爽,老公的妹妹穿的比我還像新娘意荤。我一直安慰自己,他們只是感情好只锻,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布玖像。 她就那樣靜靜地躺著,像睡著了一般齐饮。 火紅的嫁衣襯著肌膚如雪捐寥。 梳的紋絲不亂的頭發(fā)上笤昨,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天,我揣著相機與錄音上真,去河邊找鬼咬腋。 笑死,一個胖子當著我的面吹牛睡互,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陵像,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼就珠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了醒颖?” 一聲冷哼從身側響起妻怎,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泞歉,沒想到半個月后逼侦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡腰耙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年榛丢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挺庞。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡晰赞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出选侨,到底是詐尸還是另有隱情掖鱼,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布援制,位于F島的核電站戏挡,受9級特大地震影響,放射性物質發(fā)生泄漏晨仑。R本人自食惡果不足惜褐墅,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寻歧。 院中可真熱鬧掌栅,春花似錦、人聲如沸码泛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽噪珊。三九已至晌缘,卻和暖如春齐莲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背磷箕。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工选酗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人岳枷。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓芒填,卻偏偏與公主長得像,于是被迫代替她去往敵國和親空繁。 傳聞我的和親對象是個殘疾皇子殿衰,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容