前阵子接了个视觉项目,要在一堆工件里识别不同规格的零件。一开始我也想着直接往深度学习方向走,后来发现现实情况根本用不上——现场设备性能有限,光线环境还多变,反而是被人当“老古董”的OpenCV常用检测算法救了大忙。Canny边缘检测、Harris角点检测、SIFT/ORB特征点、霍夫直线、轮廓分析、Haar Cascade人脸检测,这些算法听起来像教科书目录,但真到了工业现场、嵌入式设备、快速原型验证的场合,它们依然是效率最高的那一批工具。
这篇文章我想把OpenCV里最常用、也最容易被忽视的检测算法整理一遍。不是照抄API文档,而是说清楚每个算法到底在解决什么问题、为什么适合这个场景、实际项目里怎么调参、坑在哪里。刚入门的人可以按顺序读一遍,摸清这些算法的脾气;有经验的人可以直接跳到后面看调试经验那部分,应该也能捞到点东西。
1. 先把“检测”这件事掰开揉碎
1.1 同一个“检测”,背后完全不是一回事
OpenCV里的检测算法看起来都是一行行函数调用,但它们的检测目标、数学原理、适用场景差距非常大。我在指导新人的时候,第一步永远是让他们搞清楚自己到底要检测什么。有的人说要“检测图像里的东西”,这句话就等于没说——边缘、角点、特征点、直线、轮廓、人脸,是六个完全不同的方向。
边缘检测找的是灰度剧烈变化的位置,给你的是像素轮廓;角点检测找的是两个方向梯度变化都很剧烈的点,给你的是关键位置;特征点检测则更进一步,它除了找到关键点,还会为每个关键点计算一个描述子,相当于给这个点按了个指纹;霍夫变换是把边缘点映射到参数空间去找规则线条;轮廓检测是把边缘连成闭合或不闭合的边界,然后分析它的面积、周长、形状;Haar Cascade则是用训练好的级联分类器去框出特定目标。
我经常打一个比方:边缘检测是看剪影,角点检测是找转折处,特征点检测是采集指纹,霍夫变换是拿尺子找直线,轮廓检测是拼拼图,Haar Cascade是拿着通缉令找人。这六个东西完全不在一个维度上,选错了整个流程都会白搭。
1.2 算法选型应该先于代码
我在项目里的习惯是,先花五到十分钟想清楚检测目标,再决定用哪一套流程,最后才打开编辑器写代码。
| 需求场景 | 首选算法 | 典型应用 |
|---|---|---|
| 提取物体边界、测量尺寸边缘 | Canny + 轮廓分析 | 工件尺寸质检、边缘对齐 |
| 找图像中的显著转折点 | Harris / Shi-Tomasi | 相机标定板角点定位 |
| 跨图像寻找同一物体、拼接全景 | SIFT / ORB + 特征匹配 | 全景拼接、相似图片查找 |
| 检测笔直的道路、电线、刻度线 | HoughLinesP | 车道线检测、仪表盘读数 |
| 识别固定形状、统计数量 | findContours + 多边形逼近 | 零件分拣、细胞统计 |
| 人脸区域快速定位 | Haar Cascade | 闸机人脸抓拍、照片人像框选 |
这个表我用了很多年,基本覆盖了80%以上应用OpenCV检测算法的场景。关键点是:大部分需求可以拆成“预处理→基础检测→几何分析”三段,很多人觉得算法难用,其实是跳过了预处理,把半成品扔给了检测函数。
1.3 预处理是检测算法的地基
这是我踩过最大的坑之一。早年我拿到图像直接跑Canny,结果边缘碎得像打碎的玻璃。后来明白了一个道理:像Canny、Harris、霍夫变换这类算法,都对输入图像的质量有严格要求。教科书上给的都是理想图像,但现实里的图像有噪声、有光照不均、有反光。
基础的预处理链路一般是这样:去噪(高斯模糊、中值滤波)→ 灰度化 →(按需)增强对比度→ 二值化。检测算法是高高在上的决策者,预处理则是给这个决策者递上一份干净情报的侦察兵。不看情报就做决策,结果自然离谱。后面每一个算法,我都会强调它和预处理之间的配合关系。
2. Canny边缘检测:工业视觉里的万金油
2.1 Canny的完整处理管线拆解
Canny边缘检测在OpenCV里就是一行cv2.Canny(),但这一行后面藏了四个完整步骤:高斯滤波、Sobel梯度计算、非极大值抑制、双阈值滞后连接。我建议所有想深入用OpenCV的人都把它拆开看一遍,因为后面很多算法的效果瓶颈就藏在Canny内部。
第一步高斯滤波,是对图像做加权平均,把像素级噪声抹平一点。第二步用Sobel算子算水平和垂直方向的梯度,然后合成梯度的幅值和方向。第三步非极大值抑制,是沿着梯度方向只保留局部极大值——这句话翻译成人话就是“让边缘变细”。没有这一步,边缘会是一大片模糊的亮带。第四步双阈值滞后连接,设一个高阈值和一个低阈值,高于高阈值的点确定为边缘,低于低阈值的点丢弃,介于中间的像素点只有连接在确定边缘上才会被保留。
2.2 双阈值到底怎么定
双阈值是Canny里最需要手感的地方。官方文档只告诉你设两个值,但不告诉你比例关系。我摸了很久之后总结的经验是:高阈值和低阈值一般取2:1到3:1之间。高阈值太高,边缘会断成虚线;低阈值太低,图像会变成一张蜘蛛网。
实际项目里我很少把阈值固定死。环境光线一变,最优阈值就会漂移。工程做法有两种。一种是离线标定几组阈值,运行时根据图像亮度做插值;另一种是先做自适应阈值处理,再用Canny。第二个方法在OpenCV里就是cv2.adaptiveThreshold(),它对光照渐变的鲁棒性好很多,但计算量会上去一点。
2.3 一个实时边缘检测的实战例子
下面这个例子把摄像头画面实时提取边缘,用滑动条调阈值,方便现场调试。这段代码我在不同项目里改过很多次,算是固定起手式。
import cv2 import numpy as np def nothing(x): pass cv2.namedWindow('Canny Demo') cv2.createTrackbar('Low', 'Canny Demo', 50, 255, nothing) cv2.createTrackbar('High', 'Canny Demo', 150, 255, nothing) cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 先做高斯模糊,降低噪声对边缘的干扰 gray = cv2.GaussianBlur(gray, (5, 5), 1.0) low = cv2.getTrackbarPos('Low', 'Canny Demo') high = cv2.getTrackbarPos('High', 'Canny Demo') edges = cv2.Canny(gray, low, high) cv2.imshow('Canny Demo', edges) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()调试时可以先把高阈值固定在一个值,只拉低阈值,观察边缘从哪里开始变密;然后反过来固定低阈值,拉高阈值,看边缘从哪里开始断裂。找到那个“再多一点就碎、再少一点就密”的临界点,就是合适的高阈值区间。
3. Harris与Shi-Tomasi角点检测:不是所有点都是角点
3.1 角点的数学直觉:窗口移动到哪都在变
角点检测的经典思路是这样的:在一个像素附近放一个小窗口,如果这个窗口向任何方向做微小移动,窗口内的灰度都发生明显变化,那这个点就是角点。如果只有沿某个方向移动会变,那是边缘;如果怎么移动都不变,那是平坦区域。
Harris就是把这个直觉数学化了。它对窗口内的梯度做协方差矩阵M,然后计算一个响应值R = det(M) - k * trace(M)^2。k是个经验常数,一般取0.04到0.06。R大说明这个点的两个方向梯度都强,大概率是角点;R接近零或负,说明是边缘或平坦区。
3.2 Harris和Shi-Tomasi怎么选
OpenCV里Harris要自己写一部分计算逻辑,而cv2.goodFeaturesToTrack()实现的Shi-Tomasi方法更容易上手。两者的区别在于评分标准:Harris用det(M) - k*trace(M)^2,Shi-Tomasi直接取协方差矩阵两个特征值中较小的那个min(λ1, λ2)。
我自己实测的感觉是,Shi-Tomasi对高质量角点的筛选更纯粹,不会出现Harris那种角点扎堆的现象。做相机标定、找棋盘格角点的时候,我基本只用Shi-Tomasi。但Harris有个好处是数学解释清楚,适合做理论分析,可以自己控制响应值的分布。
3.3 参数里容易被忽视的细节
cv2.goodFeaturesToTrack()有四个参数:最大角点数、质量水平qualityLevel、最小欧氏距离minDistance、以及是否使用Harris角点响应的标志。qualityLevel和minDistance这两个值直接影响效果,但很多人喜欢忽略。
qualityLevel取0.01到0.1之间,表示候选角点的质量阈值是“全局最大响应值的百分比”。设得太低会冒出大量伪角点,太高又会导致稀疏。minDistance控制角点之间的最小像素距离,设成图像短边的1/50到1/20,效果一般比较舒服。另外,角点检测对噪声极敏感,进函数之前务必高斯模糊一把,哪怕是5x5的小核。
import cv2 import numpy as np img = cv2.imread('checkerboard.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (3, 3), 0.8) corners = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=10) corners = np.int0(corners) for pt in corners: x, y = pt.ravel() cv2.circle(img, (x, y), 5, (0, 0, 255), -1) cv2.imshow('Corners', img) cv2.waitKey(0) cv2.destroyAllWindows()这里有个注意点:corners返回的每个元素是二维数组[[x, y]],不是一维点坐标,取坐标时要.ravel()或者在索引上多加一层。这个细节坑过不少人,报错时报的就是维度问题,很迷惑。
4. 特征点检测与匹配:SIFT和ORB怎么挑
4.1 特征点为什么比角点更高一层
角点检测只告诉你“这里有个角”,但对这个点的描述到此为止。特征点检测则更进一步:先定位关键点,再计算描述子。描述子是一个向量,相当于这个点的“生物特征”,哪怕图像发生了旋转、缩放、亮度变化,同一个点的描述子依然保持相似。有了这一层,就能做图像匹配、目标识别、全景拼接这些更上层的任务。
4.2 SIFT:精度派的选择
SIFT(尺度不变特征变换)是特征点检测领域的标杆。它最大的特点是尺度不变和旋转不变,能够检测到不同尺度下的特征点。我之前用一个杯子在不同距离、不同角度下拍照,SIFT能稳定地匹配到杯身上同一个文字区域的点,效果让人放心。
代价是慢。在手机上跑1080p图像,SIFT的特征提取加匹配能明显感觉到卡顿。还有一个历史原因:SIFT相关专利在2020年到期之前,非商业用途之外的使用是有争议的,所以很长一段时间大家用ORB替代它。现在专利过期了,能用SIFT的地方就放开用,只要硬件能扛住。
4.3 ORB:速度和轻量的代名词
ORB结合了FAST关键点检测和BRIEF描述子,而且做了降尺度处理,所以它的描述子不是浮点向量,而是二进制字符串。这意味着匹配时可以用汉明距离快速运算,比浮点距离计算快一个量级。在树莓派、嵌入式设备、手机端做实时匹配,ORB几乎是无脑选。
但它的精度确实不如SIFT。旋转变化大、尺度变化大的场景,ORB很容易找错匹配对。所以我的选型标准很简单:要精度、硬件允许→SIFT;要实时、轻量、场景规整→ORB。两者代码切换成本几乎为零,OpenCV里流程一样,换一个创建函数就行。
4.4 匹配器的选择与提纯
匹配阶段常用两种方式:BFMatcher和FLANN。BFMatcher是暴力匹配,遍历所有特征点对算距离,适合特征点数量不多的情况。FLANN是近似最近邻搜索,适合特征点多、追求速度的场景。
但无论哪种匹配,直接拿最好距离当结果都会混入大量错误匹配。我常配合knnMatch加一个距离比阈值来提纯,这一招被称作ratio test,经典做法是取最近的两个匹配,如果最近距离比次近距离小得多(比如小于0.75),才认为这个匹配可靠。这个比例我按场景在0.65到0.85之间调,严一点匹配对少但精确,松一点匹配对多但会有误配。
import cv2 img1 = cv2.imread('book_a.jpg', cv2.IMREAD_GRAYSCALE) img2 = cv2.imread('book_b.jpg', cv2.IMREAD_GRAYSCALE) sift = cv2.SIFT_create() kp1, des1 = sift.detectAndCompute(img1, None) kp2, des2 = sift.detectAndCompute(img2, None) bf = cv2.BFMatcher() matches = bf.knnMatch(des1, des2, k=2) good = [] for m, n in matches: if m.distance < 0.75 * n.distance: good.append(m) result = cv2.drawMatches(img1, kp1, img2, kp2, good, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) cv2.imshow('Matched', result) cv2.waitKey(0) cv2.destroyAllWindows()如果匹配结果还是乱,我会再叠加一个几何约束:用cv2.findHomography()求单应矩阵,配合RANSAC剔除不符合透视变换的离群点。这个过程是特征匹配里最重要的一次“清洗”,做完之后图像拼接的准确率会明显上一个台阶。
5. 霍夫变换直线检测:从像素到参数空间的思维跳跃
5.1 一条直线上无数个点,为什么能投票出来
霍夫变换的核心思想很反直觉:图像空间里的一条直线,可以表示为参数空间里的一个点。OpenCV里用极坐标表示直线,rho是直线到原点的距离,theta是法线与水平轴的夹角。图像上每一个边缘点会在参数空间里“投出”一条曲线,如果很多条曲线汇聚到同一个(rho, theta)处,就说明图像上有很多点共线,这条直线就被“投票”投出来了。
这个思路最大的好处是抗断线能力强。一条直线哪怕中间有断裂、有缺口,只要两侧的点还在,就能在参数空间形成一个明显的峰值。拿它来做车道线检测、板材边缘校准、刻度线识别,都比直接连通边缘点要稳定。
5.2 HoughLinesP参数不是拍脑袋定的
OpenCV里新版本推荐用HoughLinesP,它直接输出线段端点,省去自己连线的麻烦。它的核心参数是累加器阈值threshold、最短线段长度minLineLength、允许线段断裂合并的最大间隙maxLineGap。
threshold决定一个候选直线需要多少个点支持。设大了,直线投不出来;设小了,满屏都是碎线。minLineLength是按图像尺寸比例调的,比如要做长直线检测,我会取图像对角线的5%到10%。maxLineGap是最容易被误解的参数,它不代表两条线段之间的空隙多大才允许拼接,而是代表在一条线段内允许的最大空隙——理解反了就会把不该连的两条线连起来。
import cv2 import numpy as np img = cv2.imread('road.png') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 50, 150) lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi/180, threshold=80, minLineLength=60, maxLineGap=15) for line in lines: x1, y1, x2, y2 = line[0] cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)rho取1像素、theta取1度,对大多数场景都够用,追求极端精度的场合再把rho降到0.5,但计算量会涨不少。我实际项目中除非是毫米级测量,否则不会动这两个底层的分辨率参数。
5.3 霍夫变换的常见坑
霍夫变换最容易出现两个问题:重复线和误检线。重复线的根源是Canny边缘太粗,同一物理直线在边缘图里占了两三行像素,导致参数空间出现相邻的几个峰。解法是检测完线段之后做一次合并,或者干脆在检测前对Canny结果做一次膨胀,让边缘变细。误检线则是图像纹理杂乱导致的,比如百叶窗、布料纹理。解法是提高threshold,或者在检测后按斜率过滤,保留角度合理的线段。
我见过不少新手把霍夫变换当作“画线神器”,什么图都往里扔,结果出来的线全在纹路上。其实霍夫变换吃得是干净的边缘图,而且最好预先知道你要找的线大致朝哪个方向。开个滑动条,边调边看,比猜参数高效得多。
6. 轮廓检测:真正适合做几何分析的工具
6.1 边缘检测给像素,轮廓检测给对象
Canny输出的是“哪些像素属于边缘”,而findContours输出的是“哪些边缘像素连成一圈并构成了一个对象”。这中间的差距恰恰是工程里最需要的。有了完整轮廓,才能算面积、周长、外接矩形、凸包、多边形顶点数,才能判断这个物体是圆还是方、是大还是小。
我做过一个零件分拣的Demo,工作很简单:传送带上的零件有圆形和三角形两种,用轮廓检测直接按面积和外接矩形长宽比分类,帧率能做到飞起,比上深度学习模型省太多事。
6.2 轮廓提取模式必须搞清楚
cv2.findContours()的第二个参数叫mode,第三个叫method。mode决定轮廓的层级结构:RETR_EXTERNAL只提取最外层轮廓,RETR_LIST提取所有轮廓但不建立层级,RETR_TREE提取所有轮廓并建立完整的父子层级。
我的经验是:只关心物体外边界时用RETR_EXTERNAL,能省掉大量内部干扰;要识别有孔洞的物体,比如垫片、圆环,就用RETR_TREE配合Hierarchy信息判断谁是外圈谁是被包含的内圈。method用CHAIN_APPROX_SIMPLE,它只保存轮廓的转折点,能省内存,对后续计算也没影响。
还有一个版本差异必须提醒:OpenCV 4.x里findContours返回两个值(contours, hierarchy),但网上大量老教程写的是三个返回值。如果你照着老教程写,会得到“not enough values to unpack”的报错。这是新手期最常见的卡壳之一。
6.3 用形状特征过滤干扰
轮廓提取完,图像里往往有大量小而杂的轮廓,不能直接拿去分析。我一般先算contourArea()过滤面积,再算boundingRect()过滤宽高比例,把明显不合理的候选剔除掉。比如检测矩形零件,我会先过滤面积小于500像素的噪点,再要求宽高比在0.7到1.3之间,最后用轮廓面积与外接矩形面积的比值做进一步筛选——越接近1,说明轮廓越饱满,越近似矩形。
6.4 多边形逼近:从轮廓到几何形状
approxPolyDP是轮廓检测的“点睛之笔”。它用道格拉斯-普克算法在轮廓上找转折点,epsilon参数控制逼近精度。我常用的经验是epsilon = 0.02 * cv2.arcLength(contour, True)。arcLength算轮廓周长,乘以一个系数作为容差。系数越小,逼近结果越贴近原始轮廓,顶点数越多;系数越大,轮廓被“揉”得越圆滑,顶点数越少。想判断轮廓是不是四边形,只要看逼近后顶点数是否等于4就行。
import cv2 import numpy as np img = cv2.imread('shapes.png') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < 500: continue peri = cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, 0.02 * peri, True) cv2.drawContours(img, [approx], -1, (0, 255, 0), 2) # 顶点数为3是三角形,4是矩形/四边形,更多则是多边形状 if len(approx) == 3: shape = 'Triangle' elif len(approx) == 4: shape = 'Quadrilateral' else: shape = 'Circle' if cv2.isContourConvex(cnt) else 'Star' # 再把文字放到图像上(省略)轮廓检测的预处理有一点微妙:它一般依赖二值图,而不是灰度图。cv2.threshold()得到的黑白图是最直接的输入。光照不均时,cv2.adaptiveThreshold()比全局阈值稳定得多,但会把背景纹理也切进来,所以通常要配合形态学开闭运算把细碎块消掉。这一步是很多人轮廓检测忽好忽坏的根源。
7. Haar Cascade人脸检测:最经典的目标级检测
7.1 它到底是怎么工作的
Haar Cascade检测人脸,核心是三个词:Haar特征、AdaBoost、级联。Haar特征是一些边缘、线段的矩形模板,用来描述图像局部的明暗变化。比如眼睛区域通常比脸颊区域暗,这个特征就能作为一个分类依据。AdaBoost负责从大量Haar特征里挑出区分度最高的特征组合成强分类器。级联则是把一堆分类器串成管道,先粗筛后细筛,大部分非人脸区域在前几级就被快速扔掉,只有通过全部级的区域才被认为包含人脸。
这个设计在那个年代非常巧妙,它在不牺牲太多速度的前提下做了层层过滤。但放到今天看,它对大角度侧脸、严重遮挡、暗光环境的鲁棒性确实一般。不过对于正脸检测、人像相册归类、智能门锁这类受限场景,它依然是一个启动快、占用小的选择。
7.2 快速上手和参数门道
OpenCV自带训练好的Haar Cascade模型文件,按官方路径能找到haarcascade_frontalface_default.xml。搭配CascadeClassifier加载,然后一行detectMultiScale就能出框。
detectMultiScale参数里,scaleFactor控制图像缩放步长,一般取1.1到1.2。取1.01效果更好但在大图上慢到哭。minNeighbors控制一个候选框周围要有多少个邻近框确认才算有效,取3到6比较常见。取小了会出现大量假框,取大了会把真的脸也过滤掉。minSize和maxSize是物理世界的约束,比如人脸在画面里至少不会小于30x30像素,这能过滤掉很多误检。
import cv2 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 小尺度图检测快,先缩一下再检测 small = cv2.resize(gray, (0, 0), fx=0.5, fy=0.5) faces = face_cascade.detectMultiScale(small, 1.1, 5, minSize=(30, 30)) for (x, y, w, h) in faces: x *= 2; y *= 2; w *= 2; h *= 2 cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imshow('Face', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()7.3 什么时候别硬扛Haar Cascade
我在一个安防项目里试过用Haar Cascade检测侧脸,结果检测率惨不忍睹。后来换了基于方向梯度直方图加级联分类器的替代方案,虽然传统,但对侧脸的容忍度明显好一些。到了更复杂的环境,那就只能上深度学习检测器了。
我不是说Haar Cascade万能,而是说它作为OpenCV自带、零训练成本的人脸检测方案,在正脸、均匀光照、固定位姿场景下依然非常能打。很多场景根本不需要上几百兆的深度学习模型,先跑一下Haar Cascade,如果实测误检率能接受,方案就成立了。你要检测的目标如果跟人脸特征一样有鲜明的明暗规律,OpenCV也有训练工具支持;如果目标本身特征不明显,还是老老实实上深度学习。
8. 参数调试的几条经验法则
8.1 先把中间结果可视化
我调试检测算法时,第一个动作永远是“把每一步中间结果存成图片”。灰度图长什么样、滤波后长什么样、二值图长什么样、边缘图长什么样,全部存下来看一遍。很多人直接跳到最后结果看效果,效果不好就盲调参数,结果绕了半天是在补救一个预处理步骤埋下的错误。
有一次,某项目的边缘检测怎么调都不干净,我一看中间图才发现是光照让工件表面的细纹路反光过于严重,Canny把那些反光纹路全当成了边缘。后来在预处理里加了一步形态学闭运算,把细小反光覆盖掉,检测立刻干净了。如果当初不看中间图,我可能还在调整Canny的高低阈值,纯属浪费时间。
8.2 用滑动条做参数调优
OpenCV的createTrackbar是调试检测算法的神器。不管是Canny的阈值、霍夫变换的长度阈值,还是轮廓检测的面积阈值,我都建议先挂到滑动条上,边滑边看效果。这样能快速感知每个参数的敏感方向和量级。当你对参数区间有了手感,再固化到代码里、再去做自动多组参数试验,效率会高很多。
我见过有人用循环遍历几百组参数自动选最优,听起来很省力,但没有人工全局观察,很容易选到在测试集上效果拔群、换个环境就崩的“过拟合”参数。参数调试不是纯数学优化,得结合人的视觉判断。
8.3 建立你自己的一份参数清单
下面对照表是我给自己和身边人常用的一份调试入口清单,不说是标准答案,但至少在踩坑初期能快速定位问题方向。
| 现象 | 优先检查的参数 | 经验方向 |
|---|---|---|
| 边缘断裂严重 | Canny低阈值、高斯模糊核大小 | 降低低阈值,或减少模糊核 |
| 边缘过于密集杂乱 | Canny低阈值、预处理去噪 | 提高低阈值,或先形态学开运算 |
| 角点稀疏、关键位置缺失 | qualityLevel、minDistance | 降低qualityLevel,缩小minDistance |
| 角点扎堆 | minDistance | 增大到短边1/30左右 |
| 线段完整但滑出目标线 | threshold、斜率过滤 | 提高threshold,按角度范围过滤 |
| 线段大量重复 | Canny边缘细化、Hough合并 | 膨胀边缘后再检测,或检测后合并近邻线段 |
| 轮廓数量多到爆 | 面积阈值、RETR模式 | 提高最小面积,改用RETR_EXTERNAL |
| 人脸检测多框 | minNeighbors、minSize | 提高minNeighbors,设置minSize |
这张表单看每行都没什么高深技术,但它解决的都是我在多个项目里真实遇到过、又容易被各种教程忽略的问题。
8.4 经典算法和深度学习并不是对立面
最后说句实在话。写这类文章,总有人会问“这都什么年代了,干吗不用深度学习和目标检测框架?”我的回答始终是:看场景,看资源,看约束条件。深度学习确实在复杂任务上碾压传统算法,但在边缘设备上、在需要极度可控和可解释的工业场景里,OpenCV这些检测算法依然有不可替代的位置。
我做了这么多年视觉相关项目,最大的体会就是:算法和人一样,各有各的生态位。经典检测算法像一把螺丝刀,深度学习像一台数控机床。做家具的时候,螺丝刀未必比数控机床“落后”,关键看你是不是真的需要切出一个复杂的曲面。那些经典的检测算法,只要吃透原理、养好预处理的习惯、练出调参的手感,很多需求根本不用绕路去跑模型。
我自己还在用Canny和轮廓检测处理大量工业测量问题,速度、稳定性、可控性都让人安心。希望这篇文章能把这份手感传递给你——下次拿着一张图,别急着堆模型,先想想你要检测的到底是什么,再决定要不要让这些老伙计们出山。