仙石浩明の日記

2017年8月11日

ZenFone3 で撮影した写真ファイル内の、位置情報が壊れている Exif データを修復するコードを書いてみた

昨年発表された ASUS のハイエンド スマホ ZenFone 3 Deluxe ZS570KL の購入時の OS は Android 6.0 Marshmallow だが、 この OS (UL-Z016-WW-4.12.40.1698) の標準のカメラ アプリには、 撮影した写真の位置情報が他のアプリで参照できないというバグがあった。

つまり、 カメラの設定で 「場所サービス」 を 「オン」 にすると、 撮影した場所 (GPS などの位置情報) を写真ファイルに付加する (Exif データ) が、 この位置情報に問題があって、 他のアプリで写真を見ても位置情報が表示されない。 例えば撮影した写真を Google フォトへアップロードしても、 撮影場所が表示されない

browsed with Gallery Application

この写真を Google フォトからダウンロードしてみれば、 Exif データ内に位置情報が保持されていることが分かるし、 アプリによってはこの位置情報を参照できるものもある。

例えば OS 標準の 「ギャラリー」 アプリで見ると、 位置情報が正しく保持されているように見える。 例えば、 御成町の交差点で撮影したこの写真を 「ギャラリー」 で見ると (右図 ⇒ ZenFone のスクリーンショット)、 場所が 「鎌倉市, 御成町」 と正しく表示される。

位置情報を正しく表示できるアプリがある以上、 バグではなく仕様 (Exif のバージョンの違い?) の可能性もある。 このとき私は、 この写真の位置情報を参照できないアプリは、 アプリ側にも問題があるのだろうと思ってしまった。 おそらく開発元でも同様の理由で、 このバグが見逃されてしまったのだろう。

そしてもし仕様なら、 Google フォトで撮影場所が表示されない問題も、 そのうち (Google 側で) 解決されるのだろうと思って放置してしまった。 ところが先日バンクーバーに行ったことで、 これは仕様などではなく、 紛れもないバグだったことを確信した。

browsed with Gallery Application

つまり、 日本や ZenFone 製造元である台湾など、 「東経」 な地域では正しい場所が表示されるが、 バンクーバーなど 「西経」 な地域だと全くかけ離れた場所が表示されてしまうということ。

例えば、 バンクーバーで撮った写真を 「ギャラリー」 で見ると (右図 ⇒ ZenFone のスクリーンショット)、 場所が 「内モンゴル, フルンボイル, オロチョン自治旗, 中華人民共和国」 などと広大な草原の真っ只中な場所が表示されてしまう (データが無いためか地図が真っ白, そもそも 「自治旗」 なんて行政単位は初めて見た)。 「49.288132,123.130722」 という位置座標を、 「49.288132,-123.130722」 に直してみる (経度をマイナスにしてみる) と、 正しい座標になることから、 「西経」 で撮影しても 「東経」 で記録してしまうバグであることが分かった。

もちろん、 このバグは現行の Android 7.0 Nougat では修正されているが、 Android 7.0 へ更新する前に撮影した写真ファイルの位置情報は (当然のことながら) 壊れたままなので不便このうえない。

そこで Android 6.0 の時に撮影した写真ファイルの位置情報を、 修正してみることにした。 まずは Exif データに保持された位置情報の何が問題なのか調べてみる。

Exif データの構造」 という検索語でググってみると、 いくつか解説ページが見つかる。 ありがたいことに 「Exif データにアクセスするコード」 まで見つかった (libexif project のような本格的なライブラリだと読むのが大変)。 長い解説文よりも動く簡潔なコードの方が話が早い。

コードを読みつつ、 まずは動かしてみて、 ZenFone 3 で撮影した写真ファイルの何がマズイのか調べてみる。

senri:~ $ gcc -o exif sample_main.c exif.c
senri:~ $ ./exif P_20170701_100640_vHDR_Auto.jpg
[P_20170701_100640_vHDR_Auto.jpg] createIfdTableArray: result=5

{0TH IFD}
 - DateTime: [2017:07:01 10:06:40]
 - GPSInfoIFDPointer: 22232 
 - Model: [ASUS_Z016D]

	…… 中略 ……

{GPS IFD}
 - GPSLongitude: 123/1 7/1 505923/10000 
 - GPSLatitudeRef: [N]
 - GPSProcessingMethod: [ASCII]
 - GPSAltitudeRef: 200/100 
 - GPSLatitude: 49/1 17/1 172751/10000 
 - GPSLongitudeRef: [se]

	…… 後略 ……

Longitude は経度、 Latitude は緯度。 GPSLongitude や GPSLatitudeRef のことをタグと呼び、 タグの値を保持するのが Exif データの役割。

Exif では緯度・経度の値を、 3つの分数で表現する。 例えば 「GPSLongitude: 123/1 7/1 505923/10000」 というのは、 タグ GPSLongitude の値が 「123/1 度 7/1 分 505923/10000 秒」、 すなわち撮影場所が 「経度 123 度 7 分 50.5923 秒」 であることを意味する。

