6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PillowでExif情報を遊ぶ ~辞書の復習を兼ねて~

Posted at

はじめに

今回は少しずつコードが充実していく仕様なので最終的なソースコードはありません。
恐縮ですがそのつもりでお読みください。

Exif情報を取得する

PILのExifTagsでJPEG画像のExif情報を取得することができる。そのためのメソッドは_getexif()だ。この安駄婆もといアンダーバーは何なの。

ソース1
from PIL import Image, ExifTags
from pprint import pprint

filename = "hoge.jpg"
img = Image.open(filename)
dict = img._getexif()
pprint(dict)

結果は辞書の形で得られる。

結果1
{前略
 256: 3264,
 257: 2448,
 後略}

256はImageWidthを、257はImageLengthを意味する。
それぞれの項目はタグ番号で登録されているのだ。
exif_chart.png
ExifTagsにはタグ番号とタグ名称が紐づいた辞書がある。ExifTags.TAGSだ。

ソース2
tags = ExifTags.TAGS
pprint(tags)
結果2
{前略
 256: 'ImageWidth',
 257: 'ImageLength',
 後略}

ExifTags.TAGSを使って画像の中のExif情報をタグ名称をキーとして取得することができる。

ソース3
dict = img._getexif()
exif = {}  # 空の辞書を定義
for key, value in dict.items():
    exif[ExifTags.TAGS[key]] = value  # 辞書の値を新たな辞書のキーとしている

pprint(exif)
結果3
{前略
 '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機能を持つものがある。

ソース1の結果の一部
 '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'}

GPSLatitudeGPSLongitudeが緯度と経度。これらは三つの数値のタプルになっている。度・分・秒だ。
今回の写真ではGPSAltitude(高度)がゼロになってしまっているのが残念だ。

PILのバージョンに注意

Exifの規格ではGPSLatitudeGPSLongitudeについて「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マップでの緯度経度の書き方は以下に記されている。

  緯度と経度の確認、入力(Google)

多くの先人たちは度分秒を十進数の度に変換している。そこで私は度分秒のままアクセスするコードを書いてみた。

ソース4
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)
結果4
35°25'21.098327"N 136°24'38.26538"E

Googleマップでこの場所を表示させるには検索ボックスに緯度経度を入力してやればいいのだが、せっかくなのでその部分もPythonでおこないたい。
面倒くさい、楽をしたい。だから頑張る。これがプログラミングの原動力だ。

ソース5
import webbrowser
url = "https://www.google.com/maps/search/?api=1&query=" + location
webbrowser.open(url)

答え合わせ

これまでExifデータを見てきたのは伊吹山山頂の日本武尊の像の写真でした。
下の画像ではExifは見れませんのであしからず。
[ibukiyama.jpg](https://www.google.com/maps/search/?api=1&query=35°25'21.098327"N 136°24'38.26538"E)
さあ、我がカメラ(というかスマホ)のGPSはどれくらい正確かなっと。
ibukiyama_map.jpg
…うーん、直線距離で500mほど離れてるぞ。日常使いではそれほど酷いとは思わないけどどういうことなんだろう。山なのに高さが取得できていないことが緯度経度のズレとして出ている、とか?

最終形にするには

一連の処理をくっつければアプリケーションらしきものは作れる。だが、画像の中にExif情報がなかったら? GPSInfoがなかったら? GPSInfoがあってもその中にGPSLatitudeなどがなかったら? ちゃんとしたアプリケーションならそういうことも考慮する必要がある。私はしないけど。

Exif情報を書き込む

PILでできないか

PILのsave()メソッドはexifを指定することでExif情報を書き込むことができる。
ただし辞書を指定すると

TypeError: a bytes-like object is required, not 'dict'

と叱られてしまう。バイト型にするにはどうしたらよいのだろう。多くの先人たちはpiexifモジュールを使っている。
ここまで来たらPILだけで済ませたいのでドキュメントを調べたのだが、バイト型にする方法を見つけることはできなかった。
その一方でGitHubの中に同様のイシューを見つけることができた。

  Make EXIF plugin #520(GitHub)

どうやらここでも「piexif使えばいいじゃん」で終わっているようだ。
ならば私もpiexifを使おう。開発者は日本の方のようだし。

終わりに

続く。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?