Трекинг маркера в видеопотоке

в 2:52, , рубрики: image processing, дополненная реальность, обработка изображений, Программирование, распознавание изображений, метки: , ,

В данной статье пойдет речь о том, как сделать трекинг контрастного объекта (маркера) в видеопотоке. Если требуется сделать нечто подобное как на видео, то добро пожаловать под кат.

Описание решения

Список задач для решения поставленной цели

Для того, чтобы справиться с поставленной целью, следует реализовать следующие задачи:
1. Захват видеопотока с web-камеры
2. Заданное количество раз в секунду выполнять действия:

  • Сграбить текущее изображение с камеры
  • Поиск объекта в кадре для трекинга
  • Отрисовка графического эффекта на базе истории трекинга

Задачи Поиск объекта в кадре для трекинга и Отрисовка графического эффекта на базе истории трекинга будет реализованы в классе ObjectTracking, который будет описан в данной статье на языке C#. Взаимодействие с другими задачами также будет продемонстрировано.

Давайте более детально рассмотрим подзадачи Поиск объекта в кадре для трекинга:

  • С целью оптимизации использования ресурсов производить уменьшение изображения в несколько раз
  • Поиск области на изображении, которая относится к оттенкам заданного цвета
  • Вычисление центра данной области
  • Сохранение информации об объекте в историю трекинга
Дополнительные библиотеки

Потребуется библиотека для захвата видеопотока с камеры, я воспользовался DirectShowLib_2005, работа с которой описана здесь.
Также потребуется класс UnsafeBitmap для более быстрой работы с Bitmap.

Реализация класса ObjectTracking

Опишем члены класса и базовый конструктор

        private Queue<Point> history;   // History for object tracking (based on FIFO)
        private int sizeHistory;        // Maximum size for history
        private short nSize;            // Resizing parameter
        private short pSize;            // Pen size
        private Bitmap resultBmp;       // Result bitmap

        // Base constructor
        public ObjectTracking(int sizeHistory, short nSizeImg, short penSize)
        {
            this.sizeHistory = sizeHistory; 
            this.history     = new Queue<Point>();
            this.nSize       = nSizeImg;
            this.pSize       = penSize;
        }

Дополнительные методы по изменению размера кадра и возврату истории трекинга (если нужно использовать будет вне класса)

        // Resizing picture
        private Bitmap ResizeBitmap(Bitmap b, int nWidth, int nHeight)
        {
            Bitmap result = new Bitmap(nWidth, nHeight);
            using (Graphics g = Graphics.FromImage((Image)result))
                g.DrawImage(b, 0, 0, nWidth, nHeight);
            
            return result;
        }

        // Get history (Queue FIFO)
        public Queue<Point> GetHistory()
        {
            return this.history;
        }

Метод поиска центра маркера в текущем кадре, вычисление его центра и запись в историю трекинга

        // Find center of object on image
        private Point FindRedObject(Image srcImage)
        {
            // Default value for object
            Point result = new Point(0, 0);
            int minX = srcImage.Width;  int maxX = 0;
            int minY = srcImage.Height; int maxY = 0;

            UnsafeBitmap tmpBmp = new UnsafeBitmap(this.ResizeBitmap(this.resultBmp,
                this.resultBmp.Width / this.nSize,
                this.resultBmp.Height / this.nSize));
            tmpBmp.LockBitmap();
            
            // If pixel is red
            for (int i = 0; i < tmpBmp.Bitmap.Width; i++)
                for (int j = 0; j < tmpBmp.Bitmap.Height; j++)
                {
                    PixelData pxl = tmpBmp.GetPixel(i, j);
                    if (pxl.red > (pxl.blue + pxl.green))
                    {
                        // Get critical points
                        if (i < minX) minX = i;
                        if (i > maxX) maxX = i;
                        if (j < minY) minY = j;
                        if (j > maxY) maxY = j;
                    }
                }
            tmpBmp.UnlockBitmap();

            // Calculating center
            result.X = (minX + maxX) / 2 * this.nSize;
            result.Y = (minY + maxY) / 2 * this.nSize;

            // Check on default value (screen center)
            if (result.X != (int)(this.resultBmp.Width / 2 * this.nSize) || 
                result.Y != (int)(this.resultBmp.Height / 2 * this.nSize))
                this.history.Enqueue(result);
            else this.history.Enqueue(new Point(0, 0));

            // Check on history size
            if (this.history.Count > this.sizeHistory - 1)
                this.history.Dequeue();
            return result;
        }

