OpenCV 入门

news/2024/7/20 14:33:01 标签: 人工智能, matlab, 内存管理

##1.图像的表示 们看到的是 Lena 的头像,但是计算机看来,这副图像只是一堆亮度 各异的点。一副尺寸为 M × N 的图像可以用一个 M × N 的矩阵来表示,矩 阵元素的值表示这个位置上的像素的亮度,一般来说像素值越大表示该点越 亮。如图 3.1 中白色圆圈内的区域,进行放大并仔细查看,将会如图 3.2 所 示。

一般来说,灰度图用 2 维矩阵表示,彩色(多通道)图像用 3 维矩阵(M × N × 3)表示。对于图像显示来说, 目前大部分设备都是用无符号 8 位整 数(类型为 CV_8U)表示像素亮度。 图像数据在计算机内存中的 存储顺序为以图像最左上点(也可能是最左下 点)开始,存储如表 3-1 所示。
Iij 表示第 i 行 j 列的像素值。如果是多通道图像,比如 RGB 图像,则每个 像素用三个字节表示。在 OpenCV 中,RGB 图像的通道顺序为 BGR ,存储如 表 3-2 所示。
##2.Mat类 早期的 OpenCV 中, 使用 IplImage 和 CvMat 数据结构来表示图像。IplImage 和 CvMat 都是 C 语言的结构。使用这两个结构的问题是内存需要手动管理,开 发者必须清楚的知道何时需要申请内存,何时需要释放内存。这给开发者带来了 一定的负担,开发者应该将更多精力用于算法设计,因此在新版本的 OpenCV 中 引入了 Mat 类。 新加入的 Mat 类能够自动管理内存。使用 Mat 类,你不再需要花费大量精 力在内存管理上。而且你的代码会变得很简洁,代码行数会变少。但 C++接口唯 一的不足是当前一些嵌入式开发系统可能只支持 C 语言, 如果你的开发平台支持 C++,完全没有必要再用 IplImage 和 CvMat。在新版本的 OpenCV 中,开发者依 然可以使用 IplImage 和 CvMat,但是一些新增加的函数只提供了 Mat 接口。本书 中的例程也都将采用新的 Mat 类,不再介绍 IplImage 和 CvMat。 #####Mat 类的定义如下所示,关键的属性如下方代码所示:

class CV_EXPORTS Mat
   {
     public:
    //一系列函数 ...

    /* flag参数中包含许多关于矩阵的信息,如: 
         -Mat 的标识
         -数据是否连续
         -深度
         -通道数目
    */
    int flags;
    //矩阵的维数,取值应该大于或等于 2 int dims;
    //矩阵的行数和列数,如果矩阵超过 2 维,这两个变量的值都为-1 int rows, cols;
    //指向数据的指针 uchar* data;
    //指向引用计数的指针 //如果数据是由用户分配的,则为 NULL int* refcount;
    //其他成员变量和成员函数
    ...
   };
复制代码

##3.创建 Mat 对象 Mat 是一个非常优秀的图像类,它同时也是一个通用的矩阵类,可以用来创 建和操作多维矩阵。有多种方法创建一个 Mat 对象。 ####3.1 构造函数方法 Mat 类提供了一系列构造函数,可以方便的根据需要创建 Mat 对象。下面是 一个使用构造函数创建对象的例子。

Mat M(3,2, CV_8UC3, Scalar(0,0,255));//第一行代码创建一个行数(高度)为 3,列数(宽度)为 2 的图像,图像元 素是 8 位无符号整数类型,且有三个通道。图像的所有像素值被初始化为(0, 0, 255)。由于 OpenCV 中默认的颜色顺序为 BGR,因此这是一个全红色的图像。
cout << "M = " << endl << " " << M << endl;//第二行代码是输出 Mat 类的实例 M 的所有像素值。Mat 重定义了<<操作符, 使用这个操作符,可以方便地输出所有像素值,而不需要使用 for 循环逐个像素 输出。
复制代码

第一行代码创建一个行数(高度)为 3,列数(宽度)为 2 的图像,图像元 素是 8 位无符号整数类型,且有三个通道。图像的所有像素值被初始化为(0, 0, 255)。由于 OpenCV 中默认的颜色顺序为 BGR,因此这是一个全红色的图像。 第二行代码是输出 Mat 类的实例 M 的所有像素值。Mat 重定义了<<操作符, 使用这个操作符,可以方便地输出所有像素值,而不需要使用 for 循环逐个像素 输出。

