OpenCV入门
开始准备
安装完OpenCV开发包后,我们的首要任务自然是立即用它来做一些有趣的事情。为此,还需要搭建编程环境。
在VisualStudio开发环境中,我们需要先创建一个项目(project),然后配置好它的各项设置,以使OpenCV开发包中的highgui.lib,cxcore.lib,ml.lib和cv.lib库能被正确链接,并保证编译器的预处理器能搜索到"OpenCV…/opencv/*/include"目录下的各个头文件。OpenCV开发包中有许多"include"目录,它们一般都位于"C:/programfiles/opencv/cv/include","…/opencv/cxcore/include","…/opencv/ml/include"和"…/opencv/otherlibs/highgui"路径下。完成这些工作后,便可新建一个C程序文件并编写你的***个程序。
注意:有一些重要的头文件可以使工作变得更轻松。在头文件"…/opencv/cxcore/include/cxtypes.h"和"cxmisc.h"中,包含许多有用的宏定义。使用这些宏,仅用一行程序便可完成结构体和数组的初始化、对链表进行排序等工作。在编译时,有几个头文件非常重要,它们分别是:机器视觉中所要用到的"…/cv/include/cv.h"和"…/cxcore/include/cxcore.h";I/O操作中所要用到的"…/otherlibs/highgui/highgui.h";机器学习中所要用到的"…/ml/include/ml.h"。
初试牛刀——显示图像
OpenCV开发包提供了读取各种类型的图像文件、视频内容以及摄像机输入的功能。这些功能是OpenCV开发包中所包含的HighGUI工具集的一部分。我们将使用其中的一些功能编写一段简单的程序,用以读取并在屏幕上显示一张图像。如例2-1所示。
例2-1:用于从磁盘加载并在屏幕上显示一幅图像的简单OpenCV程序
   #include"highgui.h" 
    
   intmain(intargc,char**argv){ 
       IplImage*img=cvLoadImage(argv[1]); 
       cvNamedWindow("Example1",CV_WINDOW_AUTOSIZE); 
       cvShowImage("Example1",img); 
       cvWaitKey(0); 
       cvReleaseImage(&img); 
       cvDestroyWindow("Example1"); 
   }
当以上程序编译后,我们就可以在命令行模式下通过输入一个参数执行它。执行时,该程序将向内存加载一幅图像,并将该图像显示于屏幕上,直至按下键盘的任意一个键后它才关闭窗口并退出程序。下面我们将对以上代码做逐行分析,并仔细讲解每个函数的用法以帮助读者理解这段程序。
   IplImage*img=cvLoadImage(argv[1]);
该行程序的功能是将图像文件加载至内存。cvLoadImage()函数是一个高层调用接口,它通过文件名确定被加载文件的格式;并且该函数将自动分配图像数据结构所需的内存。需要指出的是,cvLoadImage()函数可读取绝大多数格式类型的图像文件,这些类型包括BMP,DIB,JPEG,JPE,PNG,PBM,PGM,PPM,SR,RAS和TIFF。该函数执行完后将返回一个指针,此指针指向一块为描述该图像文件的数据结构(IplImage)而分配的内存块。IplImage结构体将是我们在使用OpenCV时会最常用到的数据结构。OpenCV使用IplImage结构体处理诸如单通道(single-channel)、多通道(multichannel)、整型的(integer-valued)、浮点型的(floating-point-valued)等所有类型的图像文件。
   cvNamedWindow("Example1",CV_WINDOW_AUTOSIZE);
