三(3.1 core 模块)OpenCV Mat对象

一、Mat类的综述

1、Mat类存储图像

  1. 在计算机内存中,数字图像是已矩阵的形式保存的。OpenCV2中,数据结构Mat是保存图像像素信息的矩阵,它主要包含两部分:矩阵头和一个指向像素数据的矩阵指针。

  2. 矩阵头主要包含,矩阵尺寸、存储方法、存储地址和引用次数等。矩阵头的大小是一个常数,不会随着图像的大小而改变,但是保存图像像素数据的矩阵则会随着图像的大小而改变,通常数据量会很大,比矩阵头大几个数量级。这样,在图像复制和传递过程中,主要的开销是由存放图像像素的矩阵而引起的。

  3. 那么Mat类如何存储的图像呢?
    我们都知道图像分为彩色图像和灰度图像,这里我有一个误区,一直认为彩色图像是一种三维矩阵,就是立方体的那种结构,一个图像分为三层。
    但是这种理解是错误的,是错误的,是错误的!
    其实在存储的图像不管是彩色的还是灰度图像,都是二维的矩阵,具体的存储格式如下

(1)灰度图像的格式:


灰色图像

(2)彩色图像的格式:


彩色图像

看到了吗,虽然彩色图像由BGR三个通道,但是是存储在同一个平面内的,只不过OpenCV在这里把三列才当作一列,因此有img.cols等于图像的列数。
一般我们用Opencv读取的灰度图像的数据类型为uchar类型的,而彩色图像的一个像素的数据类型为<Vec3b>类型的,灰度图一个像素占用1个字节,而彩色图像一个像素3个字节。

接下来就引出了我们如何按像素读取图像呢?下面有详解

Mat对象属性

Mat的常见属性

  • data uchar型的指针。Mat类分为了两个部分:矩阵头和指向矩阵数据部分的指针,data就是指向矩阵数据的指针。
  • dims 矩阵的维度,例如5*6矩阵是二维矩阵,则dims=2,三维矩阵dims=3.
  • rows 矩阵的行数
  • cols 矩阵的列数
  • size 矩阵的大小,size(cols,rows),如果矩阵的维数大于2,则是size(-1,-1)
  • channels 矩阵元素拥有的通道数,例如常见的彩色图像,每一个像素由RGB三部分组成,则channels = 3

下面的几个属性是和Mat中元素的数据类型相关的。

  • type
    表示了矩阵中元素的类型以及矩阵的通道个数,它是一系列的预定义的常量,其命名规则为CV_(位数)+(数据类型)+(通道数)。具体的有以下值:
CV_8UC1 CV_8UC2 CV_8UC3 CV_8UC4
CV_8SC1 CV_8SC2 CV_8SC3 CV_8SC4
CV_16UC1 CV_16UC2 CV_16UC3 CV_16UC4
CV_16SC1 CV_16SC2 CV_16SC3 CV_16SC4
CV_32SC1 CV_32SC2 CV_32SC3 CV_32SC4
CV_32FC1 CV_32FC2 CV_32FC3 CV_32FC4
CV_64FC1 CV_64FC2 CV_64FC3 CV_64FC4

这里U(unsigned integer)表示的是无符号整数,S(signed integer)是有符号整数,F(float)是浮点数。
例如:CV_16UC2,表示的是元素类型是一个16位的无符号整数,通道为2.
C1,C2,C3,C4则表示通道是1,2,3,4
type一般是在创建Mat对象时设定,如果要取得Mat的元素类型,则无需使用type,使用下面的depth

  • depth
    矩阵中元素的一个通道的数据类型,这个值和type是相关的。例如 type为 CV_16SC2,一个2通道的16位的有符号整数。那么,depth则是CV_16S。depth也是一系列的预定义值,
    将type的预定义值去掉通道信息就是depth值:
    CV_8U CV_8S CV_16U CV_16S CV_32S CV_32F CV_64F
  • elemSize
    矩阵一个元素占用的字节数,例如:type是CV_16SC3,那么elemSize = 3 * 16 / 8 = 6 bytes
  • elemSize1
    矩阵元素一个通道占用的字节数,例如:type是CV_16CS3,那么elemSize1 = 16 / 8 = 2 bytes = elemSize / channels
Mat img(3, 4, CV_16UC4, Scalar_<uchar>(1, 2, 3, 4));
    
    cout << img << endl;

    cout << "dims:" << img.dims << endl;
    cout << "rows:" << img.rows << endl;
    cout << "cols:" << img.cols << endl;
    cout << "channels:" << img.channels() << endl;
    cout << "type:" << img.type() << endl;
    cout << "depth:" << img.depth() << endl;
    cout << "elemSize:" << img.elemSize() << endl;
    cout << "elemSize1:" << img.elemSize1() << endl;

首先创建了一个3*4的具有4个通道的矩阵,其元素类型是CV_16U。Scalar_是一个模板向量,用来初始化矩阵的每个像素,因为矩阵具有4个通道,Scalar_有四个值。其运行结果:


image.png

