OpenCV Mat —— 基本的图像容器



目标

现实中我们有很多种方法来获取数字图像:数字摄像头、扫描仪、计算机断层扫描以及核磁共振生成图像等等。对我们人类来说这些设备生成的结果我们称之为图像。而我们从这些设备获取的图像最终是以组成点阵的数值来表示的。

就好像是一张车的图片中就是包含了点阵强度值的矩阵。我们可以根据需要来获取或者存储点阵,但最终所有计算机中的图片就剩下点阵以及描述点阵的信息。OpenCV 是一个计算机视觉库,主要用来处理和操作这类图像信息。因此你首先需要熟悉的是 OpenCV 是如何存取图像的。



Mat

OpenCV 项目大约在 2001 年推出,之前主要是提供了 C 接口并通过名为 IplImage  的 C 结构体来处理内存中的图像。在一些老的教程和学习材料中你经常会看到这个结构体。使用这个结构体的问题是让 OpenCV 严重受限于 C 语言的特性和缺点,最大的问题就是需要进行手工内存管理。它要求用户必须小心的操作内存的分配和释放。对一些小程序而言,这不是什么大问题,但是一旦你的代码量增长越来越迅速时,这个问题变得非常严重。

幸运的是,C++ 语言实现了类的概念,可以轻松的实现自动化的内存管理(或多或少)。好消息是 C++ 完全兼容 C 语言,因此改用 C++ 并没有兼容性问题需要解决。所以 OpenCV 2.0 引入了全新的 C++ 接口,意味着你无需再关系内存管理的问题,让你的代码运行更加可靠。而 C++ 接口的缺点是很多嵌入式开发系统当前还只是支持 C 语言。因此,除非你使用一些特定的嵌入式系统,否则没有理由继续使用老的接口(除非你就是想自寻烦恼)。

首先我们需要了解的是 Mat 无需手工进行内存的分配和释放。虽然这样仍然只是一种可能性,因为绝大多数的 OpenCV 函数将自动的分配输出数据所需的内存。作为一个很好的红利,如果你传递一个已有的而且已经分配了阵列内存空间的 Mat 对象,它会被重用。换句话说,任何时候我们只需要使用最少的内存来执行各种任务。

Mat 是一个类,包含了阵列的头(阵列大小、存储的方法以及存储地址等等)和指向阵列点阵数据的指针(维度取决于存储的方法)。阵列的头部大小是一个常量,不同图片的头部存放的阵列大小是不同的。

OpenCV 是一个图像处理库。其包含大量各种图像处理函数。为了满足计算的要求,绝大多数时间你都会使用多个 OpenCV 函数。例如传递图片给某个函数是经常需要做的。我们别忘了我们正在讨论图像处理算法,这往往是非常沉重的计算。最后我们需要做的是进一步提升程序的速度,减少潜在的不必要的大图片拷贝。

为了解决这个问题,OpenCV 使用引用计数系统。这个思路就是每个 Mat 拥有独立的头部信息,而阵列数据是共享的。此外,拷贝操作只拷贝头部而不拷贝数据。

Mat A, C;  // 创建头部
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 分配阵列

Mat B(A);  // 使用拷贝构造函数

C = A;     // 赋值操作

上述代码中所有的对象都指向同一个数据阵列。而它们的头部是不同的,这样的话对某个对象进行操作就会影响到其他的对象。实际上不同的对象只是提供不同的访问方法来访问相同的底层数据。真正有趣的是你可以创建只指向部分数据的头部。例如,你可以使用如下代码来创建图像的兴趣点,包含新的头部和新的边界:

Mat D (A, Rect(10, 10, 100, 100) ); // 使用矩形
Mat E = A(Range::all(), Range(1,3)); // 使用行列边界

这时候你可能会问该阵列本身是否属于多个 Mat 对象,这些对象负责在其不需要的时候进行数据清理。最短的回答就是:它使用的是最后一个对象。这是通过引用计数机制来实现的。当某人拷贝一个 Mat 对象的头部,该阵列的计数器就会加1.当头部被清理时计数器就会减1.当计数器值为0的时候,阵列就会被释放。有时候你也想拷贝阵列本身,OpenCV 提供了 clone() 和 copyTo() 函数。

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

现在修改 F 或者 G 都不会影响 Mat 头部所指向的阵列。你需要记住的是:

  • 为 OpenCV 函数输出图像的内存分配是自动的(除非特别说明)
  • 使用 OpenCV 的 C++ 接口不需要考虑内存管理的问题
  • 赋值操作和拷贝构造函数只拷贝了头部信息
  • 图像底层的矩阵可以通过 clone() 和 copyTo() 函数进行拷贝

存储函数

这是关于点阵值的存储问题。你可以选择色彩空间和所使用的数据类型。色彩空间指的是我们如何利用给定的颜色代码组合成颜色组件。最简单的是灰度图,我们所需要处理的颜色只有黑白两色。这样的组合可以让我们创建很多灰色阴影。

而我们有很多的方法来处理彩色图。每一种方法都至少包含 3 到 4 中基本组件,我们可以对这些进行合并来创建彩色图。最通用的是 RGB,主要因为这是我们眼睛对色彩的识别方式。其基准色是红、绿、蓝。为了生成透明图像我们还需要第四个元素 —— alpha(A).

