HDTVtoMPEG2 海賊2

メンテナ様に無断で配布してるので海賊版と称しています。
前回に引き続き、またまた改竄しました。こんどは、解像度を上げてみました。
HDTVtoMPEG2-Pirated-up.zip(VisualStudio2008評価版のソリューションごと梱包。Releaseフォルダに実行形式入ってます。)

(2012.02.27 追記:確認ダイアログ付加、海賊3に更なる改竄版あります。機能は変わってないのでお好きなほうをどうぞ)

画像を比べると「清武」とか「4:58」がくっきりしたのがわかると思います。だがしかし・・・色がおかしいというか、画が汚い。解像度は上げられたようですが、画質が悪いという状態。

なぜかというと、きっとQuantizer Table(matrix) があまりに適当だということだと思われます*1。しかし、どうすればいいのかわからない。変更点とこれまでにわかったことを列挙します。

画像サイズについて

  • HardCodedConstants.hを作成して、HARD_CODED_xxxという固定値をまとめました。
  • 画像関係のサイズは4種類あります。
    1. 画面に表示される画像サイズ(PREVIEW)...480x270 (固定)
    2. 内部で用意するビットマップサイズ(BITMAP)...480x270 (固定)
    3. 実際に画素が書かれる画像サイズ(rendered)...360x270 (TS画像サイズの1/4)
    4. TS画像サイズ...1440x1080 (TSファイルから取得される)
  • 作成された画像の画面への描画は次の関数でStretchビルドされています。

void CHDTVtoMPEG2Dlg::DrawPreview(void) 関数
void CProgressDialog::DrawPreview(void) 関数

StretchDIBits( pDC->GetSafeHdc(), 
               left, top, width, height, 
               0, m_activeBuffer->bm.height-m_activeBuffer->renderedHeight, 
               m_activeBuffer->renderedWidth, m_activeBuffer->renderedHeight,
               m_activeBuffer->bm.buffer, (LPBITMAPINFO) &m_BMIH, DIB_RGB_COLORS, SRCCOPY);
  • 常に見えている画像は Preview とされ、480x270に固定しています。HARD_CODED_PREVIEW_HEIGHT(=270)から16:9のアスペクト比で幅を内部で計算し求めています。
  • 内部で確保しているビットマップ(bm)は、480x270に固定しています(HARD_CODED_BITMAP_WIDTH、HARD_CODED_BITMAP_HEIGHT)が、1920x1080という大きなサイズでも構いません。renderedWidth×renderedHeightを格納できる十分な大きさを確保すればよいのです。
  • renderedWidth×renderedHeight は、地デジ動画(TS)ファイルのサイズ(1440x1080)の1/4になります。これは、HARD_CODED_DECIMATION(=2)によって固定しており、360x270 となります。つぎの箇所で計算とチェックが行われています。
mpeg2stuff.cpp: bool process_PES_video_packet()関数

    activeBuffer->renderedWidth = sh->horizontal_size_value >> HARD_CODED_DECIMATION;
    if (activeBuffer->renderedWidth > activeBuffer->bm.width)
             activeBuffer->renderedWidth = activeBuffer->bm.width;

現状の固定した値では、renderedWidth(=360) < bm.width(=480) となるので、チェックには引っかかりません。

H2M内部のデータ名など

  • TS(mpeg2)には、I、P、Bフレームがありますが、Iフレームだけが扱われています。画面にプレビューされているのはIフレームであり、P、Bは処理されていません。
    1. I:イントラフレーム(キーフレーム)
    2. P:前のIフレームからの差分フレーム
    3. B:前後2枚のIフレームとの差分フレーム
  • 画面へのプレビュー処理と、TSの切り出し処理は別に行われていますので、プレビュー画像に関する改造をしても、切り出したTSに影響は出ません。
  • HDTVtoMPEG2(H2M)内部では、intra(イントラ)という識別は、Iフレームのことを指しています。一方、non_intra は、P、Bフレームを指しています。
  • 輝度(Y0,Y1,Y2,Y3)に4ブロック(block)+色(色差 Cb,Cr)に2ブロックの6ブロック(マクロブロック(macroblock)と呼ぶ)が処理の単位となります。輝度4+色差2の形式を 4:2:0 と表すようです。
  • 1ブロックは8x8画素に当たります。
block (4:2:0)
       8       8             8              8
   ┌───┬───┐    ┌───┐     ┌───┐
   │      │      │    │      │     │      │
  8│  Y0  │  Y1  │    │  Cb  │     │  Cr  │
   │      │      │    │      │     │      │
   ├───┼───┤    └───┘     └───┘
   │      │      │
  8│  Y2  │  Y3  │
   │      │      │
   └───┴───┘
macroblock(blockcount==6)
   ┌───┬───┬───┬───┬───┬───┐
   │      │      │      │      │      │      │
   │  Y0  │  Y1  │  Y2  │  Y3  │  Cb  │  Cr  │
   │      │      │      │      │      │      │
   └───┴───┴───┴───┴───┴───┘
  • H2M内部では QFがマクロブロックを表し、QF[0..5][0..7][0..7] という配列でブロックを格納しています。またコードを簡略化するために、次のようにポインタQdを使っています。
   short *Qd = &QF[i][0][0];
  • オリジナルのH2Mでは、1/8のサイズで固定することで処理を簡略化し、高速化を図っているようです。
  • 画像の幅(=1440)に対する 90マクロブロック(=1440/16)の帯をスライス(slice)と呼びます。
  • H2M内部では、frame=>slice=>macroblock=>block と処理を呼出しています。

