☰
OpenCV 圆与矩形识别的方法
2026/11/5 4:48:04 网站建设 项目流程

前言


在工业检测、答题卡识别、零件计数这类场景里,「找出图里的圆和矩形」几乎是最常见的起点。很多人第一反应是找一个万能函数,把图片丢进去就返回所有形状。实际上 OpenCV 并没有这样的接口:圆和矩形走的是两条完全不同的技术路线,圆靠霍夫变换在参数空间里投票,矩形靠轮廓分析加多边形逼近。把两者混在一个流程里,参数会互相牵制,结果往往两边都不准。


另一个常见误解是把「识别」等同于「检测」。检测只要求给出位置,识别还要求判断这个形状到底属于哪一类。霍夫圆变换天然只输出圆,而轮廓法找到的四边形可能是矩形、平行四边形、梯形,甚至是一个被噪声削掉一角的圆,必须再补几个几何判据才能下结论。


本文以 OpenCV 4.x 的 C++ 接口为准,先讲清两条路线的原理与参数含义,再给出一份把圆和矩形放在同一张图上标注的完整示例。文中所有阈值都是起点而不是标准答案,实际图像需要自己调;不同 OpenCV 小版本之间个别默认行为可能有差异,具体以官方文档为准。


一、两条路线与共用的预处理


圆的参数只有三个:圆心横坐标、纵坐标、半径。这让它非常适合霍夫变换——把每个边缘点在参数空间里可能对应的所有圆都投一票,票数最高的参数组合就是圆。矩形没有这么简洁的参数化方式,它的位置、尺寸、旋转角组合起来维度更高,所以工程上普遍改用「先取轮廓,再把轮廓逼近成多边形,最后看顶点」的办法。


两条路线共用的部分是预处理。彩色图先转灰度,因为这两个算法都只关心亮度梯度;再用高斯模糊压掉传感器噪声,核大小取奇数,常见是 3 或 5。区别在于:霍夫圆直接吃灰度图,它内部会自己做边缘提取;轮廓法通常要再经过 Canny 或阈值化,得到一张二值图。


预处理阶段最容易被忽略的是分辨率。同一组参数在 640 像素宽的图上合适,换到 4000 像素的图上就会失效,因为minDist、minRadius这类参数都是以像素为单位的绝对量。稳妥的做法是把图像缩放到一个固定宽度再处理,或者把所有像素参数按缩放比例同步放大。


二、霍夫圆变换与它的四个关键参数


cv::HoughCircles的核心是梯度霍夫变换。它先算每个像素的梯度方向,然后沿着梯度方向(以及反方向)在累加器里为可能的圆心投票,投票时用圆心到边缘点的距离作为半径。相比最早的完全三维累加器,利用梯度方向把搜索范围缩小到一条射线,内存和计算量都降下来了。


函数签名里参数不少,含义如下:




参数含义调整方向



method检测方法,目前实用的是cv::HOUGH_GRADIENT固定

dp累加器分辨率与图像分辨率之比一般取 1;取 2 时累加器减半,速度略快、精度略降

minDist两个圆心之间的最小距离太小会重复检出同一个圆,太大会漏掉相邻圆

param1内部 Canny 的高阈值,低阈值取它的一半边缘弱时调低

param2累加器阈值,越小检出的圆越多误检多就调大,漏检多就调小

minRadius/maxRadius半径范围,传 0 表示不限已知目标尺度时一定要限制



param2是最难调的一个,它和图像分辨率、边缘强度都相关,没有通用数值。经验做法是先把它设得很低,观察误检的分布,再逐步提高直到误检消失;如果这时真目标也被滤掉,说明param1太低导致边缘太碎,应该先调param1。


还有一点:dp取大于 1 的值时,累加器坐标是缩放过的,最终输出的圆心坐标仍会换算回原图尺度,不需要自己乘回去。这一点常被误解,有人手动再乘一次dp,位置就整体偏移了。


三、轮廓逼近与矩形的几何判据


矩形检测的流程是:二值化或 Canny 得到边缘,cv::findContours取出轮廓,cv::approxPolyDP把轮廓简化成折线,最后判断折线的顶点数与角度。


