OpenCV-2-数据类型

1 摘要

OpenCV使用一系列基础数据类型作为模块,特例化这些模版能够得到大量的数据类型,另外你也能够联系你的使用场景对其扩展从而更灵活的完成自己的应用。OpenCV严重依赖标准模版库(STL),如很多函数的参数都要求传入向量类的实例。除此之外OpenCV自己的数据类型分为三类,

  • 基本类型,它直接对C++基本数据类型组装,如向量、矩阵以及表示点、矩形等几何概念的数据类型。
  • 辅助类型,它们表示如垃圾收集指针类,用于切片的范围对象以及终止条件抽象等更抽象的概念。
  • 大数组类型,它们是包含数组或者其他基本数据类型集合的集合数据类型,它也也能包含基础数据类型。如类cv::Mat,它用于表示任意维度包含基础元素的数组,图片是该类的一个特例化类,和OpenCV 2.1之前的版本不同,特例化的类型仍使用原名。如稀疏矩阵类cv::SparseMat描述更稀疏的数据,如直方图。

2 基本类型

下表列出OpenCV的基本数据类型,在OpenCV中,尽管几乎所有的基本数据类型都是模版类,但是我们很少直接使用他们来定义变量,通常我们都使用其别名形式。

模版 别名 示例
cv::Vec<> cv::Vec{2,3,4,6}{b,w,s,i,f,d} cv::Vec2i
cv::Matx<> cv::Matx{1,2,3,4,6}{1,2,3,4,6}{f,d} cv::Vec33f
cv::Point<> cv::Matx{2,3}{i,f,d} cv::Point2i
- - cv::Scalar
cv::Size cv::Size2{i,l,d,f} cv::Size2i
cv::Rect cv:: Rect2{i,d,f} cv:: Rect2i
- - cv:: RotatedRect
- - cv::Complexd
cv::Complexd

相比于STL的向量类,模版类cv::Vec<>具有固定的长度,被称为固定向量向量类(FIxed Vector Class),这意味着编译时就知道向量的大小,从而使得处理此类问题的代码更高效。另外该类的元素个数是有限的,如在OpenCV2.2中,该类的元素个数不能超过9个。如果需要处理更复杂的数据,请使用类cv::Mat。尽管该类是一个模版类,但是我们通常不使用模版类的实例化方式,相反我们通常使用别名的方式去实例花一个向量对象,其所有的别名类型及示例如上表。

和向量类类似,cv::Matx<>是轻量级矩阵模块类,这意味着所包含的数据尺寸也受限制,通常在计算机视觉中,2✖️2、3✖️3和4✖️4的矩阵用于大多数变换操作,而cv::Matx<>就是设计于这些数据类型的,它最多支持6✖️6矩阵。同意的程序在编译时就知道容器的数据大小,不需要在运行时动态分布内存,这会使得代码执行效率更高。其所有的别名及示例如上表。

模版类cv::Point<>表示一个点,和向量及矩阵使用下标不同,它使用x、y、z变量访问其中包含的元素。类cv::Scalar继承于cv::Vec<double,4>,它表示一个由四个双精度元素组成的集合。

模版类cv::Size<>cv::Rect<>分别表示一个尺寸和一个矩形,需要注意的是cv::Sizecv::Rect分别是cv::Size2icv::Rect2i的别名。类cv::RotatedRect表示非轴对其的矩阵,它包含一个额外的浮点型角度值。

2.1 Point

Point是最简单的基本数据类型,它的开销很小。尽管该类定义的接口并不多,但是在需要的时候可以很方便的转换为更复杂的类型,如矩阵和向量类,其所有的接口如下表。

操作 示例
默认构造函数 cv::Point2i p;
cv::Point3f p;
拷贝构造函数 cv::Point3f p2( p1 );
值构造函数 cv::Point2i p( x0, x1 );
cv::Point3d p( x0, x1, x2 );
转换为固定向量类实例 (cv::Vec3f) p;
成员访问 p.x; p.y; p.z
点乘 float x = p1.dot( p2 )
双精度点乘 double x = p1.ddot( p2 )
叉乘 p1.cross( p2 ) // 仅支持3元素实例
检测是否位于矩形内 p.inside( r ) // 仅支持2元素实例

2.2 Scalar

cv::Scalar<>是一个模版类,它是Vec<_Tp, 4>的子类,同时cv::Scalar又是一个别名,它包含了4个双精度的元素。在四元素相关的运算中常会使用到该类,其支持的操作如下。

操作 示例
默认构造函数 cv::Scalar s;
拷贝构造函数 cv::Scalar s2( s1 );
值构造函数 cv::Scalar s( x0 );
cv::Scalar s( x0, x1, x2, x3 );
乘法运算 s1.mul( s2 );
共轭四元素 s.conj();
四元素真值测试 s.isReal();

2.3 Size

Size类实例不能转化为向量类实例,但是向量类和Point类实例可以转化为Size类实例,其支持的操作如下。

操作 示例
默认构造函数 cv::Size sz;
cv::Size2i sz;
cv::Size2f sz;
拷贝构造函数 cv::Size sz2( sz1 );
值构造函数 cv::Size2f sz( w, h );
成员访问 sz.width; sz.height;
计算面积 sz.area();
2.4 Rect

Rect类表示一个矩形,其包含成员变量xy,分别表示矩形左上角顶点和矩形的大小,其直接支持的操作如下。

操作 示例
默认构造函数 cv::Rect r;
拷贝构造函数 cv::Rect r2( r1 );
值构造函数 cv::Rect( x, y, w, h );
使用顶点和大小构造 cv::Rect( p, sz );
使用两个顶点构造 cv::Rect( p1, p2 );
成员访问 r.x; r.y; r.width; r.height;
面积计算 r.area();
提取左上角顶点 r.tl();
提取右下角顶点 r.br();
判断是否包含某点 r.contains( p );

此外Rect类还重载了一些运算符,用于应对一些常见的几何运算。

操作 示例
计算矩形重叠部分 cv::Rect r3 = r1 & r2;
r1 &= r2;
包含两个矩形的最小矩形 cv::Rect r3 = r1|r2;
r1 |= r2;
平移矩形 cv::Rect rx = r + x;
r += x;
增加矩形大小 cv::Rect rs = r + s;
比较两个矩形是否相同 bool eq = (r1 == r2);
比较两个矩形是否不同 bool ne = (r1 != r2);

2.5 RotatedRect

cv::RotatedRect是OpenCV底层少有的几个未使用模版的C++接口类之一,它包含一个cv::Point2f实例属性定义矩形中心,一个cv::Size2f实例属性定义矩阵大小,一个浮点型数据定义矩形绕中心旋转的角度。其支持的操作如下。

操作 示例
默认构造函数 cv::RotatedRect rr();
拷贝构造函数 cv::RotatedRect rr2( rr1 );
使用两个顶点构建 cv::RotatedRect( p1, p2 );
使用值构建 cv::RotatedRect rr( p, sz, theta ) ;
成员访问 rr.center; rr.size; rr.angle;
获取顶点列表 rr.points( pts[4] );

2.6 固定矩阵

固定矩阵类是大多数OpenCV的C++接口基本类型核心,如固定向量类继承于该类,而Scalar类又继承于固定向量类。它在编译阶段就能够确定变量的内存大小,因此在程序运行时该变量在栈上创建和释放,有着较高的效率。它适用于6维及以下的矩阵,如果需要表示更多的元素,需要使用类cv::Mat。其支持的操作如下。

