節(jié):字符分割工具caveman的原理與實踐)
去年我維護一個對內(nèi)數(shù)據(jù)核對腳本跑批時候頻繁被登錄驗證碼卡住。人工識別雖然準確但一多起來就想吐。后來我試了一堆OCR方案tesseract、ddddocr都上了粘連字符一多照樣翻車。直到有人甩給我一個叫caveman的開源小工具我才意識到問題根本不在于“認不出來”而在于“沒切對”。這個工具的名字聽起來像原始人處理方式也確實有點像——不講什么高深模型就用長度分割法硬切但效果意外地扎實。如果你也在搞驗證碼識別、OCR預(yù)處理、爬蟲自動化或者只是想驗證自己生成的驗證碼是否合理這篇文章值得花幾分鐘看完。我會從caveman的定位、原理講起再帶一個完整可復(fù)現(xiàn)的識別流水線最后把我在實際調(diào)試中踩過的坑和參數(shù)調(diào)優(yōu)經(jīng)驗一并交代清楚。1. caveman是誰它解決的是驗證碼識別里最容易被忽略的一環(huán)1.1 先說清楚這個工具的定位caveman是一個開源字符分割工具核心功能就是從圖片驗證碼里把一個個字符分離開。它不做“識別”這件事只做“切分”。一個完整的OCR流程通常分兩步先分割后識別。分割質(zhì)量直接決定識別上限因為再強的識別模型吃進去一張“W”和“a”粘在一起或出格的圖也很難輸出正確結(jié)果。為什么說它容易被忽略因為現(xiàn)在大部分人遇到驗證碼第一反應(yīng)是“上深度學(xué)習(xí)模型”或者是“調(diào)API”。模型確實能端到端識別但它是個黑盒不好干預(yù)中間過程API有調(diào)用成本和隱私顧慮也不適合所有場景。caveman提供的是一條傳統(tǒng)的、可解釋的、完全本地化的路徑先把字符切干凈再用任何一個OCR引擎或輕量分類器去做單字識別。好處是你隨時能查看每一步切出來的圖問題出在哪一目了然。我見過很多人在tesseract上折騰半天調(diào)psm、換whitelist識別率死活上不去最后發(fā)現(xiàn)是圖片里字符連成了一片。這時候如果先用caveman把粘連字符拆開準確率能立刻上一個臺階。所以caveman適合這些場景圖片型驗證碼識別前置處理、OCR流水線里的分割模塊、字符分割算法的學(xué)習(xí)和對比實驗。1.2 為什么粘連字符是OCR的“使絆子”角色驗證碼生成方為了對抗自動識別最常用的手段就是把字符壓扁、拉近、交錯排列甚至讓兩個字符的筆畫疊在一起。對OCR引擎來說字符分割一旦出錯后面所有環(huán)節(jié)都白搭。比如下面這類情況“m”和“n”連成一條波浪線OCR直接當(dāng)成一個字符讀輸出亂碼字符太寬單個連通域里包含了兩個字符識別引擎又把它們當(dāng)成一個整體字符出格或傾斜垂直投影無法形成干凈的間隔傳統(tǒng)按列切分直接失效。這些問題在真實數(shù)據(jù)里非常常見。我統(tǒng)計過一批帶干擾線的驗證碼樣本“肉眼能看清但不粘連”的大概只有四成剩下的不是粘連就是傾斜或帶噪聲。所以只靠tesseract的默認分割邏輯基本是碰運氣。caveman解決的就是這個“碰運氣”的部分——它用一套基于字符寬度估算的規(guī)則把粘連域強制拆開并且盡量拆在正確的邊界上。1.3 長度分割法的核心思想等寬字符假設(shè)caveman的底層邏輯說穿了不復(fù)雜就是一個假設(shè)驗證碼里的字符基本都是等寬或近似等寬的。在這個前提下只要估算出“平均字符寬度”再結(jié)合連通域分析就能推算出某個連通區(qū)域里大概藏著幾個字符然后在合適的位置下刀。具體拆解下來它的判斷依據(jù)有三個通過統(tǒng)計整張圖里所有文字像素的分布估算出平均字符寬度通過連通域分析找出一個個獨立的筆畫塊當(dāng)一個連通域的寬度明顯大于平均字符寬度或者相鄰連通域的合并寬度異常時判定為“這里有多個字符粘在一起”需要切分。這個思路和人類辨認粘連字符的心理過程很像你看到一個超寬的筆畫塊自動會想“這肯定不止一個字母”然后在視覺上找“最窄處”或“凹陷位置”切進去。caveman干的也是這事只是用像素和寬度數(shù)據(jù)代替了人眼。值得一提的是長度分割法并不是唯一的分割思路。業(yè)界還有垂直投影分割法、滴水算法、基于深度學(xué)習(xí)的分割模型等。垂直投影法對干凈字符有效但一遇到輕微傾斜或字符搭邊就崩滴水算法能模擬水滴沿字符輪廓下滑找路徑抗粘連能力強但實現(xiàn)復(fù)雜、計算慢深度學(xué)習(xí)方法效果好但需要大量標注數(shù)據(jù)。caveman選擇的長度分割法優(yōu)勢在于速度極快、不依賴訓(xùn)練數(shù)據(jù)、可解釋性強——這點在做批量任務(wù)或快速驗證時很實用。2. 動手前先弄懂caveman的核心處理流水線2.1 圖片預(yù)處理從彩色驗證碼到干凈的二值圖caveman處理的第一步是拿到一張“干凈”的圖片這一步往往被人忽視但直接決定后續(xù)分割成功率。理想輸入是白底黑字、邊緣清晰的二值圖。如果喂給它的是一張帶彩色噪點、抗干擾線或漸變背景的驗證碼最好先做預(yù)處理。我常用的預(yù)處理流程是這樣灰度化把RGB三通道圖轉(zhuǎn)成單通道灰度圖二值化用固定閾值如127或自適應(yīng)閾值算法把灰度圖變成0/1二值圖降噪去掉孤立噪點可以借用中值濾波或形態(tài)學(xué)開運算校正如果字符有傾斜做一下旋轉(zhuǎn)校正避免投影統(tǒng)計失真。實際操作中二值化閾值的選擇很講究。閾值太高字符筆畫會被切斷變成一堆碎點閾值太低背景噪點全留下連通域爆炸。我通常會寫一個快速循環(huán)腳本把同一張圖按不同閾值處理跑一遍caveman看哪組參數(shù)切出來最穩(wěn)定。這種“跑批試參”的方法在后文的調(diào)參環(huán)節(jié)還會用到。2.2 字符分割階段連通域分析和粘連判斷的邏輯預(yù)處理完成后caveman會進入核心分割階段。這個階段的執(zhí)行流程大致如下第一步掃描二值圖找出所有文字像素連通區(qū)域記錄每個連通域的外接矩形坐標第二步統(tǒng)計所有連通域的寬度分布估算出“平均字符寬度”。這里要注意驗證碼里可能混入干擾線形成的連通域所以它通常取的是寬度分布的中位數(shù)或眾數(shù)更抗噪第三步遍歷每個連通域如果寬度落在合理字符寬度范圍內(nèi)就標記為“單字符”如果寬度明顯超標就根據(jù)超標倍數(shù)推斷里面包含幾個字符第四步對標記為“多字符”的連通域執(zhí)行切分。切分時優(yōu)先找凹陷點即在字符輪廓的凹進去的位置下刀找不到明顯凹陷點時按等分寬度切第五步輸出每個字符子圖的邊界坐標供后續(xù)OCR引擎逐塊識別。這里有一個容易被誤解的點caveman并不總是“每一個寬度超標的連通域都能切對”。當(dāng)兩個字符的筆畫大面積交疊時按等分寬度切出的位置可能剛好砍在某個字符的中間導(dǎo)致識別時出現(xiàn)“半個字母”。所以后面我們還要靠識別器的置信度做二次修正這個我在實戰(zhàn)環(huán)節(jié)會具體演示。2.3 caveman能輸出什么從坐標框到字符候選集如果你的理解還停留在“輸入一張圖輸出一段文本”那說明你還沒真正用好caveman。這個工具定位是一個分割器所以它最有價值的輸出是“每個字符子圖的坐標和切分后的圖片”而不是最終識別結(jié)果。拿到這些坐標框之后你可以做很多事情把切好的字符子圖逐個保存為文件用tesseract按時識別把坐標框畫回原圖可視化檢查切分位置是否合理把坐標框和另一個方向——比如字符上下溢出——結(jié)合起來做清洗把這些切分結(jié)果作為訓(xùn)練數(shù)據(jù)去訓(xùn)練一個小型單字符分類器。我個人的經(jīng)驗是永遠先可視化你的切分結(jié)果再談識別率。caveman切得好不好一眼就能看出來。如果你發(fā)現(xiàn)切分框歪七扭八那后面接什么識別引擎都救不回來不如回到預(yù)處理環(huán)節(jié)去調(diào)參數(shù)。3. 實戰(zhàn)用caveman tesseract跑通一個完整的識別流程3.1 環(huán)境準備和最小可運行代碼我的環(huán)境是Python 3.10caveman通過pip直接安裝即可。注意它依賴Pillow和numpy這兩個庫做圖像處理是標配。tesseract則需要單獨安裝OCR引擎本體和語言包我用的是tesseract 5.0版本的英文訓(xùn)練數(shù)據(jù)。最小識別代碼其實很短核心就是“先切分、后識別”from PIL import Image import caveman import pytesseract # 1. 讀取圖片并做預(yù)處理 img Image.open(captcha.png).convert(L) img img.point(lambda x: 255 if x 160 else 0) img.save(captcha_bin.png) # 2. 用caveman找出所有字符子圖 boxes caveman.find_boxes(captcha_bin.png) # 3. 逐個識別對每個子圖單獨送tesseract result_text for i, box in enumerate(boxes): piece img.crop((box[x], box[y], box[x] box[width], box[y] box[height])) piece piece.resize((piece.width * 4, piece.height * 4)) # 放大后再識別效果更好 char pytesseract.image_to_string(piece, config--psm 10 -c tessedit_char_whitelistabcdefghijklmnopqrstuvwxyz0123456789) result_text char.strip() piece.save(fchar_{i}.png) print(識別結(jié)果:, result_text)這里有兩個容易踩的細節(jié)。第一--psm 10表示“識別單個字符”這是關(guān)鍵配置因為我們已經(jīng)切分了不需要tesseract再自動分段。第二子圖放大4倍再識別能顯著提高單字符識別準確率但不要用最近鄰插值放大鋸齒會干擾識別建議用PIL默認的Lanczos重采樣。3.2 一頓調(diào)試當(dāng)默認參數(shù)碰上“J”我踩的坑第一次跑這個流程我的識別器幾乎把驗證碼里的“J”全識別成了“]”把“l(fā)”識別成了“1”。這個問題不是caveman切錯了而是切出來的子圖里字符本身帶有輕微的傾斜和筆畫殘缺。單獨看字符“J”的底鉤如果因為二值化掉了幾個像素看起來就真的像“]”。我當(dāng)時的排查思路是先保存所有字符子圖人工查看一遍。結(jié)果發(fā)現(xiàn)caveman切分坐標基本是對的但有一小部分字符筆畫出界正好在子圖邊緣被裁掉了一截。這種情況多半出現(xiàn)在首尾字符上因為驗證碼生成時字符不自帶邊距邊緣像素貼得太緊。解決辦法有兩個在傳給caveman之前先給整張圖加一圈白邊比如用ImageOps.expand(img, border20, fill255)這樣所有字符都離開圖片邊緣二值化時把閾值放寬到170~180之間寧可多留一點淺灰也不要過早把筆畫切掉。加上白邊之后“J”識別成“]”的問題立刻消失了。這個小改動也讓我意識到caveman分割本身沒問題但它的輸出質(zhì)量高度依賴輸入圖的邊界條件。做任何OCR預(yù)處理都要先想清楚“字符是不是被裁到了”。3.3 換個驗證碼風(fēng)格參數(shù)要跟著改一個批量調(diào)參腳本我手里的驗證碼有兩類一類是字符間有明顯間距的簡單驗證碼一類是字符擠在一起甚至部分重疊的復(fù)雜驗證碼。第一類用默認參數(shù)幾乎百分之百切對第二類就需要調(diào)節(jié)caveman的字符寬度估算參數(shù)。caveman允許你傳入最大字符寬、最小字符寬和平均字符寬等參數(shù)。簡單驗證碼用默認值沒問題復(fù)雜驗證碼則需要根據(jù)樣本重新估算。我寫了一個簡單的批量調(diào)參腳本import caveman import statistics widths [] for sample in samples: result caveman.find_boxes(sample) for box in result: if box[width] 5: # 過濾掉孤點噪聲 widths.append(box[width]) avg_width statistics.median(widths) min_width int(avg_width * 0.5) max_width int(avg_width * 1.5) print(f中位數(shù)寬度: {avg_width}, 最小寬: {min_width}, 最大寬: {max_width})把統(tǒng)計出來的寬度參數(shù)回傳給caveman重新跑分割切分準確率提升非常明顯。核心邏輯是當(dāng)驗證碼風(fēng)格變化時平均字符寬度也會變只有用樣本自己統(tǒng)計出來的寬度才能適配這一套樣式。這個思路和訓(xùn)練模型時的數(shù)據(jù)集校準是同一道理只不過它發(fā)生在規(guī)則層。4. 識別率之外的事穩(wěn)定性、邊界條件和工程整合4.1 用樣本量說話什么情況才叫“參數(shù)調(diào)好了”很多人在調(diào)參時憑感覺覺得“這張切對了就行”。但在真實工程里驗證碼樣式是會變的偶爾還會出現(xiàn)你從沒見過的字體變體。所以我會專門留一批標注過的小樣本集每次調(diào)完參數(shù)都跑一遍整體統(tǒng)計。具體做法是準備50~100張已人工標注過答案的驗證碼圖片跑完整流水線統(tǒng)計整圖識別準確率、單字符準確率、分割失敗率三個指標。如果整圖準確率穩(wěn)定在90%以上單字符準確率在97%以上再疊加合理的重試策略整體方案就基本可用了。這里有個容易被忽略的原則不要追求單張圖的完美而是追求整批圖的高可用。哪怕偶爾有一張識別錯了只要重試機制能兜底整體體驗依然是好的。相反如果為了把那張刁鉆樣本切對而犧牲掉大量普通樣本就得不償失了。4.2 容易翻車的字符形態(tài)i、l、1、0、O的前世今生即便caveman切得很好識別階段仍會有一批“天生難分”的字符組合。我踩得最多的就是三組l和1、0和O、i和l。尤其是切分出來的子圖分辨率不高時這些組合幾乎無法靠字符形態(tài)區(qū)分只能靠上下文或白名單約束。處理思路有三個層次第一層在驗證碼識別場景里如果能確認來源驗證碼只包含數(shù)字那就直接限定字符集為數(shù)字O和0的歧義自動消失第二層如果你不能確定字符集可以做后處理糾錯比如用字典校驗對于連串字符0旁邊的字母通常是O第三層在字符圖片本身上下工夫比如把“i”的上點去掉之后再識別減少它和“l(fā)”的混淆概率。我個人最推薦的是第一層方案在可控場景下把字符集白名單寫死。這不僅解決了歧義問題還順手提升了單字符識別的速度。這也是為什么我在上面的最小示例代碼里就加了-c tessedit_char_whitelist的原因。4.3 放到真實工程里異步化、重試和日志留痕當(dāng)你把caveman識別流水線從腳本搬到正式服務(wù)時有幾個工程化問題會被立刻放大。首先是并發(fā)與并發(fā)安全。caveman本身是CPU密集型的純Python計算受GIL影響多線程并不會帶來線性加速建議用進程池實現(xiàn)并行。我這里用的是concurrent.futures.ProcessPoolExecutor每個進程獨立初始化模型和caveman實例這樣既規(guī)避了GIL又能共享穩(wěn)定的內(nèi)存池。其次是識別失敗的重試策略。OCR識別天然不是100%準確在工程上要默認它可能失敗。我習(xí)慣做兩到三次識別嘗試每次隨機微調(diào)預(yù)處理參數(shù)比如二值化閾值上下浮動10放大倍數(shù)浮動0.5倍取置信度最高的那次結(jié)果。如果仍然失敗記錄日志并拋給人工流程處理不要硬撐。最后是留痕。每條識別記錄我都會保存原始圖片、切分中間圖、識別結(jié)果和置信度時間戳。這樣無論用戶反饋什么都能回溯到具體哪一步出了問題。這套日志機制曾經(jīng)幫我從一堆“識別異?!崩锞珳识ㄎ坏健澳硞€時間段網(wǎng)站驗證碼換了字體”堪稱救命。4.4 一條更隱蔽的坑caveman對非等寬字符的誤判前面講了caveman依賴等寬字符假設(shè)但在真實驗證碼中有的字體不是等寬字體比如手寫體風(fēng)格。字符“W”和“i”的寬度差異極大平均字符寬度的估算就會失真。“W”可能被判定為“包含兩個字符”然后被錯誤切分反過來“i”這種窄字符可能被當(dāng)成噪點過濾掉。解決思路有兩個方向。一個是繞過換用滴水算法或基于輪廓分析的分割器它們不依賴等寬假設(shè)適合手寫體另一個是改造在調(diào)用caveman之前對字符區(qū)域做寬度歸一化的預(yù)處理比如把每個連通域單獨提取并縮放到統(tǒng)一寬度再拼接成一行輸入。第二個方案我試過效果不錯但要注意縮放會破壞字符比例導(dǎo)致“O”變成“0”之類的變形。5. 必須說清楚的合法使用邊界5.1 這套技術(shù)能做和不能做的事任何OCR技術(shù)都有雙面性。caveman能做的是自動化識別你自己擁有、或被明確授權(quán)測試的系統(tǒng)中的驗證碼比如你自己開發(fā)的網(wǎng)站或應(yīng)用里的驗證碼可用性測試你在做爬蟲數(shù)據(jù)采集時目標網(wǎng)站明確允許自動化訪問的場景內(nèi)部系統(tǒng)的自動化回歸測試。它不能也不應(yīng)該用來做這件事未經(jīng)授權(quán)繞過他人網(wǎng)站的驗證碼批量注冊賬號、搶票、刷接口、發(fā)送垃圾請求。這類行為在國內(nèi)外法律框架下都有明確的違法或違約風(fēng)險我也強烈不建議任何人去碰。驗證碼存在的目的是保護系統(tǒng)資源繞過它去薅羊毛輕則封IP重則吃官司完全不值得。我說這些不是站在道德高地上說教而是作為從業(yè)者基于風(fēng)險給你一個實在的建議。技術(shù)能力應(yīng)該用在提升自身效率上而不是破壞別人的防線。如果你不確定當(dāng)前應(yīng)用場景是否合規(guī)我的建議是先停下來問一下你自己“如果我做的這件事被公開報道了我能坦然面對嗎”只要有一絲猶豫就別做。5.2 一個更穩(wěn)妥的用法用caveman檢驗?zāi)阕约因炞C碼的質(zhì)量我在實際工作中發(fā)現(xiàn)caveman最好的應(yīng)用場景其實是“驗證碼質(zhì)量評估”。很多開發(fā)者在生成驗證碼后不知道自己的驗證碼到底夠不夠“抗自動識別”。人工看覺得還行但機器識別的風(fēng)險心里沒底。這時候你可以直接用cavemantesseract搭一個自動識別器拿它來測量自家驗證碼的自動識別通過率。如果識別率太高說明驗證碼太弱需要增加字符粘連程度、干擾線或旋轉(zhuǎn)角度如果識別率低到連正常用戶都看不清那說明驗證碼設(shè)計過頭了容易流失用戶。理想狀態(tài)是自動識別通過率控制在可接受范圍同時人類用戶能輕松通過。這個“攻防平衡測試”的玩法比單純拿caveman去攻破別人網(wǎng)站有價值得多也更符合行業(yè)倫理。我在給自家業(yè)務(wù)做驗證碼優(yōu)化時就專門建了一套自動化評估鏈路自動生成幾百個驗證碼樣本 → 用caveman切分 → 用tesseract識別 → 統(tǒng)計自動通過率 → 調(diào)整生成參數(shù)。這套流程幫我迅速定位到“背景噪聲密度”和“字符間距離”兩個關(guān)鍵參數(shù)的最優(yōu)區(qū)間效果立竿見影。6. 最后聊點實在的我把caveman用順手之后的一些體會用caveman跑了半年多從最初在腳本里玩一玩到現(xiàn)在沉淀成內(nèi)部OCR流水線的固定前置模塊我的感受有兩個。第一不要迷信端到端黑盒。深度學(xué)習(xí)模型確實能解決很多傳統(tǒng)算法搞不定的問題但它不擅長給你反饋。caveman這種傳統(tǒng)可解釋工具的價值在于每個環(huán)節(jié)都能看、能調(diào)、能量化。當(dāng)你發(fā)現(xiàn)識別率下降時你能立刻判斷是預(yù)處理的問題、切分的問題還是識別的問題而不是對著一個模型發(fā)呆。這種“可干預(yù)性”在做工程時比什么都重要。第二參數(shù)調(diào)優(yōu)的本質(zhì)是理解數(shù)據(jù)。很多人在調(diào)參時盯著代碼看卻忘了把頭抬起來看看樣本。caveman默認參數(shù)好不好用完全取決于你的驗證碼長什么樣。先用一批有代表性的樣本做統(tǒng)計再根據(jù)統(tǒng)計結(jié)果設(shè)置參數(shù)往往比盲目試參數(shù)更快見效。工具永遠是次要的理解和掌控數(shù)據(jù)才是核心能力。如果你現(xiàn)在也被OCR識別、驗證碼處理這類問題卡著不妨像我一樣搭建這樣一套用caveman做分割、可視化驗證一下切分結(jié)果、再搭配輕量識別器的流水線。切分對了識別就是水到渠成的事。把這個環(huán)節(jié)做扎實之后你會發(fā)現(xiàn)后續(xù)所有跟字符提取相關(guān)的任務(wù)都變得異常順滑。