MotoJapan's Tech-Memo

技術めも

Linuxのジョブ/プロセス(jobs/process)関連のコマンドまとめ (起動、中断、終了、検索、ジョブ強制終了、ログアウト継続実行)

ubuntu/raspberry piでの開発で重宝しているコマンドを覚書
今回は、hoge.pyというスクリプトを例にとる

プロセスを起動する方法

$ python hoge.py   #フォアグラウンド
$ python hoge.py & #バックグランド
[1] 20664          # [ジョブID]プロセスID

特定のプロセスを検索する方法

hoge.pyを検索する場合

$ ps aux | grep hoge.py
pi       20664  100  0.5   8992  4992 pts/0    R    15:44   4:01 python hoge.py
pi       20744  100  0.5   8992  4916 pts/0    R    15:46   2:30 python hoge.py
pi       20745  3.1  0.5   8992  4928 pts/0    T    15:46   0:04 python hoge.py
pi       20903  0.0  0.1   4272  1844 pts/0    S+   15:48   0:00 grep --color=auto hoge.py

3回実行しているとこんな感じ

フォアグラウンドのプロセスを終了する方法

python hoge.pyを実行中に下記コマンド

ctrl+c 強制終了 ジョブにから消える
ctrl+z 中断 ジョブに残る(Stopped)

バックグランドのプロセスを終了する方法

方法1. python hoge.py &を実行すると、実行直後のログに" [ジョブID]プロセスID "が出力されるのでそれをメモしてkill
方法2. プロセスを検索してkill

プロセスを検索する

$ ps aux | grep hoge.py

ここで見つかるプロセスIDをメモ

プロセスkill

$ sudo kill プロセスID

ジョブを確認する方法

現在のジョブを確認するコマンドは下記

$ jobs
[1]   Running                 python hoge.py &
[2]-  Running                 python hoge.py &
[3]+  Stopped                 python hoge.py

Ctrl+Zで終わるとstatusが[Stopped]になっている
これを再起動したり、強制終了させる方法は下記

中断したジョブをフォアグラウンド/バックグラウンドで再開する方法

フォアグラウンドで再開する場合

$ fg %ジョブ番号

バックグラウンドで再開する場合

$ bg %ジョブ番号

ジョブの切り替えは下記
Stoppedのジョブをバックグランドで再開し、フォアグラウンドに切り替える場合

$ bg %ジョブ番号 # バックグランドで再開
$ fg %ジョブ番号 # バックグランドからフォアグラウンドに切り替え

中断したジョブを強制終了する方法

中断した場合はstatusがstoppedになるので、ジョブを終了する

$ sudo kill %ジョブ番号

※実際に即ジョブが強制終了されるわけではないので、少し待つ

ログアウト後もコマンドを実行する方法

sshで繋いでいると切りたくなることがある
(私の場合、特に機械学習とか)
下記コマンドで可能

sudo nohup python hoge.py &

ログアウト後も実行しているコマンドを終了する方法

バックグランドのプロセスを終了する方法と同じ

【GAS】googleスプレッドシートで管理した賞味期限リストをメールで定期通知してみた

GAS(Google Apps Script)という、Google製サービス上で動くJavascript互換のスクリプトを初めて触ってみた。

家の課題を解決するために。

課題:家の調味料や保存食の賞味期限がいつの間に切れていく

こんなことありますよね。(え、ないですか?)
私と妻で掃除してみたところ65品目ありましたが、この期限を全て把握することは記憶力が鶏レベルの私には無謀です。

調味料だけでもよくわからないほどあった。

写真は調味料棚の一部
f:id:motojapan:20170918224818p:plain
実際棚はこの9倍くらいの容積があるので覚えられるわけがない。

これに対して、賞味期限を定期通知することで、家の中の賞味期限食品を管理し、ゴミを減らし、地球温暖を防ぎ、さらに世界の家庭で蔓延しているらしい「夕食何にする?」という問いを「今晩あれを使って何か作ろう」という積極的な思考へ変換することで、世の中全ての家庭が円満になることを妄想して作ってみた。

概略図
f:id:motojapan:20170919232912p:plain

やったこと

0.夫婦共有のスプレットシートを作る

夫婦共有のGoogle Driveフォルダにスプレットシートを作る。

1.スプレットシートに賞味期限を書く

