[Android] YUV와 RGB의 색공간 변환과 LIBYUV를 이용한 하드웨어 가속

2019. 12. 13. 17:35Programming/Android

반응형

YUV란?

YUV(YIQ/YCbCr/YPbPr)는 색을 구성하는 방법 중 하나로 밝기(Y)와 청색 색차(U), 적색 색차(V) 정보로 색을 구성한다. 여기서 Y 신호만 받는다면 흑백이 된다. 여러 번 복제한 VHS 테이프나 방송 상태가 좋지 못한 채널에서 흑백으로 보이는 것도 이 때문. Lab과 마찬가지로 인간이 색을 인식하는 방식으로 구성되었다. - 나무위키 발췌(https://namu.wiki/w/YUV)

사실 발췌는 해왔지만 정확하게 아는 것은 아니다. 흑백 TV의 위치를 컬러 TV가 조금씩 대체해가던 과도기 시절, RGB를 흑백 TV에 송출하기 어렵기 때문에 만들어진 색 공간 개념이라고 한다. RGB보다 전송에 용이하기 때문에 흑백 TV가 더 이상 사용되지 않는 요즈음에도 많이 사용하고 있다고 한다.

YUV 색공간에 대한 설명은 이 이상 할 생각이 없다. 사실 글을 작성하는 본인이 색공간에 대한 지식은 발췌한 내용보다도 적고, 당장 YUV만 검색하더라도 수두룩하게 많이 나오기 때문이다. 뭣보다 이 글을 작성하는 이유는, libyuv를 사용하여 컬러 스페이스 컨버젼(이하 CSC)을 하기 위해 상당히 긴 시간동안 크로미움을 가지고 삽질을 했기 때문이다. 사실 소스코드만 받으면 그럴 필요도 없었는데 말이다.

YUV2RGB와 ARM8-V64A

일반적으로 카메라에서 수신된 영상을 디코딩하여 프레임을 얻게되면, YUV 포멧으로 되어있다. 이를 사용하기 위해서는 RGB로 변환해야 하는데, 동작하는 코드 자체는 크게 어렵지 않다. 레거시 앱에서는 PINK NOISE PRODUCTIONSYUV2RGB를 사용하여 CSC를 하고 있었는데, .S로 작성된 어셈블리 코드가 있기 때문에 하드웨어 가속을 사용할 수 있다. YUV2RGB는 상당히 오래된 라이브러리기 때문에 arm64-v8칩셋에 대한 고려는 되어있지 않고, 만약 YUV2RGB를 사용하는 레거시 코드를 64bits로 빌드하려고 하면 에러를 뿜뿜하는 안드로이드 스튜디오를 발견하게 된다.

Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
arm7에서 arm8로 넘어가면서 삭제된 명령어들이 꽤 되기 때문에, arm7용으로 작성된 .S파일을 사용하려고 하면 에러를 뿜뿜하게 된다.

앞서 말했듯이 YUVRGB로 변환하는 방법은 어렵지 않다. 물론 YUV 포멧에도 여러가지가 있고, RGB의 포멧도 여러가지가 있지만... 대부분 라이브러리로 구현되어있기 때문에, 인터넷을 참조하여 코드를 작성하더라도 크게 문제는 없다. 나의 경우에는 64bits에 임시책으로 Tensorflow에 포함되어있던 .cc코드를 사용했다. 아래의 코드는 깃에 올라온 Tensorflow의 예제 코드로, YUV420ARGB8888로 변환하는 코드다.

//  Accepts a YUV 4:2:0 image with a plane of 8 bit Y samples followed by an
//  interleaved U/V plane containing 8 bit 2x2 subsampled chroma samples,
//  except the interleave order of U and V is reversed. Converts to a packed
//  ARGB 32 bit output of the same pixel dimensions.
void ConvertYUV420SPToARGB8888(const uint8_t* const yData,
                               const uint8_t* const uvData,
                               uint32_t* const output, const int width,
                               const int height) {
  const uint8_t* pY = yData;
  const uint8_t* pUV = uvData;
  uint32_t* out = output;

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      int nY = *pY++;
      int offset = (y >> 1) * width + 2 * (x >> 1);
#ifdef __APPLE__
      int nU = pUV[offset];
      int nV = pUV[offset + 1];
#else
      int nV = pUV[offset];
      int nU = pUV[offset + 1];
#endif

      *out++ = YUV2RGB(nY, nU, nV);
    }
  }
}

위의 C코드를 사용하면 색변환이 되서 정상적으로 동작하지만, 하드웨어 가속이 적용되지 않아 처리 속도가 느리다. 프레임을 한 두장 정도 처리하는데는 문제가 없지만, 연속으로 처리하는 경우에는 문제가 발생한다. 2019년 11월부터 64bits를 지원하지 않는 어플리케이션은 스토어에 등록할 수 없으므로, 빠른 속도로 CSC를 처리하기 위해서는 다른 방법이 필요하다.

LIBYUV

LIBYUVYUV/RGB 변환 및 스케일링을 제공하는 라이브러리로, 간단하게 적용하여 사용할 수 있다. 뿐만 아니라 어셈블리 코드가 포함되어있어, 하드웨어 가속이 적용되므로 보다 빠른 처리를 기대할 수 있다. 만약 YUV2RGB를 사용하여 CSC를 처리하고 있다면(물론 이 시점에 그런 레거시 코드를 발견한다면 도망치는게 좀 더 좋은 선택일 수 있겠지만), LIBYUV를 사용하여 64bits를 지원하도록 수정할 수 있다. 적용은 크게 어렵지 않다.

Getting Start

