opencv_opencv3 - CSDN
opencv 订阅
OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在Linux、Windows、Android和Mac OS操作系统上。 [1]  它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。OpenCV用C++语言编写,它具有C ++,Python,Java和MATLAB接口,并支持Windows,Linux,Android和Mac OS,OpenCV主要倾向于实时视觉应用,并在可用时利用MMX和SSE指令, 如今也提供对于C#、Ch、Ruby,GO的支持。 [2] 展开全文
OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在Linux、Windows、Android和Mac OS操作系统上。 [1]  它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。OpenCV用C++语言编写,它具有C ++,Python,Java和MATLAB接口,并支持Windows,Linux,Android和Mac OS,OpenCV主要倾向于实时视觉应用,并在可用时利用MMX和SSE指令, 如今也提供对于C#、Ch、Ruby,GO的支持。 [2]
信息
软件版本
OpenCV–4.3.0 [2]
软件平台
Linux、Windows、Android、Mac OS等
软件语言
C++
软件大小
1.16GB
开发商
OpenCV
软件授权
Intel、Willow Garage、NVIDIA等 [3]
软件名称
OpenCV
更新时间
2020-04-06
opencv历史发展
OpenCV 拥有包括 500 多个C函数的跨平台的中、高层 API。它不依赖于其它的外部库——尽管也可以使用某些外部库。OpenCV 为Intel® Integrated Performance Primitives(IPP)提供了透明接口。这意味着如果有为特定处理器优化的 IPP 库,OpenCV 将在运行时自动加载这些库。 [4]  (注:OpenCV 2.0版的代码已显著优化,无需IPP来提升性能,故2.0版不再提供IPP接口) [5]  最新版本是3.4 ,2017年12月23日发布 [6]  。1999年1月,CVL项目启动。主要目标是人机界面,能被UI调用的实时计算机视觉库,为Intel处理器做了特定优化。2000年6月,第一个开源版本OpenCV alpha 3发布。2000年12月,针对linux平台的OpenCV beta 1发布。2006年,支持Mac OS的OpenCV 1.0发布。2009年9月,OpenCV 1.2(beta2.0)发布。2009年10月1日,Version 2.0发布。2010年12月6日,OpenCV 2.2发布。2011年8月,OpenCV 2.3发布。2012年4月2日,发布OpenCV 2.4。2014年8月21日,发布OpenCv 3.0 alpha。2014年11月11日,发布OpenCV 3.0 beta。2015年6月4日,发布OpenCV 3.0。2016年12月,发布OpenCV 3.2版(合并969个修补程序,关闭478个问题)2017年8月3日,发布OpenCV 3.3版(最重要的更新是把DNN模块从contrib里面提到主仓库)OpenCV 使用类BSDlicense,所以对非商业应用和商业应用都是免费(FREE)的。(细节参考 license)OpenCV提供的视觉处理算法非常丰富,并且它部分以C语言编写,加上其开源的特性,处理得当,不需要添加新的外部支持也可以完整的编译链接生成执行程序,所以很多人用它来做算法的移植,OpenCV的代码经过适当改写可以正常的运行在DSP系统和ARM嵌入式系统中,这种移植在大学中经常作为相关专业本科生毕业设计或者研究生课题的选题。
收起全文
  • 基于OpenCV最新版本OpenCV4开始,从基础开始,详解OpenCV核心模块Core、Imgproc、video analysis核心API与相关知识点,讲解从图像组成,像素操作开始,一步一步教你如何写代码,如何用API解决实际问题,从图像处理到...
  • Python+OpenCV计算机视觉

    2018-12-28 17:42:46
    Python+OpenCV计算机视觉系统全面的介绍。
  • OpenCV视频分析与对象跟踪实战视频培训课程概况:基于OpenCV新版本3.2 从基本的OpenCV视频读写与摄像头输入视频API讲解开始, 用原理与代码实现相结合的方式讲述了视频背景建模、前景ROI对象获取、特定颜色对象发现与...
  • OpenCV 概述

    2014-03-09 16:36:39
    OpenCV概述 OpenCV是Intel开元计算机视觉库,它有一系列开源的C函数和少量C++类构成,实现了图像处理和计算机视觉很多通用的算法。 OpenCV特点: 1.拥有包括300多个C函数的跨平台的中、高层API; 2.跨平台:...

    OpenCV概述

    OpenCV是Intel开元计算机视觉库,它有一系列开源的C函数和少量C++类构成,实现了图像处理和计算机视觉很多通用的算法。


    OpenCV特点:

    1.拥有包括300多个C函数的跨平台的中、高层API;

    2.跨平台:Windows/Linux;

    3.Free:无论是对于商业还是非商业应用;

    4.速度快;

    5.使用方便。


    OpenCV结构:


    CXCORE:包含数据结构、矩阵运算、数据变换、对象持久(Object Persistence)、内存管理、错误处理、动态连接、绘图、文本和基本的数学功能。

    CV:包含图形处理、图像结构分析、运动描述和跟踪、模式识别和摄像机标定。

    Machine Learning(ML):包含许多聚类,分类和数据分析函数。

    HighGUI:包含图形用户界面和图像/视频的读/写。

    CVCAM:摄像机接口,在OpenCV1.0以后的版本中被移除。


    OpenCV编码样式指南:

    文件命名:有CV和CVAUS库文件的命名必须服从于以下规则:

    1.所有的CV库文件名前缀为cv;

    2.混合的C/C++接口头文件扩展名为.h;

    3.纯C++接口头文件扩展名为.hpp;

    4.实现头文件扩展名为.cpp;

    5.为了与POSIX兼容,文件名都以小写字符组成。


    命名约定:

    1.OpenCV中使用大小写混合样式来标识外部函数、数据类型和类方法。

    2.宏全部使用大写字符,词间用下划线分隔。

    3.所有的外部或内部名称,若在多个文件中可见,则必须含有前缀:

       外部函数使用前缀cv;

       内部函数使用前缀lcv;

       数据结构(C结构体、枚举、联合体和类)使用前缀CV

       外部或某些内部宏使用前缀CV_

       内部宏使用前缀ICV_


    接口函数设计:

       为了保持库的一致性,以如下方式设计接口非常关键。函数接口元素包括:功能、名称、返回值、参数类型、参数顺序和参数默认值。

       函数功能必须定义良好并保持精简。函数应该容易镶入到使用其它OpenCV函数的不同处理过程。函数名称应该简单并能体现函数的功能。

       大多数函数名形式:cv<ActionName>


    图像处理---平滑处理

      “平滑处理”也称为“模糊处理”,是一项简单且使用频率很高的图像处理方法。平滑处理最常见的用途是用来减少图像上的噪声或者失真。目前OpenCV中,所有平滑处理操作都由cvSmooth函数实现。

    函数说明:

    void cvSmooth(const CvArr* src, CvArr* dst, int smoothtype=CV_GAUSSIAN, int param1=3, int param2=0, double param3=0, double param4=0);

      src和dst分别是平滑操作的输入头像和结果,cvSmooth()函数包含4个参数:param1-param4. 这些参数的含义取决于smoothtype的值。


    图像处理---图像形态学

      OpenCV为进行图像形态学变换提供了快速、方便的函数,基于形态学的转换是膨胀与腐蚀,他们能够实现多种功能:消除噪声,分隔出独立的图像元素以及在图像中连接相邻的元素。

      膨胀是指将一个图像(或图像中的一部分区域,称之为A)与核(称之为B)进行卷积。即,计算核B覆盖区域的像素最大值,并把这个最大值赋值给参考点指定的坐标。这样就会使图像中的高亮区域逐渐增大。

      腐蚀是膨胀的反操作。腐蚀操作要求计算核区像素的最小值。即,当核B与图像卷积时,计算被核B覆盖区域的最小像素值,并把这个值放到参考点。

      在OpenCV,利用cvErode()和cvDilate()函数实现上述变换:

    void cvErode(lplImage* src, lplIMage* dst, lpIlConvKernel* B=NULL, int iterations=1);

    void cvDilate(lplImage* src, lplIMage* dst, lpIlConvKernel* B=NULL, int iterations=1);

    该两个函数的第三个参数是核,默认值是NULL。当为空时,所使用的是参考点位于中心的3*3核。第四个参数是迭代的次数。

    开运算:将二值图像先腐蚀,然后,再膨胀,通常用来统计二值图像中的区域数。开运算则去除一些小的斑点。

    闭运算:将二值图像先膨胀,然后,再腐蚀,通常用来去除噪声引起的区域。闭运算使得小洞被填上,临近的目标连接到了一起(任何结构元容纳不下的小洞或者缝隙都会被填充)。

      对于连通区域分析,通常先采用腐蚀或者闭运算来消除纯粹由噪声引起的部分,然后用开运算来连接临近的区域。最显著的效果是,闭运算消除了低于临近点的孤立点,而,开运算是消除高于其临近点的孤立点。 


    图像处理---拉普拉斯变换

    OpenCV的拉普拉斯函数实现了拉普拉斯算子的离散模拟。

    OpenCV中,拉普拉斯变换函数为:

    void cvLaplace(const CvArr* src, CvArr* dst, int apertureSize=3);

    该函数通常把源图像和目标图像以及中控大小作为变量。源图像既可以是8位(无符号)图像,也可以是32位(浮点)图像。而目标图像必须是16位(有符号)或者32位(浮点)图像。因为拉普拉斯算子的形式是沿着X轴和Y轴的二次导数的和,这就意味着周围是更高值的单电或者小块会将使这个函数值最大化。反过来,周围是更低值的点将会是函数的负值最大化。反过来,周围是更低值的点将会是函数的负值最大化。基于这种思想,拉普拉斯也可以用于边缘检测。


    图像处理---Cannay算子

      Canny边缘检测法与基于拉普拉斯算法的不同点之一是在Canny算法中,首先在x和y方向求一阶导数,然后,组合为4个方向的导数。这些方向导数达到局部最大值的点就是组成边缘的候选点。另外,Canny算法的最终要的新特点是阈值的概念,如果一个像素的梯度大于上线阈值,则被认为是边缘像素,如果低于下限阈值,则被抛弃,如果介于二者时间,只有当其与高于上限阈值的像素连接时才会被接受。Canny推荐的上下限阈值比为2:1到3:1之间。

      void cvCanny(const CvArr* img, CvArr* edges, double lowThresh, double highThresh, int apertureSize=3);

      此函数是需要输入一张灰度图,输出也是灰度的。


      形态学操作

      形态学操作主要有四种:腐蚀、膨胀、开和闭。最基本的形态学操作是腐蚀和膨胀。其他的操作都可以通过腐蚀和膨胀推导出来。

      用集合论的观点介绍他们很繁琐。这里换一种思路,如下:

      我们先做一定的假设:对于一幅图像,前景(我们感兴趣的部分);背景(不感兴趣的部分)是黑色的。然后就可以望文生义一下了,腐蚀操作会使得前景变小,而膨胀会使得前景变大。这主要是当结构元(用来对图像处理的基本模板)作用于图像的边沿时,两种操作的定义引起的。腐蚀操作时,只有当整个结构元都在图像边沿内时,锚点(结构元与图像中每个像素对其的点,通常取作结构元的几何中心)对准的像素才会被保留,判为前景;否则,这个点判为背景。膨胀操作则是只要结构元与图像有交集时,锚点对准的像素就会被保留,判为前景。腐蚀可以用来消除一些小的误检测的前景;而膨胀则可以填充一些小洞。

      注意到,用3*3的模板腐蚀3次与用7*7的模板腐蚀一次效果是相同的。膨胀的结果可以类推。

      

    展开全文
  • Opencv学习(一)

    2018-10-05 16:12:06
    Opencv学习笔记(一) 1、Image Watch 图像监视是微软VisualStudio的插件,它允许您在调试应用程序时在内存映像中可视化。这有助于跟踪错误,或者简单地理解给定代码的操作。 可以在vs里面的工具--&amp;...

    1、Image Watch

    图像监视是微软VisualStudio的插件,它允许您在调试应用程序时在内存映像中可视化。这有助于跟踪错误,或者简单地理解给定代码的操作。

    可以在vs里面的工具-->扩展和更新里面获取Image Watch

    然后再视图-->其他窗口调用。

    2、Load and Display an Image

    Code

    #include <opencv2/core/core.hpp>
    #include <opencv2/imgcodecs.hpp>
    #include <opencv2/highgui/highgui.hpp>
    #include <iostream>
    #include <string>
    using namespace cv;
    using namespace std;
    int main(int argc, char** argv)
    {
    	string imageName("1.jpg"); // by default
    	if (argc > 1)
    	{
    		imageName = argv[1];
    	}
    	Mat image;
    	image = imread(imageName.c_str(), IMREAD_COLOR); // Read the file
    	if (image.empty())                      // Check for invalid input
    	{
    		cout << "Could not open or find the image" << std::endl;
    		return -1;
    	}
    	namedWindow("Display window", WINDOW_AUTOSIZE); // Create a window for display.
    	imshow("Display window", image);                // Show our image inside it.
    	waitKey(0); // Wait for a keystroke in the window
    	return 0;
    }
    

    vs2017中使用命令行参数调试程序

    在这里插入图片描述

    Explanation

    现在我们调用CV::imRead函数,它加载由第一个参数指定的图像名称(ARGV〔1〕)。第二个参数指定了我们想要的图像的格式。

    IimeRead不变(<0)按原样加载图像(包括alpha通道,如果存在)

    IimeRead灰度(0)将图像加载为强度图像。

    IdRead颜色(> 0)以RGB格式加载图像

    image = imread(imageName.c_str(), IMREAD_COLOR); // Read the file
    

    在检查图像数据被正确加载之后,我们想要显示我们的图像,所以我们使用CV::NAMEDLE窗口函数创建OpenCV窗口。一旦创建了OpenCV,它们就会自动管理。为此,您需要指定它的名称以及它应该如何从大小的角度来处理它包含的图像的变化。

    如果不使用QT后端,则只支持WistWOWAutoSead。在这种情况下,窗口大小将占用它所显示的图像的大小。不允许调整大小!

    Qt上的Windows WORKWORD,您可以使用它来允许窗口调整大小。图像将根据当前窗口大小调整自身大小。通过使用该运算符,还需要指定是否希望图像保持其纵横比(WOWDOWKEEPRATIO)或不(WOWDOWFRIERATIO)。

    namedWindow( "Display window", WINDOW_AUTOSIZE ); // Create a window for display.
    

    最后,使用新的图像更新OpenCV窗口的内容,使用imshow函数。指定要更新的OpenCV窗口名称和在此操作期间要使用的图像:

     imshow( "Display window", image ); // Show our image inside it.
    

    因为我们希望我们的窗口显示出来,直到用户按下一个键(否则程序将结束得太快),我们使用CV::WaWiKEY函数,它的唯一参数是它等待用户输入的时间(毫秒测量)。零意味着永远等待。

    waitKey(0); // Wait for a keystroke in the window
    

    3、Load, Modify, and Save an Image

    Code:

    #include <opencv2/opencv.hpp>
    using namespace cv;
    using namespace std;
    int main(int argc, char** argv)
    {
    	char* imageName = argv[1];
    	Mat image;
    	image = imread(imageName, 1);
    	if (argc != 2 || !image.data)
    	{
    		printf(" No image data \n ");
    		return -1;
    	}
    	Mat gray_image;
    	cvtColor(image, gray_image, COLOR_BGR2GRAY);
    	imwrite("Gray_Image.jpg", gray_image);
    	namedWindow(imageName, WINDOW_AUTOSIZE);
    	namedWindow("Gray image", WINDOW_AUTOSIZE);
    	imshow(imageName, image);
    	imshow("Gray image", gray_image);
    	waitKey(0);
    	return 0;
    }
    

    Explanation

    1.imread不在赘述

    2. cv::cvtColor

    cvtColor( image, gray_image, COLOR_BGR2GRAY );

    参数:

    源图像(图像)

    目的地图像(GrayIX图像),在其中我们将保存转换后的图像。

    指示将执行什么类型的转换的附加参数。在这种情况下,我们使用CurryBGR2GRY(因为在彩色图像的情况下具有BGR默认信道顺序)。

    3.imwrite

    imwrite( “…/…/images/Gray_Image.jpg”, gray_image );

    参数:

    写入的图片名字

    写入的Mat

    4、Mat - The Basic Image Container

    MAT基本上是一个具有两个数据部分的类:矩阵标头(包含诸如矩阵的大小、用于存储的方法、地址是存储的矩阵等)和指向包含像素值的矩阵的指针(取决于选择用于存储的方法。矩阵头的大小是恒定的,但是矩阵本身的大小可以从图像到图像变化,并且通常是按数量级大的。

    OpenCV是一个图像处理库。它包含了大量的图像处理功能集合。为了解决计算上的挑战,大多数情况下,您将最终使用库的多个函数。因此,将图像传递给函数是一种常见的做法。我们不应该忘记,我们正在谈论图像处理算法,这往往是相当沉重的计算。我们想做的最后一件事是通过制作不必要的大图像拷贝来进一步降低程序的速度。

    为了解决这个问题,OpenCV使用一个参考计数系统。其思想是每个Mat对象都有自己的标头,但是矩阵可以通过它们的矩阵指针指向相同的地址而在它们的两个实例之间共享。此外,复制运算符只将标题和指针复制到大矩阵,而不是数据本身。

    Mat A, C;                          // creates just the header parts
    A = imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
    Mat B(A);                                 // Use the copy constructor
    C = A;                                    // Assignment operator
    

    最后,所有这些对象指向相同的单个数据矩阵。然而,它们的标题是不同的,使用它们中的任何一个进行修改也会影响所有其他的标题。在实际应用中,不同的对象只对同一基础数据提供不同的访问方法。然而,它们的标题部分是不同的。真正有趣的部分是,您可以创建仅引用完整数据的一个子段的标题。例如,为了在图像中创建感兴趣区域(ROI),您只需创建具有新边界的新标题:

    Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
    Mat E = A(Range::all(), Range(1,3)); // using row and column boundaries
    

    现在你可能会问,矩阵本身是否可能属于多个物体,当它不再需要时,它负责清理它。简短的回答是:最后一个使用它的对象。这是通过使用参考计数机制来处理的。每当有人复制垫对象的头时,矩阵的计数器就会增加。每当清空报头时,计数器就会减少。当计数器达到零时,矩阵也被释放。有时,您也希望复制矩阵本身,因此OpenCV提供CV::MAT::clone()CV::MAT::CopyTo()函数。

    Mat F = A.clone();
    Mat G;
    A.copyTo(G);
    

    现在修改F或G不会影响垫头所指向的矩阵。你需要记住的是:

    OpenCV函数的输出图像分配是自动的(除非另有说明)。

    你不需要考虑内存管理与opencvs C++接口。

    赋值运算符和复制构造函数只复制报头。

    可以使用CV::Mat:clone()CV::MAT::CopyTo()函数复制图像的底层矩阵。

    构造函数

    CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]

    CVY8UC3意味着我们使用8位长的无符号字符类型,每个像素都有三个来形成三个通道。这是预定义的多达四个频道号码。CV::标量是四元短向量。指定这个值,并且可以用自定义值初始化所有矩阵点。如果需要更多,可以用上宏创建类型,在括号中设置通道编号,如下所示。

    5.How to scan images, lookup tables and time measurement with OpenCV

    The iterator (safe) method

    迭代器方法被认为是一种更安全的方法,因为它从用户那里接管这些任务。所有你需要做的是询问图像矩阵的开始和结束,然后只需增加开始迭代器,直到达到结束。若要获取迭代器所指向的值,请使用*运算符(在它之前加上它)。

    Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
    {
        // accept only char type matrices
        CV_Assert(I.depth() != sizeof(uchar));
        const int channels = I.channels();
        switch(channels)
        {
        case 1:
            {
                MatIterator_<uchar> it, end;
                for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
                    *it = table[*it];
                break;
            }
        case 3:
            {
                MatIterator_<Vec3b> it, end;
                for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
                {
                    (*it)[0] = table[(*it)[0]];
                    (*it)[1] = table[(*it)[1]];
                    (*it)[2] = table[(*it)[2]];
                }
            }
        }
        return I;
    }
    

    在彩色图像的情况下,每列有三个UCHAR项。这可能被认为是一个短向量uChar项目。要访问第n个子列,我们使用简单的运算符[]访问。记住OpenCV迭代器经过列并自动跳转到下一行是很重要的。因此,在彩色图像的情况下,如果使用简单的UCHAR迭代器,则只能访问蓝色通道值。

    Mask operations on matrices

    矩阵上的掩码运算是相当简单的。想法是根据掩模矩阵(也称为内核)重新计算图像中的每个像素值。该掩模保持将调整相邻像素(和当前像素)对新像素值有多大影响的值。从数学的角度,我们用我们的指定值做加权平均。

    void Sharpen(const Mat& myImage, Mat& Result)
    {
        CV_Assert(myImage.depth() == CV_8U);  // accept only uchar images
        Result.create(myImage.size(), myImage.type());
        const int nChannels = myImage.channels();
        for(int j = 1; j < myImage.rows - 1; ++j)
        {
            const uchar* previous = myImage.ptr<uchar>(j - 1);
            const uchar* current  = myImage.ptr<uchar>(j    );
            const uchar* next     = myImage.ptr<uchar>(j + 1);
            uchar* output = Result.ptr<uchar>(j);
            for(int i = nChannels; i < nChannels * (myImage.cols - 1); ++i)
            {
                *output++ = saturate_cast<uchar>(5 * current[i]
                             -current[i - nChannels] - current[i + nChannels] - previous[i] - next[i]);
            }
        }
        Result.row(0).setTo(Scalar(0));
        Result.row(Result.rows - 1).setTo(Scalar(0));
        Result.col(0).setTo(Scalar(0));
        Result.col(Result.cols - 1).setTo(Scalar(0));
    }
    

    6、Adding (blending) two images using OpenCV

    合成两张图片

    Code:

    #include <opencv2/opencv.hpp>
    #include <iostream>
    using namespace cv;
    int main(int argc, char** argv)
    {
    	double alpha = 0.5; double beta; double input;
    	Mat src1, src2, dst;
    	std::cout << " Simple Linear Blender " << std::endl;
    	std::cout << "-----------------------" << std::endl;
    	std::cout << "* Enter alpha [0-1]: ";
    	std::cin >> input;
    	if (input >= 0.0 && input <= 1.0)
    	{
    		alpha = input;
    	}
    	src1 = imread("1.jpg");
    	src2 = imread("2.jpg");
    	if (!src1.data) { printf("Error loading src1 \n"); return -1; }
    	if (!src2.data) { printf("Error loading src2 \n"); return -1; }
    	namedWindow("Linear Blend", 1);
    	beta = (1.0 - alpha);
    	addWeighted(src1, alpha, src2, beta, 0.0, dst);
    	imshow("Linear Blend", dst);
    	imwrite("3.jpg", dst);
    	waitKey(0);
    	return 0;
    }
    

    Explanation

    addWeighted( src1, alpha, src2, beta, 0.0, dst);

    图片:src1,src2

    比例:alpha,beta,和为1

    新图片:dst

    7、Changing the contrast and brightness of an image! :

    一般的图像处理算子是一个获取一个或多个输入图像并产生输出图像的函数。

    图像变换可以看作:

    点算子(像素变换)

    邻域(基于区域)算子

    像素变换

    在这种图像处理变换中,每个输出像素的值仅取决于相应的输入像素值。

    这样的例子包括亮度和对比度调整以及颜色校正和变换。

    亮度和对比度调整

    两个常用的点过程是乘法和加法:

    g(x)=αf(x)+β

    参数α>0和β通常称为增益和偏置参数,有时这些参数分别用来控制对比度和亮度。

    可以将f(x)作为源图像像素和g(x)作为输出图像像素。然后,我们更方便地将表达式写成:

    g(i,j)=αf(i,j)+β

    Code

    #include <opencv2/opencv.hpp>
    #include <iostream>
    using namespace cv;
    double alpha; /*< Simple contrast control */
    int beta;  /*< Simple brightness control */
    int main(int argc, char** argv)
    {
    	Mat image = imread(argv[1]);
    	Mat new_image = Mat::zeros(image.size(), image.type());
    	std::cout << " Basic Linear Transforms " << std::endl;
    	std::cout << "-------------------------" << std::endl;
    	std::cout << "* Enter the alpha value [1.0-3.0]: "; std::cin >> alpha;
    	std::cout << "* Enter the beta value [0-100]: "; std::cin >> beta;
    	for (int y = 0; y < image.rows; y++) {
    		for (int x = 0; x < image.cols; x++) {
    			for (int c = 0; c < 3; c++) {
    				new_image.at<Vec3b>(y, x)[c] =
    					saturate_cast<uchar>(alpha*(image.at<Vec3b>(y, x)[c]) + beta);
    			}
    		}
    	}
    	namedWindow("Original Image", 1);
    	namedWindow("New Image", 1);
    	imshow("Original Image", image);
    	imshow("New Image", new_image);
    	imwrite("2.jpg", new_image);
    	waitKey();
    	return 0;
    }
    

    Explanation

    1.我们首先创建参数来保存用户输入的α和β:

    double alpha;
    int beta;
    

    2.我们使用cv::imread加载图像并将其保存在一个MAT对象中:

    Mat image = imread( argv[1] );
    

    3.我们使用CV::iMead加载图像并将其保存在一个Mat对象中:现在,因为我们将对该图像进行一些转换,所以我们需要一个新的Mat对象来存储它。此外,我们希望这具有以下特征:

    初始像素值等于零

    与原始图像相同大小和类型

    Mat new_image = Mat::zeros( image.size(), image.type() );
    

    4.现在,为了执行操作G(i,j)=αf(i,j)+β,我们将访问图像中的每个像素。由于我们正在使用BGR图像,我们将有三个像素(B,G和R)的值,所以我们也将分别访问它们。

    for( int y = 0; y < image.rows; y++ ) {
        for( int x = 0; x < image.cols; x++ ) {
            for( int c = 0; c < 3; c++ ) {
                new_image.at<Vec3b>(y,x)[c] =
                  saturate_cast<uchar>( alpha*( image.at<Vec3b>(y,x)[c] ) + beta );
            }
        }
    }
    

    注意以下事项:

    为了访问图像中的每个像素,我们使用这个语法:图像。在<Vec3b>(Y,X)[C],其中Y是行,X是列,C是R,G或B(0, 1或2)。

    因为运算 αp(i,j)+β可以给出超出范围的值,或者不是整数(如果α是浮点数),我们使用cv::SudialType强制转换来确保值是有效的。

    5.最后,我们创建窗口和显示图像,通常的方式。

    	namedWindow("Original Image", 1);
    	namedWindow("New Image", 1);
    	imshow("Original Image", image);
    	imshow("New Image", new_image);
    	imwrite("2.jpg", new_image);
    	waitKey();
    

    8、Basic Drawing

    Point

    它代表一个2D点,由它的图像坐标x和y指定。我们可以将它定义为:

    Point pt;
    pt.x = 10;
    pt.y = 8;
    //Point pt = Point(10, 8);
    

    Scalar

    表示一个4元向量。在OpenCV中,类型标量被广泛用于传递像素值;

    在本教程中,我们将广泛使用它来表示BGR颜色值(3个参数)。如果不使用最后一个参数,则不必定义最后一个参数。

    让我们来看看一个例子,如果我们要求一个颜色参数,我们给出:

    Scalar( a, b, c )
    

    我们将定义一个BGR颜色,例如:蓝色= A,绿色= B和红色= C。

    Code

    #include <opencv2/core.hpp>
    #include <opencv2/imgproc.hpp>
    #include <opencv2/highgui.hpp>
    
    #define w 400
    
    using namespace cv;
    
    /// Function headers
    void MyEllipse( Mat img, double angle );
    void MyFilledCircle( Mat img, Point center );
    void MyPolygon( Mat img );
    void MyLine( Mat img, Point start, Point end );
    
    /**
     * @function main
     * @brief Main function
     */
    int main( void ){
    
      //![create_images]
      /// Windows names
      char atom_window[] = "Drawing 1: Atom";
      char rook_window[] = "Drawing 2: Rook";
    
      /// Create black empty images
      Mat atom_image = Mat::zeros( w, w, CV_8UC3 );
      Mat rook_image = Mat::zeros( w, w, CV_8UC3 );
      //![create_images]
    
      /// 1. Draw a simple atom:
      /// -----------------------
    
      //![draw_atom]
      /// 1.a. Creating ellipses
      MyEllipse( atom_image, 90 );
      MyEllipse( atom_image, 0 );
      MyEllipse( atom_image, 45 );
      MyEllipse( atom_image, -45 );
    
      /// 1.b. Creating circles
      MyFilledCircle( atom_image, Point( w/2, w/2) );
      //![draw_atom]
    
      /// 2. Draw a rook
      /// ------------------
    
      //![draw_rook]
      /// 2.a. Create a convex polygon
      MyPolygon( rook_image );
    
      //![rectangle]
      /// 2.b. Creating rectangles
      rectangle( rook_image,
             Point( 0, 7*w/8 ),
             Point( w, w),
             Scalar( 0, 255, 255 ),
             FILLED,
             LINE_8 );
      //![rectangle]
    
      /// 2.c. Create a few lines
      MyLine( rook_image, Point( 0, 15*w/16 ), Point( w, 15*w/16 ) );
      MyLine( rook_image, Point( w/4, 7*w/8 ), Point( w/4, w ) );
      MyLine( rook_image, Point( w/2, 7*w/8 ), Point( w/2, w ) );
      MyLine( rook_image, Point( 3*w/4, 7*w/8 ), Point( 3*w/4, w ) );
      //![draw_rook]
    
      /// 3. Display your stuff!
      imshow( atom_window, atom_image );
      moveWindow( atom_window, 0, 200 );
      imshow( rook_window, rook_image );
      moveWindow( rook_window, w, 200 );
    
      waitKey( 0 );
      return(0);
    }
    
    /// Function Declaration
    
    /**
     * @function MyEllipse
     * @brief Draw a fixed-size ellipse with different angles
     */
    //![my_ellipse]
    void MyEllipse( Mat img, double angle )
    {
      int thickness = 2;
      int lineType = 8;
    
      ellipse( img,
           Point( w/2, w/2 ),
           Size( w/4, w/16 ),
           angle,
           0,
           360,
           Scalar( 255, 0, 0 ),
           thickness,
           lineType );
    }
    //![my_ellipse]
    
    /**
     * @function MyFilledCircle
     * @brief Draw a fixed-size filled circle
     */
    //![my_filled_circle]
    void MyFilledCircle( Mat img, Point center )
    {
      circle( img,
          center,
          w/32,
          Scalar( 0, 0, 255 ),
          FILLED,
          LINE_8 );
    }
    //![my_filled_circle]
    
    /**
     * @function MyPolygon
     * @brief Draw a simple concave polygon (rook)
     */
    //![my_polygon]
    void MyPolygon( Mat img )
    {
      int lineType = LINE_8;
    
      /** Create some points */
      Point rook_points[1][20];
      rook_points[0][0]  = Point(    w/4,   7*w/8 );
      rook_points[0][1]  = Point(  3*w/4,   7*w/8 );
      rook_points[0][2]  = Point(  3*w/4,  13*w/16 );
      rook_points[0][3]  = Point( 11*w/16, 13*w/16 );
      rook_points[0][4]  = Point( 19*w/32,  3*w/8 );
      rook_points[0][5]  = Point(  3*w/4,   3*w/8 );
      rook_points[0][6]  = Point(  3*w/4,     w/8 );
      rook_points[0][7]  = Point( 26*w/40,    w/8 );
      rook_points[0][8]  = Point( 26*w/40,    w/4 );
      rook_points[0][9]  = Point( 22*w/40,    w/4 );
      rook_points[0][10] = Point( 22*w/40,    w/8 );
      rook_points[0][11] = Point( 18*w/40,    w/8 );
      rook_points[0][12] = Point( 18*w/40,    w/4 );
      rook_points[0][13] = Point( 14*w/40,    w/4 );
      rook_points[0][14] = Point( 14*w/40,    w/8 );
      rook_points[0][15] = Point(    w/4,     w/8 );
      rook_points[0][16] = Point(    w/4,   3*w/8 );
      rook_points[0][17] = Point( 13*w/32,  3*w/8 );
      rook_points[0][18] = Point(  5*w/16, 13*w/16 );
      rook_points[0][19] = Point(    w/4,  13*w/16 );
    
      const Point* ppt[1] = { rook_points[0] };
      int npt[] = { 20 };
    
      fillPoly( img,
            ppt,
            npt,
            1,
            Scalar( 255, 255, 255 ),
            lineType );
    }
    //![my_polygon]
    
    /**
     * @function MyLine
     * @brief Draw a simple line
     */
    //![my_line]
    void MyLine( Mat img, Point start, Point end )
    {
      int thickness = 2;
      int lineType = LINE_8;
    
      line( img,
        start,
        end,
        Scalar( 0, 0, 0 ),
        thickness,
        lineType );
    }
    

    Explanation

    1.由于我们计划画两个例子(原子和一个木棒),我们必须创建两个图像和两个窗口来显示它们。

    char atom_window[] = "Drawing 1: Atom";
    char rook_window[] = "Drawing 2: Rook";
    Mat atom_image = Mat::zeros( w, w, CV_8UC3 );
    Mat rook_image = Mat::zeros( w, w, CV_8UC3 );
    

    2.我们创建了绘制不同几何形状的函数。例如,为了画原子,我们使用了MyEngEs和MyFilledCircle:

    MyEllipse( atom_image, 90 );
    MyEllipse( atom_image, 0 );
    MyEllipse( atom_image, 45 );
    MyEllipse( atom_image, -45 );
    MyFilledCircle( atom_image, Point( w/2.0, w/2.0) );
    

    3.为了绘制木屋,我们采用了MyLoinrectangleMyPosion

    
    MyPolygon( rook_image );
    rectangle( rook_image,
           Point( 0, 7*w/8.0 ),
           Point( w, w),
           Scalar( 0, 255, 255 ),
           -1,
           8 );
    MyLine( rook_image, Point( 0, 15*w/16 ), Point( w, 15*w/16 ) );
    MyLine( rook_image, Point( w/4, 7*w/8 ), Point( w/4, w ) );
    MyLine( rook_image, Point( w/2, 7*w/8 ), Point( w/2, w ) );
    MyLine( rook_image, Point( 3*w/4, 7*w/8 ), Point( 3*w/4, w ) );
    

    4.让我们检查一下这些函数的内部内容:

    MyLine

    void MyLine( Mat img, Point start, Point end )
    {
        int thickness = 2;
        int lineType = 8;
        line( img, start, end,
              Scalar( 0, 0, 0 ),
              thickness,
              lineType );
    }
    

    正如我们所看到的,MyLayle只调用函数cv::Load,它执行以下操作:

    从点到点画一条线

    该线显示在图像IMG中。

    线条颜色由标量(0, 0, 0)定义,这是布莱克的RGB值。

    线的厚度设置为厚度(在这种情况下为2)。

    该线是8连通的(LyeType=8)。

    MyEllipse

    void MyEllipse( Mat img, double angle )
    {
        int thickness = 2;
        int lineType = 8;
        ellipse( img,
           Point( w/2.0, w/2.0 ),
           Size( w/4.0, w/16.0 ),
           angle,
           0,
           360,
           Scalar( 255, 0, 0 ),
           thickness,
           lineType );
    }
    

    从上面的代码中,我们可以看到函数cv::fillPoly绘制椭圆,使得:

    椭圆在图像IMG中显示。

    椭圆中心位于点**(w/2,w/2),并被封装在大小为的框中(w/4,w/16)**。

    椭圆旋转角度

    椭圆在0度和360度之间延伸一个弧。

    图形的颜色将是标量(255, 0, 0),这意味着蓝色在RGB值。

    椭圆的厚度为2。

    MyFilledCircle

    void MyFilledCircle( Mat img, Point center )
    {
        int thickness = -1;
        int lineType = 8;
        circle( img,
            center,
            w/32.0,
            Scalar( 0, 0, 255 ),
            thickness,
            lineType );
    }
    

    类似于椭圆函数,我们可以观察到圆圈作为参数接收:

    将显示圆的图像(IMG)

    以圆心为中心的圆的中心

    圆的半径:W/32

    圆的颜色:标量(0, 0, 255),表示BGR中的红色。

    由于厚度=1,将画圆填充。

    MyPolygon

    void MyPolygon( Mat img )
    {
        int lineType = 8;
        /* Create some points */
        Point rook_points[1][20];
        rook_points[0][0] = Point( w/4.0, 7*w/8.0 );
        rook_points[0][1] = Point( 3*w/4.0, 7*w/8.0 );
        rook_points[0][2] = Point( 3*w/4.0, 13*w/16.0 );
        rook_points[0][3] = Point( 11*w/16.0, 13*w/16.0 );
        rook_points[0][4] = Point( 19*w/32.0, 3*w/8.0 );
        rook_points[0][5] = Point( 3*w/4.0, 3*w/8.0 );
        rook_points[0][6] = Point( 3*w/4.0, w/8.0 );
        rook_points[0][7] = Point( 26*w/40.0, w/8.0 );
        rook_points[0][8] = Point( 26*w/40.0, w/4.0 );
        rook_points[0][9] = Point( 22*w/40.0, w/4.0 );
        rook_points[0][10] = Point( 22*w/40.0, w/8.0 );
        rook_points[0][11] = Point( 18*w/40.0, w/8.0 );
        rook_points[0][12] = Point( 18*w/40.0, w/4.0 );
        rook_points[0][13] = Point( 14*w/40.0, w/4.0 );
        rook_points[0][14] = Point( 14*w/40.0, w/8.0 );
        rook_points[0][15] = Point( w/4.0, w/8.0 );
        rook_points[0][16] = Point( w/4.0, 3*w/8.0 );
        rook_points[0][17] = Point( 13*w/32.0, 3*w/8.0 );
        rook_points[0][18] = Point( 5*w/16.0, 13*w/16.0 );
        rook_points[0][19] = Point( w/4.0, 13*w/16.0) ;
        const Point* ppt[1] = { rook_points[0] };
        int npt[] = { 20 };
        fillPoly( img,
                  ppt,
                  npt,
                      1,
                  Scalar( 255, 255, 255 ),
                  lineType );
    }
    

    为了画一个填充多边形,我们使用函数cv:fIyPull。我们注意到:

    多边形将绘制在IMG上。

    多边形的顶点是PPT中的点集。

    要绘制的顶点的总数是NPT

    绘制的多边形数仅为1个。

    多边形的颜色由标量(255, 255, 255)定义,这是白色的BGR值。

    rectangle

    rectangle( rook_image,
               Point( 0, 7*w/8.0 ),
               Point( w, w),
               Scalar( 0, 255, 255 ),
               -1, 8 );
    

    最后,我们有了cv::rectangle函数(我们没有为这个家伙创建一个特殊的函数)。我们注意到:

    矩形将绘制在Rookx图像上。

    矩形的两个相对顶点由**点(0, 7×w/8)**和点(w,w)**定义。

    矩形的颜色由标量(0, 255, 255)给出,这是黄色的BGR值。

    由于厚度值由** -1 **给出,矩形将被填充。

    9、Put Text, Point or Circle

        //插入文字  
        //参数为:承载的图片,插入的文字,文字的位置(文本框左下角),字体,大小,颜色  
        string words= "good luck";  
        putText( picture, words, Point( picture.rows/2,picture.cols/4),CV_FONT_HERSHEY_COMPLEX, 1, Scalar(255, 0, 0) );  
        imshow("1.jpg",picture);  
    
    //参数为:承载的图像、圆心、半径、颜色、粗细、线型、最后一个0  
    circle(picture,center,r,Scalar(0,0,0));  
    

    椭圆

       //参数为:承载的图像、圆心、长短轴、径向夹角(水平面到长轴的夹角)、起始角度(长轴到起始边沿的夹角)、结束角度(长轴到结束点的夹角)、倾斜的矩形(可选项)、颜色、粗细、线性、偏移  
        ellipse(picture,center,Size( 250, 100 ),0,30,240,Scalar(0,0,0));  
    

    line:

        //画线  
        Point a = Point (600,600);  
        //参数为:承载的图像、起始点、结束点、颜色、粗细、线型  
        line(picture,a,center,Scalar(255,0,0));  
        imshow("1.jpg",picture);  
    

    矩形:

    //画矩形  
    //参数为:承载的图像、顶点、对角点、颜色(这里是蓝色)、粗细、大小  
    rectangle(picture,a,center,Scalar(255,0,0));  
    imshow("底板",picture); 
    

    10、Discrete Fourier Transform

    傅立叶变换将图像分解成其正弦和余弦分量。换句话说,它将将图像从其空间域转换到其频域。其思想是任何函数都可以用无穷窦和余弦函数之和精确地近似。

    变换的结果是复数。通过真实图像和复数图像或通过幅度和相位图像来显示这一点是可能的。然而,在整个图像处理算法中,只有幅度图像是有趣的,因为这包含了关于图像几何结构所需的所有信息。然而,如果你打算对这些表单中的图像做一些修改,然后你需要重新变换它,你就需要保存这两个。

    在这个示例中,我将展示如何计算和显示傅立叶变换的幅度图像。在数字图像的情况下是离散的。这意味着它们可以从给定的域值中获取一个值。例如,在基本灰度值中,图像值通常介于0和255之间。因此,傅立叶变换也需要是离散的类型,导致离散傅立叶变换(DFT)。每当你需要从几何的角度来确定图像的结构时,你就需要使用它。这里是要遵循的步骤(在灰度输入图像I的情况下):

    1.Expand the image to an optimal size将图像扩展到最佳大小。

    DFT的性能取决于图像的大小。它往往是最快的图像大小是倍数的二,三和五。因此,为了达到最大的性能,通常将一个边界值映射到图像以获得具有这些特征的大小是一个好主意。CV::GoDestaldDftsie()返回这个最佳大小,我们可以使用CV::CopyMaBurdReand()函数来扩展图像的边框:

    Mat padded;                            //expand input image to optimal size
    int m = getOptimalDFTSize( I.rows );
    int n = getOptimalDFTSize( I.cols ); // on the border add zero pixels
    copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0));
    

    2.Make place for both the complex and the real values为复杂的和真实的价值创造位置。

    傅立叶变换的结果是复杂的。这意味着对于每个图像值,结果是两个图像值(每个分量一个)。此外,频域范围远大于其空间对应部分。因此,我们通常至少以浮点格式存储这些文件。因此,我们将将输入图像转换为这种类型,并将其扩展到另一个通道以保存复数值:

    Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)};
    Mat complexI;
    merge(planes, 2, complexI);         // Add to the expanded another plane with zeros
    

    3.Make the Discrete Fourier Transform进行离散傅立叶变换。

    有可能进行就地计算(与输出相同的输入):

    
    

    4.Transform the real and complex values to magnitude将实数和复值变换为幅值。

    split(complexI, planes);                   // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
    magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude
    Mat magI = planes[0];
    

    5.Switch to a logarithmic scale切换到对数刻度。

    结果表明,傅立叶系数的动态范围太大,不能在屏幕上显示。我们有一些小的和一些高度变化的值,我们不能像这样观察到。因此,高值将全部变为白色点,而小值则变为黑色。为了使用灰度值来可视化,我们可以将我们的线性尺度转换成对数尺度

    翻译成OpenCV代码:

    magI += Scalar::all(1);                    // switch to logarithmic scale
    log(magI, magI);
    

    6.Crop and rearrange

    magI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2));
    int cx = magI.cols/2;
    int cy = magI.rows/2;
    Mat q0(magI, Rect(0, 0, cx, cy));   // Top-Left - Create a ROI per quadrant
    Mat q1(magI, Rect(cx, 0, cx, cy));  // Top-Right
    Mat q2(magI, Rect(0, cy, cx, cy));  // Bottom-Left
    Mat q3(magI, Rect(cx, cy, cx, cy)); // Bottom-Right
    Mat tmp;                           // swap quadrants (Top-Left with Bottom-Right)
    q0.copyTo(tmp);
    q3.copyTo(q0);
    tmp.copyTo(q3);
    q1.copyTo(tmp);                    // swap quadrant (Top-Right with Bottom-Left)
    q2.copyTo(q1);
    tmp.copyTo(q2);
    
    

    请记住,在第一步,我们扩大了图像?是时候抛弃新引进的价值观了。为了可视化目的,我们也可以重新排列结果的象限,以便原点(0,0)对应于图像中心。

    7.Normalize.归一化。

    这是为了可视化目的再次进行的。我们现在有了大小,但是这仍然是我们的图像显示范围为0至1。我们使用CV::标准化()函数将我们的值标准化到这个范围

    normalize(magI, magI, 0, 1, NORM_MINMAX); // Transform the matrix with float values into a
                                              // viewable image form (float between values 0 and 1).
    

    11、文件的读入与写入XML

        cv::FileStorage(const string& source, int flags, const string& encoding=string());  
    

    参数:

    **source –**存储或读取数据的文件名(字符串),其扩展名(.xml 或 .yml/.yaml)决定文件格式。

    flags – 操作模式,包括:

    • FileStorage::READ 打开文件进行读操作
    • FileStorage::WRITE 打开文件进行写操作
    • FileStorage::APPEND打开文件进行附加操作
    • FileStorage::MEMORY 从source读数据,或向内部缓存写入数据(由FileStorage::release返回)

    encoding – 文件编码方式。目前不支持UTF-16 XML 编码,应使用 8-bit 编码

    演示写入数值、矩阵、多个变量、当前时间和关闭文件:

        // 1.create our writter  
            cv::FileStorage fs("test.yml", FileStorage::WRITE);  
              
            // 2.Save an int  
            int imageWidth= 5;  
            int imageHeight= 10;  
            fs << "imageWidth" << imageWidth;  
            fs << "imageHeight" << imageHeight;  
          
            // 3.Write a Mat  
            cv::Mat m1= Mat::eye(3,3, CV_8U);  
            cv::Mat m2= Mat::ones(3,3, CV_8U);  
            cv::Mat resultMat= (m1+1).mul(m1+2);  
            fs << "resultMat" << resultMat;  
          
            // 4.Write multi-variables   
            cv::Mat cameraMatrix = (Mat_<double>(3,3) << 1000, 0, 320, 0, 1000, 240, 0, 0, 1);  
            cv::Mat distCoeffs = (Mat_<double>(5,1) << 0.1, 0.01, -0.001, 0, 0);  
            fs << "cameraMatrix" << cameraMatrix << "distCoeffs" << distCoeffs;  
          
            // 5.Save local time  
            time_t rawtime; time(&rawtime); //#include <time.h>  
            fs << "calibrationDate" << asctime(localtime(&rawtime));  
          
            // 6.close the file opened  
            fs.release();  
    
    展开全文
  • Opencv概述

    2015-07-29 11:00:24
    opencv是什么 OpenCV是一个用于图像处理、分析、机器视觉方面的开源函数库.   无论你是做科学研究,还是商业应用,opencv都可以作为你理想的工具库,因为,对于这两者,它完全是免费的。 该库采用C及C++语言...

    opencv是什么

    OpenCV是一个用于图像处理、分析、机器视觉方面的开源函数库. 

          无论你是做科学研究,还是商业应用,opencv都可以作为你理想的工具库,因为,对于这两者,它完全是免费的。
    该库采用C及C++语言编写,可以在windows, linux, mac OSX系统上面运行。该库的所有代码都经过优化,计算效率很高,因为,它更专注于设计成为一种用于实时系统的开源库。opencv采用C语言进行优化,而且,在多核机器上面,其运行速度会更快。它的一个目标是提供友好的机器视觉接口函数,从而使得复杂的机器视觉产品可以加速面世。该库包含了横跨工业产品检测、医学图像处理、安防、用户界面、摄像头标定、三维成像、机器视觉等领域的超过500个接口函数。
          同时,由于计算机视觉与机器学习密不可分,该库也包含了比较常用的一些机器学习算法。或许,很多人知道,图像识别、机器视觉在安防领域有所应用。但,很少有人知道,在航拍图片、街道图片(例如google street view)中,要严重依赖于机器视觉的摄像头标定、图像融合等技术。
          近年来,在入侵检测、特定目标跟踪、目标检测、人脸检测、人脸识别、人脸跟踪等领域,opencv可谓大显身手,而这些,仅仅是其应用的冰山一角。

          如今,来自世界各地的各大公司、科研机构的研究人员,共同维护支持着opencv的开源库开发。这些公司和机构包括:微软,IBM,索尼、西门子、google、intel、斯坦福、MIT、CMU、剑桥。。。

    OpenCV的全称,是Open source Computer Vision Library,开放源代码计算机视觉库。也就是说,它是一套关于计算机视觉的开放源代码的API函数库。这也就意味着,(1)不管是科学研究,还是商业应用,都可以利用它来作开发;(2)所有API函数的源代码都是公开的,你可以看到其内部实现的程序步骤;(3)你可以修改OpenCV的源代码,编译生成你需要的特定API函数。但是,作为一个库,它所提供的,仅仅是一些常用的,经典的,大众化的算法的API。一个典型的计算机视觉算法,应该包含以下一些步骤:(1)数据获取(对OpenCV来说,就是图片);(2)预处理;(3)特征提取;(4)特征选择;(5)分类器设计与训练;(6)分类判别;而OpenCV对这六个部分,分别(记住这个词)提供了API。下面我分别就这六个部分对一些常见问题进行必要的解释。

            对于数据获取,计算机视觉领域的数据,无非就是图片和视频两种。图片,有bmp,jpg,png,tiff....各种压缩和非压缩格式。所以,对压缩格式的图片而言,OpenCV内部必然包含了对应的图片解压缩函数(一般都是包含了开源的图片解压函数库,例如,对于jpg压缩格式而言,就包含了libjpg开源库)。而对于视频而言,常见的有.rmvb,.avi,.asf等格式,不同的格式,代表着不同的视频压缩算法(对于AVI格式,尽管都是avi格式,但内部的压缩算法仍然不相同。具体原因请参考我的另一篇博客:http://blog.csdn.net/carson2005/article/details/6314089),也就需要对应的解压算法来解压。尽管OpenCV提供了一些读写视频文件的API,但是,它也仅仅是一个接口而已,其内部,依然需要调用相应的视频编解码器的API来进行解码。常用的视频编解码器有:xvid,ffmpeg等。也就是说,如果你想利用OpenCV来进行视频读写之类的操作,是需要安装此类视频编解码器的。安装了相应的视频解码器之后,你就可以调用OpenCV的视频相关API来进行视频文件的读取操作了,当然,视频文件被解码之后,变成了一张一张的图片,然后才能被OpenCV所处理。另外,还有一种情况,就是数据来自于相机,包括数字相机和模拟相机。不管是哪种相机,你都要想办法获取到相机发送给PC的图片数据(PC在内存里面接收到的来自相机的数据可能是jpg格式,也可能是bmp格式)。如果,你在PC内存中接收到的是相机发送过来的jpg压缩格式,还需要进行图片数据的内存解压。关于相机和OpenCV的这部分内容,请见我另一篇博客:

    http://blog.csdn.net/carson2005/article/details/6243476

            对于预处理,一般就是去除或者降低噪声,光照归一化,亮度归一化,模糊化,锐化,膨胀,腐蚀、开闭等这些操作(详见,冈萨雷斯,《数字图像处理》一书)。而对于这些操作,OpenCV分别(又提到这个词了)提供了相应API函数。而光照的预处理,OpenCV提供了一个直方图均衡化的API,后续可能会提供一些gammar矫正之类的函数。

            对于特征提取,个人认为,可以算是整个计算机视觉系统中最为复杂也最难的部分(纯属个人意见,如有异议,请保留),到底什么是特征,该如何来理解这个看似简单却又包罗万象的名词呢?其实,要想仔细解释,还真的花费很多时间(有兴趣的可以看看,Richard O.Duda(著),李宏东(译),《模式识别》,机械工业出版社)。简单点说,特征,就是一个可以将若干个类别可以尽量分开的一种描述。举例来说,如果你要进行男人和女人的分类,显然,用“身高和体重”这一描述来衡量,是可以的,但是,这两个描述没有“胸部大小”这一描述更加准确,而“胸部大小”这一描述,又没有“喉结的有无”这一描述更准确。很显然,“身高和体重”,“胸部大小”,“喉结的有无”,这三种描述,都可以用来进行男人和女人的分类,只不过,它们对事物的描述的准确(或者说全面)程度是不同的,而诸如此类的描述,有一个更加专业的称谓,叫做“特征”。OpenCV里面,提供了一些特征描述的API,比如,对于人脸检测而言,它提供了haar特征的API,行人检测,提供了hog特征的API,甚至,它提供了LBP纹理特征的API。但是,这些还远远不够。例如,如果你要进行字符识别,OpenCV并没有提供字符识别所对应的特征。这个时候,就需要你自己来编程实现了。当然,该选择什么特征来描述字符呢?哪些特征更好呢?对于这些问题,我建议你去阅读相应的会议,期刊,杂志,硕士、博士毕业论文(毕竟硕士、博士研究生本就该从事“研究”工作),看看别人写的文章,自然就知道了。

            对于特征选择,OpenCV并没有提供特定的函数来进行衡量。而特征的分类能力的高低评价,有很多种分析方法,有兴趣的朋友,可以阅读"《机器学习》Tom. Mitchell(著),曾华军(译),机械工业出版社"这本书;

            对于分类器部分,OpenCV提供了SVM,CART,boost,bayes,bdt,ANN,这几种常用的算法。而这些基本已经覆盖了常用的分类器。所以,你需要做的,就是知道怎么调用其接口,各种分类器的优点和缺点(该部分,建议阅读“机器学习”这本书)。

            通过以上的分析,你或许已经发现,OpenCV不过是一个工具而已。或者,你可以将它理解为幼儿园小朋友过家家玩的积木,而OpenCV中的函数,则可以理解为一个一个的积木块,利用所有或者部分积木块,你可以快速的搭建起来具体的计算机视觉方面的应用(比如,字符识别,车牌识别,遗留物检测)。想必你也已经发现,在利用OpenCV这个积木来搭建具体的计算机视觉应用的时候,真正核心的,应该是这些积木块,如果你明白了积木块的工作原理,那么,是不是就可以不用这些积木块了呢?完全正确!不过,一般部分情况下,我们不需要这么做,因为,OpenCV已经帮你做好了一些工作(已经帮你做好了一些积木块,直接拿来用就是了)。但是,诸如前面提到的特征提取模块,很多情况下,OpenCV就无能为力了。这个时候,你就需要翻阅计算机视觉、模式识别、机器学习领域顶级会议、期刊、杂志上面发表的文章了。然后,根据这些文章中阐述的原理和方法,来编程实现你要的东西。实际上,也就等于搭建一个属于你私有的积木块。其实,OpenCV中的每一个API函数,也就是这么来的。

    展开全文
  • 前言:刚开始是不打算写这篇博客的,但是我最近为了完成对老师布置的区域生长算法,强行要配置一波opencv,因为换了电脑,所以选择了有黑黑主题酷酷的VS2017,但无奈网上的博客关于vs2017配置Opencv不够全(vs2010...

     

    前言:刚开始是不打算写这篇博客的,但是我最近为了完成对老师布置的区域生长算法,强行要配置一波opencv,因为换了电脑,所以选择了有黑黑主题酷酷的VS2017,但无奈网上的博客关于vs2017配置Opencv不够全(vs2010配置opencv的倒是贼多!),我当时是看了将近10篇文章才配置成功,所以在此我来从一个新入坑的角度来说一下怎么完整的配置一个OPencv!下面就分步来进行说明吧!

    注意:不是博主这个版本的opencv本教程也是实用的,只需要改一点点东西就行,后面会进行提示!

     

    第一步:下载opencv安装包

    下载的途径有三种:

    1、官网下载

    在此,说明一下,没有必要去根据博主 的博客下载与博客相应的版本,其实只需要改一点点东西即可,后面进行说明,官网下载真的是贼头大,首先下载好好的突然说下载中断,而且无法恢复,此外,还慢,毕竟外网,没有办法。一个安装包大概100多M,而下载速度只有50多K(校园网20K,对面宿舍宽带50K),所以最后我实在无法忍受,已经失败了N次,时间紧,就没必要在这一棵树上撞死。

    但是网好的朋友也可以试试,在此我给出官网的网址,大家可以根据自己情况选择合适的版本(我的是3.4.1版本)

    网址:https://opencv.org/releases.html#

     

     

    2、网上资源

    靠谱的是CSDN等博主提供的安装包,但是是要C币的,我一般不会充值的,所以如果条件允许的话可以选择这个

    https://download.csdn.net/download/yu2439/10369150?utm_source=bbsseo(其中的一个网址)

    还有一个建议就是不要去尝试在一些钓鱼网站去下载opencv,我上回为了下载一个matlab破解版,虽然一路小心翼翼,最终下载了将近7、8个捆绑软件,连卸载时也给我加两个。。。。

     

    3、身边同学或者一起搞编辑的朋友们啊

    我的opencv就来自我对面宿舍的同学,他在官网下载成功了,直接用U盘拷过来用,还不是美滋滋,在此我给个百度网盘链接:opencv3.41

     

    二、解压下载文件

     

    双击下载好的或者拷好的安装包,选择解压路径后点Extract即可完成解压,解压后会自动生成一个opencv的文件夹(图2)

    图3中的文件夹如果配置环境比较多的筒子们还是非常眼熟的。

     

    第三步:配置系统变量(WIN10)

    1、找到此电脑(图4)

    2、右键->属性->高级系统设置->环境变量

    找到系统变量中的path变量,双击它,点击新建,将你解压的opencv文件夹中的***opencv\build\x64\vc14\bin路径添加到当中。这里我是选择了vc14这个子文件夹,也可以选择15,但后面要做出相应调整,所以不妨就vc14咯!

    注意:路径粘贴后记得点确定,并在外面点能点的确定全点选一遍!

     

    详细操作可看下图!

     

     

     

     

     

     

     

     

    以上完成了系统变量的配置,其实不是特别复杂,说简单点就是拷贝个路径

     

     

    第四步:相关文件的配置

     

    注意:网上很多教程没有这一步,会报那种找不到dll文件的错误,是因为没有将opencv里面的相关文件复制到C盘中的文件夹里面

    操作方法:将bin目录下面的opencv_world341.dll和opencv_world341d.dll文件复制到C:\Windows\SysWOW64这个文件夹里面即可

    将bin目录里面的opencv_ffmpeg341_64.dll复制到C:\Windows\System32这个文件夹里面(详细看图)

    如果是opencv其他的版本,把对应的dll文件移动到上述两个C盘文件夹即可!

     

     

     

     

     

     

     

    第五步:进入VS2017配置属性管理器进行相关配置

     

    1、新建一个项目,我这里建了一个空项目

    2、进入属性管理器,菜单栏->视图->其他窗口->属性管理器

    3、对Debug|X64进行配置,右键Microsoft.Cpp.x64.user,点击属性

    4、这里我们对属性中的 VC++目录->包含目录和VC++目录->库目录进行添加相关路径,对 链接器->输入->附加依赖项进行添加相关路径

    5、包含目录中加入

    F:\OPENCV\opencv\build\include

    F:\OPENCV\opencv\build\include\opencv

    F:\OPENCV\opencv\build\include\opencv2

    库目录中加入

    F:\OPENCV\opencv\build\x64\vc14\lib

    6、链接器->输入->附加依赖项中加入

    opencv_world341d.lib

    注意:博主后面了解到release|X64的配置为可选项,后面也有热心读者提醒,所以下面的这一步可以不做,因为编译时只用到了debug,release是发布程序时才会用到。

    //7、对Release|X64进行配置,右键Microsoft.Cpp.x64.user,点击属性

    //链接器->输入->附加依赖项中加入

    //opencv_world341.lib

    注意:如果是opencv其他版本,只需要将将上述的依赖项文件稍作修改即可;eg:如果是opencv版本3.4.0,只需要将dll名字改为opencv_world340d.lib即可,依次类推!

     

     

     

    第六步:勾选微软符号服务器(此步为可选项)

    其实到上一步已经配好了,但是到了关键时候VS2017总是说我少各种dll文件,百度上面说不用理这种错误,可以选择勾选微软符号服务器来下载缺失的dll文件

    操作步骤:左键调试->选项->常规 勾选启动源服务器支持

    符号那一项勾上微软符号服务器

    注意:可以用一个文件夹来缓存下载的dll文件,只是第一次加载,不用担心。或者,你也可以等加载完了之后,再把之前勾选的取消掉,也没有问题。 我建了一个文件夹(文件夹是我自己建的),后面发现它下载了很多pdb文件!

     

     

     

     

    注意:到上面所有工作,opencv已经配完了,注意解决方案平台那一栏要换成X64(因为我们一直都在配X64)

     

    第七步:测试环节!

    注意:由于很多人遇到图片无法输出的情况,在此特地添加一条需要注意的事项,读入图片路径时有两种方法。

    (1)绝对路径

    eg:Mat img = imread("F:\\suoxiaotu\\poyanghu缩小图.tif");

    注意这个地方的双斜杠很重要,直接弄过来的图片途径是只有一个斜杠的,两个斜杠的原因应该是第一个斜杠是转义字符。

    (2)图片路径

    eg:Mat img = imread("poyanghu缩小图.tif");

    这个地方有两点需要注意:第一个就是大家的图片格式一定要分清楚,建议自己右键查看目标图片的属性,另一个就是要注意这样做的前提条件就是你的图片就在你所建工程的目录下。不然会显示找不到图片。

    #include <iostream>  
    #include <opencv2/core/core.hpp>  
    #include <opencv2/highgui/highgui.hpp>  
    
    using namespace cv;
    
    int main()
    {
    	// 读入一张图片(poyanghu缩小图)    
    	Mat img = imread("F:\\suoxiaotu\\poyanghu缩小图.tif");
    	// 创建一个名为 "图片"窗口    
    	namedWindow("图片");
    	// 在窗口中显示图片   
    	imshow("图片", img);
    	// 等待6000 ms后窗口自动关闭    
    	waitKey(6000);
    	return 0;
    }

     

    输出:

     

    以上,配置完成!

    [-_-]眼睛累了吧,注意劳逸结合呀[-_-]

    ”若您愿意的话,请博主喝杯咖啡呗“

     

    下面是交流群,大家可以互相交流交流经验,互相解决配置遇到的问题,互相学习,不只是opencv!群号:830792425

    (2019年2月6号更新)可以说,博主这一篇文章就解决了我绝大多数问题!唯一补充的一点是,我用博主的方法,在运行代码的时候,发现waitKey(6000);这个函数结束后,其申请的内存没有正常释放,所以会引发异常。所以需要在这句的后面加上destroyAllWindows();实现内存的手动回收 @jerryNYX

     

    (2019年3月4号更新)加载符号遇到问题的朋友:“第六步勾选微软服务器”可以省略,把勾取消掉就能运行了,不然会卡住。希望对大家有帮助。@æ°å:

    (2019年4月22号更新)  谢谢博主,除了release部分我没有配置以外,其他部分按照博主的方法配置成功了。谢谢。@blackkkkk

    (2019年5月8日更新)小白成功了!非常感谢楼主!我遇到的问题“无法打开PDB文件”或者是“图像显示出来是灰色”的可能解决方法是:Debug|X64只放opencv_world341d,Release|X64只放opencv_world341 @https://me.csdn.net/weixin_40288785

    (2019年5月9日更新)主要针对CSDN编辑器的bug(给我的博客增加了好多空格,强迫症受不了),另外为方便阅读,将所有图片全部居中。

    展开全文
  • OpenCV学习

    2018-07-24 19:19:01
    另一篇参考文献: 中文手册 ...的番外篇,因为严格来说不是在讲Python而是讲在Python下使用OpenCV。本篇将介绍和深度学习数据处理阶段最相关的基础使用,并完成4个有趣实用的小例子: - 延时摄影小程序 - 视频中...

    另一篇参考文献: 中文手册

    转自:打开链接

    本篇是前面两篇教程:给深度学习入门者的Python快速教程 - 基础篇

    给深度学习入门者的Python快速教程 - numpy和Matplotlib篇

    的番外篇,因为严格来说不是在讲Python而是讲在Python下使用OpenCV。本篇将介绍和深度学习数据处理阶段最相关的基础使用,并完成4个有趣实用的小例子:

    - 延时摄影小程序

    - 视频中截屏采样的小程序

    - 图片数据增加(data augmentation)的小工具

    - 物体检测框标注小工具

    其中后两个例子的代码可以在下面地址直接下载:

    frombeijingwithlove/dlcv_for_beginners

    6.1 OpenCV简介

    OpenCV是计算机视觉领域应用最广泛的开源工具包,基于C/C++,支持Linux/Windows/MacOS/Android/iOS,并提供了Python,Matlab和Java等语言的接口,因为其丰富的接口,优秀的性能和商业友好的使用许可,不管是学术界还是业界中都非常受欢迎。OpenCV最早源于Intel公司1998年的一个研究项目,当时在Intel从事计算机视觉的工程师盖瑞·布拉德斯基(Gary Bradski)访问一些大学和研究组时发现学生之间实现计算机视觉算法用的都是各自实验室里的内部代码或者库,这样新来实验室的学生就能基于前人写的基本函数快速上手进行研究。于是OpenCV旨在提供一个用于计算机视觉的科研和商业应用的高性能通用库。 第一个alpha版本的OpenCV于2000年的CVPR上发布,在接下来的5年里,又陆续发布了5个beta版本,2006年发布了第一个正式版。2009年随着盖瑞加入了Willow Garage,OpenCV从Willow Garage得到了积极的支持,并发布了1.1版。2010年OpenCV发布了2.0版本,添加了非常完备的C++接口,从2.0开始的版本非常用户非常庞大,至今仍在维护和更新。2015年OpenCV 3正式发布,除了架构的调整,还加入了更多算法,更多性能的优化和更加简洁的API,另外也加强了对GPU的支持,现在已经在许多研究机构和商业公司中应用开来。

    6.1.1 OpenCV的结构

    和Python一样,当前的OpenCV也有两个大版本,OpenCV2和OpenCV3。相比OpenCV2,OpenCV3提供了更强的功能和更多方便的特性。不过考虑到和深度学习框架的兼容性,以及上手安装的难度,这部分先以2为主进行介绍。

    根据功能和需求的不同,OpenCV中的函数接口大体可以分为如下部分:

    - core:核心模块,主要包含了OpenCV中最基本的结构(矩阵,点线和形状等),以及相关的基础运算/操作。

    - imgproc:图像处理模块,包含和图像相关的基础功能(滤波,梯度,改变大小等),以及一些衍生的高级功能(图像分割,直方图,形态分析和边缘/直线提取等)。

    - highgui:提供了用户界面和文件读取的基本函数,比如图像显示窗口的生成和控制,图像/视频文件的IO等。

    如果不考虑视频应用,以上三个就是最核心和常用的模块了。针对视频和一些特别的视觉应用,OpenCV也提供了强劲的支持:

    - video:用于视频分析的常用功能,比如光流法(Optical Flow)和目标跟踪等。

    - calib3d:三维重建,立体视觉和相机标定等的相关功能。

    - features2d:二维特征相关的功能,主要是一些不受专利保护的,商业友好的特征点检测和匹配等功能,比如ORB特征。

    - object:目标检测模块,包含级联分类和Latent SVM

    - ml:机器学习算法模块,包含一些视觉中最常用的传统机器学习算法。

    - flann:最近邻算法库,Fast Library for Approximate Nearest Neighbors,用于在多维空间进行聚类和检索,经常和关键点匹配搭配使用。

    - gpu:包含了一些gpu加速的接口,底层的加速是CUDA实现。

    - photo:计算摄像学(Computational Photography)相关的接口,当然这只是个名字,其实只有图像修复和降噪而已。

    - stitching:图像拼接模块,有了它可以自己生成全景照片。

    - nonfree:受到专利保护的一些算法,其实就是SIFT和SURF。

    - contrib:一些实验性质的算法,考虑在未来版本中加入的。

    - legacy:字面是遗产,意思就是废弃的一些接口,保留是考虑到向下兼容。

    - ocl:利用OpenCL并行加速的一些接口。

    - superres:超分辨率模块,其实就是BTV-L1(Biliteral Total Variation – L1 regularization)算法

    - viz:基础的3D渲染模块,其实底层就是著名的3D工具包VTK(Visualization Toolkit)。

    从使用的角度来看,和OpenCV2相比,OpenCV3的主要变化是更多的功能和更细化的模块划分。

    6.1.2 安装和使用OpenCV

    作为最流行的视觉包,在Linux中安装OpenCV是非常方便的,大多数Linux的发行版都支持包管理器的安装,比如在Ubuntu 16.04 LTS中,只需要在终端中输入:

    >> sudo apt install libopencv-dev python-opencv

    当然也可以通过官网下载源码编译安装,第一步先安装各种依赖:

    >> sudo apt install build-essential

    >> sudo apt install cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev

    >> sudo apt-get install python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev

    然后找一个clone压缩包的文件夹,把源码拿下来:

    >> git clone opencv/opencv

    然后进入OpenCV文件夹:

    >> mkdir release

    >> cd release

    >> cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local ..

    准备完毕,直接make并安装:

    >> make

    >> sudo make install

    Windows下的安装也很简单,直接去OpenCV官网下载:

    DOWNLOADS | OpenCV

    执行exe安装后,会在<安装目录>/build/python/2.7下发现一个叫cv2.pyd的文件,把这个文件拷贝到<Python目录>\Lib\site-packages下,就可以了。Windows下如果只想在Python中体验OpenCV还有个更简单的方法是加州大学尔湾分校(University of California, Irvine)的Christoph Gohlke制作的Windows下的Python科学计算包网页,下载对应版本的wheel文件,然后通过pip安装:

    http://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv

    本书只讲Python下OpenCV基本使用,Python中导入OpenCV非常简单:

    import cv2
    

    就导入成功了。

    6.2 Python-OpenCV基础

    6.2.1 图像的表示

    前面章节已经提到过了单通道的灰度图像在计算机中的表示,就是一个8位无符号整形的矩阵。在OpenCV的C++代码中,表示图像有个专门的结构叫做cv::Mat,不过在Python-OpenCV中,因为已经有了numpy这种强大的基础工具,所以这个矩阵就用numpy的array表示。如果是多通道情况,最常见的就是红绿蓝(RGB)三通道,则第一个维度是高度,第二个维度是高度,第三个维度是通道,比如图6-1a是一幅3×3图像在计算机中表示的例子:

    图6-1 RGB图像在计算机中表示的例子

    图6-1中,右上角的矩阵里每个元素都是一个3维数组,分别代表这个像素上的三个通道的值。最常见的RGB通道中,第一个元素就是红色(Red)的值,第二个元素是绿色(Green)的值,第三个元素是蓝色(Blue),最终得到的图像如6-1a所示。RGB是最常见的情况,然而在OpenCV中,默认的图像的表示确实反过来的,也就是BGR,得到的图像是6-1b。可以看到,前两行的颜色顺序都交换了,最后一行是三个通道等值的灰度图,所以没有影响。至于OpenCV为什么不是人民群众喜闻乐见的RGB,这是历史遗留问题,在OpenCV刚开始研发的年代,BGR是相机设备厂商的主流表示方法,虽然后来RGB成了主流和默认,但是这个底层的顺序却保留下来了,事实上Windows下的最常见格式之一bmp,底层字节的存储顺序还是BGR。OpenCV的这个特殊之处还是需要注意的,比如在Python中,图像都是用numpy的array表示,但是同样的array在OpenCV中的显示效果和matplotlib中的显示效果就会不一样。下面的简单代码就可以生成两种表示方式下,图6-1中矩阵的对应的图像,生成图像后,放大看就能体会到区别:

    import numpy as np
    import cv2
    import matplotlib.pyplot as plt
    
    # 图6-1中的矩阵
    img = np.array([
        [[255, 0, 0], [0, 255, 0], [0, 0, 255]],
        [[255, 255, 0], [255, 0, 255], [0, 255, 255]],
        [[255, 255, 255], [128, 128, 128], [0, 0, 0]],
    ], dtype=np.uint8)
    
    # 用matplotlib存储
    plt.imsave('img_pyplot.jpg', img)
    
    # 用OpenCV存储
    cv2.imwrite('img_cv2.jpg', img)
    

    不管是RGB还是BGR,都是高度×宽度×通道数,H×W×C的表达方式,而在深度学习中,因为要对不同通道应用卷积,所以用的是另一种方式:C×H×W,就是把每个通道都单独表达成一个二维矩阵,如图6-1c所示。

    6.2.2 基本图像处理

    存取图像

    读图像用cv2.imread(),可以按照不同模式读取,一般最常用到的是读取单通道灰度图,或者直接默认读取多通道。存图像用cv2.imwrite(),注意存的时候是没有单通道这一说的,根据保存文件名的后缀和当前的array维度,OpenCV自动判断存的通道,另外压缩格式还可以指定存储质量,来看代码例子:

    import cv2
    
    # 读取一张400x600分辨率的图像
    color_img = cv2.imread('test_400x600.jpg')
    print(color_img.shape)
    
    # 直接读取单通道
    gray_img = cv2.imread('test_400x600.jpg', cv2.IMREAD_GRAYSCALE)
    print(gray_img.shape)
    
    # 把单通道图片保存后,再读取,仍然是3通道,相当于把单通道值复制到3个通道保存
    cv2.imwrite('test_grayscale.jpg', gray_img)
    reload_grayscale = cv2.imread('test_grayscale.jpg')
    print(reload_grayscale.shape)
    
    # cv2.IMWRITE_JPEG_QUALITY指定jpg质量,范围0到100,默认95,越高画质越好,文件越大
    cv2.imwrite('test_imwrite.jpg', color_img, (cv2.IMWRITE_JPEG_QUALITY, 80))
    
    # cv2.IMWRITE_PNG_COMPRESSION指定png质量,范围0到9,默认3,越高文件越小,画质越差
    cv2.imwrite('test_imwrite.png', color_img, (cv2.IMWRITE_PNG_COMPRESSION, 5))
    

    缩放,裁剪和补边

    缩放通过cv2.resize()实现,裁剪则是利用array自身的下标截取实现,此外OpenCV还可以给图像补边,这样能对一幅图像的形状和感兴趣区域实现各种操作。下面的例子中读取一幅400×600分辨率的图片,并执行一些基础的操作:

    import cv2
    
    # 读取一张四川大录古藏寨的照片
    img = cv2.imread('tiger_tibet_village.jpg')
    
    # 缩放成200x200的方形图像
    img_200x200 = cv2.resize(img, (200, 200))
    
    # 不直接指定缩放后大小,通过fx和fy指定缩放比例,0.5则长宽都为原来一半
    # 等效于img_200x300 = cv2.resize(img, (300, 200)),注意指定大小的格式是(宽度,高度)
    # 插值方法默认是cv2.INTER_LINEAR,这里指定为最近邻插值
    img_200x300 = cv2.resize(img, (0, 0), fx=0.5, fy=0.5, 
                                  interpolation=cv2.INTER_NEAREST)
    
    # 在上张图片的基础上,上下各贴50像素的黑边,生成300x300的图像
    img_300x300 = cv2.copyMakeBorder(img, 50, 50, 0, 0, 
                                           cv2.BORDER_CONSTANT, 
                                           value=(0, 0, 0))
    
    # 对照片中树的部分进行剪裁
    patch_tree = img[20:150, -180:-50]
    
    cv2.imwrite('cropped_tree.jpg', patch_tree)
    cv2.imwrite('resized_200x200.jpg', img_200x200)
    cv2.imwrite('resized_200x300.jpg', img_200x300)
    cv2.imwrite('bordered_300x300.jpg', img_300x300)
    

    这些处理的效果见图6-2。

    色调,明暗,直方图和Gamma曲线

    除了区域,图像本身的属性操作也非常多,比如可以通过HSV空间对色调和明暗进行调节。HSV空间是由美国的图形学专家A. R. Smith提出的一种颜色空间,HSV分别是色调(Hue),饱和度(Saturation)和明度(Value)。在HSV空间中进行调节就避免了直接在RGB空间中调节是还需要考虑三个通道的相关性。OpenCV中H的取值是[0, 180),其他两个通道的取值都是[0, 256),下面例子接着上面例子代码,通过HSV空间对图像进行调整:

    # 通过cv2.cvtColor把图像从BGR转换到HSV
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    
    # H空间中,绿色比黄色的值高一点,所以给每个像素+15,黄色的树叶就会变绿
    turn_green_hsv = img_hsv.copy()
    turn_green_hsv[:, :, 0] = (turn_green_hsv[:, :, 0]+15) % 180
    turn_green_img = cv2.cvtColor(turn_green_hsv, cv2.COLOR_HSV2BGR)
    cv2.imwrite('turn_green.jpg', turn_green_img)
    
    # 减小饱和度会让图像损失鲜艳,变得更灰
    colorless_hsv = img_hsv.copy()
    colorless_hsv[:, :, 1] = 0.5 * colorless_hsv[:, :, 1]
    colorless_img = cv2.cvtColor(colorless_hsv, cv2.COLOR_HSV2BGR)
    cv2.imwrite('colorless.jpg', colorless_img)
    
    # 减小明度为原来一半
    darker_hsv = img_hsv.copy()
    darker_hsv[:, :, 2] = 0.5 * darker_hsv[:, :, 2]
    darker_img = cv2.cvtColor(darker_hsv, cv2.COLOR_HSV2BGR)
    cv2.imwrite('darker.jpg', darker_img)
    

    无论是HSV还是RGB,我们都较难一眼就对像素中值的分布有细致的了解,这时候就需要直方图。如果直方图中的成分过于靠近0或者255,可能就出现了暗部细节不足或者亮部细节丢失的情况。比如图6-2中,背景里的暗部细节是非常弱的。这个时候,一个常用方法是考虑用Gamma变换来提升暗部细节。Gamma变换是矫正相机直接成像和人眼感受图像差别的一种常用手段,简单来说就是通过非线性变换让图像从对曝光强度的线性响应变得更接近人眼感受到的响应。具体的定义和实现,还是接着上面代码中读取的图片,执行计算直方图和Gamma变换的代码如下:

    import numpy as np
    
    # 分通道计算每个通道的直方图
    hist_b = cv2.calcHist([img], [0], None, [256], [0, 256])
    hist_g = cv2.calcHist([img], [1], None, [256], [0, 256])
    hist_r = cv2.calcHist([img], [2], None, [256], [0, 256])
    
    # 定义Gamma矫正的函数
    def gamma_trans(img, gamma):
        # 具体做法是先归一化到1,然后gamma作为指数值求出新的像素值再还原
        gamma_table = [np.power(x/255.0, gamma)*255.0 for x in range(256)]
        gamma_table = np.round(np.array(gamma_table)).astype(np.uint8)
        
        # 实现这个映射用的是OpenCV的查表函数
        return cv2.LUT(img, gamma_table)
    
    # 执行Gamma矫正,小于1的值让暗部细节大量提升,同时亮部细节少量提升
    img_corrected = gamma_trans(img, 0.5)
    cv2.imwrite('gamma_corrected.jpg', img_corrected)
    
    # 分通道计算Gamma矫正后的直方图
    hist_b_corrected = cv2.calcHist([img_corrected], [0], None, [256], [0, 256])
    hist_g_corrected = cv2.calcHist([img_corrected], [1], None, [256], [0, 256])
    hist_r_corrected = cv2.calcHist([img_corrected], [2], None, [256], [0, 256])
    
    # 将直方图进行可视化
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    
    fig = plt.figure()
    
    pix_hists = [
        [hist_b, hist_g, hist_r],
        [hist_b_corrected, hist_g_corrected, hist_r_corrected]
    ]
    
    pix_vals = range(256)
    for sub_plt, pix_hist in zip([121, 122], pix_hists):
        ax = fig.add_subplot(sub_plt, projection='3d')
        for c, z, channel_hist in zip(['b', 'g', 'r'], [20, 10, 0], pix_hist):
            cs = [c] * 256
            ax.bar(pix_vals, channel_hist, zs=z, zdir='y', color=cs, alpha=0.618, edgecolor='none', lw=0)
    
        ax.set_xlabel('Pixel Values')
        ax.set_xlim([0, 256])
        ax.set_ylabel('Channels')
        ax.set_zlabel('Counts')
    
    plt.show()
    

    上面三段代码的结果统一放在下图中:

     

    可以看到,Gamma变换后的暗部细节比起原图清楚了很多,并且从直方图来看,像素值也从集中在0附近变得散开了一些。

    6.2.3 图像的仿射变换

    图像的仿射变换涉及到图像的形状位置角度的变化,是深度学习预处理中常到的功能,在此简单回顾一下。仿射变换具体到图像中的应用,主要是对图像的缩放旋转剪切翻转平移的组合。在OpenCV中,仿射变换的矩阵是一个2×3的矩阵,其中左边的2×2子矩阵是线性变换矩阵,右边的2×1的两项是平移项:

    A=\begin{bmatrix} a_{00} & a_{01}\\ a_{10} & a_{11} \end{bmatrix} , B = \begin{bmatrix} b_{0}\\ b_{1} \end{bmatrix}

    M=\begin{bmatrix} A & B \end{bmatrix} = \begin{bmatrix} a_{00} & a_{01} & b_{0}\\ a_{10} & a_{11} & b_{1} \end{bmatrix}

    对于图像上的任一位置(x,y),仿射变换执行的是如下的操作:

    T_{affine}=A\begin{bmatrix} x\\ y \end{bmatrix}+B=M\begin{bmatrix} x\\ y\\ 1 \end{bmatrix}

    需要注意的是,对于图像而言,宽度方向是x,高度方向是y,坐标的顺序和图像像素对应下标一致。所以原点的位置不是左下角而是右上角,y的方向也不是向上,而是向下。在OpenCV中实现仿射变换是通过仿射变换矩阵和cv2.warpAffine()这个函数,还是通过代码来理解一下,例子中图片的分辨率为600×400:

    import cv2
    import numpy as np
    
    # 读取一张斯里兰卡拍摄的大象照片
    img = cv2.imread('lanka_safari.jpg')
    
    # 沿着横纵轴放大1.6倍,然后平移(-150,-240),最后沿原图大小截取,等效于裁剪并放大
    M_crop_elephant = np.array([
        [1.6, 0, -150],
        [0, 1.6, -240]
    ], dtype=np.float32)
    
    img_elephant = cv2.warpAffine(img, M_crop_elephant, (400, 600))
    cv2.imwrite('lanka_elephant.jpg', img_elephant)
    
    # x轴的剪切变换,角度15°
    theta = 15 * np.pi / 180
    M_shear = np.array([
        [1, np.tan(theta), 0],
        [0, 1, 0]
    ], dtype=np.float32)
    
    img_sheared = cv2.warpAffine(img, M_shear, (400, 600))
    cv2.imwrite('lanka_safari_sheared.jpg', img_sheared)
    
    # 顺时针旋转,角度15°
    M_rotate = np.array([
        [np.cos(theta), -np.sin(theta), 0],
        [np.sin(theta), np.cos(theta), 0]
    ], dtype=np.float32)
    
    img_rotated = cv2.warpAffine(img, M_rotate, (400, 600))
    cv2.imwrite('lanka_safari_rotated.jpg', img_rotated)
    
    # 某种变换,具体旋转+缩放+旋转组合可以通过SVD分解理解
    M = np.array([
        [1, 1.5, -400],
        [0.5, 2, -100]
    ], dtype=np.float32)
    
    img_transformed = cv2.warpAffine(img, M, (400, 600))
    cv2.imwrite('lanka_safari_transformed.jpg', img_transformed)
    

    代码实现的操作示意在下图中:

    6.2.4 基本绘图

    OpenCV提供了各种绘图的函数,可以在画面上绘制线段,圆,矩形和多边形等,还可以在图像上指定位置打印文字,比如下面例子:

    import numpy as np
    import cv2
    
    # 定义一块宽600,高400的画布,初始化为白色
    canvas = np.zeros((400, 600, 3), dtype=np.uint8) + 255
    
    # 画一条纵向的正中央的黑色分界线
    cv2.line(canvas, (300, 0), (300, 399), (0, 0, 0), 2)
    
    # 画一条右半部份画面以150为界的横向分界线
    cv2.line(canvas, (300, 149), (599, 149), (0, 0, 0), 2)
    
    # 左半部分的右下角画个红色的圆
    cv2.circle(canvas, (200, 300), 75, (0, 0, 255), 5)
    
    # 左半部分的左下角画个蓝色的矩形
    cv2.rectangle(canvas, (20, 240), (100, 360), (255, 0, 0), thickness=3)
    
    # 定义两个三角形,并执行内部绿色填充
    triangles = np.array([
        [(200, 240), (145, 333), (255, 333)],
        [(60, 180), (20, 237), (100, 237)]])
    cv2.fillPoly(canvas, triangles, (0, 255, 0))
    
    # 画一个黄色五角星
    # 第一步通过旋转角度的办法求出五个顶点
    phi = 4 * np.pi / 5
    rotations = [[[np.cos(i * phi), -np.sin(i * phi)], [i * np.sin(phi), np.cos(i * phi)]] for i in range(1, 5)]
    pentagram = np.array([[[[0, -1]] + [np.dot(m, (0, -1)) for m in rotations]]], dtype=np.float)
    
    # 定义缩放倍数和平移向量把五角星画在左半部分画面的上方
    pentagram = np.round(pentagram * 80 + np.array([160, 120])).astype(np.int)
    
    # 将5个顶点作为多边形顶点连线,得到五角星
    cv2.polylines(canvas, pentagram, True, (0, 255, 255), 9)
    
    # 按像素为间隔从左至右在画面右半部份的上方画出HSV空间的色调连续变化
    for x in range(302, 600):
        color_pixel = np.array([[[round(180*float(x-302)/298), 255, 255]]], dtype=np.uint8)
        line_color = [int(c) for c in cv2.cvtColor(color_pixel, cv2.COLOR_HSV2BGR)[0][0]]
        cv2.line(canvas, (x, 0), (x, 147), line_color)
    
    # 如果定义圆的线宽大于半斤,则等效于画圆点,随机在画面右下角的框内生成坐标
    np.random.seed(42)
    n_pts = 30
    pts_x = np.random.randint(310, 590, n_pts)
    pts_y = np.random.randint(160, 390, n_pts)
    pts = zip(pts_x, pts_y)
    
    # 画出每个点,颜色随机
    for pt in pts:
        pt_color = [int(c) for c in np.random.randint(0, 255, 3)]
        cv2.circle(canvas, pt, 3, pt_color, 5)
    
    # 在左半部分最上方打印文字
    cv2.putText(canvas,
                'Python-OpenCV Drawing Example',
                (5, 15),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.5,
                (0, 0, 0),
                1)
    
    cv2.imshow('Example of basic drawing functions', canvas)
    cv2.waitKey()
    

    执行这段代码得到如下的图像:

    6.2.4 视频功能

    视频中最常用的就是从视频设备采集图片或者视频,或者读取视频文件并从中采样。所以比较重要的也是两个模块,一个是VideoCapture,用于获取相机设备并捕获图像和视频,或是从文件中捕获。还有一个VideoWriter,用于生成视频。还是来看例子理解这两个功能的用法,首先是一个制作延时摄影视频的小例子:

    import cv2
    import time
    
    interval = 60       	# 捕获图像的间隔,单位:秒
    num_frames = 500    	# 捕获图像的总帧数
    out_fps = 24        	# 输出文件的帧率
    
    # VideoCapture(0)表示打开默认的相机
    cap = cv2.VideoCapture(0)
    
    # 获取捕获的分辨率
    size =(int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
           int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
           
    # 设置要保存视频的编码,分辨率和帧率
    video = cv2.VideoWriter(
        "time_lapse.avi", 
        cv2.VideoWriter_fourcc('M','P','4','2'), 
        out_fps, 
        size
    )
    
    # 对于一些低画质的摄像头,前面的帧可能不稳定,略过
    for i in range(42):
        cap.read()
    
    # 开始捕获,通过read()函数获取捕获的帧
    try:
        for i in range(num_frames):
            _, frame = cap.read()
            video.write(frame)
    
            # 如果希望把每一帧也存成文件,比如制作GIF,则取消下面的注释
            # filename = '{:0>6d}.png'.format(i)
            # cv2.imwrite(filename, frame)
    
            print('Frame {} is captured.'.format(i))
            time.sleep(interval)
    except KeyboardInterrupt:
        # 提前停止捕获
        print('Stopped! {}/{} frames captured!'.format(i, num_frames))
    
    # 释放资源并写入视频文件
    video.release()
    cap.release()
    

    这个例子实现了延时摄影的功能,把程序打开并将摄像头对准一些缓慢变化的画面,比如桌上缓慢蒸发的水,或者正在生长的小草,就能制作出有趣的延时摄影作品。比如下面这个链接中的图片就是用这段程序生成的:

    http://images.cnitblog.com/blog2015/609274/201503/251904209276278.gif

    程序的结构非常清晰简单,注释里也写清楚了每一步,所以流程就不解释了。需要提一下的有两点:一个是VideoWriter中的一个函数cv2.VideoWriter_fourcc()。这个函数指定了视频编码的格式,比如例子中用的是MP42,也就是MPEG-4,更多编码方式可以在下面的地址查询:

    Video Codecs by FOURCC

    还有一个是KeyboardInterrupt,这是一个常用的异常,用来获取用户Ctrl+C的中止,捕获这个异常后直接结束循环并释放VideoCapture和VideoWriter的资源,使已经捕获好的部分视频可以顺利生成。

    从视频中截取帧也是处理视频时常见的任务,下面代码实现的是遍历一个指定文件夹下的所有视频并按照指定的间隔进行截屏并保存:

    import cv2
    import os
    import sys
    
    # 第一个输入参数是包含视频片段的路径
    input_path = sys.argv[1]
    
    # 第二个输入参数是设定每隔多少帧截取一帧
    frame_interval = int(sys.argv[2])
    
    # 列出文件夹下所有的视频文件
    filenames = os.listdir(input_path)
    
    # 获取文件夹名称
    video_prefix = input_path.split(os.sep)[-1]
    
    # 建立一个新的文件夹,名称为原文件夹名称后加上_frames
    frame_path = '{}_frames'.format(input_path)
    if not os.path.exists(frame_path):
        os.mkdir(frame_path)
    
    # 初始化一个VideoCapture对象
    cap = cv2.VideoCapture()
    
    # 遍历所有文件
    for filename in filenames:
        filepath = os.sep.join([input_path, filename])
        
        # VideoCapture::open函数可以从文件获取视频
        cap.open(filepath)
        
        # 获取视频帧数
        n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    
        # 同样为了避免视频头几帧质量低下,黑屏或者无关等
        for i in range(42):
            cap.read()
        
        for i in range(n_frames):
            ret, frame = cap.read()
            
            # 每隔frame_interval帧进行一次截屏操作
            if i % frame_interval == 0:
                imagename = '{}_{}_{:0>6d}.jpg'.format(video_prefix, filename.split('.')[0], i)
                imagepath = os.sep.join([frame_path, imagename])
                print('exported {}!'.format(imagepath))
                cv2.imwrite(imagepath, frame)
    
    # 执行结束释放资源
    cap.release()
    

    6.3 用OpenCV实现数据增加小工具

    到目前我们已经熟悉了numpy中的随机模块,多进程调用和OpenCV的基本操作,基于这些基础,本节将从思路到代码一步步实现一个最基本的数据增加小工具。

    第三章和第四章都提到过数据增加(data augmentation),作为一种深度学习中的常用手段,数据增加对模型的泛化性和准确性都有帮助。数据增加的具体使用方式一般有两种,一种是实时增加,比如在Caffe中加入数据扰动层,每次图像都先经过扰动操作,再去训练,这样训练经过几代(epoch)之后,就等效于数据增加。还有一种是更加直接简单一些的,就是在训练之前就通过图像处理手段对数据样本进行扰动和增加,也就是本节要实现的。

    这个例子中将包含三种基本类型的扰动:随机裁剪,随机旋转和随机颜色/明暗。

    6.3.1 随机裁剪

    AlexNet中已经讲过了随机裁剪的基本思路,我们的小例子中打算更进一步:在裁剪的时候考虑图像宽高比的扰动。在绝大多数用于分类的图片中,样本进入网络前都是要变为统一大小,所以宽高比扰动相当于对物体的横向和纵向进行了缩放,这样除了物体的位置扰动,又多出了一项扰动。只要变化范围控制合适,目标物体始终在画面内,这种扰动是有助于提升泛化性能的。实现这种裁剪的思路如下图所示:

    图中最左边是一幅需要剪裁的画面,首先根据这幅画面我们可以算出一个宽高比w/h。然后设定一个小的扰动范围δ和要裁剪的画面占原画面的比例β,从-\delta\delta之间按均匀采样,获取一个随机数\hat \delta作为裁剪后画面的宽高比扰动的比例,则裁剪后画面的宽和高分别为:

    w'=w\sqrt {\beta(1+ \hat\delta)}

    h'=h\sqrt {\frac \beta {1+ \hat\delta}}

    想象一下先把这个宽为w’,高为h’的区域置于原画面的右下角,则这个区域的左上角和原画面的左上角框出的小区域,如图中的虚线框所示,就是裁剪后区域左上角可以取值的范围。所以在这个区域内随机采一点作为裁剪区域的左上角,就实现了如图中位置随机,且宽高比也随机的裁剪。

    6.3.2 随机旋转

    前面讲到过的旋转比起来,做数据增加时,一般希望旋转是沿着画面的中心。这样除了要知道旋转角度,还得计算平移的量才能让仿射变换的效果等效于旋转轴在画面中心,好在OpenCV中有现成的函数cv2.getRotationMatrix2D()可以使用。这个函数的第一个参数是旋转中心,第二个参数是逆时针旋转角度,第三个参数是缩放倍数,对于只是旋转的情况下这个值是1,返回值就是做仿射变换的矩阵。

    直接用这个函数并接着使用cv2.warpAffine()会有一个潜在的问题,就是旋转之后会出现黑边。如果要旋转后的画面不包含黑边,就得沿着原来画面的轮廓做个内接矩形,该矩形的宽高比和原画面相同,如下图所示:

    在图中,可以看到,限制内接矩形大小的主要是原画面更靠近中心的那条边,也就是图中比较长的一条边AB。因此我们只要沿着中心O和内接矩形的顶点方向的直线,求出和AB的交点P,就得到了内接矩形的大小。先来看长边的方程,考虑之前画面和横轴相交的点,经过角度-θ旋转后,到了图中的Q点所在: \begin{bmatrix} x_{Q} \\ y_{Q} \end{bmatrix}=\begin{bmatrix} \cos(-\theta) & -\cos(-\theta)\\ \sin(-\theta) & \cos(-\theta) \end{bmatrix} \begin{bmatrix} - \frac w 2\\ 0 \end{bmatrix}= \begin{bmatrix} - \frac w 2 \cos(\theta)\\ \frac w 2 \sin(\theta) \end{bmatrix}

    因为长边所在直线过Q点,且斜率为1/tan(θ),所以有:

    \frac {y-\frac w 2\sin(\theta)} {x+\frac w 2\cos(\theta)}=\frac 1 {\tan(\theta)}

    这时候考虑OP这条直线:

    y=-\frac h wx

    把这个公式带入再前边一个公式,求解可以得到:

    x=-\frac w 2 \cdot \frac {\cos(\theta)+\sin(\theta)\tan(\theta)} {\frac h w \tan(\theta)+1}

    注意到在这个问题中,每个象限和相邻象限都是轴对称的,而且旋转角度对剪裁宽度和长度的影响是周期(T=π)变化,再加上我们关心的其实并不是四个点的位置,而是旋转后要截取的矩形的宽w’和高h’,所以复杂的分区间情况也简化了,首先对于旋转角度,因为周期为π,所以都可以化到0到π之间,然后因为对称性,进一步有:

    \theta=\begin{cases} \theta, & \text{if $\theta \leq \frac \pi 2$} \\ \pi-\theta, & \text{else} \end{cases}

    于是对于0到π/2之间的θ,有:

    r=\begin{cases} h/w, & \text{if h$>$w} \\ w/h, & \text{else} \end{cases}

    \beta=\frac {\cos(\theta)+\sin(\theta)\tan(\theta)} {r\tan(\theta)+1}

    w'=\beta w

    h'=\beta h

    当然需要注意的是,对于宽高比非常大或者非常小的图片,旋转后如果裁剪往往得到的画面是非常小的一部分,甚至不包含目标物体。所以是否需要旋转,以及是否需要裁剪,如果裁剪角度多少合适,都要视情况而定。

    6.3.3 随机颜色和明暗

    比起AlexNet论文里在PCA之后的主成分上做扰动的方法,本书用来实现随机的颜色以及明暗的方法相对简单很多,就是给HSV空间的每个通道,分别加上一个微小的扰动。其中对于色调,从-\delta\delta之间按均匀采样,获取一个随机数\hat \delta作为要扰动的值,然后新的像素值x’为原始像素值x +\hat \delta;对于其他两个空间则是新像素值x’为原始像素值x的(1+\hat \delta)倍,从而实现色调,饱和度和明暗度的扰动。

    因为明暗度并不会对图像的直方图相对分布产生大的影响,所以在HSV扰动基础上,考虑再加入一个Gamma扰动,方法是设定一个大于1的Gamma值的上限γ,因为这个值通常会和1是一个量级,再用均匀采样的近似未必合适,所以从-logγ到logγ之间均匀采样一个值α,然后用e^{\alpha}

    作为Gamma值进行变换。

    6.3.4 多进程调用加速处理

    做数据增加时如果样本量本身就不小,则处理起来可能会很耗费时间,所以可以考虑利用多进程并行处理。比如我们的例子中,设定使用场景是输入一个文件夹路径,该文件夹下包含了所有原始的数据样本。用户指定输出的文件夹和打算增加图片的总量。执行程序的时候,通过os.listdir()获取所有文件的路径,然后按照上一章讲过的多进程平均划分样本的办法,把文件尽可能均匀地分给不同进程,进行处理。

    6.3.5 代码:图片数据增加小工具

    按照前面4个部分的思路和方法,这节来实现这么一个图片数据增加小工具,首先对于一些基础的操作,我们定义在一个叫做image_augmentation.py的文件里:

    import numpy as np
    import cv2
    
    '''
    定义裁剪函数,四个参数分别是:
    左上角横坐标x0
    左上角纵坐标y0
    裁剪宽度w
    裁剪高度h
    '''
    crop_image = lambda img, x0, y0, w, h: img[y0:y0+h, x0:x0+w]
    
    '''
    随机裁剪
    area_ratio为裁剪画面占原画面的比例
    hw_vari是扰动占原高宽比的比例范围
    '''
    def random_crop(img, area_ratio, hw_vari):
        h, w = img.shape[:2]
        hw_delta = np.random.uniform(-hw_vari, hw_vari)
        hw_mult = 1 + hw_delta
    	
    	# 下标进行裁剪,宽高必须是正整数
        w_crop = int(round(w*np.sqrt(area_ratio*hw_mult)))
    	
    	# 裁剪宽度不可超过原图可裁剪宽度
        if w_crop > w:
            w_crop = w
    		
        h_crop = int(round(h*np.sqrt(area_ratio/hw_mult)))
        if h_crop > h:
            h_crop = h
    	
    	# 随机生成左上角的位置
        x0 = np.random.randint(0, w-w_crop+1)
        y0 = np.random.randint(0, h-h_crop+1)
    	
        return crop_image(img, x0, y0, w_crop, h_crop)
    
    '''
    定义旋转函数:
    angle是逆时针旋转的角度
    crop是个布尔值,表明是否要裁剪去除黑边
    '''
    def rotate_image(img, angle, crop):
        h, w = img.shape[:2]
    	
    	# 旋转角度的周期是360°
        angle %= 360
    	
    	# 用OpenCV内置函数计算仿射矩阵
        M_rotate = cv2.getRotationMatrix2D((w/2, h/2), angle, 1)
    	
    	# 得到旋转后的图像
        img_rotated = cv2.warpAffine(img, M_rotate, (w, h))
    
    	# 如果需要裁剪去除黑边
        if crop:
    	    # 对于裁剪角度的等效周期是180°
            angle_crop = angle % 180
    		
    		# 并且关于90°对称
            if angle_crop > 90:
                angle_crop = 180 - angle_crop
    			
    		# 转化角度为弧度
            theta = angle_crop * np.pi / 180.0
    		
    		# 计算高宽比
            hw_ratio = float(h) / float(w)
    		
    		# 计算裁剪边长系数的分子项
            tan_theta = np.tan(theta)
            numerator = np.cos(theta) + np.sin(theta) * tan_theta
    		
    		# 计算分母项中和宽高比相关的项
            r = hw_ratio if h > w else 1 / hw_ratio
    		
    		# 计算分母项
            denominator = r * tan_theta + 1
    		
    		# 计算最终的边长系数
            crop_mult = numerator / denominator
    		
    		# 得到裁剪区域
            w_crop = int(round(crop_mult*w))
            h_crop = int(round(crop_mult*h))
            x0 = int((w-w_crop)/2)
            y0 = int((h-h_crop)/2)
    
            img_rotated = crop_image(img_rotated, x0, y0, w_crop, h_crop)
    
        return img_rotated
    
    '''
    随机旋转
    angle_vari是旋转角度的范围[-angle_vari, angle_vari)
    p_crop是要进行去黑边裁剪的比例
    '''
    def random_rotate(img, angle_vari, p_crop):
        angle = np.random.uniform(-angle_vari, angle_vari)
        crop = False if np.random.random() > p_crop else True
        return rotate_image(img, angle, crop)
    
    '''
    定义hsv变换函数:
    hue_delta是色调变化比例
    sat_delta是饱和度变化比例
    val_delta是明度变化比例
    '''
    def hsv_transform(img, hue_delta, sat_mult, val_mult):
        img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float)
        img_hsv[:, :, 0] = (img_hsv[:, :, 0] + hue_delta) % 180
        img_hsv[:, :, 1] *= sat_mult
        img_hsv[:, :, 2] *= val_mult
        img_hsv[img_hsv > 255] = 255
        return cv2.cvtColor(np.round(img_hsv).astype(np.uint8), cv2.COLOR_HSV2BGR)
    
    '''
    随机hsv变换
    hue_vari是色调变化比例的范围
    sat_vari是饱和度变化比例的范围
    val_vari是明度变化比例的范围
    '''
    def random_hsv_transform(img, hue_vari, sat_vari, val_vari):
        hue_delta = np.random.randint(-hue_vari, hue_vari)
        sat_mult = 1 + np.random.uniform(-sat_vari, sat_vari)
        val_mult = 1 + np.random.uniform(-val_vari, val_vari)
        return hsv_transform(img, hue_delta, sat_mult, val_mult)
    
    '''
    定义gamma变换函数:
    gamma就是Gamma
    '''
    def gamma_transform(img, gamma):
        gamma_table = [np.power(x / 255.0, gamma) * 255.0 for x in range(256)]
        gamma_table = np.round(np.array(gamma_table)).astype(np.uint8)
        return cv2.LUT(img, gamma_table)
    
    '''
    随机gamma变换
    gamma_vari是Gamma变化的范围[1/gamma_vari, gamma_vari)
    '''
    def random_gamma_transform(img, gamma_vari):
        log_gamma_vari = np.log(gamma_vari)
        alpha = np.random.uniform(-log_gamma_vari, log_gamma_vari)
        gamma = np.exp(alpha)
        return gamma_transform(img, gamma)
    

    调用这些函数需要通过一个主程序。这个主程序里首先定义三个子模块,定义一个函数parse_arg()通过Python的argparse模块定义了各种输入参数和默认值。需要注意的是这里用argparse来输入所有参数是因为参数总量并不是特别多,如果增加了更多的扰动方法,更合适的参数输入方式可能是通过一个配置文件。然后定义一个生成待处理图像列表的函数generate_image_list(),根据输入中要增加图片的数量和并行进程的数目尽可能均匀地为每个进程生成了需要处理的任务列表。执行随机扰动的代码定义在augment_images()中,这个函数是每个进程内进行实际处理的函数,执行顺序是镜像\rightarrow裁剪\rightarrow旋转\rightarrowHSV\rightarrowGamma。需要注意的是镜像\rightarrow裁剪,因为只是个演示例子,这未必是一个合适的顺序。最后定义一个main函数进行调用,代码如下:

    import os
    import argparse
    import random
    import math
    from multiprocessing import Process
    from multiprocessing import cpu_count
    
    import cv2
    
    # 导入image_augmentation.py为一个可调用模块
    import image_augmentation as ia
    
    # 利用Python的argparse模块读取输入输出和各种扰动参数
    def parse_args():
        parser = argparse.ArgumentParser(
            description='A Simple Image Data Augmentation Tool',
            formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    
        parser.add_argument('input_dir',
                            help='Directory containing images')
        parser.add_argument('output_dir',
                            help='Directory for augmented images')
        parser.add_argument('num',
                            help='Number of images to be augmented',
                            type=int)
    
        parser.add_argument('--num_procs',
                            help='Number of processes for paralleled augmentation',
                            type=int, default=cpu_count())
    
        parser.add_argument('--p_mirror',
                            help='Ratio to mirror an image',
                            type=float, default=0.5)
    
        parser.add_argument('--p_crop',
                            help='Ratio to randomly crop an image',
                            type=float, default=1.0)
        parser.add_argument('--crop_size',
                            help='The ratio of cropped image size to original image size, in area',
                            type=float, default=0.8)
        parser.add_argument('--crop_hw_vari',
                            help='Variation of h/w ratio',
                            type=float, default=0.1)
    
        parser.add_argument('--p_rotate',
                            help='Ratio to randomly rotate an image',
                            type=float, default=1.0)
        parser.add_argument('--p_rotate_crop',
                            help='Ratio to crop out the empty part in a rotated image',
                            type=float, default=1.0)
        parser.add_argument('--rotate_angle_vari',
                            help='Variation range of rotate angle',
                            type=float, default=10.0)
    
        parser.add_argument('--p_hsv',
                            help='Ratio to randomly change gamma of an image',
                            type=float, default=1.0)
        parser.add_argument('--hue_vari',
                            help='Variation of hue',
                            type=int, default=10)
        parser.add_argument('--sat_vari',
                            help='Variation of saturation',
                            type=float, default=0.1)
        parser.add_argument('--val_vari',
                            help='Variation of value',
                            type=float, default=0.1)
    
        parser.add_argument('--p_gamma',
                            help='Ratio to randomly change gamma of an image',
                            type=float, default=1.0)
        parser.add_argument('--gamma_vari',
                            help='Variation of gamma',
                            type=float, default=2.0)
    
        args = parser.parse_args()
        args.input_dir = args.input_dir.rstrip('/')
        args.output_dir = args.output_dir.rstrip('/')
    
        return args
    
    '''
    根据进程数和要增加的目标图片数,
    生成每个进程要处理的文件列表和每个文件要增加的数目
    '''
    def generate_image_list(args):
        # 获取所有文件名和文件总数
        filenames = os.listdir(args.input_dir)
        num_imgs = len(filenames)
    
    	# 计算平均处理的数目并向下取整
        num_ave_aug = int(math.floor(args.num/num_imgs))
    	
    	# 剩下的部分不足平均分配到每一个文件,所以做成一个随机幸运列表
    	# 对于幸运的文件就多增加一个,凑够指定的数目
        rem = args.num - num_ave_aug*num_imgs
        lucky_seq = [True]*rem + [False]*(num_imgs-rem)
        random.shuffle(lucky_seq)
    
    	# 根据平均分配和幸运表策略,
    	# 生成每个文件的全路径和对应要增加的数目并放到一个list里
        img_list = [
            (os.sep.join([args.input_dir, filename]), num_ave_aug+1 if lucky else num_ave_aug)
            for filename, lucky in zip(filenames, lucky_seq)
        ]
    	
    	# 文件可能大小不一,处理时间也不一样,
    	# 所以随机打乱,尽可能保证处理时间均匀
        random.shuffle(img_list)
    
    	# 生成每个进程的文件列表,
    	# 尽可能均匀地划分每个进程要处理的数目
        length = float(num_imgs) / float(args.num_procs)
        indices = [int(round(i * length)) for i in range(args.num_procs + 1)]
        return [img_list[indices[i]:indices[i + 1]] for i in range(args.num_procs)]
    
    # 每个进程内调用图像处理函数进行扰动的函数
    def augment_images(filelist, args):
        # 遍历所有列表内的文件
        for filepath, n in filelist:
            img = cv2.imread(filepath)
            filename = filepath.split(os.sep)[-1]
            dot_pos = filename.rfind('.')
    		
    		# 获取文件名和后缀名
            imgname = filename[:dot_pos]
            ext = filename[dot_pos:]
    
            print('Augmenting {} ...'.format(filename))
            for i in range(n):
                img_varied = img.copy()
    			
    			# 扰动后文件名的前缀
                varied_imgname = '{}_{:0>3d}_'.format(imgname, i)
    			
    			# 按照比例随机对图像进行镜像
                if random.random() < args.p_mirror:
    			    # 利用numpy.fliplr(img_varied)也能实现
                    img_varied = cv2.flip(img_varied, 1)
                    varied_imgname += 'm'
    			
    			# 按照比例随机对图像进行裁剪
                if random.random() < args.p_crop:
                    img_varied = ia.random_crop(
                        img_varied,
                        args.crop_size,
                        args.crop_hw_vari)
                    varied_imgname += 'c'
    			
    			# 按照比例随机对图像进行旋转
                if random.random() < args.p_rotate:
                    img_varied = ia.random_rotate(
                        img_varied,
                        args.rotate_angle_vari,
                        args.p_rotate_crop)
                    varied_imgname += 'r'
    			
    			# 按照比例随机对图像进行HSV扰动
                if random.random() < args.p_hsv:
                    img_varied = ia.random_hsv_transform(
                        img_varied,
                        args.hue_vari,
                        args.sat_vari,
                        args.val_vari)
                    varied_imgname += 'h'
    			
    			# 按照比例随机对图像进行Gamma扰动
                if random.random() < args.p_gamma:
                    img_varied = ia.random_gamma_transform(
                        img_varied,
                        args.gamma_vari)
                    varied_imgname += 'g'
    			
    			# 生成扰动后的文件名并保存在指定的路径
                output_filepath = os.sep.join([
                    args.output_dir,
                    '{}{}'.format(varied_imgname, ext)])
                cv2.imwrite(output_filepath, img_varied)
    
    # 主函数
    def main():
        # 获取输入输出和变换选项
        args = parse_args()
        params_str = str(args)[10:-1]
    
    	# 如果输出文件夹不存在,则建立文件夹
        if not os.path.exists(args.output_dir):
            os.mkdir(args.output_dir)
    
        print('Starting image data augmentation for {}\n'
              'with\n{}\n'.format(args.input_dir, params_str))
    
    	# 生成每个进程要处理的列表
        sublists = generate_image_list(args)
    	
    	# 创建进程
        processes = [Process(target=augment_images, args=(x, args, )) for x in sublists]
    
    	# 并行多进程处理
        for p in processes:
            p.start()
    
        for p in processes:
            p.join()
    
        print('\nDone!')
    
    if __name__ == '__main__':
        main()
    

    为了排版方便,并没有很遵守Python的规范(PEP8)。注意到除了前面提的三种类型的变化,还增加了镜像变化,这主要是因为这种变换太简单了,顺手就写上了。还有默认进程数用的是cpu_count()函数,这个获取的是cpu的核数。把这段代码保存为run_augmentation.py,然后在命令行输入:

    >> python run_augmentation.py -h

     

    或者

    >> python run_augmentation.py --help

     

    就能看到脚本的使用方法,每个参数的含义,还有默认值。接下里来执行一个图片增加任务:

    >> python run_augmentation.py imagenet_samples more_samples 1000 --rotate_angle_vari 180 --p_rotate_crop 0.5

    其中imagenet_samples为一些从imagenet图片url中随机下载的一些图片,--rotate_angle_vari设为180方便测试全方向的旋转,--p_rotate_crop设置为0.5,让旋转裁剪对一半图片生效。扰动增加后的1000张图片在more_samples文件夹下,得到的部分结果如下:

    6.4 用OpenCV实现数据标注小工具

    除了对图像的处理,OpenCV的图形用户界面(Graphical User Interface, GUI)和绘图等相关功能也是很有用的功能,无论是可视化,图像调试还是我们这节要实现的标注任务,都可以有所帮助。这节先介绍OpenCV窗口的最基本使用和交互,然后基于这些基础和之前的知识实现一个用于物体检测任务标注的小工具。

    6.4.1 OpenCV窗口循环

    OpenCV显示一幅图片的函数是cv2.imshow(),第一个参数是显示图片的窗口名称,第二个参数是图片的array。不过如果直接执行这个函数的话,什么都不会发生,因为这个函数得配合cv2.waitKey()一起使用。cv2.waitKey()指定当前的窗口显示要持续的毫秒数,比如cv2.waitKey(1000)就是显示一秒,然后窗口就关闭了。比较特殊的是cv2.waitKey(0),并不是显示0毫秒的意思,而是一直显示,直到有键盘上的按键被按下,或者鼠标点击了窗口的小叉子才关闭。cv2.waitKey()的默认参数就是0,所以对于图像展示的场景,cv2.waitKey()或者cv2.waitKey(0)是最常用的:

    import cv2
    
    img = cv2.imread('Aitutaki.png')
    cv2.imshow('Honeymoon Island', img)
    cv2.waitKey()
    

    执行这段代码得到如下窗口:

    cv2.waitKey()参数不为零的时候则可以和循环结合产生动态画面,比如在6.2.4的延时小例子中,我们把延时摄影保存下来的所有图像放到一个叫做frames的文件夹下。下面代码从frames的文件夹下读取所有图片并以24的帧率在窗口中显示成动画:

    import os
    from itertools import cycle
    import cv2
    
    # 列出frames文件夹下的所有图片
    filenames = os.listdir('frames')
    
    # 通过itertools.cycle生成一个无限循环的迭代器,每次迭代都输出下一张图像对象
    img_iter = cycle([cv2.imread(os.sep.join(['frames', x])) for x in filenames])
    
    key = 0
    while key & 0xFF != 27:
        cv2.imshow('Animation', next(img_iter))
        key = cv2.waitKey(42)
    

    在这个例子中我们采用了Python的itertools模块中的cycle函数,这个函数可以把一个可遍历结构编程一个无限循环的迭代器。另外从这个例子中我们还发现,cv2.waitKey()返回的就是键盘上出发的按键。对于字母就是ascii码,特殊按键比如上下左右等,则对应特殊的值,其实这就是键盘事件的最基本用法。

    6.4.2 鼠标和键盘事件

    因为GUI总是交互的,所以鼠标和键盘事件基本使用必不可少,上节已经提到了cv2.waitKey()就是获取键盘消息的最基本方法。比如下面这段循环代码就能够获取键盘上按下的按键,并在终端输出:

    while key != 27:
        cv2.imshow('Honeymoon Island', img)
        key = cv2.waitKey()
        # 如果获取的键值小于256则作为ascii码输出对应字符,否则直接输出值
        msg = '{} is pressed'.format(chr(key) if key < 256 else key)
        print(msg)
    

    通过这个程序我们能获取一些常用特殊按键的值,比如在笔者用的机器上,四个方向的按键和删除键对应的值如下:

    - 上(↑):65362

    - 下(↓):65364

    - 左(←):65361

    - 右(→):65363

    - 删除(Delete):65535

    需要注意的是在不同的操作系统里这些值可能是不一样的。鼠标事件比起键盘事件稍微复杂一点点,需要定义一个回调函数,然后把回调函数和一个指定名称的窗口绑定,这样只要鼠标位于画面区域内的事件就都能捕捉到。把下面这段代码插入到上段代码的while之前,就能获取当前鼠标的位置和动作并输出:

    # 定义鼠标事件回调函数
    def on_mouse(event, x, y, flags, param):
    
        # 鼠标左键按下,抬起,双击
        if event == cv2.EVENT_LBUTTONDOWN:
            print('Left button down at ({}, {})'.format(x, y))
        elif event == cv2.EVENT_LBUTTONUP:
            print('Left button up at ({}, {})'.format(x, y))
        elif event == cv2.EVENT_LBUTTONDBLCLK:
            print('Left button double clicked at ({}, {})'.format(x, y))
    
        # 鼠标右键按下,抬起,双击
        elif event == cv2.EVENT_RBUTTONDOWN:
            print('Right button down at ({}, {})'.format(x, y))
        elif event == cv2.EVENT_RBUTTONUP:
            print('Right button up at ({}, {})'.format(x, y))
        elif event == cv2.EVENT_RBUTTONDBLCLK:
            print('Right button double clicked at ({}, {})'.format(x, y))
    
        # 鼠标中/滚轮键(如果有的话)按下,抬起,双击
        elif event == cv2.EVENT_MBUTTONDOWN:
            print('Middle button down at ({}, {})'.format(x, y))
        elif event == cv2.EVENT_MBUTTONUP:
            print('Middle button up at ({}, {})'.format(x, y))
        elif event == cv2.EVENT_MBUTTONDBLCLK:
            print('Middle button double clicked at ({}, {})'.format(x, y))
    
        # 鼠标移动
        elif event == cv2.EVENT_MOUSEMOVE:
            print('Moving at ({}, {})'.format(x, y))
    
    # 为指定的窗口绑定自定义的回调函数
    cv2.namedWindow('Honeymoon Island')
    cv2.setMouseCallback('Honeymoon Island', on_mouse)
    

    6.4.3 代码:物体检测标注的小工具

    基于上面两小节的基本使用,就能和OpenCV的基本绘图功能就能实现一个超级简单的物体框标注小工具了。基本思路是对要标注的图像建立一个窗口循环,然后每次循环的时候对图像进行一次拷贝。鼠标在画面上画框的操作,以及已经画好的框的相关信息在全局变量中保存,并且在每个循环中根据这些信息,在拷贝的图像上再画一遍,然后显示这份拷贝的图像。

    基于这种实现思路,使用上我们采用一个尽量简化的设计:

    - 输入是一个文件夹,下面包含了所有要标注物体框的图片。如果图片中标注了物体,则生成一个相同名称加额外后缀名的文件保存标注信息。

    - 标注的方式是按下鼠标左键选择物体框的左上角,松开鼠标左键选择物体框的右下角,鼠标右键删除上一个标注好的物体框。所有待标注物体的类别,和标注框颜色由用户自定义,如果没有定义则默认只标注一种物体,定义该物体名称叫“Object”。

    - 方向键的←和→用来遍历图片,↑和↓用来选择当前要标注的物体,Delete键删除一张图片和对应的标注信息。

    每张图片的标注信息,以及自定义标注物体和颜色的信息,用一个元组表示,第一个元素是物体名字,第二个元素是代表BGR颜色的tuple或者是代表标注框坐标的元组。对于这种并不复杂复杂的数据结构,我们直接利用Python的repr()函数,把数据结构保存成机器可读的字符串放到文件里,读取的时候用eval()函数就能直接获得数据。这样的方便之处在于不需要单独写个格式解析器。如果需要可以在此基础上再编写一个转换工具就能够转换成常见的Pascal VOC的标注格式或是其他的自定义格式。

    在这些思路和设计下,我们定义标注信息文件的格式的例子如下:

    ('Hill', ((221, 163), (741, 291)))
    ('Horse', ((465, 430), (613, 570)))
    

    元组中第一项是物体名称,第二项是标注框左上角和右下角的坐标。这里之所以不把标注信息的数据直接用pickle保存,是因为数据本身不会很复杂,直接保存还有更好的可读性。自定义标注物体和对应标注框颜色的格式也类似,不过更简单些,因为括号可以不写,具体如下:

    'Horse', (255, 255, 0)
    'Hill', (0, 255, 255)
    'DiaoSi', (0, 0, 255)
    

    第一项是物体名称,第二项是物体框的颜色。使用的时候把自己定义好的内容放到一个文本里,然后保存成和待标注文件夹同名,后缀名为labels的文件。比如我们在一个叫samples的文件夹下放上一些草原的照片,然后自定义一个samples.labels的文本文件。把上段代码的内容放进去,就定义了小山头的框为黄色,骏马的框为青色,以及红色的屌丝。基于以上,标注小工具的代码如下:

    import os
    import cv2
    
    # tkinter是Python内置的简单GUI库,实现一些比如打开文件夹,确认删除等操作十分方便
    from tkFileDialog import askdirectory
    from tkMessageBox import askyesno
    
    # 定义标注窗口的默认名称
    WINDOW_NAME = 'Simple Bounding Box Labeling Tool'
    
    # 定义画面刷新的大概帧率(是否能达到取决于电脑性能)
    FPS = 24
    
    # 定义支持的图像格式
    SUPPOTED_FORMATS = ['jpg', 'jpeg', 'png']
    
    # 定义默认物体框的名字为Object,颜色蓝色,当没有用户自定义物体时用默认物体
    DEFAULT_COLOR = {'Object': (255, 0, 0)}
    
    # 定义灰色,用于信息显示的背景和未定义物体框的显示
    COLOR_GRAY = (192, 192, 192)
    
    # 在图像下方多出BAR_HEIGHT这么多像素的区域用于显示文件名和当前标注物体等信息
    BAR_HEIGHT = 16
    
    # 上下左右,ESC及删除键对应的cv.waitKey()的返回值
    # 注意这个值根据操作系统不同有不同,可以通过6.4.2中的代码获取
    KEY_UP = 65362
    KEY_DOWN = 65364
    KEY_LEFT = 65361
    KEY_RIGHT = 65363
    KEY_ESC = 27
    KEY_DELETE = 65535
    
    # 空键用于默认循环
    KEY_EMPTY = 0
    
    get_bbox_name = '{}.bbox'.format
    
    # 定义物体框标注工具类
    class SimpleBBoxLabeling:
    
        def __init__(self, data_dir, fps=FPS, window_name=None):
            self._data_dir = data_dir
            self.fps = fps
            self.window_name = window_name if window_name else WINDOW_NAME
    
            #pt0是正在画的左上角坐标,pt1是鼠标所在坐标
            self._pt0 = None
            self._pt1 = None
    
            # 表明当前是否正在画框的状态标记
            self._drawing = False
    
            # 当前标注物体的名称
            self._cur_label = None
    
            # 当前图像对应的所有已标注框
            self._bboxes = []
    
            # 如果有用户自定义的标注信息则读取,否则用默认的物体和颜色
            label_path = '{}.labels'.format(self._data_dir)
            self.label_colors = DEFAULT_COLOR if not os.path.exists(label_path) else self.load_labels(label_path)
    
            # 获取已经标注的文件列表和还未标注的文件列表
            imagefiles = [x for x in os.listdir(self._data_dir) if x[x.rfind('.') + 1:].lower() in SUPPOTED_FORMATS]
            labeled = [x for x in imagefiles if os.path.exists(get_bbox_name(x))]
            to_be_labeled = [x for x in imagefiles if x not in labeled]
    
            # 每次打开一个文件夹,都自动从还未标注的第一张开始
            self._filelist = labeled + to_be_labeled
            self._index = len(labeled)
            if self._index > len(self._filelist) - 1:
                self._index = len(self._filelist) - 1
    
        # 鼠标回调函数
        def _mouse_ops(self, event, x, y, flags, param):
    
            # 按下左键时,坐标为左上角,同时表明开始画框,改变drawing标记为True
            if event == cv2.EVENT_LBUTTONDOWN:
                self._drawing = True
                self._pt0 = (x, y)
    
            # 左键抬起,表明当前框画完了,坐标记为右下角,并保存,同时改变drawing标记为False
            elif event == cv2.EVENT_LBUTTONUP:
                self._drawing = False
                self._pt1 = (x, y)
                self._bboxes.append((self._cur_label, (self._pt0, self._pt1)))
    
            # 实时更新右下角坐标方便画框
            elif event == cv2.EVENT_MOUSEMOVE:
                self._pt1 = (x, y)
    
            # 鼠标右键删除最近画好的框
            elif event == cv2.EVENT_RBUTTONUP:
                if self._bboxes:
                    self._bboxes.pop()
    
        # 清除所有标注框和当前状态
        def _clean_bbox(self):
            self._pt0 = None
            self._pt1 = None
            self._drawing = False
            self._bboxes = []
    
        # 画标注框和当前信息的函数
        def _draw_bbox(self, img):
    
            # 在图像下方多出BAR_HEIGHT这么多像素的区域用于显示文件名和当前标注物体等信息
            h, w = img.shape[:2]
            canvas = cv2.copyMakeBorder(img, 0, BAR_HEIGHT, 0, 0, cv2.BORDER_CONSTANT, value=COLOR_GRAY)
    
            # 正在标注的物体信息,如果鼠标左键已经按下,则显示两个点坐标,否则显示当前待标注物体的名称
            label_msg = '{}: {}, {}'.format(self._cur_label, self._pt0, self._pt1) \
                if self._drawing \
                else 'Current label: {}'.format(self._cur_label)
    
            # 显示当前文件名,文件个数信息
            msg = '{}/{}: {} | {}'.format(self._index + 1, len(self._filelist), self._filelist[self._index], label_msg)
            cv2.putText(canvas, msg, (1, h+12),
                        cv2.FONT_HERSHEY_SIMPLEX,
                        0.5, (0, 0, 0), 1)
    
            # 画出已经标好的框和对应名字
            for label, (bpt0, bpt1) in self._bboxes:
                label_color = self.label_colors[label] if label in self.label_colors else COLOR_GRAY
                cv2.rectangle(canvas, bpt0, bpt1, label_color, thickness=2)
                cv2.putText(canvas, label, (bpt0[0]+3, bpt0[1]+15),
                            cv2.FONT_HERSHEY_SIMPLEX,
                            0.5, label_color, 2)
    
            # 画正在标注的框和对应名字
            if self._drawing:
                label_color = self.label_colors[self._cur_label] if self._cur_label in self.label_colors else COLOR_GRAY
                if self._pt1[0] >= self._pt0[0] and self._pt1[1] >= self._pt0[1]:
                    cv2.rectangle(canvas, self._pt0, self._pt1, label_color, thickness=2)
                cv2.putText(canvas, self._cur_label, (self._pt0[0] + 3, self._pt0[1] + 15),
                            cv2.FONT_HERSHEY_SIMPLEX,
                            0.5, label_color, 2)
            return canvas
    
        # 利用repr()导出标注框数据到文件
        @staticmethod
        def export_bbox(filepath, bboxes):
            if bboxes:
                with open(filepath, 'w') as f:
                    for bbox in bboxes:
                        line = repr(bbox) + '\n'
                        f.write(line)
            elif os.path.exists(filepath):
                os.remove(filepath)
    
        # 利用eval()读取标注框字符串到数据
        @staticmethod
        def load_bbox(filepath):
            bboxes = []
            with open(filepath, 'r') as f:
                line = f.readline().rstrip()
                while line:
                    bboxes.append(eval(line))
                    line = f.readline().rstrip()
            return bboxes
    
        # 利用eval()读取物体及对应颜色信息到数据
        @staticmethod
        def load_labels(filepath):
            label_colors = {}
            with open(filepath, 'r') as f:
                line = f.readline().rstrip()
                while line:
                    label, color = eval(line)
                    label_colors[label] = color
                    line = f.readline().rstrip()
            return label_colors
    
        # 读取图像文件和对应标注框信息(如果有的话)
        @staticmethod
        def load_sample(filepath):
            img = cv2.imread(filepath)
            bbox_filepath = get_bbox_name(filepath)
            bboxes = []
            if os.path.exists(bbox_filepath):
                bboxes = SimpleBBoxLabeling.load_bbox(bbox_filepath)
            return img, bboxes
    
        # 导出当前标注框信息并清空
        def _export_n_clean_bbox(self):
            bbox_filepath = os.sep.join([self._data_dir, get_bbox_name(self._filelist[self._index])])
            self.export_bbox(bbox_filepath, self._bboxes)
            self._clean_bbox()
    
        # 删除当前样本和对应的标注框信息
        def _delete_current_sample(self):
            filename = self._filelist[self._index]
            filepath = os.sep.join([self._data_dir, filename])
            if os.path.exists(filepath):
                os.remove(filepath)
            filepath = get_bbox_name(filepath)
            if os.path.exists(filepath):
                os.remove(filepath)
            self._filelist.pop(self._index)
            print('{} is deleted!'.format(filename))
    		
        # 开始OpenCV窗口循环的方法,定义了程序的主逻辑
        def start(self):
    
    	    # 之前标注的文件名,用于程序判断是否需要执行一次图像读取
            last_filename = ''
    		
            # 标注物体在列表中的下标
            label_index = 0
    		
            # 所有标注物体名称的列表
            labels = self.label_colors.keys()
    
            # 待标注物体的种类数
            n_labels = len(labels)
    
            # 定义窗口和鼠标回调
            cv2.namedWindow(self.window_name)
            cv2.setMouseCallback(self.window_name, self._mouse_ops)
            key = KEY_EMPTY
    
            # 定义每次循环的持续时间
            delay = int(1000 / FPS)
    
            # 只要没有按下Esc键,就持续循环
            while key != KEY_ESC:
    
                # 上下键用于选择当前标注物体
                if key == KEY_UP:
                    if label_index == 0:
                        pass
                    else:
                        label_index -= 1
    
                elif key == KEY_DOWN:
                    if label_index == n_labels - 1:
                        pass
                    else:
                        label_index += 1
    
                # 左右键切换当前标注的图片
                elif key == KEY_LEFT:
                    # 已经到了第一张图片的话就不需要清空上一张
                    if self._index > 0:
                        self._export_n_clean_bbox()
    
                    self._index -= 1
                    if self._index < 0:
                        self._index = 0
    
                elif key == KEY_RIGHT:
                    # 已经到了最后一张图片的话就不需要清空上一张
                    if self._index < len(self._filelist) - 1:
                        self._export_n_clean_bbox()
    
                    self._index += 1
                    if self._index > len(self._filelist) - 1:
                        self._index = len(self._filelist) - 1
    
                # 删除当前图片和对应标注信息
                elif key == KEY_DELETE:
                    if askyesno('Delete Sample', 'Are you sure?'):
                        self._delete_current_sample()
                        key = KEY_EMPTY
                        continue
    
                # 如果键盘操作执行了换图片,则重新读取,更新图片
                filename = self._filelist[self._index]
                if filename != last_filename:
                    filepath = os.sep.join([self._data_dir, filename])
                    img, self._bboxes = self.load_sample(filepath)
    
                # 更新当前标注物体名称
                self._cur_label = labels[label_index]
    
                # 把标注和相关信息画在图片上并显示指定的时间
                canvas = self._draw_bbox(img)
                cv2.imshow(self.window_name, canvas)
                key = cv2.waitKey(delay)
    
                # 当前文件名就是下次循环的老文件名
                last_filename = filename
    
            print('Finished!')
    
            cv2.destroyAllWindows()
            # 如果退出程序,需要对当前进行保存
            self.export_bbox(os.sep.join([self._data_dir, get_bbox_name(filename)]), self._bboxes)
    
            print('Labels updated!')
    
    if __name__ == '__main__':
        dir_with_images = askdirectory(title='Where are the images?')
        labeling_task = SimpleBBoxLabeling(dir_with_images)
        labeling_task.start()
    

    需要注意的是几个比较通用且独立的方法前加上了一句@staticmethod,表明是个静态方法。执行这个程序,并选择samples文件夹,标注时的画面如下图:

    展开全文
  • 感谢以下作者提供的资料: 1https://www.cnblogs.com/xinxue/p/5766756.html 2https://www.bilibili.com/video/av28288594/?p=1 3https://blog.csdn.net/qq_41175905/article/details/80560429 ...
  • 一、Python OpenCV 入门欢迎阅读系列教程,内容涵盖 OpenCV,它是一个图像和视频处理库,包含 C ++,C,Python 和 Java 的绑定。 OpenCV 用于各种图像和视频分析,如面部识别和检测,车牌阅读,照片编辑,高级机器人...
  • opencv是什么

    2011-05-02 15:27:00
    OpenCV是一个用于图像处理、分析、机器视觉方面的开源函数库. 无论你是做科学研究,还是商业应用,opencv都可以作为你理想的工具库,因为,对于这两者,它完全是免费的。该库采用C及C++语言编写,可以在windows, ...
  • 目录 1 背景 ...在本教程中,我们将了解OpenCV 3中引入的OpenCV目标跟踪API。我们将学习如何以及何时使用OpenCV 3中提供的8种不同的跟踪器BOOSTING,MIL,KCF,TLD,MEDIANFLOW,GOTURN,MOSS...
  • opencv概述及安装步骤

    2014-04-13 21:52:13
    本科时对于Opencv这个名词只闻其名,一直不识庐山面目,也没想过以后有一天会用到它。近日由于课题原因,我只能拿出学长推荐给我那本《学习opencv》开始零基础...一) opencv概述 1)OpenCV的全称是:OpenSource Comp
  • 一、OPenCV的概念和结构OpenCV:开源的计算机视觉库,一般用C和C++ 编写OpenCV的结构共分为五个部分,分别是图像处理和视觉算法(CV)、机器学习库(ML)、图像和视频输入/输出库(HighGUI)、基本结构和算法,XML...
  • 一:OpenCV概述 1.opencv相关概念 图像处理 利用计算机对图像进行分析处理,达到所需结果的技术,一般指的是数字图像处理,通过数码设备得到的数字图像是一个很大的二维数组,数组的元素叫像素,像素的值叫灰度值...
  • OpenCV概述

    2011-09-13 15:06:21
    Intel® 开源计算机视觉库OpenCV 目录  [隐藏] 1 什么是OpenCV2 重要特性3 谁创建了它4 新特征5 从哪里下载 OpenCV6 如果在安装/运行/使用 OpenCV 中遇到问题7 OpenCV参考手册8 中文...
  • OpenCV概述和安装

    2020-04-19 16:11:12
    OpenCV概述 OpenCV 的理论支撑来源于《数字图像处理》,数字图像处理使用的都是一些比较传统的算法,而目前也添加了一些比较前沿的算法,虽然有深度学习算法,但是主流不是深度学习算法。【更新速度也没有深度学习...
  • opencv概述

    2009-08-03 14:53:00
    最近接触了一些计算机视觉方面的项目,在这个领域广泛应用opencv库,下面是一些从opencv官网上摘抄的概述,连接http://www.opencv.org.cn。什么是OpenCV OpenCV是Intel®开源计算机视觉库。它由一系列 C 函数和少量 ...
  • OpenCV概述极简版

    2020-07-27 16:15:19
    OpenCV概述极简版 OpenCV的全称是Open Source Computer Vision Library,是一个跨平台的计算机视觉库。 OpenCV是由英特尔公司发起并参与开发,以BSD许可证授权发行,可以在商业和研究领域中免费使用。 OpenCV可用于...
  • 第一章 Opencv概述

    2018-12-19 23:44:28
    第一章 OpenCv概述 1 什么是OpenCv     OpenCv是一个开源的计算机视觉库,该项目由Gary Bradski带头启动,OpenCv库使用C和C++语言编写,并进行了深度的优化,内部许多函数都使用多线程进行加速。可在Windows、...
1 2 3 4 5 ... 20
收藏数 3,977
精华内容 1,590
关键字:

opencv