QuickTime(.mov)形式動画の経緯情報を読み取とる

# はじめに
iPhoneで撮った動画はQuickTime形式(.mov)ってフォーマットで保存されるらしいんですが、位置の情報が通常のExifツールとかだと読めないみたいです。ということで位置情報読み出して動画の最初の絵に位置情報つけてJpegで保存してみたいと思います。

# 環境
Google Colabratory(colab) と Google Drive(drive)を使っています。

* iPhoneで動画撮影
* Google Driveに保存
* ColaboratryでDriveマウント

って感じでやってみました。
Google Driveにはvideoってフォルダ作っってそこに動画を格納しておきます。


https://colab.research.google.com/

https://www.google.com/intl/ja_jp/drive/

https://github.com/LeoHsiao1/pyexiv2

動画の処理にOpenCV使っています。この時点でColaboratoryのバージョンは4.1.2でした

https://docs.opencv.org/4.1.2/

# colabの初期設定
## 環境判定のセット

```python:
try:
    import google.colab
    IN_COLAB = True
except:
    IN_COLAB =False
```

こうしておくと、ノートブックをローカルのJupyterでもやりたいときにどっちの環境かって判定できるのでまずはこれを入れておきます。

## colabでの準備など

```python:
if IN_COLAB:
    %matplotlib inline
    from google.colab import drive
    from google.colab.patches import cv2_imshow
    drive.mount('/content/gdrive')
    DRIVE_ROOT='/content/gdrive/MyDrive/video/'
    !pip install pyexiv2
import pyexiv2
import matplotlib.pylab as plt
```

ここでは

* matplotlibをinlineにしておかないと表示してくれない
* Google Driveのマウント
* pyexiv2のインストール

をやっておきます。 

# QuickTimeのファイル形式

iPhoneで撮った動画をGoogle Driveに保存すると、IMG_0001.MOVみたいな名前で格納されています。
こいつはATOMとかBOXとかいう形式のバイナリファイルで、

| name | bytes |
|:-:|-:|
| length | 4 |
| tag | 4 |
| data | length - 8 |

これが集まってできています。
ファイルの先頭から4バイトで長さ(length)、次の4バイトで識別タグ(tag)、読み取った長さ分から8バイト引いた分のデータ(data)が繰り返し出てきます。また、入れ子にすることが可能でデータ部もこの形式でいくつかのATOMに分かれているって構造をしています。

# やってみよう
## ファイルからATOM読み取ってみる

```python:
def read_atom(filename):
    f = open(filename,'rb')
    atom_dict = {}
    length = f.read(4)
    while length:
        size = int.from_bytes(length,byteorder='big')
        code = f.read(4)
        seek = size-8
        data = f.read(seek)
        atom_dict[code]=data
        
        length = f.read(4)
    return atom_dict
```

ファイルをバイナリモードで開いてlength,tag,dataの順に読み取り、dictionaryに打ち込んでみます。

```python:
import os

def print_dict(d,title):
    print(title)
    for k,v in d.items():
        print(k,len(v))

filename = os.path.join(DRIVE_ROOT,"IMG_0001.MOV")
atom_dict = read_atom(filename)
print_dict(atom_dict,'file')
```
実行すると

```:結果
file
b'ftyp' 12
b'wide' 0
b'mdat' 16996875
b'moov' 18081
```

実はちょいちょいバイナリエディタで中身覗きながらやっていたんですが、moovのデータ内に位置情報を含むメタ情報があるようだとあたりをつけていました。なお、サイズ的にmdatが動画データみたいです。

## moovの中身を解析

あたりが付いたのでmoovのデータを解析してみます。形式は同じなのでバイト列から読み取る処理を作ってみます。

```python:
def read_atom_bin(b,ofset=0):
    index = ofset
    atom_dict = {}
    while index<len(b):
        length = b[index:index+4]
        size = int.from_bytes(length,byteorder='big')
        # print('size=',size)
        code = b[index+4:index+8]
        data = b[index+8:index+size-8]
        atom_dict[code]=data
        index += size
    return atom_dict

moov = read_atom_bin(atom_dict[b'moov'])    
print_dict(moov,'moov')
```


