動画の手ブレを機械学習(Detectron2)を使って補正

# はじめに
私の愛車、スズキ・カプチーノの動画を、物体検出を使ってブレを抑えてきれいに加工しようとした記録です。

カプチーノは、コペンやS660につながる軽オープンスポーツの元祖とも言える存在です。
~~この車は初期型(EA11R)のF6Aエンジンで、FRなので重量配分など走行性能も高く、インタークーラーターボ搭載、軽自動車の自主規制上限の64PSを出力でき、あのイニDにも登場した素晴らs~~

はい、被写体については重要ではないのでこれくらいに。

# 動画の撮影
広めの駐車場で車の周囲を移動しながら一周し、iPhone SE2 を手持ちで撮影しました。
歩くたびに上下に揺れますし、移動に合わせて向きが少しずつ変わるため、左右方向も安定せず、もうグラグラです。
これは酔うぞ。
(Qiitaに掲載するためGIFに変換しています)
![手ブレ補正前](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122348/7b4b1203-7002-66b9-5975-cbc5e9c766fe.gif)

# 画像の加工
物体検出で車の位置を特定し、中央にずらしてやればブレがなくなるのでは？
やってみましょう。
![方針](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122348/a6b07b6a-6e98-beab-8514-ccad70d7010e.png)


## 環境

|名称  |説明  |
|---|---|
|Google Colaboratory  |Jupyter Notebook に似た、Google の Python 実行環境  |
|Detectron2  |Facebook AI Research が開発した、物体検出ライブラリ|

## フレーム分割
まずは動画から静止画を取り出します。
全フレームを処理してもよいのですが、今回は抜粋で。

```python
import cv2
import os
from google.colab.patches import cv2_imshow
from google.colab import drive
drive.mount('/content/drive')
```
`cv2_imshow`は、colaboratory環境で推奨される `cv2.imshow` の代替です。
また、データの参照と保存のため、Googleドライブをマウントしています。実行時に表示されるURLにアクセスし、発行されるトークンを画面に貼り付けることで認証され、利用できるようになります。

```python
# 読み込む動画ファイル
video_path = '/content/drive/My Drive/Colab Datasets/car_images/Cappuccino/IMG_1915.MOV'
# 何分割するか
output_images = 36

cap = cv2.VideoCapture(video_path)

# 総フレーム数
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT));

# フレーム出力間隔
interval = total_frames / output_images

frame_count = 0
n = 1

while True:
  ret, frame = cap.read()
  if ret != True:
    break

  if frame_count % interval < (frame_count - 1) % interval:
    #cv2_imshow(frame)
    print(os.path.dirname(video_path) + '/image/' + str(n) + '.jpg')
    cv2.imwrite(os.path.dirname(video_path) + '/image/' + str(n) + '.jpg', frame)
    n += 1

  frame_count += 1
```
この結果、静止画ファイルがドライブに保存されます。

## 物体検出

必要なライブラリを `pip instal`し、github から Detectron2 をインストールします。

```python
!pip install -U torch torchvision
!pip install git+https://github.com/facebookresearch/fvcore.git
!git clone https://github.com/facebookresearch/detectron2 detectron2_repo
!pip install -e detectron2_repo
```

各種ライブラリを読み込みます。

```python
import torch, torchvision
torch.__version__

import detectron2

import matplotlib.pyplot as plt
import cv2

from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg
from detectron2.utils.visualizer import Visualizer

import json
from PIL import Image, ImageChops

import os
import glob

from google.colab import drive
drive.mount('/content/drive')
```
物体検出には Faster RCNN を使用し、一般物体検出用に学習された学習済みモデルを使います。

```python
cfg = get_cfg()
cfg.merge_from_file("./detectron2_repo/configs/COCO-Detection/faster_rcnn_X_101_32x8d_FPN_3x.yaml")
cfg.MODEL.WEIGHTS = "detectron2://COCO-Detection/faster_rcnn_X_101_32x8d_FPN_3x/139173657/model_final_68b088.pkl"
os.makedirs(cfg.OUTPUT_DIR, exist_ok=True)

cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5
cfg.MODEL.DEVICE = "cpu"
predictor = DefaultPredictor(cfg)
```
物体検出を行い、物体の中心と画像の中心を一致させます。

```python
file_path = '/content/drive/My Drive/Colab Datasets/car_images/Cappuccino/image/*.jpg'

files = glob.glob(file_path)

# 複数ファイル(各フレーム)を処理する
for file_name in files:
    im = cv2.imread(file_name)
    outputs = predictor(im)
    v = Visualizer(im[:, :, ::-1],
               scale=1.0
    )

    v = v.draw_instance_predictions(outputs["instances"].to("cpu"))
    pred_boxes = outputs["instances"].to("cpu").pred_boxes.tensor.numpy()

    # 推論結果のボックスを大きさ順でソート
    pred_boxes = list(pred_boxes)
    pred_boxes.sort(key=lambda x:(x[2]-x[0])*(x[3]-x[1]))

    # 最大のボックスを取得
    largest_box = pred_boxes[-1]
    x_center = int((largest_box[2]+largest_box[0])/2)
    y_center = int((largest_box[3]+largest_box[1])/2)

    plt.figure(figsize = (20,90))
    plt.imshow(cv2.cvtColor(v.get_image()[:, :, ::-1], cv2.COLOR_BGR2RGB), interpolation='nearest')
    plt.show()

    # 物体中心と画像中心が一致するようにオフセットをずらす
    offset_im = ImageChops.offset(Image.open(file_name), 960-x_center, 540-y_center)
    offset_im.save(file_name + ".offset.jpg")
```

さてさて、このコードは最初からうまく動いたわけではありません。
フレームによっては、遠くに映っている小さい物体を拾ってしまい、それに位置を合わせようとして大きくずれてしまいます。
いっぱい検出しちゃった！どれに合わせれば！？
![複数検出の図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122348/94dff0d6-0a08-6b07-ec34-4c5e234d1d63.png)
そこで、lambda式を使ってボックスの大きさ順にソートし、最も大きいボックスの中心に合わせることとしました。
上記コードの`largest_box`を計算している前後をご覧ください。

`ImageChops.offset`で位置合わせをした結果が以下になります。画像のはみ出した部分は反対側から出てきますので、動画に戻すときに除去します。
![24.jpg.offset.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122348/08ae2fb8-968c-e9a0-c8f2-c9e5a739e480.jpeg)


# 結果発表
出力された画像を、ImageMagickなどを使って動画に戻します。
画像をずらした分、周辺部分が汚くなりますので適切にトリミングします。
実際に使った変換コマンドはこちら。
`convert -shave 240x135 -resize 320x180 -delay 20 *.jpg output.gif`

上下左右のブレがなくなりました。
Qiita掲載の都合でGIFにしていますが、もっときれいなHD画質の動画で出力することもできます。
![最終結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122348/f02eeb65-3087-9eb8-6524-8da285068af0.gif)