MagickNetでGIFアニメーションを作ろう

前回は9分割した画像を合成してもとに戻すというシナリオで画像合成について説明しました。

今回はGIFアニメーションを作成してみましょう。
シナリオとしては、前回まで使っていた
sample-mini
を9分割し、600ms間隔で各パーツを表示し
最後のパーツが表示されてから600ms後に合成されたものを表示するというGIFアニメーションを作成します。

今回は各フレームをMagickImageであらわし、それを束ねてアニメーションを行うためMagickImageCollectionというクラスを利用します。
MagickImageCollectionに複数のMagickImageを束ねて出力することでGIFアニメーションを作成することができます。

事前に今回3つのポイントについて説明します。

一つめは以下のコンストラクタを使ってまとめて登録することが可能です。

using(var collection in new MagickImageCollection(IEnumerable<MagickImage> images))
{
 //処理
}

追記:初出時、60msと誤記していましたが、600msの間違いでした。既に訂正済みです。

また、600msの間隔はcollectionで指定するのではなく、各フレーム用のMagickImage側で指定します。
以下のプロパティに値を設定します。
単位は1/100secで1です。つまり60と指定すると600msとなります。
前のフレームに対して600ms間隔をあけて表示されます。

MagickNet.AnimationDelay = 60;

最後に各フレーム間の差があるところだけを使うことでファイルを最適化するために以下のメソッドを使います。
MagickImageCollectionに用意されています。

MagickImageCollection.Optimize();

また、今回は分割した結果はファイルではなくバイト配列で持ちます。
上記を踏まえてサンプルコードは以下のようになります。

        private static void GIFアニメーションを作るよ()
        {
            var path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample.jpg");
            byte[] byteArray = null;
            var width = 0;
            var height = 0;
            using (var image = new MagickImage(path))
            {
                //リサイズ他にもスケールする%を渡したりなど、いろいろできるので今回は20%指定で
                image.Resize(0.2);

                //リサイズ後のサイズを取得
                width = image.Width;
                height = image.Height;

                //pngを指定するよ
                image.Format = MagickFormat.Png;

                //バイト配列に出力
                byteArray = image.ToByteArray();
            }

            //今回は分割した部分はバイト配列で持っておきます
            var images = Enumerable.Range(0, 9).Select(i =>
            {
                using (var divImage = new MagickImage(byteArray))
                {
                    var x = width / 3 * (i % 3);
                    var y = height / 3 * (i / 3);

                    divImage.Crop(new MagickGeometry(x, y, width / 3, height / 3));

                    return new { index = i, image = divImage.ToByteArray() };
                }
            }).ToDictionary(x => x.index, x => x.image);

            //MaigckImageを束ねるコレクションに9分割したそれぞれの部分をまとめて登録します
            using(var collection = new MagickImageCollection(
                Enumerable.Range(0, 9).Select(i =>
                {
                    var canvas = new MagickImage(MagickColor.Transparent, width, height);
                    using (var divImage = new MagickImage(images[i]))
                    {
                        var x = width / 3 * (i % 3);
                        var y = height / 3 * (i / 3);
                        //ここで合成しているよ。
                        canvas.Composite(divImage, x, y, CompositeOperator.Over);
                    }
                    //ここで600msずつ遅らせることを指定します
                    canvas.AnimationDelay = 60;
                    return canvas;
                })))
            {
                
                //すべての部分が表示されるコマを作成します
                var canvas = new MagickImage(MagickColor.Transparent, width, height);
                foreach(var i in Enumerable.Range(0, 9))
                {
                    using (var divImage = new MagickImage(images[i]))
                    {
                        var x = width / 3 * (i % 3);
                        var y = height / 3 * (i / 3);
                        //ここで合成しているよ。
                        canvas.Composite(divImage, x, y, CompositeOperator.Over);
                    }
                    //ここでも600ms遅らせることを指定します
                    canvas.AnimationDelay = 60;
                }
                collection.Add(canvas);

                //保存するまえにAnimationGifにする時各コマの差分だけがCropされてフレームになるように最適化を行います。
                collection.Optimize();
               
                var outputPngPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "animation.gif");
                collection.Write(outputPngPath);
            }

        }

そして結果は以下のようになります。

animation

このように、MagickNetでは少ないコード量でGIFアニメーションを作ることが可能です。

追記:
ちなみに、MagickImage側でAnimationDelayするならCollectionじゃなくても前回までのMagickImage.Compositeでいいのでは?と思うかも(思わないかも)しれませんが、その指定は無視されてただの合成画像になるだけです。

FacebookTwitterHatenaTumblr共有

MagickNetで画像を合成してみよう

前回は画像の切り取りかたについて説明しました。

