본문 바로가기

CS/CS 정리

[영상처리] NV21과 YV12의 차이점

사진: Unsplash 의 Павло Павлюк

 

앞선 글에서 Bitmap과 YUV의 차이점을 알아보며, YUV 데이터가 실제로 사용되기 위해선 코덱이 지원하는 데이터 포맷으로 가공되어야한다는 점을 언급했다.

보통 안드로이드의  CameraX Analysis가 방출하는 row데이터는 YUV420_888 Color format이다.

YUV420_888

안드로이드에서 이 데이터를 영상 프레임같은 곳에 사용하기 위해선 NV21이나 YV12데이터로 변환해 주어야한다.

그럼 NV21은 뭐고, YV12은 뭘까? 이 둘의 차이점은 YUV데이터 중 색차 정보인 U와 V의 저장순서이다.

 

이미지로 보면 이렇다.

 

NV21은 효율적인 메모리 접근이 가능하다. 위 이미지에서 확인 가능하듯, U와 V가 교차하여 존재하기 때문에 메모리 접근이 효율적이다.

반면, YV12는 V와 U의 값이 별도의 평면에 배열된다. 때문에, 압축 알고리즘에 유리하다.

 

때문에 NV21은 실시간 카메라 처리 및 메모리 접근 효율성을 중요시 하는 상황에서 유리하며, YV12는 비디오 인코딩 및 압축 효율성을 중시하는 상황에서 유리하다.

 

 

가끔, YUV를 변환해서 Video frame에 적용 시켰는데 색상이 이상하게 나타나는 경우가 있다.(이것 때문에 2주간 고생했다)

이건 사용 중인 미디어 코덱이 지원하는 Color format과 현재 변환되어 적용 중인 Color format이 달라서 그렇다.

예를 들면 아래 사진들과 같은 경우가 그러하다.

사진 크기 조정이 안되네;;

 

위와 같은 경우를 방지하기 위해 적절한 Color Fotmat을 사용해 주어야 하는데.. 디바이스마다 지원되는 미디어 코덱이 다르기 때문에, 사용 전에 반드시 어떤 포맷을 사용해야하는지 파악해 줄 필요가 있다.

 

아래는 가상 기기인 Pixel6에서 지원되는 미디어 코덱과, 해당 코덱이 사용하는 Color format의 정보를 로그로 찍어본 것이다.

같은 코덱이라도, 사용되는 Color format이 다른걸 확인 할 수 있다.

만약, MediaCodec을 초기화할 때 코덱을 직접 지정해 주지 않는다면, 위 로그에서 확인 가능한 가장 상단의 코텍으로 값이 자동 지정된다. 이걸 몰라서 고생 좀 했다. 꼭 필요한 포맷을 파악한 후 초기화에 지정해주는 것을 추천한다.

 

테스트 디바이스가 몇개 없긴 하지만, 일반적으로 PlayStore가 설치되는 폰들은 위의 코덱들이 전부 사용 가능했고,

화웨이와 같은 대륙폰들은 잘 몰르겠지만, c2.android.avc.encoder 얘네는 지원해 주지 않을까..? 싶다.

 

 

YUV데이터를 NV21이나 YV12로 가공하는 작업은 50ms 이상 소요되는 무거운 작업이다.

그래서 실시간 영상처리가 필요한 경우 OpenGL ES2나 C++로 JNI를 구현하여 변환작업에 사용하는 것을 추천한다.

Stackoverflow에서 libyuv라는 라이브러리의 존재를 알게되었지만, 본인은 굳이 짧은 시간에 변환이 이루어질 필요가 없기 때문에 사용 하지는 않았다.

 

아래는 본인이 작성한 ImageProxy의 YUV420_888데이터를 NV21로 변환해주는 Kotlin 코드이다.

fun imageToNV21(image: ImageProxy): ByteArray {
    val planes = image.planes
    val width = image.width
    val height = image.height

    val ySize = width * height
    val numPixels = (ySize * 1.5f).toInt()
    val yuvData = ByteArray(numPixels)

    // Process Y plane
    val yPlane = planes[0]
    val yBuffer = yPlane.buffer
    val yRowStride = yPlane.rowStride

    var index = 0

    val yPixelStride = yPlane.pixelStride
    for (y in 0 until height) {
        for (x in 0 until width) {
            val rowOffset = (y * yRowStride) + (x * yPixelStride)
            yuvData[index++] = yBuffer[rowOffset]
        }
    }

    // Process U plane
    val uPlane = planes[1]
    val vPlane = planes[2]
    val uBuffer = uPlane.buffer
    val vBuffer = vPlane.buffer
    val uvRowStride = uPlane.rowStride
    val uvPixelStride = uPlane.pixelStride

    for (y in 0 until height / 2) {
        for (x in 0 until width / 2) {
            val rowOffset = (y * uvRowStride) + (x * uvPixelStride)
            yuvData[index++] = uBuffer[rowOffset]
            yuvData[index++] = vBuffer[rowOffset]
        }
    }

    return yuvData
}

 

모쪼록, 나 처럼 정보를 찾아다녔지만 답이 안보이던 이들에게 조금이라도 도움이 되었길 바란다.

다음 글은 좀 더 도움이 되는 글로 돌아오겠다.

 

Ref

https://gist.github.com/Jim-Bar/3cbba684a71d1a9d468a6711a6eddbeb

https://stackoverflow.com/questions/74653288/how-to-do-fast-memory-copy-from-yuv-420-888-to-nv21

'CS > CS 정리' 카테고리의 다른 글

[가볍게 읽는 CS] TCP vs UDP  (1) 2024.07.22
Bitmap과 YUV의 차이점  (0) 2024.06.24
[네트워크] 캐시(Cache)  (0) 2023.01.08