文章目录

  • 一.图像的存储
  • 二.OpenCV中图像坐标系的定义
  • 三.OpenCV中的Mat
  • 四.OpenCV中的图像存储方式
  • 五.显式创建Mat对象
    • 1.使用Mat构造函数
    • 2.使用C/C++数组初始化Mat
    • 3.使用cv::Mat::create函数
    • 4.使用cv::Mat::zeros , cv::Mat::ones , cv::Mat::eye 函数.
    • 5.使用逗号分隔的初始化器或初始化器列表
    • 6.为现有Mat对象和cv::Mat::clone或cv::Mat::copyTo创建新矩阵头
    • 7.使用randu()函数为现有矩阵头填充随机数
  • 六.访问Mat元素的方式
    • 1.直接指针访问(最高效的方法)
    • 2.迭代器方法
    • 3.带引用返回的动态地址计算访问方式
    • 4.使用核心函数(The Core Function)
    • 5.不同访问方式的性能比较

一.图像的存储

我们有多种方法从现实世界获取数字图像:数码相机、扫描仪、计算机断层扫描和磁共振成像等。在任何情况下,人类看到的都是图像。当将其转换为数字图像在设备中进行存储时,我们记录的是图像中每个点的数值。

例如,在上面的图像中,可以看到汽车的镜子只是一个包含像素点的所有强度值的矩阵。我们获取和存储像素值的方式可能会根据我们的需要而变化,但最终,世界中的所有图像都会被简化为数字矩阵和描述矩阵本身的其他信息存储在计算机中。OpenCV是一个计算机视觉库,其主要重点是处理和操纵这些信息。因此,首先需要熟悉OpenCV存储和处理图像的基本单元-Mat对象。

二.OpenCV中图像坐标系的定义


说明:

  1. Mat::Mat(int rows, int cols, int type)为创建行数为rows,列数为cols,类型为type的图像
  2. 坐标体系中的零点坐标为图片的左上角,X轴为图像矩形的上面那条水平线;Y轴为图像矩形左边的那条垂直线。该坐标体系在诸如结构体Mat,Rect,Point中都是适用的。
  3. 在使用image.at()来访问图像中点的值的时候,如果是以坐标方式进行访问,则坐标顺序为image.at(y, x)。但是,要是以坐标点(image图像中的Point(x, y)点)的方式进行访问,则为image.at(Point(x, y))
  4. 如果所画图像是多通道的,比如说image图像的通道数时n,则使用Mat::at(x, y)时,其x的范围依旧是0到image的height,而y的取值范围则是0到image的width乘以n,因为这个时候是有n个通道,所以每个像素需要占有n列。但是如果在同样的情况下,使用Mat::at(point)来访问的话,则这时候可以不用考虑通道的个数,因为Mat::at(x, y)返回的是一个数字,而,Mat::at(point)返回的是一个对应的n维向量。

三.OpenCV中的Mat

OpenCV从2001年就已经出现了。早期,OpenCV的库是围绕C接口构建的,为了将图像存储在内存中,使用了一个名为IplImage的C结构。这样做的问题是需要手动进行内存管理。IplImage是建立在用户负责处理内存分配和释放的假设之上的。虽然这对于较小的程序来说不是问题,但一旦代码库增长,处理内存管理将更加困难,而不是专注于解决开发目标。因此,OpenCV2.0引入了一个新的C++接口,它提供了一种新的工作方式-Mat对象,Mat的引入简化了用户的内存管理(不再需要手动分配其内存),使代码简洁(编写更少,实现更多)。C++接口的主要缺点是,目前许多嵌入式开发系统只支持C。

Mat基本上是一个包含两个数据部分的类:

  • 矩阵头(包含矩阵大小、用于存储的方法、存储矩阵的地址等信息)
  • 指向包含像素值的矩阵的指针(根据选择的存储方法采用任何维度)

OpenCV是一个图像处理库。它包含大量图像处理功能。为了解决计算难题,大多数时候将使用库的多个函数对同一个Mat对象进行操作。因此,将Mat对象传递给函数是一种常见的做法。但是对较大的图像进行不必要的复制会降低程序的速度。为了解决这个问题,OpenCV使用了参考计数系统。其思想是每个Mat对象都有自己的头,里面包含了一些图像的基本信息(图像大小,数据类型,通道数),然后,通过使两个Mat对象的矩阵指针指向同一地址,这样就可以在两个Mat对象之间共享矩阵,节省存储空间。另外,复制运算符也只是复制矩阵头和指向包含像素值的矩阵的指针,而不是数据本身。

Mat A, C;// 仅创建矩阵头A = imread(argv[1], IMREAD_COLOR); // 分配矩阵内存Mat B(A);// 使用拷贝构造函数进行Mat的复制C = A; // 赋值运算符进行Mat的复制

