gexiv2を用いたexifデータの抽出
Rationalを使った加工方法
exifdataの変更と書き戻しについては、convertに任せる
Debian10(buster)上の python3 (Python 3.7.3)です。したがってパッケージ名はDebianのものです。私はsynapticでgexiv2をキーワードに検索して入れましたが、aptでも入れられます。
次のパッケージを入れます。
# apt install gir1.2-gexiv2-0.10
これにより入るファイルは
/usr/lib/python2.7/dist-packages/gi/overrides/GExiv2.py /usr/lib/python3/dist-packages/gi/overrides/GExiv2.py /usr/lib/x86_64-linux-gnu/girepository-1.0/GExiv2-0.10.typelib /usr/share/doc/gir1.2-gexiv2-0.10/changelog.Debian.gz /usr/share/doc/gir1.2-gexiv2-0.10/changelog.gz /usr/share/doc/gir1.2-gexiv2-0.10/copyright /usr/share/lintian/overrides/gir1.2-gexiv2-0.10
となっていて、python3にも対応していると確認が取れます。
import文の書き方が面倒です。
import gi gi.require_version('GExiv2', '0.10') from gi.repository import GExiv2
importしてしまえば使い方は簡単で、文字列のfilenameを引数にして
metadata = GExiv2.Metadata(filename)
これで、type(metadata)はgi.overrides.GExiv2.Metadataですが、実質はkey,valueとも文字列の辞書であるかのように扱うことができます。もっとも、辞書のようにmetadata.keys()は使えません。metadata.get_tags()とします。
for k in metadata.get_tags(): v = metadata.get(k) t = type(v) print (k,t,v)
一覧の出力が出ます
Exif.GPSInfo.GPSVersionID <class 'str'> 2 3 0 0 Exif.Image.Artist <class 'str'> Exif.Image.Copyright <class 'str'> Exif.Image.DateTime <class 'str'> 2020:02:09 15:40:25 Exif.Image.ExifTag <class 'str'> 348 Exif.Image.GPSTag <class 'str'> 18828 Exif.Image.Make <class 'str'> NIKON CORPORATION Exif.Image.Model <class 'str'> NIKON D500 Exif.Image.Orientation <class 'str'> 6 Exif.Image.ResolutionUnit <class 'str'> 2 ......
これでタグの文字列を調べて、
(A) metadata.get('Exif.Photo.DateTimeOriginal','not data')) (B) metadata.get('Exif.Photo.DateTimeOriginal')) (C) metadata['Exif.Photo.DateTimeOriginal']
などとします。得られるvalueはすべて文字列です。(A)の'not data'は辞書になかったときに返す値です。これがおすすめ。
(B)は辞書にないときは None が返ります。
(C)は辞書にないときは Unknown tag のエラーになります。
上記が基本の値の出し方ですが、不都合なものもあります。
取得メソッド | strでの値 | 一般的な表示 |
.get('Exif.Photo.ExposureTime') | 10/1250 | 1/125 |
.get('Exif.Photo.FNumber') | 56/10 | 5.6 |
.get('Exif.Photo.FocalLength') | 600/10 | 60 |
ExposureTimeは分子が1の分数か整数。少なくとも既約分数になっていてほしい。
FNumberは2, 2.8, 4, 5.6, 8,...など小数点以下1桁の小数で.0は省略する。
FocalLengthは整数で、.0はつけない。(スマートフォンなどでは小数があり得る)
この3つのタグにはstr以外の値が返る特別なメソッドがあります。
取得メソッド | type(v) | str(v) |
.get_exposure_time() | Fraction | 1/125 |
.get_fnumber() | float | 5.6 |
.get_focal_length() | float | 60.0 |
少し一般的な表示に近づきますが、.0を省くという部分が煩わしくなります。
そこで、この特別なメソッドは使わず、他と同じメソッドを使って文字列の値を得、 その文字列からFractionに変更する方法で不都合を回避します。
Fractionは分数を分数として扱う変数の型です。'56/10', '11', '0.3' など文字列で表現された分数や整数、小数を引数にして値を定義できます(floatやintつまり、56/10や0.3など文字列でない値も入れられますが二進で計算されるため意外な結果になります)。
from fractions import Fraction tmp = Fraction('10/1250') print(str(tmp)) # 1/125 tmp = Fraction('3') print(str(tmp)) # 3
tmp = Fraction('56/10') if tmp.denominator == 1: sfnum = 'f{} '.format(tmp.numerator) else: sfnum = 'f{:.1f} '.format(float(tmp.numerator/tmp.denominator)) # f5.6
tmp = Fraction('600/10') if tmp.denominator == 1: sflen = '({}mm) '.format(tmp.numerator) else: sflen = '({:.1f}mm) '.format(float(tmp.numerator/tmp.denominator)) # 60mm
レンズ交換式カメラの場合に、取り付けたレンズがわかると嬉しい。Olympusの場合は、MakerNoteではなく、Exif.Photo.LensModel で 'OLYMPUS M.14-42mm F3.5-5.6 EZ' のように得られます。Nikonでは、MakerNoteで知るしかありません。
Olympusの場合は、他社のレンズを使うことも想定してMakerNoteでないところに置いているようです。値にOLYMPUSが入っているのもそのためでしょう。MakerNoteにも値があります。
このタグは、辞書にキーが含まれないということが頻繁におこるでしょうから.get('Exif.GPSInfo.GPSLatitude','')などとしてデフォルト値を定めて対処するのが良いでしょう。
取得メソッド | strでの値 | 一般的な表示 |
.get('Exif.Photo.LensModel') | OLYMPUS M.14-42mm F3.5-5.6 EZ | 同左 |
.get('Exif.Photo.LensSpecification') | 14/1 42/1 35/10 56/10 | 14-42mm f3.5-5.6 |
.get('Exif.OlympusEq.LensModel') | OLYMPUS M.14-42mm F3.5-5.6 EZ | 同左 |
.get('Exif.Nikon3.Lens') | 160/10 850/10 35/10 56/10 | 16-85mm f3.5-5.6 |
'Exif.Nikon3.Lens'はgexiv2では機種が違っても(つまりMakerNoteのバージョンが違っても)同じ形で返されました。すべてのNikon、将来のNikonでも保証されるかは不明ですが、今の所は便利。exifreadは現在でも違うkeyになります。
"160/10 850/10 35/10 56/10".split()で文字列を4つに分けて、それぞれをFractionを使って整形していけます。
プログラムの一部を示します。
metadata = GExiv2.Metadata(filename) #...略... lensinfo = '' #retv strv = metadata.get('Exif.Photo.LensModel','') if strv != '': lensinfo += strv if lensinfo == '': strv = metadata.get('Exif.Nikon3.Lens','') if strv != '': rlist = strv.split() #space+ if len(rlist) == 4: flens = Fraction(rlist[0]) if flens.denominator == 1: lensinfo += '{}'.format(flens.numerator) else: lensinfo += '{:.1}'.format(float(flens.numerator/flens.denominator)) if rlist[0] != rlist[1]: flens = Fraction(rlist[1]) if flens.denominator == 1: lensinfo += '-{}mm '.format(flens.numerator) else: fltv = float(flens.numerator/flens.denominator) lensinfo += '-{:.1f}mm '.format(fltv) else: lensinfo += 'mm ' flens = Fraction(rlist[2]) #lensinfo += '({},{},{}) '.format(rlist[2],rlist[3],rlist[3].den) if flens.denominator == 1: lensinfo += 'f{}'.format(flens.numerator) else: fltv = float(flens.numerator/flens.denominator) lensinfo += 'f{:.1f}'.format(fltv) if rlist[2] != rlist[3]: flens = Fraction(rlist[3]) if flens.denominator == 1 or flens.denominator == 0 : lensinfo += '-{}'.format(flens.numerator) else: #print('flens',flens) fltv = float(flens.numerator/flens.denominator) lensinfo += '-{:.1f}'.format(fltv) else: for r in rlist: flens = Fraction(r) if flens.denominator == 1: lensinfo += '{} '.format(flens.numerator) else: fltv = float(flens.numerator/flens.denominator) lensinfo += '{:.1f} '.format(fltv)
たぶん、使うとすれば次の4つでしょうか。
取得メソッド | strでの値 | 一般的な表示 |
.get('Exif.GPSInfo.GPSLatitude') | 43/1 8/1 16307373/1000000 | 43°8′16.3″ |
.get('Exif.GPSInfo.GPSLatitudeRef') | N | 同左 |
.get('Exif.GPSInfo.GPSLongitude') | 141/1 1/1 43216552/1000000 | 141°1′43.2″ |
.get('Exif.GPSInfo.GPSLongitudeRef') | E | 同左 |
これも"43/1 8/1 16307373/1000000".split()で3つに分けて、それぞれをFractionを使って整形するのがよいでしょう。それぞれ度分秒なので、60で割って加えて43.14度などと丸めてしまうのもありです。
プログラムの一部を示します。
metadata = GExiv2.Metadata(filename) #...略... gpsinfo = '' #retv gpsinfo += metadata.get('Exif.GPSInfo.GPSLatitudeRef','') strv = metadata.get('Exif.GPSInfo.GPSLatitude','') if strv != '': gpsinfo += gps_str2dms(strv) gpsinfo += metadata.get('Exif.GPSInfo.GPSLongitudeRef','') strv = metadata.get('Exif.GPSInfo.GPSLongitude','') if strv != '': gpsinfo += gps_str2dms(strv) #...略... def gps_str2dms(strv): retv = '' rlist = strv.split() #space+ if len(rlist) == 3: d = Fraction(rlist[0]) if d.denominator == 1: retv += '{}°'.format(d.numerator) else: retv += '{:.1f}°'.format(float(d.numerator/d.denominator)) m = Fraction(rlist[1]) if m.denominator == 1: retv += '{}′'.format(m.numerator) else: retv += '{:.1f}′'.format(float(m.numerator/m.denominator)) s = Fraction(rlist[2]) if s.denominator == 1: retv += '{}″ '.format(s.numerator) else: retv += '{:.1f}″ '.format(float(s.numerator/s.denominator)) else: for dms in rlist: d = Fraction(dms) if d.denominator == 1: retv += '{} '.format(d.numerator) else: retv += '{:.1f} '.format(float(d.numerator/d.denominator)) return retv
たくさんのデジタル写真からサムネイル付きのhtmlページをexifデータの注釈とともに作っています。以前からrubyでこのプログラム書いていましたが今回pythonで作り直しました。
rubyでもexifはいくつかライブラリがあってLinuxディストリビューションがバージョンを上げるとexif部分が使えなくなるということがありました。python3ならもう少し整備されていると期待していましたが、やはり乱立と言える状態に感じます。
rubyでは写真のOrientationによる回転とサムネイルづくりの縮小はImageMagickのconvertコマンドを呼び出してやっていました。
pythonではPILがあって、pythonだけで処理が完結するように書けそうだと考えて調べていたのですが、やはりconvertに任せることにしました。
(1)回転済みの画像はOrientationの値を1に戻しておかなければなりませんが、exifreadでMakerNoteが読めなくなる不具合が見つかったりして、書き込み部分が怪しいのです。convertではOrientationの面倒も見てくれます。
(2)縮小の指定がPILでは画像が横長か縦長かによって異なる指示をしなければなりません。convertでは800x800と指示すれば縦長は縦が800、横長は横が800に縮小されます。これは便利です。
ここでも、プログラムの一部を示します。
python3からコマンドを呼び出す方法はいくつかあります。Linuxで作成していますが、Windowsでもこのまま動くと思います。ただし、linuxもWindowsもImageMagickがインストールされている前提です。
Windowsとの差を吸収するためにpathlibを使ってみていますが、Windowsでは試していませんので、だめならば素直にパス名+ファイル名を初めから文字列で書いてください。
pathlibとファイル名との連結は / を文字列連結の + の様に使います。これは便利。
import subprocess import pathlib #...略... sdir = pathlib.Path('imgorg') ddir = pathlib.Path('imgsm') fname = 'p001.jpg' strlist = [ 'convert', str(sdir / fname) ] strlist.extend( '-auto-orient -resize 960x960'.split() ) strlist.extend( '-quality 95'.split() ) strlist.append( str(ddir / fname) ) subprocess.run(strlist)
subprocess.run()の引数として、コマンドラインに書く一連の文字列を書いてshellに解釈させる方法もありますが、ここで採用したのは文字列のlistを書く方法です。それぞれの項目をプログラムで用意する場合にも親和性がありますし、行を分けるのも楽です。
スペースで分かち書きした文字列から.split()でlistを作って、.expand()でlist同士を結合しています。一つの文字列をlistに加えるのに.append()を使っています。