復習

今回は、復習として、いつもの画像を9分割にして保存してみましょう。

リサイズ後の元画像は以下の通り。
sample-mini

サンプルは以下の通りです

        private static void リサイズして9分割()
        {
            var path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample.jpg");
            byte[] byteArray = null;
            var width = 0;
            var height = 0;
            using (var image = new MagickImage(path))
            {
                //リサイズ他にもスケールする%を渡したりなど、いろいろできるので今回は20%指定で
                image.Resize(0.2);

                //リサイズ後のサイズを取得
                width = image.Width;
                height = image.Height;
    
                //pngを指定するよ
                image.Format = MagickFormat.Png;

                //バイト配列に出力
                byteArray = image.ToByteArray();
            }

            foreach (var i in Enumerable.Range(0, 9))
            {

                using (var divImage = new MagickImage(byteArray))
                {
                    var x = width / 3 * (i % 3);
                    var y = height / 3 * (i / 3);
                    //ここで画像を切り取っているよ
                    divImage.Crop(new MagickGeometry(x,y,width / 3,height/3));

                    var outputPngPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), string.Format("image{0}.png", i));
                    divImage.Write(outputPngPath);
                }
            }
        }

今回一度リサイズしpngにした後の結果をファイルに保存するのではなく、

                //バイト配列に出力
                byteArray = image.ToByteArray();

とすることで、そろぞれを9分割して保存するときに、毎回元ネタの画像を読むのではなく既に読み込んであるバイト配列を以下のように後続のMagickImageで利用することができています。

                using (var divImage = new MagickImage(byteArray))
                {

分割後の結果は以下のようになります。

image0 image1 image2
image3 image4 image5
image6 image7 image8

本題

復習はここまでで、今回の本題合成に入ります。

題材として上の分割した画像を合成して、元の画像に戻します。

合成する方法はいくつかありますが今回紹介するのは以下のメソッドを利用した方法です。
オーバーロードが9種類あるのですが、今回は、以下を使います。
MaigckImageInstanceは合成したい画像。
x,yはオフセット、CompositeOperatorは、どうやって合成するのかを指定するEnumとなっています。

    MagickImage.Composite(MaigckImageInstance,x,y,CompositeOperator);

CompositeOperatorに定義されている合成方法には60種類以上があるのですが、今回は特にほかの合成画像と重ねるわけではないのでOverを利用します。

サンプルコードは以下のようになります。
リサイズして9分割した後に、それを復元します。

        private static void リサイズして9分割した後に復元()
        {
            //リサイズして9分割するコードは上記と同じなので省略

            //復元するためのキャンバスを透過色でリサイズ後のサイズで作成します
            using (var canvas = new MagickImage(MagickColor.Transparent, width, height))
            {
                foreach (var i in Enumerable.Range(0, 9))
                {
                    var readPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), string.Format("image{0}.png", i));
                    //ここで分割した画像を読んでいます
                    using (var divImage = new MagickImage(readPath))
                    {
                        var x = width / 3 * (i % 3);
                        var y = height / 3 * (i / 3);
                        //ここで合成しているよ。分割した画像とオフセット、合成方法を渡しています
                        canvas.Composite(divImage, x, y, CompositeOperator.Over);
                    }
                }

                var outputPngPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "image-merge.png");
                canvas.Write(outputPngPath);
            }
        }

すると結果は以下のようになります。
image-merge

このように、合成も簡単にできます。
Overのほかにも60種類上の合成方法(オーバーレイしてみたりなど)がありますのでいろいろなCompositeOperatorを与えてみて確認してみるのも面白いと思います。

MagickNetで画像を切り取ってみよう

前回は、画像の回転と画像フォーマットの変更について説明しました。

今回は、画像を切り取ってみようと思います。

リサイズで20%にした画像を中心を軸に縦横半分ずつ残します。

リサイズしただけの画像
sample-mini

画像を切り取るときにはCropというメソッドを利用し、オーバーロードが3種類あります。

                
//重心を規定し、そこから縦横指定したサイズで切り取る
image.Crop(width, height, Gravity.Center);               
//中心を重心とし、そこから縦横指定したサイズで切り取る
image.Crop(width, height);               
//平面領域を指定し、そこを残す(下の例だと開始位置と、そこからのサイズを指定している)
image.Crop(new MagickGeometry(x,y,width,height));

上2つは基本的にはGravityによって指定された場所から切り取る指定方法となっている。

では実際に試してみましょう。
以下にそれぞれのサンプルを示します。
両方共結果はおなじになります。

