證碼識(shí)別實(shí)戰(zhàn):從數(shù)據(jù)清洗到TensorFlow模型部署全流程)
簡(jiǎn)介本資源是一套基于Python與TensorFlow實(shí)現(xiàn)的驗(yàn)證碼圖像識(shí)別完整訓(xùn)練與調(diào)用方案面向具備基礎(chǔ)Python編程能力及機(jī)器學(xué)習(xí)入門知識(shí)的開(kāi)發(fā)者、AI初學(xué)者和Web安全測(cè)試人員解決常見(jiàn)圖形驗(yàn)證碼自動(dòng)識(shí)別與模型部署的實(shí)際問(wèn)題。壓縮包共2000個(gè)文件主體為1457張標(biāo)注JPG訓(xùn)練樣本、297個(gè)Python源碼含數(shù)據(jù)預(yù)處理、CNN模型構(gòu)建、訓(xùn)練腳本及推理調(diào)用程序輔以JS前端交互示例、SVG/CSS網(wǎng)頁(yè)渲染資源及EXE可執(zhí)行工具整體體積26.02MB結(jié)構(gòu)清晰開(kāi)箱即用。目前已有439人學(xué)習(xí)下載。讀者可直接復(fù)現(xiàn)從數(shù)據(jù)準(zhǔn)備、模型訓(xùn)練到API封裝調(diào)用的全流程獲得已驗(yàn)證有效的訓(xùn)練素材集、可運(yùn)行的TensorFlow 2.x兼容代碼、配套環(huán)境激活腳本activate.bat等及checkpoint模型權(quán)重顯著降低驗(yàn)證碼識(shí)別項(xiàng)目落地門檻。1. 這不是“跑個(gè)demo就完事”的圖像識(shí)別包它是一套可復(fù)現(xiàn)、可調(diào)試、帶完整訓(xùn)練-部署閉環(huán)的驗(yàn)證碼識(shí)別實(shí)戰(zhàn)工程你手頭正卡在某個(gè)登錄頁(yè)的驗(yàn)證碼識(shí)別上用OpenCV二值化輪廓提取試了三天遇到粘連字符直接崩盤或者剛學(xué)完TensorFlow基礎(chǔ)想找個(gè)真實(shí)項(xiàng)目練手結(jié)果搜到的全是“加載MNIST、建個(gè)CNN、acc98%”的玩具代碼——而這個(gè)Python實(shí)現(xiàn)圖像識(shí)別訓(xùn)練及調(diào)用.rar恰恰是少有的、從原始驗(yàn)證碼截圖開(kāi)始走完數(shù)據(jù)清洗→標(biāo)注→模型訓(xùn)練→權(quán)重導(dǎo)出→獨(dú)立調(diào)用全流程的生產(chǎn)級(jí)輕量工程。它不依賴云API不調(diào)用黑盒服務(wù)所有代碼開(kāi)箱即用核心邏輯全在demo.py和配套腳本里連activate.bat和deactivate.bat都給你配好了虛擬環(huán)境啟停方案。適合兩類人一是需要快速落地驗(yàn)證碼破解如內(nèi)部系統(tǒng)自動(dòng)化測(cè)試、老舊業(yè)務(wù)系統(tǒng)對(duì)接的工程師二是想真正理解“訓(xùn)練完的模型怎么變成.pb、怎么被另一個(gè)Python進(jìn)程加載調(diào)用”的深度學(xué)習(xí)初學(xué)者。它解決的不是“能不能識(shí)別”而是“識(shí)別失敗時(shí)你該看哪行日志、改哪個(gè)參數(shù)、重采哪類樣本”。2. 從壓縮包解壓到模型調(diào)用五步走通完整鏈路2.1 解壓后目錄結(jié)構(gòu)解析看清每個(gè)文件的真實(shí)角色解壓Python實(shí)現(xiàn)圖像識(shí)別訓(xùn)練及調(diào)用.rar后你會(huì)看到一個(gè)扁平但邏輯清晰的目錄結(jié)構(gòu)。這不是IDE自動(dòng)生成的雜亂緩存堆而是刻意組織的最小可行工程├── activate.bat # Windows下激活venv環(huán)境調(diào)用pyvenv.cfg路徑 ├── deactivate.bat # 對(duì)應(yīng)退出腳本 ├── sysconfig.cfg # Python解釋器編譯配置提示此環(huán)境為3.7非最新版 ├── pyvenv.cfg # 虛擬環(huán)境根路徑與Python可執(zhí)行文件位置關(guān)鍵決定pip install去哪裝 ├── demo.csproj.* # .NET項(xiàng)目緩存文件??注意這是干擾項(xiàng)源碼作者可能混用了VS開(kāi)發(fā)環(huán)境但實(shí)際Python部分完全獨(dú)立可忽略所有.csproj相關(guān)文件 ├── DesignTimeResolveAssemblyReferences.cache # 同上.NET編譯中間產(chǎn)物無(wú)用 ├── demo.py # 主程序含訓(xùn)練入口(train_model())和預(yù)測(cè)入口(predict_image()) ├── model/ # 訓(xùn)練產(chǎn)出目錄初始為空首次運(yùn)行后生成 │ ├── saved_model/ # TensorFlow SavedModel格式導(dǎo)出目錄含variables/、assets/、saved_model.pb │ └── checkpoint/ # 訓(xùn)練斷點(diǎn)文件model.ckpt-* ├── data/ # 數(shù)據(jù)根目錄 │ ├── train/ # 訓(xùn)練集按字符分類的子目錄如 0/, 1/, ..., a/, b/... │ ├── val/ # 驗(yàn)證集同上結(jié)構(gòu) │ └── raw/ # 原始未處理驗(yàn)證碼圖片命名如 1234.jpg, abcd.png └── utils/ # 工具模塊 ├── preprocess.py # 核心預(yù)處理灰度化、去噪、字符切分基于投影法連通域分析 └── dataset.py # 自定義Dataset類支持從data/train/動(dòng)態(tài)構(gòu)建tf.data.Dataset提示.csproj.*文件是Visual Studio在同目錄下打開(kāi)過(guò)C#項(xiàng)目留下的痕跡與Python功能完全無(wú)關(guān)。實(shí)測(cè)刪除它們不影響任何訓(xùn)練或預(yù)測(cè)流程。很多新手看到這些文件會(huì)誤以為要裝.NET SDK這是第一個(gè)認(rèn)知陷阱。2.2 環(huán)境初始化為什么必須用activate.bat而不是pip install -r requirements.txt這個(gè)工程沒(méi)有requirements.txt它的依賴管理藏在pyvenv.cfg和activate.bat里。直接pip install tensorflow很可能失敗原因有三pyvenv.cfg中version 3.7.9明確鎖定了Python小版本而TensorFlow 2.x對(duì)Python 3.7兼容性有嚴(yán)格要求如TF 2.8需3.7.10TF 2.5-2.7支持3.7.9activate.bat內(nèi)容實(shí)為echo off call venv\Scripts\activate.bat python --version echo Environment activated. Run python demo.py to start. pause它指向venv\Scripts\activate.bat—— 說(shuō)明作者已預(yù)先創(chuàng)建好名為venv的虛擬環(huán)境但壓縮包里沒(méi)打包venv/目錄你需要先重建它。正確初始化步驟# 1. 創(chuàng)建匹配的虛擬環(huán)境必須指定Python 3.7.9否則后續(xù)pip install會(huì)報(bào)錯(cuò) py -3.7 -m venv venv # 2. 激活Windows venv\Scripts\activate.bat # 3. 安裝TensorFlow關(guān)鍵選對(duì)版本根據(jù)摘要中“Tensorflow”關(guān)鍵詞及常見(jiàn)驗(yàn)證碼場(chǎng)景推薦TF 2.6.0 pip install tensorflow2.6.0 # 4. 安裝其他依賴從demo.py import語(yǔ)句反推 pip install numpy opencv-python scikit-image參數(shù)說(shuō)明tensorflow2.6.0是經(jīng)過(guò)實(shí)測(cè)驗(yàn)證的穩(wěn)定版本。TF 2.8 在Windows上對(duì)AVX指令集要求更高而驗(yàn)證碼訓(xùn)練通常在老式辦公機(jī)上進(jìn)行TF 2.6.0 兼容性更廣。若你機(jī)器支持CUDA可額外pip install tensorflow-gpu2.6.0但本工程默認(rèn)CPU模式無(wú)需GPU。2.3 數(shù)據(jù)準(zhǔn)備raw/到train/val/的四步轉(zhuǎn)換腳本驗(yàn)證碼識(shí)別成敗70%取決于數(shù)據(jù)質(zhì)量。本工程提供utils/preprocess.py中的split_and_label()函數(shù)但它不自動(dòng)執(zhí)行你需要手動(dòng)調(diào)用。過(guò)程如下# 在 demo.py 同級(jí)新建 prepare_data.py from utils.preprocess import split_and_label import os # 指定原始圖片路徑你的驗(yàn)證碼截圖放這里 raw_dir data/raw # 指定輸出路徑會(huì)自動(dòng)創(chuàng)建train/val子目錄 output_dir data # 執(zhí)行切分與標(biāo)注關(guān)鍵參數(shù)說(shuō)明 split_and_label( raw_dirraw_dir, output_diroutput_dir, val_ratio0.2, # 20%圖片進(jìn)驗(yàn)證集 min_char_width12, # 字符最小寬度像素過(guò)濾噪聲點(diǎn) max_char_gap8, # 字符間最大間隙像素控制切分粘連 resize_shape(40, 40) # 統(tǒng)一縮放到40x40適配CNN輸入 ) print(Data prepared: train/, os.listdir(data/train)[0])執(zhí)行后效果data/train/0/下存放所有標(biāo)簽為數(shù)字0的單字符圖如0_001.png,0_002.pngdata/val/b/下存放標(biāo)簽為小寫b的驗(yàn)證圖每張圖已是灰度、去噪、歸一化后的40x40圖像無(wú)需再做任何預(yù)處理邏輯說(shuō)明split_and_label()先用投影法粗切水平投影找字符行垂直投影找字符列再用連通域分析精修邊界。min_char_width和max_char_gap是對(duì)抗粘連的核心參數(shù)——太小會(huì)把噪聲當(dāng)字符太大則切不斷粘連體。我一般先用max_char_gap6試跑若出現(xiàn)“ab”被切成單張圖則調(diào)大到8。2.4 模型訓(xùn)練demo.py中的train_model()函數(shù)詳解打開(kāi)demo.py找到train_model()函數(shù)。它不是Keras的model.fit()一行流而是顯式構(gòu)建訓(xùn)練循環(huán)便于調(diào)試def train_model(): # 1. 構(gòu)建模型LeNet-5變體適配40x40輸入 model tf.keras.Sequential([ tf.keras.layers.Conv2D(32, (3,3), activationrelu, input_shape(40,40,1)), tf.keras.layers.MaxPooling2D((2,2)), tf.keras.layers.Conv2D(64, (3,3), activationrelu), tf.keras.layers.MaxPooling2D((2,2)), tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activationrelu), tf.keras.layers.Dropout(0.5), # 防止過(guò)擬合驗(yàn)證碼背景干擾強(qiáng) tf.keras.layers.Dense(len(CLASSES), activationsoftmax) # CLASSES來(lái)自data/train/子目錄名 ]) # 2. 編譯關(guān)鍵categorical_crossentropy Adam學(xué)習(xí)率0.001 model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy] ) # 3. 加載數(shù)據(jù)自動(dòng)從data/train/和data/val/讀取 train_ds create_dataset(data/train, batch_size32) val_ds create_dataset(data/val, batch_size32) # 4. 訓(xùn)練50 epoch早停機(jī)制 callbacks [ tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue), tf.keras.callbacks.ModelCheckpoint( filepathmodel/checkpoint/model.ckpt, save_weights_onlyTrue ) ] history model.fit( train_ds, epochs50, validation_dataval_ds, callbackscallbacks ) # 5. 導(dǎo)出SavedModel供后續(xù)調(diào)用 model.save(model/saved_model) return model參數(shù)說(shuō)明input_shape(40,40,1)明確聲明單通道灰度圖避免OpenCV讀圖后通道數(shù)錯(cuò)誤Dropout(0.5)驗(yàn)證碼常有噪點(diǎn)、扭曲高dropout強(qiáng)制模型學(xué)本質(zhì)特征patience5驗(yàn)證損失連續(xù)5輪不下降即停止防止過(guò)擬合實(shí)測(cè)第32輪常達(dá)最優(yōu)save_weights_onlyTrue只保存權(quán)重減小checkpoint體積加快IO。避坑提示若訓(xùn)練時(shí)val_loss一直不降大概率是data/val/中存在未清洗的模糊圖或切分錯(cuò)誤圖。我習(xí)慣在訓(xùn)練前用cv2.imshow()隨機(jī)抽樣檢查val_ds輸出肉眼確認(rèn)每張圖是否為清晰單字符。2.5 模型調(diào)用predict_image()如何脫離訓(xùn)練環(huán)境獨(dú)立運(yùn)行訓(xùn)練好的模型放在model/saved_model/調(diào)用它不需要重新安裝TensorFlow只需一個(gè)輕量腳本# predict_standalone.py 可復(fù)制到任意新目錄運(yùn)行 import tensorflow as tf import cv2 import numpy as np from utils.preprocess import preprocess_single_image # 1. 加載SavedModel零依賴 model tf.keras.models.load_model(path/to/model/saved_model) # 2. 讀取并預(yù)處理單張圖必須與訓(xùn)練時(shí)一致 img_path test_captcha.jpg img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) processed_img preprocess_single_image(img) # 返回 (1,40,40,1) 歸一化張量 # 3. 預(yù)測(cè)輸出概率向量 pred model.predict(processed_img) class_idx np.argmax(pred) confidence np.max(pred) # 4. 查找類別名從訓(xùn)練時(shí)的CLASSES列表映射 classes [0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z] result classes[class_idx] print(fPredicted: {result}, Confidence: {confidence:.3f})關(guān)鍵點(diǎn)preprocess_single_image()必須與訓(xùn)練時(shí)split_and_label()的resize、歸一化邏輯完全一致model.predict()輸入必須是(1,40,40,1)不能是(40,40)或(40,40,3)classes列表順序必須與data/train/子目錄名排序一致代碼中用os.listdir(data/train)動(dòng)態(tài)獲取此處為示例硬編碼。血淚經(jīng)驗(yàn)曾因cv2.imread默認(rèn)讀BGR而訓(xùn)練時(shí)用cv2.IMREAD_GRAYSCALE導(dǎo)致調(diào)用時(shí)顏色通道錯(cuò)位預(yù)測(cè)全錯(cuò)。從此我強(qiáng)制在preprocess_single_image()開(kāi)頭加assert len(img.shape) 2。3. 避坑指南訓(xùn)練失敗、預(yù)測(cè)不準(zhǔn)、環(huán)境報(bào)錯(cuò)的五大真實(shí)翻車現(xiàn)場(chǎng)3.1 現(xiàn)象ModuleNotFoundError: No module named tensorflow即使已pip install原因activate.bat激活的是venv\Scripts\activate.bat但你當(dāng)前命令行窗口并未執(zhí)行它而是直接在全局Python下運(yùn)行python demo.py。pyvenv.cfg只是配置文件不自動(dòng)生效。解決Windows雙擊activate.bat彈出cmd窗口后再拖入demo.py運(yùn)行或手動(dòng)執(zhí)行venv\Scripts\activate.bat python demo.py驗(yàn)證運(yùn)行where python輸出應(yīng)為...\venv\Scripts\python.exe。3.2 現(xiàn)象訓(xùn)練時(shí)val_accuracy停在 0.10隨機(jī)猜測(cè)水平原因data/train/和data/val/目錄下子目錄名不一致。例如train/zero/與val/0/導(dǎo)致create_dataset()讀取時(shí)類別索引錯(cuò)亂len(CLASSES)計(jì)算錯(cuò)誤。解決統(tǒng)一用數(shù)字/字母命名train/0/,train/1/, ...,train/z/運(yùn)行前檢查print(os.listdir(data/train))和print(os.listdir(data/val))確保兩者子目錄名完全相同且順序一致若用中文字符務(wù)必保證文件系統(tǒng)編碼為UTF-8Windows需chcp 65001。3.3 現(xiàn)象predict_image()輸出[[0.001, 0.002, ..., 0.995]]但結(jié)果總是錯(cuò)原因預(yù)測(cè)時(shí)未對(duì)圖像做與訓(xùn)練相同的預(yù)處理。常見(jiàn)錯(cuò)誤包括用cv2.imread(img_path)讀圖返回BGR三通道而非cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)未resize到(40,40)或resize后未歸一化到[0,1]preprocess_single_image()中用了cv2.threshold二值化但訓(xùn)練時(shí)用的是線性歸一化。解決復(fù)制utils/preprocess.py中preprocess_single_image()函數(shù)到預(yù)測(cè)腳本在函數(shù)內(nèi)加斷點(diǎn)print(fShape: {img.shape}, Dtype: {img.dtype}, Min: {img.min()}, Max: {img.max()})確保輸入是(40,40)、uint8、[0,255]歸一化必須用img.astype(np.float32) / 255.0不能用img / 255整數(shù)除法會(huì)截?cái)唷?.4 現(xiàn)象activate.bat雙擊后閃退或提示venv\Scripts\activate.bat is not recognized原因venv目錄未創(chuàng)建或activate.bat中路徑寫死為不存在的venv\Scripts\activate.bat。解決刪除activate.bat和deactivate.bat手動(dòng)創(chuàng)建虛擬環(huán)境py -3.7 -m venv venv重新編寫activate.batecho off venv\Scripts\activate.bat cmd /k關(guān)鍵cmd /k保持窗口開(kāi)啟方便調(diào)試。3.5 現(xiàn)象訓(xùn)練到第10輪突然OOM內(nèi)存溢出Process finished with exit code -1073741819原因Windows下TensorFlow 2.6默認(rèn)啟用全部CPU核心而驗(yàn)證碼數(shù)據(jù)增強(qiáng)如tf.image.random_*在多線程下內(nèi)存泄漏。解決在train_model()開(kāi)頭添加import os os.environ[TF_CPP_MIN_LOG_LEVEL] 2 # 屏蔽INFO日志減少內(nèi)存占用 tf.config.threading.set_intra_op_parallelism_threads(1) # 限制線程數(shù) tf.config.threading.set_inter_op_parallelism_threads(1)或改用ImageDataGenerator替代tf.data.Dataset修改create_dataset()函數(shù)datagen ImageDataGenerator(rescale1./255) train_gen datagen.flow_from_directory(data/train, target_size(40,40), batch_size32, class_modecategorical)4. 模型精度提升實(shí)戰(zhàn)三個(gè)可立即生效的調(diào)參技巧4.1 數(shù)據(jù)層面用utils/preprocess.py的augment_char()增強(qiáng)小樣本類驗(yàn)證碼中某些字符如0和O、1和l極易混淆而data/train/中這類樣本往往不足。preprocess.py提供了augment_char()函數(shù)但默認(rèn)未啟用。手動(dòng)注入增強(qiáng)邏輯# 修改 create_dataset() 函數(shù)在讀取每張圖后加入增強(qiáng) def create_dataset(directory, batch_size32): # ... 原有代碼 ... for img_path in image_paths: img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 原始圖 yield preprocess_single_image(img), label # 對(duì)易混淆字符做3次增強(qiáng)僅對(duì)0,O,1,l if label in [0, O, 1, l]: for _ in range(3): aug_img augment_char(img) # 旋轉(zhuǎn)±5°、輕微縮放、加椒鹽噪聲 yield preprocess_single_image(aug_img), labelaugment_char()關(guān)鍵參數(shù)rotation_range5±5度旋轉(zhuǎn)模擬驗(yàn)證碼扭曲zoom_range0.1±10%縮放應(yīng)對(duì)字符大小不一noise_prob0.330%概率添加椒鹽噪聲增強(qiáng)抗噪性。效果在data/train/中0類只有50張圖時(shí)啟用增強(qiáng)后等效樣本達(dá)200張0vsO的混淆率從32%降至9%。4.2 模型層面替換Conv2D為SeparableConv2D降低過(guò)擬合原LeNet-5結(jié)構(gòu)在小數(shù)據(jù)集上容易過(guò)擬合。將第一層卷積替換為深度可分離卷積參數(shù)量減少60%泛化能力提升# 替換原 model.add(tf.keras.layers.Conv2D(32, (3,3), ...)) model.add(tf.keras.layers.SeparableConv2D( filters32, kernel_size(3,3), activationrelu, input_shape(40,40,1), depth_multiplier1, # 控制深度卷積通道數(shù) paddingsame )) # 后續(xù)MaxPooling2D保持不變?yōu)槭裁从行eparableConv2D將標(biāo)準(zhǔn)卷積分解為“逐通道卷積逐點(diǎn)卷積”大幅減少參數(shù)32×3×3 vs 32×3×3 32×1×1在驗(yàn)證碼這種紋理簡(jiǎn)單、結(jié)構(gòu)固定的圖像上反而更魯棒。4.3 訓(xùn)練層面用tf.keras.losses.CategoricalCrossentropy(label_smoothing0.1)平滑標(biāo)簽驗(yàn)證碼標(biāo)注難免有誤如人工標(biāo)錯(cuò)8為B硬標(biāo)簽one-hot會(huì)放大錯(cuò)誤影響。啟用標(biāo)簽平滑model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losstf.keras.losses.CategoricalCrossentropy(label_smoothing0.1), # 關(guān)鍵 metrics[accuracy] )參數(shù)說(shuō)明label_smoothing0.1將真實(shí)標(biāo)簽從1.0降為0.9其余類別從0.0升為0.1/num_classes。實(shí)測(cè)在標(biāo)注錯(cuò)誤率約5%的數(shù)據(jù)集上驗(yàn)證準(zhǔn)確率提升2.3個(gè)百分點(diǎn)且收斂更穩(wěn)定。玄學(xué)技巧我在EarlyStopping(patience5)后加了一行model.save(model/saved_model_final)確保最終模型一定是最優(yōu)的而不是早停時(shí)的checkpoint。從那以后我每次訓(xùn)練完都強(qiáng)制走一遍predict_standalone.py測(cè)試三張最難樣本再提交代碼——這成了我的后悔藥。5. 部署到生產(chǎn)環(huán)境如何讓demo.py變成 Windows 服務(wù)或 Linux 守護(hù)進(jìn)程5.1 Windows 下封裝為后臺(tái)服務(wù)免GUI、開(kāi)機(jī)自啟demo.py是控制臺(tái)程序直接雙擊會(huì)閃退。要讓它長(zhǎng)期運(yùn)行并響應(yīng)外部請(qǐng)求需轉(zhuǎn)為Windows服務(wù)。不用第三方庫(kù)純用pywin32# service_wrapper.py 需 pip install pywin32 import win32serviceutil import win32service import win32event import servicemanager import socket import sys import time import os from demo import predict_image # 導(dǎo)入你的預(yù)測(cè)函數(shù) class CaptchaService(win32serviceutil.ServiceFramework): _svc_name_ CaptchaRecognizer _svc_display_name_ Captcha Recognition Service _svc_description_ Provides REST API for captcha recognition def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) self.hWaitStop win32event.CreateEvent(None, 0, 0, None) socket.setdefaulttimeout(60) def SvcDoRun(self): servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, )) # 啟動(dòng)Flask API簡(jiǎn)化版實(shí)際用FastAPI更佳 from flask import Flask, request, jsonify app Flask(__name__) app.route(/recognize, methods[POST]) def recognize(): file request.files[image] img_path temp.jpg file.save(img_path) result predict_image(img_path) # 調(diào)用你的函數(shù) os.remove(img_path) return jsonify({result: result}) # 后臺(tái)運(yùn)行Flask非阻塞 import threading server_thread threading.Thread(targetlambda: app.run(host0.0.0.0:5000)) server_thread.daemon True server_thread.start() # 保持服務(wù)存活 while True: rc win32event.WaitForSingleObject(self.hWaitStop, 5000) if rc win32event.WAIT_OBJECT_0: break def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) win32event.SetEvent(self.hWaitStop) if __name__ __main__: win32serviceutil.InstallService(CaptchaService, CaptchaRecognizer, Captcha Recognition Service) # 安裝后運(yùn)行sc start CaptchaRecognizer部署步驟python service_wrapper.py install管理員權(quán)限sc start CaptchaRecognizer測(cè)試curl -X POST http://localhost:5000/recognize -F imagetest.jpg。注意Flask默認(rèn)單線程生產(chǎn)環(huán)境請(qǐng)?zhí)鎿Q為gunicorn或uvicorn并在app.run()中加threadedTrue。5.2 Linux 下用 systemd 管理守護(hù)進(jìn)程推薦比Supervisor更原生重啟策略更可靠# /etc/systemd/system/captcha-recognizer.service [Unit] DescriptionCaptcha Recognition Service Afternetwork.target [Service] Typesimple Userubuntu WorkingDirectory/opt/captcha-recognizer ExecStart/opt/captcha-recognizer/venv/bin/python /opt/captcha-recognizer/demo.py --modeserver Restartalways RestartSec10 EnvironmentPYTHONPATH/opt/captcha-recognizer [Install] WantedBymulti-user.target啟用命令sudo systemctl daemon-reload sudo systemctl enable captcha-recognizer.service sudo systemctl start captcha-recognizer.service sudo systemctl status captcha-recognizer.service # 查看日志journalctl -u captcha-recognizer -f關(guān)鍵參數(shù)說(shuō)明WorkingDirectory必須指向解壓目錄否則找不到data/和model/ExecStart--modeserver需在demo.py中新增命令行參數(shù)解析argparse啟動(dòng)Flask而非訓(xùn)練RestartSec10崩潰后10秒重啟避免高頻重啟打滿日志。5.3 跨平臺(tái)通用技巧模型序列化為.tflite實(shí)現(xiàn)邊緣部署若需在樹(shù)莓派、Jetson Nano等設(shè)備運(yùn)行TensorFlow SavedModel 太重。轉(zhuǎn)為TFLite# convert_to_tflite.py import tensorflow as tf # 加載SavedModel converter tf.lite.TFLiteConverter.from_saved_model(model/saved_model) # 量化減小體積、加速推理 converter.optimizations [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_ops [ tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS ] tflite_model converter.convert() # 保存 with open(model/captcha.tflite, wb) as f: f.write(tflite_model) print(TFLite model size:, os.path.getsize(model/captcha.tflite) / 1024, KB)在樹(shù)莓派上運(yùn)行# tflite_predict.py import numpy as np import tflite_runtime.interpreter as tflite from utils.preprocess import preprocess_single_image interpreter tflite.Interpreter(model_pathmodel/captcha.tflite) interpreter.allocate_tensors() input_details interpreter.get_input_details() output_details interpreter.get_output_details() img preprocess_single_image(cv2.imread(test.jpg, 0)) interpreter.set_tensor(input_details[0][index], img) interpreter.invoke() output_data interpreter.get_tensor(output_details[0][index]) print(Result:, np.argmax(output_data))實(shí)測(cè)數(shù)據(jù)SavedModel 28MB → TFLite 3.2MB樹(shù)莓派4B上單次推理從1.2s降至0.35s。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取