読者です 読者をやめる 読者になる 読者になる

雑念日記

主に技術的なことをつらとらと(書ければいいな)。

PNG を自力で読んで表示しよう その2

前回の続き。コードはこちら → display PNG image without using PImage.

前回の最後では Lenna さんがホラーな感じで表示されてしまいました。
なぜあのようなことになってしまったかというと、IDAT チャンクにおける画像データの保存方法に秘密があります。

IDAT 内画像データの構造

2x2 の PNG 画像があるとしましょう。

f:id:hoshi_sano:20130818112804p:plain

この画像の IDAT 画像データを展開すると以下のような構造になります。

f:id:hoshi_sano:20130818112826p:plain

先頭 1 byte が謎に余分ですね。
ここにはその行の RGB 値がどのようなルールで保存されているかを表す Filter Type が保持されています。
つまり、IDAT の画像データには何らかのフィルタリングをされた値が入っているんですね。

フィルタータイプ

フィルターのタイプは以下の5種類があります。

タイプ 説明
None 0 何も処理しない。(それぞれの値がそのまま保存されている。)
Sub 1 当該ピクセルの左隣のピクセルの色(フィルタリング前の値)との差分として保存されている。
Up 2 当該ピクセルの直上のピクセルの色(フィルタリング前の値)との差分として保存されている。
Average 3 当該ピクセルの左隣のピクセルと直上のピクセルの色(フィルタリング前の値)の平均との差分として保存されている。
Paeth 4 当該ピクセルの左隣、直上、左斜め上のピクセルの色から Paeth predictor という手法で選ばれた色との差分として保存されている。