Основной цикл предназначен для поиска маркера, базируясь на фильтрации изображении. Суть условия заключается в поиске пикселей, которые относятся к оттенкам красного цвета. Результат работы данного фильтра можно посмотреть на изображении ниже. Красным отмечены области, где присутствует объект, а черным фон. Для других цветов следует изменить данное условие. Чтобы усилить или уменьшить чувствительность к цвету, можно поставить коэффициент (больше или меньше единицы).

image

Рисование эффекта шлейфа на изображении. Можно использовать свой эффект или ничего.

        // Draw effect on source image
        private void DrawEffect()
        {
            Graphics g = Graphics.FromImage(this.resultBmp);
            Point prevPoint = new Point(0, 0);
            bool first = false;

            // Drawing all queue
            foreach (Point p in this.GetHistory())
            {
                // If center exists
                if ((p.X > 0) && (p.Y > 0) && (p.X < this.resultBmp.Width) && (p.X < this.resultBmp.Height))
                {
                    if (!first)
                    {
                        prevPoint = new Point(p.X, p.Y);
                        first = true;
                    }

                    g.DrawLine(new Pen(Color.Orange, this.pSize), prevPoint, p);
                    prevPoint = new Point(p.X, p.Y);
                }
            }
        }

Public-метод получения кадра с эффектом, использует описанные выше методы

        // Get result frame for showing
        public Bitmap GetFrameWithEffect(Image srcImage)
        {
            if (srcImage != null)
            {
                this.resultBmp = srcImage as Bitmap;
                this.FindRedObject(srcImage);
                this.DrawEffect();
            }

            return this.resultBmp;
        }

Этих методов достаточно, чтобы реализовать задачи Поиск объекта в кадре для трекинга и Отрисовка графического эффекта на базе истории трекинга.

Код класса ObjectTracking

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;

namespace MagicMotion
{
    public class ObjectTracking
    {
        private Queue<Point> history;   // History for object tracking (based on FIFO)
        private int sizeHistory;        // Maximum size for history
        private short nSize;            // Resizing parameter
        private short pSize;            // Pen size
        private Bitmap resultBmp;       // Result bitmap

        // Base constructor
        public ObjectTracking(int sizeHistory, short nSizeImg, short penSize)
        {
            this.sizeHistory = sizeHistory; 
            this.history     = new Queue<Point>();
            this.nSize       = nSizeImg;
            this.pSize       = penSize;
        }

        // Get result frame for showing
        public Bitmap GetFrameWithEffect(Image srcImage)
        {
            if (srcImage != null)
            {
                this.resultBmp = srcImage as Bitmap;
                this.FindRedObject(srcImage);
                this.DrawEffect();
            }

            return this.resultBmp;
        }

        // Get history (Queue FIFO)
        public Queue<Point> GetHistory()
        {
            return this.history;
        }