操作 示例
默认构造函数 cv::Matx33f m33f;
cv::Matx43d m43d;
拷贝构造函数 cv::Matx22d m22d( n22d );
值构造函数 cv::Matx21f m(x0,x1);
cv::Matx44d m(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15);
含相同元素矩阵 m33f = cv::Matx33f::all( x );
全零矩阵 m23d = cv::Matx23d::zeros();
全一矩阵 m16f = cv::Matx16f::ones();
单位矩阵 m33f = cv::Matx33f::eye();
提取矩阵对角线 m31f = cv::Matx33f::diag();
创建均匀分布矩阵 m33f = cv::Matx33f::randu( min, max );
创建正态分布矩阵 m33f = cv::Matx33f::nrandn( mean, variance );
成员访问 m( i, j ), m( I );
矩阵代数运算 m1 = m0; m0 * m1; m0 + m1; m0 – m1;
和标量的代数运算 m * a; a * m; m / a;
比较 m1 == m2; m1 != m2;
点乘 m1.dot( m2 ); //返回值精度和参数一致
m1.ddot( m2 ); //返回值为双精度类型
改变矩阵结构 m91f = m33f.reshape<9,1>();
类型转换 m44f = (Matx44f) m44d
提取指定位置指定大小的子矩阵 m44f.get_minor<2, 2>( i, j );
提取某行 m14f = m44f.row( I );
提取某列 m41f = m44f.col( j );
转置矩阵 n44f = m44f.t();
逆矩阵 n44f = m44f.inv( method ); // 默认方法是cv::DECOMP_LU
解线性系统 m31f = m33f.solve( rhs31f, method )
m32f = m33f.solve<2>( rhs32f, method );
//模版类型,默认方法是DECOMP_LU
逐元素乘法 m1.mul( m2 );

2.7 固定向量类

固定向量类继承于固定矩阵类,即模版类cv::Vec<>可以理解为列数为1的cv::Matx<>类。其别名中的w后缀表示无符号短整形。其支持的操作如下。

操作 示例
默认构造函数 Vec2s v2s; Vec6f v6f;等
拷贝构造函数 Vec3f u3f( v3f );
值构造函数 Vec2f v2f(x0,x1); Vec6d v6d(x0,x1,x2,x3,x4,x5);
成员访问 v4f[ i ]; v3w( j );
向量点乘 v3f.cross( u3f );

2.8 复数类

OpenCV的复数类和STL里面的模版复数类complex<>有一定区别,它们相互兼容,并可互相转换,其支持的操作如下。

操作 示例
默认构造函数 cv::Complexf z1; cv::Complexd z2;
拷贝构造函数 cv::Complexf z2( z1 );
值构造函数 cv::Complexd z1(re0); cv::Complexd(re0,im1) ;
成员访问 z1.re; z1.im;
共轭复数 z2 = z1.conj();

3 辅助类型

辅助类型包含控制大量算法(如终止条件)行为,以及在容器内执行诸如范围确定和切片等操作的基础类型,以及从C++集成到OpenCV中的智能指针类型cv::Ptr等。

3.1 终止条件

大多数算法都需要一个终止条件,通常这个条件是算法迭代的次数达到了上线,或者算法计算出的误差在被允许的范围内。大多数场景下,我们会同时从这两个方面去限制算法的行为,因为计算的结果可能永远无法达到可接受的误差范围。类cv::TermCriteria封装了这两个条件,它包含类型、最大迭代数和可接受误差三个参数,可以通过cv::TermCriteria( int type, int maxCount, double epsilon )方式构建实例。参数type可选的值为cv::TermCriteria::COUNTTermCriteria::EPS,当你需要同时限制这两个条件时,使用逻辑或符合,即cv::TermCriteria::COUNT|TermCriteria::EPS

3.2 范围

cv::Range用于表示连续的整数序列,通过构造函数cv::Range( int start, int end )初始化,需要注意的是这个序列是一个半开半闭区间,其取值范围为[start, end),即调用cv::Range( 1, 3 )得到的序列是1、2。函数cv::Range::size()用于获取范围对象的容量,cv::Range::empty()用于判断范围对象是否为空,cv::Range::all()用于获取对象的取值范围。

3.3 指针和垃圾收集

C++中一个非常有用的类是智能指针,它也被集成到了OpenCV中。智能指针使我们可以创建一个变量的多个引用,并将它们任意传递,每创建一个引用都会增加原始对象的引用计数,当某个引用出作用域后其引用计数就会减1,当最后一个引用出作用域后,元素对象的引用就会为0,此时其析构函数会被隐式自动调用,程序员就不需要再担心内存问题。

使用智能指针时,需要包装一个具体类型,可以通过代码cv::Ptr<Matx33f> p( new cv::Matx33f )或者cv::Ptr<Matx33f> p = makePtr<cv::Matx33f>()创建一个指向类cv::Matx33f实例的指针对象。它可以像正常指针一样被传递,以及支持常用的操作符,如*()->()。在创建出智能指针对象p后,可以使用赋值符号将创建一个新的智能指针实例,如Ptr<Mat33f> q = p;,和直接使用裸指针(原始数据类型的指针一样),堆上的cv::Matx33f实例仅存在一份。不同的是智能指针会记录对该实例的引用计数,当q被释放时,p对原始数据的引用计数减1,当p被释放时,引用计数变为0,则原始数据被销毁,内存被回收。

cv::Ptr<>还提供了一些接口用于管理职能指针的引用计数,函数addref()release()分别增加和减少引用计数,这是一组带风险的函数,建议只有在当不得不对引用计数进行操作时才调用这两个函数。函数empty()适用于两种场景,第一是判断一个智能指针指向的实例是否已经被释放。第二在调用某些能够返回null函数初始化智能指针时,判断这些方法返回值是否为null,如使用c语言函数cvLoadImage()fopen()等。

该类还提供了一个函数delete_obj(),该函数会在引用计数变为0时自动调用。默认情况下该函数内部未实现任何功能。当智能指针指向的实例在销毁时需要额外操作时我们需要重载该函数并实现自己的逻辑。例如使用老版本OpenCV的C语言接口IplImage时,需要调用函数cvLoadImage()从磁盘中加载图片数据,使用C语言编程代码如下。

IplImage* img_p = cvLoadImage( ... );
// 这里省略内存管理相关逻辑

而使用智能指针后,代码应该是这样的。

cv::Ptr<IplImage> img_p = cvLoadImage( "an_image" );
cv::Ptr<IplImage> img_p( cvLoadImage( "an_image" ) );

该类型的智能指针对应的delete_obj()函数已经被重载,其被定义在OpenCV的头文件中。其中obj是智能指针内部实际指向具体实例的成员变量,该实现在其引用计数为0时释放了内存资源。

template<> inline void cv::Ptr<IplImage>::delete_obj() {
    cvReleaseImage(&obj);
}

在更多的情况下,如果我们希望包装的实例类型在释放时需要定制逻辑,我们需要自己去重载该模版的delete_obj()方法。例如使用智能指针包装FILE来处理文件相关操作时,我们需要以如下方式重载模版cv::Ptr<FILE>delete_obj()方法。

template<> inline void cv::Ptr<FILE>::delete_obj() {
    fclose(obj);
}

在如下代码中,当智能指针实例f出作用域后,该实例被销毁,同时检测到指向的FILE实例的引用计数为0时,文件句柄就会自动关闭。这里仅为举例说明,通常情况下我们建议文件关闭逻辑显示调用,而不是重载delete_obj()方法隐式调用。

{
  cv::Ptr<FILE> f(fopen("myfile.txt", "r"));
  if(f.empty())
  fprintf(f, ...);
  ...
}

需要注意的是智能指针内部的引用计数以及OpenCV中其他使用引用计数的类都是线程安全的。

3.4 异常类和异常处理

cv::Exception是OpenCV内部负责处理异常的类,它继承于STL的异常处理类std::exception,除了改变命名空间外,它没有做任何其他处理。其属性code表示错误码,err表示错误描述,func表示抛出异常的函数,而fileline具体表示具体的文件和行号。

你也可以使用一些内置宏来抛出自己的错误。CV_Error( errorcode, description )CV_Error_( errorcode, printf_fmt_str, [printf-args] )可以抛出自定义的异常,区别在于后者可以使用格式化的字符串作为错误描述。另外CV_Assert( condition )可以CV_DbgAssert( condition )用于条件断言,后者只在Debug环境中生效。这些宏会自己处理异常的函数、文件和行号信息,我们不需要做额外操作。

3.5 类型

在OpenCV内部需要传递类型时需要使用到模版类cv::DataType<>,在实际使用的时候我们需要使用到其特例化类。模版类cv::DataType<>的定义如下,其中包含了编译时能得到的信息,它们主要通过typedef的方式声明,也包含了一些运行时才能确定的信息,它们通过枚举的方式提供。该模版类可以实现一些复杂的交互,如能够使得一些算法实现不依赖于特定的数据类型。

