ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++与OpenCV实现虹膜分割:从霍夫圆检测到掩膜生成与参数调优

C++与OpenCV实现虹膜分割:从霍夫圆检测到掩膜生成与参数调优

简介:这份资源聚焦虹膜分割与虹膜提取的工程实现,面向具备一定图像处理基础、希望用C++与OpenCV落地生物特征识别项目的开发者与学习者。包内提供可直接编译运行的Visual Studio工程,核心代码以cpp源文件形式给出,并配有测试图片与一份方法讲解PPT,便于对照理解分割流程与算法思路。资源共39个文件,包含10张bmp与10张jpg测试图像、2个cpp实现文件,以及sln、vcproj、exe、pdb等工程与调试文件,另有pdf说明文档,压缩包约3.26MB,结构完整、开箱即用。目前已有1124人学习下载,适合作为课程设计、毕业设计或虹膜识别入门阶段的参考实现,读者可借此快速搭建实验环境,观察分割效果并在此基础上做算法改进与二次开发。

1. 虹膜分割到底在分什么:从一张人眼图到可用的掩膜

很多人第一次拿到虹膜分割任务,以为就是把瞳孔那一圈抠出来。真上手才发现,一张人眼图里要分的东西至少有三层:瞳孔、虹膜环带、以及把这两者包住的眼睑和睫毛干扰区。虹膜分割代码要输出的不是一张好看的图,而是一张二值掩膜,告诉后续特征提取模块「哪些像素属于虹膜、哪些是噪声」。这份 C++ 和 OpenCV 实现的资源,核心价值就在于把这条链路用原生代码跑通,不依赖深度学习框架,编译完就能在本地对单张图或批量图做虹膜提取。

它适合两类人:一类是做生物特征识别、想理解传统图像处理流程的从业者;另一类是需要把虹膜定位嵌进 C++ 工程、又不想引入庞大推理依赖的开发者。整份代码围绕灰度化、边缘检测、霍夫圆变换、掩膜生成几个环节展开,参数都暴露在源码里,改起来直观。下面按「先跑通、再调参、最后避坑」的顺序拆。

2. 环境搭建与第一张掩膜:把 C++ 工程跑起来

2.1 依赖选型:为什么是 OpenCV 而不是别的

虹膜分割的经典路线是「边缘 + 圆拟合」,这条路线上 OpenCV 提供的HoughCircles、Canny、GaussianBlur几乎覆盖了全部需求。选 OpenCV 而不是自己写卷积,理由很实际:霍夫圆变换内部涉及累加器空间和半径搜索,手写一版能跑但调参成本极高,而 OpenCV 的实现已经做了非极大值抑制和梯度筛选,参数语义清晰。C++ 版本相比 Python 的优势在于,批量处理上千张图时没有解释器开销,且能直接编进已有的桌面或嵌入式工程。

常见做法是用 CMake 管理工程,把 OpenCV 作为外部依赖链接进来。我一般会先确认本机 OpenCV 版本,因为 4.x 和 3.x 在HoughCircles的参数默认值上有细微差别,尤其是param2的敏感度。

# 查看本机 OpenCV 版本,决定后续链接的库名 pkg-config --modversion opencv4 # 若输出 4.x,则 CMake 里用 find_package(OpenCV 4 REQUIRED)

这段命令的作用是确认版本号。如果输出为空,说明pkg-config路径没配好,需要手动指定OpenCV_DIR。参数上没什么可调的,纯粹是环境探测。

2.2 编译与运行:从源码到可执行文件

工程结构通常是src/放源码、include/放头文件、CMakeLists.txt在根目录。下面是一个最小可用的 CMake 配置,链接 OpenCV 的核心和图像处理模块。

cmake_minimum_required(VERSION 3.10) project(IrisSegmentation) # 只链接需要的模块,减少编译体积 find_package(OpenCV REQUIRED COMPONENTS core imgproc highgui) add_executable(iris_seg src/main.cpp src/iris_segment.cpp) target_include_directories(iris_seg PRIVATE ${OpenCV_INCLUDE_DIRS} include) target_link_libraries(iris_seg PRIVATE ${OpenCV_LIBS})

逻辑说明:COMPONENTS里只写core、imgproc、highgui,是因为虹膜分割用不到dnn、videoio这些模块,少链接能加快编译。target_include_directories把自定义头文件目录加进去,避免源码里写一长串相对路径。参数上,REQUIRED保证找不到 OpenCV 时直接报错而不是静默跳过。

编译命令就三步:

mkdir build && cd build cmake .. make -j4

-j4是按四核并行编译,机器核多可以改大。编译完成后build/下会生成iris_seg可执行文件,运行时传入图片路径即可。