```:結果
moov
b'mvhd' 92
b'trak' 3365
b'meta' 512
```

メタデータが格納されている箇所を見つけました。

## metaの中身
更に追っかけてみましょう

```python:
meta = read_atom_bin(moov[b'meta'])
print_dict(meta,'meta')
```

```:結果
meta
b'hdlr' 18
b'keys' 241
b'ilst' 213
```

keysとilstが見つかりました。
ちょっと変な形式なんですが、keysにメタ情報の名称、ilstにデータが入っているって形式なんですね。しかもATOMの形状が絶妙に違ったりするので、コイツらの読み取り用には別の処理作りました。

## keys,ilstの解析


```python:
def read_keys(b,ofset=8):
    index = ofset
    keys = []
    while index < len(b):
        length = b[index:index+4]
        size = int.from_bytes(length,byteorder='big')
        code = b[index+4:index+8]
        data = b[index+8:index+size]
        keys.append(data.decode())
        index += size
    return keys

def read_ilst(b):
    index=0
    l = []
    while index < len(b):
        length = b[index:index+4]
        size = int.from_bytes(length,byteorder='big')
        code = b[index+4:index+8]
        data = b[index+8:index+size]
        l.append([code,size,data])
        index += size
    return l
```

実行してみると

```python:
keys = read_keys(meta[b'keys'])
print(keys)
```


```python:結果
['com.apple.quicktime.location.accuracy.horizontal',
 'com.apple.quicktime.location.ISO6709',
 'com.apple.quicktime.make',
 'com.apple.quicktime.model', 'com.apple.quicktime.software',
 'com.apple.quicktime.crea']

```
ilstはさらに形式が変なので一個処理挟んでみます。

```python:
ilst = read_ilst(meta[b'ilst'])
values = []
for item in ilst:
    dt = item[2]
    data = read_ilst(dt)
    values.append(data[0][2][8:].decode())
print(values)
```


```python:結果
['4.620686',
 '+36.7001+137.8188+1295.662/',
 'Apple',
 'iPhone 11',
 '14.2',
 '2020-12-29T10:21']
```

別々のリストになっているのでマージします。

```python:
d = {k:v for k, v in zip(keys,values)}
print(d)
```


```python:結果
{'com.apple.quicktime.location.accuracy.horizontal': '4.620686',
 'com.apple.quicktime.location.ISO6709': '+36.7001+137.8188+1295.662/',
 'com.apple.quicktime.make': 'Apple',
 'com.apple.quicktime.model': 'iPhone 11',
 'com.apple.quicktime.software': '14.2',
 'com.apple.quicktime.crea': '2020-12-29T10:21'}

```

com.apple.quicktime.location.ISO6709が位置情報です。
緯度、経度、高度の順に格納されているので取り出してみます。

```python:
import re
# タグのリストから位置情報だけ取り出す
def get_location(d):
  loc =d['com.apple.quicktime.location.ISO6709']
  lat,lon,alt = re.findall(r'[\+-]\d+\.\d+',loc)
  return lat,lon,alt
```

```python:
lat,lon,alt = get_location(d)
print("latitude",lat)
print("longitude",lon)
print("latitude",alt)
```

```:結果
latitude +36.7001
longitude +137.8188
latitude +1295.662
```

プラスなのは赤道より北側（北緯）、グリニッジ天文台の東側(東経)、海抜より上って意味です。南半球やロンドンより西側のヨーロッパ西部、アフリカ西部、南北アメリカ大陸とか海の中を考えるとマイナス値を考慮しなきゃいけないんですが、日本で山の上しか考えないので全部プラスです。

実数値から度分秒になおしてみます。

```python:
# 実数型の経度緯度値を度分秒に変換
def getDegree(val):
  x = float(val)
  d = int(x)
  mod = x % 1
  m = int(mod*60)
  s = ((mod*60) % 1)*60
  return d,m,s
```

```python:
lat_d,lat_m,lat_s = getDegree(lat)
lon_d,lon_m,lon_s = getDegree(lon)
print('北緯:{}度{}分{:.2f}秒'.format(lat_d,lat_m,lat_s))
print('東経:{}度{}分{:.2f}秒'.format(lon_d,lon_m,lon_s))
```

