ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

数字验证码识别实战:Python图像预处理、CNN建模与Flask部署

2026/10/5 14:49:29 拓冰建站 浏览量
数字验证码识别实战:Python图像预处理、CNN建模与Flask部署 简介这是一份基于Python的数字验证码识别毕业设计论文面向计算机、网络安全相关专业的本科生及开发者解决粘连、扭曲且存在干扰噪声的验证码识别性能欠佳问题。论文通过对比多种识别方法确定采用KNN算法作为核心方案并按预处理、匹配识别、分析识别率三个步骤展开。预处理阶段涉及灰度化、二值化、降噪和字符分割针对分割得到四、三、二及一个字符的不同情况分别提出处理策略随后利用Python工具进行单字符匹配最终由KNN算法实现结果识别实验识别率达到94.4%。资源仅1份PDF文档压缩包大小2.78MB便于打印或电子阅读目前已有284人学习下载。文档完整涵盖摘要、绪论、算法原理、实现细节、实验数据与结论既适合作为毕业设计选题和论文写作的参考也能作为图像识别与KNN分类的入门案例帮助读者复现实验并理解完整技术路线。1. 数字验证码识别毕业论文选题的热门方向但90%的人卡在预处理深夜两点我盯着终端里那个刺眼的准确率发呆——测试集上98%的数字验证码识别准确率放到真实场景里竟然连一半都不到。这个场景在数字验证码识别项目里太常见了它不是模型的问题而是预处理和分割阶段藏着一堆反直觉的细节。基于Python的数字验证码识别是近几年本硕毕业论文里出现频率极高的题目核心目标是从一张含干扰的验证码图片中提取出一串数字跑通从图像输入到结果输出的完整流程。它适合想在毕业设计中同时体现工程能力和算法理解的学生也适合想快速验证OCR方向的从业者。很多人以为难点在模型实际上真正劝退大部分人的是预处理阶段的那些“玄学”问题。2. 验证码图像的预处理灰度化、二值化与字符分割的实现2.1 灰度化与二值化为什么Otsu阈值比固定阈值更稳验证码识别第一步不是识别而是让字符从背景里“浮”出来。数字验证码虽然结构简单但真实采集的图片可能存在彩色背景、网格干扰线、字符边框等噪声。常见的做法是先做灰度化再做二值化。这里我一般会用OpenCV处理而不是PIL因为OpenCV的threshold函数内置了Otsu自动阈值算法能省掉大量手动调参的时间。import cv2 import numpy as np # 读取图像 img cv2.imread(captcha.png) # 转灰度 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Otsu自动阈值二值化返回(阈值, 二值图) thresh_val, binary cv2.threshold( gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU ) print(fOtsu自动选择的阈值: {thresh_val}) cv2.imwrite(binary.png, binary)这段代码里最核心的参数是cv2.THRESH_BINARY_INV cv2.THRESH_OTSU。THRESH_BINARY_INV表示反转二值化把原本是黑色的字符变成白色背景变成黑色。为什么要反转因为OpenCV的findContours在白色背景下找黑色轮廓的兼容性更好统一成“白字黑底”能让后续分割流程只写一套逻辑。THRESH_OTSU则让算法自动计算分割阈值它的原理是寻找一个阈值使前景和背景两类像素的类内方差最小。固定阈值cv2.THRESH_BINARY的问题在于不同批次的验证码图片亮度可能差异很大固定阈值要么把字符和背景一起变成白色要么把背景噪点全部当成前景。建议优先用Otsu它免去了人工配置阈值的环节对毕业论文里的实验对比章节也有利。二值化之后图片里可能还有独立噪点和细小的干扰线残留。一般我这还会补一步中值滤波它的窗口大小直接影响去噪效果。# 3x3中值滤波去孤立噪点 denoised cv2.medianBlur(binary, 3) # 形态学开运算先腐蚀后膨胀消除细线干扰 kernel np.ones((2, 2), np.uint8) opened cv2.morphologyEx(denoised, cv2.MORPH_OPEN, kernel)中值滤波的核大小3不要轻易改成5或更大。验证码字符笔画宽度通常只有2到4个像素核过大会把数字笔画内部“掏空”导致后续模型提取不到完整的字符形状。开运算的kernel是2x2这个尺寸只去除1像素宽的噪点又能保留细笔画字符。如果你的验证码图片分辨率较高比如单个字符超过40x40像素kernel可以适当放大到3x3否则不要动。这些参数在毕业论文的“系统参数设置”一节中值得单独列表说明。2.2 去噪与字符分割轮廓法分割的代码实现与参数分割是把一串字符切成单个数字。毕业论文里最常见的是轮廓法——先找出图像里所有连通区域再按从左到右的顺序排序筛选出真正的字符区域。这个方法对均匀间距、无粘连的验证码非常有效也是答辩时最好讲清楚的一个环节。# 找轮廓只取外轮廓使用简单的链式逼近 contours, hierarchy cv2.findContours( opened, cv2.RETR_EXTERNAL, # 只检测外部轮廓避免内部空洞干扰 cv2.CHAIN_APPROX_SIMPLE # 压缩轮廓点降低内存 ) # 按x坐标从左到右排序 contours sorted(contours, keylambda c: cv2.boundingRect(c)[0]) char_regions [] for c in contours: x, y, w, h cv2.boundingRect(c) area w * h # 过滤面积过小的噪点保留合理宽高比的区域 if area 50 and 0.3 w / h 1.5: char_regions.append((x, y, w, h)) print(f检测到 {len(char_regions)} 个字符区域)这里三个细节需要留意。第一RETR_EXTERNAL只取外部轮廓如果验证码字符内部有空腔比如数字0、6、8的中心内部轮廓需要用RETR_CCOMP或RETR_LIST才能取到但实际分割我们只关心外部边界取内部轮廓反而会干扰字符区域计数。第二CHAIN_APPROX_SIMPLE会压缩水平、垂直和对角线方向的冗余点能有效减少计算量不会丢失字符的区域信息。第三面积过滤阈值50和宽高比范围需要根据实际图片分辨率调整这是预处理环节中“玄学”最重的地方。如果图片是180x60像素、4个字符单个字符大约35x50像素面积阈值50就很安全如果图片混有较粗的边框线边框的面积会远超字符这时候必须额外校验宽高比边框通常是贯穿整张图的长条形宽高比远超1.5自然会被过滤掉。分割的最终产物是4张或5张独立字符的小图。我建议在分割后统一缩放到固定尺寸比如32x48像素这样后续输入到CNN时不需要处理变长输入的问题。resized_chars [] for x, y, w, h in char_regions: # 扩大一个像素避免切掉字符边缘笔画 pad 1 x1 max(0, x - pad) y1 max(0, y - pad) x2 min(opened.shape[1], x w pad) y2 min(opened.shape[0], y h pad) char_img opened[y1:y2, x1:x2] # 统一缩放为32x48保持宽高比不变用0填充边距 scale min(32 / char_img.shape[1], 48 / char_img.shape[0]) new_w int(char_img.shape[1] * scale) new_h int(char_img.shape[0] * scale) resized cv2.resize(char_img, (new_w, new_h), interpolationcv2.INTER_NEAREST) canvas np.zeros((48, 32), dtypenp.uint8) x_offset (32 - new_w) // 2 y_offset (48 - new_h) // 2 canvas[y_offset:y_offset new_h, x_offset:x_offset new_w] resized resized_chars.append(canvas)注意cv2.resize的interpolation参数建议用INTER_NEAREST而不是INTER_LINEAR。二值图只有0和255两个取值线性插值会产生中间灰度值破坏“白字黑底”的二值属性。缩放到32x48一是参考了经典手写数字识别中28x28的经验二是给高瘦型数字比如1留出横向边距避免缩放时变形。这一步完成后每个字符都是一张标准化的二值图可以直接作为CNN的输入。3. 特征提取与模型选型从模板匹配到CNN的路径选择3.1 为什么毕业论文选CNN而不是模板匹配做完分割下一步就是识别。很多毕设论文会把模板匹配列出来做对比实验这是很常规的操作——模板匹配的思路是把每个数字的模板图与待识别字符做逐像素相似度计算选最相似的作为结果。实现极其简单几十行代码就能跑通。但缺陷也很明显模板匹配对字体、旋转、缩放、笔画粗细的变化几乎没有鲁棒性只要验证码的生成方式换一种字体准确率立刻跌到一半以下。另一种常见中间方案是HOG特征SVM。HOG方向梯度直方图提取的是字符的局部梯度分布特征比模板匹配对形变的容忍度高一些但遇到强干扰背景时特征容易失真。CNN的核心优势在于它把“特征工程”这件事也交给网络学习不需要手动设计特征提取器。数字验证码字符类别只有10类0到9参数量不大但CNN提供的局部感受野机制本身就更适合图像识别这是模板匹配和HOG特征在原理上没法比的。为了毕业论文的对比表建议至少跑一组“模板匹配 vs CNN”的实验把准确率差距和推理时间差距列出来作为方案选型的依据。方案实现成本对形变压制对噪点鲁棒性推理速度论文说服力模板匹配极低差差极快弱HOG SVM中中中快中CNNLeNet变体中高强较强中GPU/CPU均可强个人建议如果毕业设计时间只有一个月直接上CNN如果时间充裕可以做“模板匹配作为baseline、CNN作为主要方案”的对比实验这个结构在论文里说服力最强。3.2 一个能跑的LeNet变体网络结构与PyTorch实现LeNet-5是1998年杨立昆提出的经典卷积网络原版是针对32x32灰度图的。验证码字符图不是手写数字那样完整居中地出现在画面上字符图片里会有边距、偏移和轻微的笔画变形所以我一般把LeNet稍作修改把第一层卷积核从5x5改成3x3增加一层卷积让网络在保持参数量的前提下获得更大的有效感受野同时提高对变形字符的适应能力。这是一个“基础结构 小改动”的思路适合写进论文的“网络结构设计”一节既显得有理有据又不至于为了堆参数把网络弄成一个难以复现的巨型结构。import torch import torch.nn as nn class CaptchaNet(nn.Module): 数字验证码识别网络4位数字每位10分类 def __init__(self, num_digits4, num_classes10): super().__init__() # 输入: 1x48x32 的二值图 self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 24x16 nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 12x8 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 6x4 ) # 展平后是 128*4*8 4096 self.classifier nn.Sequential( nn.Linear(128 * 4 * 8, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(128, num_digits * num_classes) # 4*1040 ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) # 拆成4个独立的10分类头 return x.view(-1, 4, 10)这段网络结构有三个设计点值得在论文里展开。第一最后一层全连接输出40维然后view(-1, 4, 10)拆成4个独立的数字分类头每个头对应验证码中的一个字符位置。这样设计的优势是一次前向推理就输出整串验证码的预测结果不需要循环预测4次。第二Dropout放在全连接层之间比例设为0.5能有效防止对训练集过拟合——验证码识别任务经常在几千张图上反复迭代全连接层很容易把噪声也背下来。第三padding1的3x3卷积让特征图的高宽在卷积后保持不变配合池化缩小尺寸最终压到6x4的特征图对应字符图中的全局信息。如果你的验证码字符数不是4位改num_digits参数即可不用动网络主体。4. 训练与评估数据集构建、训练参数与准确率验证4.1 自建数据集的两种方式和标注要点数字验证码识别的数据集和传统公开数据集比如MNIST有一个本质差别验证码种类繁多字体、颜色、干扰方式各不相同公开数据集比较少见。毕业论文里最常见的数据集构建方式是自己写生成器——用PIL随机生成数字并加上干扰元素。这非常合理因为验证码本身就是机器生成的我们可以模拟它的生成规律生成海量带标签的训练样本。from PIL import Image, ImageDraw, ImageFont import random import os def generate_captcha(save_path, font_path, width180, height60, num_digits4): 生成一张带干扰的数字验证码图片返回图片文件路径和标签 # 随机背景底色 bg_color (random.randint(180, 240), random.randint(180, 240), random.randint(180, 240)) img Image.new(RGB, (width, height), bg_color) draw ImageDraw.Draw(img) # 选字体数字部分随机偏移 font ImageFont.truetype(font_path, random.randint(28, 36)) code .join(str(random.randint(0, 9)) for _ in range(num_digits)) # 绘制每个数字带随机位置偏移和颜色 x_cursor random.randint(10, 20) for ch in code: color (random.randint(0, 80), random.randint(0, 80), random.randint(0, 80)) y_off random.randint(-5, 5) draw.text((x_cursor, 10 y_off), ch, fontfont, fillcolor) x_cursor random.randint(35, 42) # 画干扰线 for _ in range(random.randint(3, 6)): x1, y1 random.randint(0, width), random.randint(0, height) x2, y2 random.randint(0, width), random.randint(0, height) draw.line((x1, y1, x2, y2), fill(random.randint(90, 150),) * 3, width1) # 文件名格式:标签.png img.save(os.path.join(save_path, f{code}.png)) return code # 使用示例 generate_captcha(./train_data/, font_patharial.ttf)这段生成器的设计逻辑和一个典型生成器有三处不同。第一字符颜色统一在0到80之间的深色区间背景色在180到240之间的浅色区间保证字符和背景的可分性直接降低训练难度——现实中获取的数据不一定这么干净但毕业论文里先用可控数据验证模型结构再放到真实环境这是让训练流程更稳定的常用做法。第二字体大小随机在28到36之间模拟字符大小的变化。第三干扰线颜色取90到150之间的中间灰度让干扰线介于字和背景之间训练时模型必须学习“忽略中间灰度噪声”。除了生成器另一个数据集来源是爬取真实网站的验证码图片然后人工标注。这个方法非常耗时我不建议学生在毕设阶段做这种脏活累活除非导师有明确要求。如果一定要用真实数据记得收集至少2000张并保证每个数字出现次数均衡否则模型会对出现频率高的数字产生偏置。4.2 训练循环与关键参数学习率、batch size与早停训练环节参数的选择常常被毕设学生忽略但准确率基本由这几个参数决定。我没有用复杂的分布式训练或混合精度就是一个标准的PyTorch训练循环重点在设置好学习率策略和早停机制。import torch import torch.optim as optim from torch.utils.data import DataLoader, Dataset class CaptchaDataset(Dataset): 读取生成好的验证码图片预处理并返回(样本, 标签) def __init__(self, image_dir, transformNone): self.image_paths glob.glob(os.path.join(image_dir, *.png)) self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): path self.image_paths[idx] # 文件名是数字标签如 3521.png label os.path.basename(path).split(.)[0] label [int(c) for c in label] # [3,5,2,1] img cv2.imread(path, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) if self.transform: binary self.transform(binary) return binary, torch.tensor(label) # 训练配置 device torch.device(cuda if torch.cuda.is_available() else cpu) model CaptchaNet(num_digits4).to(device) optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3) criterion nn.CrossEntropyLoss() # 早停参数 best_acc 0.0 patience_counter 0 patience_limit 5 for epoch in range(20): model.train() train_loss 0.0 for x_batch, y_batch in train_loader: x_batch torch.FloatTensor(x_batch).unsqueeze(1).to(device) / 255.0 y_batch y_batch.to(device) optimizer.zero_grad() outputs model(x_batch) # outputs形状: [batch, 4, 10], 需要转成两维计算loss loss criterion(outputs.view(-1, 10), y_batch.view(-1)) loss.backward() optimizer.step() train_loss loss.item() # 每个epoch后在验证集上算整串准确率 val_acc evaluate(model, val_loader, device) print(fEpoch {epoch1}: train_loss{train_loss/len(train_loader):.4f}, val_acc{val_acc:.4f}) # 早停逻辑 if val_acc best_acc: best_acc val_acc patience_counter 0 torch.save(model.state_dict(), best_model.pth) else: patience_counter 1 if patience_counter patience_limit: break这段代码中学习率1e-3是Adam优化器在图像分类任务中一个非常稳的起点。如果你的训练集只有几千张图不建议用更大的学习率否则loss会震荡不收敛。ReduceLROnPlateau会在验证集准确率连续3个epoch不提升时将学习率减半这种“训练后期小步走”的策略能帮模型在最优值附近收敛得更精准。早停的patience_limit5意味着连续5个epoch验证集准确率没有刷新记录就停止训练这个机制能有效避免过拟合——在毕设项目里生成的训练数据量通常不够大训练轮数超过20后模型大概率会在训练集上无限降低loss但验证集表现反而变差。我建议把验证集划到总数据量的20%并在训练阶段就统计“整串识别正确率”而不是“单字识别正确率”。整串正确率才是衡量最终效果的金标准单字正确率99%不代表4位验证码的整串正确率高。def evaluate(model, loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for x_batch, y_batch in loader: x_batch torch.FloatTensor(x_batch).unsqueeze(1).to(device) / 255.0 outputs model(x_batch) preds outputs.argmax(dim-1) # [batch, 4] for i in range(len(preds)): if (preds[i] y_batch[i].to(device)).all(): correct 1 total 1 return correct / total整串准确率评估函数里argmax(dim-1)在最后一维类别维度上取最大概率对应的类别索引。(preds[i] y_batch[i]).all()是判断4个字符全部预测正确的唯一条件。毕业论文里把“单字准确率”和“整串准确率”两个指标都写出来能显著提升实验的可信度。5. 识别阶段的避坑清单5个真实翻车现场5.1 测试集准确率98%真实环境却频繁识别失败现象模型在自己生成的测试集上准确率达到98%但用手机拍屏幕或从线上环境截取的真实验证码图片测试时准确率骤降到50%以下。原因训练数据分布和测试数据分布不一致。生成的验证码图片背景干净、字符颜色统一真实场景中可能有摩尔纹、背景渐变、字符被干扰线穿过等情况。模型学到了“对这种干净图像的识别规则”并没有学到真正的泛化能力。解决在生成器中加入更多干扰类型比如不同背景纹理、字符噪点。更有效的做法是在预处理阶段增加随机扰动随机平移、轻微旋转、随机亮度变化做数据增强让模型被迫面对多样化的输入。提示数据增强时旋转角度控制在±10度以内。验证码字符旋转超过15度后数字1和7、3和8的边界开始模糊反而增加识别难度。5.2 字符粘连导致分割直接丢字符现象预处理后得到的分割结果只有3个字符区域但验证码明明有4个数字。原因字符间距太密或干扰线将两个字符连成了一个连通域findContours只检测到一个大轮廓于是输出3个区域。解决先检查二值化后的图是否还有横线残留。如果确认是字符粘连可以用垂直投影法辅助分割——统计每列黑色像素个数找到投影值为0的“谷底”作为分割点。另一种常用做法是把粘连区域单独切下来送入网络让网络直接输出两个字符但这个方法实现成本高毕设阶段不推荐。建议优先微调生成器把字符间距调大到不会粘连的程度先保证主流程跑通再在论文中把粘连分割作为待改进问题。5.3 归一化忘做loss降不下去现象训练loss在1.5左右徘徊无论怎么调学习率就是不下降。原因输入的图像像素值是0到255的整数直接喂给网络后梯度计算受过大数值影响权重更新不稳定。CNN内部虽然BatchNorm能部分缓解这个问题但图像输入层不做归一化第一层卷积的梯度还是会被放大。解决在训练循环里加上x_batch / 255.0这行代码。注意不要在数据集类里改原始图而是要在输入网络前转为torch.FloatTensor后归一化避免影响预处理阶段的二值化判断。5.4 二值化后噪点全部变成白色噪声现象二值化后图片里出现大量白色小颗粒字符轮廓反而被淹没。原因Otsu阈值在处理“字符颜色深、背景颜色浅”的图片时效果很好但如果验证码的背景本身就是深色底或者背景里有渐变元素Otsu会把部分背景误判为前景。解决先用cv2.GaussianBlur做一次轻度模糊kernel 3x3再进threshold或者改用自适应阈值cv2.adaptiveThreshold。自适应阈值会计算每个像素邻域的局部阈值对渐变背景的鲁棒性更强。不过自适应阈值速度较慢在毕设数据集规模下问题不大可以直接换过去。5.5 不足4个字符的图片被强行预测成4位数字现象真实测试中偶尔遇到只有3位数字甚至1位数字的图片模型仍然输出4个字符后几位全是“幻觉”。原因网络被设计成固定输出4个字符训练时没见过“不足4位”的样本。输入一个实际只有3位字符的图时模型基于图像中最显著的特征硬猜第四位。解决训练时人为构造一批“不足4位”的负样本在标签中给空缺位置填充为某一固定值比如用10表示空位然后num_classes11或单独加一个空白类别。或者在推理阶段增加一个“置信度门槛”逻辑如果某个字符预测概率低于0.7判定该位置为空输出位数随之减少。推荐后者改动小而且能在论文里增加一个“低置信度样本分析”的小节。6. 把识别服务封装成接口Flask部署与端到端验证6.1 模型加载与单张图片预测脚本训练完成后需要一套独立的推理脚本把预处理、分割、模型预测串起来。这个脚本不仅是答辩时演示用的工具也是后期做接口封装的底层模块。import torch from PIL import Image import cv2 import numpy as np def predict_single(image_path, model, device): 输入图片路径返回识别结果和置信度 # 1. 读图 预处理 img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) denoised cv2.medianBlur(binary, 3) # 2. 分割沿用之前的分割逻辑 contours, _ cv2.findContours(denoised, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours sorted(contours, keylambda c: cv2.boundingRect(c)[0]) char_crops [] for c in contours: x, y, w, h cv2.boundingRect(c) if w * h 50 and 0.3 w / h 1.5: char_crops.append(denoised[y:yh, x:xw]) # 3. 统一缩放到32x48堆叠成tensor batch [] for crop in char_crops: char cv2.resize(crop, (32, 48), interpolationcv2.INTER_NEAREST) batch.append(char) batch_tensor torch.FloatTensor(np.array(batch)).unsqueeze(1).to(device) / 255.0 # 4. 预测 置信度 with torch.no_grad(): outputs model(batch_tensor) probs torch.softmax(outputs, dim-1) confs, preds torch.max(probs, dim-1) code .join(str(p.item()) for p in preds) avg_conf confs.mean().item() return code, avg_conf这个脚本把训练时用的预处理流程原样搬了过来。注意模型推理时用torch.softmax拿到概率分布而不是直接argmax因为我们需要置信度来辅助判断。如果分割出的字符数不等于4脚本也会正常输出实际长度不会报错。6.2 Flask接口与多张图片批量验证最后一个环节是让识别脚本暴露成一个HTTP接口方便调用或者对接前端演示页面。用Flask写这个接口非常直接这也是毕设答辩“系统实现”部分最加分的展示方式——现场打开一个网页或发一个POST请求就能看到识别结果。写一下加载模型和服务启动的逻辑from flask import Flask, request, jsonify import io import numpy as np from PIL import Image import cv2 import torch app Flask(__name__) device torch.device(cuda if torch.cuda.is_available() else cpu) model CaptchaNet(num_digits4).to(device) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model.eval() app.route(/predict, methods[POST]) def predict(): # 接收上传的图片文件 file request.files[image] img_bytes np.frombuffer(file.read(), np.uint8) img cv2.imdecode(img_bytes, cv2.IMREAD_COLOR) # 保存临时文件给预测函数用 tmp_path /tmp/tmp_captcha.png cv2.imwrite(tmp_path, img) code, conf predict_single(tmp_path, model, device) return jsonify({code: code, confidence: conf, success: True}) if __name__ __main__: app.run(host0.0.0.0, port5000)接口的返回格式是JSON包含识别结果和置信度这个结构在毕业论文的“系统测试”章节可以直接贴上返回示例。测试时用requests库发送一个POST请求就能完成验证。批量验证时建议准备20到50张真实图片写个循环统计整串准确率这和训练时的评估函数保持一致。这个部署方案做毕业设计已经足够不需要上Docker或者云端部署。答辩时把模型权重文件和推理脚本放在一起就能现场演示省去繁琐的环境配置。最后说一个我的习惯捕获所有图片预测的输入输出日志保存在本地记录里只留近三天的量。验证码识别的数据分布会随着网站改版而变化保留日志能帮我快速定位是哪一类新图片导致准确率下降。这个不起眼的习惯曾经在一次演示前帮我提前发现了一个字体变更导致的系统性识别错误及时调整了训练数据避免在台上翻车。希望这篇东西帮到你也祝你顺利跑通自己的数字验证码识别系统。本文还有配套的精品资源点击获取