常用的构造函数有:

  • Mat::Mat() 无参数构造方法;
  • Mat::Mat(int rows, int cols, int type) 创建行数为 rows,列数为 col,类型为 type 的图像;
  • Mat::Mat(Size size, int type) 创建大小为 size,类型为 type 的图像;
  • Mat::Mat(int rows, int cols, int type, const Scalar& s) 创建行数为 rows,列数为 col,类型为 type 的图像,并将所有元素初始 化为值 s;
  • Mat::Mat(Size size, int type, const Scalar& s) 创建大小为 size,类型为 type 的图像,并将所有元素初始化为值 s;
  • Mat::Mat(const Mat& m) 将 m 赋值给新创建的对象,此处不会对图像数据进行复制,m 和新对象 共用图像数据;
  • Mat::Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP) 创建行数为 rows,列数为 col,类型为 type 的图像,此构造函数不创建 图像数据所需内存,而是直接使用 data 所指内存,图像的行步长由 step 指定。
  • Mat::Mat(Size size, int type, void* data, size_t step=AUTO_STEP) 创建大小为 size,类型为 type 的图像,此构造函数不创建图像数据所需 内存,而是直接使用 data 所指内存,图像的行步长由 step 指定。
  • Mat::Mat(const Mat& m, const Range& rowRange, const Range& colRange) 创建的新图像为 m 的一部分,具体的范围由 rowRange 和 colRange 指 定,此构造函数也不进行图像数据的复制操作,新图像与 m 共用图像数 据;
  • Mat::Mat(const Mat& m, const Rect& roi) 创建的新图像为 m 的一部分,具体的范围 roi 指定,此构造函数也不进 行图像数据的复制操作,新图像与 m 共用图像数据。

这些构造函数中,很多都涉及到类型 type。type 可以是 CV_8UC1,CV_16SC1,..., CV_64FC4 等。里面的 8U 表示 8 位无符号整数,16S 表示 16 位有符号整数,64F 表示 64 位浮点数(即 double 类型);C 后面的数表示通道数,例如 C1 表示一个 通道的图像,C4 表示 4 个通道的图像,以此类推。

如果你需要更多的通道数,需要用宏 CV_8UC(n),例如: Mat M(3,2, CV_8UC(5));//创建行数为3,列数为2,通道数为5的图像 ####3.2 create()函数创建对象 除了在构造函数中可以创建图像,也可以使用 Mat 类的 create()函数创建图 像。如果 create()函数指定的参数与图像之前的参数相同,则不进行实质的内存 申请操作;如果参数不同,则减少原始数据内存的索引,并重新申请内存。使用 方法如下面例程所示:

Mat M(2,2, CV_8UC3);//构造函数创建图像 
M.create(3,2, CV_8UC2);//释放内存重新创建图像
复制代码

需要注意的时,使用 create()函数无法设置图像像素的初始值。 ####3.3 Matlab风格的创建对象方法 OpenCV 2 中提供了 Matlab 风格的函数,如 zeros(),ones()和 eyes()。这种方 法使得代码非常简洁,使用起来也非常方便。使用这些函数需要指定图像的大小 和类型,使用方法如下:

Mat Z = Mat::zeros(2,3, CV_8UC1);
cout << "Z = " << endl << " " << Z << endl;
Mat O = Mat::ones(2, 3, CV_32F);
cout << "O = " << endl << " " << O << endl;
Mat E = Mat::eye(2, 3, CV_64F);
cout << "E = " << endl << " " << E << endl;
复制代码

该代码中,有些 type 参数如 CV_32F 未注明通道数目,这种情况下它表示单 通道。上面代码的输出结果如图 3.4 所示

####3.4 矩阵的基本元素表达 对于 单通道图像,其元素类型一般为 8U(即 8 位无符号整数),当然也可以 是 16S、32F 等;这些类型可以直接用 uchar、short、float 等 C/C++语言中的基本 数据类型表达。

如果多通道图像,如 RGB 彩色图像,需要用三个通道来表示。在这种情况 下,如果依然将图像视作一个二维矩阵,那么矩阵的元素不再是基本的数据类型。

OpenCV 中有模板类Vec,可以表示一个向量。OpenCV 中使用 Vec 类预定义了一 些小向量,可以将之用于矩阵元素的表达。

   typedef Vec<uchar, 2> Vec2b;
   typedef Vec<uchar, 3> Vec3b;
   typedef Vec<uchar, 4> Vec4b;
   typedef Vec<short, 2> Vec2s;
   typedef Vec<short, 3> Vec3s;
   typedef Vec<short, 4> Vec4s;
   typedef Vec<int, 2> Vec2i;
   typedef Vec<int, 3> Vec3i;
   typedef Vec<int, 4> Vec4i;
   typedef Vec<float, 2> Vec2f;
   typedef Vec<float, 3> Vec3f;
   typedef Vec<float, 4> Vec4f;
   typedef Vec<float, 6> Vec6f;
   typedef Vec<double, 2> Vec2d;
   typedef Vec<double, 3> Vec3d;
   typedef Vec<double, 4> Vec4d;
   typedef Vec<double, 6> Vec6d;
复制代码

例如 8U 类型的 RGB 彩色图像可以使用 Vec3b,3 通道 float 类型的矩阵可以 使用 Vec3f。 对于 Vec 对象,可以使用[]符号如操作数组般读写其元素,如:

Vec3b color; //用color变量描述一种RGB颜色 
color[0]=255; //B分量
color[1]=0; //G分量
color[2]=0; //R分量
复制代码

####3.5 像素值的读写 很多时候,我们需要读取某个像素值,或者设置某个像素值;在更多的时候, 我们需要对整个图像里的所有像素进行遍历。OpenCV 提供了多种方法来实现图 像的遍历。 ######3.5.1 at()函数 函数 at()来实现读去矩阵中的某个像素,或者对某个像素进行赋值操作。下 面两行代码演示了 at()函数的使用方法。

