python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python OCR文字识别

Python与OCR文字点选验证码识别全流程实战解析

作者:用户甲

本文用Python和PaddleOCR带你从合成数据、模型微调到模拟点击,一步步实现96%准确率的自动识别方案,代码适配Windows,低配置也能跑,附完整资源包,直接照着做就能通过验收,感兴趣的朋友跟随小编一起看看吧

简介:面向Python学习者与验证码识别开发者的课程设计资源,聚焦“文字点选/选字/选择文字”类验证码的自动识别,适合完成课设、毕设或快速搭建识别服务时参考。资源包共48个文件,含19个Python脚本、模型权重bin、测试图片、前端展示样式等,压缩包约121.82MB;整体工程结构清晰,涵盖模型训练、接口服务、Web演示等模块,代码已适配Windows下Python3.6/3.8/3.10,低配置服务器也可平稳运行。已有437人学习下载。该方案在仅300张小样本训练条件下实现约96%准确率,单次识别耗时100~300ms,兼顾效果与性能;同时附带模型文件、演示图片、依赖清单及说明文档,便于直接运行验证或二次开发,是理解文字点选验证码识别原理与工程落地的实用资料。

文字点选验证码的核心思路是:先用OCR识别出图片里需要点击的文字,再定位这些文字在图片上的坐标,最后模拟点击完成验证。跟你之前做爬虫和自动化时处理反爬的思路是一脉相承的。

🖼️ 整体流程拆解

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

这个脚本里值得注意的参数有三个:

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 对外提供服务,这一点尤其重要,否则每个请求都要经历几秒钟的模型加载过程。

到此这篇关于Python与OCR文字点选验证码识别全流程实战解析的文章就介绍到这了,更多相关Python OCR文字识别内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文