简介:面向需要完成文字点选验证码识别课设或研究Python图像识别应用的开发者,本资源提供一套完整可运行的选字验证码识别方案。模型基于小样本训练,仅用300张验证码即可达到96%准确率,单次识别耗时约100~300毫秒,在1核2G低配置服务器上也能流畅运行,已通过Windows下Python3.6、3.8、3.10版本测试。压缩包共48个文件,总体积约122MB,包含19个Python源码、4个pyd编译模块、2个模型权重bin文件、多张png/jpg示例图片及说明文档等。py文件覆盖数据预处理、模型训练、API推理与演示脚本;png/jpg/gif用于展示识别效果与界面;txt/md提供使用说明与项目介绍;目录划分了src、utils、static、docs等模块,结构清晰,便于快速定位和二次开发。已有437人学习下载,适合作为课设参考、学习文字定位与识别方法,也可直接部署到实际业务中。
1. 这个课设到底在做什么:点选验证码识别的技术闭环
如果你在Python课设里拿到“点击选择文字验证码识别”这个题目,大概率看到的是“请按顺序点击安全验证”那张图——背景有波浪纹、字体有扭曲,甚至整行字是歪的。
这种验证码和我们熟悉的数字字母验证码最大的差别在于,它要的不是“识别出一个字符串”,而是“识别出图片里每个文字的坐标,并按顺序模拟点击”。把这个过程拆开,就是图像预处理、文字检测、坐标映射、浏览器事件模拟一条链。
这个课设很适合用来练OpenCV、OCR和Selenium,因为每一环都有能看到的输出,且翻车点非常多。下面我会顺着一条可以直接交差的路给出完整方案,先讲选型理由,再给能跑的代码,最后把最容易卡住的四个坑讲透。
2. 拆解点选验证码的技术栈:预处理、OCR选型与完整链路
2.1 点选验证码的常见形态与识别难点
这类验证码常见有“文字点选”和“文字选字”两种。文字点选通常是一张大图里散布着若干汉字,提示区写着“请点击:登录”“请点击:验证”;文字选字则是把几个字和十几个干扰字混排,要求按特定顺序点选。不论哪种,核心都是“先识别文字内容,再定位文字中心点”。
难点来自三处:一是背景干扰,波浪线、雪花噪点、渐变阴影都会影响二值化;二是字体虽然来自统一字体库,但随机旋转和拉伸会让OCR误判;三是点击顺序必须和提示一致,OCR返回的顺序往往不能直接用。课设里最容易踩的就是把验证码当成普通字符验证码,直接OCR输出字符串然后拿去匹配,结果发现没有坐标,也没法点击。正确思路是把识别结果当成一组带坐标的文本对象,再按提示词重新排序。
2.2 图像预处理:去噪、二值化与颜色过滤
图像预处理的目的不是让图片变漂亮,而是让文字和背景在颜色分布上能分开。点选验证码的背景通常和文字颜色不同,比如背景是浅蓝色波纹、文字是深灰色,这时先用HSV颜色过滤比直接灰度更有效。以下是我课设里常用的预处理函数:
import cv2 import numpy as np def preprocess(image_path): img = cv2.imread(image_path) if img is None: raise FileNotFoundError(f"图片读取失败: {image_path}") # 转HSV后做颜色范围过滤,深色文字的灰度值大致在0~200之间 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, (0, 0, 0), (180, 255, 200)) # 对原图灰度再按mask取交集,背景的干扰线会被去掉 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.bitwise_and(gray, gray, mask=mask) # 反向二值化,让文字变成白色、背景变黑,便于后续轮廓检测 _, thresh = cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY_INV) # 开运算去小噪点,核太大会把浅色文字的边缘也吃掉 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) return thresh这段代码里最重要的是inRange的两个参数。(0, 0, 0)到(180, 255, 200)表示的并不是固定颜色,而是“饱和度不限、亮度小于200”的深色区域,适合大多数深色文字验证码。如果你的验证码是白色文字,就把inRange改成(0, 0, 200)到(180, 255, 255),同时把THRESH_BINARY_INV换成THRESH_BINARY。kernel大小也要跟着调,文字笔画细的用(2, 2),粗的用(4, 4),千万别用(10, 10)那种大核,会把“横折钩”拆断。
预处理做完之后,把二值图交给OCR,识别率通常能提升好几个点。我见过直接拿原图跑PaddleOCR然后抱怨误识别的情况,多半是漏了这一步。
2.3 OCR引擎选型:Tesseract、PaddleOCR、ddddocr怎么选
选型直接决定你能不能按时交差。Tesseract的优点是轻、离线可用,但中文识别需要额外下载chi_sim语言包,且对扭曲汉字的效果一般,课设里如果没有网络环境会卡在安装上。ddddocr 这类验证码专用库对纯字符串验证码很友好,一行代码就能输出结果,不过它对“带坐标、多文字散布”的点选图支持有限,更适用于滑块缺口或普通字符验证码。PaddleOCR 是这几个里对“检测+识别”支持最完整的:它既能给出每个文字的四点坐标,也能给出识别文本和置信度,正好满足点选验证码的两个需求。
装完依赖之后,记得在PyCharm的Settings里确认当前Project的Python Interpreter是同一个环境,不然代码里import paddleocr会直接报ModuleNotFoundError。以下是我常用的安装方式:
pip install paddlepaddle paddleocr opencv-python如果机器没有NVIDIA显卡,不要额外装CUDA版本,CPU版就能跑,只是识别一张图要多等一两秒。在使用PaddleOCR时,建议把show_log关掉,不然终端会被调试日志淹没:
from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False)注意这里用的是PaddleOCR 2.x的API,如果你的环境里装的是3.x,返回格式会从“列表套列表”变成字典结构,解析方式不一样。我的建议是课设环境固定安装2.x版本,因为网上的教程大多按2.x写,遇到问题好查。上述代码会加载方向分类器和中文识别模型,第一次运行会下载模型文件,大概一两百兆,建议提前跑通,不要在答辩当天现场下载。
3. 文字检测与坐标映射:把“图里的字”变成鼠标可点的坐标
3.1 用PaddleOCR检测文字位置并提取候选词
拿到预处理后的图片,下一步是让OCR输出“每个字的位置”。PaddleOCR的ocr.ocr()返回结果里,每一项是一个包含[box, (text, score)]的对象,box是四个角点的坐标,text是识别的文本,score是置信度。直接写一个解析函数:
import json def parse_ocr_result(result): items = [] # PaddleOCR 2.x 返回的是 [[box, (text, score)], ...],最外层还有一层列表 for line in result: if line is None: continue for box, (text, score) in line: xs = [point[0] for point in box] ys = [point[1] for point in box] item = { "text": text, "score": float(score), "cx": int(sum(xs) / 4), "cy": int(sum(ys) / 4), "box": box, } items.append(item) return items这个函数的逻辑很简单:四个角点横纵坐标分别取平均,得到文字框的中心点。后面模拟点击时,用的就是cx和cy。解析完后可以先打印出来看一眼,确认识别结果里有没有混入背景文字。
3.2 从OCR结果中过滤干扰项
点选验证码的提示词通常写在图片右侧或上方,例如“请依次点击:安全 验证 登录”。OCR会把提示词也当成图片里的文字识别出来,如果不过滤,点击时会多出很多错误坐标。过滤策略分两步:先把置信度低于0.5的项丢掉,再把和候选词列表无关的项丢掉。下面是匹配函数:
def filter_candidates(items, keywords): result = [] for item in items: if item["score"] < 0.5: continue text = item["text"].strip() # 点选验证码一般只包含汉字,过滤掉字母、数字和符号 pure = "".join(c for c in text if "\u4e00" <= c <= "\u9fff") if not pure: continue # 用“包含”而不是“相等”,因为OCR偶尔会多识别一个偏旁或空格 if pure in keywords or keywords in pure: result.append(item) return result这里用“候选词包含在识别文本里”而不是“完全相等”,是因为OCR有时会把“验证”识别成“验证一”或“验证?”,多一个偏旁或符号更常见。keywords可以直接从验证码页面上抓取,比如用Selenium读取提示元素的文字。如果拿不到提示词,就退化成“把所有置信度高的候选按左上到右下排序”,但那样成功率会低不少。
3.3 把识别结果映射到原始图片坐标
PaddleOCR返回的坐标是原始图片的像素坐标。如果验证码图片通过浏览器缩放显示,直接点击必然偏移。常见的网页会把一张400×200的验证码显示成300×150,这个比例变化必须换算回去。我习惯先获取图片原始尺寸和显示尺寸,再统一到一个函数里处理:
def scale_coords(item, src_w, src_h, disp_w, disp_h): return { "cx": round(item["cx"] * disp_w / src_w, 1), "cy": round(item["cy"] * disp_h / src_h, 1), }src_w和src_h来自图片文件本身,在浏览器里对应naturalWidth和naturalHeight;disp_w和disp_h是元素实际显示宽度。如果你用PIL读图,可以用Image.open(...).size;用Selenium则直接读属性。比例换算看似简单,却是课设里最容易翻车的地方,因为不少网页还会给图片加边框和内边距,点击基准要额外再加一次元素偏移。
4. 模拟点击与答题逻辑:怎么让验证码真正通过
4.1 构造点击顺序:从文本匹配到坐标排序
验证码要求“按提示顺序点击”,所以不仅要找到文字坐标,还要按顺序生成点击序列。假设提示词是“安全 验证 登录”,正确的点击顺序就是它们的排列顺序。这里有一个技巧:先把提示词拆成列表,再用每个词去匹配过滤后的候选框,匹配失败的文字要给出明确提示,不要静默跳过。
def build_click_sequence(candidates, prompt_words): seq = [] for word in prompt_words: matched = None for cand in candidates: if cand["text"] == word: matched = cand break if matched is None: print(f"警告: 未匹配到 {word}") else: seq.append({"text": word, "cx": matched["cx"], "cy": matched["cy"]}) return seq如果OCR结果里文字本身没问题,只是顺序乱,这个方法能保证输出顺序和提示一致。但现实中OCR经常把“安”识别成“口”或“女”,这时需要调整过滤逻辑:对单个汉字的点选,优先做“字符包含匹配”而不是“词匹配”,比如word in matched_text or matched_text in word。另外,很多点选验证码的提示词本身就是图片里的一个词,比如提示“点击:验证”,图片里可能既有“验”和“证”两个单字,也有“验证”一个词,这时要优先匹配词,其次匹配单字,避免重复点击。
4.2 用Selenium驱动浏览器并点击
坐标算好之后,剩下的就是模拟点击。我一般用Selenium,因为课设环境兼容性最好。核心是拿到图片元素的位置,再相对于图片左下角移动鼠标点击。
from selenium.webdriver import Chrome, ChromeOptions from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains options = ChromeOptions() # 无头模式适合批量测试,但部分验证码页面会检测无头环境,自行决定是否开启 # options.add_argument("--headless") browser = Chrome(options=options) browser.get("https://example.com/login") img = browser.find_element(By.ID, "captcha_img") src_w = int(img.get_attribute("naturalWidth")) src_h = int(img.get_attribute("naturalHeight")) disp_w = img.size["width"] disp_h = img.size["height"] for step in click_seq: target = scale_coords(step, src_w, src_h, disp_w, disp_h) ActionChains(browser).move_to_element_with_offset( img, target["cx"], target["cy"] ).pause(0.2).click().perform()move_to_element_with_offset的基准点是元素左上角,所以传入的偏移不需要再加图片在页面里的绝对位置。pause(0.2)是为了模拟人类点击间隔,很多验证码会检测点击间隔是否过度均匀或过快。如果点击后验证码还是刷新,先检查坐标有没有落在文字中心,其次检查点击速度。
4.3 给课设加一个Web演示界面
课设答辩时不能只给别人看命令行输出,最好有一个本地Web页面,上传验证码图片就能显示识别结果和点选坐标。用Flask写这个界面很快,一个.py文件加一段HTML模板就够:
from flask import Flask, request, render_template_string from paddleocr import PaddleOCR import cv2, os app = Flask(__name__) ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) UPLOAD_DIR = "./uploads" os.makedirs(UPLOAD_DIR, exist_ok=True) HTML = """ <form method="post" enctype="multipart/form-data"> <input type="file" name="img"> <button type="submit">识别</button> </form> {% if items %} <table border="1"> <tr><th>文字</th><th>置信度</th><th>中心点</th></tr> {% for item in items %} <tr><td>{{ item.text }}</td><td>{{ item.score }}</td><td>{{ item.cx }}, {{ item.cy }}</td></tr> {% endfor %} </table> {% endif %} """ @app.route("/", methods=["GET", "POST"]) def index(): if request.method == "POST": f = request.files["img"] path = os.path.join(UPLOAD_DIR, f.filename) f.save(path) result = ocr.ocr(path, cls=True) items = parse_ocr_result(result) return render_template_string(HTML, items=items) return render_template_string(HTML, items=None) if __name__ == "__main__": app.run(debug=True)这个演示界面最大的价值是让你在答辩前用真实截图批量验证识别和坐标映射是否正确。很多细节问题,比如“某个字总是识别成另一个字”,通过网页反复传图很快就能看出来,比命令行一遍遍跑要直观得多。生产环境不要开debug=True,但课设演示可以,方便改完代码自动重载。
5. 避坑:文字点选验证码识别中我踩过的四个坑
5.1 OCR把背景纹路识别成文字
现象:解析出来的候选列表里有大量无意义字符,比如“·”“了”“一”这种高频误判,点击时直接多点了背景。
原因:验证码背景里的波浪线、雪花噪点在灰度图里的颜色接近文字,二值化后形态和汉字笔画相似,OCR模型把部分纹路当成文字检测框。
解决:在预处理里加上HSV颜色过滤,只保留文字颜色范围。如果背景和文字颜色太接近,就改用“颜色差异增强”:计算背景主色调,把与主色调距离较远的像素保留,其他的置为白色。核大小不要设得太大,否则会把文字和背景粘连。另一个有效手段是过滤掉面积过小的检测框,因为单个汉字的最小尺寸是有限度的。
5.2 文字旋转导致识别乱码
现象:OCR把“验”识别成“金”或“又”,置信度也不高,关键词匹配失败。
原因:这类验证码的文字随机旋转角度,PaddleOCR的方向分类器对椭圆包围盒支持不够,旋转超过30度时检测框虽然还在,但识别头吃不到完整笔画。
解决:在预处理阶段用OpenCV的minAreaRect对检测到的每个文本区域做旋转校正,把文字转正后再喂给OCR。如果没有精力做旋转校正,最低限度是把use_angle_cls=True打开,并适当放大图片,因为小尺寸下旋转后的汉字信息损失更严重。我试过把图片用cv2.resize放大到原来的2倍,识别率能提升不少。
5.3 点击坐标偏了一个身位
现象:中心点算出来了,点击位置总是偏上或偏左,验证码提示“点错了”。
原因:最常见的不是OCR坐标算错,而是网页里的图片被CSS缩放。PaddleOCR返回的是原图像素坐标,而move_to_element_with_offset用的是浏览器显示坐标,两者没有换算。除此之外,图片外层如果有border或padding,元素左上角已经包含了这部分偏移,但鼠标偏移里没有再加。
解决:统一用naturalWidth/naturalHeight和img.size做比例换算,并且在点击前先用浏览器打开一张已知位置的图片测试坐标是否准确。如果图片是背景图,而不是img元素,就要先拿到背景图所在的div的坐标,再用背景图的实际尺寸和CSS尺寸比进行换算。
5.4 答题顺序出错导致验证码刷新
现象:所有文字都能识别出来,坐标也对,但点击完三个字后验证码马上刷新,提示失败。
原因:验证码要求按照提示词顺序点选,但你按OCR输出的顺序点。OCR输出顺序是从上到下检测到的,和验证码设定顺序基本无关。尤其当验证码要求“先点右下角,再点左上角”时,按检测顺序点必然全错。
解决:先从页面里抓取提示词,比如“请依次点击:登录 验证 安全”,拆成列表,按列表顺序找到对应文字框再点击。抓不到提示词时,退化为“从左到右、从上到下”排序,但成功率会明显下降。更高阶的做法是把提示词的顺序也交给模型理解,但课设阶段用文本匹配就够。点击间隔建议在0.2到0.5秒之间随机,过快的机械化点击会被风控识别。
6. 让课设更完整:加一个批量验证脚本与四个参数调优建议
最后一章的落点,放在“如何证明你的方案真的可用”。课设答辩最怕的是演示时现场翻车。我建议你把验证码截图分成三组:同源背景不同文字、不同背景同字体、带旋转文字,每组准备十张图,写一个批量验证脚本,统计“识别率”和“点击成功率”。识别成功指所有提示词都找到了坐标,点击成功指验证码通过。
批量验证其实很简单:循环遍历文件夹里的图片,调用预处理和OCR,用提示词匹配判断结果,把失败图片单独存到一个目录里。跑过一轮之后,统计出来的数据比口头解释“识别率很高”有说服力得多。针对失败样本,再调整下面四个参数,这是我试下来影响最大的:
| 参数位置 | 建议值 | 影响 |
|---|---|---|
use_angle_cls | True | 方向分类器对倾斜文字很关键,能提升旋转场景的识别率 |
| 置信度过滤阈值 | 0.5~0.6 | 过滤低置信度结果,太低会混入背景噪点,太高会丢掉有效字 |
| 预处理kernel大小 | (3, 3)或(5, 5) | 小核保留细节,大核去噪,文字笔画细的用前者 |
| 图片缩放倍数 | 2倍 | 旋转小字放大后再识别,准确率提升明显,代价是耗时变长 |
如果你用的是PaddleOCR 2.x,部分版本的ocr.ocr直接支持传drop_score,如果提示参数错误,就在解析结果时按上面的置信度阈值自己过滤。另一个能让答辩更出彩的技巧是:把识别失败的文字截图存下,用cv2.imwrite单独放一个目录,最后总结时直接展示“失败原因是旋转过大”比空谈“需要调参”更有技术含量。
我自己做这个课设时,最头疼的就是坐标映射,第一次演示时所有文字都识别对了,但怎么点都提示“请正确点击”,后来才发现是CSS缩放导致偏移。从那次以后我养成了一个习惯:所有涉及坐标的功能,先打印原始坐标和缩放后的坐标,用一张图校准后再上整套流程。希望帮到你。
本文还有配套的精品资源,点击获取