一、Mat类的综述
1. Mat 类存储图像
Mat 类是 OpenCV 里面使用广泛的一个类,最重要的一个作用就是作为存储图像的数据结构。Mat类是如何存储图像的呢?图像分为彩色图像和灰度图像,不管是彩色图像还是灰度图像,都是二维的矩阵,具体的存储格式如下:
(1)灰度图像的格式:
(2)彩色图像的格式:
虽然彩色图像由BGR三个通道,但是是存储在同一个平面内的,只不过OpenCV在这里把三列才当作一列,因此有 img.cols 等于图像的列数。
一般我们用 OpenCV 读取的灰度图像的数据类型为 uchar 类型的,而彩色图像的一个像素的数据类型为 <Vec3b> 类型,灰度图一个像素占用一个字节,而彩色图像一个像素3个字节。接下来简单介绍如何按像素读取图像。
2. Mat 按像素读取图像内容
这里主要介绍两种方法:一种非常简单,易于编程,但效率比较低;另一种效率高,但不好记。
(1)易于编程的方法
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat img = imread("1.jpg");
resize(img, img, Size(375, 500));//resize为500*375的图像
cvtColor(img, img, CV_RGB2GRAY);//转为灰度图
imshow("gray_ori", img);
for (int i = 0; i < img.rows; i++)
{
for (int j = 0; j < img.cols; j++)
{
//at<类型>(i,j)进行操作,对于灰度图
img.at<uchar>(i, j) = i+j;
}
}
imshow("gray_result", img);
waitKey(0);
return 0;
}
效果图:
使用 at 操作实现定位,就和操作一个普通的二位数组一样。对于彩色图像,方法也很简单,只需把 at<类型> 中的类型改为 Vec3b 即可,代码如下:
#include <opencv2\core\core.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\highgui\highgui.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat img = imread("1.jpg");
resize(img, img, Size(375, 500));//resize为500*375的图像
imshow("ori", img);
for (int i = 0; i < img.rows; i++)
{
for (int j = 0; j < img.cols; j++)
{
//at<类型>(i,j)进行操作,对于灰度图
img.at<Vec3b>(i, j)[0] = 255;//对于蓝色通道进行操作
//img.at<Vec3b>(i, j)[1] = 255;//对于绿色通道进行操作
//img.at<Vec3b>(i, j)[2] = 255;//对于红色通道进行操作
}
}
imshow("result", img);
waitKey(0);
return 0;
}
效果图:
(2)采用指针对图像进行访问(这里直接写对彩色图像的操作):
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\core\core.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat img = imread("1.jpg");
int rows = img.rows;
int cols = img.cols * img.channels();
if(img.isContinuous())//判断是否在内存中连续
{
cols = cols * rows;
rows = 1;
}
imshow("ori",img);
for(int i = 0;i<rows;i++)
{
//调取存储图像内存的第i行的指针
uchar *pointer = img.ptr<uchar>(i);
for(int j = 0;j<cols;j += 3)
{
//pointer[j] = 255;//对蓝色通道进行操作
//pointer[j+1] = 255;//对绿色通道进行操作
pointer[j+2] = 255;//对红色通道进行操作
}
}
imshow("result",img);
waitKey();
return 0;
}
效果图:
从代码可以看出,我们是如何操作图像的数据以及在 Mat 中的存放格式,彩色图像中的每一个像素点分成三份,每一份都是 uchar 类型,因此我们这里不需要使用 Vec3b 数据类型。把彩色图像看成一个 rows *(cols * channels) 的二维数组进行操作,其中每个元素的类型都是 uchar 类型。
这里需要注意的是 j += 3 是因为我们按照一个像素点进行操作,而一个像素点在这里又被分成三份,因此需要 j += 3,如果是灰度图像则直接 j ++ 即可。这种操作方式虽然复杂一些,但是在执行效率上高很多。
测试代码:
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\core\core.hpp>
#include <iostream>
#include <time.h>
using namespace cv;
using namespace std;
int main()
{
Mat img = imread("1.jpg");
Mat img2;
img.copyTo(img2);
cout<<"图像的行数: "<<img.rows<<endl;
cout<<"图像的列数: "<<img.cols<<endl;
cout<<"图像通道数: "<<img.channels()<<endl;
double time1;
time1 = (double)getTickCount();
int rows = img.rows;
int cols = img.cols * img.channels();
if(img.isContinuous())//判断是否在内存中连续
{
cols = cols * rows;
rows = 1;
}
for(int i = 0;i<rows;i++)
{
//调取存储图像内存的第i行的指针
uchar *pointer = img.ptr<uchar>(i);
for(int j = 0;j<cols;j += 3)
{
//pointer[j] = 255;//对蓝色通道进行操作
//pointer[j+1] = 255;//对绿色通道进行操作
pointer[j+2] = 255;//对红色通道进行操作
}
}
time1 = 1000 * ((double)getTickCount() - time1) / getTickFrequency();
//imshow("result",img);
cout<<"第一种方法用时: "<<time1<<endl;
double time2 = (double)getTickCount();
for (int i = 0; i < img2.rows; i++)
{
for (int j = 0; j < img2.cols; j++)
{
//at<类型>(i,j)进行操作,对于灰度图
img2.at<Vec3b>(i, j)[0] = 255;//对于蓝色通道进行操作
//img.at<Vec3b>(i, j)[1] = 255;//对于绿色通道进行操作
//img.at<Vec3b>(i, j)[2] = 255;//对于红色通道进行操作
}
}
time2 = 1000 * ((double)getTickCount() - time2)/getTickFrequency();
cout<<"第二种方法用时: "<<time2<<endl;
imshow("img",img);
imshow("img2",img2);
waitKey(0);
return 0;
}
测试结果:
3、Mat 类中的变量参数
Mat 中包含了很多的变量,比如用的比较多的 rows,cols等。这里总结Mat类中用的比较少的几个变量和函数 step1(),step[],size, elemSize 和 elemSize1。
step1() | Mat中每一维的通道数 |
step[i] | Mat 中每一维的大小,以字节为单位 |
size[i] | Mat中元素的个数 |
elemSize() | 每个元素大小,以字节为单位 |
elemSize1() | 每个元素中每个通道的大小,以字节为单位 |
测试代码:
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <opencv2\core\core.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat img = imread("1.jpg");
cout<<"img.rows: "<<img.rows<<" img.cols: "<<img.cols<<" img.channels(): "<<img.channels()<<endl;
cout<<"###############################################"<<endl;
cout<<"img.step1(0): "<<img.step1(0)<<endl;
cout<<"img.step1(1): "<<img.step1(1)<<endl;
cout<<"img.step[0]: "<<img.step[0]<<endl;
cout<<"img.step[1]: "<<img.step[1]<<endl;
cout<<"img.size[0]: "<<img.size[0]<<endl;
cout<<"img.size[1]: "<<img.size[1]<<endl;
cout<<"img.elemSize(): "<<img.elemSize()<<endl;
cout<<"img.elemSize1(): "<<img.elemSize1()<<endl;
return 0;
}
输出显示:
结果分析:读取一张 1000*750 彩色图像。step1(i) 表示每一维的通道数,彩色图像存储方式是一种二维矩阵,因此只有二维,其中最后一维表示的是一个点,前面一维表示的是线。如果有第三维,那么再前面一维表示的是面。这里有一个规律,就是最后一维一定是点,其他的依次向前推即可。
img.step1(1)表示的是一个点的通道数。彩色图像的一个像素通道数为3(BGR),因此 img.step1(1) = 3,而 img.step1(0) 是一行的像素通道数,因此为 750*3 = 2250
img.step[i] 表示的每一维中的大小,以字节为单位。在彩色图像中一个像素分为三块,每一块为一个字节,因此 step[0] = 2250, step[1] = 3
size 表示的是每一维元素的大小,以元素为单位,即像素。 size[0] 表示 rows, size[1] 表示 cols
elemSize 表示的每一个元素的大小,以字节为单位。彩色图像一个元素分为三块,一块1个字节,因此为3
elemSize1 表示的是一个元素的每一个通道的大小,因此为1