uchar value = grayim.at<uchar>(i,j);//读出第i行第j列像素值
grayim.at<uchar>(i,j)=128; //将第i行第j列像素值设置为128
复制代码

如果要对图像进行遍历,可以参考下面的例程。这个例程创建了两个图像, 分别是单通道的 grayim 以及 3 个通道的 colorim,然后对两个图像的所有像素值 进行赋值,最后现实结果。

#include <iostream>
#include "opencv2/opencv.hpp"
   using namespace std;
   using namespace cv;
   int main(int argc, char* argv[])
   {
    Mat grayim(600, 800, CV_8UC1); Mat colorim(600, 800, CV_8UC3);
    //遍历所有像素,并设置像素值
    for( int i = 0; i < grayim.rows; ++i)
    for( int j = 0; j < grayim.cols; ++j ) grayim.at<uchar>(i,j) = (i+j)%255;
    //遍历所有像素,并设置像素值
    for( int i = 0; i < colorim.rows; ++i)
    for( int j = 0; j < colorim.cols; ++j ) {
        Vec3b pixel;
        pixel[0] = i%255; //Blue 
        pixel[1] = j%255; //Green 
        pixel[2] = 0; //Red             
        colorim.at<Vec3b>(i,j) = pixel;
     }
    //显示结果
    imshow("grayim", grayim); imshow("colorim", colorim);
    waitKey(0);
    return 0; 
}
复制代码

需要注意的是,如果要遍历图像,并不推荐使用 at()函数。使用这个函数的 优点是代码的可读性高,但是效率并不是很高

####3.5.2 使用迭代器 如果你熟悉 C++的 STL 库,那一定了解迭代器(iterator)的使用。迭代器可 以方便地遍历所有元素。Mat 也增加了迭代器的支持,以便于矩阵元素的遍历。 下面的例程功能跟上一节的例程类似,但是由于使用了迭代器,而不是使用行数 和列数来遍历,所以这儿没有了 i 和 j 变量,图像的像素值设置为一个随机数。

#include <iostream>
#include "opencv2/opencv.hpp"
   using namespace std;
   using namespace cv;
   int main(int argc, char* argv[])
   {
     Mat grayim(600, 800, CV_8UC1); Mat colorim(600, 800, CV_8UC3);
     //遍历所有像素,并设置像素值 
     MatIterator_<uchar> grayit, grayend;
     for( grayit = grayim.begin<uchar>(), grayim.end<uchar>();  grayit != grayend;  ++grayit)*grayit = rand()%255;
     //遍历所有像素,并设置像素值
     MatIterator_<Vec3b> colorit, colorend;
     for(colorit = colorim.begin<Vec3b>(),colorim.end<Vec3b>(); colorit != colorend; ++colorit) {
        (*colorit)[0] = rand()%255; //Blue
        (*colorit)[1] = rand()%255; //Green 
        (*colorit)[2] = rand()%255; //Red
     }
     //显示结果
     imshow("grayim", grayim); 
     imshow("colorim", colorim); 
     waitKey(0);
     return 0; 
   }
复制代码

######3.5.3 通过数据指针 使用 IplImage 结构的时候,我们会经常使用数据指针来直接操作像素。通过 指针操作来访问像素是非常高效的,但是你务必十分地小心。 C/C++中的指针操 作是不进行类型以及越界检查的,如果指针访问出错,程序运行时有时候可能看上去一切正常,有时候却突然弹出“段错误”(segment fault)。

当程序规模较大,且逻辑复杂时,查找指针错误十分困难。对于不熟悉指针 的编程者来说,指针就如同噩梦。如果你对指针使用没有自信,则不建议直接通 过指针操作来访问像素。虽然at()函数和迭代器也不能保证对像素访问进行充分 的检查,但是总是比指针操作要可靠一些。

如果你非常注重程序的运行速度,那么遍历像素时,建议使用指针。下面的 例程演示如何使用指针来遍历图像中的所有像素。此例程实现的操作跟第 3.5.1 节中的例程完全相同。例程代码如下:

#include <iostream>
#include "opencv2/opencv.hpp"
   using namespace std;
   using namespace cv;
   int main(int argc, char* argv[])
   {
      Mat grayim(600, 800, CV_8UC1); Mat colorim(600, 800, CV_8UC3);
      //遍历所有像素,并设置像素值
      for( int i = 0; i < grayim.rows; ++i) {
          //获取第 i 行首像素指针
          uchar * p = grayim.ptr<uchar>(i);
          //对第 i 行的每个像素(byte)操作
          for( int j = 0; j < grayim.cols; ++j )
             p[j] = (i+j)%255;
      }
      //遍历所有像素,并设置像素值
      for( int i = 0; i < colorim.rows; ++i) {
          //获取第 i 行首像素指针
          Vec3b * p = colorim.ptr<Vec3b>(i);
          for( int j = 0; j < colorim.cols; ++j ) {
                  p[j][0] = i%255; //Blue p[j][1] = j%255; //Green p[j][2] = 0; //Red
          }
       }
       //显示结果
      imshow("grayim", grayim); 
      imshow("colorim", colorim);
      waitKey(0);
      return 0; 
  }
复制代码

