C++ 实时目标跟踪器






4.93/5 (48投票s)
本文描述了一种物体跟踪方法,该方法通过估计一个时间平均背景场景,用于在缩减的图像数据上实时跟踪静态和移动的物体。
目录
引言
本文介绍了一种使用传统网络摄像头以实时速度跟踪移动或静态物体的方法。一种简单的跟踪方法是将预定义的背景图像与物体开始出现时的同一背景帧进行比较。这种情况适用于静态环境,您可以在没有要跟踪的任何前景物体的情况下学习背景图像(例如,室内、门厅、办公室、商店、仓库等)。如果一个物体在场景中移动并长时间保持静止,您可以将其视为场景的一部分,并将其图像叠加到背景上以避免进一步检测。同样适用于从场景中移除某些背景项的情况。这种方法的缺点包括光照或摄像头位置的变化。在这种情况下,您将需要重新估计新的背景。另一种解决方案是使用边缘算子来避免光照变化。但如果您想识别物体的实体而不只是其轮廓,则需要额外的代码。
传统的 640x480 分辨率的网络摄像头远超所需。您可以将其缩小约三倍,以去除通常存在的噪声,并显着提高处理速度,而不会对跟踪能力造成实质性损失,除非您期望跟踪非常微小的物体。缩小步骤可以大大提高物体跟踪的处理速度。在没有物体的情况下,其速度约为 100fps(在 2Ghz 单核上),有物体时为 30 到 90fps。物体越大,估计属于该斑块的所有像素所需的时间就越多。
背景
本文和代码基于我之前的提交,除了 ImageBlobs
类。如果您对 GUI 界面或特定代码片段有疑问,请查看以下文章:
- 用于图像处理的 2D 快速小波变换库
- 使用 Haar 变换快速进行二维图像缩放
- 2D 向量类包装器 SSE 优化数学运算
- 使用 SampleGrabber 以缓冲模式预览视频并捕获帧到内存
- 带肤色和运动分析的 C++ 人脸检测库
GUI 针对 640x480 的网络摄像头,如果您想将其用于不同的分辨率,请查看“人脸检测”文章以添加必要的更改。您不能指望我为每一个自定义视频设备提供完整的应用程序。
使用代码
按照 使用 SampleGrabber 以缓冲模式预览视频并捕获帧到内存 中的说明设置摄像头和帧捕获速率,然后开始视频捕获。单击“背景”单选框以首先开始背景估计过程。所有捕获的帧将被加在一起,一旦您单击“开始跟踪
”单选框以开始物体跟踪,就会估计出平均背景帧并将其保存到 JPEG 文件 background.jpg 中。我建议您将摄像头对准一个您可能期望有移动物体的区域。估计背景约几秒钟,以适应摄像头噪声或场景中短暂出现的偶然移动的物体。平均背景估计将有效地去除它们。现在,将您想要跟踪的物体引入场景,或者如果它们是活的,请等待它们出现在那里。
库
平均背景帧估计是可取的,因为它允许过滤掉任何噪声或微小移动。
1: background[i] = 0;
frames_number = 0;
2: while(frames_number < N)
background[i] = current_frame[i] + background[i];
frames_number++;
3: background[i] = background[i] / frames_number;
库中的新类是:
MotionDetector
ImageBlobs
MotionDetector
中的更改允许设置将与每个新图像帧进行比较的背景帧,而不是像“人脸检测”文章中那样计算连续帧之间的差异。背景帧和新图像帧通过 ImageResize
对象呈现。
首先,您需要使用视频帧的宽度、高度和缩放因子来初始化 MotionDetector
对象。
void MotionDetector::init(unsigned int image_width, unsigned int image_height, float zoom);
要将图像缩小三倍,请使用 zoom
= 0.125f。
在完成背景估计过程后,调用 set_background()
函数。
inline int MotionDetector::set_background(const unsigned char* pBGR)
{
return m_background.resize(pBGR);
}
现在,您可以调用 detect()
函数来估计属于前景的像素。
const vec2Dc* MotionDetector::detect(const unsigned char* pBGR)
{
if (status() < 0)
return 0;
m_image.resize(pBGR);
//RGB version
char** r1 = m_image.getr();
char** g1 = m_image.getg();
char** b1 = m_image.getb();
char** r2 = m_background.getr();
char** g2 = m_background.getg();
char** b2 = m_background.getb();
for (unsigned int y = 0; y < m_motion_vector->height(); y++) {
for (unsigned int x = 0; x < m_motion_vector->width(); x++) {
if (abs(r1[y][x] - r2[y][x]) > m_TH ||
abs(g1[y][x] - g2[y][x]) > m_TH ||
abs(b1[y][x] - b2[y][x]) > m_TH)
(*m_motion_vector)(y, x) = 1;
else
(*m_motion_vector)(y, x) = 0;
}
}
//gray scale version
/*m_difference_vector->sub(*m_image.gety(), *m_background.gety());
for (unsigned int y = 0; y < m_motion_vector->height(); y++) {
for (unsigned int x = 0; x < m_motion_vector->width(); x++) {
if (fabs((*m_difference_vector)(y, x)) > m_TH)
(*m_motion_vector)(y, x) = 1;
else
(*m_motion_vector)(y, x) = 0;
}
}*/
m_tmp_motion_vector->dilate(*m_motion_vector, 3, 3);
m_motion_vector->erode(*m_tmp_motion_vector, 5, 5);
m_tmp_motion_vector->dilate(*m_motion_vector, 3, 3);
return m_tmp_motion_vector;
}
您可以使用 RGB 值比较或灰度图像数据。后者,我发现当转换为灰度值时,对于颜色相似的物体不太稳健。膨胀和腐蚀算子可以填充由于背景和物体相似的像素颜色而可能在物体斑块中出现的任何间隙,并去除微小移动或非常细对象的噪声。
返回的向量用于通过 ImageBlobs
对象进行斑块提取。您需要以下函数来使用该类:
void ImageBlobs::init(unsigned int width, unsigned int height);
int ImageBlobs::find_blobs(const vec2Dc& image, unsigned int min_elements_per_blob = 0);
void ImageBlobs::find_bounding_boxes();
void ImageBlobs::delete_blobs();
首先,您需要将对象初始化为缩小后的图像的宽度和高度(例如,init(zoom * image_width, zoom * image_height)
)。然后,您可以继续使用 MotionDetector::detect()
函数调用返回的 image
来估计斑块。该函数在 image
向量中搜索水平或垂直相邻的非零元素,形成一个斑块。然后,它用当前找到的斑块编号标记每个元素。
例如,用作 image
向量的 10x10 向量:
1 1 1 0 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0
0 1 0 0 0 1 1 1 1 0
0 0 0 0 1 1 1 1 1 0
1 1 0 1 1 1 1 1 1 1
1 1 0 1 1 1 1 1 1 1
1 0 1 1 1 1 1 1 1 1
0 0 0 1 1 1 1 1 1 1
find_blobs()
将从此 image
中估计出三个斑块。您可以使用 const vec2Dc* ImageBlobs::get_image() const
函数获取包含找到的斑块的向量。
1 1 1 0 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0
0 1 0 0 0 2 2 2 2 0
0 0 0 0 2 2 2 2 2 0
3 3 0 2 2 2 2 2 2 2
3 3 0 2 2 2 2 2 2 2
3 0 2 2 2 2 2 2 2 2
0 0 0 2 2 2 2 2 2 2
使用 min_elements_per_blob
,您可以丢弃小斑块不被检测到(例如,min_elements_per_blob
= 5 将导致第三个斑块未被检测到)。
要访问找到的斑块的元素,您可以使用以下函数:
inline unsigned int ImageBlobs::get_blobs_number() const;
inline const struct Blob* ImageBlobs::get_blob(unsigned int i) const;
斑块在 Blob
结构中返回:
struct Blob {
unsigned int elements_number;
vector<struct Element> elements;
unsigned int area;
RECT bounding_box; //[top,left; right,bottom)
};
其中 elements_number
是 Element
结构数组 elements
中包含的斑块中的元素数量。
struct Element {
vector<struct Element> neighbs;
struct Coord coord;
};
neighbs
包含直接相邻的元素,coord
是 image
向量中元素的坐标。
struct Coord {
int x;
int y;
};
调用 find_blobs()
后,您可以选择调用 find_bounding_boxes()
来估计所有斑块的边界框,并将其存储在 Blobs::bounding_box
窗口的 RECT
结构中。在下一次调用 find_blobs()
之前,您需要使用 delete_blobs()
从 ImageBlobs
对象中删除找到的斑块。
下面展示了 find_blobs()
函数:
int ImageBlobs::find_blobs(const vec2Dc& image,
unsigned int min_elements_per_blob)
{
if (m_image == 0)
//not initialized
return -1;
m_image->copy(image);
while (true) {
struct Blob blob;
blob.elements_number = 0;
blob.area = 0;
unsigned int y, x;
//find first non-zero entry//////////////////////////////////
for (y = 0; y < m_image->height(); y++) {
for (x = 0; x < m_image->width(); x++) {
if ((*m_image)(y, x) != 0) {
struct Element element;
element.coord.x = x;
element.coord.y = y;
blob.elements_number = 1;
blob.elements.push_back(element);
blob.area = 0;
memset(&blob.bounding_box, 0, sizeof(RECT));
break;
}
}
if (blob.elements_number > 0)
break;
}
if (blob.elements_number == 0) {
mark_blobs_on_image();
return get_blobs_number();
}
blob.elements.reserve(m_image->width() * m_image->height());
//find blob//////////////////////////////////////////////////
unsigned int index = 0;
while (index < blob.elements_number) {
unsigned int N = (unsigned int)blob.elements_number;
for (unsigned int i = index; i < N; i++) {
add_up_neighbour(blob, i);
add_right_neighbour(blob, i);
add_down_neighbour(blob, i);
add_left_neighbour(blob, i);
}
index = N;
}
remove_blob_from_image(blob);
if (blob.elements_number > min_elements_per_blob) {
blob.area = (unsigned int)blob.elements_number;
blob.elements.reserve(blob.elements_number);
m_blobs.push_back(blob);
}
}
}
add_*_neighbour()
函数会检查当前 blob
中第 i 个 Element
的上方、下方、左方或右方的直接相邻图像元素,并将其添加到 Blob
元素数组中。
inline unsigned int ImageBlobs::add_up_neighbour(struct Blob& blob, unsigned int i)
{
const struct Element& element = blob.elements[i];
if (element.coord.y - 1 < 0)
return 0;
else if ((*m_image)(element.coord.y - 1, element.coord.x) > 0) {
struct Element new_element;
new_element.coord.x = element.coord.x;
new_element.coord.y = element.coord.y - 1;
if (has_neighbour(element, new_element) == false) {
int index = is_element_present(blob, new_element);
if (index >= 0) {
blob.elements[index].neighbs.push_back(element);
return 0;
}
new_element.neighbs.push_back(element);
blob.elements_number++;
blob.elements.push_back(new_element);
return 1;
}
else
return 0;
}
else
return 0;
}
has_neighbour()
和 is_element_present
确定新元素是否已存在于 blob
中。
inline int ImageBlobs::is_element_present(const struct Blob& blob,
const struct Element& new_element) const
{
//int index = 0;
for(int i = (int)blob.elements_number - 1; i >= 0; i--) {
const struct Element& element = blob.elements[i];
if (element.coord.x == new_element.coord.x &&
element.coord.y == new_element.coord.y) {
//wprintf(L" %d\n", blob.elements_number - 1 - i);
return i;
}
//if (++index > 2) //inspect at least 2 last elements
// break;
}
return -1;
}
inline bool ImageBlobs::has_neighbour(const struct Element& element,
const struct Element& new_element) const
{
unsigned int N = (unsigned int)element.neighbs.size();
if (N > 0) {
for (unsigned int i = 0; i < N; i++) {
if (element.neighbs[i].coord.x == new_element.coord.x &&
element.neighbs[i].coord.y == new_element.coord.y)
return true;
}
return false;
}
else
return false;
}
结果
在这里,我选择了一个静态背景,并使用现有物体进行了一些物体跟踪实验。下面展示了我使用的背景。它经过了几秒钟的平均处理。
接下来的两个物体(LCD-TFT 屏幕清洁剂和手机)以 66.67fps 的速度进行跟踪。
手机电源适配器以 71.43fps 的速度进行跟踪。您可以看到由于腐蚀和膨胀算子,电源线未被检测到,因为它非常细。
现在,是一些笔和钥匙:90.91fps。
更复杂的场景:以 29.41fps 检测到三个不同的物体。
吉米·亨德里克斯(《Are You Experienced》专辑,音质极佳)和 9v 电池以 55.56fps 的速度跟踪。
另一个背景设置。
地毯上的几个 9v 电池和一个卷筒以 90.91fps 的速度检测到。您可以看到一个电池由于最小元素数量限制未被检测到。
电源适配器、LCD-TFT 清洁剂和 9v 电池以 90.91fps 的速度检测到。小的电池由于腐蚀和膨胀算子再次未被检测到。
接下来,一些蜡烛和手机以 83.33fps 的速度跟踪。
现在,正如您所看到的,物体越大,估计斑块元素所需的时间就越长。此外,它也无法处理物体在白色背景墙和桌子上投下的阴影。但是,更明显的阴影将被跟踪为属于该物体。
关注点
您可以扩展该算法来监控物体的边界框位置。如果它们在指定的时间内保持静止,您可以将该区域添加到背景场景图像中。这样,物体就成为了场景的一部分(例如,某个物体移动到场景中并保持静止)。