首页 > 编程笔记 > C++笔记 阅读:4

OpenCV遍历Mat矩阵的5种方式(附带实例)

遍历就是对每个元素数据进行访问。遍历 Mat 矩阵的方式通常有指针数组方式、.ptr 方式、.at 方式、内存连续法和迭代器遍历法,其中.ptr 方式和指针数组方式是快速高效的方式。

指针数组方式遍历矩阵

该方式定义一个指针,指向 Mat 矩阵数据区开头:
uchar* pdata = (uchar*)mymat.data;
然后开始循环移动指针,移动的时候一般用两个 for 循环进行行和列的遍历。

需要注意的是,矩阵的实际列数是像素列数乘以通道数。另外,如果要输出某个元素值,要注意 cout 输出的 uchar 是 ASCII 码,即如果直接通过 cout<< pdata[j]<<endl 输出数据,则将输出数据对应的 ASCII 码。

如果需要输出整数,还需要强制转换,比如:
int img_pixel=(int)padata[j];  // 像素中数据为0~255,所以使用int类型即可
cout<<padata[j]<<endl;

【实例】使用指针数组方式遍历矩阵并输出。
1) 打开 Qt Creator,新建一个控制台工程,工程名是 test。

2) 在 IDE 中打开 main.cpp,输入如下代码:
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;  // 所有 OpenCV 类都在命名空间 cv 下
using namespace std;

int main()
{
    Mat mymat = cv::Mat::ones(cv::Size(3, 2), CV_8UC3);  // 定义 2 行 3 列三通道矩阵,实际矩阵共有 9 列
    uchar* pdata = (uchar*)mymat.data;
    for (int i = 0; i < mymat.rows; i++)
    {
        for (int j = 0; j < mymat.cols * mymat.channels(); j++)
            cout << (int)pdata[j] << "   ";
        cout << endl;
    }
}
代码很简单,首先让指针 pdata 指向矩阵数据区开头,然后开始逐行逐列得到每个元素的值,并强制转为 int 后输出。

值得注意的是 pdata[j],平面图像在逻辑结构上虽然为二维数组,但在计算机中是用一维数组储存图像,也就是说不论是灰度图像还是彩色图像,图像矩阵虽是一个二维数组,但在计算机内存中都是以一维数组的形式存储的。

用 Mat 存储一幅图像时,若图像在内存中是连续存储的(Mat 对象的 isContinuous == true),则可以将图像的数据看作一个一维数组,而 Mat::data(类型是 uchar*)就是指向图像数据的第一个字节,因此可以用 data 指针访问图像的数据,访问的方式就是在访问一个一维数组。

3) 保存工程并运行,结果如下:

1   0   0   1   0   0   1   0   0
1   0   0   1   0   0   1   0   0

当然,真正开发时,如果需要输出矩阵,不必采用遍历的方式逐个输出,这里主要是为了演示遍历。要输出矩阵直接使用 cout<<mymat; 即可。

.ptr方式遍历Mat矩阵

该方式也是比较高效的方式。这种方式使用形式如下:
mat.ptr<type>(row)[col]
对于 Mat 的 ptr 函数,返回的是 <> 中的模板类型指针,指向的是 () 中的第 row 行的起点。通常 <> 中的类型和 Mat 的元素类型应该一致,然后用该指针访问对应 col 列位置的元素。

ptr 函数访问任意一行像素的首地址,特别方便图像一行一行地横向访问,如果需要一列一列地纵向访问图像,就稍微麻烦一点。但是 ptr 的访问效率比较高,程序也比较安全,有越界判断。

在 OpenCV 中,Mat 矩阵 data 数据的存储方式和二维数组不一致:二维数组按照行优先的顺序依次存储,而 Mat 中还有一个标记步进距离的变量 Step。我们可以使用 Mat.ptr<DataTyte>(row) 方法来获取指定行的指针,从而定位到每一行数据。

在 Mat 矩阵中,数据指针 Mat.data 是 uchar 类型的指针,CV_8U 系列可以通过计算指针位置快速地定位矩阵中的任意元素。二维单通道元素可以使用 MAT::at(i,j),i 是行号,j 是列号。

但对于多通道的非 uchar 类型矩阵来说,以上方法不适用,此时可以用 Mat::ptr() 来获得指向某行元素的指针,再通过行数与通道数计算相应点的指针。比如以下代码:
// 单通道
cv::Mat image = cv::Mat(400, 600, CV_8UC1);   // 定义了一个Mat变量image,宽400,长600
uchar * data00 = image.ptr<uchar>(0);// data00是指向image第一行第一个元素的指针
uchar * data10 = image.ptr<uchar>(1);// data10是指向image第二行第一个元素的指针
uchar * data01 = image.ptr<uchar>(0)[1];  // data01是指向image第一行第二个元素的指针
uchar * data = image.ptr<uchar>(3)[42];  // 得到第3行第43个像素的指针

