OpenCV 圆与矩形识别的方法
前言在工业检测、答题卡识别、零件计数这类场景里「找出图里的圆和矩形」几乎是最常见的起点。很多人第一反应是找一个万能函数把图片丢进去就返回所有形状。实际上 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 并不等于矩形。一个梯形、一个被遮挡了一部分的方块、甚至一个分辨率很低的圆都可能被逼近成四个点。所以要再加三道判据凸性cv::isContourConvex必须为真排除凹四边形。直角四个内角都接近 90 度用相邻两边向量的点积判断余弦绝对值小于 0.2 大约对应 78 到 102 度。面积比轮廓面积与外接矩形面积之比接近 1排除极度细长的形状。对于允许旋转的矩形还可以用cv::minAreaRect得到最小面积旋转矩形再比较轮廓面积与旋转矩形面积的比值。这个比值对矩形接近 1对三角形约为 0.5区分度比正外接矩形更好。反过来如果要把圆从候选里剔掉可以算圆度4πA/P²其中 A 是面积、P 是周长。理想圆为 1正方形约为 0.785细长形状更低。这个指标对轮廓的锯齿比较敏感二值化质量差时会偏低只能作为辅助。四、完整示例下面的程序在同一张图上先标圆、再标矩形。为了让矩形判据不把圆误判成四边形代码里同时用了凸性、直角和面积比三个条件。// 适用于 C17OpenCV 4.x#include opencv2/opencv.hpp#include algorithm#include cmath#include iostream#include vectornamespace {// 判断四边形四个内角是否都接近直角bool isRightAngled(const std::vectorcv::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_castdouble(prev.x - cur.x);double v1y static_castdouble(prev.y - cur.y);double v2x static_castdouble(next.x - cur.x);double v2y static_castdouble(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;}} // namespaceint 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::vectorcv::Vec3f circles;cv::HoughCircles(gray, circles, cv::HOUGH_GRADIENT,1.0, // dpgray.rows / 8.0, // minDist120.0, // param1内部 Canny 高阈值35.0, // param2累加器阈值10, // minRadiusstd::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::vectorstd::vectorcv::Point contours;cv::findContours(edges, contours,cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);for (const std::vectorcv::Point contour : contours) {double area cv::contourArea(contour);if (area 800.0) continue; // 过滤小噪点double perimeter cv::arcLength(contour, true);std::vectorcv::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_castdouble(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::vectorstd::vectorcv::Point如果同时需要层级信息还要再传一个std::vectorcv::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需用周长比例圆和矩形没有统一的识别入口选择哪条路线取决于目标是否有简洁的参数化形式。霍夫变换把「找形状」变成「找参数空间的峰」轮廓法则把「找形状」变成「找顶点」两者的调参思路完全不同。把预处理、尺度归一化和几何判据这三层做好识别结果才稳定单靠调一个阈值换个场景就会失效。