cvNamedWindow()函数也是一个高层调用接口,该函数由HighGUI库提供。cvNamedWindow()函数用于在屏幕上创建一个窗口,将被显示的图像包含于该窗口中。函数的***个参数指定了该窗口的窗口标题(本例中为"Example1"),如果要使用HighGUI库所提供的其他函数与该窗口进行交互时,我们将通过该参数值引用这个窗口。
cvNamedWindow()函数的第二个参数定义了窗口的属性。该参数可被设置为0(默认值)或CV_WINDOW_AUTOSIZE,设置为0时,窗口的大小不会因图像的大小而改变,图像只能在窗口中根据窗口的大小进行拉伸或缩放;而设置为CV_WINDOW_AUTOSIZE时,窗口则会根据图像的实际大小自动进行拉伸或缩放,以容纳图像。
   cvShowImage("Example1",img);
只要有一个与某个图像文件相对应的IplImage*类型的指针,我们就可以在一个已创建好的窗口(使用cvNamedWindow()函数创建)中使用cvShowImage()函数显示该图像。cvShowImage()函数通过设置其***个参数确定在哪个已存在的窗口中显示图像。cvShowImage()函数被调用时,该窗口将被重新绘制,并且图像也会显示在窗口中。如果该窗口在创建时被指定CV_WINDOW_AUTOSIZE标志作为cvNamedWindow()函数的第二个参数,该窗口将根据图像的大小自动调整为与图像一致。
   cvWaitKey(0);
【17~18】
cvWaitKey()函数的功能是使程序暂停,等待用户触发一个按键操作。但如果将该函数参数设为一个正数,则程序将暂停一段时间,时间长为该整数值个毫秒单位,然后继续执行程序,即使用户没有按下任何键。当设置该函数参数为0或负数时,程序将一直等待用户触发按键操作。
   cvReleaseImage(&img);
一旦用完加载到内存的图像文件,我们就可以释放为该图像文件所分配的内存。我们通过为cvReleaseImage()函数传递一个类型为IplImage*的指针参数调用该函数,用以执行内存释放操作。对cvReleaseImage()函数的调用执行完毕后,img指针将被设置为NULL。
   cvDestroyWindow("Example1"); 
***,可以销毁显示图像文件的窗口。cvDestroyWindow()函数将关闭窗口,并同时释放为该窗口所分配的所有内存(包括窗口内部的图像内存缓冲区,该缓冲区中保存了与img指针相关的图像文件像素信息的一个副本)。因为当应用程序的窗口被关闭时,该应用程序窗口所占用的一切资源都会由操作系统自动释放,所以对一些简单程序,不必调用cvDestroyWindow()或cvReleaseImage()函数显式释放资源。但是,养成习惯每次都调用这些函数显式释放资源总是有好处的。
尽管现在已经有了这个可供我们随意把玩的简单程序,但我们并不能就此停滞不前。我们的下一个任务将要去创建一个几乎与例2-1一样非常简单的程序--编写程序读取并播放AVI视频文件。完成这个新任务后,需要开始深入思考一些问题了。
第二个程序——播放AVI视频
使用OpenCV播放视频,几乎与使用它来显示图像一样容易。播放视频时只需要处理的新问题就是如何循环地顺序读取视频中的每一帧,以及如何从枯燥的电影视频的读取中退出该循环操作。具体如例2-2所示。
例2-2:一个简单的OpenCV程序,用于播放硬盘中的视频文件
    #include "highgui.h" 
    
    int main( int argc, char** argv ) { 
        cvNamedWindow( "Example2", CV_WINDOW_AUTOSIZE ); 
        CvCapture* capture = cvCreateFileCapture( argv[1] ); 
        IplImage* frame; 
        while(1) { 
            frame = cvQueryFrame( capture ); 
            if( !frame ) break; 
            cvShowImage( "Example2", frame ); 
            char c = cvWaitKey(33); 
            if( c == 27 ) break; 
        } 
        cvReleaseCapture( &capture ); 
        cvDestroyWindow( "Example2" ); 
    }
【18】
这里我们还是通过前面的方法创建一个命名窗口,在"例2"中,事情变得更加有趣了。
    CvCapture* capture = cvCreateFileCapture( argv[1] );
