はじめに
今回は少しずつコードが充実していく仕様なので最終的なソースコードはありません。
恐縮ですがそのつもりでお読みください。
Exif情報を取得する
PILのExifTags
でJPEG画像のExif情報を取得することができる。そのためのメソッドは_getexif()
だ。この安駄婆もといアンダーバーは何なの。
from PIL import Image, ExifTags
from pprint import pprint
filename = "hoge.jpg"
img = Image.open(filename)
dict = img._getexif()
pprint(dict)
結果は辞書の形で得られる。
{前略
256: 3264,
257: 2448,
後略}
256
はImageWidthを、257
はImageLengthを意味する。
それぞれの項目はタグ番号で登録されているのだ。
ExifTags
にはタグ番号とタグ名称が紐づいた辞書がある。ExifTags.TAGS
だ。
tags = ExifTags.TAGS
pprint(tags)
{前略
256: 'ImageWidth',
257: 'ImageLength',
後略}
ExifTags.TAGS
を使って画像の中のExif情報をタグ名称をキーとして取得することができる。
dict = img._getexif()
exif = {} # 空の辞書を定義
for key, value in dict.items():
exif[ExifTags.TAGS[key]] = value # 辞書の値を新たな辞書のキーとしている
pprint(exif)
{前略
'ImageWidth': 3264,
'ImageLength': 2448,
後略}
存在しないキーの辞書を呼び出すエラーを回避する
辞書の値はdict[key]
で取得することができるが、辞書にないキーを指定するとエラーになってしまう。
それを避けるには**dict.get(key[, default])**を使う。key
が辞書にあればそれに対応する値を、そうでなければdefault
を返す。default
値は必須ではなく、指定されなかった場合はNone
を返す。
今回はタグ番号とタブ名称の辞書だから、存在しないタグ番号ならばタグ番号そのものを返すのがいいだろう。
# exif[ExifTags.TAGS[key]] = value
# ↓
exif[ExifTags.TAGS.get(key, key)] = value
内包表記
さらに、これを1ラインで記述することができる。
# exif = {} # 空の辞書を定義
# for key, value in dict.items():
# exif[ExifTags.TAGS.get(key, key)] = value
# ↓
exif = {ExifTags.TAGS.get(key, key): dict[key] for key in dict}
このワンライナー、ぱっと見では何をやっているのかわかりづらいが、もう少し簡単な例を見れば納得がいく。
こういうのを内包表記と呼ぶ。無理して使う必要はないが、目にする機会は結構多い。これから何度もググることになるだろうから名前だけは覚えておこう。
arr = [x for x in "ABC"]
print (arr)
# 結果
['A', 'B', 'C']
GPS情報を取得する
スマホのカメラで撮影するとGPSの情報が含まれていることがある。コンデジやデジイチの中にもGPS機能を持つものがある。
'GPSInfo': {0: b'\x02\x02\x00\x00',
1: 'N',
2: (35.0, 25.0, 21.098327),
3: 'E',
4: (136.0, 24.0, 38.26538),
5: b'\x01',
6: 0.0,
7: (4.0, 7.0, 13.0),
27: 'CELLID',
29: '2019:05:11'},
上に示した簡単なソース(完成形を示していなくて恐縮です)では入れ子構造になっているGPSInfo
の値である辞書のキーまでは取得できていない。これもExifTags.GPSTAGS
でタグ名称を取得することができる。この部分のソースは略。
辞書をpprintで見やすく表示する
以前のPythonはdict
の登録順が保証されていなかったが、最近のバージョンでは保証されるようになった。
ただしpprint
するとソートされて出力される。ソートさせないようにするにはsort_dicts=True
と指定する。
…ということは覚えておいたほうがよいだろう。
参考 python3.7でdictが格納順を保持するようになったが、pprint出力は自動でソートされる
{'GPSVersionID': b'\x02\x02\x00\x00',
'GPSLatitudeRef': 'N',
'GPSLatitude': (35.0, 25.0, 21.098327),
'GPSLongitudeRef': 'E',
'GPSLongitude': (136.0, 24.0, 38.26538),
'GPSAltitudeRef': b'\x01',
'GPSAltitude': 0.0,
'GPSTimeStamp': (4.0, 7.0, 13.0),
'GPSProcessingMethod': 'CELLID',
'GPSDateStamp': '2019:05:11'}
GPSLatitude
とGPSLongitude
が緯度と経度。これらは三つの数値のタプルになっている。度・分・秒だ。
今回の写真ではGPSAltitude
(高度)がゼロになってしまっているのが残念だ。
PILのバージョンに注意
Exifの規格ではGPSLatitude
やGPSLongitude
について「3つのRATIONALによって表現し」とあり、PILも少し前のバージョンまでは度・分・秒がそれぞれ分数(分子と分母のタプル)で表されていた。
ウェブ上にはその仕様のプログラムが紹介されていることも少なくないが、Pillow==7.2.0
以降は上記のように小数になっている。
公式な説明はこちら。
7.2.0 — Pillow (PIL Fork) 8.1.0 documentation
{前略
'GPSLatitudeRef': 'N',
'GPSLatitude': ((35, 1), (25, 1), (21098327, 1000000)),
'GPSLongitudeRef': 'E',
'GPSLongitude': ((136, 1), (24, 1), (3826538, 1000000)),
後略}
Googleマップにアクセスする
緯度経度を取得したらGoogleマップでその場所を表示させたい。
Googleマップでの緯度経度の書き方は以下に記されている。
多くの先人たちは度分秒を十進数の度に変換している。そこで私は度分秒のままアクセスするコードを書いてみた。
def getpos(dr, value):
d = int(value[0])
m = int(value[1])
s = value[2]
return f"{d}°{m}'" + f'{s}"{dr}' # 'を含む文字列を""で "を含む文字列を''で囲む
dict = exif["GPSInfo"]
gps = {ExifTags.GPSTAGS.get(key, key): dict[key] for key in dict}
lat = getpos(gps["GPSLatitudeRef"], gps["GPSLatitude"])
lon = getpos(gps["GPSLongitudeRef"], gps["GPSLongitude"])
location = f"{lat} {lon}"
print(location)
35°25'21.098327"N 136°24'38.26538"E
Googleマップでこの場所を表示させるには検索ボックスに緯度経度を入力してやればいいのだが、せっかくなのでその部分もPythonでおこないたい。
面倒くさい、楽をしたい。だから頑張る。これがプログラミングの原動力だ。
import webbrowser
url = "https://www.google.com/maps/search/?api=1&query=" + location
webbrowser.open(url)
答え合わせ
これまでExifデータを見てきたのは伊吹山山頂の日本武尊の像の写真でした。
下の画像ではExifは見れませんのであしからず。
[](https://www.google.com/maps/search/?api=1&query=35°25'21.098327"N 136°24'38.26538"E)
さあ、我がカメラ(というかスマホ)のGPSはどれくらい正確かなっと。
…うーん、直線距離で500mほど離れてるぞ。日常使いではそれほど酷いとは思わないけどどういうことなんだろう。山なのに高さが取得できていないことが緯度経度のズレとして出ている、とか?
最終形にするには
一連の処理をくっつければアプリケーションらしきものは作れる。だが、画像の中にExif情報がなかったら? GPSInfo
がなかったら? GPSInfo
があってもその中にGPSLatitude
などがなかったら? ちゃんとしたアプリケーションならそういうことも考慮する必要がある。私はしないけど。
Exif情報を書き込む
PILでできないか
PILのsave()
メソッドはexif
を指定することでExif情報を書き込むことができる。
ただし辞書を指定すると
TypeError: a bytes-like object is required, not 'dict'
と叱られてしまう。バイト型にするにはどうしたらよいのだろう。多くの先人たちはpiexif
モジュールを使っている。
ここまで来たらPILだけで済ませたいのでドキュメントを調べたのだが、バイト型にする方法を見つけることはできなかった。
その一方でGitHubの中に同様のイシューを見つけることができた。
どうやらここでも「piexif使えばいいじゃん」で終わっているようだ。
ならば私もpiexif
を使おう。開発者は日本の方のようだし。
終わりに
続く。