テスターですが何か?

ホビープログラマ略してHPです

画像をSVGで描画しようとしてみた→挫折→そしてCanvasが本気を出す

with 6 comments

そろそろクラウディアタイマーを更新しようと考えています。もともと思いつきで2~3日で作ったものなので、もっとああしたい、こうしたいというアイデアがあるんですよ。その1つに

画像を画像のまま描画してもおもしろくないよね?

があります。画像でクラウディアを描画するのではなく、HTML5のSVGをつかってクラウディアを描画しようと思います。で、どうやってSVGのタグを生成するかというと、JPEG画像を読み込んでピクセル単位で色情報を解析してSVG用のタグ(縦横1ピクセルの正方形)を生成します。超細かいドット絵のイメージですね。ソースはこんな感じです。

    class Program

    {

        // 画像のピクセル情報リスト

        private static List<PixelData> PixelList;

 

        static void Main(string[] args)

        {

            // ローカルに画像が保存されている場合はこのようになる

            string ImagePath = @"D:\Develop\sample\SVGClaudia\SVGClaudia\image\クラウディアSD1.jpg";

            Bitmap Bitmap = new Bitmap(ImagePath);

 

            // 画像をピクセルごとに色情報を取得してデータ化する

            PixelList = new List<PixelData>();

            PixelData d = null;

 

            for (var y = 0; y < Bitmap.Height; y++)

            {

                for (var x = 0; x < Bitmap.Width; x++)

                {

                    d = new PixelData();

                    d.x = x;

                    d.y = y;

                    d.ColorName = ColorTranslator.ToHtml(Bitmap.GetPixel(x, y));

                    PixelList.Add(d);

                }

            }

 

            foreach (var p in PixelList)

            {

                    Console.WriteLine("<rect x=\"{0}\" y=\"{1}\" fill=\"{2}\" width=\"1\" height=\"1\"/>", p.x.ToString(), p.y.ToString(), p.ColorName);

            }

        }

    }

 

これで実際にテキスト形式のファイルを作成したところ、オリジナル画像で87MBになりましたwww 1/4サイズの画像でも、5.3MBでしたwww こんなサイズのHTMLじゃ話にならないですね…ブラウザで描画させようという気にもなりません。このSVGのサイズをダイエットする必要があります。

その1.白は描画しない。ブラウザの背景は白ですから、白の長方形を描画しても意味がありません。色が白”#FFFFFF”の場合はタグを出力しないようにします。

その2.右隣が同じ色であれば、長方形(縦1ピクセル、横Nピクセル)を描画する。横方向に同じ色が続いているのであれば、正方形をたくさん描画するのではなく、長方形を1つ描画することでタグを削減できます。本来であれば縦方向にも同じ色が続くかどうかも判定したほうがタグの削減につながるのですが、今回は横方向だけにしました。

ソースはこうなります。

    class Program

    {

        // 画像のピクセル情報リスト

        private static List<PixelData> PixelList;

 

        static void Main(string[] args)

        {

            // ローカルに画像が保存されている場合はこのようになる

            string ImagePath = @"D:\Develop\sample\SVGClaudia\SVGClaudia\image\クラウディアSD1_25.jpg";

            Bitmap Bitmap = new Bitmap(ImagePath);

 

            // 画像をピクセルごとに色情報を取得してデータ化する

            PixelList = new List<PixelData>();

            PixelData d = null;

            string PixelColor;

 

            // 継続判定用

            string PreColor = null;

            int XStart = 0;

 

            for (var y = 0; y < Bitmap.Height; y++)

            {

                for (var x = 0; x < Bitmap.Width; x++)

                {

                    PixelColor = ColorTranslator.ToHtml(Bitmap.GetPixel(x, y));

 

                    if ((string.IsNullOrEmpty(PreColor) || PreColor == PixelColor) && x != Bitmap.Width – 1)

                    {

                        // 継続

                        PreColor = PixelColor;

                        continue;

                    }

 

                    d = new PixelData();

                    d.x = XStart;

                    d.y = y;

                    d.l = x – XStart;

                    d.c = PreColor;   // 色は分かりやすくするため、HTMLの色情報に変換

                    if (PreColor != "#FFFFFF")

                    {

                        PixelList.Add(d);

                    }

 

                    PreColor = PixelColor;

                    if (x != Bitmap.Width – 1)

                    {

                        XStart = x;

                    }

                    else

                    {

                        XStart = 0;

                    }

 

                }

            }

 

            foreach (var p in PixelList)

            {

                if (p.c != "#FFFFFF")

                {

                    Console.WriteLine("<rect x=\"{0}\" y=\"{1}\" fill=\"{2}\" width=\"{3}\" height=\"1\"/>", p.x.ToString(), p.y.ToString(), p.c, p.l.ToString());

                }

            }

        }

    }

 

