Adsense_top

2010年3月6日土曜日

C# 画像を使った変形ウインドウの改良

以前書いたエントリー「C# 画像を使った変形ウインドウを作ってみた。」で
「for文2つとwhileの中にGetPixel()2つはものすごい時間がかかりそう。
Byte配列で処理するように変更すれば実用性がでそうだね。

とのコメントをいただきましたので、透過色以外の部分をRegionとして構成する部分をバイト配列での処理に再考し変更してみました。
今回は、変更したメソッド部のみ書いておきますので、他の部分は「C# 画像を使った変形ウインドウを作ってみた。」や「C# 変形ウインドウにマウス操作を追加した。」をご覧ください。

//
// 画像からウインドウの領域を作成し返します。
//
// 引数 bmp:対象画像
// 引数 transparencyKey:透過色
private Region BitmapToRegion(Bitmap bmp,
Color transparencyKey)
{
int height = bmp.Height;
int width = bmp.Width;
int xStart = 0;
System.Drawing.Drawing2D.GraphicsPath path =
new System.Drawing.Drawing2D.GraphicsPath();
// 縦方向の走査
for (int i = 0; i < height; i++)
{
// 横方向の走査
for (int j = 0; j < width; j++)
{
if (bmp.GetPixel(j, i) != transparencyKey)
{
// 透過色でない部分の連続を1ピクセル高の矩形にまとめ、
// GraphicsPathに追加していく
// (点ごとに登録処理するより効率がよいので)
xStart = j;
while ((j < width) &&
(bmp.GetPixel(j, i) != transparencyKey))
j++;
path.AddRectangle(
new Rectangle(xStart, i, j - xStart, 1));
}
}
}
// GraphicsPathからRegionを作成し返す。
Region region = new Region(path);
path.Dispose();
return region;
}

を以下のように変更しました。結構長くなってしまいましたの、内容はコメントを参考にMSDNの各クラスのリファレンスをお読みください。

// 以下の3つの行は、前のコードでは使用していなかったり
// コード内部でフルに書いていたため記述していなかった
// namespaceですので以前の分に追加が必要です。
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