###3.6 选取图像局部区域 Mat 类提供了多种方便的方法来选择图像的局部区域。使用这些方法时需要 注意, 这些方法并不进行内存的复制操作。如果将 局部区域赋值给新的 Mat 对 象,新对象与原始对象共用相同的数据区域,不新申请内存,因此这些方法的执 行速度都比较快。 ######3.6.1 单行或单列选择 提取 矩阵的一行或者一列可以使用函数 row()或 col()。函数的声明如下:

 Mat Mat::row(int i) const
 Mat Mat::col(int j) const
复制代码

参数 i 和 j 分别是行标和列标。例如取出 A 矩阵的第 i 行可以使用如下代码: Mat line = A.row(i); 例如取出 A 矩阵的第 i 行,将这一行的所有元素都乘以 2,然后赋值给第 j 行,可以这样写: A.row(j) = A.row(i)*2; ######3.6.2 用Range选择多行或多列 Range 是 OpenCV 中新增的类,该类有两个关键变量 star 和 end。Range 对 象可以用来表示矩阵的多个连续的行或者多个连续的列。其表示的范围为从 start 到 end,包含 start,但不包含 end。Range 类的定义如下:

 class Range
   {
      public:
      ...
      int start, end;
   };
复制代码

Range 类还提供了一个静态方法all(),这个方法的作用如同 Matlab 中的“:”, 表示所有的行或者所有的列。

//创建一个单位阵
Mat A = Mat::eye(10, 10, CV_32S);
//提取第 1 到 3 列(不包括 3)
Mat B = A(Range::all(), Range(1, 3));
//提取 B 的第 5 至 9 行(不包括 9)
//其实等价于 C = A(Range(5, 9), Range(1, 3)) Mat C = B(Range(5, 9), Range::all());
复制代码

######3.6.3 感兴趣区域 从图像中提取感兴趣区域(Region of interest)有两种方法,一种是使用构造 函数,如下例所示:

//创建宽度为 320,高度为 240 的 3 通道图像
 Mat img(Size(320,240),CV_8UC3);
//roi 是表示 img 中 Rect(10,10,100,100)区域的对象 
 Mat roi(img, Rect(10,10,100,100));
复制代码

除了使用构造函数,还可以使用括号运算符,如下: Mat roi2 = img(Rect(10,10,100,100)); 当然也可以使用 Range 对象来定义感兴趣区域,如下:

//使用括号运算符
Mat roi3 = img(Range(10,100),Range(10,100));
//使用构造函数
Mat roi4(img, Range(10,100),Range(10,100));
复制代码

######3.6.4 取对角线元素 矩阵的对角线元素可以使用 Mat 类的 diag()函数获取,该函数的定义如下: Mat Mat::diag(int d) const 参数d=0时,表示取主对角线;当参数 d>0 是,表示取主对角线下方的次对角线,如 d=1 时,表示取主对角线下方,且紧贴主多角线的元素;当参数 d<0 时, 表示取主对角线上方的次对角线如同 row()和 col()函数,diag()函数也不进行内存复制操作,其复杂度也是 O(1)。 ####3.7 Mat 表达式 利用 C++中的运算符重载,OpenCV 2 中引入了Mat 运算表达式。这一新特 点使得使用 C++进行编程时,就如同写 Matlab 脚本,代码变得简洁易懂,也便于 维护。 如果矩阵 A 和 B 大小相同,则可以使用如下表达式: C = A + B + 1; 其执行结果是 A 和 B 的对应元素相加,然后再加 1,并将生成的矩阵赋给 C 变量 下面给出 Mat 表达式所支持的运算。下面的列表中使用 A 和 B 表示 Mat 类 型的对象,使用 s 表示 Scalar 对象,alpha 表示 double 值

  • 加法,减法,取负:A+B,A-B,A+s,A-s,s+A,s-A,-A
  • 缩放取值范围:A*alpha
  • 矩阵对应元素的乘法和除法: A.mul(B),A/B,alpha/A
  • 矩阵乘法:A*B (注意此处是矩阵乘法,而不是矩阵对应元素相乘)
  • 矩阵转置:A.t()
  • 矩阵求逆和求伪逆:A.inv()
  • 矩阵比较运算:A cmpop B,A cmpop alpha,alpha cmpop A。此处 cmpop 可以是>,>=,==,!=,<=,<。如果条件成立,则结果矩阵(8U 类型矩 阵)的对应元素被置为 255;否则置 0。
  • 矩阵位逻辑运算:A logicop B,A logicop s,s logicop A,~A,此处 logicop 可以是&,|和^。
  • 矩阵对应元素的最大值和最小值:min(A, B),min(A, alpha),max(A, B), max(A, alpha)。
  • 矩阵中元素的绝对值:abs(A)
  • 叉积和点积:A.cross(B),A.dot(B)

下面例程展示了 Mat 表达式的使用方法,例程的输出结果如图 3.8 所示。

#include <iostream>
#include "opencv2/opencv.hpp"
  using namespace std;
  using namespace cv;
  int main(int argc, char* argv[])
  {
    Mat A = Mat::eye(4,4,CV_32SC1); 
    Mat B = A * 3 + 1;
    Mat C = B.diag(0) + B.col(1);
    cout << "A = " << A << endl << endl;
    cout << "B = " << B << endl << endl;
    cout << "C = " << C << endl << endl;
    cout << "C .* diag(B) = " << C.dot(B.diag(0)) << endl;
    return 0;
  }
