本文为学习opencv4快速入门的学习记录
0 总体架构
1 数据载入、显示与保存
1.1 图像存储容器
- 定义
**Mat类:**用来保存矩阵类型的数据信息,包括向量、矩阵、灰度或彩色图像等数据。
注:可以采用枚举法赋值。如:
cv::Mat a = (cv::Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
- Mat类在计算机中的存储方式⭐
Mat类矩阵在计算机中存储时时将三维数据变成二维数据,先存储第一个元素每个通道的数据,之后再存储第二个元素每个通道的数据,依次为BGR。
读取Mat类的数据
- 方法一:通过at方法
// at方法读取元素需要再后面跟上“<数据类型>”,此处的数据类型需要和定义的a数据类型一致。 int val = (int) a.at<uchar>(0, 0);
- 方法二:通过指针ptr
cv::Mat a(3, 4, CV_8UC3, cv::Scalar(0, 0, 1)); for (int i = 0; i < a.rows; i++) { uchar* ptr = a.ptr<uchar>(i); for (int j = 0; j < a.cols * a.channels(); j++) { cout << (int)ptr[j] << endl; } }
- 方法三:通过迭代器
cv::MatIterator_<uchar> it = a.begin<uchar>(); cv::MatIterator_<uchar> it_end = a.end<uchar>(); for (int i = 0; it != it_end; it++) { cout << (int)*it << " "; if ((++i % a.cols) == 0) { cout << endl; } }
1.2 图像读取与显示
imread():用于读取指定的图像,并返回给一个Mat类变量。如果返回为空,则读取失败。
nameWindow():创建一个窗口变量,用于显示图像和滑动条。
destroyWindow():关闭一个指定名称的窗口
destroyAllWindow():关闭程序中的所有窗口
imshow():在指定的窗口显示图像。如果之前没有创建指定的同名窗口,就会自动创建一个。
1.3 视频加载与摄像头调用
- 视频加载:VideoCapture类
默认构造,具体读取时采用.open(filename);
有参构造,直接读取。
isOpened():判断是否读取成功
// 步骤 // 1. 构造VideoCapture类,加载视频文件 VideoCapture video("xxx.xxx"); // 2. 判断是否读取成功 if (!video.isOpened()) { return; } // 3. 利用循环读取每一帧图像 while (1) { Mat frame; video >> frame; // 从VideoCapture中依次读取每一帧; if (frame.empty()) { // 如果没有图片了,则终止循环 break; } // 4. 对每一帧图像进行操作 }
- 摄像头调用,同上,仅在构造VideoCapture类时的第一个参数不同。
注: 二者的主要区别为:第一个参数的不同。打开视频的第一参数为 视频文件名 ,调用摄像头的第一个参数为要打开的 摄像头设备的ID 。
1.4 数据保存
- 图像保存
imwrite():将Mat类矩阵保存为图像文件,如果保存成功返回true,否则返回false;
- 视频保存
VideoWriter()类:实现多张图像保存为视频文件。默认构造:使用时需要采用内置open()方法,有参构造直接使用。
// 步骤 // 1. 构造VideoWriter类 VideoWriter writer; // 2. 设置相关参数选择编码格式 bool isColor = (img.type() == CV_8U3C); // 是否为彩色 int codec = VideoWriter::fourcc('M', 'J', 'P', 'G'); // 选择编码格式 double fps = 25.0; // 设置视频帧率 string filename = "xxx.xxx"; // 保存的视频文件名称 // 3. 创建保存视频文件的视频流, writer.open(filename, codec, fps, img.size(), isColor); // 4. 判断视频流是否创建成功 if (!writer.isOpened()) { return; } // 5. 利用循环写入每一张图像 while (1) { if (img.empty()) { // 如果没有图片了,则终止循环 break; } writer.write(img); }
- 保存和读取XML和YAML文件: FileStorage类
- 默认构造:创建后采用open()加载指定文件,并指定操作类型
- 有参构造:创建时已指定对文件的操作类型
- write(var_name, var_val):将指定的变量名和值写入文件中
- 流的形式读取:“>>” 读取,”<<“ 写入
2 图像基本操作
2.1 图像颜色空间
颜色模型
RGB颜色模型:分别表示红色(R)、绿色(G)和蓝色(B)。可以进一步增加第四个通道为RGBA颜色模型,A为颜色的透明度;
YUV颜色模型:为电视信号系统所采用的颜色编码方式,分别表示亮度(Y)、红色分量与亮度的信号差值(U)和蓝色分量与亮度的信号差值(V);黑白电视只需要使用Y通道,彩色电视需要将YUV转换为RGB;
HSV颜色模型:分别表示色度(H)、饱和度(S)和亮度(V)。其更加符合人类感知颜色的方式:颜色、深浅及亮度;
Lab颜色模型:它是一种设备无关和基于生理特征的颜色模型。分别表示亮度(L)、从绿到红的颜色通道(a)和从蓝到黄的颜色通道(b);
GRAY颜色模型:单通道的灰度图像模型。0表示黑色,255表示白色
注: opencv采用的是BGR颜色模型,即第一个颜色通道为蓝色,第二个为绿色,第三个为红色。
相关函数
cv::cvtColor(): 将图像转换到指定颜色模型中。
cvtColor(res_img, cur_img, COLOR_BGR2HSV); // 从BGR颜色模型转换为HSV颜色模型
cv::Mat::convertTo(): 转换图像的数据类型
res_img.convertTo(cur_img, CV_32F, 1.0 / 255); // 转换为CV_32F类型
cv::split(): 将多通道的图像分离成若干单通道的图像
cv::merge(): 将若干图像(可以是不同通道数)合并成一个多通道图像
2.2 图像像素操作处理
- 单图像的最值 ,均值与标准差
cv::minMaxLoc(): 寻找图像像素中最大值和最小值。必须为 单通道图像 ,多通道的矩阵数据需要使用reshape()将多通道变成单通道,或者分别寻找每个通道的最值,然后进行比较,寻找全局最值。如果存在多个最值,则结果为按行扫描从左向右第一次检测到的位置
cv::Mat::reshape(): 改变矩阵的维度
cv::mean(): 求取图像矩阵的每个通道的平均值。返回值为cv::Scalar类,4位,多余的为0;
cv::meanStdDev(): 同时求取图像每个通道的平均值和标准差,无返回值,用于存放平均值和标准差的是Mat类型
- 两图像间的像素操作
cv::max(): 比较图像每个像素的大小,按要求保留较大值,生成新的图像;
cv::min(): 比较图像每个像素的大小,按要求保留较小值,生成新的图像;
cv::bitwise_and(): 两图像像素求“与”运算;
cv::bitwise_or(): 两图像像素求“或”运算;
cv::bitwise_xor(): 两图像像素求“异或”运算;
cv::bitwise_not(): 单图像像素求“非”运算;
- 图像二值化 (图像像素只有最大值和最小值两种取值)
cv::threshold(): 通过给定一个阈值,计算所有像素灰度值与这个阈值关系,进行二值化;
cv::adaptiveThreshold(): 为局部自适应阈值的二值方法, 只能是CV_8UC1 。通过均值法或高斯法自适应地计算 blockSize× blockSize 邻域内的阈值,之后进行二值化
cv::LUT(): 根据图像256个像素灰度值的LUT查找表进行映射。
2.3 图像变换
- 图像连接 :将两个具有相同高度或宽度的图像连接在一起
cv::vconcat(): 对存放在数组矩阵中的Mat类型进行纵向连接(上下连接);
cv::hconcat(): 对存放在数组矩阵中的Mat类型进行横向连接(左右连接);
- 图像尺寸变换:改变图像的长和宽,实现图像的缩放
cv::resize(): 对图像尺寸进行缩放,可以采用最近邻插值法、双线性插值、双三次插值等。
- 图像翻转变换
cv::flip(): 对图像进行翻转,可以绕x轴或者y轴进行翻转。
- 图像的仿射变换 :就是图像的旋转、平移和缩放操作的统称
cv::getRotationMatrix2D(): 通过输入旋转角度和旋转中心,得到图像的旋转矩阵。2×3
cv::getAffineTransform(): 根据变换前后两幅图像中3个像素点坐标的对应关系求得仿射变换中的变换矩阵。2×3
cv::warpAffine(): 根据图像的旋转(仿射变换)矩阵进行仿射变换。
- 图像透视变换 :按照物体成像投影规律进行变换,即将物体重新投影到新的成像平面
cv::getPerspectiveTransform(): 根据变换前后4个像素点坐标的对应关系求得透视变换矩阵。3×3。求解方法可选SVD、QR等。⭐️⭐️
cv::warpPerspective(): 根据透视变换矩阵进行透视变换
- 极坐标变换 : 将图像在直角坐标系与极坐标系互相变换
cv::warpPolar(): 实现图像极坐标变换、半对数极坐标变换和逆变换。插值方法与极坐标映射方法标志可以通过 “+” 或 “|”连接
warpPolar(res_img, cur_img, size, center, r, INTER_LINEAR + WARP_POLAR_LINEAR); // 正极坐标变换 warpPolar(cur_img, res1_img, size, center, r, INTER_LINEAR + WARP_POLAR_LINEAR + WARP_INVERSE_MAP); // 逆极坐标变换
2.4 在图像上绘制几何图形
-
绘制圆形
cv::circle(): 根据圆心、半径绘制圆形
-
绘制直线
cv::line(): 根据两点绘制直线
-
绘制椭圆
cv::ellipse(): 通过选定椭圆中心位置和主轴大小唯一地确定一个椭圆;
cv::ellipse2Poly(): 输出椭圆边界的像素坐标,不绘制。
-
绘制矩形
cv::rectangle(): 利用矩形对角线上两个顶点的坐标或者利用左上角顶点坐标和矩形的长和宽唯一地确定一个矩形。
-
绘制多边形
cv::fillPoly(): 通过依次连接多边形的顶点来实现多边形的绘制。
-
文字生成
cv::putText(): 在图像中生成文字,目前仅支持英文,若为中文需要添加其它依赖
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat img = Mat::zeros(Size(512, 512), CV_8UC3); //生成一个黑色图像用于绘制几何图形
//绘制圆形
circle(img, Point(50, 50), 25, Scalar(255, 255, 255), -1); //绘制一个实心圆
circle(img, Point(100, 50), 20, Scalar(255, 255, 255), 4); //绘制一个空心圆
//绘制直线
line(img, Point(100, 100), Point(200, 100), Scalar(255, 255, 255), 2, LINE_4,0); //绘制一条直线
//绘制椭圆
ellipse(img, Point(300, 255), Size(100, 70), 0, 0, 100, Scalar(255, 255, 255), -1); //绘制实心椭圆的一部分
ellipse(img, RotatedRect(Point2f(150, 100), Size2f(30, 20), 0), Scalar(0, 0, 255), 2); //绘制一个空心椭圆
vector<Point> points;
ellipse2Poly(Point(200, 400), Size(100, 70),0,0,360,2,points); //用一些点来近似一个椭圆
for (int i = 0; i < points.size()-1; i++) //用直线把这个椭圆画出来
{
if (i==points.size()-1)
{
line(img, points[0], points[i], Scalar(255, 255, 255), 2); //椭圆中后于一个点与第一个点连线
break;
}
line(img, points[i], points[i+1], Scalar(255, 255, 255), 2); //当前点与后一个点连线
}
//绘制矩形
rectangle(img, Point(50, 400), Point(100, 450), Scalar(125, 125, 125), -1);
rectangle(img, Rect(400,450,60,50), Scalar(0, 125, 125), 2);
//绘制多边形
Point pp[2][6];
pp[0][0] = Point(72, 200);
pp[0][1] = Point(142, 204);
pp[0][2] = Point(226, 263);
pp[0][3] = Point(172, 310);
pp[0][4] = Point(117, 319);
pp[0][5] = Point(15, 260);
pp[1][0] = Point(359, 339);
pp[1][1] = Point(447, 351);
pp[1][2] = Point(504, 349);
pp[1][3] = Point(484, 433);
pp[1][4] = Point(418, 449);
pp[1][5] = Point(354, 402);
Point pp2[5];
pp2[0] = Point(350, 83);
pp2[1] = Point(463, 90);
pp2[2] = Point(500, 171);
pp2[3] = Point(421, 194);
pp2[4] = Point(338, 141);
const Point* pts[3] = { pp[0],pp[1],pp2 }; //pts变量的生成
int npts[] = { 6,6,5 }; //顶点个数数组的生成
fillPoly(img, pts, npts, 3, Scalar(125, 125, 125),8); //绘制3个多边形
//生成文字
putText(img, "Learn OpenCV 4",Point(100, 400), 2, 1, Scalar(255, 255, 255));
imshow("", img);
waitKey(0);
return 0;
}
2.5 感兴趣区域
在原图中将需要截取的感兴趣区域(ROI)在图像中的位置标记出来
注: opencv4中对图像的赋值和拷贝分为浅拷贝和深拷贝,“=" 为浅拷贝需慎用
- 深拷贝方式
cv::Mat::copyTo():
cv::copyTo():
2.6 图像金字塔
定义
图像“金字塔”是通过多个分辨率表示图像的一种有效且简单的结构。一个图像“金字塔”是一系列以金字塔形状排列、分辨率逐步降低的图像集合。
高斯”金字塔“
高斯”金字塔“是值通过下采用不断地将图像的尺寸缩小,进而在图像”金字塔“中包含多个尺寸的图像。解决尺度不确定性的一种常用方法。自底向上构建图像。
cv::pyrDown(): 实现图像模糊并对其进行下采样
拉普拉斯“金字塔”(差值金字塔图像)
拉普拉斯“金字塔”通过上层小尺寸的图像构建下层大尺寸的图像。
具有预测残差的作用,需要与高斯“金字塔”联合使用。
拉普拉斯金字塔本质是高斯金字塔与其上一层通过上采样扩大后的差值图像
cv::pyrUp(): 实现图像插值并对其进行上采样
2.7 窗口交互操作
交互操作能够增加用户对程序流程的控制,使程序可以根据用户需求实现不同的处理结果
-
图像窗口滑动条:在显示图像的窗口中创建能够通过滑动改变数值的滑动条
cv::createTrackbar(): 在窗口的上方创建一个范围从0开始的整数滑动条。如果需要得到小数,需要对其进行后处理。
-
鼠标响应:通过鼠标在图像中标记出重要的区域
本质就是当鼠标位于对应的图像窗口内时,时刻检测鼠标状态,当鼠标状态发生改变时,调用回调函数,并根据回调函数中的判断逻辑选择执行相应的操作。
cv::setMouseCallback(): 为指定的图像窗口创建鼠标响应
MouseCallback类型的回调函数是一个无返回值的函数,函数名可以任意设置,有5个参数(鼠标响应事件标志、鼠标当前所在图像中的位置x、y、鼠标响应标志及传递给回调函数的可选参数),在鼠标状态发生改变时被调用。
#include <opencv2/opencv.hpp> #include <iostream> using namespace std; using namespace cv; Mat img, imgPoint; //全局的图像 Point prePoint; //前一时刻鼠标的坐标,用于绘制直线 void mouse(int event, int x, int y, int flags, void*); int main() { img = imread("lena.png"); if (!img.data) { cout << "请确认输入图像名称是否正确! " << endl; return -1; } img.copyTo(imgPoint); imshow("图像窗口 1", img); imshow("图像窗口 2", imgPoint); setMouseCallback("图像窗口 1", mouse, 0); //鼠标影响 waitKey(0); return 0; } void mouse(int event, int x, int y, int flags, void*) { if (event == EVENT_RBUTTONDOWN) //单击右键 { cout << "点击鼠标左键才可以绘制轨迹" << endl; } if (event == EVENT_LBUTTONDOWN) //单击左键,输出坐标 { prePoint = Point(x, y); cout << "轨迹起始坐标" << prePoint << endl; } if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON)) //鼠标按住左键移动 { //通过改变图像像素显示鼠标移动轨迹 imgPoint.at<Vec3b>(y, x) = Vec3b(0, 0, 255); imgPoint.at<Vec3b>(y, x - 1) = Vec3b(0, 0, 255); imgPoint.at<Vec3b>(y, x + 1) = Vec3b(0, 0, 255); imgPoint.at<Vec3b>(y + 1, x) = Vec3b(0, 0, 255); imgPoint.at<Vec3b>(y - 1, x) = Vec3b(0, 0, 255); imshow("图像窗口 2", imgPoint); //通过绘制直线显示鼠标移动轨迹 Point pt(x, y); line(img, prePoint, pt, Scalar(0, 0, 255), 2, 5, 0); prePoint = pt; imshow("图像窗口 1", img); } }
3 图像直方图与模板匹配
3.1 图像直方图的绘制
-
图像直方图是对图像像素的统计。即统计图像中每个灰度值的个数,之后将图像灰度值作为横轴,以灰度值的个数或者灰度值所占比率作为纵轴绘制的统计图。
-
由于同一物体无论是旋转还是平移,在图像中都具有相同的灰度值,因此直方图具有 平移不变性、放缩不变性 等优点。
-
cv::calcHist(): 统计出图像中每个灰度值的个数。注:如需绘制,需自行进一步操作
// 1. 打开图像 Mat img = imread("xxx.xxx"); // 2. 设置提取直方图的相关变量 Mat hist; // 用于存放直方图计算结果 const int channels[1] = {0}; // 通道索引 float inRanges[2] = {0, 255}; const float* ranges[1] = {inRanges}; // 像素灰度值范围 const int bins[1] = {256}; // 直方图的维度,其实就是像素灰度值的最大值 // 3. 计算图像直方图 calcHist(&gray, 1, channels, Mat(), hist, 1, bins, ranges); // 4. 绘制直方图 int hist_w = 512, hist_h = 400; int width = 2; Mat histImage = Mat::zeros(hist_h, hist_W, CV_8UC3); for (int i = 1; i <= hist.rows; i++) { rectangle(histImage, Point(width*(i - 1), hist_h - 1), Point(width*i - 1, hist_h - cvRound(hist.at<float>(i - 1) / 15)), Scalar(255, 255, 255), -1); } // 5. 显示绘制结果 imshow("histImage", histImage);
3.2 直方图操作
直方图归一化: 将统计结果再除以图像中的像素个数 或 灰度值个数的最大值
绘制直方图时,将所有数据都缩小1/15或者更小,目的就是为了能够将直方图完整地绘制在图像中。
图像的像素灰度值统计结果主要目的之一就是查看某个灰度值在所有像素中所占的比例。
因此,直方图归一化操作可以保证每个灰度值的统计结果在0-1区间;
除以最大值的方法:可以避免过多的灰度分布导致每个灰度值所占比例均很小,从而表现区分不明显的问题。
cv::normalize(): 可以实现多种形式的归一化。
直方图比较: 通过两幅图像的直方图特性可以比较两幅图像的相似程度,从而进行初筛与识别
cv::compareHist(): 需要输入 同一种方式 归一化后的 相同尺寸 图像直方图。
3.3 直方图应用
直方图均衡化: 通过映射关系,将图像中灰度值的范围扩大,增加原来两个灰度值之间的差值,提高图像的对比度,进而将图像中的纹理突出显现出来。
缺点:不能指定均衡化后的直方图分布形式。
cv::qualizeHist(): 用于将图像的直方图均衡化,只能对 单通道的灰度图 进行直方图均衡化。
直方图匹配: 将直方图映射成指定分布形式,能够有目的地增强某个灰度区间。
// 没有内置函数,需要自己根据算法进行实现; // 可以通过构建原直方图累积概率与目标直方图累积概率之间的差值表,寻找原直方图中灰度值n的累计概率与目标直方图中所有灰度值累计概率差值的最小值,这个最小值对应的灰度值r就是n匹配后的灰度值。 #include <opencv2\opencv.hpp> #include <iostream> using namespace cv; using namespace std; void drawHist(Mat &hist, int type, string name) //归一化并绘制直方图函数 { int hist_w = 512; int hist_h = 400; int width = 2; Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3); normalize(hist, hist, 1, 0, type, -1, Mat()); for (int i = 1; i <= hist.rows; i++) { rectangle(histImage, Point(width*(i - 1), hist_h - 1), Point(width*i - 1, hist_h - cvRound(20 * hist_h*hist.at<float>(i - 1)) - 1), Scalar(255, 255, 255), -1); } imshow(name, histImage); } //主函数 int main() { Mat img1 = imread("histMatch.png"); Mat img2 = imread("equalLena.png"); if (img1.empty() || img2.empty()) { cout << "请确认图像文件名称是否正确" << endl; return -1; } Mat hist1, hist2; //计算两张图像直方图 const int channels[1] = { 0 }; float inRanges[2] = { 0,255 }; const float* ranges[1] = { inRanges }; const int bins[1] = { 256 }; calcHist(&img1, 1, channels, Mat(), hist1, 1, bins, ranges); calcHist(&img2, 1, channels, Mat(), hist2, 1, bins, ranges); //归一化两张图像的直方图并可视化 drawHist(hist1, NORM_L1, "hist1"); drawHist(hist2, NORM_L1, "hist2"); // ------------------ 重点 ------------------- //计算两张图像直方图的累积概率 float hist1_cdf[256] = { hist1.at<float>(0) }; float hist2_cdf[256] = { hist2.at<float>(0) }; for (int i = 1; i < 256; i++) { hist1_cdf[i] = hist1_cdf[i - 1] + hist1.at<float>(i); hist2_cdf[i] = hist2_cdf[i - 1] + hist2.at<float>(i); } //构建累积概率误差矩阵 float diff_cdf[256][256]; for (int i = 0; i < 256; i++) { for (int j = 0; j < 256; j++) { diff_cdf[i][j] = fabs(hist1_cdf[i] - hist2_cdf[j]); } } //生成LUT映射表 Mat lut(1, 256, CV_8U); for (int i = 0; i < 256; i++) { // 查找源灰度级为i的映射灰度 // 和i的累积概率差值最小的规定化灰度 float min = diff_cdf[i][0]; int index = 0; //寻找累积概率误差矩阵中每一行中的最小值 for (int j = 1; j < 256; j++) { if (min > diff_cdf[i][j]) { min = diff_cdf[i][j]; index = j; } } lut.at<uchar>(i) = (uchar)index; } Mat result, hist3; // 根据LUT表进行映射 LUT(img1, lut, result); imshow("待匹配图像", img1); imshow("匹配的模板图像", img2); imshow("直方图匹配结果", result); calcHist(&result, 1, channels, Mat(), hist3, 1, bins, ranges); drawHist(hist3, NORM_L1, "hist3"); //绘制匹配后的图像直方图 waitKey(0); return 0; }
直方图反向投影: 记录给定图像中的像素点如何适应直方图模型像素分布方式的一种方法。即首先计算某一特征的直方图模型,然后使用该模型取寻找图像中是否存在该特征的方法。
cv::calcBackProject(): 用于在输入图像中寻找与特定图像最匹配的点或者区域。
/* 使用步骤: 1. 加载模板图像和待反向投影图像 2. 转换图像颜色空间,常用的颜色空间为灰度图像和HSV图像 3. 计算模板图像的直方图,灰度图像为一维直方图,HSV图像为H-S通道的二维直方图 4. 将待反向投影的图像和模板图像的直方图赋值给反向投影函数calcBackProject(),最终得到反向投影结果 */ // 1. 加载模板图像和待反向投影图像 Mat img = imread("apple.jpg"); Mat sub_img = imread("sub_apple.jpg"); // 2. 转成HSV空间,提取S、V两个通道 Mat img_HSV, sub_HSV, hist, hist2; cvtColor(img, img_HSV, COLOR_BGR2HSV); cvtColor(sub_img, sub_HSV, COLOR_BGR2HSV); // 3. 绘制H-S二维直方图 int h_bins = 32; int s_bins = 32; int histSize[] = { h_bins, s_bins }; float h_ranges[] = { 0, 180 }; // H通道值的范围由0到179 float s_ranges[] = { 0, 256 }; // S通道值的范围由0到255 const float* ranges[] = { h_ranges, s_ranges }; //每个通道的范围 int channels[] = { 0, 1 }; //统计的通道索引 calcHist(&sub_HSV, 1, channels, Mat(), hist, 2, histSize, ranges, true, false); // 4. 反向投影 Mat backproj; calcBackProject(&img_HSV, 1, channels, hist, backproj, ranges, 1.0); //直方图反向投影
3.4 图像的模板匹配
模板匹配的概念
通过比较像素灰度值来寻找相同内容的方法
常用于在一幅图像中寻找特定内容的任务
模板匹配的流程
- 在待匹配图像中选取与模板尺寸大小相同的滑动窗口
- 比较滑动窗口中每个像素与模板中对应像素灰度值的关系,计算模板与滑动窗口的相似性
- 将滑动窗口从左上角开始先向右滑动,滑动到最右边后向下滑动一行,然后从最左侧重新开始滑动,记录每一次移动后计算的模板与滑动窗口的相似性
- 比较所有位置的相似性,选择相似性最大的滑动窗口作为备选匹配结果
内置函数:cv::matchTemplate()
注意:
需要模板图像尺寸小于待匹配图像尺寸,且两者具有相同的数据类型。
返回结果为相似性矩阵,需要注意选择方法是取最大值区域还是最小值区域。需要配合 cv::minMaxLoc() 使用
// 模板匹配 Mat result; matchTemplate(img, temp, result, TM_CCOEFF_NORMED);//模板匹配 double maxVal, minVal; Point minLoc, maxLoc; //寻找匹配结果中的最大值和最小值以及坐标位置 minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc); //绘制最佳匹配区域 rectangle(img, cv::Rect(maxLoc.x, maxLoc.y, temp.cols, temp.rows), Scalar(0, 0, 255), 2);
4 图像滤波
由于采集图像的设备可能会收到光子噪声、暗电流噪声等干扰,使得采集到的图像具有噪声,另外图像信号在传输过程中也有可能产生噪声,因此去除图像中的噪声是图像预处理中十分重要的步骤。
图像滤波 就是指去除图像中不重要的内容而使关心的内容表现得更清晰得方法。
- 线性滤波:通过滤波范围内所有像素之间的线性组合,得到中心位置的像素滤波后的像素值
- 非线性滤波:不是通过线性组合计算得到,其计算过程可能包含排序、逻辑计算等
4.1 图像卷积
概念: 图像卷积可以看成是一个卷积模板在另外一个大的图像上移动,对每个卷积模板覆盖的区域进行点乘,得到的值作为中心像素点的输出值。 卷积模板又称为卷积核或内核。
注意: 如果卷积模板不是中心对称的,需要将其 旋转180° 。 深度学习中的卷积核因其参数是学习得来,故不需要旋转!
连续卷积公式:
离散卷积公式:
步骤
- 将卷积模板旋转180°
- 将卷积模板中心放在原始图像中需要计算卷积的像素上,卷积模板中其余部分对应在原始图像相应的像素上
- 用卷积模板中的系数乘以图像中对应位置的像素数值,并对所有结果求和
- 将计算结果存放在原始图像中与模板中心对应的像素处
- 将卷积模板在图像中从左至右、从上到下移动,重复第2~4步,直到处理完所有的像素值
上述方法由于卷积模板中心无法放置在图像的边缘像素处,因此只能对图像中心区域进行卷积,无法对图像边缘进行卷积。可以主动 将图像的边缘外推 出去。如边缘用0填充一圈。
为避免数值超出像素范围,常将模板数值归一化
内置函数
cv::filter2D(): 实现图像和卷积模板之间的卷积运算,图像中不同通道的卷积模板相同,如需用不同的卷积模板需要先用split() 函数将图像多个通道分离之后单独对每一个通道求取卷积运算。
注意: filter2D()函数不会将卷积模板进行旋转,如果卷积模板不对称,需要先将其旋转180°
4.2 噪声的种类与生成
图像在获取或者传输过程中会受到随机信号的干扰而产生噪声,从而妨碍人们对图像的理解以及后续的处理工作。
噪声种类
- 高斯噪声
- 椒盐噪声
- 泊松噪声
- 乘性噪声
椒盐噪声 (脉冲噪声):会随机改变图像中的像素值
产生原因:由相机成像、图像传输、解码处理等过程产生的黑白相间的亮暗点噪声
高斯噪声
定义:高斯噪声是指噪声分布的概率密度函数服从高斯分布(正态分布)的一类噪声
产生原因:主要是由于相机在拍摄时视场较暗且亮度不均匀,相机长时间工作导致温度过高等
4.3 线性滤波
均值滤波
定义:均值滤波就是将滤波器内所有的像素值都看作中心像素值的测量,将滤波器内所有的像素值的平均值作为滤波器中心处图像像素值
缺点:缩小像素值之间的差距,使得细节信息变得更加模糊。
优点:在像素值变换趋势一致的情况下,可以将受噪声影响而突然变化的像素值修正为周围邻近像素值的平均值。
内置函数: cv::blur()
方框滤波
定义:方框滤波是均值滤波的一般形式,可以选择不进行归一化,将所有像素值的和作为滤波结果。
内置函数:
- cv::boxFilter()——直接将所有像素的和作为滤波结果;
- cv::sqrBoxFilter()——对滤波器内每个像素值的平方求和
高斯滤波
定义:以滤波器中心位置为高斯分布的均值,根据高斯分布公式和每个像素离中心位置的距离计算出滤波器内每个位置的数值,得到高斯滤波器,进行滤波操作。
其考虑了像素离滤波器中心距离的影响
内置函数:
- cv::GaussianBlur(): 自动生成高斯滤波器,实现对图像的高斯滤波
- cv::getGaussianKernel(): 用于生成指定尺寸的高斯滤波器。注:生成一个二维的高斯滤波器需要调用两次getGaussianKernel()函数,将X方向的一维高斯滤波器和Y方向的一维高斯滤波器相乘,得到最终的二维高斯滤波器
可分离滤波
cv::filter2D() 可以根据自定义滤波器实现图像滤波,从而实现自定义滤波操作。
图像滤波具有 可分离性 如可以先对X方向进行滤波,再对Y方向进行滤波,结果等效一起滤波。
内置函数:cv::sepFilter2D()——可以输入两个方向滤波器实现滤波
4.4 非线性滤波
可以解决线性滤波无法彻底消除噪声的问题
中值滤波
定义:中值滤波就是用滤波器范围内 所有像素值的中值 来替代滤波器中心位置像素值的滤波方法
优点:中值滤波对于脉冲干扰信号和图像扫描噪声的处理效果更佳;一定条件下,对图像的边缘信息保护效果较好,可以避免均值滤波对图像细节的模糊
缺点:尺寸变大后,同样会产生图像模糊,耗时长
内置函数:cv::medianBlur(),只能处理符合图像信息的Mat类数据,即其通道数只能是1、3或4。滤波器必须是正方形且尺寸大于1的奇数
双边滤波
定义:双边滤波是一种综合考虑滤波器内图像空域信息和滤波器内图像像素灰度值相似性的滤波算法,可以实现再保留区域信息的基础上实现对噪声的去除、对局部边缘的平滑。
内置函数:cv::bilateralFilter()
4.5 图像的边缘检测
原理
图像的边缘指的是图像中像素灰度值突然发生变化的区域。
将图像的每一行(列)像素看作为一个关于灰度值的函数,则可以通过寻找 导数值较大 的区域寻找函数中突然变化的区域,进而确定图像中的边缘位置。
由于图像时离散信号,则 d f ( x , y ) d x = f ( x , y ) − f ( x − 1 , y ) \frac{df(x, y)} {dx} = f(x,y) - f(x-1,y) dxdf(x,y)=f(x,y)−f(x−1,y) ,即对x方向求导对应的滤波器为 [-1 1]… 但这种求导方式的计算结果最接近于两个像素中间位置的梯度,而两个临近的像素中间不再有任何的像素,因此需要改进如下:
d f ( x , y ) d x = f ( x + 1 , y ) − f ( x − 1 , y ) 2 \frac{df(x,y)}{dx} = \frac{f(x+1,y) - f(x-1,y)}{2} dxdf(x,y)=2f(x+1,y)−f(x−1,y)
图像的边缘可以是由高像素值变为低像素值,也可以是由低到高。因此无论结果正负,均为边缘。
cv::convertScaleAbs(): 用于计算矩阵中所有数据的绝对值
Sobel算子
定义:sobel算子是通过离散微分方法求取图像边缘的边缘检测算子,
步骤:
提取X方向的边缘
[ − 1 0 1 − 2 0 2 − 1 0 1 ] \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} ⎣ ⎡−1−2−1000121⎦ ⎤
提取Y方向的边缘
[ − 1 − 2 − 1 0 0 0 1 2 1 ] \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} ⎣ ⎡−101−202−101⎦ ⎤
综合两个方向的边缘信息得到整幅图像的边缘。两种方式:1. 求取两幅图像对应像素的像素值的绝对值之和;2. 求取对应像素的像素值的平方和的二次方根
优点:可以有效提取图像边缘
缺点:对图像中较弱的边缘提取能力差
内置函数:cv::Sobel()
Scharr算子
定义:scharr算子是对sobel算子的增强,通过增大像素间的差距从而有效提取图像中较弱的边缘
G x = [ − 3 0 3 − 10 0 10 − 3 0 3 ] G_x=\begin{bmatrix} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \end{bmatrix} Gx=⎣ ⎡−3−10−30003103⎦ ⎤ G y = [ − 3 − 10 − 3 0 0 0 3 10 3 ] G_y=\begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ 3 & 10 & 3 \end{bmatrix} Gy=⎣ ⎡−303−10010−303⎦ ⎤
内置函数:cv::Scharr()
注: sharr算子只有3×3的尺寸,sobel可以由不同尺寸
生成边缘检测滤波器
用途:得到提取图像边缘的滤波器,通过对滤波器修改提升边缘检测效果。
内置函数:cv::getDerivKernels():可以得到scharr算子和不同尺寸、不同阶级的sobel算子的滤波器
Laplacian算子
laplacian算子具有各方向同性的特点,能够对任意方向的边缘进行提取,具有无方向性的优点。它是一种二阶导数算子,对噪声比较敏感,常需配合高斯滤波一起使用。
[ 0 1 0 1 − 4 1 0 1 0 ] \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix} ⎣ ⎡0101−41010⎦ ⎤
内置函数:cv::Laplacian()
Canny算子
该算法不容易受到噪声二点影响,能够识别图像中的弱边缘和强边缘,并结合强弱边缘的位置关系,综合给出图像整体的边缘信息。
步骤:
- 使用高斯滤波平滑图像,减少图像中噪声
- 计算图像中每个像素的梯度方向和幅值
- 应用非极大值抑制算法消除边缘检测带来的杂散响应
- 应用双阈值法划分强边缘和弱边缘
- 消除孤立的弱边缘
内置函数:cv::Canny()
注: 高斯模糊在边缘纹理较多的区域能减少边缘检测的结果,但是对纹理少的区域影响较小
5 图像形态学操作
图像形态学用具有一定形态的结构元素度量和提取图像中得对应形状,以达到对图像分析和识别的目的。
主要应用于从图像中提取对于表达和描述区域形状有意义的图像分量,以便后续的识别工作能够抓住对象最为本质的形状特性,如边界、连通域等。
5.1 像素距离与连通域
像素距离
像素之间的距离阔以用于表示两个连通域之间的关系
- 欧式距离:两个像素点之间的直线距离
- 街区距离:两个像素点X方向和Y方向的距离之和
- 棋盘距离:两个像素点X方向和Y方向的距离最大值
内置函数:cv::distanceTransform()——统计图像中所有像素 距离0像素 的最小距离
连通域
定义: 图像的连通域 是指图像中具有相同像素值并且位置相邻的像素组成的区域。常将不与其它区域连接的独立区域称为集合或者连通域
连通域分析 是指在图像中寻找彼此互相独立的连通域并将其标记出来
4邻域与8邻域
常用的图像邻域分析法:
两边扫描法
第一次遍历图像时会给每一个非零像素赋予一个数字标签,当某个像素的上方和左侧邻域内的像素已经由数字标签时,取两者中的较小值作为当前像素的标签,否则赋予当前像素一个新的数字标签
第二次遍历需要将这些属于同一个连通域的不同标签合并,最后实现同一个邻域内的所有像素具有相同的标签
种子填充法
首先将所有非零像素放到一个集合中,之后在集合中随机选出一个像素作为种子像素,根据邻域关系不断扩充种子像素所在的连通域,并在集合中删除扩充出的像素,知道种子像素所在的连通域无法扩充,之后再从集合中随机选取一个像素作为新的种子像素,重复上述过程知道集合中没有像素
内置函数:
- cv::connectedComponents()——用于计算二值图像中连通域的个数,并在图像中将不同连通域用不同的数字标签标记
- cv::connectedComponentsWithStats()——用于在标记出图像中不同连通域的同时统计连通域的位置、面积的信息
5.2 腐蚀和膨胀
图像腐蚀
就是使用结构元素和图像进行and操作
步骤:
- 定义结构元素
- 将结构元素绕中心旋转180°
- 将结构元素的中心点依次放到图像中每一个非零元素处,如果此时结构元素内所有的元素所覆盖的图像像素 均不为0 ,那么保留,否则删除结构元素中心点所对应的像素。
内置函数:
- cv::getStructuringElement(): 用于生成常用的矩形结构元素、十字结构元素和椭圆结构元素
- cv::erode(): 用于图像的腐蚀操作
注意: 图像的腐蚀过程只针对图像中的非零像素,因此如果图像是以0像素为背景,那么腐蚀操作后会看到图像中的内容变得更瘦更小;如果图像是以255像素为背景,那么腐蚀操作后会看到图像中的内容更粗更大
图像膨胀
就是使用结构元素和图像进行or操作
步骤:
- 定义结构元素
- 将结构元素绕中心旋转180°
- 将结构元素的中心点依次放到图像中每一个非零元素处,如果原图中某个元素被结构元素覆盖,但是该像素的像素值不与结构元素相同,那么将原图中该像素的像素值修改为结构元素中心点对应的像素值
内置函数:
cv::dilate()
5.3 形态学应用
均采用内置函数:cv::morphologyEx()——实现图像腐蚀,膨胀,开、闭、顶帽、黑帽、
开运算 先腐蚀后膨胀
作用:去除图像中的噪声,消除较小连通域,保留较大连通域;同时能够在两个物体纤细的连接处将两个物体分离,并且在不明显改变较大连通域面积的同时能够平滑连通域的边界
步骤:首先对图像进行腐蚀,消除图像中的噪声和较小的连通域;然后进行膨胀弥补较大连通域因腐蚀造成的面积缩小
闭运算 先膨胀后腐蚀
作用:去除连通域内的小型空洞,平滑物体轮廓,连接两个临近的连通域
步骤:首先对图像进行膨胀,填充连通域内的小型空洞,扩大连通域的边界,将邻近的两个连通域连接;然后通过腐蚀运算减少由膨胀运算引起的连通域边界的扩大以及面积的增加
形态学梯度
形态学梯度能够描述目标的边界,是根据图像腐蚀和膨胀与原图之间的关系计算得到的。分为:
- 基本梯度:为原图像膨胀后图像与腐蚀后图像间的差值图像
- 内部梯度:为原图像与腐蚀后图像之间的差值图像
- 外部梯度:为膨胀后图像与原图像之间的差值图像
顶帽运算
定义:顶帽运算时原图像与开运算结果之间的差值
作用:常用于分离比邻近点亮一些的斑块
黑帽运算
定义:黑帽运算时原图像与闭运算结果之间的差值
作用:常用于分离比邻近点暗一些的斑块
击中击不中变换
定义:击中击不中变换要求原图像中需要存在与结构元素一摸一样的结构。
图像细化
定义:图像细化是将图像的线条从多像素宽度减少到单位像素宽度的过程
作用:常用于文字识别中,能够有效将文字细化,增加文字的可辨识度
分类:
- 迭代细化算法——通过重复删除图像边缘处满足一定条件的像素,最终得到单位像素宽度”骨架“的算法
- 串行细化算法:每次迭代都用固定的次序检查像素来判断是否删除像素。它不但取决于前次迭代的结果,还取决于本次迭代中已处理过像素点分布情况
- 并行细化算法:所有像素能在每次迭代中以并行的方式独立地被检测,即像素点删除与否与像素值图像中的顺序无关,仅取决于本次迭代的结果
- 非迭代细化算法——不以像素为基础,该方法经过一次遍历,产生线条的某一中值或中心线,而不检查所有单个像素
内置函数:cv::ximgproc::thinning() 使用时需要包含头文件 #include<opencv2\ximgproc.h>
6 目标检测
通过检测形状确定目标的位置,并通过对目标大小、位置等信息的处理进一步理解图像中的重要信息。
6.1 形状检测
直线检测 ——霍夫变换
霍夫变换通过将图像中的像素在一个空间坐标系中变换到另一个空间坐标系中,使得在原空间中具有相同特性的曲线或者直线映射到另一个空间中形成峰值,从而把检测任意形状的问题转换为统计峰值的问题
霍夫变换的两个重要结论:
- 图像空间中的每条直线在参数空间中都对应着单独一个点来表示
- 图像空间中的直线上任何像素点在参数空间对应的直线都相交于同一个点
霍夫变换步骤:
- 将参数空间的坐标轴离散化,例如 θ = 0°, 10°, 20°, …, r = 0.1, 0.2, 0.3, …。
- 将图像中每个非零像素通过映射关系求取在参数空间通过的方格
- 统计参数空间内每个方格出现的次数,选取次数大于某一阈值的方格作为表示直线的方格
- 将参数空间中表示直线的方格的参数作为图像中直线的参数
优点:具有抗干扰能力强,对图像中直线的残缺部分、噪声以及其它共存的非直线结构不敏感,能容忍特征边界描述中的间隙,并且相对不受图像噪声影响
缺点:时间复杂度和空间复杂度都很高,并且检测精度受参数离散间隔制约
内置函数:
- cv::HoughLines()——标准霍夫变换和多尺度霍夫变换法,用于寻找图像中的直线,并以极坐标的形式将图像中直线的极坐标参数输出
- cv::HoughLinesP()——渐进概率式霍夫变换法,阔以得到图像中满足条件的直线或者线段两个端点的坐标,进而确定直线或者线段的位置
- cv::HoughLinesPointSet()——在含有坐标的二维点的集合中寻找直线,检测方法为标准霍夫变换法
注: 必须是CV_8U类型的单通道二值图像,如果需要检测彩色图像或者灰度图像中是否存在直线,阔以通过Canny()函数计算图像的边缘,并将边缘检测结果二值化后的图像作为输入图像进行检测
#include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std; void drawLine(Mat &img, //要标记直线的图像 vector<Vec2f> lines, //检测的直线数据 double rows, //原图像的行数(高) double cols, //原图像的列数(宽) Scalar scalar, //绘制直线的颜色 int n //绘制直线的线宽 ) { Point pt1, pt2; for (size_t i = 0; i < lines.size(); i++) { float rho = lines[i][0]; //直线距离坐标原点的距离 float theta = lines[i][1]; //直线过坐标原点垂线与x轴夹角 double a = cos(theta); //夹角的余弦值 double b = sin(theta); //夹角的正弦值 double x0 = a*rho, y0 = b*rho; //直线与过坐标原点的垂线的交点 double length = max(rows, cols); //图像高宽的最大值 //计算直线上的一点 pt1.x = cvRound(x0 + length * (-b)); pt1.y = cvRound(y0 + length * (a)); //计算直线上另一点 pt2.x = cvRound(x0 - length * (-b)); pt2.y = cvRound(y0 - length * (a)); //两点绘制一条直线 line(img, pt1, pt2, scalar, n); } } int main() { Mat img = imread("HoughLines.jpg", IMREAD_GRAYSCALE); if (img.empty()) { cout << "请确认图像文件名称是否正确" << endl; return -1; } Mat edge; //检测边缘图像,并二值化 Canny(img, edge, 80, 180, 3, false); threshold(edge, edge, 170, 255, THRESH_BINARY); //用不同的累加器进行检测直线 vector<Vec2f> lines1, lines2; HoughLines(edge, lines1, 1, CV_PI / 180, 50, 0, 0); HoughLines(edge, lines2, 1, CV_PI / 180, 150, 0, 0); //在原图像中绘制直线 Mat img1, img2; img.copyTo(img1); img.copyTo(img2); drawLine(img1, lines1, edge.rows, edge.cols, Scalar(255), 2); drawLine(img2, lines2, edge.rows, edge.cols, Scalar(255), 2); //显示图像 imshow("edge", edge); imshow("img", img); imshow("img1", img1); imshow("img2", img2); waitKey(0); return 0; }
直线拟合
定义:已知获取到的数据在一条直线上,需要将所有数据拟合出一条直线
内置函数:cv::fitLine()——利用最小二乘法拟合出距离所有点最近的直线,直线描述形式阔以转换为点斜式
圆形检测
原理:同霍夫直线检测,将图像空间投影到像素空间,寻找是否存在交点
内置函数:cv::HoughCircles()——必须是单通道的灰度图像,进行检测是否存在圆形;
6.2 轮廓检测
定义
图像轮廓是指图像中对象的边界,是图像目标的外部特征,这个特征对于图像分析、目标识别和理解更深层次的含义具有重要作用
轮廓发现与绘制
图像的轮廓不但能够提供物体的边缘,而且能够提供物体边缘之间的层次关系及拓扑关系
内置函数:
- cv::findContours()——用于检测图像中的轮廓信息,并输出个轮廓之间的结构信息
- cv::drawContours()——绘制findContours()检测到的图像轮廓
#include <opencv2\opencv.hpp> #include <iostream> #include <vector> using namespace cv; using namespace std; int main() { system("color F0"); //更改输出界面颜色 Mat img = imread("keys.jpg"); if (img.empty()) { cout << "请确认图像文件名称是否正确" << endl; return -1; } imshow("原图", img); Mat gray, binary; cvtColor(img, gray, COLOR_BGR2GRAY); //转化成灰度图 GaussianBlur(gray, gray, Size(13, 13), 4, 4); //平滑滤波 threshold(gray, binary, 170, 255, THRESH_BINARY | THRESH_OTSU); //自适应二值化 // 轮廓发现与绘制 vector<vector<Point>> contours; //轮廓 vector<Vec4i> hierarchy; //存放轮廓结构变量 findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point()); //绘制轮廓 for (int t = 0; t < contours.size(); t++) { drawContours(img, contours, t, Scalar(0, 0, 255), 2, 8); } //输出轮廓结构描述子 for (int i = 0; i < hierarchy.size(); i++) { cout << hierarchy[i] << endl; } //显示结果 imshow("轮廓检测结果", img); waitKey(0); return 0; }
轮廓面积
cv::contourArea()——用于统计轮廓像素点围成区域的面积
findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point()); //输出轮廓面积 for (int t = 0; t < contours.size(); t++) { double area1 = contourArea(contours[t]); cout << "第" << t << "轮廓面积=" << area1 << endl; }
轮廓长度(周长)
cv::arcLength()——用于统计轮廓周长或者曲线的长度
轮廓外接多边形
寻找轮廓外接最大矩形就是寻找轮廓X方向和Y方向两端的像素
内置函数:
- cv::boundingRect()——求取包含输入图像或者二维点集的最大外接矩形
- cv::minAreaRect()——求取包含输入图像或者二维点集的最小外接矩形
- cv::approxPolyDP()——寻找逼近轮廓的多边形
点到轮廓距离
作用:对于计算轮廓在图像中的位置、两个轮廓之间的距离以及确定图像上某一点是否在轮廓内部具有重要的作用
内置函数:cv::pointPolygonTest()——计算指定像素点距离轮廓的最小距离并以double类型的数据返回
凸包检测
定义:将二维平面上的点集最外层的点连接起来构成的凸多边形称为凸包
内置函数:cv::convexHull()——用于寻找二维点集或者轮廓的凸包
// 轮廓发现 vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(binary, contours, hierarchy, 0, 2, Point()); for (int n = 0; n < contours.size(); n++) { //计算凸包 vector<Point> hull; convexHull(contours[n], hull); //绘制凸包 for (int i = 0; i < hull.size(); i++) { //绘制凸包顶点 circle(img, hull[i], 4, Scalar(255, 0, 0), 2, 8, 0); //连接凸包 if (i == hull.size() - 1) { line(img, hull[i], hull[0], Scalar(0, 0, 255), 2, 8, 0); break; } line(img, hull[i], hull[i + 1], Scalar(0, 0, 255), 2, 8, 0); } } imshow("hull", img);
6.3 矩的计算
定义
矩是描述图像特征的算子
几何矩与中心矩
定义:
几何矩是图片中对目标区域边界在空间域的分析方法之一,不同阶级的几何矩代表着图片不同的物理性质
- 零阶矩(m00):与图像或某个轮廓的面积相关;
- 一阶矩(m01,m10):与图像或某个轮廓的质心相关;
- 二阶矩(m02,m11,m20):与图像或某个轮廓的旋转半径相关;
- 三阶矩(m03,m12,m21,m30):与图像或某个轮廓的斜度或扭曲程度相关。
中心距的结果,会一直围绕质心为中心来计算,也就是说会以图像或轮廓中的每个点到质心的距离来计算,所以当图像或轮廓发生平移时,中心距不会发生改变。即中心距具有空间不变性,针对只进行平移的图像或者轮廓,可以用中心距来作为它的一个特征。
内置函数:cv::moments()——用于计算图像连通域的几何矩和中心矩,以及归一化的几何矩
Hu矩
- Hu 矩具有旋转、平移和缩放不变性
- Hu 矩是由二阶和三阶中心矩计算得到 7 个不变矩
内置函数:cv::HuMoments()——根据图像的中心距计算图像的Hu矩
基于Hu矩的轮廓匹配
内置函数:cv::matchShapes()——用于实现在图像或者轮廓中寻找与模板图像或者轮廓像素匹配的区域
6.4 点集拟合
针对情况
关注的区域是一些面积较小、数目较多的连通域或者像素点,并且这些区域相对集中。
将这些连通域或者像素点看成一个大的区域,寻找包围这些区域的规则图形,例如三角形、圆形等。
内置方法
minEnclosingTriangle()——寻找二维点集的最小包围三角形
minEnclosingCircle()——寻找二维点集的最小包围圆形
6.5 QR二维码检测
步骤
搜索二维码的位置探测图形
对二维码进行解码,提取其中的信息
内置函数
detect():定位QR二维码;得到四个顶点。
decode():根据定位结果解码二维码
detectAndDecode():同时定位和解码
7 图像分析与修复
在存储或者使用图像过程中,有时可能会收到“污染“,遮盖住部分图像,对图像分析造成影响,因此,在处理图像前,需要将图像进行修复,将”污染”部分去掉
7.1 傅里叶变换
离散傅里叶变换
离散傅里叶变换是指傅里叶变换在时域和频域上都呈现离散的形式,将时域信号的采样变成离散时间傅里叶变换频域的采样。
即将任意形式的连续函数或者离散函数分解成多个正弦或者余弦函数相加的形式。
图像中像素波动较大的区域对应的频域是高频区域,因此高频区域体现的是图像的细节、纹理信息,而低频信息代表了图像的轮廓信息。
内置函数
- dft():能够直接对图像进行离散傅里叶变换
- idft():离散傅里叶变换的逆变换
- getOptimaIDFTSize():用于计算最优的输入矩阵的尺寸
- copyMakeBorder():用于在图像周围形成外框
- magnitude():用于计算由两个矩阵组成的二维向量矩阵的幅值矩阵
傅里叶变换进行卷积
傅里叶变换可以将两个矩阵的卷积转换成两个矩阵傅里叶变换结果的乘积,可以极大地提高卷积的计算速度
图像傅里叶变换结果都是具有复数共轭对称性的复数矩阵,两个矩阵相乘需要计算对应位置的两个复数乘积
内置函数:cv::mulSpectrums()——实现两个离散傅里叶变换后的矩阵每个元素之间的乘法
离散余弦变换
变换过程中只使用实数
用途:常用于对信号和图像的有损数据压缩
内置函数:
- cv::dct()——对一维或二维的数据进行正向和逆向的离散余弦变换
- cv::idct()——对一维或二维的数据进行离散余弦变换的逆变换
7.2 积分图像
用途
积分图像主要用于快速计算图像某些区域像素的平均灰度
积分图像能够降低图像模糊、边缘检测和对象检测的计算量,提高图像分析的速度
定义
积分图像中每个像素的像素值为原图像中该像素点与坐标原点组成的矩形内所有像素值的和
类似于算法中的前缀和
内置函数:cv::integral()——实现图像的标准求和积分、平方求和积分及倾斜求和积分
7.3 图像分割
定义
图像分割是指将图像中属于某一类的像素点与其他像素点分开
漫水填充法
定义:漫水填充法是根据像素灰度值之间的差值寻找相同区域以实现分割
步骤:
- 选择种子点(x, y)
- 以种子点为中心,判断4-邻域或者8-邻域的像素值与种子点像素值的差值,将差值小于阈值的像素点添加进区域内
- 将新加入的像素点作为新的种子点,反复执行第二步,直到没有新的像素点被添加进该区域为止
内置函数:cv::floodFill()——根据给定像素点的像素值,寻找邻域内与其像素值接近的区域
分水岭法
定义:分水岭法会在多个局部最低点开始注水,随着注水量的增加,水位越来越高,“淹没”局部像素值较小的像素点,最后两个相邻的凹陷区域的“水“会汇集在一起,并在汇集处形成了”分水岭“
步骤:
- 排序过程。首先对图像像素的灰度值进行排序,确定灰度值较小的像素点,该像素点即为开始注水点
- ”淹没“过程。对每个最低点开始不断”注水“,不断”淹没“周围的像素点,不同”注水“处的”水“汇集在一起,形成分割线
内置函数:cv::watershed()——根据期望标记结果实现图像分水岭分割
Grabcut法
定义:使用高斯混合模型估计目标区域的背景和前景,使用迭代的方法解决了能量函数最小化的问题,使得结果具有更高的可靠性
内置函数:cv::grabCut()
Mean-Shift法
定义:均值漂移法是一种基于颜色空间分布的图像分割算法
原理:Mean-Shift法中每个像素点用一个五维向量(x, y, b, g, r)表示;从颜色分布的峰值处开始,通过滑动窗口不断寻找属于同一类的像素点并统一像素点的像素值;由于分割后同一类像素点具有相同像素值,因此Mean-Shift算法的输出结果是一个颜色渐变、纹理平缓的图像
内置函数:
cv::pyrMeanShiftFiltering()——基于彩色图像的像素值实现对图像的分割,输入必须是CV_8U3C的三通道彩色图像。
注: 会使用到opencv中表示迭代算法终止条件的数据类型TermCriteria()
7.4 图像修复
定义
图像修复技术就是利用图像中损坏区域边缘的像素,即根据像素值的大小以及像素间的结构关系,估计处损坏区域可能的像素排列,从而去除图像中受”污染“的区域
内置函数
cv::inpaint()——该函数利用图像修复算法对图像中指定的区域进行修复,函数无法判断哪些区域需要修复,因此在使用过程中需要明确指出需要修复的区域
#include <opencv2\opencv.hpp> #include <iostream> using namespace cv; using namespace std; int main() { Mat img1 = imread("inpaint1.png"); Mat img2 = imread("inpaint2.png"); if (img1.empty() || img2.empty()) { cout << "请确认图像文件名称是否正确" << endl; return -1; } imshow("img1", img1); imshow("img2", img2); //转换为灰度图 Mat img1Gray, img2Gray; cvtColor(img1, img1Gray, COLOR_RGB2GRAY, 0); cvtColor(img2, img2Gray, COLOR_RGB2GRAY, 0); //通过阈值处理生成Mask掩模 Mat img1Mask, img2Mask; threshold(img1Gray, img1Mask, 245, 255, THRESH_BINARY); threshold(img2Gray, img2Mask, 245, 255, THRESH_BINARY); //对Mask膨胀处理,增加Mask面积 Mat Kernel = getStructuringElement(MORPH_RECT, Size(3, 3)); dilate(img1Mask, img1Mask, Kernel); dilate(img2Mask, img2Mask, Kernel); //图像修复 Mat img1Inpaint, img2Inpaint; inpaint(img1, img1Mask, img1Inpaint, 5, INPAINT_NS); inpaint(img2, img2Mask, img2Inpaint, 5, INPAINT_NS); //显示处理结果 imshow("img1Mask", img1Mask); imshow("img1修复后", img1Inpaint); imshow("img2Mask", img2Mask); imshow("img2修复后", img2Inpaint); waitKey(); return 0; }
8 特征点检测与匹配
定义:能够表示图像特性或者局部特性的像素点称为角点或特征点
8.1 角点检测
定义
角点是图像中某些属性较为突出的像素点;如像素值的最大或者最小的点、线段的顶点、孤立的边缘点等
常用的角点
- 灰度梯度的最大值对应的像素点
- 两条直线或者曲线的交点
- 一阶梯度的导数最大值和梯度方向变化率最大的像素点
- 一阶导数值最大,但是二阶导数值为0的像素点
显示关键点
定义:关键点是对图像中含有特殊信息的像素点的一种称呼,主要含有像素点的位置、角度等信息
内置函数:cv::drawKeypoints()——一次性在图像中绘制所有的关键点,以关键点为圆心绘制空心圆,以突出显示关键点在图像中的位置;关键点用KeyPoint类表示
Harris角点检测
定义:Harris角点是从像素值变化的角度对角点进行定义,像素值的局部最大峰值即为Harris角点
检测过程:
- 首先以某一个像素为中心构建一个矩形滑动窗口。滑动窗口覆盖的图像像素值通过线性叠加得到滑动窗口所有像素值的衡量系数,该系数与滑动窗口范围内的像素值成正比,当滑动窗口范围内像素值整体变大时,该衡量系数也变大。
- 在图像中以每一个像素为中心向各个地方移动滑动窗口,当滑动窗口无论向哪个方向移动像素值衡量系数都缩小时,滑动窗口中心点对应的像素点即为Harris角点
用途:主要用于检测图像中线段的端点或者两条线段的交点
注: 像素值衡量系数为梯度协方差矩阵的特征向量;越大越相似
内置函数:cv::cornerHarris()——能够计算处图像中每个像素点的Harris评价系数,通过对该系数大小的比较,去欸的那个该点是否为Harris角点;
注: 由于其取值范围较广且有正有负,常需通过normalize()函数将其归一化到指定区域后,再通过阈值比较判断像素点是否为Harris角点。
缺点:Harris角点评价系数时两个特征向量的组合,并不能完全地概括两个特征向量之间的大小关系
Shi-Tomas角点检测
定义:将特征向量的最小值作为角点评价系数;本质上是Harris角点的一种变形
内置函数:goodFeaturesToTrack()——寻找图像中指定区域内的Shi-Tomas角点
亚像素级别角点检测
原理:寻找一点,其指向邻域范围内每一个像素点的向量与该像素点的梯度向量的乘积之和最小
内置函数:cv::cornerSubPix()——能够根据角点位置的初始坐标,通过不断迭代得到优化后的亚像素级别的角点坐标
#include <opencv2/opencv.hpp> #include <iostream> #include <string> using namespace cv; using namespace std; int main() { system("color F0"); //改变DOS界面颜色 Mat img = imread("lena.png",IMREAD_COLOR); if (!img.data) { cout << "读取图像错误,请确认图像文件是否正确" << endl; return -1; } //彩色图像转成灰度图像 Mat gray; cvtColor(img, gray, COLOR_BGR2GRAY); //提取角点 int maxCorners = 100; //检测角点数目 double quality_level = 0.01; //质量等级,或者说阈值与最佳角点的比例关系 double minDistance = 0.04; //两个角点之间的最小欧式距离 vector<Point2f> corners; goodFeaturesToTrack(gray, corners, maxCorners, quality_level, minDistance, Mat(), 3, false); //计算亚像素级别角点坐标 vector<Point2f> cornersSub = corners; //角点备份,方式别函数修改 Size winSize = Size(5, 5); Size zeroZone = Size(-1, -1); TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 40, 0.001); cornerSubPix(gray, cornersSub , winSize, zeroZone, criteria); //输出初始坐标和精细坐标 for (size_t i = 0; i < corners.size(); i++) { string str = to_string(i); str = "第" + str + "个角点点初始坐标:"; cout << str << corners[i] << " 精细后坐标:" << cornersSub[i] << endl; } return 0; }
8.2 特征点检测
定义
特征点是能够表现图像中局部特征的像素点,通常特征点由关键点和描述子组成
关键点
关键点类KeyPoint,可以存放关键点的坐标、方向等相关数据
Features2D虚类:类中定义了检测特征点时需要的关键点检测函数、描述子计算函数、描述子类数据类型及读写操作等函数
只要其他某个特征点类继承了Features2D类,就可以通过其中的函数计算关键点和描述子
内置函数:cv::Features2D::detect()——根据需要计算不同种特征点中的关键点;需要再特征点具体类中才能使用
描述子
定义:描述子是用来描述关键点的一串数字,与每个人的个人信息类似,通过描述子可以区分两个不同的关键点,也可以在不同的图像中寻找同一个关键点
内置函数:
- cv::Features2D::compute()——根据输入图像和指定图像中的关键点坐标计算得到每个关键点的描述子
- cv::Features2D::detectAndCompute()——将计算关键点和描述子两个功能集成在一起,可以根据输入图像直接计算出关键点和关键点对应的描述子
SIFT特征点检测
步骤:
- 计算SIFT特征点首先需要构建多尺度高斯“金字塔”。将图片按照组和层进行划分,不同组内的图片大小不同,小尺寸图片由大尺寸图片下采样得到;同一组内图片大小相同,但在下采样时使用不同标准差的高斯卷积核,层数越高,标准差越大
- 对同一组内的相邻图片进行相减操作,构建高斯差分“金字塔”
关键点是由高斯差分空间的局部极值点组成的
关键点的初步检测是通过同一组内各高斯差分空间相邻两层图像之间比较完成的。离散的像素点
关键点的进一步检测是通过在关键点附近进行泰勒展开实现亚像素级别的定位,之后进行筛选,剔除噪声和边缘效应
SURF特征点检测
定义:其直接使用方框滤波器去逼近高斯差分空间
优点:可以借助积分图像轻松地计算出方框滤波器的卷积,进而加速
在SURF特征点中,不同组间图像的尺寸都是相同的,但不同组使用的方框滤波器的尺寸逐渐增大;同一组内不同层间使用相同尺寸的滤波器,但是滤波器的模糊系数逐渐增大
ORB特征点检测
定义:ORB特征点由FAST角点和BRIEF描述子组成,首先通过FAST角点确定图像中与周围像素存在明显差异的像素点作为关键点,之后计算每个关键点的BRIEF描述子,从而唯一确定ORB特征点
FAST角点
- 通过比较图像像素灰度值变化确定关键点
- 核心思想:如果某个灰度值较小的区域中存在一个灰度值明显较大的像素点,那么该像素点在这个区域中具有明显的特征,可以作为特征点
- FAST角点不具有尺度不变性和旋转不变性;
- FAST角点容易集中出现,需要采用非极大值抑制算法
ORB特征点尺度不变性和旋转不变性
- ORB特征点通过构建图像“金字塔”,并在每一层分别提取FAST角点,在多层图像中能检测到的FAST角点具有尺度不变性,ORB特征点只保留具有尺度不变性的FAST角点
- ORB特征点提出由FAST角点指向周围矩形区域质心的向量作为ORB特征点的方向向量,由方向向量可以得到特征点的方向,从而解决旋转不变性
BRIEF描述子
- 用于描述特征点周围像素灰度值的变化趋势
- BRIEF描述子会按照一定分布规律随机比较特征点周围两个像素点p和q的灰度值大小。p>q用1表示,p<q用0表示
示例
#include <opencv2\opencv.hpp> #include <iostream> #include <vector> using namespace std; using namespace cv; int main() { Mat img = imread("lena.png"); if (!img.data) { cout << "请确认图像文件名称是否正确" << endl; return -1; } //创建 ORB 特征点类变量 Ptr<ORB> orb = ORB::create(500, //特征点数目 1.2f, //金字塔层级之间的缩放比例 8, //金字塔图像层数系数 31, //边缘阈值 0, //原图在金字塔中的层数 2, //生成描述子时需要用的像素点数目 ORB::HARRIS_SCORE, //使用 Harris 方法评价特征点 31, //生成描述子时关键点周围邻域的尺寸 20 //计算 FAST 角点时像素值差值的阈值 ); //计算 ORB 关键点 vector<KeyPoint> Keypoints; orb->detect(img, Keypoints); //确定关键点 //计算 ORB 描述子 Mat descriptions; orb->compute(img, Keypoints, descriptions); //计算描述子 //绘制特征点 Mat imgAngel; img.copyTo(imgAngel); //绘制不含角度和大小的结果 drawKeypoints(img, Keypoints, img, Scalar(255, 255, 255)); //绘制含有角度和大小的结果 drawKeypoints(img, Keypoints, imgAngel, Scalar(255, 255, 255), DrawMatchesFlags::DRAW_RICH_KEYPOINTS); //显示结果 imshow("不含角度和大小的结果", img); imshow("含有角度和大小的结果", imgAngel); waitKey(0); return 0; }
8.3 特征点匹配
定义
特征点匹配就是在不同的图像中寻找同一个物体的同一个特征点。也就是在两个图像中寻找具有相似描述子的两个特征点
用于匹配特征点描述子集合分别称为查询描述子和训练描述子
相似性度量
- 计算两个描述子之间的欧氏距离;如SIFT、SURF
- 计算两个描述子之间的汉明距离;如ORB、BRISK
内置函数
- cv::DescriptorMatcher::match()——根据输入的两个特征点描述子集合,计算出两个特征点集合里最佳匹配的一一对应的描述子
- cv::DMatch{}——用于存放特征点描述子匹配关系的类型,存放着两个描述子之间的距离以及在自己集合中的索引
- cv::DescriptorMatcher::knnMatch()——可以在训练子集合中寻找k个查询描述子最佳匹配的描述子;匹配数<=k个
- cv::DescriptorMatcher::radiusMatch()——可以在训练子集合中寻找与查询描述子之间距离小于阈值的描述子
注: 以上函数均需要DescriptorMatcher类被其他类继承之后才能使用,即只有在特征点匹配的类中才能使用
暴力匹配
定义:暴力匹配就是计算训练描述子集合中每个描述子与查询描述子之间的距离,之后将所有距离排序,选择距离最小或者距离满足阈值要求的描述子作为匹配结果
内置函数:cv::BFMatcher::BFMatcher()——初始化暴力匹配类
显示特征点结果
内置函数:cv::drawMatches()——将两幅图像匹配成功的特征点通过直线连接,将没有匹配成功的特征点用圆圈显示
FLANN匹配
定义:快速最近邻搜索库(FLANN)用于实现特征点的高校匹配
内置函数:cv::FlannBasedMatcher::FlannBasedMatcher()——初始化FLANN匹配类
RANSAC优化点匹配
定义:RANSAC算法是随机抽样一致算法的简称,该算法假设所有数据符合一定的规律,通过随机抽样的方式获取这个规律,并且通过重复获取规律寻找使得较多数据符合的规律
步骤:
- 在匹配结果中随机选取4对特征点,计算单应矩阵
- 将第一帧图像中的特征点根据单应矩阵求取在第二帧图像中的重投影坐标,比较重投影坐标与已匹配的特征点坐标之间的距离,如果小于一定阈值,则为正确匹配,并记录正确匹配点对的数量
- 重复第一步和第二步,比较多次循环后统计的正确匹配点对的数量,将正确匹配点对数量最多的情况作为最终结果,剔除错误匹配,输出正确匹配
内置函数:cv::findHomography()——主要用于计算两幅图像间的单应矩阵,但是利用RANSAC算法计算单应矩阵的同时可以计算满足单应矩阵的特征点对,因此可以用来优化特征点匹配
#include <iostream> #include <opencv2\opencv.hpp> #include <vector> using namespace std; using namespace cv; void match_min(vector<DMatch> matches, vector<DMatch> & good_matches) { double min_dist = 10000, max_dist = 0; for (int i = 0; i < matches.size(); i++) { double dist = matches[i].distance; if (dist < min_dist) min_dist = dist; if (dist > max_dist) max_dist = dist; } cout << "min_dist=" << min_dist << endl; cout << "max_dist=" << max_dist << endl; for (int i = 0; i < matches.size(); i++) if (matches[i].distance <= max(2 * min_dist, 20.0)) good_matches.push_back(matches[i]); } //RANSAC算法实现 void ransac(vector<DMatch> matches, vector<KeyPoint> queryKeyPoint, vector<KeyPoint> trainKeyPoint, vector<DMatch> &matches_ransac) { //定义保存匹配点对坐标 vector<Point2f> srcPoints(matches.size()), dstPoints(matches.size()); //保存从关键点中提取到的匹配点对的坐标 for (int i = 0; i<matches.size(); i++) { srcPoints[i] = queryKeyPoint[matches[i].queryIdx].pt; dstPoints[i] = trainKeyPoint[matches[i].trainIdx].pt; } //匹配点对进行RANSAC过滤 vector<int> inliersMask(srcPoints.size()); //Mat homography; //homography = findHomography(srcPoints, dstPoints, RANSAC, 5, inliersMask); findHomography(srcPoints, dstPoints, RANSAC, 5, inliersMask); //手动的保留RANSAC过滤后的匹配点对 for (int i = 0; i<inliersMask.size(); i++) if (inliersMask[i]) matches_ransac.push_back(matches[i]); } void orb_features(Mat &gray, vector<KeyPoint> &keypionts, Mat &descriptions) { Ptr<ORB> orb = ORB::create(1000, 1.2f); orb->detect(gray, keypionts); orb->compute(gray, keypionts, descriptions); } int main() { Mat img1 = imread("box.png"); //读取图像,根据图片所在位置填写路径即可 Mat img2 = imread("box_in_scene.png"); if (!(img1.data && img2.data)) { cout << "读取图像错误,请确认图像文件是否正确" << endl; return -1; } //提取ORB特征点 vector<KeyPoint> Keypoints1, Keypoints2; Mat descriptions1, descriptions2; //基于区域分割的ORB特征点提取 orb_features(img1, Keypoints1, descriptions1); orb_features(img2, Keypoints2, descriptions2); //特征点匹配 vector<DMatch> matches, good_min,good_ransac; BFMatcher matcher(NORM_HAMMING); matcher.match(descriptions1, descriptions2, matches); cout << "matches=" << matches.size() << endl; //最小汉明距离 match_min(matches, good_min); cout << "good_min=" << good_min.size() << endl; //用ransac算法筛选匹配结果 ransac(good_min, Keypoints1, Keypoints2, good_ransac); cout << "good_matches.size=" << good_ransac.size() << endl; //绘制匹配结果 Mat outimg, outimg1, outimg2; drawMatches(img1, Keypoints1, img2, Keypoints2, matches, outimg); drawMatches(img1, Keypoints1, img2, Keypoints2, good_min, outimg1); drawMatches(img1, Keypoints1, img2, Keypoints2, good_ransac, outimg2); imshow("未筛选结果", outimg); imshow("最小汉明距离筛选", outimg1); imshow("ransac筛选", outimg2); waitKey(0); //等待键盘输入 return 0; //程序结束 }