https://juejin.cn/post/7035293015757307918
CameraX比较新的版本中,
Preview.setOnPreviewOutputUpdateListener(...)
已经不可用了
一直都想研究一下图像处理,实现一些简单的相机预览及拍照功能。顺便了解一下CameraX的使用。找了一些有关CameraX + OpenGL
实现相机预览的文章,但可能是由于CameraX
的api
变化的缘故,代码无法正常运行,所以参考了这些资料结合自己的研究,写一篇关于CameraX + OpenGL
实现相机预览的文章作为记录和分享。
本文代码所使用的CameraX版本为
1.0.0-rc03
,测试设备为OPPO R15。
有关CameraX
的简单使用介绍,可以参考官方文档或CameraX入门。里面会有比较详细的介绍。由于CameraX配置使用并不复杂,本文对于此部分只是简单贴代码,为后续的对于OpenGL
的扩展作铺垫。
配置一个预览分辨率为640 * 480,后置摄像头的相机预览:
// MainActivity.kt
private fun setUpCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview: Preview = Preview.Builder()
.setTargetResolution(Size(480, 640))
.setTargetRotation(this.display.rotation)
.build()
// 拍照时使用
val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.setTargetRotation(this.display.rotation)
.build()
preview.setSurfaceProvider(previewView)
cameraProvider.unbindAll()
val camera = cameraProvider.bindToLifecycle(
this as LifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
imageCapture,
preview)
// 控制闪光灯、切换摄像头等。。。
val cameraInfo = camera.cameraInfo
val cameraControl = camera.cameraControl
}, ContextCompat.getMainExecutor(this))
}
预览画面与视图的绑定设置发生在:
preview.setSurfaceProvider(previewView)
其中previewView
是Jetpack提供的,即原生帮开发者做了相机预览的封装兼容:
<androidx.camera.view.PreviewView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
当然,这里我们使用SurfaceView/TextureView
自己实现也是可以的,只是需要自己实现Preview.SurfaceProvider
接口进行绑定。
回归到本文的主题:使用OpenGL实现相机预览,也就是利用OpenGL将预览帧渲染出来。那么OpenGL结合CameraX的话,解决问题的关键就是利用上述的绑定关系自行实现Preview.SurfaceProvider
接口来解决了。
init {
setEGLContextClientVersion(2)
setRenderer(cameraRender)
renderMode = RENDERMODE_WHEN_DIRTY
}
先来看看OpenGL的各生命周期,即在渲染器接口GLSurfaceView.Renderer
各个回调内,会干什么
public interface Renderer {
void onSurfaceCreated(GL10 gl, EGLConfig config);
void onSurfaceChanged(GL10 gl, int width, int height);
void onDrawFrame(GL10 gl);
}
由于需要使用OpenGL绘制预览帧,所以在onSurfaceCreated
时需要利用OpenGL的api创建一个SurfaceTexture
,后面会将这个SurfaceTexture
绘制出来。
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
gl?.let {
it.glGenTextures(textures.size, textures, 0)
surfaceTexture = SurfaceTexture(textures[0])
screenFilter = ScreenFilter(context)
}
}
textures[0]
作为为OpenGL Texture
的唯一标识SurfaceTexture
ScreenFilter
作为与OpenGL的GLSL脚本绑定的逻辑,它的初始化时会执行:顶点坐标、纹理坐标的内存空间创建,顶点着色器、片元着色器的程序创建,及GLSL内部的变量映射创建。// ScreenFilter.kt
init {
vertexBuffer = ByteBuffer.allocateDirect(4 * 4 * 2)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
vertexBuffer.clear()
vertexBuffer.put(VERTEX)
textureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
textureBuffer.clear()
textureBuffer.put(TEXTURE)
val vertexShader = OpenGLUtils.readRawTextFile(context, R.raw.camera_vert)
val textureShader = OpenGLUtils.readRawTextFile(context, R.raw.camera_frag)
program = OpenGLUtils.loadProgram(vertexShader, textureShader)
vPosition = GLES20.glGetAttribLocation(program, "vPosition")
vCoord = GLES20.glGetAttribLocation(program, "vCoord")
vTexture = GLES20.glGetUniformLocation(program, "vTexture")
vMatrix = GLES20.glGetUniformLocation(program, "vMatrix")
}
onSurfaceChanged
时,由于宽高的确定,可以开始之前提到的相机预览,以及设置OpenGL的视窗大小。
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
setUpCamera()
gl?.glViewport(0, 0, width, height)
}
onDrawFrame
,顾名思义就是得将当前代表的最新一帧预览帧利用OpenGL绘制出来。
override fun onDrawFrame(gl: GL10?) {
val surfaceTexture = this.surfaceTexture
if (gl == null || surfaceTexture == null) return
gl.glClearColor(0f, 0f, 0f, 0f)
gl.glClear(GLES20.GL_COLOR_BUFFER_BIT)
surfaceTexture.updateTexImage()
screenFilter?.onDrawFrame(textures[0])
}
// ScreenFilter.kt
fun onDrawFrame(textureId: Int): Int {
// 使用着色器程序
GLES20.glUseProgram(program)
// 给着色器程序中传值
// 给顶点坐标数据传值
vertexBuffer.position(0)
GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer)
// 激活
GLES20.glEnableVertexAttribArray(vPosition)
// 给纹理坐标数据传值
textureBuffer.position(0)
GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, textureBuffer)
GLES20.glEnableVertexAttribArray(vCoord)
// 给片元着色器中的 采样器绑定
// 激活图层
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
// 图像数据
GLES20.glBindTexture(GLES11Ext.GL_SAMPLER_EXTERNAL_OES, textureId)
// 传递参数
GLES20.glUniform1i(vTexture, 0)
//参数传递完毕,通知 opengl开始画画
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
// 解绑
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
return textureId
}
surfaceTexture.updateTexImage()
:更新到图像流中的最新一帧。screenFilter?.onDrawFrame(textures[0])
:将参数(包括最新一帧)传递给着色器程序,并通知其绘制。onSurfaceCreated
:SurfaceTexture的创建以及与OpenGL环境的绑定,OpenGL的着色器程序初始化onSurfaceChanged
:相机初始化(ps:这个时机也可以提前),设置OpenGL的视窗大小(ps:宽高也可先保存,后续在绘制时设置视窗大小)onDrawFrame
:刷新SurfaceTexture,并使用OpenGL绘制出来。回到实现Preview.SurfaceProvider
接口,绑定相机预览输出的事情。接下来要做的事情,就是将onSurfaceCreated
时创建的SurfaceTexture
与Preview
绑定,绑定后的SurfaceTexture
能表示实时的预览帧。ps:该情况与使用TextureView
作为相机预览的情况类似。
实现Preview.SurfaceProvider
,并重写onSurfaceRequested
方法:
override fun onSurfaceRequested(request: SurfaceRequest) {
surfaceTexture?.setOnFrameAvailableListener(this)
val surface = Surface(surfaceTexture)
request.provideSurface(surface, executor) {
surface.release()
surfaceTexture?.release()
}
}
其中,surfaceTexture
即为onSurfaceCreated
时创建的SurfaceTexture
。
至此,相机输出的预览画面,经过CameraX
的Preview
与SurfaceTexture
进行了绑定,预览帧的更新会在SurfaceTexture中得到体现。
此处还会设置SurfaceTexture.OnFrameAvailableListener
,作为在新的预览帧刷新时,及时进行OpenGL的绘制。
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
cameraView.requestRender()
}
// 顶点坐标
attribute vec4 vPosition;
// 纹理坐标
attribute vec4 vCoord;
// 传给片元着色器的像素点
varying vec2 aCoord;
void main() {
gl_Position = vPosition;
aCoord = vCoord.xy;
}
#extension GL_OES_EGL_image_external : require
//SurfaceTexture比较特殊
//float数据是什么精度的
precision mediump float;
//采样点的坐标
varying vec2 aCoord;
//采样器
uniform samplerExternalOES vTexture;
void main() {
// 变量 接收像素值
// texture2D:采样器 采集 aCoord的像素
// 赋值给gl_FragColor
/// 正常预览
gl_FragColor = texture2D(vTexture, aCoord);
}
至此,整个预览就完成了,效果为下图。两个比较明显的问题:
画面旋转可以通过改变纹理坐标的角度出发实现,简单的粗暴的做法是通过调整纹理坐标的顺序实现。但比较通用的做法则是通过获取SurfaceTexture的变换矩阵。
在onDrawFrame
时,可以在updateTexImage
后获取变换矩阵。之后在顶点着色器中,将变换矩阵与纹理坐标相乘,再将结果提供给片元着色器使用。
片元着色器:
surfaceTexture.updateTexImage()
surfaceTexture.getTransformMatrix(textureMatrix)
screenFilter?.setTransformMatrix(textureMatrix)
// ScreenFilter#onDrawFrame
GLES20.glUniformMatrix4fv(vMatrix, 1, false, mtx, 0)
顶点着色器:
// 顶点坐标
attribute vec4 vPosition;
// 纹理坐标
attribute vec4 vCoord;
uniform mat4 vMatrix;
// 传给片元着色器的像素点
varying vec2 aCoord;
void main() {
gl_Position = vPosition;
aCoord = (vMatrix * vec4(vCoord.x, vCoord.y, 1.0, 1.0)).xy;
}
官方文档对于Preview.SurfaceProvider
介绍中,示例代码有提到结合OpenGL的使用,其中有修改SurfaceTexture的操作。笔者从AndroidCode Search
中查找到了一个关于CameraX结合OpenGL预览的示例代码。里面有提到需要调用SurfaceTexture#setDefaultBufferSize
,设置尺寸。于是就有:
override fun onSurfaceRequested(request: SurfaceRequest) {
// request.resolution可以为刚开始设置Preview的预览分辨率,640 * 480
val resetTexture = resetPreviewTexture(request.resolution) ?: return
val surface = Surface(resetTexture)
request.provideSurface(surface, executor) {
surface.release()
surfaceTexture?.release()
}
}
@WorkerThread
private fun resetPreviewTexture(size: Size): SurfaceTexture? {
return this.surfaceTexture?.let { surfaceTexture ->
surfaceTexture.setOnFrameAvailableListener(this)
surfaceTexture.setDefaultBufferSize(size.width, size.height)
surfaceTexture
}
}
最终效果(ps:清晰度可能从截图上看不算明显)
还有一个值得注意的点是:由于预览分辨率定了4:3
,所以SurfaceView
的宽高比也应该是4:3
。可以通过onMeasure
强制设置,也可通过xml布局设置。不然就可能会出现画面被拉伸的感觉…
本文总结了如果利用CameraX
结合OpenGL
进行相机预览,当然使用Camera/Camera2
的api
也是可行的,重点还是预览画面的获取与绑定。最后附上demo:xcyoung/opengl-camera。