В данной статье пойдет речь о том, как сделать трекинг контрастного объекта (маркера) в видеопотоке. Если требуется сделать нечто подобное как на видео, то добро пожаловать под кат.
Описание решения
Список задач для решения поставленной цели
Для того, чтобы справиться с поставленной целью, следует реализовать следующие задачи:
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;
}
Основной цикл предназначен для поиска маркера, базируясь на фильтрации изображении. Суть условия заключается в поиске пикселей, которые относятся к оттенкам красного цвета. Результат работы данного фильтра можно посмотреть на изображении ниже. Красным отмечены области, где присутствует объект, а черным фон. Для других цветов следует изменить данное условие. Чтобы усилить или уменьшить чувствительность к цвету, можно поставить коэффициент (больше или меньше единицы).
Рисование эффекта шлейфа на изображении. Можно использовать свой эффект или ничего.
// 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;
}
Этих методов достаточно, чтобы реализовать задачи Поиск объекта в кадре для трекинга и Отрисовка графического эффекта на базе истории трекинга.
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