2.3 第一张掩膜的生成流程

主流程分四步:读图转灰度、高斯模糊降噪、霍夫圆检测定位瞳孔和虹膜外边界、根据两个圆生成环带掩膜。下面这段是核心函数的骨架。

// 输入:原始 BGR 图;输出:虹膜环带掩膜 cv::Mat segmentIris(const cv::Mat& src) { cv::Mat gray, blurred; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); // 高斯核 5x5,sigma 由 OpenCV 自动推算,先压掉睫毛高频噪声 cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0); std::vector<cv::Vec3f> circles; // param1 是 Canny 高阈值,param2 是累加器阈值,越小越容易检出假圆 cv::HoughCircles(blurred, circles, cv::HOUGH_GRADIENT, 1, gray.rows / 8, 200, 50, 20, 200); cv::Mat mask = cv::Mat::zeros(gray.size(), CV_8UC1); if (circles.size() >= 2) { // 半径大的当虹膜外边界,小的当瞳孔边界 cv::Vec3f pupil = circles[0], iris = circles[1]; if (pupil[2] > iris[2]) std::swap(pupil, iris); cv::circle(mask, cv::Point(pupil[0], pupil[1]), pupil[2], 0, -1); // 挖掉瞳孔 cv::circle(mask, cv::Point(iris[0], iris[1]), iris[2], 255, -1); // 填充虹膜 } return mask; }

逻辑说明:先转灰度是因为霍夫圆只吃单通道。高斯模糊的核大小 5x5 是经验值,太大虹膜边缘会糊,太小睫毛噪声压不住。HoughCircles里dp=1表示累加器分辨率与原图一致,minDist设为行高的八分之一,防止同一个圆被检出多次。param1=200是 Canny 高阈值,param2=50是累加器阈值,这两个是调参重点。最后用「先填白再挖黑」的方式生成环带,比逐像素判断半径更省事。

参数怎么改:如果检不出圆,先把param2降到 30 左右;如果检出太多假圆,把param2提到 70 以上。minDist太小会导致瞳孔和虹膜被当成同一个圆,太大则可能漏掉半径接近的双圆。

3. 参数调优与鲁棒性:让掩膜在脏图上也能用

3.1 霍夫圆参数的实际影响

HoughCircles的五个关键参数里,dp、minDist、param1、param2、minRadius/maxRadius每一个都会显著改变结果。下面这张表是我在几十张不同质量人眼图上试出来的经验区间,不是理论值,是踩坑踩出来的。

参数作用保守取值激进取值翻车表现
dp累加器分辨率12取 2 时小瞳孔直接漏检
minDist圆心最小间距rows/8rows/4太小双圆合并,太大漏检
param1Canny 高阈值200100太低边缘碎,圆拟合发散
param2累加器阈值5030太低满屏假圆
minRadius最小半径2010太小把噪点当瞳孔

调参顺序建议:先固定dp=1、minDist=rows/8,然后调param2让圆的数量稳定在 2 到 4 个,最后用minRadius/maxRadius把明显不合理的圆筛掉。param1一般不动,除非图像对比度特别低。

3.2 预处理对分割结果的影响

原始人眼图里睫毛、反光、眼睑阴影都是干扰源。直接上霍夫圆,十张里能有三张检出正确双圆就不错了。常见做法是在灰度化之后加一步形态学开运算,把细长的睫毛去掉,同时保留虹膜边缘。