template<typename _Tp> class DataType
{
  typedef _Tp        value_type;
  typedef value_type work_type;
  typedef value_type channel_type;
  typedef value_type vec_type;

  enum {
    generic_type = 1,
    depth        = -1,
    channels     = 1,
    fmt          = 0,
    type         = CV_MAKETYPE(depth, channels)
  };
};

在模版的定义中,通过typedef在编译时确定的4个类型都是相同的,但是在模版的特例化实现中它们通常是不一样的。为了更深入了解这些变量的含义,参考如下在core.hpp头文件中该模版的特例化实现。在下面的实例中,通过typedef定义的几个类型都是float类型。枚举部分的常量中,generic_type被定义为0,对于在core.hpp头文件中所有该模版的特例化实现该值都被定义为0。depth表示实际的基本数据类型标识符,这里是常量CV_32Fchannel表示该数据类型所包含的基础数据类型数量,这里是1。fmt通过一个单一字符的方式表示数据的格式,这里是ftype通过关联基础数据类型和通道数表示了当前的数据类型,这里是CV_32FC1

template<> class DataType<float>
{
public:
  typedef float      value_type;
  typedef value_type work_type;
  typedef value_type channel_type;
  typedef value_type vec_type;

  enum {
    generic_type = 0,
    depth        = DataDepth<channel_type>::value,
    channels     = 1,
    fmt          = DataDepth<channel_type>::fmt,
    type         = CV_MAKETYPE(depth, channels)
  };
};

模版类cv::DataType<>也能够以更复杂的方式使用,如下面的例子,该模版类的特例化结果仍然包含范型。首先该模版类包装的数据类型cv::Rect_<>本身也是一个模版类,在使用时它还需要特例化为具体的类型,如cv::DataType<Rect>或者cv::DataType< Rect_<float> >

template<typename _Tp> class DataType<Rect_<_Tp> >
{
public:
  typedef Rect_<_Tp>                               value_type;
  typedef Rect_<typename DataType<_Tp>::work_type> work_type;
  typedef _Tp                                      channel_type;
  typedef Vec<channel_type, channels>              vec_type;

  enum {
    generic_type = 0,
    depth        = DataDepth<channel_type>::value,
    channels     = 4,
    fmt          = ((channels-1)<<8) + DataDepth<channel_type>::fmt,
    type         = CV_MAKETYPE(depth, channels)
  };
};

对于cv::DataType<Rect>,此处范型_Tp的类型被编译为int,即value_type类型为Rect,它是模版类cv::DataType<>包装的数据类型的描述。work_type此时也为Rect,表示数据在计算过程中的类型。channel_typeint,表示最小单元数据类型。vec_typecv::Vec<int,4>,它表示单个模版类cv::DataType<>所包装的对象的数据类型组成。在枚举部分,generic_type仍然是0,depth为宏定义CV_32Schannels为4,因为一个Rect数据需要4个单元组成,fmt为0x3069,typeCV_32SC4

3.6 输入和输出

大多数OpenCV的函数都需要数组作为输入参数,或者会返回一个数组,但是在OpenCV内部数组类的数据类型有很多,如cv::Scalarcv::Veccv::Matxcv::Mat等,以及STL提供的std::vector<>等。为了使接口不太复杂,OpenCV定义了类型cv::InputArraycv::OutputArray。实际上这两个类型可以支持上述的任意数据类型。它们的区别是前者是一个常量,不能修改。另外还定义了类型cv::InputOutputArray用于就地的计算任务。

在OpenCV内部实现中我们常看到这些数组类型的使用,尽管我们很少在自己的函数中使用这类参数。当我们使用OpenCV提供的带这类参数函数时,我们可以使用任意的数组类型,如cv::Scalar。另外还有一个重要的函数cv::noArray()会返回一个cv::InputArray的实例,当它作为参数传入的时候表示没有任何输入数据需要被处理。对于一些具有可选输出数组的函数时,当我们不需要任何输出结果时可以传入该实例。

4 工具函数

OpenCV提供了如下系列工具函数用于处理数学计算,测试,错误生成,内存和线程操作和优化等。

函数/宏 描述
cv::alignPtr() 以指定的字节对齐指针
cv::alignSize() 以指定的字节对齐缓存大小
cv::allocate() 分配C语言的数组
cvCeil() 对某个变量向上取整
cv::cubeRoot() 计算某个数的立方根
cv::CV_Assert() 断言,条件不满足时抛出异常,生产和工作环境都有效
cv::CV_DbgAssert() 断言,条件不满足时抛出异常,仅在工作环境生效
cv::deallocate() 释放C语言的数组
cv::error() 提示错误并抛出异常
cv::fastAtan2() 使用反三角函数计算角度
cv::fastFree() 释放缓存对象的内存资源
cv::fastMalloc() 分配一块大小对齐的缓存
cvFloor() 对某个变量向下取整
cv::format() 以STL中的sprintf()函数类似的方式创建STL字符串
cv::getCPUTickCount() 获取CPU时钟的滴答数
cv::getNumThreads() 获取OpenCV当前使用的线程数
cv::getOptimalDFTSize() 在将一个数组传递给函数cv::DFT(),获取该数组的最佳大小
cv::getThreadNum() 获取当前线程的索引
cv::getTickCount() 获取系统的滴答数
cv::getTickFrequency() 获取每秒的tick数
cvIsInf() 检查某个浮点型数是否是无穷大
cvIsNaN() 检查某个浮点型变量是否是无效值
cvRound() 计算某个变量的最接近整数
cv::setNumThreads() 设置OpenCV使用的线程数
cv::setUseOptimized() 启用/禁用代码优化,如SSE2等
cv::useOptimized() 获取代码优化功能的状态

函数cv::alignPtr()定义如下。需要注意的是在某些架构上访问多字节格式数据时,如果访问的地址不能被对象的大小整除,则这个操作无法完成。如从被4整除的地址中读取32位整型数据。尽管在x86等架构上可以通过多次读取最后组装数据,但是这种方式会产生严重的性能成本。

/**
根据范型T的具体类型,对齐ptr指针,并返回对齐后的指针。
其计算公式为(T*)(((size_t)ptr + n+1) & -n)

@param ptr 需要被计数的指针
@param n 需要对齐的大小
*/
template<T> T* cv::alignPtr(T*  ptr, int n = sizeof(T));

函数cv::alignSize()定义如下。

/**
根据指定的元素大小,对齐一个尺寸
其计算公式为(sz + n-1) & -n

@param sz 需要对齐的尺寸
@param n 单位元素大小
*/
size_t cv::alignSize(size_t sz, int n);

函数cv::AutoBuffer::allocate()定义如下。

/**
分配一块可以容纳n个T类型元素的缓存空间
为每个对象都调用默认的构造函数,并把指向第一个元素的地址返回

@param sz 需要分配的内存空间大小
*/
void allocate(size_t _size);

该方式使用示例如下。

cv::AutoBuffer<cv::VideoCapture> buffer;
buffer.allocate(10);

也可以使用定义在头文件cvstd.hpp中的cv::Allocator类相关函数来完成类似的任务,其使用实例如下。

cv::Allocator<cv::VideoCapture> allocator;
allocator.allocate(100);

函数cv::AutoBuffer::deallocate()可以释放通过cv::AutoBuffer::allocate()分配的缓存,同样的也可以使用定义在头文件cvstd.hpp中的cv::Allocator类相关函数来完成类似的任务,函数cv::Allocator::deallocate()定义如下。

/**
释放通过函数cv::allocate()分配的内存空间,会调用数组内每个实例的析构函数
需要注意的是它释放的内存空间大小需要和分配的大小相同

@param ptr 指向需要释放的内存空间地址
@param sz 需要释放的内存空间大小
*/
void deallocate(pointer p, size_t sz);

函数cv::fastAtan2()需要两个浮点型参数xy,它使用反三角公式求y/x表示的角度,其返回值的取值区间为[0.0, 360.0),其定义如下。

// 使用反正切函数求y/x的对应的角度
float cv::fastAtan2(float y, float x);

函数cv::cvCeil()原型如下。如果输出参数x超过32位整型能够表示的范围,则返回值是未定义的。

// 计算不小于x的整数
int cvCeil(float x);

函数cv::cubeRoot()定义如下。如果返回值的正负和参数x相同。