函数cvCreateFileCapture()通过参数设置确定要读入的AVI文件,返回一个指向CvCapture结构的指针。这个结构包括了所有关于要读入AVI文件的信息,其中包含状态信息。在调用这个函数后,返回指针所指向的CvCapture结构被初始化到所对应AVI文件的开头。
    frame = cvQueryFrame( capture );
一旦进入while(1)循环,我们便开始读入AVI文件,cvQueryFrame的参数为CvCapture结构的指针。用来将下一帧视频文件载入内存(实际是填充或更新CvCapture结构中)。返回一个对应当前帧的指针。与cvLoadImage不同的是,cvLoadImage为图像分配内存空间,而cvQueryFrame使用已经在cvCapture结构中分配好的内存。这样的话,就没有必要通过cvReleaseImage()对这个返回的图像指针进行释放,当CvCapture结构被释放后,每一帧图像所对应的内存空间即会被释放。
    c = cvWaitKey(33); 
    if( c == 27 ) break;
当前帧被显示后,我们会等待33 ms。 [11]如果其间用户触发了一个按键,c会被设置成这个按键的ASCII码,否则,c会被设置成-1。如果用户触发了ESC键(ASCII 27),循环被退出,读入视频停止。否则33 ms以后继续执行循环。
需要指出的是,在这个简单的例子程序中,我们没有使用任何准确的方法来控制视频帧率。我们只是简单的通过cvWaitKey来以固定时间间隔载入帧图像,在一个精度要求更高的程序中,通过从CvCapture结构体中读取实际帧率是一个更好的方法!
    cvReleaseCapture( &capture );
退出循环体(视频文件已经读入结束或者用户触发了Esc键)后,我们应该释放为CvCapture结构开辟的内存空间,这同时也会关闭所有打开的AVI文件相关的文件句柄。
视频播放控制
完成了前面的程序以后,现在我们可对其加以改进,进一步探索更多可用的功能。首先会注意到,例2-2所实现的AVI播放器无法在视频播放时进行快速拖动。我们的下一个任务就是通过加入一个滚动条来实现这个功能。     【19】
HighGUI工具包不仅提供了我们刚刚使用的一些简单的显示函数,还包括一些图像和视频控制方法。其中一个经常使用的就是滚动条,滚动条可以使我们方便地从视频的一帧跳到另外一帧。我们通过调用cvCreateTrackbar()来创建一个滚动条,并且通过设置参数确定滚动条所属于的窗口。为了获得所需的功能,只需要提供一个回调函数。具体如例2-3所示。
例2-3:用来添加滚动条到基本浏览窗口的程序:拖动滚动条,函数onTrackSlide()便被调用并被传入滚动条新的状态值
    #include "cv.h" 
    #include "highgui.h" 
    
    int             g_slider_position   = 0; 
    CvCapture*  g_capture           = NULL; 
    
    void onTrackbarSlide(int pos) { 
        cvSetCaptureProperty( 
            g_capture, 
            CV_CAP_PROP_POS_FRAMES, 
            pos 
        ); 
    } 
    
    int main( int argc, char** argv ) { 
        cvNamedWindow( "Example3", CV_WINDOW_AUTOSIZE ); 
        g_capture = cvCreateFileCapture( argv[1] ); 
    
        int frames = (int) cvGetCaptureProperty( 
            g_capture, 
            CV_CAP_PROP_FRAME_COUNT 
        ); 
        if( frames!= 0 ) { 
          cvCreateTrackbar( 
              "Position", 
              "Example3", 
              &g_slider_position, 
              frames, 
              onTrackbarSlide 
         ); 
        } 
        IplImage* frame; 
        // While loop (as in Example 2) capture & show video 
        ... 
        // Release memory and destroy window 
        ... 
        return(0); 
    }
从本质上说,这种方法是通过添加一个全局变量来表示滚动条位置并且添加一个回调函数更新变量以及重新设置视频读入位置。我们通过一个调用来创建滚动条和确定回调函数 。下面让我们看看细节。
    int g_slider_position = 0; 
    CvCapture* g_capture  = NULL;
