物体追従の処理パフォーマンスを上げる


問題とすること

opencv_contrib には物体追従(トラッキング)をしてくれる trackingモジュール という素晴らしいモジュールが用意されています。しかし、このトラッキング処理はイメージサイズに反比例してパフォーマンスが低下するという、ある意味当たり前ですが、パフォーマンスの問題を抱えています。

時に映像内の物体を追従したいといった時、特にリアルタイム処理したい場合には、この問題は顕著にフレームレートの低下をもたらしてしまうことがあります。この記事はその問題に対する1つの回答です。

解決方法は?

アイディアは至って簡単です。大きなイメージサイズを扱わず、小さなイメージにリサイズしてからトラッキングを行います。リサイズ処理のコストはトラッキング処理のコストよりも遥かに小さく済みます。

パフォーマンス改善の結果

VisualStudioのパフォーマンスプロファイラーを使って、CPU処理を計測してみました。

測定プログラムについて

OpenCvSharpを使用してカメラ入力の映像をリアルタイムで処理するWPFアプリケーションを作成しました。カメラ入力映像はHD解像度(1280 x 720)ですが、対応前はそのまま処理しており、対応後は内部的にSD解像度(640 x 480)に変換してから処理をしています。

測定方法

トラッキング処理のコアメソッドである OpenCvSharp.Tarcking.Update のCPU占有率を比較しました。計測機器はLenovo Thinkpad T470s (CPU Core i7-7500U @ 2.7Ghz)を使っています。

結果

対応前

  • 入力映像:HD(1280 x 720)
  • 処理イメージサイズ:HD(1280 x 720)
  • CPU占有率: 30.76%

対応後

  • 入力映像:HD(1280 x 720)
  • 処理イメージサイズ:SD(640 x 480)
  • CPU占有率: 19.80%

大雑把ですが、結果として、トラッキング処理に使われているCPUが、2/3になりました。

おまけ

参考までに計測に使用したコードの一部を加工して載せます。OpenCvSharpを使う人なら普通に実装できると思います。

大雑把な解説ですが、カメラ映像等の取得ループ内で使う前提で、トラッキング領域ごとにnewして、Initでトラッキングしたい領域を指定、イメージが変わるたびにUpdateを呼び出して追従処理をする感じです。マジックナンバーになっていますが、内部的には幅640px超のイメージはアスペクト比を保ったまま640pxにリサイズして処理します。

SmartTracker.cs
using OpenCvSharp;
using OpenCvSharp.Tracking;

namespace MyApp
{
    public class SmartTracker
    {
        private Tracker _tracker;
        private bool _tarcking = false;
        private Rect2d _boundingBox;
        private Rect? _initRect;
        private double _imageRatio = 1.0;
        private const double _MAX_IMAGE_WIDTH = 640.0;

        public SmartTracker()
        {
            _tracker = TrackerMedianFlow.Create();
        }

        public bool Init(Mat image, Rect target)
        {
            // トラッキングの処理効率を上げるために内部的には最大幅640pxのイメージとして処理する
            // この時、内部の_initRect, _boundingBox等は縮小したイメージで処理されるので、
            // publicメソッド内で復元して呼び出し元に返却する
            if (image.Width > _MAX_IMAGE_WIDTH)
            {
                _imageRatio = _MAX_IMAGE_WIDTH / image.Width;
                image = image.Resize(new Size(image.Width * _imageRatio, image.Height * _imageRatio));
                target = ConvertInternalSizeRect(target);
            }
            else
            {
                _imageRatio = 1.0;
            }

            _boundingBox = new Rect2d(target.X, target.Y, target.Width, target.Height);
            _initRect = target;

            try
            {
                _tarcking = _tracker.Init(image, _boundingBox);
            }
            catch
            {
                _tarcking = false;
            }

            return _tarcking;
        }

        public Rect? Update(Mat image, Rect? area = null)
        {
            if(_initRect == null)
            {
                return null;
            }
            if (image.Width > _MAX_IMAGE_WIDTH)
            {
                _imageRatio = _MAX_IMAGE_WIDTH / image.Width;
                image = image.Resize(new Size(image.Width * _imageRatio, image.Height * _imageRatio));
                if (area.HasValue)
                {
                    area = ConvertInternalSizeRect((Rect)area);
                }
            }
            else
            {
                _imageRatio = 1.0;
            }

            Rect retRect;
            try
            {
                _tracker.Update(image, ref _boundingBox);
                retRect = new Rect((int)(_boundingBox.X), (int)(_boundingBox.Y), (int)(_boundingBox.Width), (int)(_boundingBox.Height));

                // 指定エリアの外になったら抜ける
                if (area.HasValue)
                {
                    Rect a = (Rect)area;
                    if (!a.Contains(retRect.TopLeft) || !a.Contains(retRect.BottomRight))
                    {
                        Reset();
                        return null;
                    }
                }

                return retRect;
            }
            catch
            {
                Reset();
                return null;
            }
        }

        public void Reset()
        {
            _initRect = null;
            _tarcking = false;
            if (!_tracker.IsDisposed)
            {
                _tracker.Dispose();
            }
            _tracker = TrackerMedianFlow.Create();
        }

        public bool IsTracking()
        {
            return (_initRect.HasValue) ? true : false;
        }

        private Rect ConvertOriginalSizeRect(Rect rect)
        {
            return new Rect((int)(rect.X / _imageRatio), (int)(rect.Y / _imageRatio), (int)(rect.Width / _imageRatio), (int)(rect.Height / _imageRatio));
        }

        private Rect ConvertInternalSizeRect(Rect rect)
        {
            return new Rect((int)(rect.X * _imageRatio), (int)(rect.Y * _imageRatio), (int)(rect.Width * _imageRatio), (int)(rect.Height * _imageRatio));
        }
    }
}

以上。
OpenCVって使い慣れてくると便利です。OpenCvSharpを作ったshimatさん、感謝しています。皆さんも良いOpenCVライフを送ってください。