OpenCV入门(二):实现鼠标事件,处理图像和创建视频片段






4.58/5 (9投票s)
介绍如何使用OpenCV写入视频,如何实现鼠标事件,以及展示一些图像处理命令。
引言
本文内容建立在之前的文章(见OpenCV入门:播放和处理视频文件 点击这里)之上,但我尽量以一种不需阅读第一部分就能理解的方式来组织。当然,本文可能比前一篇更简单,甚至可能更适合作为第一部分。尽管如此,我很有信心,感兴趣的读者不会感到困惑,并将此视为一个小问题。
正如标题所示,本文主要关注开源计算机视觉库(OpenCV),这是一个软件平台,提供了大量用于加载、保存、显示和处理图像和视频的高级编程工具。当然,在这种简短的教程中不可能描述该库提供的每个方面,因此我将只讨论一小部分主题。如果您需要更广泛的概述,对数学背景感兴趣,或者正在寻找本文涉及主题的更多细节,您可以在OpenCV相关书籍中找到答案,例如Gary Bradski和Adrian Kaehler合著的《Learning OpenCV》,我的一些“智慧”就来源于此。
OpenCV函数调用是纯C代码,但我将它们封装在两个C++类中(一个用于图像操作,另一个用于继承图像类方法的视频操作)。有些人可能会认为这种编程约定混合是失礼的,但我认为这是保持代码整洁并将大部分代码集中在一处的好方法。由于OpenCV库提供的创建图形界面的可能性有限,我通过创建Visual C++ 2010 Express版中的Win32程序,在OpenCV代码周围构建了一个Windows GUI。
如果您想将本文代码转换为可执行程序,您需要安装OpenCV库(请在此处获取最新版本,例如http://opencv.willowgarage.com),并为您的程序包含正确的lib文件和头文件。我在UsingOpenCV.cpp源文件顶部提供了所有内容的逐步说明。图形说明对于找到解决方案非常有帮助,而且幸运的是,在网上(例如,通过搜索“Using OpenCV in VC++2010”)可以找到提供截图的进一步解释。
在本教程的第一部分,我将简要介绍如何处理OpenCV中的事件(尤其是鼠标事件),然后将介绍一些可用于处理图像的OpenCV命令,最后我将展示如何将视觉输入转换为大多数标准视频播放器可以读取的视频格式。我不是本文内容的数学细节方面的专家。我能提供的是对所介绍功能如何工作以及如何在程序中使用它们的粗略概述。
使用OpenCV处理鼠标事件
当您在计算机鼠标上按下某个键或按钮,或者只是将鼠标指针移到窗口上时,Win32程序的消息循环会处理这些事件,并使其对程序员可用。对于有开发交互式软件经验的人来说,处理此类消息是日常工作。由于OpenCV有自己的创建窗口的命令,因此它也提供了自己的程序来为这些窗口设置消息循环。在大多数情况下,OpenCV特有的事件处理代码易于实现,并且应该优先于通过标准消息循环处理来自OpenCV窗口的消息(我没有尝试过,但肯定可以做到)。下面的代码涉及鼠标事件,但为了完整性,我也简要讨论了键盘输入的处理。
键盘事件可以非常轻松地处理。命令cvWaitKey(timespan);
等待一段时间以按下某个键,并将该键作为整数值(ASCII码)返回。因此,如果您想处理键盘事件,请设置一个while循环,插入命令行key = cvWaitKey(timespan);
,然后通过检查变量key
的值来处理键盘输入,例如, if(key == ‘q’){ 执行某些操作}
。请注意,cvWaitKey();
还会使程序等待指定的时间(例如,cvWaitKey(100)使程序等待100毫秒)。这对于以特定帧速率处理图像是必需的(例如,在上一篇文章以及下面我讨论保存视频的代码部分查找示例)。
鼠标事件需要更多的关注,尽管它们也不难实现。它们包含两个部分
首先,您必须调用cvSetMouseCallback (const char* window_name, CvMouseCallback my_Mouse_Handler, void* param);
来注册一个回调。此函数的第一个参数是要附加回调的窗口名称(使用cvNamedWindow(“window name”,0);
创建的窗口)。第二个参数是回调函数本身,第三个参数例如是应用回调的图像。
之后,您必须设置一个鼠标处理函数(cvSetMouseCallback
的第二个参数)。这个函数,我在我的程序中命名为 my_mouse_Handler (int events, int x, int y, int flags, void* param)
,接受5个参数。
第一个也是最重要的参数是一个整数变量,它可以具有以下值之一(从0到9):CV_EVENT_MOUSE_MOVE
(=鼠标指针在指定窗口上移动),CV_EVENT_LBUTTONDOWN
(=按下左鼠标按钮),CV_EVENT_RBUTTONDOWN
(=按下右鼠标按钮),CV_EVENT_MBUTTONDOWN
(=按下中键),CV_EVENT_LBUTTONUP
,CV_EVENT_RBUTTONUP
,CV_EVENT_MBUTTONUP
(=释放相应按钮后发生的事件),CV_LBUTTONDBLCLK
,CV_LBUTTONDBLCLK
,和CV_LBUTTONDBLCLK
(=当用户双击相应按钮时)。
回调函数的第二个和第三个参数是鼠标指针的x(=水平)和y(=垂直)位置,以窗口左上角为参考点(0,0)。
第四个参数在您想在鼠标事件期间访问附加信息时很有用。CV_EVENT_FLAG_LBUTTON
,CV_EVENT_FLAG_RBUTTON
,CV_EVENT_MBUTTON
检查用户是否按下了相应按钮。如果您想知道在鼠标指针移动时是否按下了按钮(例如,拖放操作),这可能很有用。CV_EVENT_FLAG_CTRLKEY
,CV_EVENT_FLAG_SHIFTKEY
,CV_EVENT_FLAG_ALTKEY
检查在鼠标事件期间是否按下了Ctrl,Shift或Alt键。
最后一个参数是一个void指针,用于任何需要的附加信息。在下面的代码示例中,我使用此参数获取事件处理程序正在操作的图像的指针。
使用代码
如上所述,我将OpenCV特有的代码封装在两个类中。第一个类包含一些图像操作和鼠标处理函数。第二个类继承了图像类的函数,但也包含视频处理代码。请注意,在一个类中,回调函数及其变量必须定义为静态的。
我编写的程序处理视频。它提供了对视频数据的访问;然后加载视频的第一帧并在其自己的窗口中显示。程序实现了处理器的鼠标操作是在此窗口中显示的图像上完成的。
最重要的步骤是
- 通过调用
Get_Video_from_File(char* file_name);
来捕获视频文件,该函数我在Video_OP
类中定义。请在下面的代码示例中查找该方法的具体内容。它也应该让您“感受”到如何使用一些OpenCV命令(例如cvNamedWindow();
)。
//
// code skeleton of Video_OP::Get_Video_from_File(char*) method
//
// methods needs file pathname as char string
bool Video_OP::Get_Video_from_File(char* file_name)
{
// checks if filename is available
if(!file_name)
return false;
// OpenCV command for capturing video files; returns pointer to CvCapture structure
// (defined in the variables section of the class)
my_p_capture = cvCreateFileCapture(file_name);
// checks if capturing was successful; e.g. fails if required video codec
// is not installed on machine
if (!my_p_capture) return false;
// gets first frame (= IplImage*) of video for accessing video properties
this->my_grabbed_frame = cvQueryFrame(my_p_capture);
// gets width and height of frame loaded with cvQueryFrame()
// assigns these properties to variable of the type CvSize
this->captured_size.width = (int)cvGetCaptureProperty(my_p_capture,
CV_CAP_PROP_FRAME_WIDTH);
this->captured_size.height = (int)cvGetCaptureProperty(my_p_capture,
CV_CAP_PROP_FRAME_HEIGHT);
// creates window in which first frame (= my_grabbed_frame) will be displayed
// window is named “choose area”
cvNamedWindow(“choose area”,CV_WINDOW_AUTOSIZE)
// displays first frame in Window “choose area”
cvShowImage(“choose area”,my_grabbed_frame);
// gets the video’s number of frames
this->my_total_frame = (int) cvGetCaptureProperty(my_p_capture,CV_CAP_PROP_FRAME_COUNT);
// sets up the mouse callback; method below invokes cvSetMouseCallback(char*,CvMouseCallback,void*);
this->Set_Mouse_Callback_for_Image(this->my_grabbed_frame);
return true;
}
Image_OP
类的my_Mouse_Handler();
函数中处理鼠标事件。下面的代码示例没有描述所有可能的鼠标事件。它只显示了绘制矩形到图像所需的鼠标事件。请注意,用于静态方法my_Mouse_Handler();
的静态变量必须在类外部定义。//
// code skeleton of the static method Image_OP::my_MouseHandler()
//
// see above for a description of the callback function’s parameters
void Image_OP::my_Mouse_Handler(int events, int x, int y, int flags, void* param)
{
IplImage *img_orig;
// Operations are mostly done on a cloned image, in order
// to restore original settings, if operations need to be repeated
IplImage *img_clone;
// param is used for getting access to the image, for which
// the mouse callback had been implemented
img_orig = (IplImage*) param;
int x_ROI =0, y_ROI =0 , wi_ROI =0, he_ROI =0;
switch(events)
{
// user presses left button somewhere within the image
// ( = first frame of video in this case)
case CV_EVENT_LBUTTONDOWN:
{
// saves mouse pointer coordinates (x,y) send by button pressed message
// in static variable (CvPoint)
my_point = cvPoint(x, y);
}
break;
// event, when mouse moves over image
case CV_EVENT_MOUSEMOVE:
{
// mouse pointer moves over specified window with left button pressed
if(flags == CV_EVENT_FLAG_LBUTTON )
{
// makes a copy of original image
img_clone = cvCloneImage(img_orig);
// draws green [see CV_RGB(red,green,blue)=> single values
// ranging from 0 -255] rectangle
// onto cloned image using point coordinates
// from CV_EVENT_LBUTTONDOWN as one corner
// and coordinates retrieved here
// as the other corner; here: 1 = thickness;
// 8 = line_type; 0 = shift;
cvRectangle(img_clone, my_point,cvPoint(x,y),
CV_RGB(0,255,0),1,8,0);
// shows cloned image with rectangle drawn on it
cvShowImage(“choose area”,img_clone);
}
}
break;
// user releases left button
case CV_EVENT_LBUTTONUP:
{
// clones original image again
img_clone = cvCloneImage(img_orig);
// checks position of starting point
// stored in my_point (see CV_EVENT_LBUTTONWDOWN)
// in relation to end point, in order
// to avoid negative values
if(my_point.x > x)
{
x_ROI = x;
wi_ROI = my_point.x - x;
}
else
{
x_ROI = my_point.x;
wi_ROI = x - my_point.x;
}
if(my_point.y > y)
{
y_ROI = y;
he_ROI = my_point.y - y;
}
else
{
y_ROI = my_point.y;
he_ROI = y - my_point.y;
}
// stores coordinates of Region of Interest
// in static variable my_ROI
my_ROI.x = x_ROI;
my_ROI.y = y_ROI;
my_ROI.width = wi_ROI;
my_ROI.height = he_ROI;
// selects region of interest (= ROI) in cloned image;
// needed for cvNot operation
// cvRect function requires upper, left corner
// of ROI, its width and its height
cvSetImageROI(img_clone,cvRect(x_ROI,
y_ROI,wi_ROI, he_ROI));
// inverts color information of image, in order
// to make selected area clearly visible; in this case
// source (first argument) and destination are the same
cvNot(img_clone, img_clone);
// “turns off“ region of interest
cvResetImageROI(img_clone);
// shows cloned image in window
cvShowImage(“choose area”, img_clone);
}
break;
} // end of switch
}
用于处理图像的OpenCV函数选集
本教程的第二部分主要关注一些(通常是简单的)OpenCV图像处理命令。在使用复杂的方法(如光流;参见本教程的第一部分)来检测或跟踪运动时,通常通过“平滑”图像(=或处理过的帧)来获得更好的结果,以消除由噪声和相机伪影产生的异常值。
OpenCV提供了五种基本的平滑操作,可以通过命令cvSmooth(IplImage* source, IplImage* destination, int smooth_type, int param1 = 3, int param2 = 0, double param3 = 0, double param4 =0);
调用。我认为前两个参数代表输入和输出图像是很清楚的。第三个参数更有趣,它是一个占位符,可以接受五个不同的值(这些值也决定了参数param1到param4的含义)。在接下来的部分,我将概述第三个参数的可能值。如果您需要更多细节,请查阅书籍(如Gary Bradski和Adrian Kaehler的《Learning OpenCV》)或相关专家文章。
smooth_type
CV_BLUR
,例如,计算一个中心像素周围区域内所有像素的平均颜色值(区域由param1
和param2
指定)。
CV_BLUR_NO_SCALE
的作用与CV_BLUR
相同,但没有除法运算来创建平均值。
CV_MEDIAN
执行类似的操作,唯一的区别是它计算指定区域的中间值。
CV_GAUSSIAN
更复杂,并且基于高斯函数(=正态分布)进行平滑操作。param1
和param2
再次定义了算法应用的区域。param3是高斯函数的sigma值(如果未指定,将自动计算),如果为param4提供了值,则在水平方向(=在这种情况下为param3)和垂直方向将有不同的sigma值。
CV_BILATERAL
与高斯平滑类似,但对相似度更高的像素赋予更高的权重。
本文的示例代码中只实现了一种上述“平滑”函数。Image_OP
类的方法Blur(int square_size, IplImage*, IplImage*)
(参见页面顶部的图像)执行基于像素正方形区域平均值的简单模糊。通过编译本教程附带的源代码可以演示更改此区域大小如何影响“模糊”。只需加载一个电影,选择“Blur”选项按钮,然后移动滑块控件的条。注意:如果您打算使用其他类型的平滑函数(如CV_GAUSSIAN
),可能会出现问题,因为这些函数不接受滑块返回的所有值。
膨胀和腐蚀
另一种去除图像噪声,同时分离或连接不同区域的方法基于膨胀和腐蚀。对于这两种变换,OpenCV都提供了相应的函数(cvDilate()
和cvErode()
)。这些函数有一个内核(一个带有中心锚点的小正方形或圆形区域)在图像上运行。在此过程中,计算内核的最大(=膨胀)或最小(=腐蚀)像素值,并用这个最大值或最小值替换锚点下的图像像素。
因为这两个函数执行相似的任务,所以它们接受相同的参数。因此,我只为cvErode(IplImage* src, IplImage* dest,IplConvKernel* kernel = NULL, int iterations = 1);
讨论它们。前两个参数是源图像和目标图像,第三个参数是指向IplConvKernel
结构的指针,最后一个参数是算法执行的迭代次数。这里不讨论使用IplConvKernel
结构创建自己的内核,因此将使用标准的(3x3正方形内核)内核。
同样,这两个函数都作为Image_OP
类的函数实现,并与主窗口滑块控件的行为相关联。只需加载一个视频,然后单击“Erode”(或“Dilate”)选项按钮。移动滑块的条将改变Image_OP::Erode()
或Image_OP::Dilate()
方法的参数iter
(=迭代次数)。根据您选择的两个选项之一,图像将显示扩展的亮区域或扩展的暗区域。
绘制轮廓
在本节中,我将展示一些能够提取图像轮廓的代码。在OpenCV中,轮廓被表示为构成曲线的点序列。为了过滤这些点位置,OpenCV提供了函数cvFindContours(IplImage*, CvMemStorage*, CvSeq**,int headerSize,CvContourRetrievalMode,CvChainApproxMethod)
。
第一个参数应该是一个8位单通道图像,它将被解释为二值图像(所有非零像素为1)。第二个参数是用于处理动态内存分配的内存块链表。第三个参数代表指向链表的指针,其中存储找到的点(轮廓)。
接下来的参数是可选的,并且不会在此详细讨论,因为它们未在代码示例中使用。第四个参数可以简单地设置为sizeof(CvContour)
。第五个参数包含四个选项:CV_RETR_EXTRENAL
=提取最外层轮廓;CV_RETR_LIST
=是标准选项,提取所有轮廓;CV_RETR_CCOMP
=提取轮廓并将其组织成两级层次结构;CV_RETR_TREE
=生成嵌套轮廓的层次结构。第六个参数决定轮廓的近似方式(请在OpenCV书籍中查找)。
需要执行的唯一一步操作就是显示图像的轮廓,可以在Image_OP::Draw_Contours()
方法中找到(见下文)。与前面讨论的方法类似,方法的其中一个参数(这里是定义阈值的第一个参数)与程序主窗口的滑块相关联。
使用代码
//
// code skeleton for drawing contours
//
void Image_OP::Draw_Contours(int threshold, IplImage* orig_image, IplImage* manipulated_img)
{
//... omitted code
// creates linked list of memory blocks
CvMemStorage* mem_storage = cvCreateMemStorage(0);
// defines pointer to a sequence of stored contours
CvSeq* contours =0;
// allocates memory for a gray_scale image
IplImage* gray_img = cvCreateImage(cvSize(orig_img->width,orig_img->height)
IPL_DEPTH_8U,1);
int found_contours =0;
//... omitted code
// creates window to display results of operations
cvNamedWindow(“contours only”);
// turns image into gray scale image
cvCvtColor(orig_img, gray_img, CV_RGB2GRAY),
// defines a threshold for operations;
// uses binary information (only 0 and 1 as pixel values);
// depending on the threshold type pixels will be
// set to 0, to the source value or to the max value
// here: CV_THRESH_BINARY => destination
// value = if source > threshold then MAX else 0
// Parameters => 1) source- and 2) destination image
// 3) threshold, 4) MAX value (255 in 8 bit grayscale) 5) threshold type
cvThreshold (gray_img, gray_img, threshold, 255, CV_THRESH_BINARY);
// returns number of found contours
// Parameters => 1) Image, that is used as space
// to perform calculations 2) => stores recorded contours
// 3) pointer to contours stored in memory
// 4) final parameters are optional
found_contours = cvFindContours(gray_img, mem_storage, &contours);
// sets all elements to NULL
cvZero(gray_img);
if(contours)
{
// draws contours: Parameters => Image to draw on,
// 2) pointer to sequence where contours were stored
// 3) color of contours, 4) color of contours marked
// as a hole; here: same color as other contours
// 5) specifies how many contours of different
// levels are drawn; rest is optional and not used here
cvDrawContours(gray_img,cvScalarAll(255),cvScalarAll(255),100);
}
//... omitted code
cvShowImage(“contours only“, gray_img);
// release memory
cvReleaseImage(gray_img);
cvReleaseMemStorage(&mem_storage);
}
将运动数据保存为视频文件
本节内容与我在上一篇OpenCV教程中介绍的内容密切相关。下面代码示例的基本结构已在其中(参见Video_OP::Play_Video()
方法)。
因此,我对这个主题的介绍非常简短。我只想谈谈FourCC表示法,它被开发用来标识数据格式,并广泛用于访问AVI视频编解码器。OpenCV宏CV_FOURCC
提供了此功能,并接受一个表示特定编解码器的四字符代码(例如,CV_FOURCC(’D’,’I’,’V’,’X’)
)。成功应用CV_FOURCC
的前提是,当然,相应的视频编解码器已安装在您使用的机器上。
使用代码
- 通过调用
this->Get_Video_from_File(char* file_name);
捕获视频文件。 - 调用
Video_OP::Write_Video(int from, int to, char* path);
(参见下面的代码)。 - 通过调用
cvCreateVideoWriter(path, CV_FOURCC(’M’,’J’,’P’,’G’);
创建视频写入器。 - 设置循环以处理视频文件的连续帧(或图像)。
- 通过调用
cvQueryFrame(CvCapture*);
抓取帧。 - 通过调用
cvWriteFrame(CvVideoWriter *,IplImage*);
将帧(=图像)添加到视频文件中。 - 通过使用
cvWaitKey(int);
定义演示延迟(此处:仅用于演示目的)。
//
// code skeleton for writing a video
//
void Video_OP::Write_Video(int from, int to, char* path)
{
this->my_on_off = true;
int key =0;
int frame_counter = from;
// retrieves frames per second (fps);
// is used to define speed of presentation
// and frame rate for video writer; see (A) & (B)
// here: the same as the input video
int fps = this->Get_Frame_Rate();
// creates window in which movie is displayed;
cvNamedWindow( "write to avi", CV_WINDOW_AUTOSIZE );
// sets pointer to position, where the video shall start from
this->Go_to_Frame(from);
// variable frame_counter stops video after “last” frame
// (= to) has been grabbed
int frame_counter = from;
// creates cvVideoWriter; parameters: (1) filepath of video
// (2) video codec name for AVI videofile format;
// codec must be installed on the machine
// (3) frames per second; (4) size of frames
// (5) optional(1 => color;0 => grayscale)
CvVideoWriter *video_writer = cvCreateVideoWriter(path,
CV_FOURCC('M','J','P','G'),fps,size);
// 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;
// adds frame to video file
cvWriteFrame(video_writer,my_grabbed_frame);
// displays grabbed image
cvShowImage( "write to avi" ,my_grabbed_frame);
// keeps track of the frames already processed
frame_counter++;
// makes program wait until the time span of 1000/frame rate
// milliseconds has elapsed; see above (B)
key = cvWaitKey(1000 /fps);
if (key == ’q’) //breaks loop when ‘q’ is pressed
break;
}
//release memory
cvReleaseCapture( &my_p_capture );
cvDestroyWindow( "write to avi");
cvReleaseVideoWriter(&video_writer);
}
...
其他注意事项
此处介绍的大多数方法和操作都可以组合使用。这意味着对视频文件第一帧执行的图像操作可以限制在鼠标选择的区域内。此外,如果您单击程序主窗口上的“GO”按钮,这些修改将应用于视频的所有帧。
源文件中有一些未在此讨论的方法。例如,Video_OP
类包含一个将电影转换为单张图像的方法,以及一个做相反事情的方法,即从单张图像创建电影。如果您尝试后者,您还会找到一些代码,演示如何通过调用Win32 API函数FindFirstFile()
和FindNextFile()
来检索文件夹中的文件。
OpenCV提供自己的代码来创建滑块(或滑动条)并为其设置消息处理程序。我更喜欢使用Win32 GUI滑块,因为对我来说更方便。尽管如此,您会在源文件中找到一些代码,展示如何使用OpenCV自己的滑块控件。顺便说一句,程序及其源文件还演示了如何在窗口上放置按钮、滑块、文本框和选项按钮,并在Win32程序中使用它们。
不能保证提供的代码没有错误(并且并非所有异常都已处理),但我希望它能帮助那些正在寻找本文所讨论主题的指导的人。