学习OpenCV3——第四章:图像和大型数组类型

一、动态可变的存储

本章,我们将进入大型数组类型的世界,他们之中最主要的当属cv::Mat,这个结构可以视为是OpenCV所有C++实现的核心,OpenCV所有主要函数都或是cv::Mat类的成员,或是将cv::Mat作为参数,或是返回一个cv::Mat类型。很少有函数和这三者都没有关系。

cv::Mat类用于表示任意维度的稠密数组。在本章中,“稠密”表示该数组的所有部分都有一个值存储,即使这个值为0。对于大多数图像来说,都是以稠密数组的形式存储的;与此相对的当然还有稀疏数组,在spare array中被实现,稀疏数组中只有非0的数值会被存储。从结论上来说,如果数组的很多地方都是0,那么稀疏数组会非常节约内存。但是在数组比较稠密的时候,稀疏数组反而会浪费大量内存。一个常用的使用稀疏数组比稠密数组好的例子是,统计直方图的大部分数值都为0,我们没有必要存储所有值为0的部分。

1、cv::Mat类N维稠密数组
        cv::Mat类可以作为任意维度的数组使用,其数据可以看做是以按照栅格扫描顺序存储的n维数组。这意味着在一维数组中,元素是按顺序排列的;而在一个二维数组中,数据按行组织,每一行也按顺序排列,对于三维数组来说,所有的通道都被行填充,每一个通道同样按顺序排列。
        所有的矩阵都包含:

  • 一个表示它所包含数组类型的元素flag
  • 一个表示其维度的元素dims
  • 分别表示行和列的数目的元素rows和cols(在dims大于2的时候无效)
  • 一个表示数据真正存储位置的data指针
  • 一个表示该内存区域有多少个引用的refcount元素,类似于cv::Ptr<>的引用计数器

2、创建一个数组 

       数据类型包括CV_{8U,16S,16U,32S,32F,64F}C{1,2,3} 。例如:CVC_32FC3 表示一个三通道的32位浮点数数据。

常见的构造方式有:

// 先声明,再创建数据
cv::Mat m;
// Create data area for 3 rows and 10 columns of 3-channel 32-bit floats
m.create( 3, 10, CV_32FC3 );
// Set the values in the 1st channel to 1.0, the 2nd to 0.0, and the 3rd to 1.0
m.setTo( cv::Scalar( 1.0f, 0.0f, 1.0f ) );

等效于:
// 声明同时创建
cv::Mat m( 3, 10, CV_32FC3, cv::Scalar( 1.0f, 0.0f, 1.0f ) );

表4-1是一个完整的cv:Mat构造函数的列表,有很多,但有些并不常用,这个表格在你需要的时候用来查找会帮助到你。

表4-1:cv::Mat的构造函数(非复制构造)
构造函数说明
cv::Mat;默认构造函数
cv::Mat( int rows, int cols, int type );指定类型的二维数组
cv::Mat(int rows, int cols, int type,const Scalar& s);指定类型的二维数组,并指定初始值
cv::Mat(int rows, int cols, int type,void* data, size_t step=AUTO_STEP);指定类型的二维数组,并指定预先存储的数据
cv::Mat( cv::Size sz, int type );指定类型的二维数组(大小由sz决定)
cv::Mat(cv::Size sz,int type, const Scalar& s);指定类型的二维数组,并指定初始化值(大小由sz决定)
cv::Mat(cv::Size sz, int type,void* data, size_t step=AUTO_STEP);指定类型的二维数组,并指定预先存储的数据(大小由sz决定)
cv::Mat(int ndims, const int* sizes,int type);指定类型的多维数组
cv::Mat(int ndims, const int* sizes,int type, const Scalar& s);指定类型的多维数组,并指定初始化值
cv::Mat(int ndims, const int* sizes,int type, void* data,size_t step=AUTO_STEP);指定类型的多维数组,并指定预先存储的数据
表4-2:cv::Mat从其他cv::Mat复制数据的构造函数
构造函数描述
cv::Mat( const Mat& mat );复制构造函数
cv::Mat(const Mat& mat,const cv::Range& rows,const cv::Range& cols);只从指定的行列中复制数据的复制构造函数
cv::Mat(const Mat& mat,const cv::Rect& roi);只从感兴趣的去榆中复制数据的复制构造函数
cv::Mat(const Mat& mat,const cv::Range* ranges);服务于n维数组的,从而泛化的感兴趣区域中复制数据的复制构造函数
cv::Mat( const cv::MatExpr& expr );从其他矩阵的线性代数表述中声称新矩阵的复制构造函数
表4-4:cv::Mat模板构造函数
构造函数描述
cv::Mat(const cv::Vec& vec,bool copyData=true);构造一个如同cv::Vec所指定的数据类型为T、大小为n的一维数组
cv::Mat(const cv::Matx& vec,bool copyData=true);构造一个如同cv::Matx所指定的数据类型为T、大小为m*n的二维数组
cv::Mat(const std::vector& vec,bool copyData=true);构造STL的vector所指定的数据类型为T、大小为vector元素数的一维数组
表4-5:构造cv::Mat的静态方法
函数描述
cv::Mat::zeros( rows, cols, type );构造一个大小为rows*cols、数据类型为type指定类型的、数值全为0的矩阵
cv::Mat::ones( rows, cols, type );构造一个大小为rows*cols、数据类型为type指定类型的、数值全为1的矩阵
cv::Mat::eye( rows, cols, type );构造一个大小为rows*cols、数据类型为type指定类型的单位矩

