MPOファイルをJPEGに変換する

ステレオ3Dカメラ搭載のニンテンドー3DSが発売になり、これによってステレオ3Dカメラはいくらか普及するのかもしれません。

ステレオということでレンズが2つ付いており、それぞれで撮影された複数の画像が .mpo という拡張子のファイルとなってまとめて保存されます。これはマルチピクチャフォーマットという形式で、仕様をみるとそこまで複雑なものではありません。JPEGを単純に複数並べたような形式になっていて、実のところ拡張子を .jpg に変えてやると1枚目の画像なら見ることができます。

http://www.cipa.jp/hyoujunka/kikaku/pdf/DC-007_J.pdf

手っ取り早くどんな画像なのか見たければ、中のJPEGを取り出してやればいいわけです。これをC#で書いてみました。単にJPEGっぽいバイト列を見つけてくるだけです。


なお、まともにヘッダを解析してやったのはこちらの記事にまとめています。
http://d.hatena.ne.jp/Schima/20110306/1299402379

1. ファイルからバイト列を読み込む

byte[] bytes = System.IO.File.ReadAllBytes(fileName);

2. バイト列の中からJPEGっぽいところを探索

ヘッダを見て判定します。JPEGはフォーマットが緩いのでBMP,GIF,PNGなどのようにヘッダを見ただけで確実に判定できるわけではないようですが、だいたいはうまくいきます。

JPEGは最初にSOI (Start of Image)というマーカがあり、2バイトで値は0xFF, 0xD8です。このへんの仕様を利用して探します。

GetJpegOffsetsメソッドで、バイト列におけるJPEGの開始位置と思われるインデックスを探してきます。

int[] GetJpegOffsets(byte[] buffer)
{
    byte[][] jpegHeaders = new byte[][]{
        new byte[] { 0xff, 0xd8, 0xff, 0xe1 },
        new byte[] { 0xff, 0xd8, 0xff, 0xe0 },
    };
    int offset = 0;
    List<int> result = new List<int>();

    while (true)
    {
        int currentOffset = -1;
        foreach (byte[] header in jpegHeaders)
        {
            currentOffset = SearchArray(buffer, header, offset);
            if (currentOffset != -1)
                break;
        }

        if (currentOffset != -1)
        {
            offset = currentOffset;
            result.Add(offset);
            offset += jpegHeaders[0].Length;
        }
        else
            break;
    }

    return result.ToArray();
}

int SearchArray(byte[] bytes, byte[] pattern, int startIndex)
{
    int length = bytes.Length - pattern.Length + 1;
    for (int i = startIndex; i < length; i++)
    {
        bool matched = true;
        for (int j = 0; j < pattern.Length; j++)
        {
            if (bytes[i + j] != pattern[j])
            {
                matched = false;
                break;
            }
        }
        if (matched)
            return i;
    }
    return -1;
}

3. Bitmapに変換

こうして求められたバイト列中のJPEG開始位置により、その次の開始位置までの領域をそのままJPEGとみなすことができます。あとはバイト列をこれで区切ってSystem.Drawing.Bitmapに放り込むだけです。

Bitmap[] ConvertToBitmaps(byte[] buffer)
{
    int[] offsetList = GetJpegOffsets(buffer);
    List<Bitmap> result = new List<Bitmap>();

    for (int i = 0; i < offsetList.Length; i++)
    {
        int nextOffset = (i < offsetList.Length - 1) ? offsetList[i + 1] : buffer.Length;
        int count = nextOffset - offsetList[i];

        MemoryStream ms = new MemoryStream(buffer, offsetList[i], count);
        result.Add(new Bitmap(ms));                    
    }

    return result.ToArray();
}

.mpoに格納された複数のJPEGが配列となって返されます。3DS含め多くの3Dカメラでは、2つのBitmapが返されると思います。

まとめ

上記のコードをクラスにまとめたものです。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO; 

namespace Mpo2Jpg
{
    class MpoDecoder
    {
        private byte[] _buffer;

        public MpoDecoder(string fileName)
        {
            _buffer = ReadFile(fileName);
        }

        private byte[] ReadFile(string fileName)
        {
            using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                byte[] buffer = new byte[fs.Length];
                fs.Read(buffer, 0, buffer.Length);
                return buffer;
            }            
        }

        public Bitmap[] ConvertToBitmaps()
        {
            int[] offsetList = GetJpegOffsets(_buffer);
            List<Bitmap> result = new List<Bitmap>();

            for (int i = 0; i < offsetList.Length; i++)
            {
                int nextOffset = (i < offsetList.Length - 1) ? offsetList[i + 1] : _buffer.Length;
                int count = nextOffset - offsetList[i];

                MemoryStream ms = new MemoryStream(_buffer, offsetList[i], count);
                result.Add(new Bitmap(ms));                    
            }

            return result.ToArray();
        }

        private int[] GetJpegOffsets(byte[] bytes)
        {
            byte[][] jpegHeaders = new byte[][]{
                new byte[] { 0xff, 0xd8, 0xff, 0xe1 },
                new byte[] { 0xff, 0xd8, 0xff, 0xe0 },
            };
            int offset = 0;
            List<int> result = new List<int>();

            while (true)
            {
                int currentOffset = -1;
                foreach (var header in jpegHeaders)
                {
                    currentOffset = SearchArray(bytes, header, offset);
                    if (currentOffset != -1)
                        break;
                }

                if (currentOffset != -1)
                {
                    offset = currentOffset;
                    result.Add(offset);
                    offset += jpegHeaders[0].Length;;
                }
                else
                    break;
            }

            return result.ToArray();
        }

        private int SearchArray(byte[] bytes, byte[] pattern, int startIndex)
        {
            int length = bytes.Length - pattern.Length + 1;
            for (int i = startIndex; i < length; i++)
            {
                bool matched = true;
                for (int j = 0; j < pattern.Length; j++)
                {
                    if (bytes[i + j] != pattern[j])
                    {
                        matched = false;
                        break;
                    }
                }
                if (matched)
                    return i;
            }
            return -1;
        }
    }
}

以下、このクラスの使用例です。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

namespace Mpo2Jpg
{
    class Program
    {
        static void Main(string[] args)
        {
            MpoDecoder mpo = new MpoDecoder(@"HNI_0001.MPO");
            Bitmap[] bitmaps = mpo.ConvertToBitmaps();

            string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            for (int i = 0; i < bitmaps.Length; i++)
            {
                bitmaps[i].Save(Path.Combine(desktop, string.Format("{0}.jpg", i)), ImageFormat.Jpeg);
                bitmaps[i].Dispose();
            }
        }
    }
}