复制代码

####3.8 Mat_类 Mat_类是对 Mat 类的一个包装,其定义如下:

template<typename _Tp> class Mat_ : public Mat {
  public:
  //只定义了几个方法
  //没有定义新的属性
 };
复制代码

这是一个非常轻量级的包装,既然已经有 Mat 类,为何还要定义一个 Mat_? 下面我们看这段代码:

Mat M(600, 800, CV_8UC1);
for( int i = 0; i < M.rows; ++i) {
    uchar * p = M.ptr<uchar>(i);
    for( int j = 0; j < M.cols; ++j ) {
        double d1 = (double) ((i+j)%255);
        M.at<uchar>(i,j) = d1;
       double d2 = M.at<double>(i,j);//此行有错 
    }
}
复制代码

在读取矩阵元素时,以及获取矩阵某行的地址时,需要指定数据类型。这样 首先需要不停地写“<uchar>”,让人感觉很繁琐,在繁琐和烦躁中容易犯错,如上面代码中的错误,用at()获取矩阵元素时错误的使用了 double 类型。这种错误 不是语法错误,因此在编译时编译器不会提醒。在程序运行时,at()函数获取到 的不是期望的(i,j)位置处的元素,数据已经越界,但是运行时也未必会报错。这样 的错误使得你的程序忽而看上去正常,忽而弹出“段错误”,特别是在代码规模 很大时,难以查错。

如果使用Mat_类,那么就可以在变量声明时确定元素的类型,访问元素时不再需要指定元素类型,即使得代码简洁,又减少了出错的可能性。上面代码可 以用 Mat_实现,实现代码如下面例程里的第二个双重 for 循环。

#include <iostream>
#include "opencv2/opencv.hpp" #include <stdio.h>
using namespace std;
using namespace cv;
   int main(int argc, char* argv[])
   {
       Mat M(600, 800, CV_8UC1);
       for( int i = 0; i < M.rows; ++i) {
          //获取指针时需要指定类型
          uchar * p = M.ptr<uchar>(i);
          for( int j = 0; j < M.cols; ++j ) {
              double d1 = (double) ((i+j)%255);
              //用 at()读写像素时,需要指定类型 M.at<uchar>(i,j) = d1;
              //下面代码错误,应该使用 at<uchar>() //但编译时不会提醒错误
              //运行结果不正确,d2 不等于 d1
              double d2 = M.at<double>(i,j);
           }
        }
        //在变量声明时指定矩阵元素类型
        Mat_<uchar> M1 = (Mat_<uchar>&)M;
        for( int   i = 0; i < M1.rows; ++i) {
        //不需指定元素类型,语句简洁
           uchar * p = M1.ptr(i);
           for( int j = 0; j < M1.cols; ++j ) {
              double d1 = (double) ((i+j)%255);
              //直接使用 Matlab 风格的矩阵元素读写,简洁 
              M1(i,j) = d1;
              double d2 = M1(i,j);
            } 
        }
        return 0;
 }
复制代码

####3.9 Mat 类的内存管理 使用 Mat 类,内存管理变得简单,不再像使用 IplImage 那样需要自己申请 和释放内存。虽然不了解 Mat 的内存管理机制,也无碍于 Mat 类的使用,但是 如果清楚了解 Mat 的内存管理,会更清楚一些函数到底操作了哪些数据。 Mat 是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸,存储方法, 存储地址等信息)和一个指向存储所有像素值的矩阵的指针,如图 3.9 所示。矩 阵头的尺寸是常数值,但矩阵本身的尺寸会依图像的不同而不同,通常比矩阵头 的尺寸大数个数量级。复制矩阵数据往往花费较多时间,因此除非有必要,不要 复制大的矩阵。 为了解决矩阵数据的传递,OpenCV 使用了引用计数机制。其思路是让每个 Mat 对象有自己的矩阵头信息,但多个 Mat 对象可以共享同一个矩阵数据。让矩 阵指针指向同一地址而实现这一目的。很多函数以及很多操作(如函数参数传值) 只复制矩阵头信息,而不复制矩阵数据。 前面提到过,有很多中方法创建 Mat 类。如果 Mat 类自己申请数据空间, 那么该类会多申请 4 个字节,多出的 4 个字节存储数据被引用的次数。引用次数存储于数据空间的后面,refcount 指向这个位置,如图 3.9 所示。当计数等于 0 时,则释放该空间

关于多个矩阵对象共享同一矩阵数据,我们可以看这个例子:

Mat A(100,100, CV_8UC1); 
Mat B = A;
Mat C = A(Rect(50,50,30,30));
复制代码

上面代码中有三个 Mat 对象,分别是 A,B 和 C。这三者共有同一矩阵数据, 其示意图如图 3.10 所示。

