运动检测算法






4.95/5 (662投票s)
一些在视频流中检测运动的方法。
引言
在连续视频流中检测运动的方法有很多种。它们都基于将当前视频帧与前一帧进行比较,或者与我们将称为背景的内容进行比较。在本文中,我将尝试描述一些最常见的方法。
在描述这些算法时,我将使用 AForge.NET 框架,该框架在 Code Project 的其他文章中有所介绍:[1],[2]。所以,如果您熟悉它,那将非常有帮助。
演示应用程序支持以下类型的视频源
- AVI 文件(使用 Video for Windows,已包含互操作库);
- 更新网络摄像头中的 JPEG 图像;
- 来自不同网络摄像头的 MJPEG(运动 JPEG)流;
- 本地捕获设备(USB 摄像头或其他捕获设备,已包含 DirectShow 互操作库)。
算法
最常见的方法之一是将当前帧与前一帧进行比较。这在视频压缩中很有用,当您需要估算变化并只写入变化而不是整个帧时。但对于运动检测应用程序来说,它并不是最佳选择。所以,让我更详细地描述一下这个想法。
假设我们有一个名为 current frame(image
)的原始 24 bpp RGB 图像,它的灰度副本(currentFrame
)以及同样灰度化的前一个视频帧(backgroundFrame
)。首先,让我们找到这两个帧之间有微小差异的区域。为此,我们可以使用 Difference
和 Threshold
过滤器。
// create filters
Difference differenceFilter = new Difference( );
IFilter thresholdFilter = new Threshold( 15 );
// set backgroud frame as an overlay for difference filter
differenceFilter.OverlayImage = backgroundFrame;
// apply the filters
Bitmap tmp1 = differenceFilter.Apply( currentFrame );
Bitmap tmp2 = thresholdFilter.Apply( tmp1 );
在这一步,我们将得到一个图像,其中当前帧与前一帧在指定阈值上不同的地方显示为白色像素。这时已经可以计算像素数量了,如果数量大于预设的报警级别,我们就可以发出运动事件信号。
但大多数摄像头产生的图像都有噪点,因此我们会在根本没有运动的地方检测到运动。为了去除随机的噪点像素,我们可以使用 Erosion
过滤器,例如。这样,现在我们主要只剩下实际发生运动的区域。
// create filter
IFilter erosionFilter = new Erosion( );
// apply the filter
Bitmap tmp3 = erosionFilter.Apply( tmp2 );
最简单的运动检测器就准备好了!如果需要,我们可以突出显示运动区域。
// extract red channel from the original image
IFilter extrachChannel = new ExtractChannel( RGB.R );
Bitmap redChannel = extrachChannel.Apply( image );
// merge red channel with motion regions
Merge mergeFilter = new Merge( );
mergeFilter.OverlayImage = tmp3;
Bitmap tmp4 = mergeFilter.Apply( redChannel );
// replace red channel in the original image
ReplaceChannel replaceChannel = new ReplaceChannel( RGB.R );
replaceChannel.ChannelImage = tmp4;
Bitmap tmp5 = replaceChannel.Apply( image );
这是它的结果
![]() |
从上图可以看出这种方法的缺点。如果物体平滑移动,我们每帧都会得到微小的变化。因此,不可能获得整个移动的物体。当物体移动非常缓慢时,情况会变得更糟,此时算法根本不会给出任何结果。
还有另一种方法。可以将当前帧与视频序列的第一帧进行比较,而不是与前一帧进行比较。因此,如果初始帧中没有任何物体,将当前帧与第一帧进行比较将独立于其运动速度获得整个移动的物体。但是,这种方法有一个很大的缺点——如果初始帧中有一个汽车,但后来它消失了呢?是的,我们总会在汽车原来所在的位置检测到运动。当然,我们可以不时地更新初始帧,但在无法保证第一帧仅包含静态背景的情况下,它仍然无法给我们带来好的结果。但是,也可能出现相反的情况。如果我在房间的墙上贴一张照片呢?在更新初始帧之前,我会一直检测到运动。
最高效的算法是基于构建所谓的场景背景并将每一当前帧与背景进行比较。构建场景的方法有很多,但大多数都过于复杂。我将在此描述我构建背景的方法。它相当简单,并且可以很快实现。
与前一种情况一样,我们假设有一个名为 current frame(image
)的原始 24 bpp RGB 图像,它的灰度副本(currentFrame
)以及同样灰度化的背景帧(backgroundFrame
)。开始时,我们将视频序列的第一帧作为背景帧。然后,我们将始终将当前帧与背景帧进行比较。但这会给我们带来上述结果,我们显然不太想要。我们的方法是按照指定的量(我使用了每帧 1 个级别)将背景帧“移动”到当前帧。我们稍微移动背景帧,使其朝向当前帧的方向——我们每帧更改背景帧中像素的颜色一个级别。
// create filter
MoveTowards moveTowardsFilter = new MoveTowards( );
// move background towards current frame
moveTowardsFilter.OverlayImage = currentFrame;
Bitmap tmp = moveTowardsFilter.Apply( backgroundFrame );
// dispose old background
backgroundFrame.Dispose( );
backgroundFrame = tmp;
现在,我们可以使用与上面相同的方法。但是,让我稍微扩展它以获得更有趣的结果。
// create processing filters sequence
FiltersSequence processingFilter = new FiltersSequence( );
processingFilter.Add( new Difference( backgroundFrame ) );
processingFilter.Add( new Threshold( 15 ) );
processingFilter.Add( new Opening( ) );
processingFilter.Add( new Edges( ) );
// apply the filter
Bitmap tmp1 = processingFilter.Apply( currentFrame );
// extract red channel from the original image
IFilter extrachChannel = new ExtractChannel( RGB.R );
Bitmap redChannel = extrachChannel.Apply( image );
// merge red channel with moving object borders
Merge mergeFilter = new Merge( );
mergeFilter.OverlayImage = tmp1;
Bitmap tmp2 = mergeFilter.Apply( redChannel );
// replace red channel in the original image
ReplaceChannel replaceChannel = new ReplaceChannel( RGB.R );
replaceChannel.ChannelImage = tmp2;
Bitmap tmp3 = replaceChannel.Apply( image );
![]() |
现在看起来好多了!
还有一种基于这个想法的方法。与前几种情况一样,我们有一个原始帧以及它的灰度版本和背景帧的灰度版本。但在进一步处理之前,让我们将 Pixellate
过滤器应用于当前帧和背景。
// create filter
IFilter pixellateFilter = new Pixellate( );
// apply the filter
Bitmap newImage = pixellateFilter( image );
因此,我们得到了当前帧和背景帧的像素化版本。现在,我们需要像之前那样,将背景帧朝着当前帧移动。接下来的变化只是主要的处理步骤。
// create processing filters sequence
FiltersSequence processingFilter = new FiltersSequence( );
processingFilter.Add( new Difference( backgroundFrame ) );
processingFilter.Add( new Threshold( 15 ) );
processingFilter.Add( new Dilatation( ) );
processingFilter.Add( new Edges( ) );
// apply the filter
Bitmap tmp1 = processingFilter.Apply( currentFrame );
将 tmp1
图像与原始图像的红色通道合并后,我们将得到以下图像。
![]() |
也许它看起来不如上一个完美,但这种方法在性能优化方面具有巨大的潜力。
查看上一张图片,我们可以看到物体被一条曲线突出显示,该曲线代表了移动物体的边界。但有时更可能获得物体的矩形。不仅如此,如果我们不仅想突出显示物体,还想获得它们的数量、位置、宽度和高度,该怎么办?最近我一直在想:“嗯,这是可能的,但并不那么简单”。不要害怕,这很简单。可以使用我最近开发的成像库中的 BlobCounter
类来完成。使用 BlobCounter
,我们可以获得二进制图像中对象的数量、位置和尺寸。因此,让我们尝试应用它。我们将它应用于包含移动对象的二进制图像,即 Threshold
过滤器的结果。
BlobCounter blobCounter = new BlobCounter( ); ... // get object rectangles blobCounter.ProcessImage( thresholdedImage ); Rectangle[] rects = BlobCounter.GetObjectRectangles( ); // create graphics object from initial image Graphics g = Graphics.FromImage( image ); // draw each rectangle using ( Pen pen = new Pen( Color.Red, 1 ) ) { foreach ( Rectangle rc in rects ) { g.DrawRectangle( pen, rc ); if ( ( rc.Width > 15 ) && ( rc.Height > 15 ) ) { // here we can higligh large objects with something else } } } g.Dispose( );
这是这段小代码的结果。看起来不错。哦,我忘了。在我最初的实现中,有一些代码代替了这个注释,用于处理大对象。因此,我们可以看到对象上的一些小数字。
![]() |
[2006年6月14日] 有很多人抱怨用于更新背景图像的 MoveTowards
过滤器的概念很难理解。所以,我稍微想了想,是否可以将其更改为其他更清晰易懂的过滤器。解决方案是使用 AForge.Imaging 库 2.4 版本中提供的 Morph
过滤器。新过滤器有两个优点:
- 它更易于理解;
- 过滤器的实现效率更高,因此过滤器提供了更好的性能。
该过滤器的想法是保留源过滤器的指定百分比,并从叠加图像中添加缺失的百分比。因此,如果过滤器应用于源图像,其百分比值为 60%,则结果图像将包含 60% 的源图像和 40% 的叠加图像。使用大约 90% 的百分比值应用过滤器,可以使背景图像不断地向当前帧变化。
运动警报
为所有这些运动检测算法添加运动警报功能非常容易。每个算法都会计算一个包含当前帧与背景帧之间差异的二进制图像。因此,我们只需要计算此差异图像中白色像素的数量。
// Calculate white pixels private int CalculateWhitePixels( Bitmap image ) { int count = 0; // lock difference image BitmapData data = image.LockBits( new Rectangle( 0, 0, width, height ), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed ); int offset = data.Stride - width; unsafe { byte * ptr = (byte *) data.Scan0.ToPointer( ); for ( int y = 0; y < height; y++ ) { for ( int x = 0; x < width; x++, ptr++ ) { count += ( (*ptr) >> 7 ); } ptr += offset; } } // unlock image image.UnlockBits( data ); return count; }
对于某些算法,可以更简单地完成。例如,在斑点计数方法中,我们可以累加的不是白色像素的数量,而是每个检测到的对象的面积。然后,如果计算出的变化量大于预定值,我们可以触发警报事件。
视频保存
处理运动警报事件的方法有很多种:可以仅仅在视频周围画一个闪烁的矩形,或者播放声音来吸引注意。但是,当然,最有用的一种是根据运动检测来保存视频。在演示应用程序中,我使用了 AVIWriter
类,该类使用 Video for Windows
互操作来提供 AVI 文件保存功能。以下是使用该类编写一个绘制对角线的短 AVI 文件的示例。
SaveFileDialog sfd = new SaveFileDialog( ); if ( sfd.ShowDialog( ) == DialogResult.OK ) { AVIWriter writer = new AVIWriter( "wmv3" ); try { writer.Open( sfd.FileName, 320, 240 ); Bitmap bmp = new Bitmap( 320, 240, PixelFormat.Format24bppRgb ); for ( int i = 0; i < 100; i++ ) { bmp.SetPixel( i, i, Color.FromArgb( i, 0, 255 - i ) ); writer.AddFrame( bmp ); } bmp.Dispose( ); } catch ( ApplicationException ex ) { } writer.Dispose( ); }
注意:在这个小示例和演示应用程序中,我使用了 Windows Media Video 9 VCM 编解码器。
AForge.NET 框架
运动检测应用程序基于 AForge.NET 框架,该框架提供了此应用程序中使用的所有过滤器和图像处理例程。有关该框架的更多信息,您可以阅读 Code Project 上 的专门文章,或者访问 项目的首页,在那里您可以获取所有最新信息,参与讨论组,或提交问题或增强请求。
运动检测的应用
有些人不时地问我一个问题,这对我来说有点奇怪。这个问题是“运动检测器有什么应用?”。它们有很多用途,取决于想象力。最直接的应用之一是视频监控,但它并非唯一用途。自从这个应用程序的第一个版本发布以来,我收到了来自不同人的许多电子邮件,他们将这个应用程序应用于令人难以置信的事情。其中一些人有自己的文章,所以您可以看看:
- Ashish Derhgawen 的“激光手势识别”;
- Scott Hanselman 的“人人爱宝宝!网络摄像头和运动检测”;
- Brian Peek 的“运动感应、喷血万圣节骷髅”。
结论
我在这里只描述了想法。要在实际应用中使用这些想法,您需要优化其实现。我为了简化使用了图像处理库,它不是视频处理库。此外,该库允许我比从头开始编写优化解决方案更快地研究不同的领域。可以在源代码中找到一个小优化示例。
历史
- [20.04.2007] - 1.5
- 项目已转换为 .NET 2.0;
- 与 AForge.NET 框架集成;
- 运动检测器已更新,以利用 AForge.NET 的新功能来加速处理。
- [2006年6月15日] - 1.4 - 添加了第五种方法,基于 AForge.Imaging 库的 Morph 过滤器。
- [2006年4月8日] - 1.3 - 添加了运动警报和视频保存功能。
- [2005年8月22日] - 1.2 - 添加了第四种方法(使用 Blob Counter 获取对象的矩形)。
- [2005年6月1日] - 1.1 - 添加了对本地捕获设备和 MMS 流的支持。
- [2005年4月30日] - 1.0 - 初始发布。