はじめに
iPhoneで撮った動画はQuickTime形式(.mov)ってフォーマットで保存されるらしいんですが、位置の情報が通常のExifツールとかだと読めないみたいです。ということで位置情報読み出して動画の最初の絵に位置情報つけてJpegで保存してみたいと思います。
環境
Google Colabratory(colab) と Google Drive(drive)を使っています。
- iPhoneで動画撮影
- Google Driveに保存
- ColaboratryでDriveマウント
って感じでやってみました。
Google Driveにはvideoってフォルダ作っってそこに動画を格納しておきます。
動画の処理にOpenCV使っています。この時点でColaboratoryのバージョンは4.1.2でした
colabの初期設定
環境判定のセット
try:
import google.colab
IN_COLAB = True
except:
IN_COLAB =False
こうしておくと、ノートブックをローカルのJupyterでもやりたいときにどっちの環境かって判定できるのでまずはこれを入れておきます。
colabでの準備など
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読み取ってみる
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に打ち込んでみます。
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のデータを解析してみます。形式は同じなのでバイト列から読み取る処理を作ってみます。
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の中身
更に追っかけてみましょう
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の解析
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
実行してみると
keys = read_keys(meta[b'keys'])
print(keys)
['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はさらに形式が変なので一個処理挟んでみます。
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)
['4.620686',
'+36.7001+137.8188+1295.662/',
'Apple',
'iPhone 11',
'14.2',
'2020-12-29T10:21']
別々のリストになっているのでマージします。
d = {k:v for k, v in zip(keys,values)}
print(d)
{'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が位置情報です。
緯度、経度、高度の順に格納されているので取り出してみます。
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
lat,lon,alt = get_location(d)
print("latitude",lat)
print("longitude",lon)
print("latitude",alt)
latitude +36.7001
longitude +137.8188
latitude +1295.662
プラスなのは赤道より北側(北緯)、グリニッジ天文台の東側(東経)、海抜より上って意味です。南半球やロンドンより西側のヨーロッパ西部、アフリカ西部、南北アメリカ大陸とか海の中を考えるとマイナス値を考慮しなきゃいけないんですが、日本で山の上しか考えないので全部プラスです。
実数値から度分秒になおしてみます。
# 実数型の経度緯度値を度分秒に変換
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
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の形式にします。
# 経度緯度文字列を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)
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形式で取得します。
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
日付も:
区切りなのがなんだかね。
動画から静止画抜き出して位置情報とタイムスタンプを埋め込む
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を使っています。
確認してみましょう
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で保存して読み取った位置情報とタイムスタンプを埋め込んでみました。