文字点选验证码识别的关键不只是OCR认字,而是把“定位文字、识别文字、按题目顺序匹配、坐标换算、模拟点击”串成一条可验证的流水线。原文围绕课程设计和自动化测试场景拆解了数据合成、PaddleOCR推理、点击序列生成和鲁棒性评估。原文给出的参考数据是:300张小样本训练条件下约96%准确率,单次识别100~300ms,适配Windows下Python3.6/3.8/3.10,可作为性能基线。
一、全链路与核心难点
典型流程是:先用浏览器开发者工具抓取验证码接口,拿到背景图地址和题目文字;再用OCR识别背景图中的文字及位置;然后按题目字符顺序做匹配;最后把图片坐标按页面渲染比例换算成点击坐标,用Selenium或pyautogui完成点击。
这类验证码的难点在“先找到字在哪”。背景纹理、字体变形、旋转错位、颜色混杂和干扰线都会影响识别。因此不能把整页截图直接丢给OCR就当前点击候选,检测阈值要调高并过滤低置信度文本框,避免干扰线被识别成“一”等字符后扰乱匹配。原文建议用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):
- 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, '_'.join(texts) + '_' + str(len(os.listdir(save_dir))) + '.png'))
- 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:
- conda create -n captcha python=3.9 -y
- conda activate captcha
- pip install paddlepaddle==2.5.2
- pip install paddleocr==2.7.3
复制代码
最小推理代码如下,PaddleOCR的PP-OCRv4把检测和识别能力封装在同一个Python包中,predict接口返回检测框坐标、识别文本和置信度。
- 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包含四角点坐标、识别文字和置信度,中心点用左上角和右下角平均即可。PaddleOCR返回的是原始图片像素坐标,图片一旦缩放,点击坐标也必须按比例缩放,否则点击偏移会导致验证失败。
四、微调与两类识别错误
原文的判断是:PaddleOCR官方中文识别模型对印刷体汉字精度已经很高,课程设计里的黑体、宋体验证码微调必要性不大,真正值得做的是数据合成和检测阈值调试。如果必须训练,标注格式可写成图片路径、制表符、文本框四点坐标、制表符、文字内容;训练命令例如:
- 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可在内层循环未找到时提示缺字。
- 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}')
复制代码
如果OCR把“快”识别成“怏”,精确匹配会失败,可用编辑距离做相似度兜底。原文给出的改进函数用SequenceMatcher计算相似度,再乘OCR置信度和经验系数0.8;系数调低会错误接受,调高会漏识别,课设中重点是流水线跑通。
- 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
复制代码
六、从图片坐标到屏幕点击坐标
隐蔽坑在于OCR坐标是图片坐标,而Selenium点击发生在页面元素坐标系。验证码img元素渲染尺寸可能和原图不一致,必须先算缩放比例,再叠加元素在页面中的位置。
- 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屏幕坐标,浏览器未置顶或页面有滚动条时还要额外调整。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,再执行匹配逻辑,比较点击序列与目标文字是否完全一致,统计全部正确、部分错误、完全失败比例。下面代码保留原文评估脚本结构。
- 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')
复制代码
如果准确率低于80%,优先调det_db_box_thresh和匹配置信度阈值,而不是直接换模型。更多时候问题来自合成数据与真实图片的分布差异,例如合成用黑体、线上是宋体,OCR置信度天然偏低;此时加入几十张真实线上截图,分布对齐带来的提升往往超过调参。进阶可做模板匹配双通路:首次用OCR,失败后降级到模板相似度。工程化还要注意PaddleOCR实例会加载多个模型文件,首次调用很慢,不要在循环里频繁创建,应在程序启动时创建一次并全局复用;用Flask或FastAPI对外服务时尤其重要,否则每个请求都要经历模型加载。 |