上述例子中所有对象都指向同一个数据矩阵,对其中任何一个对象的像素值进行修改都会影响其他所有对象。实际上,不同的对象只是为相同的底层数据提供不同的访问方法。然而,它们的头部是不同的。我们可以创建仅引用完整数据的一个子部分的矩阵头。例如,要在图像中创建感兴趣区域(ROI),只需创建具有新矩阵头的Mat对象:

Mat D (A, Rect(10, 10, 100, 100) ); // 使用ROI区域来创建新的Mat对象Mat E = A(Range::all(), Range(1,3)); // 使用ROI区域来创建新的Mat对象

矩阵是由最后一个使用它的对象来进行内存释放的。这是通过使用引用计数机制来处理的。每当有人复制Mat对象的矩阵头时,矩阵的计数器就会增加。每个指向该矩阵的Mat对象被释放,此计数器都会减少。当计数器达到零时,该矩阵被释放。要想要复制矩阵本身,使用cv::Mat::clone()和cv::Mat::copyTo()函数。

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

四.OpenCV中的图像存储方式

除了灰度图像外,最流行图像存储方式是RGB。它的底色是红色、绿色和蓝色。为了编码颜色的透明度,有时会添加第四个元素alpha(a)。
对于灰度图,OpenCV中的数据存储方式为:

对于彩色图,OpenCV中的数据存储方式为:

还有许多其他颜色系统,每个都有自己的优势:

  • RGB是最常见的,因为我们的眼睛使用类似的颜色,但是OpenCV标准显示系统使用的是BGR颜色空间合成颜色(红色和蓝色通道交换位置)。
  • HSV和HLS将颜色分解为色调、饱和度和值/亮度分量,这是描述颜色会更加自然。例如,我们可以忽略最后一个分量(亮度),从而使算法对输入图像的光线条件不太敏感。
  • 流行的JPEG图像格式使用YCrCb。
  • CIE Lab*是一个感知上均匀的颜色空间,如果需要测量给定颜色与另一种颜色的距离,它很有用。

我们如何存储像素定义了我们对其域的控制。最小数据类型是char,表示一个字节或8位。可以是无符号的(可以存储0到255的值)或有符号的(-127到+127的值)。虽然在三个分量(如RGB)的情况下,这个颜色宽度已经达到了(256x256x256=1638400)1600万种可能的颜色,也可以通过为每个分量使用浮点(4字节=32位)或双(8字节=64位)数据类型来获得更精细的颜色。但是,增加每个像素的大小也会增加内存中整个图片的大小。

五.显式创建Mat对象

1.使用Mat构造函数

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

对于二维和多通道图像,首先定义它们的大小:按行(rols)和列(cols)计数。然后,我们需要指定用于存储元素的数据类型以及每个矩阵点的通道数。为此,OpenCV根据以下约定构建了多个Mat数据类型的定义:

CV_[比特位数][有无符号][数据类型]C[通道数]

例如,CV_8UC3表示使用8位长的无符号字符类型,每个像素有三个这样的字符类型来形成三个通道。最多可为四个通道预定义类型。

2.使用C/C++数组初始化Mat

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

上面的示例显示了如何创建具有两个以上维度的矩阵。首先指定其维度,然后传递包含每个维度大小的指针,其余的保持不变。

3.使用cv::Mat::create函数

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

无法使用此构造初始化矩阵值。只有当新的大小与旧的大小不匹配时,它才会重新分配矩阵数据内存。

4.使用cv::Mat::zeros , cv::Mat::ones , cv::Mat::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;

5.使用逗号分隔的初始化器或初始化器列表

 Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); cout << "C = " << endl << " " << C << endl << endl; C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3); cout << "C = " << endl << " " << C << endl << endl;

6.为现有Mat对象和cv::Mat::clone或cv::Mat::copyTo创建新矩阵头

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

7.使用randu()函数为现有矩阵头填充随机数

可以使用cv::randu()函数用随机值填充矩阵。这时需要为随机值提供下限和上限:

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

六.访问Mat元素的方式

1.直接指针访问(最高效的方法)