前回の記事でも一応フィルタータイプは解析していましたが、そのタイプ (filterType) に応じた処理はしておらず byte から変換した数値をそのまま色の値として利用していました。

    int filterType = 0;
    for (int j = 0; j < imageInfo.imageWidth; j++) {
      int rowHead = (i * (imageInfo.imageWidth * 3 + 1));
      // 行の先頭は色の持ち方
      if (j == 0) {
        filterType = readAsInt(imageData, rowHead, 1);
      }
      // TODO: filterType によって処理は変更する必要がある
      int p = rowHead + (j * 3 + 1);
      color c = color(readAsInt(imageData, p,     1),
                      readAsInt(imageData, p + 1, 1),
                      readAsInt(imageData, p + 2, 1));
      colors[(i * imageInfo.imageWidth) + j] = c;

何らかのフィルター処理が施されている色を正常に表示するには、取得した値に逆フィルタリングを施してあげる必要があります。
前回の記事で正常に表示された画像は、すべての行のフィルタータイプが None(0) だったので問題なかったんですね。

フィルタータイプ別の処理(逆フィルタリング)

None は既に処理できているので、その他の4つのフィルターについて対応します。
前準備として、以下のような filterType による switch の分岐を用意しましょう。

  color left = color(0), above = color(0), upperLeft = color(0);
  switch (filterType) {
  case FT_NONE:
    // do nothing
    break;
  default:
    break;
  }

Sub フィルター

Sub フィルターは上述のとおり、左隣との差分が保存されています。
そのため、逆フィルタリングするには左隣の色を足してあげれば良いです。
左隣がない(左端の)ピクセルの場合は左隣が (R, G, B) = (0, 0, 0) として扱います。

  case FT_SUB:
    if (x > 0) left = rawColors[(y * imageInfo.imageWidth) + (x - 1)];
    c = colorAdditionWithOverFlow(c, left);
    break;

colorAdditionWithOverFlow() では色の足し算をしています。
このとき、色値が 255 を越えた場合には 256 を減算します。

  • (例) 180 + 120 = 300 → 44
color colorAdditionWithOverFlow(color c1, color c2) {
  float r = (  red(c1) +   red(c2)) % 256;
  float g = (green(c1) + green(c2)) % 256;
  float b = ( blue(c1) +  blue(c2)) % 256;
  return color(r, g, b);
}

これは PNG のフィルタリング処理の中で、マイナス方向への差分を保存する際に以下のような手法をとっているためです。

  • (例: 正方向への差分) 左隣:100, 対象:120 のとき → 差分 20
  • (例: 負方向への差分) 左隣:120, 対象:100 のとき → 差分 236

Sub フィルターに対応すると、前回表示に失敗した画像は以下のようになりました。

f:id:hoshi_sano:20130818112907p:plain

...ほぼ変わってないですね。
それもそのはず、この画像、Sub フィルターが使われているのは最初の1行だけでした。

ちなみに Sub フィルターは横方向に変化の少ない箇所に有効(圧縮効率が良い)とされています。

Up フィルター

Up も Sub と同じ論法で、逆フィルタリングするには直上の色を足してあげれば良いです。
Sub と同様、上がない(上端の)ピクセルの場合は直上が (R, G, B) = (0, 0, 0) として扱います。

  case FT_UP:
    if (y > 0) above = rawColors[((y - 1) * imageInfo.imageWidth) + x];
    c = colorAdditionWithOverFlow(c, above);
    break;

Sub, Up フィルターに対応すると、画像は以下のようになりました。

f:id:hoshi_sano:20130818112925p:plain

画像の上の方に少しだけ、正常な色が見えてきましたね。
下半分にも少し変化が見られますが、上の行が正常に表示されていない行は正しい逆フィルタリングができないため、改善は見られません。

ちなみに Up フィルターは縦方向に変化の少ない箇所に有効とされています。

Average フィルター

こんな具合で分岐を追加して行きましょう。
Average の逆フィルタリングでは、左隣と直上のピクセルから平均をとって加算します。

  case FT_AVRG:
    if (x > 0) left  = rawColors[(y * imageInfo.imageWidth) + (x - 1)];
    if (y > 0) above = rawColors[((y - 1) * imageInfo.imageWidth) + x];
    int avrgR = floor((  red(left) +   red(above)) / 2);
    int avrgG = floor((green(left) + green(above)) / 2);
    int avrgB = floor(( blue(left) +  blue(above)) / 2);
    color avrg = color(avrgR, avrgG, avrgB);
    c = colorAdditionWithOverFlow(c, avrg);
    break;

Sub, Up, Average フィルターに対応すると、画像は以下のようになりました。

f:id:hoshi_sano:20130818112952p:plain

ちゃんと表示される範囲が増えてきました。もう一息です。

Paeth フィルター

最後に Paeth の逆フィルタリングに対応しましょう。
ここでは paethPredictor() というものを使います。

  case FT_PAETH:
    if (x > 0) left  = rawColors[(y * imageInfo.imageWidth) + (x - 1)];
    if (y > 0) above = rawColors[((y - 1) * imageInfo.imageWidth) + x];
    if (x > 0 && y > 0) upperLeft = rawColors[((y - 1) * imageInfo.imageWidth) + (x - 1)];
    float paethR = paethPredictor(red(left),   red(above),   red(upperLeft));
    float paethG = paethPredictor(green(left), green(above), green(upperLeft));
    float paethB = paethPredictor(blue(left),  blue(above),  blue(upperLeft));
    c = colorAdditionWithOverFlow(c, color(paethR, paethG, paethB));
    break;

処理内容は以下のような感じ。

float paethPredictor(float a, float b, float c) {
  float p = a + b - c;
  float pa = abs(p - a);
  float pb = abs(p - b);
  float pc = abs(p - c);
  if (pa <= pb && pa <= pc) {
    return a;
  } else if (pb <= pc) {
    return b;
  } else {
    return c;
  }
}

これですべてのフィルターに対応しました。
画像は以下のように表示されます。

f:id:hoshi_sano:20130818113014p:plain

ちゃんと表示されました!やった!

PNG の道は長く険しい

前回と今回の記事で戯れに PNG を自分で読んでみましたが、扱ったのはかなりシンプルな PNG 画像のみ、まだまだ PNG はさまざまな情報を持っています。
ラディッツを倒したはいいけど、まだまだナッパやベジータみたいな連中がゴロゴロしてるみたいな。
具体的なところで言えば、アルファはどうするのかとか、ビット深度がどうとか、カラータイプがどうとかなんとか...。
気が向いたらそのあたりも対応したいですが、とりあえず今回はこの辺で。