이번에 다뤄 볼 주제는 화상압축 라이브러리 구축 과정입니다. 화상압축에 관한 알고리즘 문제는 다른 포스트를 통해서 다루도록 하고, 여기서는 라이브러리 구축 과정 자체에만 포커스를 가져가겠습니다.
우선 이 포스트에서 다루는 라이브러리 및 소스는 아래 주소를 참고하시기 바랍니다.
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 파일