####3.10 输出 从前面的例程中,可以看到 Mat 类 重载了<<操作符,可以方便得使用流操作 来输出矩阵的内容。默认情况下输出的格式是类似 Matlab 中矩阵的输出格式。 除了默认格式,Mat 也支持其他的输出格式。代码如下: 首先创建一个矩阵,并用随机数填充。填充的范围由 randu()函数的第二个 参数和第三个参数确定,下面代码是介于 0 到 255 之间

Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
复制代码

默认格式输出的代码如下: cout << "R (default) = " << endl << R << endl << endl;

Python 格式输出的代码如下: cout << "R (python) = " << endl << format(R,"python") << endl<< endl;
以逗号分割的输出的代码如下: cout << "R (csv) = " << endl << format(R,"csv" ) << endl << endl;
numpy 格式输出的代码如下: cout << "R (numpy) = " << endl << format(R,"numpy" ) << endl << endl;
C 语言格式输出的代码如下: cout << "R (c) = " << endl << format(R,"C" ) << endl << endl;
除了 Mat 对象可以使用<<符号输出,其他的很多类型也支持<<输出。 二维点:

Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;
复制代码

三维点:

Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
复制代码

####3.11 Mat与IplImage和CvMat的转换 在 OpenCV 2 中虽然引入了方便的 Mat 类,出于兼容性的考虑,OpenCV 依 然是支持 C 语言接口的 IplImage 和 CvMat 结构。如果你要与以前的代码兼容, 将会涉及 Mat 与 IplImage 和 CvMat 的转换。 ######3.11.1Mat 转为 IplImage 和 CvMat 格式 假如你有一个以前写的函数,函数的定义为: void mycvOldFunc(IplImage * p, ...); 函数的参数需要 IplImage 类型的指针。Mat 转为 IplImage,可以用简单的等 号赋值操作来进行类型转换,这样实现:

Mat img(Size(320, 240), CV_8UC3);
...
IplImage iplimg = img; //转为IplImage结构 
mycvOldFunc( & iplimg, ...);//对iplimg取地址
复制代码

如果要转为 CvMat 类型,操作类似: CvMat cvimg = img; //转为CvMat结构 需要特别注意的是,类型转换后,IplImage 和 CvMat 与 Mat 共用同一矩阵数 据,而 IplImage 和 CvMat 没有引用计数功能,如果上例中的 img 中数据被释放, iplimg 和 cvimg 也就失去了数据。因此要牢记不可将 Mat 对象提前释放。 ######3.11.2IplImage 和 CvMat 格式转为 Mat Mat 类有两个构造函数,可以实现 IplImage 和 CvMat 到 Mat 的转换。这两 个函数都有一个参数 copyData。如果 copyData 的值是 false,那么 Mat 将与 IplImage 或 CvMat 共用同一矩阵数据;如果值是 true,Mat 会新申请内存,然后将 IplImage 或 CvMat 的数据复制到 Mat 的数据区

如果共用数据,Mat 也将不会使用引用计数来管理内存,需要开发者自己来 管理。本书建议做此转换是将参数置为 true,这样内存管理变得简单。

Mat::Mat(const CvMat* m, bool copyData=false)
Mat::Mat(const IplImage* img, bool copyData=false)
复制代码

例子代码如下:

IplImage * iplimg = cvLoadImage("lena.jpg"); 
Mat im(iplimg, true);
复制代码

##第4章 数据获取与存储 ####4.1 读写图像文件 将图像文件读入内存,可以使用 imread()函数;将 Mat 对象以图像文件格式 写入内存,可以使用 imwrite()函数。 ######4.1.1 读图像文件 imread()函数返回的是 Mat 对象,如果读取文件失败,则会返回一个空矩阵, 即 Mat::data 的值是 NULL。执行 imread()之后,需要检查文件是否成功读入,你 可以使用 Mat::empty()函数进行检查。imread()函数的声明如下: Mat imread(const string& filename, int flags=1 ) 很明显参数 filename 是被读取或者保存的图像文件名;在 imread()函数中, flag 参数值有三种情况:

  • flag>0,该函数返回 3 通道图像,如果磁盘上的图像文件是单通道的灰 度图像,则会被强制转为 3 通道;
  • flag=0,该函数返回单通道图像,如果磁盘的图像文件是多通道图像,则 会被强制转为单通道;
  • flag<0,则函数不对图像进行通道转换。

imread()函数支持多种文件格式,且该函数是根据图像文件的内容来确定文 件格式,而不是根据文件的扩展名来确定。所只是的文件格式如下:

  • Windows 位图文件 - BMP, DIB;
  • JPEG 文件 - JPEG, JPG, JPE;
  • 便携式网络图片 - PNG;
  • 便携式图像格式 - PBM,PGM,PPM;
  • Sun rasters - SR,RAS;
  • TIFF 文件 - TIFF,TIF;
  • OpenEXR HDR 图片 - EXR;
  • JPEG 2000 图片- jp2

你所安装的 OpenCV 并不一定能支持上述所有格式,文件格式的支持需要特定的库,只有在编译 OpenCV 添加了相应的文件格式库,才可支持其格式。 ######4.1.2 写图像文件 将图像写入文件,可使用 imwrite()函数,该函数的声明如下:

bool imwrite(const string& filename, InputArray image,
const vector<int>& params=vector<int>())
复制代码

