Python课设实战:文字点选验证码识别与自动点击全流程
简介这是一套用于Python课程设计的文字点选验证码识别方案面向需要完成选字类验证码课设或入门OCR方向的学生与开发者。整个工程围绕“点击选择文字”这一交互式验证码场景展开覆盖从训练样本准备、模型加载、图像预处理到识别结果输出的完整链路。方案基于小样本训练仅用约300张验证码图片即可达到96%准确率单次识别约100300ms并已在Windows下的Python 3.6、3.8、3.10环境中测试通过。代码经编译后可在1核2G低配服务器上稳定运行适合部署演示。压缩包共48个文件约121.82MB主要包含19个py源码文件、10个png图片样本、4个pyd编译模块、4个jpg示意图、bin模型文件、txt/md说明文档以及Web服务相关配置等另有动图演示便于快速核对效果。已有437人学习下载适合作为课设参考、算法复现或后续扩展的基础工程。1. 为什么点选验证码是Python课设里的“硬骨头”文字点选验证码是现在很多业务系统还在用的交互式验证码页面上给出一张带文字的图片上面散落着若干汉字或词语用户必须按照提示文字的顺序逐个点击图片中对应的文字。对键盘侠来说这就是拼眼力对写Python课设的人来说这就是一条完整的“图像检测 OCR识别 坐标映射”流水线。很多人初看觉得简单不就是找到文字再点吗真上手才发现干扰线、字体变形、文字旋转、背景纹理能把一个看似入门级的目标检测题变成调参玄学。这篇笔记就是要把“点击选择文字验证码识别”这条链路讲透从方案选型、数据集制作、模型训练到避坑和演示全部落到可复现的代码和参数上。适合正在做Python大作业、毕业设计或想练手计算机视觉的同学也适合想快速评估“这个方向值不值得投入时间”的工程师。接下来直接从选型开始。2. 先选方案为什么模板匹配在点选验证码上必然翻车2.1 模板匹配的问题位置变化和字体变形很多人在刚接到“文字点选验证码识别”这个需求时第一反应是用模板匹配。理由很朴素提示文字是给定的我提前把每个字的模板截下来在图片里滑窗找相似度不就行了。模板匹配在纯白底、字间距大、字体固定的验证码上确实能跑。但点选验证码为了防脚本通常不会让你这么舒服文字会旋转、加粗、加干扰线、用不同背景纹理甚至同一个字在不同图片里用了不同字体。模板匹配的本质是像素级相关性计算一旦字体轮廓或颜色变化超过阈值相似度就断崖下跌。我见过一组实测数据同一字体、不同旋转角度下模板匹配的置信度能从0.9掉到0.5以下完全不够用来做点击决策。2.2 目标检测负责“在哪”OCR负责“是什么”所以现在做文字点选验证码主流做法是拆成两个子问题先用目标检测模型把图片里每个候选文字的位置框出来再对每个框里的文字做识别得到“文字内容 框坐标”的列表。最后拿提示文字去这个列表里匹配找到对应的坐标再换算成点击位置。这正好是两条成熟技术线的组合目标检测YOLOv5/v8、SSD或者更轻量的关键点检测都行。课设场景我建议YOLOv5或v8部署简单训练快显存要求低。文字识别CRNN、PaddleOCR或者直接用PaddleOCR的detrec流程。中文点选场景优先考虑PaddleOCR中文识别效果好而且有现成的Python部署方案。为什么不直接用OCR的检测头一起做因为两者任务目标不同OCR检测是把“文字行”当成一个区域而点选验证码要把“单个字”当成独立目标。直接用OCR检测头经常会把相邻两个字合并成一个框导致后续点击坐标错误。2.3 课设的选型优先级先看数据量再选模型课设和工业项目最大的区别是你只有有限的时间大概率没有现成数据集。所以我一般按这个优先级选数据量少于500张不要自己训检测模型直接用轻量文字检测模型PaddleOCR自带的可控性更高或基于YOLO做小样本微调。数据量1000张以上YOLOv8nnano版训练分类头用PaddleOCR的文本识别做二次识别。数据量极少又没有采样渠道先用程序合成数据用合成数据训练检测模型再用少量真实数据微调。这里插一个重要前提不管选哪个模型你都要先把“点选验证码”当成一个图像检测问题来理解而不是一个“识别整张图然后输出点击顺序”的黑匣子任务。不要相信端到端输出要相信可解释的中间结果。2.4 整体流程粗定位、细识别、坐标映射整个识别流程我建议按这条线走图片输入读入验证码图记录原始宽高。目标检测检测图中所有候选文字框输出框坐标和尺寸。单字识别把每个框裁剪出来缩放或保持原有比例送入文字识别模型输出字符串。提示匹配把提示文字比如“点击 清明节 中的 清 明 节”按字拆分与识别结果做匹配。坐标转换把匹配到的框中心点从模型坐标系映射回原图坐标系输出点击顺序。第2、3步是模型第4、5步才是大学问。很多翻车案例就出在第5步模型训练和推理用的图片尺寸是640x640但页面原图是320x240你如果不做坐标缩放点击位置会偏得离谱。后面避坑章我会专门讲这个。核心代码如下def solve_captcha(image_path, prompt_text): # 1. 读原图记录尺寸 img cv2.imread(image_path) h, w img.shape[:2] # 2. 检测 - 得到候选框列表 boxes boxes detect_text_boxes(img) # 3. 识别每个候选框 - (box, char) result [] for box in boxes: crop crop_box(img, box) char recognize_char(crop) result.append((box, char)) # 4. 匹配提示文字 target_chars list(prompt_text.replace( , )) matched match_chars(result, target_chars) # 5. 映射坐标 clicks [] for box, char in matched: cx, cy box_center(box) clicks.append((int(cx * w / IMG_SIZE), int(cy * h / IMG_SIZE))) return clicks逻辑说明这一段把所有关键步骤抽象成了五个函数其中detect_text_boxes和recognize_char后续可以用 YOLO 和 PaddleOCR 替换实现。IMG_SIZE是检测模型的输入边长代码里我用w / IMG_SIZE做了等比缩放实际上如果检测前没有做letterbox这里要用具体缩放比例。参数说明放在真正实现的时候再展开。3. 数据准备没有数据集就先“人造”一套3.1 数据来源采集、合成、标注怎么选点选验证码的课设卡住大部分人的不是模型而是数据。你可以有几个来源手动从公开网页采集需要写爬虫但注意很多验证码图片是接口动态返回的采集时一定要保存原图不要存截图。截图的色彩空间和分辨率变化会影响训练。程序合成用PIL或OpenCV“画”出文字点选图这是课设最稳的方案可以精确控制字体、旋转、背景纹理、干扰线数量。公开数据集确实有一些带文字标注的检测数据集但和“点选验证码”的分布差异很大直接拿来训练换到真实图片上经常泛化失败。对课设来说我推荐“合成为主、少量真实样本校准”的组合。合成数据能解决“有没有”的问题真实数据才能解决“好不好”的问题。3.2 标注格式用YOLO的txt格式还是VOC的xml如果你用YOLO那标注格式是每个图片一个txt文件每行内容为class_id x_center y_center width height坐标是归一化到[0,1]的相对值x_center和y_center是框中心的相对坐标width和height是框宽高相对图片宽高的比例。这个格式有一个很大的优势即使图像被缩放或做letterbox坐标仍然是相对的不容易出错。缺点是看txt文件不能直观确定框在哪所以标注工具很重要。我一般用Labelme先标注成JSON再写一个脚本转YOLO格式。这样可以在标注时直接看到文字内容方便后续核对。转换脚本的关键代码如下import json import glob import os def labelme_to_yolo(json_path, img_w, img_h, class_map, out_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label] points shape[points] # [[x1,y1], [x2,y2]] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 归一化 cx ((x_min x_max) / 2) / img_w cy ((y_min y_max) / 2) / img_h bw (x_max - x_min) / img_w bh (y_max - y_min) / img_h lines.append(f{class_map[label]:d} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_name os.path.basename(json_path).replace(.json, .txt) with open(os.path.join(out_dir, out_name), w, encodingutf-8) as f: f.write(\n.join(lines))参数说明class_map是字典把“清”“明”“节”这样的文字映射到0、1、2的类别编号img_w和img_h是原图尺寸。这里要注意Labelme标注的坐标是像素坐标YOLO需要的是归一化坐标所以必须除以宽高。如果标注框是旋转框YOLO标准格式不直接支持需要先转成外接矩形点选验证码的文字通常没有大角度旋转用外接矩形足够。3.3 合成数据脚本用PIL画出可控制的验证码图合成数据是课设能不能在两天内跑起来的生命线。我提供一个自己常用的生成脚本骨架from PIL import Image, ImageDraw, ImageFont import random import os FONT_PATH simhei.ttf # 中文字体Windows可用 C:/Windows/Fonts/simhei.ttf def synth_captcha(out_dir, chars_pool, num_images1000): os.makedirs(out_dir, exist_okTrue) for i in range(num_images): # 随机选3~4个汉字作为目标 n random.randint(3, 4) target_chars random.sample(chars_pool, n) # 画布背景用浅色纹理 img Image.new(RGB, (320, 180), (random.randint(230, 250), random.randint(230, 250), random.randint(230, 250))) draw ImageDraw.Draw(img) # 随机放置目标汉字 for idx, ch in enumerate(target_chars): font ImageFont.truetype(FONT_PATH, random.randint(22, 32)) x random.randint(10, 240) y random.randint(10, 120) draw.text((x, y), ch, fontfont, fill(random.randint(10, 90), random.randint(10, 90), random.randint(10, 90))) # 干扰线 for _ in range(random.randint(3, 8)): x1, y1 random.randint(0, 320), random.randint(0, 180) x2, y2 random.randint(0, 320), random.randint(0, 180) draw.line((x1, y1, x2, y2), fill(random.randint(100, 200), random.randint(100, 200), random.randint(100, 200)), width1) img.save(os.path.join(out_dir, fsyn_{i:05d}.png))参数说明chars_pool是你准备生成的目标汉字集合建议写20-50个常用字与提示文字的“点击 XXX 中的 X”保持一致FONT_PATH必须指向真实存在的中文字体文件否则PIL会直接抛异常干扰线数量和文字尺寸可以调节但不要让文字旋转角度超过15度否则YOLO检测框不稳定。合成图片建议保存原图和对应的提示文本因为训练检测模型只需要图片和框标签但之后调试匹配逻辑时需要提示文本。3.4 数据增强让模型不那么“脆”只靠合成数据训练模型很容易在真实图片上翻车。原因很简单合成的图片太“干净”了。真实验证码会把文字压扁、拉长、加噪点、叠加背景图案。所以训练之前要做增强至少包含随机旋转-15度到15度。随机亮度/对比度模拟不同屏幕截图的色彩差异。高斯噪声模拟压缩伪影。随机裁剪增强模型对文字位置的鲁棒性注意裁剪后要同步更新标注框。用OpenCV或albumentations都能实现增强但albumentations更省事。import albumentations as A import cv2 transform A.Compose([ A.Rotate(limit15, border_mode0, p0.5), A.RandomBrightnessContrast(p0.6), A.GaussNoise(p0.3), A.RandomSizedCrop(min_max_height(100, 160), height180, width320, p0.5), ], bbox_paramsA.BboxParams(formatyolo, min_visibility0.5)) # 读图和对应txt做增强 # boxes格式: [[cx, cy, w, h]...] augmented transform(imageimg, bboxesboxes)参数说明border_mode0表示旋转后补黑边不要用白色补边因为验证码背景不是纯黑min_visibility0.5表示裁剪后框至少有50%区域在图中否则丢弃避免出现半个字。这个参数直接影响训练稳定性如果设太低模型会学到“半个字也是字”检测框会偏。4. 训练与识别实现从YOLO检测到点击坐标输出4.1 检测模型的训练参数选择YOLOv8训练点选文字检测参数不用太夸张。我这里给出一个课设级别的配置思路模型yolov8n.pt 预训练权重。图片尺寸640x640。如果原图是320x180YOLO会自动letterbox到640训练时不需要你手动resize。batch size显存不够就8够就16。epochs合成数据500张左右训练80-100轮足够多了反而过拟合。训练命令from ultralytics import YOLO model YOLO(yolov8n.pt) model.train( datacaptcha.yaml, epochs100, imgsz640, batch8, patience20, project./runs, namecaptcha_det )参数说明patience20是早停阈值如果20轮内验证集没有提升就停止训练防止课设时间被浪费在无效迭代上。data是YAML配置里面至少要有train标签、val标签和类别列表格式如下train: dataset/images/train val: dataset/images/val nc: 20 names: [清, 明, 节, ...]这里有个很容易踩的坑nc必须和names长度完全一致一旦不一致训练会直接报错或者类别错乱。我自己遇到过一次训练出来的模型把“清”识别成“节”看概率曲线又很正常最后检查发现是YAML里类别顺序和标注文本不一致。这个坑我放在避坑章再展开说。4.2 单字识别用PaddleOCR还是自训CRNN检测框出来后每个框就是一个文字图片。这里有两种路线路线A直接用PaddleOCR的识别接口。好处是中文识别开箱即用不挑字体坏处是模型较重CPU推理慢单字识别反而要几毫秒到几十毫秒且它对单字没有上下文偶尔会把“末”识别成“未”。路线B用简单的CRNN或MobileNet分类器。点选验证码的文字集合是有限的你完全可以把“识别几十个汉字”当成一个图像分类任务来解。每个框缩放到统一大小用MobileNetV3或ResNet18微调。这个路线在课设里更可控准确率能达到95%以上。我建议课设选路线B原因有二第一分类模型对“字库”的边界把握清晰题外字不会乱识别这很重要第二答辩时你可以解释“我用了一个轻量分类网络把检测到的文字图块映射到预定义的字库”比端到端OCR更好讲。下面是一个基于PIL和PyTorch的分类器构建骨架import torch import torch.nn as nn from torchvision import models, transforms def build_classifier(num_classes): model models.mobilenet_v3_small(weightsNone) model.classifier[3] nn.Linear(model.classifier[3].in_features, num_classes) return model # 预处理所有裁剪图缩放到 64x64 transform transforms.Compose([ transforms.Resize((64, 64)), transforms.ToTensor(), ])参数说明mobilenet_v3_small输入要求64x64这个尺寸已足够识别单个汉字再大只会增加计算量。num_classes对应字库大小和检测模型的类别数保持一致。训练时用交叉熵损失优化器用Adam学习率1e-310轮左右就能收敛。4.3 匹配与坐标映射这个函数决定了你能拿多少分模型跑通之后最后的匹配和坐标映射是纯逻辑代码。很多同学在这里栽跟头检测出20个框识别结果也对但输出坐标就是点不到验证码的中心。问题出在letterbox上。YOLO推理时会把输入图按比例缩放并填充灰边到640x640模型输出的坐标是相对于640x640的你需要逆运算得到原图坐标。有人直接用“原图宽高/640”来缩放遇到非等比图片时坐标就会偏移。正确做法是记录YOLO推理时的缩放因子和填充偏移。用ultralytics的API其实有更简单的方式YOLO的predict返回的boxes坐标默认已经是原图坐标系了。from ultralytics import YOLO model YOLO(./runs/captcha_det/weights/best.pt) img cv2.imread(demo.png) results model.predict(img, imgsz640, conf0.5) # results[0].boxes.xyxy 已经是原图坐标 for box in results[0].boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 [int(v) for v in box] cx, cy (x1 x2) // 2, (y1 y2) // 2 print(cx, cy)逻辑说明这段代码拿到的是原图坐标系下的点可以直接用于后续点击。但如果验证码在页面中被CSS缩放显示这里拿到的原图坐标还不等于页面坐标需要在浏览器端再除以缩放比例。这个细节非常隐蔽我在避坑章里会单独讲。4.4 一个完整的“检测识别点击”入口最后把检测、分类、匹配串起来。这里假设你已经训练好了检测模型best.pt和分类模型cls.pth。def solve_captcha(img_path, prompt_text): # 1. 检测 results det_model.predict(img_path, imgsz640, conf0.45) crop_boxes results[0].boxes.xyxy.cpu().numpy() # 2. 识别每个框 char_map [] for box in crop_boxes: x1, y1, x2, y2 [int(v) for v in box] crop img[y1:y2, x1:x2] char classify_char(crop) # 返回字库idx char_map.append((char, (int((x1x2)/2), int((y1y2)/2)))) # 3. 提示匹配 targets list(prompt_text.replace( , )) result [] for t in targets: for char, pos in char_map: if char t: result.append(pos) break return result参数说明conf0.45是检测置信度阈值对点选验证码来说0.4-0.5之间比较合适。太高会漏字太低会多框。char_map里存的是字库索引和坐标匹配提示时直接用字库索引比对不要比对字符串因为不同字体的同一个字分类器输出的索引是一样的。提示文本拆字时要去掉空格有些提示是“点击 清明 中的 清明”两个词可能重叠匹配时要设计一个优先级按提示顺序依次找找到就把该坐标从候选列表里移除。5. 点选验证码识别避坑指南五个典型翻车现场5.1 现象检测框莫名偏移点击位置偏左上角原因训练时用了矩形标注但实际文字是倾斜的外接矩形中心不等于文字视觉中心或者推理时图片被隐式缩放。解决如果文字旋转较多标注时用旋转框YOLOv8支持angle模式如果只是推理时偏移检查是否有letterbox。我自己的做法是先输出检测框画在原图上逐一核对不要直接信坐标。5.2 现象分类器把“未”识别成“末”准确率卡在92%原因字库里有形近字且分类器输入分辨率太低笔画差异被压缩掉。解决把分类器输入从64x64提到96x96同时注意做归一化时不要丢失笔画的深浅信息。如果还不行在字库里加一组成对样本做“刻意对比”用两次训练或加三元组损失来拉大区分度。形近字问题在点选验证码场景里非常常见尤其是“己、已、巳”这种三胞胎靠分类器硬扛不如在匹配逻辑里做二次校验。5.3 现象训练时损失很低真实图片上检测不到文字原因合成数据和真实数据分布不一致。合成图片背景干净真实图片有纹理、压缩伪影、文字模糊。解决在合成脚本里增加背景纹理和模糊处理。我建议用一个简单的模糊增强cv2.GaussianBlur(img, (3,3), 0)模拟验证码图片被二次压缩后的效果。更关键的是训练集中至少要加入20-30张真实图片做微调哪怕手动标注这一步不能省。5.4 现象点击坐标在图像上是对的页面里就是点不中原因这是一个非常经典的课设翻车点。验证码图片在页面里被CSS缩放显示而你点击的目标坐标是原图坐标。比如原图是320x180页面显示成160x90那所有坐标都要除以2。解决在Selenium或Playwright点击时先获取验证码元素的rect然后按scale_x rect.width / img_w换算。注意验证码元素在页面中可能有边框或padding坐标系基准不同需要按元素content区域重新对齐。我见过最隐蔽的一种情况页面里验证码图片居中显示但图片外层有一个内边距导致图片左上角不是元素左上角。这时候你要在点击前scrollIntoView然后取getBoundingClientRect()用图片实际显示区域重新计算坐标。5.5 现象答辩时被问“模型为什么这么设计”答不上来原因很多人直接把一个预训练YOLO拉过来训练没记录选型依据也没对比数据。解决这是一个“非技术坑”但比技术坑更致命。做课设时建议保留一组对比数据模板匹配的失败率、YOLO检测的准确率、分类器的混淆矩阵。至少做一组“有没有数据增强”的对比实验。不需要很严谨但要把实验记录写进报告。这是最有效且省力的答辩准备方式。6. 进阶玩法把课设从“能跑”做到“能讲”到这里核心链路已经全部打通合成数据 → 目标检测 → 单字分类 → 提示匹配 → 坐标映射 → 自动点击。但对于课设来说“能跑”只算及格“能讲清楚设计过程”才算优秀。最后一个章节我分享几个能显著提升完成度的进阶做法。第一给识别过程加可视化。做一个简单的Python脚本把检测框、识别结果、点击顺序按序号画在图片上输出成一张带标注的图。这不仅是展示给老师看更是你自己调试时最直观的工具。我每次调坐标偏移时都是看这张图比看数字快得多。第二做一个函数式API把识别流程封装成几个独立函数。这样在演示时可以一条命令跑出结果也能在答辩时流畅地展示“调参 → 重跑 → 看结果”的过程。第三增加一个“失败重试”机制。点选验证码的图片往往有多个候选批次如果第一次点击失败可以重新取一张图。这个机制在代码上很简单但能极大提升体验感也是课设报告里的一个亮点。如果还有余力可以考虑用ONNX把检测模型导出做成一个可脱离训练环境的轻量推理文件。这样答辩时不用现场装PyTorch也可以直观地说明“模型可以部署到生产环境”。这个细节在评审老师眼里非常加分。回过头看这个课设最核心的技巧不是模型调参而是“把任务拆成检测、识别、匹配三步然后分别验证”。我自己的教训是一开始太相信端到端流程跳过中间验证结果在坐标映射上浪费了整整一天后来老老实实把完整脚本按步骤拆开每步打印中间结果问题很快定位到是letterbox换算错误。希望这篇笔记对你也有同样的用处能帮你绕过这些坑把有限的时间花在真正有挑战的部分上。希望帮到你。本文还有配套的精品资源点击获取