简介:面向Python学习者与验证码识别开发者的课程设计资源,聚焦“文字点选/选字/选择文字”类验证码的自动识别,适合完成课设、毕设或快速搭建识别服务时参考。资源包共48个文件,含19个Python脚本、模型权重bin、测试图片、前端展示样式等,压缩包约121.82MB;整体工程结构清晰,涵盖模型训练、接口服务、Web演示等模块,代码已适配Windows下Python3.6/3.8/3.10,低配置服务器也可平稳运行。已有437人学习下载。该方案在仅300张小样本训练条件下实现约96%准确率,单次识别耗时100~300ms,兼顾效果与性能;同时附带模型文件、演示图片、依赖清单及说明文档,便于直接运行验证或二次开发,是理解文字点选验证码识别原理与工程落地的实用资料。
1. 文字点选验证码识别:课设题目到底在考什么
文字点选验证码(也叫选字验证码、文字点击验证码)和传统的四位数字、滑块验证码完全不同:用户需要看图里的若干中文文字,再按照题目的要求逐个点击正确的那几个字。比如背景图里有“春”“节”“快”“乐”四个字,题目要求“请依次点击:春节快乐”,系统就按点击顺序判断对不对。这类验证码识别在 IT 从业者的日常爬虫、自动化测试、反爬对抗里经常遇到,也是很多 Python 课程设计喜欢选的方向——因为它的技术链路很完整:图片处理、目标检测、文字识别、坐标计算、模拟点击,甚至有 UI 展示和结果评估,适合做成一个期末课设项目。
这个标题的核心难点不在“识别某个字是什么”,而在于“先找到字在哪”。图表里的文字会有背景干扰、字体变形、旋转错位、颜色混杂,有时候还有干扰线,所以至少要分成“定位”和“识别”两个子问题。训练样本的标注、模型的选型与裁剪、识别结果到点击坐标的映射,这三步最容易卡住新手。下文从数据出发,把每一步涉及的 Python 代码、模型接口和参数都过一遍,读完可以直接照着搭一套能跑通的方案。整个方案在普通笔记本上就能完成,深度学习部分用 PaddleOCR 的预训练权重微调即可,不要求 GPU 性能。
2. 先解决数据:文字点选验证码的定位与样本生成
2.1 文字点选验证码图片的组成与难点分析
拿到一张待识别的验证码图片,先用眼睛去拆分:底图是一张带纹理的背景,文字可能叠加在图标、曲线、色块上,字体随机,字号随机,每个字有独立的旋转角度。这决定了模型不能像 OCR 整段文字那样直接识别,而是需要先把“候选文字区域”切割出来。从工程角度看,这不只是算法问题,更是数据标注成本问题——因为凡是做课程设计,最缺的不是模型,而是标注数据。自己手工 label 几百张验证码对个人来说耗时太大。
这里有一个常见的误区:不要一开始就上复杂的目标检测网络去做端到端模型,而是先看文字点选验证码的结构。它包含的“文字区域”本质上就是带旋转角度的文本框,数量在 4~8 个之间,远少于通用目标检测数据集的规模。所以最简单的可复现方案是:自己用 Python 模拟生成训练样本,把字体、背景、干扰全部用代码控制。这样天然带有精确的标注框和文字内容,绕开了人工标注的高成本,而且可以随时扩增数据量、更换字体路径、增加干扰线。诚实地说,合成数据在实力上的分布与真实线上数据有一点点差异,但对课设题目来说完全够用。
模板化生成的核心步骤如下:先用 Pillow 创建纯色或渐变背景,再用 OpenCV 在图上随机摆放文字、画干扰线、加噪点,最后在保存标注文件时记录每个文字的类别和中心点坐标。这个中心点坐标就是后续逻辑里的“点击坐标”。
2.1.1 合成样本的 Python 脚本骨架
下面的代码实现了一个简单的样本生成器,核心接口是generate_sample(texts, save_dir):
import os import random import numpy as np import cv2 from PIL import Image, ImageDraw, ImageFont def random_bg(w, h): """生成随机渐变背景,避免模型只学到纯色背景的特征""" base = np.random.randint(100, 200, (h, w, 3), dtype=np.uint8) noise = np.random.randint(-20, 20, (h, w, 3), dtype=np.int16) bg = np.clip(base.astype(np.int16) + noise, 0, 255).astype(np.uint8) return bg def put_text_with_rotation(draw, pos, text, font, angle): """在 draw 对象上按角度渲染文字,Pillow 的 rotated text 以中心为锚点""" txt_img = Image.new('RGBA', (120, 120), (0, 0, 0, 0)) txt_draw = ImageDraw.Draw(txt_img) txt_draw.text((60, 60), text, font=font, fill=(0, 0, 0, 255), anchor='mm') txt_img = txt_img.rotate(angle, expand=True, resample=Image.BICUBIC) return txt_img def generate_sample(texts, save_dir): w, h = 300, 200 bg = random_bg(w, h) img = Image.fromarray(bg) draw = ImageDraw.Draw(img) font = ImageFont.truetype("simhei.ttf", random.randint(28, 36)) boxes = [] for text in texts: angle = random.randint(-25, 25) rotated = put_text_with_rotation(draw, None, text, font, angle) # 计算有效的放置区域,避免文字超出边界 x = random.randint(30, w - 80) y = random.randint(30, h - 80) img.paste(rotated, (x, y), rotated.split()[3]) boxes.append({"text": text, "x": x + rotated.width // 2, "y": y + rotated.height // 2}) # 加干扰线 for _ in range(random.randint(3, 6)): x1, y1 = random.randint(0, w), random.randint(0, h) x2, y2 = random.randint(0, w), random.randint(0, h) draw.line((x1, y1, x2, y2), fill=(100, 100, 100), width=random.randint(1, 2)) img.save(os.path.join(save_dir, f"{'_'.join(texts)}_{len(os.listdir(save_dir))}.png")) return boxes这个脚本里值得注意的参数有三个:
truetype("simhei.ttf", ...)中的字体路径可以根据系统环境变化,Windows 常见是C:/Windows/Fonts/simhei.ttf,Linux 则可能需要wqy-zenhei.ttc。字体差异直接影响后续识别模型的泛化能力。put_text_with_rotation里的anchor='mm'指定 Pillow 的文本锚点为中心,这样旋转后旋转轴才是文字中心,否则位置会有几个像素的偏移,累积起来会影响点击坐标。img.paste(rotated, (x, y), rotated.split()[3])第三个参数是蒙版,表示只贴不透明部分,避免把一个黑色矩形块贴到背景上。
2.1.2 标注格式与点击坐标的关系
保存坐标时只需要中心点位置,不需要文本框的四点坐标,因为模拟点击操作最后只需要一个点。代码里的boxes列表直接输出文字内容和中心点,对应了验证码的语义标签。生成时记录一个 JSON 文件会更方便后续训练:
{"image": "sample_1.png", "texts": ["春", "节", "快", "乐"], "points": [[45, 67], [120, 89], [210, 145], [266, 88]]}这里四个坐标和文字一一对应,表示用户在图片上依次点这四个位置就能通过验证。如果你打算用 YOLO 系列训练检测模型,要额外把四点坐标转换成cx, cy, w, h格式,YOLO 的标注比例需要除以图片宽高。不过课设场景里直接存中心点也够用,因为后续 PaddleOCR 天生返回的是多边形四点坐标,取均值就是中心。
2.2 常见误用:直接把整页截图拿去“识别”
另一个高频问题:学生拿到验证码图片,第一反应是直接调用 OCR 接口把图片里所有文字一次性识别出来,再挨个去找。这在简单背景上可行,但凡是带背景纹理或干扰线的,OCR 会误识别出大量不存在的文字。文字检测网络输出的候选框可能比实际文字多或少,如果 OCR 把干扰线识别成了“一”,后续匹配就乱了。所以实践上必须先把检测阈值调高,过滤置信度低的框,让干扰误识别的文本框无法进入点击候选列表。合成数据时也要刻意加入干扰线,让检测模型学会忽略它们。
3. 模型选型与训练:让 Python 同时完成文字定位和识别
3.1 双阶段方案:目标检测+OCR 为什么够用
从工程成本和课设验收标准来看,端到端的深度学习模型实现难度较大,也没有必要。文字点选验证码识别只需要两个能力:一是“图里有哪些文字、坐标在哪”,二是“这个坐标上的字是什么”。前者是目标检测任务,后者是 OCR 任务。拆成两个独立的模型,可以分别调参和评估,出现问题时定位也更清晰。
PaddleOCR 的 PP-OCRv4 模型恰好把这两个能力集成在一个 Python 包里面,PaddleOCR类的predict()方法直接返回检测框坐标、识别文本和置信度。接口调用上你不需要自己写检测模型和识别模型的推理代码,但在课程设计答辩时,要能说清楚内部结构:检测是基于 DBNet 的变体,识别是 CRNN 系列,两者共用一个文本检测坐标系。
这里有一个关键点:PaddleOCR 的检测结果返回的是原始图片尺寸下的坐标,单位是像素,正好可以直接用于模拟点击。如果图片缩放过去,坐标也必须跟着缩放,否则点击偏移会导致验证失败。因此在实际工程里,进入模型的图片尺寸和 UI 界面上展示的图片尺寸要严格一致,或者记录缩放比例。
3.2 PaddleOCR 的安装与最小推理代码
安装 PaddleOCR 最容易踩的坑是版本不兼容。建议用以下命令创建一个干净的虚拟环境:
conda create -n captcha python=3.9 -y conda activate captcha pip install paddlepaddle==2.5.2 pip install paddleocr==2.7.3注意paddlepaddle是 CPU 版本,不需要额外装 CUDA。模型推理代码只需要几行:
from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='ch', det_db_thresh=0.3, det_db_box_thresh=0.5) result = ocr.ocr('sample.png', cls=True) for line in result[0]: box = line[0] text = line[1][0] score = line[1][1] cx = int((box[0][0] + box[2][0]) / 2) cy = int((box[0][1] + box[2][1]) / 2) print(f"文字: {text}, 置信度: {score:.2f}, 中心点: ({cx}, {cy})")参数det_db_thresh控制检测阶段二值化的阈值,数值越低越容易检测到模糊文字,但也会引入误检;det_db_box_thresh控制输出文本框的置信度阈值,0.5 算是一个经验值。如果图片里的文字很清晰,可以提高到 0.6,减少干扰线带来的误检框。
result[0]是图片上的所有检测结果。每个line是一个文本框,包含四个角点坐标、识别文字和一个置信度分数。中心点用box[0]和box[2](左上角和右下角)取平均即可。
3.3 微调还是直接用预训练模型
很多刚接触验证码识别的同学会纠结要不要自己训练。实际上 PaddleOCR 官方提供的中文识别模型对印刷体汉字已经有非常高的精度,课程设计的验证码基本是印刷体字体(黑体、宋体),微调的必要性不大。真正值得自己做的是数据合成和检测阈值调试。
当然,如果题目要求必须训练自己的模型,可以用 PaddleOCR 提供的微调脚本。核心流程是:把合成数据转换成 PaddleOCR 要求的标注格式,每行写图片路径 + 制表符 + 文本框四点坐标 + 文字内容。下面是格式示例:
train/001.jpg\t[[45,67],[80,67],[80,110],[45,110]]\t春节快乐之后运行训练命令:
python tools/train.py -c configs/det/ch_PP-OCRv4_det.yml训练参数由 YAML 文件控制,一般只需要改数据集路径和epochs。对课设来说,epochs=50、batch_size=8,用 CPU 训练大概两三个小时能收敛到一个可用的效果。不过要提醒一句:文字点选验证码比整段文本简单,因为每个字都相对独立、字间无粘连,检测难度反而低于现实中拍摄的复杂街景文本。所以直接把预训练模型拿来用,在绝大多数课设场景下都是更聪明的选择。
3.4 识别结果可能出现的两种错误
第一种错误是漏检:图里有些字和背景融合度高,检测阈值过滤掉了。解决方向是降低det_db_box_thresh,比如调到 0.3;如果还漏检,就在图像预处理阶段做一次对比度增强。第二种错误是误检:干扰线被当成文字。误检的直接后果是候选文字列表多于实际文字。此时你需要写一层过滤逻辑去和题目文字做匹配,而不是把 OCR 结果全部当作有效候选。
4. 实现点击序列:从识别结果到模拟点击
4.1 文字匹配:OCR 结果和题目顺序怎么对齐
现在图里的文字内容已识别出来,用户要根据题目的顺序依次点击。假设题目是一个字符串target = "春节快乐",OCR 返回的结果是一个个单独的候选目标,我们需要把每个候选文字和目标文字做一一匹配。
最朴素的匹配逻辑是“先按文本内容过滤,再按题目顺序排列”:
target = "春节快乐" ocr_results = [("春", 0.99, (45, 67)), ("节", 0.98, (120, 89)), ("快", 0.97, (210, 145)), ("歹", 0.56, (90, 160)), ("乐", 0.95, (266, 88))] matched = [] for ch in target: for text, score, point in ocr_results: if text == ch and score >= 0.6: matched.append(point) break else: print(f"缺少文字: {ch}")这段代码按题目中的字符顺序逐个去候选列表里找对应文字,找到就记录中心点。for...else...的else分支会在内层循环没有找到匹配时执行,用于提示缺字。这个做法的优点是直观,缺点是如果 OCR 把某个字识别错了,比如“快”识别成“怏”,匹配就会失败。所以工程上建议把“字符匹配”升级为“相似度匹配”,用编辑距离或字形象似度来做。
一个简单有效的方法是:如果候选文字和题目目标字符不相等,就计算候选文字对应的图像裁剪区域和目标字符模板之间的相似度。这在课程设计里可以用difflib.SequenceMatcher快速实现,但真正的生产逻辑通常用向量相似度,比如使用 PaddleOCR 的文本向量特征。不过课设代码里用编辑距离就够了。
4.1.1 相似度匹配的改进代码
from difflib import SequenceMatcher def match_char(target_ch, ocr_item): text, score, point = ocr_item if text == target_ch: return score ratio = SequenceMatcher(None, text, target_ch).ratio() return score * ratio * 0.8这个函数的意义在于:target_ch是目标字符,ocr_item是候选识别结果。如果文本完全相同则直接用 OCR 置信度;如果不完全相同,就用编辑相似度打折。打折系数 0.8 是经验值,调低会导致错误接受、调高会导致漏识别。对课设来说这个参数通常不需要特别精确,只要保证流水线能跑通就行。
4.2 坐标转换:从模型坐标到屏幕点击坐标
这里有一个非常隐蔽的坑:OCR 识别的坐标是图片坐标,而自动化点击操作发生在浏览器或桌面窗口里。以 Selenium 模拟浏览器点击为例,验证码图片显示在网页里时,<img>元素的渲染尺寸可能和原始图片尺寸不一致。如果直接把图片像素坐标交给 Selenium 的ActionChains去点击,点击位置必然偏移。
正确的做法是计算缩放比例。假设原始图片宽为img_w,在网页中显示宽度为display_w,那么scale_x = display_w / img_w。点击坐标为(center_x * scale_x, center_y * scale_y),再叠加上图片元素在页面中的偏移量。Selenium 获得元素位置的方式如下:
from selenium import webdriver from selenium.webdriver.common.action_chains import ActionChains element = driver.find_element('id', 'captcha_img') location = element.location # 元素左上角在页面中的位置 size = element.size # 元素渲染后的宽高 scale_x = size['width'] / img_w scale_y = size['height'] / img_h for (cx, cy) in matched: click_x = location['x'] + cx * scale_x click_y = location['y'] + cy * scale_y ActionChains(driver).move_to_element_with_offset(element, cx * scale_x, cy * scale_y).click().perform()move_to_element_with_offset以元素左上角为原点,所以坐标要传入相对的偏移量。如果操作的系统是桌面客户端而不是网页,可以改用pyautogui.click(click_x, click_y),两者的坐标系都是屏幕左上角。注意不要混用:Selenium 的坐标是页面坐标,pyautogui是屏幕物理坐标,如果浏览器窗口没有置顶或页面有滚动条,屏幕坐标还需要额外调整。
4.3 环境适配与 pip 运行时的常见报错
运行 Python 脚本时最影响体验的问题集中在依赖库上。第一次pip install paddleocr后直接跑,经常会遇到两个报错:
第一个是ModuleNotFoundError: No module named 'paddle'。这说明paddlepaddle没有正确安装。在最新版网络热词里,python 环境配置被频繁提及,建议用pip list检查已装包,确认paddlepaddle出现在列表中。没有就单独安装 CPU 版本,安装命令已经在前文给出。
第二个是cv2.error: OpenCV(4.x) ... assertion failed。这类报错大多是因为图片路径不对或读取出来的img是None,常见于 Linux 系统下中文路径或文件名编码问题。代码里应在cv2.imread后判断返回结果是否为None,如果为None就用os.path.exists先检查文件是否存在。
点击坐标计算这一步还有一个细节:PaddleOCR 返回的box坐标类型是 numpy 浮点数,直接传给 Selenium 可能会因为类型问题报错。上文代码里已经做了int()转换,这里要再强调一次,因为 Selenium 对 float 坐标的容忍度不如pyautogui,存在隐性精度截断风险。
5. 用验证脚本把鲁棒性量化出来
很多课程设计交上去之后,老师会现场拿几张新的验证码图片来测。这时候最怕的是“在训练集上表现好,换张图就崩”。因此最后一章单独讲验证方法:怎么量化你的系统稳定性。
先建立一个基准验证集。从合成数据中留出 20% 的图片不参与训练,代码遍历这些图片,对每张图片调用ocr.ocr(),再执行匹配逻辑。判断的标准是:点击序列是否与目标文字完全一致。统计“全部正确”“部分错误”“完全失败”三类比例。这个验证脚本的要点是不允许人工干预,所有图片一视同仁,跑完之后输出一个准确率数字,作为答辩时的客观依据。
def evaluate(test_dir, target_file): total = 0 correct = 0 for img_path, target in load_test_samples(test_dir, target_file): ocr_result = run_ocr(img_path) click_seq = build_click_sequence(ocr_result, target) total += 1 if click_seq == expected_click_points(img_path): correct += 1 print(f"点击准确率: {correct / total:.2%} ({correct}/{total})") if __name__ == "__main__": evaluate("test_images", "targets.json")这段代码的逻辑很直白:load_test_samples读图片列表和标准答案,run_ocr返回坐标候选,build_click_sequence按目标顺序匹配文字得到点击坐标序列,最后与标准答案比对。如果准确率低于 80%,优先调整det_db_box_thresh和匹配时的置信度阈值,而不是换模型。更多时候问题出在合成数据与真实图片的分布差异上,比如你合成的字体是黑体,线上验证码是宋体,那么 OCR 对宋体字形的置信度天然偏低。这时候最有效的做法不是调整参数,而是把真实线上验证码截图加入训练集,哪怕只有几十张,分布对齐带来的提升也远超调参。
进阶方向是绕开 OCR,直接用模板匹配来做点击验证码。因为验证码的文字集合和字体相对有限,把每个候选文字裁剪成小图,再与题库里的模板做相似度计算,可以做到比 OCR 更快的速度。PaddleOCR 对单字图片的推理大概几十毫秒,但如果图片上有很多候选字,总耗时累计仍可能超过验证码的过期时间。实践中可以预留一个开关:首次识别用 OCR,失败后降级到模板匹配。这种双通路设计放在课设报告里也是很好的加分项。
最后提醒一个容易忽略的生产陷阱:PaddleOCR 每次实例化都会加载多个模型文件,首次调用耗时很长。在验证码识别的循环里不要频繁创建PaddleOCR对象,应当在程序启动时创建一次并全局复用。如果你用 Flask 或 FastAPI 对外提供服务,这一点尤其重要,否则每个请求都要经历几秒钟的模型加载过程。
本文还有配套的精品资源,点击获取