cv::calcOpticalFlowPyrLK の使い方【OpenCV/C++】〜LK法でオプティカルフローを追跡する〜

📌 準備: OpenCV の環境構築がまだの方はこちら → C++ 環境構築ガイド / C#(OpenCvSharp)セットアップ

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::calcOpticalFlowPyrLK は、goodFeaturesToTrack と組み合わせるだけでリアルタイム特徴点追跡を実装できる実用的な関数です。status による成功点フィルタリングと追跡点の自動補充を組み込むことで、実運用に耐える安定した追跡ループが構築できます。ウィンドウサイズとピラミッドレベルを映像の動き量に合わせて調整するのが精度向上の第一歩です。

🛠 画像処理のプロが開発するSDK/API

本ブログを運営するスワローインキュベートは、OpenCV ベースのなりすまし判定SDK/API(C++製・OpenCV 4.10)を開発しています。顔認証システムへの組み込み実績多数。

→ 顔認証のなりすまし対策 設計チェックリスト(当ブログ)
→ なりすまし判定SDK/APIの詳細を見る
→ API仕様書・サンプルコード

タイトルとURLをコピーしました