상세 컨텐츠

본문 제목

화상회의 프로젝트 에피소드 4 - 화상압축 라이브러리 구축

프로젝트/Ah!FreeCa

by ryujt 2010. 11. 30. 13:50

본문

이번에 다뤄 볼 주제는 화상압축 라이브러리 구축 과정입니다.  화상압축에 관한 알고리즘 문제는 다른 포스트를 통해서 다루도록 하고, 여기서는 라이브러리 구축 과정 자체에만 포커스를 가져가겠습니다.

우선 이 포스트에서 다루는 라이브러리 및 소스는 아래 주소를 참고하시기 바랍니다.

Subversion : https://dev.naver.com/svn/megacast (password is 'anonsvn')

아래 [그림 1]은 압축용 클래스와 디코딩용 클래스에 대한 다이어그램 입니다.  상당히 허전해서 함께 올렸습니다.

[그림 1] 엔코더와 디코더의 Class Diagram


우선 엔코더 클래스인 TMegaCastEncoder의 멤버들을 살펴보겠습니다.
  • Method
    • procedure Clear;
    • procedure Execute(AData:pointer; AWidth,AHeigh:integer);
  • Event
    • property OnNewData : TDataEvent;

procedure Clear;
엔코더는 이전 화면과 비교하면서 압축을 진행합니다.  완전히 새로운 동영상을 압축하려면, Clear를 통해서 이전에 진행하였던 작업을 삭제합니다.

procedure Execute(AData:pointer; AWidth,AHeigh:integer);
화면(프레임)을 압축합니다.
  • AData에는 Bitmap raw 데이터의 주소를 입력하시면 됩니다.
    • Bitmap은 무조건 32비트 컬러만 사용해야 합니다.  특별한 이유가 있었던 것은 아니고, 32비트 컬러를 사용해도 충분한 압축이 이뤄지기 때문에 인터페이스를 간략하게 고정하였습니다.
  • AWidth,AHeigh에는 Bitmap의 가로 세로 Picxel 크기를 입력하시면 됩니다.
property OnNewData : TDataEvent;
Execute를 실행하고 압축이 이뤄지면서 발생하는 데이터에 대한 이벤트 입니다.
  • 하나의 프레임을 32x32 크기의 블럭으로 나눠서 압축하게 됩니다.  따라서, 최대 전체 화면의 크기를 32x32 블록으로 나눈 숫자만큼 이벤트가 발생합니다.
  • 화면의 데이터의 변화가 없는 블록은 생략됩니다.


이번에는 디코더 클래스 TMegaCastDecoder의 멤버를 살펴보겠습니다.
  • Method
    • procedure DataIn(AData:pointer; ASize:integer);
    • function GetBitmap(ABitmap:TBitmap):boolean;
procedure DataIn(AData:pointer; ASize:integer);
엔코더에서 압축된 블록의 데이터의 포인터(메모리 주소)와 데이터 크기를 입력하시면 압축을 해제하고, 내부의 Bitmap 데이터에 복원 시켜둡니다.

function GetBitmap(ABitmap:TBitmap):boolean;
압축 해제된 전체 화면을 보고 싶을 때, 사용합니다.  이미 한 번 받은 화면이 변화가 없다면 Result는 false가 되며, 화면은 가져오지 않습니다.


아래의 [소스 1]은 압축을 하는 과정의 일부입니다.  전체 소스는 위에서 설명한 Subversion 저장소의 Samples/Screen Thief 폴더에 있습니다.

[소스 1]
procedure TfmMain.on_Schedule(Sender: TObject; ATick: cardinal);
begin
  if FSocket.Connected then begin
    FScreenCapture.Capture;
    FMegaCastEncoder.Execute(
      FScreenCapture.Bitmap.ScanLine[FScreenCapture.Bitmap.Height-1],
      FScreenCapture.Bitmap.Width,
      FScreenCapture.Bitmap.Height
    );
  end;
end;
5: 라인에 보시면 엔코더의 Execute 메소드의 실행 과정이 보입니다.  화면을 캡쳐하는 클래스에서 Bitmap의 ScanLine 멤버를 이용하여, Bitmap raw 데이터의 주소를 얻어옵니다.  

이때, Bitmap의 가로 라인이 raw 데이터에는 반대로 저장되어 있기 때문에 , ScanLine[0]가 아닌 ScanLine[FScreenCapture.Bitmap.Height-1]으로 지정해야 하는 것을 유의하시기 바랍니다.

이후, Bitmap의 가로 세로 크기를 파라메터로 넘겨주면 압축은 완료됩니다.  (어때요, 참쉽죠? ㅡ.ㅡ)

아래의 [소스 2]는  압축이 된 후 발생하는 OnNewData 이벤트 처리 과정입니다.  파라메터로 넘어 온 AData와 ASize를 소켓 클래스의 Send 메소드에 바로 대입하여 전송하는 것이 전부입니다.

[소스 2]
procedure TfmMain.on_Received(Sender: TObject; AData: pointer; ASize: integer);
begin
  FMegaCastDecoder.DataIn(AData, ASize);
end;

procedure TfmMain.tmRefreshTimer(Sender: TObject);
begin
  tmRefresh.Enabled := false;
  try
    if FMegaCastDecoder.GetBitmap(Image.Picture.Bitmap) then Image.Repaint;
  finally
    tmRefresh.Enabled := true;
  end;
end;


아래의 [소스 3]은 수신된 데이터를 처리하는 과정입니다.

[소스 3]
procedure TfmMain.on_Received(Sender: TObject; AData: pointer; ASize: integer);
begin
  FMegaCastDecoder.DataIn(AData, ASize);
