
简介这是一份面向计算机相关专业学生与Python开发者的课程设计资源围绕文字点选、选字及选择文字验证码的识别任务展开可帮助读者快速完成从模型构建到接口部署的完整流程。项目在Windows下的Python 3.6、3.8、3.10版本均测试通过识别速度约一百至三百毫秒准确率达到百分之九十六且仅使用三百张样本完成小样本训练在一核二G的低配服务器上也能稳定运行。压缩包共四十八个文件包括十九个Python脚本、四个pyd编译模块、十张png图片、四张jpg演示图、两个bin模型文件以及txt、map等配置文档包体约一百二十一点八二MB目录涵盖src、utils、app、static、model等模块结构清晰便于按功能拆解学习。目前已有四百三十七人学习这类资源既适合作为课程设计或毕业设计的完整参考方案也可用于生产环境中的轻量验证码识别。1. 文字点选验证码是什么一个课设选题凭什么卡住一半人很多人第一次看到“文字点选验证码”这个题目第一反应是“不就是识别几个字吗”真上手才发现和传统数字字母验证码完全不是一回事。传统验证码是“一张图里有一串字符”你要做的是把字符读出来文字点选验证码是“一句话提示 一张散落着文字的图”你要先找到字在哪读对是什么字再按提示顺序点过去。这三步任何一步出错整题失败。更反直觉的是真正让识别率暴跌的往往不是 OCR 认不出字而是坐标映射出错——验证码图片在网页上被 CSS 缩放了设备像素比又不一样你按原图坐标点过去每次偏半个字宽。这类题目适合做 Python 课设是因为它把图像预处理、文本识别、坐标换算、自动化操作四个知识点串成了一条完整链路工作量适中、区分度高。这篇文章就把从“识别”到“点击”的完整落地路径拆开讲清楚。2. 从样本结构到方案选型为什么课设推荐“切字 OCR”而不是目标检测2.1 文字点选验证码的三种常见版式与识别难点我见过、也处理过三类最常见的版式它们的识别难度完全不同。第一类是“提示语 九宫格文字”。整张图被均分成 3×3 或 4×4 的格子每个格子里一个字提示语是“请依次点击‘春’‘夏’‘秋’‘冬’”。这种版式的难点不在定位而在文字识别——格子里的字可能用了艺术字体笔画和常用字库差距很大。第二类是“一句话提示 散落文字”。文字随机分布在图里位置不规律文字之间可能存在轻微交错这类要同时解决“在哪”和“是什么”两个问题也是实际业务里最常见的形态。第三类是“点击图中所有包含‘口’部首的字”这类语义题表面上考文字实际考语义判断课设一般不碰。识别难点也分三档第一档是文字本身小、分辨率低OCR 容易把“己”看成“已”第二档是文字有旋转或倾斜预处理时如果直接做水平矩形切割框里会混入背景噪点第三档是提示文字里的目标字在图中用完全不同的字体渲染训练集里没见过这个字型识别模型直接懵。做课设之前先把自己的输入归好类能避免后面大量的无效调参。2.2 三类实现路线对比模板匹配、YOLO目标检测、OCR切字怎么选针对文字点选行得通的技术路线其实有三条成本和效果差异很大。方案优点缺点适合场景模板匹配matchTemplate代码量最小不用训练几十行就能出效果对字体、字号、颜色极度敏感字体一变就全废固定页面、固定字体的演示 DemoYOLO 系列目标检测能直接输出文字框和类别对旋转文字鲁棒需要手工标注几百张图片的每个文字框课设时间根本不够数据集充足、追求实际效果的项目图像切字 OCR不依赖标注数据利用现成 OCR 引擎零训练成本对图像预处理质量要求高粘连文字容易切错课设、小样本、快速验证的最优选我的建议是第三条路线先通过图像处理把图里的每一个字单独切出来再对切出来的小图做 OCR。理由很直接文字点选验证码里的文字通常是大字号、独立摆放、彼此不粘连这恰好是轮廓检测最舒服的场景而 OCR 引擎无论是 PaddleOCR 还是 RapidOCR对“单字大图”的识别准确率远高于“整图多字混排”。YOLO 路线看着高级但你要标注数据一个班三十个人一起做课设光标注就够你熬夜两周。模板匹配路线则太脆换个字体就翻车答辩演示时经不起问。那为什么不直接对整张图做 OCR 拿到所有文本框这里有个实用的细节整图 OCR 适合读连续的文本行而验证码图里的字是散的、且字号不一OCR 引擎倾向于把相邻的字合并成一个框导致你拿到的坐标是多个字的中心没法精确映射到单个字上。自己切字再识别坐标完全由你掌控匹配逻辑也更清楚答辩时还能顺势讲清楚每一环为什么这么做。2.3 环境准备先把依赖装对再谈识别准确率这个项目最少需要四个包OpenCV图像处理、NumPy数组操作、PaddleOCR 或 RapidOCR文字识别、PyAutoGUI 或 Selenium点选动作。命令行装依赖的方式python -m pip install opencv-python numpy python -m pip install paddlepaddle paddleocr python -m pip install pyautogui如果你的电脑性能一般不想装 PaddlePaddle 全家桶可以换 RapidOCR纯 ONNX Runtime 推理体积小很多python -m pip install rapidocr_onnxruntime参数说明PaddleOCR 第一次运行会自动下载检测和识别模型大约几十 MB网络不好时要等很久这不是卡死是它在后台下载耐心等即可。RapidOCR 的优势是依赖少但对 CPU 的占用偏高识别单张图时可能把 CPU 打满这是它的典型槽点后文 6.3 我会说怎么绕开。安装阶段最容易翻车的不是 OCR 包本身而是更基础的库很多同学跑pip install opencv-python时报错本质是 Python 环境变量没配好、pip 指向了别的 Python 版本。我的习惯是装完依赖后先跑一句验证别直接上完整代码python -c import cv2, numpy; print(cv2.__version__, numpy.__version__)能打印出版本号再继续往下做。如果 import cv2 失败先检查是不是装了opencv而不是opencv-python这两个包名只差几个字符坑过无数人。另外一个问题是 Pillow 缺失PaddleOCR 内部会用到直接装上python -m pip install pillow。3. 图像预处理与单字切割把验证码图变成“一张图一个字”3.1 预处理顺序灰度化、去噪、二值化顺序不能乱图像预处理直接决定后面的轮廓检测能不能切出干净的单字而预处理步骤的顺序是有讲究的。我一般遵循“彩色转灰度 → 去噪 → 二值化 → 形态学过滤”的固定顺序。先灰度化因为颜色信息对文字识别没有帮助反而会增加计算量先降噪再二值化是为了避免把噪点放大成黑点最后做形态学开运算是为了断开文字笔画之间的细微粘连。核心代码如下import cv2 import numpy as np img cv2.imread(captcha.png) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊核大小用奇数3 或 5 就够太大容易把笔画糊掉 blur cv2.GaussianBlur(gray, (3, 3), 0) # Otsu 自动阈值二值化不需要手调阈值适合背景复杂的验证码 _, binary cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 开运算去噪点2x2 的矩形核对细小的孤点很有效 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)逻辑说明THRESH_BINARY_INV的作用是把文字变成白色、背景变成黑色因为 OpenCV 的findContours默认找白色区域。如果你的验证码图背景是深色、文字是浅色要改用THRESH_BINARY不然后续全反了。THRESH_OTSU会自动计算最优阈值省去反复手调的过程这是做验证码识别时一个能明显提升效率的细节。形态学开运算是先腐蚀再膨胀作用是去掉图像中的孤立噪点同时尽量保持文字笔画结构。3.2 轮廓检测切单字boundingRect和minAreaRect怎么选二值化之后就可以找轮廓了。OpenCV 的findContours会返回图中所有白色连通区域对文字点选验证码来说每个文字基本就是一个独立的轮廓。但这里有个常见问题文字如果有轻微旋转直接用一个平行于坐标轴的正矩形去框它会把旋转产生的大量空白背景也框进来影响后续 OCR 识别。教科书里一般只教boundingRect但真实验证码图里文字带旋转是常态。我一般会先判断文字的旋转程度再做两种处理。如果文字是水平的直接boundingRect如果有明显角度用minAreaRect最小外接矩形拿旋转角度再做个透视矫正把文字转正。完整代码如下contours, _ cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) char_boxes [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) # 过滤噪点太小的轮廓基本是干扰 if w 10 or h 10: continue # 过滤太大可能是整张图的背景或其他元素 if w img.shape[1] * 0.5 or h img.shape[0] * 0.5: continue char_boxes.append((x, y, w, h)) # 按从左到右、从上到下排序方便后续匹配顺序 char_boxes.sort(keylambda b: (b[1] // 30, b[0]))参数说明RETR_EXTERNAL只取最外层轮廓避免文字内部笔画形成的空洞产生干扰CHAIN_APPROX_SIMPLE压缩轮廓点减少内存占用。宽高过滤阈值要按你的图片尺寸调整10 像素是我做过的最保守的下限图片整体分辨率偏高时可以提高到 20。排序时b[1] // 30是把纵向距离在 30 像素内的文字视为同一行如果图中有明显多行文字这个值要适当调大。如果你发现两个相邻字被圈进同一个轮廓说明之前二值化或形态学的参数没配合好笔画之间没有完全断开。这时候把开运算的核从(2, 2)调到(3, 3)再做一次通常能解决。具体排查思路我会放在第 5 章的避坑部分展开。3.3 统一尺寸与padding影响OCR识别率的最大变量切出来的单字图尺寸不统一有大有小直接丢给 OCR 引擎时识别率会很不稳定。OCR 模型在固定输入尺寸上效果最好所以切完图之后要统一做缩放。但这里有一个细节直接resize会破坏文字的宽高比长宽比本来就失调的字会更难认。我的做法是等比例缩放后做 padding 补边保持文字形状不变。代码如下def normalize_char(char_img, size48): h, w char_img.shape[:2] scale min(size / w, size / h) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(char_img, (new_w, new_h), interpolationcv2.INTER_CUBIC) canvas np.ones((size, size), dtypenp.uint8) * 255 x_off (size - new_w) // 2 y_off (size - new_h) // 2 canvas[y_off:y_off new_h, x_off:x_off new_w] resized return canvas逻辑说明scale取宽和高中较小的比值保证文字完整不裁切INTER_CUBIC双三次插值放大效果比默认的线性插值更平滑小字放大时能减少马赛克感canvas用 255白色填充因为二值化图里文字是白色、背景是黑色如果文字本身是黑色背景是白色需要反转后再做 padding。48 像素是我测试下来性价比比较高的尺寸再大 OCR 的耗时成倍增加准确率提升却非常有限。这一步还有个容易忽略的坑padding 用的颜色必须和文字背景色一致如果背景是黑色而你把 padding 填成白色相当于在文字周围加了一个白色大边框OCR 会把边框内容也当成了输入的一部分。做课设时我建议把每一步的中间图都保存下来亲眼看看输入到 OCR 里的图到底长什么样很多玄学问题一眼就找到了。4. 目标文字匹配与坐标映射从“识别出字”到“点对位置”4.1 OCR选型与单字识别循环单字切割完成后接下来就是让 OCR 逐个识别切出来的小图。我自己的选择是优先 PaddleOCR因为它的中文识别准确率在开源方案里属于第一梯队如果环境受限装不了 PaddlePaddle再退到 RapidOCR。单字识别的核心代码如下from paddleocr import PaddleOCR # 关闭方向分类器文字点选的文字基本都是正立的开着反而多一层计算 ocr PaddleOCR(use_angle_clsFalse, langch, show_logFalse) char_results [] for (x, y, w, h) in char_boxes: char_img gray[y:y h, x:x w] norm_img normalize_char(char_img, size48) # 把 numpy 数组转成图片文件给 OCR避免临时文件读写 result ocr.ocr(norm_img, clsFalse) if result and result[0]: text result[0][0][1][0] score result[0][0][1][1] char_results.append({ text: text, score: score, box: (x, y, w, h), center: (int(x w / 2), int(y h / 2)) })参数说明use_angle_clsFalse关闭方向分类器这个参数很关键——文字点选验证码里的字没有 90 度旋转的情况开着方向分类器只会增加推理时间和误判概率。clsFalse是调用时再确认一次不启用分类。识别结果的结构是result[0][0][1]里面是一个(文本, 置信度)的元组这个层级结构在 PaddleOCR 不同版本里有差异如果你的版本取不到值打印一下result的结构再调整索引。单字 OCR 的速度大约每张 20 到 50 毫秒整张验证码图切出十几个字也就小一秒性能完全够用。如果你用的是 RapidOCR接口调用方式略有不同它返回的是(文本框, 文本, 置信度)三元组但整体思路一致。做课设时建议把识别置信度也存下来如果某张图置信度普遍偏低说明预处理质量有问题而不是 OCR 引擎不行。4.2 相似字匹配策略编辑距离与形近字词典识别出所有文字后下一步是把提示文字和目标图里的字进行匹配。提示文字字符串一般是“请依次点击图中的‘礼’‘物’‘盒’”首先需要把这个字符串解析成目标字列表。这一步用简单的正则提取即可import re prompt 请依次点击图中的“礼”“物”“盒” target_chars re.findall(r“(.*?)”, prompt) print(target_chars) # [礼, 物, 盒]解析出来后对每个目标字遍历 OCR 识别出的候选字计算相似度。相似度算法我用两种一是编辑距离Levenshtein Distance适合处理字长不同的情况二是difflib的SequenceMatcher适合处理字符级别匹配。但光靠算法不够中文里形近字太多“未”和“末”、“日”和“曰”编辑距离全是 1但语义完全不同。所以我建议加一个形近字词典做二次修正import difflib # 常见的形近字映射表按需扩充 similar_chars { 未: [末, 朱], 日: [曰, 目], 己: [已, 巳], 人: [入, 八], } def match_target(target, candidates): best_text, best_score None, 0.0 for cand in candidates: score difflib.SequenceMatcher(None, target, cand[text]).ratio() # 如果目标字在形近字表里命中表的优先 if target in similar_chars and cand[text] in similar_chars[target]: score 0.3 if score best_score: best_score score best_text cand return best_text, best_score逻辑说明SequenceMatcher对单字的相似度计算比较保守两个不同的字 ratio 通常只有 0 到 0.4 之间相同字能到 1.0。加similar_chars加权是因为“未”和“末”编辑距离为 1ratio 值可能到 0.7 以上只靠算法阈值很难区分人工维护一个形近字表在课设规模下完全可行。实际使用时还有一个更稳的兜底策略如果匹配到的分数低于 0.5直接丢弃该候选宁可不点也不乱点至少不会因为点错导致整题失败。4.3 坐标映射三要素原始像素、CSS渲染尺寸、设备像素比这一步是整个方案里最容易翻车的环节也是我和团队在实际项目中花时间最多的部分。先说现象本地识别出的框在图片上画出来完全正确但用脚本模拟点击时位置总是偏。问题出在你拿到的坐标是“验证码原始图片的像素坐标”而网页上显示的验证码图片可能经过了 CSS 缩放。举个例子验证码原图是 300×200 像素但网页通过 CSS 把它显示成 150×100 的尺寸宽高直接缩了一半。如果你的自动化脚本拿原图坐标去点击点到的位置全部偏了一倍。聪明的做法是把坐标换算成“比例”而不是直接用像素值# 原图尺寸 img_w, img_h img.shape[1], img.shape[0] # 网页上验证码实际渲染尺寸从浏览器开发者工具或 DOM 获取 css_w, css_h 150, 100 # 设备像素比一般来自 window.devicePixelRatioRetina 屏常见 2.0 dpr 2.0 # 实际渲染像素 CSS 尺寸 × 设备像素比 render_w, render_h css_w * dpr, css_h * dpr # 中心点归一化到 [0, 1] 再映射到渲染坐标 click_x int((center_x / img_w) * render_w) click_y int((center_y / img_h) * render_h)参数说明关键在最后两步先把原图坐标除以原图宽高得到相对比例再乘实际渲染宽高。这样不管网页怎么缩放比例关系不变。设备像素比是很多人忽略的变量——如果你的电脑是 Retina 屏浏览器 CSS 里写 150px 宽的元素实际上用了 300 个物理像素来渲染鼠标点击的坐标系统是物理像素所以必须乘上dpr。如果验证码图片在页面里还有外边距或偏移不要只算图片本身坐标还要加上图片父容器的偏移量。更进一步用 Selenium 执行点击时可以直接读取元素的位置from selenium import webdriver from selenium.webdriver.common.by import By element driver.find_element(By.ID, captcha_img) # 返回元素在页面中的实际位置和大小 rect element.get_rect() css_x, css_y rect[x], rect[y] css_w, css_h rect[width], rect[height]get_rect()拿到的就是元素的 CSS 坐标和尺寸省去了手动拼偏移量的过程。我的习惯是拿到rect之后打印出来和原图尺寸做对比确认缩放比例是否和自己算的一致这一步排查能省下后面一半的调试时间。4.4 把“识别、匹配、点击”串成一次完整流程把前面所有部分串起来一个完整流程大概是这样的import time import pyautogui # ... 前面识别的 char_results 和 target_chars ... def run_captcha(driver, captcha_element): # 1. 截图验证码区域得到原图 captcha_img capture_element(driver, captcha_element) # 2. 预处理 切字 OCR调用前面的函数 char_results recognize_chars(captcha_img) # 返回带 text 和 center 的列表 # 3. 按提示文字匹配目标位置 click_points [] for target in target_chars: matched, score match_target(target, char_results) if matched and score 0.5: click_points.append(matched[center]) # 4. 坐标换算后依次点击 for point in click_points: click_x, click_y map_coords(point, captcha_img, captcha_element) pyautogui.moveTo(click_x, click_y, duration0.15) pyautogui.click() time.sleep(0.3) # 间隔太短容易触发服务端检测逻辑说明click_points的顺序就是target_chars的顺序这一步严格对应提示语“请依次点击”的语义。每点完一个字符等待 0.3 秒是为了避免点击间隔过短被服务端判定为机器操作。另外要注意pyautogui的坐标是以整个屏幕为参考系的如果浏览器窗口没置顶点击会落到其他应用上配合 Selenium 时用ActionChains直接发事件给元素会更稳。具体我建议from selenium.webdriver.common.action_chains import ActionChains for point in click_points: # 计算元素内的相对偏移 actions ActionChains(driver) actions.move_to_element_with_offset(captcha_element, offset_x, offset_y) actions.click().perform() time.sleep(0.3)move_to_element_with_offset的偏移量是相对元素左上角的所以前面计算坐标时要先减去元素左上角位置。这个方案不依赖屏幕分辨率也不怕窗口被遮挡是一个更稳妥的自动化点选方式。5. 避坑与排查文字点选识别课设最容易翻车的五个点5.1 识别出“已”不是“己”字体差异比模型能力更致命现象目标字是“己”OCR 返回的是“已”编辑距离只有 1程序果断匹配了“已”点击位置错了。原因单字切割后图像分辨率太低加上目标验证码使用的字体和 OCR 训练数据里的字体差异大“己”和“已”在低分辨率下笔画几乎一样。这不是 OCR 引擎菜而是输入图像本身信息量就不够。解决先放大再识别把单字从 48 像素提到 64 像素同时把 PaddleOCR 的识别阈值调低一点让它返回多个候选更实用的是维护一个形近字黑白名单遇到“己/已/巳”这类字单独走规则不要全靠 OCR。5.2 坐标偏了半个字宽图片缩放的换算漏了哪一步现象在本地把识别框画在原图上位置完全正确用同一个坐标去网页上点击每次点偏而且偏移量随图片缩放比例变化。原因本地画框用的是原图像素坐标网页点击用的是渲染坐标两者之间差了一个 CSS 缩放系数再加上设备像素比后误差更大。解决严格按 4.3 的比例映射法先把原图坐标归一化到 [0,1]再乘渲染尺寸和设备像素比不要直接拿原图坐标减偏移量来用。验证方法是把计算出的坐标画在一个与原图同宽的空白图上再用浏览器截图对比一眼就能看出对不对。5.3 两个粘连字被框成一个框二值化与形态学参数的配合现象切字结果里有两个字的轮廓合并在了一起比如“月”和“半”笔画边缘相触被当成一个整体框OCR 识别出“月半”或直接报错。原因二值化阈值偏低导致笔画周边的浅色噪点被归入前景或者形态学开运算核太大把两字的边缘“焊接”起来了。解决先调形态学核从(2, 2)到(3, 3)或(4, 4)同时把 Otsu 换成自适应阈值cv2.adaptiveThreshold对光照不均的图更友好。如果框的宽高比明显大于正常文字比如宽高比超过 2就按水平投影把这个框拆成两段再分别识别。5.4 单字全对、整题不过点击顺序与间隔被服务端校验现象本地把每个字都识别对了代码逻辑也没问题但页面始终提示“验证失败”。单看每次点击的坐标位置都对。原因文字点选验证码不只是看“点没点对”还看“点击顺序”和“点击间隔”。如果你的脚本以毫秒级速度连续点完所有字服务端会直接判定为机器操作。解决在两次点击之间加入随机间隔不要固定time.sleep(0.3)写成time.sleep(random.uniform(0.2, 0.6))更逼真一点可以模拟鼠标移动轨迹不要直接瞬移到目标点。另外确认代码里点击顺序确实按target_chars的顺序执行——我见过有人把字典里的键顺序当点击顺序结果目标字全对但顺序错了。5.5 离线识别率虚高你的测试集和真实截图差在哪现象自己从网上下载的干净验证码图测试时识别率 90% 以上一到真实页面截图就跌到 60%。原因真实页面的验证码经过压缩、带背景纹理、有遮挡线和你测试用的原图不是同一分布。你会发现用高分辨率缓存图测试和用浏览器截图测试结果可以差 30 个百分点。解决从第一步就按“浏览器截图 元素定位裁剪”的方式收集测试集不要直接下载原始图片文件。用 Selenium 截元素区域并保存归档保证训练、测试、运行三个环节的图片来源一致。我现在的习惯是测试集至少 20 张真实页面截图每张都人工标注好正确答案所有调参决策都以这个集为基准而不是临时拿一张图看单个结果。6. 让项目拿得出手验证指标、可视化与提分方向6.1 建一个20到30张的小评估集统计两个关键指标做课设最忌讳对着三张图反复调参调到自己都信了。我建议一开始就建一个 20 到 30 张图的评估集每张图人工标注好目标字和坐标用脚本统一评测。指标只看两个单字识别准确率和整题通过率。单字准确率是“识别出的文字中正确的比例”整题通过率是“全部目标字按顺序点对的比例”。对文字点选来说整题通过率才是真正有意义的指标单字错一个整题就失败。total_chars 0 correct_chars 0 pass_count 0 for sample in test_set: # 识别 匹配 点击坐标返回 predicted_chars pred_chars run_recognition(sample[image]) if pred_chars sample[target_chars]: pass_count 1 for p, t in zip(pred_chars, sample[target_chars]): total_chars 1 if p t: correct_chars 1 print(单字准确率:, correct_chars / total_chars) print(整题通过率:, pass_count / len(test_set))逻辑说明这里没有比较坐标而是比较预测的字符序列和目标字符序列因为坐标是否准确最终体现在“点完能否通过验证”而不是“框画得准不准”。统计指标前先确认pred_chars的长度和目标长度一致——长度不一致本身就是一种失败不匹配时直接记整题失败。6.2 把识别过程可视化中间图片是排错的第一手证据调试文字点选识别时最大的痛点是“黑匣子”——你只知道结果错了不知道错在预处理、切字、识别还是匹配。我的做法是把每一步中间结果都画出来存成图片一眼定位问题环节debug_img img.copy() for item in char_results: x, y, w, h item[box] cv2.rectangle(debug_img, (x, y), (x w, y h), (0, 0, 255), 2) cv2.putText(debug_img, item[text], (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1) cv2.imwrite(debug_predicted.png, debug_img)这段代码把每个识别框和识别出的文字都画在原图上。如果框的位置不对问题在预处理和切字环节如果框的位置对但文字错问题在 OCR 识别环节如果文字对但最终点位不对问题在坐标映射环节。可视化之后的排查效率比盯着终端日志高一个量级这也是我强烈建议课设里保留的部分——答辩时直接把对比图贴上去说服力远超“准确率 95%”这句话。 6.1 里的评估集也可以和这里的可视化配合批量跑完所有图后把结果拼成一张大图快速扫一眼就能发现规律。6.3 课设加分项与性能优化别只跑一个黑乎乎的控制台脚本最后说两个能让课设在答辩时明显拉开差距的方向。第一个加分方向是给代码加一层 Web 界面。用 Flask 起一个本地服务提供一个图片上传接口前端页面展示用户上传的验证码图后端返回识别出的文字和坐标再在原图上画框展示。这样一来你的项目就从一个“能跑的脚本”变成了“一个完整系统”评审老师对后者的印象分完全不同。界面不需要复杂一个文本框加一张图就够了。第二个方向是性能优化。前面提到 RapidOCR 太吃 CPU容易把 CPU 打满这个问题在实际运行时会很明显尤其是批量跑图时。我一般这么处理一是把 OCR 初始化和推理分开避免每次调用都重新加载模型二是输入到 OCR 的图先缩放到 48 像素再识别减小计算量三是如果只是做单字识别可以考虑换用更轻量的模型或者干脆只保留识别分支关掉检测分支。代码层面一个立竿见影的优化是全局复用 OCR 实例ocr_engine None def get_ocr(): global ocr_engine if ocr_engine is None: # 模型只加载一次后续调用复用 ocr_engine PaddleOCR(use_angle_clsFalse, langch, show_logFalse) return ocr_engine同样思路也适用于 OpenCV视频流或批量任务里重复读取图片、重复初始化算子都会造成不必要的开销。课设做到这里在纯代码层面已经超过大多数只把模型跑通就交差的作业了。回到开头那个问题文字点选验证码为什么能卡住一半人它不是识别不出字而是“切字、匹配、坐标换算、点击动作”每一步都有坑。我做过的最蠢的一次就是在坐标映射上卡了整整一个下午最后发现是 CSS 把图片缩放了 0.5 倍而我一直按原图宽高算坐标。从那以后我养成了一个习惯所有涉及坐标的代码第一件事不是写逻辑而是把原始尺寸、渲染尺寸、设备像素比三个值打印出来核对一遍。这个习惯帮我省下的调试时间远比写代码的时间多。希望这篇文章里提到的思路和避坑点能让你少走几段弯路。如果课设时间紧优先把第 3 章的预处理和第 4 章的坐标映射打磨好这两块通了整条链路基本就稳了祝你的项目顺利跑通。本文还有配套的精品资源点击获取