前言
越来越多的开发人员选择基于开源的Qt框架与OpenCV来实现界面和算法,其原因不单单是无版权问题,更多是两个社区的发展蓬勃,可用来学习的资料与例程特别丰富。以下是关于利用Qt构建GUI并使用OpenCV中的HoughLinesP/HoughCircles/findContours&drawContours函数进行图像检测。
软件版本:Qt-5.12.0/OpenCV-4.5.3
平台:Windows10/11–64
一、函数介绍
1、HoughLinesP
函数原型:
cv::HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0, double maxLineGap=0 )
参数解释:
image:输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里;
Iines:经过调用HoughLinesP函数后后存储了检测到的线条的输出矢量,每一条线由具有四个元素的矢量(x_1,y_1, x_2, y_2) 表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点;
rho:以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径;
theta:以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度;
threshold:累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中;
minLineLength:有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来;
maxLineGap:有默认值0,允许将同一行点与点之间连接起来的最大的距离;
2、HoughCircles
函数原型:
cv::HoughCircles(InputArray image,OutputArray circles, int method, double dp, double minDist, double param1=100,double param2=100, int minRadius=0, int maxRadius=0 )
参数解释:
image输入图像,即源图像,需为8位的灰度单通道图像;
circles:经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x, y, radius)表示;
method:即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为CV_HOUGH_GRADIENT,在此参数处填这个标识符即可;
dp:用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。上述文字不好理解的话,来看例子吧。例如,如果dp= 1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度;
minDist:为霍夫变换检测到的圆的圆心之间的最小距离,即让我们的算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大的话,某些圆就不能被检测出来了;
param1:有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半;
param2:也有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了;
minRadius:有默认值0,表示圆半径的最小值;
maxRadius:也有默认值0,表示圆半径的最大值;
3、findContours
函数原型:
cv::findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy,
int mode, int method, Point offset = Point());
参数解释:
image:单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像;
contours:contours定义为“vector contours”,是一个双重向量(向量内每个元素保存了一组由连续的Point构成的点的集合的向量),每一组点集就是一个轮廓,有多少轮廓,contours就有多少元素;
hierarchy:hierarchy定义为“vector hierarchy”,Vec4i的定义:typedef Vec<int, 4> Vec4i;(向量内每个元素都包含了4个int型变量),所以从定义上看,hierarchy是一个向量,向量内每个元素都是一个包含4个int型的数组。向量hierarchy内的元素和轮廓向量contours内的元素是一一对应的,向量的容量相同。hierarchy内每个元素的4个int型变量是hierarchy[i][0] ~ hierarchy[i][3],分别表示当前轮廓 i 的后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓的编号索引。如果当前轮廓没有对应的后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓,则相应的hierarchy[i][*]被置为-1。
mode:定义轮廓的检索模式,取值如下:
CV_RETR_EXTERNAL:只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略;
CV_RETR_LIST:检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1,具体下文会讲到;
CV_RETR_CCOMP: 检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层;
CV_RETR_TREE: 检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
method:定义轮廓的近似方法,取值如下:
CV_CHAIN_APPROX_NONE:保存物体边界上所有连续的轮廓点到contours向量内;
CV_CHAIN_APPROX_SIMPLE:仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留;
CV_CHAIN_APPROX_TC89_L1:使用teh-Chinl chain 近似算法;
CV_CHAIN_APPROX_TC89_KCOS:使用teh-Chinl chain 近似算法。
offset:Point偏移量,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,并且Point还可以是负值!
4、 drawContours
函数原型:
cv::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:输入的所有轮廓(每个轮廓以点集的方式存储)
contoursIdx:指定绘制轮廓的下标(若为负数,则绘制所有轮廓)
color:绘制轮廓的颜色
thickness:绘制轮廓的线的宽度(若为负数,则填充轮廓内部)
lineType:绘制轮廓的线型(4连通、8连通或者反锯齿)
hierarchy:关于层级的可选信息,仅用于当你想要绘制部分轮廓的时候
maxLevel:绘制轮廓的最大层级,若为0,则仅仅绘制指定的轮廓;若为1,则绘制该轮廓及其内嵌轮廓,若为2,则绘制该轮廓、其内嵌轮廓以及内嵌轮廓的内嵌轮廓,依次类推。该参数只有在有层级信息输入时才被考虑。
offset:可选的轮廓偏移参数,所有的轮廓将会进行指定的偏移
当thickness = FILLED,即使没有提供层级信息也可以正确处理带孔洞的连通域情况(分析轮廓时采用奇偶规则),但如果是单独检索的轮廓合并则可能会出现错误的情况,该情况下则需要分开处理。
二、演示
1、GUI
如上图创建Operator的QComboBox控件进行函数选择,Action的功能按钮QPushButton,对当前窗口进行检测,并输出状态信息。
2、代码实现
HoughLinesP/HoughCircles/findContours&drawContours实现代码:
geometryBtn的clicked()槽函数
void MainWindow::on_geometryBtn_clicked() { std::size_t numView = ui->tabWidget->currentIndex() % 3; if (dispMat[numView]->empty()) { outputInfo(2, tr("Please make sure the Mat exist!")); return; } if (dispMat[numView]->channels() == 3) { cv::cvtColor(*dispMat[numView], *dispMat[numView], cv::COLOR_RGB2GRAY); } tmpMat->zeros(dispMat[numView]->size(), \ dispMat[numView]->type()); cv::GaussianBlur(*dispMat[numView], *dispMat[numView], cv::Size(3, 3), \ 0, 0, cv::BORDER_DEFAULT); int operatorType = ui->geometryCombo->currentIndex(); // 0: line, 1: circle, 2: contours switch (operatorType) { case 0: { int cannyThresh = ui->cannyThreshSlider->value(); int lineThresh = ui->lineThreshSlider->value(); std::vector<cv::Vec4i> lines; cv::Canny(*dispMat[numView], *tmpMat, cannyThresh, \ cannyThresh * 2, 3); cv::HoughLinesP(*tmpMat, lines, 1, CV_PI/180, lineThresh, 50, 10); for (size_t i = 0; i < lines.size(); i++) { cv::Vec4i l = lines[i]; cv::line(*tmpMat, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), \ cv::Scalar(0, 0, 255), 1, 1, 1); } outputInfo(1, tr("Lines done.")); break; } case 1: { int circleThresh = ui->circleThreshSlider->value(); std::vector<cv::Vec3f> circles; double startTime = static_cast<double>(cv::getTickCount()); cv::HoughCircles(*dispMat[numView], circles, cv::HOUGH_GRADIENT, \ 1, dispMat[numView]->rows / 16, \ circleThresh, circleThresh/2, \ 0, 0); double timeCost = (static_cast<double>(cv::getTickCount()) - \ startTime) / cv::getTickFrequency(); QString costTime = "Cost time: " + QString::number(timeCost); outputInfo(1, costTime); for (size_t i = 0; i < circles.size(); i++) { cv::Point center(cvRound(circles[i][0]), \ cvRound(circles[i][1])); int radius = cvRound(circles[i][2]); cv::circle(*tmpMat, center, 3, \ cv::Scalar(0, 255, 0), -1, 8, 0); cv::circle(*tmpMat, center, radius, \ cv::Scalar(0, 0, 255), 3, 8, 0); } outputInfo(1, tr("Circles done.")); break; } case 2: { cv::RNG rng(12345); int cannyThresh = ui->cannyThreshSlider->value(); cv::Canny(*dispMat[numView], *tmpMat, cannyThresh, \ cannyThresh * 2, 3); std::vector<std::vector<cv::Point>> contours; std::vector<cv::Vec4i> hierarchy; cv::findContours(*tmpMat, contours, hierarchy, \ cv::RETR_TREE, \ cv::CHAIN_APPROX_SIMPLE, \ cv::Point(0, 0)); std::vector<std::vector<cv::Point>> contoursPoly(contours.size()); std::vector<cv::Rect> boundRect(contours.size()); std::vector<cv::Point2f> center(contours.size()); std::vector<float> radius(contours.size()); for (size_t i = 0; i < contours.size(); i++) { cv::approxPolyDP(cv::Mat(contours[i]), contoursPoly[i], 3, true); boundRect[i] = cv::boundingRect(cv::Mat(contoursPoly[i])); minEnclosingCircle(contoursPoly[i], center[i], radius[i]); } for (size_t i = 0; i < contours.size(); i++) { cv::Scalar color = cv::Scalar(rng.uniform(0, 255), \ rng.uniform(0, 255), \ rng.uniform(0, 255)); cv::drawContours(*tmpMat, contoursPoly, static_cast<int>(i), \ color, 1, 8, \ std::vector<cv::Vec4i>(), 0, \ cv::Point()); cv::rectangle(*tmpMat, boundRect[i].tl(), \ boundRect[i].br(), \ color, 2, 8, 0); cv::circle(*tmpMat, center[i], static_cast<int>(radius[i]), \ color, 2, 8, 0); } outputInfo(1, tr("Contours done.")); break; } } if (ui->geometryChkBox->isChecked()) { *dispMat[numView] = tmpMat->clone(); cvtMatPixmap(dispMat, dispPixmap, numView); } else { if (tmpMat->channels() == 3) { QImage tmpImage = QImage(tmpMat->data, tmpMat->cols,tmpMat->rows, \ static_cast<int>(tmpMat->step), \ QImage::Format_RGB888); dispPixmap[numView]->setPixmap(QPixmap::fromImage(tmpImage.rgbSwapped())); } else { QImage tmpImage = QImage(tmpMat->data, tmpMat->cols,tmpMat->rows, \ static_cast<int>(tmpMat->step), \ QImage::Format_Grayscale8); dispPixmap[numView]->setPixmap(QPixmap::fromImage(tmpImage.rgbSwapped())); } } } void MainWindow::on_geometryBtn_clicked() { std::size_t numView = ui->tabWidget->currentIndex() % 3; if (dispMat[numView]->empty()) { outputInfo(2, tr("Please make sure the Mat exist!")); return; } if (dispMat[numView]->channels() == 3) { cv::cvtColor(*dispMat[numView], *dispMat[numView], cv::COLOR_RGB2GRAY); } tmpMat->zeros(dispMat[numView]->size(), \ dispMat[numView]->type()); cv::GaussianBlur(*dispMat[numView], *dispMat[numView], cv::Size(3, 3), \ 0, 0, cv::BORDER_DEFAULT); int operatorType = ui->geometryCombo->currentIndex(); // 0: line, 1: circle, 2: contours switch (operatorType) { case 0: { int cannyThresh = ui->cannyThreshSlider->value(); int lineThresh = ui->lineThreshSlider->value(); std::vector<cv::Vec4i> lines; cv::Canny(*dispMat[numView], *tmpMat, cannyThresh, \ cannyThresh * 2, 3); cv::HoughLinesP(*tmpMat, lines, 1, CV_PI/180, lineThresh, 50, 10); for (size_t i = 0; i < lines.size(); i++) { cv::Vec4i l = lines[i]; cv::line(*tmpMat, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), \ cv::Scalar(0, 0, 255), 1, 1, 1); } outputInfo(1, tr("Lines done.")); break; } case 1: { int circleThresh = ui->circleThreshSlider->value(); std::vector<cv::Vec3f> circles; double startTime = static_cast<double>(cv::getTickCount()); cv::HoughCircles(*dispMat[numView], circles, cv::HOUGH_GRADIENT, \ 1, dispMat[numView]->rows / 16, \ circleThresh, circleThresh/2, \ 0, 0); double timeCost = (static_cast<double>(cv::getTickCount()) - \ startTime) / cv::getTickFrequency(); QString costTime = "Cost time: " + QString::number(timeCost); outputInfo(1, costTime); for (size_t i = 0; i < circles.size(); i++) { cv::Point center(cvRound(circles[i][0]), \ cvRound(circles[i][1])); int radius = cvRound(circles[i][2]); cv::circle(*tmpMat, center, 3, \ cv::Scalar(0, 255, 0), -1, 8, 0); cv::circle(*tmpMat, center, radius, \ cv::Scalar(0, 0, 255), 3, 8, 0); } outputInfo(1, tr("Circles done.")); break; } case 2: { cv::RNG rng(12345); int cannyThresh = ui->cannyThreshSlider->value(); cv::Canny(*dispMat[numView], *tmpMat, cannyThresh, \ cannyThresh * 2, 3); std::vector<std::vector<cv::Point>> contours; std::vector<cv::Vec4i> hierarchy; cv::findContours(*tmpMat, contours, hierarchy, \ cv::RETR_TREE, \ cv::CHAIN_APPROX_SIMPLE, \ cv::Point(0, 0)); std::vector<std::vector<cv::Point>> contoursPoly(contours.size()); std::vector<cv::Rect> boundRect(contours.size()); std::vector<cv::Point2f> center(contours.size()); std::vector<float> radius(contours.size()); for (size_t i = 0; i < contours.size(); i++) { cv::approxPolyDP(cv::Mat(contours[i]), contoursPoly[i], 3, true); boundRect[i] = cv::boundingRect(cv::Mat(contoursPoly[i])); minEnclosingCircle(contoursPoly[i], center[i], radius[i]); } for (size_t i = 0; i < contours.size(); i++) { cv::Scalar color = cv::Scalar(rng.uniform(0, 255), \ rng.uniform(0, 255), \ rng.uniform(0, 255)); cv::drawContours(*tmpMat, contoursPoly, static_cast<int>(i), \ color, 1, 8, \ std::vector<cv::Vec4i>(), 0, \ cv::Point()); cv::rectangle(*tmpMat, boundRect[i].tl(), \ boundRect[i].br(), \ color, 2, 8, 0); cv::circle(*tmpMat, center[i], static_cast<int>(radius[i]), \ color, 2, 8, 0); } outputInfo(1, tr("Contours done.")); break; } } if (ui->geometryChkBox->isChecked()) { *dispMat[numView] = tmpMat->clone(); cvtMatPixmap(dispMat, dispPixmap, numView); } else { if (tmpMat->channels() == 3) { QImage tmpImage = QImage(tmpMat->data, tmpMat->cols,tmpMat->rows, \ static_cast<int>(tmpMat->step), \ QImage::Format_RGB888); dispPixmap[numView]->setPixmap(QPixmap::fromImage(tmpImage.rgbSwapped())); } else { QImage tmpImage = QImage(tmpMat->data, tmpMat->cols,tmpMat->rows, \ static_cast<int>(tmpMat->step), \ QImage::Format_Grayscale8); dispPixmap[numView]->setPixmap(QPixmap::fromImage(tmpImage.rgbSwapped())); } } }
总结
以上是关于利用Qt进行GUI构建并使用OpenCV中的 HoughLinesP/HoughCircles/findContours&drawContours函数进行图像检测。