ドキュメンテーション

最新のリリースでは、このページがまだ翻訳されていません。 このページの最新版は英語でご覧になれます。

カルマン フィルターを使用したオブジェクトの追跡

この例では、vision.KalmanFilter オブジェクトと関数 configureKalmanFilter を使用してオブジェクトを追跡する方法を説明します。

この例は、本体部分が上部にあり、ヘルパー ルーチンが入れ子関数の形式で下部に置かれた関数です。

function kalmanFilterForTracking

はじめに

カルマン フィルターには、制御、ナビゲーション、コンピューター ビジョン、時系列計量経済学での応用など、数多くの用途があります。この例では、カルマン フィルターを使用してオブジェクトを追跡する方法を説明し、次の 3 つの重要な機能に焦点を当てます。

  • オブジェクトの将来位置の予測

  • 不正確な検出から生じるノイズの削減

  • 複数のオブジェクトをトラックに関連付ける処理の簡略化

オブジェクト追跡の課題

カルマン フィルターの使い方を説明する前に、まずビデオ内のオブジェクトを追跡する場合の課題について考えてみましょう。次のビデオには床の上を左から右に移動する緑色のボールが映っています。

showDetections();

ボール上の白い領域では、vision.ForegroundDetector を使用して検出されたピクセルを強調表示しています。これにより動くオブジェクトが背景から分離されます。ボールと床のコントラストが弱いため、背景差分ではボールの一部だけが検出されます。すなわち、検出プロセスが完全ではないためにノイズが発生します。

オブジェクトの軌跡全体を簡単に可視化するため、すべてのビデオ フレームを 1 つのイメージに重ね合わせます。「+」の記号はブロブ解析で計算された重心を示します。

showTrajectory();

ここで問題が 2 つあります。

  1. 通常、領域の中心はボールの中心とは異なります。言い換えれば、ボールの位置の測定には誤差があります。

  2. ボールが箱に隠されるとその位置は取得できなくなり、測定値は得られません。

カルマン フィルターを使用すると、この両方の問題に対処できます。

カルマン フィルターを使用した単一オブジェクトの追跡

先ほど見たビデオを使用し、関数 trackSingleObject で以下を行う方法を示します。

  • configureKalmanFilter を使用して vision.KalmanFilter を作成する

  • predict メソッドと correct メソッドを順に使用して、追跡システムにあるノイズを排除する

  • predict メソッドを単独で使用して、箱で隠されているボールの位置を推定する

カルマン フィルターのパラメーターの選択は難しいことがあります。関数 configureKalmanFilter を使用すると、この問題を簡略化できます。詳細については、例の中で説明していきます。

関数 trackSingleObject は、入れ子にされた補助関数を含んでいます。入れ子関数の間でデータをやり取りするには、次の最上位レベルの変数が使用されます。

frame            = [];  % A video frame
detectedLocation = [];  % The detected location
trackedLocation  = [];  % The tracked location
label            = '';  % Label for the ball
utilities        = [];  % Utilities used to process the video

単一オブジェクトを追跡する手続きは次のとおりです。

function trackSingleObject(param)
  % Create utilities used for reading video, detecting moving objects,
  % and displaying the results.
  utilities = createUtilities(param);

  isTrackInitialized = false;
  while ~isDone(utilities.videoReader)
    frame = readFrame();

    % Detect the ball.
    [detectedLocation, isObjectDetected] = detectObject(frame);

    if ~isTrackInitialized
      if isObjectDetected
        % Initialize a track by creating a Kalman filter when the ball is
        % detected for the first time.
        initialLocation = computeInitialLocation(param, detectedLocation);
        kalmanFilter = configureKalmanFilter(param.motionModel, ...
          initialLocation, param.initialEstimateError, ...
          param.motionNoise, param.measurementNoise);

        isTrackInitialized = true;
        trackedLocation = correct(kalmanFilter, detectedLocation);
        label = 'Initial';
      else
        trackedLocation = [];
        label = '';
      end

    else
      % Use the Kalman filter to track the ball.
      if isObjectDetected % The ball was detected.
        % Reduce the measurement noise by calling predict followed by
        % correct.
        predict(kalmanFilter);
        trackedLocation = correct(kalmanFilter, detectedLocation);
        label = 'Corrected';
      else % The ball was missing.
        % Predict the ball's location.
        trackedLocation = predict(kalmanFilter);
        label = 'Predicted';
      end
    end

    annotateTrackedObject();
  end % while

  showTrajectory();
end

カルマン フィルターは 2 つの異なるシナリオに対処します。

  • ボールが検出された場合、カルマン フィルターは、まず現在のビデオ フレームにおけるボールの状態を予測してから、新たに検出されたオブジェクトの位置を使ってその状態を修正する。これにより、フィルター処理された位置が得られます。

  • ボールが見つからない場合、カルマン フィルターは前の状態のみに基づいてボールの現在の位置を予測する。

