Tistory View

Android Develop/camera

Camera Preview NV21 GLES 2.0 Shader

God Dangchy What should I do? 2017. 11. 5. 21:18

안드로이드 카메라 NV21바로 shader에서 처리하기.


안드로이드에서 Preview데이터를 받으면 NV21이라는 규격으로 받아오게 된다.

물론 다른 형태도 지원하지만 필자가 테스트해본 바로는 NV21를 그냥 사용하는 것이 가장 간단하다는 결론을 내렸다.


NV21을 RGB로 만드는 것에는 이미지라는 큰 데이터를 다루기에 부하가 많이 걸려 부하를 줄이기 위한 여러가지 작업을 하던 중 NV21을 바로 이용하여 처리하는 방법이 떠올랐다..(머리가 나빠졌는지 이 간단한 걸 알아내는 데 이리 오래 걸리다니..)


NV21은 YPlane + VUPlane으로 구성되어 있다.

이 Plane을 각각 Plane를 Texture로 만든다..(2개의 Texture를 이용한다.)

여기서 VUPlane은 YPlane의 1/4크기를 갖는다. 정확히 Y-Plane의 폭과 높이가 각각 1/2이다.(주1)


VUPlane의 시작위치는 YPlane + (w*h)byte이다.



Y-Plane Texture를 만들기위해서는 다음과 같은 코드를 사용한다.(텍스쳐를 만드는 코드는 알아서.. )

// Camera에서 제공하는 NV21은 stride의 padding이 없다.

GLES20.glPixelStorei( GLES20.GL_UNPACK_ALIGNMENT, 1 );

// Y값은 밝기만 있으므로 GL_LUMINACE를 처리한다. shader에서 rgb는 같은 값을 가지게 되므로 아무거나 써도 된다.

// 밝기만 있다는 표현보다 1byte데이터로 전송하기에 GL_LUMINANCE가 가장 어울릴 뿐이다.

GLES20.glTexImage2D( GLES20.GL_TEXTURE_2D, 0, GL_LUMINANCE, w, h, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, yPlane );


VU-Plane Texture를 만들기위해서는 다음의 코드를 사용할 것이다.

// Camera에서 제공하는 NV21은 VU-Plane 또한 stride의 padding이 없다.

GLES20.glPixelStorei( GLES20.GL_UNPACK_ALIGNMENT, 1 );

// 2Byte값을 이용하여 Texture를 만들어야 하기에 GL_LUMINACE_ALPHA로 처리한다.

shader에서 rgb는 같은 값을 가지게되고 이 값은 V값을 가지고 a값은 U값을 가지게 된다.

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,0,GL_LUMINANCE_ALPHA, w/2, h/2, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, vuPlane );


vertex shader 코드는 일반적으로 사용하는 Texture를 다루는 코드와 차이가 없다.

precision mediump float;

attribute vec4 aVtxPos;

attribute vec2 aTexPos;

uniform   mat4 uPRMatrix;

uniform   mat4 uMVMatrix;

varying   vec2 vTexPos;

void main() {

vTexPos = aTexPos;    // texture의 위치는 y-plane과 vu-plane이 동일하다. texture는 [0,1]로 normalize되어 있으니 상관이 없다.

gl_Position = uPRMatrix * uMVMatrix * vec4( aVtxPos.xyz, 1.0 );

}

fragment shader는 2개의 texture에서 yuv값을 읽어 rgb로 변환하는 코드로 만들어진다.

precision mediump float;

uniform sampler2D uTexPlaneY;

uniform sampler2D uTexPlaneVU;

varying vec2 vTexPos; 

vec4 yuv2rgb( float y, float u, float v, float a ) {

float r = clamp( y + 1.403 * (v-0.5), 0.0, 1.0 ); 

float g = clamp( y - 0.714 * (v-0.5) - 0.344 * (u-0.5), 0.0, 1.0 ); 

float b = clamp( y + 1.770 * (u-0.5), 0.0, 1.0 ); 

return vec4( r, g, b, a ); 

}

//각각의 texture에서 yuv값을 추출하여 rgb로 변환한다.

void main() {

// r g b 모두 같은 값을 갖는다. 모두 y 값이다.

float y = texture2D( uTexPlaneY, vTexPos ).r;

// vu texture는 r or g or b -> v값, a-> u값을 가진다.

vec4 vu = texture2D( uTexPlaneVU, vTexPos );

gl_FragColor = yuv2rgb( y, vu.a, vu.r, 1.0 );



drawing 코드에서 texture를 2개를 처리하는 부분은 다음과 같다.

GLES20.glActiveTexture( GLES20.GL_TEXTURE0 );
GLES20.glBindTexture( GLES20.GL_TEXTURE_2D, texIdY );
GLES20.glUniform1i( m_uTexPlaneY, 0 );

GLES20.glActiveTexture( GLES20.GL_TEXTURE1 );
GLES20.glBindTexture( GLES20.GL_TEXTURE_2D, texIdVU );
GLES20.glUniform1i( m_uTexPlaneVU, 1 );


카메라에서 넘어온 preview데이터를 아무런 가공없이 바로 texture로 만들어 cpu에 거의 부담없이 shader만으로 화면을 그릴 수 있게 되었다.


Preview데이터를 가공할 목적이 아닌 그냥 texture로 만들 경우는 preview를 사용하지 말고 SurfaceTexture를 이용하여 바로 만들어지는 Texture를 이용하는 것이 가장 효율적이다. 이 글은 preview를 가공할 필요가 있을 때만 사용하는 것이 좋을 것이다.



주1

안드로이드의 카메라에서 지원하는 preview는 가로세로크기가 짝수로만 존재한다고 가정한다. 필자의 경우 홀수로 되어있는 기기는 보지 못했다.

따라서 preview의 폭과 높이를 1/2해도 소수점처리가 필요가 없었다.


 

Replies
Reply Write