cv::approxPolyDP用的是 Douglas-Peucker 算法,第三个参数epsilon是允许的最大偏差距离。它必须是相对量,通常取轮廓周长的 1% 到 2%。如果写成固定的 5 个像素,小目标的轮廓会被压成一个三角形,大目标的轮廓则压不下去,顶点数仍然成百上千。


顶点数等于 4 并不等于矩形。一个梯形、一个被遮挡了一部分的方块、甚至一个分辨率很低的圆,都可能被逼近成四个点。所以要再加三道判据:



  1. 凸性:cv::isContourConvex必须为真,排除凹四边形。

  2. 直角:四个内角都接近 90 度,用相邻两边向量的点积判断,余弦绝对值小于 0.2 大约对应 78 到 102 度。

  3. 面积比:轮廓面积与外接矩形面积之比接近 1,排除极度细长的形状。


对于允许旋转的矩形,还可以用cv::minAreaRect得到最小面积旋转矩形,再比较轮廓面积与旋转矩形面积的比值。这个比值对矩形接近 1,对三角形约为 0.5,区分度比正外接矩形更好。


反过来,如果要把圆从候选里剔掉,可以算圆度:4πA/P²,其中 A 是面积、P 是周长。理想圆为 1,正方形约为 0.785,细长形状更低。这个指标对轮廓的锯齿比较敏感,二值化质量差时会偏低,只能作为辅助。


四、完整示例


下面的程序在同一张图上先标圆、再标矩形。为了让矩形判据不把圆误判成四边形,代码里同时用了凸性、直角和面积比三个条件。


// 适用于 C++17(OpenCV 4.x)
#include <opencv2/opencv.hpp>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <vector>

namespace {

// 判断四边形四个内角是否都接近直角
bool isRightAngled(const std::vector<cv::Point>& quad) {
for (int i = 0; i < 4; ++i) {
const cv::Point& prev = quad[(i + 3) % 4];
const cv::Point& cur = quad[i];
const cv::Point& next = quad[(i + 1) % 4];

double v1x = static_cast<double>(prev.x - cur.x);
double v1y = static_cast<double>(prev.y - cur.y);
double v2x = static_cast<double>(next.x - cur.x);
double v2y = static_cast<double>(next.y - cur.y);

double n1 = std::sqrt(v1x * v1x + v1y * v1y);
double n2 = std::sqrt(v2x * v2x + v2y * v2y);
if (n1 < 1e-6 || n2 < 1e-6) return false;

double cosine = (v1x * v2x + v1y * v2y) / (n1 * n2);
if (std::fabs(cosine) > 0.2) return false;
}
return true;
}

} // namespace

int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "usage: detect <image>\n";
return 1;
}

cv::Mat src = cv::imread(argv[1], cv::IMREAD_COLOR);
if (src.empty()) {
std::cerr << "cannot read image\n";
return 1;
}

cv::Mat gray;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, gray, cv::Size(5, 5), 1.5);

cv::Mat canvas = src.clone();

// ---------- 圆 ----------
std::vector<cv::Vec3f> circles;
cv::HoughCircles(gray, circles, cv::HOUGH_GRADIENT,
1.0, // dp
gray.rows / 8.0, // minDist
120.0, // param1,内部 Canny 高阈值
35.0, // param2,累加器阈值
10, // minRadius
std::min(gray.rows, gray.cols) / 2);

for (const cv::Vec3f& c : circles) {
cv::Point center(cvRound(c[0]), cvRound(c[1]));
int radius = cvRound(c[2]);
cv::circle(canvas, center, radius, cv::Scalar(0, 0, 255), 2);
cv::circle(canvas, center, 2, cv::Scalar(0, 0, 255), -1);
}

// ---------- 矩形 ----------
cv::Mat edges;
cv::Canny(gray, edges, 50, 150);

std::vector<std::vector<cv::Point>> contours;
cv::findContours(edges, contours,
cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

for (const std::vector<cv::Point>& contour : contours) {
double area = cv::contourArea(contour);
if (area < 800.0) continue; // 过滤小噪点

double perimeter = cv::arcLength(contour, true);
std::vector<cv::Point> approx;
cv::approxPolyDP(contour, approx, perimeter * 0.02, true);

if (approx.size() != 4) continue;
if (!cv::isContourConvex(approx)) continue;
if (!isRightAngled(approx)) continue;

// 面积比:过滤极度细长的四边形
cv::Rect box = cv::boundingRect(approx);
if (box.width <= 0 || box.height <= 0) continue;
double ratio = area / (static_cast<double>(box.width) * box.height);
if (ratio < 0.75) continue;

cv::polylines(canvas, approx, true, cv::Scalar(0, 255, 0), 2);
}

cv::imwrite("detected.png", canvas);
return 0;
}