cv::Mat denoise(const cv::Mat& gray) { cv::Mat blurred, opened; cv::GaussianBlur(gray, blurred, cv::Size(7, 7), 0); // 3x3 矩形核做开运算,先腐蚀掉睫毛再膨胀回来 cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)); cv::morphologyEx(blurred, opened, cv::MORPH_OPEN, kernel); return opened; }

逻辑说明:核从 5x5 提到 7x7 是为了让睫毛这种细结构在模糊阶段就衰减掉。开运算的核不能大,3x3 足够,再大虹膜外边缘会被啃掉一圈,导致半径偏小。这一步的代价是瞳孔边缘也会略微变钝,所以后续param2要相应降低 5 到 10。

另一个常见干扰是红外图里的高光斑。高光在灰度图上表现为一块高亮区域,霍夫圆会把它的边缘当成圆弧。处理办法是在开运算之后加一步阈值截断,把超过 240 的像素压到 240,削弱高光梯度。

3.3 双圆匹配逻辑的边界处理

HoughCircles返回的圆是无序的,不能默认第一个是瞳孔、第二个是虹膜。除了按半径排序,还要加两个约束:圆心距离不能太远、半径比要在合理范围。正常正视人眼里,瞳孔半径约为虹膜半径的 0.3 到 0.6,圆心偏移不超过虹膜半径的 0.2。

bool isValidPair(const cv::Vec3f& a, const cv::Vec3f& b) { float rMin = std::min(a[2], b[2]); float rMax = std::max(a[2], b[2]); float ratio = rMin / rMax; float dx = a[0] - b[0], dy = a[1] - b[1]; float dist = std::sqrt(dx * dx + dy * dy); // 半径比 0.3~0.6,圆心偏移不超过大圆半径的 0.2 return ratio > 0.3f && ratio < 0.6f && dist < rMax * 0.2f; }

逻辑说明:这个函数在候选圆列表里两两配对,只保留满足几何约束的组合。如果没有任何一对通过,说明这张图质量太差,应该跳过而不是硬分割。参数 0.3 和 0.6 是统计值,遇到侧视或斜视人眼可以放宽到 0.25 和 0.7,但放宽后假阳性会上升,需要配合后续掩膜面积过滤。

4. 避坑与排查:五个真实翻车记录

4.1 现象:编译通过但运行时报缺少 opencv_world 库

原因:CMake 链接的是模块化库,但运行时动态链接器找不到.so路径。解决:在~/.bashrc里把 OpenCV 的lib目录加进LD_LIBRARY_PATH,或者编译时加-Wl,-rpath把路径写死进可执行文件。我一般用后者,省得换终端就失效。

4.2 现象:同一张图两次运行结果不一样

原因:HoughCircles内部有并行累加,多线程下浮点累加顺序不同会导致结果微抖。解决:在调用前设cv::setNumThreads(1),牺牲一点速度换确定性。批量处理时这个设置尤其重要,否则日志对不上。

4.3 现象:掩膜边缘有一圈锯齿

原因:cv::circle画圆时默认用 8 邻域连接,半径大时边缘呈阶梯状。解决:把cv::circle的最后一个参数lineType设为cv::LINE_AA,开启抗锯齿。代价是掩膜边缘会有灰度过渡值,后续如果要做严格二值判断,需要再阈值化一次。

4.4 现象:瞳孔被挖掉后虹膜环带断裂

原因:瞳孔圆和虹膜圆圆心不重合时,先填白再挖黑会在偏移方向留下一块空白。解决:不要用两次cv::circle,改成逐像素判断「到瞳孔圆心距离大于瞳孔半径且到虹膜圆心距离小于虹膜半径」,这样即使圆心偏移也能得到连续环带。

4.5 现象:批量处理到某张图直接崩溃

原因:circles为空时访问circles[0]越界。解决:在取圆之前加if (circles.size() < 2) return emptyMask;,返回全黑掩膜并记录文件名。血泪经验是批量脚本一定要把失败文件名写进日志,否则几千张图里崩一张,回头找都找不到。

5. 进阶技巧:用掩膜面积和圆度做自动质检

跑通分割只是第一步,真正上线要解决的是「怎么知道这张掩膜是对的」。我的习惯是在生成掩膜后加一道自动质检,用两个指标筛掉明显错误的结果:掩膜面积占比和环带圆度。

掩膜面积占比指虹膜环带像素数除以整图像素数。正视人眼里这个值通常在 0.02 到 0.15 之间,太小说明圆检小了,太大说明把眼睑也框进去了。圆度用环带外接矩形的宽高比衡量,正常虹膜接近正圆,宽高比应在 0.8 到 1.25 之间,超出说明检出的圆被拉扁了。

// 返回 true 表示掩膜通过质检 bool qualityCheck(const cv::Mat& mask) { int area = cv::countNonZero(mask); double ratio = static_cast<double>(area) / (mask.rows * mask.cols); if (ratio < 0.02 || ratio > 0.15) return false; std::vector<std::vector<cv::Point>> contours; cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); if (contours.empty()) return false; cv::Rect box = cv::boundingRect(contours[0]); double aspect = static_cast<double>(box.width) / box.height; return aspect > 0.8 && aspect < 1.25; }

逻辑说明:countNonZero统计掩膜里白色像素数,除以总像素得到占比。findContours取最外层轮廓,boundingRect拿到外接矩形,宽高比就是圆度的粗略度量。参数 0.02、0.15、0.8、1.25 都是经验阈值,换数据集要重新统计。这套质检不能保证 100% 准确,但能把明显翻车的图挡在后续流程之外,省下大量人工复核时间。

从那以后我每次做虹膜分割,都强制先跑一遍质检再进特征提取,宁可多花几毫秒,也不想让一张烂掩膜污染整个批次的统计结果。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进