これで1/4サイズの画像からタグを生成すると、1.9MBです。う~ん、減ったことは減ったんですが、1MB未満に抑えたいですよね。試しにブラウザで描画してみると、Chromeなら1瞬、IE9でも1~2秒くらいで描画できました。

ただ、クラウディアの4頭身の画像は全部で4種類あるので、静的なHTMLにするわけにはいかないんですよ。動的にこの約2MBのタグを生成する必要があります。なので、サーバーから先程画像から取得したピクセル単位の色情報をSVGのタグではなく、JSON形式で受け取れるように変更します。先程のメソッド名をMakeに変更しPixelDataのListを返すように変更します、サーバーサイド(ASP.NET MVC)で、JSON形式でクライアントに返すアクションを書きます。

private static List<PixelData> SVG_SD1;

 

public ActionResult Index2()

{

    if (SVG_SD1 == null)

    {

var p = new MakePixelInfo();

SVG_SD1 = new List<PixelData>();

SVG_SD1 = p.Make();

    }

 

    return Json(SVG_SD1, JsonRequestBehavior.AllowGet);

}

クライアント(HTML)はJSON形式のデータを受け取り、JavaScriptでSVGタグを動的に生成します。

@*SVG描画(インライン)バージョン

_Layout.cshtmlXHTML宣言する必要がある*@

