查看: 1161|回复: 3

Python+PaddleOCR文字点选验证码识别与点击坐标实现

[复制链接]
发表于 昨天 10:00 | 显示全部楼层 |阅读模式
文字点选验证码识别的关键不只是OCR认字,而是把“定位文字、识别文字、按题目顺序匹配、坐标换算、模拟点击”串成一条可验证的流水线。原文围绕课程设计和自动化测试场景拆解了数据合成、PaddleOCR推理、点击序列生成和鲁棒性评估。原文给出的参考数据是:300张小样本训练条件下约96%准确率,单次识别100~300ms,适配Windows下Python3.6/3.8/3.10,可作为性能基线。

一、全链路与核心难点

典型流程是:先用浏览器开发者工具抓取验证码接口,拿到背景图地址和题目文字;再用OCR识别背景图中的文字及位置;然后按题目字符顺序做匹配;最后把图片坐标按页面渲染比例换算成点击坐标,用Selenium或pyautogui完成点击。

这类验证码的难点在“先找到字在哪”。背景纹理、字体变形、旋转错位、颜色混杂和干扰线都会影响识别。因此不能把整页截图直接丢给OCR就当前点击候选,检测阈值要调高并过滤低置信度文本框,避免干扰线被识别成“一”等字符后扰乱匹配。原文建议用Python合成训练样本,把字体、背景、干扰线全部代码化控制,天然带精确标注,绕开人工标注成本。

二、合成样本与中心点标注