首先为滚动条位置定义一个全局变量。由于回调函数需要使用CvCapture对象,因此我们将它定义为全局变量。为了使我们的程序可读性更强,我们在所有全局变量名称前面加上g_。
    void onTrackbarSlide(int pos) { 
        cvSetCaptureProperty( 
        g_capture, 
        CV_CAP_PROP_POS_FRAMES, 
        pos 
    );
现在我们定义一个回调函数,使其在滚动条被拖动时调用。滚动条的位置会被作为一个32位整数以参数形式传入。
后面我们会常常看到函数cvSetCaptureProperty()被调用,同时与之配套的函数cvGetCaptureProperty()也经常会被调用。这些函数允许我们设置(或查询)CvCapture对象的各种属性。在本程序中我们设置参数CV_CAP_PROP_POS_ FRAMES(这个参数表示我们以帧数来设置读入位置,如果我们想通过视频长度比例来设置读入位置,我们可以通过用AVI_RATIO代替FRAMES来实现)。***,我们把新的滚动条位置作为参数传入。因为HighGUI是高度智能化的,它会自动处理一些问题,比如滚动条对应位置不是关键帧,它会从前面一个关键帧开始运行并且快进到对应帧,而不需要我们来处理这些细节问题。
    int frames = (int) cvGetCaptureProperty( 
        g_capture, 
        CV_CAP_PROP_FRAME_COUNT 
    );
正如前面所说,当需要从CvCapture结构查询数据时,可使用cvGetCaptureProperty函数。在本程序中,我们希望获得视频文件的总帧数以对滚动条进行设置(具体实现在后面)。
    if( frames!= 0 ) { 
      cvCreateTrackbar( 
          "Position", 
          "Example3", 
          &g_slider_position, 
          frames, 
          onTrackbarSlide 
      ); 
    }
前面的代码用来创建滚动条,借助函数cvCreateTrackbar(),我们可设置滚动条的名称并确定滚动条的所属窗口。我们将一个变量绑定到这个滚动条来表示滚动条的***值和一个回调函数(不需要回调函数时置为空,当滚动条被拖动时触发)。仔细分析,你会发现一点:cvGetCaptureProperty()返回的帧数为0时,滚动条不会被创建。这是因为对于有些编码方式,总的帧数获取不到,在这种情况下,我们只能直接播放视频文件而看不到滚动条 。
值得注意的是,通过HighGUI创建的滚动条不像其他工具提供的滚动条功能这么全面。当然,也可以使用自己喜欢的其他窗口开发工具包来代替HighGUI,但是HighGUI可以较快地实现一些基本功能。
***,我们并没有将实现滚动条随着视频播放移动功能的代码包含进来,这可作为一个练习留给读者。
一个简单的变换
很好,现在你已经可以使用OpenCV创建自己的视频播放器了,但是我们所关心的是计算机视觉,所以下面会讨论一些计算机视觉方面的工作。很多基本的视觉任务包括对视频流的滤波。我们将通过修改前面程序,实现随着视频的播放而对其中每一帧进行一些简单的运算。 一个简单的变化就是对图像平滑处理,通过对图像数据与高斯或者其他核函数进行卷积有效的减少图像信息内容。OpenCV使得这个卷积操作非常容易。我们首先创建一个窗口"Example4-out"用来显示处理后的图像。然后,在我们调用 cvShowImage()来显示新捕捉的图像以后,我们可以计算和在输出窗口中显示平滑处理后的图像。具体如例2-4所示。 例2-4:载入一幅图像并进行平滑处理 #include "cv.h" #include "highgui.h" void example2_4( IplImage* image ) // Create some windows to show the input // and output images in. // cvNamedWindow( "Example4-in" ); cvNamedWindow( "Example4-out" ); // Create a window to show our input image // cvShowImage( "Example4-in", image ); // Create an image to hold the smoothed output // IplImage* out = cvCreateImage( cvGetSize(image), IPL_DEPTH_8U, 3 ); // Do the smoothing // cvSmooth( image, out, CV_GAUSSIAN, 3, 3 ); // Show the smoothed image in the output window // cvShowImage( "Example4-out", out ); // Be tidy // cvReleaseImage( &out; ); // Wait for the user to hit a key, then clean up the windows // cvWaitKey( 0 ); cvDestroyWindow( "Example4-in" ); cvDestroyWindow( "Example4-out" ); } ***个cvShowImage()调用跟前面的例子中没有什么不同。在下面的调用,我们为另一个图像结构分配空间。前面依靠cvCreateFileCapture()来为新的帧分配空间。事实上,cvCreateFileCapture()只分配一帧图像的空间,每次调用时覆盖前面一次的数据(这样每次调用返回的指针是一样的)。在这种情况下,我们想分配自己的图像结构空间用来存储平滑处理后的图像。***个参数是一个CvSize结构,这个结构可以通过cvGetSize(image)方便地获得;***个参数说明了当前图像结构的大小。第二个参数告诉了我们各通道每个像素点的数据类型,***一个参数说明了通道的总数。所以从程序中可以看出,当前图像是3个通道(每个通道8位),图像大小同image。 平滑处理实际上只是对OpenCV库函数的一个调用:我们指定输入图像,输出图像,光滑操作的方法以及平滑处理的一些参数。在本程序中,我们通过使用每个像素周围3*3区域进行高斯平滑处理。事实上,输入、输出图像可以是相同的,这将会使我们的程序更加有效,不过我们为了介绍cvCreateImage()函数而没有这样使用。 现在我们可以在我们新窗口中显示处理后的图像然后释放它:cvReleaseImage()通过给定一个指向IplImage*的指针来释放与图像对应的内存空间。 【23~24】一个复杂一点的变换
前面的工作很棒,我们学会了做一些更有趣的事情。在例2-4中,我们选择了重新创建一个IplImage结构,并且在新建的结构中写入了一个单个变换的结果。正如前面所提到的,我们可以以这样一种方式来应用某个变换,即用输出来覆盖输入变量,但这并非总是行得通的。具体说来,有些操作输出的图像与输入图像相比,大小、深度、通道数目都不一样。通常,我们希望对一些原始图像进行一系列操作并且产生一系列变换后的图像。
在这种情况下,介绍一些封装好的函数是很有用的,这些函数既包含输出图像内存空间的分配,同时也进行了一些我们感兴趣的变换。例如,可以考虑对图像进行缩放比例为2的缩放处理[Rosenfeld80]。在OpenCV中,我们通过函数cvPyrDown()来完成上述功能。这在很多重要的视觉算法中都很有用。我们在例2-5中执行这个函数。
例2-5:使用cvPyrDown() 创建一幅宽度和高度为输入图像一半尺寸的图像
    IplImage* doPyrDown( 
      IplImage* in, 
      int filter = IPL_GAUSSIAN_5x5
    ) { 
    
        // Best to make sure input image is divisible by two. 
        // 
        assert( in->width%2 == 0 && in->height%2 == 0 ); 
    
        IplImage* out = cvCreateImage(  
      
            cvSize( in->width/2, in->height/2 ), 
            in->depth, 
            in->nChannels 
        ); 
        cvPyrDown( in, out ); 
        return( out ); 
    };
细心的读者会发现,我们分配新的图像空间时是从旧的图像中读取所需的信息。在OpenCV中,所有的重要数据结构都是以结构体的形式实现,并且以结构体指针的形式传递。OpenCV中没有私有数据!现在让我们来看一个与前类似但已加入Canny边界检测的例子[Canny86](见例2-6)。在这个程序中,边缘检测器产生了一个与输入图像大小相同但只有一个通道的图像。      【24~25】
例2-6:Canny边缘检测将输出写入一个单通道(灰度级)图像
    IplImage* doCanny( 
        IplImage*   in, 
        double      lowThresh, 
        double      highThresh, 
        double      aperture 
    ) { 
        If(in->nChannels != 1) 
            return(0); //Canny only handles gray scale images 
    
        IplImage* out = cvCreateImage( 
            cvSize( cvGetSize( in ), 
            IPL_DEPTH_8U,  
            1 
        ); 
        cvCanny( in, out, lowThresh, highThresh, aperture ); 
        return( out ); 
    };
前面的实现使得我们更加容易进行一些连续的变换。例如,如果我们想缩放图像两次,然后在缩放后的图像中寻找边缘,我们可以很简单的进行操作组合,具体如                例2-7所示。
例2-7:在一个简单的图像处理流程中进行两次缩放处理与Canny边缘检测
    IplImage* img1 = doPyrDown( in, IPL_GAUSSIAN_5x5 ); 
    IplImage* img2 = doPyrDown( img1, IPL_GAUSSIAN_5x5 ); 
      
    IplImage* img3 = doCanny( img2, 10, 100, 3 ); 
    // do whatever with 'img3' 
    // 
    ... 
    cvReleaseImage( &img1 ); 
    cvReleaseImage( &img2 ); 
    cvReleaseImage( &img3 );
注意,对前面的函数进行嵌套调用是一个很不好的主意,因为那样,内存空间的释放将会成为一个很困难的问题。如果想偷懒儿,我们可以将下面这行代码加入我们的每个封装。
    cvReleaseImage( &in );
这种"自清理"模式会很干净,但是它同样有一些缺点。比如,如果想对其中一个中间步骤的图像进行处理,便无法找到此图像的入口。为了解决前面的问题,代码可以简化(如例2-8所示)。            【25】
例2-8:通过每个独立阶段释放内存来简化例2-7中图像处理流程
    IplImage* out; 
    out = doPyrDown( in, IPL_GAUSSIAN_5x5 ); 
    out = doPyrDown( out, IPL_GAUSSIAN_5x5 ); 
    out = doCanny( out, 10, 100, 3 ); 
    
    // do whatever with 'out' 
    // 
    ... 
    cvReleaseImage ( &out );
对于自清理方法的***一个提醒:在OpenCV中,我们必须确认被释放的空间必须是我们显式分配的。参考前面从cvQueryFrame()返回的IplImage*指针,这个指针指向的结构是CvCapture结构的一部分,而CvCapture在初始时就已经被初始化了而且只初始化一次。通过调用cvReleaseImage()释放IplImage*会产生很多难以处理的问题。前面这个例子主要是想说明,虽然内存垃圾处理在OpenCV中很重要,但我们只需要释放自己显式分配的内存空间。
从摄像机读入数据
在计算机世界里,"视觉"这个词的含义非常丰富。在一些情况下,我们要分析从其他地方载入的固定图像。在另外一些情况下,我们要分析从磁盘中读入的视频文件。在更多的情况下,我们想处理从某些摄像设备中实时读入的视频流。
OpenCV-- 更确切地说,OpenCV中的HighGUI模块-- 为我们提供了一种简单的方式来处理这种情况。这种方法类似于读取AVI文件,不同的是,我们调用的是cvCreateCameraCapture(),而不是cvCreateFileCapture()。后面一个函数参数为摄像设备ID而不是文件名。当然,只有存在多个摄像设备时这个参数才起作用。默认值为-1,代表"随机选择一个";自然,它更适合当有且仅有一个摄像设备的情况(详细内容参考第4章)。
函数cvCreateCameraCapture()同样返回相同的CvCapture*指针,这使得我们后面可以使用完全类似于从视频流中获取帧的方法。当然,HighGUI做很多工作才使得摄像机图像序列看起来像一个视频文件,但是我们不需要考虑那些问题。当我们需要处理摄像机图像序列时我们只需要简单地从摄像机获得图像,像视频文件一样处理。为了便于开发工作,大多程序实时处理的程序同样会有视频文件读入模式,CvCapture结构的通用性使得这非常容易实现。具体如例2-9所示。  【26】
例2-9:capture结构初始化后,从视频文件或摄像设备读入图像没有区别
    CvCapture* capture; 
    
    if( argc==1 ) { 
        capture = cvCreateCameraCapture(0); 
    } else { 
        capture = cvCreateFileCapture( argv[1] ); 
    } 
    assert( capture != NULL ); 
    
    // Rest of program proceeds totally ignorant 
    ...
由此可见,这种处理是很理想的。
写入AVI视频文件
在很多程序中,我们想将输入视频流或者捕获得的图像序列记录到输出视频流中,OpenCV提供了一个简洁的方法来实现这个功能。就像可以创建一个捕获设备以便每次都能从视频流中提取--帧一样,我们同样可以创建一个写入设备以便逐帧将视频流写入视频文件。实现这个功能的国数是cvcreatevideowriter()。 当输出设备被创建以后,我们可以通过调用cvwriteFrame{)逐帧将视频流写入文 件。写入结束后,我们调用cvReleasevideeNriter()来释放资源。例2-10描述了一个简单的程序。这个程序首先打开一个视频文件,读取文件内容,将每一帧图像转换为对数极坐标格式(就像你的眼睛真正看到的,在第6章会有详细描述),最后将转化后的图像序列写人新的视频文件中。 例2-10:一个完整的程序用来实现读入一个彩色视频文件并以灰度格式输出这个视频文件
// Convert a video to grayscale
// argv[1]: input video file
// argv[2]: name of new output file
//
#include “cv.h”
#include “highgui.h”
main( int argc, char* argv[] ) {
CvCapture* capture = 0;
capture = cvCreateFileCapture( argv[1] );
if(!capture){
return -1;
}
IplImage *bgr_frame=cvQueryFrame(capture);//Init the video read
double fps = cvGetCaptureProperty (
capture,
CV_CAP_PROP_FPS
);
CvSize size = cvSize(
(int)cvGetCaptureProperty( capture, CV_CAP_PROP_FRAME_WIDTH),
(int)cvGetCaptureProperty( capture, CV_CAP_PROP_FRAME_HEIGHT)
);
CvVideoWriter *writer = cvCreateVideoWriter(
argv[2],
CV_FOURCC(‘M’,‘J’,‘P’,‘G’),
fps,
size
);
IplImage* logpolar_frame = cvCreateImage(
size,
IPL_DEPTH_8U,
3
);
while( (bgr_frame=cvQueryFrame(capture)) != NULL ) {
cvLogPolar( bgr_frame, logpolar_frame,
cvPoint2D32f(bgr_frame->width/2,
bgr_frame->height/2),
40,
CV_INTER_LINEAR+CV_WARP_FILL_OUTLIERS );
cvWriteFrame( writer, logpolar_frame );
}
cvReleaseVideoWriter( &writer; );
cvReleaseImage( &logpolar_frame; );
cvReleaseCapture( &capture; );
return(0);
}
仔细阅读这个程序,会发现它使用了一些我们很熟悉的函数。首先,打开一个视频文件:通过cvQueryFrame()国数读和视频;然后,我们使用cvcektcapture-Property()来获得视频流的各种重要属性。打开一个视频文件进行此操作,并将 .各帧图像转换为对数极坐标格式,将转换后的图像逐帧写人视频文件,直到读人结 束。最后释放各种资源,程序结束。 在对函数cvcreatevViaeowricer()进行调用时,有L个参数是我们必须了解的。第一个参数是用来指定新建视频文件的名称。第二个参数是视频压缩的编码格式。 -目前有很多流行的编解码器格式,但是无论采用哪种格式,都必须确保自己的电脑 中有这种编解码器(编解码器的安装独立于OpenCV)。在本程序中,我们选用比较流行的MjPG编码格式,OpenCV用宏cy_FouRce()来指定编码格式,cv_FOURCC()包含4个字符参数。这4个字符构成了编解码器的“4字标记”,每个编解码器都有一个这样的标记,MotionJPEG编码格式的四字标记就是MJPG,所以如此指定宏的4个字符参数cv_FouRcc('M'npP',G'。后面两个参数用来指定播放的帧率和视频图像的大小。在本程序中,我们将它们设置成原始视频文件(彩色视频文件)的帧率和图像的大小。 小结 在开始学习第3章前,我们需要先总结一下前面的知识,并展望后面要讲的东西。我们已经知道,OpenCV为程序开发提供了很多简便易用的接口,可以用来载人图像、从磁盘或老从摄像设备中捕获视频。我们也知道,OpenCV库中包含很多基本的函数用来处理这些图像。但是更多更强大的功能我们还没有看到。它可以对抽象数据类型进行更多更复杂的处理,这些对解决实际的视觉问题有很大的帮助。 在后面的几章里,我们会更深入地了解opencV的一些基础结构并对界面相关函数和图像数据类型做更详细的分析。我们会系统地讲解一些基本的图像处理操作,
小结
在开始学习第3章前,我们需要先总结一下前面的知识,并展望后面要讲的东西。我们已经知道,OpenCV为程序开发提供了很多简便易用的接口,可以用来载入图像、从磁盘或者从摄像设备中捕获视频。我们也知道,OpenCV库中包含很多基本的函数用来处理这些图像。但是更多更强大的功能我们还没有看到。它可以对抽象数据类型进行更多更复杂的处理,这些对解决实际的视觉问题有很大的帮助。
在后面的几章里,我们会更深入地了解OpenCV的一些基础结构并对界面相关函数和图像数据类型做更详细的分析。我们会系统地讲解一些基本的图像处理操作,并在后面讲解一些高级的图像变换。***,我们会探索更多专业的应用,如摄像机标定、跟踪、识别。OpenCV为这些应用提供了许多API。准备好了吗?让我们开始吧! 
练习
如果还没有在本机上安装OpenCV,请先下载并安装。浏览目录,在docs目录中,可以找到index.htm文件,它链接到库文件的一些主要文档。下面我们进一步了解一些库文件,Cvcore 包含一些基本数据结构与算法,cv 包含图像处理和视觉的一些算法,ml由一些机器学习和聚类算法组成,otherlics/highgui 包含一些 输入/输出函数。_make文件夹下包含OpenCV的工程文件,程序实例的代码在samples文件夹里。
1. 进入目录…/opencv/_make。在Windows环境下,打开解决方案opencv.sln,在Linux环境下,打开对应的makefile,分别建立debug和release版的库文件。这会花一些时间,但是后面会用到这些库文件和动态链接库。
2. 进入目录…/opencv/samples/c/。建立一个工程(project),导入并编译lkdemo.c(这是一个运动跟踪程序实例)。安装上摄像机以后运行程序,选中运行窗口,键入"r"进行跟踪初始化,也可以在视频位置上单击鼠标以添加跟踪点。还可以通过键入"n"切换为只观察点(而非图像)。再次键入"n"可在"夜间"和"白天"这两个视图之间进行切换。
3. 使用例2-10中的视频捕捉和存储方法,结合例2-5中的doPyrDown()创建一个程序,使其从摄像机读入视频数据并将缩放变换后的彩色图像存入磁盘。
4. 修改练习3的代码,结合例2-1,将变换结果显示在窗口中。
5. 对练习4中的代码进行修改,参考例2-3,给程序加入滚动条,使得用户可以动态调节缩放比例,缩放比例的取值为2~8之间。可以跳过写入磁盘操作,但是必须将变换结果显示在窗口中。