// 返回x的立方根
float cv::cubeRoot(float x );

函数cv::error()定义如下。宏CV_Error()CV_Error_()内部生成一个异常,然后会调用该函数,通常我们不需要直接调用。在生产环境下,该函数会抛出指定的异常,在工作环境中它会故意引发一个违规的内存访问使得在调试时能够查看堆和参数信息。

void cv::error(const cv::Exception& ex);

函数cv::fastFree()释放由函数cv::fastMalloc()创建的内存资源,其定义如下。

void cv::fastFree(void* ptr);

函数cv::fastMalloc()工作原理和malloc()类似,但是它的效率更高,并且返回的缓存大小是对齐的。这意味着当你分配的缓存等于或者超过16个字节时,得到的内存大小会按16字节对齐。

void* cv::fastMalloc(size_t size);

函数cvFloor()用于计算不大于某个浮点型数据的整数,同样的如果传入的参数转换的结果超出了32位整型数能够表示的范围,则返回值是未定义的。

int cvFloor(float x);

函数cv::format()和STL中的sprintf()函数功能类似,但是它不需要调用方传入一个字符缓存,它会构建并返回一个STL字符串对象,这在使用Exception()的构造函数时非常便利,因为该构造函数需要STL字符串类型的参数。其定义如下。

string cv::format(const char* fmt, ... );

函数cv::getCPUTickCount()可以在支持的架构上(包括但是不限于x86架构)直接获取CPU时钟的滴答数。需要注意大多数情况下这个函数的返回值都很难解释,这是因为在多核系统中,一个线程可能在某个核内休眠,却在另外一个核中被唤醒,两处该该函数的调用结果可能会误导我们的判断,甚至说这个结果完全没有意义。因此除非有十足的把握,尽量不要调用这个函数,相反可以调用函数cv::getTickCount()。调用函数cv::getCPUTickCount()对于初始化随机数生成器很有帮助。该函数原型如下。

int64 cv::getCPUTickCount( void ); 

函数cv::getNumThreads()获取OpenCV正在使用的线程数量,其定义如下。

int cv::getNumThreads( void );

在调用函数cv::dft()时,其内部计算变换的算法对传递到该函数的数组参数的大小很敏感。最佳的数组大小应该遵从其内部的一些规定,这些规定较复杂,因此我们自己计算会很麻烦。调用函数cv::getOptimalDFTSize()能够获取最佳的数组大小,它会根据我们传入的参数n,即我们想要传递到函数cv::dft()中的数组大小,计算一个最佳的数组大小并返回。这样OpenCV会创建一个更大的数组,并将额外部分都填充为0。

int cv::getOptimalDFTSize( int n );

如果在编译OpenCV库时候添加了OpenMP的支持,则可以使用函数cv::getThreadNum()获取当前线程的标识,其定义如下。

int cv::getThreadNum( void );

函数cv::getTickCount()获取与一些架构相关的时钟滴答数,时钟滴答的频率也和架构和操作系统相关。每秒的滴答数可以通过函数cv::getTickFrequency()获得。使用函数cv::getTickCount()在大多数场景下比使用函数cv::getCPUTickCount()更加方便。因为它不会受到一些如当前线程具体是在哪个核上工作,以及现代处理器处于能耗控制原因会对CPU频率节流等这些底层问题的影响。该函数定义如下。

int64 cv::getTickCount( void );

函数cv::getTickFrequency()返回系统美秒的滴答数,该值和系统及架构相关,该函数原型如下。

double cv::getTickFrequency( void ); 

函数cvIsInf()用于确定某个浮点型数据是否是正无穷或者负无穷大,如果是则返回1,反之则返回0。

int cvIsInf( double x );

函数cvIsNaN()判断某个浮点型数据是否是无效值,如果是则返回1,反正则返回0。

int cvIsNan( double x ); 

函数cvRound()用于获取最接近某个浮点数的整数,同样的如果转换后的结果超出了32为整型数据的表示范围,则返回的值是未定义的。

int cvRound( double x );

如果在编译OpenCV库时加入了OpenMP支持,则可以使用函数cv::setNumThreads()指定OpenCV在并行OpenMP领域使用的线程数量。默认的线程数量和CPU的逻辑核心相同,如对于一个有4个核心,每个核心有2个超线程的GPU,该默认值为8。如果参数nthreads传入0,则使用的线程数会被重置为默认值。

void cv::setNumThreads( int nthreads ); 

早期版本的OpenCV依赖于外部库(如IPP)来获得如SSE2指令集等高性能优化,在后续的版本中,这部分功能都会被默认迁移到OpenCV内部了,除非你在编译OpenCV时明确的禁用了这些优化。在程序中,我们也可以通过调用函数cv::setUseOptimized()来选择禁用或者开启这些优化。但是需要注意的是不用在程序运行过程中去调用该函数,我们应该在自己应用的高抽象层,确切的知道哪些函数会运行哪些不会时去调用这个函数。其原型如下。

void cv::setUseOptimized( bool on_off );

函数cv::useOptimized()检测当前是否已经启用了性能优化,其原型如下。

bool cv::useOptimized( void );

5 模版结构

到目前为止我们已将介绍了几乎所有基本数据结结构的模版形式。OpenCV 2.1和之后的版本采用了和STL、Boost等库相似的模版元编程风格。这种设计使得代码能够拥有更高的质量和效率,同时也给予了开发者更多自由度。另外它还使得算法能够以一种更抽象的,不依赖C++和OpenCV中具体基础类型的方式去实现。

在我们实例化一个cv::Point对象时,实际上我们实例化的是模版特例化后的cv::Point_<int>对象。其使用的模版也可以使用其他具体类型特例化,只要这些类型支持和int类型一样的运算集合,如加法、减法和乘法等。例如,可以使用类cv::Complex来特例化模版类,甚至你可以使用自己定义的类型。同样的,这些规则和定义对于其他模版类,如cv::Scalar_<>cv::Rect_<>cv::Matx_<>以及cv::Vec_<>也适用。

在实例化这些模版类时,你需要指定范型的具体类型,以及模版的尺寸,常见的模版类及特例化时需要使用的参数如下表。

模版类定义 描述
cv::Point_<Type T> 由两个范型对象组成的点
cv::Rect_<Type T> 由一个顶点,宽和高组成的矩形,类型均为范型T
cv::Vec<Type T, int H> H个范型数据的集合
cv::Matx<Type T, int H, int W> H*W个范型数据的集合
cv::Scalar_<Type T> 四个范型数据的集合,类似于cv::Vec<T, 4>

6 图片和大数组类型

在大数组类型中,最重要的类是cv::Mat,它表示任意维度的稠密数组。这里稠密的意思指的是数组中的每个成员都有对应的内存空间存储数据,例如大多数图片都使用稠密数组的方式存储,即使存在了值为0的像素值,它们仍占据内存空间。与之相对的是稀疏数组,即cv::SparseMat,该类型与稠密数组的区别在于它只会在内存中保存非零的成员。需要注意的是对于稠密的数据,使用稀疏数组消耗的内存空间比稠密数组更大。一个常见的使用稀疏数组的例子是统计直方图,它的大多数成员都是0。

6.1 稠密矩阵

稠密数组中的数据可以被认为是按删格扫描顺序存储的。对于一维数组,其存储的数据是连续的。对于二维数组,数据按行的方式组织,下一行的数据存在该行数据之后。对于三维数据,每个面都是逐行填充,然后再存储下一个面的数据。

每个实例对象包含一个flags属性表示数组内容信息。dims属性表示数组的维度。rowscols属性表示数组的行列数,当数组维度大与2时这两个属性是无效的。data指针属性指向了数据的真实内存地址。refcount属性表示该部分数据的引用计数,它的工作原理和智能指针cv::Ptr<>使用引用计数类似。属性step[]记录了数据的内存布局信息,数组中每个元素可以用如下索引表示。

则数组中每个元素的内存地址可以通过如下公式计算得出。

对于二维稠密数组,这个公式可以简写为如下形式。

