:邊緣檢測、形態(tài)學(xué)與模板匹配實(shí)戰(zhàn))
簡介一套完整的基于OpenCV的銀行卡識別系統(tǒng)源碼包面向計(jì)算機(jī)視覺學(xué)習(xí)者、金融科技開發(fā)者及高校相關(guān)課題研究。該方案融合OpenCV圖像處理與機(jī)器學(xué)習(xí)技術(shù)覆蓋銀行卡圖像預(yù)處理、邊緣檢測、二值化、字符定位與識別等完整流程可直接運(yùn)行調(diào)試并配有設(shè)計(jì)報(bào)告與演示PPT便于理解系統(tǒng)架構(gòu)和復(fù)現(xiàn)實(shí)驗(yàn)。資源共43個文件以10個Python腳本為核心輔以16張JPEG和7張JPG測試圖片、項(xiàng)目說明文檔、演示PPT、配置文件及前端頁面資源壓縮包大小10.31MBPython腳本覆蓋界面啟動、目標(biāo)檢測與字符識別主流程設(shè)計(jì)報(bào)告則對算法原理、參數(shù)調(diào)優(yōu)和測試結(jié)果進(jìn)行了詳細(xì)說明。目前已有98人學(xué)習(xí)下載適合用于課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)或作為OCR識別項(xiàng)目的入門參考通過閱讀源碼與報(bào)告可掌握OpenCV在金融場景下的實(shí)戰(zhàn)用法及字符識別模型的調(diào)用與優(yōu)化思路。1. 基于 OpenCV 的銀行卡識別系統(tǒng)先把「找卡號」當(dāng)主戰(zhàn)場識別數(shù)字只是收尾很多人第一次接觸這個項(xiàng)目以為難點(diǎn)在「識別數(shù)字」于是上來就研究 OCR。實(shí)際上做過一遍的人都知道翻車的全是前半段——卡片沒擺正、反光區(qū)域把邊緣檢測攪亂、卡號區(qū)域被誤判成信用卡 logo 或者一大塊底色。所謂基于 OpenCV 的銀行卡識別系統(tǒng)本質(zhì)是一條傳統(tǒng)圖像處理流水線讀圖 → 灰度化 → 邊緣檢測 → 形態(tài)學(xué)閉運(yùn)算 → 輪廓篩選 → 定位卡號 ROI → 數(shù)字分割 → 模板匹配。整個鏈路不用深度學(xué)習(xí)模型每一步的中間結(jié)果都能可視化這對課程設(shè)計(jì)和畢設(shè)來說非常友好因?yàn)槟憧梢阅弥虚g結(jié)果圖寫進(jìn)設(shè)計(jì)報(bào)告里講清楚每一層為什么這么做。適合有 Python 基礎(chǔ)、想入門 OpenCV 圖像處理、或者正在做模式識別課程設(shè)計(jì)的開發(fā)者。2. 識別鏈路拆解從預(yù)處理到模板匹配先把每一步的中間結(jié)果看懂2.1 為什么第一步不是 OCR而是灰度化 邊緣檢測OCR 解決的問題是「把圖片里的文字變成字符串」但它天生不擅長處理「文字在哪兒」。一張真實(shí)拍攝的銀行卡照片里有卡面背景圖案、銀行 logo、卡號凸字、有效期、持卡人姓名甚至還有持卡人手指和桌面紋理。如果直接把整張圖丟給 OCR它會返回一堆亂七八糟的文字。所以 OpenCV 方案的第一件事不是識別而是把「卡號區(qū)域」從畫面里切出來。常見做法是先灰度化再用高斯模糊降噪最后用 Canny 做邊緣檢測。高斯模糊的核大小一般取 (5, 5)太小壓不住傳感器噪聲太大會把數(shù)字邊緣也磨平。Canny 的雙閾值建議設(shè)成 (100, 200)這個區(qū)間對銀行卡這種高對比度物體比較穩(wěn)。做完這一步你會得到一張黑白邊緣圖卡片的邊框、卡號凸字、卡面圖案的邊緣都會變成白色線條。注意這里不要急著找輪廓因?yàn)榭ㄌ柕囊淮當(dāng)?shù)字是分散的邊緣圖里的它們是一堆斷開的白色小段下一步得先用形態(tài)學(xué)把它們連成一個整體。import cv2 import numpy as np img cv2.imread(card.jpg) # 讀入銀行卡照片 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 轉(zhuǎn)灰度后續(xù)所有操作都基于單通道 blur cv2.GaussianBlur(gray, (5, 5), 0) # 高斯模糊抑制 CMOS 傳感器噪聲 edges cv2.Canny(blur, 100, 200) # Canny 邊緣檢測雙閾值 100/200 cv2.imwrite(edges.png, edges) # 保存中間結(jié)果方便排查這段代碼里GaussianBlur的第三個參數(shù)是高斯核標(biāo)準(zhǔn)差填 0 表示由核大小自動計(jì)算。Canny的第二個和第三個參數(shù)分別是低閾值和高閾值梯度幅值高于 200 的像素必保留低于 100 的必丟棄介于中間的看是否與強(qiáng)邊緣相連。我建議每次調(diào)參都把edges.png保存下來看一眼邊緣圖如果全是麻點(diǎn)就把高斯核加大到 (7, 7)如果卡號輪廓斷得太碎就把高閾值降到 150。2.2 輪廓檢測找卡號區(qū)域先用形態(tài)學(xué)把斷開的邊緣連起來邊緣檢測做完卡號區(qū)域是一堆斷開的白線直接findContours會找出幾十個碎輪廓。這時(shí)候需要形態(tài)學(xué)閉運(yùn)算。閉運(yùn)算 先膨脹后腐蝕作用是填補(bǔ)小孔、連接相鄰邊緣。對銀行卡卡號場景膨脹核要設(shè)計(jì)成「水平長條」因?yàn)榭ㄌ枖?shù)字是橫向排列的水平方向需要連接垂直方向則要克制不然相鄰兩行字會被連成一片。核的形狀用cv2.getStructuringElement生成常見配置是MORPH_RECT尺寸 (15, 5)。對于 720p 級別的輸入圖這個尺寸能在水平方向把相鄰數(shù)字邊緣搭在一起同時(shí)垂直方向留出間隔。做完閉運(yùn)算后再用findContours卡號區(qū)域應(yīng)該變成一個或兩個完整的大輪廓銀行 logo 之類的小圖案因?yàn)槊娣e不夠會被過濾掉。kernel cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5)) closed cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) roi None for c in contours: x, y, w, h cv2.boundingRect(c) area w * h ratio w / h if area 8000 and 4.0 ratio 10.0: roi gray[y:y h, x:x w] cv2.rectangle(img, (x, y), (x w, y h), (0, 0, 255), 2) break cv2.imwrite(roi.png, img)這段代碼有兩個關(guān)鍵參數(shù)需要按實(shí)際照片分辨率調(diào)area 8000是對 720p 圖片設(shè)置的面積下限如果你的圖是 1080p面積閾值要放大到 15000 以上否則卡號區(qū)域輪廓會被過濾掉ratio是寬高比卡號區(qū)域是典型的「矮胖條」比銀行卡主體更扁同時(shí)不會扁到像一條線所以 4.0 到 10.0 是一個比較穩(wěn)的起點(diǎn)。如果roi一直為空把你保存的closed.png打開看大概率是核寬度不夠?qū)е螺喞獩]連起來。這里我用的是RETR_EXTERNAL只取最外層輪廓避免卡號里的數(shù)字凹槽產(chǎn)生內(nèi)部輪廓干擾篩選。2.3 數(shù)字分割與模板匹配識別放在最后一步控制變量才能排錯拿到卡號 ROI 之后接下來做數(shù)字分割。最簡單可靠的方法是二值化后再次找輪廓然后用x坐標(biāo)排序。為什么要排序因?yàn)閒indContours返回的輪廓順序不代表從左到右它按層級和檢索順序輸出不排序的話識別出來的卡號會是亂的。排序后還要做寬度過濾把誤檢的小噪點(diǎn)輪廓去掉。識別這一步很多畢設(shè)和課程設(shè)計(jì)選模板匹配而不是 Tesseract原因很實(shí)際銀行卡卡號只有 0-9 十個數(shù)字準(zhǔn)備十張標(biāo)準(zhǔn)數(shù)字模板非常容易模板匹配對光照變化的容忍度也夠用而且不需要額外安裝 OCR 引擎環(huán)境依賴小。設(shè)計(jì)報(bào)告里畫流程圖也方便每一層都看得見。模板匹配的做法是把每個數(shù)字 ROI 縮放到模板大小然后用TM_CCOEFF_NORMED計(jì)算相似度取最大值對應(yīng)的數(shù)字為結(jié)果。def match_digit(roi_gray, templates): best_num, best_val -1, -1.0 roi_resized cv2.resize(roi_gray, (templates[0].shape[1], templates[0].shape[0])) for num, tpl in enumerate(templates): res cv2.matchTemplate(roi_resized, tpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(res) if max_val best_val: best_val, best_num max_val, num return best_num, best_valTM_CCOEFF_NORMED是歸一化相關(guān)系數(shù)輸出范圍理論上在 -1 到 1 之間越大越相似。它比TM_SQDIFF更魯棒因?yàn)閷φw亮度偏移不敏感銀行卡照片的光照不均勻正好是它的主要場景。模板匹配的坑在于模板本身的質(zhì)量常見做法是自己從一張清晰的卡號截圖上把每個數(shù)字摳下來存成 20 × 40 左右的灰度圖。摳模板時(shí)有兩條建議一是模板字體盡量和識別對象一致銀行卡上的凸字是等寬字體不要用手寫體或者襯線字體做模板二是模板背景必須是純色最好做一次二值化再存否則匹配分?jǐn)?shù)會被背景紋理拖低。3. 拿到 zip 源碼之后的復(fù)現(xiàn)路徑環(huán)境準(zhǔn)備、目錄排查與最小運(yùn)行3.1 環(huán)境準(zhǔn)備Python、OpenCV、numpy 的版本組合這個項(xiàng)目寫進(jìn)設(shè)計(jì)報(bào)告的時(shí)候一般不會把環(huán)境配置寫得很細(xì)但你在自己電腦上跑的時(shí)候環(huán)境恰恰是第一個攔路虎。最常見的報(bào)錯是ModuleNotFoundError: No module named cv2原因是 pip 安裝包的名字是opencv-python而導(dǎo)入語句寫的是import cv2兩者不一致。還有一類情況是系統(tǒng)里同時(shí)裝過 OpenCV C 版本但 Python 環(huán)境里看不到因?yàn)?C 版本不會自動供 Python 調(diào)用。我個人的建議是新建一個干凈的虛擬環(huán)境不要往系統(tǒng) Python 里直接裝不然以后裝 torch 或者 ddddocr 的時(shí)候依賴版本互相打架很頭痛。Python 版本選 3.8 到 3.10 比較穩(wěn)OpenCV 用 4.x 系列numpy 用 1.24.x 左右。這里有個隱藏的坑新版 numpy 2.x 對 OpenCV 4.x 的某些接口不兼容如果你裝的是最新 numpy可能會出現(xiàn)莫名其妙的TypeError或者CvTypeError遇到這種問題先看版本不要先懷疑算法代碼。python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate python -m pip install --upgrade pip python -m pip install opencv-python numpy1.24.4 python -c import cv2; print(cv2.__version__)最后一行命令是驗(yàn)證環(huán)境是否裝通能打印出版本號說明導(dǎo)入成功。注意opencv-python這個包只包含主模塊如果你需要cv2.xfeatures2d這類擴(kuò)展模塊得裝opencv-contrib-python但銀行卡識別用不到別多裝。如果你用的是 conda 環(huán)境conda install opencv會同時(shí)拉起來一堆依賴我反而推薦 miniconda 創(chuàng)建環(huán)境后仍然用 pip 安裝 opencv-python這樣最可控。3.2 解壓后先看什么源碼目錄與設(shè)計(jì)報(bào)告的對應(yīng)關(guān)系zip 打開之后不要急著跑代碼先按目錄結(jié)構(gòu)理清文件職責(zé)。比較規(guī)范的壓縮包里通常會有這么幾類東西一個或多個.py文件這是主程序和模塊一個template或templates文件夾放著 0-9 的數(shù)字模板圖片若干測試圖片以及設(shè)計(jì)報(bào)告文檔Word 或 PDF。如果你看到的入口文件叫main.py直接用 python 跑就行如果叫card_ocr.py或者bankcard.py找文件末尾有沒有if __name__ __main__段這段就是入口。先打開 Python 文件按函數(shù)名快速掃一遍。我一般會先找五個關(guān)鍵函數(shù)圖像讀取、預(yù)處理、輪廓定位、數(shù)字分割、模板匹配。有的源碼會把模板匹配寫成一個大函數(shù)這不影響閱讀但你得確認(rèn)它內(nèi)部是否調(diào)用了cv2.imread去讀模板文件如果是模板路徑是相對路徑還是絕對路徑?jīng)Q定了你解壓后能不能直接跑起來。設(shè)計(jì)報(bào)告里通常有系統(tǒng)結(jié)構(gòu)圖和流程圖先看流程圖對照代碼能少走很多彎路畢竟課程設(shè)計(jì)報(bào)告里的流程圖往往是作者對代碼結(jié)構(gòu)的真實(shí)映射。# 這段代碼說明源碼入口常見結(jié)構(gòu) import argparse def main(): parser argparse.ArgumentParser(descriptionBank Card Recognition) parser.add_argument(--image, requiredTrue, helppath to input card image) parser.add_argument(--templates, default./templates, helptemplate dir) args parser.parse_args() result recognize(args.image, args.templates) # 核心識別函數(shù) print(card number:, result) if __name__ __main__: main()這個入口函數(shù)用了argparse接收命令行參數(shù)。為什么要確認(rèn)入口結(jié)構(gòu)因?yàn)橛械脑创a把圖像路徑硬編碼在文件里你換了圖片就得改代碼跑起來很別扭??吹絽?shù)解析結(jié)構(gòu)你就可以在命令行里傳不同的圖片而不用動源碼這在嘗試多張卡面時(shí)非常重要。3.3 最小運(yùn)行命令入口腳本與參數(shù)解析環(huán)境裝好、目錄理順后運(yùn)行就簡單了。假設(shè)源碼入口是main.py模板目錄是./templates測試圖是./imgs/test1.jpg最小運(yùn)行命令如下python main.py --image ./imgs/test1.jpg --templates ./templates跑完之后程序通常會在終端打印識別出的卡號同時(shí)把定位標(biāo)記圖保存成文件比如result.jpg。如果程序運(yùn)行沒有任何報(bào)錯但輸出結(jié)果是空的或者全-1說明圖片進(jìn)到了識別流程但模板匹配階段沒有拿到合格的置信度問題大概率出在模板和 ROI 的預(yù)處理不一致上比如模板沒有二值化而 ROI 是灰度圖或者兩者長寬比差異太大被 resize 拉伸變形了。還有一種情況是你拿到的源碼入口不接受參數(shù)直接硬編碼了路徑。這時(shí)候不要急著改代碼結(jié)構(gòu)先看開頭的幾個路徑變量把它們改成你本機(jī)的相對路徑注意 Python 文件的工作目錄是運(yùn)行命令時(shí)所在的目錄不是文件所在目錄。換句話說你在venv里運(yùn)行python /home/user/card_src/main.py程序里的./templates指的是當(dāng)時(shí)終端所在的目錄不是card_src目錄。這個細(xì)節(jié)我踩過無數(shù)次建議直接在源碼里用os.path.join(os.path.dirname(__file__), templates)來拼模板路徑一勞永逸。4. 讓識別率從「能跑」變成「能用」的 5 個必調(diào)參數(shù)4.1 形態(tài)學(xué) kernel 尺寸水平連字符別讓它連成一片閉運(yùn)算的 kernel 尺寸直接決定卡號區(qū)域定位成敗。核太短數(shù)字邊緣連不起來輪廓就是碎的一百多塊面積和寬高比過濾根本找不到目標(biāo)核太長兩行數(shù)字或者卡號和旁邊文字被連成一個整體定位框把整個卡片下半部分框進(jìn)去。對于 720p 輸入圖(15, 5) 是我比較常用的起點(diǎn)如果卡號區(qū)域定位框偏窄裁掉了一兩個數(shù)字說明水平方向閉運(yùn)算沒連夠把核寬加到 20如果定位框把相鄰的卡面機(jī)構(gòu)名稱也框進(jìn)來了就縮小到 10。還有一個很容易忽略的點(diǎn)kernel 的垂直尺寸。銀行卡卡號兩行之間間距不大如果垂直尺寸設(shè)置成 10 以上兩行數(shù)字會被連成一個大塊寬高比瞬間失真。所以垂直方向控制在 3 到 5 比較安全。4.2 輪廓面積閾值與寬高比控制抓取的是整個卡片還是卡號區(qū)域源碼里通常有兩個閾值在起作用最小輪廓面積和寬高比。面積閾值太低桌面的紋理邊緣、銀行卡上的 logo 都會被當(dāng)作候選后面模板匹配浪費(fèi)在錯誤 ROI 上閾值太高如果攝像頭離得遠(yuǎn)導(dǎo)致卡號區(qū)域像素面積小就會被直接過濾掉。寬高比的設(shè)定取決于你要定位什么定位整張卡則寬高比在 1.5 附近定位卡號區(qū)域則在 4.0 到 10.0 之間。我見過不少源碼把兩個閾值直接寫死在代碼里沒有參數(shù)注釋。建議你在調(diào)試時(shí)把這兩個值提升為命令行參數(shù)或者至少寫成文件頂部的常量并加上注釋說明「這個值是在什么分辨率下調(diào)出來的」。不然你換一張 1080p 的測試圖所有輪廓面積都會放大快三倍固定閾值直接失效。4.3 模板匹配的置信度閾值不是 max 值大于 0.8 就萬事大吉cv2.matchTemplate返回的匹配分?jǐn)?shù)只是一個相對相似度TM_CCOEFF_NORMED在理想情況下接近 1但因?yàn)殂y行卡凸字有立體陰影、照片有反光實(shí)際能到 0.7 以上就算不錯了。很多人在源碼里看到if max_val 0.8這樣的閾值就以為低于 0.8 的識別都是失敗的這往往會漏掉大量本來可以正確識別的數(shù)字。更好的做法是不設(shè)絕對閾值而是記錄整張卡號序列的平均置信度最后判斷平均值比如低于 0.5 時(shí)輸出「卡片可能識別失敗」。你也可以把每個數(shù)字的置信度打印出來如果你發(fā)現(xiàn)某個位置的數(shù)字總是被分成兩個輪廓說明分割階段出了問題而不是匹配閾值的問題。調(diào)閾值永遠(yuǎn)先看單數(shù)字的分布不要拍腦袋設(shè) 0.8。4.4 數(shù)字排序x 坐標(biāo)排序 vs 行排序分割出的數(shù)字輪廓必須排序后再拼接否則輸出的卡號順序是亂的。銀行卡卡號有兩種常見形式一種是單行排列比如 16 位卡號印成一行另一種是兩行前 8 位一行后 8 位一行。單行排序列按cv2.boundingRect的x坐標(biāo)升序排列即可。雙行排列就不能只按x排得先按y坐標(biāo)分出行再在每一行內(nèi)按x排。我建議直接寫一個分組函數(shù)先對每個輪廓取中心點(diǎn)(cx, cy)按cy做聚類。同一行的輪廓cy差值應(yīng)該很小差值超過輪廓自身高度的一半就認(rèn)為換行了。這部分用numpy就可以實(shí)現(xiàn)不要依賴輪廓的索引順序。4.5 二值化方法全局 threshold 處理不了復(fù)雜光照很多模板匹配代碼默認(rèn)對 ROI 做cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)這在均勻光照下能用但銀行卡是塑料材質(zhì)側(cè)面一打光就會出現(xiàn)漸變反光全局閾值會直接把部分卡號數(shù)字從白色變成黑色。遇到這種問題優(yōu)先換cv2.adaptiveThreshold它按局部鄰域計(jì)算閾值能解決大部分光照梯度問題。代價(jià)是會把卡面的紋理誤判成前景所以自適應(yīng)閾值之后通常要加一個中值濾波或者先用面積過濾掉小噪點(diǎn)。如果換自適應(yīng)閾值后效果反而變差還有一種常見做法先對 ROI 做大津法THRESH_OTSU。大津法自動根據(jù)直方圖選擇閾值在卡號 ROI 這種前景背景比例相對固定的場景里表現(xiàn)不錯。我自己的經(jīng)驗(yàn)是優(yōu)先 OTSU它不需要調(diào)參數(shù)只有 OTSU 效果不好時(shí)才上自適應(yīng)閾值并調(diào)塊大小。參數(shù)常見設(shè)置調(diào)試信號閉運(yùn)算 kernel(15, 5)定位框碎或包含多余區(qū)域時(shí)調(diào)整輪廓最小面積8000 720pROI 為空時(shí)按分辨率等比放大寬高比范圍4.0 ~ 10.0定位到整張卡或 logo 時(shí)收窄匹配置信度0.5 ~ 0.7低于 0.4 優(yōu)先檢查預(yù)處理二值化方式OTSU / adaptive反光嚴(yán)重?fù)Q adaptive紋理干擾換 OTSU5. 避坑環(huán)境報(bào)錯、空輪廓、誤匹配的三類常見翻車與排查5.1 No module named cv2pip 包名和導(dǎo)入名不是一回事現(xiàn)象運(yùn)行源碼第一行import cv2就拋ModuleNotFoundError: No module named cv2。 原因很多人直接用pip install opencv或者根本沒裝庫。PyPI 上不存在名為opencv的 Python 包正確的包名是opencv-python而它的導(dǎo)入名是cv2包名和導(dǎo)入名不一致是 Python 生態(tài)里比較特殊的一處。 解決執(zhí)行python -m pip install opencv-python不要用pip install opencv。如果已經(jīng)裝了很多包怕沖突先python -m pip list | grep opencv看是否裝過別的 OpenCV 發(fā)行版有opencv-contrib-python同時(shí)在環(huán)境里時(shí)建議只保留一個兩個都裝會導(dǎo)致符號沖突具體表現(xiàn)是findContours等函數(shù)報(bào)奇怪的 C 類型錯誤。5.2 imread 返回 None中文路徑與文件缺失問題現(xiàn)象代碼沒有報(bào)錯但cv2.imread讀出來的img是 None整個流程在cv2.cvtColor處拋出空指針異常。 原因OpenCV 的imread內(nèi)部用的是 C 的文件讀取接口不支持中文路徑。如果你的測試圖片放在D:\測試圖片\card.jpg這種目錄下imread會返回 None而且不會報(bào)任何 warning。 解決把圖片路徑改成純英文目錄或者用cv2.imdecode配合numpy從字節(jié)流讀取繞開imread的路徑解析限制。第二種方法的寫法是img cv2.imdecode(np.fromfile(D:/測試圖片/card.jpg, dtypenp.uint8), cv2.IMREAD_COLOR)。同樣的問題也會出現(xiàn)在cv2.imwrite上寫結(jié)果圖到中文路徑時(shí)一樣會失敗只是imwrite通常會返回 False注意檢查返回值而不是只看有沒有報(bào)錯。5.3 cv2.error: OpenCV(4.4.0)... 與 findContours 返回值不一致現(xiàn)象運(yùn)行到contours, hierarchy cv2.findContours(...)時(shí)報(bào)錯錯誤信息類似too many values to unpack。 原因OpenCV 3.x 和 4.x 的findContours簽名不同。3.x 返回三個值(image, contours, hierarchy)4.x 返回兩個值(contours, hierarchy)。如果你下載的源碼用的是舊教程的寫法跑到新版 OpenCV 上就會解包失敗。報(bào)錯里帶著OpenCV(4.4.0) C:\users\...\pip-req-build...就是典型的 OpenCV 版本差異。 解決確認(rèn)你的 OpenCV 版本python -c import cv2; print(cv2.__version__)如果是 4.x把代碼改成contours, _ cv2.findContours(...)。如果你需要兼容兩個版本可以寫一個小的分支判斷cnt cv2.findContours(...); contours cnt[0] if len(cnt) 2 else cnt[1]。這個兼容寫法在給別的同學(xué)跑代碼時(shí)特別有用因?yàn)樗沫h(huán)境很可能和你不一樣。5.4 卡號區(qū)域定位錯亂輪廓沒連起來或者連成一大塊現(xiàn)象定位框用紅框畫出來之后框的位置要么在桌面雜物上要么把整張卡下半部分全包住要么框里只有半個卡號。 原因三種情況最容易發(fā)生。第一是閉運(yùn)算 kernel 太小數(shù)字邊緣之間還有缺口輪廓不完整寬高比不符合卡號區(qū)域被過濾后反而選到了旁邊的 logo第二是 kernel 垂直尺寸太大把兩行數(shù)字連成了一個整體第三是輪廓面積閾值太低選中了卡面上的小裝飾圖案。 解決逐層排查。先保存閉運(yùn)算后的closed.png肉眼看輪廓是不是一條完整的白色長條如果斷成幾段加寬水平 kernel如果連成一片減垂直 kernel。然后打印所有候選輪廓的面積和寬高比對照卡號實(shí)際尺寸把閾值范圍收緊。不要憑感覺調(diào)打印數(shù)據(jù)說話。5.5 數(shù)字識別混淆模板字體和卡面字體的差距現(xiàn)象識別結(jié)果里 6 經(jīng)常變成 83 變成 84 變成 9整體置信度都在 0.5 到 0.7 徘徊。 原因模板匹配的原理決定了它只認(rèn)「接近模板的形狀」。銀行卡卡號用的是等寬凸字字體和系統(tǒng)自帶字體、網(wǎng)上隨便下的字體差異很大。特別是 6 和 8、3 和 8 這種筆畫結(jié)構(gòu)接近的數(shù)字只要模板的邊緣和實(shí)際字符差幾個像素匹配分?jǐn)?shù)就會倒向錯誤的那一邊。 解決重新制作模板而且只從你實(shí)際要識別的卡種上摳模板。準(zhǔn)備一張分辨率高、沒有反光的真實(shí)卡號圖手動用矩形框切出每一個數(shù)字存成模板。摳模板時(shí)把數(shù)字四周空白裁掉不要留大塊背景然后統(tǒng)一縮放到相同尺寸比如 24 × 40。模板制作好之后單獨(dú)測試每個數(shù)字的匹配分?jǐn)?shù)如果 6 和 8 的分?jǐn)?shù)差不到 0.05說明模板太模糊或者 ROI 里有干擾線回到分割環(huán)節(jié)檢查是不是把兩個數(shù)字的邊界裁歪了。6. 進(jìn)階用腳本生成一張自測銀行卡先驗(yàn)證流程再驗(yàn)證效果拿到這個項(xiàng)目后最容易掉進(jìn)去的陷阱是拿一張真實(shí)卡照片反復(fù)調(diào)參結(jié)果卡面反光、拍攝角度等因素混在一起根本分不清是哪個環(huán)節(jié)出了問題。我習(xí)慣先做一張可控的測試卡把變量全部固定住再逐步貼近真實(shí)場景。做法是用 OpenCV 的putText在純色背景上畫出一串卡號保存成test_card.jpg。這張圖沒有反光、沒有透視變形、字體位置標(biāo)準(zhǔn)適合先驗(yàn)證整條流程是否走通。import numpy as np import cv2 img np.full((180, 640, 3), 30, dtypenp.uint8) # 深色底模擬卡面 card_no 6222 1234 5678 9012 # 16 位卡號 cv2.putText(img, card_no, (40, 110), cv2.FONT_HERSHEY_SIMPLEX, 1.6, (0, 0, 255), 4) cv2.imwrite(test_card.jpg, img)用這張測試圖跑通識別流程后再做三件事一是給測試卡加旋轉(zhuǎn)用cv2.warpAffine旋轉(zhuǎn) 5 度以內(nèi)觀察定位是否穩(wěn)定二是加模擬反光在卡號區(qū)域手動畫一個半透明白色矩形觀察模板匹配置信度變化三是換不同字體生成模板觀察同一套識別代碼的泛化能力。你會發(fā)現(xiàn)旋轉(zhuǎn)超過 8 度之后水平閉運(yùn)算核的效果大幅下降這時(shí)就需要在預(yù)處理里加透視矯正這是把系統(tǒng)做完整的一個重要進(jìn)階方向。另外如果你有精力可以把最后的模板匹配換成 PaddleOCR 或者 ddddocr 這類輕量 OCR 方案OpenCV 負(fù)責(zé)定位卡號區(qū)域OCR 負(fù)責(zé)數(shù)字識別。這樣設(shè)計(jì)報(bào)告里可以寫兩套方案的對比實(shí)驗(yàn)識別率數(shù)據(jù)會更漂亮。但換 OCR 之前先保證 OpenCV 定位環(huán)節(jié)已經(jīng)穩(wěn)定因?yàn)?OCR 識別的上限取決于你裁出來的 ROI 質(zhì)量。我自己當(dāng)年做類似項(xiàng)目時(shí)卡在定位環(huán)節(jié)的時(shí)間遠(yuǎn)比識別多后面總結(jié)出一條教訓(xùn)凡是圖像處理項(xiàng)目先確保每一層中間結(jié)果都可視化、可保存出現(xiàn)問題才能精準(zhǔn)定位到某一層而不是對著最終結(jié)果瞎猜。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取