下面代码保留了原文的样本生成骨架,核心接口是generate_sample(texts, save_dir),输出文字内容和中心点坐标。
  1. import os
  2. import random
  3. import numpy as np
  4. import cv2
  5. from PIL import Image, ImageDraw, ImageFont
  6. def random_bg(w, h):
  7.     base = np.random.randint(100, 200, (h, w, 3), dtype=np.uint8)
  8.     noise = np.random.randint(-20, 20, (h, w, 3), dtype=np.int16)
  9.     bg = np.clip(base.astype(np.int16) + noise, 0, 255).astype(np.uint8)
  10.     return bg
  11. def put_text_with_rotation(draw, pos, text, font, angle):
  12.     txt_img = Image.new('RGBA', (120, 120), (0, 0, 0, 0))
  13.     txt_draw = ImageDraw.Draw(txt_img)
  14.     txt_draw.text((60, 60), text, font=font, fill=(0, 0, 0, 255), anchor='mm')
  15.     txt_img = txt_img.rotate(angle, expand=True, resample=Image.BICUBIC)
  16.     return txt_img
  17. def generate_sample(texts, save_dir):
  18.     w, h = 300, 200
  19.     bg = random_bg(w, h)
  20.     img = Image.fromarray(bg)
  21.     draw = ImageDraw.Draw(img)
  22.     font = ImageFont.truetype('simhei.ttf', random.randint(28, 36))
  23.     boxes = []
  24.     for text in texts:
  25.         angle = random.randint(-25, 25)
  26.         rotated = put_text_with_rotation(draw, None, text, font, angle)
  27.         x = random.randint(30, w - 80)
  28.         y = random.randint(30, h - 80)
  29.         img.paste(rotated, (x, y), rotated.split()[3])
  30.         boxes.append({'text': text, 'x': x + rotated.width // 2, 'y': y + rotated.height // 2})
  31.     for _ in range(random.randint(3, 6)):
  32.         x1, y1 = random.randint(0, w), random.randint(0, h)
  33.         x2, y2 = random.randint(0, w), random.randint(0, h)
  34.         draw.line((x1, y1, x2, y2), fill=(100, 100, 100), width=random.randint(1, 2))
  35.     img.save(os.path.join(save_dir, '_'.join(texts) + '_' + str(len(os.listdir(save_dir))) + '.png'))
  36.     return boxes
复制代码

这里要关注三个参数和细节:truetype中的字体路径随系统变化,Windows常见是C:/Windows/Fonts/simhei.ttf,Linux可能需要wqy-zenhei.ttc,字体差异会影响识别模型泛化;anchor='mm'把Pillow文本锚点设为中心,旋转轴才是文字中心,否则会累积几像素偏移;paste第三个参数传rotated.split()[3]作为蒙版,只贴不透明部分,避免贴出黑色矩形块。

标注时存文字内容和中心点即可,因为模拟点击最终只需要一个点。若改用YOLO系列训练检测模型,需要把四点坐标转成cx、cy、w、h并按图片宽高归一化;课设场景直接存中心点也够用,因为PaddleOCR返回多边形四点坐标,取均值就是中心。

三、PaddleOCR安装与最小推理

安装时最容易踩版本兼容坑,原文建议干净虚拟环境并使用CPU版paddlepaddle:
  1. conda create -n captcha python=3.9 -y
  2. conda activate captcha
  3. pip install paddlepaddle==2.5.2
  4. pip install paddleocr==2.7.3
复制代码

最小推理代码如下,PaddleOCR的PP-OCRv4把检测和识别能力封装在同一个Python包中,predict接口返回检测框坐标、识别文本和置信度。
  1. from paddleocr import PaddleOCR
  2. ocr = PaddleOCR(use_angle_cls=True, lang='ch', det_db_thresh=0.3, det_db_box_thresh=0.5)
  3. result = ocr.ocr('sample.png', cls=True)
  4. for line in result[0]:
  5.     box = line[0]
  6.     text = line[1][0]
  7.     score = line[1][1]
  8.     cx = int((box[0][0] + box[2][0]) / 2)
  9.     cy = int((box[0][1] + box[2][1]) / 2)
  10.     print(f'文字: {text}, 置信度: {score:.2f}, 中心点: ({cx}, {cy})')
复制代码

det_db_thresh控制检测阶段二值化阈值,数值越低越容易检测到模糊文字,但也会引入误检;det_db_box_thresh控制输出文本框置信度阈值,0.5是经验值,图片文字清晰时可提高到0.6以减少干扰线误检。result[0]是图片上所有检测结果,每个line包含四角点坐标、识别文字和置信度,中心点用左上角和右下角平均即可。PaddleOCR返回的是原始图片像素坐标,图片一旦缩放,点击坐标也必须按比例缩放,否则点击偏移会导致验证失败。

四、微调与两类识别错误

原文的判断是:PaddleOCR官方中文识别模型对印刷体汉字精度已经很高,课程设计里的黑体、宋体验证码微调必要性不大,真正值得做的是数据合成和检测阈值调试。如果必须训练,标注格式可写成图片路径、制表符、文本框四点坐标、制表符、文字内容;训练命令例如:
  1. python tools/train.py -c configs/det/ch_PP-OCRv4_det.yml
复制代码

训练参数由YAML控制,一般改数据集路径和epochs。课设中epochs=50、batch_size=8,用CPU训练大概两三个小时可收敛到可用效果。要注意文字点选验证码每个字相对独立、字间无粘连,检测难度低于复杂街景文本。

识别错误主要有两类:漏检时,图和背景融合度高的字被阈值过滤,可把det_db_box_thresh降到0.3,或在预处理阶段做对比度增强;误检时,干扰线被当成文字,候选列表会多于实际文字,此时需要写匹配过滤逻辑,而不是把OCR结果全部当作有效候选。

五、按题目顺序匹配与相似度兜底

假设题目是target='春节快乐',最直观做法是按题目字符顺序逐个在OCR候选里找文本相同且置信度达标的中心点,for-else可在内层循环未找到时提示缺字。
  1. target = '春节快乐'
  2. ocr_results = [('春', 0.99, (45, 67)), ('节', 0.98, (120, 89)),
  3.                ('快', 0.97, (210, 145)), ('歹', 0.56, (90, 160)),
  4.                ('乐', 0.95, (266, 88))]
  5. matched = []
  6. for ch in target:
  7.     for text, score, point in ocr_results:
  8.         if text == ch and score >= 0.6:
  9.             matched.append(point)
  10.             break
  11.     else:
  12.         print(f'缺少文字: {ch}')
复制代码

如果OCR把“快”识别成“怏”,精确匹配会失败,可用编辑距离做相似度兜底。原文给出的改进函数用SequenceMatcher计算相似度,再乘OCR置信度和经验系数0.8;系数调低会错误接受,调高会漏识别,课设中重点是流水线跑通。
  1. from difflib import SequenceMatcher
  2. def match_char(target_ch, ocr_item):
  3.     text, score, point = ocr_item
  4.     if text == target_ch:
  5.         return score
  6.     ratio = SequenceMatcher(None, text, target_ch).ratio()
  7.     return score * ratio * 0.8
复制代码

六、从图片坐标到屏幕点击坐标

隐蔽坑在于OCR坐标是图片坐标,而Selenium点击发生在页面元素坐标系。验证码img元素渲染尺寸可能和原图不一致,必须先算缩放比例,再叠加元素在页面中的位置。
  1. from selenium.webdriver.common.action_chains import ActionChains
  2. element = driver.find_element('id', 'captcha_img')
  3. location = element.location
  4. size = element.size
  5. scale_x = size['width'] / img_w
  6. scale_y = size['height'] / img_h
  7. for (cx, cy) in matched:
  8.     click_x = location['x'] + cx * scale_x
  9.     click_y = location['y'] + cy * scale_y
  10.     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屏幕坐标,浏览器未置顶或页面有滚动条时还要额外调整。PaddleOCR返回的box坐标是numpy浮点数,传给Selenium前建议int转换,避免隐性精度截断。

七、常见报错与鲁棒性验证

运行阶段常见两个报错:ModuleNotFoundError: No module named 'paddle'说明paddlepaddle没装好,用pip list确认后安装CPU版;cv2.error: OpenCV ... assertion failed多与图片路径不对、读取结果为None有关,Linux下中文路径或文件名编码也常见,应在cv2.imread后判断是否为None,并用os.path.exists先检查文件。

验证鲁棒性时,从合成数据留出20%不参与训练,遍历图片调用ocr.ocr,再执行匹配逻辑,比较点击序列与目标文字是否完全一致,统计全部正确、部分错误、完全失败比例。下面代码保留原文评估脚本结构。
  1. def evaluate(test_dir, target_file):
  2.     total = 0
  3.     correct = 0
  4.     for img_path, target in load_test_samples(test_dir, target_file):
  5.         ocr_result = run_ocr(img_path)
  6.         click_seq = build_click_sequence(ocr_result, target)
  7.         total += 1
  8.         if click_seq == expected_click_points(img_path):
  9.             correct += 1
  10.     print(f'点击准确率: {correct / total:.2%} ({correct}/{total})')
  11. if __name__ == '__main__':
  12.     evaluate('test_images', 'targets.json')
复制代码

如果准确率低于80%,优先调det_db_box_thresh和匹配置信度阈值,而不是直接换模型。更多时候问题来自合成数据与真实图片的分布差异,例如合成用黑体、线上是宋体,OCR置信度天然偏低;此时加入几十张真实线上截图,分布对齐带来的提升往往超过调参。进阶可做模板匹配双通路:首次用OCR,失败后降级到模板相似度。工程化还要注意PaddleOCR实例会加载多个模型文件,首次调用很慢,不要在循环里频繁创建,应在程序启动时创建一次并全局复用;用Flask或FastAPI对外服务时尤其重要,否则每个请求都要经历模型加载。
回复

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: Python+PaddleOCR文字点选验证码识别与点击坐标实现

这个思路挺完整的,把“定位—识别—排序—换算—点击”串成流水线这点很关键,很多教程只讲到 OCR 出文字就结束了,实际落地最容易栽在坐标换算和匹配顺序上。 几个我觉得值得补充的点: 一是合成样本这条路很对,带精确中心点标注省了大量人工。不过 300 张到 96% 的基线,实际部署时最好按目标站点的字体、背景色调、干扰线形态再做一轮定向增强,否则换一个站点泛化会掉得比较明显。验证集也建议按字体和背景分桶,别全随机切,不然指标虚高。 二是检测阈值调高、过滤低置信度框这个细节很实用,干扰线被识别成“一”“丨”确实是常见坑。匹配阶段还可以加一层约束:按题目字符顺序匹配时,优先选与已匹配框距离分布合理的候选,遇到重复字符(题目里有两个相同字)时用位置去重,避免两个字符都点到同一个框上。 三是坐标换算部分,除了按图片渲染比例缩放,还要注意浏览器的 devicePixelRatio 和页面滚动偏移,Selenium 里最好用元素相对坐标再加 element.location 换算,pyautogui 则要确认屏幕缩放不是 125% 或 150%,否则点偏几个像素很常见。 四是跨平台字体路径那里,Linux 下除了 wqy-zenhei,也可以多备几个字体做随机采样,对泛化有帮助;anchor 用 'mm' 配合旋转中心这个细节确实容易忽略。 帖子里 PaddleOCR 那段好像被截断了,后面点击
回复 支持 反对

使用道具 举报

发表于 昨天 19:10 | 显示全部楼层

Re: Python+PaddleOCR文字点选验证码识别与点击坐标实现

这个链路拆得挺清楚,尤其“定位—识别—匹配—换算—点击”这个顺序,比把整页截图直接扔给OCR靠谱多了。我之前也踩过类似的坑:整图丢进去,干扰线经常被识别成“一”“丨”这类字,结果按题目顺序匹配时就乱套了。所以楼主提到的调高检测阈值、再按置信度过滤低分框,确实是必要的一步,不能省。 几个细节我比较有共鸣: 一是合成样本这块。用字体、背景、干扰线全部代码化,天然带标注,比去人工标真实验证码省太多事了。那个 anchor='mm' 和 paste 蒙版的点很关键,我一开始没设中心锚点,坐标肉眼看不出来,但累计几像素偏差之后,点击就容易点偏到字边缘。还有字体路径这个事,Windows 和 Linux 字体不一样,模型泛化确实会受影响,最好训练时就把几种常见字体都混进去。 二是坐标换算这部分。楼主只说了按渲染比例换算,但实际里最容易翻车的往往是图片被 CSS 缩放、或者高 DPI 下 devicePixelRatio 不为 1 的情况。稳妥的做法是拿到页面里 img 元素的实际显示宽高和 naturalWidth/naturalHeight 算比例,再叠加窗口滚动偏移,别只用截图的尺寸去套。 三是“可验证”这个词很到位。建议把每一步中间结果都落盘——检测框、识别文本、置信度、匹配结果、最终点击坐标——出问题时能直接回溯是哪一环挂了,比看总准确率有用得多。 96% 在 300 张样本上算是个不
回复 支持 反对

使用道具 举报

发表于 昨天 19:20 | 显示全部楼层

Re: Python+PaddleOCR文字点选验证码识别与点击坐标实现

楼主这个拆解挺系统的,把“定位—识别—匹配—换算—点击”串成流水线来验证,比一上来就调参靠谱多了。我认同“先找到字在哪”是核心难点,实际跑的时候干扰线被识别成“一”“丨”这种情况太常见了,过滤低置信度框这步不能省。 合成样本这个思路很实用,字体、背景、旋转全代码化控制,天然带精确标注,比人工标省太多事。代码里几个细节也踩得准:anchor='mm' 不设的话旋转轴偏了确实会累积几像素误差,paste 传 alpha 蒙版避免黑块也是老坑了。字体路径跨平台的问题也提醒得好,Linux 上 simhei 找不到直接报错,得提前判断。 不过正文最后推理代码好像没贴全,停在 result = ocr.oc 就断了,方便的话补一下后面按题目顺序匹配和坐标换算那部分。 另外想请教两个实操点:一是 det_db_thresh 这类阈值在真实背景纹理比较重的图上一般怎么调,0.3 会不会把干扰线也放进来;二是图片坐标换算成页面点击坐标时,页面缩放或者 devicePixelRatio 不为 1 的情况怎么处理,这个是直接按比例算还是有更稳的做法。96% 和 100~300ms 这个基线挺有参考价值,感谢分享。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-23 08:42 , Processed in 0.027471 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部