すべてのビデオ フレームを重ね合わせることでボールの軌跡を確認できます。

param = getDefaultParameters();  % get Kalman configuration that works well
                                 % for this example

trackSingleObject(param);  % visualize the results

カルマン フィルターのコンフィギュレーション オプションの調査

カルマン フィルターの構成は非常に難しい場合があります。一連のコンフィギュレーション パラメーターを適切に設定するには、カルマン フィルターについての基本的な理解だけでなく、実験が必要となる場合がよくあります。上で定義した関数 trackSingleObject は、関数 configureKalmanFilter で提供されるさまざまな構成オプションを調べるのに役立ちます。

関数 configureKalmanFilter はカルマン フィルター オブジェクトを返します。5 つの入力引数を指定しなければなりません。

kalmanFilter = configureKalmanFilter(MotionModel, InitialLocation,
         InitialEstimateError, MotionNoise, MeasurementNoise)

MotionModel の設定は、オブジェクトの動きの物理的な特徴に対応しなければなりません。等速度モデルまたは等加速度モデルに設定できます。次の例は、準最適の設定を行った場合の結果を示しています。

param = getDefaultParameters();         % get parameters that work well
param.motionModel = 'ConstantVelocity'; % switch from ConstantAcceleration
                                        % to ConstantVelocity
% After switching motion models, drop noise specification entries
% corresponding to acceleration.
param.initialEstimateError = param.initialEstimateError(1:2);
param.motionNoise          = param.motionNoise(1:2);

trackSingleObject(param); % visualize the results

ボールが予測位置とはかなり違うところに現れている点に注意してください。カーペットの摩擦抵抗のため、ボールは放たれた瞬間から一定した減速を受けます。したがって、ここでは等加速度モデルの方が適しています。等速度モデルに設定したままでは、他の値をどのように設定しても、追跡結果は準最適のものになります。

一般的に、InitialLocation の入力はオブジェクトが最初に検出された位置に設定します。また、1 つの検出から初期状態を得るとノイズが非常に多くなる場合があるため、InitialEstimateError ベクトルは大きな値に設定します。次の図は、これらのパラメーターの構成を誤った場合の結果を示しています。

param = getDefaultParameters();  % get parameters that work well
param.initialLocation = [0, 0];  % location that's not based on an actual detection
param.initialEstimateError = 100*ones(1,3); % use relatively small values

trackSingleObject(param); % visualize the results

パラメーターの構成を間違えたため、カルマン フィルターで返された位置がオブジェクトの実際の軌跡と一致するまでに数ステップを要しています。

MeasurementNoise の値は検出器の精度に基づいて選択しなければなりません。検出器が正確でない場合は、測定ノイズの値を大きく設定してください。次の例は、セグメンテーションのしきい値を正しく設定しなかったために検出のノイズが大きくなる場合を示しています。測定ノイズの値を大きくすると、カルマン フィルターは、入力される測定値よりも内部状態に大きく依存するようになり、したがって検出ノイズは補正されます。

param = getDefaultParameters();
param.segmentationThreshold = 0.0005; % smaller value resulting in noisy detections
param.measurementNoise      = 12500;  % increase the value to compensate
                                      % for the increase in measurement noise

trackSingleObject(param); % visualize the results

通常は、オブジェクトが等加速度や等速度で動くことはありません。理想的な運動モデルとの偏差の量を指定するには、MotionNoise を使用します。運動ノイズを大きくすると、カルマン フィルターはその内部状態よりも、入力される測定値により大きく依存するようになります。MotionNoise パラメーターの効果の詳細については、パラメーターの調整を実際に試してみてください。

ここまでカルマン フィルターの使い方と構成方法について説明してきました。次の節では、複数のオブジェクトを追跡するための使用方法を学びます。

メモ: 上記の例では構成プロセスを簡略化するため、関数 configureKalmanFilter を使用しました。この関数ではいくつかの仮定を行います。詳細については、関数のドキュメンテーションを参照してください。構成プロセスでより高度な制御を行う必要がある場合は、vision.KalmanFilter オブジェクトを直接使用できます。

カルマン フィルターを使用した複数オブジェクトの追跡

複数のオブジェクトの追跡では、いくつかの追加課題が生じます。

  • 複数の検出を正しいトラックに関連付けなければならない

  • シーンに出現した新しいオブジェクトを処理しなければならない

  • 複数のオブジェクトを単一の検出にマージした場合に各オブジェクトの同一性が維持されなければならない

vision.KalmanFilter オブジェクトを関数 assignDetectionsToTracks と共に使用すると、以下の問題を解決しやすくなります。

  • トラックへの検出の割り当て

  • 検出が新しいオブジェクトに対応するかどうかの判断 (トラックの作成)

  • 隠された単一オブジェクトの場合と同様に、予測を使用して、互いに近距離にある複数のオブジェクトを分離する