不同的色彩方案有不同的优势:

  • RGB 最常用,因为跟我们的眼睛识别方式类似,但需要注意的是 OpenCV 显示系统用的是 BGR 色彩
  • HSV 和 HLS 将颜色分解成色调、饱和度和亮度组件,用来描述色彩更为直观。它可以让你忽略值组件,使你的算法对输入图像的光照条件不那么敏感
  • YCrCb 常用语 JPEG 图像格式
  • CIE L*a*b 是一个感知均匀的色彩空间,可以方便的用来计算从一个颜色到另外一个颜色的差异。

每个颜色组件都有其有效的域,这个决定了我们所使用的数据类型:我们是如何存储一个组件决定了我们在这个域上的控制。最小的数据类型是 char,相当于一个字节或者 8 位数据。这个可以是无符号的(可以存储 0 - 255) 或者有符号的(-127 - 127)。虽然在三组件情况下(如 BGR)已经提供了 1600 多万的色彩值。我们还可以使用 float (4 byte = 32 bit) 或者 double (8 byte = 64 bit) 数据类型来定义每个组件。不过,需要记住的是,提升组件的值同样也提升了整个图片占用内存的大小。

显式的创建 Mat 对象

在教程 Load, Modify, and Save an Image 中我们已经知道如何通过 imwrite() 函数将点阵数据写到图像文件中。这样做可以大大方便调试的过程。你可以使用 Mat 的 << 操作符,不过需要注意的是这个只适合二维的阵列。

虽然 Mat 作为一个图像的容器挺合适,但它同时也是一个矩阵类。所以可以用它来创建和操作多维的矩阵。有很多方法来创建一个 Mat 对象:

  • Mat() 构造函数

        Mat M(2,2, CV_8UC3, Scalar(0,0,255));
        cout << "M = " << endl << " " << M << endl << endl;
    

对于两个维度或者多个维度的图像我们首先要定义大小,包括行列数。

然后需要指定用来存储元素的数据类型和每个点阵的通道数量。可以使用使用如下代码来一次定义多个变量:

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

例如 CV_8UC3 意味着使用无符号 char 类型,8位长度长整数以及每个点阵使用 3 通道。最多可预定义 4 个通道数量。 Scalar 是一个 4 元素的短整数向量。指定完后可以使用定制值来初始化点阵。如果你需要更多的通道,可以使用 upper 宏来创建,并在括号中指定通道数量,如下所示:

  • 使用 C/C++ 数组并通过构造函数初始化

        int sz[3] = {2,2,2};
        Mat L(3,sz, CV_8UC(1), Scalar::all(0));
    

    上述示例显示如何创建一个超过 2 个维度的阵列。指定阵列的维度数,并传递包含每个维度大小的指针。

  • 为已有的 IplImage 指针创建一个头部:

    IplImage* img = cvLoadImage("greatwave.png", 1);
    Mat mtx(img); // convert IplImage* -> Mat
    
  • Create() 函数:

        M.create(4,4, CV_8UC(2));
        cout << "M = "<< endl << " "  << M << endl << endl;
    

你不能在这个构造函数中初始化阵列值,它只在其阵列数据存储大小与老的不匹配时重新分配。

  • MATLAB 风格的初始化: zeros()ones()eye(). 指定大小和数据类型:

        Mat E = Mat::eye(4, 4, CV_64F);
        cout << "E = " << endl << " " << E << endl << endl;
    
        Mat O = Mat::ones(2, 2, CV_32F);
        cout << "O = " << endl << " " << O << endl << endl;
    
        Mat Z = Mat::zeros(3,3, CV_8UC1);
        cout << "Z = " << endl << " " << Z << endl << endl;
    
  • 对于一些小的阵列你可以使用逗号隔开初始化方法:

        Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
        cout << "C = " << endl << " " << C << endl << endl;
    
  • 为一个已有的 Mat 对象创建一个新的头部,并进行 clone() 后者 copyTo() .

        Mat RowClone = C.row(1).clone();
        cout << "RowClone = " << endl << " " << RowClone << endl << endl;
    

注意

你可以使用 randu() 函数来为一个阵列填充随机值,需要指定随机值的上下限::

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

输出格式化

在前面的例子中我们看到了默认的格式化选项。而 OpenCV 允许你自定义阵列输出的格式化方式::

  • 默认

        cout << "R (default) = " << endl <<        R           << endl << endl;
    

  • Python

        cout << "R (python)  = " << endl << format(R,"python") << endl << endl;
    

  • 逗号分隔的值 (CSV)

        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;
    

其他常用条目的输出

其他常用的 OpenCV 数据结构也可以使用 << 操作符来输出:

  • 2D Point

        Point2f P(5, 1);
        cout << "Point (2D) = " << P << endl << endl;
    
    Default Output
  • 3D Point

        Point3f P3f(2, 6, 7);
        cout << "Point (3D) = " << P3f << endl << endl;
    
    Default Output
  • std::vector via cv::Mat

        vector<float> v;
        v.push_back( (float)CV_PI);   v.push_back(2);    v.push_back(3.01f);
    
        cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;
    
    Default Output
  • std::vector of points

        vector<Point2f> vPoints(20);
        for (size_t i = 0; i < vPoints.size(); ++i)
            vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));
    
        cout << "A vector of 2D Points = " << vPoints << endl << endl;
    
    Default Output

这里的大多数示例都包含一个小的控制台程序,你可以从这里 下载 这些代码。

你也可以在 YouTube 观看视频教程.



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值