ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

文字点选验证码识别:目标检测与YOLO实战指南

文字点选验证码识别:目标检测与YOLO实战指南 简介这是一份Python实现文字点选验证码文字点选/选字识别课程设计资源面向正在完成相关课设、毕设或想了解小样本验证码识别方案的开发者。整套方案用约300张样本完成训练识别精度约96%单次识别耗时100~300ms并在Windows的Python3.6、3.8、3.10及1核2G低配服务器上实测可运行适合轻量部署。压缩包共48个文件总大小121.82MB以py源码、pyd编译模块为主配合bin模型权重、png/jpg训练图片、gunicorn部署配置、service服务脚本及README说明覆盖数据处理、模型推理、接口封装和上线运行四个环节。资源还包含demo演示脚本与辅助图片可对照查看模型调用流程、接口返回格式和前端选字页面方便快速跑通项目。已有437人学习既可作为课设参考也可直接复用为选字验证码识别工具。1. 文字点选验证码的课设难点不在认字在找字和排序期末课设拿到“Python 作业点击选择文字验证码识别”很多人第一反应是装好 Tesseract、调通 PaddleOCR然后发现一个残酷现实文字点选验证码的难点根本不在“认字”而在“找到字在哪、按什么顺序点”。这类验证码通常给一句提示词——请依次点击请、点、击——页面上的文字被旋转、加干扰线、压在花哨背景里你要先检测出每个文字块的位置再识别内容按提示词的顺序依次执行点击。这本质上是一条“目标检测 文字识别 语义匹配 坐标操作”的组合链路。适合想把计算机视觉课设做出完整工程感的学生也适合爬虫对抗方向想理解文字点选识别原理的从业者。认清这一点你才不会在 OCR 环节白耗两周。2. 技术路线怎么选模板匹配、端到端单模型还是检测识别组合链文字点选验证码的“点选”动作决定了它和你平时做过的验证码识别课设完全不同。普通字符验证码只要把一串字符转成文本文字点选要求的是从图中挑出目标文字并且按提示词的顺序逐个点击。这意味着哪怕你把每个字都认对了只要不知道它在图片上的坐标或者把点击顺序搞反结果都是失败。所以选型前先想清楚你要的最终产物是什么不是一段识别文本而是一串有序的点击坐标。2.1 模板匹配只适合“考试型”验证码最朴素的方案是模板匹配把提示词里的汉字渲染成模板图在验证码大图上做滑窗算归一化互相关得分最高的位置就是该字的坐标。这个方案在答辩 demo 里经常能跑通因为课设演示一般用字体固定、无旋转、背景干净的样例。但真实场景一上来就翻车文字旋转 30 度匹配得分直接掉到 0.2背景有噪点误检框比目标还多。模板匹配的优点是零数据、零训练缺点是稍微换一种字体、加一条干扰线就失效。它本质上是一个“考试型”方案适合你只想在一张图上演示效果不适合做成一个能应对多套验证码的识别系统。2.2 端到端单模型数据成本直接劝退课设有论文思路是把整个任务做成一个目标检测/关键点检测问题输入验证码图片输出 n 个文字的中心点坐标并按提示词顺序回归出一个排序向量。听起来最优雅但课设周期根本扛不住数据成本。文字点选验证码的标注比普通检测数据贵得多每个样本要标 n 个文本框位置、每个位置对应的文字内容、以及点击的先后顺序。一张图上三五个字三个维度的标注缺一不可半手工标注一天也就标一两百张。端到端模型还要解决顺序监督信号怎么定义的问题不同验证码的文字数量不同输出维度是动态的Loss 里的匈牙利匹配得自己写坑非常多。我的结论是除非你已经有上千张标注好的数据否则课设不要选这条路线。2.3 检测识别匹配最稳的组合链绝大多数能落地的文字点选识别方案都是把它拆成三个子问题检测先用检测模型在验证码大图上框出所有文字候选区域输出若干个 bbox 和类别不关心先后顺序识别对每个候选框确认它到底是哪个汉字匹配与排序把提示词解析成目标字符列表逐个在候选框里做匹配再按提示词顺序排序按序点击。三个子问题各自都有成熟工具拆开做的好处是任何一个环节出问题都能单独排查。检测用 YOLO 系列识别有两个分支选择字库有限时直接用 YOLO 的分类头预测字 id字库不可控时裁出文字块丢给 PaddleOCR。最推荐的课设做法是前者——把“识别”退化成“分类”因为常见文字点选验证码的字库就几百个字用一个有限类别检测模型同时输出“文字位置字类别”比“检测OCR”两条模型串联稳定得多。方案需要数据量抗干扰能力开发周期课设通过率模板匹配0极差1 天40%仅演示图端到端单模型1000 张全量标注中3~4 周60%容易过拟合检测分类/识别匹配200~500 张强5~7 天90%这张表的结论很直接目标检测承担“找字”分类头承担“认字”匹配逻辑承担“排序”三个环节用的是同一个模型框架训练和推理链路最短。你不需要会写复杂的 CTC Loss也不需要准备一万张数据这是课设性价比最高的路线。3. 造数据集是第一步合成脚本、标注转换与配比规则文字点选验证码没有公开数据集这是一个所有做这个方向的人都要面对的硬约束。比赛和论文里见过的公开数据大多是标准印刷体 OCR 数据集真实文字点选样本涉及业务方利益基本不会外流。所以课设数据只能自己造而且动力很足你不需要把验证码厂商的样本偷出来只需要合成足够逼真的样例模型照样能学。3.1 合成数据怎么做才接近真实验证码见过太多人用白底黑字合成数据训练出来的模型在真实验证码上直接废掉。合成数据的核心不是把字摆上去而是模拟干扰旋转、色彩抖动、干扰线、背景纹理缺一不可。下面这个脚本把验证码图片生成做成一个可配置的流程# 合成文字点选验证码数据输出图片 每张图的文字位置与类别标签 import random import uuid import os from PIL import Image, ImageDraw, ImageFont, ImageFilter # 字库有限文字点选验证码常见的汉字集合可按实际场景扩充 WORD_POOL [请, 点, 击, 提, 交, 验, 证, 码, 确, 认, 旋, 转, 下, 载, 登, 录, 注, 册, 安, 全] def random_color(lightTrue): if light: return (random.randint(120, 255), random.randint(120, 255), random.randint(120, 255)) return (random.randint(0, 80), random.randint(0, 80), random.randint(0, 80)) def random_font(size): # 注意Windows 请换成 C:/Windows/Fonts/msyh.ttc 或 simhei.ttf font_path /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf return ImageFont.truetype(font_path, size) def draw_one_sample(img_size(480, 320), num_chars4): img Image.new(RGB, img_size, random_color(lightTrue)) draw ImageDraw.Draw(img) # 背景干扰线随机画几条扭曲的线模拟验证码常见干扰 for _ in range(random.randint(5, 10)): x1, y1 random.randint(0, img_size[0]), random.randint(0, img_size[1]) x2, y2 random.randint(0, img_size[0]), random.randint(0, img_size[1]) draw.line([(x1, y1), (x2, y2)], fillrandom_color(lightFalse), width1) labels [] used_chars random.sample(WORD_POOL, num_chars) for ch in used_chars: font_size random.randint(36, 52) font random_font(font_size) # 每个字单独渲染便于控制旋转 char_img Image.new(RGBA, (font_size * 2, font_size * 2), (0, 0, 0, 0)) char_draw ImageDraw.Draw(char_img) char_draw.text((font_size // 2, font_size // 2), ch, fontfont, fill(*random_color(lightTrue), 255)) # 随机旋转-25 到 25 度文字点选验证码最典型的干扰 char_img char_img.rotate(random.randint(-25, 25), expandTrue, resampleImage.BICUBIC) # 随机位置避免文字重叠简化起见直接用随机坐标 cx random.randint(30, img_size[0] - 80) cy random.randint(30, img_size[1] - 80) img.paste(char_img, (cx, cy), char_img) bbox (cx, cy, cx char_img.width, cy char_img.height) labels.append({char: ch, bbox: bbox}) # 整图轻微模糊模拟真实截图的清晰度损耗 img img.filter(ImageFilter.GaussianBlur(radius0.5)) return img, labels if __name__ __main__: os.makedirs(syn_images, exist_okTrue) for i in range(500): img, labels draw_one_sample() img.save(fsyn_images/{uuid.uuid4().hex[:8]}.jpg) with open(fsyn_images/{uuid.uuid4().hex[:8]}.txt, w) as f: for lab in labels: f.write(f{lab[char]} {lab[bbox]}\n) print(synthetic data done)这个脚本的逻辑是把每个汉字单独渲染到透明图层上做随机旋转、随机缩放、随机位置粘贴最后叠加干扰线和轻微模糊。有几个参数值得注意旋转角度设在 25 度以内超过 30 度人眼都难认模型学出来也不可靠字体字号控制在 36 到 52 像素之间太小了检测器下采样后特征丢失太大了一张 480x320 的图放不下互不重叠的四五个字。GaussianBlur 的 0.5 半径不要省略真实验证码是从网页截图里来的截图本身就有缩放损耗不加这步模型会被格式化的“干净”文字惯坏。3.2 标注格式选型VOC 转 YOLO 的转换脚本合成脚本输出的标签是“文字 左上右下坐标”但 YOLO 训练需要的是归一化的 category 和 bbox。如果你的标注工具导出的是 Pascal VOC 的 XML 格式下面这段转换脚本可以直接用# VOC XML 转 YOLO txt把标注工具导出的 XML 转成 YOLO 训练格式 import xml.etree.ElementTree as ET import os # 类别字典必须和合成脚本里的字库对齐训练时字 id 保持一致 CLASS_MAP {请: 0, 点: 1, 击: 2, 提: 3, 交: 4, 验: 5, 证: 6, 码: 7, 确: 8, 认: 9, 旋: 10, 转: 11} # 按需扩充 def voc_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: continue # 过滤掉不在字库里的类别 cls_id CLASS_MAP[name] box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # YOLO 格式class_id, x_center, y_center, width, height均归一化 x_center ((x1 x2) / 2) / img_w y_center ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_name os.path.splitext(os.path.basename(xml_path))[0] .txt with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines)) # 批量转换 xml_dir, out_dir annotations, labels os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if xml_file.endswith(.xml): voc_to_yolo(os.path.join(xml_dir, xml_file), out_dir)这段代码的重点是 CLASS_MAP 必须和训练时的类别顺序严格一致。YOLO 的类别 id 是从 0 开始递增的如果你在合成脚本里字库顺序是“请点击提交…”那么训练脚本里的 data.yaml 也要按这个顺序写一旦错位模型训出来永远把“请”当“点”点击位置全部偏移。转换脚本里我加了类别过滤不在字库里的标注会被跳过这一步能避免标注员误标了一些生僻字导致类别数不统一。3.3 数据集配比与验证集划分合成数据 500 张真实数据哪怕只有 50 张也要混着用。合成数据负责教模型“文字长什么样、干扰长什么样”真实数据负责教模型“网页截图里的纹理和字体风格”。配比上我习惯按 9:1 切训练集和验证集但切分时要用随机抽样不能把合成数据全放训练集、真实数据全放验证集——那样训练集和验证集分布差距太大loss 会忽高忽低你无法判断是模型问题还是数据集问题。如果你能拿到少量真实验证码截图别直接整图塞进训练集。真实截图往往包含提示文字、提交按钮等干扰元素YOLO 会把这些 UI 元素当成背景或者误检成文字。我一般会把真实截图先人工裁掉顶部提示区域只保留放置文字的区域再标注。这一步脏活累活决定了模型的最终上限值得花一下午。4. 把流程跑通YOLO 检测文字框、匹配提示词、按序模拟点击数据集就位后训练本身反而最无脑。用 YOLO 的官方命令行直接训难点在训练参数和后面的推理链路——检测、识别、匹配、排序、点击五个环节任何一个断了最终通过率都上不去。4.1 用 YOLO 训练文字检测器命令与参数# 训练文字检测器yolov8n 轻量模型适合课设场景的几百张数据 yolo detect train \ datadataset.yaml \ modelyolov8n.pt \ epochs50 \ imgsz640 \ batch16 \ freeze10dataset.yaml 里要写清楚训练集和验证集路径以及类别列表path: ./dataset train: images/train val: images/val nc: 12 names: [请, 点, 击, 提, 交, 验, 证, 码, 确, 认, 旋, 转]这里几个参数是课设场景最容易调错的。imgsz 用 640 是速度和精度的平衡点如果你的验证码图片本身就很大可以升到 768但训练时间会明显变长batch 设为 16 的前提是显存不低于 8G如果笔记本显存只有 4G改成 8 更稳否则直接 OOM。freeze10 是冻结前 10 层 backbone对小数据集非常有用你只有几百张图从头训练 backbone 极易过拟合冻结早期层让它只学中高层的语义特征收敛更稳。epochs 50 是上限配合 early stopping 实际可能 20 轮就停了不用死等。4.2 推理脚本检测、匹配、排序、点击一次跑通训练完拿到 best.pt接下来是推理链路。这个脚本把整个点选流程串起来# 文字点选推理主流程检测 - 匹配 - 排序 - 点击 import re import time import random import pyautogui from ultralytics import YOLO from pypinyin import lazy_pinyin from rapidfuzz import fuzz model YOLO(runs/detect/train/weights/best.pt) def parse_prompt(prompt_text): 从提示词里取出目标文字序列忽略请依次点击等指令前缀 # 常见句式请依次点击请、点、击 去掉指令部分保留中文目标字 segment re.split(r[:], prompt_text)[-1] chars [ch for ch in segment.strip() if re.match(r[\u4e00-\u9fa5], ch)] return chars def char_similarity(a, b): 字相似度 max(表面编辑距离, 拼音相似度)拼音兜底同音字 surface fuzz.ratio(a, b) / 100.0 try: pa, pb lazy_pinyin(a)[0], lazy_pinyin(b)[0] phonetic fuzz.ratio(pa, pb) / 100.0 except Exception: phonetic 0.0 return max(surface, phonetic) def detect_text_boxes(screenshot_path): YOLO 检测返回 (类别名, bbox, 置信度) 列表按置信度降序 results model.predict(screenshot_path, conf0.55, iou0.45) boxes [] names model.names for r in results: for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cls_id int(box.cls[0]) score float(box.conf[0]) boxes.append((names[cls_id], (x1, y1, x2, y2), score)) # 置信度降序保证匹配时优先取最可靠的框 boxes.sort(keylambda x: x[2], reverseTrue) return boxes def match_prompt_to_boxes(boxes, target_chars): 对提示词里的每个字在检测框里找最相似的候选返回有序的点击坐标 used set() ordered [] for ch in target_chars: best_bi, best_score None, 0.0 for bi, (cls_name, bbox, score) in enumerate(boxes): if bi in used: continue # 每个框只能被点一次 sim char_similarity(ch, cls_name) if sim best_score: best_score, best_bi sim, bi if best_bi is None: return None # 提示词里有字没找到本次识别失败 used.add(best_bi) ordered.append(boxes[best_bi][1]) return ordered def click_in_order(ordered_boxes, offset_x0, offset_y0, scale1.0): 按顺序点击坐标做缩放和偏移时间间隔模拟人工降低行为特征 for bbox in ordered_boxes: x1, y1, x2, y2 bbox cx (x1 x2) / 2 * scale offset_x cy (y1 y2) / 2 * scale offset_y # 中心点附近加随机偏移避免每次都点同一像素 jitter_x random.uniform(-3, 3) jitter_y random.uniform(-3, 3) pyautogui.click(cx jitter_x, cy jitter_y) time.sleep(random.uniform(0.2, 0.5)) # 随机等待模拟人工点击节奏 # 主流程 if __name__ __main__: screenshot_path captcha_now.png prompt_text 请依次点击请、点、击 target_chars parse_prompt(prompt_text) boxes detect_text_boxes(screenshot_path) ordered match_prompt_to_boxes(boxes, target_chars) if ordered is None: print(存在未匹配到的目标字需要保留现场排查) else: # 实际使用时按浏览器截图的偏移量和缩放比调整 click_in_order(ordered, offset_x0, offset_y0, scale1.0)这个脚本的每个环节我都加了超出“能跑”的细节。parse_prompt 里用正则先切掉冒号前的指令前缀是为了防止“请依次点击”这五个字里的“请”被误当成目标字如果提示词句式变化比如“按顺序选择…”也能兼容match_prompt_to_boxes 里用“每个框只能被点一次”的 used 集合是防止两个字匹配到同一个检测框——这种情况出现频率比你想象的高字库相近的字模型本来就容易混淆click_in_order 里的随机延迟不是玄学验证码风控会记录点击间隔如果两次点击间隔恒定为 0 毫秒行为特征一眼假。4.3 阈值怎么调conf 与 iou 的边界检测的 conf 和 iou 是推理环节最值得调的两个参数。conf0.55 意味着置信度低于 0.55 的框会被丢弃调低了容易把背景纹理误检成文字调高了真实文字被漏检尤其在旋转超过 20 度的样子上。iou0.45 是 NMS 的合并阈值多个重叠框会合并成一个调太大会把相邻的两个文字框合并成一个调太小同一个字会输出两个框导致匹配逻辑去重失效。我的习惯是先跑 100 张验证集统计漏检数和误检数conf 从 0.5 起步每次加 0.05直到漏检率显著上升的一刻那个点就是当前数据集的 conf 上限。阈值调参没有标准答案每套验证码的合成数据和真实数据分布不同唯一靠谱的方法就是回放测试。5. 通过率暴跌的 5 个坑现象、原因与解法这一节的每条经验都来自实际跑数据翻车后的排查记录按“现象 → 原因 → 解决”写清楚基本覆盖课设答辩前最常卡住你的问题。5.1 检测框很准但单字识别总对不上现象YOLO 把四个文字框全部框对了位置但提示词是“请、点、击”模型预测的分类结果却是“讠青、点、陆”相似度匹配直接失败。原因合成数据里的字体和真实验证码字体不一致模型学到的是合成字体的纹理特征真实验证码的文字旋转加压缩后笔画挤压让“请”看起来像“讠青”令分类头错判。解决字库有限时不要把识别和检测拆成两个模型直接用 YOLO 的分类头输出字 id因为分类头只区分字库里的几十个类比通用 OCR 的几千个类简单得多同时在合成数据里混入 10% 到 20% 的真实截图字体让分类头学到真实字体的笔画变形。5.2 点击坐标和文字位置偏了一截现象检测框在图片上的位置画出来完全正确但 pyautogui 点下去点到了文字旁边的空白处。原因模型预测的坐标是相对于验证码图片的而 pyautogui 点击的是屏幕坐标。如果验证码图片是截屏后裁剪的或者浏览器有缩放比例模型输出坐标必须经过缩放和偏移换算。解决在建图时就记录截图的原始尺寸和模型输入尺寸换算公式很简单# 截图坐标 - 屏幕坐标先除模型缩放比再加截图区域在屏幕上的偏移 screen_x model_x / scale_factor_x offset_x screen_y model_y / scale_factor_y offset_yscale_factor 是截图原始宽度除以模型输入宽度offset 是截图区域左上角在屏幕上的位置。我一般会在推理脚本里把这两个值做成配置项而不是写死在代码里因为浏览器窗口位置一变就得重新算。5.3 合成数据太干净换一套验证码就翻车现象训练集 loss 收敛到 0.02验证集准确率 99%但只要换一个网站的验证码通过率直接掉到 30%。原因合成数据用的是纯色背景加规则干扰线真实验证码的背景是复杂纹理、渐变、噪点叠在一起模型只学会了在“干净”图上找文字背景一变就当机。解决在合成脚本里加背景纹理混合层把真实验证码的截图背景直接抠出来当底图再把文字贴上去训练时开启 HSV 扰动和随机模糊。这条是数据增强里最有效的一招背景一变你的模型泛化能力立刻暴露。5.4 提示词带方向“从右往左依次点击”现象提示词是“请从右往左依次点击提、交、验、证”模型识别和匹配全部正确但点击顺序变成从左到右验证失败。原因parse_prompt 只提取了目标汉字把“从右往左”这个方向指令丢弃了。解决在解析函数里先检测方向词包含“右往左”就对目标坐标按 x 降序排列包含“下往上”按 y 降序排列其他情况按 y 升序通常验证码文字是横向排列的。不要小看这个细节课设答辩时老师经常故意换一套带方向词的验证码来试你。5.5 笔记本训练几百张图过拟合到亲妈都不认识现象训练到第 15 轮验证集 loss 开始反弹但训练集 loss 还在降说明模型在背训练图。原因数据量只有几百张模型容量太大把每张图的随机干扰线都记住了。解决第一模型从 yolov8l 换成 yolov8n参数减少一大截第二freeze10 冻结 backbone 前 10 层第三数据增强打开合成数据里把旋转角度、干扰线条数、背景纹理的随机范围加大第四加 early stoppingpatience 设为 10 轮val loss 连续 10 轮不降就停。这四步组合下来几百张图也能训出不明显过拟合的检测器。6. 验证方法兜底与把课设做出进阶分训练完模型不要只看 val loss那东西说服不了答辩老师。我会写一个批量回放脚本准备 50 张真实验证码截图每张图配好提示词脚本自动执行“截图 - 检测 - 匹配 - 点击”全流程然后统计最终通过率。这里的关键不是说单字识别率而是“整题通过率”——五个字点对四个也算失败这个指标才和真实业务一致。我一般给自己定的及格线是 80%低于这个数字回去调数据增强和阈值高于 90% 再考虑扩展字库。6.1 用真实验证码批量回放测通过率回放脚本不用特意写直接把第 4 章的推理脚本包一层循环失败时保留“提示词截图模型输出”的三元组现场方便排查是哪一环挂了。注意每次回放之间要加随机等待连续高频请求会触发风控通过率会被人为拉低测出来不是模型真实水平。回放还有个好处你能统计出失败样本的分布——是“字识别错了”还是“字找到了但排序错”这两个问题的对策完全不同。6.2 把同类字合并成语义簇用拼音词向量兜底如果做完基础版还想冲优秀我建议在两个方向上升级。第一个是把匹配逻辑从单字编辑距离换成“拼音相似度 语义词向量”的融合打分。文字点选验证码经常用同音字干扰提示词写“请点点击”图片里放的是“请点讠击”表面编辑距离匹配不上但拼音完全一致用 pypinyin 转拼音后比对就能对上。第二个方向是把纯检测模型升级成带中心点回归的轻量关键点模型直接用 CenterNet 的思路回归 n 个文字中心点省掉 NMS 环节推理更快——这个方向写进课设报告里能体现出你对检测原理的理解不止于调包。我当年在这个课设上最大的教训就是死磕 PaddleOCR 的单字识别率花了整整一周在“请”被识别成“讠青”的泥潭里打转。后来把思路从“识别一张图里的所有字”转成“用分类头预测有限字库里是哪个字”所有问题迎刃而解。这个方向如果你能老老实实把合成数据做好、把坐标换算写对、把回放脚本跑起来通过率做到 85% 以上是大概率事件。希望帮到你。本文还有配套的精品资源点击获取
返回列表