常用数据类型定义:
cv::Size(cols, rows);
cv::Size(width, height);
cv::Scalar(gray)
cv::Scalar(blue, green, red)
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;
typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;
typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;
图像矩阵在内存中的存储
图像矩阵的大小取决于我们所用的颜色模型,确切地说,取决于所用通道数。
灰度图像矩阵:
RGB颜色模型的矩阵:
OpenCV中子列的通道顺序是反过来的:BGR而不是RGB。
很多情况下,因为内存足够大,可实现连续存储,因此,图像中的各行就能一行一行地连接起来,形成一个长行。连续存储有助于提升图像扫描(遍历)速度,我们可以使用 isContinuous()
来去判断矩阵是否是连续存储的。
矩阵基本操作
全零矩阵
CV_NODISCARD_STD static MatExpr Mat::zeros(int rows, int cols, int type);
CV_NODISCARD_STD static MatExpr Mat::zeros(Size size, int type);
CV_NODISCARD_STD static MatExpr Mat::zeros(int ndims, const int* sz, int type);
//not recommended
全一矩阵
CV_NODISCARD_STD static MatExpr Mat::ones(int rows, int cols, int type);
CV_NODISCARD_STD static MatExpr Mat::ones(Size size, int type);
CV_NODISCARD_STD static MatExpr Mat::ones(int ndims, const int* sz, int type);
//not recommended
单位矩阵
CV_NODISCARD_STD static MatExpr Mat::eye(int rows, int cols, int type);
CV_NODISCARD_STD static MatExpr Mat::eye(Size size, int type);
矩阵转置
MatExpr Mat::t() const;
求逆矩阵
MatExpr Mat::inv(int method=DECOMP_LU) const;
逗号式分隔创建矩阵
常用于自定义卷积核
template<typename _Tp> inline
Mat_<_Tp>::Mat_(int _rows, int _cols)
: Mat(_rows, _cols, traits::Type<_Tp>::value)
{
}
template<typename _Tp> inline
Mat_<_Tp>::Mat_(int _rows, int _cols, const _Tp& value)
: Mat(_rows, _cols, traits::Type<_Tp>::value)
{
*this = value;
}
template<typename _Tp> inline
Mat_<_Tp>::Mat_(Size _sz)
: Mat(_sz.height, _sz.width, traits::Type<_Tp>::value)
{}
template<typename _Tp> inline
Mat_<_Tp>::Mat_(Size _sz, const _Tp& value)
: Mat(_sz.height, _sz.width, traits::Type<_Tp>::value)
{
*this = value;
}
Mat a=(Mat_<int>(2,2)<<1,2,3,4);
Mat b=(Mat_<double>(Size(2,2))<<1,2,3,4);
注意括号的位置。
给出的数据类型必须是基本数据类型,如int
,double
。不能是CV_32F
等。
通过ptr与at函数遍历矩阵
Mat a(Size(2560, 1440), CV_8UC3);
for(int i=0; i<a.rows; i++){
for(int j=0; j<a.cols; j++){
a.ptr(i,j)[0]=0;
a.ptr(i,j)[1]=0;
a.ptr(i,j)[2]=255;
}
}
for(int i=0; i<a.rows; i++){
for(int j=0; j<a.cols; j++){
a.ptr<Vec3b>(i,j)[0]=0;
a.ptr<Vec3b>(i,j)[1]=0;
a.ptr<Vec3b>(i,j)[2]=255;
}
}
for(int i=0; i<a.rows; i++){
for(int j=0; j<a.cols; j++){
a.at<Vec3b>(i,j)[0]=0;
a.at<Vec3b>(i,j)[1]=0;
a.at<Vec3b>(i,j)[2]=255;
}
}
- 用
ptr
访问可以不加Vec
类型 - 用
at
访问必须加Vec
类型,at
访问比ptr
略微慢一些
通过迭代器遍历矩阵(easy but very very slow)
Mat a(Size(2560, 1440), CV_8UC3);
for(auto iter=a.begin<Vec3b>(); iter!=a.end<Vec3b>(); iter++){
iter[0]=255;
iter[1]=0;
iter[2]=0;
}
高性能法——使用经典的C风格运算符[](指针)
Efficient Way
说到性能,推荐的效率最高的查找表赋值方法,还是经典的C风格运算符[](指针)访问:
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
int channels = I.channels();
int nRows = I.rows * channels;
int nCols = I.cols;
if (I.isContinuous())
{
nCols *= nRows;
nRows = 1;
}
int i,j;
uchar* p;
for( i = 0; i < nRows; ++i)
{
p = I.ptr<uchar>(i);
for ( j = 0; j < nCols; ++j)
{
p[j] = table[p[j]];
}
}
return I;
}
我们获取了每一行开始处的指针,然后遍历至该行末尾。如果矩阵是以连续方式存储的,我们只需请求一次指针、然后一路遍历下去就行。彩色图像的情况有必要加以注意:因为三个通道的原因,我们需要遍历的元素数目也是3倍。
另外一种方法实现遍历功能,就是使用 data
。
data
会从 Mat
中返回指向矩阵第一行第一列的指针。
注意如果该指针为
NULL
则表明对象里面无输入,所以这是一种简单的检查图像是否被成功读入的方法。
当矩阵是连续存储时,我们就可以通过遍历 data
来扫描整个图像。例如,一个灰度图像,其操作如下:
uchar* p = I.data;
for( unsigned int i =0; i < ncol*nrows; ++i)
*p++ = table[*p];
这回得出和前面相同的结果。但是这种方法编写的代码可读性方面差,并且进一步操作困难。同时,在实际应用中,该方法的性能表现上并不明显优于前一种(因为现在大多数编译器都会对这类操作做出优化)。
迭代器法
The iterator (safe) method
相比于指针遍历,迭代法则被认为是一种以更安全的方式来实现这一功能。在迭代法中,所需要做的仅仅是获得图像矩阵的begin
和end
,然后增加迭代直至从begin
到end
。将*
操作符添加在迭代指针前,即可访问当前指向的内容。
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
const int channels = I.channels();
switch(channels)
{
case 1:
{
MatIterator_<uchar> it, end;
for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
*it = table[*it];
break;
}
case 3:
{
MatIterator_<Vec3b> it, end;
for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
{
(*it)[0] = table[(*it)[0]];
(*it)[1] = table[(*it)[1]];
(*it)[2] = table[(*it)[2]];
}
}
}
return I;
}
对于彩色图像中的一行,每列中有3个uchar
元素,这可以被认为是一个小的包含uchar
元素的vector
,在OpenCV中用 Vec3b
来命名。
如果要访问第n个子列,我们只需要简单的利用[]
来操作就可以。需要指出的是,OpenCV的迭代在扫描过一行中所有列后会自动跳至下一行,所以说如果在彩色图像中如果只使用一个简单的 uchar
而不是 Vec3b
迭代的话就只能获得蓝色通道(B)
里的值。
通过指定On-the-fly地址查找
通过双层for循环直接指定行列来遍历。
事实上这个方法并不推荐被用来进行图像扫描,它本来是被用于获取或更改图像中的随机元素。它的基本用途是要确定你试图访问的元素的所在行数与列数。在前面的扫描方法中,我们观察到知道所查询的图像数据类型是很重要的。这里同样的你得手动指定好你要查找的数据类型。
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
const int channels = I.channels();
switch(channels)
{
case 1:
{
for( int i = 0; i < I.rows; ++i)
for( int j = 0; j < I.cols; ++j )
I.at<uchar>(i,j) = table[I.at<uchar>(i,j)];
break;
}
case 3:
{
Mat_<Vec3b> _I = I;
for( int i = 0; i < I.rows; ++i)
for( int j = 0; j < I.cols; ++j )
{
_I(i,j)[0] = table[_I(i,j)[0]];
_I(i,j)[1] = table[_I(i,j)[1]];
_I(i,j)[2] = table[_I(i,j)[2]];
}
I = _I;
break;
}
}
return I;
}
在 debug
模式下,它会检查你的输入坐标是否有效或者超出范围。如果坐标有误,则会输出一个标准的错误信息。
在 release
模式下,和 高性能法(the efficient way) 相比,它们之间的区别仅仅是 On-the-fly方法 对于图像矩阵的每个元素,都会获取一个新的行指针,通过该指针和[]
操作来获取列元素。
核心函数LUT
The Core Function
最被推荐的用于实现批量图像元素查找和更该操作图像方法。
在图像处理中,对于一个给定的值,将其替换成其他的值是一个很常见的操作,OpenCV 提供里一个函数直接实现该操作,并不需要你自己扫描图像,就是:operationsOnArrays:LUT() <lut>
,一个包含于core module
的函数。
/** @brief Performs a look-up table transform of an array.
The function LUT fills the output array with values from the look-up table. Indices of the entries
are taken from the input array. That is, the function processes each element of src as follows:
\f[\texttt{dst} (I) \leftarrow \texttt{lut(src(I) + d)}\f]
where
\f[d = \fork{0}{if \(\texttt{src}\) has depth \(\texttt{CV_8U}\)}{128}{if \(\texttt{src}\) has depth \(\texttt{CV_8S}\)}\f]
@param src input array of 8-bit elements.
@param lut look-up table of 256 elements; in case of multi-channel input array, the table should
either have a single channel (in this case the same table is used for all channels) or the same
number of channels as in the input array.
@param dst output array of the same size and number of channels as src, and the same depth as lut.
@sa convertScaleAbs, Mat::convertTo
*/
CV_EXPORTS_W void LUT(InputArray src, InputArray lut, OutputArray dst);
首先我们建立一个mat
型用于查表:
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.data;
for( int i = 0; i < 256; ++i)
p[i] = table[i];
然后我们调用函数 (I
是输入, J
是输出):
LUT(I, lookUpTable, J);
性能分析
OpenCV提供了两个简便的可用于计时的函数 cv::getTickCount()
和 cv::getTickFrequency()
。第一个函数返回你的CPU自某个事件(如启动电脑)以来走过的时钟周期数,第二个函数返回你的CPU一秒钟所走的时钟周期数。这样,我们就能轻松地以秒为单位对某运算计时:
double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
用一个(2560 X 1600)的彩色图片。进行数百次测试的平均值:
遍历方法 | 平均用时 |
---|---|
Efficient Way |
79.4717 milliseconds |
Iterator |
83.7201 milliseconds |
On-The-Fly RA |
93.7878 milliseconds |
LUT function |
32.5759 milliseconds |
我们可以得出一些结论:
- 尽量使用 OpenCV 内置函数。调用LUT 函数可以获得最快的速度。这是因为OpenCV库可以通过英特尔线程架构启用多线程。
- 当然,如果你喜欢使用指针的方法来扫描图像,迭代法是一个不错的选择,不过速度上较慢。
- 在debug模式下使用on-the-fly方法扫描全图是一个最浪费资源的方法,在release模式下它的表现和迭代法相差无几,但是从安全性角度来考虑,迭代法是更佳的选择。