そして GPSLatitudeRef: [N] は、 タグ GPSLatitudeRef の値が 「N」 つまり北緯 (North) であることを表わす。 では、 GPSLongitudeRef: [se] はどういう意味なんだろうか? 普通に考えればここは [se] ではなく 西経 (West) の [W] ではなかろうか?

[se] に何か特別な意味でもあるのかと思ってググってみたが、 何も見つからず。 単なるバグ? どうやったらこんなバグを作り込めるのだろう?

ちなみに GPSLongitudeRef: [se] は Exif データ内では次のような 12バイトのデータ (「タグフィールド」 と呼ぶ) になる:

00 03    00 02    00 00 00 03    73 65 00 00

最初の 2バイト 「00 03」 が、タグ GPSLongitudeRef を表わし (タグそれぞれに ID が割当てられている。GPSLongitudeRef は 3, GPSLongitude は 4 といった具合)、

次の 2バイト 「00 02」 が、タグの値のタイプが ASCII 文字列であることを表わす。

次の 4バイト 「00 00 00 03」 が、タグの値の長さ、 ここでは ASCII 文字列の長さが 3文字 (終端文字 00 を含む) であることを表わし、

最後の 4バイト 「73 65 00 00」 が、 タグの値である ASCII 文字列 「se」と終端文字 00。 最後の 00 は単なる穴埋め。

タグの値が 4バイトに収まらないときは、 実際のデータ格納場所を指し示すポインタ (ファイル内のオフセット) が、 この最後の 4バイトに格納される。

御成町の交差点で撮影した写真の位置情報も同様に調べてみた。

{GPS IFD}
 - GPSLongitude: 139/1 32/1 576664/10000 
 - GPSLatitudeRef: [N]
 - GPSProcessingMethod: [ASCII]
 - GPSAltitudeRef: 200/100 
 - GPSLatitude: 35/1 19/1 27825/10000 
 - GPSLongitudeRef: [se]

やっぱりタグ GPSLongitudeRef の値が [se] になっている。 どうりで 「西経」 でも 「東経」 と同じになってしまうわけだ。

ちなみにタグ GPSAltitudeRef は海抜 (altitude) が海水面より上 (0) か下 (1) かを表わす。 0 か 1 以外の値はダメ。 この Exif データでは RATIONAL タイプ (32ビット符号無し整数 2個で分数を表わす) になっている (正しくは BYTE タイプ)。 タグ GPSAltitude と混同しているのか? 高度 2m というのも意味不明だけど。 まあ、 タグ GPSAltitudeRef を参照するアプリが手元にないので、 とりあえず放置。

というわけで原因が分かった。 位置情報の [se] を [W] (西経の場合。東経ならば [E]) へ書き換えればうまくいきそう。 つまり、前述の 12バイトのタグフィールドの場合なら、 次のように書き換えればよい。

00 03    00 02    00 00 00 03    73 65 00 00
00 03    00 02    00 00 00 02    57 00 00 00

つまり、 データの長さ (終端文字を含む ASCII 文字列の長さ) を 3 から 2 へ書き換え、 ASCII 文字列を 「se」 から 「W」 (西経の場合) へ書き換える。 要は、 書き換えるべきタグフィールドが写真ファイル内のどこにあるかさえ分かれば、 それを読み込んで書き換えてファイルへ書き戻すだけ。 というわけで、 サクっと書いてみた。 わずか 150行足らず。

書き換えるべきタグフィールドがどこにあるか調べるのは、 「Exif データにアクセスするコード」 を流用させて頂いたので、 あっと言う間に書けた (このブログを書く方が遥かに時間がかかっている)。

ありがとうございます > DSAS 開発者の部屋のみなさま, とくに tanabe さま

東経か西経かという情報は写真ファイルには無いので、 西経の場合は -W オプションを指定する (デフォルトは東経)。 例えばバンクーバーで撮った写真を一つのディレクトリにまとめておいて、

senri:~/Vancouver $ fix_GPSInfo -W *

といった感じで一括修復できる。 修復の必要がないファイル (他のカメラで撮影した写真ファイルや、 位置情報を含んでいない写真ファイル、 および動画など写真以外のファイル等) は影響を受けない。 手元の写真ファイルはこれで全て修復できるのだけど、 Google フォトにアップロード済みの写真はどうしよう? いちど全部消してアップロードし直せばいいのだろうけど、 あまりに枚数が多すぎるなぁ...

Filed under: Android,プログラミングと開発環境 — hiroaki_sengoku @ 22:09

1件のコメント »

  1. ご無沙汰しております。先ほどトラックバックに気がつき驚いていたところです。
    私の拙いコードが多少なりともお手元のお役に立てたようで大変光栄に存じます。
    近年はめっきり旅から遠ざかっていますが、再びお目にかかれる機会を楽しみにしております。

    コメント by tanabe — 2017年9月1日 @ 11:52

この投稿へのコメントの RSS フィード。 TrackBack URL

コメントする