相机之使用OpenGL预览
相机之使用OpenGL拍照
相机之使用OpenGL录像
相机之为录像添加音频
大眼效果
要实现大眼效果,就一定要识别人脸数据,因为需要知道眼睛在哪里,才能处理眼睛部分的数据,可以使用Face++的人脸识别库
获取到眼睛的位置后,需要定义一个大眼的最大范围,在片段着色器中,判断当前位置是否在大眼的最大范围内,如果在,就需要按照比例,采样眼睛中的数据显示到该位置
识别人脸数据
使用Face++的人脸识别库
获取摄像头的每一帧byte数据
在打开摄像头的时候可以添加一个 ImageReader,并为 ImageReader 设置监听,就可以在onImageAvailable(ImageReader reader) 回调函数中,从参数 reader 获取每一帧的数据了
创建 ImageReader 时会传入一个格式参数 YUV_420_888, YUV_420_888 又分为好几种具体格式,如: YUV420P(I420、YV12) 、YUV420SP(NV12、NV21)。 因此,在 ImageReader 的 onImageAvailable 回调中,需要注意处理,不能直接复制出 planes[i].buffer 的数据,要结合 RowStride 和 PixelStride 来排列每一帧 byte 数据
方法 | 说明 |
---|---|
Image.Plane#getRowStride() | 该分量在图像中连续两行像素的起点之间分量的距离,并不一定等于图像宽度,因为有的设备会在分量数据后面补充 0,但最后一行又不补充 0 ,因此,要做特殊处理 |
Image.Plane#getPixelStride() | 两个该分量数据间隔的距离,如果 pixelStride 为 1,说明该分量是紧密相连的 |
代码
/**
* 将Image类型转换为 YUV 的Byte数组
* @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数组写入数据的偏移值
var offset = 0
for (i in planes.indices) {
val buffer = planes[i].buffer
// 该分量在图像中连续两行像素的起点之间分量的距离
val rowStride = planes[i].rowStride
// 该分量相邻的相同分量数据间隔的距离,如果 pixelStride 为 1,说明该分量是紧密相连的
val pixelStride = planes[i].pixelStride
// 代表该分量占据的宽高,如果是第一个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
// 该分量是紧密相连的,且该分量在图像中连续两行像素的起点之间分量的距离等于图像的宽度,
// 说明是Y分量,直接全部拷贝到data
if (pixelStride == 1 && rowStride == planeWidth) {
buffer.get(data, offset, planeWidth * planeHeight)
offset += planeWidth * planeHeight
} else {
// U或者V分量,需要一行一行的拷贝
val rowData = ByteArray(rowStride)
// 遍历除了最后一行的所有行,因为最后一行在有些设备上不会写满 rowStride 个数据,要做特殊处理
for (row in 0 until planeHeight - 1) {
// 获取这一行,该分量的数据
buffer.get(rowData, 0, rowStride)
// 将获取的分量数据写入data中,获取分量数据时乘以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后,会根据nav_graph自动转到startDestination属性设置的fragment中,这个项目是PermissionsFragment
setContentView(R.layout.activity_main)
// 联⽹授权
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) 回调函数中,检测人脸
setOnImageAvailableListener({
val image: Image = it.acquireNextImage()
ImageUtil.image2ByteArray(image, imageByteArray)
val w = image.width
val h = image.height
image.close()
// 检测人脸数据
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;
// 缩放系数,取值[0,1],0 表示不放大
uniform float scaleRatio;
// 放大圆半径
uniform float radius;
// 左眼中心点
uniform vec2 leftEyeCenter;
// 右眼中心点
uniform vec2 rightEyeCenter;
// 图像宽高比
uniform float aspectRatio;
// circleCenter:放大圆的中心点;textureCoord:原本采样的点;radius:放大圆的半径;scaleRatio:放大强度;exponent:放大指数,一般都是2;aspectRatio:图像宽高比
vec2 scaledCoord(vec2 circleCenter, vec2 textureCoord, float radius, float scaleRatio, float exponent, float aspectRatio){
vec2 scaledCoord = textureCoord;
// 原本的采样点到放大圆中心点距离,x乘以宽高比,是因为宽高可能不同,导致放大区域变成椭圆形
// 当宽高不同时,显示会进行拉伸,“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;
// 利用指数函数,实现放大的平滑过渡
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读取数据
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)
// 传入放大系数
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中的人脸数据是向左侧着的,因此位置信息需要旋转90度
Matrix.multiplyMV(eyeResult, 0, matrix, 0, eye, 0)
// 现在坐标是在归一化坐标系中的值,而OpenGL程序中是在texture2D函数中使用,需要转换为纹理坐标
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
}
}