3、独立获取数组元素

OpenCV有几种不同的访问矩阵的方法,这些方法均是为了方便对某一种数据类型进行访问而设计。然而,最近的OpenCV版本投入大量的精力使其几十不完全相同,也可以进行对比,并进行高效的访问。访问一个元素的两种主要的方法是通过位置迭代器访问。

  • 直接访问是通过模板函数at<>()来实现的:

    //单通道----------------------------------
    cv::Mat m = cv::Mat::eye( 10, 10, 32FC1 );
    printf("Element (3,3) is %f\n",m.at<float>(3,3));
    
    //多通道----------------------------------
    cv::Mat m = cv::Mat::eye( 10, 10, 32FC2 );
    printf("Element (3,3) is (%f,%f)\n",m.at<cv::Vec2f>(3,3)[0],m.at<cv::Vec2f>(3,3)[1]);
    
    //构建一个复数数组-------------------------
    cv::Mat m = cv::Mat::eye( 10, 10, cv::DataType<cv::Complexf>::type );
    printf("Element (3,3) is %f + i%f\n",m.at<cv::Complexf>(3,3).re,m.at<cv::Complexf>(3,3).im,);
    
    

     表4-6列举了at<>()函数可用的数据类型:

表4-6:at<>()访问器函数的变体
示例描述
M.at<int>( i );整型数组M中的元素
M.at<float>( i, j );浮点型数组M中的元素
M.at<int>( pt );整型矩阵M中处于(pt.x, pt.y)的元素
M.at<float>( i, j, k );三维浮点型矩阵M中处于(i, j, k)位置的元素
M.at<uchar>( idx );无符号字符数组M中位于idx[]所索引的n维位置的元素
  • 迭代器访问

所有的迭代器都必须在数组建立的时候声明并且指定一个对象类型。下面有一个简单的使用迭代器来计算三通道数组中“最长”元素(一个三维向量域)的例子:

int sz[3] = { 4, 4, 4 };
cv::Mat m( 3, sz, CV_32FC3 ); // A three-dimensional array of size 4-by-4-by-4
cv::randu( m, -1.0f, 1.0f ); // fill with random numbers from -1.0 to 1.0

float max = 0.0f; // minimum possible value of L2 norm
cv::MatConstIterator<cv::Vec3f> it = m.begin();
while( it != m.end() ) 
{
    len2 = (*it)[0]*(*it)[0]+(*it)[1]*(*it)[1]+(*it)[2]*(*it)[2];
    if( len2 > max ) max = len2;
    it++;
}

还有一种形式的迭代器,尽管不像cv::MatIterator<>那样将不连续的内存区域打包以同时处理多个数组的迭代。这个迭代器称为

cv::NaryMatIterator,它只要求被迭代的数组有相同的几何结构(维度以及每一个维度的范围)。

