2013년 10월 4일 금요일

mjpeg-streamer의 작동원리

 앞으로 사용해야 할 일이 발생할 것 같아서 시간도 넉넉하고 해서 mjpeg-streamer를 분석해 보려고 한다. 가장 핵심적인 부분은 MJPEG 방식으로 압축하는 부분이기에 실행파일의 껍데기에 해당하는 mjpeg-streamer.c파일에는 영양가 있는 내용이 없다. 거의 매개변수 해석해서 plugin에 해당하는 *.so 파일을 읽어 들이도록 해서 입력과 출력을 지정하는 일이 대부분이다.

 내가 원하는 MJPEG Encoding에 대한 내용은 plugin에 해당 하는 소스코드가 있는 mjpg-streamer/plugins/input_uvc/input_uvc.c에 있다. 그리고 핵심적인 부분을 살펴보면 encoding 부분에서 jpeglib를 활용하고 있다. 어쩐지 소스가 짧더라 했다.

 그래도 사진 압축 방법인 jpeglib로 어떻게 MJPEG 압축을 수행하게 되는지 알아보는 것이 필요하다. 구체적으로 다음 cam_thread() 함수에 uvc 드라이버를 이용해서 카메라의 영상을 실제적으로 MJPEG으로 압축하는 기능을 수행한다. 우선, 소스 코드는 다음과 같다. 출처는 http://sourceforge.net/p/mjpg-streamer/code/HEAD/tree/mjpg-streamer/plugins/input_uvc/input_uvc.c 이다.

/******************************************************************************
Description.: this thread worker grabs a frame and copies it to the global buffer
Input Value.: unused
Return Value: unused, always NULL
******************************************************************************/
void *cam_thread( void *arg ) {
  /* set cleanup handler to cleanup allocated ressources */
  pthread_cleanup_push(cam_cleanup, NULL);

  while( !pglobal->stop ) {

    /* grab a frame */
    if( uvcGrab(videoIn) < 0 ) {
      IPRINT("Error grabbing frames\n");
      exit(EXIT_FAILURE);
    }
  
    DBG("received frame of size: %d\n", videoIn->buf.bytesused);

    /*
     * Workaround for broken, corrupted frames:
     * Under low light conditions corrupted frames may get captured.
     * The good thing is such frames are quite small compared to the regular pictures.
     * For example a VGA (640x480) webcam picture is normally >= 8kByte large,
     * corrupted frames are smaller.
     */
    if ( videoIn->buf.bytesused < minimum_size ) {
      DBG("dropping too small frame, assuming it as broken\n");
      continue;
    }

    /* copy JPG picture to global buffer */
    pthread_mutex_lock( &pglobal->db );

    /*
     * If capturing in YUV mode convert to JPEG now.
     * This compression requires many CPU cycles, so try to avoid YUV format.
     * Getting JPEGs straight from the webcam, is one of the major advantages of
     * Linux-UVC compatible devices.
     */
    if (videoIn->formatIn == V4L2_PIX_FMT_YUYV) {
      DBG("compressing frame\n");
      pglobal->size = compress_yuyv_to_jpeg(videoIn, pglobal->buf, videoIn->framesizeIn, gquality);
    }
    else {
      DBG("copying frame\n");
      pglobal->size = memcpy_picture(pglobal->buf, videoIn->tmpbuffer, videoIn->buf.bytesused);
    }

#if 0
    /* motion detection can be done just by comparing the picture size, but it is not very accurate!! */
    if ( (prev_size - global->size)*(prev_size - global->size) > 4*1024*1024 ) {
        DBG("motion detected (delta: %d kB)\n", (prev_size - global->size) / 1024);
    }
    prev_size = global->size;
#endif

    /* signal fresh_frame */
    pthread_cond_broadcast(&pglobal->db_update);
    pthread_mutex_unlock( &pglobal->db );

    DBG("waiting for next frame\n");

    /* only use usleep if the fps is below 5, otherwise the overhead is too long */
    if ( videoIn->fps < 5 ) {
      usleep(1000*1000/videoIn->fps);
    }
  }

  DBG("leaving input thread, calling cleanup function now\n");
  pthread_cleanup_pop(1);

  return NULL;
}

 이름에서도 알 수 있듯이 영상을 받아오는 쓰레드이다. 전체적인 흐름은 uvc 드라이버로 부터 영상 프레임이 존재하는지 감시하고 이 영상을 global변수의 버퍼 내로 복사한다. 그리고 복사된 영상의 포멧에 따라서 jpeg 압축을 수행한다. 여기서, uvc 드라이버는 좀 더 검색해 보아야 할 내용이지만 잠시 살펴본 결과 많은 종류의 USB 캠을 구동하게 만들어 주는 드라이버이다. 위의 루틴이 쓰레드이기 때문에 메모리를 관리하기 위해서 필요한 mutex나 broadcast 같은 것들은 코드를 이해하는데 불필요하기 때문에 무시하고 넘어가자.

 아무튼 핵심적인 내용은 compress_yuyv_to_jpeg()에 있다. 이 루틴은 http://sourceforge.net/p/mjpg-streamer/code/HEAD/tree/mjpg-streamer/plugins/input_uvc/jpeg_utils.c 에 있다. 이 루틴을 실행하기 위해선 uvc 드라이버로 부터 넘어 오는 영상 데이터가 V4L2_PIX_FMT_YUYV이어야 한다. 일반적으로 YUV4:2:2라고 일컷는 Bayer filter 형식의 영상일 경우 jpeg 압축을 수행한다.

