連載2画像転送システム |
■サンプルプログラム
ライセンスはフリーウエアとなっています。ソースファイルは変更の有無、コンテンツの有料/無料を問わず、ご自身の責任の元自由にご利用頂けます。
コーデックを使ったエンコード(画像の圧縮)及び、デコード(画像の復元)の参考になれば幸いです。
【使い方】
プログラムはサーバーとクライアントに分かれています。
設定は付属のiniファイルをメモ帳で編集して下さい。
サーバー側はポート番号の設定のみです。
クライアント側はポート番号とサーバーのアドレスを設定してください。
設定が終わったらサーバー側を先に起動します。次にクライアントを起動すると勝手に接続されサーバー側のデスクトップ画像がどんどん転送されてきて表示します。
なお、AMV3ビデオコーデックがインストールされていないと利用できません。他のコーデックを利用する場合はプログラム内のFourCCを書き換えて下さい。
【コンパイル】
普通にコンパイルできると思いますが、サーバー側のみマルチスレッドライブラリを利用しています。その辺が原因でコンパイルできない場合は31行目にある#define USE_THREADをコメントアウトして下さい。コメントアウトするとマルチスレッド系のAPIは使いません。
■連載2-1:画像転送システムその1(2009.2.13)
ファンタジーリモートのメイン処理とも言える画像転送システムについて、コーデックの使い方を中心に紹介していこうと思います。
プログラムは画像を送る側の「サーバー」と、受け取った画像を表示する「クライアント」の2つからなり、主な処理は次のようになります。
デスクトップキャプチャーにはGDIのBitBlt()を使いDIBデータを取得します、それをコーデックのコンプレッサーで圧縮して、最後にTCPのソケット通信で送信します。この処理を延々と繰り返すのがサーバー側のプログラムです。
一方クライアント側はソケット通信でデータを受信するところから始まります。受信したデータはコーデックのデコンプレッサーを使ってDIBデータを復元します。復元されたDIBはGDIのStretchBlt()でウインドウいっぱいに表示します。クライアント側はこの処理を延々と繰り返します。
実際にはサーバー側の処理が早すぎるとクライアントの処理が間に合わず通信データが溜まって行ってしまうので、サーバー側にはウエイトを入れてフレームレートをコントロールします。
ちなみに、アマレココの場合はキャプチャーと圧縮までがサーバー側プログラムと同じで後はaviのヘッダーを付けてファイルへ保存する処理となります。送信部分がファイル処理に置き換わるだけなので殆ど同じ仕組みで動いていると言えますね。
次回からは各々の処理についてもう少し詳しく書いていきたいと思います。
■連載2-2:画像転送システムその2(2009.2.15)
今回は、サーバー側のキャプチャー部です。すべてGDIのAPIで処理しますので簡単です。
キャプチャー処理はオフスクリーンのデバイスコンテキスト(ghOffDC)を一つ用意して、そこにデスクトップのデバイスコンテキスト(ghDC)から画像をブリットすることで行います。また、取り込んだ画像はコーデックに送る必要があるので、オフスクリーンにはDIBセクションをアタッチし、画像データへアクセスするためのアドレス(glpOff)を取得しておきます。
DIBセクションでは色数等を自由に設定できますが、コーデックが対応している色数にする必要があるので注意して下さい。AMV3コーデックの場合はRGB32かRGB24が使えます。また、デスクトップの色数も同じにしておくと、最も効率良く処理することが出来ます。と言うよりも色数が違うとGDIによる変換処理が加わりものすごく遅くなりますので、そういった使い方は意図的に出来ないようにしておいた方が良いでしょう。
最近はデスクトップの色数をRGB24にすることはマレ(若しくはできない?)なので、殆どの場合はRGB32がベストと言うことになります。
// GDI処理で使う変数 HDC ghDC = NULL; //
デスクトップのデバイスコンテキスト HDC ghOffDC = NULL; //
オフスクリーンのデバイスコンテキスト(ここにデスクトップ画像を取り込む) LPVOID glpOff = NULL; //
オフスクリーンのアドレス HBITMAP ghOffBmp = NULL; //
オフスクリーンのビットマップハンドル HBITMAP ghOffBmpOld
= NULL; // ビットマップハンドルの控え(不要?) DWORD gdwSizeW = 0; //
デスクトップの横サイズ DWORD gdwSizeH = 0; //
デスクトップの縦サイズ // GDI処理の初期化 BOOL
GDIInit( SOCKET sock ) { // デスクトップの画像サイズを取得 gdwSizeW
= GetSystemMetrics( SM_CXSCREEN ); gdwSizeH
= GetSystemMetrics( SM_CYSCREEN ); // デスクトップのデバイスコンテキスト取得 ghDC = GetDC( NULL ); //
DIBSection作成 // ・コーデックはこのDIB(glpOff)を元に圧縮します。 BITMAPINFO bi; ZeroMemory(
&bi, sizeof(bi) ); bi.bmiHeader.biSize =
sizeof(BITMAPINFOHEADER); bi.bmiHeader.biBitCount = 32; //
RGB32なので32を設定する bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biWidth =
gdwSizeW; //
画像サイズ bi.bmiHeader.biHeight = gdwSizeH; //
画像サイズ bi.bmiHeader.biSizeImage = gdwSizeW*gdwSizeH*4; // RGB32で計算、1画素あたり4バイト bi.bmiHeader.biCompression
= BI_RGB; ghOffBmp = CreateDIBSection(
NULL, &bi, DIB_RGB_COLORS, (void **)(&glpOff),
NULL, 0); // デバイスコンテキストも取得しておく(キャプチャー処理で使います) ghOffDC = CreateCompatibleDC( ghDC ); ghOffBmpOld = (HBITMAP)SelectObject( ghOffDC,
ghOffBmp ); return
TRUE; } // GDI処理 BOOL
GDIMain( void ) { // キャプチャー処理 BitBlt( ghOffDC, 0, 0, gdwSizeW, gdwSizeH, ghDC, 0, 0, SRCCOPY //
CAPTUREBLT を加えると半透明ウインドウも一緒にキャプチャーします。 ); return
TRUE; } |
基本的にキャプチャー処理はBitBlt()で行いますが、GDI処理の遅いパソコンの場合は一度で画面全体を取り込もうとするとゲームなどのfpsがガクッと落ちてしまいますので、画面を何分割かして少しずつ取り込むようにした方が好ましい場合もあります。アマレココやファンタジーリモートの分散処理オプションがこれに相当します。原理はこちら。
■連載2-3:画像転送システムその3(2009.2.17)
今回はサーバー側のコーデックによる圧縮処理(準備編)を紹介します。
コーデックを使うにはvfw.hとaviriff.hの2つのヘッダーファイルをインクルードすることと、vfw32.libのリンクが必要です。
コーデックの処理はIC〜というAPIを駆使して行きますが、ややっこしいのは入力フォーマット(圧縮前のフォーマット)と出力フォーマット(圧縮後のフォーマット)の扱いでしょうか。これらは基本的にBITMAPINFOという構造体を使いますが、コーデックによってはこの構造体が拡張される場合があるので構造体のサイズは不定となります。なので、変数の定義としてはポインタにしておき、必要に応じてその都度バッファを確保するようにします。
先ずは、入力フォーマット(圧縮前のフォーマット=DIBセクションと同じ)を用意します。これはBITMAPINFOでいいので簡単です。また設定内容はDIBセクションと同じにして下さい。
次に出力フォーマットを用意しますが、これはコーデックから取得する必要があり少々面倒です。先にコーデックへアクセスする為のハンドルを取得しましょう。
ハンドルの取得はICOpen()の二番目の引数に使いたいコーデックのFourCCを指定することで得られます。今回はAMV3ビデオコーデックを使うので、この部分はFCC('AMV3')と記述します。FCC()はマクロでaviriff.hの中で定義されています。これを使うと文字列を簡単にFourCCコードに変換してくれます。
ハンドルの取得が出来たら次の3段階に分けて出力フォーマットを取得します。
(1)出力フォーマットのサイズを得る。
出力フォーマットは拡張されている可能性があるので、構造体のバイト数が幾つになるか問い合わせましょう。ICCompressGetFormat()の3番目の引数に0を指定すると戻り値として必要なバイト数が返されます。
(2)出力フォーマットを格納するバッファを確保する。
1で取得したバイト数分だけバッファを確保します。
(3)出力フォーマットを得る。
ICCompressGetFormat()の3番目の引数に2のバッファを指定することで出力フォーマットを得ることが出来ます。
入力と出力のフォーマットが用意できたらICCompressBegin()で圧縮処理の初期化、ICCompress()で圧縮、ICCompressEnd()で圧縮終了となります。
また、これとは別に圧縮後のデータを格納するのに最大何バイトのバッファが必要か調べます。多くの場合ICCompressGetFormat()で得た出力フォーマットのglpbiOut->bmiHeader.biSizeImageに格納されていますが、この部分が0のコーデックもありますので、その場合はICCompressGetSize()で取得しましょう。私の場合は両方の値を比較して大きい方を使うことにしています。バッファサイズが決まったらすかさずバッファを確保してこれで準備完了です。
なお、FourCCと出力フォーマットはクライアント側の復元処理(デコンプレス)の際に必要となりますので、クライアント側へ通知しておきます。
#include
<vfw.h> #include
<aviriff.h> #pragma
comment(lib,"vfw32") // コーデック情報の送信につけるヘッダー struct
NETWORK_CODEC_HEADER { DWORD dwbiSize; //
コーデック情報のバイト数 DWORD dwFcc; //
コーデックのFourCC DWORD dwReserve[2]; // 未使用 }; // コーデック処理で使う変数 HIC ghIC
= NULL; //
コーデックのハンドル LPVOID glpCompressBuff = NULL; // 圧縮された画像を格納するバッファへのポインタ BITMAPINFO *glpbiIn =
NULL; //
圧縮前の画像フォーマット(ビットマップインフォ) BITMAPINFO *glpbiOut = NULL; // 圧縮後の画像フォーマット(実態はコーデックにより異なる) // コーデック処理の初期化 BOOL
CodecInit( SOCKET sock ) { // 圧縮元のフォーマットを設定(DIBと同じ設定にする) glpbiIn = (BITMAPINFO*)malloc(
sizeof(BITMAPINFO) ); if (glpbiIn == NULL) return FALSE; ZeroMemory(glpbiIn, sizeof(BITMAPINFO) ); glpbiIn->bmiHeader.biSize =
sizeof(BITMAPINFOHEADER); glpbiIn->bmiHeader.biBitCount = 32; glpbiIn->bmiHeader.biPlanes = 1; glpbiIn->bmiHeader.biWidth =
gdwSizeW; glpbiIn->bmiHeader.biHeight = gdwSizeH; glpbiIn->bmiHeader.biSizeImage = gdwSizeW*gdwSizeH*4; // コーデックのハンドルを取得(FourCCでコーデックの選択ができます) ghIC = ICOpen( ICTYPE_VIDEO, FCC('AMV3'), ICMODE_COMPRESS ); if (ghIC == NULL) return FALSE; // 圧縮後のフォーマットをコーデックから取得する // (1)圧縮後のフォーマットを格納するバイト数を取得する DWORD size = ICCompressGetFormat(
ghIC, //
コーデックのハンドル glpbiIn, //
圧縮元フォーマット 0 //
圧縮後のフォーマット部を0にすると、バイト数を戻り値として返す ); if (size
== 0) return FALSE; // (2)圧縮後のフォーマットを格納するバッファを確保 glpbiOut = (BITMAPINFO*)malloc( size ); if (glpbiOut == NULL) return FALSE; // (3)圧縮後のフォーマットを取得 DWORD ret
= ICCompressGetFormat( ghIC, glpbiIn, glpbiOut //
このバッファへ圧縮後のフォーマットが格納される ); if (ret
!= ICERR_OK) return FALSE; // コーデックの圧縮処理を初期化 ret =
ICCompressBegin( ghIC, glpbiIn, glpbiOut ); if (ret
!= ICERR_OK) return FALSE; // 圧縮後のデータの最大バイト数を取得する DWORD dwSizeImage
= ICCompressGetSize( ghIC, glpbiIn, glpbiOut ); if (glpbiOut->bmiHeader.biSizeImage <
dwSizeImage) glpbiOut->bmiHeader.biSizeImage
= dwSizeImage; // 圧縮後のデータを格納するバッファを確保 glpCompressBuff
= malloc(glpbiOut->bmiHeader.biSizeImage
); if
(glpCompressBuff == NULL) return FALSE; // コーデック情報(圧縮後のフォーマット)をクライアント側へ送信(復元に使います) NETWORK_CODEC_HEADER
network_codec_header; ZeroMemory(
&network_codec_header, sizeof(network_codec_header) ); network_codec_header.dwbiSize
= glpbiOut->bmiHeader.biSize; //
コーデック情報のバイト数 network_codec_header.dwFcc = FCC('AMV3'); //
コーデックのFourCC size =
send_all( sock, (char*)&network_codec_header,
sizeof(network_codec_header), 0 ); if (size
== SOCKET_ERROR) return FALSE; // コーデック情報を送信 size =
send_all( sock, (char*) glpbiOut, glpbiOut->bmiHeader.biSize, 0 ); if (size
== SOCKET_ERROR) return FALSE; return
TRUE; } |
この連載の中で一番シンドイ部分ですが、出力フォーマットと圧縮後の最大バイト数の扱いだけ気をつければ何とかなると思います。逆に出力フォーマットをBITMAPINFO構造体に固定してしまい上手く行かないケースが多そうですね。次回は圧縮処理の本編を紹介します。
■連載2-4:画像転送システムその4(2009.2.18)
今回はコーデックを使って圧縮する処理を紹介します。圧縮処理はICCompress()を一つ使うだけなので簡単。また、引数が多いですがAMVビデオコーデックをはじめ最近のコーデックの場合は殆どの引数を無視しますので0にしておきましょう(もちろんコーデックによっては個々の引数が意味を持つ場合もあるのでそういったコーデックを利用する場合はしっかり調べましょう)。結局、重要な引数は入力フォーマットと、入力バッファのアドレス、出力フォーマットと出力バッファのアドレスと非常にシンプルです。
各フォーマットは準備編で用意したものを使いますが、ICCompress()の引数の型がなぜかLPBITMAPINFOHEADERとなっているので念のためヘッダーの方を指定しています。入力バッファのアドレスはDIBセクションのアドレスを指定します。出力バッファのアドレスは準備編で用意したものを使いましょう。
// コーデックを使ってDIBの画像を圧縮する BOOL
CodecMain( void ) { DWORD dwFlagsIn = 0; DWORD dwFlagsOut = 0; DWORD dwckid = 0; long lFrameNum = 0; DWORD dwFrameSize
= 0; // 目標ビットレート(指定なし) DWORD dwQuality = 0; DWORD ret
= ICCompress( ghIC, //
コーデックのハンドル dwFlagsIn, //
圧縮動作の制御フラグ(キーフレームなど) //
AMVコーデックでは使いません。 &glpbiOut->bmiHeader, // 圧縮後のフォーマット(ビットマップインフォヘッダー) glpCompressBuff, //
圧縮後の画像データを格納するバッファ &glpbiIn->bmiHeader, // 圧縮元のフォーマット(ビットマップインフォヘッダー) glpOff, //
圧縮元の画像データを格納したバッファ(DIB) &dwckid, //
予約されています。使わないで下さい。 &dwFlagsOut, //
圧縮後のステータスが格納される(キーフレームの検出に利用) lFrameNum, //
AMVコーデックでは使いません。 dwFrameSize, //
目標ビットレート、AMVコーデックでは使いません。 dwQuality, //
画質、AMVコーデックでは使いません。 NULL, //
前フレームのフォーマット、AMVコーデックでは使いません。 NULL //
前フレームを格納するバッファ、AMVコーデックでは使いません。 ); if (ret
!= ICERR_OK) return FALSE; DWORD
dwDataSize = glpbiOut->bmiHeader.biSizeImage; //
圧縮後のバイト数はメンバー変数に格納されている DWORD
dwKeyframe = FALSE; if
(dwFlagsOut & AVIIF_KEYFRAME) dwKeyframe = TRUE; //
キーフレームとして圧縮された return
TRUE; } |
ICCompress()が正常に終わると戻り値としてICERR_OKがセットされ、出力バッファに圧縮後のデータがセットされています。圧縮後のデータが何バイトになるかは出力フォーマットのメンバー変数glpbiOut->bmiHeader.biSizeImageで確認できます。また、処理したフレームがキーフレームになる場合はdwFlagsOutにAVIIF_KEYFRAMEがセットされます。他のフラグと一緒にセットされることもあるようなので、イコールで判定するのではなく論理andを使って該当するビットが立っているかどうかで確認するようにしましょう。
以上で、コーデックを使った圧縮処理は終わりとなりますが、APIの中には他にもICSeqCompressFrame()というコーデックを使って動画を圧縮出来るものがあります。こちらの方が細かい部分をAPIの方で管理してもらえるので扱いが楽なのですが、出力バッファを指定できないと言う欠点がありまして、処理速度が求められる場合などには向きません。アマレココの3.00まではICSeqCompressFrame()を使っていたのでコーデックにより圧縮されたデータはAPIが管理するバッファへ格納されてしまい、それをワザワザ自分で用意した出力バッファ(ファイルバッファ)へコピーしていました。このコピー処理がまるまる無駄となりコーデックを使った場合のオーバーヘッドとなっていましたが、3.01からICCompress()を使うようにしたので直接ファイルバッファへ書き出すことが出来(コピー処理が無くなった分)高速化となっています。
次回はサーバー編の最終回、ネットワークの送信処理を紹介します。
■連載2-5:画像転送システムその5(2009.2.20)
ネットワーク処理に関してはWinsockを使ってTCPで行います。恐らく普通に作れば良いと思うので準備及び接続は省略して要点のみ書きます。
送信処理で重要なのが一度に送信するバイト数で、大きすぎても小さすぎてもパフォーマンスは低下します(なんだかハードディスクドライブのアクセスのコツと似ていますね)。
例えば1MBのデータを送信するのに
send(
socket, (char *)lpbuff, 1024*1024, 0 );
とまとめて送信するよりも
for(
DWORD i=0; i<1024*1024; i+=4096 ) send( socket, (char *)((DWORD)lpbuff + i),
4096, 0 );
の様に4096バイトずつこまめに転送する方が早く転送できるようです。私の場合には6kByteくらいで分割すると最も良い結果がでました。理由は不明ですがジャンボフレームなどが影響しているのかな?
と言う事で、次のような送信関数を使っています。
// 最大送信サイズ(これより大きいデータは、このサイズに分割して送信します。) #define
PAKET_DATA_SIZE (2048*3) // 送信するデータが大きすぎるとパフォーマンスが低下するので // 一定の大きさ(PAKET_DATA_SIZE)に分割して送信します。 int
send_all( SOCKET s, char *buf, int len, int flags ) { char *p =
buf; int left
= len; while(left) { int
send_size = PAKET_DATA_SIZE; if
(left < PAKET_DATA_SIZE) send_size = left; int
size = send( s, p, send_size, 0 ); if
(size == SOCKET_ERROR) return SOCKET_ERROR; left
-= size; p
+= size; } return
len; } |
■連載2-6:画像転送システムその6(2009.2.25)
今回からはクライアント側の話になります。基本的にはサーバー側と似ていますので要点以外はチャチャッと行きます。
先ずはネットワーク処理からですが特別な処理は必要ないので省略して、コーデックの処理から始めます。
構造体やグローバル変数はサーバー側と同じモノを使い、最初にサーバー側で使ったコーデックの情報をネットワークから受信します。受信は2回に分けて行い、一回目はコーデックのFourCCとサーバー側glpbiOutのバイト数を取得。次にバイト数分のバッファをglpbiInに確保してから二回目の通信でサーバー側のglpbiOutの内容をglpbiInへ受信します。コレによりサーバー側のglpbiOutとクライアント側のglpbiInが同じ内容となりクライアントの入力フォーマットとします。
次に出力フォーマットglpbiOutです。基本的にはサーバー側のglpbiInと同じ設定になりますが完全に一致させる必要は無いのでネットワークは使わずに独自に設定しましょう。このとき画像サイズの設定だけ注意が必要です。画像サイズはコーデックにより変更される場合がありまして、例えばAMV3ビデオコーデックのハーフサイズオプションにより画像サイズが変更されます。こう言ったケースに対応するためにglpbiOutの画像サイズはキャプチャー時の画像サイズではなくglpbiInに設定されている画像サイズを使います。
入力/出力フォーマットが用意できたら、コーデックのデコンプレッサーのハンドルを取得します。このときコーデックのFourCCが必要になりますがこのFourCCはサーバー側から受信したFourCCを使います。
次にデコンプレッサーを初期化してデコンプレッサーの準備は完了です。
最後に圧縮されたデータを格納するためのバッファglpCompressBuffを確保して終わり。
#include
<vfw.h> #include
<aviriff.h> #pragma
comment(lib,"vfw32") // コーデック情報の送信につけるヘッダー struct
NETWORK_CODEC_HEADER { DWORD dwbiSize; //
コーデック情報のバイト数 DWORD dwFcc; //
コーデックのFourCC DWORD dwReserve[2]; // 未使用 }; // コーデック処理で使う変数 HIC ghIC
= NULL; //
コーデックのハンドル LPVOID glpCompressBuff = NULL; // 圧縮された画像を格納するバッファへのポインタ BITMAPINFO *glpbiIn =
NULL; //
圧縮された画像フォーマット(実態はコーデックにより異なる) BITMAPINFO *glpbiOut = NULL; // 復元された画像フォーマット(ビットマップインフォで確定) // コーデック処理の初期化 BOOL
CodecInit(SOCKET sock) { DWORD
size; // コーデック情報のヘッダーを取得(コーデック情報のバイト数とFourCC) NETWORK_CODEC_HEADER
network_codec_header; size =
recv_all(sock, (char*)&network_codec_header,
sizeof(network_codec_header), 0 ); if (size
== SOCKET_ERROR) return FALSE; // コーデック情報(可変長)を格納する為のバッファを確保 glpbiIn = (BITMAPINFO*)malloc(
network_codec_header.dwbiSize ); if (glpbiIn == NULL) return FALSE; // コーデック情報を取得(最終的な画像サイズや、どの様に圧縮したか等が記録されています) size =
recv_all(sock, (char*)glpbiIn,
network_codec_header.dwbiSize, 0 ); if (size
== SOCKET_ERROR) return FALSE; // 復元に使うフォーマットを作成 glpbiOut = (BITMAPINFO*)malloc(
sizeof(BITMAPINFO) ); if (glpbiOut == NULL) return FALSE; ZeroMemory(glpbiOut, sizeof(BITMAPINFO) ); glpbiOut->bmiHeader.biSize =
sizeof(BITMAPINFOHEADER); glpbiOut->bmiHeader.biBitCount = 32; glpbiOut->bmiHeader.biPlanes = 1; glpbiOut->bmiHeader.biWidth = glpbiIn->bmiHeader.biWidth; glpbiOut->bmiHeader.biHeight = glpbiIn->bmiHeader.biHeight; glpbiOut->bmiHeader.biSizeImage = glpbiIn->bmiHeader.biWidth* glpbiIn->bmiHeader.biHeight*4; glpbiOut->bmiHeader.biCompression = BI_RGB; // コーデックのデコンプレッサーのハンドルを取得 ghIC = ICOpen( ICTYPE_VIDEO, network_codec_header.dwFcc, // glpbiIn->biCompressionを使ってはダメ。 //
必ず、圧縮の時に使ったFourCCを使いましょう。 ICMODE_DECOMPRESS //
ICMODE_DECOMPRESSとなります。圧縮時とは異なるので注意。 ); if (ghIC == NULL) return FALSE; // デコンプレッサーの初期化 if
(ICERR_OK != ICDecompressBegin( ghIC, glpbiIn, //
圧縮された画像データのフォーマット(サーバー側のICCompressGetFormatで取得したlpbiOutをそのまま使う) glpbiOut //
復元する画像データのフォーマット(クライアント側で用意する) ))
return FALSE; // 圧縮された画像データを格納するバッファを確保する。 // 確保するバイト数はサーバー側の初期化処理でICCompressGetSize等を使って取得する // 未圧縮よりもサイズが大きくなるコーデックも有りです。必ずコーデックから // 最大データサイズを取得しましょう。 glpCompressBuff
= malloc(glpbiIn->bmiHeader.biSizeImage
); if
(glpCompressBuff == NULL) return FALSE; return
TRUE; } |
サーバー側のコンプレッサーの準備に比べると、クライアント側のデコンプレッサーの準備はその殆どをサーバ側から受信して済ませるので楽ですね。レアなケースですが画像サイズが変更される可能性があることだけ注意しておけば大丈夫だと思います。次回は実際にデコンプレッサーを使って圧縮された画像の復元処理を紹介します。
■連載2-7:画像転送システムその7(2009.2.28)
今回は圧縮された画像データを元の画像に復元する処理を紹介します。利用するAPIはICDecompress()一つなので簡単です。注意すべきは入力フォーマットのメンバーに圧縮された画像データのバイト数を事前にセットしておく事だけです。後は、入力フォーマットと、入力データが格納されたバッファ、出力フォーマットと復元後の画像を納めるバッファを指定してICDecompress()を呼び出します。尚、サンプルプログラムではの第二引数を0としていますが、ここにはキーフレームである事を示すフラグ(キーフレームでない事を示すフラグ)や、NULLフレームである事を示すフラグをセットしてコーデックにコレラの情報を伝えます。今回はNULLフレームは出てこない事とAMVコーデックはキーフレームの管理をコーデック側で行うので0となっています。
// コーデックを使って圧縮された画像データからDIBを復元する BOOL
CodecMain( void ) { // 圧縮された画像データのバイト数を圧縮フォーマットのメンバーにセットします。 glpbiIn->bmiHeader.biSizeImage = dwDataSize; // デコンプレッサーでDIBを復元 DWORD ret
= ICDecompress( ghIC, //
デコンプレッサーのハンドル 0, //
キーフレームやNULLフレームを示すフラグをセットする &glpbiIn->bmiHeader, //
圧縮された画像データのフォーマット(データサイズをbiSizeImageにセットする事) (void
*)glpCompressBuff, //
圧縮された画像データへのポインタ &glpbiOut->bmiHeader, //
復元するDIBのフォーマット (void
*)glpOff //
DIBのデータアドレス ); if (ret
!= ICERR_OK ) return FALSE; return
TRUE; } |
次回はクライアント側のGDI処理です。
■連載2-8:画像転送システムその8(2009.3.3)
今回は最後に残ったクライアント側のGDI処理を紹介します。サーバー側と殆ど同じですが、作成するDIBセクションの画像サイズはキャプチャしたときの画像サイズではなくコーデックの入力フォーマットを参照しています。コーデックのところでも書きましたが、これはコーデックのリサイズ処理に対応するためです。
後は、実際にウインドウへ画像を表示する部分は画像サイズが合わない場合が殆どなのでStretchBlt()を使って拡大または縮小表示しています。
// GDI処理で使う変数 HDC ghDC = NULL; //
デスクトップのデバイスコンテキスト HDC ghOffDC = NULL; //
オフスクリーンのデバイスコンテキスト(ここにデスクトップ画像を取り込む) LPVOID glpOff = NULL; //
オフスクリーンのアドレス HBITMAP ghOffBmp = NULL; //
オフスクリーンのビットマップハンドル HBITMAP ghOffBmpOld
= NULL; // ビットマップハンドルの控え(不要?) // GDI処理の初期化 BOOL
GDIInit( void ) { // デスクトップのデバイスコンテキスト取得 ghDC = GetDC( NULL ); //
DIBSection作成 // ・圧縮された画像データはこのDIB(glpOff)に復元します。 // ・ココではRGB32のフォーマットで復元するためのDIBを作成します。 // ・DIBの画像サイズはコーデックの画像サイズに合わせます。 // ※コーデックの方でリサイズ(ハーフサイズ処理など)される場合がある為 BITMAPINFO bi; ZeroMemory(
&bi, sizeof(bi) ); bi.bmiHeader.biSize =
sizeof(BITMAPINFOHEADER); bi.bmiHeader.biBitCount = 32; //
RGB32に復元するので32を指定する bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biWidth = glpbiIn->bmiHeader.biWidth; // コーデックの画像サイズを使う bi.bmiHeader.biHeight = glpbiIn->bmiHeader.biHeight; bi.bmiHeader.biSizeImage = glpbiIn->bmiHeader.biWidth*glpbiIn->bmiHeader.biHeight*4; // RGB32で計算 bi.bmiHeader.biCompression
= BI_RGB; //
フォーマットはRGB ghOffBmp = CreateDIBSection(
NULL, &bi, DIB_RGB_COLORS, (void **)(&glpOff),
NULL, 0); // 復元された画像を簡単に扱う為のデバイスコンテキストを作成 // ※復元された画像はデバイスコンテキスト(ghOffDC)を使って自在に加工できます。 ghOffDC = CreateCompatibleDC(ghDC); ghOffBmpOld = (HBITMAP)SelectObject( ghOffDC, ghOffBmp ); return
TRUE; } // GDI処理 BOOL
GDIMain( void ) { // ウインドウのデバイスコンテキストを取得 HDC hdc =
GetDC( ghApp ); // ブリットモードを設定 SetStretchBltMode(
hdc, COLORONCOLOR ); // ウインドウの大きさを取得 RECT
rect; GetClientRect(
ghApp, &rect ); // ウインドウいっぱいに復元した画像を表示 StretchBlt(
hdc, 0, 0, rect.right, rect.bottom, ghOffDC, 0, 0, glpbiIn->bmiHeader.biWidth,
glpbiIn->bmiHeader.biHeight, SRCCOPY ); // 後始末 ReleaseDC(
ghApp, hdc ); return
TRUE; } |
メイン処理で
//
ブリットモードを設定
SetStretchBltMode(
hdc, COLORONCOLOR );
としていますが、これを
SetStretchBltMode(
hdc, HALFTONE );
とすると画質がよくなります。ただしメチャクチャ処理に時間がかかるので通常は使えません。画像サイズが小さい場合などに使いましょう。今は廃止になりましたが、アマレココのリサイズにあった「API早い」がCOLORONCOLORで、「API綺麗」がHALFTONEでした。
サンプルプログラムでは処理を簡略化するために一番最後に画像サイズを変更していますが、ファンタジーリモートでは画像サイズを縮小する場合はキャプチャー直後にサーバー側で行っています。キャプチャー直後に縮小する事でコーデックの圧縮・復元処理やネットワークによる通信量を減らす事が出来て全体的なパフォーマンスが向上します。逆に画像を拡大しなければならないときはサンプルと同じように表示する時にストレッチしています。
次回はサンプルには登場しなかったコーデックの選択方法や設定値の管理などを紹介したいと思います。
■連載2-9:画像転送システムその9(2009.3.7)
今回はコーデックの選択画面を表示するAPIのICCompressorChoose()を紹介します。
初期状態で特定のコーデックを選択する場合はCOMPVARS構造体のfccHandlerメンバーに選択したいコーデックのFourCCをセットしてICCompressorChoose()を実行します。さらに、特定のフォーマットに対応したコーデックのみを表示する為にBITMAPINFOHEADER構造体を使い、主にbiCompressionとbiBitCountで利用可能なコーデックを絞ります。但し、画像サイズは大きくすると表示されなくなるコーデックがありますので、実際に使う画像サイズを正直に設定するのではなく小さ目の適当なサイズにしておきましょう。
ICCompressorChoose()はコーデックを選択してOKボタンが押されると戻り値としてTRUEを返します。エラーまたはキャンセルボタンが押されるとFALSEが返ります。TRUEが返った場合はCOMPVARS構造体にコーデックの情報が詰まっています。主に必要なのはFourCCを示すfccHandlerだけなので自前の変数へコピーしておきましょう。
ICCompressorChoose()はコーデックを選択した後すぐに圧縮処理が開始できるように色々な準備(メモリの確保など)をしてくれますが、結局それらは使わないのでICCompressorChoose()のあとはすぐにICCompressorFree()でリソース(メモリー)を開放しておきましょう。
BOOL
CodecChoose( DWORD *lpdwFcc ) { COMPVARS
cv; ZeroMemory(
&cv, sizeof(cv) ); cv.cbSize = sizeof(COMPVARS); cv.dwFlags = ICMF_COMPVARS_VALID; cv.fccType = ICTYPE_VIDEO; cv.fccHandler
= *lpdwFcc; BITMAPINFOHEADER bi; ZeroMemory(
&bi, sizeof( bi)); bi.biSize =
sizeof(BITMAPINFOHEADER); bi.biBitCount = 32; //RGB32なら32、RGB24なら24を設定します bi.biPlanes = 1; bi.biCompression
= BI_RGB; bi.biWidth = 320; //
画像サイズは適当に小さめにしておきます bi.biHeight = 240; bi.biSizeImage = bi.biWidth * bi.biHeight *
bi.biBitCount / 8; HWND hWnd
= NULL; //
親ウインドウのハンドル if
(!ICCompressorChoose( hWnd, 0
| ICMF_CHOOSE_DATARATE | ICMF_CHOOSE_KEYFRAME, &bi,
NULL, &cv, NULL )) { return
FALSE; } *lpdwFcc
= cv.fccHandler; ICCompressorFree(
&cv ); return
TRUE; } void
main( void ) { // RGB32に対応しているコーデックの一覧を表示する、初期状態でAMV3コーデックを選択 DWORD
dwFcc = FCC('AMV3'); if
(CodecChoose( &dwFcc ) == TRUE) { //
dwFccに選択されたコーデックのFourCCが格納されています。 //
このFourCCを使ってAMV3以外の様々なコーデックで画像転送システムを実行できます。 }else{ //
コーデックの選択はエラーまたはキャンセルされました。 } } |
次のようにBITMAPINFOHEADERを使わずにNULLを指定するとインストールされている全てのコーデックを表示する事ができます。
ICCompressorChoose(
hWnd, 0
| ICMF_CHOOSE_DATARATE | ICMF_CHOOSE_KEYFRAME, NULL, NULL, &cv, NULL ) |
この場合、実際には使えないコーデックや、圧縮処理を持たない(復元専用の)コーデックなども表示されてしまうので不便ですね。
続く。
■連載2-10:画像転送システムその10(2009.3.10)
今回はコーデックの設定画面を表示します。利用するAPIはICConfigure()となりますがこの関数は使わずにICSendMessage()を使って直接コーデックへメッセージを送る事で設定画面の表示を行ってみようと思います。ICSendMessage()ではメッセージ(ICM〜)と2つのパラメータを使ってコーデックを制御します。今回利用するメッセージはICM_CONFIGUREで第1パラメータのみ利用します。第1パラメータには設定画面の親となるウインドウハンドル(hWnd)を指定しますが、良く分からないときはNULLを指定すれば良いでしょう。また、この第1パラメータに-1を指定するとコーデックに設定画面を表示出来るかどうか問い合わせる事が出来ます。-1を指定して戻り値がICERR_OKであればコーデックは設定画面を表示できます。ICERR_OK以外の場合はコーデックは設定画面を持たないのでそこで処理を終了します。予め調べておいてコーデックの設定ボタンを無効にするなどしておくと親切ですね。
BOOL
ShowConfig( HWND hWnd ) { DWORD
dwFcc = FCC('AMV3'); HIC hic =
ICOpen( ICTYPE_VIDEO, dwFcc, ICMODE_COMPRESS ); if (hic
== 0) return FALSE; // 設定画面があるか問い合わせる DWORD
query = ICSendMessage( hic, ICM_CONFIGURE, (DWORD)-1,
0 ); if (query
!= ICERR_OK) { //
設定画面なし ICClose(
hic ); return
TRUE; } // 設定画面を表示 DWORD ret
= ICSendMessage( hic, ICM_CONFIGURE, (DWORD)hWnd,
0 ); ICClose(
hic ); if (ret
!= ICERR_OK) { //
エラー return
FALSE; } return
TRUE; } |
なを、ICM_CONFIGUREの部分をICM_ABOUTと置き換える事でコーデックのAbout画面を表示する事が出来ます。
また、今回の様にICOpen()でコーデックのハンドルを取得し、そのハンドルへICSendMessage()でメッセージを送る事でコーデックに関する殆どの処理を行う事が出来ます。今まで紹介してきたIC〜と言う関数の殆どはこのメッセージ処理に置き換えるマクロとなっているので、最終的にはこのメッセージ処理を理解することでコーデックを自由自在に扱えるようになります。
■連載2-11:画像転送システムその11(2009.3.13)
今回はコーデックの固有ステータスを制御します。固有ステータスと言うのは各コーデックにより内容が異なる為一概に言えませんが多くの場合はコーデックの設定内容を含んでいます。例えばAMVコーデックで言えばハーフサイズのON/OFFやマルチスレッドの設定など設定画面の内容が含まれています。これらのステータスをコーデックから取得したり、逆にアプリケーション側からコーデックへステータスを設定したりする事ができます。
コーデックからステータスを取得するにはICM_GETSTATEメッセージを使います。第1パラメータにステータスを格納するバッファのアドレス、第2パラメータにバッファのサイズを設定しますが、第1パラメータをNULLにするとステータスを格納するのに必要なバッファサイズを返すので最初はバッファサイズの取得、次にバッファの確保、最後にステータスの取得と三段階で処理しましょう。
次に、ステータスの設定です。利用するのはICM_SETSTATEメッセージです。こちらは第1パラメータにステータスが格納されたバッファのアドレス、第2パラメータにバッファサイズを指定するだけです。
BOOL
GetSetState( void ) { HIC hic =
ICOpen( ICTYPE_VIDEO, dwFcc, ICMODE_COMPRESS ); if (hic
== NULL)return FALSE; // 1.バッファサイズの取得 DWORD
dwSize = ICSendMessage( hic, ICM_GETSTATE, (DWORD)NULL, (DWORD)NULL ); if
(dwSize) { //
2.バッファの確保 void
*lpState = malloc( dwSize ); if
(lpState == NULL) { ICClose(
hic ); return
FALSE; } //
3.ステータスの取得 ICSendMessage(
hic, ICM_GETSTATE, (DWORD)lpState, (DWORD)dwSize ); //
ステータスを設定 ICSendMessage(
hic, ICM_SETSTATE, (DWORD)lpState, (DWORD)dwSize ); } free(
lpState ); ICClose( hic
); return
TRUE; } |
上の例では取得したステータスをそのまま設定するので意味ないですが、アマレココの場合には設定Aから設定Dまで別々にコーデックのステータスを取得し管理する事で同じコーデックに対しても異なる設定で録画できるようになっています。また、ファンタジーリモートではクライアント側でコーデックの管理を一括しています。その為、接続時にクライアント側のコーデックのステータスを取得しそれをサーバーへ送信、サーバー側は受信したステータスをコーデックに設定という流れになっています。
このようにステータスを上手く管理する事でアプリケーションの利便性が向上しますので是非とも活用したい機能です。但し全てのコーデックがステータスの取得、設定に対応しているわけではないのでその点は気を付けましょう。
|