运行结果首先打印了Mat中的矩阵,接着是Mat的各个属性。注意其type = 26,而depth = 2。这是由于上面所说的各种预定义类型
例如,CV_16UC4,CV_8U是一些预定义的常量。

  • step step这个属性理解起来有点麻烦

参考:
openCV Mat各属性简介(step1) - Sunshine_in_Moon的专栏 - CSDN博客 https://blog.csdn.net/sunshine_in_moon/article/details/45268971

Mat类常用的构造方法如下:

Mat::Mat() 
Mat::Mat(int rows, int cols, int type)
Mat::Mat(Size size, int type)
Mat::Mat(int rows, int cols, int type, const Scalar& s)
Mat::Mat(Size size, int type, const Scalar& s)
Mat::Mat(const Mat& m)
  • 无参构造方法:
    Mat::Mat()

  • 创建行数为rows,列为col,类型为type的图像(图像元素类型,如CV_8UC3等)
    Mat::Mat(int rows, int cols, int type)

  • 创建大小为size,类型为type的图像
    Mat::Mat(Size size, int type)

  • 创建行数为 rows,列数为 col,类型为 type 的图像,并将所有元素初始化为值 s
    Mat::Mat(int rows, int cols, int type, const Scalar& s)

  • 创建大小为 size,类型为 type 的图像,并将所有元素初始化为值 s
    Mat::Mat(Size size, int type, const Scalar& s)

  • 将 m 赋值给新创建的对象,此处不会对图像数据进行复制,m 和新对象共用图像数据
    Mat::Mat(const Mat& m)

OpenCV学习之路(二)——Mat对象 - 简书 https://www.jianshu.com/p/883684519e80

Mat对象的复制与克隆:

矩阵头的大小是一个常数,不会随着图像的大小而改变,但是保存图像像素数据的矩阵则会随着图像的大小而改变,通常数据量会很大,比矩阵头大几个数量级。这样,在图像复制和传递过程中,主要的开销是由存放图像像素的矩阵而引起的。因此,OpenCV使用了引用次数,当进行图像复制和传递时,不再复制整个Mat数据,而只是复制矩阵头和指向像素矩阵的指针。例如:

cv::Mat a ;//创建矩阵头
a = cv::imread("f:\\psb.jpg");//读入图像
cv::Mat b = a ;//复制 

上面的a,b有各自的矩阵头,但是其矩阵指针指向同一个矩阵,也就是其中任何一个改变了矩阵数据都会影响另外一个。
那么,多个Mat共用一个矩阵数据,最后谁来释放矩阵数据呢?
这就是引用计数的作用,当Mat对象每被复制一次时,就会将引用计数加1,而每销毁一个Mat对象(共用同一个矩阵数据)时引用计数会被减1,当引用计数为0时,矩阵数据会被清理。

上图是Mat对象a,b共用一个矩阵,故其引用计数refcount为2.
但是有些时候仍然会需要复制矩阵数据本身(不只是矩阵头和矩阵指针),这时候可以使用clone 和copyTo方法。

cv::Mat c = a.clone();
cv::Mat d ;
a.copyTo(d);

上面代码中的c,d各自拥有自己的矩阵,改变自己的矩阵数据不会相互影响。
在使用Mat中,需要记住:

  • OpenCV中的内存分配是自动完成的(不是特别指定的话)
  • 使用OpenCV的C++ 接口时不需要考虑内存释放问题
  • Mat的赋值运算和拷贝构造函数只会拷贝矩阵头,仍然共同同一个矩阵
  • 如果要复制矩阵数据,可以使用clone和copyTo函数

Mat类除构造方法外其他方法:

img.create(4,4,CV_8UC(2));
bool imwrite(const string& filename,InputArray img,constvector<int>& params=vector<int>())
src.convertTo(dst, type, scale, shift)

convertTo()函数负责转换数据类型不同的Mat,即可以将类似float型的Mat转换到imwrite()函数能够接受的类型。
而cvtColor()函数是负责转换不同通道的Mat,因为该函数的第4个参数就可以设置目的Mat数据的通道数(只是我们一般没有用到它,一般情况下这个函数是用来进行色彩空间转换的)。

mat zeros(size,type)
mat eye(size,type)

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;
}
彩色图像<Vec3b>数据类型执行结果

(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;
}
测试结果

Opencv中的Mat类使用方法总结 - OpenCV知识库 http://lib.csdn.net/article/opencv/42000

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容

  • 即使你的五官已在我的脑海里模糊,但那些的画面,就像纪录片一样时不时地冒出来,它们为什么就不能随你而去?还是我内心深...
    港姐阅读 459评论 0 2
  • 不得不承认我在这波中小板操作中上头了,目前浮亏快十个点了,仓位上的上头,导致自己很被动。但是我还是坚持的看好创业板...
    GSX是哲学家阅读 109评论 0 0
  • 人,总有一些东西比外表来得重要!这些东西不在于一个人是否聪明,也不在于是否漂亮,这些东西是传记作者或者照相机所无法...
    我叫冻柿子阅读 426评论 0 3