OpenCV入门:显示和操作视频与运动数据






4.57/5 (12投票s)
解释如何使用一些 OpenCV 命令进行视频处理。
引言
开源计算机视觉库 (OpenCV) 提供免费的编程工具,用于处理图像、视频文件或摄像机捕捉到的运动数据等视觉输入。它包含许多现成的功能,可用于广泛的应用。由于本文作者刚刚开始探索这个领域,并且 OpenCV 提供了大量功能,因此本文内容将仅限于一小部分主题。
尽管可以找到关于如何使用 OpenCV 的说明和许多代码示例,但对我来说,有用的信息散布在网络各处,因此很难追踪。本文旨在提供更连贯的画面,并为一些 OpenCV 基础知识提供指导。
我主要对视频处理和运动分析感兴趣。因此,本文讨论了 OpenCV 用于加载和播放视频文件的函数,给出了一些如何操作这些数据的示例,并提供了(可能“不成熟的”)示例代码,展示了如何设置这些过程。尽管此处介绍的 OpenCV 函数调用是纯 C 代码,但我将它们打包到一个 C++ 类中。这可能看起来不太优雅,但我认为将大部分 OpenCV 特定代码放在一个地方很方便。为了能够实现 Windows GUI,我使用 Visual C++ 2010 Express Edition 在 OpenCV 代码周围构建了一个 Win32 程序。
此处和源代码文件中呈现的内容基于网络上的代码片段、OpenCV 安装附带的示例,以及部分基于 Gary Bradski 和 Adrian Kaehler 合著的《学习 OpenCV》一书。
为了创建可执行程序,必须安装 OpenCV2.3 库 (http://opencv.willowgarage.com),并且需要包含正确的库和头文件。这需要遵循一些说明,这些说明在 UsingOpenCV.cpp 源代码文件的顶部进行了描述。关于如何完成此操作的详细描述(包括图片)也可以在网络上找到(例如,搜索“在 VC++2010 中使用 OpenCV”)。
本文分为三个部分。第一部分将解释如何加载和播放视频文件。第二部分和第三部分将以此为基础,并添加关于像素操作、帧减法和光流的更多信息。
加载和播放视频文件
OpenCV 提供了几个用于加载和播放视频文件的函数。这些函数必须按照相对严格的顺序调用(还要确保您的计算机上安装了相应的视频编解码器)。
为了从视频文件中捕获帧(即单张图像),首先需要使用命令行 CvCapture* cvCreateFileCapture(const char* filename );
。它提供一个指向 CvCapture 结构的指针(如果出现问题则返回 NULL
)。在我编写的示例代码中,此函数嵌入在类方法 bool Video_OP::Get_Video_from_File(char* file_name);
中。
一旦有了有效的 CvCapture
对象,就可以使用函数 IplImage* cvQueryFrame(CvCapture capture)
开始抓取帧,该函数返回一个指向 IplImage
数组的指针。如何实现这一点将在下面的示例代码中解释。
为了访问视频文件的属性(例如,长度、帧宽等),请使用 cvGetCaptureProperty(CvCapture* capture, int property_id);
。关于如何应用此方法的示例代码可以在类方法 int Video_OP::Get_Frame_Rate();
和 int Video_OP::Get_Width();
中找到。
为了设置视频文件的各种属性(例如,起始帧),请使用 cvSetCaptureProperty(CvCapture* capture, int property_id, double value);
。关于如何更改起始帧的示例代码可以在类方法 int Video_OP::Go_to_Frame(int frame)
中找到。
Using the Code
加载和播放视频文件的主要步骤是:
- 通过调用
this->Get_Video_from_File(char* file_name);
来捕获视频文件cvCreateFileCapture();
。 - 调用
this->Play_Video(int from, int to);
,其中from
是起始帧,to
是停止帧(在下面的骨架代码中找到该方法的内部)。 - 确定视频帧率,创建窗口以显示图像,并将视频向前推进到您希望其开始的帧。
- 设置循环以处理视频文件的连续帧(或图像)。
- 通过调用
cvQueryFrame(CvCapture*);
抓取帧。 - 通过调用
cvShowImage(char*,IplImage);
在窗口中显示图像。 - 通过调用
cvWaitKey(int);
定义显示延迟。
文件(OpenCV_VideoMETH.cpp)中的示例具有与下面代码相同的结构,但也添加了一些行,提供了更多信息。
//
// code skeleton for playing a video
//
void Video_OP::Play_Video(int from, int to)
{
this->my_on_off = true;
int key =0;
int frame_counter = from;
// retrieves frames per second (fps); is used to define speed of presentation; see (A)
// or: (int) cvGetCaptureProperty( this->my_p_capture,CV_CAP_PROP_FPS );
int fps = this->Get_Frame_Rate();
// creates Window in which movie is displayed; see below (B)
cvNamedWindow( "video" , CV_WINDOW_AUTOSIZE );
// sets pointer to frame, where the video shall start from
// or: cvSetCaptureProperty (this->my_p_capture, CV_CAP_PROP_POS_FRAMES, (double) from );
this->Go_to_Frame(from);
// variable frame_counter is used to stop video after ‘last’ frame (= to) has been grabbed
int frame_counter = from;
// creates a loop, which is stopped after video reaches position
// of last frame (= to) or after my_on_off = false (see class method this->Stop_Video();)
while(this->my_on_off == true && frame_counter <= to) {
// gets a frame; my_p_capture pointer is initialized in this->Get_Video_from_File() method
this->my_grabbed_frame = cvQueryFrame(this->my_p_capture);
// check if frame is available
if( !this->my_grabbed_frame ) break;
// displays grabbed image; see above (B)
cvShowImage( "video" ,my_grabbed_frame);
frame_counter++;
// program waits until the time span of 1000/frame
// rate milliseconds has elapsed; see above (A)
key = cvWaitKey(1000 /fps);
}
//cleaning up
cvReleaseCapture( &my_p_capture );
cvDestroyWindow( "video" );
}
...
像素操作和帧减法
本节中的解释提供了关于一系列可用于图像操作的 OpenCV 命令的信息。我将介绍一种访问单个像素的简单方法,如何减去连续帧,如何在图像上绘制,并提供关于这些应用可能有什么用的想法。
Using the Code
下面代码的基本结构建立在第一节中所示的示例之上。我只解释了已添加的最重要的部分。有关详细信息和更好的理解,请查阅文件 OpenCV_VideoMETH.cpp。
下面代码示例中最重要的步骤是:
- 使用
cvQueryFrame(cvCapture);
抓取一帧,克隆此帧,并将其转换为灰度图像。 - 设置循环以处理连续帧。
- 抓取下一帧并将其转换为灰度图像。
- 从之后抓取的灰度帧中减去首先抓取的灰度帧(参见
cvAbsDiff(CvArr *src,CvArr* src2,CvArr *dst);
)。结果会得到一个灰度图像,在帧(t)和前一帧(t-1)之间像素颜色发生变化的位置具有不同程度的灰色(即,0 是黑色,255 是纯白色)。换句话说,这些变化提供了对所发生运动的估计。 - 通过调用
cvGet2D(CvArr*,int,int);
搜索图像数组中高于特定阈值(此处为 100)的像素值。 - 计算找到的像素的平均 x 位置和平均 y 位置,并在该位置绘制一个圆圈。
- 克隆抓取的帧,从而存储其信息以供下一次减法(即,帧(t)成为新的帧(t-1))。
如第一节所示,程序逐步(即逐帧)遍历视频文件,但这次它还将每一帧与其前一帧相减。这是一种简单的技术,可以粗略估计两帧之间发生的运动量。此外,还可以跟踪单个物体移动的路径(此处通过确定高于某个阈值的像素的平均位置来完成;参见上方和页面顶部的图片)。对于某些应用,这可能就足够了,但如果需要跟踪多个物体的路径,则只有更精细的方法才能完成这项工作。这里不会讨论(诚然,我还不熟悉它),但 OpenCV 库甚至为此类复杂问题提供了解决方案(例如,检测斑点的算法)。
//
// code skeleton for subtracting successive frames
//
void Video_OP::Subtract_Successive_Frames(int from, int to)
{
//............omitted code
//cvScalar contains arrays [0] to [3]; [1] to [3] correspond to the RGB model
// in gray-scale images (1 channel) val[0] returns brightness (see below B)
CvScalar color_channels;
// captures the first frame
this->my_grabbed_frame = cvQueryFrame(this->my_p_capture);
//...........
// clones first frame after it has turned into a grey-scale (i.e. 8 bit) picture
cvCvtColor(this->my_grabbed_frame, first_frame_gray, CV_RGB2GRAY );
cloned_frame =cvCloneImage(first_frame_gray);
// creates a loop, which is stopped by setting this->my_on_off = false;
//see this->Stop_Video() method;
while(this->my_on_off == true && frame_counter <= to) {
//...........
// captures the next frame
my_grabbed_frame = cvQueryFrame( my_p_capture);
//converts grabbed frame to grey frame
cvCvtColor(my_grabbed_frame, running_frame_gray, CV_RGB2GRAY );
// subtracts grey image (t) from grey image (t-1) and stores the result in subtracted frame
cvAbsDiff(cloned_frame,running_frame_gray,subtracted_frame );
// loops through image array,accesses pixels with cvGetAt or cvGet2D and calculates mean
for ( double i = 0; i < subtracted_frame->width; i++){
for ( double u = 0; u < subtracted_frame->height; u++)
{
// gets pixel color (gray-scale values ranging from 0 to 255 (=white));
// see above B
color_channels = cvGet2D(subtracted_frame,u,i);
// 100 is threshold; if color value is higher than 100,
//pixel will be used for calculations
if( color_channels.val[0] > 100){
pixel_count++;
// sums up x and y coordinates of pixels identified
sum_x += i;
sum_y += u;
}
}
}
// calculates mean coordinates by dividing counted pixels by the coordinates of each dimension
mean_x = sum_x/pixel_count;
mean_y = sum_y/pixel_count;
// if calculations yield useful values then draw a circle at that spot
if (mean_x < subtracted_frame->width && mean_y < subtracted_frame->height)
// cvCircle(IplImage,CvPoint,int radius,CvColor,int thickness, int line_type, int shift);
cvCircle(pixels_frame, cvPoint(mean_x,mean_y ),4,cvScalar(0,255,0,0), 2,8,0);
//...........
// displays results in different windows
cvShowImage("subtraction",subtracted_frame);
cvShowImage( "movie", my_grabbed_frame );
cvShowImage("Pixels identified",pixels_frame);
// clones current frame, so it can be the previous frame for the next step in the loop
cloned_frame = cvCloneImage(running_frame_gray);
key = cvWaitKey( 1000 / fps );
}
//...........
}
...
光流
在上面的示例中,我提供了过滤两帧之间像素颜色变化的代码。这些变化可用于评估运动量并找出有趣事件发生的位置。本节中的代码将以此为基础,并介绍一种替代(更精致和复杂)的方法来检测和分析运动。
光流是一种通过检测物体从一幅图像到另一幅图像的位置移动来追踪运动物体路径的方法。这主要通过像素亮度模式分析来完成。有几种计算光流的方法,但本文只提供基于 Lucas-Kanade 方法的代码。由于我不是底层数学和所应用函数内部机制的专家,因此我不会详细阐述这些细节。我只给出关于其工作原理的非常粗略的描述,并着重介绍创建可用程序必须调用的 OpenCV 函数。如果您需要更多信息,可以在专家文章或相关书籍中找到。
Lucas-Kanade 算法做出三个基本假设。首先,像素的亮度(即其颜色值)在从一帧移动到下一帧时保持不变。其次,一个物体(或一个像素区域)在从一帧移动到下一帧时不会移动很远。第三,处于同一小区域的像素属于一起并以相似的方向移动。
这种过程的一个问题是找到好的跟踪点。一个好的点应该是唯一的,这样才能很容易在电影的下一帧中找到它(即,角点通常满足这些要求)。OpenCV 函数 cvGoodFeaturesTrack()
(基于 Shi-Tomasi 算法)旨在解决这个问题,并且可以通过 cvFindCornerSubPix();
进一步细化所获得的结果,这将特征检测的精度提高到亚像素级别。
下面示例所需的函数有许多参数,这里将不作详细解释。请阅读 OpenCV_Video_METH.cpp 中的注释,查阅 OpenCV 相关书籍或 OpenCV 文档以获取更多信息。
Using the Code
下面代码示例中最重要的步骤是:
- 为 Shi Tomasi 算法定义最大点数(或角点数)(在
cvGoodFeaturesToTrack();
结束后将用找到的角点数覆盖)。还提供一个数组来存储函数找到的点。 - 为 Lucas-Kanade 算法定义图像金字塔。图像金字塔用于迭代估计光流,从或多或少粗糙的分析级别开始,并在每个步骤中进行细化。
- 调用
cvGoodFeaturesToTrack();
。该函数需要以 8 位或 32 位单通道格式分析图像。参数eigImage
和tempImage
用于临时存储算法的结果。类似地,一个CvPoint2D32f*
数组用于存储角点检测的结果。参数qualityLevel
定义了被认为是角点的点的最小特征值。参数minDistance
是函数将两个点视为独立的最小距离(以像素数表示)。mask
定义了感兴趣区域,blockSize
是用于函数内部计算的像素周围区域。 - 调用
cvFindCornerSubPix();
。传递要分析的图像数组和已用 Shi Tomasi 算法找到的值填充的CvPoint2D32f*
数组。参数win
定义了计算开始的窗口大小。CvTermCriteria
定义了算法终止的准则。 - 将跟踪到的特征(即点)绘制到图像上。
- 设置循环以处理视频帧。
- 调用
cvCalcOpticalFlowPyrLK();
。此函数最重要的参数是:前一帧、当前帧(在此示例中均为 8 位图像)、用于当前帧和前一帧的相同金字塔图像、已填充识别为良好特征的点的CvPoint2D32f*
数组,以及用于这些点新位置的另一个CvPoint2D32f*
数组。此外,winSize
定义了用于计算局部相干运动的窗口。参数level
设置用于图像金字塔的堆栈深度(如果设置为 0,则不使用),CvTermCriteria
定义了计算终止的准则。 - 在前一帧的点(存储在
CvPoint2D32f*
数组中)及其在当前帧中的新位置之间绘制一条线。 - 将当前帧的数据转换为循环下一步的“先前”数据。
//
// code skeleton for optical flow
//
void Video_OP::Calc_optical_flow_Lucas_Kanade(int from, int to)
{
//............omitted code
// Maximum number of points for Shi-Tomasi algorithm
const int MAX_COUNT = 500;
// CvPoint2D32f is an Array of 32 bit points to contain the tracked features
// after Shi Tomasi algorithm has been run; see below cvGoodFeaturesToTrack
CvPoint2D32f* points[2] = {0,0}, *swap_points;
char* status = 0;
//............
// Image pyramids are necessary for the Lucas-Kanade algorithm to work correctly
pyramid = cvCreateImage( cvGetSize(running_frame), IPL_DEPTH_8U, 1 );
prev_pyramid = cvCreateImage( cvGetSize(running_frame), IPL_DEPTH_8U, 1 );
// allocates memory for CvPoint2D32F Arrays
points[0] = (CvPoint2D32f*)cvAlloc(MAX_COUNT*sizeof(points[0][0]));
points[1] = (CvPoint2D32f*)cvAlloc(MAX_COUNT*sizeof(points[0][0]));
//............
// temporary image information and eigenvalues are stored in images below
IplImage* eig = cvCreateImage( cvGetSize(grey), 32, 1 );
IplImage* temp = cvCreateImage( cvGetSize(grey), 32, 1 );
// quality level (minimal eigenvalue for values to be used) should not exceed 1;
// typical values are 0.10 or 0.01
double quality = 0.01;
// min_distance guarantees that no two returned points are within a certain area
double min_distance = 10;
count = MAX_COUNT;
// finds features to track and stores them in array (here: points[0])
// void cvGoodFeaturesToTrack(cvArr* image, cvArr* eigImage, cvArr* tempImage,
// CvPoint2D32f* corners, int* cornerCount, double qualityLevel, double minDistance,
// cvArr* mask, int blockSize, int useHarris, double k)
cvGoodFeaturesToTrack(prev_grey, eig, temp, points[0], &count,quality, min_distance, 0, 3, 0, 0 );
// refines search for features obtained by routines like cvGoodFeaturesToTrack
// void cvFindCornerSubPix(CvArr* image,CvPoint2D32f* corners, int count,
//CvSize win, CvSize zero_zone, CvTermCriteria criteria)
cvFindCornerSubPix( prev_grey, points[0], count, cvSize(win_size,win_size), cvSize(-1,-1),
cvTermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,20,0.03));
cvReleaseImage( &eig );
cvReleaseImage( &temp );
// loop draws found features onto the first frame
for (int i = 0; i < count; i++)
{
CvPoint p1 = cvPoint( cvRound( points[0][i].x ), cvRound( points[0][i].y ) );
cvCircle(image,p1,1,CV_RGB(255,0,0),1,8,0);
}
cvShowImage("first frame",image);
// needed for cvCalcOpticalFlowPyrLK
status = (char*)cvAlloc(MAX_COUNT);
// loop, which is stopped by setting this->my_on_off = false
while(this->my_on_off == true && frame_counter <= to) {
//............
// calculates optical flow
//void cvCalcOpticalFlowPyrLK(const CvArr* imgA, const CvArr* imgB, CvArr* pyrA,
// CvArr* pyrB,CvPoint2D32f* featuresA,CvPoint2D32f* featuresB, int count,
// CvSize winSize, int level, char* status,
// float* track_error,CvTermCriteria criteria, int flags);
cvCalcOpticalFlowPyrLK( prev_grey, grey, prev_pyramid, pyramid, points[0], points[1], count,
cvSize(win_size,win_size), 5, status, 0,
cvTermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,20,0.03), flags );
// depicts flow by drawing lines between successive frames
for( int i=0; i < count; i++ ){
CvPoint p0 = cvPoint( cvRound( points[0][i].x ), cvRound( points[0][i].y ) );
CvPoint p1 = cvPoint( cvRound( points[1][i].x ), cvRound( points[1][i].y ) );
// draws circles onto prev_grey image and grey image
cvCircle(prev_grey,p0,1,CV_RGB(0,255,0),1,8,0);
cvCircle(grey,p1,1,CV_RGB(0,255,0),1,8,0);
// connects found features of successive frames
cvLine( image, p0, p1, CV_RGB(255,0,0), 2 );
}
//............
// grey image becomes new previous grey image for next step in loop
prev_grey = cvCloneImage(grey);
// similar operations as above, positions of pyramids and points-arrays are swapped
CV_SWAP( prev_pyramid, pyramid, swap_temp );
CV_SWAP( points[0], points[1], swap_points );
//............
// delays loop
key = cvWaitKey(40);
}// end of while loop
//............
}
关注点
如果您想了解这一切在实际应用中是什么样子,请下载 UsingOpenCV.exe 文件,安装必要的 DLL(参见 UsingOpenCV.cpp),加载示例视频,并查看应用不同命令时会发生什么。示例视频非常简单。它产生很少的噪声,因此结果“很漂亮”,但它完美地完成了它的创建目的。它很好地展示了代码的工作原理。
不能保证示例没有错误,并且有些部分肯定可以改进。我有时还定义了不必要的变量,希望能提供更清晰的信息。