大掃除がてらキッチン周りの掃除と一緒にやる。
15分程度で書き込み終わり。

こんな感じ。
f:id:motojapan:20170919222810p:plain

最低限、B列とC列があればOK。

説明
A列 今日の日付(A1セル)
B列 品目名
C列 賞味期限
D列 残り日数

今回はスクリプトで完結するので使わないが
A1セルは、[=TODAY]で今日の日付を取得。
D2セルは、[=C2-$A$1]で残り日数を取得。

2.スプレットシートの情報をemailで送るスクリプトを書く

スプレットシートの[ツール]->[スクリプトエディタ]を選択。

ここで空プロジェクトができるので下記を記載。
ソース自体は30行くらいでできた。

ソースコード全体は下にあるが重要なところだけ先んじて覚書。

getRangeについて
  var MAX_LINES = 200
  //シート情報を取得
  var sheet = SpreadsheetApp.getActiveSheet();  
  //B,C列の情報を、配列で取得
  var lines = sheet.getRange(2, 2, MAX_LINES, 2).getValues();  

getRangeでシート情報を配列として取得できるが、多重定義されてるのでいろいろなデータの取り方がある。
本家API referenceを参考にする。

今回の場合下図の対応。
getRange(row, column, numRows, numColumns)

f:id:motojapan:20170919235816p:plain


ここ以外特にハマったとこはなく終わった。

ソースコード全体

下記の通り。

function myFunction() {
  var MAX_LINES = 200
  var today = new Date();

  var sheet = SpreadsheetApp.getActiveSheet();  
  var lines = sheet.getRange(2, 2, MAX_LINES, 2).getValues();  
  var text = ""
  var flag = true
  
  for (var idx in lines) {
      var date      = lines[idx][1]
      var item_name = lines[idx][0]
      if(date == null || date == "") {
        continue;
      }    
      if(item_name == null || item_name == "") {
        continue;
      }
      
      if(flag && date > today) {
        today_info = Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy年M月d日')
        text +=  "------- today : " + today_info + " ------- \n"
        flag = false
      }

      var dt = date.getTime() - today.getTime();
      var diff_day = dt / (1000 * 60 * 60 * 24) + 1;
      
      day_info = Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy年M月d日')
      text +=  day_info + "(" + Math.floor(diff_day) + ") \t" + item_name + "\n"
  }
  
  GmailApp.sendEmail("xxxxxx@gmail.com", "notify", text);  // 私のアドレス
  GmailApp.sendEmail("yyyyyy@gmail.com", "notify", text);  // 妻のアドレス
}

3.定期配信のためのスケジュール設定する

[スクリプトエディタ] → 時計マークの[現在のプロジェクトのトリガー]を選択
月曜の夜と金曜の夜に計画的な買い出しをするため、月曜と木曜に定期配信設定。
f:id:motojapan:20170919224700p:plain

結果

通知されたメールはこんな感じ(やったぜ)。
f:id:motojapan:20170919225936p:plain

雑感

  • GASはかなり簡単にかけた、必要な部分だけで済むので実装が楽
  • デバックやログも簡単に出力可能
  • 翌日だったが、apps-scripts-notifications@google.com からエラーレポートが届くので監視が楽 (下図参考)

f:id:motojapan:20170919225532p:plain


そのうち画像認識と文字認識を組み合わせて、賞味期限リストへの登録を半自動化したいと思う。
それ以外であっても業務効率化で使えそう。

では、はっぴー食べ物らいふを!(今晩はマグロの漬け丼になりました)

【行動認識 #9】t-SNEをtensorboard(Embedding Visualization)で可視化

今回はtensorboardのEmbedding Visualization対応コードを実装し、センサーデータ入力にt-SNEを可視化してみた。

センサー系の文脈ではこれの続き
【行動認識 #6】tensorflowでCAE(Convolutional Auto-Encoder)を実装してみた - MotoJapan's Tech-Memo

行動認識系全般ではこれの続き
【行動認識 #8】OpenPoseのBodyParts推定をTensorFlowでCPU実行する方法 (Linux : Ubuntu 16.04 LTS) - MotoJapan's Tech-Memo

目次

