2026/10/13 7:33:43

海天线检测实战:Otsu阈值分割与Hough变换的参数调优与踩坑记录

海天线检测实战:Otsu阈值分割与Hough变换的参数调优与踩坑记录 干图像算法这行的朋友早晚都会碰到一个看着特别简单、上手才发现全是细节的任务海天线检测。我第一次做这个是在一个岸基监控的改进项目里甲方要求把画面里的天空区域和海面区域自动分开好给后面目标检测省计算量。当时第一反应就是阈值分割加直线检测于是很自然地选了Otsu和Hough变换这套组合。但真正把算法跑起来之后我才发现这条“海天线”远没有肉眼看上去那么听话——云雾、浪花、反光、船体遮挡每一种情况都能让算法在边缘疯狂试探。这篇就把我在这类项目里积累的完整思路、参数设计、代码框架和踩坑记录整理出来给正打算做海天线检测的朋友当个参考。1. 海天线检测这件事难点从来都不只是找一条线1.1 哪些场景真的需要海天线海天线检测也叫海岸线检测、天海分界线检测在很多视觉系统里都是前置关键步骤。最常见的几种需求岸基光电监控把画面分成天空、海面两个区域目标检测只在海面区域跑可以大幅减少云层引起的误报也能缩小搜索范围。船载或艇载摄像稳定系统当船体晃动导致画面倾斜时海天线可以作为水平基准用来估计滚转角度。无人艇、无人机的海面目标识别先定位海天线再把图像分成不同波段分别处理比如天空区域做云层抑制海面区域做暗目标增强。红外或微光夜视场景这类图像纹理细节少海天线往往是少数稳定可用的结构特征会被用来做帧间配准或相机姿态估计。说白了海天线不是一个“发论文式”的孤立任务它经常要充当整个系统的地基。地基斜了上面所有模块都会跟着出问题。1.2 看着明显的图算法为什么会翻车人眼去看一张晴天海景图会觉得天空和海面分界极其清楚。但这种直觉放到像素层面往往站不住脚。第一天空和海面的灰度差异不是恒定的。晴朗正午可能相差很大但阴天、雾天、逆光时两者灰度可能非常接近直方图上可能只有一个峰。第二海面本身不是均匀的有波浪亮纹、白帽泡沫、镜面反光天空也不干净有云层、渐变、大气散射。第三船、岛礁、岸线建筑会把海天线截断导致完整直线根本不存在。第四传感器噪声和压缩编码会引入伪边缘让边缘检测结果变成一团乱麻。所以这个任务的本质不是“能不能找出一条线”而是“在干扰那么多的前提下找出一条空间上连续、时间上稳定、物理上可信的线”。理解了这点再去选Otsu和Hough心里就有数了。2. 用Otsu做海天分割双峰假设与它的边界2.1 类间方差最大化到底在做什么Otsu大津法做的事情简单说就是在灰度直方图上找一个阈值把像素分成前景和背景两类使得这两类之间的方差最大。数学上就是最大化类间方差σ_b² ω₀ω₁(μ₀ − μ₁)²其中ω₀、ω₁是两类像素占比μ₀、μ₁是两类的灰度均值。这个公式的本质是如果阈值切得好两类的中心应该离得足够远同时每类内部的像素别太散。我最初用Otsu时有个误解以为它必须精确地把“天空”和“海面”分开。其实Otsu并不关心谁是前景谁是背景它只负责“把直方图切成两坨尽量分离的分布”。在海天线场景里如果没有云层干扰天空整体偏亮、海面整体偏暗Otsu找到的阈值通常正好落在海天线附近。这个阈值分割后的二值图就变成了一幅“天空一块、海面一块”的粗划分图。2.2 分割结果如何喂给后续的直线检测很多教程会把Otsu放在预处理阶段然后直接去跑Canny加Hough。但我在项目里的做法是Otsu分割后先把二值图做形态学处理再从中提取边界最后才交给Hough。原因很简单。直接用Canny从原始灰度图上找边缘海面波浪的纹理也会产生大量边缘点而这些边缘点到了Hough空间里很可能聚成一条伪直线。但如果先通过Otsu把图像分成两大块再在二值图的边界上做Hough相当于用“区域一致性”过滤掉了大部分颗粒噪声只保留了“天空块”跟“海面块”之间的那条主要分界。实际操作时我会在二值图上使用开运算核选一个稍微偏横向的矩形比如3×5。这么做有两个考虑一是去掉海面区域离散的小白点二是因为海天线本身接近水平横向核不容易把它抹掉。如果选方形核甚至竖长核就可能在边界处留下锯齿给后续Hough投票引入干扰。2.3 Otsu的失败模式双峰假设没了怎么办Otsu最怕的就是直方图只有一个峰典型场景是海雾弥漫或者阴天。此时天空和海面灰度非常接近直方图呈现出一种近似单峰偏斜的形状Otsu就会强行在某个毫无物理意义的位置切开分割结果会乱七八糟。另一个容易出问题的场景是强烈反光海面某一片区域亮度跟云层一样高Otsu可能把“高亮海面”和“天空”划成一类导致分割边界跑到海面中间去。所以必须清醒认识Otsu只是这套算法链上的一环而不是万能的分割器。它负责提供一个“候选边界”后面的Hough参数约束、直线筛选和时序滤波才是兜底的手段。如果你测试的场景里海雾占比很高那要么增加一个基于纹理特征的分类器去判断当前画面是否适合Otsu要么直接用灰度梯度峰值定位代替Otsu这我后面会讲。3. Hough变换找直线投票机制与针对海天线的参数收编3.1 图像空间到参数空间的映射Hough变换的核心思想是把图像空间里的一条直线映射成参数空间里的一个点。对于直线y kx b直接用斜率和截距表示有个问题垂直直线是无穷大斜率没法处理。所以OpenCV里的标准Hough变换用的是极坐标形式ρ x·cosθ y·sinθ其中ρ是原点到直线的垂直距离θ是这条垂线与x轴的夹角。每条直线对应一个(ρ, θ)对。算法遍历边缘点对每个点在所有可能的方向θ上算出对应的ρ然后在累加器里投票。最后票数超过阈值的那组(ρ, θ)就代表“有好多边缘点都落在这条直线上”。这里有个值得说的直觉Hough不是“拟合”出一条线而是“选举”出一条线。每个边缘点都是选民每个可能的(ρ, θ)都是候选人票数最高且超过门槛的候选人胜出。所以它对部分遮挡、断线非常宽容——只要海天线有足够多肉眼可见的边缘点它就能被选出来。3.2 角度范围、投票阈值和分辨率的选择通用教程会让你在全角度范围0~180度内做Hough变换但在海天线这个具体任务里这样做纯属浪费算力还会引入干扰。海天线在画面里通常是接近水平的即使船体摇摆或相机安装有倾角其角度一般也不会偏离水平超过±15度。所以我把θ范围直接限制在一个窄带里比如80°~100°以标准OpenCV角度定义为参考或者更宽裕一点的70°~110°视安装环境和陀螺仪数据而定。这个约束非常关键。云层边缘常常是斜的岛礁轮廓更是各种角度都有如果把全角度都开放那些杂散边缘很可能聚集成票数不低的伪直线。角度收编之后大量干扰从源头就被排除了。投票阈值怎么选我的经验是不要直接拿OpenCV的阈值参数乱试先看一下提取出的边缘点总数。一般来说海天线如果贯穿画面宽度能贡献的边缘点数量级在几百到几千。阈值设成边缘点总数的1%到2%左右比较合理。比如边缘点有5000个阈值可以设在80到100。太小会选出碎片线太大又可能漏检。ρ和θ的分辨率也影响巨大。θ分辨率我通常设0.5度ρ分辨率设1像素。分辨率设太高比如θ0.1度不仅慢还容易把一条线上原本连续的点拆到邻近格子导致投票不集中分辨率设太低比如ρ2像素又会把相邻的平行线混在一起产生“平均化”的偏差。3.3 多条直线怎么合并成一条海天线单帧图像里Hough可能同时选出好几条高票直线。它们往往来自云层底部边界、海天线本体、船只甲板边缘、海面波浪带等。我的合并策略分两步第一步按角度和截距聚类。把所有候选直线按θ聚成小组同一组内θ差不超过2度ρ差不超过15像素。这样基本能归类出“同一条物理直线”的不同片段。第二步在每组内按“票数×长度”打分选一个代表。为什么不光看票数因为有些边缘点密集但很短比如船体的护栏票数虽高但覆盖范围不够作为海天线使用会带来很大的外推误差。长度可以从直线与图像边界的交点算出来或者用HoughLinesP统计线段的实际覆盖范围来近似。合并完成之后最终的(ρ, θ)还要结合一点物理先验海天线在画面中的位置应当在图像上部或中部不应该神秘地出现在图像底部。如果选出的直线中心点落在图像下1/3区域基本可以直接判定是误检。4. 一套能落地的流水线从输入图像到一条能用的海天线4.1 完整流程与每步的设计意图我最后落地到项目里的流程大致分七步输入图像转为灰度图必要时先降采样到宽度640~960像素提高处理速度。用高斯滤波或双边滤波轻微去噪。双边滤波在保留海天线边缘的同时抑制海面纹理效果比高斯更好但要注意参数不能太强否则边界会变得模糊。做Otsu阈值分割得到前景/背景二值图。然后用3×5矩形核对二值图做开运算去除海面白色噪点。从处理后的二值图中提取轮廓或直接用Canny提取边缘。如果直接用Canny要对二值图先做一次膨胀否则Canny会把二值边界的里外两侧都检出产生双线。限定ROI区域通常是图像中间宽度、上部到下部2/3高度范围排除近景船体、岸线这类干扰。在ROI上做HoughLines角度范围限制在水平方向±15度。对候选直线做角度-截距聚类选出置信度最高的那条作为海天线。每一步看上去都很简单但每一步都在消减一种特定的干扰。Otsu消减纹理噪声开运算消减小目标ROI消减前景干扰角度限定消减斜向云层。真正到实践里你会发现哪一个环节被省略最后选出的直线都会“飘”。4.2 核心代码实现Python OpenCV下面这段代码是这个流程最精简的可运行版本也是我最初在项目里验证方案时用的雏形。它不追求极致性能但逻辑完整性足够。import cv2 import numpy as np def detect_sea_sky_line(img): # 1. 灰度化与缩放 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) h, w gray.shape if w 960: scale 960.0 / w gray cv2.resize(gray, (960, int(h * scale))) h, w gray.shape # 2. 双边滤波保留边缘、抑制纹理 gray cv2.bilateralFilter(gray, d7, sigmaColor25, sigmaSpace25) # 3. Otsu分割 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 4. 开运算去噪核偏横向 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 3)) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 5. 提取二值图边缘 edges cv2.Canny(binary, 50, 150) edges cv2.dilate(edges, np.ones((3, 3), np.uint8)) # 让边缘更连续 # 6. 限定ROI排除画面下方近景干扰 roi np.zeros_like(edges) roi_top int(h * 0.05) roi_bottom int(h * 0.7) roi[roi_top:roi_bottom, :] edges[roi_top:roi_bottom, :] # 7. Hough变换限定接近水平的角度 lines cv2.HoughLines(roi, rho1, thetanp.deg2rad(0.5), threshold80) if lines is None: return None # 8. 筛选与合并候选直线 candidates [] for line in lines: rho, theta line[0] angle_deg np.rad2deg(theta) # 只保留接近水平方向的直线角度范围可调 if abs(angle_deg - 90.0) 15.0: continue candidates.append((rho, theta)) if not candidates: return None # 简单的按角度聚类取每类的平均 candidates.sort(keylambda x: x[1]) groups [] current_group [candidates[0]] for rho, theta in candidates[1:]: if abs(theta - current_group[-1][1]) np.deg2rad(2): current_group.append((rho, theta)) else: groups.append(current_group) current_group [(rho, theta)] groups.append(current_group) best_group max(groups, keylambda g: len(g)) rho np.mean([item[0] for item in best_group]) theta np.mean([item[1] for item in best_group]) # 9. 计算两个端点用于绘制 a np.cos(theta) b np.sin(theta) x0 a * rho y0 b * rho pt1 (int(x0 - w * (-b)), int(y0 - w * a)) pt2 (int(x0 w * (-b)), int(y0 w * a)) return pt1, pt2, (rho, theta) img cv2.imread(sea_sky.jpg) result detect_sea_sky_line(img) if result: pt1, pt2, _ result cv2.line(img, pt1, pt2, (0, 0, 255), 2) cv2.imwrite(result.jpg, img)这里面有个细节很容易忽略Canny之前我把二值图膨胀了一次。因为二值图边缘本来就锐利Canny会同时检测到前景边界和背景边界形成平行双线。膨胀后边缘会轻微拓宽Canny输出的边缘位置相对稳定Hough投票更集中。4.3 单帧结果到多帧时序滤波在视频流里单帧检测结果往往有明显的抖动。海天线在物理世界位置变化缓慢如果画面没有大幅度俯仰或滚转它的ρ和θ应该是平滑缓变的。所以我用滑动窗口或简单的一阶低通滤波来处理θ_smooth α·θ_current (1−α)·θ_prevρ_smooth α·ρ_current (1−α)·ρ_prevα通常在0.2到0.4之间。视频帧率25fps时α0.3大约对应3到5帧的有效响应时间既能抑制抖动又不会让结果跟不上画面变化。实测中这个平滑在风浪天特别有效——如果完全不做平滑输出的海天线会在海面波浪边缘之间来回跳。润色一点平滑应该在坐标域做而不是角度域直接平均。因为角度在接近0度和360度时存在绕回问题虽然海天线被限制在水平附近不太会碰到这种极端情况但养成这个习惯没坏处。5. 实测翻车与排查三个我真实遇到的失效场景5.1 场景一海天灰度接近Otsu分割错乱某次我在阴天环境做采集测试画面上方是灰白云层下方是灰暗海水直方图几乎没有双峰。Otsu切出的二值图里天空和海面被胡乱分成碎片导致后续Hough找出来的直线位置忽上忽下完全不能用。排查的时候我是这样链式分析的先输出Otsu的二值图和灰度直方图发现阈值被定在了一个“中间灰”的位置再输出边缘图发现边缘点大量聚集在中部偏上的云层底部最后Hough选取的直线就是云底边缘。所以问题的根不在Hough而在Otsu。解决方案分两条路走。一条路是加一个“雾天检测”前置判断计算直方图的峰谷比或者类间方差实际值如果类间方差过小说明双峰不成立那么放弃Otsu直接改用梯度峰值的方案。另一条路是保留Otsu但把ROI进一步缩小到图像中部区域利用“海天线在画面中间区域”的先验来降低云层影响。两种方案我最后都实现了以第一个为主因为它是从根上解决问题。5.2 场景二云层边缘被当成海天线晴天最容易翻的车是天空中有横向拉得很长的云层底部它和海天线一样接近水平而且对比度高。Hough投票时云层底部边缘点数量甚至可能超过真正的海天线毕竟云底往往更锐利。我的排查过程把Hough所有候选直线的端点可视化画出来发现被选中的那条线在图像上方位移过大和人工标注的海天线位置差了四五十个像素。进一步检查二值图发现Otsu其实分割得没问题海天线区域也有完整的边缘只是云层的投票更集中。解决办法靠两个配合。第一添加一条或多条“区域惩罚”海天线的置信度不只取决于边缘投票数还要考虑直线下方像素的灰度方差。真正的海天线下方是海面纹理灰度方差大云层边缘下方常是天空渐变区灰度方差小。这个特征在晴天场景下区分度很高。第二限制候选直线的ρ范围利用上一帧的结果只在附近一个带状区域比如±30像素搜索这样即使云层投票多也不会被选中。这就是时空连续性先验的工程化落地。5.3 场景三分辨率一变参数全部失效有次我把算法从1080P的采集画面迁移到另一个项目的512×384红外小图上结果海天线检测率直线下降。查了半天发现是Hough阈值和形态学核没跟着缩放1080P下阈值80在512宽图像上变得过严3×5的核在新分辨率下也显得太大把本来就不宽的海天线边缘抹掉了一部分。这个问题特别容易忽略因为代码里没有任何报错输出结果就是“找不到线”。现在我的做法是所有和尺寸相关的参数都按图像宽度动态计算。Hough阈值设为边缘点总数的1%~2%形态学核设为宽度的0.5%~1%高斯滤波核保持固定但尺寸按宽度缩放。参数相对化以后从1080P切到任何分辨率都不用再手工调一轮。5.4 排查方法论先把中间结果可视化再谈调参几次翻车经历让我养成一个习惯任何一行参数改动之前先把以下四个中间结果可视化保存下来Otsu二值图看分割是否符合“上天下海”的直觉。形态学后的二值图看去噪是否过度或不足。边缘图看海天线在哪个位置、有没有双线、有没有断裂。Hough候选直线叠加图看每条候选线的来源。这四张图一摆出来算法在哪一步开始出错就一目了然。我见过太多朋友抱着Hough的threshold从50调到200调了半天不知道问题其实是Otsu那边的核太大把边界抹平了。可视化中间结果花不了多少时间但能省下几晚上的空转。6. 效果评估、参数鲁棒性与可选的后手6.1 精度和稳定性怎么量化海天线检测的“准不准”不能靠肉眼感觉必须量化。我在项目里用的是三个指标角度误差预测海天线与人工标注之间的夹角单位度。晴天场景我要求平均角度误差小于1度阴天小于2度。截距误差在图像垂直方向上预测线与标注线的平均像素距离。这个指标比角度要更敏感一个小的角度偏差在图像边缘会被放大成几十像素的位置偏差。连续帧稳定度相邻帧之间预测线的ρ和θ变化量标准差。抖动过大的系统说明平滑不够。做法是取十几张覆盖不同光照、不同海况的图手工标出海天线两个端点然后写脚本自动比对。这个标注工作很机械但值得做没有量化就没有后续优化的方向。6.2 鲁棒性的进一步加固思路如果发现当前方案在特定场景下还是不稳我建议按下面两条路线打补丁对于中远海、低纹理场景可以在Hough之后加一个最小二乘拟合验证。Hough给出一个粗直线估计后用该直线附近的边缘点做亚像素精度的直线拟合能有效缓解Hough离散化带来的截距误差。对于极度复杂的近岸场景单一Otsu加Hough确实可能到极限了。可以引入基于纹理方向场的分类器或者干脆用一个轻量分割网络来输出海天线概率图再从概率图上找分界线。但轻量网络需要考虑标注成本、误检责任归属和算力预算我一般不轻易上除非Otsu加Hough已经被证明确实压不住。6.3 我的一些工程体会做了几个类似项目之后我最大的感受是这类视觉任务的成败八成不取决于算法多高级而取决于你对输入数据的脾气摸得有多清。Otsu加Hough是一对经典的组合拳各司其职又互相弥补——Otsu负责“分割出区域”Hough负责“从边界里选中那条编辑想要的线”。但每一拳都有命中范围你必须在旁边设计好约束条件和候选筛选机制让算法在“看不懂”的时候做保守判断而不是瞎猜。另外代码里一定要把每个中间状态都留成可调试的输出接口。我后来把核心函数改造成了类似“若debugTrue则保存中间图片”的结构无论是接手新项目还是排查旧问题都省很多事。如果你正准备在监控、无人船或者影像增强项目里做海天线检测希望这套流程和踩坑记录能帮你少走几步弯路。