工欲善其事,必先利其器。我们先了解可能用到的相关知识。主要包含:opencv,相关模型。
这是本项目的第二篇文章。
第一篇文章,主要介绍项目的任务和实验环境,点击阅读
第三篇文章,主要介绍人脸检测haar+adaboost的原理,点击阅读。
第四篇文章,主要介绍PCA降维和人脸识别的原理,点击阅读。
第五篇文章,主要给出相关的代码,点击阅读。
本实验主要是基于opencv实现的,了解opencv也就是重中之重了。学习opencv最好的地方当然是opencv官网,当然囿于英语阅读的速度和方便性,我把有关项目的重要信息摘要给大家。
OpenCV是一个计算机视觉库,由C和C++编写,包含着大量的图像处理函数,其拥有500多个函数,支持跨平台使用,同时也有免费、速度快和使用方便的优点。
下图是opencv的主体架构图:
OpenCV的具体应用:扫描对象的对齐、医学图像去噪、图像中的物体分析、安全和入侵检测系统、自动监视、产品质量检测系统、摄像机标定、军事应用、无人飞行器、无人汽车和无人水下机器人。
1.矩阵的使用与操作:创建删除,初始化,访问矩阵元素,矩阵或其元素的加减乘除,特征值的求解等等
2.GUI命令:创建窗口,显示图片,等待按键等等
3.图像的使用与操作:读写图像,图像转换,图像处理函数等等
4.视频的使用与操作:打开视频/摄像头,捕捉帧,保存帧
具体如下:
改变颜色空间、图像几何变换、图像阈值、图像平滑、形态转换、canny边缘检测、轮廓检测、直方图、傅里叶变换、模板匹配、图像分割、分水岭算法、特征检测、特征匹配、相机校准、姿态估计等等操作
读取图像
cv::imread()
//其中第一个参数是图像的路径,第二个参数制定了读取的方式。
IMREAD_COLOR: 加载彩色图像。任何图像的透明度都会被忽视。它是默认标志。
IMREAD_GRAYSCALE:以灰度模式加载图像
IMREAD_UNCHANGED:加载图像,包括alpha通道
除了这三个标志,你可以分别简单地传递整数1、0或-1。
//example
cv::imread("../home/picture/1.png",cv::IMREAD_GRAYSCALE)
显示图像
cv::imshow()。第一个参数是窗口名称,它是一个字符串。第二个参数是我们的对象。你可以根据需要创建任意多个窗口,但可以使用不同的窗口名称。
cv::waitKey()。是一个键盘绑定函数。其参数是以毫秒为单位的时间。如果0被传递,它将无限期地等待一次敲击键。它也可以设置为检测特定的按键,例如,如果按下键 a 等。
cv::destroyAllWindows()只会破坏我们创建的所有窗口。如果要销毁任何特定的窗口,请使用函数 cv::destroyWindow()在其中传递确切的窗口名称作为参数。
如果想提前创建一个窗口,则可以使用cv::namedWindow()函数。这个窗口可以自己指定大小。可以选择的参数为:cv::WINDOW_AUTOSIZE cv::WINDOW_NORMAL,第二个参数可以调整窗口大小。
//example:
cv::Mat a = cv::imread("1.jpg");
cv::imshow("windowname",a);
cv::waitKey(0);
cv::destroyWindow();
cv::namedWIndow("windowname",cv::WINDOW_NORMAL)
cv::imshow("windowname",a);
写入图像
使用cv::imwrite()保存图像。第一个参数是文件名,第二个参数是保存的图像。
//example
cv::imwrite(“picture”,a);
从摄像机捕捉实时画面。
要捕获视频,首先创建一个VideoCapture对象,他的参数可以使设备的索引或视频文件的名称。比如WINDOWS的摄像头的索引就是0,在LINUX下可能是0或者-1。然后通过传递1来选择第二个相机,依次类推。
//example:
cv:;VideoCapture cap(0);
if(!cap.isOpend())
std::cout<<"error'<> frame;
cv::imshow("frame",frame);
cv::waitKey(10);
}
cv::cap.release();
cv::destroyAllWindows();
从文件中播放视频
和上面的步骤一样,但是需要把cap后面的参数改为视频文件的路径。
划线
cv::line()。要绘制一条线,需要传递线的开始和结束坐标。
cv::line(img,(0,0),(511,511),(255,0,0),5);
//四个参数分别为:图像,开始点,结束点,线的宽度
画矩形
绘制矩形,需要矩形的左上角和右下角。
cv::rectangle(img,(384,0),(510,128),(0,250,0),3);
//四个参数为图像,左上角坐标,右下角坐标,颜色,宽度
画圆圈
要绘制一个圆,需要其中心坐标和半径。
cv::circle(img,(447,63),63,(0,,0,255),-1)
//参数分别为:图像,圆心点坐标,半径,颜色,宽度
添加文本
//example
font = cv::FONT_HERSHEY_SIMPLEX;
cv::putTect(img,"words",(10,500),font,4,(255,255,255),2,cv::LINE_AA);
4.鼠标事件
cv::setMouseCallback()
访问和修改像素值
可通过行和列的坐标来访问像素值。对于BGR图像,他返回一个由蓝色、绿色和红色组成的数组。对于灰度图像,返回相应的灰度。
//example
px = img[100,100]; //访问
print(px);
img[100,100]=[255,255,255]; //修改
print(img[100,100]);
//另一种方法
img.item(10,10,2); //访问
img.item((10,10,2),100); //修改
img.item(10,10,2);
访问图像属性
图像的形状可通过img.shape访问。它返回行,列和通道数的元组(如果图像是彩色的)。如果图像是灰度的,则返回的元组仅包含行数和列数,因此这是检查加载的图像是灰度还是彩色的好方法。
print(img.shape); //图像的形状
print(img.size); //像素总数
print(img,dtype); //获得图像数据类型
访问感兴趣区域ROI
ball = img[280:340, 330:390];
img[273:333,100:160] = 160;
图像加法和融合
cv::add(),两个图像相加。
cv::addWeighted(),两个图像融合
按位运算
这包括按位AND、OR、NOT 和 XOR 操作。它们在提取图像的任何部分、定义和处理非矩形 ROI 等方面非常有用。
opencv中有超过150种颜色空间转换方法。
cvtColor(input_image,flag),其中flag决定转换的类型。
COLOR_BGR2GRAY,COLOR_BGR2HSV
缩放
cv::resize()
//example
img = cv::imread('messi5.jpg')
res = cv::resize(img,None,fx=2, fy=2, interpolation = INTER_CUBIC)
平移
如果知道在(x,y)方向上平移,则将其设为(tx,ty),创建转换矩阵M
调用时:
cv::warpAffine(img,M,(cols,rows)); //最后一个参数是输出图片的大小
cv::getRotationMatrix2D(((cols-1)/2.0,(rows-1)/2.0),90,1);
dst = cv.warpAffine(img,M,(cols,rows))
对一张图像使用傅立叶变换就是将它分解成正弦和余弦两部分。也就是将图像从空间域(spatial domain)转换到频域(frequency domain)。这一转换的理论基础来自于以下事实:任一函数都可以表示成无数个正弦和余弦函数的和的形式。傅立叶变换就是一个用来将函数分解的工具。 2维图像的傅立叶变换可以用以下数学公式表达:
式中 f 是空间域(spatial domain)值, F 则是频域(frequency domain)值。 转换之后的频域值是复数, 因此,显示傅立叶变换之后的结果需要使用实数图像(real image) 加虚数图像(complex image), 或者幅度图像(magitude image)加相位图像(phase image)。 在实际的图像处理过程中,仅仅使用了幅度图像,因为幅度图像包含了原图像的几乎所有我们需要的几何信息。
步骤如下:
1.将图像延扩到最佳尺寸:离散傅立叶变换的运行速度与图片的尺寸息息相关。当图像的尺寸是2, 3,5的整数倍时,计算速度最快。 因此,为了达到快速计算的目的,经常通过添凑新的边缘像素的方法获取最佳图像尺寸。函数 getOptimalDFTSize() 返回最佳尺寸,而函数 copyMakeBorder() 填充边缘像素:
Mat padded; //将输入图像延扩到最佳的尺寸
int m = getOptimalDFTSize( I.rows );
int n = getOptimalDFTSize( I.cols ); // 在边缘添加0
copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0));
2.为傅立叶变换的结果(实部和虚部)分配存储空间. 傅立叶变换的结果是复数,这就是说对于每个原图像值,结果是两个图像值。 此外,频域值范围远远超过空间值范围, 因此至少要将频域储存在 float 格式中。 结果我们将输入图像转换成浮点类型,并多加一个额外通道来储存复数部分:
Mat planes[] = {Mat_(padded), Mat::zeros(padded.size(), CV_32F)};
Mat complexI;
merge(planes, 2, complexI); // 为延扩后的图像增添一个初始化为0的通道
3.进行离散傅立叶变换. 支持图像原地计算 (输入输出为同一图像):
dft(complexI, complexI); // 变换结果很好的保存在原始矩阵中
4.将复数转换为幅度.复数包含实数部分(Re)和复数部分 (imaginary - Im)。 离散傅立叶变换的结果是复数,对应的幅度可以表示为:
转化为OpenCV代码:
split(complexI, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude
Mat magI = planes[0];
5.对数尺度(logarithmic scale)缩放. 傅立叶变换的幅度值范围大到不适合在屏幕上显示。高值在屏幕上显示为白点,而低值为黑点,高低值的变化无法有效分辨。为了在屏幕上凸显出高低变化的连续性,我们可以用对数尺度来替换线性尺度:
转化为OpenCV代码:
magI += Scalar::all(1); // 转换到对数尺度
log(magI, magI);
6.剪切和重分布幅度图象限. 还记得我们在第一步时延扩了图像吗? 那现在是时候将新添加的像素剔除了。为了方便显示,我们也可以重新分布幅度图象限位置(注:将第五步得到的幅度图从中间划开得到四张1/4子图像,将每张子图像看成幅度图的一个象限,重新分布即将四个角点重叠到图片中心)。 这样的话原点(0,0)就位移到图像中心。
magI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2));
int cx = magI.cols/2;
int cy = magI.rows/2;
Mat q0(magI, Rect(0, 0, cx, cy)); // Top-Left - 为每一个象限创建ROI
Mat q1(magI, Rect(cx, 0, cx, cy)); // Top-Right
Mat q2(magI, Rect(0, cy, cx, cy)); // Bottom-Left
Mat q3(magI, Rect(cx, cy, cx, cy)); // Bottom-Right
Mat tmp; // 交换象限 (Top-Left with Bottom-Right)
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp); // 交换象限 (Top-Right with Bottom-Left)
q2.copyTo(q1);
tmp.copyTo(q2);
7.归一化. 这一步的目的仍然是为了显示。 现在我们有了重分布后的幅度图,但是幅度值仍然超过可显示范围[0,1] 。我们使用 normalize() 函数将幅度归一化到可显示范围。
normalize(magI, magI, 0, 1, CV_MINMAX); // 将float类型的矩阵转换到可显示图像范围
// (float [0, 1]).
离散傅立叶变换的一个应用是决定图片中物体的几何方向.比如,在文字识别中首先要搞清楚文字是不是水平排列的? 看一些文字,你就会注意到文本行一般是水平的而字母则有些垂直分布。文本段的这两个主要方向也是可以从傅立叶变换之后的图像看出来。我们使用这个 水平文本图像 以及 旋转文本图像 来展示离散傅立叶变换的结果 。
有初学者可能会由疑问,什么是模型,为什么要用模型和怎么训练模型呢?
模型:简单来说就是函数,对于使用者来说,就是一个黑盒子,我们将需要操作的数据作为输入,经过黑盒子(模型)处理,得到输出。例如在人脸检测中我们在这里输入的一张带有人的图片,希望得到的是人脸的位置或者说是人脸的图片。
在这里中间进行人脸检测的,就是我们所说的模型。
对于大量的数据,有了封装好的模型,我们可以不动脑筋的将图片输入进去,得到想要的图片。所以,训练一个好的模型,对于项目至关重要。
怎么训练模型呢,我们一般由监督学习和非监督学习两种方法。在监督学习中,我们给出两组数据,一般是training data和testing data,即训练数据和测试数据,两个数据集的比例一般是9:1。我们在训练集上训练我们的模型,在测试集上获得我准确率。而何为监督呢,就是我们在数据集中会带有label,即我们会在数据中,给出它应该有的的正确输出。在有些监督学习中,也会有一个验证集。三者的比例通常是8:1:1。通过大量的数据,我们调整模型(函数)的参数,使它的预测的错误率达到最小。
在本次项目中,我们所用到的两个关键点是:
Haar+Adaboost级联器 Haar特征和Adaboost迭代算法
Eigenface 特征脸算法
这两个模型的原理介绍,移步第三篇文章:基于QT,C++和opencv 的人脸识别项目(三)