首页 > 技术文章 > OpenCV_Tutorials——CORE MODULE.THE CORE FUNCTIONALITY—— Mat - The Basic Image Container

dmq5488287 2015-05-11 18:23 原文

在家这段时间内,发现了这样的OpenCV库自带的教程,感觉不错,尝试翻译并且添加一些tips,帮助自己学习,同时也与各位交流一下。

核心模块.核心功能

 

这里这两部分说的是核心模块以及核心功能的简介,其中蓝字部分可以链接到相关部分,这里我采用顺序方法,从第一部分,即 “Mat:The Basic Image Container”开始叙述。

2.1 基本的图像容器—矩阵

目标

      我们可以通过许多途径从真实世界获取数字图像,例如:数码相机、扫描仪、计算机断层扫描以及磁成像共振等等。不管怎样,那些都只是我们人类所看到的图像。然而,当把这些图像传输到数码设备中时,则需要把图像中的每个点按照数字化存储。

 

 

 

 

 

 

 

 

 

   

   例如,在上面的图像中,可以看到汽车的后视镜在计算机中无非是用一个矩阵来包括所有的像素点的强度值(intensity values)。我们可以根据我们的需求来存取这些像素值,但是在计算机世界中,所有的图像可能最终都会被简化为数字化的矩阵以及用于描述这个矩阵的其他信息。OpenCV就是一个主要专注于处理和操纵这些信息的计算机视觉库。因此,我们需要做的第一件事情就是去了解并熟悉OpenCV这个库是如何存储和操纵图像的。

矩阵

      OpenCV在2001年已经出现了。在那时OpenCV库使用C语言作为其接口语言,并且使用被称为IplImage的这样的一个C语言的结构体作为其图像存储结构。这些东西将会在大部分老旧的教材和手册中出现。伴随而来的一系列和C相关的问题,进行内存管理成为这其中最大的问题。早期的OpenCV库编译的基础在于它假设用户会负责进行内存的管理,例如内存分配以及析构。这些在小的程序的开发过程中都不是什么问题,一旦你的代码量开始大幅度增加,内存管理也就成了你实现你开发目的的一个很大的羁绊。

      十分幸运的是,C++的诞生引入了类的概念,从而或多或少的使用户从内存管理这个泥潭中得到解放。C++可以完全兼容C,因此这样的改变不会出现兼容问题。因此OpenCV 2.0这个版本引入新的C++语言接口,这个接口可以提供一种新的方法,使用户摆脱内存管理,简洁代码(写得少,自动实现的多)。C++实现的OpenCV接口的不利之处也就是在只支持C语言的嵌入式系统中无法使用。因此,除非你要在嵌入式平台中开发,否则还是使用C++的OpenCV借口吧(除非你你是一个有受虐倾向的程序员或者你想要找点麻烦)。

       首先你应该了解矩阵就是那个你不需要手动分配内存并且当你不需要的时候,会自动析构掉的东西。这已经实现了,大多数OpenCV函数会在输出数据的时候自动分配数据所需要的空间。如果你传入一个已经存在的并且早已分配了所需的矩阵空间的矩阵对象,这个对象会被重用。换句话说,我们只会使用任务需求的大小的内存。

       矩阵作为一个基本的类,拥有两个数据部分:矩阵头(包含类似于矩阵大小,所使用的存储方式以及矩阵存储地址等等信息)和一个指向包含像素值的矩阵的指针(通过维度来选用存储方法)。矩阵头的大小是个常数,然而矩阵体自己的大小可能不一而同,并且通常是巨大的(翻译不太好吧,usually is larger by orders of magnitude)。

       OpenCV是一个图像处理库。它包括大量的图像处理函数。为了解决计算方面的挑战,通常你会通过使用库中的多个函数来解决问题。由于这个因素,通常将一个图像传入函数。我们在想要处理大量的计算问题的时候,不要忘记我们讨论的图像处理算法。最后我们考虑一下那些会极大减慢你的程序运行速度的不必要的隐式复制大图像的行为。

       为了解决这个问题,OpenCV使用了引用计数系统。每个矩阵对象拥有自己的矩阵头,两个矩阵头可能会同时分享一个矩阵体,也就是指针指向同一个地址。此外,拥有复制行为的操作将只是复制矩阵头和指向那个大的矩阵体的指针,而不是矩阵体本身。

                           

                                    (复制前)                                               (复制后)

