2013년 11월 29일 금요일

[작업중] mjpeg 동영상 압축을 위한 jpeg verilog 코드 개발: 사전 지식

사전지식

 mjpeg은 동영상을 압축하는 표준이지만, 그 내부를 들여다 보면 가장 핵심적인 부분은  사진 압축 방식인 Jpeg이 담당하고 있다. 대표적으로 BeagleBone Black이나 Raspberry Pi에서 많이 사용되고 있는 mjpeg-streamer의 코드를 살펴보면 좀 더 확실해 진다.

 아래의 코드는 mjpeg-streamer/plugins/inputuvc에 있는 inputuvc.c 파일의 쓰레드 함수의 내용이다. 즉, 항시 실행되면서 uvc 방식의 웹캠 드라이버로 부터 영상을 얻어오고(videoIn 구조체) 해당 영상의 형식이 YUYV방식이면 compress_yuyv_to_jpeg()에 videoIn과 압축된 영상을 저장할 버퍼를 넘겨주는 작업을 항상 반복하게된다.

 /******************************************************************************  
 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 ) {  
 ......  
   /*  
    * 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);  
   }  
 .......  
 }  

 mjpeg-streamer/plugins/inputuvc밑의 jpeg_utils.c 파일내에 있는 실제적인 압축과정을 수행하는 compress_yuyv_to_jpeg()함수이다. 전체적인 흐름은 jpeg압축에 필요한 사전작업을 수행하고 struct vdIn *vd로 부터의 yuyv형태의 영상 버퍼를 rgb형태로 변환하여 ptr에 저장한다. 그리고 ptr 버퍼의 내용을 libjpeg을 이용해서 압축을 수행하는 과정이다.

 /******************************************************************************  
 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);  
 }  

 여러 곳에서 보고 되듯이 BeagleBone과 Raspberry Pi 등의 인기있는 개발 보드는 uvc 드라이버에 의해 구동되는 웹캠의 영상을 위와 같은 과정을 통하여 mjpeg 방식으로 압축한다. 하지만, mjpeg의 초당 압축 성능이 5fps정도로 만족 스럽지 않은 것으로 알려져 있다. 대부분의 시간을 잡아 먹는 과정은 YUV4:2:2를 RGB8:8:8로 변환하는 곳으로 알려져 있다.

 이와 같은 성능 저하 요인을 개선하는 방법으로 좀더 성능이 좋은 cpu를 사용하거나 yuv를 rgb로 변환하는 하드웨어 모듈을 내부에 가지고 있는 cpu를 사용하는 것이다. 가장 확실한 방법은 mjpeg 압축을 수행하는 모듈이 있는 cpu를 사용하는 것이지만, 이 같은 개선책은 어느 하나 쉬운 접근법이 존재하지 않는다. 이와 같은 이유로 mjpeg을 수행하는 독립된 외부 장치를 구성하는 방법이 될 것 같다. 그 가장 쉬운 방법은 FPGA를 활용하는 것이다. 그 이유는 하드웨어 구성에 있어서 신경쓸 것이 없고 그져 소프트웨어 적인 방법으로 모든 것이 해결 가능하기 때문이다.

  앞으로 설명할 내용은 FPGA에서 구현할 mjpeg 압축을 verilog HDL로 코딩하는 과정을 설명하는 것이다. 그 과정은 몇 가지 큰 걸림돌들이 존재하는데 성능을 극대화 하고 소모하는 FPGA의 공간을 줄이기 위해서 부동 소수점 연산의 불가피한 최적화와 이론적인 mjpeg 알고리즘과는 조금 다르게 전개 되는 DCT과정은 이해 하기가 조금 까다롭고 설명이 많이 필요하다. 이후의 내용은 jpeg 압축의 전체적인 설명, 부동 소수점 연산의 최적화, DCT 과정및 최적화, 마지막으로 이 모든 것을 연결하고 그 결과를 살펴보는 과정으로 이루어 질 것이다.

jpeg 압축

 jpeg 압축은 기본적으로 손실 압축이다. 손실 압축임에도 불구하고 jpeg압축 사진을 볼때 압축 이전의 영상과 별다른 차이를 느끼지는 못한다. 그 원인은 사람이 별로 느끼지 못하는 성분만 원래의 영상에서 제거하기 때문이다. 달리 말하면 사람이 차이를 느끼지 못하는 영상의 고주파 성분을 DCT(Discrete Cosine Transform) 통해서 구분해 내고 이진화(Quantization)과정을 통하여 제거하기 때문이다. 이와 같은 과정이 Jpeg 압축이 갖는 핵심 기능이다.

.....................

2013년 11월 25일 월요일

FreeCAD로 30mm Aluminum Profile의 3D 도면 그리기

 3D 프린터가 앞으로 많이 보급되게 됨에 따라 3D 캐드 프로그램이 새로이 관심을 받게 될 것으로 생각된다. 아무래도 돈 주고 사야하는 프로그램이 사용하기 편하고 기능도 다양하겠지만, 간단한 작업을 주로 하게될 대부분의 사람들에게 무료이면서 왠만한 기능은 다 가지고 있는 FreeCAD 프로그램은 대단히 유용하다.

 이 문서는 FreeCAD 프로그램을 가지고 3D의 30mm의 알류미늄 프로파일을 그리는 작업을 설명하고자 한다. 30mm 알루미늄 프로파일은 모두 동일한 단면을 가지고 있기 때문에, 단면에 대한 2D 도면을 그리고 그 도면을 z-축으로 확장(extrude) 하는 방법으로 생성하면 된다.

 먼저, 2D 도면을 가지고 있는 sketch에서 3D 물체를 그리기 위해서는 FreeCAD를 실행하고 View 메뉴의 Part Design모드를 선택한다.


그럼 다음과 같이 창과 메뉴 구성이 설정된다.


그리고 새로운 프로젝트를 생성하자.


Create sketch를 선택하여 새로운 스케치를 생성하자.


 Choose orientation 창에서 sketch가 XY-Plane을 갖도록 설정하고 OK를 선택하자. 그럼 다음과 같이 구성이 변경된다.


툴바를 활성화 시키기 위해선  Tree view에서 Sketch를 선택해야한다. 이제 실제로 30mm 알루미늄 프로파일의 단면이 어떻게 구성되어 있는지 살펴보아야 한다.

30mm 알루미늄 프로파일의 단면 도면.
출처 : http://www.valuframe.co.uk/Series-6-Aluminium-Profiles.html

위의 그림에서 보듯이 간단하지가 않다. 반복되는 문양의 네 개가 있는데 우선 좌측 아래 쪽을 먼저 그려나가 보도록 하자. 우선 'ㄴ'자 모양을 그리기 위해서 확장 툴바를 클릭하고 다중 직선 그리기 툴바를 선택하자.


그리고 아무 곳에 'ㄴ'자 문양의 도형을 그린다.


'ㄴ'자 의 수평 위치를 설정하기 위해서 'ㄴ'자의 수직변과 원점을 선택하여 녹색으로 만들고  두 지점의 거리를 설정하는 툴바를 선택한다.


그리고 그 거리를 15mm로 설정한다.


수직 위치도 마찬가지로 'ㄴ'자의 수평변과 원점과의 거리를 15mm로 설정한다.


 'ㄴ'자의 수평변의 길이를 설정하기 위해서 수평변을 선택하여 녹색으로 만들고 수평 길이를 설정하는 툴바를 선택한다.


그리고 수평변의 길이를 11.9mm로 설정하자.


마찬가지로 수직변의 길이도 11.9mm로 설정하면 도면에 자유도가 전혀 없는 상태를 알리며 도면의 도형이 전부 녹색으로 변화 한다.


 'ㄴ'자의 모서리의 모따기를 수행하기 위해서 확장 툴바에서 모따기를 선택한다.


 그리고 'ㄴ'자의 수직변과 수평변을 선택하고


모서리의 모따기를 수행한다.


모따기가 수행된후 그 반경을 설정하기 위해서 모따기된 호를 선택하고 반지를 설정하는 툴바를 선택한다.


그리고 그 반지름을 3mm로 설정한다.


그럼 마찬가지로 자유도가 전혀없는 완벽한 도면임을 말해주는 녹색으로 변화한다. 이 부분은 항상 중간 중간에 확인해 주어야 한다. 나중에 어느 부분에서 자유도 문제가 발생하면 프로그램이 중단되거나 다운된다. 그러니 매 단계에서 파일 저장을 주기적으로 해주어야 하고 중간 단계를 확인하기 위해서 각 단계마다 자유도가 없는 완벽한 도면이 되도록 만들어 주어야 한다.


나머지 변을 그리기 위해서 위해서 직선 그리기를 선택하자.


그리고 수직 성분만 가지고 있는 직선을 그리자. 직선을 그리다 보면 주변의 도형에 따라서 접선 성분이나. 접점 성분이 첨가된 직선이 만들어 지기도 하는데 항상 확인해 주어야 한다. 만약 이러한 불필요한 성분이 있다면 좌측 Tasks 탭에서 해당 성분을 찾아 일일이 제거해 주도록 하자.


그리고 접점 성분을 추가하기 위해서 접점이 되어야 할 두 점을 선택하고 접점을 만들어 주는 툴바를 선택하자.


그리고 추가된 직선의 길이를 설정하자.


길이는  3mm이다.


마찬가지로 반대편도 3mm의 수평선의 접선을 그리자.


아래의 그림에서 보듯이 3mm의 길이를 갖는 수평 및 수직 접선을 그리자.


이제는  1.5mm의 길이를 갖는 수평 및 수직 접선을 아래의 그림과 같이 그리자.


같은 작업을 수직 및 수평 방향에 대해서 수행하자.


이제는 길이의 제한이 없는 수직 및 수평 접선을 그리자. 그리고 두 직선이 접점을 형성하도록 적당히 길이를 조절하자.


접점을 형성하는 두 직선을 접점을 경계로 자르기를 하기 위해서 확장 메뉴의 trim기능을 선택하자.


그리고 수직 및 수평 방향으로 튀어 나온 부분을 제거하자. 그럼 자유도가 없는 완변한 도면임을 나타내는 녹색으로 변하게 될 것이다.


자 이제는 내부의 사각형을 그리기 위해서 툴바에서 사각형 그리기를 선택하자.


그리고 사각형 성분 이외의 성분, 접점, 접선등 과 같이 다른 도형과 연계되는 성분이 없도록 작업 영역과 멀리 떨어져서 사각형을 그리자. 만약 위와 같은 성분이 생성 되면 Tasks 탭에 가서 해당 성분을 수동으로 제거 하도록 하자.


사각형의 윗 선분의 위치를 지정하기 위해서 아래의 그림과 같이 사각형의 한 꼭지점과 한 선분을 선택하고 두 거리를 지정하는 툴바를 선택하자.


그 거리는 1.5mm로 설정하자.


그럼 사각형의 윗 선분과 이전의 도면과의 상대적이 수직 위치는 고정되게 된다.


이제는 사각형의 우측 변과 이전 도면의 상대 위치를 설정하기 위해서 사각형 우측의 한 꼭지점과 이전 도면의 한 변을 선택하고 이 거리를 지정하는 툴바를 선택하자.


마찬가지로 그 거리를 1.5mm로 하자. 그럼 다음 그리고 같이 사각형의 모양이 변하게 된다.


나머지 변들도 마찬가지로 1.5mm의 간격을 갖도록 설정하자.


그리고 새로 그려진 사각형의 좌측 하단의 꼭지점에 모따기를 수행하고 그 반경은 1.5mm로 설정하자.


이제는 원을 추가하기 위해서 원 그리는 툴바를 선택하자.


그리고 이전 도면의 간섭을 받지 않도록 이전 도면과 좀 떨어진 위치에 원을 그리자.


원의 중심을 원점에 위치 시키기위해서 원의 중심과 원점을 선택하고 접점을 만들어 주는 툴바를 선택하자.


그리고 마지막 남은 자유도인 원의 반지름을 설정하기 위해서 원주를 선택하고 반지를 설정하는 툴바를 선택하자.


반지름을 2.5mm로 설정하자.


반지름이 지정되면 자유도가 없는 상태를 알리는 녹색으로 도면이 변하게 된다.


원점에 중심을 있고 10.5mm의 가로 세로 길이를 갖는 사각형을 그리자.


직선을 추가하자. 마찬가지로 주의 할 것은 직선과 수직성분을 포함한 어떠한 성분도 갖지 않도록 이전의 도면과 멀리 떨어져서 그려야 한다는 것이다.


아래의 그림과 같이 각 꼭지점과 해당 직선이 거리 1mm를 유지하도록 설정한다. 아직 이 직선의 길이는 설정하지 않는다. 대신 양 쪽으로 접점을 형성할 정도로 알맞게 길이를 조절하자.


비슷한 직선을 하나 더 추가해 보자.


이전에 추가한 직선과 마찬가지로 1mm의 간격을 유지하도록 설정하자.


추가한 두 개의 직선으로 생기는 접점에서 발생하는 쓸모 없는 직선을 제거하기 위해서 trim을 수행하자.


이와 같은 작업을 나머지 세 모서리에 대해서 반복하자. 그럼 아래의 그림과 같이 30mm의 알류미늄 프로파일의 단면이 형성될 것이다.


이제는 이 작업 Sketch를 떠나기 위해서 esc 키를 누르자.


2D 도면을 z축으로 확장해서 3D 객체를 만들어 내는 명령을 Autocad에서 extrude라고 했는데 FreeCAD에서는 해당 scatch를 선택하고 pad로 만드는 작업이 이에 해당하는 것 같다. 위의 그림에서 보듯이 해당 scatch에 커서를 놓고 Tasks탭에서 Pad 메뉴를 선택하자. 아니면 아래의 그림과 같이 툴바에서 Pad툴바를 선택하고 extrude 할 scatch를 선택하자.


그럼 아래의 그림과 같이 높이 10mm의 알류미늄 프로파일이 생성된다. 높이의 설정은 좌측하단의 Tasks 탭 안에 Pad parameters의 length를 조절하여 변경할 수 있다.


그럼 길이를 100mm로 설정해보자. 아래의 그림과 같이 높이 100mm를 갖는 30mm 알류미늄 프로파일을 완성할 수 있다.