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

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

# Exif情報を取得する

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

```Python:ソース1
from PIL import Image, ExifTags
from pprint import pprint

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

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

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

`256`はImageWidthを、`257`はImageLengthを意味する。
それぞれの項目はタグ番号で登録されているのだ。
![exif_chart.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/458b3afd-243d-884e-adc5-5f2695d40ddc.png)
`ExifTags`にはタグ番号とタグ名称が紐づいた辞書がある。`ExifTags.TAGS`だ。

```Python:ソース2
tags = ExifTags.TAGS
pprint(tags)
```

```Python:結果2
{前略
 256: 'ImageWidth',
 257: 'ImageLength',
 後略}
```

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

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

pprint(exif)
```

```Python:結果3
{前略
 'ImageWidth': 3264,
 'ImageLength': 2448,
 後略}
```

### 存在しないキーの辞書を呼び出すエラーを回避する
辞書の値は`dict[key]`で取得することができるが、辞書にないキーを指定するとエラーになってしまう。
それを避けるには**dict.get(key[, default])**を使う。`key`が辞書にあればそれに対応する値を、そうでなければ`default `を返す。`default`値は必須ではなく、指定されなかった場合は`None`を返す。
今回はタグ番号とタブ名称の辞書だから、存在しないタグ番号ならばタグ番号そのものを返すのがいいだろう。

```Python:こう直す
#    exif[ExifTags.TAGS[key]] = value
#      ↓
    exif[ExifTags.TAGS.get(key, key)] = value
```

### 内包表記
さらに、これを1ラインで記述することができる。

```Python:内包表記
# 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}
```

このワンライナー、ぱっと見では何をやっているのかわかりづらいが、もう少し簡単な例を見れば納得がいく。
こういうのを**内包表記**と呼ぶ。無理して使う必要はないが、目にする機会は結構多い。これから何度もググることになるだろうから名前だけは覚えておこう。

```Python:簡単な例
arr = [x for x in "ABC"]
print (arr)

# 結果
['A', 'B', 'C']
```

## GPS情報を取得する

スマホのカメラで撮影するとGPSの情報が含まれていることがある。コンデジやデジイチの中にもGPS機能を持つものがある。

```Python:ソース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出力は自動でソートされる](https://qiita.com/manjuu_eater/items/59d76f786bdf2b08c80d)

```Python:タグ番号をタグ名称に直した結果
{'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](https://pillow.readthedocs.io/en/stable/releasenotes/7.2.0.html?highlight=exif)


```Python:以前のバージョン
{前略
 'GPSLatitudeRef': 'N',
 'GPSLatitude': ((35, 1), (25, 1), (21098327, 1000000)),
 'GPSLongitudeRef': 'E',
 'GPSLongitude': ((136, 1), (24, 1), (3826538, 1000000)),
 後略}
```

## Googleマップにアクセスする

緯度経度を取得したらGoogleマップでその場所を表示させたい。
Googleマップでの緯度経度の書き方は以下に記されている。

　　[緯度と経度の確認、入力（Google）](https://support.google.com/maps/answer/18539)

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

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

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

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

## 答え合わせ

これまでExifデータを見てきたのは伊吹山山頂の日本武尊の像の写真でした。
下の画像ではExifは見れませんのであしからず。
[![ibukiyama.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/630fe611-69b4-09c2-ed45-66c8cdce186a.jpeg)](https://www.google.com/maps/search/?api=1&query=35°25'21.098327"N 136°24'38.26538"E)
さあ、我がカメラ（というかスマホ）のGPSはどれくらい正確かなっと。
![ibukiyama_map.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/36122219-64a3-95bc-a3bd-368847f222e6.jpeg)
…うーん、直線距離で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）](https://github.com/python-pillow/Pillow/issues/520)

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

# 終わりに
続く。