Python验证码识别最小可行系统:本地可复现的CNN训练推理闭环 简介本资源是一套基于Python与TensorFlow实现的验证码图像识别完整训练与调用方案面向具备基础Python编程能力及机器学习入门知识的开发者解决实际业务中常见的简单验证码自动识别需求。压缩包共2000个文件主体为1457张标注用JPG验证码样本、297个核心Python脚本含数据预处理、模型构建、训练与推理调用模块辅以JS前端交互示例、EXE可执行工具及SVG/CSS等配套资源整体体积26.02MB结构清晰便于快速定位训练流程与部署环节。目前已有440人学习下载。读者可直接复用训练代码与素材完成端到端建模获取已验证有效的模型文件.pth、.ckpt、完整虚拟环境配置pyvenv.cfg、activate.bat、调试缓存与编译产物.pyd/.dll/.pdb并参考多语言混合工程结构含.csproj、.sln、.cs等理解跨平台集成思路。1. 这不是“跑通就行”的验证码识别Demo它是一套可复现、可调试、带完整训练-推理闭环的Python图像识别最小可行系统你见过太多“pip install完就报错”“train.py一运行就OOM”“predict.py调用模型时提示shape不匹配”的所谓“Python图像识别源码”了吧这个.rar包里没有一句“请自行安装依赖”没有一个“环境配置见README但根本没写”的玄学指引。它用纯 Python TensorFlow 实现了从原始验证码图片采集、预处理、CNN模型定义、训练循环、权重保存到最终封装成可直接import调用的 inference 模块——整条链路全部落地在本地 Windows 环境下且所有脚本都经过实测Python 3.8.10 TensorFlow 2.11.0 CUDA 11.2连activate.bat和deactivate.bat都是真实可用的虚拟环境启停脚本不是摆设。它解决的不是“能不能识别”而是“你能不能在自己机器上从零开始把一张乱码图喂进去三分钟内拿到预测结果”。适合刚学完 Keras 基础、正卡在“模型训完怎么用”这个坎上的工程师也适合需要快速验证 OCR 类小任务 pipeline 是否健壮的算法侧同学——它不炫技不堆模块只做一件事让图像识别这件事在你本地电脑上真正“动起来”。2. 从压缩包结构看设计逻辑为什么这6个.bat和3个.cache文件比代码还关键这个.rar包表面是“Python实现图像识别”但真正决定你能否复现成功的其实是它对开发环境的显式契约化声明。我们先解压观察根目录结构├── activate.bat ├── deactivate.bat ├── DesignTimeResolveAssemblyReferencesInput.cache ├── demo.csprojResolveAssemblyReference.cache ├── demo.csproj.GenerateResource.Cache ├── DesignTimeResolveAssemblyReferences.cache ├── demo.csproj.CoreCompileInputs.cache ├── sysconfig.cfg ├── pyvenv.cfg ├── train.py ├── predict.py ├── model/ │ └── best_model.h5 ├── data/ │ ├── train/ │ │ ├── 0/ # 标签为0的样本 │ │ ├── 1/ # 标签为1的样本 │ │ └── ... │ └── test/ ├── utils/ │ ├── preprocess.py │ └── dataset_loader.py注意.csproj.*.cache文件看似是 Visual Studio 的残留实则是该工程曾用 C# 做过前端或数据标注工具的痕迹但不影响Python主流程。它们的存在恰恰说明这套方案经历过真实业务场景——验证码来源不是网上爬的而是有配套标注流程的。2.1activate.bat与pyvenv.cfg环境隔离不是可选项是启动前提打开activate.bat内容极简echo off call venv\Scripts\activate.bat python -c import tensorflow as tf; print(TF version:, tf.__version__) pause而pyvenv.cfg明确指定了 Python 路径和是否包含系统 site-packageshome C:\Python38 include-system-site-packages false version 3.8.10这说明作者强制要求使用 Python 3.8.10且禁用全局包污染。你不能直接pip install tensorflow到系统 Python必须用venv\Scripts\python.exe执行所有命令。这是避免“我本地能跑你本地报错”的第一道防线。2.2sysconfig.cfg隐藏的 CUDA 适配开关该文件并非标准 Python 配置而是作者手动添加的 CUDA 版本锚点[build] cuda_version 11.2 cudnn_version 8.1它不被 Python 解释器读取但train.py开头会做校验# train.py 第12行 import os if os.environ.get(CUDA_VISIBLE_DEVICES, ) : os.environ[CUDA_VISIBLE_DEVICES] 0 # 强制绑定GPU0 # 后续加载模型前会读取 sysconfig.cfg 并 warn 若 cuda_version 不匹配这意味着如果你用的是 RTX 4090需 CUDA 12.x或 M1 Mac无 CUDA这套代码默认会 fallback 到 CPU 训练——但速度会慢 8~10 倍。这不是 bug是设计它用配置文件把硬件依赖显性化而不是让错误发生在model.fit()的第 37 个 epoch。2.3demo.csproj.*.cache的真实用途标注数据生成器的编译产物这些.cache文件来自一个未打包进.rar的 C# 工具LabelTool.exe其功能是从网页截图中自动裁剪单个字符区域人工点击确认标签0-9, a-z输出为data/train/{label}/xxx.png格式.csproj.*.cache是 VS 编译该工具时生成的中间文件证明训练集不是随便找的公开数据集而是按真实验证码样式扭曲、粘连、噪点人工标注的。这也是它识别准确率原文博客提到测试集 92.3%远超通用 OCR 库的原因——数据域高度对齐。3. 训练流程拆解train.py里的四个不可跳过的预处理硬编码train.py全长仅 217 行但核心逻辑集中在load_and_preprocess_data()和build_model()两个函数。它没用tf.data.Dataset的高级 API而是用最直白的cv2numpy流水线确保每一步都可 debug。3.1 图像预处理不是 resize 就完事而是四步归一化# utils/preprocess.py def preprocess_image(img_path): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 强制灰度 img cv2.resize(img, (64, 32)) # 统一分辨率宽64×高32 img cv2.GaussianBlur(img, (3, 3), 0) # 模糊去椒盐噪点 _, img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 大津法二值化 img img.astype(np.float32) / 255.0 # 归一化到 [0,1] return img.reshape(32, 64, 1) # 添加通道维度关键参数说明resize目标(64, 32)是硬编码——因为验证码字符宽度约 12px高度约 24px留出 padding 后定为 64×32不是随意选的GaussianBlur的(3,3)核大小针对的是该数据集中高频出现的“细颗粒噪点”若换其他验证码需调为(5,5)THRESH_OTSU自动计算阈值比固定127更鲁棒但要求图像整体对比度足够——这也是为什么前面要GaussianBlur先平滑。3.2 模型架构轻量级 CNN专为单字符识别设计# train.py def build_model(input_shape(32, 64, 1), num_classes36): # 0-9 a-z 36类 model tf.keras.Sequential([ tf.keras.layers.Conv2D(32, (3, 3), activationrelu, input_shapeinput_shape), 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), tf.keras.layers.Dense(num_classes, activationsoftmax) ]) model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy]) return model为什么不用 ResNet 或 EfficientNet验证码字符尺寸小32px深层网络会丢失细节Dropout(0.5)放在全连接层前是因为训练集仅 2000 张/类过拟合风险高sparse_categorical_crossentropy对应labels是整数如5而非 one-hot如[0,0,0,0,0,1,...]节省内存。3.3 训练循环没有 fancy callback只有ModelCheckpoint和EarlyStopping# train.py callbacks [ tf.keras.callbacks.ModelCheckpoint( filepathmodel/best_model.h5, monitorval_accuracy, save_best_onlyTrue, modemax ), tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) ] history model.fit( x_train, y_train, batch_size32, epochs100, validation_data(x_val, y_val), callbackscallbacks )batch_size32是平衡显存占用GTX 1060 6GB与梯度稳定性后的实测值patience10意味着验证损失连续 10 个 epoch 不下降才停防止过早终止restore_best_weightsTrue确保最终模型是验证集 accuracy 最高的那版不是最后 epoch 的。4. 推理调用实战predict.py如何绕过 TensorFlow 的版本陷阱训练完的model/best_model.h5是 Keras 原生格式但直接tf.keras.models.load_model()在不同 TF 版本下可能报Unknown layer: Conv2D。作者用了一个更底层、更稳定的加载方式4.1predict.py的安全加载模式# predict.py import tensorflow as tf import numpy as np from utils.preprocess import preprocess_image def load_model_safe(model_path): # 方式1尝试直接加载TF 2.11 try: return tf.keras.models.load_model(model_path) except Exception as e: print(fDirect load failed: {e}) # 方式2手动重建模型结构 加载权重兼容性最强 model build_model() # 重新调用 build_model() model.load_weights(model_path.replace(.h5, _weights.h5)) # 注意实际权重另存为 _weights.h5 return model model load_model_safe(model/best_model.h5)但.rar包里只有best_model.h5没有_weights.h5——这是因为train.py末尾做了转换# train.py 末尾 model.save(model/best_model.h5) # 保存完整模型 model.save_weights(model/best_model_weights.h5) # 同时保存权重所以predict.py中的model_path.replace(...)是真实存在的。这种双保险策略让代码能在 TF 2.8 ~ 2.13 全系列运行。4.2 单图预测从路径到结果三行代码搞定# predict.py def predict_single_image(img_path): img preprocess_image(img_path) # 预处理输出 (32,64,1) img_batch np.expand_dims(img, axis0) # 增加 batch 维度 → (1,32,64,1) pred model.predict(img_batch) # 输出 shape: (1,36) class_id np.argmax(pred[0]) # 取概率最大索引 labels [str(i) for i in range(10)] [chr(i) for i in range(97, 123)] # 0-9,a-z return labels[class_id], float(np.max(pred[0])) result, confidence predict_single_image(data/test/5/abc123.png) print(fPredicted: {result}, Confidence: {confidence:.3f})np.expand_dims(img, axis0)是必须的因为模型输入要求 batch 维度labels列表顺序必须与训练时y_train的编码顺序严格一致0→0, 1→1, ..., 10→aconfidence是 softmax 输出的最大概率值不是准确率而是模型对该预测的置信度——低于 0.7 时建议人工复核。4.3 批量预测用os.listdir替代tf.data只为可控# predict.py def predict_batch(folder_path, label_mapNone): results [] for img_file in os.listdir(folder_path): if not img_file.lower().endswith((.png, .jpg, .jpeg)): continue full_path os.path.join(folder_path, img_file) try: pred_char, conf predict_single_image(full_path) results.append({ filename: img_file, prediction: pred_char, confidence: conf }) except Exception as e: results.append({ filename: img_file, prediction: ERROR, confidence: 0.0, error: str(e) }) return results为什么不直接tf.data.Dataset.list_files()os.listdir抛异常时能精准定位到哪张图出问题label_map参数预留了与真实业务对接的入口比如传入{5: number_5, a: letter_a}返回list of dict方便后续用pandas.DataFrame(results)做统计分析。5. 避坑指南我在三台不同配置机器上踩过的5个真实坑这套代码看似简单但在不同环境复现时有5个坑我反复栽过每次都要重装环境或改代码。以下是血泪经验总结按发生频率排序5.1 现象activate.bat双击后窗口闪退python -c import cv2报DLL load failed原因OpenCV 的 DLL 依赖与系统 PATH 冲突尤其当电脑装过 Anaconda 或旧版 OpenCV 时venv\Scripts\python.exe仍会优先加载系统路径下的opencv_python-*.dll。解决删除venv\Lib\site-packages\cv2目录在venv\Scripts\activate.bat末尾添加set PATH%~dp0..\\Lib\\site-packages\\cv2\\; %PATH%重新pip install opencv-python4.5.5.64该版本与 TF 2.11 兼容性最佳。5.2 现象train.py运行到model.fit()时报ValueError: Input 0 is incompatible with layer conv2d...原因preprocess_image()返回的 shape 是(32,64,1)但模型input_shape写成了(64,32,1)宽高颠倒。解决检查utils/preprocess.py第17行return img.reshape(32, 64, 1)——必须是height,width,channel即(32,64,1)与build_model(input_shape(32,64,1))严格一致。很多教程写反了这里必须按 OpenCV 的cv2.resize(img, (w,h))习惯来。5.3 现象训练 10 个 epoch 后val_accuracy停在 0.111≈1/36完全不学习原因y_train和y_val是字符串标签如a,5但model.fit()传入的是sparse_categorical_crossentropy要求整数标签如10,5。作者用了LabelEncoder但utils/dataset_loader.py中漏写了fit_transform导致所有标签被映射为0。解决在dataset_loader.py的load_dataset()函数里找到le LabelEncoder()后必须加y_train_encoded le.fit_transform(y_train) # 注意是 fit_transform不是 transform y_val_encoded le.transform(y_val)5.4 现象predict.py识别结果全是0且confidence恒为0.999原因model.predict()返回的是(1,36)但np.argmax(pred[0])取的是第一个样本的 argmax —— 这没错问题出在labels列表顺序与训练时LabelEncoder的classes_顺序不一致。解决在train.py保存模型后追加一行np.save(model/label_classes.npy, le.classes_) # 保存编码映射然后在predict.py中labels np.load(model/label_classes.npy).tolist()5.5 现象GPU 显存占用 100%但nvidia-smi显示python.exe进程 GPU 利用率 0%原因TensorFlow 默认启用memory growth但某些驱动版本下会卡死。pyvenv.cfg里version 3.8.10对应的 TF 2.11 需要显式禁用。解决在train.py开头添加gpus tf.config.experimental.list_physical_devices(GPU) if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, False) # 关键设为 False print(GPU memory growth disabled) except RuntimeError as e: print(e)6. 进阶技巧如何把predict.py改造成 Web API并支持多字符验证码端到端识别这套代码默认只识别单字符每个文件一个字符但真实验证码是 4~6 位组合。我把它升级为端到端服务时没碰train.py只改了predict.py和加了一个api.py效果立竿见影。6.1 步骤1用pytesseract做字符切分预处理真实验证码常有粘连单字符模型会失效。我在predict.py里加了一个split_captcha()函数# predict.py import pytesseract from PIL import Image def split_captcha(img_path): 用 Tesseract 定位字符边界返回每个字符的 ROI 图像路径列表 img Image.open(img_path) # 获取字符 box 坐标Tesseract 会返回每个字符的 bounding box boxes pytesseract.image_to_boxes(img, config--psm 8 -c tessedit_char_whitelist0123456789abcdefghijklmnopqrstuvwxyz) # psm 8 单行文本whitelist 限定字符集 rois [] for box in boxes.splitlines(): parts box.split() if len(parts) 5: char, x1, y1, x2, y2 parts[0], int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]) # 转换为 OpenCV 坐标系y轴翻转 h img.height roi np.array(img.crop((x1, h-y2, x2, h-y1))) rois.append(roi) return rois注意pytesseract需提前下载tessdata推荐eng.traineddata并设置pytesseract.pytesseract.tesseract_cmd rC:\Program Files\Tesseract-OCR\tesseract.exe。6.2 步骤2批量预测封装成predict_captcha()def predict_captcha(img_path): 端到端识别切分 单字符预测 拼接结果 rois split_captcha(img_path) if not rois: return SPLIT_FAILED, 0.0 preds [] confs [] for i, roi in enumerate(rois): # 将 PIL.Image 转 OpenCV 格式 roi_cv cv2.cvtColor(np.array(roi), cv2.COLOR_RGB2GRAY) # 调用原单字符预测 char, conf predict_single_image_from_array(roi_cv) # 新增函数接受 array 而非 path preds.append(char) confs.append(conf) final_pred .join(preds) avg_conf np.mean(confs) return final_pred, avg_conf def predict_single_image_from_array(img_array): 支持直接传入 numpy array避免重复读盘 img cv2.resize(img_array, (64, 32)) img cv2.GaussianBlur(img, (3,3), 0) _, img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) img img.astype(np.float32) / 255.0 img img.reshape(32, 64, 1) img_batch np.expand_dims(img, axis0) pred model.predict(img_batch) class_id np.argmax(pred[0]) labels np.load(model/label_classes.npy).tolist() return labels[class_id], float(np.max(pred[0]))6.3 步骤3用 Flask 暴露 HTTP 接口api.py# api.py from flask import Flask, request, jsonify from predict import predict_captcha app Flask(__name__) app.route(/predict, methods[POST]) def api_predict(): if image not in request.files: return jsonify({error: No image provided}), 400 file request.files[image] temp_path ftemp_{int(time.time())}.png file.save(temp_path) try: result, confidence predict_captcha(temp_path) return jsonify({ prediction: result, confidence: round(confidence, 3), status: success }) except Exception as e: return jsonify({error: str(e)}), 500 finally: if os.path.exists(temp_path): os.remove(temp_path) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 生产环境关 debug启动命令python api.py调用示例curlcurl -X POST http://localhost:5000/predict \ -F image./test_captcha.png # 返回{prediction:a7x9,confidence:0.824,status:success}6.4 关键参数对照表不同场景下的推荐配置场景split_captcha()的psmbatch_sizeconfidence阈值备注单字符训练集原包不适用32不校验直接用predict_single_image()粘连验证码如极验psm8160.6psm8强制单行适合横向排列扭曲验证码如百度psm780.5psm7单文本行容忍轻微旋转移动端截图低分辨率psm1040.4psm10单字符适合模糊图从那以后我每次接到新验证码识别需求都强制走一遍这三步用split_captcha()可视化切分结果加cv2.imshow()看 ROI 是否准确对每个 ROI 跑predict_single_image_from_array()记录confidence分布只有当所有字符confidence 0.6时才返回拼接结果否则标记REVIEW_REQUIRED。这套流程让我在三个项目里把误识率从 12% 降到 1.7%代价只是多写 47 行代码。希望帮到你。本文还有配套的精品资源点击获取