OpenCV のサンプルプログラム(ビデオ関係)

概要

OpenCV 5 を用いて、ビデオを扱うC++サンプルプログラムをまとめる。前景・背景分離、オプティカルフロー、動き検出、画像フィルタ・変換、機械学習を扱う。

目次

関連する外部ページ

サイト内の関連情報

前景・背景分離

BackgroundSubtractorMOG2 を用いた前景と背景の分離

ガウス混合モデル(GMM)を用いて、動画から前景と背景を分離する。動く物体を前景として抽出できる。ビデオカメラからの入力にも使える。

#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/video.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main( int argc, char** argv )
{
  VideoCapture cap;
  if( argc > 1 )
    cap.open( argv[1] );
  else
    cap.open( 0 );

  if( !cap.isOpened() )
  {
    cout << "ビデオを開けなかった" << endl;
    return 1;
  }

  Ptr pBackSub = createBackgroundSubtractorMOG2(); // 背景差分器を作成する

  namedWindow( "frame", WINDOW_AUTOSIZE );
  namedWindow( "foreground mask", WINDOW_AUTOSIZE );

  Mat frame, fgMask;
  for(;;)
  {
    cap >> frame;
    if( frame.empty() )
      break;

    pBackSub->apply( frame, fgMask ); // 前景マスクを計算する

    imshow( "frame", frame );
    imshow( "foreground mask", fgMask );

    if( waitKey(30) == 27 ) // ESCキーで終了する
      break;
  }
  return 0;
}

オプティカルフロー

calcOpticalFlowFarneback を用いたオプティカルフロー

Farneback法による密なオプティカルフローを計算する。画像内の全画素について動きベクトルを推定し、物体の動きを可視化できる。ビデオキャプチャしながらオプティカルフローを表示する。

#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/video.hpp>
#include <iostream>

using namespace cv;
using namespace std;

static void drawOpticalFlow( const Mat& flow, Mat& dst, int step, const Scalar& color )
{
  for( int y = 0; y < dst.rows; y += step )
    for( int x = 0; x < dst.cols; x += step )
    {
      const Point2f& fxy = flow.at(y, x);
      line( dst, Point(x,y), Point(cvRound(x+fxy.x), cvRound(y+fxy.y)), color );
      circle( dst, Point(x,y), 1, color, -1 );
    }
}

int main( int argc, char** argv )
{
  VideoCapture cap;
  if( argc > 1 )
    cap.open( argv[1] );
  else
    cap.open( 0 );

  if( !cap.isOpened() )
  {
    cout << "ビデオを開けなかった" << endl;
    return 1;
  }

  Mat prevGray, gray, flow, frame;
  namedWindow( "flow", WINDOW_AUTOSIZE );

  for(;;)
  {
    cap >> frame;
    if( frame.empty() )
      break;
    cvtColor( frame, gray, COLOR_BGR2GRAY );

    if( !prevGray.empty() )
    {
      calcOpticalFlowFarneback( prevGray, gray, flow, 0.5, 3, 15, 3, 5, 1.2, 0 ); // 密なオプティカルフローを計算する
      Mat dst;
      cvtColor( prevGray, dst, COLOR_GRAY2BGR );
      drawOpticalFlow( flow, dst, 16, Scalar(0,255,0) );
      imshow( "flow", dst );
    }

    if( waitKey(30) == 27 ) // ESCキーで終了する
      break;
    std::swap( prevGray, gray );
  }
  return 0;
}

動き検出

motempl(モーションテンプレート)

モーションテンプレートを用いた動き検出と方向推定を行う。フレーム間差分の履歴を蓄積し、動きの軌跡と方向を可視化する。ジェスチャ認識などに応用可能。ビデオデバイスを入力として使用する。OpenCV 5 では、この機能は opencv_contriboptflow モジュールに含まれる。

#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/optflow.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main( int argc, char** argv )
{
  VideoCapture cap;
  if( argc > 1 )
    cap.open( argv[1] );
  else
    cap.open( 0 );

  if( !cap.isOpened() )
  {
    cout << "ビデオを開けなかった" << endl;
    return 1;
  }

  Mat frame, gray, prevGray, silh, mhi;
  double timestamp = 0, MHI_DURATION = 1.0;

  namedWindow( "motempl", WINDOW_AUTOSIZE );

  for(;;)
  {
    cap >> frame;
    if( frame.empty() )
      break;
    cvtColor( frame, gray, COLOR_BGR2GRAY );

    if( mhi.empty() )
      mhi = Mat::zeros( frame.size(), CV_32F );

    if( !prevGray.empty() )
    {
      absdiff( gray, prevGray, silh );
      threshold( silh, silh, 30, 1, THRESH_BINARY );
      timestamp += 1.0/30;
      motempl::updateMotionHistory( silh, mhi, timestamp, MHI_DURATION ); // モーション履歴を更新する

      Mat mhiVis;
      mhi.convertTo( mhiVis, CV_8U, 255.0/MHI_DURATION,
                     (MHI_DURATION - timestamp)*255.0/MHI_DURATION );
      imshow( "motempl", mhiVis );
    }

    if( waitKey(30) == 27 ) // ESCキーで終了する
      break;
    std::swap( prevGray, gray );
  }
  return 0;
}