カルマン フィルターを使用した複数オブジェクトの追跡の詳細については、動きに基づく複数のオブジェクトの追跡という例を参照してください。

例で使用されるユーティリティ関数

オブジェクトの検出と結果の表示にはユーティリティ関数が使用されています。この節では、これらの関数のこの例における実装方法を説明します。

カルマン フィルターを作成してボールをセグメント化するための、既定のパラメーターを取得します。

function param = getDefaultParameters
  param.motionModel           = 'ConstantAcceleration';
  param.initialLocation       = 'Same as first detection';
  param.initialEstimateError  = 1E5 * ones(1, 3);
  param.motionNoise           = [25, 10, 1];
  param.measurementNoise      = 25;
  param.segmentationThreshold = 0.05;
end

ビデオ ファイルから次のビデオ フレームを読み取ります。

function frame = readFrame()
  frame = step(utilities.videoReader);
end

ビデオ内でボールを検出して注釈を付けます。

function showDetections()
  param = getDefaultParameters();
  utilities = createUtilities(param);
  trackedLocation = [];

  idx = 0;
  while ~isDone(utilities.videoReader)
    frame = readFrame();
    detectedLocation = detectObject(frame);
    % Show the detection result for the current video frame.
    annotateTrackedObject();

    % To highlight the effects of the measurement noise, show the detection
    % results for the 40th frame in a separate figure.
    idx = idx + 1;
    if idx == 40
      combinedImage = max(repmat(utilities.foregroundMask, [1,1,3]), frame);
      figure, imshow(combinedImage);
    end
  end % while

  % Close the window which was used to show individual video frame.
  uiscopes.close('All');
end

現在のビデオ フレームでボールを検出します。

function [detection, isObjectDetected] = detectObject(frame)
  grayImage = rgb2gray(frame);
  utilities.foregroundMask = step(utilities.foregroundDetector, grayImage);
  detection = step(utilities.blobAnalyzer, utilities.foregroundMask);
  if isempty(detection)
    isObjectDetected = false;
  else
    % To simplify the tracking process, only use the first detected object.
    detection = detection(1, :);
    isObjectDetected = true;
  end
end

現在の検出と追跡の結果を表示します。

function annotateTrackedObject()
  accumulateResults();
  % Combine the foreground mask with the current video frame in order to
  % show the detection result.
  combinedImage = max(repmat(utilities.foregroundMask, [1,1,3]), frame);

  if ~isempty(trackedLocation)
    shape = 'circle';
    region = trackedLocation;
    region(:, 3) = 5;
    combinedImage = insertObjectAnnotation(combinedImage, shape, ...
      region, {label}, 'Color', 'red');
  end
  step(utilities.videoPlayer, combinedImage);
end

すべてのビデオ フレームを重ね合わせて、ボールの軌跡を表示します。

function showTrajectory
  % Close the window which was used to show individual video frame.
  uiscopes.close('All');

  % Create a figure to show the processing results for all video frames.
  figure; imshow(utilities.accumulatedImage/2+0.5); hold on;
  plot(utilities.accumulatedDetections(:,1), ...
    utilities.accumulatedDetections(:,2), 'k+');

  if ~isempty(utilities.accumulatedTrackings)
    plot(utilities.accumulatedTrackings(:,1), ...
      utilities.accumulatedTrackings(:,2), 'r-o');
    legend('Detection', 'Tracking');
  end
end

ビデオ フレーム、検出した位置および追跡した位置を累積し、ボールの軌跡を表示します。

function accumulateResults()
  utilities.accumulatedImage      = max(utilities.accumulatedImage, frame);
  utilities.accumulatedDetections ...
    = [utilities.accumulatedDetections; detectedLocation];
  utilities.accumulatedTrackings  ...
    = [utilities.accumulatedTrackings; trackedLocation];
end

説明用に、カルマン フィルターで使用された初期位置を選択します。

function loc = computeInitialLocation(param, detectedLocation)
  if strcmp(param.initialLocation, 'Same as first detection')
    loc = detectedLocation;
  else
    loc = param.initialLocation;
  end
end

ビデオを読み取り、動くオブジェクトを検出し、結果を表示するユーティリティを作成します。

function utilities = createUtilities(param)
  % Create System objects for reading video, displaying video, extracting
  % foreground, and analyzing connected components.
  utilities.videoReader = vision.VideoFileReader('singleball.mp4');
  utilities.videoPlayer = vision.VideoPlayer('Position', [100,100,500,400]);
  utilities.foregroundDetector = vision.ForegroundDetector(...
    'NumTrainingFrames', 10, 'InitialVariance', param.segmentationThreshold);
  utilities.blobAnalyzer = vision.BlobAnalysis('AreaOutputPort', false, ...
    'MinimumBlobArea', 70, 'CentroidOutputPort', true);

  utilities.accumulatedImage      = 0;
  utilities.accumulatedDetections = zeros(0, 2);
  utilities.accumulatedTrackings  = zeros(0, 2);
end
end