简介:《基于Hough变换和轮廓匹配的红外视频瞳孔精确检测算法研究》是一份面向图像处理与计算机视觉研究人员、工程师的PDF文献,聚焦红外视频中瞳孔中心与直径检测精度不足的问题,可服务于心理健康研究、犯罪侦查和医疗诊断等场景。资源包仅含1个PDF文件,大小约1.04MB,篇幅集中,便于快速查阅算法原理与实验结论。文献提出Hough-Contour方法:先灰度化与滤波去噪,再用改进Hough梯度法初检瞳孔,并在其附近进行圆形轮廓匹配,通过限定位置与半径变化范围、设置40组点对等参数,将精确匹配率提升至99.8%,并可利用累积值阈值自动排除闭眼帧;实验覆盖320×240与640×480视频,兼顾低端设备实时性与高端设备处理需求。目前已有170人学习,适合具备基础图像处理与Hough变换知识、希望深入理解参数设置与实现细节的读者参考。
1. 从 10% 到 99.8%:红外视频瞳孔检测的精度坎在哪
红外眼图里的瞳孔几乎是最好认的目标:位置固定在画面中央、边缘清晰、对比度高,看上去随便一个圆检测都能拿下。但真把 OpenCV 自带的圆变换直接跑上去,直径测量的精确率只有 10% 上下,这个数字挺反直觉。原因在于 Hough 变换本质上是在参数空间里投票找"最像圆的位置",它对圆心位置够敏感,对半径的量化却很粗,半径偏一两个像素,换算到直径就是几个百分点,而瞳孔直径这件事恰恰要的就是那一个像素级的分辨力。这套 Hough-Contour 方案走的是两段式:先用改进的 Hough 梯度法把瞳孔圈出来当初始值,再在初始圆附近用轮廓匹配做精修,把精确率从 10% 推到 99.8%,同时在低端机器上 320×240 的帧 60ms 出一帧。适合正在做红外视频瞳孔检测、眼动特征提取这类课题,卡在"能检出但测不准"这一步的人。
2. 算法总体架构:四步流水线与 Hough-Contour 的双阶段分工
整条流水线只有四步:灰度化、滤波、边缘提取与 Hough 初检、轮廓匹配精修。步骤少不代表简单,真正的设计工作量集中在两个地方——每一级交给谁做,以及两级之间的边界怎么切。切宽了,精修在几百个候选里瞎转,耗时爆炸;切窄了,初检一旦偏出半个瞳孔,精修再怎么搜也救不回来。这一章把分工和骨架先定下来,后面两章再往里填实现。
2.1 预处理与边缘提取的取舍
红外摄像头出来的帧通常是三通道的,但红外照明下画面本身几乎没有色彩信息,转灰度只是去掉冗余通道,不损失有效内容。真正影响后续所有环节的是滤波:红外成像的噪声来源比较杂,有传感器暗电流噪声,也有眼角膜反光造成的局部过曝,5×5 的高斯核在抑制随机噪点和保留边缘锐度之间是个折中值。窗口开到 7×7 以上,瞳孔边界会明显发糊,Hough 拿到的边缘变粗,半径估计跟着偏大,这是后面避坑章要专门讲的一条。
边缘提取只服务 Hough,不服务精修。Canny 的高低阈值取 100 和 50,比例 2:1,在红外图像这种高对比度场景里这个比例足够稳:高阈值负责锁定真正的瞳孔边界,低阈值负责把因反光断裂的边缘接上。比值如果压到 1.5:1,噪点会被当成边缘,Hough 的累积数组里会冒出一堆假峰;拉到 3:1 以上,下眼睑附近那段弱边缘就断了,圆拟合会缺一段弧。
精修阶段读的是滤波后的灰度图,不是 Canny 的二值边缘图。这个设计是整套算法里最容易被忽略但影响最大的一处:二值边缘把灰度信息压成了 0/1,边缘两侧到底差多少亮度这个信息丢了;而轮廓匹配的代价函数正是靠边缘内外的灰度差来判定"这个圆贴不贴边",用二值图等于把判据废掉。
2.2 粗定位与精修的职责边界
Hough 梯度法在 (a, b, r) 三维参数空间里做穷举投票,它的强项是全局搜索——不管瞳孔在画面哪个位置、半径大概多少,只要边缘存在,它就能给出一个不离谱的候选。它的弱项也是穷举带来的:想在三维空间里做到 1 像素级的精度,要么把累加器的分辨率提到极高(内存和时间都受不了),要么接受粗量化。所以这一级的输出被明确限定为"初始圆",只要求圆心误差在若干个像素内、半径误差在 10% 以内,不要求精确。
轮廓匹配接的是精修这一段。它的搜索空间被强行约束在初始圆周围半径为 r0/10 的立方邻域里,圆心两个方向各 ±d、半径 ±d,一共 (2d+1)³ 个候选位置。这个约束是有代价的——如果 Hough 初检偏出超过 r0/10,真值就落在搜索窗外,精修必然失败;但收益更大,因为搜索空间从全图缩到一个十几个像素的小盒子里,可以放心地用暴力枚举把每一个候选都算一遍代价,不用任何启发式剪枝,工程上非常稳。
两级之间还有个隐含约定:初检给的半径必须量级正确。半径偏差过大时,r0/10 这个相对搜索幅度会跟着失真,r0 报大了,搜索窗跟着放大,精修反而跑到虹膜外沿去了。所以 Hough 那一步的半径上下界要卡死,按实际成像尺度设,不能图省事留 0 到图像宽度。
2.3 数据结构与主流程代码骨架
把两级串起来的主流程很短,短到一眼能看完,但每一行的参数都有来历,后面调参基本都在改这几行。
// 单帧处理主流程:预处理 -> 边缘 -> Hough 粗定位 -> 轮廓匹配精修 bool processFrame(const cv::Mat& srcBGR, PupilResult& out) { cv::Mat gray; cv::cvtColor(srcBGR, gray, cv::COLOR_BGR2GRAY); // 红外帧转灰度,去掉冗余通道 cv::GaussianBlur(gray, gray, cv::Size(5, 5), 0); // 5x5 高斯,抑制传感器噪声 cv::Mat edge; cv::Canny(gray, edge, 100, 50); // 高低阈值 100/50,只给 Hough 用 Circlef init; // 返回 false 表示本帧没有可信的初始圆(多为闭眼帧) if (!houghInit(edge, init)) return false; // 精修读的是 gray,不是 edge,灰度差信息必须保留 return contourRefine(gray, init, out); }processFrame的返回值承担了闭眼帧过滤的语义:返回 false 的帧不进后续统计,也不参与瞳孔直径计算,这比事后再加一个睁闭眼分类器省事得多。houghInit内部只接受边缘图,因为它只需要梯度方向信息;contourRefine内部只接受灰度图,因为它要算像素亮度差。两个函数各拿各的输入,互不越界,这样后期谁出问题就改谁,不会牵一发动全身。
参数上唯一需要按分辨率调整的是houghInit里的半径上下界。320×240 和 640×480 两种采集分辨率下,瞳孔占的像素数差一倍,半径区间要跟着缩放,否则 640×480 下正常大小的瞳孔会因为超出 maxRadius 而一律检不出。分数的形式比写死像素值安全,例如把上下界写成rows * 0.04和rows * 0.25。
3. Hough 初检:累积值求最大值替代排序的实现细节
粗定位这一段调用的是 OpenCV 内部的 Hough 梯度实现,老版本的 C 接口叫icvHoughGradient,C++ 接口包成cv::HoughCircles的HOUGH_GRADIENT模式。算法本身没改,改的是它输出候选之后的那段收尾逻辑。改动不大,但直接影响帧率能否达标,也顺带解决了闭眼帧过滤这个原本要额外写模块的问题。
3.1 icvHoughGradient 的输入输出与参数含义
理解这个函数的关键是知道它内部维护了两个东西:一个三维的圆心累加器,和一组按半径分箱的直方图。边缘图上每个点沿梯度方向投票,票数落到累加器对应的 (a, b) 格子里;投票结束后,累加值高的格子就是候选圆心,再对每个候选圆心统计各半径上的票数,票数最高的半径作为该圆的半径。所以它输出的不是"一个圆",而是一批候选,收尾时还要做选择。
几个参数各管一段,调错了症状很明确:
| 参数 | 本方案取值 | 管什么 | 调错的典型症状 |
|---|---|---|---|
| dp | 1.0 | 累加器分辨率与原图比例 | 大于 2 时圆心坐标被量化,精度直接掉 |
| minDist | rows/8 | 候选圆心之间的最小间距 | 太小则同一次检测出多个近重合圆 |
| param1 | 100 | Canny 高阈值 | 见 2.1,边缘断裂或噪点入局 |
| param2 | 20 | 累加器判定阈值 | 偏高则漏检,偏低则假圆泛滥 |
| minRadius | rows*0.04 | 半径搜索下界 | 设为 0 会让累加器在极小圆上浪费票 |
| maxRadius | rows*0.25 | 半径搜索上界 | 偏小则正常瞳孔直接检不出 |
dp取 1.0 是必须的,很多人为了提速把它改成 2,结果圆心坐标被量化到 2 像素的网格上,后面精修再准也补不回这个损失。param2这一项在原始算法里叫累加器阈值,它同时承担了"过滤弱候选"和"判断有没有圆"两个职责,是个一参数两用的设计,下面 3.3 会专门用它做闭眼帧判定。
3.2 从降序排序到求最大值:省掉的那一半时间
老实现的收尾逻辑是把所有候选圆心按累积值降序排一遍,取排在最前面的那个作为最优解,然后再为它算半径。排序本身不贵,但在嵌入式或低端机器上,候选数量在复杂背景下能到几十上百个,n log n的排序和后续为每个候选单独算半径的开销叠加起来,一帧就多出十几毫秒。
真正的需求其实只是"票数最多的那一个",用不着完整有序序列。改成一次线性扫描求最大值,复杂度从n log n降到n;更关键的是半径计算这一步被推迟了——只有最大值的那个候选才需要算半径,其余候选的半径直方图根本不用碰。这两处合起来是这一级提速的主要来源。
// 改进点一:累积值不排序,线性扫描求最大值,且只对最优候选算半径 bool houghInit(const cv::Mat& edge, Circlef& init) { const int rMin = (int)(edge.rows * 0.04); const int rMax = (int)(edge.rows * 0.25); const int accMin = 60; // 累积值下限,兼作闭眼帧判据 std::vector<cv::Vec3f> cands; cv::HoughCircles(edge, cands, cv::HOUGH_GRADIENT, 1.0, edge.rows / 8.0, accMin, 20, rMin, rMax); if (cands.empty()) return false; // 连候选都没有,直接判闭眼 int bestIdx = 0; float bestAcc = -1.0f; // 只做一次遍历找最大票数,不做全排序 for (size_t i = 0; i < cands.size(); ++i) { float acc = accOfCandidate(cands[i]); // 从累加数组里取该候选的票数 if (acc > bestAcc) { bestAcc = acc; bestIdx = (int)i; } } // 票数不足阈值,判定为闭眼帧,本帧丢弃 if (bestAcc < accMin) return false; init.x = cands[bestIdx][0]; init.y = cands[bestIdx][1]; init.r = cands[bestIdx][2]; return true; }accOfCandidate是从累加数组里读票数的封装,如果直接改内部实现,这里等价于在投票结束后记录最大值和它的下标,一次遍历同时完成"找最优"和"记阈值"两件事。accMin传了两遍(一次给 HoughCircles 做阈值,一次给自己做二次判定)是有意的冗余:前者在函数内部就砍掉了弱候选,后者在候选全被砍光时仍有兜底判断,避免拿到一个空数组就当成"没检测到"而丢掉闭眼这个语义。
这段改动在 320×240 上省下的绝对时间不算多,但它是把单帧压到 60ms 以内的必要一环。工程上的血泪经验是:性能优化一定要挑在算法天然允许的地方做,排序改求最大值不改变结果,属于白捡;反过来,靠降低累加器分辨率或跳帧来提速,都是拿精度换的。
3.3 用累积值阈值剔除闭眼帧
红外视频里眨眼和闭眼是常态,一秒钟里可能有好几帧完全没有瞳孔。如果这些帧也走完整流程,轻则输出一堆无意义的圆心坐标污染统计,重则把眼皮的边缘误判成一个巨大的圆,直径统计被拉出离群点。
专门写一个睁闭眼分类器是常规思路,但要多一份标注数据和一轮训练。这套算法换了个更省的做法:闭眼时瞳孔不存在,边缘图上没有瞳孔那圈圆弧,累加器里就不太可能出现高票数的峰。也就是说,累加数组的最大值本身就携带了"有没有圆"的信息,直接拿它的分布来定阈值就行。
具体做法是离线统计一段样本:把所有睁眼帧的累加数组最大值和所有闭眼帧的最大值各画一条分布,两条分布之间通常有个明显的谷底,阈值就取在谷底位置。线上的判定逻辑就是 3.2 代码里的bestAcc < accMin,一行比较,零额外开销。
这套做法的边界要清楚:阈值和成像条件强相关。换一套红外光源、换一个拍摄距离,瞳孔在画面里的面积变了,票数整体会平移,阈值必须重新标一次。我一般会把阈值写成可配置项,上线前拿几百帧现场数据跑一遍重新定,而不是沿用上一套设备的数字。闭眼帧被正确剔除之后,后续所有关于直径的统计量才站得住,这一步省不得。
4. 轮廓匹配精修:搜索范围与点对数的参数标定
精修是整套算法里唯一真正决定精度的一步。它的思路朴素得有点不像算法:在初始圆周围把所有可能的圆都试一遍,哪个圆"最像瞳孔边缘"就选哪个。"最像"由灰度差代价函数定义,搜索范围由偏置 d 控制,采样密度由点对数 k 控制。三个量各有一个最优值,偏一点精度就掉一截,所以这一章把每个参数的来历都说清楚。
4.1 圆环点对采样与灰度差代价函数
给定一个候选圆 (x, y, r),在圆周上均匀取 k 个角度 θ,每个角度上取两个点:一个在半径 r-1 的内侧,一个在半径 r+1 的外侧。这两个点跨越瞳孔边界,一个落在暗的瞳孔内部,一个落在亮的虹膜上。如果这个候选圆真的贴合瞳孔边缘,那么所有点对的亮度差都大且一致;如果候选圆偏了,某些角度上的点对会同时落在一侧,差值变小。
数学表达很直接:
p1 = (x + (r-1)·cosθ, y + (r-1)·sinθ) 内侧点 p1' = (x + (r+1)·cosθ, y + (r+1)·sinθ) 外侧点 cost = (1/n) · Σ | f(p1') - f(p1) | n 为有效点对数f是灰度图上的像素值,n是实际参与统计的点对数——贴边的圆会有部分采样点跑到图像外,这些点直接跳过不计入,否则边界处会产生假的低代价。
代价取绝对值之和的最大值,不是最小值。这一点跟很多模板匹配的直觉相反:模板匹配找的是"最相似",而这里是找"边界最显著"。瞳孔内部吸收红外光,灰度很低,虹膜和巩膜反射率高,灰度值高,跨过真实瞳孔边缘的两个采样点亮度差最大。取最大值意味着选中的是"边缘对比最强烈"的那个圆,这跟瞳孔的成像特性是对应的。
内外偏移量固定为 1 像素,这个值不宜放大。取 2 或 3 像素看似更稳,实际会跨越虹膜纹理或者睫毛阴影,把不相关的结构差异混进代价里,反而降低了区分度。1 像素刚好卡在边缘两侧,是最紧的采样。
4.2 d = r0/10 是怎么定下来的
搜索偏置 d 决定了精修能纠正多大的初检误差,它按初始半径的比例给,不按固定像素给。这个设计的原因是:不同分辨率、不同拍摄距离下瞳孔半径差很多,固定像素值在一种场景下合适,换一种就完全失效。
原方案做过一组对比,把 d 分别设成 r0/20、r0/10、r0/2 三种量级跑同一批数据。d = r0/20 时搜索窗太窄,Hough 初检那点固有偏差(几个像素)就已经把真值挤出窗外,精修只能在错的邻域里找局部最优,精度反而比不精修还难看。d = r0/2 时窗口一直扩到虹膜外沿甚至眼皮,代价函数在更大的区域里更容易被虹膜外边界的高对比吸走,耗时也成倍上涨。r0/10 落在一个很舒服的区间:以半径 30 像素的瞳孔为例,d = 3,搜索窗覆盖圆心 ±3、半径 ±3,刚好把 Hough 的量化误差和轻微形变包住,又不至于跑到别的结构上去。
| d 取值 | 相对搜索幅度 | 表现 |
|---|---|---|
| r0/20 | 约 5% | 搜索窗过窄,初检偏差无法纠正,精度回落 |
| r0/10 | 10% | 覆盖初检误差,代价曲面单一峰,精度 99.8% |
| r0/2 | 50% | 跑出瞳孔,被虹膜外沿吸走,耗时不划算 |
这张表要说明的不是"10% 最好"这个结论,而是"这个参数必须跟半径挂钩"。工程上换个采集设备,可以不动 d 的比例,让它自动跟着 r0 缩放,这是一次写对、长期受益的设计。
4.3 点对数 k=40 与角度步长
点对数 k 决定圆周采样的密度,角度步长就是 2π/k。k 太小,采样点稀疏,某几个点恰好落在睫毛阴影或者反光斑上就能把代价带偏;k 太大,每个候选圆的计算量线性增长,而精度早就饱和了。
原方案给的实测数据很清楚:
| k | 精确率 | 单帧耗时 |
|---|---|---|
| 10 | 50.0% | 5.2 ms |
| 20 | 90.0% | 11.7 ms |
| 30 | 99.0% | 15.9 ms |
| 40 | 99.8% | 22.2 ms |
从 10 到 20 是陡升段,20 到 30 还在明显改善,30 往后收益就很小了,40 相对 30 只多 0.8 个百分点却多花 6 毫秒左右。选 40 的理由是它留了余量:这批数据是在比较干净的样本上测的,实际视频里眨眼、睫毛遮挡的比例更高,稀疏采样在脏数据上的精度衰减比干净数据快,多一点冗余能扛住。
如果目标平台实在跑不动 40,30 是个可接受的降级点,99.0% 对多数应用足够。低于 20 就不要考虑了,90% 的精确率意味着十帧里有一帧瞳孔直径是错的,这种错误在后续的注意力分析里会被放大成假的波动。
还有个细节:有效点对数 n 小于 k/2 时候选圆直接跳过。这是为了防止贴近图像边缘的候选圆因为大部分采样点越界而拿到一个虚高的平均代价,不设这个门槛,边缘区域会冒出一些莫名其妙的"最优解"。
4.4 精修循环的完整实现
三重循环加一层角度采样,结构上就是暴力枚举,没有任何技巧,靠的是搜索空间被 d 压得足够小。
// 改进点二:在初始圆邻域内枚举候选圆,取跨边缘灰度差最大者 bool contourRefine(const cv::Mat& gray, const Circlef& c0, PupilResult& out) { const int d = std::max(1, (int)(c0.r / 10)); // 搜索幅度 = 初始半径的 1/10 const int k = 40; // 圆周采样点对数 const double thetaStep = 2 * CV_PI / k; // 角度步长 const int cx0 = (int)c0.x, cy0 = (int)c0.y, r0 = (int)c0.r; double bestCost = -1.0; // 取最大值,初值置负 for (int r = r0 - d; r <= r0 + d; ++r) for (int cx = cx0 - d; cx <= cx0 + d; ++cx) for (int cy = cy0 - d; cy <= cy0 + d; ++cy) { double sum = 0.0; int cnt = 0; for (int i = 0; i < k; ++i) { double th = i * thetaStep; // 内外各偏移 1 像素,跨过瞳孔边界 int x1 = (int)(cx + (r - 1) * std::cos(th)); int y1 = (int)(cy + (r - 1) * std::sin(th)); int x2 = (int)(cx + (r + 1) * std::cos(th)); int y2 = (int)(cy + (r + 1) * std::sin(th)); if (!inImage(gray, x1, y1) || !inImage(gray, x2, y2)) continue; sum += std::fabs(gray.at<uchar>(y2, x2) - gray.at<uchar>(y1, y1)); ++cnt; } if (cnt < k / 2) continue; // 越界点过多,跳过该候选 double cost = sum / cnt; if (cost > bestCost) { // 灰度差越大,越贴边 bestCost = cost; out.x = cx; out.y = cy; out.r = r; } } return bestCost > 0.0; }复杂度可以直接算出来:候选圆数量是 (2d+1)³,每个候选算 k 次采样。半径 30 的瞳孔,d = 3,候选数 343,乘 40 是 13720 次采样,每次采样两次内存读、一次减法一次取绝对值。这个量级在普通 CPU 上是毫秒级的事,不需要任何并行或 SIMD 优化。反过来说,如果哪天发现这一步超过 20 毫秒,八成是 d 或者 r0 出了异常,先去看 Hough 输出的半径是不是离谱,别急着优化内层循环。
inImage的判定必须放在最内层,不能提到外层去预计算。原因是一批候选圆会贴着图像边界(瞳孔被画面裁切的情况确实存在),提前过滤会导致这些候选取不到任何有效解,直接返回失败。
5. 避坑与排查:参数标定和性能统计里最容易翻车的五处
这套算法的实现代码不长,坑基本都在参数上。同样的代码,参数对了精度 99.8%,参数偏了可能比不加精修还差。下面五条是实际调试里反复遇到的,每条按现象、原因、解决写,能对上号就直接改。
5.1 预处理和边缘提取的两处
现象:Hough 报出来的半径系统性偏大,精修后的圆总是比肉眼看到的瞳孔大一圈,边界落在虹膜上而不是瞳孔上。
原因:高斯滤波窗口开到 7×7 或更大。红外图像的瞳孔边界本来就是一段只跨两三个像素的陡变,大窗口把这个过渡带抹平拉宽,Canny 在模糊后的梯度上定位的边缘会向亮侧偏移,Hough 拟合出的圆跟着外扩。
解决:滤波核固定 5×5,不要因为"看着噪点多"就往上加。噪点的正确解法是调低 Canny 高阈值或者做一次中值滤波去椒盐,而不是把高斯核放大。判断方法很简单:把滤波前后的边缘图并排看一眼,瞳孔那一圈弧线的粗细有没有明显变化。
现象:画面里明明有瞳孔,Hough 偶尔整帧返回空,或者累积值最大的一点落在眼角。
原因:Canny 高阈值照抄了别的场景的参数。红外帧经常会有一段角膜反光的过曝区,那块区域的梯度极大,如果高阈值设得不够,反光边缘会盖过瞳孔边缘成为主导峰;反过来阈值设太高,过曝区附近那段瞳孔边缘断掉,圆弧不闭合。
解决:高低阈值按 2:1 联动调,先用 100/50 起步,然后只做一件事——看瞳孔那圈弧有没有连续。断了就等比例降低,噪点进来就等比例升高。不要单独改其中一个。
5.2 搜索范围与采样密度的两处
现象:精修之后圆心反而跳到了虹膜外沿,或者跑到了上眼睑的位置,输出的半径比初始值大出好几个像素。
原因:d 写成了固定像素值,比如直接写d = 5。在瞳孔半径 30 的样本上这相当于 r0/6,搜索窗偏大;到了半径 15 的小瞳孔样本上就变成 r0/3,窗口直接盖住虹膜外边界,而虹膜外侧(巩膜)和瞳孔内部的灰度差比瞳孔边缘还大,代价函数被拉走。
解决:d 必须按比例给,d = r0/10,并且加个下限max(1, ...)防止半径很小时 d 归零。改完拿一批不同半径的样本验证,正常情况下每个样本的搜索窗都应该落在瞳孔边界附近,不出现跨结构的情况。
现象:k 从 40 降到 15 想提帧率,精度掉到 80% 出头,而且掉得不均匀,某些帧很准某些帧完全错。
原因:采样点太稀疏,某个点恰好落在睫毛阴影或反光斑上时,它对平均代价的影响权重被放大了。密集采样会把这种局部异常平均掉,稀疏采样则让它直接决定结果。
解决:k 不低于 30。真要压时间,优先看 Hough 那一步有没有可省的(例如缩小图像分辨率后再放大坐标),别动精修的采样密度,它换来的那点时间远不如精度值钱。
5.3 性能统计里的一处
现象:报告的耗时是每帧 3 毫秒,实际跑起来明显卡顿,跟报告完全对不上。
原因:两个叠加的错误。一是用了 Windows 上毫秒级的计时接口,两次调用之间的分辨率就是 1 毫秒,测 60 毫秒以内的操作,误差能到 5% 以上,测几毫秒的操作基本是噪声;二是拿调试版编译的二进制测的,OpenCV 的循环在 Debug 下慢一个数量级。
解决:计时换成高精度计数器,并且统计一段连续帧的总耗时再除以帧数,不要单帧单次测。编译一律走 Release,测之前确认 OpenCV 的运行时库也是发布版。这一条听起来像常识,但每年都有人栽在"为什么我测出来这么快但跑起来这么卡"上。
6. 验收与答辩:把参数敏感性做成能讲的实验
这类算法课题最容易被追问的一句是"你这个 99.8% 是怎么来的"。答辩现场如果只报一个数字,评委十有八九会拿"样本是不是挑过的""参数是不是凑出来的"来问。稳妥的做法是把验收拆成三层展示,每一层都拿得出可复现的过程。
第一层是精度口径。不要只报一个总体精确率,按 d 和 k 分别做参数扫描,每个参数一组,每组给精确率和耗时两列,就是 4.2 和 4.3 那两张表。答辩时重点讲 k 从 10 到 40 那条曲线为什么在 30 以后就平了——这体现的是"知道自己的优化为什么该停"。下面这张表可以直接拿去当答辩的指标页:
| 验收项 | 判定方法 | 合格线 |
|---|---|---|
| 直径精确率 | 人工标注直径与算法输出比对 | ≥ 99% |
| 圆心偏差 | 与人工标注圆心算欧氏距离 | ≤ 1 像素 |
| 闭眼帧剔除 | 闭眼样本集中被丢弃的比例 | ≥ 95% |
| 单帧耗时 | 连续 200 帧总耗时均值 | 320×240 ≤ 60 ms |
| 分辨率适配 | 640×480 下同流程跑通 | 无参数硬编码冲突 |
第二层是闭眼帧的现场演示。这个点讲好了很加分,因为它体现的是"从数据特性倒推算法设计"的思路:不是额外训练一个分类器,而是复用 Hough 累加数组的最大值当判据。演示时连续播一段包含眨眼的视频,把每帧的累加最大值打印成一条曲线,曲线在眨眼处明显下凹,阈值线一画上去,判定过程一目了然,比讲一堆公式管用。
第三层是失败案例。主动给一两帧检错的,说明为什么错——通常是强烈反光或者瞳孔被眼睑遮了一半,然后说明在你的使用场景里这类帧占比多少、影响多大。评委会觉得你对边界条件是清楚的。
我一般会把参数扫描的原始数据和可执行脚本一起留着,答辩前跑一遍确认结果能复现。从那以后每次交方案,我都会强制走一遍"参数扫描 → 阈值标定 → 闭眼样本验证"这套流程,哪怕时间再紧也不跳步,因为评委问的不是结果好不好,是你能不能解释结果是怎么来的。希望帮到你。
本文还有配套的精品资源,点击获取