解像度について

ここまでの改造で、プレビューは1/8から1/4に拡大されますが、オリジナルのH2Mでは、低周波成分(ブロックの左上)だけを使うため、解像度は1/8のままです。解像度を1/4にするためには、つぎのfullSize処理を有効にします。

  • decode_slice()関数の呼出しで、fullSize処理をtrueにする。
mpeg2stuff.cpp:bool process_PES_video_packet()関数内 4050行目以降

    // pass slice through for decoding
    // XXX testing full frame decode
 //   if (!decode_slice(stream_id, false, &activeBuffer->bm, sh, ph))
    if (!decode_slice(stream_id, true, &activeBuffer->bm, sh, ph))
    {

これにより、1440x1080のTS画像全体をIDCTできるようになります。

  • mpeg2stuff.cpp:bool decode_slice()関数内の 4614行目以降を改造して、4x4ピクセル単位で結果が反映されるようにします。
  for( int i = 0; i < 4; i++) // このループは Y0、Y1、Y2、Y3 を指定するため(サイズは各々8x8ピクセル)。
  {
    for( int sp =0; sp < 4; sp++) // このループは 4x4 ピクセルで処理するため。HARD_CODED_DECIMATIONが2の場合。 
    {
      // 8x8ピクセルで処理する場合
      //    uint x = ((mb_column<<1) + (i&1));
      //    uint y = ((mb_row<<1) + (i>>1));
      // 4x4ピクセルで処理する場合
      uint x = (((mb_column<<1) +(i&0x01))<<1) + (sp&0x01);
      uint y = (((mb_row<<1) +(i>>1))<<1) +(sp>>1);
      uint xi=(sp&0x01)<<2;
      uint yi=(sp>>1)<<2;

      if ((x < bm->width) && (y < bm->height))
      {
        int Y = QF[i][yi][xi];
    ..................................

QF配列は、IDCT処理結果の画素値を格納しています。オリジナルのH2Mでは、QF配列は左上つまり、QF[i][0][0]にしか値が入っていませんでした。1/8のサイズならば問題ないのですが、1/4だと黒(輝度0)の点を描画してしまうことになります。で、QFを作成しているコードの改造も必要になります。

  • IDCT処理は mpeg2stuff.cpp: bool decode_slice()関数内の 4519行目以降 にあります。
  if (eob_read)
  {
    // end of block
    while (n++ < 64)
      Qd[*scan++] = 0;
    Qd[63] ^= quant_sum & 1;

    gpIDCT->transform(Qd, dst);
    for( int idst=0; idst<64; idst++ ) *(Qd+idst)=dst[idst]<<3;

    // XXX Temp - recalc average to see if it displays correctly.
    /*
    int sum = 0;
    for (int idst = 0; idst < 64; idst++)
    sum += dst[idst];
    *Qd = sum / 8;
    if ((sum % 8) & 0x4)
      (*Qd)++;
   */
  } else {
    for (uint m = 0; (m < run) && (n++ < 64); m++)
      Qd[*scan++] = 0;
    if (n++ < 64)
    {
      int scanCur = *scan++;
//    int f = (signed_level * sf * ph->intra_quantiser_matrix[scanCur]) / 16;
      int f = (signed_level * sf / Q50[scanCur]);
      Qd[scanCur] = f;
      quant_sum += f;
    }
  }

IDCT処理は、gpIDCT->transform(Qd, dst) で行われています。Qd の値をIDCTしてdst配列に返す関数で、調べたところ概ねIntel社の技術資料 AP-945 のサンプルコードでした。
処理全体はランレングスを展開しながらQd[0...63]にデータを格納し、IDCT変換するというものです。オリジナルのH2Mでは、fの値が全て0になっていました。原因は、intra_quantiser_matrix 配列の要素が全て0だったためです。quantizer matrix は量子化テーブルと呼ばれるものらしく、画像の質に影響するらしいのですが、僕はまだ理解していません。とりあえず、Image Compression and the Discrete Cosine Transformに載っていたQ50を使ってみました*2

あとがき

さて、長々とかきましたが、今後続けられるかどうかは微妙です。というのは、DCT-IDCTを理解する能力が僕になければこれ以上の改造はできないからです。でも、ソースコードをソリューションごと梱包してあるので「こうだよ」とか「こうじゃないの」とかがあったら、ちょっと教えてください。

海賊版ですので、メンテナの方にはチクらないでください。
現在のメンテナさん:Jacob Balazer氏
情報:AV Science Forum

*1:適当過ぎて、輝度がコケて暗くなっていまい、画素値を8倍にしてるのでグラデーションが崩壊しているのです。

*2:今週はこれを探しだすのに精一杯で、まだ内容は理解していません。