cv::Mat中不仅可以存储最基本的数据类型,也可以存储其他数据类型。该实例中的每个元素可以时一个数字或者多个数组,后者被称为多通道数组。稠密数组存储的数据在内存中布局时会考虑内存对齐问题,如对于一个2维3通道数组,每个通道都是32位的浮点数据,这意味着每个元素需要占据12个字节的内存空间,在内存布局时,首先每行数据内部一定是连续的,但是在行间可能会存在一个很小的间隔使得每行的首地址以一定的规则对齐,从而提升数据读取效率。

6.1.1 创建稠密矩阵

你可以通过实例化一个cv::Mat的对象来创建一个数组,但是得到的数组对象是没有大小和数据类型的。或者你也可以使用如下方式来创建数组对象,需要注意的是数据类型同时指定了数据的格式,以及通道数。它可以理解为形如格式CV_{8U,16S,16U,32S,32F,64F}C{1,2,3},其中第一个数字表示的是每个通道的数据类型,而第二个数字表示的是通道数,如CV_32FC3表示3通道的32位浮点型数据。

cv::Mat m;
// 创建一个包含3通道32位浮点型数据的3行10列数组
m.create(3, 10, CV_32FC3);
// 设置其中每个元素的初始值为(1.0,0.0,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));

需要注意的是cv::Mat实例和其真正存储的数据是分离开的,cv::Mat实例仅仅包含数据的一些描述信息,以及指向真是数据的内存地址。其包含的真实数据也是通过类似智能指针的方式管理的,这意味着当你有两个数组对象m和n时,如果你执行了赋值操作m=n,则首先所有与m共享真实数据的实例对应的引用计数会-1,如果其引用计数已经为0,则真实的数据会被释放,内存被回收。然后m的data指针会指向n所包含的真实数据,并从n中读取该数据段段引用计数,将其加1并更新所有共享这部分真实数据的数组对象中的引用计数。最后m对象再从n中更新对于真实数据的描述信息。

下面列举了所有可用的基本数组构造方法。

// 默认构造方法
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);
// 二维数组,指定尺寸,数据类型和默认值的构造方法
cv::Mat(cv::Size sz, int type,
        const Scalar& s);
// 二维数组,指定尺寸,数据类型,使用已有数据的构造方法
cv::Mat(cv::Size sz, int type,
        void* data, size_t step=AUTO_STEP);

// 多维数组,指定尺寸,数据类型的构造方法
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);

下面列举了所有可用的拷贝数组构造方法。

// 拷贝构造函数
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);
// 拷贝另外一个n维矩阵的指定区域,通过数组指定每个维度的范围
cv::Mat(const Mat& mat,
        const cv::Range* ranges);

// 使用另外一个数组的线性代数结构来构造一个新的数组
cv::Mat(const cv::MatExpr& expr);

如果你还在维护2.1版本之前的OpenCV,你可以使用如下方法将过时的数据结构转换成新的数据结构。下面代码中的参数copyData设置为false时将创建矩阵描述信息,并使用已存在的数据。当该参数设置为true时,将会分配一片新的内存空间,并拷贝原始数据。

// 使用CvMat构造数组对象
cv::Mat(const CvMat* old, bool copyData=false);
// 使用IplImage构造数组对象
cv::Mat(const IplImage* old, bool copyData=false);

实际上这些构造函数隐藏了很多额外的操作,特别是它允许表达式混合C++和C数据类型,另外隐式的构造函数会生成需要的C++数据类型。这样,在需要cv::Mat类型的地方简单的传入C结构体的指针也能够使得函数正常工作,这也是参数copyData为什么默认值是false的原因。此外,也有一些转换符号能够将cv::Mat对象转换为CvMat或者IplImage,这些转换也不会拷贝数据。

此外还可以使用一些模版类型来构造cv::Mat对象,这些模版构造函数如下。

// 构造1维数组,使用类型为T,尺寸为N的有限向量
cv::Mat(const cv::Vec<T,n>& vec, bool copyData=true);
// 构造2维数组,使用类型为T,尺寸为m*n的有限矩阵
cv::Mat(const cv::Matx<T,m,n>& vec, bool copyData=true);
// 构造1维数组,使用类型为T的STL向量
cv::Mat(const std::vector<T>& vec, bool copyData=true);

cv::Mat也提供一些静态方法来创建一些特殊的矩阵,如0矩阵、1矩阵和单位矩阵。

// 构建元素全为0的矩阵
cv::Mat::zeros(rows, cols, type);
// 构建元素全为1的矩阵
cv::Mat::ones(rows, cols, type);
// 构建单位矩阵,即该矩阵和某个符合乘法规律的向量结果仍然为原向量
cv::Mat::eye(rows, cols, type);
6.1.2 访问稠密矩阵元素

OpenCV中可以通过位置或者迭代器访问矩阵中的元素。使用位置访问是最简单也是最直接的,类cv::Mat提供了模版函数at<>()及大量有着不同参数组合的变体来访问矩阵的中某个元素,其使用方式如下。

// 访问单通道的矩阵元素
cv::Mat m1 = cv::Mat::eye(10, 10, CV_32FC1);
printf("Element (3,3) is %f\n", m1.at<float>(3,3));

// 访问多通道的矩阵元素
cv::Mat m2 = cv::Mat::eye(10, 10, CV_32FC2);
printf("Element (3,3) is (%f,%f)\n",
m2.at<cv::Vec2f>(3,3)[0], m2.at<cv::Vec2f>(3,3)[1]);

当然你也可以使用更复杂的数据类型来实例化矩阵对象,如使用复数。这里需要注意在构建矩阵实例时,需要传入一个int类型的参数确定基本数据类型,这里基本数据类型是cv::Complexf,但这是编译时的数据结构,可以通过cv::traits::Type<cv::Complexf>::value生成其对应的运行时类型标识。在OpenCV3.3以前,需要使用cv::DataType<cv::Complexf>::type生成该类型。

cv::Mat m = cv::Mat::eye(10, 10, cv::traits::Type<cv::Complexf>::value);
printf("Element (3,3) is %f + i%f\n",
m.at<cv::Complexf>(3,3).re, m.at<cv::Complexf>(3,3).im);

所有可用的模版函数at<>()的变体列举如下。

// 获取一维整型矩阵中的某个元素
M.at<int>(i);
// 获取二维浮点型矩阵中的某个元素
M.at<float>(i, j);
// 获取二维整型矩阵中的某个元素,(pt.x,pt.y)表示二维某个点
M.at<int>(pt);
// 获取三维浮点型矩阵中的某个元素
M.at<float>(i, j, k);
// 获取n维无符号字符型矩阵中的某个元素,idx[]表示n维空间中的某个点
M.at<uchar>(idx);

访问2维矩阵时,通过cv::Mat内部定义的模版函数ptr<>(),可以使用C语音风格的指针访问矩阵内的某一行元素。需要注意的是矩阵是按行存储数据的,因此无法通过该方式获取某一列的数据,稍后会讲到获取某列数据的正确方法。该函数的返回值是指向矩阵构成的最基本元素的指针,也就是说如果矩阵的每个元素类型是CV_32FC3,则该函数的返回值是float*类型的变量。即对于由该数据类型构成的矩阵mtx,通过mtx.ptr<Vec3f>(3)的到的是第4行的第一个元素的第一个通道的数据地址。

使用函数at<>()和指针访问的性能差异取决于编译器的优化。如果优化足够好,则通过函数at<>()访问元素的效率仅仅比通过指针访问的方式差一点,如果编译优化被关闭(如使用在调试环境编译),则其效率将会相对于使用指针的方式差上一个数量级以上。

获取矩阵中某个数据的地址方式除了使用函数ptr<>()外,还可以使用整个数据段的指针即data属性,然后通过保存内部布局信息的step[]属性计算。由于函数at<>()ptr<>(),以及迭代器的存在,这种方式通常不被推荐,但是不得不承认,直接计算地址的方式是最有效率的,特别是当你在处理一个2维以上的矩阵时。

在访问矩阵内部所有元素时,通常我们需要逐行访问,这是因为矩阵内部每行数据在内存中的存储可能是连续的,也可能是不连续的。当然我们也可以使用函数isContinuous()判断内存布局,如果其布局是连续的,那么我们也可以拿到第一个元素的指针,然后再循环遍历。但是这种方式是不推荐的,我们推荐使用后面即将讲到的迭代器方式来访问矩阵内部元素。

