虹膜分割:從霍夫圓檢測到掩膜生成與參數(shù)調(diào)優(yōu))
簡介這份資源聚焦虹膜分割與虹膜提取的工程實現(xiàn)面向具備一定圖像處理基礎(chǔ)、希望用C與OpenCV落地生物特征識別項目的開發(fā)者與學(xué)習(xí)者。包內(nèi)提供可直接編譯運行的Visual Studio工程核心代碼以cpp源文件形式給出并配有測試圖片與一份方法講解PPT便于對照理解分割流程與算法思路。資源共39個文件包含10張bmp與10張jpg測試圖像、2個cpp實現(xiàn)文件以及sln、vcproj、exe、pdb等工程與調(diào)試文件另有pdf說明文檔壓縮包約3.26MB結(jié)構(gòu)完整、開箱即用。目前已有1124人學(xué)習(xí)下載適合作為課程設(shè)計、畢業(yè)設(shè)計或虹膜識別入門階段的參考實現(xiàn)讀者可借此快速搭建實驗環(huán)境觀察分割效果并在此基礎(chǔ)上做算法改進與二次開發(fā)。1. 虹膜分割到底在分什么從一張人眼圖到可用的掩膜很多人第一次拿到虹膜分割任務(wù)以為就是把瞳孔那一圈摳出來。真上手才發(fā)現(xiàn)一張人眼圖里要分的東西至少有三層瞳孔、虹膜環(huán)帶、以及把這兩者包住的眼瞼和睫毛干擾區(qū)。虹膜分割代碼要輸出的不是一張好看的圖而是一張二值掩膜告訴后續(xù)特征提取模塊「哪些像素屬于虹膜、哪些是噪聲」。這份 C 和 OpenCV 實現(xiàn)的資源核心價值就在于把這條鏈路用原生代碼跑通不依賴深度學(xué)習(xí)框架編譯完就能在本地對單張圖或批量圖做虹膜提取。它適合兩類人一類是做生物特征識別、想理解傳統(tǒng)圖像處理流程的從業(yè)者另一類是需要把虹膜定位嵌進 C 工程、又不想引入龐大推理依賴的開發(fā)者。整份代碼圍繞灰度化、邊緣檢測、霍夫圓變換、掩膜生成幾個環(huán)節(jié)展開參數(shù)都暴露在源碼里改起來直觀。下面按「先跑通、再調(diào)參、最后避坑」的順序拆。2. 環(huán)境搭建與第一張掩膜把 C 工程跑起來2.1 依賴選型為什么是 OpenCV 而不是別的虹膜分割的經(jīng)典路線是「邊緣 圓擬合」這條路線上 OpenCV 提供的HoughCircles、Canny、GaussianBlur幾乎覆蓋了全部需求。選 OpenCV 而不是自己寫卷積理由很實際霍夫圓變換內(nèi)部涉及累加器空間和半徑搜索手寫一版能跑但調(diào)參成本極高而 OpenCV 的實現(xiàn)已經(jīng)做了非極大值抑制和梯度篩選參數(shù)語義清晰。C 版本相比 Python 的優(yōu)勢在于批量處理上千張圖時沒有解釋器開銷且能直接編進已有的桌面或嵌入式工程。常見做法是用 CMake 管理工程把 OpenCV 作為外部依賴鏈接進來。我一般會先確認本機 OpenCV 版本因為 4.x 和 3.x 在HoughCircles的參數(shù)默認值上有細微差別尤其是param2的敏感度。# 查看本機 OpenCV 版本決定后續(xù)鏈接的庫名 pkg-config --modversion opencv4 # 若輸出 4.x則 CMake 里用 find_package(OpenCV 4 REQUIRED)這段命令的作用是確認版本號。如果輸出為空說明pkg-config路徑?jīng)]配好需要手動指定OpenCV_DIR。參數(shù)上沒什么可調(diào)的純粹是環(huán)境探測。2.2 編譯與運行從源碼到可執(zhí)行文件工程結(jié)構(gòu)通常是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把自定義頭文件目錄加進去避免源碼里寫一長串相對路徑。參數(shù)上REQUIRED保證找不到 OpenCV 時直接報錯而不是靜默跳過。編譯命令就三步mkdir build cd build cmake .. make -j4-j4是按四核并行編譯機器核多可以改大。編譯完成后build/下會生成iris_seg可執(zhí)行文件運行時傳入圖片路徑即可。2.3 第一張掩膜的生成流程主流程分四步讀圖轉(zhuǎn)灰度、高斯模糊降噪、霍夫圓檢測定位瞳孔和虹膜外邊界、根據(jù)兩個圓生成環(huán)帶掩膜。下面這段是核心函數(shù)的骨架。// 輸入原始 BGR 圖輸出虹膜環(huán)帶掩膜 cv::Mat segmentIris(const cv::Mat src) { cv::Mat gray, blurred; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); // 高斯核 5x5sigma 由 OpenCV 自動推算先壓掉睫毛高頻噪聲 cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0); std::vectorcv::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) { // 半徑大的當(dāng)虹膜外邊界小的當(dāng)瞳孔邊界 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; }邏輯說明先轉(zhuǎn)灰度是因為霍夫圓只吃單通道。高斯模糊的核大小 5x5 是經(jīng)驗值太大虹膜邊緣會糊太小睫毛噪聲壓不住。HoughCircles里dp1表示累加器分辨率與原圖一致minDist設(shè)為行高的八分之一防止同一個圓被檢出多次。param1200是 Canny 高閾值param250是累加器閾值這兩個是調(diào)參重點。最后用「先填白再挖黑」的方式生成環(huán)帶比逐像素判斷半徑更省事。參數(shù)怎么改如果檢不出圓先把param2降到 30 左右如果檢出太多假圓把param2提到 70 以上。minDist太小會導(dǎo)致瞳孔和虹膜被當(dāng)成同一個圓太大則可能漏掉半徑接近的雙圓。3. 參數(shù)調(diào)優(yōu)與魯棒性讓掩膜在臟圖上也能用3.1 霍夫圓參數(shù)的實際影響HoughCircles的五個關(guān)鍵參數(shù)里dp、minDist、param1、param2、minRadius/maxRadius每一個都會顯著改變結(jié)果。下面這張表是我在幾十張不同質(zhì)量人眼圖上試出來的經(jīng)驗區(qū)間不是理論值是踩坑踩出來的。參數(shù)作用保守取值激進取值翻車表現(xiàn)dp累加器分辨率12取 2 時小瞳孔直接漏檢minDist圓心最小間距rows/8rows/4太小雙圓合并太大漏檢param1Canny 高閾值200100太低邊緣碎圓擬合發(fā)散param2累加器閾值5030太低滿屏假圓minRadius最小半徑2010太小把噪點當(dāng)瞳孔調(diào)參順序建議先固定dp1、minDistrows/8然后調(diào)param2讓圓的數(shù)量穩(wěn)定在 2 到 4 個最后用minRadius/maxRadius把明顯不合理的圓篩掉。param1一般不動除非圖像對比度特別低。3.2 預(yù)處理對分割結(jié)果的影響原始人眼圖里睫毛、反光、眼瞼陰影都是干擾源。直接上霍夫圓十張里能有三張檢出正確雙圓就不錯了。常見做法是在灰度化之后加一步形態(tài)學(xué)開運算把細長的睫毛去掉同時保留虹膜邊緣。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 是為了讓睫毛這種細結(jié)構(gòu)在模糊階段就衰減掉。開運算的核不能大3x3 足夠再大虹膜外邊緣會被啃掉一圈導(dǎo)致半徑偏小。這一步的代價是瞳孔邊緣也會略微變鈍所以后續(xù)param2要相應(yīng)降低 5 到 10。另一個常見干擾是紅外圖里的高光斑。高光在灰度圖上表現(xiàn)為一塊高亮區(qū)域霍夫圓會把它的邊緣當(dāng)成圓弧。處理辦法是在開運算之后加一步閾值截斷把超過 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; }邏輯說明這個函數(shù)在候選圓列表里兩兩配對只保留滿足幾何約束的組合。如果沒有任何一對通過說明這張圖質(zhì)量太差應(yīng)該跳過而不是硬分割。參數(shù) 0.3 和 0.6 是統(tǒng)計值遇到側(cè)視或斜視人眼可以放寬到 0.25 和 0.7但放寬后假陽性會上升需要配合后續(xù)掩膜面積過濾。4. 避坑與排查五個真實翻車記錄4.1 現(xiàn)象編譯通過但運行時報缺少 opencv_world 庫原因CMake 鏈接的是模塊化庫但運行時動態(tài)鏈接器找不到.so路徑。解決在~/.bashrc里把 OpenCV 的lib目錄加進LD_LIBRARY_PATH或者編譯時加-Wl,-rpath把路徑寫死進可執(zhí)行文件。我一般用后者省得換終端就失效。4.2 現(xiàn)象同一張圖兩次運行結(jié)果不一樣原因HoughCircles內(nèi)部有并行累加多線程下浮點累加順序不同會導(dǎo)致結(jié)果微抖。解決在調(diào)用前設(shè)cv::setNumThreads(1)犧牲一點速度換確定性。批量處理時這個設(shè)置尤其重要否則日志對不上。4.3 現(xiàn)象掩膜邊緣有一圈鋸齒原因cv::circle畫圓時默認用 8 鄰域連接半徑大時邊緣呈階梯狀。解決把cv::circle的最后一個參數(shù)lineType設(shè)為cv::LINE_AA開啟抗鋸齒。代價是掩膜邊緣會有灰度過渡值后續(xù)如果要做嚴格二值判斷需要再閾值化一次。4.4 現(xiàn)象瞳孔被挖掉后虹膜環(huán)帶斷裂原因瞳孔圓和虹膜圓圓心不重合時先填白再挖黑會在偏移方向留下一塊空白。解決不要用兩次cv::circle改成逐像素判斷「到瞳孔圓心距離大于瞳孔半徑且到虹膜圓心距離小于虹膜半徑」這樣即使圓心偏移也能得到連續(xù)環(huán)帶。4.5 現(xiàn)象批量處理到某張圖直接崩潰原因circles為空時訪問circles[0]越界。解決在取圓之前加if (circles.size() 2) return emptyMask;返回全黑掩膜并記錄文件名。血淚經(jīng)驗是批量腳本一定要把失敗文件名寫進日志否則幾千張圖里崩一張回頭找都找不到。5. 進階技巧用掩膜面積和圓度做自動質(zhì)檢跑通分割只是第一步真正上線要解決的是「怎么知道這張掩膜是對的」。我的習(xí)慣是在生成掩膜后加一道自動質(zhì)檢用兩個指標篩掉明顯錯誤的結(jié)果掩膜面積占比和環(huán)帶圓度。掩膜面積占比指虹膜環(huán)帶像素數(shù)除以整圖像素數(shù)。正視人眼里這個值通常在 0.02 到 0.15 之間太小說明圓檢小了太大說明把眼瞼也框進去了。圓度用環(huán)帶外接矩形的寬高比衡量正常虹膜接近正圓寬高比應(yīng)在 0.8 到 1.25 之間超出說明檢出的圓被拉扁了。// 返回 true 表示掩膜通過質(zhì)檢 bool qualityCheck(const cv::Mat mask) { int area cv::countNonZero(mask); double ratio static_castdouble(area) / (mask.rows * mask.cols); if (ratio 0.02 || ratio 0.15) return false; std::vectorstd::vectorcv::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_castdouble(box.width) / box.height; return aspect 0.8 aspect 1.25; }邏輯說明countNonZero統(tǒng)計掩膜里白色像素數(shù)除以總像素得到占比。findContours取最外層輪廓boundingRect拿到外接矩形寬高比就是圓度的粗略度量。參數(shù) 0.02、0.15、0.8、1.25 都是經(jīng)驗閾值換數(shù)據(jù)集要重新統(tǒng)計。這套質(zhì)檢不能保證 100% 準確但能把明顯翻車的圖擋在后續(xù)流程之外省下大量人工復(fù)核時間。從那以后我每次做虹膜分割都強制先跑一遍質(zhì)檢再進特征提取寧可多花幾毫秒也不想讓一張爛掩膜污染整個批次的統(tǒng)計結(jié)果。希望幫到你。本文還有配套的精品資源點擊獲取