```:結果
北緯:36度42分0.36秒
東経:137度49分7.68秒
```

EXIFの形式にします。

```python:
# 経度緯度文字列をExifのGPS情報フォーマットに変換
def getExifLatLonformat(s):
  f = float(s)
  d,m,s = getDegree(f)
  return "{}/1 {}/1 {:.0f}/100".format(d,m,s*100)

def getExifAltformat(s):
  f = float(s)
  return "{:.0f}/100".format(f*100)
```

```python:
print('Exif Latitude',getExifLatLonformat(lat))
print('Exif Longitude',getExifLatLonformat(lon))
print('Exif Altitude',getExifAltformat(alt))
```

```:結果
Exif Latitude 36/1 42/1 36/100
Exif Longitude 137/1 49/1 768/100
Exif Altitude 129566/100
```

ついでにタイムスタンプもExif形式で取得します。

```python:
import datetime

def get_timestamp(d):
  dt = datetime.datetime.strptime(d["com.apple.quicktime.crea"],'%Y-%m-%dT%H:%M')
  return dt, dt.strftime('%Y:%m:%d %H:%M:%S') 

dt,ds = get_timestamp(d)
print(ds)
```

```:結果
2020:12:29 10:21:00
```
日付も`:`区切りなのがなんだかね。

## 動画から静止画抜き出して位置情報とタイムスタンプを埋め込む

```python:
import cv2

# Jpegのファイル名
jpeg_name = os.path.splitext(filename)[0]+'.jpg'

# OpenCVで動画の最初のフレームを取得
cap = cv2.VideoCapture(filename)
ret, frame = cap.read()

# 縦向き動画の場合90度回転
#frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)

# Jpeg保存
cv2.imwrite(jpeg_name,frame)

# EXIF編集
img_exif = pyexiv2.Image(jpeg_name)
metadata = img_exif.read_exif()
metadata['Exif.GPSInfo.GPSLatitudeRef']='N'
metadata['Exif.GPSInfo.GPSLatitude']=getExifLatLonformat(lat)
metadata['Exif.GPSInfo.GPSLongitudeRef']='E'
metadata['Exif.GPSInfo.GPSLongitude']=getExifLatLonformat(lon)
metadata['Exif.GPSInfo.GPSAltitudeRef']='0'
metadata['Exif.GPSInfo.GPSAltitude']=getExifAltformat(alt)
dt, ts = get_timestamp(d)
metadata['Exif.Image.DateTime']=ts
metadata['Exif.Photo.DateTimeOriginal']=ts
metadata['Exif.Photo.DateTimeDigitized']=ts

img_exif.modify_exif(metadata)
img_exif.close()
```

* 動画の読み込みにはOpenCVを使っています。


確認してみましょう

```python:
from PIL import Image

# Jpeg表示
fig = plt.figure(figsize=(15,15))
image = Image.open(jpeg_name)
plt.imshow(image)

# EXIF情報の表示
im = pyexiv2.Image(jpeg_name)
metadata = im.read_exif()
for k in metadata:
  print(k,metadata[k])
```

```:結果
Exif.Image.DateTime 2020:12:29 10:21:00
Exif.Image.ExifTag 70
Exif.Photo.DateTimeOriginal 2020:12:29 10:21:00
Exif.Photo.DateTimeDigitized 2020:12:29 10:21:00
Exif.Image.GPSTag 140
Exif.GPSInfo.GPSLatitudeRef N
Exif.GPSInfo.GPSLatitude 36/1 42/1 36/100
Exif.GPSInfo.GPSLongitudeRef E
Exif.GPSInfo.GPSLongitude 137/1 49/1 768/100
Exif.GPSInfo.GPSAltitudeRef 0
Exif.GPSInfo.GPSAltitude 129566/100
```

という感じで、QuickTime(.mov)ファイルから位置情報とタイムスタンプを読み取り、動画の最初のフレームをjpegで保存して読み取った位置情報とタイムスタンプを埋め込んでみました。

https://gist.github.com/xiaomuc/e22f6ce499933131eed1eb99e53e6709#file-location-ipynb