相機之使用OpenGL預覽
相機之使用OpenGL拍照
相機之使用OpenGL錄像
相機之為錄像添加音頻
大眼效果
要實現(xiàn)大眼效果,就一定要識別人臉數(shù)據(jù)掠拳,因為需要知道眼睛在哪里癞揉,才能處理眼睛部分的數(shù)據(jù),可以使用Face++的人臉識別庫
獲取到眼睛的位置后碳想,需要定義一個大眼的最大范圍烧董,在片段著色器中,判斷當前位置是否在大眼的最大范圍內(nèi)胧奔,如果在逊移,就需要按照比例,采樣眼睛中的數(shù)據(jù)顯示到該位置
識別人臉數(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
}
}