检测无人机 - .NET 初学者 OpenCV 指南 (Emgu CV 3.2, Visual Studio 2017)。第 2 部分
检测无人机 - .NET 初学者 OpenCV 指南
概述
在第 1 部分中,您已经了解了 OpenCV 是什么,Emgu CV 包装器的作用,以及如何创建使用这两个库的 Visual Studio 2017 C# 项目。在本部分中,我将向您展示如何循环遍历从视频文件中捕获的帧。请查看第 1 部分以观看演示视频并查找有关示例项目的信息(所有有趣的内容都在 Program.cs 中 - 将此文件单独打开,因为本帖将显示其片段)...
步骤 1:从文件捕获视频
在任何处理发生之前,我们需要从视频文件中获取一帧。这可以通过使用 VideoCapture 类轻松完成(许多教程提到的是 Capture
类,但它在较新的 Emgu 版本中不可用)。
查看我们示例项目中的 Main
方法
private const string BackgroundFrameWindowName = "Background Frame";
// ...
private static Mat backgroundFrame = new Mat(); // Frame used as base for change detection
// ...
static void Main(string[] args)
{
string videoFile = @"PUT A PATH TO VIDEO FILE HERE!";
using (var capture = new VideoCapture(videoFile)) // Loading video from file
{
if (capture.IsOpened)
{
// ...
// Obtaining and showing first frame of loaded video
// (used as the base for difference detection)
backgroundFrame = capture.QueryFrame();
CvInvoke.Imshow(BackgroundFrameWindowName, backgroundFrame);
// Handling video frames (image processing and contour detection)
VideoProcessingLoop(capture, backgroundFrame);
}
else
{
Console.WriteLine($"Unable to open {videoFile}");
}
}
}
VideoCapture
有四种构造函数重载。我们使用的是接收 string
参数的重载,该参数是视频文件的路径或视频流。其他版本允许我们连接到摄像头。如果您正确设计程序,从文件输入切换到网络摄像头可能就像更改 new VideoCapture
调用一样简单!
一旦创建了 VideoCapture
实例,我们就可以通过访问 IsOpened
属性来确认打开是否成功(也许路径错误或缺少编解码器?)。
VideoCapture
提供了几种获取帧的方法,但我发现最方便的是通过调用 QueryFrame 方法。此方法返回 Mat
类实例(您已从第 1 部分中了解它)并移动到下一帧。如果找不到下一帧,则返回 null
。我们可以利用这一事实轻松地循环遍历视频。
步骤 2:加载和显示背景帧
我们的无人机检测项目基于查找背景帧与其他帧之间的差异。假设我们可以将从视频获取的第一帧视为背景,因此在创建 VideoCapture
对象后立即调用 QueryFrame
。
backgroundFrame = capture.QueryFrame();
加载背景后,我们可以通过调用 Imshow
方法来查看它的外观(您也从第 1 部分了解它)。
CvInvoke.Imshow(BackgroundFrameWindowName, backgroundFrame);
在视频中查找(有意义的!)差异总是像减去帧一样简单吗?不,并非如此。首先,背景可能不是静止的(想象一下无人机飞过被风吹动的树木,或者房间里的光线发生显著变化)。第二个挑战可能来自摄像机的移动。固定的背景和摄像机位置使我们的无人机检测任务足够简单,适合初学者的 OpenCV 教程,而且也并非完全不切实际。视频检测/识别通常在完全受控的环境中使用,例如工厂的一部分……OpenCV 能够处理更复杂的场景 - 您可以阅读有关背景减除技术和光流以获得提示...
步骤 3:循环遍历视频帧
我们知道可以使用 QueryFrame
获取单个帧图像(Mat
实例)并前进到下一帧,并且我们知道 QueryFrame
在无法继续时返回 null
。让我们利用这些知识来构建一个遍历帧的循环方法。
private static void VideoProcessingLoop(VideoCapture capture, Mat backgroundFrame)
{
var stopwatch = new Stopwatch(); // Used for measuring video processing performance
int frameNumber = 1;
while (true) // Loop video
{
rawFrame = capture.QueryFrame(); // Getting next frame (null is returned
// if no further frame exists)
if (rawFrame != null)
{
frameNumber++;
stopwatch.Restart();
ProcessFrame(backgroundFrame, Threshold, ErodeIterations, DilateIterations);
stopwatch.Stop();
WriteFrameInfo(stopwatch.ElapsedMilliseconds, frameNumber);
ShowWindowsWithImageProcessingStages();
int key = CvInvoke.WaitKey(0); // Wait indefinitely until key is pressed
// Close program if Esc key was pressed (any other key moves to next frame)
if (key == 27)
Environment.Exit(0);
}
else
{
capture.SetCaptureProperty(CapProp.PosFrames, 0); // Move to first frame
frameNumber = 0;
}
}
}
在每次循环迭代中,会从视频文件中抓取一帧。然后将其传递给 ProcessFrame
方法,该方法执行图像差异、降噪、轮廓检测和绘制(将在下一篇文章中详细讨论)……调用 ProcessFrame
被 System.Diagnostics.Stopwatch
用法包围 - 这样,我们可以测量视频处理性能。我的笔记本电脑处理每一帧仅用了大约1.5 毫秒 - 我告诉过您 OpenCV 很快!:)
如果 QueryFrame
返回 null
,则程序会通过调用 VideoCapture
实例上的 SetCaptureProperty
方法返回到第一帧(视频将重新处理)。
WriteFrameInfo
在帧的左上角放入有关其编号和处理时间的文本信息。ShowWindowsWithImageProcessingStages
确保我们可以看到当前(原始)帧、背景帧、中间帧和最终帧显示在单独的窗口中……这两种方法将在下一篇文章中展示。
除非在显示帧的任何窗口(不是控制台窗口!)中按下了Escape 键,否则 while
循环将永远运行。如果将 0 作为 WaitKey
参数传递,则程序等待直到按下某个键。这使您可以根据需要查看每个帧。如果将其他数字传递给 WaitKey
,则程序将等待直到按下按键或延迟结束。您可以使用它以指定的帧速率自动播放视频。
int fps = (int)capture.GetCaptureProperty(CapProp.Fps);
int key = CvInvoke.WaitKey(1000 / fps); // 40ms delay
警告:您在处理视频时可能会注意到的一点是,在文件中移动并不总是像将 CapProp.PosFrame
设置为您想要的数字那样容易。您的体验可能因格式而异。这是因为视频文件针对以自然速度正向播放进行了优化,并且帧可能不只是简单地作为图像序列存储。全高清 (1920x1080) 电影在每一帧中都有超过 200 万像素。现在,假设我们有 1 小时的视频,帧率为 30 FPS -> 3600 * 30 * 2,073,600 = 223,948,800,000。独立的帧压缩不足以压低这个数字!难怪有些人需要将他们的科学/软件职业生涯奉献给视频压缩……
好了,暂时就到这里 - 下一部分即将推出!
更新:第 3 部分已准备就绪!