#1行概要
カメラ映像中のギターを、構造特徴に基づきHough変換でトラッキングし、回転矩形を切り出しと時系列移動平均でクリッピングする。
#はじめに
「HoloLensでOpenCVしようとしたら苦戦した話」の延長線で、カメラ映像に映ったギターのネック(竿にあたる箇所で演奏者が指で押さえて音程を操作する部分)だけを切り出して表示したいと思った。
エレキギター、アコースティックギター、クラシックギター、さらにはベースギター、これらは共通してネック端・弦という直線領域を多く含む。この構造特徴を利用することで、画像処理的にギターのトラッキングを実現する。
#処理の流れ
カメラ映像の各フレーム行列に対して次のような操作を行う。
###1. グレースケール化
フレーム行列をcv::cvtColor
でグレースケール化する。
###2. エッジ検出
グレースケール化した行列のエッジをCanny
で検出する。
###3. Hough変換
前処理で作成した行列に対しcv::HoughLinesP
で確率的Hough変換を行う。
このとき、第5パラメータthreshold、第6パラメータminLineLength、第7パラメータmaxLineGapを調整することにより直線の採用度を決定する。
threshold – 投票の閾値パラメータ.十分な票( >\texttt{threshold} )を得た直線のみが出力されます.
minLineLength – 最小の線分長.これより短い線分は棄却されます.
maxLineGap – 2点が同一線分上にあると見なす場合に許容される最大距離.
特徴検出 - opencv 2.2 documentationより引用。
###4. 中央値の算出
前述の通りギターには共通して複数の直線領域があり、ギターのほかにもフレーム行列中に直線が映り込むことが予想される。そんな中、本稿ではギターのネックとして想定する代表値に複数直線の中の中央値を採用する。
以下画像中、赤・緑線がHough変換により検出した直線であり、うち緑線が今回採用する中央値である。中央値を代表値としたことで、机の縁のようなギターのネックではない直線領域(外れ値)を除外できていることが分かる。
なお、6. 回転矩形切り出しでは、画像中の緑線を中心とする青四角をクリッピング領域の候補としている。
###5. 時系列移動平均
当然ではあるが、算出した中央値の生データをもとに毎フレーム回転矩形切り出しを行うのでは、切り出した映像が目まぐるしく変化する可能性がある。それを避けるため、つまりはヌルっと映像が動くように中央値を記録しておき、その平均値を切り出しのための座標値として採用する。
###6. 回転矩形切り出し
時系列移動平均をもとに割り出した座標値をもとにcv::RotatedRect
で回転矩形切り出しを行う。
これで、カメラ映像に映ったギターのネックの切り出し表示が実現できた。
#コード例
// 各フレーム行列
cv::Mat src;
// Hough変換入力行列
cv::Mat hough_src;
// Hough変換で検出された直線配列
std::vector<cv::Vec4i> lines;
// Hough変換で直線を検出した回数
int l_counter = 0;
// 移動平均範囲
int l_max = 10;
// 移動平均検出直線
int lines_[10][4];
// グレースケール化
cv::cvtColor(src, hough_src, CV_BGR2GRAY);
// エッジ検出
Canny(hough_src, hough_src, 100, 200, 3);
// Hough変換
cv::HoughLinesP(hough_src, lines, 1, CV_PI / 200, 50, 400, 20);
/*
* 検出した線分の描画【テスト用】
* cv::Vec4i l;
* std::vector<cv::Vec4i>::iterator it = lines.begin();
* for (; it != lines.end(); ++it)
* {
* l = *it;
* cv::line(dst, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), cv::Scalar(0, 0, 255), 2, CV_AA);
* }
*/
// 直線を1本以上検出
if (!lines.empty())
{
// 古い直線情報の削除
if (l_counter >= l_max)
{
for (int i = 0; i < 4; i++)
{
sum[i] -= lines_[l_counter % l_max][i];
}
}
// 直線を3本以上検出
if (lines.size() > 2)
{
// 中央値までのみ選択ソート
for (int i = 0; i < ((lines.size() - 1) / 2); i++)
{
for (int j = i + 1; j < lines.size(); j++)
{
if (lines[i][1] > lines[j][1])
{
for (int k = 0; k < 4; k++)
{
std::swap(lines[i][k], lines[j][k]);
}
}
}
}
// 中央値を代表値として採用
for (int i = 0; i < 4; i++)
{
lines_[l_counter % l_max][i] = lines[(lines.size() - 1) / 2][i];
}
}
else
{
// 先頭要素を代表値として採用
for (int i = 0; i < 4; i++)
{
lines_[l_counter % l_max][i] = lines[0][i];
}
}
// 新しい直線情報の追加
for (int i = 0; i < 4; i++)
{
sum[i] += lines_[l_counter % l_max][i];
}
l_counter++;
if (l_counter <= l_max)
{
for (int i = 0; i < 4; i++)
{
ave[i] = sum[i] / l_counter;
}
}
else
{
for (int i = 0; i < 4; i++)
{
ave[i] = sum[i] / l_max;
}
}
/*
* 中央線(緑)の描画【テスト用】
* cv::line(dst, cv::Point(ave[0], ave[1]), cv::Point(ave[2], ave[3]), cv::Scalar(0, 255, 0), 2, CV_AA);
*/
}
// 直線を1回以上検出
if(l_counter != 0)
{
// 座標間情報を算出
int width = ave[2] - ave[0];
int height = ave[3] - ave[1];
double theta = (std::atan2(height, width)) * (180 / CV_PI);
cv::RotatedRect rect;
if (ave[1] < ave[3]) // theta >= 0
{
rect = cv::RotatedRect(cv::Point(ave[0] + (width / 2),
ave[1] + (std::abs(height) / 2)),
cv::Size(std::sqrt(width * width + height * height),
std::sqrt(width * width + height * height) * 0.2),
(float)theta);
}
else // theta < 0
{
rect = cv::RotatedRect(cv::Point(ave[0] + (width / 2),
ave[1] - (std::abs(height) / 2)),
cv::Size(std::sqrt(width * width + height * height),
std::sqrt(width * width + height * height) * 0.2),
(float)theta);
}
float angle = rect.angle;
cv::Size rect_size = rect.size;
if (rect.angle < -45.0)
{
angle += 90.0;
std::swap(rect_size.width, rect_size.height);
}
// 回転矩形の角度から回転行列を算出
cv::Mat M = cv::getRotationMatrix2D(rect.center, angle, 1.0);
// 画像を回転
cv::warpAffine(src, src, M, rgb.size(), cv::INTER_CUBIC);
// 回転した画像から回転矩形切り出し
cv::getRectSubPix(src, rect_size, rect.center, dst);
/*
* 回転矩形領域(青)の描画【テスト用】
* cv::Point2f vertices2f[4];
* rect.points(vertices2f);
* std::vector<cv::Point> vertices;
* for (int i = 0; i < 4; i++)
* {
* vertices.push_back(vertices2f[i]);
* }
* const cv::Point* pts = (const cv::Point*) cv::Mat(vertices).data;
* int npts = cv::Mat(vertices).rows;
* cv::polylines(dst, &pts, &npts, 1, true, cv::Scalar(255, 0, 0), 3);
*/
}
#おわりに
今回はカメラ映像に映ったギターをトラッキングし、ギターのネックをクリッピングする一連の処理の例を提示した。
本来、ギターの構造特徴に着目するのであれば、フレット(ネック上に打ち込まれた金属棒で等比間隔で並んでいる)やポジションマーク(フレット番号を明示化するためネックに埋め込まれている印)の情報も画像処理的に拾うのが理想であると考える。本稿では、開発しようとしているシステムが要件的にそこまで詳細なギターのポジション情報を必要としておらず、机の縁やブラインドといった直線領域が多く存在する周辺環境でも想定通りの動作をすることから本手法での実装となっている。また、フレットレスギターやポジションマークがないギターも少数ながら存在することから、汎用性の観点でも本手法は有用と考える。