红外瞳孔检测精度提升:Hough-Contour两段式算法实战 简介这份资源面向图像处理与计算机视觉领域的研究人员和工程师针对红外视频中瞳孔直径检测精度偏低的痛点提出结合Hough变换与轮廓匹配的Hough-Contour算法。算法先对每帧图像灰度化并滤波去噪再用改进的Hough梯度法初步定位瞳孔随后在瞳孔附近进行圆形轮廓匹配精确计算瞳孔中心坐标与直径通过将候选圆心按累积值降序排序改为直接搜索最大值有效降低耗时并借助累积值阈值自动排除闭眼帧。资源包为1个pdf文件大小约1.04MB即论文全文含算法原理、参数设置与实验对比。实验表明轮廓移动与拉伸范围取初始半径的十分之一、点对数取40时检测精度由OpenCV圆变换的约10%提升至99.8%低端设备处理320×240视频、高端设备处理640×480视频均可实时单帧约60ms。目前已有170人学习适合心理健康研究、犯罪侦查、医疗诊断等场景参考。1. 红外瞳孔检测的精度困局为什么 OpenCV 自带的圆变换不够用做过红外眼动视频分析的人多半遇到过这种场景用 OpenCV 的HoughCircles跑一遍瞳孔定位肉眼看画面里瞳孔清清楚楚可输出的直径数值却像抽风一样来回跳同一段视频相邻两帧能差出十几个像素。某实验室做疲劳监测的 A 同学就吐槽过他拿标准圆变换跑 320×240 的红外眼图瞳孔直径检测的准确率只有 10% 上下基本没法用于后续的直径变化分析。问题出在标准 Hough 圆变换的机制上。它把图像空间里每个边缘点映射到参数空间的一票最后在累加器里找峰值。瞳孔边缘在红外图里往往不是理想圆——眼睑遮挡、睫毛阴影、角膜反光都会让边缘断裂或偏移累加器峰值就飘。这份来自期刊的算法资料给出的思路是先用改进的 Hough 梯度法拿到一个粗略的初始圆再在这个圆附近做轮廓匹配用一圈可变位置和半径的采样点去贴合真实瞳孔边缘把精度从 10% 拉到 99.8%。它适合做红外视频瞳孔直径测量、心理健康研究、疲劳评估这类对精度敏感的场景也适合想搞懂 Hough 变换工程化改造的开发者。2. Hough-Contour 的两段式设计从粗定位到精匹配2.1 为什么拆成两段而不是一步到位标准 Hough 圆变换是三维参数搜索圆心 a、b 加半径 r在 320×240 的图上直接做全参数空间投票计算量随半径范围线性增长而且对边缘噪声极其敏感。这份算法的核心判断是瞳孔在红外图里位置相对固定、半径变化范围有限没必要全局盲搜。于是它拆成两段。第一段用改进的 Hough 梯度法对应 OpenCV 里的icvHoughGradient思路做粗定位只求找到一个大致靠谱的初始圆c0(x0, y0, r0)不追求精度。第二段以c0为中心在一个受限范围内让一个圆形轮廓去匹配真实瞳孔边缘通过最大化轮廓上采样点对的灰度差来逼近最优圆心和半径。粗定位负责缩小搜索空间精匹配负责把精度做上去两段各司其职。这个拆法的好处是计算量可控。粗定位阶段把候选圆心的累加值排序改成直接找最大值省掉了排序开销精匹配阶段只在r0的十分之一范围内移动和缩放搜索空间被压得很小。资料里给的实测数据是低端机1.86 GHz 单核处理一帧 320×240 约 60 ms高端机上 640×480 能跑到实时。2.2 粗定位改进 Hough 梯度法的三个关键动作粗定位的输入是灰度化加滤波后的图像。流程是 Canny 边缘提取然后对每个边缘点沿梯度方向投票。改进点集中在三处。第一候选圆心累加值不排序直接遍历找最大值。标准实现会把累加器数组排序后取前几名再逐一验证半径排序在候选点多的时候是 O(n log n)。改成找最大值是 O(n)在嵌入式或低端 CPU 上省下的时间很可观。第二用累加值阈值排除闭眼帧。闭眼时瞳孔被眼睑盖住边缘特征消失累加器最大值会明显偏低。算法在累加值数组里搜一个最大值阈值低于阈值的帧直接判定为闭眼并跳过不需要额外的分类器。这个设计在连续视频里很实用避免了闭眼帧污染直径序列。第三半径计算延后。标准 Hough 梯度法在投票时同步累加半径直方图改进版先只定圆心半径在精匹配阶段一并优化。这样粗定位阶段的计算更轻。import cv2 import numpy as np def coarse_pupil_detect(gray_frame, canny_low100, canny_high50): 粗定位改进 Hough 梯度法思路的近似实现 gray_frame: 单通道灰度图 canny_low/high: Canny 双阈值资料里用的是 100/50 返回: (x0, y0, r0) 初始圆参数或 None 表示疑似闭眼 # 滤波去噪红外图常见高斯噪声 blurred cv2.GaussianBlur(gray_frame, (5, 5), 0) # Canny 边缘提取 edges cv2.Canny(blurred, canny_high, canny_low) # 用 OpenCV 的 HoughCircles 做粗定位的工程近似 # param2 是累加器阈值调低召回高但误检多调高反之 circles cv2.HoughCircles( edges, cv2.HOUGH_GRADIENT, dp1, minDist50, param1canny_low, param230, minRadius10, maxRadius60 ) if circles is None: return None # 无有效圆按闭眼帧处理 x0, y0, r0 circles[0][0] return int(x0), int(y0), int(r0)这段代码用HoughCircles近似粗定位。param2对应累加器阈值是控制闭眼帧排除的关键参数——调太低闭眼帧也会被当成圆调太高正常瞳孔可能漏检。资料里没有直接给param2的数值工程上一般从 30 起步根据实际红外图的对比度上下调。minRadius和maxRadius要按你的成像距离和分辨率设设太宽会拖慢速度。2.3 精匹配轮廓采样点对的灰度差最大化精匹配是这份算法的精度来源。思路是在初始圆c0周围让一个圆形轮廓在位置和半径上做小范围变动对每个候选轮廓沿圆周均匀取 k 个角度每个角度上取一对点一个在半径r-1处一个在r1处。瞳孔边缘是暗瞳和亮虹膜的分界边缘两侧灰度差最大所以让这对点的灰度差之和最大的那个轮廓就是最贴合真实瞳孔边缘的。数学上对每个角度 θ内点p1 (x (r-1)cosθ, y (r-1)sinθ)外点p1 (x (r1)cosθ, y (r1)sinθ)累加所有角度的f(p1) - f(p1)取平均得到diffenceAV找使diffenceAV最大的(x, y, r)。搜索范围由两个参数控制d r0/10即圆心在x0±d、y0±d内移动半径在r0±d内变化k是采样点对数资料里取 40。这两个参数直接决定精度和速度的平衡。import numpy as np def refine_pupil(gray_frame, x0, y0, r0, k40, d_ratio0.1): 精匹配轮廓采样点对灰度差最大化 gray_frame: 滤波后的灰度图注意不是边缘图 x0, y0, r0: 粗定位结果 k: 圆周采样点对数资料推荐 40 d_ratio: 搜索范围占初始半径比例资料推荐 0.1 返回: (x_opt, y_opt, r_opt) h, w gray_frame.shape d max(1, int(r0 * d_ratio)) theta np.linspace(0, 2 * np.pi, k, endpointFalse) cos_t, sin_t np.cos(theta), np.sin(theta) best_diff -1e9 best_params (x0, y0, r0) for r in range(r0 - d, r0 d 1): if r 2: continue for x in range(x0 - d, x0 d 1): for y in range(y0 - d, y0 d 1): # 内点坐标 xi np.clip((x (r - 1) * cos_t).astype(int), 0, w - 1) yi np.clip((y (r - 1) * sin_t).astype(int), 0, h - 1) # 外点坐标 xo np.clip((x (r 1) * cos_t).astype(int), 0, w - 1) yo np.clip((y (r 1) * sin_t).astype(int), 0, h - 1) inner gray_frame[yi, xi].astype(float) outer gray_frame[yo, xo].astype(float) diff np.mean(outer - inner) if diff best_diff: best_diff diff best_params (x, y, r) return best_paramsk40是资料里验证过的值低于 40 精度下降明显高于 40 提升有限但耗时线性增长。d_ratio0.1对应资料里d r0/10的设定搜索范围太小可能错过真实边缘太大则退化成全局搜索、失去粗定位的意义。注意精匹配用的是滤波后的灰度图不是 Canny 边缘图——灰度差比边缘二值图保留了更多亚像素信息。3. 参数怎么调k、d、Canny 阈值的实测边界3.1 采样点对数 k 的精度拐点资料里给了一组对照数据值得直接抄进调参笔记。当d r0/10固定时k从 10 增到 40瞳孔直径检测的准确率从 50% 爬到 99.8%。具体是k10 时约 50%k20 时约 90%k30 时约 99%k40 时 99.8%。再往上加准确率提升进入平台期但每帧耗时继续涨。所以 k40 是精度和速度的甜点。如果你的场景对实时性要求极高、可以接受 99% 而不是 99.8%k30 能省下约四分之一的时间。反过来如果做离线分析、不在乎单帧耗时k 可以加到 60 试试但收益很小。3.2 搜索范围 d 与半径的关系d r0/10是资料推荐的默认值。它的物理含义是粗定位的圆心误差和半径误差不超过初始半径的十分之一。这个假设在瞳孔清晰、粗定位靠谱时成立。但如果你的红外图质量差、粗定位经常偏出瞳孔d要适当放大比如r0/5代价是搜索空间变成原来的 8 倍三维各翻倍耗时明显上升。另一个坑是d不能小于 1 像素。当r0很小比如 10 像素时r0/10 1搜索范围只有 ±1 像素可能不够。工程上一般对d取max(2, int(r0 * d_ratio))保证最小搜索余量。3.3 Canny 双阈值与滤波核的配合资料里 Canny 用的是 100/50 的高低阈值滤波是 5×5 高斯。这两个参数要一起看。红外图对比度通常比可见光低如果 Canny 阈值设太高瞳孔边缘会断成碎片粗定位的累加器峰值就弱闭眼帧阈值也跟着失效。设太低则睫毛、反光点都变成边缘投票被污染。实操建议是先把滤波核调好。5×5 高斯对 320×240 的红外图够用如果噪声特别重可以上 7×7但核越大边缘越糊Canny 阈值要相应降低。判断标准很简单把 Canny 结果可视化出来瞳孔应该是一个连续或接近连续的圆环如果断成好几段就是阈值偏高或滤波过度。参数资料推荐值可调范围影响k采样点对数4020~60精度与耗时40 后收益递减d搜索范围r0/10r0/20 ~ r0/5搜索空间与鲁棒性Canny 高阈值10060~150边缘连续性Canny 低阈值5030~80弱边缘保留高斯核5×53×3 ~ 7×7去噪与边缘模糊的平衡4. 避坑与排查闭眼帧、反光点和实时性翻车4.1 闭眼帧被当成瞳孔直径序列出现尖刺现象连续视频的瞳孔直径曲线里突然冒出几个异常大的值对应帧其实是闭眼或眨眼。原因闭眼时眼睑边缘也可能形成类圆结构标准 Hough 会误检。改进算法用累加值阈值排除但如果阈值设得不对或者闭眼帧的眼睑边缘恰好很强仍会漏过。解决在粗定位后加一道累加值检查低于阈值的帧直接标记为无效不进入精匹配。阈值不要写死用前若干帧的正常瞳孔累加值均值乘一个系数比如 0.6动态更新。另外可以在直径序列上做中值滤波把孤立尖刺剔掉。4.2 角膜反光点导致轮廓匹配偏向现象精匹配输出的圆心总是往反光点方向偏几个像素直径偏小。原因红外光源在角膜上形成亮斑亮斑边缘的灰度差可能比瞳孔边缘还大采样点对的最大化过程被亮斑吸引。解决在精匹配前对反光点做掩膜。反光点特征是局部极亮且面积小用形态学顶帽变换或简单的阈值加连通域分析就能定位定位后把该区域灰度置为邻域均值再匹配。资料里没提这一步但实际红外图里反光几乎必然存在不加这道处理精度会打折。4.3 低端机上 640×480 跑不到实时现象在 320×240 上 60 ms 一帧没问题换成 640×480 直接掉到 200 ms 以上。原因精匹配的三重循环耗时与d² × d成正比而d又和r0成正比。分辨率翻倍r0翻倍d翻倍循环次数变成 8 倍。加上像素数本身 4 倍总耗时涨得比想象快。解决高分辨率下先降采样做粗定位拿到r0后按比例放大回原分辨率再在原图上做精匹配但把d按降采样比例缩小。或者直接限制精匹配只在粗定位圆环附近的 ROI 内做不遍历全图。资料里高端机能实时靠的是 CPU 主频和缓存低端机必须靠 ROI 裁剪。4.4 瞳孔直径单位换算被忽略现象算法输出的是像素直径但研究需要的是毫米直径直接拿像素值做统计结论全错。原因像素到毫米的换算依赖成像距离和相机内参不同被试、不同拍摄距离下换算系数不同。解决在拍摄时放一个已知尺寸的标定物比如标准直径的圆环每段视频单独标定一次像素当量。别用固定系数套所有视频这是血泪经验——某项目组就是因为这个把三个月的直径数据全部作废重采。5. 进阶技巧用累加值阈值做闭眼检测的工程化改造闭眼检测这块值得单独拎出来说因为它是整条流水线里最容易被低估、又最容易翻车的环节。资料里的做法是在 Hough 累加值数组里搜一个最大值阈值低于阈值的帧判为闭眼。这个思路简单但直接照搬会有两个问题阈值怎么定以及阈值要不要随光照变化调整。我一般会把它改造成滑动窗口自适应。维护一个长度为 N比如 30 帧的累加值队列取队列的中位数乘 0.6 作为当前帧的闭眼判定阈值。这样光照缓慢变化时阈值跟着走不会因为环境光变暗就把正常瞳孔误判成闭眼。队列里只放被判定为睁眼的帧的累加值避免闭眼帧拉低中位数。from collections import deque class BlinkDetector: def __init__(self, window30, ratio0.6): self.window window self.ratio ratio self.history deque(maxlenwindow) def is_open(self, accum_max): accum_max: 当前帧 Hough 累加器最大值 返回 True 表示睁眼False 表示闭眼 if len(self.history) self.window // 2: # 冷启动阶段先按绝对阈值兜底 self.history.append(accum_max) return accum_max 50 # 兜底阈值按你的图调 median sorted(self.history)[len(self.history) // 2] threshold median * self.ratio if accum_max threshold: self.history.append(accum_max) return True return Falsewindow取 30 是经验值对应 30 帧约 1 秒30 fps 下能覆盖一次正常眨眼而不至于反应太慢。ratio0.6是闭眼帧累加值相对睁眼中位数的典型比例如果你的红外图闭眼时边缘特别弱可以调到 0.7 让判定更严格。冷启动阶段的绝对阈值50要按实际图像的累加值量级调不同分辨率下这个数差很多。验证这套逻辑是否靠谱有个笨但有效的办法拿一段包含多次自然眨眼的视频把每帧的accum_max和判定结果打出来人工对照画面看有没有误判。误判集中在眨眼过渡帧半睁半闭是正常的只要完全闭眼和完全睁眼的帧判对就行。如果过渡帧误判太多说明ratio太松往上调。从那以后我每次做瞳孔检测都强制先把闭眼检测单独跑一遍、可视化验证再接入直径测量。这一步花不了十分钟但能省掉后面清洗异常值的几个小时。希望帮到你。本文还有配套的精品资源点击获取