文件的格式由 filename 参数指定的文件扩展名确定。推荐使用 PNG 文件格 式。BMP 格式是无损格式,但是一般不进行压缩,文件尺寸非常大;JPEG 格式 的文件娇小,但是 JPEG 是有损压缩,会丢失一些信息。PNG 是无损压缩格式, 推荐使用。 imwrite()函数的第三个参数 params 可以指定文件格式的一些细节信息。这 个参数里面的数值是跟文件格式相关的:

  • JPEG:表示图像的质量,取值范围从 0 到 100。数值越大表示图像质量 越高,当然文件也越大。默认值是 95。
  • PNG:表示压缩级别,取值范围是从 0 到 9。数值越大表示文件越小, 但是压缩花费的时间也越长。默认值是 3。
  • PPM,PGM 或 PBM:表示文件是以二进制还是纯文本方式存储,取值为 0 或 1。如果取值为 1,则表示以二进制方式存储。默认值是 1。

并不是所有的 Mat 对象都可以存为图像文件,目前支持的格式只有8U 类型 的单通道和 3 通道(颜色顺序为 BGR)矩阵;如果需要要保存 16U 格式图像,只 能使用 PNG、JPEG 2000 和 TIFF 格式。如果希望将其他格式的矩阵保存为图像文 件,可以先用 Mat::convertTo()函数或者 cvtColor()函数将矩阵转为可以保存的格 式。 另外需要注意的是,在保存文件时,如果文件已经存在,imwrite()函数不会 进行提醒,将直接覆盖掉以前的文件。 下面例程展示了如何读入一副图像,然后对图像进行 Canny 边缘操作,最后 将结果保存到图像文件中。

#include <iostream>
#include "opencv2/opencv.hpp"
   using namespace std;
   using namespace cv;
   int main(int argc, char* argv[])
   {
      //读入图像,并将之转为单通道图像
      Mat im = imread("lena.jpg", 0);
      //请一定检查是否成功读图 
      if( im.empty() ){
          cout << "Can not load image." << endl;
          return -1;
       }
      //进行 Canny 操作,并将结果存于 result Mat result;
      Canny(im, result, 50, 150);
      //保存结果 imwrite("lena-canny.png", result);
      return 0;
   }
复制代码

将 lena.jpg 文件放在当前目录,运行该例程后,lena-canny.png 将会出现在当 前目录。lena-canny.png 图像如图 4.1 所示,是 lena.jpg 的边缘提取结果。

####4.2 读写视频 介绍 OpenCV 读写视频之前,先介绍一下编解码器(codec)。如果是图像文 件,我们可以根据文件扩展名得知图像的格式。但是此经验并不能推广到视频文 件中。有些 OpenCV 用户会碰到奇怪的问题,都是 avi 视频文件,有的能用 OpenCV 打开,有的不能。 视频的格式主要由压缩算法决定。压缩算法称之为编码器(coder),解压算 法称之为解码器(decoder),编解码算法可以统称为编解码器(codec)。视频文 件能读或者写,关键看是否有相应的编解码器。编解码器的种类非常多,常用的 有 MJPG、XVID、DIVX 等,完整的列表请参考 FOURCC 网站3。因此视频文件的扩 展名(如 avi 等)往往只能表示这是一个视频文件。 OpenCV 2 中提供了两个类来实现视频的读写。 读视频的类是VideoCapture, 写视频的类是 VideoWriter。 ######4.2.1 读视频 VideoCapture 既可以从视频文件读取图像,也可以从摄像头读取图像。可以 使用该类的构造函数打开视频文件或者摄像头。如果 VideoCapture 对象已经创 建,也可以使用 VideoCapture::open()打开,VideoCapture::open()函数会自动调用 VideoCapture::release()函数,先释放已经打开的视频,然后再打开新视频。 如果要读一帧,可以使用 VideoCapture::read()函数。VideoCapture 类重载了>> 操作符,实现了读视频帧的功能。下面的例程演示了使用 VideoCapture 类读视 频。

#include <iostream>
#include "opencv2/opencv.hpp"
   using namespace std;
   using namespace cv;
   int main(int argc, char** argv)
   {
    //打开第一个摄像头
    //VideoCapture cap(0);
    //打开视频文件
    VideoCapture cap("video.short.raw.avi");
    //检查是否成功打开 
  if(!cap.isOpened()) {
        cerr << "Can not open a camera or file." << endl;
        return -1;
   }
  Mat edges;
  //创建窗口 
  namedWindow("edges",1);
  for(;;) {
    Mat frame;
    //从 cap 中读一帧,存到 frame 
    cap >> frame;
    //如果未读到图像 
    if(frame.empty())
    break;
    //将读到的图像转为灰度图
    cvtColor(frame, edges, CV_BGR2GRAY);
    //进行边缘提取操作
    Canny(edges, edges, 0, 30, 3);
    //显示结果 imshow("edges", edges);
    //等待 30 秒,如果按键则推出循环
     if(waitKey(30) >= 0)
        break; 
    }  
    //退出时会自动释放 cap 中占用资源
    return 0;
 }
复制代码