Getting Start 페이지를 참조하여 쉽게 적용할 수 있다. Getting Start에는 Pre-requisites라는 항목을 통해 depot-tools을 설치하는 과정과, depot-tools에 포함된 gclient를 사용하여 코드를 다운로드 및 빌드하는 내용이 포함되어있다. 여기에 낚여서(?) 소스코드를 다운받고, 리눅스 혹은 윈도우에서 안드로이드 용으로 크로스 컴파일을 시도하다보면 높은 확률로 머리아픈 상황에 처하게 된다. 보통의 경우에는 소스코드를 보고 눈치를 채겠지만, 나의 경우에는 3일간 어떻게건 컴파일을 해야겠다며 윈도우와 개발서버를 오가며 씨름을 해야했다. 이 글을 남기게 된 이유도 나중에 똑같은 상황이 왔을 때, 참조하기 위함이다. ' ㅅ';

depot-tools를 설치해서 환경변수를 구성한 뒤, gclient를 이용하여 소스코드를 받아도 되지만, To get just the source (not buildable): git clone [https://chromium.googlesource.com/libyuv/libyuv](https://chromium.googlesource.com/libyuv/libyuv) 항목처럼 git을 통해 소스코드를 받아도 된다. not buildable이라고 적혀있는데, Android.mk를 사용하여 Shared Library를 빌드하는데는 전혀 문제가 없다. 오히려 gclient를 사용하여 sync를 하게되면 시간이 오래걸리니, git을 통해 소스코드만 받는게 나을수도 있다. - ㅅ-)

Shared Library Build

gclient를 사용해서 받았다면 src디렉터리를, git을 사용해서 받았다면 libyuv 혹은 체크아웃 받은 경로 내의 Android.mk파일을 열어보자. NDK에서 libyuv를 사용하기 위해서 libyuv_static을 빌드하고, 최종적으로는 libyuv_unittest를 빌드하는 내용이 들어있다. 다만 NDK를 사용하여 결과물을 Java/Kotlin으로 가져오기 위해서는 Static이 아닌 Shared여야 하는 모양이다. .a파일을 사용하게 될 경우 에러가 발생하므로, BUILD_STATIC_LIBRARY라고 되어있는 라인을 BUILD_SHARED_LIBRARY로 변경하도록 하자.

libyuv/source에 위치한 .cc파일들과 libyuv/include에 위치한 .h파일들을 앱의 JNI경로에 넣어준 뒤, Android.mk에 기재된 파일 경로를 재조정해주자. 빌드해보면 LOCAL_MODULE에 지정해준 값으로 .so파일이 생성된 것을 볼 수 있다. 이제, JNI에서 libyuv를 사용할 수 있다.

include $(CLEAR_VARS)
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := \
    source/compare.cc           \
    source/compare_common.cc    \
    source/compare_gcc.cc       \
    source/compare_mmi.cc       \
    source/compare_msa.cc       \
    source/compare_neon.cc      \
    source/compare_neon64.cc    \
    source/convert.cc           \
    source/convert_argb.cc      \
    source/convert_from.cc      \
    source/convert_from_argb.cc \
    source/convert_to_argb.cc   \
    source/convert_to_i420.cc   \
    source/cpu_id.cc            \
    source/planar_functions.cc  \
    source/rotate.cc            \
    source/rotate_any.cc        \
    source/rotate_argb.cc       \
    source/rotate_common.cc     \
    source/rotate_gcc.cc        \
    source/rotate_mmi.cc        \
    source/rotate_msa.cc        \
    source/rotate_neon.cc       \
    source/rotate_neon64.cc     \
    source/row_any.cc           \
    source/row_common.cc        \
    source/row_gcc.cc           \
    source/row_mmi.cc           \
    source/row_msa.cc           \
    source/row_neon.cc          \
    source/row_neon64.cc        \
    source/scale.cc             \
    source/scale_any.cc         \
    source/scale_argb.cc        \
    source/scale_common.cc      \
    source/scale_gcc.cc         \
    source/scale_mmi.cc         \
    source/scale_msa.cc         \
    source/scale_neon.cc        \
    source/scale_neon64.cc      \
    source/video_common.cc
common_CFLAGS := -Wall -fexceptions
ifneq ($(LIBYUV_DISABLE_JPEG), "yes")
LOCAL_SRC_FILES += \
    source/convert_jpeg.cc      \
    source/mjpeg_decoder.cc     \
    source/mjpeg_validate.cc
common_CFLAGS += -DHAVE_JPEG
LOCAL_SHARED_LIBRARIES := libjpeg
endif

LOCAL_CFLAGS += $(common_CFLAGS)
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include

LOCAL_MODULE := libyuv_shared
LOCAL_MODULE_TAGS := optional

include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)

LOCAL_WHOLE_SHARED_LIBRARIES := libyuv_shared
LOCAL_MODULE := libyuv
ifneq ($(LIBYUV_DISABLE_JPEG), "yes")
LOCAL_SHARED_LIBRARIES := libjpeg
endif

include $(BUILD_SHARED_LIBRARY)

마무리

이번에는 libyuv 라이브러리를 사용하여, NDK에서 CSC를 할 시 하드웨어 가속을 적용하는 방법에 대해 정리해봤다. 또한 크로미움에 올라온 문서를 참조할 시 나처럼 시행착오를 겪을 가능성이 있어보이는데, 참조할만한 문서가 없어서 더 오래 걸리지 않았나 싶었다. 모쪼록 libyuv를 적용하는 데 도움이 되기를 바라며, 글을 마무리해본다. ' ㅅ')/

반응형