2014/02/22

Exifのサムネイル

カメラで写真を撮ると自動的にExifのメタデータが画像ファイルに記録される。というのは言うまでもないが、その一つとしてサムネイル画像も埋め込まれることはあまり意識されてないと思う。

この埋め込みサムネイル画像が実際どこまで利用されているかは知らないが、論理的には、これを利用すればサムネイル表示をする度に画像全体を読み込んでサムネイル画像を作成するより素早く表示することができる(Windowsでは一度作成したサムネイルデータをフォルダーごとに保存しているので、表示する度に作成しているわけではないが)。

ではアプリでサムネイル表示をするときにどれぐらい差があるか、確かめてみた。

1. コーディング

言語はC#で、以下のようなサムネイル表示機能を持つ.NET Framework 4.5のWPFアプリを作成した。

1.1. System.Drawingでサムネイル画像を読み出す

まずSystem.Drawing.Imageを使って画像ファイルからサムネイル画像だけ読み出す例があったので、これを参考にさせていただく。
サムネイルデータのプロパティIDは0x501B(Property Item DescriptionsのPropertyTagThumbnailData)なので、このプロパティの値を読み出してBitmapImageに出力する。画像ファイルのパスをlocalPathで指定。
private const int ThumbnailDataId = 0x501B; // Property ID for PropertyTagThumbnailData

public static BitmapImage ReadThumbnailFromExifByDrawing(string localPath)
{
    if (!File.Exists(localPath))
        return null;

    using (var fs = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        using (var drawingImage = Image.FromStream(fs, false, false)) // System.Drawing.Image
        {
            if (!drawingImage.PropertyIdList.Any(propertyId => propertyId == ThumbnailDataId))
                return null;

            var property = drawingImage.GetPropertyItem(ThumbnailDataId);

            using (var ms = new MemoryStream(property.Value))
            {
                var image = new BitmapImage();
                image.BeginInit();
                image.CacheOption = BitmapCacheOption.OnLoad;
                image.StreamSource = ms;
                image.EndInit();

                return image;
            }
        }
    }
}
1.2. System.Windows.Media.Imagingでサムネイル画像を読み出す

WPF本来のSystem.Windows.Media.Imagingを使う方法としては、BitmapFrameのThumbnailプロパティから読み出す方法がある。
public static BitmapImage ReadThumbnailFromExifByImaging(string localPath)
{
    if (!File.Exists(localPath))
        return null;

    using (var fs = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        var frame = BitmapFrame.Create(fs, BitmapCreateOptions.DelayCreation, BitmapCacheOption.OnDemand);
        var source = frame.Thumbnail;

        if (source == null)
            return null;

        using (var ms = new MemoryStream())
        {
            var encoder = new JpegBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(source));
            encoder.Save(ms);
            ms.Seek(0, SeekOrigin.Begin);

            var image = new BitmapImage();
            image.BeginInit();
            image.CacheOption = BitmapCacheOption.OnLoad;                    
            image.StreamSource = ms;
            image.EndInit();

            return image;
        }
    }
}
少し難しいのは、
  • サムネイル画像だけ読み出そうとすれば、FileStreamからまずBitmapFrameのCreateメソッドに流し、そのThumbnailプロパティの値だけ通す必要がある。
  • 非同期に読もうとすれば、FileStreamからまずMemoryStreamに非同期にコピーする必要がある(BitmapFrameのCreateメソッドに非同期版はないので)。
という二つが両立しないことで、前者の方を取った形。ただ、BitmapFrameのCreateメソッドでBitmapCreateOptions.DelayCreationを指定すれば、すぐには読み込まず必要になったときに非同期で読み込むっぽいので、これでいいのかもしれないが、確証はない。

1.3. 画像ファイル全体を読んでサムネイル画像を作成する

比較のために画像ファイル全体を読んでサムネイル画像を作成する方法。サイズは埋め込みサムネイル画像のサイズが160×120pixelなので、これに合わせる。
public static async Task<BitmapImage> CreateThumbnailFromImageAsync(string localPath)
{
    if (!File.Exists(localPath))
        return null;

    using (var fs = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        using (var ms = new MemoryStream())
        {
            await fs.CopyToAsync(ms);
            ms.Seek(0, SeekOrigin.Begin);

            var image = new BitmapImage();
            image.BeginInit();
            image.CacheOption = BitmapCacheOption.OnLoad;
            image.StreamSource = ms;
            image.DecodePixelWidth = 160; // Width of Exif thumbnail
            image.DecodePixelHeight = 120; // Height of Exif thumbnail
            image.EndInit();

            return image;
        }
    }
}
こちらは普通の非同期処理。

2. テスト

これらの機能を使ったときにファイルをどれだけリードしたかをProcess Monitorで計測した。対象は以下のJPGファイルで、ファイルサイズは2,617,678Bytes=2.49MiB。
Wonder Festival Signboard in Snow

まずSystem.Drawingでサムネイル画像を読み出した場合。

サムネイル画像のサイズは7,048Bytesで、これはカメラによるのだろうが、圧縮率が高いらしく劣化が一目で分かる。このときのリードは以下のように4KBの繰り返し(最後の方だけ端切れで違う)。

合計した結果、リード量は410,956Bytesで、これはファイル全体の16%に当たる。が、リードの開始位置(Offset)に着目すると、ごく細切れに何度も重複する位置を読みに行っていて、実は先頭から8KBの範囲を繰り返し4KB単位で読むというかなり効率の悪いことをやっていた。まあキャッシュがあるから、これがそのままストレージへのアクセスになっているとは限らないが……。

次にSystem.Windows.Media.Imagingでサムネイル画像を読み出した場合。

サムネイル画像のサイズは7,043Bytesで、同じデータを読み出しているはずなのに微妙に違ってたりする。このときのリードも4KBの繰り返しだが、回数は圧倒的に少ない。

合計した結果、リード量は24,609Bytesで、ファイル全体の1%以下。System.Drawingより効率よく読んでいることが分かる。

最後に画像ファイル全体を読んでサムネイル画像を作成した場合。

サムネイルデータのサイズは8,317Bytesで、劣化はほとんど目立たない。このときのリードは以下のように80KBの繰り返し(同上)。

合計した結果、リード量は2,699,598Bytesで、ファイル全体と同じ量に加えて1回余分に読んでこの数字になっていた。

3. まとめ

埋め込みサムネイル画像を利用した方がリード量が少なくて済むことが確認できた。とくにSystem.Windows.Media.Imagingを使った場合はファイル全体の1%以下で、これならファイルアクセス時間もまず問題にならないと思う。

0 コメント :