アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

WPF で GeoTag 読み込み 2

October 27, 2009開発.NET, GeoTag, WPF

WPF で GeoTag を利用するシリーズその 2。

前回は BitmapMetadata.GetQuery を利用して EXIF の GPS 情報を GeoTag の位置情報へ変換していたが、南北・西北の概念を考慮していなかった。また GeoTag から GPS 情報へ戻す手段がなかったので、今回はこれらを検討してみる。

サンプル コード

前回の記事のクラスをベースに、以下のような実装を行った。

/// <summary>
/// Google Map 内のマーカーを表すクラスです。
/// </summary>
class Marker
{
    /// <summary>
    /// 画像ファイルから Marker インスタンスを生成します。
    /// </summary>
    /// <param name="fileName">画像ファイルのパス。</param>
    /// <returns>成功時は Marker インスタンス。失敗時は null 参照。</returns>
    public static Marker FromImage( string fileName )
    {
        using( var stream = new FileStream( fileName, FileMode.Open, FileAccess.Read, FileShare.Read ) )
        {
            var decoder   = new JpegBitmapDecoder( stream, BitmapCreateOptions.None, BitmapCacheOption.None );
            var metadata  = ( BitmapMetadata )decoder.Frames[ 0 ].Metadata;

            // 緯度
            double latitude = 0.0;
            {
                var gpsLatitude = metadata.GetQuery( "/app1/ifd/gps/subifd:{ulong=2}" ) as ulong[];
                if( gpsLatitude == null ) { return null; }

                var direction = metadata.GetQuery( "/app1/ifd/gps/subifd:{short=1}" ) as string;
                if( direction == null ) { return null; }

                // 南半球なら負の値
                latitude = Marker.GetGeoLocationFromGpsValue( gpsLatitude );
                if( direction == "S" )
                {
                    latitude *= -1;
                }
            }

            // 経度
            double longitude = 0.0;
            {
                var gpsLongitude = metadata.GetQuery( "/app1/ifd/gps/subifd:{ulong=4}" ) as ulong[];
                if( gpsLongitude == null ) { return null; }

                var direction = metadata.GetQuery( "/app1/ifd/gps/subifd:{short=3}" ) as string;
                if( direction == null ) { return null; }

                // 西半球なら負の値
                longitude = Marker.GetGeoLocationFromGpsValue( gpsLongitude );
                if( direction == "W" )
                {
                    longitude *= -1;
                }
            }

            return new Marker() { Latitude = longitude, Longitude = latitude };
        }
    }

    /// <summary>
    /// EXIF から得られた GPS 情報を表す { 度, 分, 秒 } の配列から GeoTag 用の位置情報を取得します。
    /// </summary>
    /// <param name="values">GPS 情報の配列。</param>
    /// <returns>GeoTag 用の位置情報。</returns>
    private static double GetGeoLocationFromGpsValue( ulong[] values )
    {
        double degrees = Marker.NormalizeGpsValue( values[ 0 ] );
        double minutes = Marker.NormalizeGpsValue( values[ 1 ] );
        double seconds = Marker.NormalizeGpsValue( values[ 2 ] );

        // GeoTag の位置へ変換
        return degrees + ( minutes / 60.0 ) + ( seconds / 3600 );
    }

    /// <summary>
    /// GeoTag の位置情報から EXIF 用の GPS 情報を取得します。
    /// </summary>
    /// <param name="location">GeoTag の位置情報。</param>
    /// <returns>EXIF 用の GPS 情報。</returns>
    private static ulong[] GetGpsValuesFromGeoLocation( double location )
    {
        location = Math.Abs( location );

        // 度
        double degrees = Math.Floor( location );
        location -= degrees;

        // 分
        double minutes = Math.Floor( location * 60.0 );
        location -= ( minutes / 60.0 );

        // 秒
        double seconds = Math.Round( location * 3600.0 * 1000000.0 );

        // GPS 情報へ変換する
        ulong[] values = new ulong[ 3 ];
        values[ 0 ] = Convert.ToUInt64( degrees + 0x100000000 );
        values[ 1 ] = Convert.ToUInt64( minutes + 0x100000000 );
        values[ 2 ] = Marker.GetGpsSecondsFromGeoSeconds( seconds );

        return values;
    }

    /// <summary>
    /// GeoTag の秒データから GPS 用の秒データを取得します。
    /// </summary>
    /// <param name="seconds">GeoTag の秒。</param>
    /// <returns>作成した GPS 用の秒データ。</returns>
    private static ulong GetGpsSecondsFromGeoSeconds( double seconds )
    {
        byte[] num = BitConverter.GetBytes( ( int )seconds );
        byte[] den = BitConverter.GetBytes( ( int )1000000 );
        byte[] sec = new byte[ 8 ] { num[ 0 ], num[ 1 ], num[ 2 ], num[ 3 ], den[ 0 ], den[ 1 ], den[ 2 ], den[ 3 ] };

        return BitConverter.ToUInt64( sec, 0 );
    }

    /// <summary>
    /// GPS 情報の一つのデータ ( GPSLatitude または GPSLongitude ) を正規化します。
    /// EXIF のタグ情報については、以下のページを参照して下さい。
    /// <span>
    /// http://homepage1.nifty.com/gigo/DC/GPS/gpsifd.html
    /// </span>
    /// </summary>
    /// <param name="value">GPS 情報。</param>
    /// <returns>正規化されたデータ。</returns>
    private static double NormalizeGpsValue( ulong value )
    {
        byte[]    bytes = BitConverter.GetBytes( value );
        int        upper = BitConverter.ToInt32( bytes, 0 );
        int        lower = BitConverter.ToInt32( bytes, 4 );

        return ( ( double )upper / ( double )lower );
    }

    /// <summary>
    /// 緯度を取得または設定します。
    /// </summary>
    public double Latitude { get; set; }

    /// <summary>
    /// 経度を取得または設定します。
    /// </summary>
    public double Longitude { get; set; }
}

24 行と 41 行の metadata.GetQuery as string している処理は南北・東西を示す文字の取得である。それぞれ "N" か "S"、"E" か "W" が格納される。GeoTag では緯度の場合 "N" がプラスで "S" がマイナス、経度ならば "E" がプラスで "W" がマイナスとして表現される。これらの文字を判定して緯度・経度の正と負を設定している。

GetGpsValuesFromGeoLocation メソッドでは、指定された GeoTag の位置情報から EXIF の GPS 情報を復元している。基本的には GetGeoLocationFromGpsValue の逆算である。

GPS 情報の「度・分・秒」は「分子/分母」形式で格納されている。度と分は GeoTag から抽出した値に 0x100000000 を足す事で「値/1」という値を作成する事ができる。秒については Picasa などで付けた GPS 情報や Google Maps API の GeoTag だと分母が 100 万になるようなので、とりあえずこの値を指定している。

値の解析結果をもとに実装したので例外的な値には対応できていない。明確な仕様があればそれに準拠したいのだけど。「度・分・秒」についても「度・分」となるケースがある。これについても調べる必要があるだろう。カシミール 3D などは「度・分」の形式らしい。

GPS 情報と GeoTag の相互変換の入り口まではこれたはず。今日はここまでにしておく。