3-2.이미지 출력

비디오는 정지 영상을 시간별로 모아 놓은 것이고 각 영상은 폭, 높이만큼의 픽셀로 구성된다. 각 픽셀의 색상을 표현하는 방법이 색상 포맷이다. 원자적인 단위인 픽셀의 속성을 결정하는 값이어서 잘 알아 두어야 한다. 좀 복잡하지만 나름 재미는 있다. 비디오 스트림의 codecpar format 멤버가 색상 포맷인데 대략 100여 가지가 있다. 자주 사용하는 색상 포맷은 다음과 같다.

 

포맷

설명

AV_PIX_FMT_YUV420P

평면 YUV 4:2:0

AV_PIX_FMT_YUYV422

일차원 YUV 4:2:2

AV_PIX_FMT_RGB24       

RGB

AV_PIX_FMT_BGR24       

BGR

AV_PIX_FMT_YUV444P

평면 YUV 4:4:4

AV_PIX_FMT_YUV411P

평면 YUV 4:1:1

PIX_FMT_GRAY8             

Y 밝기값 8비트로만 구성된 그레이스케일

AV_AV_PIX_FMT_MONOWHITE

1비트 흑백. 0이 흰색, 1이 검정이다. 상위 비트에서 하위 비트로 픽셀을 나열한다.

AV_PIX_FMT_PAL8

8비트 팔레트를 사용하는 포맷

 

가장 직관적이고 이해하기 쉬운 색상 포맷은 빨강, 초록, 파랑색의 강도를 조합하여 표현하는 RGB이다. 그것도 바이트 순서에 따라 RGB BGR 두 가지 종류가 있고 알파까지 포함하면 ARGB, BGRA가 된다. 윈도우가 기본적으로 사용하는 포맷이 ARGB이다.

삼원색을 조합하는 방식이 가장 편리하지만 각 픽셀에 모든 색 요소 정보가 다 있어 용량이 크고 흑백 디스플레이에는 비효율적이다. 흑백 텔레비전 시절에 방송국은 각 픽셀의 밝기 정보만 쏘면 되었으나 컬러 텔레비전이 등장함으로써 색상 정보를 보내야 했다. 이때 RGB를 보내면 흑백 텔레비전은 색상을 밝기로 바꾸느라 느려진다.

그래서 고안한 색상 포맷이 YUV이다. YUV는 밝기 정보인 Y와 청색인 U, 적색인 V 정보로 구성된다. 사람의 눈은 RGB의 조합이 아닌 색상과 밝기로 사물을 인식하는데 이 방식대로 색상을 만드는 것이다. 흑백 텔레비전은 Y정보만 빼내 사용하면 밝기를 쉽게 표현할 수 있어 효율적이다. 이런 역사적인 이유로 고안한 YUV는 픽셀 압축에도 효과적이어서 이후에도 계속 사용되고 있다.

RGB 포맷은 각 점마다 세 요소가 다 필요하며 하나를 생략하면 금방 티가 난다. 그러나 YUV 포맷은 밝기인 Y에 민감할 뿐 색조에 해당하는 U, V는 인접 픽셀이 비슷해도 별로 티가 나지 않는다. 그래서 인접한 픽셀의 U, V 정보를 합치는데 이를 서브 샘플링이라고 한다.

     

RGB YUV 4:4:4는 각 픽셀당 3바이트씩의 색상 요소를 개별적으로 가진다. 4개 표현에 12바이트를 사용하며 화질이 가장 좋다. YUV 4:2:2 Y값은 픽셀별로 각각 가지지만 인접한 두 픽셀은 U, V값을 공유한다. 이러면 점 4개 표현에 8바이트면 되니 4바이트가 절약된다.

YUV 4:1:1은 인접한 네 픽셀이 U, V값을 공유하는 식이며 6바이트가 되어 애초의 절반으로 줄어든다. 그러나 4:1:1은 길게 늘어선 픽셀이 색조를 공유하는 식이며 Y1 Y4가 너무 떨어져 있어 색조차이가 눈에 거슬릴 수 있다. 4:2:0은 아래 위로 인접한 4개의 픽셀이 U, V를 공유하여 좀 덜하다.

색상 요소를 나열하는 방식도 두 가지가 있는데 각 요소를 일차원으로 나열하는 방식(packed)이 있고 별도의 메모리 블록에 요소끼리 묶어서 평면적(planar)으로 표현하는 방식이 있다. RGB YUV 4:4:4는 요소값이 규칙적으로 반복되어 일차원적으로 나열하면 된다. 하지만 YUV 4:2:2 Y의 개수와 U, V의 개수가 달라 별도의 메모리에 따로 나열하는 방식이 효율적이다. 두 방식을 그림으로 비교해 보자.

