右手首の関節を特徴点検出してみた
はじめに
この記事は富士通システムズウェブテクノロジー Advent Calendar 2019 22日目の投稿です。
記事の内容は全て個人の見解であり、執筆内容は執筆者自身の責任です。所属する組織は関係ありません。
(おやくそく)
概要
私は趣味でヴァイオリン演奏を嗜んでいますが、ヴァイオリンを弾くは大変なんです。
演奏すること自体が困難な事は、しずかちゃんのおかげもあって広く知られていますが、演奏以外にも大変なことがあります。
ヴァイオリンを弾くとき、曲を弾く前に決まってやることがあります。
それはボーイングといって、上げ弓で弾くのか、下げ弓で弾くのかを楽譜をみながら1つ1つ決めていく作業です。
- V → アップボウ(下から上)
- П → ダウンボウ(上から下)
曲のフレーズ感や、弾きやすさなど考慮して、ボーイングを楽譜に書き込んでいくのですが、この作業が結構骨が折れます。ボーイングには、唯一の正解はなく、奏者の感性が現れるものでもあります。また、カルテットやオーケストラで弾く場合、ヴィオラやチェロなど他の楽器とも所々合わせる必要が出てきます。
ボーイング自動付け機の開発は、弦楽器奏者の夢といっても過言ではないでしょう。
そこで、ボーイング自動付け機までいかずとも、他の人の演奏動画からボーイングを読み取り、それを参考にすることで手間を軽減できないか?
と考えました。
作戦
作戦はこうです。
- ポーズ推定可能なオープンソースライブラリを活用し、演奏動画から弓を持っている右手首の座標を刻々と記録する。
- 右手首の座標の軌跡を楽譜に重ね合わせる。
これで、演奏動画のボーイングがバッチリ分かるはず!
利用したライブラリは、OpenPoseをTensorflow向けに実装したオープンソースのtf-pose-estimationです。
OpenPoseとは?
OpenPoseは、コンピュータビジョンに関する国際学会CVPR2017でCMU(カーネギーメロン大学)が発表した、keypoint(特徴点)の検出とkeypoint同士の関係の推定を行う技術です。
OpenPoseを使うと、以下のように関節の位置など、人の体における特徴点がどの座標にあるかが分かります。
https://github.com/CMU-Perceptual-Computing-Lab/openpose
tf-pose-estimation とは?
tf-pose-estimationは、OpenPoseと同じニューラルネットワークをTensorflow向けに実装したものです。
今回これを利用しようと考えたのは、errno-mmd様のtf-pose-estimation拡張が便利そうだったからです。
ポーズ推定した二次元の関節位置情報をJSONフォーマットでファイルに出力するオプションを加えていただいており、これを利用したいと考えました。
解析する演奏動画
フリーで利用可能なヴァイオリンの演奏動画に心当たりがなかったため、自分の演奏を録画したものを解析に利用しました。演奏した曲は、モーツァルトのアイネクライネナハトムジーク1楽章の冒頭10小節です。メトロノームを使い一定のテンポで弾きました。
右手首の関節点のみをマーキングするように微修正
tf-pose-estimationはデフォルトだと、目、肩、肘、手首、足首など様々な特徴点をマーキングし、それらを線で繋いで表示します。今回は、右手首のみをマーキングするように少しコードを修正しました。
440 # 右手首のみ円の画像を描画
441 if i == CocoPart.RWrist.value:
442 cv2.circle(npimg, center, 8, common.CocoColors[i], thickness=3, lineType=8, shift=0)
449 # 特徴点を繋ぐ線描画を無効化
450 # cv2.line(npimg, centers[pair[0]], centers[pair[1]], common.CocoColors[pair_order], 3)
ポーズ推定の実行
実行環境はGoogle Colaboratoryを利用しました。
https://github.com/errno-mmd/tf-pose-estimation
のRead.mdに従い、セットアップのコマンドを実行していった後に、下記コマンドを実行します。
%run -i run_video.py --video "/content/drive/My Drive/violin_playing/EineKleineNachtmusik_20191226.mp4" --model mobilenet_v2_large --write_json "/content/drive/My Drive/violin_playing/json" --no_display --number_people_max 1 --write_video "/content/drive/My Drive/violin_playing/EineKleine_keypoints_20191226.mp4"
事前にGoogleDriveにアップロードしていた演奏動画をインプットにして、右手首の関節点を描画した動画と、1フレーム毎の関節点の座標が書かれたJSONファイルが出力されます。
出力された動画ファイル
出力動画を数秒間間隔切り出してgifにしたものがこちらです。
これを見る限り、それなりの精度で右手首の関節点をトレースしてくれているように見えます。
出力されたJSONファイル
特徴点の座標が記載されたJSONファイルは、フレーム毎(1/60秒)に出力されます。
10フレーム目のJSONファイルがこちらです。
{
"version": 1.2,
"people": [
{
"pose_keypoints_2d": [
265.5925925925926,
113.04347826086956,
0.7988795638084412,
244.55555555555557,
147.82608695652175,
0.762155294418335,
197.22222222222223,
149.56521739130434,
0.6929810643196106,
165.66666666666669,
189.56521739130434,
0.7044630646705627,
220.88888888888889,
166.95652173913044,
0.690696656703949,
289.2592592592593,
146.08695652173913,
0.5453883409500122,
299.77777777777777,
212.17391304347825,
0.6319900751113892,
339.22222222222223,
177.3913043478261,
0.6045356392860413,
213,
253.91304347826087,
0.23064623773097992,
0,
0,
0,
0,
0,
0,
268.22222222222223,
276.52173913043475,
0.2685505151748657,
0,
0,
0,
0,
0,
0,
257.7037037037037,
106.08695652173913,
0.8110038042068481,
270.85185185185185,
107.82608695652173,
0.7383710741996765,
231.4074074074074,
107.82608695652173,
0.7740614414215088,
0,
0,
0
]
}
],
"face_keypoints_2d": [],
"hand_left_keypoints_2d": [],
"hand_right_keypoints_2d": [],
"pose_keypoints_3d": [],
"face_keypoints_3d": [],
"hand_left_keypoints_3d": [],
"hand_right_keypoints_3d": []
}
コードを読むと、pose_keypoints_2d の 13
番目(0から振って)の要素が右手首のy座標の値のようです。上記の10フレーム目の例だと、166.95652173913044
になります。
右手首のy座標をグラフ表示
matplotlibでグラフ化をしてみたいと思います。
まずは、フレーム毎に出力されたJSONファイルから目的となる値を収集していきます。
import json
import pprint
import os
import glob
import numpy as np
files = glob.glob('/content/drive/My Drive/violin_playing/json/*')
x = list(range(len(files)))
y = []
for file in files:
with open(file) as f:
df = json.load(f)
y.append(df['people'][0]['pose_keypoints_2d'][13])
変数xにフレーム数、変数yに右手首の関節点y座標の値を格納しています。
続いて、matplotlibでグラフ化します。
x座標のメモリは、30フレーム間隔にしました。
というのも、アイネクを4部音符=120のテンポで弾いたので、30フレームはちょうど4分音符1拍分に相当することになり、楽譜との対応関係が見やすくなるからです。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
fig = plt.figure(figsize=(120, 10))
ax = fig.add_subplot(1, 1, 1)
ax.xaxis.set_major_locator(ticker.MultipleLocator(30))
ax.plot(x, y, "red", linestyle='solid')
出力されたグラフはこちらです。
重ね合わせ
楽譜と重ね合わせた結果がこちらです。重ね合わせは手動で行いました。
ダウンボウのときは、下に向かい、アップボウのときは、上に向かっているはずですが、、、う~ん、、、大体合っているかな (^_^;)
という感じです。
まとめ
感覚として、まだまだこのままでは実際に活用のレベルには達しないというのが正直なところです。
この手法で、16分音符など、細かいパッセージのボーイングを特定するのは厳しそうです。
もっとゆったりとした曲であれば、ある程度参考になるかもしれません。
ただ、譜面との突き合わせという点で以下の課題が残ります。
- 楽譜上、1小節の横幅の長さが揃っていない
- 同様に拍が均等の間隔になっていない
- 曲中にテンポが変わる(Adagio → Allegro など)
- テンポ表記は同じでもフレーズに合わせて微妙にテンポが揺れる
譜面との突き合わせは自動で行えないと、ボーイングをつける作業の効率化にならないため、上記課題は克服しないといけません。
音程を認識し、譜面とマッピングするのも1つの解決方法かも知れません。難易度は高そうですが(^_^;)
今回、ボーイングをつける作業の効率化に着目してみましたが、熟練者と初心者の弓の扱い方の違いを可視化し、そこから気づきを得るというアプローチも面白そうです。
この動画からポーズ推定を行う技術は、いろいろな応用の可能性がある素晴らしい技術だと感じました。