引: (2026-01-31周六)第一版的找眼算法差强人意,在鲁棒性和准确性上有待进一步提升,今天更新出一般热乎的,效果基本达到理想的状态,及时更新记录防老年痴呆!!!(但是算法的迭代时没有上限的,优化空间仍然巨大!!!)
1 数据特点
自然图像中的找眼(找瞳孔)在目前计算机视觉方法库中已经是小学生的加减乘除(当然你得是大学生),但是特定领域下的非自然图像由于光学系统、被摄对象的特殊性,仍旧无法直接照搬现有的方法解决问题,需要具体问题具体分析。

Fig 1. 非自然图像对比自然图像下的瞳孔特征
我们的光学系统有以下几个特点:
1 视场角(FOV)极小 -- 导致ccd捕捉到的信息量骤减,Harr等现有算法失效
2 对象远离景深范围 -- 信息同质化(说人话就是 :糊)
3 光照不均匀 -- 干扰图像真实的灰度分布
2 现有方法及问题
图像内容(Fig1)是模糊可见的瞳孔和部分眼白,接受绿光照射,投影有效像素范围大概限制在内切圆(500*500)内,其中由于光源打在人眼上的漫反射加上远离了景深范围,导致图像各部分的梯度并不明显,同时瞳孔可能在画面的任何地方(因为光学系统所在的电机平台可运动)。
2.1 大致算法流程(屏蔽细节)
我们目前设计的瞳孔检测算法能用,但是依旧存在一些问题,导致效果不够理想。
目前算法流程:
1 reise && clip
2 cv::GaussianBlur
3 灰度二值化
cpp
// 分割方法1 -- bernsen局部阈值分割 -- 抖动太厉害
bernsenThreshold(gray, binary, 15, 15); //51
// 分割方法2 -- 大津Otsu -- 太保守,检测范围不够大
cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
// 分割方法3 -- shanbhagThreshold -- 比Otsu范围增大一点
shanbhagThreshold(gray, binary, 3); // radius默认为5
4 形态学处理(谨慎使用非对称算子)
cpp
cv::Mat element = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(7, 3)); //7
cv::dilate(binary, binary, element);
element = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 7)); //7
cv::erode(binary, binary, element); //腐蚀
5 霍夫圆检测
cpp
// ========== 5. 霍夫圆检测 ==========
std::vector<cv::Vec3f> circles2;
cv::HoughCircles( );
关键步骤在二值化和形态学处理上,前者找到瞳孔所在大区域,后者对瞳孔作修边和填充。
二者共同为后面的霍夫圆检测提供尽可能准确干净的数据用于回归。二值化方法的使用上,Otsu和bernsen方法在调整参数后依然无法做到鲁棒,遂使用shanbhag方法。
2.2 shanbhag自适应阈值分割
Shanbhag 方法,通常指 Shanbhag 自适应阈值分割算法(Shanbhag Thresholding, 1994) ,是一种基于熵(Entropy)的自动阈值选择方法,常用于灰度图像二值化。
它和 Otsu、Kapur 属于同一类: **直方图驱动的全局阈值方法,**但思想不一样。
Shanbhag 的核心思想是:找到一个阈值,使"前景 + 背景"的信息不确定性(熵)达到一种平衡状态。既不过度强调前景,也不过度强调背景。(下面表格整理自GPT供参考)
| 方法 | 阈值来源 | 是否依赖直方图 | 是否假设双峰 | 主要失败原因 |
|---|---|---|---|---|
| Otsu | 类间方差 | ✅ | ✅ 强 | 灰度拖尾、单峰 |
| Bernsen | 局部极值 | ❌ | ❌ | 噪声、纹理 |
| Shanbhag | 熵平衡 | ✅ | ❌ | 极端光照不均 |
| [Table 1. 关键机制对比] |
| 方法 | 主要参数 | 调参难度 | 工程稳定性 |
|---|---|---|---|
| Otsu | 无 | ⭐ 极低 | ⭐⭐⭐ |
| Bernsen | 窗口大小、对比度阈值 | ❌ 高 | ⭐ |
| Shanbhag | 无 | ⭐⭐ 低 | ⭐⭐⭐⭐ |
| [Table 2. 参数 & 调参成本] |
3. 尝试过的改进思路
3.1 滑动窗口+步长 + 灰度分析(均值方差、梯度)
起初是想借鉴分治思想,将全图(经过预处理)划分成网格,计算每个sub_image的灰度信息给出大致的瞳孔范围再进一步处理。网格大小的设置需要考虑到瞳孔(或其他特征)在画面中的比例以及电机前后运动时的缩放情况,容易漏检。进一步想到滑动窗口机制(数据结构课本中的数组遍历、YOLO检测模型中的端到端检测思想均是滑动窗口机制的体现),使用固定步长的滑动窗口遍历全图,找到最有区分度的分度特征作为瞳孔区域的是非判断。

Fig 2. 滑动窗口+梯度计算
如上图Fig2,在实际滑动过程中使用每个子窗的导数信息统计边界的分布情况,期望找到瞳孔和眼白的过渡区作为初步的mask。但是反复调整参数也无效,因为边界过度区域太多,无法通过限制条件做筛选,当瞳孔远离图像中心时情况加剧。
python
# 先进行高斯平滑
# gray_smooth = cv2.GaussianBlur(gray, (15, 15), 1.0)
# gray_smooth = gray
#
# # 使用Laplacian算子求导数
# laplacian = cv2.Laplacian(gray_smooth, cv2.CV_64F)
# # laplacian = cv2.Laplacian(laplacian, cv2.CV_64F)
# # 取绝对值以便更好地可视化
# Z_laplacian = np.abs(laplacian)
3.2 深度学习方法
有考虑是否能快速训练一个UNet实现精度和鲁棒性的提升,但是模型的训练加上嵌入式调试,需要的时间成本巨大,所以后期再考虑。
3.3 shanbhag+嵌套霍夫检测
因后续可能考虑专利申请,此部分暂时无法详细描述算法流程。
大致思路是对shanbhag算子使用大胆一点的参数设置,后续叠加复杂的形态学处理后使用嵌套霍夫圆检测作错值剔除,最终实现了较为满意的效果。但是不考虑工程之外的成本和结构设计复杂度,最好的方法就是加外设。有时候一个硬件能解决的简单问题,使用算法替代硬件会复杂几十倍不止,而且还达不到最好的效果。呜呜呜
最后军火展示

Fig 3. 结果