        // Find center of object on image
        private Point FindRedObject(Image srcImage)
        {
            // Default value for object
            Point result = new Point(0, 0);
            int minX = srcImage.Width;  int maxX = 0;
            int minY = srcImage.Height; int maxY = 0;

            /*Bitmap tmpBmp = this.ResizeBitmap(this.resultBmp,
                this.resultBmp.Width / this.nSize,
                this.resultBmp.Height / this.nSize);*/

            UnsafeBitmap tmpBmp = new UnsafeBitmap(this.ResizeBitmap(this.resultBmp,
                this.resultBmp.Width / this.nSize,
                this.resultBmp.Height / this.nSize));
            tmpBmp.LockBitmap();
            
            // If pixel is red
            for (int i = 0; i < tmpBmp.Bitmap.Width; i++)
                for (int j = 0; j < tmpBmp.Bitmap.Height; j++)
                {
                    PixelData pxl = tmpBmp.GetPixel(i, j);
                    if (pxl.red > (pxl.blue + pxl.green))
                    {
                        // Get critical points
                        if (i < minX) minX = i;
                        if (i > maxX) maxX = i;
                        if (j < minY) minY = j;
                        if (j > maxY) maxY = j;
                    }
                }
            tmpBmp.UnlockBitmap();

            // Calculating center
            result.X = (minX + maxX) / 2 * this.nSize;
            result.Y = (minY + maxY) / 2 * this.nSize;

            // Check on default value (screen center)
            if (result.X != (int)(this.resultBmp.Width / 2 * this.nSize) || 
                result.Y != (int)(this.resultBmp.Height / 2 * this.nSize))
                this.history.Enqueue(result);
            else this.history.Enqueue(new Point(0, 0));

            // Check on history size
            if (this.history.Count > this.sizeHistory - 1)
                this.history.Dequeue();
            return result;
        }

        // Draw effect on source image
        private void DrawEffect()
        {
            Graphics g = Graphics.FromImage(this.resultBmp);
            Point prevPoint = new Point(0, 0);
            bool first = false;

            // Drawing all queue
            foreach (Point p in this.GetHistory())
            {
                // If center exists
                if ((p.X > 0) && (p.Y > 0) && (p.X < this.resultBmp.Width) && (p.X < this.resultBmp.Height))
                {
                    if (!first)
                    {
                        prevPoint = new Point(p.X, p.Y);
                        first = true;
                    }

                    g.DrawLine(new Pen(Color.Orange, this.pSize), prevPoint, p);
                    prevPoint = new Point(p.X, p.Y);
                }
            }
        }

        // Resizing picture
        private Bitmap ResizeBitmap(Bitmap b, int nWidth, int nHeight)
        {
            Bitmap result = new Bitmap(nWidth, nHeight);
            using (Graphics g = Graphics.FromImage((Image)result))
                g.DrawImage(b, 0, 0, nWidth, nHeight);
            
            return result;
        }

    }
}

Вызов методов класса ObjectTracking

К примеру у нас есть приложение с классом MainForm. Объявим члены класса: камеру и трекер объектов

        private ObjectTracking ot;
        private CameraCapture cam;

Конструктор будет выглядеть следующим образом

        public MainForm()
        {
            InitializeComponent();

            // Create class for object tracking
            ot = new ObjectTracking(30, 2, 10);

            try
            {
                cam = new CameraCapture();
                cam.InitcamNew();
                cam.UpdateImage();
            }
            catch (Exception ex)
            {
                MessageBox.Show("Проверьте корректность работы камеры",
                    "Ошибка при инициализации камеры", MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
                this.Close();
            }

            // Launch tracking
            timerSnap.Enabled = true;
        }

Объект ot будет создан с параметрами: история длиной — 30, изменение размера кадра при анализе — 2, ширина линии шлейфа — 2.

Также имеется таймер, в событии которого происходит следующее

            try
            {
                cam.UpdateImage();
                pbVideo.Image = cam.AcurrShot;
            }
            catch (Exception ex)
            {
                MessageBox.Show("Проверьте корректность работы камеры",
                    "Ошибка при отображении видео", MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
                this.Close();
            }

            // Track the object
            pbVideo.Image = (Image)ot.GetFrameWithEffect(pbVideo.Image) as Image;

Происходит захват камеры в PictureBox и отрисовка кадра с нанесенным эффектом, что и требовалось для реализации поставленных задач.

Заключение

В статье приведен простой способ трекинга маркера. Если потребуется анализировать объекты другого цвета, то следует изменить условие в методе FindRedObject. Также можно накладывать абсолютно другой эффект на видеопоток, достаточно написать свой вывод графики в методе DrawEffect.

Автор: sermal

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js