/******************************************************************************
Description.: yuv2jpeg function is based on compress_yuyv_to_jpeg written by
              Gabriel A. Devenyi.
              It uses the destination manager implemented above to compress
              YUYV data to JPEG. Most other implementations use the
              "jpeg_stdio_dest" from libjpeg, which can not store compressed
              pictures to memory instead of a file.
Input Value.: video structure from v4l2uvc.c/h, destination buffer and buffersize
              the buffer must be large enough, no error/size checking is done!
Return Value: the buffer will contain the compressed data
******************************************************************************/
int compress_yuyv_to_jpeg(struct vdIn *vd, unsigned char *buffer, int size, int quality) {
  struct jpeg_compress_struct cinfo;
  struct jpeg_error_mgr jerr;
  JSAMPROW row_pointer[1];
  unsigned char *line_buffer, *yuyv;
  int z;
  static int written;

  line_buffer = calloc (vd->width * 3, 1);
  yuyv = vd->framebuffer;

  cinfo.err = jpeg_std_error (&jerr);
  jpeg_create_compress (&cinfo);
  /* jpeg_stdio_dest (&cinfo, file); */
  dest_buffer(&cinfo, buffer, size, &written);

  cinfo.image_width = vd->width;
  cinfo.image_height = vd->height;
  cinfo.input_components = 3;
  cinfo.in_color_space = JCS_RGB;

  jpeg_set_defaults (&cinfo);
  jpeg_set_quality (&cinfo, quality, TRUE);

  jpeg_start_compress (&cinfo, TRUE);

  z = 0;
  while (cinfo.next_scanline < vd->height) {
    int x;
    unsigned char *ptr = line_buffer;

    for (x = 0; x < vd->width; x++) {
      int r, g, b;
      int y, u, v;

      if (!z)
        y = yuyv[0] << 8;
      else
        y = yuyv[2] << 8;
      u = yuyv[1] - 128;
      v = yuyv[3] - 128;

      r = (y + (359 * v)) >> 8;
      g = (y - (88 * u) - (183 * v)) >> 8;
      b = (y + (454 * u)) >> 8;

      *(ptr++) = (r > 255) ? 255 : ((r < 0) ? 0 : r);
      *(ptr++) = (g > 255) ? 255 : ((g < 0) ? 0 : g);
      *(ptr++) = (b > 255) ? 255 : ((b < 0) ? 0 : b);

      if (z++) {
        z = 0;
        yuyv += 4;
      }
    }

    row_pointer[0] = line_buffer;
    jpeg_write_scanlines (&cinfo, row_pointer, 1);
  }

  jpeg_finish_compress (&cinfo);
  jpeg_destroy_compress (&cinfo);

  free (line_buffer);

  return (written);
}


 위의 코드는 기본적으로 libjpeg을 사용한다. 이 라이브러리는 디폴트로 압축된 결과를 기본 입출력에 해당하는 하드 디스크에 파일로 저장한다. 그래서 압축 결과물이 메모리에 담겨 지도록 다음과 같이 설정한다.

  /* jpeg_stdio_dest (&cinfo, file); */
  dest_buffer(&cinfo, buffer, size, &written);

  그리고 YUV 4:2:2 방식의 영상을 RGB로 변환하는 Interpolation 과정을 수행하기 위한 버퍼(line_buffer)를 새로 할당하고, uvc로 부터 날라온 YUV 4:2:2 형식의 영상 버퍼도 지정한다.

  line_buffer = calloc (vd->width * 3, 1);
  yuyv = vd->framebuffer;

 나머지 것들은 libjpeg을 이용할 때 수행하는 표준적인 방법으로 압축을 수행한다.

  while (cinfo.next_scanline < vd->height) {
    int x;
    unsigned char *ptr = line_buffer;

    for (x = 0; x < vd->width; x++) {
      int r, g, b;
      int y, u, v;

      if (!z)
        y = yuyv[0] << 8;
      else
        y = yuyv[2] << 8;
      u = yuyv[1] - 128;
      v = yuyv[3] - 128;

      r = (y + (359 * v)) >> 8;
      g = (y - (88 * u) - (183 * v)) >> 8;
      b = (y + (454 * u)) >> 8;

      *(ptr++) = (r > 255) ? 255 : ((r < 0) ? 0 : r);
      *(ptr++) = (g > 255) ? 255 : ((g < 0) ? 0 : g);
      *(ptr++) = (b > 255) ? 255 : ((b < 0) ? 0 : b);

      if (z++) {
        z = 0;
        yuyv += 4;
      }
    }

    row_pointer[0] = line_buffer;
    jpeg_write_scanlines (&cinfo, row_pointer, 1);
  }

  jpeg_finish_compress (&cinfo);
  jpeg_destroy_compress (&cinfo);

 대체적인 내가 필요한 부분은 다 파악한 것 같다. 여기 저기서 보고 되는 문제점인 YUV를 RGB로 변환하는 과정이 너무나도 씨피유를 잡아 먹는 다는 점이다. 우선 가지고 테스트 해볼 만한 보드가 없어서 위의 코드에서 inline assembly를 사용한다던가 하는 개선책이 효과적일지 알아 볼 수 없지만, 추후에 이거 너무 느린걸 하면 위의 코드를 건드려 보도록해야 겠다.

 참고로 지금 불티나게 팔리고 있는 BeagleBone Black과 Raspberry Pi에서 VGA 급의 영상은 5fps도 안나온다고 한다. 그리고 그 원인의 대부분은 YUV->RGB 변환에 너무 많은 것이 소요하고 있다는 점이다.

 그리고 하나 더 mjpeg 압축이 어떻게 이루어 지는 명확해 졌다. 그냥 한 프레임씩 jpeg으로 압축하는 게 전부 인것 같다. 좀더 살펴 보아야 하겠지만, 위의 코드로는 그렇게 파악되어 진다.