t-SNEとは?

  • t-distributed Stochastic Neighbor Embedding
  • Laurens van der Maaten & Geoffrey Hinton 提唱 [2008]
  • 高次元データに対する次元圧縮/可視化手法の1つ(PCA, ICA, etc)
  • KL情報量を目的関数とした、高次元から低次元へのデータのローカル距離の保つ非線形マッピングを行う
    • 各点間のユークリッド距離の近さを確率分布で表現
    • 分布の近さを測る方法であるKL情報量で、次元圧縮前後の分布の近さを測り、これを最小化する
    • PCAの場合、分散最大化を目的関数とした、高次元から低次元へのデータの線形構造を保つマッピングを行う(線形アルゴリズム
  • ハイパーパラメータ
    • 「perplexity」 (候補数, 複雑度)
      • 小さいほど局所的、大きいほど大域的な特性を考慮できる
      • 5 - 50 に収まるのが良いが、データ数より小さい方がよい
    • 「iteration」 (学習回数)
      • 同じデータセットであっても同じiterationで安定せず、安定するタイミングは不明
      • 構造の再現性を保証しない
    • 「epsilon」 (学習率)
  • 他の手法に比べて
    • 視覚的な図示が綺麗なことが多い (case-by-case)
    • 計算コストとメモリ消費が大きい
    • (PCAで前処理をしてからt-SNEを試行する高速化手法もある)
    • 次元圧縮も可能だが、2次元から2次元への変換さえ可能

詳細は他に譲りますが参考文献をみればよくわかる。

結果

学習データ 7344 [samples]に対してt-SNEを試行。(512次元を3次元に次元圧縮)
わかりやすく可視化するため、1 [sample]毎にmeta情報(index, labelなど)をEmbeddingした。
(慣れればtensorboardはすごく扱いやすい。)

Iterationによるクラスタリング遷移

・600 [iteration]程で大体収束
f:id:motojapan:20170831054135g:plain
見ていて面白い。

3D操作例

センサーから読み取れる行動は、中心から幾つもエッジが生える様な形状を有していた。
f:id:motojapan:20170831054155g:plain
すごく便利。
動画では途中でmetaデータのembeddingを実行してみたが、グラフィックが切り替わり、データ解析が捗る。

形状から読み取れることは

  • 中心部には異なる行動でも、似たような動きのサンプルが集まる
  • エッジに伸びるほど、同じ行動に特徴的なサンプルが集まる

中心部拡大

別々のクラスが密集しているところでわかることは、

  • WALKING_xxx 3つは隣接している
    • 「共通かつ繰り返し動作で根本的に似ていること」がt-SNEから表現
  • STANDING - WALKING_UPSTAIRSは隣接している
    • 体幹を上げるという点で似ていること」がt-SNEから表現
  • STANDING - SITTINGは隣接していない





メモ

tensorflowでの実装メモ

embeddingするmetaデータのフォーマット.tsvについて
1行目は、ヘッダー、2行目以降は、embedding情報。
なので.tsvの総行数は、[ヘッダー(1行)+データ数]となる
(詳細は本家)

今回は次の要領で書き込んだ。

[metadata.tsv]
'index \t activity \t subject \n'
'1 \t WALKING \t 15 \n'
'2 \t WALKING \t 15 \n'
'3 \t SITTING \t 15 \n'
...

python上での実装について
実装は次を参考(すごくわかりやすかったです)
TensorFlow 0.12 のEmbedding Visualizationを動かす - すぎゃーんメモ

学習処理後に下記を追記

embedding_var = tf.Variable(tf.stack([tf.squeeze(y) for y in <labels>], axis=0), trainable=False, name='fc2')

summary_writer = tf.summary.FileWriter(<dst_dir>)
config = projector.ProjectorConfig()
embedding = config.embeddings.add()
embedding.tensor_name = embedding_var.name #'fc2/Variable'
embedding.metadata_path = <meta_path>
projector.visualize_embeddings(summary_writer, config)

sess.run(tf.variables_initializer([embedding_var]))
saver = tf.train.Saver([embedding_var])
saver.save(sess, <store_model_path>)

Embedding Visualizationでできること

  • 2D/3Dへの次元圧縮
  • 次元圧縮の選択肢は、PCA, t-SNE, CUSTOM
  • t-SNEは perplexity, learning rateをGUIで操作
  • 任意点の近傍探索
  • meta情報(複数可能)をembeddingして、Coloring / Sort / Grepが簡単 (超絶便利)

【行動認識 #8】OpenPoseのBodyParts推定をTensorFlowでCPU実行する方法 (Linux : Ubuntu 16.04 LTS)

OpenPoseのBodyParts推定をTensorFlowでCPU実行する方法について。(想定環境はUbuntu 16.04 LTS)

どうしてもTensorFlowで動かしたい人はやってみてもいいかも。(そんな人いる?)
個人的にはTensorFlowのアーキテクチャの方が好きですが。

OpenPoseの大枠を知りたいなら前回を参考。
motojapan.hateblo.jp

アプローチ

OpenPoseのBodyParts推定モデルはCaffeで提供されるので、これをTensorflowモデルに変換すればできるはず。

想定環境

手順

1. OpenPoseのCaffeモデルをダウンロード

本家からportable OpenPose demo 1.0.1.をダウンロード
この中に pose_deploy_linevec.prototxt(モデル情報)、pose_iter_440000.caffemodel(重み情報)が格納されている。

2. Caffeモデル/重みをTensorFlow用に変換

ここ(caffe-tensorflow)のconvert.pyで変換できるらしい。
このスクリプトで、.prototxt→.py、.caffemodel→.npyに変換される。
例えば、model.py/model.npyに変換したとする。

変換方法、変換済みファイルを扱う基本実装の参考
テンソルフローでfinetuning | OpenBook

3. Tensorflowモデルを読み込んでヒートマップを推論

下記で読み込み。
重要なのは、placeholderのテンソル順序で次の通り。
[batch_size, image_height, iamge_width, color_channel]

import tensorflow as tf
# networkはmodel.pyを編集して各自定義(例えばBodyPartsNet)
from model import BodyPartsNet 
# 入力(画像)用テンソルを準備
# [batch_size, image_height, iamge_width, color_channel]
x = tf.placeholder(tf.float32, [None, H, W, 3])
# 出力(ヒートマップ)用テンソルを準備
pred = net.layers['Mconv7_stage6_L2']
#モデルの読み込み
net = BodyPartsNet({'iamge': x})
sess = tf.Session()
sess.run(tf.initialize_all_variables()) 
#重みの読み込み
net.load('model.npy', sess)

後は、forwardのsession.runをすれば動くはず。

BodyParts出力順は本家の通り以下の順。

    POSE_COCO_BODY_PARTS {
        {0,  "Nose"},
        {1,  "Neck"},
        {2,  "RShoulder"},
        {3,  "RElbow"},
        {4,  "RWrist"},
        {5,  "LShoulder"},
        {6,  "LElbow"},
        {7,  "LWrist"},
        {8,  "RHip"},
        {9,  "RKnee"},
        {10, "RAnkle"},
        {11, "LHip"},
        {12, "LKnee"},
        {13, "LAnkle"},
        {14, "REye"},
        {15, "LEye"},
        {16, "REar"},
        {17, "LEar"},
        {18, "Bkg"},
    }

caffe-tensorflowエラー対策

caffe-tensorflowは環境によって時折エラーがでるのでいくつか対策が必要。

エラーケース1:TypeError: Descriptors should not be created directly, but only retrieved from their parent.

convert.pyでの変換時のエラー

TypeError: Descriptors should not be created directly, but only retrieved from their parent.

対策:
protobufのバージョン変更。

pip install protobuf==3.0.0b2

エラーケース2:TypeError: concat() got an unexpected keyword argument 'concat_dim'

networkにconcatが含まれる場合の実行時エラー

TypeError: concat() got an unexpected keyword argument 'concat_dim'

対策:
concatの呼び出し記述が古いのが原因。
tensorflow 1.0.0に対応した記述にNetwork.pyを変更(下記のような感じ)

    @layer
    def concat(self, inputs, axis, name):
-        return tf.concat(concat_dim=axis, values=inputs, name=name)
+        return tf.concat(axis=axis, values=inputs, name=name)

おまけ

1.Caffeの環境を構築するのが億劫な人にはいいかも
Caffe環境構築も大変だし。

2.windowsでは簡単には動かないと思う
理由は、tensorflowがwindows x python 2.7系に対応していない。
そして、convert.pyはpython 2.7系。

最近はiOS stand-aloneでも動かしたりしていて、320x320の画像は5秒くらいで認識できた。
需要があれば方法だけでも纏めようかなー。

以上。

【行動認識 #7】OpenPoseのBodyParts推定をCPU実行してみた雑感

今回は、今年のCVPR2017でも報告されている何かと話題のOpenPoseの内、人体部位-BodyParts-推定をCPUで動かしてみた。

下記のセンサー入力周りとは文脈が変わるが、画像から行動を特定するタスクもあるという点では関連性があるテーマなので今回取り扱う。
【行動認識 #6】tensorflowでCAE(Convolutional Auto-Encoder)を実装してみた - MotoJapan's Tech-Memo

もし、Ubuntu 16.04 x tensorflow x CPUで動かしたい人がいれば次回#8(17/08/29)の記事が参考になるかも。

そもそも、OpenPoseの全体像は、BodyParts推定(18点)からボトムアップ手法で複数人2次元ポーズ推定を実現している模様。
そのうち、BodyParts推定は深層学習で行われており、BodyParts推定だけでも動かして、どんなものなのか把握してみたい。

OpenPoseのCVPR2017 paperを知りたい人は下記。(面白かったです)
[1611.08050] Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields

ライセンス上の注意ですが、「非商用研究目的での使用ということ」、「派生物や改変は可能であるが、派生物や改変は非営利の内部的研究目的でのみ使用すること」に気を付けてください。
商用で利用したい場合、別途契約が必要です。
OpenPose - Realtime Multiperson 2D Keypoint Detection from Video | Flintbox

参考記事

学習済みモデル

最近、本家からportable OpenPose demo 1.0.1.がリリースされている。
例えば、この中にある pose_deploy_linevec.prototxt(モデル情報)、pose_iter_440000.caffemodel(重み情報)が格納されているので、これを駆使するとForward処理ができたりする。
細かい環境、手順はまた今度、纏めようと思う。

一応実行環境としては、

で動かしてみた。

動かしてみて

結構いろいろできそうで面白い。

【画像上に人物が1人の場合】

(左画像)入力画像
(中央画像)ある程度確率の高い結果の上位3座標を取り出してみた
(右画像)適当に関節をつないでみるとポーズ推定っぽい感じになる

f:id:motojapan:20170820213829p:plain

詳細の出力は以下。
関節データの出力は、各関節の座標ではなくピクセル毎のヒートマップとなる。
ポイントは、BodyParts18点+backgroundの19枚のヒートマップが出力されるという点。

雑感ですが、

  • 期待する人物付近で期待通り反応する
  • 期待箇所付近だけでなく、"RHip"、"RKnee"、RAnkle"、"LHip"、LKnee" (id: 8~12) は木の枝の複雑なクロス部分にも微弱に反応している
  • OpenPoseは論文を読むとわかるが、ボトムアップ手法なので深層学習で積極的に人を認識しているようではなさそうという印象







































画像は左上から次の順でBodyParts出力(本家記載)

    POSE_COCO_BODY_PARTS {
        {0,  "Nose"},
        {1,  "Neck"},
        {2,  "RShoulder"},
        {3,  "RElbow"},
        {4,  "RWrist"},
        {5,  "LShoulder"},
        {6,  "LElbow"},
        {7,  "LWrist"},
        {8,  "RHip"},
        {9,  "RKnee"},
        {10, "RAnkle"},
        {11, "LHip"},
        {12, "LKnee"},
        {13, "LAnkle"},
        {14, "REye"},
        {15, "LEye"},
        {16, "REar"},
        {17, "LEar"},
        {18, "Bkg"},
    }

【画像上に人物が複数人の場合】

(左画像)入力画像
(右画像)ある程度確率の高い結果をプロット
f:id:motojapan:20170820222253p:plain

出力ヒートマップは例えば、LShoulder、hip、backgroundはこんな感じ。

雑感ですが、

  • LShoulderは、3人が写る画像上で、特徴的に3箇所強い反応
  • hipは、斜め後ろ向きの女性において、少し弱い反応(他の2人に比べて)
  • 男性2人の輪郭がぼやけていても認識が割とできている(すごい)
  • 同一部位に対して複数検出するヒートマップを、ボトムアップに上手く複数人ポーズ推定ができるのがOpenPoseの強みということかな(OpenPoseすごい)







スループット

画像サイズにも依るが、320 x 240 で 2秒、640 x 480 で 10秒程度だった。
あと同じ画像サイズでも人数が増えると、スループットが変化した。(たまたまか?)
iOS stand-aloneでは機種にも依るが、320 x 320であれば5秒程度。

CPUでもいろいろ出来そうだなぁという印象。

おわりに

近々、手元のノートPC windows 8/10 でも動かしてみたな。
本家では、windowsでも実行できるバイナリも提供され始めたので、おもしろそう。

根気強く頑張れば、Tensorflowでも動くしね。

以上。

【opencv 基礎知識 #5】透視投影変換行列に関連する関数まとめ (getPerspectiveTransform, calibrateCamera, warpPerspective, perspectiveTransform, undistort, remap)

忘れやすい透視投影変換行列周りの関数をメモ。(python)

具体的に前回の投稿でも使っている。
motojapan.hateblo.jp

1. 透視投影変換行列を求める方法

1.1. 台形補正の場合 [getPerspectiveTransform]

#pts_src 変換前座標 ※台形補正であれば4点
#pts_dst 変換先座標 ※台形補正であれば4点
M = cv2.getPerspectiveTransform(pts_src, pts_dst)

1.2. レンズ歪み補正の場合 [calibrateCamera]

# object_points  チェスボードのパターンの点群
# image_points   チェスボードのコーナーの点群
rms, camera_M, dist_coef, r, t = cv2.calibrateCamera(object_points,image_points,(img.shape[1],img.shape[0]),None,None)

2. 透視投影変換行列による変換方法

2.1. 画像を変換する [warpPerspective]

#img       変換前画像
#img_trans 変換後画像
img_trans = cv2.warpPerspective(img, M, (w, h))

2.2. 座標を変換する [perspectiveTransform]

#pts       変換前座標
#pts_trans 変換後座標
pts_trains = cv2.perspectiveTransform(pts, M)

2.3. 画像を変換する1[undistort](レンズ歪み補正)

img_undistort = cv2.undistort(img, camera_M, dist_coef)

2.4. 画像を変換する2[remap](レンズ歪み補正)

cam_M_ = cv2.getOptimalNewCameraMatrix(camera_M, dist_coef, (img.shape[1], img.shape[0]), 1)[0]
mapx, mapy = cv2.initUndistortRectifyMap(camera_M, dist_coef, np.eye(3), cam_M_, (img.shape[1], img.shape[0]), cv2.cv.CV_32FC1)
img_undistort = cv2.remap(img, mapx, mapy, cv2.cv.CV_INTER_AREA)

おまけ

逆行列について

透視投影変換行列Mの逆行列は、np.linalg.inv(M)
これでimage <-> iamge_trans間を行き来できる

レンズ歪み補正について

  • 歪み種類
    • 放射状歪み
    • 接線歪み
      • この2つはdist_coefで補正する

  • 補正に必要なパラメータ
    • 上記dist_coef
    • カメラの内部パラメータ
      • カメラ固有の焦点距離(fx, fy)や光学中心(cx, cy)
      • camera_Mで補正する
    • カメラの外部パラメータ
      • ある座標系における3次元点の座標を別の座標系での座標に変換するための回転と並進のパラメータ
  • 補正に必要な画像数
    • チェスボードが写った画像10枚程度

camera_M、dist_coefの定義

camera\_M = \begin{pmatrix} f_x&0&c_x \\ 0&f_y&c_y \\ 0&0&1 \end{pmatrix}
dist\_coef = \begin{pmatrix} k_1&k_2&p_1&p_2&p_3  \end{pmatrix}

詳細はここ
カメラ・キャリブレーション — OpenCV-Python Tutorials

具体的な camera_M、dist_coefの中身の例

 In [1]: camera_M
 Out[1]:
 array([[ 452.30622078,    0.        ,  332.44877655],
        [   0.        ,  453.36588503,  267.73560929],
        [   0.        ,    0.        ,    1.        ]])

 In [2]: dist_coef
 Out[2]: array([-0.19224447,  0.07401813,  0.0044831 ,  0.00214658, -0.02052423])

以上。

【opencv 基礎知識 #4】動画の手ぶれ補正をpython実装 (AKAZE, KNN, RANSAC)

行動認識が多かったので、半日くらいで動画の手ぶれ補正を作ってみた。

実装は数多あるので、そのうちコードをリファクタリングしたらGithubに載せようかと思う。
(すぐほしい人がいたら、コメントください)

すぐ忘れることをメモ。

結果

動画の通り、チューニングしなくても結構いい感じになっている。

上が補正前/下が補正後。
www.youtube.com

今年GWに山登りした時に撮った動画ですが、手ぶれを気にせず雫が落ちていくのが見れます。(最高です)
これもっと精度良くして無限ループすると、湯水のように時間が費やせそうです。

アルゴリズム

これを実装すれば基本的には動く。

  1. 動画を読み込み、フレームを読み出す
  2. 特徴量抽出する(AKAZE)
  3. キーポイントマッチングする(Brute-Force, KNN)
  4. 透視投影変換行列を求める(RANSAC)
  5. 画像を回転する

メモは以下。

1. 動画を読み込み、フレームを読み出す
cap = cv2.VideoCapture(mov_path)
ret, frame1 = cap.read()
2. 特徴量抽出する(AKAZE)

今回は権利が緩やかなAKAZEを用いる。

  • その他の選択肢
    • SHIFT, ORB, FAST, BRISK, KAZE, etc...
detector = cv2.AKAZE_create()
gray1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
keypoints1, descriptors1 = detector.detectAndCompute(gray1, None)
#frame2, keypoints2, descriptors2も同様に生成

frame1をquery画像、frame2をtrain画像とする。
query、trainの説明は、次に記載。

keypointsはkeypoint型である。
詳細は下記が分かりやすい。
Common Interfaces of Feature Detectors — OpenCV 2.4.13.3 documentation

3. キーポイントマッチングする(Brute-Force, KNN)

ここでハマったのは、queryとtrainの概念
ウェブの記事を見てもなかなか説明無かったが、一般的にこういうことらしい。

説明
query 探したいターゲット画像 犬のクロップ画像
train queryを探し出したいシーン画像 犬を散歩している風景画像

今回はqueryを基準フレーム(初期フレームなど)、trainは毎フレームとしてみた。
ここからBrute-Force(総当たり)で最近傍の特徴量を検索し、キーポイントマッチングする。

  • その他の選択肢
    • FLANN: Fast Library for Approximate Nearest Neighbors (高速近似近傍探索法)
#Brute-Force
bf = cv2.BFMatcher()
#matchesの要素数は、queryDescriptorsの要素数に一致
#上位k個の特徴点を返す
matches = bf.knnMatch(queryDescriptors = descriptors1, trainDescriptors = descriptors2, k=2)

matchesはDMatch型オブジェクトのリスト。
DMatch型の詳細は下記が分かりやすい。
特徴点のマッチング — OpenCV-Python Tutorials 1 documentation

(箸休め)

どんなマッチングをしているかは、下記コードでvisualizeして確認できる。

#good_matches = matchesから選び出したもの
frame3 = cv2.drawMatchesKnn(frame1, keypoints1, frame2, keypoints2, good_matches, None, flags=2)

たとえばこんなの(左画像がquery、右画像がtrain)
f:id:motojapan:20170816235844p:plain

4. 透視投影変換行列を求める(RANSAC)

マッチングが分かったので次のどういう一致になるか具体的に透視投影変換行列 Mを求める。
Mが分かればそれでtrain画像を回したり、query画像を回せば、互いに一致する画像が得られる。

具体的には、cv2.findHomographyで行列を得られるが、外れ値をいい感じに無視して変数推定をするRANSACを用いる。

#src_points/dst_pointsは、good_matchesからindexでアクセスしたquery/trainのkeypoint座標である
#(求めたいMの順にsrc_points/dst_pointsの入力順を決める)
M, mask = cv2.findHomography(src_points, dst_points, cv2.RANSAC, 5.0)
5. 画像を回転する

上記Mは、query画像座標系→train画像座標系とする行列である。
手ぶれ補正をするなら、Mの逆行列が必要であるので、linalg.inv(M)で得ると楽。

frame2_trans = cv2.warpPerspective(frame2, np.linalg.inv(M), size)
#frame1_trans = cv2.warpPerspective(frame1, M, size)

これで、frame2_transは、「train画像座標系がquery画像座標系にいい感じでキーポイントに一致する形で透視投影された画像」となる。


あとは各フレームごりごりやればうまくいく。

めでたしめでたし。