
简介资源包以霍夫圆变换为核心演示了如何用Python与OpenCV对虹膜图像进行内外圆检测与识别适合计算机视觉初学者和生物识别技术爱好者用于理解圆检测原理与代码实现。压缩包共9个文件包含1个Python脚本和8张JPG图像整体仅2.38MB其中py文件实现霍夫圆检测与内外圆识别流程图像样本涵盖原始虹膜图、预处理后图像及内外圆标注结果便于对照调试。目前已有1221人学习下载。通过学习这份资源读者可以直观掌握HoughCircles函数的关键参数及其作用学会结合灰度化、边缘检测等预处理手段提高圆检测稳定性并参考内圆瞳孔边界与外圆虹膜边界的区分逻辑形成一套完整的虹膜圆检测思路。虽然包体轻量但代码结构简洁、样本配套齐全适合作为课设参考或入门练习也可以在此基础上继续扩展为更完整的虹膜识别系统。1. 霍夫圆变换检测虹膜内外圆几十行代码能跑通的事霍夫圆变换检测虹膜内外圆听起来像是生物识别论文里的工作实际上用 Python 和 OpenCV 就能在本地跑通。我第一次做这个任务是手头有一批近距离眼睛照片要做瞳孔定位不想为几十张图训练模型于是直接捡起经典图像处理里的HoughCircles。整套流程做完后发现真正影响结果的不是算法有多新而是预处理和半径参数给不给得准。这个项目把虹膜内圆瞳孔边界和外圆虹膜边界的检测脚本、示例图都打包好了适合想快速复现、理解霍夫圆在真实图像中怎么用的人。无论你是做门禁系统原型还是单纯想把眼睛图像里的圆形区域框出来这套思路都够用。2. 原理与选型为什么虹膜内外圆检测用霍夫圆而不是边缘拟合2.1 从直线到圆霍夫变换的参数空间是怎么投票的霍夫变换本质上是一个投票游戏。以直线检测为例图像空间里的每一个边缘点都可以在参数空间里画出一条对应的直线多个边缘点对应的直线会在参数空间汇聚到同一个点这个点的坐标就是原始图像里那条直线的方程参数。点汇得越多峰值越高就越说明图像里存在一条真实的直线。圆检测把参数从两个推广到三个圆心横坐标、纵坐标和半径。理论上需要在三维空间里投票计算量很大。OpenCV 里的HoughCircles采用的是霍夫梯度法它先把 Canny 边缘检测跑一遍拿到每个边缘点的梯度方向再沿着梯度方向去累加圆心候选最后对每个圆心统计满足对应距离的边缘点数量。这样一来三维投票被拆成了两步速度能压到实时附近。这里有个容易误解的地方霍夫圆检测并不是直接拿二值图上的所有白点去投票而是先用 Canny 提边缘。Canny 的参数param1会直接影响边缘图长什么样所以你在调HoughCircles的时候实际上也在间接调 Canny。很多文档只告诉你param1是 Canny 高阈值却不提醒你它的高低决定边缘密集程度这会直接影响后面的圆投票。2.2 内圆与外圆半径和位置提供的先验虹膜图像里的内圆是瞳孔边界外圆是虹膜与巩膜的交界。瞳孔通常是人眼图像里最暗、边界最锐利的区域因为瞳孔本身接近黑色在近红外或普通可见光下对比度都高。外圆则麻烦一些虹膜外边界与眼白之间的灰度差不如瞳孔那么明显而且上眼睑和下眼睑经常遮挡虹膜上下区域导致边缘弧段不完整。这两个圆的几何差异给了我们一个很实用的筛选先验内圆半径小外圆半径大在同一张距离固定的采集图中两者半径比通常落在二到四倍之间。即便一次检测出五六个候选圆只要按半径排序最小的一般是瞳孔最大的大概率是虹膜外边界。前提是你把minRadius和maxRadius限制在一个合理范围内否则最小候选可能是高光区域的小圆最大候选可能是眼皮边缘形成的大弧。另外瞳孔中心和外圆圆心在生理上并不是严格重合的有些人瞳孔天生偏内或偏下。所以不要假设它们共用一个圆心检测时要分别保存两个圆心坐标。后续做虹膜展开或特征编码时这个差异如果被忽略展开出来的环带会出现严重的纹理错位。2.3 资源包里的文件到底怎么配合用拿到资源包后先别急着跑脚本。包里原始图片.jpg和my_eye.jpg是输入图iris1内.jpg、iris1外.jpg、iris2内.jpg、iris2外.jpg是作者跑出来的检测效果图my_eye_dispose.jpg和my_eye2_dispose.jpg是预处理后的中间图主脚本是霍夫圆变换检测虹膜.py。最合理的阅读顺序是先打开效果图理解目标长什么样再看主脚本把参数与效果对应起来最后用原始图片.jpg复现一遍。文件用途原始图片.jpg / my_eye.jpg输入原图用于检测iris1内.jpg / iris1外.jpg内圆、外圆检测结果示例iris2内.jpg / iris2外.jpg第二组检测结果示例my_eye_dispose.jpg / my_eye2_dispose.jpg去噪、灰度化后的中间图霍夫圆变换检测虹膜.py完整检测主脚本我习惯先把中间图和最终检测结果并排放在一起看这样能快速判断问题出在预处理还是霍夫投票阶段。如果你发现中间图里瞳孔边缘都被磨掉了那后面再怎么调param2都是白费。3. 环境准备与图像预处理把照片喂给HoughCircles前的三个动作3.1 安装与版本pip install opencv-python 之后还要检查什么这个资源用的是 Python 加 OpenCV安装命令很简单。如果你的机器上已经装过其他版本的 OpenCV建议先确认cv2.__version__再决定要不要重新装避免依赖冲突。pip install opencv-python numpy python -c import cv2; print(cv2.__version__) python -c from cv2 import HoughCircles; print(HoughCircles)第一行安装的是 OpenCV 的 Python 包numpy是数字计算基础库HoughCircles返回的数组要依赖它做 reshape。第二行打印版本号我目前用的是 4.x3.x 在某些参数行为上有细微差异。第三行确认函数能被导入如果报ModuleNotFoundError大概率是opencv-python没装成功换成国内镜像源再装一次通常能解决。这里多说一句如果你只是跑这几张眼睛图不需要编译带 CUDA 的 OpenCV普通 pip 包在 CPU 上处理几百像素的图片已经足够。那些花时间配 Linux CUDA 版本 OpenCV 的场景是给视频流大分辨率识别的这个项目用不上。3.2 读图、灰度化、去噪把眼睛照片变成霍夫圆爱吃的样子HoughCircles要求输入单通道灰度图而且必须是 8-bit。最常见的第一步是读图后直接转灰度然后做一次中值滤波。import cv2 img cv2.imread(原始图片.jpg) if img is None: raise FileNotFoundError(检查图片路径OpenCV 不打印这个错误) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray cv2.medianBlur(gray, 5)代码里的if img is None很重要。cv2.imread读取失败时只返回None不会抛异常如果你直接往下走后面所有调用都会报出莫名其妙的AttributeError。cvtColor把三通道 BGR 转成单通道灰度medianBlur的窗口设为 5作用是去掉高光点带来的椒盐噪声同时比高斯模糊更保边缘。预处理不是越多越好。很多新手会在这一步叠加直方图均衡、形态学开闭运算结果把瞳孔边缘搞出毛刺霍夫圆反而检不准。我通常只做灰度加中值滤波如果图像对比度太低再考虑 CLAHE 局部直方图均衡但窗口参数要保守否则外圆区域会出现大量虚假边缘。3.3 图像尺寸为什么同一份代码换个分辨率就要重新设半径霍夫圆检测里的minRadius和maxRadius单位是像素这直接受图像分辨率影响。同一只眼睛在 300 像素宽的图里瞳孔半径大约 20 像素到 1200 像素宽的图里瞳孔半径可能变成 80 像素。如果你拿一份固定半径参数的代码去跑不同来源的图结果会忽好忽坏。我一般会先把图像宽度缩放到统一尺度比如 600 像素然后再定半径范围。这样参数只调一次后面所有图都按同一标准进管线。注意缩放要记录比例最后把检测到的圆心和半径乘回原图尺度不然你保存标注图时框的位置会错位。def resize_by_width(gray, target_width600): h, w gray.shape[:2] scale target_width / w resized cv2.resize(gray, (target_width, int(h * scale))) return resized, scale这个函数返回缩放后的图和缩放比例。等HoughCircles跑完把圆心坐标和半径除以scale就能还原到原图坐标。我有一次漏了这个还原直接在缩小后的图上画标注拿去报告里用事后被问“为什么框比眼睛小一圈”就是因为没处理缩放比例。4. HoughCircles实战虹膜内外圆检测脚本与参数调优4.1 一份可直接跑的检测脚本把预处理、霍夫圆检测、内外圆筛选和可视化放在一个函数里方便后续批量调用。import cv2 import numpy as np def detect_iris(path, blur5, dp1, minDist_ratio0.25, param1100, param260, minR20, maxR200): img cv2.imread(path) if img is None: raise FileNotFoundError(path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray cv2.medianBlur(gray, blur) h, w gray.shape[:2] circles cv2.HoughCircles( gray, cv2.HOUGH_GRADIENT, dpdp, minDistint(max(h, w) * minDist_ratio), param1param1, param2param2, minRadiusminR, maxRadiusmaxR ) if circles is None: return img, [] circles np.round(circles[0]).astype(int) circles sorted(circles, keylambda c: c[2]) # 按半径排序后最小的是瞳孔最大的是虹膜外边界 inner circles[0] outer circles[-1] for x, y, r in [inner, outer]: cv2.circle(img, (x, y), r, (0, 255, 0), 2) cv2.circle(img, (x, y), 2, (0, 0, 255), 3) return img, [inner, outer]代码逻辑分三段。第一段读图转灰度并中值滤波第二段调用HoughCircles把检测到的所有候选圆堆到一个列表里第三段按半径从小到大排序取最小和最大圆当作内圆和外圆并在原图上画出来。return img, [inner, outer]让调用方既能保存可视化结果又能拿到数值坐标。这段脚本的优点是把参数都暴露成了函数形参不需要每次改代码直接改调用参数即可。minDist这里用max(h, w) * 0.25表示两个圆心的最小距离相当于告诉算法不要找到一堆靠得很近的重复圆这个值对眼睛图像一般够用。4.2 六个参数逐一拆解dp、minDist、param1、param2、minRadius、maxRadius下面这个表是排查问题的核心建议收藏。参数含义对结果的影响典型范围dp累加器分辨率与图像分辨率的反比越大越快但定位越粗1~2minDist检测到的圆之间最小圆心距离太小会出大量重叠圆图像宽高乘积的 0.2~0.3 倍param1Canny 高阈值越小边缘越多圆越多80~150param2圆心投票累加器阈值越小假圆越多越大漏检越多40~80minRadius允许的最小圆半径限制瞳孔搜索范围瞳孔半径的 0.6 倍左右maxRadius允许的最大圆半径限制外圆搜索范围外圆半径的 1.2 倍左右dp我一般固定为 1含义是累加器和原图分辨率一致设成 2 会牺牲一半精度但速度接近翻倍。minDist是最容易忽略的如果设太小同一段瞳孔边缘会被投票出多个圆心相差几个像素的圆导致排序后取到的“最小圆”其实只是重复圆之一。param2则是玄学重灾区设 30 会出现十几个候选圆设 100 可能一个都检不着我通常从 60 开始一张图一张图地试。4.3 内外圆筛选策略同圆心假设与半径比约束只按半径最小和最大取圈在干净图里没问题但遇到高光、眼睑遮挡或图像质量差时会翻车。更稳的做法是加两个约束内圆半径与外圆半径的比例要落在 1.5 到 4 之间内圆圆心和外圆圆心的距离不能超过内圆半径的一半。def pick_inner_outer(circles): if len(circles) 2: return None, None # 首先按圆心距离聚类同簇内选半径最小和最大 candidates [] for c in circles: for d in candidates: dx c[0] - d[0] dy c[1] - d[1] if abs(dx) 20 and abs(dy) 20: d.append(c) break else: candidates.append([c]) best None for group in candidates: if len(group) 2: continue inner min(group, keylambda c: c[2]) outer max(group, keylambda c: c[2]) if 1.5 outer[2] / inner[2] 4.0: best (inner, outer) break return best这段代码先把圆心距离相近的圆归到同一个组相当于认为同一只眼睛检测出的多个候选圆圆心应该接近。然后在每组内部取最小和最大圆作为内外圆候选最后用半径比过滤掉比例不合理的结果。这样做比单纯全局排序稳定得多因为眼角的夹缝、眼白的弧线也可能检测出与大圆同半径的干扰圆但在同圆心约束下它们很难和瞳孔组成合理组合。5. 避坑与排查霍夫圆在眼睛照片上的五个翻车现场5.1 瞳孔高光被误检成小圆现象一张图里检测出三四个小圆其中最小圆不是瞳孔而是瞳孔高光区域半径只有五六个像素。原因瞳孔中心常有光源反射产生的高光斑高光边缘对比度极高Canny 会把它当独立边缘霍夫投票在小范围内形成一个假峰值。解决中值滤波窗口从 5 提到 9把高光斑磨平一些同时把minRadius抬高到瞳孔真实半径的一半以上。如果瞳孔在图像里至少 20 像素minRadius直接设 15就不会去理会那些 5 像素的小圆。5.2 外圆被眼睑和睫毛带着跑现象外圆半径明显偏大圆的上半部分贴住上眼皮边缘或者跟睫毛弧线重合看起来不像虹膜边界。原因虹膜外边界在上下方向被眼睑遮挡Canny 在眼皮边缘会提取出一条弧段霍夫投票时这条长弧比不完整的虹膜边界更容易凑出一个大圆。瞳孔又黑又圆所以内圆没问题外圆却常年翻车。解决收紧maxRadius不让它去够眼皮位置的大弧更有效的方式是先把边缘图上下区域抑制掉只保留左右两侧的虹膜边界参与投票。我写了一个按行加权的 mask把图像上下各 25% 区域的边缘像素权重降低然后再跑HoughCircles外圆稳定很多。5.3 图像缩放后结果跳变现象同一张图600 像素宽时检测正常改成 900 像素宽后内圆忽大忽小甚至检测出三四个圆。原因minRadius、maxRadius和minDist都是像素绝对值图像放大后半径期望值变了但参数没跟着变。param2累加器阈值对边缘密度敏感缩放后边缘数量变化原本合适的阈值可能累积出大量假圆心。解决先固定输入图像宽度把所有图都缩放到 600 像素再跑检测。记录缩放比例检测完成后把圆心和半径还原到原始分辨率。如果数据集本身就来自同一设备分辨率固定那就没必要缩放直接标定一次minR/maxR即可。5.4 HoughCircles 报错和中文路径问题现象调用HoughCircles时出现error: (-215:Assertion failed) image.depth() CV_8U或者cv2.imread返回None代码后面一访问gray.shape就崩。原因报错是因为传入的不是 8-bit 灰度图常见情况是忘了cvtColor把三通道 BGR 图直接丢了进去。返回None则多半是路径里有中文Windows 上 OpenCV 的imread对非 ASCII 路径支持很差。解决先if img is None兜底中文路径用下面这个替代函数读取def imread_chinese(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)np.fromfile按原始字节读入imdecode再解码成图像绕开imread对中文路径的坑。这两个问题只要按这个顺序处理基本不会再看到奇怪的底层报错。5.5 内外圆圆心不重合导致展开错位现象检测出的内圆和外圆圆心差出十几个像素如果后面做环带展开时强制用同一个圆心展开图里的纹理条带斜着走边缘模糊。原因瞳孔中心和虹膜中心本身就存在生理偏差加上拍摄角度略有倾斜后会放大这个偏差。同一个圆心假设在这类数据上不成立。解决把内外圆当作两个独立对象分别保存各自的圆心。展开环带时以内圆圆心作为极坐标原点外圆半径作为最大采样半径这样瞳孔边界在展开图上是平整的外圆边界就算有些许偏差也不会让整条纹理带扭曲。6. 进阶环带展开与批量验证把检测结果真正用起来6.1 把虹膜环带展开成矩形画圈只是第一步虹膜识别或纹理分析通常需要把环形区域展开成矩形方便后续做特征提取。这段代码用极坐标采样把内外圆之间的像素搬进一个 64 行 256 列的矩阵。def unwrap_iris(img, inner, outer, height64, width256): icx, icy, r_in inner _, _, r_out outer theta np.linspace(0, 2 * np.pi, width, endpointFalse) radius np.linspace(r_in, r_out, height) t theta.reshape(1, -1) r radius.reshape(-1, 1) map_x (icx r * np.cos(t)).astype(np.float32) map_y (icy r * np.sin(t)).astype(np.float32) unwrapped cv2.remap(img, map_x, map_y, cv2.INTER_LINEAR) return unwrapped这里把内圆圆心作为极坐标原点theta是 360 度方向的采样角radius从内圆半径均匀走到外圆半径。cv2.remap会根据映射坐标把原图像素搬到新位置展开后环带变成矩形竖方向是径向横方向是角度。如果展开图纹理斜向跑回去检查内外圆圆心偏差是否过大。6.2 批量跑图把检测结果写进 CSV到了验证阶段一张张保存图片效率太低我会把结果汇总到 CSV 里顺带保存标注图。import csv import glob import os results [] for path in glob.glob(images/*.jpg): try: img, circles detect_iris(path) if circles: inner, outer circles results.append([path, *inner, *outer]) cv2.imwrite(out/ os.path.basename(path), img) else: results.append([path, no_circles]) except Exception as e: results.append([path, error, str(e)]) with open(result.csv, w, newline, encodingutf-8) as fp: writer csv.writer(fp) writer.writerow([file, inner_x, inner_y, inner_r, outer_x, outer_y, outer_r]) writer.writerows(results)这段代码遍历images目录下的全部 JPG 文件逐个调用检测函数把内圆三个参数和外圆三个参数写入 CSV同时把画好圆的结果图存到out目录。遇到读图失败或检测不到圆的样本也会记录状态这样批量结束后能一眼看出哪些图需要特殊处理。我自己的习惯是每次拿到新的眼睛图像集先把其中三张图拖进脚本里输出中间过程图确认边缘干净后再去动霍夫圆参数而不是上来就玄学调参。这个习惯救了我好几次每次新数据都强制走一遍“原图、灰度图、边缘图、检测结果图”四张图的对照流程元凶基本都是预处理或半径范围。希望帮到你。本文还有配套的精品资源点击获取