<< NEW | main | OLD>>
基本的な実装についてはすでにわかりやすい記事を
書かれている方がいたので詳しい解説はそちらに譲ります。

SWFファイルから画像を抽出する

ここでは透過情報を持つDefineBitsJPEG3タグを処理する際の注意点と
DefineBitsLosslessタグに対する実装、およびそれに関連したSwf2XNA側の
バグについて言及しておこうと思います。



まず透過処理について。
上記サイトのサンプルを動かしていて気がついたのですが、
抽出した画像の透過箇所が黒っぽくなってしまうという問題があるようです。
結論から言うと、これはアルファ乗算処理が二回かかっているのが原因です。

例えばRGB(255,0,0)に対してアルファ値128を適用する場合を考えます。
透過処理ではRGB各要素にアルファ値を掛けることで透過による色の変化を表現します。
(.NETのBitmapクラス等で使用されている)アルファ値は0が完全透明で
255が完全不透明を表すので、128はちょうど半透明、倍率で言うと0.5になります。

R = 255 * 0.5 = 128
G = 0 * 0.5 = 0
B = 0 * 0.5 = 0

この少し暗くなったRGB(128,0,0)がアルファ補正された色です。
そしてどうやらDefineBitsJPEG3タグのImageDataにはすでにこの補正がかかっているようです。
少なくとも今回使用した手元のSWFファイルでは全てそうなっていました。
ただSWF仕様書には書かれていないので全てのSWFに当てはまるのかはわかりません。

とにかくこの画像データをオプション無しでBitmapクラスにロードしてしまうと
通常の(=アルファ補正されていない)画像として扱われてしまいます。
この状態でpixelにアルファ値128を設定するとさらに内部で補正がかかり、
RGB(64,0,0)となり結果的に二重に補正がかかってしまうことになります。
ご覧のようにアルファ補正を行うと元の色よりも暗くなる傾向にあるため
透過部分が黒ずんだ画像になってしまうわけです。

これを回避するためにはPixelFormatを指定してBitmapクラスを生成します。
使用するフォーマットはPixelFormat.Format32bppPArgbです。

Format32bppPArgb
1 ピクセルあたり 32 ビットの形式であることを指定します。つまり、アルファ、赤、緑、および青のコンポーネントに、それぞれ 8 ビットを使用します。 アルファ コンポーネントに応じて、赤、緑、および青のコンポーネントが前乗算されます。


最初説明を見てもなんのこっちゃ意味がわからなかったのですが、
前乗算(premultiplied)というのが、上で説明したような処理のことを言っていようです。

……と、ここまでドヤ顔で説明してますが、私は画像に関しては素人なので
理屈とかは多少間違っているところもあるかもしれません。
私の拙い説明でうまく伝わらなかった方は「前乗算」「premultiplied」あるいは
「乗算済アルファ」などをキーワードに検索してみると良いでしょう。
個人的にはこちらの説明が大変わかりやすかったです。

コンポジターに必要なアルファチャンネルの知識(後編)

とりあえず以上を踏まえた実装を見ていただきましょう。
冒頭紹介したサイトのコードをベースに少し弄ってあります。