编译时用 CMake 配置 OpenCV 比较省事:


cmake_minimum_required(VERSION 3.16)
project(detect_shapes CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(OpenCV 4 REQUIRED)

add_executable(detect_shapes main.cpp)
target_link_libraries(detect_shapes PRIVATE ${OpenCV_LIBS})
target_include_directories(detect_shapes PRIVATE ${OpenCV_INCLUDE_DIRS})

代码里有几处需要说明。cv::HoughCircles的minDist参数类型是double,这里写gray.rows / 8.0是为了避免整数除法把结果截断。cvRound是 OpenCV 提供的四舍五入函数,返回int,对负值和中值的处理与std::round可能不同,用在坐标上是安全的。cv::findContours的第二个参数类型是std::vector<std::vector<cv::Point>>,如果同时需要层级信息,还要再传一个std::vector<cv::Vec4i>。cv::polylines接受单个点序列时会把它当成一条折线,最后一个参数true表示闭合。


另外,程序用cv::imwrite而不是cv::imshow,是为了在没有图形界面的服务器上也能跑。需要窗口预览时再补cv::imshow和cv::waitKey(0),但要确认所用的 OpenCV 构建带 highgui 后端。


常见坑点


坑点一:把三通道彩色图直接传给HoughCircles。


❌cv::HoughCircles(src, circles, cv::HOUGH_GRADIENT, 1, 20, 100, 30);,其中src来自cv::imread的默认读取。


✅ 先cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY),函数只接受 8 位单通道图。


坑点二:param2直接照抄教程数值。


❌ 在高分辨率图上仍用param2 = 30,结果同一片区域检出十几个同心圆。


✅ 先降低param1保证边缘完整,再逐步提高param2直到重复检测消失,同时用minDist约束圆心间距。


坑点三:用绝对像素做approxPolyDP的精度。


❌cv::approxPolyDP(contour, approx, 5, true);换一张分辨率不同的图就失效。


✅double eps = 0.02 * cv::arcLength(contour, true);让精度随尺度变化。


坑点四:只凭顶点数等于 4 就判定矩形。


❌if (approx.size() == 4) { /* 认为是矩形 */ },梯形和部分圆都会被算进去。


✅ 再检查cv::isContourConvex、四个直角和面积比。


坑点五:在 Canny 结果上直接找矩形,边缘不闭合。


❌ 对低对比度目标只做cv::Canny就调用findContours,轮廓断成几段,凑不出四个顶点。


✅ 改用自适应阈值或 OTSU 得到实心前景,必要时先做一次形态学闭运算连接断口。


坑点六:忽略imread的失败返回。


❌cv::Mat src = cv::imread(path);之后直接cvtColor,路径写错时在深处崩溃。


✅ 立即检查src.empty(),失败时打印路径并返回非零值。


坑点七:在无图形界面的环境调用imshow。


❌ 服务器上运行cv::imshow加cv::waitKey,程序抛异常或直接中止。


✅ 用cv::imwrite输出结果;确需预览时确认 highgui 后端可用。


总结




环节圆矩形



核心算法梯度霍夫变换轮廓 + 多边形逼近

输入8 位单通道灰度图二值图或边缘图

关键参数dp、minDist、param1、param2epsilon、面积阈值

主要判据累加器票数4 顶点、凸性、直角、面积比

常见误检同心重复圆、纹理噪点梯形、被遮挡的方块

尺度敏感度高,半径与间距都是像素量中,epsilon需用周长比例



圆和矩形没有统一的识别入口,选择哪条路线取决于目标是否有简洁的参数化形式。霍夫变换把「找形状」变成「找参数空间的峰」,轮廓法则把「找形状」变成「找顶点」,两者的调参思路完全不同。把预处理、尺度归一化和几何判据这三层做好,识别结果才稳定;单靠调一个阈值,换个场景就会失效。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询