在性能方面,最有效的访问方式为经典的C风格运算符[](指针)访问方式。因此,推荐的最有效的访问方法是:

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table){// 只能接收char类型数组CV_Assert(I.depth() == CV_8U);//获取数组基本信息int channels = I.channels();int nRows = I.rows;int nCols = I.cols * channels;//判断数组内容在内存中是否为连续存储if (I.isContinuous()){nCols *= nRows;nRows = 1;}//通过指针的方式访问数组中指定位置的元素int i,j;uchar* p;for( i = 0; i < nRows; ++i){p = I.ptr<uchar>(i);for ( j = 0; j < nCols; ++j){p[j] = table[p[j]];}}return I;}

这里,我们基本上只需要获取一个指向每行数据开头的指针,然后遍历它直到每行的结尾。在矩阵以连续方式存储的特殊情况下,我们只需要一次请求指针并一直到最后。如果需要寻找彩色图像:那么每行有三个通道(BGRBGRBGR…),所以我们需要在每行中访问不同通道的值。
还有另一种方式。Mat对象的数据数据成员返回指向第一行、第一列的指针。如果此指针为空,则该对象中没有有效输入。如果存储是连续的(Mat::isContinuous()),我们可以使用它来遍历整个数据指针。如果是灰度图像,则如下所示:

uchar* p = I.data;for( unsigned int i = 0; i < ncol*nrows; ++i)*p++ = table[*p];

2.迭代器方法

使用迭代器,程序员就不用考虑上面的问题了。只需要获得图像矩阵的begin和end迭代器,传统方式一直 ++ 即可。迭代器会自动跳到下一行并跳过内存中不连续的地方。使用解引用运算符 * 获得访问值。但是这种方法会损失效率

Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table){// 只能接收char类型数组CV_Assert(I.depth() == CV_8U);// 获取数组基本信息const int channels = I.channels();// 分通道数进行数据的访问switch(channels){case 1:{//使用迭代器进行Mat元素的访问MatIterator_<uchar> it, end;for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)*it = table[*it];break;}case 3:{//使用迭代器进行Mat元素的访问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;}

上述代码中,彩色图像对应的三通道情况下,使用vector存储每个像素三个通道的值,OpenCV中专门有一个Vec3b数据类型,相当于 vector。

3.带引用返回的动态地址计算访问方式

它只是用来随机访问图像中任意一个像素,可以读取或修改该像素,这里用到 at() 函数。无论用何用方法访问图像矩阵,都需要指定数据类型。因此,需要给函数提供访问数据类型,数据所在的行号和列号

Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table){// 只能接收char类型数组CV_Assert(I.depth() == CV_8U);// 获取数组基本信息const int channels = I.channels();// 分通道数进行数据的访问switch(channels){case 1:{//通过cv::Mat::at()进行元素的访问for( int i = 0; i < I.rows; ++i)for( int j = 0; j < I.cols; ++j )I.at<uchar>(i,j) = table[I.at<uchar>(i,j)];break;}case 3:{ Mat_<Vec3b> _I = I; //通过cv::Mat::at()进行元素的访问 for( int i = 0; i < I.rows; ++i)for( int j = 0; j < I.cols; ++j ) { _I(i,j)[0] = table[_I(i,j)[0]]; _I(i,j)[1] = table[_I(i,j)[1]]; _I(i,j)[2] = table[_I(i,j)[2]];} I = _I; break;}}return I;}

at() 函数输入数据类型和坐标,返回相应像素值的 引用。读操作是一个const函数,写操作是非const函数。 仅在debug模式下,函数会检查输入的坐标是否存在和有效,无效则通过标准错误输出流给出错误提示信息。相比于在release模式的高效率(第一种)方法,使用该函数的唯一的区别是,对图像中的每一个元素,你会得到一个行指针,以便于我们用 C风格的 [ ] 运算符获得列元素。
因为每次查找都要输入数据类型和坐标,用这种方法在一幅图像中多次执行查找操作是麻烦和费时的。为了解决这个问题,OpenCV提供了一个 Mat_ 的数据类型。它和 Mat 数据类型唯一的不同是需要额外指定 Mat_ 对象存储的数据矩阵的数据类型。它可以使用 () 运算符来快速访问元素,也可以容易地与 Mat 对象相互转换类型。上述代码中对彩色图像的处理(case 3)就是使用的 Mat_ 对象。

4.使用核心函数(The Core Function)

图像处理中经常需要将一幅图像中的所有值都替换成其他值,通过查找表可以高效实现这种替换。OpenCV提供了一个函数,使程序员不用手动地遍历图像,查表,写入替换值,而是直接利用 LUT() 实现 。实现代码如下:

Mat lookUpTable(1, 256, CV_8U);uchar* p = lookUpTable.data;for( int i = 0; i < 256; ++i)p[i] = table[i]; LUT(I, lookUpTable, J);

首先,创建一个1行256列的Mat对象用来存储查找表,然后调用 LUT() 函数实现转换。函数的参数中I是输入图像,J是输出图像。

5.不同访问方式的性能比较

教程中对上述四种方法进行了性能比较,采用了大小为 2560×1600 的彩色图像进行遍历替换,每种方法运行了上百次取平均值,得到如下结果:

总结: OpenCV自身提供的函数 LUT() 速度最快,这主要是因为OpenCV的库是通过Intel Threaded Building Blocks实现了多线程。如果是简单的图像遍历,可以使用第一种方法。迭代器方法损失了速度获得了安全性。Debug模式下的随机引用访问是最耗时的,Release模式下的随机引用访问效率和迭代器方法相当,但是它牺牲了迭代器具有的安全性。