重心を中心に指定して、そこから元サイズの半分ずつ残すサンプル

        private static void リサイズして中央から縦横半分ずつ切り取ってpngで保存()
        {
            var path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample.jpg");
            using (var image = new MagickImage(path))
            {
                //リサイズ他にもスケールする%を渡したりなど、いろいろできるので今回は20%指定で
                image.Resize(0.2);

                //リサイズ後のサイズを取得
                var width = image.Width;
                var height = image.Height;

                //中心から半分ずつ残して画像を切り取る
                image.Crop(width / 2, height / 2, Gravity.Center);               
                
                //pngを指定するよ
                image.Format = MagickFormat.Png;

                var outputPngPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample-mini-cropGravity.png");
                image.Write(outputPngPath);

            }
        }

結果
sample-mini-cropGravity

領域を指定して切り取るサンプル

        private static void リサイズしてポジション指定で縦横半分ずつ切り取ってpngで保存()
        {
            var path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample.jpg");
            using (var image = new MagickImage(path))
            {
                //リサイズ他にもスケールする%を渡したりなど、いろいろできるので今回は20%指定で
                image.Resize(0.2);

                //リサイズ後のサイズを取得
                var width = image.Width;
                var height = image.Height;

                //中心から半分ずつ残して画像を切り取る
                image.Crop(new MagickGeometry(width / 4, height / 4, width / 2, height / 2));


                //pngを指定するよ
                image.Format = MagickFormat.Png;

                var outputPngPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample-mini-cropGeometry.png");
                image.Write(outputPngPath);

            }
        }

結果
sample-mini-cropGeometry

というように、ケースに応じて、指定方法を使い分けて画像を切り取ることができます。

というわけで今回は画像の切り取りについて説明してみました。

MagickNetで画像の回転と画像フォーマットの変更をしよう

前回のMagickNetことはじめに続いて、画像を回転させてみましょう。

あ、あけましておめでとうございます。今年もよろしくお願いします。

前回はリサイズで20%のサイズにしました。
今回はそれに加えて画像を45°傾けてみましょう。

とりあえずjpegのままで保存します。
コードとしては以下のように前回のコードにRotateが追加されただけです。
これに傾けたい度数をdoubleで与えてあげるだけで画像が傾きます。

                image.Rotate(45d);

サンプル:

            var path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample.jpg");
            using (var image = new MagickImage(path))
            {
                //リサイズ他にもスケールする%を渡したりなど、いろいろできる。
                image.Resize(387, 518);
                //ついでに45度傾けてみようか
                image.Rotate(45d);

                var outputPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample-mini-rotate45.jpg");
                //これで保存
                image.Write(outputPath);

            }

でできるのが以下の画像
45°傾けたので、全体のサイズが大きくなり、新しくできた背景の部分は白くなっています。(わかりやすくするためにブログの背景を一部、黒にしています)

sample-mini-rotate45

できれば、新しくできた背景は透過にしたいので、pngに変換して保存してみます。
変換するときは、以下のようにFormatプロパティに変換したいファイルフォーマットを指定するだけです。

                //pngを指定するよ
                image.Format = MagickFormat.Png;

サンプル:

            var path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample.jpg");
            using (var image = new MagickImage(path))
            {
                //リサイズ他にもスケールする%を渡したりなど、いろいろできる。
                image.Resize(387, 518);
                //ついでに45度傾けてみようか
                image.Rotate(45d);

                //pngを指定するよ
                image.Format = MagickFormat.Png;

                var outputPngPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample-mini-rotate45.png");
                image.Write(outputPngPath);

            }

で以下のようにPNGで保存されています。
が、新しい背景の部分は依然白いままです。
無駄にファイルサイズだけ大きくなった感じですね。

sample-mini-rotate45

ではちゃんと回転後新しい領域が透過になるようにしてみます。
こちらも簡単で、回転等の操作をする前にBackgroundColorプロパティにMagickColor.Transparentを指定するだけです。

                image.BackgroundColor = MagickColor.Transparent;

サンプル:

            var path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample.jpg");
            using (var image = new MagickImage(path))
            {
                //リサイズ他にもスケールする%を渡したりなど、いろいろできる。
                image.Resize(387, 518);

                //背景色を透過に設定しておく
                image.BackgroundColor = MagickColor.Transparent;

                //ついでに45度傾けてみようか
                image.Rotate(45d);

                //pngを指定するよ
                image.Format = MagickFormat.Png;

                var outputPngPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "sample-mini-rotate45-transparent.png");
                image.Write(outputPngPath);

            }
sample-mini-rotate45-transparent

次はちゃんと新しい領域が透過された画像になりました。

というわけで、今回は画像の回転と画像フォーマットの変更を合わせて説明してみました。
このように、簡単に使えるのがよいところですね。