MATLAB Hough变换例程详解:直线检测、圆检测与文档倾斜校正

发布时间:2026/9/13 19:13:56
MATLAB Hough变换例程详解:直线检测、圆检测与文档倾斜校正 简介这套MATLAB例程聚焦Hough变换在图像处理中的典型应用尤其针对圆形检测与圆心定位适合正在学习图像处理、计算机视觉或MATLAB工具箱的初学者与开发者。压缩包共10个文件涵盖4个m源代码脚本、4张tif测试图像以及2个fig图形文件资源总大小3.07MB结构清晰便于直接运行与对照学习。已有145人学习下载。代码以main为主入口演示了图像预处理、Hough变换、峰值提取到圆心坐标恢复的完整流程测试图像包含单个圆、多个圆等不同场景可供观察算法在不同输入下的表现。通过该例程学习者既能掌握hough、houghpeaks等核心函数的用法也能理解亚像素精度圆心坐标的实现思路是一份兼顾原理与实战的入门级参考。1. hough.rar 这种 MATLAB 例程包到底在帮你解决什么问题网上流传下来的 hough.rar_matlab例程 这类压缩包解压后通常是一个脚本加几张测试图脚本里把hough、houghpeaks、houghlines三件套串起来输出一张画着红线的结果图。Hough 变换在 MATLAB 图像处理任务里的地位是「找直线和圆的参数」的默认答案倾斜文档校正要看文字行角度车道线检测要拟合边缘直线棋盘格标定要定位网格线表盘读数要先找到圆心。这些东西的共同点是从大量噪声像素里提取出少数几个几何参数而 Hough 变换用投票的方式把这个问题变成找累加器峰值。这篇博文就从三件套的参数关系讲起给一份可直接运行的 MATLAB 直线检测例程再扩展到圆检测的参数调优最后用一个 deskew 函数收尾。适合有 MATLAB 基础、想在现成例程上二次开发的工程师也适合刚接触 MATLAB 图像处理但被阈值和坐标系绕晕的新手。2. Hough 变换的 MATLAB 三件套hough、houghpeaks、houghlines 怎么配合2.1 参数空间投票是怎么回事Hough 变换的核心思想是点线对偶。图像空间里的一个像素点(x0, y0)对应参数空间rho x0*cos(theta) y0*sin(theta)里的一条正弦曲线。图像上一条直线上的所有像素点它们对应的正弦曲线都会交于同一个(rho, theta)点这个交点累积的投票数就是这条直线在图像里的可信度。所以整个检测流程被拆成两个阶段第一阶段把所有边缘像素投进累加器第二阶段在累加器里找局部峰值。峰值对应的rho是原点到直线的垂线距离theta是这条垂线与 x 轴的夹角。MATLAB 的hough函数返回的H就是累加器矩阵theta和rho是两把坐标尺子。需要注意MATLAB 的hough输入必须是二值边缘图而不是灰度图或彩色图。如果直接把imread的结果塞进去累加器会被大量非目标像素淹没峰值完全不突出。这也是很多例程包在代码开头必定先写一行edge的原因。2.2 先看一个最小例子竖直边缘线的累加器长什么样img zeros(200, 200); img(50:150, 50) 1; % 一条竖直边缘线x50, y从50到150 [H, theta, rho] hough(img); imshow(H, [], XData, theta, YData, rho, InitialMagnification, fit); xlabel(theta (deg)); ylabel(rho (pixels));这段代码里img只在(501:150, 50)位置有非零像素。hough遍历所有非零像素把它代入参数空间画一条正弦曲线最后累加成H。imshow里XData和YData的作用是把累加器的下标映射成真实的theta和rho坐标图像上会看到一个明显的亮斑位置大约在theta0、rho50处。这里的H是二维矩阵行数等于rho网格数量列数等于theta网格数量。hough默认theta范围是-90:89度每度一列rho范围由图像对角线长度决定通常是diagLength的负一半到正一半。竖直边缘线峰值落在theta0水平边缘线则会落在接近±90的边界上这个细节在后面做倾斜校正时会直接用到。2.3 三件套的参数关系一共 8 个参数需要提前决定实际例程里不会直接拿hough的输出画图而是继续交给houghpeaks找候选峰值再用houghlines把峰值还原成线段端点。三个函数的关键参数如下表。函数参数默认值作用houghThetaResolution1度theta 网格步长越小累加器越宽houghRhoResolution1像素rho 网格步长越小对直线位置越敏感houghpeaksNumPeaks1最多找多少个局部峰值houghpeaksThreshold0.5 * max(H(:))峰值最小票数低于此值的峰值被忽略houghpeaksNHoodSizesize(H)/25 取奇峰值周围抑制邻域大小防止同一个直线被重复检出houghlinesFillGap20像素同一直线上断点之间的最大空隙超过则断开houghlinesMinLength40像素合并后线段的最小长度太短的丢弃这几个参数是例程里最常改的也是调参时最容易互相牵连的。Threshold设高了会漏掉弱直线设低了会把噪声点也当成直线FillGap设大了会把本不相干的两条线段强行接成一条设小了车道线这类有断痕的直线会被拆成很多段。我一般会先把NumPeaks设得大一些比如 50 或 100然后靠Threshold和NHoodSize来控制质量而不是一上来就限制数量。3. 用 MATLAB 例程跑通 Hough 直线检测完整可复现代码与参数对照3.1 为什么例程的输入总是 edge 而不是原图Hough 投票是对每个非零像素做正弦曲线累加。如果输入是灰度图几乎所有像素都会参与投票导致累加器里每个位置都有票数峰值不明显。更糟的是MATLABhough内部会对非零像素遍历灰度图会让计算量成倍增加。常见做法是先用 Canny 边缘检测得到二值图再做 Hough 变换。Canny 的两个阈值控制边缘的敏感度[0.1 0.25]这类低阈值能找到更多边缘适合对比度低的图[0.2 0.4]这类高阈值适合干净、线条分明的图。例程里这行通常是BW edge(I, canny, [0.15 0.3], 2);最后一个参数 2 是高斯滤波器的标准差影响边缘平滑程度。3.2 一个可以直接运行的直线检测例程%% 直线检测例程以棋盘格测试图为例 I checkerboard(20, 5, 5) 0.5; % 生成网格图 I imnoise(I, gaussian, 0, 0.02); % 加一点噪声模拟真实场景 if size(I, 3) 3 I rgb2gray(I); end BW edge(I, canny, [0.15 0.3], 2); % 1. 边缘检测 [H, theta, rho] hough(BW, ThetaResolution, 1, RhoResolution, 1); P houghpeaks(H, 30, Threshold, 0.3 * max(H(:)), NHoodSize, [51 11]); lines houghlines(BW, theta, rho, P, FillGap, 25, MinLength, 30); imshow(I); hold on; for k 1:numel(lines) xy [lines(k).point1; lines(k).point2]; plot(xy(:, 1), xy(:, 2), LineWidth, 2, Color, [1 0 0]); end hold off;第一步的edge把灰度图变成二值图[0.15 0.3]是 Canny 的双阈值低于 0.15 的像素不会成为边缘高于 0.3 的像素确定为边缘中间的像素只有在与强边缘相连时才会保留。第二步的hough生成累加器ThetaResolution和RhoResolution都取默认的 1这对大多数图像已经够用。第三步houghpeaks找 30 个候选峰值Threshold取0.3 * max(H(:))表示只有票数超过全局峰值 30% 的位置才被当作候选直线方向。最后houghlines负责把参数还原成图像坐标里的线段并输出结构体数组。lines结构体包含point1、point2、theta、rho四个字段其中point1和point2是线段端点的[x, y]坐标直接拿plot画即可。这里用checkerboard生成棋盘格是为了让读者不需要额外准备图片换成本地图片时只需要替换第一行imread读取的内容。3.3 从 lines 结果反推斜率和角度theta 的正负号约定angles [lines.theta]; % 提取所有线段的法线角度 lineSlope 90 - angles; % 转换成与水平方向的夹角 lineSlope(lineSlope 0) lineSlope(lineSlope 0) 180;lines.theta存储的是直线法线与 x 轴的夹角不是直线本身的倾角。把theta换算成直线与水平方向夹角需要做一次90 - theta结果落在[-90, 90]之间负值加 180 统一到[0, 180]范围。3.3.1 为什么 theta 正负号经常把人绕晕图像中的情况theta 值说明竖直线接近 0法线沿 x 轴方向水平线接近 ±90法线沿 y 轴方向从左下到右上的斜线负值区法线方向偏左上从左上到右下的斜线正值区法线方向偏右上这张表在做倾斜校正时特别有用。例程跑出来的 theta 如果集中在某个值附近说明图像里存在大量平行线如果分布很散说明是噪声或曲线边缘直接取均值没有意义。调试时可以先跑一段代码把histogram([lines.theta], -90:5:90)画出来比盯着数字判断快得多。4. Hough 例程的扩展MATLAB 圆检测 imfindcircles 与两个关键参数4.1 为什么找圆不能用三件套的 hough 函数标准 Hough 圆变换要把圆心(a, b)和半径r三个参数放进累加器存储量和计算量都比直线高一个量级。MATLAB 的hough函数只支持直线圆检测走的是另一条函数imfindcircles它内部用的是 Hough 梯度法变体先估计边缘方向再投票给圆心候选。imfindcircles返回两个数组centers是圆心坐标radii是半径。它还支持第三个输出metric表示每个圆的置信度数值越接近 1 越可靠。这个 metric 在过滤误检时很有用官方文档给出了三个输出的完整用法。4.2 一个圆检测例程及调参方法I imread(coins.png); [centers, radii, metric] imfindcircles(I, [15 40], ... ObjectPolarity, bright, ... Sensitivity, 0.92, ... EdgeThreshold, 0.2); imshow(I); hold on; idx metric 0.5; % 只保留置信度大于0.5的圆 viscircles(centers(idx, :), radii(idx), EdgeColor, r); hold off;[15 40]是半径范围单位是像素需要根据图中目标大小预估。ObjectPolarity指定目标是亮背景上的暗物体还是暗背景上的亮物体coins.png里硬币比背景亮所以用bright。Sensitivity默认 0.85取值范围 0 到 1越大越容易找到置信度低的圆目标但误检也会增多。EdgeThreshold默认 0.1控制边缘梯度阈值值越高要求边缘越强。调参顺序我一般固定成先把半径范围收紧再用默认 Sensitivity 跑一次观察漏检还是误检为主。漏检就每步加 0.03 提高 Sensitivity误检就提高 EdgeThreshold 并配合 metric 过滤。注意不要同时调两个参数否则无法判断是谁起了作用。置信度过滤那段代码只保留metric 0.5的结果实际项目中阈值可以提高到 0.7 甚至 0.8。4.3 圆检测例程里常见的失败模式现象原因参数回退策略硬币少检了几个半径范围太宽或 Sensitivity 太低把半径范围上限下调Sensitivity 每次加 0.03把椭圆也当圆检出来EdgeThreshold 太低梯度弱区域参与投票EdgeThreshold 升到 0.3 附近同心圆被拆成多个候选同一目标的多个边缘环都投票用 metric 排序只保留每个圆心附近 metric 最高的背景纹理被当成圆目标与背景对比度不足先做直方图均衡再重跑例程圆检测比直线检测更依赖图像质量因为圆心投票对边缘方向的精度很敏感。如果例程在噪声图上直接跑建议在前面加一步imgaussfilt(I, 1)做平滑而不是只加高 Sensitivity。平滑会把噪声边缘的梯度方向打散让真正的圆弧边缘在投票中更占优势。5. 例程收尾技巧用 houghlines 输出写一个文档倾斜校正函数5.1 用角度众数求主方向把 houghlines 结果里的 theta 统计成直方图峰值对应的角度就是图像里占主导的平行线方向。对文档扫描件来说这个方向基本等于文字行的方向。注意不要直接取均值因为少数竖直线和斜线会把均值拉偏用众数或直方图峰值更稳。function J deskew_by_hough(I) if size(I, 3) 3 I rgb2gray(I); end BW edge(I, canny, [0.1 0.25], 1.5); [H, theta, rho] hough(BW, Theta, -89:0.5:89); P houghpeaks(H, 20, Threshold, 0.2 * max(H(:))); lines houghlines(BW, theta, rho, P, FillGap, 20, MinLength, 50); angs [lines.theta]; edges -89:2:89; cnts histcounts(angs, edges); % 统计角度直方图 [~, idx] max(cnts); % 找到数量最多的区间 thetaMode edges(idx) 1; % 取区间中心 rot 90 - thetaMode; % 法线角转旋转角 if abs(rot) 45 rot rot - sign(rot) * 90; % 把角度限制在 ±45 内 end J imrotate(I, rot, bilinear, crop); end这段代码里histcounts比直接mode更可靠因为mode在多峰时返回的是第一个最小值容易选到噪声方向。rot的正负号需要说明imrotate正数表示逆时针旋转如果扫描件整体顺时针歪了 3 度这时的thetaMode大约是 93 度90 - 93 -3imrotate(I, -3)会顺时针转回 3 度方向刚好对上。可以用checkerboard(20, 5, 5)先转 5 度再跑这个函数验证。5.2 验证校正结果的两个方法第一个方法是把校正前后的图片各自跑一遍上面的函数对比输出角度。校正前的峰值角度接近 5校正后应接近 0。第二个方法更直观对校正后的图做一次sum投影文字行方向的投影方差最大旋转回去后这个方差应该明显变大。proj sum(double(BW_corrected), 2); variance_before var(sum(double(BW_original), 2)); variance_after var(proj);这个方法只适合有大量平行文字行的文档和表格类图像对照片或曲线为主的图不适用。有一点需要注意imrotate的crop会把旋转产生的黑色三角区裁掉如果原始图边缘有重要信息改成loose再配合后处理裁剪。5.3 分辨率与性能的取舍有人为了把角度算得更准会把ThetaResolution改成 0.1这会让累加器列数膨胀 10 倍三个函数的计算时间明显增加但峰值定位的收益有限。更稳妥的做法是用默认的 1 度网格跑出粗角度然后取峰值附近的角度区间再局部精搜或者在第一次拿到theta后用插值拟合峰值位置。另外如果例程脚本里通过 MEX 或 Python 调用 OpenCV 辅助计算启动时遇到dll 初始化例程失败这类 WinError多半是 MATLAB 位数与外部库的位数不一致或者编译器路径没配对先重新执行mex -setup指定同一套工具链再回头查算法逻辑环境问题比算法问题更容易被忽略。本文还有配套的精品资源点击获取