public static class DefineBitsTagExtention
{
public static Bitmap GetBitmap(this DefineBitsTag tag)
{
Bitmap bmp = null;
switch (tag.TagType)
{
case TagType.DefineBits:

// 本当は JPEGTablesTag(6) も見ないといけないが、
// ここではJPEGTablesTagにデータが存在しない(Length=0)前提で
// 画像データの前後にマーカーを入れるだけの手抜き実装

int size = tag.JpegData.Length + 4;
byte[] buf = new byte[size];

// jpeg SOI marker
buf[0] = 0xFF;
buf[1] = 0xD8;

// jpeg body
tag.JpegData.CopyTo(buf, 2);

// jpeg EOI marker
buf[size - 2] = 0xFF;
buf[size - 1] = 0xD9;

bmp = new Bitmap(new MemoryStream(buf));
// Bitmapクラスの内部でMemoryStreamを保持しているのでここでMemoryStream.Dispose()してはいけない
// 使用後に呼び出し側で責任をもってBitmap.Dispose()すること
break;

case TagType.DefineBitsJPEG2:
case TagType.DefineBitsJPEG3:

// 透過情報なし
if (!tag.HasAlphaData)
{
bmp = new Bitmap(new MemoryStream(tag.JpegData));
break;
}

// 透過情報あり
var jpeg = new Bitmap(new MemoryStream(tag.JpegData));
int w = jpeg.Width;
int h = jpeg.Height;
byte[] alpha = SwfReader.Decompress(tag.CompressedAlphaData, (uint)(w * h));

// 透過情報に対応するために再フォーマット
// tag.JpegDataは前乗算済っぽいので 32bpp'P'Argb を使用すること
// 32bppArgb を使用すると二重に乗算処理され透過箇所が黒っぽくなってしまう
bmp = jpeg.Clone(new Rectangle(0, 0, w, h), PixelFormat.Format32bppPArgb);
//bmp = bmp.Clone(new Rectangle(0, 0, w, h), PixelFormat.Format32bppArgb);

// 透過情報を合成する
// GetPixel / SetPixel は体感できるレベルで遅いのでLockBitsしてunsafeで処理
// 並列化すればさらに高速化が望めるが、実用十分な速度が得られたのでこれでよしとする
BitmapData bmpData = null;
try
{
bmpData = bmp.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.ReadWrite, bmp.PixelFormat);
int pad = bmpData.Stride - w * 4; // 4byte境界に合わせるための端数byte(32bitなので常に揃っているはずだけど一応)

unsafe
{
byte* p = (byte*)bmpData.Scan0.ToPointer();
int i = 0;
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
// [0]=Blue, [1]=Grean, [2]=Red, [3]=Alpha
p[3] = alpha[i];
p += 4;
i++;
}
p += pad;
}
}
}
finally
{
if (bmpData != null)
{
bmp.UnlockBits(bmpData);
}
}
break;
}
return bmp;
}
}

画像データがバイト配列なのでMemoryStreamからBitmapを生成することになりますが
生憎Streamを扱えるコンストラクタでPixelFormatを指定できるものはありません。
なので一旦普通に読み込んでから、PixelFormatを指定してCloneしてます。
その他の注意点などもコメントで入れてあるので参考にしていただければ幸いです。



さて、次はDefineBitsLosslessの処理です。
といってもこっちはGetBitmapというそのものズバリのメソッドが
用意されているのでそれを呼ぶだけです。
さきほどの拡張メソッド名も一応これに合わせてあるんですが、
ISwfTagインターフェースに定義されてるわけじゃないのでキャストが必要です。
ふいんきだけぽりもふぃずむです。


var reader = new SwfReader(swfData);
var swf = new SwfCompilationUnit(reader);
var jpegTablesTag = swf.Tags.FirstOrDefault(x => x.TagType == TagType.JPEGTables) as JPEGTables;
if (jpegTablesTag != null && 0 < jpegTablesTag.JpegTable.Length)
{
// JPEGTablesTagにデータが存在する場合には未対応
throw new NotImplementedException();
}
var imageTags = swf.Tags.FindAll(x =>
x.TagType == TagType.DefineBits ||
x.TagType == TagType.DefineBitsJPEG3 ||
x.TagType == TagType.DefineBitsJPEG2 ||
x.TagType == TagType.DefineBitsLossless ||
x.TagType == TagType.DefineBitsLossless2);
for (int i = 0; i < imageTags.Count; i++)
{
var tag = imageTags[i];
Bitmap bmp = null;
try
{
// タグの画像データからBitmapオブジェクトを生成
switch (tag.TagType)
{
case TagType.DefineBits:
case TagType.DefineBitsJPEG2:
case TagType.DefineBitsJPEG3:
bmp = ((DefineBitsTag)tag).GetBitmap();
break;

case TagType.DefineBitsLossless:
case TagType.DefineBitsLossless2:
bmp = ((DefineBitsLosslessTag)tag).GetBitmap();
break;
}

if (bmp != null)
{
// 処理
}
}
finally
{
if (bmp != null)
{
bmp.Dispose();
}
}
}