private Region BitmapToRegion(Bitmap bmp, Color transparencyKey)
{
// if (bmp == null)
// throw new ArgumentNullException(
// "Bitmap", "Bitmapがnullを参照しています");
int height = bmp.Height;
int width = bmp.Width;
// 画像データをバイト配列にコピー
BitmapData bmpdat =
bmp.LockBits(new Rectangle(Point.Empty, bmp.Size),
ImageLockMode.ReadOnly, bmp.PixelFormat);
// コピー先の配列に領域確保
// bmpdat.Strideはスキャン幅
byte[] bytes = new byte[bmpdat.Stride * bmp.Height];
Marshal.Copy(bmpdat.Scan0, bytes, 0, bytes.Length);
bmp.UnlockBits(bmpdat);

GraphicsPath path =
new System.Drawing.Drawing2D.GraphicsPath();
// 8ビット・24ビット画像時の、BitmapDataで行が4バイト毎に行が
// 切り上げられるために、実際の画像幅との差、
// および24ビット・32ビット画像時のループカウンタと配列位置の
// 差を埋めるために使用する値を、変数diffに設定します。
int diff = bmpdat.Stride - width
// ループカウンタと配列位置の差を格納する変数
int adjust = 0;
int xStart = 0;
// 8ビットインデックス付きカラーデータ
if (bmp.PixelFormat == PixelFormat.Format8bppIndexed)
{
// カラーパレット(Color配列)から、透過色のインデックスを取得
int index =
Array.IndexOf(bmp.Palette.Entries, transparencyKey);

for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
// 一次配列のため、位置を調節しながら透過色と異なるか確認
if (bytes[i * width + j + adjust] != index)
{
// 透過色でないピクセルを連続するパスとしてパスを追加
xStart = j;
while ((j < width) &&
(bytes[i * width + j + adjust] != index))
j++;
path.AddRectangle(
new Rectangle(xStart, i, j - xStart, 1));
}
}
adjust += diff;
}
}
// 24ビットカラーデータ
else if (bmp.PixelFormat == PixelFormat.Format24bppRgb)
{
// 透過色をバイト配列に変換
// (後で比較する時に分かり易いように、RGB順でなくBGR順にしています)
Byte[] transBytes =
new Byte[] { transparencyKey.B, transparencyKey.G,
transparencyKey.R };
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
// 一次配列のため、位置を調節しながら透過色と異なるか確認
// バイト配列に前方方向からBGRの順に値が入っている
if (bytes[i * width + j * 3 + adjust] != transBytes[0] ||
bytes[i * width + j * 3 + 1 + adjust] != transBytes[1] ||
bytes[i * width + j * 3 + 2 + adjust] != transBytes[2])
{
// 透過色でないピクセルを連続するパスとしてパスを追加
// 以下の配列の比較はC# 3.0では、SequenceEqualが使えます。
xStart = j;
while ((j < width) &&
(bytes[i * width + j * 3 + adjust] != transBytes[0] ||
bytes[i * width + j * 3 + 1 + adjust] != transBytes[1] ||
bytes[i * width + j * 3 + 2 + adjust] != transBytes[2]))
j++;
path.AddRectangle(new Rectangle(xStart, i, j - xStart, 1));
}
}
adjust += diff;
}
}
// 32ビットカラーデータ
// ビットマップ側のアルファ値は実質無視しています。
else if (bmp.PixelFormat == PixelFormat.Format32bppArgb)
{
// 透過色をバイト配列に変換
// (後で比較する時に分かり易いように、ARGB順でなくBGRA順にしています)
Byte[] transBytes =
new Byte[] { transparencyKey.B, transparencyKey.G,
transparencyKey.R,transparencyKey.A };
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
// 一次配列のため、位置を調節しながら透過色と異なるか確認
// バイト配列に前方方向からBGRAの順に値が入っている
if (bytes[i * width + j * 4 + adjust] != transBytes[0] ||
bytes[i * width + j * 4 + 1 + adjust] != transBytes[1] ||
bytes[i * width + j * 4 + 2 + adjust] != transBytes[2] ||
bytes[i * width + j * 4 + 3 + adjust] != transBytes[3])
{
// 透過色でないピクセルを連続するパスとしてパスを追加
xStart = j;
while ((j < width) &&
(bytes[i * width + j * 4 + adjust] != transBytes[0] ||
bytes[i * width + j * 4 + 1 + adjust] != transBytes[1] ||
bytes[i * width + j * 4 + 2 + adjust] != transBytes[2] ||
bytes[i * width + j * 4 + 3 + adjust] != transBytes[3]))
j++;
path.AddRectangle(new Rectangle(xStart, i, j - xStart, 1));
}
}
adjust += diff;
}
}
else
{
// 今回は上記の3つのみ対応したため他のPixelFormatは
// 例外をスローします。
throw new Exception(
string.Format("'{0}'はサポートされていないカラーデータ形式です。",
bmp.PixelFormat.ToString()));
}
Region region = new Region(path);
path.Dispose();
return region;
}

Stopwatchクラスで計測してみたところ、何と平均で100倍前後高速でした。
コメントをいただいた方(名前は入力されていませんでした)ご指摘ありがとうございました。


2 件のコメント:

  1. 出来たら前のコードと合わせて出して頂けると
    良いかなぁ…って思いました。
    これをちょっとデスクトップマスコットもどきに
    流用しようとちょっと実験してるので。

    返信削除
  2. mizukiさんへ
    前回の分と合わせると長くなるのでこのような形で書きました。「http://pub.ne.jp/arayan/?entry_id=2428565」の方に前回の完成形がありますので、お手数ですがそちらをご覧ください。

    返信削除