该迭代器不会返回一个用于迭代的单独元素,而通过返回一堆数组来进行N-ary迭代器操作,这些返回的数组也称为“面”。一个面表示输入数组有连续内存的部分(一般来说是一维或者二维的片段)。

示例4-1:按面进行多维数相加

const int n_mat_size = 5;
const int n_mat_sz[] = { n_mat_size, n_mat_size, n_mat_size };
cv::Mat n_mat( 3, n_mat_sz, CV_32FC1 );
cv::RNG rng;
rng.fill( n_mat, cv::RNG::UNIFORM, 0.f, 1.f );
const cv::Mat* arrays[] = { &n_mat, 0 };
cv::Mat my_planes[1];
cv::NAryMatIterator it( arrays, my_planes );

// On each iteration, it.planes[i] will be the current plane of the
// i-th array from 'arrays'.
float s = 0.f; // Total sum over all planes
int n = 0; // Total number of planes
for (int p = 0; p < it.nplanes; p++, ++it) 
{
    s += cv::sum(it.planes[0])[0];
    n++;
}

为了看到N-ary迭代器的实际效用,考虑将上个例子稍微拓展一下,变成两个数组相加(示例4-2)

const int n_mat_size = 5;
const int n_mat_sz[] = { n_mat_size, n_mat_size, n_mat_size };
cv::Mat n_mat0( 3, n_mat_sz, CV_32FC1 );
cv::Mat n_mat1( 3, n_mat_sz, CV_32FC1 );
cv::RNG rng;
rng.fill( n_mat0, cv::RNG::UNIFORM, 0.f, 1.f );
rng.fill( n_mat1, cv::RNG::UNIFORM, 0.f, 1.f );
const cv::Mat* arrays[] = { &n_mat0, &n_mat1, 0 };
cv::Mat my_planes[2];
cv::NAryMatIterator it( arrays, my_planes );

float s = 0.f; // Total sum over all planes in both arrays
int n = 0; // Total number of planes
for(int p = 0; p < it.nplanes; p++, ++it) 
{
    s += cv::sum(it.planes[0])[0];
    s += cv::sum(it.planes[1])[0];
    n++;
}
  • 通过块访问数组元素

通过以上内容,我们了解了按照独立元素和按照序列进行迭代访问的方法。还有一种常见的情况是,你可能需要将一个数组的子集作为另一个数组访问。这个子集可能是一行或者一列,也可能是原始数组的一个子集。如表4-7所示,有很多种方法都可以让我们完成这个目的,所有的方法都是cv::Mat的成员函数,并且都返回我们所请求的数组的子集。

表4-7:cv::Mat区块函数
示例描述
m.row( i );m中的第i行数组
m.col( j );m中第j列数组
m.rowRange( i0, i1 );

m中第i0行到第i1-1行所构成的数组

m.rowRange( cv::Range( i0, i1 ) );m中第i0行到第i1-1行所构成的数组
m.colRange( j0, j1 );m中第j0行到第j1-1行所构成的数组
m.colRange( cv::Range( j0, j1 ) );m中第j0行到第j1-1行所构成的数组
m.diag( d );m中偏移为d的对角线所组成的数组
m( cv::Range(i0,i1), cv::Range(j0,j1) );m中从点(i0, j0)到点(i1-1, j1-1)所包含数据构成的数组
m( cv::Rect(i0,i1,w,h) );m中从点(i0, j0)到点(i0+w-1, j0+h-1)所包含数据构成的数组
m( ranges );m中依据ranges[0]到ranges[ndim-1]所索引区域构成的数
  • 矩阵表达式:代数和cv::Mat

表4-8列出了可使用的代数操作的样例。

