cv::calcOpticalFlowPyrLK の使い方【OpenCV/C++】〜LK法でオプティカルフローを追跡する〜
cv::calcOpticalFlowPyrLK は、Lucas-Kanade 法(ピラミッド拡張版)で特徴点を フレーム間追跡するオプティカルフロー関数です。cv::goodFeaturesToTrack で検出した特徴点をカメラ映像の次フレームに引き継ぎ、各点の移動量を取得できます。
最小の呼び出し形は次のとおりです。
cv::calcOpticalFlowPyrLK(prevGray, currGray, prevPts, currPts, status, err);
動作環境
- OpenCV 4.x
- コンパイル:
g++ -std=c++17 main.cpp $(pkg-config --cflags --libs opencv4) -o main
環境構築がまだの方は環境構築ガイドを先にどうぞ。
基本の使い方
以下は、カメラ映像から Shi-Tomasi 特徴点を検出し、フレーム間で追跡して軌跡を描画する完全プログラムです。
#include <opencv2/opencv.hpp>
#include <vector>
#include <iostream>
int main()
{
cv::VideoCapture cap(0);
if (!cap.isOpened()) {
std::cerr << "カメラを開けませんでした" << std::endl;
return 1;
}
cv::Mat prevFrame, prevGray;
cap >> prevFrame;
if (prevFrame.empty()) {
std::cerr << "フレームの取得に失敗しました" << std::endl;
return 1;
}
cv::cvtColor(prevFrame, prevGray, cv::COLOR_BGR2GRAY);
// Shi-Tomasi 法で追跡開始点を検出
std::vector<cv::Point2f> prevPts;
cv::goodFeaturesToTrack(prevGray, prevPts, 200, 0.01, 10);
// 軌跡描画用のマスク(黒背景)
cv::Mat mask = cv::Mat::zeros(prevFrame.size(), prevFrame.type());
cv::TermCriteria termCrit(
cv::TermCriteria::COUNT | cv::TermCriteria::EPS, 30, 0.01);
while (true) {
cv::Mat currFrame, currGray;
cap >> currFrame;
if (currFrame.empty()) break;
cv::cvtColor(currFrame, currGray, cv::COLOR_BGR2GRAY);
if (prevPts.empty()) {
// 追跡点がなくなったら再検出
cv::goodFeaturesToTrack(prevGray, prevPts, 200, 0.01, 10);
mask = cv::Mat::zeros(prevFrame.size(), prevFrame.type());
}
std::vector<cv::Point2f> currPts;
std::vector<uchar> status; // 追跡成否フラグ(1:成功, 0:失敗)
std::vector<float> err; // 各点の追跡誤差
// LK オプティカルフロー計算
cv::calcOpticalFlowPyrLK(
prevGray, currGray,
prevPts, currPts,
status, err,
cv::Size(21, 21), // 探索ウィンドウサイズ
3, // ピラミッド最大レベル
termCrit
);
// 追跡成功点のみ残す
std::vector<cv::Point2f> goodPrev, goodCurr;
for (size_t i = 0; i < status.size(); i++) {
if (status[i]) {
goodPrev.push_back(prevPts[i]);
goodCurr.push_back(currPts[i]);
// 軌跡を線で描画
cv::line(mask, prevPts[i], currPts[i],
cv::Scalar(0, 255, 0), 2);
// 現在位置を丸で描画
cv::circle(currFrame, currPts[i], 4,
cv::Scalar(0, 0, 255), -1);
}
}
cv::Mat display;
cv::add(currFrame, mask, display);
cv::imshow("Optical Flow (LK)", display);
if (cv::waitKey(30) == 'q') break;
prevGray = currGray.clone();
prevPts = goodCurr;
}
return 0;
}
実行すると、カメラウィンドウに緑の軌跡と赤い追跡点が表示されます。動く物体にカメラを向けると、各特徴点が次フレームへ引き継がれ流線が伸びていく様子が確認できます。q キーで終了します。
主要行の解説
| 箇所 | 説明 |
|---|---|
goodFeaturesToTrack |
Shi-Tomasi 法で追跡の開始点を最大 200 点検出 |
calcOpticalFlowPyrLK の第5引数 status |
要素数 = prevPts のサイズ。1 なら追跡成功、0 なら失敗 |
cv::Size(21, 21) |
探索ウィンドウ。大きいと動きの大きな物体に対応できるが重くなる |
ピラミッドレベル 3 |
粗い解像度から段階的に探索。大きい動きに有効 |
termCrit |
反復の打ち切り条件(最大反復数 or 収束閾値) |
引数と戻り値
void cv::calcOpticalFlowPyrLK(
InputArray prevImg, // 前フレーム(グレースケール推奨)
InputArray nextImg, // 現フレーム(同上)
InputArray prevPts, // 追跡開始点群 (vector<Point2f>)
InputOutputArray nextPts, // 追跡後の点群(出力)
OutputArray status, // 追跡成否フラグ (vector<uchar>)
OutputArray err, // 追跡誤差 (vector<float>)
Size winSize = Size(21,21), // 探索ウィンドウ
int maxLevel = 3, // ピラミッド最大レベル
TermCriteria criteria = TermCriteria(...), // 収束条件
int flags = 0, // 動作フラグ
double minEigThreshold = 1e-4 // 最小固有値閾値
)
| 引数 | 型 | 説明 |
|---|---|---|
prevImg |
InputArray |
前フレーム画像(8bit/16bit/32fのグレースケール) |
nextImg |
InputArray |
現フレーム画像(同型) |
prevPts |
InputArray |
前フレームの追跡点群 |
nextPts |
InputOutputArray |
現フレームの追跡点群(出力)。初期値を与えると OPTFLOW_USE_INITIAL_FLOW フラグと組み合わせて使える |
status |
OutputArray |
追跡成否 (uchar)。1 = 成功 |
err |
OutputArray |
追跡誤差 (float) |
winSize |
Size |
各ピラミッドレベルの探索ウィンドウサイズ |
maxLevel |
int |
0 = ピラミッドなし。大きいほど大きな動きに対応 |
criteria |
TermCriteria |
反復打ち切り条件 |
flags |
int |
OPTFLOW_USE_INITIAL_FLOW / OPTFLOW_LK_GET_MIN_EIGENVALS |
minEigThreshold |
double |
低テクスチャ点の除外閾値。大きくすると追跡点が減る |
戻り値はありません(void)。結果は nextPts / status / err に格納されます。
実践例
追跡点が減ったら自動再検出(実運用パターン)
実際の映像処理では、追跡点がフレームアウトや遮蔽で減っていきます。閾値を設けて自動補充するのが定石です。
#include <opencv2/opencv.hpp>
#include <vector>
#include <iostream>
int main()
{
cv::VideoCapture cap(0);
if (!cap.isOpened()) {
std::cerr << "カメラを開けませんでした" << std::endl;
return 1;
}
const int MAX_CORNERS = 200;
const int MIN_CORNERS = 20; // この数を下回ったら再検出
cv::Mat prevGray;
cv::Mat frame;
cap >> frame;
if (frame.empty()) return 1;
cv::cvtColor(frame, prevGray, cv::COLOR_BGR2GRAY);
std::vector<cv::Point2f> pts;
cv::goodFeaturesToTrack(prevGray, pts, MAX_CORNERS, 0.01, 10);
cv::TermCriteria termCrit(
cv::TermCriteria::COUNT | cv::TermCriteria::EPS, 30, 0.01);
while (true) {
cap >> frame;
if (frame.empty()) break;
cv::Mat currGray;
cv::cvtColor(frame, currGray, cv::COLOR_BGR2GRAY);
std::vector<cv::Point2f> nextPts;
std::vector<uchar> status;
std::vector<float> err;
cv::calcOpticalFlowPyrLK(
prevGray, currGray, pts, nextPts, status, err,
cv::Size(21, 21), 3, termCrit);
// 成功点だけ残す
std::vector<cv::Point2f> survived;
for (size_t i = 0; i < status.size(); i++) {
if (status[i]) {
survived.push_back(nextPts[i]);
cv::circle(frame, nextPts[i], 4, cv::Scalar(0, 255, 0), -1);
}
}
// 追跡点が MIN_CORNERS を下回ったら補充
if (static_cast<int>(survived.size()) < MIN_CORNERS) {
cv::goodFeaturesToTrack(currGray, survived, MAX_CORNERS, 0.01, 10);
std::cout << "追跡点を再検出: " << survived.size() << "点" << std::endl;
}
cv::imshow("Tracking", frame);
if (cv::waitKey(30) == 'q') break;
prevGray = currGray.clone();
pts = survived;
}
return 0;
}
追跡点数が MIN_CORNERS(ここでは 20)を下回ると goodFeaturesToTrack で再検出します。長時間の映像解析や監視カメラ用途で安定動作させるための基本パターンです。
つまずきポイント
⚠️ prevPts が空のまま渡すとクラッシュする
goodFeaturesToTrack がテクスチャのない平坦な画面(真っ白など)で0点を返した場合、空の prevPts を calcOpticalFlowPyrLK に渡すと assertion エラーになります。呼び出し前に prevPts.empty() チェックを必ず入れてください。
if (!prevPts.empty()) {
cv::calcOpticalFlowPyrLK(...);
}
⚠️ カラー画像をそのまま渡すと結果が劣化する
prevImg / nextImg にカラー(BGR)画像を渡しても動作はしますが、内部でグレースケール変換が行われず精度が落ちます。事前に cv::cvtColor でグレースケール変換してから渡すのが公式の推奨です。
⚠️ status のチェックを省くと無効な点を使い続ける
追跡失敗点(status[i] == 0)の nextPts[i] は不定値です。フィルタリングせずに次フレームの prevPts に使い回すと、追跡点が画面端に張り付いたり急激に飛んだりします。必ず status で成功点のみ抽出してください。
関連する関数
- cv::goodFeaturesToTrack — LK 法の前処理として追跡開始点を検出する。本記事と常にセットで使う関数
- cv::buildOpticalFlowPyramid — ピラミッド画像を事前計算しておくことで、複数フレームで
calcOpticalFlowPyrLKを繰り返す場合のパフォーマンスを改善できる - cv::VideoCapture の使い方【OpenCV/C++】〜カメラ映像を取り込む〜 — カメラ映像の取り込みに使用。本記事のサンプルコードと直接組み合わせる
- cv::VideoWriter の使い方【OpenCV/C++】〜動画ファイルを書き出す〜 — 追跡結果を動画ファイルとして保存したい場合に組み合わせる
まとめ
cv::calcOpticalFlowPyrLK は、goodFeaturesToTrack と組み合わせるだけでリアルタイム特徴点追跡を実装できる実用的な関数です。status による成功点フィルタリングと追跡点の自動補充を組み込むことで、実運用に耐える安定した追跡ループが構築できます。ウィンドウサイズとピラミッドレベルを映像の動き量に合わせて調整するのが精度向上の第一歩です。
🛠 画像処理のプロが開発するSDK/API
本ブログを運営するスワローインキュベートは、OpenCV ベースのなりすまし判定SDK/API(C++製・OpenCV 4.10)を開発しています。顔認証システムへの組み込み実績多数。
→ 顔認証のなりすまし対策 設計チェックリスト(当ブログ)
→ なりすまし判定SDK/APIの詳細を見る
→ API仕様書・サンプルコード
