效果图如下:
一、为预览控件设置圆角
为控件设置ViewOutlineProvider
1
2
3
4
5
6
7
8
9
10
11
|
public RoundTextureView(Context context, AttributeSet attrs) { super (context, attrs); setOutlineProvider( new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { Rect rect = new Rect( 0 , 0 , view.getMeasuredWidth(), view.getMeasuredHeight()); outline.setRoundRect(rect, radius); } }); setClipToOutline( true ); } |
在需要时修改圆角值并更新
1
2
3
4
5
6
7
|
public void setRadius( int radius) { this .radius = radius; } public void turnRound() { invalidateOutline(); } |
即可根据设置的圆角值更新控件显示的圆角大小。当控件为正方形,且圆角值为边长的一半,显示的就是圆形。
二、实现正方形预览
1. 设备支持1:1预览尺寸
首先介绍一种简单但是局限性较大的实现方式:将相机预览尺寸和预览控件的大小都调整为1:1。
一般Android设备都支持多种预览尺寸,以Samsung Tab S3为例
在使用Camera API时,其支持的预览尺寸如下:
1
2
3
4
5
6
7
8
9
10
11
|
2019-08-02 13:16:08.669 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1920x1080 2019-08-02 13:16:08.669 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1280x720 2019-08-02 13:16:08.669 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1440x1080 2019-08-02 13:16:08.669 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1088x1088 2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1056x864 2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 960x720 2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 720x480 2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 640x480 2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 352x288 2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 320x240 2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 176x144 |
其中1:1的预览尺寸为:1088x1088。
在使用Camera2 API时,其支持的预览尺寸(其实也包含了PictureSize)如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 4128x3096 2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 4128x2322 2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 3264x2448 2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 3264x1836 2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 3024x3024 2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2976x2976 2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2880x2160 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2592x1944 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2560x1920 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2560x1440 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2560x1080 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2160x2160 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2048x1536 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2048x1152 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1936x1936 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1920x1080 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1440x1080 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1280x960 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1280x720 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 960x720 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 720x480 2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 640x480 2019-08-02 13:19:24.982 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 320x240 2019-08-02 13:19:24.982 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 176x144 |
其中1:1的预览尺寸为:3024x3024、2976x2976、2160x2160、1936x1936。
只要我们选择1:1的预览尺寸,再将预览控件设置为正方形,即可实现正方形预览;
再通过设置预览控件的圆角为边长的一半,即可实现圆形预览。2. 设备不支持1:1预览尺寸的情况
选择1:1预览尺寸的缺陷分析
分辨率局限性
上述说到,我们可以选择1:1的预览尺寸进行预览,但是局限性较高,
可选择范围都很小。如果相机不支持1:1的预览尺寸,这个方案就不可行了。
资源消耗
以Samsung tab S3为例,该设备使用Camera2 API时,支持的正方形预览尺寸都很大,在进行图像处理等操作时将占用较多系统资源。
处理不支持1:1预览尺寸的情况
添加一个1:1尺寸的ViewGroup
将TextureView放入ViewGroup
设置TextureView的margin值以达到显示中心正方形区域的效果
示意图
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//将预览控件和预览尺寸比例保持一致,避免拉伸 { FrameLayout.LayoutParams textureViewLayoutParams = (FrameLayout.LayoutParams) textureView.getLayoutParams(); int newHeight = 0 ; int newWidth = textureViewLayoutParams.width; //横屏 if (displayOrientation % 180 == 0 ) { newHeight = textureViewLayoutParams.width * previewSize.height / previewSize.width; } //竖屏 else { newHeight = textureViewLayoutParams.width * previewSize.width / previewSize.height; } ////当不是正方形预览的情况下,添加一层ViewGroup限制View的显示区域 if (newHeight != textureViewLayoutParams.height) { insertFrameLayout = new RoundFrameLayout(CoverByParentCameraActivity. this ); int sideLength = Math.min(newWidth, newHeight); FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(sideLength, sideLength); insertFrameLayout.setLayoutParams(layoutParams); FrameLayout parentView = (FrameLayout) textureView.getParent(); parentView.removeView(textureView); parentView.addView(insertFrameLayout); insertFrameLayout.addView(textureView); FrameLayout.LayoutParams newTextureViewLayoutParams = new FrameLayout.LayoutParams(newWidth, newHeight); //横屏 if (displayOrientation % 180 == 0 ) { newTextureViewLayoutParams.leftMargin = ((newHeight - newWidth) / 2 ); } //竖屏 else { newTextureViewLayoutParams.topMargin = -(newHeight - newWidth) / 2 ; } textureView.setLayoutParams(newTextureViewLayoutParams); } } |
三、使用GLSurfaceView进行自定义程度更高的预览
使用上面的方法操作已经可完成正方形和圆形预览,但是仅适用于原生相机,当我们的数据源并非是原生相机的情况时如何进行圆形预览?接下来介绍使用GLSurfaceView显示NV21的方案,完全是自己实现预览数据的绘制。
1. GLSurfaceView使用流程
OpenGL渲染YUV数据流程
其中的重点是渲染器(Renderer)的编写,Renderer的介绍如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
/** * A generic renderer interface. * <p> * The renderer is responsible for making OpenGL calls to render a frame. * <p> * GLSurfaceView clients typically create their own classes that implement * this interface, and then call {@link GLSurfaceView#setRenderer} to * register the renderer with the GLSurfaceView. * <p> * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For more information about how to use OpenGL, read the * <a href="{@docRoot}guide/topics/graphics/opengl.html" rel="external nofollow" >OpenGL</a> developer guide.</p> * </div> * * <h3>Threading</h3> * The renderer will be called on a separate thread, so that rendering * performance is decoupled from the UI thread. Clients typically need to * communicate with the renderer from the UI thread, because that's where * input events are received. Clients can communicate using any of the * standard Java techniques for cross-thread communication, or they can * use the {@link GLSurfaceView#queueEvent(Runnable)} convenience method. * <p> * <h3>EGL Context Lost</h3> * There are situations where the EGL rendering context will be lost. This * typically happens when device wakes up after going to sleep. When * the EGL context is lost, all OpenGL resources (such as textures) that are * associated with that context will be automatically deleted. In order to * keep rendering correctly, a renderer must recreate any lost resources * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method * is a convenient place to do this. * * * @see #setRenderer(Renderer) */ public interface Renderer { /** * Called when the surface is created or recreated. * <p> * Called when the rendering thread * starts and whenever the EGL context is lost. The EGL context will typically * be lost when the Android device awakes after going to sleep. * <p> * Since this method is called at the beginning of rendering, as well as * every time the EGL context is lost, this method is a convenient place to put * code to create resources that need to be created when the rendering * starts, and that need to be recreated when the EGL context is lost. * Textures are an example of a resource that you might want to create * here. * <p> * Note that when the EGL context is lost, all OpenGL resources associated * with that context will be automatically deleted. You do not need to call * the corresponding "glDelete" methods such as glDeleteTextures to * manually delete these lost resources. * <p> * @param gl the GL interface. Use <code>instanceof</code> to * test if the interface supports GL11 or higher interfaces. * @param config the EGLConfig of the created surface. Can be used * to create matching pbuffers. */ void onSurfaceCreated(GL10 gl, EGLConfig config); /** * Called when the surface changed size. * <p> * Called after the surface is created and whenever * the OpenGL ES surface size changes. * <p> * Typically you will set your viewport here. If your camera * is fixed then you could also set your projection matrix here: * <pre class="prettyprint"> * void onSurfaceChanged(GL10 gl, int width, int height) { * gl.glViewport(0, 0, width, height); * // for a fixed camera, set the projection too * float ratio = (float) width / height; * gl.glMatrixMode(GL10.GL_PROJECTION); * gl.glLoadIdentity(); * gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); * } * </pre> * @param gl the GL interface. Use <code>instanceof</code> to * test if the interface supports GL11 or higher interfaces. * @param width * @param height */ void onSurfaceChanged(GL10 gl, int width, int height); /** * Called to draw the current frame. * <p> * This method is responsible for drawing the current frame. * <p> * The implementation of this method typically looks like this: * <pre class="prettyprint"> * void onDrawFrame(GL10 gl) { * gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); * //... other gl calls to render the scene ... * } * </pre> * @param gl the GL interface. Use <code>instanceof</code> to * test if the interface supports GL11 or higher interfaces. */ void onDrawFrame(GL10 gl); } |
void onSurfaceCreated(GL10 gl, EGLConfig config)
在Surface创建或重建的情况下回调
void onSurfaceChanged(GL10 gl, int width, int height)
在Surface的大小发生变化的情况下回调
void onDrawFrame(GL10 gl)
在这里实现绘制操作。当我们设置的renderMode为RENDERMODE_CONTINUOUSLY时,该函数将不断地执行;
当我们设置的renderMode为RENDERMODE_WHEN_DIRTY时,将只在创建完成和调用requestRender后才执行。一般我们选择RENDERMODE_WHEN_DIRTY渲染模式,避免过度绘制。
一般情况下,我们会自己实现一个Renderer,然后为GLSurfaceView设置Renderer,可以说,Renderer的编写是整个流程的核心步骤。以下是在void onSurfaceCreated(GL10 gl, EGLConfig config)进行的初始化操作和在void onDrawFrame(GL10 gl)进行的绘制操作的流程图:
渲染YUV数据的Renderer
2. 具体实现
坐标系介绍
Android View坐标系
OpenGL世界坐标系
如图所示,和Android的View坐标系不同,OpenGL的坐标系是笛卡尔坐标系。
Android View的坐标系以左上角为原点,向右x递增,向下y递增;
而OpenGL坐标系以中心为原点,向右x递增,向上y递增。
着色器编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/** * 顶点着色器 */ private static String VERTEX_SHADER = " attribute vec4 attr_position;\n" + " attribute vec2 attr_tc;\n" + " varying vec2 tc;\n" + " void main() {\n" + " gl_Position = attr_position;\n" + " tc = attr_tc;\n" + " }" ; /** * 片段着色器 */ private static String FRAG_SHADER = " varying vec2 tc;\n" + " uniform sampler2D ySampler;\n" + " uniform sampler2D uSampler;\n" + " uniform sampler2D vSampler;\n" + " const mat3 convertMat = mat3( 1.0, 1.0, 1.0, -0.001, -0.3441, 1.772, 1.402, -0.7141, -0.58060);\n" + " void main()\n" + " {\n" + " vec3 yuv;\n" + " yuv.x = texture2D(ySampler, tc).r;\n" + " yuv.y = texture2D(uSampler, tc).r - 0.5;\n" + " yuv.z = texture2D(vSampler, tc).r - 0.5;\n" + " gl_FragColor = vec4(convertMat * yuv, 1.0);\n" + " }" ; |
内建变量解释
1
|
gl_Position |
VERTEX_SHADER
代码里的gl_Position
代表绘制的空间坐标。由于我们是二维绘制,所以直接传入OpenGL
二维坐标系的左下(-1,-1)、右下(1,-1)、左上(-1,1)、右上(1,1),也就是{-1,-1,1,-1,-1,1,1,1}
1
|
gl_FragColor |
FRAG_SHADER
代码里的gl_FragColor
代表单个片元的颜色
其他变量解释
1
|
ySampler、uSampler、vSampler |
分别代表Y、U、V
纹理采样器
1
|
convertMat |
根据以下公式:
1
2
3
|
R = Y + 1.402 (V - 128) G = Y - 0.34414 (U - 128) - 0.71414 (V - 128) B = Y + 1.772 (U - 128) |
我们可得到一个YUV转RGB的矩阵
1
2
3
|
1.0, 1.0, 1.0, 0, -0.344, 1.77, 1.403, -0.714, 0 |
部分类型、函数的解释
1
|
vec3、vec4 |
分别代表三维向量、四维向量。
1
|
vec4 texture2D(sampler2D sampler, vec2 coord) |
以指定的矩阵将采样器的图像纹理转换为颜色值;如:
texture2D(ySampler, tc).r
获取到的是Y数据,
texture2D(uSampler, tc).r
获取到的是U数据,
texture2D(vSampler, tc).r
获取到的是V数据。
在Java代码中进行初始化
根据图像宽高创建Y、U、V对应的ByteBuffer纹理数据;
根据是否镜像显示、旋转角度选择对应的转换矩阵;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
public void init( boolean isMirror, int rotateDegree, int frameWidth, int frameHeight) { if ( this .frameWidth == frameWidth && this .frameHeight == frameHeight && this .rotateDegree == rotateDegree && this .isMirror == isMirror) { return ; } dataInput = false ; this .frameWidth = frameWidth; this .frameHeight = frameHeight; this .rotateDegree = rotateDegree; this .isMirror = isMirror; yArray = new byte [ this .frameWidth * this .frameHeight]; uArray = new byte [ this .frameWidth * this .frameHeight / 4 ]; vArray = new byte [ this .frameWidth * this .frameHeight / 4 ]; int yFrameSize = this .frameHeight * this .frameWidth; int uvFrameSize = yFrameSize >> 2 ; yBuf = ByteBuffer.allocateDirect(yFrameSize); yBuf.order(ByteOrder.nativeOrder()).position( 0 ); uBuf = ByteBuffer.allocateDirect(uvFrameSize); uBuf.order(ByteOrder.nativeOrder()).position( 0 ); vBuf = ByteBuffer.allocateDirect(uvFrameSize); vBuf.order(ByteOrder.nativeOrder()).position( 0 ); // 顶点坐标 squareVertices = ByteBuffer .allocateDirect(GLUtil.SQUARE_VERTICES.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); squareVertices.put(GLUtil.SQUARE_VERTICES).position( 0 ); //纹理坐标 if (isMirror) { switch (rotateDegree) { case 0 : coordVertice = GLUtil.MIRROR_COORD_VERTICES; break ; case 90 : coordVertice = GLUtil.ROTATE_90_MIRROR_COORD_VERTICES; break ; case 180 : coordVertice = GLUtil.ROTATE_180_MIRROR_COORD_VERTICES; break ; case 270 : coordVertice = GLUtil.ROTATE_270_MIRROR_COORD_VERTICES; break ; default : break ; } } else { switch (rotateDegree) { case 0 : coordVertice = GLUtil.COORD_VERTICES; break ; case 90 : coordVertice = GLUtil.ROTATE_90_COORD_VERTICES; break ; case 180 : coordVertice = GLUtil.ROTATE_180_COORD_VERTICES; break ; case 270 : coordVertice = GLUtil.ROTATE_270_COORD_VERTICES; break ; default : break ; } } coordVertices = ByteBuffer.allocateDirect(coordVertice.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); coordVertices.put(coordVertice).position( 0 ); <span style= "font-size: 15px; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; white-space: normal; word-spacing: 0px; text-transform: none; float: none; font-weight: 400; color: rgb(51,51,51); font-style: normal; orphans: 2; widows: 2; display: inline !important; letter-spacing: normal; background-color: rgb(255,255,255); text-indent: 0px; font-variant-ligatures: normal; font-variant-caps: normal; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial" ><font color= "#000000" face= "NSimsun" >}</font></span> |
在Surface创建完成时进行Renderer初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
|
private void initRenderer() { rendererReady = false ; createGLProgram(); //启用纹理 GLES20.glEnable(GLES20.GL_TEXTURE_2D); //创建纹理 createTexture(frameWidth, frameHeight, GLES20.GL_LUMINANCE, yTexture); createTexture(frameWidth / 2 , frameHeight / 2 , GLES20.GL_LUMINANCE, uTexture); createTexture(frameWidth / 2 , frameHeight / 2 , GLES20.GL_LUMINANCE, vTexture); rendererReady = true ; } |
其中createGLProgram用于创建OpenGL Program并关联着色器代码中的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
private void createGLProgram() { int programHandleMain = GLUtil.createShaderProgram(); if (programHandleMain != - 1 ) { // 使用着色器程序 GLES20.glUseProgram(programHandleMain); // 获取顶点着色器变量 int glPosition = GLES20.glGetAttribLocation(programHandleMain, "attr_position" ); int textureCoord = GLES20.glGetAttribLocation(programHandleMain, "attr_tc" ); // 获取片段着色器变量 int ySampler = GLES20.glGetUniformLocation(programHandleMain, "ySampler" ); int uSampler = GLES20.glGetUniformLocation(programHandleMain, "uSampler" ); int vSampler = GLES20.glGetUniformLocation(programHandleMain, "vSampler" ); //给变量赋值 /** * GLES20.GL_TEXTURE0 和 ySampler 绑定 * GLES20.GL_TEXTURE1 和 uSampler 绑定 * GLES20.GL_TEXTURE2 和 vSampler 绑定 * * 也就是说 glUniform1i的第二个参数代表图层序号 */ GLES20.glUniform1i(ySampler, 0 ); GLES20.glUniform1i(uSampler, 1 ); GLES20.glUniform1i(vSampler, 2 ); GLES20.glEnableVertexAttribArray(glPosition); GLES20.glEnableVertexAttribArray(textureCoord); /** * 设置Vertex Shader数据 */ squareVertices.position( 0 ); GLES20.glVertexAttribPointer(glPosition, GLUtil.COUNT_PER_SQUARE_VERTICE, GLES20.GL_FLOAT, false , 8 , squareVertices); coordVertices.position( 0 ); GLES20.glVertexAttribPointer(textureCoord, GLUtil.COUNT_PER_COORD_VERTICES, GLES20.GL_FLOAT, false , 8 , coordVertices); } } |
其中createTexture用于根据宽高和格式创建纹理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
private void createTexture( int width, int height, int format, int [] textureId) { //创建纹理 GLES20.glGenTextures( 1 , textureId, 0 ); //绑定纹理 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[ 0 ]); /** * {@link GLES20#GL_TEXTURE_WRAP_S}代表左右方向的纹理环绕模式 * {@link GLES20#GL_TEXTURE_WRAP_T}代表上下方向的纹理环绕模式 * * {@link GLES20#GL_REPEAT}:重复 * {@link GLES20#GL_MIRRORED_REPEAT}:镜像重复 * {@link GLES20#GL_CLAMP_TO_EDGE}:忽略边框截取 * * 例如我们使用{@link GLES20#GL_REPEAT}: * * squareVertices coordVertices * -1.0f, -1.0f, 1.0f, 1.0f, * 1.0f, -1.0f, 1.0f, 0.0f, -> 和textureView预览相同 * -1.0f, 1.0f, 0.0f, 1.0f, * 1.0f, 1.0f 0.0f, 0.0f * * squareVertices coordVertices * -1.0f, -1.0f, 2.0f, 2.0f, * 1.0f, -1.0f, 2.0f, 0.0f, -> 和textureView预览相比,分割成了4 块相同的预览(左下,右下,左上,右上) * -1.0f, 1.0f, 0.0f, 2.0f, * 1.0f, 1.0f 0.0f, 0.0f */ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT); /** * {@link GLES20#GL_TEXTURE_MIN_FILTER}代表所显示的纹理比加载进来的纹理小时的情况 * {@link GLES20#GL_TEXTURE_MAG_FILTER}代表所显示的纹理比加载进来的纹理大时的情况 * * {@link GLES20#GL_NEAREST}:使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色 * {@link GLES20#GL_LINEAR}:使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色 */ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0 , format, width, height, 0 , format, GLES20.GL_UNSIGNED_BYTE, null ); } |
在Java代码中调用绘制
在数据源获取到时裁剪并传入帧数据
1
2
3
4
5
6
7
|
@Override public void onPreview( final byte [] nv21, Camera camera) { //裁剪指定的图像区域 ImageUtil.cropNV21(nv21, this .squareNV21, previewSize.width, previewSize.height, cropRect); //刷新GLSurfaceView roundCameraGLSurfaceView.refreshFrameNV21( this .squareNV21); } |
NV21数据裁剪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
/** * 裁剪NV21数据 * * @param originNV21 原始的NV21数据 * @param cropNV21 裁剪结果NV21数据,需要预先分配内存 * @param width 原始数据的宽度 * @param height 原始数据的高度 * @param left 原始数据被裁剪的区域的左边界 * @param top 原始数据被裁剪的区域的上边界 * @param right 原始数据被裁剪的区域的右边界 * @param bottom 原始数据被裁剪的区域的下边界 */ public static void cropNV21( byte [] originNV21, byte [] cropNV21, int width, int height, int left, int top, int right, int bottom) { int halfWidth = width / 2 ; int cropImageWidth = right - left; int cropImageHeight = bottom - top; //原数据Y左上 int originalYLineStart = top * width; int targetYIndex = 0 ; //原数据UV左上 int originalUVLineStart = width * height + top * halfWidth; //目标数据的UV起始值 int targetUVIndex = cropImageWidth * cropImageHeight; for ( int i = top; i < bottom; i++) { System.arraycopy(originNV21, originalYLineStart + left, cropNV21, targetYIndex, cropImageWidth); originalYLineStart += width; targetYIndex += cropImageWidth; if ((i & 1 ) == 0 ) { System.arraycopy(originNV21, originalUVLineStart + left, cropNV21, targetUVIndex, cropImageWidth); originalUVLineStart += width; targetUVIndex += cropImageWidth; } } } |
传给GLSurafceView并刷新帧数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/** * 传入NV21刷新帧 * * @param data NV21数据 */ public void refreshFrameNV21( byte [] data) { if (rendererReady) { yBuf.clear(); uBuf.clear(); vBuf.clear(); putNV21(data, frameWidth, frameHeight); dataInput = true ; requestRender(); } } |
其中putNV21用于将NV21中的Y、U、V数据分别取出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/** * 将NV21数据的Y、U、V分量取出 * * @param src nv21帧数据 * @param width 宽度 * @param height 高度 */ private void putNV21( byte [] src, int width, int height) { int ySize = width * height; int frameSize = ySize * 3 / 2 ; //取分量y值 System.arraycopy(src, 0 , yArray, 0 , ySize); int k = 0 ; //取分量uv值 int index = ySize; while (index < frameSize) { vArray[k] = src[index++]; uArray[k++] = src[index++]; } yBuf.put(yArray).position( 0 ); uBuf.put(uArray).position( 0 ); vBuf.put(vArray).position( 0 ); } |
在执行requestRender后,onDrawFrame函数将被回调,在其中进行三个纹理的数据绑定并绘制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
@Override public void onDrawFrame(GL10 gl) { // 分别对每个纹理做激活、绑定、设置数据操作 if (dataInput) { //y GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yTexture[ 0 ]); GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0 , 0 , 0 , frameWidth, frameHeight, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, yBuf); //u GLES20.glActiveTexture(GLES20.GL_TEXTURE1); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, uTexture[ 0 ]); GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0 , 0 , 0 , frameWidth >> 1 , frameHeight >> 1 , GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, uBuf); //v GLES20.glActiveTexture(GLES20.GL_TEXTURE2); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, vTexture[ 0 ]); GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0 , 0 , 0 , frameWidth >> 1 , frameHeight >> 1 , GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, vBuf); //在数据绑定完成后进行绘制 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0 , 4 ); } } |
即可完成绘制。
四、加一层边框
有时候需求并不仅仅是圆形预览这么简单,我们可能还要为相机预览加一层边框
边框效果
一样的思路,我们动态地修改边框值,并进行重绘。
边框自定义View中的相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
@Override protected void onDraw(Canvas canvas) { super .onDraw(canvas); if (paint == null ) { paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setAntiAlias( true ); SweepGradient sweepGradient = new SweepGradient((( float ) getWidth() / 2 ), (( float ) getHeight() / 2 ), new int []{Color.GREEN, Color.CYAN, Color.BLUE, Color.CYAN, Color.GREEN}, null ); paint.setShader(sweepGradient); } drawBorder(canvas, 6 ); } private void drawBorder(Canvas canvas, int rectThickness) { if (canvas == null ) { return ; } paint.setStrokeWidth(rectThickness); Path drawPath = new Path(); drawPath.addRoundRect( new RectF( 0 , 0 , getWidth(), getHeight()), radius, radius, Path.Direction.CW); canvas.drawPath(drawPath, paint); } public void turnRound() { invalidate(); } public void setRadius( int radius) { this .radius = radius; } |
五、完整Demo代码:
https://github.com/wangshengyang1996/GLCameraDemo
使用Camera API和Camera2 API并选择最接近正方形的预览尺寸
使用Camera API并为其动态添加一层父控件,达到正方形预览的效果
使用Camera API获取预览数据,使用OpenGL的方式进行显示最后,给大家推荐一个好用的Android免费离线人脸识别的sdk,可以和本文实现技术的完美结合: https://ai.arcsoft.com.cn/product/arcface.html
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://segmentfault.com/a/1190000019982091