表4-8:矩阵表达式可用的运算操作
示例描述
m0 + m1, m0 – m1;矩阵的加法和减法
m0 + s; m0 – s; s + m0, s – m1;矩阵和单个元素的加和减
-m0;矩阵取负
s * m0; m0 * s;矩阵的单元素缩放
m0.mul( m1 ); m0/m1;按元素将m0和m1相乘,按元素将m0和m1相除
m0 * m1;m0和m1进行矩阵乘法
m0.inv( method );对m0矩阵求逆(默认使用DECONP_LU)
m0.t();对m0矩阵求转置
m0>m1; m0>=m1; m0==m1; m0<=m1; m0按元素进行比较,返回元素只有0和255的uchar类型矩阵
m0&m1; m0|m1; m0^m1; ~m0;m0&s; s&m0; m0|s; s|m0; m0^s; s^m0;矩阵和矩阵之间或者矩阵和单个元素之间按位进行的逻辑操作
min(m0,m1); max(m0,m1); min(m0,s);min(s,m0); max(m0,s); max(s,m0);矩阵和矩阵之间或者矩阵和单个元素之间按元素取最大值或者最小值
cv::abs( m0 );对m0按元素取绝对值
m0.cross( m1 ); m0.dot( m1 );向量叉乘和点乘操作(叉乘操作只适用于3*1矩阵)
cv::Mat::eye( Nr, Nc, type );cv::Mat::zeros( Nr, Nc, type );cv::Mat::ones( Nr, Nc, type );用于返回规定类型的N*N矩阵的静态方法
  • 饱和转换

在opencv中,对计算式进行计算的时候经常会出现值溢出(上溢出或下溢出),尤其是对无符号数值进行加减操作时。opencv中使用Saturation Casting“转换很好的解决这个问题,使用saturate_cast<>(),opencv对数组或矩阵进行代数运算或其他操作时,会自动检测underflows and overflows,当结果上溢出时就会用最大的可用值代替,当下溢出时就会用最小的可用值代替。

uchar& Vxy = m0.at<uchar>( y, x );
Vxy = cv::saturate_cast<uchar>((Vxy-128)*2 + 128);}

如果Vxy=10,那么Vxy-128 =-118,(Vxy-128)*2 + 128=-108,超出了uchar(8-bit)Vxy 范围,cv::saturate_cast就会把范围限定为uchar的最小值0。

  • 数组还可以做很多事情

表4-9:cv::Mat类的更多函数成员
示例描述
m1 = m0.clone();从m0进行完全复制,该复制将复制所有的数据元素
m0.copyTo( m1 );将m0复制给m1,如果有必要,将给m1重分配内存空间(等同于m1 = m0.clone())
m0.copyTo( m1, mask );和m0.copyTo( m1 )一样,但是只复制mask所指示的区域
m0.convertTo(m1, type, scale, offset);

转换m0中元素的类型(比如CV_32F)并且在尺度变换(默认为1)和增加偏置(默认为0)之后赋值给m1

m0.assignTo( m1, type );只在内部使用(集成在convertTo中)
m0.setTo( s, mask );设置m0内部所有元素为s,如果存在mask,则只对mask指示区域进行操作
m0.reshape( chan, rows );改变二维数组的有效形状,chan和rows变量可能为0,表示不做更改
m0.push_back( s );在末尾增加一个m*1大小的数组
m0.push_back( m1 );向m*n大小的矩阵m0增加k行并且复制到m1中,m1大小必须是k*n
m0.pop_back( n );从m*n大小的矩阵移除n行(默认是1)
m0.locateROI( size, offset );将m0的全尺寸写入cv::Size变量size,如果m0指示一个大矩阵的一块小区域,还会写入一个Point类型的offset
m0.adjustROI( t, b, l, r );通过四个值t(最上),b(最下),l(最左),r(最右)调整ROI范围
m0.total();计算数组序列的元素的数目(不包括通道)
m0.isContinuous();如果m0的行之间没有空隙,将返回true
m0.elemSize();返回m0的位长度(比如三通道浮点矩阵将返回12)
m0.elemSize1();返回m0最基本元素的位长度(比如三通道浮点矩阵将返回4)
m0.type();返回m0元素的类型(比如CV_32FC3)
m0.depth();返回m0通道中的元素类型(比如CV_32F)
m0.channels();返回m0的通道数目
m0.size();以cv::Size返回m0的大小
m0.empty();如果数组没有元素,将返回true(比如m0.total==0或者m0.data==NULL)
  • 稀疏数据类cv::SparesMat

cv::SparesMat类在数组非零元素非常少的情况下使用。这里就不做多介绍了,很少碰到,感兴趣可自行百度。

二、小结

在本章中,我们介绍了OpenCV最重要的数组结构cv::Mat,它用来表示矩阵、图像、和多维数组。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值