画像フィルタ・変換

Laplacian によるエッジ検出

Laplacian演算子を用いたエッジ検出を行う。画像の2次微分を計算し、輝度変化の激しい箇所(エッジ)を検出する。ビデオデバイスからの入力に使用する。

#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main( int argc, char** argv )
{
  VideoCapture cap;
  if( argc > 1 )
    cap.open( argv[1] );
  else
    cap.open( 0 );

  if( !cap.isOpened() )
  {
    cout << "ビデオを開けなかった" << endl;
    return 1;
  }

  namedWindow( "laplace", WINDOW_AUTOSIZE );

  Mat frame, gray, blurred, laplaceResult, dst;
  for(;;)
  {
    cap >> frame;
    if( frame.empty() )
      break;

    cvtColor( frame, gray, COLOR_BGR2GRAY );
    GaussianBlur( gray, blurred, Size(3,3), 0 );
    Laplacian( blurred, laplaceResult, CV_16S, 5 ); // Laplacian演算子でエッジを検出する
    convertScaleAbs( laplaceResult, dst );

    imshow( "laplace", dst );
    if( waitKey(30) == 27 ) // ESCキーで終了する
      break;
  }
  return 0;
}

warpPolar による極座標変換

線形極座標変換と対数極座標変換のデモ。直交座標系の画像を極座標系に変換する。回転不変な画像処理や、中心からの距離に応じた解像度変化が必要な場合に使用される。

#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main( int argc, char** argv )
{
  string filename = argc > 1 ? argv[1] : "fruits.jpg";
  Mat src = imread( samples::findFile(filename), IMREAD_COLOR );
  if( src.empty() )
  {
    cout << "画像を読み込めなかった: " << filename << endl;
    return 1;
  }

  Point2f center( (float)src.cols/2, (float)src.rows/2 );
  double maxRadius = min(center.x, center.y);

  Mat linPolar, logPolarImg;
  warpPolar( src, linPolar, Size(), center, maxRadius, INTER_LINEAR );                   // 線形極座標変換
  warpPolar( src, logPolarImg, Size(), center, maxRadius, INTER_LINEAR + WARP_POLAR_LOG ); // 対数極座標変換

  namedWindow( "linear polar", WINDOW_AUTOSIZE );
  imshow( "linear polar", linPolar );
  waitKey(0); // ESCキーを含む任意のキーで終了する

  namedWindow( "log polar", WINDOW_AUTOSIZE );
  imshow( "log polar", logPolarImg );
  waitKey(0); // ESCキーを含む任意のキーで終了する
  return 0;
}

機械学習

決定木(Decision Tree)を用いた分類

決定木(Decision Tree)を用いた分類のサンプル。UCI Machine Learning Repositoryのキノコデータセットを使用し、キノコが食用か毒性かを分類する機械学習の例題。

#include <opencv2/ml.hpp>
#include <iostream>
#include <fstream>

using namespace cv;
using namespace cv::ml;
using namespace std;

int main( int argc, char** argv )
{
  string filename = argc > 1 ? argv[1] : "agaricus-lepiota.data";
  ifstream ifs( filename );
  if( !ifs.is_open() )
  {
    cout << "データファイルを読み込めなかった: " << filename << endl;
    return 1;
  }

  vector> rows;
  string line;
  while( getline(ifs, line) )
  {
    vector row;
    string field;
    for( size_t i = 0; i < line.size(); i++ )
    {
      char c = line[i];
      if( c == ',' )
      {
        row.push_back( field.empty() ? 0.f : (float)field[0] );
        field.clear();
      }
      else
        field += c;
    }
    if( !field.empty() )
      row.push_back( (float)field[0] );
    if( !row.empty() )
      rows.push_back( row );
  }

  int nSamples = (int)rows.size();
  int nVars = (int)rows[0].size() - 1;
  Mat samples( nSamples, nVars, CV_32F );
  Mat responses( nSamples, 1, CV_32F );

  for( int i = 0; i < nSamples; i++ )
  {
    responses.at(i,0) = rows[i][0]; // 先頭列が食用・毒性のラベル
    for( int j = 0; j < nVars; j++ )
      samples.at(i,j) = rows[i][j+1];
  }

  Ptr data = TrainData::create( samples, ROW_SAMPLE, responses );
  data->setTrainTestSplitRatio(0.9);

  Ptr dtree = DTrees::create();
  dtree->setMaxDepth(8);
  dtree->setMinSampleCount(10);
  dtree->setCVFolds(0);
  dtree->train( data ); // 決定木を学習する

  Mat testSamples = data->getTestSamples();
  Mat testResponses = data->getTestResponses();
  int correct = 0;
  for( int i = 0; i < testSamples.rows; i++ )
  {
    float r = dtree->predict( testSamples.row(i) );
    if( (r > 0) == (testResponses.at(i,0) > 0) )
      correct++;
  }
  cout << "テストデータでの正答率: "
       << (100.0 * correct / testSamples.rows) << " %" << endl;
  return 0;
}

◆ Linux でのビルド手順例(各プログラムをそれぞれ hoge.cpp として保存した場合)

g++ -o a.out hoge.cpp -std=c++17 `pkg-config --cflags --libs opencv5`