Ricoh THETAのEXIF (回転を読むためのPythonコード付き)

追記 (2014/2/12)

2014/1/30のアップデート(https://theta360.com/ja/info/news/2014-01-30-2/)で、Sphere XMPフォーマットでも回転情報が埋め込まれるようになったようです。

Photo Sphere XMPAdobeメタデータ形式であるXMPGoogleが拡張したもの(https://developers.google.com/photo-sphere/metadata/?hl=ja)で、ちゃんとドキュメントもあるので新しいデータについてはこちらを使うほうがよいでしょう。

はじめに

(主にVR界隈で?)最近人気のthetaですが、

公式のviewerでみるとちゃんと向きが自動で調整されるんですが、中に入ってるjpegはこんな感じで向きの補正はされてません。

もちろん、撮った写真を見るだけなら公式サイトにアップロードすれば(ちょっと重いのを除いて)特に問題はないんですが、Oculusでみるぞーとか思うと、当然向きの情報にアクセスしたいところです。

で、公式のWindows用viewerだとこのjpegだけを開いて向きを調整できるので、jpegのどこかに入ってるはずだ、ということになります。

EXIFを見る

試しに一つ開いてExifReaderでみてみるとこんなかんじです。(GPS情報がある場合)


ファイル名 : R0010004.JPG
Exif : Exif
▼メイン情報
タイトル :
メーカー名 : RICOH
機種 : RICOH THETA
画像方向 : 左上
幅の解像度 : 72/1
高さの解像度 : 72/1
解像度単位 : インチ
ソフトウェア : RICOH THETA Ver 1.02
変更日時 : 2013:11:09 17:40:42
YCbCrPositioning : 一致
著作権 :
Exif情報オフセット : 434
GPS情報オフセット : 904
▼サブ情報
露出時間 : 1/30秒
レンズF値 : F2.1
露出制御モード : プログラムAE
ISO感度 : 800
Unknown (8830)3,1 : 1
Exifバージョン : 0230
オリジナル撮影日時 : 2013:11:09 17:40:42
デジタル化日時 : 2013:11:09 17:40:42
コンポーネントの意味 : YCbCr
画像圧縮率 : 320/100 (bit/pixel)
レンズ絞り値 : F2.1
対象物の明るさ : EV-1.5
露光補正量 : EV0.0
開放F値 : F2.1
自動露出測光モード : 分割測光
光源 : 不明
フラッシュ : オフ
レンズの焦点距離 : 0.75(mm)
カメラの内部情報 : RIOCH Format [.............]
ユーザーコメント :
FlashPixのバージョン : 0100
色空間情報 : sRGB
画像幅 : 3584
画像高さ : 1792
ExifR98拡張情報 : 58224
撮影モード : オート
ホワイトバランスモード : オート
レンズの焦点距離(35mm) : 6(mm)
シーン撮影タイプ : 標準
シャープネス : 標準
GPS情報
GPSタグバージョン : 2,3,0,0
緯度(N/S) : N
緯度(数値) : 34゚ ****.** [DMS]
経度(E/W) : E
経度(数値) : 135゚ ****.** [DMS]
高度基準 : 海抜基準
高度(数値) : 2148/100 メートル
GPS時間(UTC) : 08:40:37
撮影した画像の方向基準 : 真方位
撮影した画像の方向 : 22.50°
測地系 : WGS84
タイムスタンプ : 2013:11:09
TOKYO測地系換算緯度 : 34/**/**.*** [DMS]
TOKYO測地系換算経度 : 135/**/**.*** [DMS]
▼ExifR98情報
互換性識別子 : R98
バージョン : 0100
▼サムネイル情報
圧縮の種類 : OLDJPEG
幅の解像度 : 72/1
高さの解像度 : 72/1
解像度単位 : インチ
JPEGInterchangeFormat : 58356
JPEGInterchangeFormatLength : 3225
で、"撮影した画像の方向"(GPSImgDirection)はあるんですが、加速度の情報はありません。これは"カメラの内部情報"、いわゆるmakernoteに謎のフォーマットで入っているに違いありません。*1

Makernote

写真を沢山とって変化する部分とか調べてたんですが、さすがに効率が悪いのでちょっと方針を変えてWindows版のアプリを調べて見ることにしました。よく見るとAdobe Airでできていたので、早速SphericalViewer.swf(1.5MB)を逆コンパイルしてみました*2

するとjp.co.ricoh.exif.RicohIFDEntryとjp.co.ricoh.receptor.entities.EquirectangularImageというのがすぐ見つかったので、この辺を調べてみます。すると

  • ZenithEs (TagId=0x0003)
  • Zenith (TagId=0x0006)
  • CompassEs (TagId=0x0004)
  • Compass (TagId=0x0007)

というタグがあるのが分かります。Jpegの標準にもIFD(Image File Directory)というのはあるらしく、それほど変な形式でも無いようですが違いはよくわからないのでこの辺のタグ付近の形式だけ書いておきます。

基本的にはbig endianで、


Entry
= TagId(uint16) TypeId(uint16) NumData(uint32) Offset(uint32) (Dataが大きい時)
| TagId(uint16) TypeId(uint16) NumData(uint32) Data{NumData} (そうでないとき)

TypeId
= 0x0005 (unsigned rational)
| 0x000a (signed rational)

Data
= a(uint32) b(uint32) (unsigned rational, a/b)
| a(int32) b(int32) (signed rational, a/b)

となっています。データ本体は小さい場合(データの合計が4B以下の時)はそのままインラインに入っていて、大きい時はファイルのオフセットが入っています。で、なぜかこのオフセットに+12したところから実際のデータは始まります。

で、私がみたファイルの中にはZenithEsとCompassEsが定義されていて、

  • ZenithEs: signed ratioanl, NumData=2
  • CompassEs: unsigned rational, NumData=1

となっていました。エラーチェックのコードから値の範囲が分かり、全てdegreeで

  • 0 <= ZenithEs[0] <= 360
  • -90 <= ZenithEs[1] <= 90
  • 0 <= CompassEs <= 360

となっているようです。

で、この辺の値がTilt3Dというクラスに入り、ZenithX(ZenithEs[0]),ZenithY(ZenithEs[1]),ZenithZ(0),Compass(CompassEs)と呼ばれます。座標系等はまだ調べていませんが、このうちZenithX,ZenithYだけを使って、このような回転行列を作っているようです。


m =
cos(zY) -sin(zY)*cos(zX) -sin(zY)*sin(zX)
sin(zY) cos(zY)*cos(zX) cos(zY)*sin(zX)
0 sin(zX) cos(zX)

取り出し方

本当はIFDをちゃんとパースするといいのでしょうが、面倒そうなので簡易的に取り出せそうな方法を書いておきます。さっきあげたタグはバイナリの中で比較的ユニークなシグネチャになるので、それを検索して値を取り出すと良さそうです。

#!/bin/python2

import os
import subprocess
import struct

def find_data(s, tag):
	ix = s.find(tag)
	if ix < 0:
		raise Exception('Cannot find tag')
	return ix + len(tag)

def parse_u_rational(s):
	a, b = struct.unpack('>II', s)
	return float(a) / float(b)

def parse_s_rational(s):
	a, b = struct.unpack('>ii', s)
	return float(a) / float(b)

def get_angles(path):
	f = open(path, 'rb')
	head = f.read(10 * 1000)  # take long enough header

	# Find CompassEs
	ix = find_data(head, '\x00\x04\x00\x05\x00\x00\x00\x01')  # search CompassEs,UnsignedRational,1
	offset = struct.unpack('>I', head[ix : ix + 4])[0] + 12
	compass = parse_u_rational(head[offset : offset + 8])

	# Find ZenithEs
	ix = find_data(head, '\x00\x03\x00\x0a\x00\x00\x00\x02')  # search ZenithEs,SignedRational,2
	offset = struct.unpack('>I', head[ix : ix + 4])[0] + 12
	zenith_x = parse_s_rational(head[offset : offset + 8])
	zenith_y = parse_s_rational(head[offset + 8 : offset + 16])
	
	return {
		'zenith_x': zenith_x,
		'zenith_y': zenith_y,
		'compass': compass
	}	

で、手元のファイルに対してはそれっぽい値がでてきています。本当にこれらの値を使うためには座標系をもうちょっと調べたりする必要があるでしょうが、そこは比較的楽だと思います。

雑感

他にもおもしろそうなタグがいくつか定義されているようなので、気になる人は調べてみると良いと思います。

  • HDRType
  • HDRData
  • AbnormalAcc

*1:ここで多少のプロプライエタリな形式に対応しているexiftoolも使ってみますが、thetaは新しすぎて対応してませんでした

*2:JPEXS Free Flash Decompiler http://www.free-decompiler.com/flash/ が便利でした