<script type="text/javascript">

    $(function () {

        $.getJSON("Timer/Index2",

            function (data) {

                $.each(data, function (key, val) {

                var rect = document.createElementNS(http://www.w3.org/2000/svg&#8217;, ‘rect’);

                rect.setAttribute(‘x’, val.x);

                rect.setAttribute(‘y’, val.y);

                rect.setAttribute(‘width’, val.l);

                rect.setAttribute(‘height’, 1);

                rect.setAttribute(‘fill’, val.c);

                $(‘svg’).append(rect);

            });

        });

    });

</script>

<h2>Index</h2>

<svg>

</svg>

 

1.4MBの通信を行なっています(JSON形式なのでHTMLタグに比べてデータ量が削減された)が、「これでイケる!!」ええ、そう思った時期が自分にもありました。心踊らせてF5キーを叩きます…遅い…え…これ、ブラウザのベンチマークプログラムですか?ってくらいに遅いです。IE9で遅いのはもちろん、Chrome18でもクラウディアの表示に3分くらいかかります。クアッドコア、12GBメモリのPCなので、スペックが足りないことはないと思いますが。

ブラウザのJavaScriptの実行速度が上がったといっても、JavaScriptで約33,000個のSVGタグを動的に生成するのは無理があるようです。タグさえ生成できれば表示はあっという間なんですが。C#なら33,000回ループして文字列を追加するのはあっという間なんですが、JavaScriptの実行速度をC#と同じと考えてはいけませんね。

HTML5にはもう1つ、図形を描画するタグがあります。そう、<canvas>ですね。HTML+JavaScriptでグラフを描画するライブラリは、この<canvas>タグが使われていることが多いです。先ほどのSVG描画のcshtmlをcanvas描画に変更します。

@*Canvas描画バージョン*@

<script type="text/javascript">

    $(function () {

 

        $.getJSON("Timer/Index2",

            function (data) {

                $.each(data, function (key, val) {

 

                    var canvas = document.getElementById(‘claudia’);

                    if (canvas.getContext) {

                        var context = canvas.getContext(‘2d’);

 

                        context.fillRect(val.x, val.y, val.l, 1);

                        context.fillStyle = val.c;

 

                    } else {

 

                    }

 

                });

            });

    });

</script>

<canvas id="claudia" width="214" height="426" class="backimage">

</canvas>

実行してみます。

速っ!!

ダウンロードが完了するとほぼ一瞬で描画まで完了します。JavaScriptで33,000回のループを行うと遅いのではなく、動的にSVGタグを生成する処理が遅いんですね。ただ、あの記述以外にどうやってSVGタグを生成していいのかわかりません…とりあえずcanvasを使用して目的は果たせたのでよしとしましょう。こんな感じでクラウディアを描画できています、最初にも言いましたが、これはJPEG画像ではなく長方形の集まりです。(JPEGも原理的には同じかもしれませんが)

キャプチャ

今回からしばらくは新しいクラウディアタイマーの開発をしていきます、Windows Developer Dayまでには完成させたいな。作成したソースはこちらにアップしてあります、「SVGClaudiaTimer.zip」をダウンロードしてください。

 

Written by david9142

2012年4月10日 @ 12:54 AM

コメント / トラックバック6件

Subscribe to comments with RSS.

  1. JavaScriptでSVGのところ、タグをcreateElementNSで作ってappendするのではなく、全部文字列処理にすると早くなるかもしれません。文字列をinnerHTMLプロパティにセットします。
    #すぐ試せる環境にないので試してません、ごめんなさい。

    なおJavaScriptでもC#同様、文字列連結は遅い処理なので、配列のjoinを使うテクニックがよく使われます。例えば:
    [ ” ].join(”);
    このjoin結果を文字列連結しないようにご注意を。join結果もjoinした方が速いです。

    神原淳史 (@atsukanrock)

    2012年4月10日 at 1:28 AM

    • あぁ、JavaScriptのサンプルコードがカットされてしまいました。。。

      神原淳史 (@atsukanrock)

      2012年4月10日 at 1:29 AM

      • コメントありがとうございます。jQueryで$(‘svg’).html()を使ってhtmlを直接出力してみましたが、描画されませんでした。

        $.each(data, function (key, val) {
        $(‘svg’).html(”);
        });

        とか

        $(‘svg’).html(”);

        SVGはXHTML宣言しないと描画できないのでそれが関係あるのかもしれません。あと、jQueryでは動的にSVGを生成できないと見た気が…jQueryを使わなければ文字列操作でいけるのかもしれません。あとで時間を作って試してみます。

        david9142

        2012年4月10日 at 1:59 AM

  2. SVG要素を作るのと、点を描画するだけのCANVASではスピードに差がありすぎるのはしようがないですよ。

    SVG
     
    CANVAS
     drawImage()
    というのもありますが、
    画像を解析して1ピクセルずつ表示しているのって、実験ですよね、、、?

    あと、ブラウザってマルチスレッドに対応していないのでは?

    Dab

    2012年4月12日 at 8:21 AM

    • タグが消えた、、、全角ならいけるかな?
      SVG
      <image xlink:href=”picture.png” x=”0″ y=”0″ width=”100″ height=”300″ />

      Dab

      2012年4月12日 at 8:22 AM

  3. はじめまして。
    DOMツリーに追加するとその度に画面レイアウトと再描画が走るため遅いのかと思います。
    documentへ追加前にSVG要素を全部作成しておいて、後でまとめてdocumentへappendすれば早くなるのではないでしょうか?

    dsuke (@dsuket)

    2012年7月2日 at 4:00 PM


コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。