これで一見うまく抽出できているように見えたものの、
いくつかSWFファイルを処理させてみると、斜めに引き伸ばされたような感じに
歪んだ画像が出力されることがあることに気づきました。
斜めということはなにか周期的なズレが発生するような問題だろうと予想されます。
しかし私が書いた部分ではそんな細かい処理に心当たりがなかったため
(というかGetBitmap呼んでるだけですし…)
早い段階からライブラリ(Swf2XNA)側のバグを疑っていました。

そして注意深くコードを追っていき、
画像の4byte境界を合わせるための端数を計算する処理にバグを見つけました。
DefineBitsLosslessTag.csの76行目を以下のように修正します。


if (BitmapFormat == BitmapFormat.Colormapped8Bit) // 8-bit colormapped image
{
this.ColorCount = (uint)r.GetByte() + 1;

this.isIndexedColors = true;
uint colorBytes = hasAlpha ? (uint)4 : (uint)3;
//uint padWidth = this.Width + (4 - (this.Width % 4));
uint padWidth = this.Width + ((4 - (this.Width % 4)) % 4);

// temp for debugging
uint pos = r.Position;
OrgBitmapData = r.GetBytes(curTagLen - 8);
r.Position = pos;
// end temp

ライブラリ使わせてもらっているのでほんとはpull requestとかした方がいいんでしょうが
gitとか良くわからんのでこの辺境のブログにひっそりと。
誰かの参考になれば良いなあと。
Windows/.NET comments(0) -


コメント


フォーム

ブログ内検索

自作ツールなど
■棒読みちゃんプラグイン
2ch専用ブラウザ読み上げ(改良版)

■IntelliPark設定ツール
WDIDLE3 for Windows

■マウスユーティリティ
Wheelpool

■ユーザー入力監視ソフト
iDLEM@STER

■さぽている攻略 [公開終了]
さぽつ~る(さぽつーる)
アイテムリスト成型
調合成功率計算
カテゴリー別

openclose

プロフィール

Author:百合亞
敬虔な百合信仰者かつ崇拝者
将来の夢は女の子

管理人にメール

お名前:
メール:
件 名:

りんく
「天結いキャッスルマイスター」応援中!

『想いを捧げる乙女のメロディー』2017年3月24日発売予定

オトメ*ドメイン

eye★phon(アイ・フォン)『つい・ゆり ~おかあさんにはナイショだよ~』

AXL新作第12弾「恋する乙女と守護の楯~薔薇の聖母~」 2016年1月29日発売予定!

お嬢様と秘密の乙女

カミツレ

FLOWERS

2014年発売予定のNavel新作『月に寄りそう乙女の作法2』を応援しています!

【ハピメア】応援バナー

【白雪の騎士】応援バナー

ノブレスオブルージュ

2013年発売予定のNavel新作『乙女理論とその周辺』を応援しています!

シロガネオトメ

『ヒメゴト・マスカレイド』応援中!

2012.10.26発売のNavel新作『月に寄りそう乙女の作法』を応援しています!

屋上の百合霊さん

天使の羽根を踏まないでっ

「キミとボクとエデンの林檎」公式サイトへ

『るいは智を呼ぶファンディスク』を応援しています!

『処女はお姉さまに恋してる ~2人のエルダー~』絶賛発売中!!

りんく2
藤真拓哉オフィシャルブログ
CrystalDiskInfo - 水晶雫

マリかう

Powered by FC2 Blog    Templete by hacca*days.

PR