######4.2.2 写视频 使用 OpenCV 创建视频也非常简单,与读视频不同的是,你需要在创建视频 时设置一系列参数,包括: 文件名,编解码器,帧率,宽度和高度等编解码器 使用四个字符表示,可以是 CV_FOURCC('M','J','P','G')、CV_FOURCC('X','V','I','D')及 CV_FOURCC('D','I','V','X')等。如果使用某种编解码器无法创建视频文件,请尝试其 他的编解码器。 将图像写入视频可以使用 VideoWriter::write()函数,VideoWriter 类中也重载 了<<操作符,使用起来非常方便。 另外需要注意:待写入的图像尺寸必须与创建 视频时指定的尺寸一致。 下面例程演示了如何写视频文件。本例程将生成一个视频文件,视频的第 0 帧上是一个红色的“0”,第 1 帧上是个红色的“1”,以此类推,共 100 帧。生成 视频的播放效果如图 4.3 所示。

#include <stdio.h>
#include <iostream>
#include "opencv2/opencv.hpp"
   using namespace std;
   using namespace cv;
   int main(int argc, char** argv)
  {
    //定义视频的宽度和高度 Size s(320, 240);
    //创建 writer,并指定 FOURCC 及 FPS 等参数
    VideoWriter writer = VideoWriter("myvideo.avi", CV_FOURCC('M','J','P','G'), 25, s);
    //检查是否成功创建
    if(!writer.isOpened()) {
        cerr << "Can not create video file.\n" << endl;
        return -1;
     }
    //视频帧
    Mat frame(s, CV_8UC3);
       for(int i = 0; i < 100; I++)
       {
        //将图像置为黑色
        frame = Scalar::all(0);
        //将整数 i 转为 i 字符串类型
        char text[128];
        snprintf(text, sizeof(text), "%d", i); //将数字绘到画面上
       putText(frame, text, Point(s.width/3, FONT_HERSHEY_SCRIPT_SIMPLEX, 3, Scalar(0,0,255), 3, 8);
      //将图像写入视频
          writer << frame;
       }
    //退出程序时会自动关闭视频文件
    return 0;
 }
复制代码


http://www.niftyadmin.cn/n/961412.html

相关文章

linux设备驱动归纳总结(三):4.ioctl的实现

linux设备驱动归纳总结&#xff08;三&#xff09;&#xff1a;4.ioctl的实现 一、ioctl的简介&#xff1a; 虽然在文件操作结构体"struct file_operations"中有很多对应的设备操作函数&#xff0c;但是有些命令是实在找不到对应的操作函数。如CD-ROM的驱动&#xff…

star法则 java_STAR法则(示例代码)

STAR法则是情境(situation)、任务(task)、行动(action)、结果(result)四项的缩写。STAR法则是一种常常被面试官使用的工具&#xff0c;用来收集面试者与工作相关的具体信息和能力。STAR法则比起传统的面试手法来说&#xff0c;可以更精确地预测面试者未来的工作表现。STAR法则,…

Android未知格式文件下载及展示

最近项目需要通过指定地址id的方式下载文件并且展示&#xff0c;文件格式未知&#xff0c;可能是txt、xls、pdf、doc&#xff0c;那么问题来了&#xff0c;怎么在未知格式的情况下下载文件并展示呢&#xff1f; 通过参考PC端做法&#xff0c;完美的解决了这个问题&#xff0c;附…

linux设备驱动归纳总结(三):5.阻塞型IO实现

linux 设备驱动归纳总结&#xff08;三&#xff09;&#xff1a; 5. 阻塞型 IO 实现xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 一、休眠简介&#xff1a; 进程休眠&#xff0c;简单的说就是正在运行的进程让出CPU。休眠的进程会…

properties java 键值对_在spring中获取properties文件键值对的两种方式

前言系统参数我们都喜欢用properties格式文件来配置&#xff0c;在applicationContext.xml加入如下配置&#xff1a;classpath*:/config.propertiesfile:/c:/htconf/fms/sysconfig.properties一.spring上下文管理类的调用在Tomcat启动的时加载到Spring上下文&#xff0c;在spri…

Error:Could not determine the class-path for interface com.android.builder.model

导致这种错误的原因是Gradle版本不匹配版本修改成你本地Gradle Build Tool的最新版本&#xff0c;可以看一下你的文件中最新的是多少&#xff0c;也可以新建一个项目看一下Gradle版本号&#xff0c;图一改成图二这样就可以了转载于:https://juejin.im/post/5a32248b5188254dd93…

java bsh特点_BSH--Java Shell,Java也执行脚本语言

谁说JVM只能执行编译过后的class&#xff1f;(java语言)JSH帮你轻松搞定shell脚本&#xff0c;这点现在已经成为了一个标准&#xff1a;JSR-274。能够应用于用动态规则改变业务流程的场景。另&#xff1a;在这之前&#xff0c;还有Groovy(JSR- 241)也能在JVM中运行。最新版本&a…

php5.4.34,PHP 5.4.34 unserialize UAF exploit

之前在Sebug沙龙分享的PHP 5.4.34 unserialize UAF exploit&#xff0c;EXP放到博客来&#xff0c;还有那天的PPT&#xff1a;PHP反序列化UAF漏洞的研究与Exp编写EXP代码&#xff1a;php 5.4.34cve-2014-8142php server script content for this vulnerability: import reimpor…