ドライブレコーダーの動画からGPSデータを取り出したい

# 発端

ドライブレコーダーを買ったんですよ。
買ったのは[コムテックのHDR-352GH](http://www.e-comtec.co.jp/0_recorder/hdr352gh.html)
GPS搭載モデルで、windowsだとビューワーソフトがあり、動画と軌跡を同時に閲覧することが出来ます。

![soft_352ghp.gif](https://qiita-image-store.s3.amazonaws.com/0/29014/eb833b8c-a226-692f-77b2-39cded7a589d.gif)

ビューワーでは地図表示はあるもののGPSログの取り出しは出来ませんし、windows以外にはビューワーソフトがありません。
折角GPSのログも保存されているはずなので、なんとかして取り出してGPSロガーとしての機能も見いだしたいと思います。

# 動画を調べてみる

micro SDカードをドライブレコーダーから取り出してmacから読み出してみると、動画の拡張子はmov, 1分ごとに分割されていました。
隠しファイルでもGPSデータは保存されていません。
つまり動画の中に埋め込まれているようです。

<img width="291" alt="スクリーンショット 2017-10-01 10.47.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/29014/d40fa1cf-b7ae-67b3-7a72-ac3063f9df03.png">

動画と言えばffmpeg。
ffmpegに含まれているffprobeで解析してみます。

```
ffprobe 003_202423_A.MOV
ffprobe version 3.3.4 Copyright (c) 2007-2017 the FFmpeg developers
  built with Apple LLVM version 8.0.0 (clang-800.0.42.1)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/3.3.4 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-libmp3lame --enable-libx264 --enable-libxvid --enable-opencl --enable-videotoolbox --disable-lzma --enable-vda
  libavutil      55. 58.100 / 55. 58.100
  libavcodec     57. 89.100 / 57. 89.100
  libavformat    57. 71.100 / 57. 71.100
  libavdevice    57.  6.100 / 57.  6.100
  libavfilter     6. 82.100 /  6. 82.100
  libavresample   3.  5.  0 /  3.  5.  0
  libswscale      4.  6.100 /  4.  6.100
  libswresample   2.  7.100 /  2.  7.100
  libpostproc    54.  5.100 / 54.  5.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '003_202423_A.MOV':
  Metadata:
    major_brand     : qt
    minor_version   : 0
    compatible_brands:
    creation_time   : 2017-10-01T20:24:24.000000Z
  Duration: 00:00:59.99, start: 0.000000, bitrate: 11264 kb/s
    Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080, 10546 kb/s, 29 fps, 29 tbr, 90k tbn, 180k tbc (default)
    Metadata:
      creation_time   : 2017-10-01T20:24:24.000000Z
      handler_name    : iCatchTek Video Handler
    Stream #0:1(eng): Audio: pcm_s16le (sowt / 0x74776F73), 44100 Hz, 1 channels, s16, 705 kb/s (default)
    Metadata:
      creation_time   : 2017-10-01T20:24:24.000000Z
      handler_name    : iCatchTek Audio Handler
```

動画の中身は分かりましたが、これでは位置情報に関して分からないですね。
ドライブレコーダーによっては字幕ファイルとして動画を合わせて保存しているものもあるようですが、字幕ファイルもない様子。
動画ファイルにどうやって埋め込まれているんでしょうか。

バイナリにそれっぽいデータがあるか見てみましょう。

```
xxd 003_202423_A.MOV | grep gps
17ef160: 3556 7367 7073 4b6a 6911 e1a9 3470 6ced  5VsgpsKji...4pl.
1c5a880: ef53 6770 73e4 547b d05f 92cb 3a28 9051  .Sgps.T{._..:(.Q
3dc9a00: 4872 3967 7073 6594 7eed 2376 071d cafa  Hr9gpse.~.#v....
```

gpsでgrepしても見つからないので、バイナリデータ全体を覗いてみると

```
507c430: 2020 2020 5220 3030 3230 3041 3334 3635      R 00200A3465
507c440: 2e39 3635 344e 3133 3632 382e 3432 3930  .9654N13628.4290
507c450: 4530 3037 3431 3731 3030 3132 3032 3432  E007417100120242
507c460: 3400 2020 2020 2020 2020 3030 3230 302d  4.        00200-
507c470: 3030 3835 3030 3032 382d 3030 3137 4e4e  008500028-0017NN
507c480: 4e4e 4e4e 2020 2020 2020 2020 2020 2020  NNNN
```

`3465.9654N13628.4290E` なんてそれっぽいですね。[^gps]
[^gps]: 位置情報は編集しています
小数点の位置からするとDMM形式でしょう。

# 動画からデータを抜き出す

動画からデータを抜き出すには[MP4動画のバイナリファイルをコマンドラインでいじる方法のメモ](https://qiita.com/kunichiko/items/6fc1ec29989da28e6287)が大変参考になりました。

動画はBoxと呼ばれる単位で構造化されているので、読み出すのは簡単にできそうです。
先頭のBoxを読んでみましょう

```rb
irb(main):001:0> io = File.open '003_202423_A.MOV'
=> #<File:003_202423_A.MOV>
irb(main):002:0> length = io.read(4)
=> "\x00\x00\x00\x18"
irb(main):003:0> code = io.read(4)
=> "ftyp"
```

不要なData部は読み飛ばしたいので、lengthからバイトに変換します。

```rb
irb(main):004:0> size = length.unpack('H*').first.to_i(16)
=> 24
```

[^mp4]: Box構造自体はツリーになっている場合もあります。
トップレベルで並んでいるBoxのサイズと種類を見てみましょう。[^mp4]


```rb
io = File.open '003_202423_A.MOV'

while length = io.read(4)
  size = length.unpack('H*').first.to_i(16)
  seek = size - 8
  code = io.read(4)
  p "code #{code}"
  p "box size #{size} byte"
  io.seek(seek, IO::SEEK_CUR)
end
```

これを実行すると

```
"code ftyp"
"box size 24 byte"
"code mdat"
"box size 84367444 byte"
"code moov"
"box size 27428 byte"
"code udat"
"box size 76928 byte"
"code ICAT"
"box size 12 byte"
```

Boxの種別の中でも以下は代表的なものなので

- ftyp：ファイルタイプ
- mdat：ビデオデータ自体
- moov：メディアのメタデータ

残るはudatかICAT、サイズ的にはudatが一番怪しいですね。[^udat]
[^udat]: 他のドライブレコーダーではこの限りではありません
udatの中身を書き出してみます。

```rb
io = File.open '003_202423_A.MOV'

while length = io.read(4)
  size = length.unpack('H*').first.to_i(16)
  seek = size - 8
  code = io.read(4)
  if code == 'udat'
    udat = io.read(seek)
    File.write 'udat', udat
  else
    io.seek(seek, IO::SEEK_CUR)
  end
end
```

生成されたファイルの中身を確認すると、先ほどの緯度経度が入っていることを確認できます。
後は文字列から緯度経度を抜き出すだけですね。

# GPXファイルを生成する

DMM形式の緯度経度では使いにくいので、変換した上でGPSファイルに書き出してみることにします。
ドライブレコーダーの動画は一度のドライブで一つのディレクトリ、動画は1分ごとに分割されています。
なのでディレクトリの中の動画の位置情報は一つのGPXファイルにまとめることにしてみます。

```rb
require 'pathname'
require 'rexml/document'

# DMM形式から変換
def dmm_to_dms str
  number, fractional = str.split('.')
  number[0...-2].to_r + "#{number[-2..-1]}.#{fractional}".to_r / 60
end

doc = REXML::Document.new
doc << REXML::XMLDecl.new('1.0', 'UTF-8')

gpx = REXML::Element.new('gpx')
gpx.add_attributes({
  'creator' => "ruby",
  'version' => "1.1",
  'xmlns' => 'http://www.topografix.com/GPX/1/1',
  'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
  'xsi:schemaLocation' => 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
})
doc.add_element(gpx)

Pathname.glob("*.MOV") do |path|
  if path.file?
    p "#{path.basename.to_s} processing"
    io = File.open path.basename.to_s
    trk = REXML::Element.new('trk')
    gpx.add_element(trk)
    trkseg = REXML::Element.new('trkseg')
    trk.add_element(trkseg)

    while length = io.read(4)
      size = length.unpack('H*').first.to_i(16)
      seek = size - 8
      code = io.read(4)
      if code == 'udat'
        udat = io.read(seek)
        udat.scan(/A([0-9.]*)([NS])([0-9.]*)([EW])/) do |m|
          # 緯度経度を読み込んで変換
          lat = dmm_to_dms(m[0]).to_f * (m[1] == 'N' ? 1 : -1)
          lon = dmm_to_dms(m[2]).to_f * (m[3] == 'E' ? 1 : -1)
          trkpt = REXML::Element.new('trkpt')
          trkpt.add_attributes({'lat' => lat, 'lon' => lon})
          trkseg.add_element(trkpt)
        end
      else
        io.seek(seek, IO::SEEK_CUR)
      end
    end
  end
end

# GPXファイルを書き出し
File.open('output.gpx', 'w') do |file|
  doc.write(file, indent=2)
end
```

これを実行するとGPXファイルが生成されます。
GoogleMapなどにインポートすると通った軌跡が描画されます。

# 緯度経度以外の情報も取り出す
udatの中には緯度経度の情報以外にも速度、時刻、加速度が含まれていました。
それぞれの動画トラックからそれらのデータをCSVにして取り出してみます。

```rb

require 'csv'
require 'pathname'

pat = /A([0-9.]*)([NS])([\d.]*)([EW])(\d{4})(\d{12})\s+(\d{5})([\d-]{1}\d{4})([\d-]{1}\d{4})([\d-]{1}\d{4})/

Pathname.glob("*.MOV") do |path|
  if path.file?
    p "#{path.basename.to_s} processing"
    io = File.open path.basename.to_s
    csv = CSV.open("#{path.basename.to_s.split('.').first}.csv", 'w') do |csv|

      while length = io.read(4)
        size = length.unpack('H*').first.to_i(16)
        seek = size - 8
        code = io.read(4)
        if code == 'udat'
          udat = io.read(seek)
          udat.gsub(/[[:cntrl:]]/,"").scan(pat) do |m|
            p m
            csv << m
          end
        else
          io.seek(seek, IO::SEEK_CUR)
        end
      end
    end
  end
end
```

CSVには
緯度, 緯度の符号, 経度, 経度の符号, 速度, UTC時刻, 経過時間（ミリ秒、60000でリセット）, X加速度, Y加速度, Z加速度 が書き出されています。

# Gemにする

ソースコードをきれいにして、Gem化したものをGithubに公開しました
コードは[こちら](https://github.com/k-shogo/comtec-dr)

# まとめ

ドライブレコーダーの動画の中身を覗いて、GPXファイルの取り出しをしました。
HDR-352GHは32GBまでのカードをサポートしており、最高画質でも6時間ほどの動画を残すことが出来ます。
さらにそれ以上の容量のカードでも認識するので、自分も64GBのカードを入れて使用しています。[^sd]
[^sd]: あくまで自己責任ではありますが
今まではiPhoneのアプリで旅程を記録していたのですが、起動し忘れなどもあるのでドライブレコーダーが常に記録してくれているのは気分が楽です。
~~最初から汎用的なデータで書き出してくれればいいのに~~