视觉监控实验室 第二部分






4.80/5 (13投票s)
2007年5月7日
6分钟阅读

82288
跟踪系统的基本描述。
引言
在上一篇文章中,我们描述了监控系统的基本结构并概述了代码。
本文描述了前一篇文章中跟踪系统示例所使用的算法。其目的是展示简单启发式算法在受限环境中的有效性。
但首先,让我们了解演示所需的图像处理基础知识。
图像处理
为了简化模型,我们只考虑灰度图像。
直方图
8 位图像中的每个像素可以有 256 个不同的值(0-255),其中 0 表示完全黑色,255 表示完全白色。直方图是某个值中有多少像素的计数。例如
上图显示了以下图像的直方图
仅通过观察直方图,就可以了解图像的很多信息。例如,如果峰值接近 0,则表示图像较暗;另一方面,如果峰值接近 255,则表示图像较亮。
差分滤波器
测量两幅图像之间的差异意味着取图像 A 中一个像素的值,取图像 B 中相同像素(相同位置)的值,然后计算它们之间的绝对差。如果两个像素的值相等,则差异将为 0,表示完全黑色。例如,右侧图像和左侧图像之间的差分滤波器
结果是
阈值滤波器
阈值是一种帮助我们“删除”图像中不需要的像素,只关注我们想要的像素的技术。对于图像中的每个像素,如果像素值高于某个阈值,则将其转换为 255(白色),否则将其转换为 0(黑色)。例如,阈值为 120
转换为
有关该主题的更多信息,请阅读[6]。
现在,让我们开发一个朴素算法。
朴素方法
function naiveApproach1(currentFrame)
{
difference <-- differenceFilter(currentFrame, oldFrame)
for each (pixel != 0) in difference
{
object <-- createObject(pixel, id)
objects.Add(object)
id++
}
return objects
}
这种方法将当前帧中每个变化的像素视为我们要跟踪的移动对象的指示。从这个意义上说,它是一个简单的运动检测算法。以下是上述方法可能存在的问题
- 我们希望忽略移动的背景,即落叶、照明变化、脏像素等。这种方法会将每一个微小的变化都视为移动对象。
- 计算量大,可能出现太多移动对象,处理它们需要时间。
- 重要对象的大小不可能只有一个像素,很明显移动对象要大得多。
- 我们不“记住”从一帧到另一帧跟踪的对象。
因此需要进行改进。
function naiveApproach2(currentFrame, threshold)
{
difference <-- differenceFilter(currentFrame, oldFrame)
blackAndWhite <-- for each pixel in difference
{
if (pixel > threshold)
pixel <-- 255
else
pixel <-- 0
}
for each (pixel == 255) in blackAndWhite
{
object <-- createObject(pixel, id)
objects.Add(object)
id++
}
return objects
}
通过使用阈值来“删除”变化不大的像素,我们设法忽略了落叶等缓慢移动的物体。
但是,我们仍然将每个新像素注册为一个新对象,因此我们可能希望将“接近的”像素彼此“连接”起来,形成一个单独的对象。
function naiveApproach3(currentFrame, threshold)
{
difference <-- differenceFilter(currentFrame, oldFrame)
blackAndWhite <-- for each pixel in difference
{
if (pixel > threshold)
pixel <-- 255
else
pixel <-- 0
}
blobs <-- connectPixels(blackAndWhite)
for each blob
{
object <-- createObject(pixel, id)
objects.Add(object)
id++
}
return objects
}
合并接近的像素是一个很大的改进。connectPixels
算法实际上称为“连通分量标记算法”,您可以在[2]中了解更多信息。
naiveApproach3
算法实际上与上一篇文章中给出的示例算法代码中的运动分割部分相似。但是,naiveApproach3
需要对图像进行更多操作以降低噪声。
上述方法,称为背景减法,有许多变体,但其核心保持不变;取两幅图像,使用差分滤波器分析它们之间的差异,结果就是你的移动对象。
让我们尝试实现对象分类。
对象分类
function naiveApproach4(currentFrame, threshold)
{
difference <-- differenceFilter(currentFrame, oldFrame)
blackAndWhite <-- for each pixel in difference
{
if (pixel > threshold)
pixel <-- 255
else
pixel <-- 0
}
blobs <-- connectPixels(blackAndWhite)
for each blob
{
object <-- createObject(pixel, id)
if (object.totalSizeInPixels > 500)
object.Class <-- Person
else
object.Class <-- Junk
objects.Add(object)
id++
}
return objects
}
这种方法是朴素的,因为它假设任何足够大(即 500 像素或更多)的东西都是一个人。真正的问题是这种假设是否正确。这取决于对环境的期望,即环境实际包含什么,例如大堂入口场景将只包含人,因此这种假设可能是正确的,另一方面,这种假设在高速公路上将不正确。
有更复杂的方法来分类对象,例如通过知道人通常比宽更高,我们可以利用这种区别。
仍然留下一个问题,每个新对象都被赋予一个新 ID,但仍然没有跟踪。
跟踪
实现跟踪将使用一个非常简单但出人意料地成功的对象重叠启发式算法[3]。
这个想法背后的假设是,在视频拍摄中通常每秒有 10 帧以上。这意味着同一个对象将出现在与前一帧中出现的位置非常接近的位置,因此如果对象在连续帧序列中重叠,则意味着它们是同一个对象。
function naiveApproach5(currentFrame, threshold)
{
difference <-- differenceFilter(currentFrame, oldFrame)
blackAndWhite <-- for each pixel in difference
{
if (pixel > threshold)
pixel <-- 255
else
pixel <-- 0
}
blobs <-- connectPixels(blackAndWhite)
for each blob
{
newObject <-- createObject(pixel, id)
if (newObject.totalSizeInPixels > 500)
newObject.Class <-- Person
else
newObject.Class <-- Junk
if (objects.ContainOverlapingObject(newObject)
update old object position with the current position of newObject
else
objects.Add(newObject)
}
return objects
}
这里的跟踪有一个本文不打算解决的主要问题;如果一个人走在树后(树遮挡了他),这意味着他第二次出现时将被视为一个全新的对象,带有新的 ID。遮挡问题对任何监控系统都有重大影响,但在此示例中,我们假设视野清晰。
上述代码不完整,因此需要添加一些内容
- 有两个列表,一个包含待处理对象,即尚未被跟踪的对象,另一个包含正在被跟踪的活动对象。
- 待处理对象是刚被发现的移动对象。
- 在对象被视为活动对象之前,算法会等待至少 4 帧,以查看它是否继续移动。这允许忽略垃圾像素和小的移动对象,例如叶子。
- 如果一个活动对象停止移动 10 帧,则将其从列表中删除。
Iterate over all blobs, ignoring Unknown blobs.
if pendingBlobs contains blob then
increase the size of pending blob by 1.
update blob location.
if pending blob frame time >= 4 then move blob to activeBlobs and
give them id.
else if activeBlobs contains blob
update blob location.
initialize times not touched to 0.
else
add blob to pending blobs.
Iterate over all pending blobs
delete blob if it wasn't updated.
Iterate over all active blobs
delete blob if it wasn't updated for 10 frames.
return blobs in activeBlobs list.
可以将代码分为三个主要部分,这些部分对应于上一篇文章中学习的部分。请注意,没有任何环境建模技术的实现,有关该主题的信息,欢迎阅读[4]。
使用代码
代码本身是我们在这里所做工作的翻译,代码写在 SimpleTrackingSystemExample
项目中,并编译为 DLL。
运动分割、对象分类和对象跟踪这三个部分都是实现的接口,代码中真正有趣的部分是它们如何连接在一起。
你们可能都记得视觉监控系统的总体结构。我们想要封装这个结构,但让程序员选择具体细节,这里就用到了很棒的建造者模式[5]来帮助我们。
BaseImageProcess
在其构造函数中接受三个参数
/// <summary>
/// A constructor
/// </summary>
/// <param name="ms">The Background Subtraction algorithm.</param>
/// <param name="classObj">The Classify Blobs algorithm.</param>
/// <param name="tracker">The Tracking algorithm.</param>
/// <exception cref="ArgumentNullException">One of the arguments is null.
</exception>
public BaseImageProcess(IMotionSegmentation ms, IClassifyObjects classObj,
ITrackObjects tracker) {...
程序员可以更改参数以创建不同类型的图像处理,但处理本身保持不变
// Find regions of moving objects.
ICollection<ExtendedBlob> blobs = motionSegmentation.execute(frame);
// Classify blobs.
ICollection<ExtendedBlob> classifiedBlobs = classifyObjects.execute(blobs);
// Track blobs.
ICollection<ExtendedBlob> finalLocation =
trackObjects.execute(classifiedBlobs);
return finalLocation;
当然,你不必在你的跟踪算法中使用 BaseImageProcess
,你所要做的就是实现 IImageProcess
接口。
接下来做什么
在各种场景中测试项目附带的示例算法,检查当你在人们从右向左行走时使用它会发生什么,检查当你尝试跟踪落叶时会发生什么。
测试限制,检查它在哪里失败,检查它在哪里成功。
结论
- 我们学习了不同的图像处理技术。
- 我们尝试开发可用于视觉监控系统的简单算法。
参考文献
特别感谢
感谢 Anat Kravitz 的帮助。