Mat A,C;
A=imread(argv[1],CV_LOAD_IMAGE_COLOR);
Mat B(A);
C=A;

      上面所有的对象,到最后,都指向了同一个矩阵体。他们的矩阵头是不同的,然而,通过修改其中任意一个,都会影响所有的头部。一般来说,不同的对象对于同一个底层的数据提供不同的读取方式。然而,他们的矩阵头部部分是不同的。最有意思的部分是,你可以创造一个只引用整个数据的子集的矩阵头。例如,创建感兴趣区域(ROI)图像中你只需要创建一个具有新的边界的新的矩阵头。

Mat D(A,Rect(10,10,100,100);
Mat E=A(Range::all(),Range(1,3));   

       现在你可能想问一个不再需要的矩阵体的析构是不是由多个矩阵对象负责?简短的回答是:最后一个使用矩阵体的对象负责。这是由于使用引用计数机制所导致的结果。当对矩阵对象进行复制操作时,对于这个矩阵体的计数器就会加一。当一个矩阵头不再指向这个矩阵体,那么这个计数器就会减一。当这个计数器的数值到达零,这个矩阵体也该进行析构动作了。有些时候,你想要复制矩阵体,而不是简单的复制矩阵矩阵头,你可以使用OpenCV提供的clone(),和copyTo()这两个函数。

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

        F、G、A任意几个的修改动作不会对其余对象造成任何影响。你需要记住以下几点:

1、输出图像所需要的内存是OpenCV自动分配的(除非特殊情况)。

2、你在OpenCV的C++接口下无需考虑内存管理。

3、assignment operator 和copy constructor 只会复制矩阵头。

4、底层的图像矩阵(也就是矩阵体)可以通过clone()、copyTo()函数举行复制。

 

存储方式

     这一部分所讲的就是你如何存储像素值。你可以选择你要是用的色域以及所使用的数据类型。色域就是我们如何使用色彩分量来编码一种给定的颜色。最简单的一种就是灰度,颜色都是黑白两色。通过黑白两色的组合,我们可以创造出色调的灰色。对于多色彩,我们有更多的办法可以选择。每一种色彩可以分成三种或四种基本的分量,我们可以利用这里的分量来创作其他的颜色。最常用的是RGB,主要因为RGB可以让我们的眼睛识别颜色。RGB的及出色为红、绿、蓝。如果需要编码透明度,你需要增加另外一种色彩元素:alpha(用A来表示)。

      每一种色彩系统都有其优势:

1、RGB是我们最常用的也是我们的眼睛所使用的类似的颜色系统。我们的显示系统同样将颜色使用这个系统进行组合。

2、HSV和HLS系统将颜色分解为色调、饱和度以及亮度这三部分。这是一种描述色彩更加自然的方式。例如,你可能忘记最后的那个组成部分(亮度),这会让你的算法对于输入图像缺乏亮度调节能力。

3、YCrCb被普遍用于JPEG图像格式中。

4、CIE L * * b * 一个感知均匀色域如果需要测量给定颜色另一种颜色距离就会使用到

     每一种组成分量都有其自己的可用域。这导致了数据类型的使用。我们是怎样拥有控制存储分量的权利呢?最小的数据类型可能是char,也就是说1 byte或者8 bits。char类型可以是unsigned(也就是说值域为0~255)或者signed(值域为-127~127)。在这种情况下,三种分量已经组成了1600万的可能出现的色彩类型(类似RGB这种颜色系统)我们可以要求一个更加精细的控制,每个分量使用float(4 byte=32bit)或者double(8 byte=64 bit)数据类型。然而,要记得,增加了分量的大小同时也会增加整个图片在内存中的大小。

创建一个矩阵对象

      在“存储、修改以及保存图片”教程中你已经学习到如何使用imwrite()函数把一个矩阵重的内容保存到一个图片文件中。然而如果只是为了调试的目的,那么直接显示其具体数值岂不是更方便。你可以使用矩阵的<< operator来满足这个需要。切记,这些只在二维度的矩阵中可以使用。


 

tips:

如果使用<<operator 在二维以上的矩阵中使用时,会出下如下错误提示:

 

如果在一维矩阵矩阵中使用,则一维矩阵矩阵做逆置,从行矩阵变换为列矩阵。因此原则上还是属于二维矩阵。


 

       矩阵类作为图像容器是很好的,当然矩阵类首先是个矩阵类。因此通过矩阵类可以创建和操作多维度的矩阵。你可以使用多种方法创建矩阵对象:

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的意思就是我们使用unsigned char类型,也就是8 bit 长度并且每一个像素有三个这样的unsigned char 来组成三通道。这里的预定义中,最多到达四通道。Scalar 类型是四个元素的短的向量。特别的,这里你可以使用你喜欢的值来初始化所有的矩阵点。如果你需要更多的类型,你可以使用上面所提及的宏来创建更多的类型,通过下面的小括号,可以设置通道数。

使用C\C++数组通过构造函数来进行初始化

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

    上面的例子告诉我们如何创建一个多于二维的矩阵体。指定它的维度,然后传入一个包含每一个维度大小的指针,其他的保持相同。

为一个已经存在的IplImage指针创建一个矩阵头。

IplImage *img=cvLoadImage("greatwave.png",1);
Mat mtx(img);

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<<endll<<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 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<<formate(R,"csv")<<endl<<endl;

Numpy

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

C

cout<<"R(C)="<<endl<<format(R,"C")<<endll<<endl;

输出其它常见类型

OpenCV同样可以通过<<operator输出它所支持的数据结构

2D Point

 

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

 

3D Point

Point3f P3f(2,6,7);
cout<<"Point(3D)="<<P3f<<endl<<endl;

通过std::vector构造cv::Mat

vector<float> v;
v.push_back((float)CV_PI);
v.push_back(2);
v.push_back(3.01f);
cout<<"Vector of float via Mat="<<Mat(v)<<endl<<endl;

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 Points="<<vPoints<<endl<<endl;

这里的大多数例子已经被包含进了一个小的控制台程序。你可以下载或者进入opencv\sources\samples\cpp\tutorial_code\core地址寻找程序。


 

一、博文作者注:如果需要进行代码编译,请加上

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include <sstream>

using namespace std;
using namespace cv;

二、这里我们需要提及一下关于Mat的数据布局问题,这里的资料引用OpenCV Helper中

 

class Mat

 

OpenCV C++ n-dimensional dense array class

 

class CV_EXPORTS Mat
{
public:
    // ... a lot of methods ...
    ...

    /*! includes several bit-fields:
         - the magic signature
         - continuity flag
         - depth
         - number of channels
     */
    int flags;
    //! the array dimensionality, >= 2
    int dims;
    //! the number of rows and columns or (-1, -1) when the array has more than 2 dimensions
    int rows, cols;
    //! pointer to the data
    uchar* data;

    //! pointer to the reference counter;
    // when array points to user-allocated data, the pointer is NULL
    int* refcount;

    // other members
    ...
};

 

The class Mat represents an n-dimensional dense numerical single-channel or multi-channel array. It can be used to store real or complex-valued vectors and matrices, grayscale or color images, voxel volumes, vector fields, point clouds, tensors, histograms (though, very high-dimensional histograms may be better stored in a SparseMat ). The data layout of the array M is defined by the array M.step[], so that the address of element (i_0,...,i_{M.dims-1}), where 0\leq i_k<M.size[k], is computed as:

 

addr(M_{i_0,...,i_{M.dims-1}}) = M.data + M.step[0]*i_0 + M.step[1]*i_1 + ... + M.step[M.dims-1]*i_{M.dims-1}

 

In case of a 2-dimensional array, the above formula is reduced to:

 

addr(M_{i,j}) = M.data + M.step[0]*i + M.step[1]*j

 这张图放在这里比较合适,能够表现出step是怎样的一个东西。图片转自http://www.cnblogs.com/wangguchangqing/p/4016179.html

Note that M.step[i] >= M.step[i+1] (in fact, M.step[i] >= M.step[i+1]*M.size[i+1] ). This means that 2-dimensional matrices are stored row-by-row, 3-dimensional matrices are stored plane-by-plane, and so on. M.step[M.dims-1] is minimal and always equal to the element size M.elemSize() .

 

So, the data layout in Mat is fully compatible with CvMat, IplImage, and CvMatND types from OpenCV 1.x. It is also compatible with the majority of dense array types from the standard toolkits and SDKs, such as Numpy (ndarray), Win32 (independent device bitmaps), and others, that is, with any array that uses steps (or strides) to compute the position of a pixel. Due to this compatibility, it is possible to make a Mat header for user-allocated data and process it in-place using OpenCV functions.

 

引用自http://www.douban.com/note/265479171/部分的有:

depth:深度,即每一个像素的位数(bits),在opencv的Mat.depth()中得到的是一个 0 – 6 的数字,分别代表不同的位数:enum { CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6 };

 

推荐阅读