文章目录
目录
core模块核心功能
1Mat - 基本图像容器
1.1.1 Mat
某些时候你仍会想拷贝矩阵本身(不只是信息头和矩阵指针),这时可以使用函数 clone() 或者 copyTo() 。
Mat F = A.clone();
Mat G;
A.copyTo(G);
- OpenCV函数中输出图像的内存分配是自动完成的(如果不特别指定的话)。
- 使用OpenCV的C++接口时不需要考虑内存释放问题。
- 赋值运算符和拷贝构造函数( ctor )只拷贝信息头。
- 使用函数 clone()或者 copyTo()来拷贝一副图像的矩阵。
1.1.2存储方法
RGB颜色空间是最常用的一种颜色空间,它的基色是红色、绿色和蓝色,有时为了表示透明颜色也会加入第四个元素 alpha (A)。
- RGB是最常见的,这是因为人眼采用相似的工作机制,它也被显示设备所采用。
- HSV和HLS把颜色分解成色调、饱和度和亮度/明度。这是描述颜色更自然的方式,比如可以通过抛弃最后一个元素,使算法对输入图像的光照条件不敏感。
- YCrCb在JPEG图像格式中广泛使用。
- CIE Lab*是一种在感知上均匀的颜色空间,它适合用来度量两个颜色之间的 距离 。
最小的数据类型是 char ,占一个字节或者8位,可以是无符号型(0到255之间)或有符号型(-127到+127之间)。尽管使用三个 char 型元素已经可以表示1600万种可能的颜色(使用RG颜色空间),但若使用float(4字节,32位)或double(8字节,64位)则能给出更加精细的颜色分辨能力。
1.1.3创建一个Mat对象
可以通过 Mat 的运算符 << 来实现看实际值。这只对二维矩阵有效。
Mat 不但是一个很赞的图像容器类,它同时也是一个通用的矩阵类,所以可以用来创建和操作多维矩阵。
Mat构造函数
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
对于二维多通道图像,首先要定义其尺寸,即行数和列数。然后,需要指定存储元素的数据类型以及每个矩阵点的通道数。 CV_8UC3 表示使用8位的 unsigned char 型,每个像素由三个元素组成三通道。预先定义的通道数可以多达四个。 ==Scalar 是个short型vector。==指定这个能够使用指定的定制化值来初始化矩阵。当然,如果你需要更多通道数,你可以使用大写的宏并把通道数放在小括号中。
int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));//第一个是多维的维度
Create() function: 函数
M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " " << M << endl << endl;
这个创建方法不能为矩阵设初值,它只是在改变尺寸时重新为矩阵数据开辟内存。
MATLAB形式
Mat E = Mat::eye(4, 4, CV_64F); //单位阵
cout << "E = " << endl << " " << E << endl << endl;
Mat O = Mat::ones(2, 2, CV_32F); //全部为1
cout << "O = " << endl << " " << O << endl << endl;
Mat Z = Mat::zeros(3,3, CV_8UC1);//全部为0
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;
使用 clone()或者 copyTo()
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl << endl;
1.1.4 格式化打印
调用函数 randu()来对一个矩阵使用随机数填充,需要指定随机数的上界和下界:
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));
cout << "R(default)="<<endl<<R<< endl << endl;//默认方式
cout << "R(python)="<<endl<< format(R,"python") << endl << endl;//Python
cout << "R(csv)="<<endl<<format(R,"csv")<<endl<<endl;//逗号分隔
cout << "R(numpy)="<< endl << format(R,"numpy")<<endl<<endl;//Numpy格式
cout << "R(c)="<< endl << format(R,"C") << endl << endl;//C语言
1.1.5打印其它常用项目
Point2f P(5, 1);//2维点
cout << "Point (2D) = " << P << endl << endl;
Point3f P3f(2, 6, 7);//3维点
cout << "Point (3D) = " << P3f << endl << endl;
vector<float> v;//基于cv::Mat的std::vector
v.push_back( (float)CV_PI); v.push_back(2); v.push_back(3.01f);
cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;
vector<Point2f> vPoints(20);//std::vector点
for (size_t E = 0; E < vPoints.size(); ++E)
vPoints[E] = Point2f((float)(E * 5), (float)(E % 7));
cout << "A vector of 2D Points = " << vPoints << endl << endl;
2 OpenCV如何扫描图像、利用查找表和计时
2.1 测试用例
有时候,仅用这些颜色的一小部分,就足以达到同样效果。常用的一种方法是 颜色空间缩减 。其做法是:将现有颜色空间值除以某个输入值,以获得较少的颜色数。例如,颜色值0到9可取为新值0,10到19可取为10,以此类推。
uchar (无符号字符,即0到255之间取值的数)类型的值除以 int 值,结果仍是 char 。因为结果是char类型的,所以求出来小数也要向下取整。
简单的颜色空间缩减算法就可由下面两步组成:一、遍历图像矩阵的每一个像素;二、对像素应用上述公式。
对于较大的图像,有效的方法是预先计算所有可能的值,然后需要这些值的时候,利用查找表直接赋值即可。查找表是一维或多维数组,存储了不同输入值所对应的输出值,其优势在于只需读取、无需计算。
OpenCV提供了两个简便的可用于计时的函数 getTickCount()和 getTickFrequency()。第一个函数返回你的CPU自某个事件(如启动电脑)以来走过的时钟周期数,第二个函数返回你的CPU一秒钟所走的时钟周期数。
2.2图像矩阵是如何存储在内存之中的
图像矩阵的大小取决于我们所用的颜色模型,确切地说,取决于所用通道数。
子列的通道顺序是反过来的:BGR而不是RGB。因为内存足够大,可实现连续存储,因此,图像的各行就能一行一行地连接起来,形成一个长行。连续存储有助于提升图像扫描速度,可以使用 isContinuous()来去判断矩阵是否是连续存储的.
2.3高效的方法 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则表明对象里面无输入,所以这是一种简单的检查图像是否被成功读入的方法。但是这种方法编写的代码可读性方面差,性能表现上并不明显优于前一种。
2.4 迭代法 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)里的值。
2.5 通过相关返回值的On-the-fly地址计算
它的基本用途是要确定你试图访问的元素的所在行数与列数。它本来是被用于获取或更改图像中的随机元素。不推荐被用来进行图像扫描。
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;
}
该函数输入为数据类型及需求元素的坐标,返回的是一个对应的值-如果用 get 则是constant,如果是用 set 、则为non-constant.
为避免反复输入数据类型和at带来的麻烦和浪费的时间,OpenCV 提供了:basicstructures:Mat_ data type. 它同样可以被用于获知矩阵的数据类型,你可以简单利用()操作返回值来快速获取查询结果.可以利用 at()函数来用同样速度完成相同操作. 它仅仅是为了让懒惰的程序员少写点 >_< .
2.6 核心函数LUT(The Core Function)
是最被推荐的用于实现批量图像元素查找和更该操作图像方法。OpenCV 提供里一个函数直接实现给定值的替换,并不需要你自己扫描图像,就是:operationsOnArrays:LUT() ,一个包含于core module的函数.
Mat lookUpTable(1, 256, CV_8U);//首先我们建立一个mat型用于查表
uchar* p = lookUpTable.data;
for( int i = 0; i < 256; ++i)
p[i] = table[i];
LUT(I, lookUpTable, J);//然后我们调用函数 (I 是输入 J 是输出)
尽量使用 OpenCV 内置函数. 调用LUT 函数可以获得最快的速度. 喜欢使用指针的方法来扫描图像,迭代法是一个不错的选择,不过速度上较慢。
3 矩阵的掩码操作
矩阵的掩码操作很简单。其思想是:==根据掩码矩阵(也称作核)重新计算图像中每个像素的值。==掩码矩阵中的值表示近邻像素值(包括该像素自身的值)对新像素值有多大影响。从数学观点看,我们用自己设置的权值,对像素邻域内的值做了个加权平均。
虽然这两种形式是完全等价的,但在大矩阵情况下,下面的形式看起来会清楚得多。一种方法是用基本的像素访问方法,另一种方法是用 filter2D 函数。
3.1基本方法
void Sharpen(const Mat& myImage,Mat& Result)
{
CV_Assert(myImage.depth() == CV_8U); // 仅接受uchar图像,若为false则跑出错误
Result.create(myImage.size(),myImage.type());//创建与输入相同大小类型的输出图像
const int nChannels = myImage.channels();
for(int j = 1 ; j < myImage.rows-1; ++j)
{
const uchar* previous = myImage.ptr<uchar>(j - 1);
const uchar* current = myImage.ptr<uchar>(j );
const uchar* next = myImage.ptr<uchar>(j + 1);
uchar* output = Result.ptr<uchar>(j);
for(int i= nChannels;i < nChannels*(myImage.cols-1); ++i)
{
*output++ = saturate_cast<uchar>(5*current[i]
-current[i-nChannels] - current[i+nChannels] - previous[i] - next[i]);
}
}
Result.row(0).setTo(Scalar(0));
Result.row(Result.rows-1).setTo(Scalar(0));
Result.col(0).setTo(Scalar(0));
Result.col(Result.cols-1).setTo(Scalar(0));
}
在图像的边界上,上面给出的公式会访问不存在的像素位置(比如(0,-1))。因此我们的公式对边界点来说是未定义的。一种简单的解决方法,是不对这些边界点使用掩码,而直接把它们设为0。
Result.row(0).setTo(Scalar(0)); // 上边界
Result.row(Result.rows-1).setTo(Scalar(0)); // 下边界
Result.col(0).setTo(Scalar(0)); // 左边界
Result.col(Result.cols-1).setTo(Scalar(0)); // 右边界
3.2filter2D函数
OpenCV也有个用到了滤波器掩码(某些场合也称作核)的函数。
Mat kern = (Mat_<char>(3,3) << 0, -1, 0,//定义一个表示掩码的 Mat 对象:
-1, 5, -1,
0, -1, 0);
filter2D(I, K, I.depth(), kern );//调用 filter2D 函数,参数包括输入、输出图像以及用到的核
它还带有第五个可选参数——指定核的中心,和第六个可选参数——指定函数在未定义区域(边界)的行为。代码更加清晰简洁、通常比 自己实现的方法速度更快。