// 多通道
cv::Mat image = cv::Mat(400, 600, CV_8UC3); // 宽400,长600,三通道彩色图片
cv::Vec3b * data000 = image.ptr<cv::Vec3b>(0);
cv::Vec3b * data100 = image.ptr<cv::Vec3b>(1);
cv::Vec3b * data001 = image.ptr<cv::Vec3b>(0)[1];
Vec3b 可以看作 vector<uchar, 3>,简单而言就是一个 uchar 类型、长度为 3 的向量。

【实例】使用 .ptr 方式遍历矩阵并输出。
1) 打开 Qt Creator,新建一个控制台工程,工程名是 test。

2) 在 IDE 中打开 main.cpp,输入如下代码:
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;  // 所有 OpenCV 类都在命名空间 cv 下
using namespace std;

int main()
{
    Mat mymat = cv::Mat::ones(cv::Size(3, 2), CV_8UC3);  // 定义 2 行 3 列三通道矩阵,实际矩阵共有 9 列
    uchar* pdata = (uchar*)mymat.data;
    for (int i = 0; i < mymat.rows; i++)
    {
        for (int j = 0; j < mymat.cols * mymat.channels(); j++)
            cout << (int)pdata[j] << " ";
        cout << endl;
    }
}
我们定义 pdata 使其指向每一行开头的地址,然后在本行的列中递增。

3) 保存工程并运行,结果如下:

1   0   0   1   0   0   1   0   0
1   0   0   1   0   0   1   0   0

值得说明的是,每一行数据元素在内存中是连续存储的,每个三通道像素按顺序存储。但是这种用法不能用在行与行之间,因为图像在 OpenCV 中的存储机制问题,行与行之间可能有空白单元,这些空白单元对图像来说没有意义,只是为了在某些架构上能够更有效率,比如 Intel MMX 可以更有效地处理个数是4或8的倍数的行。

也就是说行与行之间不一定连续。当然,这里只是说不一定,一般情况下,如果该 Mat 只有 1 行,那当然是连续的;如果有多行,那么每行的 end 要与下一行的 begin 连在一起才算连续。

一般情况下我们建立的 Mat 都是连续的,但是用 Mat::col() 截取建立的 Mat 是不连续的。如果 Mat 连续,那么我们在访问时就可以将其当成一个长行,这样访问更加高效快速。

我们可以通过 Mat 的成员函数 isContinuous 来判断 Mat 中的像素点在内存中的存储是否连续。

.at方式遍历Mat矩阵

Mat 类提供了 .at 方式来获取图像上的点,它是一个模板函数,可以获取任何类型的图像上的点。该函数声明如下:
template<typename _Tp >
_Tp& at ( int  row,  int  col);
其中 row 是元素所在的行,col 是元素所在的列,函数返回元素值。

使用 .at 方式获取图像中的点的用法如下:
image.at<uchar>(i,j);     // 取出灰度图像中i行j列的点
image.at<Vec3b>(i,j)[k];  // 取出彩色图像中i行j列第k通道的颜色点
其中 uchar、Vec3b 都是图像像素值的类型。读者不要对 Vec3b 这种类型感到害怕,其实在 Core 中,它是通过 typedef Vec<T,N> 来定义的,N 代表元素的个数,T 代表类型,比如:
image.at<uchar>(10, 200) = 255;  // 在矩阵(10, 200)位置赋值255

下面我们通过一个图像处理中的实际应用来说明它的用法。在实际应用中,很多时候需要对图像进行降色(Reduce Color),如常见的 RGB24 图像有 256×256×256 种颜色,通过降色将每个通道的像素减少为原来的 1/8,即 256/8=32 种,则图像只有 32×32×32 种颜色。假设量化减少的分数是 1/N,则代码实现时就是简单的 value/N×N,通常我们会再加上 N/2 以得到相邻的N的倍数的中间值,最后图像被量化为(256/N)×(256/N)×(256/N)种颜色。

【实例】使用 .at 方式遍历矩阵并降色。
1) 打开 Qt Creator,新建一个控制台工程,工程名是 test。

2) 在 IDE 中打开 main.cpp,输入如下代码:
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; // 所有 OpenCV 类都在命名空间 cv 下
using namespace std;

