
简介一个面向计算机视觉学习者的OpenCV C实践资源包围绕图像处理、机器学习与深度学习所需的图像预处理环节系统演示边缘检测、轮廓提取、形状识别、透视变换与图像校正等技术重点解决正方形和四边形检测问题适合课设、竞赛及工业视觉项目参考。压缩包共12个文件、约951KB包含核心C源文件squares.cpp、CMakeLists与Makefile构建脚本、4张真实场景JPG测试图、3个TXT说明文件、1份PDF附赠资料及Markdown文档文件划分直观既适合完整运行也可按需查看思路说明。目前已有247人学习下载。通过阅读源码与说明读者可以借鉴基于霍夫变换、阈值处理和特征提取的完整检测流程理解从图像预处理到形状识别、透视校正的工程实现为后续图像识别、图像分割等计算机视觉应用打下扎实基础。1. 从 OpenCV 的 C 接口到正方形检测一条能落地的视觉处理链路计算机视觉课程项目里最常出现的任务是把一张斜着拍的文档、答题卡或者工件图自动找到其中的正方形或四边形区域再通过透视变换拉正成俯视图。这个标题里的 OpenCV、C、图像处理、边缘检测、轮廓提取、霍夫变换、透视变换、图像校正串在一起正是这套完整流程。单纯调用findContours只能拿到一堆散点单纯跑warpPerspective又需要手动点四个角。真正可用的方案是把阈值分割、轮廓筛选、多边形逼近、顶点排序和单应性矩阵计算按顺序接起来每一步的参数都直接影响下一环节的输入质量。下面用 C 和 OpenCV 4.x 把这条链路完整写一遍重点放在四边形判定、顶点排序和透视校正这三个最容易翻车的环节上。2. OpenCV C 图像预处理灰度、阈值与 Canny 边缘检测参数怎么选预处理是整个流程的地基。很多人直接对彩色图跑边缘检测结果轮廓碎成一片也有人跳过模糊直接阈值化导致噪声点被当成候选顶点。正确的顺序应该是灰度化 → 去噪 → 阈值分割或直接边缘检测→ 得到干净的轮廓输入。2.1 读图、灰度化与高斯模糊imread的通道坑和核大小选择imread默认加载为 BGR 三通道如果后续要用threshold或Canny必须先用cvtColor转灰度。这里有一个容易踩的坑OpenCV 的灰度化公式是Y 0.299R 0.587G 0.114B与 RGB 权重不同所以cvtColor的结果和直接用cv::Mat手动加权不完全一致但视觉差异极小直接调用官方接口即可。模糊核GaussianBlur的 Size 建议用奇数(5, 5)sigma传 0 表示由函数根据核大小自动计算这个默认行为在大多数场景下比手动指定 sigma 更稳。cv::Mat gray, blurred; cv::cvtColor(bgrInput, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);GaussianBlur的核越大边缘越平滑但弱边缘也会被抹掉。做正方形检测时目标边缘通常是强边缘(5, 5)足够如果图像噪声特别重可以加到(7, 7)但要注意分割后的轮廓边界会向内收缩几个像素对透视校正影响不大。2.2 阈值化全局阈值、Otsu 与自适应阈值三选一阈值化是为了把前景和背景分开得到二值图。实际项目里常用的有三类方式方式适用场景关键参数注意事项THRESH_BINARY光照均匀、前景背景灰度差稳定手动阈值如 127光照一变就失效THRESH_BINARY THRESH_OTSU双峰灰度分布阈值传 0由 Otsu 计算直方图单峰时分割结果随机adaptiveThreshold光照不均、阴影、渐变blockSize奇数、C 常数参数敏感块大小过小会出纹理很多教程推荐THRESH_BINARY但在真实拍摄场景文档扫描、答题卡拍照中光照几乎不可能完全均匀。我的常规做法是优先试 Otsu效果不好再切自适应阈值。注意自适应阈值的blockSize必须是奇数通常取 31 到 51C是常数偏置表示从邻域均值中减去的量C 太小会把背景噪声带进来C 太大会把目标的浅色部分切掉。正方形检测时目标往往是深色线条或深色色块所以阈值模式用THRESH_BINARY_INV把目标变成白色前景方便后续findContours提取。cv::Mat binary; cv::threshold(blurred, binary, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);Otsu 的核心思想是最大化前景和背景的类间方差它假设灰度直方图是双峰的。如果目标例如文档占画面比例过大或过小直方图可能只有一个峰Otsu 就会失效。此时改用adaptiveThresholdcv::Mat adaptive; cv::adaptiveThreshold(blurred, adaptive, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, 41, 10);blockSize 41表示每个像素的阈值由 41×41 邻域的高斯加权均值减去C10决定。这个值越大对局部光照变化越不敏感但块边缘容易出现伪影。2.3 Canny 边缘检测双阈值设置与二值图输入的问题拿到二值图之后有人习惯再跑一次 Canny这是个常见误区。Canny 内部包含梯度计算和非极大值抑制对灰度图效果好如果输入已经是二值图Canny 会退化成一个轮廓细化工具白白增加处理时间。更合理的路线有两条路线一二值图直接进findContours用轮廓面积和多边形逼近筛目标路线二灰度图直接进 Canny再从边缘图做霍夫变换提取直线。如果坚持用 Canny低阈值和高阈值的比例建议在 1:2 到 1:3 之间例如(50, 150)。低阈值以下的梯度直接丢弃高阈值以上的强边缘保留介于两者之间的像素只有与强边缘相连才会被保留。这个“滞后阈值”机制能有效抑制噪声边缘。cv::Mat edges; cv::Canny(blurred, edges, 50, 150);Canny 输出的边缘图是单通道 8 位图非边缘为 0边缘为 255可以直接作为霍夫变换的输入。3. 轮廓提取与四边形判定findContours、approxPolyDP 与形状识别这一章是整个检测流程的核心。目标是从预处理得到的二值图或边缘图中找到“最像四边形”的那一个轮廓并取出它的四个顶点。这里优先用轮廓方法因为四边形在图像中对应闭合边界轮廓提取天然适合霍夫变换则作为边缘断裂时的兜底方案。3.1 findContours 的检索模式选 RETR_EXTERNAL 还是 RETR_TREEOpenCV 4.x 的findContours函数签名如下void cv::findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset Point());注意第一个参数是InputOutputArray函数内部会修改输入图像OpenCV 3.2 之后不再破坏原图但为了兼容性仍然建议传入clone()否则后续代码可能拿到被污染的数据。mode参数决定轮廓的组织方式。常用的四个取值对比检索模式行为适用场景RETR_EXTERNAL只返回最外层轮廓正方形检测首选速度快RETR_LIST返回所有轮廓不建立层级简单场景但可能拿到噪声块RETR_CCOMP两层结构外层 孔洞需要区分内外边界时RETR_TREE完整层级关系复杂场景需要父子关系做正方形检测时目标通常是画面中最大的闭合区域用RETR_EXTERNAL可以跳过内部文字产生的杂散轮廓。如果目标内部有大量文字比如一张写着字的 A4 纸RETR_EXTERNAL会直接忽略内部轮廓只留下纸张的外边界这恰恰是我们想要的。method参数用CHAIN_APPROX_SIMPLE它会压缩水平、垂直和对角方向的连续点只保留端点。一个正方形轮廓原本可能有几百个点压缩后只剩 4 个点大幅降低后续计算量。std::vectorstd::vectorcv::Point contours; cv::findContours(binary.clone(), contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);3.2 approxPolyDP 多边形逼近epsilon 决定顶点数量得到轮廓点集之后直接用contourArea筛选面积最小阈值然后对通过面积筛选的轮廓做多边形逼近std::vectorcv::Point approx; double peri cv::arcLength(contour, true); cv::approxPolyDP(contour, approx, 0.02 * peri, true);arcLength计算轮廓周长approxPolyDP用道格拉斯-普克算法对轮廓做折线逼近。第三个参数是最大逼近误差一般取周长的 1% 到 3%这里用0.02 * peri。epsilon 越小逼近越精确顶点数可能多于 4epsilon 越大矩形可能被压成三角形。如果逼近结果approx.size() ! 4说明这个轮廓不是四边形直接跳过。这里有一个隐含的顺序问题先用面积过滤掉小噪声再做多边形逼近可以避免噪声轮廓消耗逼近计算。面积阈值建议按图像尺寸动态设置而不是固定像素值double imgArea binary.cols * binary.rows; double minArea imgArea * 0.01; // 目标至少占画面 1%在多个轮廓都满足条件时取面积最大的那个作为目标。这个策略在文档校正场景中非常有效因为文档总是占据画面主体。3.3 四边形筛选与正方形判定面积、角度和边长比光有 4 个顶点还不够还要验证这四个点组成的形状“像不像”四边形或正方形。因为透视变形会让正方形在图像中变成任意四边形所以不能直接要求四边相等或四个角都是 90 度只能做带容差的判定bool isQuadrilateral(const std::vectorcv::Point approx, double angleToleranceDeg 20.0) { if (approx.size() ! 4) return false; for (int i 0; i 4; i) { cv::Point pPrev approx[(i 3) % 4]; cv::Point pCurr approx[i]; cv::Point pNext approx[(i 1) % 4]; cv::Point v1 pPrev - pCurr; cv::Point v2 pNext - pCurr; double cosAngle (v1.dot(v2)) / (cv::norm(v1) * cv::norm(v2)); double angleDeg std::acos(std::max(-1.0, std::min(1.0, cosAngle))) * 180.0 / CV_PI; if (std::fabs(angleDeg - 90.0) angleToleranceDeg) return false; } return true; }容差取 20 度意味着内角在 70 度到 110 度之间都接受。透视变形严重的四边形可能偏离直角更多但那种情况即使校正好视觉观感也不佳所以这个阈值是经验值的合理折中。正方形和一般四边形的区别在于边长比。这个过程放在透视变换之后再判断会更准确具体见第 5 章。3.4 霍夫变换兜底HoughLinesP 找直线再求交点轮廓方法有一个致命弱点如果目标边缘与背景对比度低或边缘被遮挡findContours拿到的是断裂的轮廓approxPolyDP会得到 5 个、6 个甚至更多顶点。此时改用霍夫变换检测直线再求直线交点来还原四边形。std::vectorcv::Vec4i lines; cv::HoughLinesP(edges, lines, 1, CV_PI / 180, 60, 50, 20);参数含义分别是rho1表示距离分辨率 1 像素thetaCV_PI/180表示角度分辨率 1 度threshold60是投票数阈值一条直线至少需要 60 个点支持minLineLength50是最短线段长度maxLineGap20是同一直线上断点之间的最大连接间距。得到线段集合后按斜率或角度聚类成四组取每组中最长的线段然后求四条直线的交点。这四个交点就是四边形的近似顶点。这个方案适合边缘线明显但轮廓破碎的场景缺点是参数多、调试繁琐。日常项目中我通常先用轮廓法失败才切霍夫两条路线都要在代码里预留切换开关。4. 四点透视变换实现图像校正getPerspectiveTransform 与顶点排序找到了四边形的四个顶点接下来的问题是如何把它们映射到一个正矩形上。这一步叫透视校正在计算机视觉中的透视几何里属于单应性变换把图像平面上的一个四边形映射到另一个四边形。三个核心环节顶点排序、变换矩阵计算、输出尺寸确定。4.1 四角点排序为什么直接传点会得到翻转图getPerspectiveTransform要求源点和目标点按顺序对应。如果顺序错了校正结果会出现镜像翻转、左右颠倒甚至“蝴蝶结”形状。最常见的顺序要求是左上 → 右上 → 右下 → 左下对应目标矩形的(0,0) → (w,0) → (w,h) → (0,h)。但findContours返回的顶点顺序是不确定的尤其是经过approxPolyDP之后四个点的排列可能顺时针也可能逆时针起点也可能不同。必须重新排序。最稳定的排序方法是利用几何位置特征std::vectorcv::Point2f orderCorners(const std::vectorcv::Point2f corners) { std::vectorcv::Point2f pts corners; std::sort(pts.begin(), pts.end(), [](const cv::Point2f a, const cv::Point2f b) { return a.x a.y b.x b.y; }); cv::Point2f tl pts[0]; cv::Point2f br pts[3]; cv::Point2f tr, bl; if (pts[1].x pts[2].x) { tr pts[1]; bl pts[2]; } else { tr pts[2]; bl pts[1]; } return {tl, tr, br, bl}; }逻辑说明左上角的xy最小右下角的xy最大剩余两个点中x较大的是右上角因为图像坐标系 x 向右增大。这个排序法在目标旋转角度小于 45 度时可靠。如果目标被旋转了 90 度比如竖拍的文档横过来了排序仍然正确但校正结果会旋转 90 度实际应用中需要在排序后加一个旋转检测比较tl到tr的向量与tl到bl的向量如果前者接近垂直方向说明图像需要旋转。4.2 getPerspectiveTransform 与输出尺寸比例如何不拉伸顶点排序完成后就可以计算变换矩阵。输出尺寸如果设得不对校正结果会被横向或纵向拉伸变形。最接近真实比例的做法是用四边形的边长来推算宽高。cv::Mat rectifyQuad(const cv::Mat src, const std::vectorcv::Point2f corners, int outputLongSide 640) { auto pt orderCorners(corners); cv::Point2f tl pt[0], tr pt[1], br pt[2], bl pt[3]; double wTop cv::norm(tr - tl); double wBottom cv::norm(br - bl); double hLeft cv::norm(bl - tl); double hRight cv::norm(br - tr); double outW std::max(wTop, wBottom); double outH std::max(hLeft, hRight); double scale outputLongSide / outW; int dstW static_castint(outW * scale); int dstH static_castint(outH * scale); std::vectorcv::Point2f srcPts {tl, tr, br, bl}; std::vectorcv::Point2f dstPts { cv::Point2f(0, 0), cv::Point2f(dstW, 0), cv::Point2f(dstW, dstH), cv::Point2f(0, dstH) }; cv::Mat H cv::getPerspectiveTransform(srcPts, dstPts); cv::Mat corrected; cv::warpPerspective(src, corrected, H, cv::Size(dstW, dstH), cv::INTER_CUBIC, cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0)); return corrected; }这个处理想说明一个重点为什么用max而不是平均。用max会保留四边形较短边的信息保证校正后短边方向不会出现截断缺点是个别情况下比例会略有拉伸。如果你要求严格的宽高比可以把max换成(wTop wBottom) / 2和(hLeft hRight) / 2两种方案在这个流程里都常见按需求选择。getPerspectiveTransform和多点估计的findHomography差别在于前者只用 4 个点适合已知边界对应关系的情况后者至少需要 4 个点通常配合 RANSAC 用于特征点匹配场景。这里用前者就够了。4.3 warpPerspective 插值与边界填充参数warpPerspective的插值参数需要单独说明。INTER_LINEAR双线性插值速度最快适合实时处理INTER_CUBIC三次插值在图像放大时边缘更平滑适合校正后要送去 OCR 或做边缘检测的场景INTER_LANCZOS4质量最高但耗时明显。正方形检测校正的输出尺寸通常不大推荐直接用INTER_CUBIC。边界填充用BORDER_CONSTANT并把填充色设为黑色。如果改成BORDER_REPLICATE校正后的图像边缘会出现一条拉伸的伪影干扰后续的字符识别或形状分析。实际调用方式cv::Mat img cv::imread(input.jpg); // ...预处理得到 binaryfindContours 得到四边形 approx... std::vectorcv::Point2f corners; for (const auto p : approx) corners.emplace_back(p.x, p.y); cv::Mat result rectifyQuad(img, corners, 640); cv::imwrite(result.jpg, result);校正后的图像就是一张“看起来像正对目标拍摄”的图可以直接交给后面的分类器、OCR 或者人工查看。5. 正方形检测的边界条件与进阶调优5.1 精度验证用合成透视图做回归测试人工看几张图觉得“差不多”不算数。最有效的验证方法是合成测试集用cv::warpPerspective生成随机仿射或透视变换的矩形图片记录下真实顶点坐标再跑检测代码计算检测顶点与真值顶点之间的平均像素误差。误差小于 3 像素说明参数合理。比如生成 100 张带随机旋转、随机光照的图批量跑一遍统计角点误差的均值和最大值比肉眼排查高效得多。这个测试框架建议在项目一开始就搭好每次改线框参数或阈值选择都能立刻看到精度变化。5.2 从四边形到正方形边长比怎么设容差“四边形检测”能筛掉非矩形的形状但要判断它是不是正方形还需要检查边长比例。透视校正之后正方形在正视图中的四条边应该等长。判断方式bool isSquare(const std::vectorcv::Point2f corners) { double d01 cv::norm(corners[1] - corners[0]); double d12 cv::norm(corners[2] - corners[1]); double d23 cv::norm(corners[3] - corners[2]); double d30 cv::norm(corners[0] - corners[3]); double maxLen std::max({d01, d12, d23, d30}); double minLen std::min({d01, d12, d23, d30}); return maxLen / minLen 1.2; }边长比阈值取 1.2 表示允许 20% 的误差。拍照角度导致的透视失真在校正后通常能压到 5% 以内所以 1.2 是一个比较宽容的设定。如果你想区分“严格正方形”和“普通矩形”把阈值收到 1.05 即可。5.3 传统 CV 与深度学习方案的边界在哪里标题里的机器学习、深度学习在这个场景里通常有两个角色一是校正后的质量分类判断图像是否清晰、是否倾斜二是端到端的目标检测用语义分割或关键点回归替代手工轮廓提取。传统 CV 方案的优势是无需数据标注、运行在 CPU 上也能达到毫秒级适合工位固定、光照可控的工业环境。深度学习方案的优势是抗遮挡、抗背景杂乱但需要几千张标注图和一小时的训练时间。实际项目里常见的做法是先用传统方案快速出原型如果失败率超过 10% 再考虑用深度学习做第一阶段的四边形检测传统 CV 的方法则保留在光源稳定场景兜底。角点误差和形态学闭运算这两件小事决定了透视校正项目在真实环境里的可用性。形态学闭运算能补轮廓断裂而角点误差直接反映排序算法和顶点坐标的稳定性。把这两个指标写进项目的测试流程后续迭代就不用反复改代码、看效果、猜参数。本文还有配套的精品资源点击获取