python3でexifを扱うパッケージのまとめ

はじめに

生のexifはkeyが整数で、valueがバイト列(つまり文字コード、int、float、とおよびその配列)になっているようです。

それぞれのライブラリでkeyを文字列に、valueを文字列やint, float, fraction, tuple など使いやすいと思われる型に変換していると考えられます。

valueの変換の仕方だけではなく、keyの文字列(タグ)もライブラリごとに微妙に異なります。

それぞれ、必要なimportと、タグの一覧を求める方法、一覧の一部を書いておきます。

ここのまとめはexifの読み出しが中心です。この他にデータの編集と、jpegファイルへの書き出しが必要になりますが、調査が足りませんので扱いません。

PILの_getexif

PILの_getexifを使ったプログラムの一部
#import
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS

#プログラムの主要部分
pilimage = Image.open(fname)
print(type(pilimage))
rawdic = pilimage._getexif()
print(type(rawdic))    #class:dict
for k, v in rawdic.items():
    typv = type(v)
    strv = str(v)
    if len(strv) > 32: strv = strv[:32] + "..."
    print(k,":",TAGS.get(k,k),":",strv,":",typv)

出力の一部

274 : Orientation : 1 : <class 'int'>
36867 : DateTimeOriginal : 2020:02:24 10:33:22 : <class 'str'>
37386 : FocalLength : (400, 10) : <class 'tuple'>
33434 : ExposureTime : (10, 500) : <class 'tuple'>
33437 : FNumber : (110, 10) : <class 'tuple'>
34853 : GPSInfo : {0: b'\x02\x02\x00\x00'... : <class 'dict'>
37500 : MakerNote : b'Nikon\x00\x02\x10\x00\x00MM\x0... :  <class 'bytes'>

274などの整数コードをTAGSを使って文字列にしたものが、Orientationなどです。

特定の値を取り出したいときは、rawdic[274]などとする必要があります。'Orientation'を使って取り出すには、あらかじめ文字列をキーにした辞書を作っておくなどの工夫が必要です。

GPSについては、GPSInfoの値が辞書になっていて、さらにこの辞書をGPSTAGSを使って展開すると

PILの_getexifを使ったプログラムの一部(GPS部分)
gpsdic = rawdic[34853]
print(type(gpsdic))    #class:dict
for k, v in gpsdic.items():
    typv = type(v)
    strv = str(v)
    if len(strv) > 32: strv = strv[:32] + "..."
    print(k,":",GPSTAGS.get(k,k),":",strv,":",typv)

出力の一部

.....
3:GPSLongitudeRef : E : <class 'str'>
4:GPSLongitude : ((141, 1), (1, 1), (43216552, 1000000)) : <class 'tuple'>
.....

の様に求めることができます。2階層目の展開が用意されているということです。

MakerNoteの値は長いバイト列のデータになっています。これをGPSデータの様に展開する機能は用意されていないということでしょう。メーカーの独自仕様で機種によっても異なる場合があり、対応するのは難しいのでしょう。

exifread

readexifを使ったプログラムの一部
#import
import exifread

#プログラムの主要部分
with open(fname, 'rb') as f:
    exifdic = exifread.process_file(f)
print(type(exifdic))    #class:dict
for k, v in exifdic.items():
    tv = type(v)
    if len(str(v)) > 32:
        v = str(v)[:32] + "..."
    print(k,":",v,":",tv)

出力の一部

Image Orientation : Horizontal (normal) : <class 'exifread.classes.IfdTag'>
EXIF DateTimeOriginal : 2020:02:24 10:33:22 : <class 'exifread.classes.IfdTag'>
EXIF FocalLength : 40 : <class 'exifread.classes.IfdTag'>
EXIF ExposureTime : 1/50 : <class 'exifread.classes.IfdTag'>
EXIF FNumber : 11 : <class 'exifread.classes.IfdTag'>
Image GPSInfo : 8462 : <class 'exifread.classes.IfdTag'>
EXIF MakerNote : [78, 105, 107, 111, 110, 0, 2, 1... : <class 'exifread.classes.IfdTag'>
....略
GPS GPSLongitudeRef : E : <class 'exifread.classes.IfdTag'>
GPS GPSLongitude : [141, 1, 5402069/125000] : <class 'exifread.classes.IfdTag'>
....略
MakerNote LensMinMaxFocalMaxAperture : [40, 40, 14/5, 14/5] : <class 'exifread.classes.IfdTag'>

GPSについても Image GPSInfo : 8462 : <class 'exifread.classes.IfdTag'>と、この中身を展開したGPSで始まるタグとして混ざって出てきます。もMakerNote同様に展開されて上記出力に混ざっています。

MakerNoteについても、EXIF MakerNoteのバイト列と、これをさらに解析して追加したものが同列に並んでいます。MakerNoteで始まるタグはメーカーにもよりますが多くなります。ここではMakerNote LensMinMaxFocalMaxApertureだけに注目しています。

exifreadで個々のデータを得る

上記のexifdicは文字列をkeyとしたexifread独自の辞書なので、それに合わせたkey(例えば'EXIF ExposureTime')で値を得ることができます。

readexifを使ったプログラムの一部(個別データ取得)
print(exifdic.get("Image Orientation","Orie. not exist"))
print(exifdic.get("EXIF ExposureTime","Expo. not exist"))
print(exifdic.get("GPS GPSLongitude","GPS. not exist"))
print(exifdic.get("MakerNote LensMinMaxFocalMaxAperture","Lens. not exist"))

printなので出力は文字列化されていますが、得られる値のtypeは、intのlist、Ratioのlist、または文字列です。

listといっても多くは要素が一つしかありません。

実行例です。

Horizontal (normal)
1/50
GPS. not exist
[40, 40, 14/5, 14/5]

piexifの一層目の辞書

piexifは辞書の辞書のような構造になります。

まず、外側の辞書を見ます。文字列のパス付きファイル名から直接に辞書を得ることができます。

piexifを使ったプログラムの一部 ({str:dict,..}を確認)
#import
import piexif

#プログラムの主要部分
exifdic = piexif.load(fname)
print(type(exifdic))    #class:dict
for k, v in exifdic.items():
    tk = type(k)
    tv = type(v)
    sv = str(v)
    if len(sv) > 32:
        sv = sv[:32] + "..."
    print(k,tk,":",sv,tv)

出力の全部。

<class 'dict'>
0th <class 'str'> {256: 4160, 257: 2336, 258: (8, 8, 8), 270:... <class 'dict'>
Exif <class 'str'> {33434: (711000, 1000000000), 33437: (200, ... <class 'dict'>
GPS <class 'str'> {0: (2, 2, 0, 0), 1: b'N', 2: ((43, 1), (8,... <class 'dict'>
Interop <class 'str'> {1: b'R98'} <class 'dict'>
1st <class 'str'> {256: 512, 257: 288, 259: 6, 274: 0, 282: (... <class 'dict'>
thumbnail <class 'str'> b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\... <class 'bytes'>

keyが0th, Exif, GPS, 1stなどで、対応するvalueが更に辞書です。

最後の、thumnailのvalueはバイト列で、辞書にはなっていません。

exifの全データをいくつかのグループに分けて、それぞれのグループ辞書を大きな辞書にまとめたという構造になっています。PILのexifではGPSだけがサブグループのような扱いでしたが、piexifでは他のグループと対等な位置にまとめられています。

残念ながらMakerNoteは加えられていません。

piexifの2層目の辞書

下位の辞書を展開してみます。PILのexifに似た構造になっています。

piexifを使ったプログラムの一部 下位のdictも展開
#import
import piexif

#プログラムの主要部分
exifdic = piexif.load(fname)
print(type(exifdic))
for k, v in exifdic.items():
    print(k,":",type(v))
    if type(v) != dict: continue
    for kk, vv in v.items():
        tag = piexif.TAGS[k][kk]["name"]
        tv = type(vv)
        if len(str(vv)) > 32:
            vv = str(vv)[:32] + "..."
        print(kk,tag,":",vv,":",tv)

下位の辞書はkeyがintでvalueがbytesかintかtuple。bytesは文字列の場合もあるしそうでないときもあります。tupleの要素は多くはintですが、さらにtupleの時とtupleみたいに見えてstrというのもありました。keyのintに対応するtagの名前はPILの時のようにTAGS辞書から取り出します。

出力の一部です。「0th : <class 'dict'>」などが上位辞書の切り替えポイントです。

0th : <class 'dict'>
271 Make : b'NIKON CORPORATION' : <class 'bytes'>
272 Model : b'NIKON D500' : <class 'bytes'>
274 Orientation : 1 : <class 'int'>
....略
Exif : <class 'dict'>
33434 ExposureTime : (10, 500) : <class 'tuple'>
33437 FNumber : (110, 10) : <class 'tuple'>
34850 ExposureProgram : 1 : <class 'int'>
34855 ISOSpeedRatings : 800 : <class 'int'>
34864 SensitivityType : 2 : <class 'int'>
36864 ExifVersion : b'0230' : <class 'bytes'>
36867 DateTimeOriginal : b'2020:02:24 10:33:22' : <class 'bytes'>
....略
GPS : <class 'dict'>
0 GPSVersionID : (2, 2, 0, 0) : <class 'tuple'>
1 GPSLatitudeRef : b'N' : <class 'bytes'>
2 GPSLatitude : ((43, 1), (8, 1), (16307373, 100... : <class 'tuple'>
3 GPSLongitudeRef : b'E' : <class 'bytes'>
4 GPSLongitude : ((141, 1), (1, 1), (43216552, 10... : <class 'tuple'>
....略
Interop : <class 'dict'>
1 InteroperabilityIndex : b'R98' : <class 'bytes'>
1st : <class 'dict'>
259 Compression : 6 : <class 'int'>
282 XResolution : (300, 1) : <class 'tuple'>
283 YResolution : (300, 1) : <class 'tuple'>
....略
thumbnail : <class 'bytes'>

piexif.TAGS

TAGSのpiexif.TAGS[k][kk]["name"]は奇異に感じられますが、piexif.TAGS[k][kk]という辞書の'name'というkeyに対するvalueを求めているものです。

piexif.TAGS[k][kk]は要素が2つの辞書です。

{'name':タグの名前の文字列, 'type':データの型をintで表現したもの}

例えば、piexif.TAGS['0th'][274]であれば、

{'name':'Orientation', 'type':3}

ですが、ここは深入りする必要はないでしょう。

piexifで個々のデータを得る

exifdic = piexif.load(fname) などで得られるpiexifの辞書の1層目のキーは'0th','Exif','GPS'などの文字列ですが、2層目のキーはintでもともとのexifのキーの値です。ですから、Orientationを求めるには、

print(exifdic['0th'][274])

とする必要がありますが、piexifでは定数が用意されていて、

print(exifdic['0th'][piexif.ImageIFD.Orientation])

という指定が可能です。

piexifを使ったプログラムの一部(個別データ取得)
exifdic = piexif.load(fname)
print('Orie:',exifdic['0th'][piexif.ImageIFD.Orientation])
print('Date:',exifdic['Exif'][piexif.ExifIFD.DateTimeOriginal])
print('Foca:',exifdic['Exif'][piexif.ExifIFD.FocalLength])
print('GPSL:',exifdic['GPS'].get(piexif.GPSIFD.GPSLongitude,'nodata'))

実行例です。

Orie: 0
Date: b'2019:02:24 11:12:09'
Foca: (3790, 1000)
GPSL: ((141, 1), (1, 1), (43216552, 1000000))

GExiv2

GExiv2を使ったプログラムの一部(全データ取得)
#import
import gi
gi.require_version('GExiv2', '0.10')
from gi.repository import GExiv2

#プログラムの主要部分
gdata = GExiv2.Metadata(fname)
print(type(gdata))    #class 'gi.overrides.GExiv2.Metadata'
for k in gdata.get_tags():
    tk = type(k)
    v = gdata.get(k)
    tv = type(v)
    if len(str(v)) > 32:
        v = str(v)[:32] + "..."
    print(k,tk,":",v,tv)

出力の一部です。gexiv独自の文字列キーと、全部文字列の値になることがわかります。

<class 'gi.overrides.GExiv2.Metadata'>
Exif.GPSInfo.GPSVersionID <class 'str'> : 2 3 0 0 <class 'str'>
Exif.Image.Artist <class 'str'> :  <class 'str'>
Exif.Image.Copyright <class 'str'> :  <class 'str'>
Exif.Image.DateTime <class 'str'> : 2020:02:24 10:33:22 <class 'str'>
....略
Exif.MakerNote.ByteOrder <class 'str'> : II <class 'str'>
Exif.MakerNote.Offset <class 'str'> : 976 <class 'str'>
Exif.Nikon3.0x002b <class 'str'> : 48 49 48 48 0 4 2 17 0 0 0 0 0 0... <class 'str'>
Exif.Nikon3.0x002c <class 'str'> : 48 49 48 49 35 0 128 2 170 1 0 0... <class 'str'>
....略
Exif.Photo.DateTimeOriginal <class 'str'> : 2020:02:24 10:33:22 <class 'str'>
Exif.Photo.ExifVersion <class 'str'> : 48 50 51 48 <class 'str'>
Exif.Photo.ExposureTime <class 'str'> : 10/500 <class 'str'>
Exif.Photo.FNumber <class 'str'> : 110/10 <class 'str'>
....略
Exif.GPSInfo.GPSLatitude <class 'str'> : 43/1 8/1 16307373/1000000 <class 'str'>
Exif.GPSInfo.GPSLatitudeRef <class 'str'> : N <class 'str'>
Exif.GPSInfo.GPSLongitude <class 'str'> : 141/1 1/1 43216552/1000000 <class 'str'>
Exif.GPSInfo.GPSLongitudeRef <class 'str'> : E <class 'str'>

MakerNoteの値も独自の解析をしているようです。Nikonのデータは機種により大きく異なるようですが、exifreadで読めなかった機種のものも、読んで再構成しているように見えます。

gexiv2でで個々のデータを得る

pythonの辞書における.get(key,default)で得ることができます。keyの値は上で得られるgexiv独自のものを使用します。

GExiv2を使ったプログラムの一部(個別データ取得)
gdata = GExiv2.Metadata(fname)
print('Orie:',gdata.get('Exif.Image.Orientation','Orie. not exist'))
print('Date:',gdata.get('Exif.Photo.DateTimeOriginal','Date. not exist'))
print('Foca:',gdata.get('Exif.Photo.FocalLength','Foca. not exist'))
print('GPSL:',gdata.get('Exif.GPSInfo.GPSLongitude','GPS not exist'))
print('Lens:',gdata.get('Exif.Nikon3.Lens','Lens. not exist'))

実行結果は

Orie: 1
Date: 2020:02:24 10:33:22
Foca: 400/10
GPSL: GPS not exist
Lens: 400/10 400/10 28/10 28/10