1. 图像的表示
对计算机而言,一幅图像只是一堆亮度各异的点。一副尺寸为M ×N的图像可以用一个M ×N的矩阵来表示,矩阵元素的值表示这个位置上的像素的亮度,一般来说像素值越大表示该点越亮。
一般来说,灰度图用2维矩阵表示,彩色(多通道)图像用3维矩阵(M ×N×3)表示。对于图像显示来说,目前大部分设备都是用无符号8 位整数(类型为CV_8U)表示像素亮度。
2. 常用的Mat构造函数:
Mat::Mat()无参数构造方法;
Mat::Mat(introws, int cols, int type)创建行数为rows,列数为col,类型为type的图像;
Mat::Mat(Sizesize, int type)创建大小为size,类型为type的图像;
Mat::Mat(int rows, int cols, int type, constScalar& s) 创建行数为rows,列数为col,类型为type的图像,并将所有元素初始化为值s;
Mat::Mat(Size size, int type,const Scalar& s)创建大小为size,类型为type的图像,并将所有元素初始化为值s;
Mat::Mat(const Mat& m)将m赋值给新创建的对象,此处不会对图像数据进行复制,m和新对象共用图像数据;
Mat::Mat(int rows, int cols, inttype, void* data, size_t step=AUTO_STEP)创建行数为rows,列数为col,类型为type的图像,此构造函数不创建图像数据所需内存,而是直接使用data所指内存,图像的行步长由step指定。
Mat::Mat(Size size, int type,void* data, size_t step=AUTO_STEP)创建大小为size,类型为type的图像,此构造函数不创建图像数据所需内存,而是直接使用data所指内存,图像的行步长由step指定。
Mat::Mat(const Mat& m, constRange& rowRange, const Range& colRange)创建的新图像为m的一部分,具体的范围由rowRange和colRange指定,此构造函数也不进行图像数据的复制操作,新图像与m共用图像数据;
Mat::Mat(const Mat& m, constRect& roi)创建的新图像为m的一部分,具体的范围roi指定,此构造函数也不进行图像数据的复制操作,新图像与m共用图像数据。
type可以是CV_8UC1,CV_16SC1,…,CV_64FC4等。里面的8U表示8位无符号整数,16S表示16位有符号整数,64F表示64位浮点数(即double类型);C后面的数表示通道数,例如C1表示一个通道的图像,C4表示4个通道的图像,以此类推。
3. create()函数创建对象
例:
Mat M(2,2, CV_8UC3);//构造函数创建图像
M.create(3,2,CV_8UC2);//释放内存重新创建图像
使用create()函数无法设置图像像素的初始值
4. zeros(),ones()和eyes()
例:
Mat Z = Mat::zeros(2,3, CV_8UC1);
cout << "Z = " << endl<< " " << Z << endl;
Mat O = Mat::ones(2, 3, CV_32F);
cout << "O = " << endl<< " " << O << endl;
Mat E = Mat::eye(2, 3, CV_64F);
cout << "E = " << endl<< " " << E << endl;
输出:
5. Vec模板类—可以表示一个向量
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<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
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;
6. 像素值读写
1)at()函数:如果要遍历图像,不推荐使用at()函数。优点是代码可读性高,缺点是效率不是很高。(img.at<type>(x,y))
例程代码:
#include <stdio.h>
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat grayim(600, 800, CV_8UC1);//灰度图
Mat colorim(600, 800, CV_8UC3);//彩色图
//遍历赋值
for (int i = 0; i < grayim.rows; ++i)
{
for (int j =0; j < grayim.cols; ++j)
grayim.at<uchar>(i,j) = (i + j) % 255;
}
for (int i = 0; i < colorim.rows; ++i)
{
for (int j =0; j < colorim.cols; ++j)
{
Vec3bpixel;
pixel[0] = i % 255;//B
pixel[1] = j % 255;//G
pixel[2] = 0;//R
colorim.at<Vec3b>(i,j) = pixel;
}
}
imshow("grayim", grayim);
imshow("colorim", colorim);
waitKey(0);
return 0;
}
2) 迭代器iterator:比较方便的遍历所有元素。(MatIterator_<type>)
例程代码:
#include <stdio.h>
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat grayim(600, 800, CV_8UC1);//灰度图
Mat colorim(600, 800, CV_8UC3);//彩色图
//遍历赋值
MatIterator_<uchar> grayit, grayend;
for (grayit = grayim.begin<uchar>(),grayend = grayim.end<uchar>(); grayit != grayend; ++grayit)
*grayit = rand() % 255;
MatIterator_<Vec3b> colorit, colorend;
for (colorit = colorim.begin<Vec3b>(),colorend = colorim.end<Vec3b>(); colorit != colorend; ++colorit)
{
(*colorit)[0] = rand() % 255;//B
(*colorit)[1] = rand() % 255;//G
(*colorit)[2] = rand() % 255;//R
}
imshow("grayim", grayim);
imshow("colorim", colorim);
waitKey(0);
return 0;
}
3)通过数据指针:使用IpIImage结构时经常使用,通过指针操作来访问像素十分高效,但容易出错。C/C++中的指针操作是不进行类型以及越界检查的,如果指针访问出错,程序运行时有时候可能看上去一切正常,有时候却突然弹出“段错误(segment fault)。(type*p=img.ptr<type>(i))
例程代码:
#include <stdio.h>
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat grayim(600, 800, CV_8UC1);//灰度图
Mat colorim(600, 800, CV_8UC3);//彩色图
//遍历赋值
for (int i = 0; i < grayim.rows; ++i)
{
//获取第i行首像素指针
uchar *p= grayim.ptr<uchar>(i);
for (int j =0; j < grayim.cols; ++j)
p[j] = (i + j) % 255;
}
for (int i = 0; i < colorim.rows; ++i)
{
//获取第i行首像素指针
Vec3b *p= colorim.ptr<Vec3b>(i);
for (int j =0; j < colorim.cols; ++j)
{
p[j][0] = i % 255;//B
p[j][1] = j % 255;//G
p[j][2] = 0;//R
}
}
imshow("grayim", grayim);
imshow("colorim", colorim);
waitKey(0);
return 0;
}
7. 选取图像局部区域以及Mat表达式
1)Mat Mat::row(int i) const
MatMat::col(int j) const
例:取出A矩阵的第i行,将这一行的所有元素都乘以2,然后赋值给第j
A. row(j)= A.row(i)*2;
2)Range():
例:C =A(Range(5, 9), Range(1,3)),前半部分提取行,后半部分提取列,包含了0行0列。
Range类还提供了一个静态方法all(),表示所有的行或者列。
3)Rect(int x, int y, int width, int height):也是从(0,0)开始的
例:
Mat roi2 = img(Rect(10,10,100,100));
取对角线元素:Mat Mat::diag(int d) const
参数d=0时,表示取主对角线;当参数d>0是,表示取主对角线下方的次对角线,如d=1时,表示取主对角线下方,且紧贴主多角线的元素;当参数d<0时,表示取主对角线上方的次对角线。
4)Mat表达式:
l 加法,减法,取负:A+B,A-B,A+s,A-s,s+A,s-A,-A
l 缩放取值范围:A*alpha
l 矩阵对应元素的乘法和除法:A.mul(B),A/B,alpha/A
l 矩阵乘法:A*B(注意此处是矩阵乘法,而不是矩阵对应元素相乘)
l 矩阵转置:A.t()
l 矩阵求逆和求伪逆:A.inv()
l 矩阵比较运算:A cmpop B,A cmpop alpha,alpha cmpop A。此处cmpop可以是>,>=,==,!=,<=,<。如果条件成立,则结果矩阵(8U类型矩阵)的对应元素被置为255;否则置0。
l 矩阵位逻辑运算:A logicop B,A logicop s,s logicop A,~A,此处logicop可以是&,|和^。
l 矩阵对应元素的最大值和最小值:min(A, B),min(A, alpha),max(A, B),max(A, alpha)。
l 矩阵中元素的绝对值:abs(A)
l 叉积和点积:A.cross(B),A.dot(B)
例程:
#include <stdio.h>
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat A = Mat::eye(4, 4, CV_32SC1);
Mat B = A * 3 + 1;
Mat C = B.diag(0) + B.col(1);
Mat D = B.diag(0);//取对角线,0主对角线,>0取主对角线下方的次对角线,<0取主对角线上方的次对角线
Mat E = B.col(1);//有0、1、2、3列,这里第1列
Mat F = B.row(1);
Mat G = B(Range(1, 3), Range::all());
Mat H = G(Range::all(), Range(1, 3));
Mat I = B(Rect(0, 0, 4, 4));//(x,y,width,height)
Mat J(B, Rect(1, 1, 3, 3));//最前3行3列,与Range不同
cout << "A="<< A << endl << endl;
cout << "B="<< B << endl << endl;
cout << "C="<< C << endl << endl;
cout << "C.*diag(B)=" << C.dot(B.diag(0)) << endl << endl;
cout << "D="<< D << endl << endl;
cout << "E="<< E << endl << endl;
cout << "F="<< F << endl << endl;
cout << "G="<< G << endl << endl;
cout << "H="<< H << endl << endl;
cout << "I="<< I << endl << endl;
cout << "J="<< J << endl << endl;
system("pause");
return 0;
}
结果:
8. Mat_类
它是对Mat类的一个包装。由于Mat类需要不停地指定数据类型,十分繁琐且容易出错,因此出现了Mat_类,可以在变量声明时确定元素的类型,访问元素时不再需要指定元素类型。
例:MatM(600, 800, CV_8UC1);
Mat_<uchar>M1 =(Mat_<uchar>&)M;
例程:
#include <stdio.h>
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
Mat M(600, 800, CV_8UC1);
//for(int i = 0; i < M.rows; ++i)
//{
// //获取第i行首像素指针
// uchar *p = M.ptr<uchar>(i);
// for (int j = 0; j < M.cols; ++j)
// {
// double d1 = (double)((i + j) % 255);
// M.at<uchar>(i, j) = d1;
// // 下面代码错误,应该使用 at<uchar>()
// // 但编译时不会提醒错误
// // 运行结果不正确,d2不等于d1
// double d2 = M.at<double>(i, j);
// }
//}
Mat_<uchar>M1 = (Mat_<uchar>&)M;
for (int i = 0; i < M1.rows; ++i)
{
//不需指定元素类型,语句简洁
uchar *p= M1.ptr(i);
for (int j =0; j < M1.cols; ++j)
{
double d1= (double)((i + j) % 255);
M.at<uchar>(i,j) = d1;
//直接用Matlab风格的矩阵元素读写,简洁
M1(i, j) = d1;
double d2= M1(i, j);
}
}
system("pause");
return 0;
}
9. Mat与IpIImage和CvMat的转换
1)Mat转为IpIImage和CvMat格式:
以前写的函数定义:void mycvOldFunc(IplImage * p, ...);
a.转为IpIImage:
Mat img(Size(320, 240), CV_8UC3);
...
IplImage iplimg = img;//转为IplImage结构
mycvOldFunc(&iplimg, ...);//对iplimg取地址
b.如果要转为CvMat类型,操作类似:
CvMat cvimg=img;//转为CvMat
需要特别注意的是,类型转换后,IplImage和CvMat与Mat共用同一矩阵数据,而IplImage和CvMat没有引用计数功能,如果上例中的img中数据被释放,iplimg和cvimg也就失去了数据。
IpIImage和CvMat格式转为Mat格式:
Mat类有两个构造函数,可以实现IplImage和CvMat到Mat的转换。这两个函数都有一个参数copyData。如果copyData的值是false,那么Mat将与IplImage或CvMat共用同一矩阵数据;如果值是true,Mat会新申请内存,然后将IplImage或CvMat的数据复制到Mat的数据区。
Mat::Mat(const CvMat* m, bool copyData=false)
Mat::Mat(const IplImage* img, boolcopyData=false)
例子代码如下:
IplImage * iplimg= cvLoadImage("lena.jpg");
Mat im(iplimg, true);
10.读写图像文件
1)读取图像:Mat imread(const string&filename, int flags=1 )
flag>0,该函数返回3通道图像,如果磁盘上的图像文件是单通道的灰度图像,则会被强制转为3通道;
flag=0,该函数返回单通道图像,如果磁盘的图像文件是多通道图像,则会被强制转为单通道;
flag<0,则函数不对图像进行通道转换。
2)写图像文件:
boolimwrite(const string& filename, InputArray image, const vector<int>¶ms=vector<int>())
imwrite()函数的第三个参数params可以指定文件格式的一些细节信息。这个参数里面的数值是跟文件格式相关的:
l JPEG:表示图像的质量,取值范围从0到100。数值越大表示图像质量越高,当然文件也越大。默认值是95。
l PNG:表示压缩级别,取值范围是从0到9。数值越大表示文件越小,但是压缩花费的时间也越长。默认值是3。
l PPM,PGM或PBM:表示文件是以二进制还是纯文本方式存储,取值为0或1。如果取值为1,则表示以二进制方式存储。默认值是1。
3)Canny边缘检测算子:
void cvCanny( const CvArr* image, CvArr*edges, double threshold1, double threshold2, int aperture_size=3 )
image
单通道输入图像.
edges
单通道存储边缘的输出图像
threshold1
第一个阈值
threshold2
第二个阈值
aperture_size
Sobel 算子内核大小 (见 cvSobel).
函数 cvCanny 采用 CANNY 算法发现输入图像的边缘而且在输出图像中标识这些边缘。threshold1和threshold2 当中的小阈值用来控制边缘连接,大的阈值用来控制强边缘的初始分割。
§ 注意事项:cvCanny只接受单通道图像作为输入。
11. 读写视频
1)读视频
VideoCapture既可以从视频文件读取图像,也可以从摄像头读取图像。
#include <stdio.h>
#include <iostream>
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int main(int argc, char* argv[])
{
//打开视频文件
VideoCapture cap("c00.avi");
if (!cap.isOpened())
{
cerr << "Can not open a camera or file." << endl;
return -1;
}
Mat edges;
namedWindow("edges", 1);//创建窗口
for (;;)
{
Matframe;
cap >> frame;//从cap中读取一帧,存到frame中
if(frame.empty())
break;
cvtColor(frame, edges, CV_BGR2GRAY);//转为灰度图
Canny(edges, edges, 0, 30, 3);//进行边缘提取
imshow("edges", edges);
if(waitKey(30) >= 0)//等待30秒,如果按键则退出循环
break;
}
return 0;
}
2)写视频
写视频需要在创建视频时设置一系列参数,包括:文件名,编解码器,帧率,宽度和高度等。编解码器使用四个字符表示,可以是CV_FOURCC('M','J','P','G')、CV_FOURCC('X','V','I','D')及CV_FOURCC('D','I','V','X')等。如果使用某种编解码器无法创建视频文件,请尝试其他的编解码器。
另外需要注意:待写入的图像尺寸必须与创建视频时指定的尺寸一致。