end;

procedure TfmMain.tmRefreshTimer(Sender: TObject);
begin
  tmRefresh.Enabled := false;
  try
    if FMegaCastDecoder.GetBitmap(Image.Picture.Bitmap) then Image.Repaint;
  finally
    tmRefresh.Enabled := true;
  end;
end;

3: 라인에서는 수신된 데이터를 DataIn 메소드를 통해서 압축을 해제하는 과정입니다.

10: 라인은 압축 해제된 화면을 Image 컴포넌트를 이용해서 화면에 표시하는 과정입니다.  변화가 없으면 GetBitmap 메소드의 결과 값이 false 이기 때문에 화면 갱신을 하지 않도록 하였습니다.  데이터가 들어 올 때마다 화면을 갱신하는 것은 비효율적이기 때문에 주기적으로 걸러서 표시하고 있습니다.

원격제어의 경우에는 속도가 중요하기 때문에 바로 바로 표시 할 수 있도록 하면서 블록을 그리는데, 전체가 갱신되지않도록 해야 합니다.  데이터가 입력될 때마다 화면을 갱신하면 CPU 사용률이 증가합니다.


위에서 설명한 데로, 압축 --> 전송 --> 해제 의 과정은 상당히 간단한 인터페이스로 구현 될 수 있습니다.  이제 실제 내부의 동작을 하나씩 추적해보도록 하겠습니다.

우선 [그림 2]는 MegaCastEncoder의 동적 분석입니다.

[그림 2] MegaCastEncoder의 동적 분석

  • MegaCastEncoder는 Slice, FrameFilter, BlockEncoder으로 구성되어 있습니다.
  • Slice는 하나의 프레임(화면)을 정해진 크기의 블록으로 자르는 기능을 제공합니다.
  • FrameFilter는 이전 화면과 달라진 블록만을 찾아내어 압축하도록 합니다.
  • BlockEncoder는 걸러진 블록을 실제 압축하는 기능을 제공합니다.
  • MegaCastEncoder.Execute()가 실행되면 Slice.Slice()를 통해서 화면을 블록으로 조각냅니다.
  • 이어서 Slice.GetBuffer()를 이용하여 잘라진 데이터 전체를 가져 옵니다.
    • 결과로 nil 또는 Data가 리턴 될 수 있음을 표시하였는데, 실제로 nil은 발생하지 않습니다.  코드 상에서는 그런 일이 벌어지면 무시하도록 되어 있습니다.
  • Data를 리턴 받으면 FrameFilter.IsClear()를 통해서 이전 데이터가 없는 빈상태인지 확인합니다.  즉, 처음으로 압축하는 것이라면, FrameSize(화면크기)를 OnNewData 이벤트를 통해서 처리합니다.  
    • 이벤트 이후의 프로세스는 생략되어 있습니다.  
    • 이것은 MegaCastEncoder를 사용하는 Actor 측에서 MegaCastEncoder.OnNewData 이벤트를 활용하여 자유롭게 처리하면 됩니다.  (소켓으로 보내거나, 파일로 저장하는 등)
  • 첫 번째 프레임이든 아니든 일단 FrameFilter.Filtering()을 통해서 변화된 블록만 걸러냅니다.
    • 첫 번째 프레임이면 모든 블록이 사용되게 됩니다.
  • 변화된 블록은 FrameFilter.OnFiltered 이벤트를 통해서 리턴 받습니다.  리턴받은 블록 데이터는 곧바로, BlockEncoder.Encode() 메소드를 통해서 실제 압축되고, MegaCastEncoder.OnNewData 이벤트를 발생 시킵니다.

아래의 [그림 3]은 MegaCastDecoder의 동적 분석 내용입니다.

[그림 3] MegaCastDecoder의 동적 분석

  • BlockDecoder와 FrameSticker는 MegaCastDecoder의 내부 객체 입니다.
  • BlockDecoder는 압축된 블록 데이터의 압축을 해제 합니다.
  • FrameSticker는 블록을 원래의 위치에 붙여서 Bitmap으로 재 구성해 줍니다.
  • 우선, Actor에 의해서 MegaCastDecoder.DataIn() 메소드가 호출되는 것으로 표현했습니다.
    • 소켓 등을 통해서 들어온 데이터를 DataIn() 메소드의 파라메터로 전달해 주시면 됩니다.
    • 데이터(패킷)의 종류가 다양하지만, 여기서는 간단하게 FrameSize와 BlockData 두 종류의 데이터에 대해서만 설명합니다.
  • FrameSize 데이터가 수신된 경우에는 FrameSticker.SetFrameSize() 메소드를 통해서 Bitmap의 크기를 지정합니다.
  • BlockData가 수신 된 경우에는 BlockEncoder.DataIn()을 통해서 압축을 해제한 뒤 FameSticker.DrawBitmap()으로 Bitmap 위에 그려주게 됩니다.
  • Actor가 화면의 데이터를 요구하는 경우에는 MegaCastDecoder.GetBitmap() 메소드를 호출하게 됩니다.  이후, FrameSticker에 있는 실제 Bitmap 데이터를 가져오면서 메소드 호출으 결과 값으로는 불린 데이터를 사용합니다.  ([소스 3]의 10: 라인 참고)

디미터 법칙에 의해서 MegaCastDecoder.FrameSticker.GetBitmap 대신 MegaCastDecoder.GetBitmap을 사용하도록 하였습니다.
그리고, FrameSticker는 외부에서 존재를 알 필요가 없기 때문에 private 속성으로 지정되어 있습니다.

[첨부 1] 본문에 사용된 Visio 파일

관련글 더보기