文章目录
- 笔记_4
- 图像细化
- thinning 图像细化函数
- 轮廓检测
- findContours 轮廓检测函数
- drawContours 轮廓绘制函数
- contourArea 计算轮廓面积:返回值 double类型
- arcLength 计算轮廓长度:返回值 double类型
- 轮廓外接多边形
- boundingRect 给定轮廓的外接矩形
- minAreaRect 给定轮廓的最小外接矩形
- approxPolyDP
- convexHull 二维凸包检测函数
- 直线检测
- HoughLines 霍夫变换 - 找直线 -输出(rho,theta)
- HoughLinesP 霍夫变换 - 找直线-输出坐标
- 点集拟合
- fitLine 拟合直线的函数
- minEnclosingTriangle 拟合最小外接三角形
- minEnclosingCircle 拟合最小外接圆
- 二维码(QR code)检测
- QRCodeDetector::detect 定位二维码,输出四个角点
- QRCodeDetector::decode 解码二维码
- QRCodeDetector::detectAndDecode 定位并解码二维码,返回解码后的字符串数据,失败返回空字符串
- 积分图像
- integral 计算积分图像函数 -- 作用 ,加快计算速度
- 图像分割
- 漫水填充算法
- floodFill 漫水填充分割函数 -- 建立种子点(seedPoint),每次输出区域为一个区域
- 分水岭法
- watershed 分水岭分割图像
- 角点获取
- Harris 海瑞斯角点检测
- cornerHarris 函数 -- Harris角点检测
- drawKeypoints 绘制角点函数
- KeyPoint
- Shi-Tomas 托马斯角点检测 -- 和Harris角点检测不同的是:评价系数R不同,其他都一样
- goodFeaturesToTrack 托马斯角点检测函数(同时也支持Harris角点检测)
- 亚像素级别角点位置优化
- cornerSubPix 亚像素角点优化提取
- 特征点获取
- 举例 - OBR特征点
- ORB特征点创建
- detectAndCompute 计算关键点和描述子函数
笔记_4
图像细化
thinning 图像细化函数
图像细化的典型用途就是文字识别。
图像细化的算法第一类是非迭代细化算法,分为两种:
- 基于距离的细化算法
- 游程长度编码
不用深究,它们的优点是速度快,缺点是容易产生噪声并且OpenCV库没有内置。
图像细化的第二类算法是迭代算法,也分为两种:
- 串行算法
- 并行算法
CV::ximgproc::thinning()函数实现了两种细化算法,THINNING_ZHANGSUEN为上面说的并行算法,THINNING_GUOHALL为上面说的串行算法;串行算法和并行算法的共同点是进行第n次迭代时都需要第n-1次处理的结果,区别是串行算法还需要本次迭代已经处理过的像素点情况。也就是并行的迭代算法,理论上可以在每一步多开n个线程增加处理速度,以空间换时间。
至于OpenCV内置的这两种细化算法的速度差异,有待大家验证了。
参数说明:
- `src`:输入的二值图像,要求为单通道、8位深度的Mat类型。
- `dst`:输出的细化结果图像,为单通道、8位深度的Mat类型。
- `thinningType`:细化算法的类型,有两种取值:`THINNING_ZHANGSUEN`和`THINNING_GUOHALL`,分别代表Zhang-Suen和Guo-Hall两种细化算法,默认为Zhang-Suen算法。
算法原理:
该函数的算法原理是基于模板匹配的思想,即对二值图像进行迭代处理,每次处理时,用预定义的模板与当前像素的邻域进行匹配,根据匹配结果更新像素的值。经过多次迭代后,最终得到细化结果。
Zhang-Suen算法和Guo-Hall算法是两种常见的细化算法。其中,Zhang-Suen算法是较为简单的一种,它采用了两个迭代过程,每次迭代都对图像进行一次扫描,分别更新黑色像素和白色像素。Guo-Hall算法则相对复杂,它采用了八个不同的模板,每个模板分别匹配当前像素的不同邻域情况,以实现更加精细的细化效果。
代码演示:
#include <opencv2/ximgproc.hpp> // 需要添加OpenCV扩展库
using namespace cv;
using namespace std;
int main()
{
//创建黑色背景
Mat words = Mat::zeros(100, 200, CV_8UC1);
putText(words, "Learn", Point(30, 30), 2, 1,Scalar(255),2);
putText(words, "OpenCV 4", Point(30, 60), 2, 1, Scalar(255), 2);
//添加实心圆
circle(words, Point(80, 75), 10, Scalar(255), -1);
//添加圆环
circle(words, Point(130, 75), 10, Scalar(255), 3);
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/learnCV.png", IMREAD_GRAYSCALE);
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat thin1, thin2, thin3, thin4;
//THINNING_ZHANGSUEN为上面说的并行算法,THINNING_GUOHALL为上面说的串行算法;
Mat thin1, thin2, thin3, thin4;
ximgproc::thinning(img, thin1, ximgproc::THINNING_ZHANGSUEN);
ximgproc::thinning(img, thin2, ximgproc::THINNING_GUOHALL);
ximgproc::thinning(words, thin3, ximgproc::THINNING_ZHANGSUEN);
ximgproc::thinning(words, thin4, ximgproc::THINNING_GUOHALL);
imshow("thin1", thin1);
imshow("thin2", thin2);
imshow("thin3", thin3);
imshow("thin4", thin4);
imshow("img", img);
imshow("words", words);
waitKey(0);
return 0;
}
轮廓检测
findContours 轮廓检测函数
函数原型如下:
void findContours(InputOutputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode, int method, Point offset = Point());
其中,各个参数的含义如下:
image
:输入图像,必须是单通道(灰度)图像,8 位无符号整型或浮点型。但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像;contours
:输出轮廓,为一个vector<vector<Point>>
类型的向量,每个元素表示一个轮廓,其中的元素为Point
类型的点,表示轮廓上的像素点。hierarchy
:输出轮廓的层次结构,为一个vector<Vec4i>
或者为Mat
类型的矩阵,每一行包含四个整数,分别表示后一个轮廓、前一个轮廓、子轮廓和父轮廓的索引,如果没有对应项,则为-1
。mode
:轮廓检索模式,有以下两种可选模式:RETR_EXTERNAL = 0
:只检索最外层的轮廓;RETR_LIST = 1
:检索所有轮廓,但不建立轮廓之间的层次关系;RETR_CCOMP = 2
:检索所有轮廓,将轮廓分为两级,即外层轮廓和内层轮廓;RETR_TREE = 3
:检索所有轮廓,并重建轮廓之间的层次关系。
method
:轮廓逼近方法,有以下两种可选方法:CHAIN_APPROX_NONE = 1
:存储所有的轮廓点,即不进行逼近处理;CHAIN_APPROX_SIMPLE = 2
:压缩水平、垂直和对角线方向上的冗余点,只保留端点;CHAIN_APPROX_TC89_L1 = 3
:运用 Teh-Chin 链逼近算法;CHAIN_APPROX_TC89_KCOS = 4
:运用 Teh-Chin 链逼近算法,并使用COS
函数进行角度的计算。
offset
:可选参数,表示轮廓中的点坐标需要加上的偏移量。
mode:(轮廓检索模式)改变会影响 hierarchy(输出轮廓的层次结构)的数据
method:(轮廓逼近方法)改变会影响 contours(输出轮廓)的数据
contours
参数是一个 vector<vector<Point>>
类型的数组,其中每个元素表示一个轮廓,每个轮廓都是一个 vector<Point>
类型的数组,表示轮廓上的像素点。
具体来说,vector<Point>
中的每个元素都是一个 cv::Point
类型的点,表示轮廓上的一个像素点的坐标。该点的坐标可以通过访问 x
和 y
成员变量来获取,例如 contours[i][j].x
和 contours[i][j].y
分别表示第 i
个轮廓上第 j
个像素点的 x 和 y 坐标。
在使用 drawContours
函数等绘制轮廓的函数时,需要将轮廓上的像素点存储在一个 vector<Point>
中,并将其作为函数的参数传递。这些像素点将被绘制成一条闭合的轮廓线。
drawContours 轮廓绘制函数
OpenCV中的drawContours()
函数用于在给定图像中绘制轮廓。函数的原型如下:
void drawContours(InputOutputArray image,
InputArrayOfArrays contours,
int contourIdx,
const Scalar& color,
int thickness = 1,
int lineType = LINE_8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX,
Point offset = Point())
参数说明:
image
: 要绘制轮廓的图像。这是一个输入输出参数,轮廓将被绘制在此图像上。contours
: 存储轮廓的向量。每个向量都是由点的坐标组成的向量,表示一个轮廓。contours
也可以是一个存储向量的向量,表示多个轮廓。contourIdx
: 表示要绘制的轮廓的索引。如果contourIdx
为负数,则表示绘制所有轮廓。color
: 轮廓的颜色。thickness
: 轮廓线条的宽度。如果为负数,则表示绘制一个填充的轮廓。lineType
: 轮廓线条的类型。hierarchy
: 轮廓的层次结构。如果不需要使用,则可以将其设置为默认值noArray()
。maxLevel
: 要绘制的层次结构的最大级别。offset
: 轮廓点的偏移量。
contourArea 计算轮廓面积:返回值 double类型
double contourArea(InputArray contour, bool oriented = false);
-
contour
:输入的轮廓。它可以是二维点向量、表示一个或多个轮廓的向量或Mat类型。 -
oriented
:布尔值,用于指示是否要考虑轮廓的方向。如果为true,则将考虑轮廓的方向,否则将忽略方向。默认值为false。 -
该函数返回输入轮廓的面积,单位为像素。
在OpenCV中,轮廓(contour)是由一系列的点组成的,这些点按照一定的顺序连接起来,形成了一个封闭的曲线。但是,这个曲线的方向可能是顺时针或逆时针的,这取决于绘制轮廓时所采用的方法。
在计算轮廓面积时,如果不考虑方向,则使用的是数学上的绝对值。也就是说,计算的结果只跟轮廓的形状有关,而与其方向无关。但是,如果考虑轮廓的方向,则使用的是带符号的面积,这意味着如果轮廓是顺时针方向的,则计算结果为负数,如果是逆时针方向的,则计算结果为正数。
因此,当我们使用contourArea
函数计算轮廓面积时,如果我们需要考虑轮廓的方向,则可以将oriented
参数设置为true。否则,可以将其设置为false或忽略该参数,默认情况下oriented
参数为false。
arcLength 计算轮廓长度:返回值 double类型
代码演示:
system("color F0"); //更改输出界面颜色
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
//克隆一个Mat
Mat imgg = img.clone();
//imshow("原图", img);
Mat gray, binary;
cvtColor(img, gray, COLOR_BGR2GRAY); //转化成灰度图
GaussianBlur(gray, gray, Size(9, 9), 2, 2); //平滑滤波
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, -1, Scalar(0, 0, 255), 2, LINE_8);
}
//绘制所有轮廓和drawContours 绘制结果一样,这里用line循环代替
for (int t = 0; t < contours.size(); t++)
{
vector<Point> temp = contours[t];
for (int i = 0; i < temp.size(); i++)
{
//首尾闭合
if (i == temp.size() - 1)
{
line(imgg, temp[0], temp[i], Scalar(0, 0, 255), 2, LINE_8, 0);
break;
}
line(imgg, temp[i], temp[i+1], Scalar(0, 0, 255), 2, LINE_8, 0);
}
}
//输出轮廓面积
for (int t = 0; t < contours.size(); t++)
{
double area = contourArea(contours[t],true);
cout << "第 " << t << " 轮廓面积=" << area << endl;
}
//输出轮廓长度
for (int t = 0; t < contours.size(); t++)
{
double length = arcLength(contours[t],true);
cout << "第 " << t << " 轮廓长度=" << length << endl;
}
//输出轮廓结构描述子
for (int i = 0; i < hierarchy.size(); i++)
{
cout << hierarchy[i] << endl;
}
//显示结果
imshow("img", img);
imshow("imgg", imgg);
waitKey(0);
输出打印:
第 0 轮廓面积=-4.5 第 1 轮廓面积=-6 第 2 轮廓面积=-3.5 第 3 轮廓面积=-16.5 第 4 轮廓面积=-14 第 5 轮廓面积=0 第 6 轮廓面积=-3.5 第 7 轮廓面积=-390 第 8 轮廓面积=-10.5 第 9 轮廓面积=-14 第 10 轮廓面积=-393.5 第 11 轮廓面积=-19.5 第 12 轮廓面积=-1.5 第 13 轮廓面积=-4 第 14 轮廓面积=-4.5 第 15 轮廓面积=0 第 16 轮廓面积=-3.5 第 17 轮廓面积=-1635 第 18 轮廓面积=11 第 19 轮廓面积=0 第 20 轮廓面积=-35 第 21 轮廓面积=-77727 第 22 轮廓面积=1667.5 第 23 轮廓面积=181.5 第 24 轮廓面积=56.5 第 25 轮廓面积=88.5 第 26 轮廓面积=19.5 第 27 轮廓面积=7480.5 第 28 轮廓面积=986 第 29 轮廓面积=31.5 第 30 轮廓面积=-8149 第 0 轮廓长度=10.2426 第 1 轮廓长度=10.8284 第 2 轮廓长度=7.41421 第 3 轮廓长度=21.0711 第 4 轮廓长度=20.8284 第 5 轮廓长度=2 第 6 轮廓长度=8.24264 第 7 轮廓长度=130.024 第 8 轮廓长度=12.2426 第 9 轮廓长度=23.6569 第 10 轮廓长度=128.61 第 11 轮廓长度=25.8995 第 12 轮廓长度=6.24264 第 13 轮廓长度=9.65685 第 14 轮廓长度=10.2426 第 15 轮廓长度=0 第 16 轮廓长度=8.24264 第 17 轮廓长度=256.51 第 18 轮廓长度=12.4853 第 19 轮廓长度=0 第 20 轮廓长度=25.6569 第 21 轮廓长度=3169.64 第 22 轮廓长度=264.208 第 23 轮廓长度=76.8701 第 24 轮廓长度=33.5563 第 25 轮廓长度=39.5563 第 26 轮廓长度=17.8995 第 27 轮廓长度=668.6 第 28 轮廓长度=137.882 第 29 轮廓长度=21.8995 第 30 轮廓长度=864.569 [1, -1, -1, -1] [2, 0, -1, -1] [3, 1, -1, -1] [4, 2, -1, -1] [5, 3, -1, -1] [6, 4, -1, -1] [7, 5, -1, -1] [8, 6, -1, -1] [9, 7, -1, -1] [10, 8, -1, -1] [11, 9, -1, -1] [12, 10, -1, -1] [13, 11, -1, -1] [14, 12, -1, -1] [15, 13, -1, -1] [16, 14, -1, -1] [17, 15, -1, -1] [19, 16, 18, -1] [-1, -1, -1, 17] [20, 17, -1, -1] [21, 19, -1, -1] [30, 20, 22, -1] [23, -1, -1, 21] [24, 22, -1, 21] [25, 23, -1, 21] [26, 24, -1, 21] [27, 25, -1, 21] [28, 26, -1, 21] [29, 27, -1, 21] [-1, 28, -1, 21] [-1, 21, -1, -1]
轮廓外接多边形
boundingRect 给定轮廓的外接矩形
boundingRect
函数是 OpenCV 库中常用的函数之一,用于计算给定轮廓的外接矩形。它的函数原型如下:
cv::Rect boundingRect(InputArray points);
其中,InputArray
是一个输入参数,表示要计算外接矩形的轮廓。boundingRect
函数返回一个 cv::Rect
对象,表示给定轮廓的外接矩形。
具体而言,boundingRect
函数可以用于以下情况:
- 给定一张二值化的图像,使用
findContours
函数找到其中的轮廓,然后使用boundingRect
函数计算每个轮廓的外接矩形,以便进一步分析图像的形状和结构。 - 给定一组点,可以使用
boundingRect
函数计算这些点的外接矩形,以便计算这些点的中心位置、面积等参数
minAreaRect 给定轮廓的最小外接矩形
minAreaRect
函数是 OpenCV 库中常用的函数之一,用于计算给定轮廓的最小外接矩形。它的函数原型如下:
RotatedRect minAreaRect(InputArray points);
其中,InputArray
是一个输入参数,表示要计算最小外接矩形的轮廓。minAreaRect
函数返回一个 RotatedRect
对象,表示给定轮廓的最小外接矩形。
具体而言,minAreaRect
函数可以用于以下情况:
- 给定一张二值化的图像,使用
findContours
函数找到其中的轮廓,然后使用minAreaRect
函数计算每个轮廓的最小外接矩形,以便进一步分析图像的形状和结构。 - 给定一组点,可以使用
minAreaRect
函数计算这些点的最小外接矩形,以便计算这些点的中心位置、面积等参数。
approxPolyDP
approxPolyDP
函数是 OpenCV 库中常用的函数之一,用于对给定的轮廓进行多边形逼近。它的函数原型如下:
void approxPolyDP(InputArray curve, OutputArray approxCurve, double epsilon, bool closed);
其中,curve
是一个输入参数,表示要逼近的轮廓。approxCurve
是一个输出参数,表示逼近后得到的多边形。epsilon
是一个控制逼近精度的参数,它越小,逼近精度越高。closed
表示逼近的多边形是否封闭。
具体而言,approxPolyDP
函数可以用于以下情况:
- 对于一条曲线,使用
approxPolyDP
函数将其逼近为多边形,以便进行进一步的处理和分析。例如,在图像处理中,可以使用approxPolyDP
函数将边缘检测得到的轮廓逼近为多边形,以便计算轮廓的面积、周长等参数。 - 对于一组点,可以使用
approxPolyDP
函数将其逼近为多边形,以便绘制图形、计算几何参数等。
convexHull 二维凸包检测函数
convexHull
是 OpenCV 库中用于计算二维点集凸包的函数。它接收一个二维点集,返回这个点集的凸包。
函数定义如下:
void cv::convexHull(
InputArray points, // 二维点集
OutputArray hull, // 凸包点集
bool clockwise=false, // 是否按照顺时针排序
bool returnPoints=true // 是否返回点集
)
参数说明:
points
:输入的二维点集,类型为InputArray
,可以是Mat
或vector
。每行表示一个点,如果是Mat
,需要是CV_32SC2
或CV_64FC2
类型。hull
:输出的凸包点集,类型为OutputArray
,可以是Mat
或vector
。如果returnPoints
为true
,则每行表示一个点,类型为CV_32SC2
或CV_64FC2
,如果为false
,则表示凸包的点在原始点集points
中的下标。clockwise
:是否按照顺时针排序,默认为false
。hull
下数据保存的顺序。returnPoints
:是否返回点集,默认为true
。如果为false
,则表示凸包的点在原始点集中的下标。
函数返回值为 void
,表示计算结果保存在 hull
中。
代码演示:
//绘制轮廓函数
void drawapp(Mat result, Mat img2)
{
for (int i = 0; i < result.rows; i++)
{
//最后一个坐标点与第一个坐标点连接
if (i == result.rows - 1)
{
Vec2i point1 = result.at<Vec2i>(i);
Vec2i point2 = result.at<Vec2i>(0);
line(img2, point1, point2, Scalar(0, 0, 255), 2, 8, 0);
break;
}
Vec2i point1 = result.at<Vec2i>(i);
Vec2i point2 = result.at<Vec2i>(i + 1);
line(img2, point1, point2, Scalar(0, 0, 255), 2, 8, 0);
}
}
int main()
{
Mat img = imread("stuff.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat img1, img2;
img.copyTo(img1); //深拷贝用来绘制最大外接矩形
img.copyTo(img2); //深拷贝迎来绘制最小外接矩形
imshow("img", img);
// 高斯滤波,去噪声
GaussianBlur(img, img, Size(3, 3), 0, 0);
// 去噪声与二值化
Mat canny;
Canny(img, canny, 50, 100, 3, false);
//膨胀运算,将细小缝隙填补上
Mat kernel = getStructuringElement(0, Size(5, 5));
morphologyEx(canny, canny, MORPH_DILATE, kernel);
imshow("", canny);
// 轮廓发现与绘制
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(canny, contours, hierarchy, 0, 2, Point());
//寻找轮廓的外接矩形
for (int n = 0; n < contours.size(); n++)
{
// 最大外接矩形
Rect rect = boundingRect(contours[n]);
rectangle(img1, rect, Scalar(0, 0, 255), 2, 8, 0);
// 最小外接矩形
RotatedRect rrect = minAreaRect(contours[n]);
Point2f points[4];
rrect.points(points); //读取最小外接矩形的四个顶点
Point2f cpt = rrect.center; //最小外接矩形的中心
// 绘制旋转矩形与中心位置
for (int i = 0; i < 4; i++)
{
if (i == 3)
{
line(img2, points[i], points[0], Scalar(0, 255, 0), 2, 8, 0);
break;
}
line(img2, points[i], points[i + 1], Scalar(0, 255, 0), 2, 8, 0);
}
//绘制矩形的中心
circle(img2, cpt, 2, Scalar(255, 0, 0), -1, 8, 0);
}
//输出绘制外接矩形的结果
imshow("max", img1);
imshow("min", img2);
{
Mat img = imread("approx.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
//多通道二值化
threshold(img, img, 50, 255, THRESH_BINARY_INV);
// 边缘检测
Mat canny;
Canny(img, canny, 80, 160, 3, false);
//膨胀运算
Mat kernel = getStructuringElement(0, Size(3, 3));
dilate(canny, canny, kernel);
// 轮廓发现与绘制
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(canny, contours, hierarchy, 0, 2, Point());
//绘制多边形
for (int t = 0; t < contours.size(); t++)
{
//用最小外接矩形求取轮廓中心
RotatedRect rrect = minAreaRect(contours[t]);
Point2f center = rrect.center;
circle(img, center, 2, Scalar(0, 255, 0), 2, 8, 0);
Mat result;
approxPolyDP(contours[t], result,1, true); //多边形拟合
drawapp(result, img);
cout << "corners : " << result.rows << endl;
//判断形状和绘制轮廓
if (result.rows == 3)
{
putText(img, "triangle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows == 4)
{
putText(img, "rectangle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows == 5)
{
putText(img, "poly-8", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows > 5)
{
putText(img, "circle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
}
imshow("result", img);
{
Mat img = imread("hand.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
//二值化
Mat gray, binary;
cvtColor(img, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 200, 255, THRESH_BINARY_INV);
imshow("binary", binary);
//开运算消除细小区域
Mat k = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(binary, binary, MORPH_OPEN, k);
//轮廓发现
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(binary, contours, hierarchy, 0, 2);
for (int n = 0; n < contours.size(); n++)
{
//计算凸包
vector<Point> hull;
convexHull(contours.at(n), hull);
//绘制凸包
for (int i = 0; i < hull.size(); i++)
{
//绘制凸包顶点
circle(img, hull.at(i), 2, Scalar(255, 0, 0), 2, 8, 0);
//连接凸包
if (i == hull.size() - 1)
{
line(img, hull.at(i), hull.at(0), Scalar(0, 0, 255), 2, 8, 0);
break;
}
line(img, hull.at(i), hull.at(i+1), Scalar(0, 0, 255), 2, 8, 0);
}
}
imshow("hull", img);
}
}
waitKey(0);
return 0;
}
直线检测
HoughLines 霍夫变换 - 找直线 -输出(rho,theta)
void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn = 0, double stn = 0, double min_theta = 0, double max_theta = CV_PI);
参数解释
image
:输入图像,必须是单通道灰度图像,因为直线检测是在灰度图像上进行的。lines
:输出参数,包含检测到的直线的参数对,每个参数对包括一个角度和一个距离值。rho
:Hough空间中距离参数的精度。theta
:Hough空间中角度参数的精度。threshold
:检测直线的阈值,只有得到票数大于阈值的直线才会被输出。srn
:可选参数,用于进行霍夫梯度计算的距离分辨率,设置为0时默认值为1。stn
:可选参数,用于进行霍夫梯度计算的角度分辨率,设置为0时默认值为CV_PI/180。min_theta
:可选参数,表示需要检测的直线的最小角度,默认值为0。max_theta
:可选参数,表示需要检测的直线的最大角度,默认值为CV_PI。
函数说明
HoughLines函数的实现过程可以简述如下:
- 在输入图像中计算边缘图像,例如使用Canny函数实现。
- 在Hough空间中初始化计数器数组,并为每个角度和距离对应一个计数器。
- 对于每个边缘点,在Hough空间中计算该点的所有可能直线,并将其对应的计数器加一。
- 对于所有票数大于阈值的直线,将其参数对(角度和距离)保存到输出参数lines中。
HoughLinesP 霍夫变换 - 找直线-输出坐标
HoughLinesP
是OpenCV中用于进行直线检测的函数之一。与HoughLines
函数不同的是,HoughLinesP
函数可以直接输出检测到的直线的起点和终点,更加便于应用。
下面是HoughLinesP
函数的函数原型:
void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0, double maxLineGap=0);
该函数的输入参数如下:
image
:输入图像,必须为单通道8位或32位浮点型图像。lines
:输出参数,包含检测到的直线的起点和终点坐标。rho
:距离分辨率。theta
:角度分辨率。threshold
:阈值,用于控制直线检测的灵敏度。只有当霍夫变换后的累加器值大于该阈值时,才认为该点属于一条直线。minLineLength
:最小直线长度。如果检测到的直线长度小于该值,将被视为无效直线并被排除。maxLineGap
:直线上最大允许的间隔。如果两个直线段之间的距离小于该值,则将它们视为同一直线段。
HoughLinesP
函数的输出参数lines
是一个大小为N x 1
的二维浮点型矩阵,其中每一行都表示一条检测到的直线的起点和终点坐标。每一行的格式为(x1,y1,x2,y2)
,分别表示直线起点和终点的坐标。
maxLineGap
是HoughLinesP
函数中一个用于控制直线检测精度的参数。它定义了在检测到直线段时,两个端点之间允许的最大间隔。如果两个直线段之间的距离小于该值,则将它们视为同一直线段,否则它们将被认为是两条不同的直线段。
换句话说,maxLineGap
可以帮助我们将两个相邻的短线段(可能是同一条直线)合并成一个长线段,从而得到更准确的直线检测结果。然而,如果maxLineGap
设置过大,那么可能会将本不属于同一条直线的线段误认为是同一条直线,从而降低直线检测的准确度。
因此,在使用HoughLinesP
函数时,需要根据具体应用场景和图像特点,合理设置maxLineGap
参数的值,以获得最佳的直线检测结果。
代码演示:
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.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
Mat edge;
//检测边缘图像,并二值化
Canny(gray, 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(0,0,255), 2);
drawLine(img2, lines2, edge.rows, edge.cols, Scalar(0, 0, 255), 2);
//显示图像
imshow("edge", edge);
imshow("img", img);
imshow("img1", img1);
imshow("img2", img2);
{
//用不同的累加器进行检测直线
vector<Vec4i> linesP1, linesP2;
HoughLinesP(edge, linesP1, 1, CV_PI / 180, 150, 30, 10);
HoughLinesP(edge, linesP2, 1, CV_PI / 180, 150, 30, 100);
//绘制两个点连接最大距离10直线检测结果
Mat img3;
img.copyTo(img3);
for (size_t i = 0; i < linesP1.size(); ++i)
{
line(img3, Point(linesP1[i][0], linesP1[i][1]), Point(linesP1[i][2], linesP1[i][3]), Scalar(0, 0, 255), 2);
}
//绘制两个点连接最大距离100直线检测结果
Mat img4;
img.copyTo(img4);
for (size_t i = 0; i < linesP2.size(); ++i)
{
line(img4, Point(linesP2[i][0], linesP2[i][1]), Point(linesP2[i][2], linesP2[i][3]), Scalar(0, 0, 255), 2);
}
imshow("img3", img3);
imshow("img4", img4);
}
waitKey(0);
return 0;
}
点集拟合
fitLine 拟合直线的函数
fitLine()
是OpenCV库中用于拟合直线的函数。该函数可以根据给定的点集拟合出一条直线,可以用于图像处理、计算机视觉和机器人等领域。
函数原型:
void cv::fitLine(
InputArray points, // 输入点集
OutputArray line, // 输出的拟合直线
int distType, // 距离类型
double param, // 距离类型相关参数
double reps, // 拟合直线参数精度
double aeps // 拟合直线角度精度
);
参数解释:
points
:输入的点集,支持Mat
、vector
等类型。line
:输出的拟合直线,一个长度为4的向量,分别表示直线上的一个点和方向向量。distType
:计算距离时采用的算法类型,可以是CV_DIST_L2
、CV_DIST_L1
或CV_DIST_L12
,分别表示欧氏距离、曼哈顿距离和归一化欧氏距离。默认为CV_DIST_L2
。param
:距离类型相关参数。对于CV_DIST_L1
和CV_DIST_C
,param
表示距离权重;对于CV_DIST_L2
和CV_DIST_L12
,param
表示距离的比例因子。默认为0。reps
:拟合直线参数精度,即拟合出的直线与原始点集的距离阈值。默认为0.01。aeps
:拟合直线角度精度,即拟合出的直线与原始点集的夹角阈值。默认为0.01。
1: 在OpenCV的`fitLine()`函数中,`param`参数是与距离类型相关的参数,它的具体含义与所选距离类型有关。以下是不同距离类型下`param`参数的含义:
- 当`distType`为`CV_DIST_L1`或`CV_DIST_C`时,`param`表示距离权重,即每个点到直线的距离要乘以一个系数,这个系数由`param`指定。默认值为0。
- 当`distType`为`CV_DIST_L2`或`CV_DIST_L12`时,`param`表示距离的比例因子,即每个点到直线的距离都要乘以`param`。默认值为0。
需要注意的是,`param`参数只有在计算距离时才会用到,而拟合直线的精度则由`reps`和`aeps`参数控制。
2: `reps` 和 `aeps` 两个参数主要用于 RANSAC 算法中的随机采样点选取,以及拟合过程中的迭代更新。
具体地说,当使用 `CV_DIST_L2`、`CV_DIST_L1`、`CV_DIST_C`、`CV_DIST_L12` 距离类型时,`reps` 和 `aeps` 参数是可选的,可以设置为 0。这是因为这些距离类型本身就已经包含了距离的度量信息,不需要额外的参数来指定。
而当使用 `CV_DIST_FAIR`、`CV_DIST_WELSCH`、`CV_DIST_HUBER` 距离类型时,`reps` 和 `aeps` 参数则非常重要。这是因为这些距离类型需要对距离进行非线性的惩罚,拟合过程需要在迭代中逐步逼近最优解,因此需要合适的采样点选取和更新策略。
具体来说,`reps` 参数指定了 RANSAC 算法中随机采样点的最大次数,一般可以设置为 100~1000 之间的数值;`aeps` 参数指定了直线拟合中迭代更新的最小误差,即当拟合误差小于该值时,认为拟合已经收敛,可以结束迭代。`aeps` 参数的取值一般设置为图像像素值的 1~10% 左右,具体取决于图像质量和噪声水平。
minEnclosingTriangle 拟合最小外接三角形
minEnclosingTriangle是OpenCV库中的一个函数,可以用来寻找一个给定点集的最小外接三角形。该函数的定义如下:
double cv::minEnclosingTriangle(
InputArray points,
OutputArray triangle,
bool clockwise=false
)
这个函数有三个参数:
points
:输入的点集。可以是Mat对象、vector等容器,用于存储输入的点。triangle
:输出的最小包围三角形。可以是Mat对象、vector等容器,用于存储计算出来的最小包围三角形的三个顶点。clockwise
:一个布尔型变量,用于指定输出三角形的方向。如果为true,输出的三角形顶点按顺时针方向排列;否则按逆时针方向排列。- 该函数的返回值为一个double类型的值,表示计算出来的最小包围三角形的面积。
该函数使用的算法是旋转卡壳(Rotating Calipers)算法,它的基本思路是先找到点集中的一个凸包,然后通过旋转卡壳的方法来寻找最小的外接三角形。
具体实现过程如下:
- 找到点集的凸包,如果点集中的所有点都共线,则凸包即为点集中的所有点。
- 对于凸包中的每一条边,以该边为直径找到能够包含点集的最小圆,圆心即为当前边所在的直径的中垂线与点集的最远点的交点。如果该圆的半径比当前找到的最小外接三角形的周长还小,则更新最小外接三角形。
- 最后找到的最小外接三角形即为所求。
minEnclosingCircle 拟合最小外接圆
minEnclosingCircle
是 OpenCV 库中的一个函数,它可以找到一个包围一组点的最小圆。这个函数的原型如下:
void cv::minEnclosingCircle(InputArray points, Point2f& center, float& radius)
其中:
points
:输入参数,包含了一组二维点的数据,可以是一个Mat
对象或者一个vector
容器。center
:输出参数,表示找到的最小圆的圆心。radius
:输出参数,表示找到的最小圆的半径。
system("color F0"); //更改输出界面颜色
Vec4f lines; //存放拟合和后的直线两点坐标
vector<Point2f> point; //待检测是否存在直线的所有点
const static float Points[20][2] = {
{ 0.0f, 0.0f },{ 10.0f, 11.0f },{ 21.0f, 20.0f },{ 30.0f, 30.0f },
{ 40.0f, 42.0f },{ 50.0f, 50.0f },{ 60.0f, 60.0f },{ 70.0f, 70.0f },
{ 80.0f, 80.0f },{ 90.0f, 92.0f },{ 100.0f, 100.0f },{ 110.0f, 110.0f },
{ 120.0f, 120.0f },{ 136.0f, 130.0f },{ 138.0f, 140.0f },{ 150.0f, 150.0f },
{ 160.0f, 163.0f },{ 175.0f, 170.0f },{ 181.0f, 180.0f },{ 200.0f, 190.0f }
};
//将所有点存放在vector中,用于输入函数中
for (int i = 0; i < 20; i++)
{
point.push_back(Point2f(Points[i][0], Points[i][1]));
}
//参数设置
double param = 0; //距离模型中的数值参数C
double reps = 0.01; //坐标原点与直线之间的距离精度
double aeps = 0.01; //角度精度
fitLine(point, lines, DIST_L1, 0, 0.01, 0.01);
double k = lines[1] / lines[0]; //直线斜率
cout << "直线斜率:" << k << endl;
cout << "直线上一点坐标x:" << lines[2] << ", y::" << lines[3] << endl;
cout << "直线解析式:y=" << k << "(x-" << lines[2] << ")+" << lines[3] << endl;
Mat img(500, 500, CV_8UC3, Scalar::all(0));
RNG& rng = theRNG(); //生成随机点
while (true)
{
int i, count = rng.uniform(1, 101);
vector<Point> points;
//生成随机点
for (i = 0; i < count; i++)
{
Point pt;
pt.x = rng.uniform(img.cols / 4, img.cols * 3 / 4);
pt.y = rng.uniform(img.rows / 4, img.rows * 3 / 4);
points.push_back(pt);
}
//寻找包围点集的三角形
vector<Point2f> triangle;
double area = minEnclosingTriangle(points, triangle);
//寻找包围点集的圆形
Point2f center;
float radius = 0;
minEnclosingCircle(points, center, radius);
//创建两个图片用于输出结果
img = Scalar::all(0);
Mat img2;
img.copyTo(img2);
//在图像中绘制坐标点
for (i = 0; i < count; i++)
{
circle(img, points[i], 3, Scalar(255, 255, 255), FILLED, LINE_AA);
circle(img2, points[i], 3, Scalar(255, 255, 255), FILLED, LINE_AA);
}
//绘制三角形
for (i = 0; i < 3; i++)
{
if (i == 2)
{
line(img, triangle[i], triangle[0], Scalar(0, 0, 255), 1, LINE_AA);
break;
}
line(img, triangle[i], triangle[i + 1], Scalar(0, 0, 255), 1, LINE_AA);
}
//绘制圆形
circle(img2, center, cvRound(radius), Scalar(0, 0, 255), 1, LINE_AA);
//输出结果
imshow("triangle", img);
imshow("circle", img2);
//按q键或者ESC键退出程序
char key = (char)waitKey();
if (key == 27 || key == 'q' || key == 'Q')
{
break;
}
二维码(QR code)检测
QRCodeDetector::detect 定位二维码,输出四个角点
QRCodeDetector::detect
是 OpenCV 库中用于识别二维码的函数,其声明如下:
bool QRCodeDetector::detect(InputArray image, OutputArray points) const;
该函数有两个参数:
image
:要识别的输入图像,可以是单通道或三通道图像,必须是8位无符号整数类型。points
:Mat类型 或 vector的输出参数,矩阵中的每一行代表QR码的一个角点,共四行。points的第一个坐标是QR码的左上角坐标。输出顺序为顺时针。
返回值:
- 如果检测到QR码,则返回true,否则返回false。
QRCodeDetector::decode 解码二维码
QRCodeDetector::decode
是 OpenCV 库中用于解码二维码的函数,其声明如下:
String QRCodeDetector::decode(InputArray image, InputArray points, OutputArray straight_qrcode=noArray());
该函数有三个参数:
image
:要解码的输入图像,可以是单通道或三通道图像,必须是8位无符号整数类型。points
:包含要解码的二维码的角点坐标的输入参数。每个二维码由四个角点定义, vector的输入出参数straight_qrcode
:用于存储解码后的二维码图像的输出参数。如果不需要输出解码后的图像,则可以将该参数设置为noArray()
。
函数返回值为 String
类型,表示解码后的二维码内容。如果解码成功,则返回二维码中包含的数据字符串。如果解码失败,则返回一个空字符串。
QRCodeDetector::detectAndDecode 定位并解码二维码,返回解码后的字符串数据,失败返回空字符串
代码演示:
Mat img = imread("QRCode.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat gray, qrcode_bin;
cvtColor(img, gray, COLOR_BGR2GRAY);
QRCodeDetector qr;
vector<Point> points;
string information;
//识别二维码四个角点 第一个角点在左上角,输出顺序顺时针
bool isQRcode = qr.detect(gray, points);
//二维码识别成功
if (isQRcode)
{
//解码二维码
information = qr.decode(gray, points, qrcode_bin);
cout << points << endl;
}
else
{
cout << "二维码识别失败." << endl;
return -1;
}
//绘制二维码的边框
for (int i = 0; i < points.size(); i++)
{
if (i == points.size() - 1)
{
line(img, points[i], points[0], Scalar(0, 0, 255), 2, 8);
break;
}
line(img, points[i], points[i+1], Scalar(0, 0, 255), 2, 8);
}
putText(img, information, Point(20, 30), 0, 1.0, Scalar(0, 0, 255), 2, 8);
//利用函数直接定位二维码并解码
vector<Point>points2;
string information2 = qr.detectAndDecode(gray, points2);
cout << points <<endl;
putText(img, information2.c_str(), Point(20, 55), 0, 1.0, Scalar(0, 0, 0), 2, 8);
namedWindow("qrcode_bin", WINDOW_NORMAL);
imshow("qrcode_bin", qrcode_bin);
imshow("img", img);
积分图像
integral 计算积分图像函数 – 作用 ,加快计算速度
vs中查看:
1:
CV_EXPORTS_W void integral( InputArray src, OutputArray sum, int sdepth = -1 );
2:
CV_EXPORTS_AS(integral2) void integral( InputArray src, OutputArray sum, OutputArray sqsum, int sdepth = -1, int sqdepth = -1 );
3:
CV_EXPORTS_AS(integral3) void integral( InputArray src, OutputArray sum,OutputArray sqsum, OutputArray tilted,int sdepth = -1, int sqdepth = -1);
OpenCV的integral函数可以计算出一个矩阵的积分图像(也称为积分图)。积分图像可以用于快速计算矩形区域内的像素值和,例如在图像处理中进行快速的目标检测和跟踪等任务。
下面是integral函数的函数原型:
void cv::integral(
InputArray src, // 输入矩阵
OutputArray sum, // 输出积分图
OutputArray sqsum = noArray(), // 可选的输出平方积分图
OutputArray tilted = noArray(), // 可选的输出倾斜积分图
int sdepth = -1, // 可选的输出矩阵深度
int sqdepth = -1 // 可选的输出平方矩阵深度
)
其中,src
是输入矩阵,sum
是输出积分图,sqsum
是可选的输出平方积分图,tilted
是可选的输出倾斜积分图,sdepth
是可选的输出矩阵深度,sqdepth
是可选的输出平方矩阵深度。
sum
是输入矩阵的积分图,每个像素值表示原始矩阵中所有左上角点在该像素左上方矩形内的像素值之和。sqsum
是输入矩阵的平方积分图,每个像素值表示原始矩阵中所有左上角点在该像素左上方矩形内的像素值的平方之和。tilted
是输入矩阵的倾斜积分图,每个像素值表示原始矩阵中所有左上角点在以该像素为中心的45度旋转矩形内的像素值之和。
sdepth
和sqdepth
是可选的输出矩阵和平方矩阵的深度。如果未指定,则输出矩阵和平方矩阵的深度与输入矩阵的深度相同。
OpenCV中的`integral`函数用于计算输入图像的积分图像。积分图像是一个新的图像,其中每个像素的值表示原始图像中该像素及其左上角所有像素的总和。
`integral`函数主要有两个用途:
1. 对于图像处理任务,可以使用积分图像快速计算图像区域内的像素总和、均值、方差等统计量。这种方法被称为积分图像技术,它比遍历图像区域并逐个计算像素值的方法更加高效。
2. 积分图像还可以用于图像特征提取,例如Haar特征和LBP特征。在计算Haar和LBP特征时,需要在图像的各个位置上计算某种特定模式的响应值。使用积分图像可以加速这个过程,使得特征提取更加快速。
因此,OpenCV中的`integral`函数可以帮助开发人员在图像处理和计算机视觉任务中更高效地计算统计量和特征。
代码演示:
//创建一个16*16全为1的矩阵, 16*16 = 256 正好一个CV_8U单通道最大值
Mat img = Mat::ones(16, 16, CV_32FC1);
//在图像中加入随机噪声
RNG rng(10086);
for(int y=0;y<img.rows;y++)
{
for (int x = 0; x < img.cols; x++)
{
float d = rng.uniform(-0.5, 0.5);
img.at<float>(y, x) += d;
}
}
imshow("img", img);
//计算标准求和积分,平方求和积分,倾斜求和积分
Mat sum, sqsum, tilted;
integral(img, sum, sqsum,tilted);
//转成CV_8U,方便显示查看
Mat sum8U, sqsum8U, tilted8U;
//找图像中的最大值,转换到CV_8U 0-255 方便显示查看
double min, max;
minMaxLoc(sum, &min, &max);
sum.convertTo(sum8U, CV_8U, 255 / max);
minMaxLoc(sqsum, &min, &max);
sqsum.convertTo(sqsum8U, CV_8U, 255 / max);
minMaxLoc(tilted, &min, &max);
tilted.convertTo(tilted8U, CV_8U, 255 / max);
转成CV_8U,方便显示查看
//Mat sum8U = Mat_<uchar>(sum);
//Mat sqsum8U = Mat_<uchar>(sqsum);
//Mat tilted8U = Mat_<uchar>(tilted);
namedWindow("sum8U", WINDOW_NORMAL);
imshow("sum8U", sum8U);
namedWindow("sqsum8U", WINDOW_NORMAL);
imshow("sqsum8U", sqsum8U);
namedWindow("tilted8U", WINDOW_NORMAL);
imshow("tilted8U", tilted8U);
waitKey(0);
Mat img = Mat::ones(16, 16, CV_32FC1);
Mat img = Mat::ones(400, 400, CV_32FC1);
图像分割
漫水填充算法
基本思想
漫水填充算法,顾名思义就像洪水漫过一样,把一块连通的区域填满,当然水要能漫过需要满足一定的条件,可以理解为满足条件的地方就是低洼的地方,水才能流过去。在图像处理中就是给定一个种子点作为起始点,向附近相邻的像素点扩散,把颜色相同或者相近的所有点都找出来,并填充上新的颜色,这些点形成一个连通的区域。 漫水填充算法可以用来标记或者分离图像的一部分,可实现类似Windows 画图油漆桶功能,或者PS里面的魔棒选择功能。
算法实现
漫水填充算法实现最常见有四邻域像素填充法,八邻域 像素填充法,基于扫描线的填充方法。根据代码实现方式又可以分为递归与非递归。
四领域的递归实现:
floodFill 漫水填充分割函数 – 建立种子点(seedPoint),每次输出区域为一个区域
/* 从指定的种子点开始填充半均匀图像区域*/
int floodFill( InputOutputArray image,
Point seedPoint,
Scalar newVal,
CV_OUT Rect* rect=0,
Scalar loDiff=Scalar(),
Scalar upDiff=Scalar(),
int flags=4 );
/* 填充半均匀图像区域和/或掩码指定种子点*/
int floodFill( InputOutputArray image,
InputOutputArray mask,
Point seedPoint,
Scalar newVal,
CV_OUT Rect* rect=0,
Scalar loDiff=Scalar(),
Scalar upDiff=Scalar(),
int flags=4 );
函数返回值:
-
返回值为填充的像素数(也就是像素面积area)。
-
image
要处理的图片,既是入参也是出参,接受单通道或3通道,8位或浮点类型的图片。如果提供了Mask而且设置了 FLOODFILL_MASK_ONLY 的flag,输入图像才不会被修改,否则调用本方法填充的结果会修改到输入图像中。 -
mask
掩码图像,既是入参也是出参,接受单通道8位的图片,要求比要处理的图片宽和高各大两个像素。mask要先初始化好,填充算法不能漫过mask中非0的区域。比如可以用边缘检测的结果来做为mask,以防止边缘被填充。做为输出参数,mask对应图片中被填充的像素会被置为1或者下面参数指定的值。因此当多次调用floodFill方法,使用同一个mask时,可以避免填充区域叠加和重复计算。 因为 mask比image大,所以image中的点 p(x,y),对应mask中的点 p(x+1, y+1) -
seedPoint 填充算法的种子点,即起始点
-
newVal 填充的颜色
-
loDiff 最小的亮度或颜色的差值
-
upDiff 最大的亮度者颜色的差值
-
rect 可选的输出参数,返回一个最小的矩形,可以刚好把填充的连通域包起来。
-
flags
• 低八位[0-7]表示连通性,默认值4表示四领域填充,8表示八领域填充。
• [8-15]位表示用来填充mask的颜色值[1-255]默认是1
• 比如flag值(4|(255«8)) 表示使用四领域填充,mask填充色值是255。
• 剩余的位有两个值可以单独设置或者用(|)同时设置:
FLOODFILL_MASK_ONLY = 1 << 17 表示不修改原始输入图像,只把结果输出到mask图中,在mask中将填充区域标上前面flag中指定的值。newVal的参数值将被忽略。
FLOODFILL_FIXED_RANGE = 1 << 16 表示待填充像素只和种子点比较。如果不设置这个标记,表示待 填充的像素是和相邻的像素比较(相当于差值范围是浮动的),这种情况下填充区域的像素可能会和种子点相差越来越大。
floodFill函数在图像处理中有广泛的应用,以下是一些常见的实际用处:
- 物体分割:可以利用floodFill函数将图像中的物体分割出来。
- 背景替换:可以将图像中的背景替换为指定的颜色或图片,可以通过floodFill函数将背景区域标记出来,然后进行替换。
- 图像修复:可以利用floodFill函数修复图像中的缺陷或噪点。
- 特征提取:可以利用floodFill函数提取图像中的特定形状或颜色的区域,用于物体识别或分类等应用。
- 面部识别:可以利用floodFill函数将图像中的人脸区域标记出来,用于人脸识别或表情分析等应用。
总之,floodFill函数在图像处理中是一个非常实用的函数,可以帮助我们实现很多图像处理任务。
代码演示:
system("color F0"); //将DOS界面调成白底黑字
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
if (!(img.data))
{
cout << "读取图像错误,请确认图像文件是否正确" << endl;
return -1;
}
RNG rng(10086);//随机数,用于随机生成像素
//设置操作标志flags
int connectivity = 4; //连通邻域方式
int maskVal = 255; //掩码图像的数值
int flags = connectivity | (maskVal << 8) | FLOODFILL_FIXED_RANGE; //漫水填充操作方式标志
//设置与选中像素点的差值
Scalar loDiff = Scalar(20, 20, 20);
Scalar upDiff = Scalar(20, 20, 20);
//声明掩模矩阵变量
Mat mask = Mat::zeros(img.rows + 2, img.cols + 2, CV_8UC1);
while (true)
{
//随机产生图像中某一像素坐标点
int py = rng.uniform(0, img.rows - 1);
int px = rng.uniform(0, img.cols - 1);
Point point = Point(px, py);
//彩色图像中填充的像素值
Scalar newVal = Scalar(rng.uniform(0, 255), rng.uniform(0, 255),rng.uniform(0, 255));
Rect rect;
//漫水填充函数
int area = floodFill(img, mask, point, newVal, &rect, loDiff, upDiff, flags);
//输出像素点和填充的像素数目
cout << "随机生成的像素坐标点x:" << point.x << " y:" << point.y
<< " 填充像数数目:" << area <<"矩形位置:"<< rect.x<<","<<rect.y
<<"高:"<<rect.height << "宽:" <<rect.width<< endl;
//绘制矩形
rectangle(img, rect, Scalar(255, 0, 0));
//输出填充的图像结果
imshow("填充的彩色图像", img);
imshow("掩模图像", mask);
//判断是否结束程序
int c = waitKey(0);
if ((c & 255) == 'q')
{
break;
}
}
//声明掩模矩阵变量
Mat mask = Mat::ones(img.rows + 2, img.cols + 2, CV_8UC1);
掩码矩阵为1(大于0)不执行变换
分水岭法
watershed 分水岭分割图像
代码演示:
#include <string>
Mat srcImage, srcImage_, maskImage;
Point clickPoint; // 鼠标点下去的位置
void on_Mouse(int event, int x, int y, int flags, void*)
{
// 如果鼠标不在窗口中则返回
if (x < 0 || x >= srcImage.cols || y < 0 || y >= srcImage.rows)
return;
// 如果鼠标左键被按下,获取鼠标当前位置;当鼠标左键按下并且移动时,绘制白线;
if (event == EVENT_LBUTTONDOWN)
{
clickPoint = Point(x, y);
}
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))
{
Point point(x, y);
line(maskImage, clickPoint, point, Scalar::all(255), 5, 8, 0);
line(srcImage, clickPoint, point, Scalar::all(255), 5, 8, 0);
clickPoint = point;
imshow("在图像中做标记", srcImage);
}
}
void helpText()
{
cout << "先用鼠标在图片窗口中标记出大致的区域" << endl;
cout << "如果想把图片分割为N个区域,就要做N个标记" << endl;
cout << "键盘按键【1】 - 运行的分水岭分割算法" << endl;
cout << "键盘按键【2】 - 恢复原始图片" << endl;
cout << "键盘按键【0】 - 依次分割每个区域(必须先按【1】)" << endl;
cout << "键盘按键【ESC】 - 退出程序" << endl << endl;
}
/* 注释一:watershed(srcImage_, maskWaterShed);
* 注意它的两个参数
* srcImage_是没做任何修改的原图,CV_8UC3类型
* maskWaterShed声明为CV_32S类型(32位单通道),且全部元素为0
* 然后作为drawContours的第一个参数传入,在上面绘制轮廓
* 最后作为watershed的参数
* 另外,watershed的第二个参数maskWaterShed是InputOutputArray类型
* 即作为输入,也作为输出保存函数调用的结果
*/
int main()
{
Mat maskWaterShed; // watershed()函数的参数
/* 操作提示 */
helpText();
srcImage = imread("fly.jpg");
srcImage_ = srcImage.clone(); // 程序中srcImage会被改变,所以这里做备份
maskImage = Mat(srcImage.size(), CV_8UC1); // 掩模,在上面做标记,然后传给findContours
maskImage = Scalar::all(0);
int areaCount = 1; // 计数,在按【0】时绘制每个区域
imshow("在图像中做标记", srcImage);
setMouseCallback("在图像中做标记", on_Mouse, 0);
while (true)
{
int c = waitKey(0);
if ((char)c == 27) // 按【ESC】键退出
break;
if ((char)c == '2') // 按【2】恢复原图
{
maskImage = Scalar::all(0);
srcImage = srcImage_.clone();
imshow("在图像中做标记", srcImage);
}
if ((char)c == '1') // 按【1】处理图片
{
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(maskImage, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
if (contours.size() == 0) // 如果没有做标记,即没有轮廓,则退出该if语句
break;
cout << contours.size() << "个轮廓" << endl;
maskWaterShed = Mat(maskImage.size(), CV_32S);
maskWaterShed = Scalar::all(0);
/* 在maskWaterShed上绘制轮廓 */
for (int index = 0; index < contours.size(); index++)
drawContours(maskWaterShed, contours, index, Scalar::all(index + 1), -1, 8, hierarchy, INT_MAX);
/* 如果imshow这个maskWaterShed,我们会发现它是一片黑,原因是在上面我们只给它赋了1,2,3这样的值,通过代码80行的处理我们才能清楚的看出结果 */
watershed(srcImage_, maskWaterShed); // 注释一
vector<Vec3b> colorTab; // 随机生成几种颜色
for (int i = 0; i < contours.size(); i++)
{
int b = theRNG().uniform(0, 255);
int g = theRNG().uniform(0, 255);
int r = theRNG().uniform(0, 255);
colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
Mat resImage = Mat(srcImage.size(), CV_8UC3); // 声明一个最后要显示的图像
for (int i = 0; i < maskImage.rows; i++)
{
for (int j = 0; j < maskImage.cols; j++)
{ // 根据经过watershed处理过的maskWaterShed来绘制每个区域的颜色
int index = maskWaterShed.at<int>(i, j); // 这里的maskWaterShed是经过watershed处理的
if (index == -1) // 区域间的值被置为-1(边界)
resImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
else if (index <= 0 || index > contours.size()) // 没有标记清楚的区域被置为0
resImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
else // 其他每个区域的值保持不变:1,2,...,contours.size()
resImage.at<Vec3b>(i, j) = colorTab[index - 1]; // 然后把这些区域绘制成不同颜色
}
}
imshow("resImage", resImage);
addWeighted(resImage, 0.3, srcImage_, 0.7, 0, resImage);
imshow("分水岭结果", resImage);
}
if ((char)c == '0') // 多次点按【0】依次显示每个被分割的区域,需要先按【1】处理图像
{
Mat resImage = Mat(srcImage.size(), CV_8UC3); // 声明一个最后要显示的图像
for (int i = 0; i < maskImage.rows; i++)
{
for (int j = 0; j < maskImage.cols; j++)
{
int index = maskWaterShed.at<int>(i, j);
if (index == areaCount)
resImage.at<Vec3b>(i, j) = srcImage_.at<Vec3b>(i, j);
else
resImage.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
}
}
imshow("分水岭结果", resImage);
areaCount++;
if (areaCount == 4)
areaCount = 1;
}
}
waitKey(0);
return 0;
}
Mat img, imgGray, imgMask;
Mat maskWaterShed; // watershed()函数的参数
img = imread("HoughLines.png"); //原图像
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
cvtColor(img, imgGray, COLOR_BGR2GRAY);
//GaussianBlur(imgGray, imgGray, Size(5, 5), 10, 20); //模糊用于减少边缘数目
//提取边缘并进行闭运算
Canny(imgGray, imgMask, 100, 200);
//Mat k = getStructuringElement(0, Size(3, 3));
//morphologyEx(imgMask, imgMask, MORPH_CLOSE, k);
imshow("边缘图像", imgMask);
imshow("原图像", img);
//计算连通域数目
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(imgMask, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
//在maskWaterShed上绘制轮廓,用于输入分水岭算法
maskWaterShed = Mat::zeros(imgMask.size(), CV_32S);
for (int index = 0; index < contours.size(); index++)
{
drawContours(maskWaterShed, contours, index, Scalar::all(index + 1),
-1, 8, hierarchy, INT_MAX);
}
//分水岭算法 需要对原图像进行处理
watershed(img, maskWaterShed);
vector<Vec3b> colors; // 随机生成几种颜色
for (int i = 0; i < contours.size(); i++)
{
int b = theRNG().uniform(0, 255);
int g = theRNG().uniform(0, 255);
int r = theRNG().uniform(0, 255);
colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
Mat resultImg = Mat(img.size(), CV_8UC3); //显示图像
for (int i = 0; i < imgMask.rows; i++)
{
for (int j = 0; j < imgMask.cols; j++)
{
// 绘制每个区域的颜色
int index = maskWaterShed.at<int>(i, j);
if (index == -1) // 区域间的值被置为-1(边界)
{
resultImg.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
}
else if (index <= 0 || index > contours.size()) // 没有标记清楚的区域被置为0
{
resultImg.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
}
else // 其他每个区域的值保持不变:1,2,…,contours.size()
{
resultImg.at<Vec3b>(i, j) = colors[index - 1]; // 把些区域绘制成不同颜色
}
}
}
//图像融合 类似addWeighted函数
resultImg = resultImg * 0.6 + img * 0.4;
imshow("分水岭结果", resultImg);
//绘制每个区域的图像
for (int n = 1; n <= contours.size(); n++)
{
Mat resImage1 = Mat(img.size(), CV_8UC3); // 声明一个最后要显示的图像
for (int i = 0; i < imgMask.rows; i++)
{
for (int j = 0; j < imgMask.cols; j++)
{
int index = maskWaterShed.at<int>(i, j);
if (index == n)
resImage1.at<Vec3b>(i, j) = img.at<Vec3b>(i, j);
else
resImage1.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
}
}
//显示图像
//imshow(to_string(n), resImage1);
}
waitKey(0);
return 0;
}
角点获取
Harris 海瑞斯角点检测
cornerHarris 函数 – Harris角点检测
CornerHarris是OpenCV中用于检测角点的函数之一,它基于Harris角点检测算法。在本文中,我们将详细介绍CornerHarris函数的使用方法和参数含义。
首先,让我们来看一下CornerHarris函数的函数原型:
void cv::cornerHarris(
InputArray src, // 输入图像,可以是灰度或彩色图像
OutputArray dst, // 输出图像,与输入图像具有相同的大小和深度
int blockSize, // 滑动窗口大小,通常为3或5
int ksize, // Sobel算子大小,通常为3
double k, // Harris算子的自由参数,取值范围为0.04~0.06
int borderType = BORDER_DEFAULT // 图像边界类型,默认使用BORDER_DEFAULT
)
下面是各个参数的详细说明:
src
:输入图像,可以是灰度或彩色图像,类型为cv::Mat
。dst
:输出图像,与输入图像具有相同的大小和深度,类型为cv::Mat
。blockSize
:滑动窗口的大小,通常为3或5。滑动窗口内的像素会被用来计算每个像素的响应值,响应值越大,说明该像素越可能是角点。ksize
:Sobel算子的大小,通常为3。Sobel算子用于计算图像的梯度信息,以此来确定角点。k
:Harris算子的自由参数,取值范围为0.04~0.06。这个参数用于控制角点响应值的阈值,取值越大,阈值越高,被认为是角点的像素就越少。borderType
:图像边界类型,默认使用BORDER_DEFAULT
。如果需要处理边缘像素,则需要指定合适的边界类型。
drawKeypoints 绘制角点函数
drawKeypoints是OpenCV中的一个函数,用于在图像上绘制特征点。其语法如下:
void cv::drawKeypoints(
cv::InputArray image, // 输入图像
const std::vector<cv::KeyPoint>& keypoints, // 特征点向量
cv::OutputArray outImage, // 输出图像
const cv::Scalar& color = cv::Scalar::all(-1), // 特征点颜色
cv::DrawMatchesFlags flags = DrawMatchesFlags::DEFAULT // 绘制参数
);
下面是每个参数的详细解释:
- image:输入图像。
- keypoints:包含特征点的向量。
- outImage:输出图像,特征点将在其中绘制。
- color:特征点的颜色。
- flags:绘制参数。
使用drawKeypoints函数时,它将在输出图像上绘制特征点。绘制的特征点将使用指定的颜色进行标记。可以通过更改flags参数来控制绘制的方式。常见的绘制参数有以下几种:
-
DrawMatchesFlags::DEFAULT
默认参数,不进行特别的处理。默认标志,表示在匹配的关键点周围绘制圆圈,并在它们之间绘制线条。 -
DrawMatchesFlags::DRAW_OVER_OUTIMG
在输出图像之上绘制特征匹配的线和关键点。这个选项是默认开启的,因此如果不需要,在调用drawMatches函数时应该显式关闭。 -
DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS
默认情况下,如果某个特征点在一张图像中找到了匹配点,但在另一张图像中没有找到匹配点,它将被绘制成一个单独的关键点。将该标志设置为true将不会绘制这些单独的关键点。 -
DrawMatchesFlags::DRAW_RICH_KEYPOINTS
使用该标志可以绘制图像中的丰富关键点信息,例如它们的方向、大小、响应等。这个选项需要在输入的关键点结构体中提供这些信息。
例如,如果我们有一个包含特征点的图像和一个指定颜色的输出图像,我们可以使用以下代码来绘制特征点:
cv::Mat image = cv::imread("input.jpg");
cv::Mat outImage;
std::vector<cv::KeyPoint> keypoints;
// ... 从图像中提取特征点
cv::Scalar color(0, 255, 0); // 绿色
cv::drawKeypoints(image, keypoints, outImage, color, cv::DrawMatchesFlags::DEFAULT);
cv::imwrite("output.jpg", outImage);
此代码将从输入图像中提取特征点,将它们绘制为绿色的标记,并将输出图像保存为output.jpg文件。
KeyPoint
OpenCV的KeyPoint
类是用于表示特征点的类,其中包含了特征点的坐标、尺度、方向和响应等信息。KeyPoint
类通常与描述子一起使用,用于图像特征提取和匹配。
下面是KeyPoint
类的构造函数和成员变量:
class KeyPoint
{
public:
KeyPoint(); // 默认构造函数
KeyPoint(Point2f _pt, float _size, float _angle = -1, float _response = 0,
int _octave = 0, int _class_id = -1); // 指定参数的构造函数
Point2f pt; // 特征点坐标
float size; // 特征点尺度
float angle; // 特征点方向,如果值为-1,则表示不计算方向
float response; // 特征点响应值,通常用于排除不稳定的特征点
int octave; // 特征点所在的图像金字塔层数
int class_id; // 类别标识符,通常用于特征点分类
};
KeyPoint
类的构造函数有两种,一种是默认构造函数,另一种是指定参数的构造函数。指定参数的构造函数可以设置特征点的坐标、尺度、方向、响应、图像金字塔层数和类别标识符等参数。其中,特征点坐标是必需的参数,其他参数可以根据需要选择是否设置。
KeyPoint
类的成员变量包括:
pt
:特征点坐标,类型为Point2f
,表示特征点在图像中的位置。size
:特征点尺度,类型为float
,表示特征点所在的尺度空间大小。angle
:特征点方向,类型为float
,表示特征点的方向,如果值为-1,则表示不计算方向。response
:特征点响应值,类型为float
,通常用于排除不稳定的特征点。octave
:特征点所在的图像金字塔层数,类型为int
。class_id
:特征点类别标识符,类型为int
,通常用于特征点分类。
KeyPoint
类还提供了一些成员函数,例如比较运算符和输出运算符等,以便于特征点的排序和打印输出。
代码演示:
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
if (img.empty())
{
cout << "图像打开失败。。。" << endl;
return -1;
}
//转变灰度图像
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
//计算Harris系数
Mat harris;
cornerHarris(gray, harris, 3, 3, 0.04);
//归一化便于进行数值比较和结果显示 -- 主要是显示结果
Mat harrisn;
normalize(harris, harrisn, 0, 255, NORM_MINMAX);
//将图像数据类型变成CV_8U 将Harris响应值转为整型(uchar)
convertScaleAbs(harrisn, harrisn);
//寻找Harris角点
vector<KeyPoint> keyPoints;
for (int row = 0; row < harrisn.rows; ++row)
{
for (int col = 0; col < harrisn.cols; ++col)
{
int R = harrisn.at<uchar>(row, col);
if (R > 80)
{
//向角点存入KeyPoint中
KeyPoint keyPoint;
keyPoint.pt.x = row;
keyPoint.pt.y = col;
keyPoints.push_back(keyPoint);
}
}
}
//绘制角点
drawKeypoints(img, keyPoints, img);
imshow("系数矩阵", harrisn);
imshow("harris角点", img);
waitKey(0);
Shi-Tomas 托马斯角点检测 – 和Harris角点检测不同的是:评价系数R不同,其他都一样
goodFeaturesToTrack 托马斯角点检测函数(同时也支持Harris角点检测)
goodFeaturesToTrack
函数是 OpenCV 中用于在图像中检测角点的函数。该函数的详细说明如下:
void cv::goodFeaturesToTrack(
cv::InputArray image, // 输入图像
cv::OutputArray corners, // 输出角点
int maxCorners, // 角点数的上限
double qualityLevel, // 角点质量水平
double minDistance, // 角点之间的最小距离
cv::InputArray mask = cv::noArray(), // 掩码图像
int blockSize = 3, // 窗口大小
bool useHarrisDetector = false, // 是否使用 Harris 角点检测器
double k = 0.04 // Harris 角点检测器参数 k
)
其中,参数的含义如下:
image
:输入图像,必须为单通道灰度图像,数据类型可以是CV_8UC1
或CV_32FC1
。corners
:输出角点,类型为std::vector<cv::Point2f>
。maxCorners
:角点数的上限,如果检测到的角点数超过该值,则会按角点响应值从大到小排序,保留前maxCorners
个角点。qualityLevel
:角点质量水平,取值范围为 0 到 1,表示保留哪些角点,只有角点的响应值大于等于所有角点响应值的qualityLevel
倍时,该角点才会被保留。
`qualityLevel` 是 `goodFeaturesToTrack` 函数的一个参数,它指定了角点的最小响应值。在函数的实现中,将计算每个像素的局部像素块的响应值,如果响应值小于 `qualityLevel`,则该像素不会被认为是角点。
该参数的取值范围为 $[0, 1]$,默认值为 $0.01$。较小的 `qualityLevel` 值会导致更多的角点被检测出来,但同时也会导致噪声点和较弱的角点被误认为是角点。较大的 `qualityLevel` 值会减少误检率,但同时也会减少检测到的角点数量。
在实际应用中,需要根据具体的场景和需求选择合适的 `qualityLevel` 值。一般来说,建议先使用默认值进行检测,然后根据实际情况调整参数以达到更好的效果。
minDistance
:角点之间的最小距离(即像素距离),如果两个角点之间的距离小于该值,则只保留其中响应值较大的角点。mask
:掩码图像,用于指定检测角点的区域,只有掩码图像中对应位置为非零时,才会检测该位置的角点。默认为空,表示检测整个图像。blockSize
:窗口大小,用于计算角点响应值的局部像素块大小。默认为 3,表示使用 3x3 的窗口。useHarrisDetector
:是否使用 Harris 角点检测器。默认为 false,表示使用 Shi-Tomasi 角点检测器。k
:Harris 角点检测器参数 k,仅当useHarrisDetector
为 true 时有效。默认值为 0.04。
goodFeaturesToTrack
函数使用的是 Shi-Tomasi 角点检测器或 Harris 角点检测器。Shi-Tomasi 角点检测器的角点响应函数为
min
(
λ
1
,
λ
2
)
\min(\lambda_1, \lambda_2)
min(λ1,λ2),其中
λ
1
\lambda_1
λ1 和
λ
2
\lambda_2
λ2 是局部像素块的两个特征值。Harris 角点检测器的角点响应函数为
det
\det
detM - k\cdot\operatorname{trace}(M)^2$,其中
M
M
M 是局部像素块的协方差矩阵,
k
k
k 是一个常数。在实际应用中,一般使用 Shi-Tomasi 角点检测器,因为它的性能比 Harris 角点检测器更好。
goodFeaturesToTrack
函数的工作流程如下:
- 对输入图像进行平滑处理,以减小噪声干扰。
- 计算每个像素的局部像素块的特征值和响应值。
- 根据响应值和其他参数选取一些角点,并进行非极大值抑制,保留一定数量的最强角点。
- 返回最终的角点列表。
代码演示:
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
if (img.empty())
{
cout << "图像打开失败。。。" << endl;
return -1;
}
//转变灰度图像
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
//存放角点
vector<Point2f> corners;
//托马斯角点检测
goodFeaturesToTrack(gray, corners, 100, 0.01, 10);
//Point2f 存放到KeyPoint中
vector<KeyPoint> keyPoints;
for (int i = 0; i < corners.size(); ++i)
{
//向角点存入KeyPoint中
KeyPoint keyPoint;
keyPoint.pt = corners[i];
keyPoints.push_back(keyPoint);
}
//绘制角点
drawKeypoints(img, keyPoints, img);
imshow("托马斯角点", img);
亚像素级别角点位置优化
亚像素角点的求法
cornerSubPix 亚像素角点优化提取
CornerSubPix 函数是 OpenCV 库中用于精确计算角点坐标的函数。在角点检测过程中,由于图像采样和噪声等因素的影响,检测到的角点坐标往往存在一定的误差。CornerSubPix 函数通过迭代优化算法,对检测到的角点坐标进行精确计算,从而提高角点检测的准确性。
CornerSubPix 函数的函数原型如下:
void cv::cornerSubPix(
InputArray image, // 输入图像
InputOutputArray corners, // 角点位置,既是输入也是输出
Size winSize, // 窗口大小
Size zeroZone, //死区大小
TermCriteria criteria // 终止准则
);
参数说明:
image
:输入图像,类型为cv::InputArray
,即可以是cv::Mat
类型的图像,也可以是其他形式的图像输入。corners
:输入和输出的角点坐标,类型为cv::InputOutputArray
,是一个N x 1
的浮点型矩阵,其中N
为检测到的角点个数。winSize
:角点搜索窗口大小,类型为cv::Size
,表示在搜索过程中使用的窗口大小。zeroZone
:死区大小,类型为cv::Size
,表示搜索过程中的死区大小,即在该区域内不再搜索新的角点。criteria
:停止条件,类型为cv::TermCriteria
,表示优化过程的停止条件,包括最大迭代次数、精度等信息。
winSize是CornerSubPix函数的参数之一,表示搜索窗口的大小。搜索窗口是用于寻找亚像素级别角点位置的区域。在CornerSubPix函数中,它会在角点周围的搜索窗口内搜索亚像素级别的角点。
winSize是一个Size类型的变量,通常为奇数,其值表示搜索窗口的宽度和高度。例如,如果winSize的值为(5,5),则搜索窗口的宽度和高度都为5。搜索窗口的大小决定了CornerSubPix函数搜索的范围。通常情况下,搜索窗口越大,可以找到的亚像素级别的角点越多,但也会导致计算时间增加。
在实际应用中,需要根据图像和角点的情况来选择合适的winSize值。通常情况下,可以根据经验选择一个合适的值,例如3、5、7等。如果不确定winSize的值,可以尝试多个不同的值,然后根据实验结果选择最优的值。
zeroZone是CornerSubPix函数的参数之一,用于指定中心区域内的哪些像素不参与拟合,也称作死区。具体来说,zeroZone是一个Size类型的变量,用于指定中心区域的宽度和高度。如果指定了zeroZone,那么搜索窗口的中心区域大小为(winSize.width - 2 * zeroZone.width) * (winSize.height - 2 * zeroZone.height),即搜索窗口减去中心区域的大小。
在CornerSubPix函数中,如果一个像素的距离角点的距离小于等于zeroZone,则该像素不参与拟合,即在拟合角点位置时不考虑这些像素的贡献。这样可以避免边缘像素对拟合结果产生的影响,从而提高拟合的精度。
如果不想使用zeroZone,可以将其设置为(-1, -1),表示没有死区。在实际应用中,可以根据图像和角点的情况来选择合适的zeroZone值。通常情况下,zeroZone的大小应该不超过搜索窗口大小的一半。如果zeroZone设置得太小,可能会导致拟合结果不准确,如果设置得太大,可能会降低计算效率。
CornerSubPix 函数的具体实现过程如下:
- 在输入图像中搜索角点,得到初始角点坐标;
- 对于每个初始角点坐标,构造一个搜索窗口,在该窗口内寻找亚像素级别的极值点;
- 根据得到的亚像素级别的极值点,更新角点坐标;
- 重复步骤 2 和步骤 3 直到达到停止条件。
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
if (img.empty())
{
cout << "图像打开失败。。。" << endl;
return -1;
}
//转变灰度图像
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
//存放角点
vector<Point2f> corners;
//托马斯角点检测
goodFeaturesToTrack(gray, corners, 100, 0.01, 10,Mat(),3,false);
//计算亚像素级别角点坐标
vector<Point2f> cornerSub = corners;
cornerSubPix(gray, cornerSub, Size(5, 5), Size(-1, -1),
TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 40, 0.001));
for (int i = 0; i < corners.size(); i++)
{
string str = to_string(i);
str = "第" + str + "个角点点初始坐标:";
cout << str << corners[i] << " 精细后坐标:" << cornerSub[i] << endl;
}
waitKey(0);
第0个角点点初始坐标:[107, 307] 精细后坐标:[107.436, 307.542]
第1个角点点初始坐标:[129, 355] 精细后坐标:[129, 355]
第2个角点点初始坐标:[114, 262] 精细后坐标:[113.581, 257.508]
第3个角点点初始坐标:[211, 210] 精细后坐标:[210.014, 207.331]
第4个角点点初始坐标:[65, 216] 精细后坐标:[65, 216]
第5个角点点初始坐标:[82, 197] 精细后坐标:[81.3992, 195.662]
第6个角点点初始坐标:[30, 339] 精细后坐标:[30, 339]
第7个角点点初始坐标:[98, 197] 精细后坐标:[95.5979, 196.395]
第8个角点点初始坐标:[110, 325] 精细后坐标:[110, 325]
第9个角点点初始坐标:[99, 265] 精细后坐标:[99.3217, 265.246]
第10个角点点初始坐标:[27, 349] 精细后坐标:[27.6825, 349.852]
第11个角点点初始坐标:[123, 251] 精细后坐标:[122.925, 252.398]
第12个角点点初始坐标:[103, 282] 精细后坐标:[105.314, 278.513]
第13个角点点初始坐标:[123, 378] 精细后坐标:[123.584, 377.271]
第14个角点点初始坐标:[120, 339] 精细后坐标:[120, 339]
第15个角点点初始坐标:[142, 254] 精细后坐标:[144.716, 256.269]
第16个角点点初始坐标:[267, 209] 精细后坐标:[267, 209]
第17个角点点初始坐标:[139, 365] 精细后坐标:[139.274, 365.359]
第18个角点点初始坐标:[217, 134] 精细后坐标:[217, 134]
第19个角点点初始坐标:[102, 374] 精细后坐标:[101.177, 373.929]
第20个角点点初始坐标:[90, 187] 精细后坐标:[90, 187]
第21个角点点初始坐标:[106, 44] 精细后坐标:[106.416, 43.7285]
第22个角点点初始坐标:[33, 315] 精细后坐标:[36.3408, 313.046]
第23个角点点初始坐标:[174, 186] 精细后坐标:[175.773, 190.164]
第24个角点点初始坐标:[49, 225] 精细后坐标:[49, 225]
第25个角点点初始坐标:[113, 273] 精细后坐标:[112.695, 271.538]
第26个角点点初始坐标:[119, 358] 精细后坐标:[119.628, 362.254]
第27个角点点初始坐标:[265, 157] 精细后坐标:[264.406, 156.374]
第28个角点点初始坐标:[228, 137] 精细后坐标:[228.338, 138.826]
第29个角点点初始坐标:[113, 252] 精细后坐标:[111.721, 255.886]
第30个角点点初始坐标:[198, 171] 精细后坐标:[199.547, 169.277]
第31个角点点初始坐标:[99, 297] 精细后坐标:[99, 297]
第32个角点点初始坐标:[213, 222] 精细后坐标:[213, 222]
第33个角点点初始坐标:[112, 379] 精细后坐标:[112, 379]
第34个角点点初始坐标:[32, 327] 精细后坐标:[32.8941, 326.648]
第35个角点点初始坐标:[80, 214] 精细后坐标:[75.6307, 217.655]
第36个角点点初始坐标:[93, 318] 精细后坐标:[93.8367, 315.064]
第37个角点点初始坐标:[90, 205] 精细后坐标:[89.8383, 204.612]
第38个角点点初始坐标:[94, 277] 精细后坐标:[94.9003, 280.412]
第39个角点点初始坐标:[189, 130] 精细后坐标:[188.457, 130.728]
第40个角点点初始坐标:[225, 49] 精细后坐标:[229.291, 50.3052]
第41个角点点初始坐标:[85, 327] 精细后坐标:[88.346, 326.139]
第42个角点点初始坐标:[114, 397] 精细后坐标:[112.189, 396.351]
第43个角点点初始坐标:[52, 251] 精细后坐标:[52, 251]
第44个角点点初始坐标:[85, 225] 精细后坐标:[83.497, 226.94]
第45个角点点初始坐标:[96, 156] 精细后坐标:[96.2629, 159.799]
第46个角点点初始坐标:[110, 294] 精细后坐标:[110, 294]
第47个角点点初始坐标:[55, 263] 精细后坐标:[55, 263]
第48个角点点初始坐标:[206, 154] 精细后坐标:[206, 154]
第49个角点点初始坐标:[124, 241] 精细后坐标:[122.204, 241.299]
第50个角点点初始坐标:[138, 265] 精细后坐标:[139.684, 263.988]
第51个角点点初始坐标:[162, 215] 精细后坐标:[159.183, 215.157]
第52个角点点初始坐标:[126, 267] 精细后坐标:[126, 267]
第53个角点点初始坐标:[150, 153] 精细后坐标:[148.081, 156.316]
第54个角点点初始坐标:[338, 81] 精细后坐标:[337.142, 82.3453]
第55个角点点初始坐标:[362, 43] 精细后坐标:[362.82, 45.6256]
第56个角点点初始坐标:[230, 261] 精细后坐标:[226.519, 261.872]
第57个角点点初始坐标:[80, 337] 精细后坐标:[80, 337]
第58个角点点初始坐标:[221, 211] 精细后坐标:[219.051, 212.273]
第59个角点点初始坐标:[127, 167] 精细后坐标:[128.63, 164.476]
第60个角点点初始坐标:[47, 332] 精细后坐标:[49.4101, 332.331]
第61个角点点初始坐标:[179, 135] 精细后坐标:[179.699, 135.454]
第62个角点点初始坐标:[250, 218] 精细后坐标:[247.814, 217.41]
第63个角点点初始坐标:[87, 294] 精细后坐标:[88.4774, 296.2]
第64个角点点初始坐标:[44, 270] 精细后坐标:[45.6132, 268.668]
第65个角点点初始坐标:[77, 245] 精细后坐标:[77, 245]
第66个角点点初始坐标:[275, 108] 精细后坐标:[275.607, 109.833]
第67个角点点初始坐标:[119, 229] 精细后坐标:[117.843, 231.374]
第68个角点点初始坐标:[242, 287] 精细后坐标:[242.37, 284.089]
第69个角点点初始坐标:[61, 273] 精细后坐标:[58.427, 271.287]
第70个角点点初始坐标:[245, 315] 精细后坐标:[244.605, 315.572]
第71个角点点初始坐标:[278, 152] 精细后坐标:[274.527, 150.041]
第72个角点点初始坐标:[247, 262] 精细后坐标:[248.89, 262.256]
第73个角点点初始坐标:[68, 179] 精细后坐标:[67.2174, 178.63]
第74个角点点初始坐标:[84, 76] 精细后坐标:[84.4115, 75.1581]
第75个角点点初始坐标:[96, 360] 精细后坐标:[98.8934, 359.677]
第76个角点点初始坐标:[60, 242] 精细后坐标:[58.876, 245.158]
第77个角点点初始坐标:[240, 275] 精细后坐标:[240, 275]
第78个角点点初始坐标:[101, 331] 精细后坐标:[102.594, 329.388]
第79个角点点初始坐标:[202, 188] 精细后坐标:[202, 188]
第80个角点点初始坐标:[77, 155] 精细后坐标:[77.2069, 156.676]
第81个角点点初始坐标:[37, 250] 精细后坐标:[37.1263, 252.902]
第82个角点点初始坐标:[22, 293] 精细后坐标:[23.4931, 293.469]
第83个角点点初始坐标:[66, 261] 精细后坐标:[66.4946, 260.123]
第84个角点点初始坐标:[128, 366] 精细后坐标:[128, 366]
第85个角点点初始坐标:[242, 66] 精细后坐标:[242.809, 67.0113]
第86个角点点初始坐标:[171, 174] 精细后坐标:[168.455, 172.596]
第87个角点点初始坐标:[116, 177] 精细后坐标:[114.027, 178.397]
第88个角点点初始坐标:[266, 222] 精细后坐标:[266, 222]
第89个角点点初始坐标:[100, 232] 精细后坐标:[100, 232]
第90个角点点初始坐标:[110, 237] 精细后坐标:[114.673, 234.192]
第91个角点点初始坐标:[63, 232] 精细后坐标:[63, 232]
第92个角点点初始坐标:[110, 337] 精细后坐标:[110, 337]
第93个角点点初始坐标:[91, 382] 精细后坐标:[90.8101, 380.856]
第94个角点点初始坐标:[327, 118] 精细后坐标:[327, 118]
第95个角点点初始坐标:[287, 345] 精细后坐标:[286.54, 344.742]
第96个角点点初始坐标:[89, 372] 精细后坐标:[91.0899, 367.077]
第97个角点点初始坐标:[254, 78] 精细后坐标:[256.299, 80.5131]
第98个角点点初始坐标:[264, 94] 精细后坐标:[264.99, 95.2756]
第99个角点点初始坐标:[352, 391] 精细后坐标:[352, 391]
特征点获取
举例 - OBR特征点
特征点检测-ORB
ORB特征点创建
//创建ORB特征点 --- 创建其他特征点类似这样
cv::Ptr<cv::ORB> detector = cv::ORB::create();
//保存关键点和描述子
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
//计算ORB关键点 和 描述子
detector->detectAndCompute(image, cv::noArray(), keypoints, descriptors);
detectAndCompute 计算关键点和描述子函数
detectAndCompute
可以实现detect
和 compute
一起的功能。
detectAndCompute
函数是 OpenCV 中一个常用的函数,它用于在输入图像中检测关键点并计算它们的描述子。该函数的完整原型如下:
void cv::Feature2D::detectAndCompute(
InputArray image,
InputArray mask,
std::vector<KeyPoint>& keypoints,
OutputArray descriptors,
bool useProvidedKeypoints = false)
下面是各个参数的详细解释:
image
: 输入图像。可以是一个单通道灰度图像,也可以是一个三通道彩色图像。mask
: 可选参数,表示掩膜图像,用于指定哪些像素需要进行特征提取。如果不需要使用掩膜,可以将该参数设置为默认值cv::noArray()
。keypoints
: 用于存储检测到的关键点的容器。该容器会在函数中被清空并重新填充。如果useProvidedKeypoints
参数设置为 true,则该容器会被视为输入,并且不会被清空。descriptors
: 用于存储计算出的关键点描述子的矩阵。每一行对应一个关键点的描述子。如果没有检测到任何关键点,该矩阵将为空。useProvidedKeypoints
: 可选参数,表示是否使用提供的关键点进行特征提取。如果设置为 true,则函数不会在输入图像中检测关键点,而是使用keypoints
参数中提供的关键点进行计算。
使用 detectAndCompute
函数进行特征提取的一般流程如下:
- 创建一个特征检测器对象,例如
cv::ORB
或cv::SIFT
。 - 加载输入图像并转换为灰度图像(如果需要)。
- 调用
detectAndCompute
函数,传入输入图像和其他参数。 - 检查是否成功检测到了关键点。如果关键点数量为 0,说明特征提取失败。
- 可以使用关键点和描述子进行后续操作,例如图像匹配或物体识别等。
注意事项:
detectAndCompute
函数只能用于计算静态图像中的关键点和描述子,不能用于实时视频处理。- 不同的特征检测器可能会具有不同的参数设置,需要根据具体情况进行调整。
- 如果关键点数量很少,可能需要更改特征检测器的参数或使用不同的特征检测器。
代码演示:
Mat img = imread("D:/OpenCV4.5.1/opencv/sources/doc/js_tutorials/js_assets/lena.jpg");
if (img.empty())
{
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;
//1:从图像中获取关键点
//orb->detect(img, keypoints);
//计算ORB描述子
Mat descriptors;
//2:从关键点中计算描述子
//orb->compute(img, keypoints, descriptors);
/*
1:orb->detect(img, keypoints)和
2:orb->compute(img, keypoints, descriptors);
等于
3:orb->detectAndCompute(img, cv::noArray(), keypoints, descriptors);
如果orb->detectAndCompute(img, cv::noArray(), keypoints, descriptors,true);
设置为ture,那么是从keypoints中获取关键点,而不是从img中获取。
*/
orb->detectAndCompute(img, cv::noArray(), keypoints, descriptors,false);
//绘制特征点
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);