void colorReduce(Mat& image, int div = 64)
{
    for (int i = 0; i < image.rows; i++)
        for (int j = 0; j < image.cols; j++)
        {
            image.at<Vec3b>(i, j)[0] = image.at<Vec3b>(i, j)[0] / div * div + div / 2;
            image.at<Vec3b>(i, j)[1] = image.at<Vec3b>(i, j)[1] / div * div + div / 2;
            image.at<Vec3b>(i, j)[2] = image.at<Vec3b>(i, j)[2] / div * div + div / 2;
        }
}

int main()
{
    Mat A; // 仅仅创建了矩阵头
    A = imread("520.jpg", 1);
    imshow("src", A);
    colorReduce(A);
    imshow("dst", A);
    waitKey(0);
}
我们通过自定义函数 colorReduce 遍历矩阵像素,并减少了颜色。

3) 保存工程并运行,结果如下图所示:

内存连续法遍历Mat矩阵

有些图像行与行之间往往是不连续存储的,但是有些图像是连续存储的,Mat 提供了一个检测图像是否连续存储的函数 isContinuous()。当图像连续存储时,我们就可以把图像完全展开,看成一行。这样访问更加高效。

【实例】使用内存连续法遍历矩阵。
1) 打开 Qt Creator,新建一个控制台工程,工程名是 test。

2) 在 IDE 中打开 main.cpp,输入如下代码:
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; // 所有 OpenCV 类都在命名空间 cv 下
using namespace std;

int main()
{
    Mat mymat = cv::Mat::ones(cv::Size(3, 2), CV_8UC3); // 定义 2 行 3 列三通道矩阵,实际矩阵共有 9 列
    int nr = mymat.rows;
    int nc = mymat.cols * mymat.channels();
    if (mymat.isContinuous()) // 判断矩阵存储是否内存连续
    {
        nr = 1;               // 如果连续,就可以当成一行
        nc = nc * mymat.rows; // 当成一行后的总列数
    }
    for (int i = 0; i < nr; i++)
    {
        uchar* pdata = mymat.ptr<uchar>(i); // 每一行图像的指针
        for (int j = 0; j < nc; j++)
            cout << (int)pdata[j] << " ";
        cout << endl;
    }
}
在上述代码中,通过函数 isContinuous 来判断矩阵存储是否连续,如果连续,就可以当成一行来处理,从而不必移动行指针。

3) 保存工程并运行,结果如下:

1 0 0 1 0 0 1 0 0 1 0 0 1 0 0

迭代器遍历法

通过迭代器方式遍历相对简单,只要设置好迭代器的开始和结束,然后递增,就可以开始遍历了。迭代器主要通过 MatConstIterator_ 这个模板类来实现的,其定义如下:
template<typename _Tp>
class cv::MatConstIterator_< _Tp >

其继承关系如下图所示:


下面来看一个实例,还是减少图像的颜色,然后把结果保存到文件 after520.jpg 中。

【实例】使用迭代器方式遍历矩阵。
1) 打开 Qt Creator,新建一个控制台工程,工程名是 test。

2) 在 IDE 中打开 main.cpp,输入如下代码:
#include <iostream>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv; // 所有 OpenCV 类都在命名空间 cv 下
using namespace std;

void colorReduce(const Mat& image, Mat& outImage, int div = 64)
{
    outImage.create(image.size(), image.type()); // 创建同样大小的矩阵,用于保存输出
    // 获得迭代器
    MatConstIterator_<Vec3b> it_in   = image.begin<Vec3b>();
    MatConstIterator_<Vec3b> itend_in = image.end<Vec3b>();
    MatIterator_<Vec3b> it_out        = outImage.begin<Vec3b>();
    MatIterator_<Vec3b> itend_out     = outImage.end<Vec3b>();

    while (it_in != itend_in)
    {
        (*it_out)[0] = (*it_in)[0] / div * div + div / 2;
        (*it_out)[1] = (*it_in)[1] / div * div + div / 2;
        (*it_out)[2] = (*it_in)[2] / div * div + div / 2;
        it_in++;
        it_out++;
    }
}

int main()
{
    Mat A, B; // 仅仅创建了矩阵头
    A = imread("520.jpg", 1);
    imshow("src", A);
    colorReduce(A, B);
    imshow("dst", B);
    imwrite("after520.jpg", B);
    waitKey(0);
}
在上述代码中,把图像 520.jpg(该文件位于源码工程目录下)加载到矩阵 A 对象中,然后对其遍历,进行颜色减少操作,同时创建一个同样大小的矩阵对象 B,并把每个元素结果存于 B 的相应位置。main 函数末尾把 B 保存到了文件 after520.jpg中。

3) 保存工程并运行,结果如下图所示:

相关文章