连续访问矩阵内部的数据可以使用cv::Mat内置的迭代器机制。该机制是建立在STL内部提供的迭代器机制,但是有一定的差异。OpenCV提供了一对迭代器模版分别用于处理常量矩阵和非常量矩阵,它们分别是cv::MatIterator<>cv::MatConstIterator<>cv::Mat的方法begin()和end()将会返回对应迭代器类型的实例。迭代器内部会处理多维数组和内存不连续布局的情况,我们不需要再处理这部分逻辑。

迭代器在声明时都必须指定矩阵内部的元素类型,下面是一个计算3维3通道矩阵中最长元素(各通道平方和最大)的示例。

int sz[3] = {4, 4, 4};
// 3维3通道,基础数据类型为32位float的矩阵
cv::Mat m(3, sz, CV_32FC3);
// 填充[-1.0, 1.0]的随机数
cv::randu(m, -1.0f, 1.0f);
float max = 0.0f;

cv::MatConstIterator_<cv::Vec3f> it = m.begin<cv::Vec3f>();
while (it != m.end<cv::Vec3f>()) {
    float len2 = (*it)[0]*(*it)[0]+(*it)[1]*(*it)[1]+(*it)[2]*(*it)[2];
    if (len2 > max) {
        max = len2;
    }
    it++;
}
矩阵数组迭代器

OpenCV还提供了矩阵数组迭代器类cv::NAryMatIterator,尽管它不能很好的处理矩阵内部内存不连续布局的问题,但是它能够很方便的对多个矩阵数组同步迭代。它不像普通的矩阵迭代器类返回指向某个元素的实例,而是返回指向某段数据的实例。这段数据被称为一个平面,它可能是任意维度,这取决于被迭代矩阵的维度,它们是相同的。每个平面内部的所有元素在内存中是连续分布的,也就是说我们可以通过数组操作符去访问该平面内的所有元素,或者通过一般遍历方式去访问它们,而不需要在考虑内存间隔的问题。需要注意的是,矩阵数组迭代器只能处理具有相同几何结构的矩阵,也就是说它们的维度和每个维度的具体长度都必须相同。一个简单的使用该类的例子如下。程序SummationOfNDArray

// 创建5*5*5的矩阵,每个元素类型为单通道32位浮点型
const int n_mat_size = 5;
const int n_mat_sz[] = { n_mat_size, n_mat_size, n_mat_size };
cv::Mat n_mat = cv::Mat(3, n_mat_sz, CV_32FC1);

// 使用均匀分布类型随机数填充矩阵,随机数取值区间为[0.0f, 1.0f]
cv::RNG rng = cv::RNG();
rng.fill(n_mat, cv::RNG::UNIFORM, 0.0f, 1.0f);

// 创建数组矩阵迭代器
const cv::Mat* arrays[] = {&n_mat, 0};
// 这里的参数my_planes是用于迭代器保存多个数组当前被迭代的平面数组,即每次迭代
// 后该数组内部的值都会更新
cv::Mat my_planes[1];
cv::NAryMatIterator it = cv::NAryMatIterator(arrays, my_planes);

// 求矩阵中所有元素的和
float s = 0.0f;
for (int p = 0; p < it.nplanes; p++, ++it) {
    // 这里使用it.planes和my_planes等价,均表示多个数组的当前平面组成的数组
    // 由于只迭代了1个数组,因此取索引值0的平面
    s += cv::sum(it.planes[0])[0];
}

在上面的例子中,我们只遍历了单个矩阵,下面是使用数组矩阵迭代器遍历多个矩阵的例子。程序SummationOfNDArrays

// 创建2个5*5*5的矩阵对象
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);

// 使用区间[0,1]的浮点型随机数填充着两个矩阵
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;
for (int p = 0; p < it.nplanes; p++, ++it) {
    // planes[]中每个元素依次为每个被遍历的数组当前平面的元素集合
    s += cv::sum(it.planes[0])[0];
    s += cv::sum(it.planes[1])[0];
}

除了使用函数cv::sum()求某个平面的和之外,还可以使用指针访问平面中的每个元素,并累加求和。此时还需要使用到另外一个cv::NAryMatIterator的实例属性size,它表示每个平面的元素个数,但是需要注意的是它不包含元素的通道数。一个使用指针访问平面内元素并进行数学计算的例子如下。

// 创建3个矩阵
cv::Mat src1, src2;
cv::Mat dst;

// 构建数组矩阵迭代器
const cv::Mat* arrays[] = {&src1, &src2, &dst, 0};
float* ptrs[3];
cv::NAryMatIterator it(arrays, (uchar**)ptrs);

// 使用指针的方式访问元素并执行数学运算
for (size_t i = 0; i < it.nplanes; i++, ++it) {
    for (size_t j = 0; j < it.size; j++) {
        ptrs[2][j] = std::pow(ptrs[0][j], ptrs[1][j]);
    }
}
6.1.3 获取子矩阵

在前面的小节中介绍了如何获取数组中的每个元素,此外类cv::Mat提供了如下一系列的实例方法获取一个矩阵的子矩阵。

函数 描述
m.row( i ); 获取某行数据
m.col( j ); 获取某列数据
m.rowRange( i0, i1 ); 获取从i0行到i1-1行的数据
m.rowRange( cv::Range( i0, i1 ) ); 获取从i0行到i1-1行的数据
m.colRange( j0, j1 ); 获取从j0行到j1-1列的数据
m.colRange( cv::Range( j0, j1 ) ); 获取从j0行到j1-1列的数据
m.diag( d ); 获取距离主对角线距离为d的对角线数据
m( cv::Range(i0,i1), cv::Range(j0,j1) ); 获取由顶点(i0, j0)和(i1-1, j1-1)包围的矩形区域数据
m( cv::Rect(i0,i1,w,h) ); 获取由顶点(i0, j0)和(i0+w-1, j1+h-1)包围的矩形区域数据
m( ranges ); 获取由数组ranges确定的子矩阵数据

需要注意的是使用上述方法获取到子矩阵的时候,得到的类cv::Mat的实例和原矩阵共享数据段,也就是说如果在类似m2 = m.row(3)表达示之后修改m中对应的数据,则m2所包含的数据也会改变。如果确实需要拷贝数据,需要使用函数copyTo()。这种设计的好处是避免了数据拷贝时的性能消耗,这种消耗并不是很低,它和原始矩阵和即将创建的矩阵的大小相关。在通过指定范围截取子矩阵时需要注意该区间是包含起始点,不包含终止点的半闭半开区间。在使用函数diag()截取子矩阵时,如果参数为正,表示向上偏移取对角线,如果为负则表示向下偏移取对角线。最后一个函数是唯一的一个取高维矩阵子矩阵的方法。

6.1.4 矩阵表达式

OpenCV从2.1版本开始引入C++后获得的一个能力就是运算符的重载,这种机制使得我们能够用简单有意义的表达式来完成复杂的操作。在这些运算符的背后OpenCV利用了很多矩阵类自身的特性来完成这些操作。例如矩阵头在需要的时候会被自动创建,工作区数据只有在需要的时候才会分配内存空间,并且在不需要的时候数据区域会自动释放内存,最终计算的结果会被通过运算符“=表达式“放到左侧的矩阵中。需要注意的是这种形式并不是赋值一个cv::Mat实例,尽管表面上看上去似乎是这样,实际上这种形式更像是一个cv::MatExpr,即矩阵表达式。cv::MatExpr的底层机制很复杂,当前我们并不需要了解,可以简单的将其理解为等式右侧的代数符合表达,它的好处是能够移除或者修改一些不必要的表达式,如求矩阵转置的转置,加0矩阵,乘逆矩阵等。最重要的区别是这种矩阵表达式的运算符一定会创建一片新的内存区域用于存储数据,并得到一个新的矩阵实例。

考虑代码m2=m1,这里只是将m2实例的引用指向m1。再考虑代码m2=m1+m0,这里m1+m0是矩阵表达式,将会计算出一个新的矩阵,并分配一片新的内存区域来存储对应数据,将新矩阵的引用赋值给m2

下表列出了可用的矩阵表达式及一些方法的示例,这里出了简单的表达式之外,还例举了一些方法来执行一些如计算转置和逆矩阵等高级操作,其中有些是之前已经遇到的,如cv::Mat::eye()。使用下表的一些表达式的关键在于如果使用一行简洁清晰的代码来应对你实际遇到的计算机视觉问题。