샘플 동영상인 fire.avi가 오른쪽 그림처럼 YUV420P 색상 포맷으로 되어 있으며 동영상은 이 포맷으로 된 경우가 가장 많다. Y의 길이에 비해 U, V1/4밖에 되지 않는다. FFmpeg은 이런 모든 경우의 색상 포맷을 모두 지원한다. 그래서 내부적인 자료 구조가 복잡할 수밖에 없다.

RGB 방식의 모니터는 YUV를 직접 출력하지 못하며 RGB로 바꿔야 한다. 프레임의 data는 색상 요소를 저장하는 배열의 배열이며 최대 크기는 8이다. data[0] Y값이 나열되어 있고 data[1], data[2] U, V값이 나열되어 있다. 위 오른쪽 그림의 배열이 순서대로 data[n] 배열에 해당한다.

data 배열의 길이는 색상 포맷에 따라 달라지는데 이 정보는 프레임의 linesize 배열에 들어 있다. linesize는 한줄을 구성하는 색상 요소의 길이 배열이다. 콘솔에서 작성한 덤프 예제로 점검해 보면 Y는 한줄의 데이터 길이가 이미지 폭과 같은 640개이지만 U, V 320개밖에 안되며 두 줄의 Y가 공유한다.

색상 모델끼리 변환하는 공식은 공개되어 있으므로 그대로 가져다 쓰면 된다. 일정 계수를 곱하고 더하는 일차 방정식일 뿐이다. 인터넷으로 검색해 보되 위키에 잘 정리되어 있다. 공식대로 코드를 작성해 보자.

 

int DrawFrame(HDC hdc) {

        int ret;

        AVPacket packet = { 0, };

        AVFrame vFrame = { 0, }, aFrame = { 0, };

 

        while (av_read_frame(fmtCtx, &packet) == 0) {

                   if (packet.stream_index == vidx) {

                              ret = avcodec_send_packet(vCtx, &packet);

                              if (ret != 0) { continue; }

                              for(;;) {

                                        ret = avcodec_receive_frame(vCtx, &vFrame);

                                        if (ret == AVERROR(EAGAIN)) break;

 

                                        // 압축 해제한 이미지 출력

                                        for (int y = 0; y < vFrame.height; y++) {

                                                   for (int x = 0; x < vFrame.width; x++) {

                                                             // 프레임 버퍼에서 YUV 요소를 구한다.

                                                             unsigned char Y, U, V;

                                                             Y = vFrame.data[0][vFrame.linesize[0] * y + x];

                                                             U = vFrame.data[1][vFrame.linesize[1] * (y / 2) + x / 2];

                                                             V = vFrame.data[2][vFrame.linesize[2] * (y / 2) + x / 2];

 

                                                             // 공식에 따라 YUV RGB로 변환한다.

                                                             int r, g, b;

                                                             r = int(Y + 1.3707 * (V - 128));

                                                             g = int(Y - 0.6980 * (U - 128) - 0.3376 * (V - 128));

                                                             b = int(Y + 1.7324 * (U - 128));

 

                                                             // 색상 요소가 0 ~ 255 범위를 넘지 않도록 한다.

                                                             r = max(0, min(255, r));

                                                             g = max(0, min(255, g));

                                                             b = max(0, min(255, b));

 

                                                             // 색상값으로 점을 찍는다.

                                                             COLORREF color = RGB(r, g, b);

                                                             SetPixel(hdc, x, y, color);

                                                   }

                                        }

                              }

 

                              av_packet_unref(&packet);

                              return 0;

                   }

        }

 

        av_frame_unref(&vFrame);

        av_frame_unref(&aFrame);

        return 1;

}

 

이미지 크기인 vFrame.height, vFrame.width 만큼 x, y 이중 루프를 돌며 각 좌표의 색상을 조사한다. Y값은 data[0] 배열을 순서대로 읽으면 되지만 U, V는 좌표를 절반으로 나누어 읽는다. 이렇게 구한 각 좌표의 YUV값을 RGB로 바꾼다.  변경 후 RGB 0~255 범위를 넘어갈 수도 있어 범위안에 넣어 준다. 이렇게 조사한 RGB 색상으로 점을 찍기를 이미지 면적만큼 반복한다.

드디어 첫 장면이 화면에 나타났다. 일일이 점을 찍으니 느리지만 어쨌거나 한 장면이 그려진다. 마우스 버튼을 계속 누르면 다음 장면도 잘 그린다. 계속 클릭하면 동영상을 끝까지 볼 수 있지만 속도가 너무 느리다. 원론적인 방법을 잘 보여 주는 예제로서 가치가 있지만 실용적으로 쓰기에는 아직 문제가 많다. 하나씩 개선해 볼 것이다.