댓글 6개:

  1. 안녕하세요 영상쪽 공부하는 학생입니다. 글 잘읽었습니다.
    궁금한부분이 있는데요 JPEG에서 색상모드에서요 YUV방식과 YIQ방식은 RGB로 변환하는 공식만 다르고 나머지 압축하는 부분은 같나요?? 답변부탁드립니다.

    답글삭제
    답글
    1. 그리고 소스상에서 RTSP방식으로 스트리밍할때요. MJPEG-Streaming방식이 한 프레임씩 JPEG압축한다음 웹상으로 보낸다음 수신컴퓨터에서 압축을 푸는 방식으로 진행되는건가요??

      삭제
    2. 네 그런 거 같습니다.

      삭제
    3. 이해가 안가는게요 수신컴퓨터에서 압축을 푸는게 맞다고하셨는데...
      핸드폰 이런걸로도 스트리밍이 가능하던데.. 하드웨어적으로 MJPEG-Streaming 이 내장되어있는건가요?

      삭제
    4. 보통 스트리밍 방식에 사용되는 압축은 H.264로 대부분의 MCU에 내장되어 있어서, 하드웨어적으로 Encoding합니다. MJPEG의 경우는 실질적으로 JPEG Encoder가 MCU에 내장되어 있다면 이러한 하드웨어를 통한 Encoding이 가능 하지만, JPEG Encoder가 내장된 MCU는 매우 드문 경우이고, 아직 그러한 use case를 경험해 보지 못했습니다.

      삭제
    5. 그쿤요 제가 Cortex-A53 을 사용하고있는데요. MJPEG Streaming을 지원한다고 써있어서요.
      그래서 제가 MJPEG Streaming 오픈소스를 사용한거거든요. 아 님이 말하신거는 MCU 에서MJPEG Streaming는 지원하나 JPEG Encoder는 지원얘기는 없어서 대부분 소스를 사용하는건가요??

      삭제