示例 描述
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.inv( method ); 求逆矩阵(默认使用DECOMP_LU)
m0.t(); 求转置矩阵,无数据拷贝
m0>m1; m0>=m1; m0==m1; m0<=m1; m0<m1; 逐元素比较,返回的矩阵元素数据类型为nchar,取值为0或者255
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.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 );
返回类型为type的Nr✖️Nc矩阵,类方法

函数inv()在求逆矩阵时可用指定使用的数学方法。第一种是cv::DECOMP_LU,即使用LU分解,该方法对所有的非奇异矩阵都有效。第二种方式是cv::DECOMP_CHOLESKY,它使用Cholesky分解求逆矩阵,这种方式只对半正定矩阵有效,在处理大矩阵时,其效率明显高于LU分解。最后一种是cv::DECOMP_SVD,他使用奇异值分解(Singular Value Decomposition, SVD)求逆矩阵,在处理奇异和非方阵的矩阵时,它是唯一可用的方法,此时得到的是矩阵的伪逆。

尽管上表中并未包含如cv::norm()cv::mean()cv::sum()等函数,但是你仍可以在矩阵表达式中使用。

6.1.5 饱和转换

在OpenCV中,在执行数学运算时,特别是对无符号类型数据执行减法运算时,会有数据溢出的风险,OpenCV依赖饱和转换(Satruration Casting)的机制来处理这个问题。也就是说OpenCV内部的算法以及对数据的其他操作时会自定的检查数据溢出情况。在数据溢出时,OpenCV的函数会使用该类型最大或者最小的值来替换运算结果,需要注意的是这种操作并不是C语言本身提供的。

另外OpenCV也提供了模版函数cv::saturate_cast<>()使得我们可以指定某个数据类型来对某个计算结果执行饱和转换操作,如下面的示例。

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

在上面的例子中,假定Vxy的值为10,则按照C语言的规则计算的结果将会是-108,显然这已经超出了无符号字符型能够表达的范围,因此饱和转换函数会将计算结果替代为0。

6.1.6 其余的矩阵操作

到目前为止已经介绍了大部分类cv::Mat的操作,剩余的一些无法分到前面所讨论的特定主题下的操作如下表。

示例 描述
m1 = m0.clone(); 深拷贝一个矩阵,会实际拷贝真正的数据段,得到的矩阵是连续的
m0.copyTo( m1 ); 将矩阵m0的内容拷贝到矩阵m1,如果m1指向的数据引用计数变为0会自动释放其内存
等同于m1=m0.clone()
m0.copyTo( m1, mask ); 在上一个函数的基础上,只复制mask指示的区域
m0.convertTo(
m1, type, scale, offset
);
转换矩阵m0中的元素为类型type(如CV_32F),并在此基础上乘以缩放系数scale(默认1),再偏移offset(默认0)并将最后的结果赋值给m1
m0.assignTo( m1, type ); 只能在内部使用,集成在函数convertTo中
m0.setTo( s, mask ); 将矩阵m0中的所有元素赋值为s,如果存在mask,则只操作mask对应的区域(即mask中的非0元素)
m0.reshape( chan, rows ); 改变二维数组的有效形状,参数chan和rows为0的时候表示不需要更改,矩阵的数据不会被拷贝
m0.push_back( s ); 扩展一个由元素s组成的m✖️1的矩阵
m0.push_back( m1 ); 将矩阵m1点数据拷贝到矩阵m0的最后一行后,需要注意矩阵m1和m0必须有相同的列数
m0.pop_back( n ); 移除矩阵m0点末尾n行数据,参数n默认值为1
m0.locateROI( size, offset ); 将矩阵m0的尺寸写入到cv::Size类型的参数size中,如果m0只是一个大矩阵的部分,则还会将其在所属大矩阵中的偏移值写入到参数offset中
m0.adjustROI( t, b, l, r ); 向上扩展t个,向左扩展l个,向下扩展b个,向右扩展r个像素
m0.total(); 矩阵矩阵的元素个数,不包含通道数
m0.isContinuous(); 判断矩阵内部的数据段是否是紧密包装的连续数据
m0.elemSize(); 返回矩阵的元素大小,如3通道的浮点型元素将会返回12字节
m0.elemSize1(); 返回矩阵的基本元素大小,如3通道的浮点型元素会返回4字节
m0.type(); 返回元素类型的标识符,如CV_32FC3
m0.depth(); 返回元素的单通道数据类型的标示符,如CV_32
m0.channels(); 返回元素的通道数
m0.size(); 返回一个cv::Size的数据描述矩阵的大小
m0.empty(); 判断矩阵中是否包含元素

6.2 稀疏矩阵

稀疏矩阵cv::SparseMat用于在某个矩阵内部非零元素占比很小的场景,通常见于高维矩阵中描述直方图等数据,因为在实际的应用的这种场景中大部分数据都是空的。稀疏矩阵仅存储需要展示的数据,因此能够节省大量的内存。但是当稀疏矩阵执行逐元素的计算逻辑时,其效率要低于稠密矩阵。需要注意的是这种计算方式并不一定非常慢,如果能够提前知道哪些操作可以避免能够节约很多计算时间。

稀疏矩阵在很多地方都与稠密矩阵类似,它们具有类似的定义,支持很多相同的操作,能够容纳同样类型的数据。但是在矩阵内部,数据以完全不同的方式组织。类cv::Mat的数据存储方式和C语言的数组类似,数据以连续的方式存储,并且每个元素的地址可以通过元素下标计算得到。但是在类cv::SparseMat中,使用了哈希表的方式来存储非零元素。(实际上当某些元素经过计算后为0时仍然会被存储,如果想要清理这些元素必须手动处理,稍后会介绍函数SparseMat::erase())OpenCV会自动维护该哈希表,当非零元素的数量变得太大不能够执行高效查询时,该哈希表会自动增长。

6.2.1 访问稀疏矩阵内的元素

稀疏矩阵和稠密矩阵之间最大的区别是访问内部元素的方式,稀疏矩阵提供了四种方式,分别为cv::SparseMat::ptr()cv::SparseMat::ref()cv::SparseMat::value()、和cv::SparseMat::find()。方法cv::SparseMat::ptr()包含多个变体,其中最简单的一个如下。

uchar* cv::SparseMat::ptr( int i0, bool createMissing, size_t* hashval=0 );

该方法可以访问一维稀疏数组中的元素,参数i0是需要访问元素的索引。参数createMissing表示如果访问的元素不被包含在稀疏数组中,是否需要创建这个元素。前面已经讲过,稀疏数组底层实现是哈希表,在哈希表中查数据包含计算哈希值和查找和该值关联的列表,通常情况下这个关联的列表会很短,理想情况是只有1个元素。其中计算哈希值是需要耗费性能的,如果已经通过如函数cv::SparseMat::hash()计算过其哈希值,则在下次查找时可以省略这个过程。因此在函数cv::SparseMat::ptr()中第四个参数hashval表示的就是这个哈希值,当其为NULL时会计算这个值,否则使用外部提供的值。

该函数的其他变体还通过提供多个索引参数的方式访问多维数组,甚至还可以通过一个索引数组的方式去访问高维数组。在所有的变体中该函数的返回值都指向类型uchar的指针,需要根据实际情况做类型强转。

函数SparseMat::ref<>()返回指定元素的引用,它接收和函数SparseMat::ptr()相同的参数,该函数是模版函数,其使用示例如下。

a_sparse_mat.ref<float>( i0, i1 ) += 1.0f;

模版函数cv::SparseMat::value<>()SparseMat::ref<>()基本相同,区别在于前者返回的是具体的值,并且该值为常量,禁止修改,而后者返回的是引用。模版函数cv::SparseMat::find<>()和前两个函数也类似,区别在于它返回的是指向特定元素和特定类型的指针,为例代码整洁,能够使用该函数时不要使用函数cv::SparseMat::ptr()。另外该函数的返回值是一个常量,不可更改,因此这两个函数并不总是可以互换的。

