C#
WPF
bitmap
BitmapSource

TransformedBitmapを使用してサムネイルを作成するとメモリ使用量が多い

事の発端

デジカメで撮った写真等を縮小して、ListBoxにサムネイルを表示するような画面を作成しました。

ListBoxのItemTemplateを使用してサムネイルを表示するようにして、ViewModelが保持しているBitmapSourceをバインドするコードを書いたのですが、画像縮小しているはずなのにメモリ使用量は縮小前の画像を保持しているとしか思えない量になってしまいました。

解決策

サムネイル画像を生成する部分のコードは下記のような感じでした。

        private BitmapSource CreateImage(string filePath, int scaledWidth, int scaledHeight)
        {
            try
            {
                using (System.IO.Stream stream = new System.IO.FileStream(
                    filePath,
                    System.IO.FileMode.Open,
                    System.IO.FileAccess.Read,
                    System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete
                ))
                {
                    // 画像をデコード
                    BitmapDecoder decoder = BitmapDecoder.Create(
                        stream,
                        BitmapCreateOptions.PreservePixelFormat,
                        BitmapCacheOption.OnLoad
                    );

                    BitmapSource bmp = null;
                    if ((scaledWidth > 0) && (scaledHeight > 0))
                    {
                        // 拡大/縮小したイメージを生成する
                        double scaleX = (double)scaledWidth / decoder.Frames[0].PixelWidth;
                        double scaleY = (double)scaledHeight / decoder.Frames[0].PixelHeight;
                        double scale = Math.Min(scaleX, scaleY);

                        // TransformedBitmapをそのまま保持する
                        bmp = new TransformedBitmap(decoder.Frames[0], new ScaleTransform(scale, scale));
                    }
                    else
                    {
                        // 原寸でイメージを生成する
                        bmp = new WriteableBitmap(decoder.Frames[0]);
                    }
                    bmp.Freeze();

                    return bmp;
                }
            }
            catch (Exception exc)
            {
                System.Diagnostics.StackFrame sf = new System.Diagnostics.StackFrame(1);
                System.Diagnostics.Debug.WriteLine(string.Format("[{0}]エラー:{1}", sf.GetMethod(), exc));
            }

            return null;
        }

ソースコードの、TransformedBitmapをそのまま保持する部分を、下記のように、TransformedBitmapからWritableBitmapを再作成するように変更することで解消できます。

// 生成したTransformedBitmapから再度WritableBitmapを生成する
bmp = new WriteableBitmap(new TransformedBitmap(decoder.Frames[0], new ScaleTransform(scale, scale)));

サンプルコード

サンプルコードは下記から取得して下さい。

Visual Studio 2015 Communityにて.Net Framework 4.5 を対象に作成しましたが、バージョンにはほとんど依存しないはずです。

https://github.com/rot-z/ThumbnailMemoryReductionSample.git

サンプルアプリケーションの使用方法
ビルドして起動すると、下記のような画面のアプリが起動します。

217ddc89-s.jpg

左上のラジオボタンにて、ListBoxに追加するサムネイルを生成する際のソースコードを選択します。


TransformedBitmap

修正前のコード(TransformedBitmapをそのまま保持)でサムネイル生成します

WrapWithWritableBitmap
修正後のコード(WritableBitmapで再作成)でサムネイル作成します

「Add contents」ボタンを押下するとフォルダ選択ダイアログが開き、指定したフォルダ内のすべての画像ファイルをサムネイル表示します。
手元の環境で、iPhone内の画像100枚、平均2.8MBをサムネイル表示してみた結果は下記のとおりです。

  • TransformedBitmapにてサムネイル生成
    8ab5d566-s.jpg

  • WrapWithWritableBitmapにてサムネイル生成
    64681f17-s.jpg

単純にコミットチャージ量だけで比較するのもどうかと思いますが、修正後のコードの方が劇的にメモリ使用量が減っているのがわかると思います。

原因

おそらく、TransformedBitmapが縮小前のBitmapFrameに対する参照を抱えているのだと思いますが、まじめに調べていないです。
ごめんなさい。

時間があったらTransformedBitmapのソースコード確認してみようと思います。
(あ、これ、結局やらないパターンな予感・・・)

参照

stackoverflowで同じような問題で困っている人がいた・・・

https://stackoverflow.com/questions/33283461/release-original-imagesource-after-a-transformedbitmap-is-constructed-from-it