除了直接访问外,还可以使用迭代器的方式访问某个元素。和稠密矩阵类似,OpenCV提供了模版迭代器类cv::SparseMa⁠t​Iterator_<>cv::SparseMatConstIterator_<>,同时稀疏矩阵类也提供了成员变量cv::SparseMat::begin<>()cv::SparseMat::end<>()来获取指向第一个元素后末尾元素的迭代器。另外非模版类的稀疏矩阵返回的是非模版类的迭代器类cv::SparseMatIteratorcv::SparseMatConstIterator示例程序PrintSparseArray打印了一个稀疏矩阵中的所有非零元素。

int size[] = {10,10};
cv::SparseMat sm( 2, size, CV_32F );

for( int i=0; i<10; i++ ) {          
    int idx[2];
    idx[0] = size[0] * rand();
    idx[1] = size[1] * rand();
    sm.ref<float>( idx ) += 1.0f;
}

cv::SparseMatConstIterator_<float> it = sm.begin<float>();
cv::SparseMatConstIterator_<float> it_end = sm.end<float>();

for(; it != it_end; ++it) {
    const cv::SparseMat::Node* node = it.node();
    printf(" (%3d,%3d) %f\n", node->idx[0], node->idx[1], *it);
}

在上面的例子中,迭代器的方法node()返回了该迭代器指向的数组中某个元素的节点信息类cv::SparseMat::Node的实例,其定义如下。其中对于同一个元素,成员变量hashval和使用函数SparseMat::ptr()SparseMat::ref()SparseMat::value()SparseMat::find()时指定的哈希值相同。cv::SparseMat::Node定义如下。

struct Node {
    size_t hashval;
    size_t next;
    int idx[cv::MAX_DIM];
};
6.2.2 稀疏矩阵独有函数

稀疏矩阵独有函数如下。

示例 描述
cv::SparseMat sm; 声明一个稀疏矩阵
cv::SparseMat sm( 3, sz, CV_32F ); 创建一个三维的稀疏矩阵
cv::SparseMat sm( sm0 ); 使用已有稀疏矩阵sm0创建新的实例
cv::SparseMat( m0, try1d ); 使用已有稠密矩阵m0创建稀疏矩阵,如果参数try1d为true,则会讲1✖️n或者n✖️1的稠密矩阵压缩为1维稀疏矩阵
cv::SparseMat( &old_sparse_mat ); 使用OpenCV2.1之前的版本中C语言风格的稀疏矩阵CvSparseMat创建新版本的矩阵实例
CvSparseMat* old_sm = (cv::SparseMat *) sm; 将新版本的稀疏矩阵实例转换为OpenCV2.1之前的版本中C语言版本CvSparseMat实例
size_t n = sm.nzcount(); 查询矩阵中非0元素的个数
size_t h = sm.hash( i0 );
size_t h = sm.hash( i0, i1 );
size_t h = sm.hash( i0, i1, i2 );
size_t h = sm.hash( idx );
返回矩阵中某个元素的哈希值
sm.ref<float>( i0 ) = f0;
sm.ref<float>( i0, i1 ) = f0;
sm.ref<float>( i0, i1, i2 ) = f0;
sm.ref<float>( idx ) = f0;
为矩阵中的某个元素赋值
f0 = sm.value<float>( i0 );
f0 = sm.value<float>( i0, i1 );
f0 = sm.value<float>( i0, i1, i2 );
f0 = sm.value<float>( idx );
读取矩阵中某个元素的值
p0 = sm.find<float>(i0);
p0 = sm.find<float>(i0, i1);
p0 = sm.find<float>( i0, i1, i2 );
p0 = sm.find<float>( idx );
读取矩阵中某个元素的值
sm.erase( i0, i1, &hashval );
sm.erase( i0, i1, i2, &hashval );
sm.erase( idx, &hashval );
移除矩阵中的某个元素,如果参数hashval不为NULL,会直接使用该值定位元素
cv::SparseMatIterator<float> it = sm.begin<float>(); 获取指向矩阵第一个元素的迭代器
cv::SparseMatIterator<uchar> it_end = sm.end<uchar>(); 获取指向矩阵最后一个元素的迭代器
6.2.3 模版结构和大数组类型

矩阵类cv::Matcv::Mat_<>同样使用了模版结构,尽管类cv::Mat已经有了表示任意类型数据的能力,但是在构建的时候仍需要显示的声明其元素数据类型。类cv::Mat_<>是类cv::Mat的特例化实现,这样就能简化其方法的使用和成员变量的访问方式。如下面的例子,cv::SparseMat_<>的函数可以不再提供模版方法访问内部元素。

// 定义一个cv::Mat类型的矩阵
cv::Mat m( 10, 10, CV_32FC2 );
// 使用模版函数为内部元素赋值
m.at< Vec2f >( 0, 0 ) = cv::Vec2f( x, y );

// 定义模版类矩阵
cv::Mat_<Vec2f> m( 10, 10 );
// 使用非模版函数为内部元素赋值
m.at( i0, i1 ) = cv::Vec2f( x, y );
m( i0, i1 ) = cv::Vec2f( x, y );

需要注意的是,上述两种声明矩阵的方式,以及使用的.at方法效率时一样的,但是第二种方式被认为是更准确的,因为在某些方法要求传入一个矩阵参数时,这种方式使编译器能够做类型检查。

// 初始化一个矩阵实例
cv::Mat m(10, 10, CV_32FC2 );
// 将其传入到如下函数中
void foo((cv::Mat_<char> *)myMat);
// 则这段代码将会在运行时崩溃,因为参数类型并不匹配

// 初始化一个模版矩阵实例
cv::Mat_<Vec2f> m( 10, 10 );
// 将其传入到如下函数中
void foo((cv::Mat_<char> *)myMat);
// 此时编译器会直接检测到类型不匹配错误

在函数中使用模版可以使函数能够处理不同类型的数据,如前文打印某个稀疏数组中的非零元素代码如下。

void print_matrix( const cv::SparseMat* sm ) {
    cv::SparseMatConstIterator_<float> it = sm.begin<float>();
    cv::SparseMatConstIterator_<float> it_end = sm.end<float>();

    for(; it != it_end; ++it) {
        const cv::SparseMat::Node* node = it.node();
        printf(" (%3d,%3d) %f\n", node->idx[0], node->idx[1], *it );
    }
}

为使该函数更加通用,可以使用模版改造如下。程序PrintMatrixWithTemplate

// 这里使用指针而不是引用的目的是方便调用者使用类型转换来处理本质上
// 具有相同的数据类型的不同引用,如下面的方法calling_function
template <class T> 
void print_matrix( const cv::SparseMat_<T>* sm ) {
    cv::SparseMatConstIterator_<T> it = sm->begin();
    cv::SparseMatConstIterator_<T> it_end = sm->end();

    for(; it != it_end; ++it) {
        // 此处typename关键字告诉编译器cv::SparseMat_<T>::Node是类型而不是变量
        const typename cv::SparseMat_<T>::Node* node = it.node();
        cout <<"( " <<node->idx[0] <<", " <<node->idx[1]
             <<" ) = " <<*it <<endl;
    }
}

void calling_function( void ) {
    ...
    cv::SparseMat sm( ndim, size, CV_32F );
    ...
    print_matrix<float>( (cv::SparseMat_<float>*) &sm );
}

7 小结

在本文中详细覆盖了OpenCV库在操作紧凑集合时所使用到的基本数据类型,这些集合包括点,以及通常用于表示颜色和坐标的小型向量,用于转换坐标和颜色的小型矩阵。同样也讲到了这些数据类型的模版类,以及它们的特例化实现。在进行OpenCV的开发工作是,你将会经常用到这些特例化类。此外,本文还覆盖了一些表示如终止条件的异常处理工具类,表示范围的工具类,以及智能指针类。接下来本文介绍了一些实用函数,这些函数提供了计算机视觉程序经常处理的任务的优化实现。其中重要的例子包含特殊运算和内存管理工具。

在第二部分内容中,本文接着介绍了最重要的数据结构cv::Mat,它可以用于表示矩阵、图片和多维数组。该类可以包含任意基本数据类型,如数字,向量等。对于图片而言,通常包含的元素类型是固定长度的向量,如Vec3b。另外本文还讲到了只存储非0元素的稀疏矩阵cv::SparseMat。最后本文讨论了这两个矩阵的模版结构。