1. 桌面截图与文字识别的整体设计思路
1.1 这个项目到底在解决什么问题
很多人第一次接触桌面截图加文字识别这个需求,脑子里冒出来的方案往往是“截个图,然后手动打开某个识别工具,把图拖进去”。偶尔用一次还行,但如果每天要重复几十次,比如从各种报表、聊天窗口、监控面板里提取文字,手动操作的时间成本就非常可观了。这个项目的核心目标,就是把“截取桌面画面”和“识别图中文字”这两步串成一条自动化的流水线,让程序自己完成从抓屏到出文本的全过程。
标题里有个关键词叫“自适应分辨率”,这是整个项目里最容易被忽略、却最影响成败的一环。所谓自适应,指的是程序不能假设用户的屏幕永远是 1920×1080,也不能假设缩放比例永远是 100%。现实中,有人用 2K 屏、有人用 4K 屏、有人笔记本外接显示器,Windows 的显示缩放还可能是 125%、150% 甚至 175%。如果截图代码写死了坐标和尺寸,换一台机器就全乱套。所以这个项目要做的,是让程序在运行时主动去问系统“你的屏幕到底多大、缩放是多少”,然后据此动态计算截图区域和后续识别所需的参数。
它适合谁来参考?我觉得有三类人比较对口。第一类是做自动化办公的开发者,需要从桌面软件界面里批量提取文字数据;第二类是做测试或运维的朋友,想定时抓取某个监控窗口的内容做记录;第三类就是单纯想练手的 Python 学习者,这个项目麻雀虽小,但把屏幕 API、图像处理、OCR 识别、坐标换算几个知识点都串起来了,是个不错的综合练习。
1.2 为什么选 Python 而不是别的语言
做这类桌面自动化,可选的语言其实不少,C# 调 Windows API 很直接,AutoHotkey 写起来也快,但 Python 的优势在于生态。截图有mss、Pillow的ImageGrab,图像预处理有OpenCV,文字识别有pytesseract、PaddleOCR、EasyOCR一大堆现成库,而且这些库的文档和社区案例都足够多,遇到坑基本能搜到答案。
更重要的是,Python 写出来的脚本改起来快。今天想加个“只截取屏幕右下角区域”,明天想加个“识别完自动存到表格”,用 Python 都是几行代码的事。如果换成编译型语言,光是改一个参数重新编译就够烦的。所以对于这种“需求会不断微调”的小工具,Python 是性价比最高的选择。
不过这里要提前说一个选型上的分水岭:截图库和 OCR 库的搭配方式,会直接决定你的程序在不同分辨率下稳不稳。我后面会详细拆开讲。
1.3 整体流程的四个阶段
把整个项目拆开,其实就四个阶段,我用一个生活化的类比来说明:这就像你去复印店复印一本书的某一页。第一步,你得先看清楚这本书摊开有多大(获取屏幕尺寸和缩放);第二步,把要复印的那一页框出来(确定截图区域);第三步,按下复印键得到一张纸(执行截图并保存为图像);第四步,把纸上的字录入电脑(OCR 识别)。
对应到代码里就是:
- 阶段一:探测屏幕物理分辨率与系统缩放比例
- 阶段二:根据目标区域计算实际像素坐标
- 阶段三:调用截图库抓取指定区域并做预处理
- 阶段四:把处理后的图像交给 OCR 引擎,拿到文字结果
这四个阶段里,第一阶段和第二阶段是“自适应”的关键,也是最容易出 bug 的地方。第三阶段的预处理决定了识别准确率的上限,第四阶段则是选型和调参的功夫。下面我逐个展开。
2. 自适应分辨率的核心细节与实操要点
2.1 物理分辨率、逻辑分辨率与缩放比例的关系
很多人搞不清一个概念:为什么我的屏幕明明是 2560×1440,但程序读出来的却是 1707×960?这就是缩放比例在作怪。操作系统为了让你在高分屏上看得清字,会把界面元素放大,比如设置 150% 缩放,那么逻辑分辨率就是物理分辨率除以 1.5。
用公式表达就是:
逻辑分辨率 = 物理分辨率 / 缩放比例
拿 2560×1440、150% 缩放举例:2560 / 1.5 ≈ 1707,1440 / 1.5 = 960。所以程序如果按逻辑分辨率去截图,得到的图会比实际屏幕小一圈,截出来的内容自然就错位了。
这里有个关键点:不同的截图库,读到的分辨率口径不一样。Pillow的ImageGrab.grab()在 Windows 上默认抓的是逻辑分辨率下的画面,而mss抓的是物理像素。这个差异如果不搞清楚,你会遇到“截图区域明明算对了,但截出来是模糊的或者缺一块”的诡异现象。
我在实际项目里踩过的坑是:一开始用ImageGrab配合写死的坐标,在自己电脑上跑得好好的,换到同事的 4K 笔记本上,截出来的图只有左上角四分之一。后来才明白,同事的缩放是 200%,逻辑分辨率只有物理的一半,坐标全对不上。
2.2 用代码探测屏幕真实参数
要拿到屏幕的物理分辨率和缩放比例,Windows 下最靠谱的方式是调用系统 API。Python 里可以通过ctypes直接调user32和shcore里的函数。下面这段是我常用的探测代码:
import ctypes def get_screen_info(): user32 = ctypes.windll.user32 # 设置进程为 DPI 感知,避免系统自动缩放 try: ctypes.windll.shcore.SetProcessDpiAwareness(2) except Exception: user32.SetProcessDPIAware() # 物理分辨率 physical_width = user32.GetSystemMetrics(0) physical_height = user32.GetSystemMetrics(1) # 获取缩放比例 try: hdc = user32.GetDC(0) dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88) # LOGPIXELSX user32.ReleaseDC(0, hdc) scale = dpi / 96.0 except Exception: scale = 1.0 return physical_width, physical_height, scale这段代码里有两个细节值得说。第一,SetProcessDpiAwareness(2)这行非常关键,它告诉系统“我这个程序自己会处理缩放,你别帮我缩放”。如果不加这行,系统会把你的程序窗口和截图结果都按缩放比例处理一遍,导致坐标计算全乱。参数 2 代表PROCESS_PER_MONITOR_DPI_AWARE,意思是每个显示器独立处理 DPI,多屏环境下更准确。
第二,GetDeviceCaps的第二个参数 88 是LOGPIXELSX的常量值,返回的是每英寸逻辑像素数,标准值是 96。用返回值除以 96 就得到缩放比例,比如返回 144 就是 150% 缩放,返回 192 就是 200%。
注意:
SetProcessDpiAwareness必须在程序启动早期调用,最好在导入其他图形库之前。如果先导入了Pillow或tkinter,有些库会自己设置 DPI 感知,导致你的设置失效。
2.3 多显示器场景下的坐标陷阱
单屏还好说,一旦涉及多显示器,坐标系统就变得复杂了。Windows 里每个显示器都有自己的坐标系,主显示器的左上角是 (0,0),副显示器可能在主显示器的左边,那它的坐标就是负的。
mss这个库对多屏支持比较好,它提供了一个monitors列表,索引 0 是所有显示器拼起来的虚拟大屏,索引 1 开始是各个独立显示器。你可以这样枚举:
import mss with mss.mss() as sct: for i, monitor in enumerate(sct.monitors): print(f"显示器 {i}: 位置={monitor['left']},{monitor['top']} 尺寸={monitor['width']}x{monitor['height']}")输出里索引 0 的那一项,宽高就是所有屏幕拼起来的总尺寸。如果你要截取的内容可能出现在任意屏幕上,用索引 0 的虚拟大屏坐标来定位是最省事的。但要注意,虚拟大屏的坐标原点不一定在左上角,如果副屏在主屏左边,原点可能是负坐标。
我个人的经验是:如果项目只针对单屏,直接用主显示器参数就行;如果确实要多屏,建议在配置里让用户指定“目标在第几号显示器”,然后程序用该显示器的 left/top 做偏移量,这样比用虚拟大屏更可控。
2.4 缩放比例对截图坐标的换算
假设你已经拿到了缩放比例 scale,现在用户告诉你“我要截取屏幕上从 (100, 200) 开始、宽 800 高 600 的区域”,这个坐标是用户按逻辑坐标给的(因为用户看到的是缩放后的界面)。那么实际截图时,需要把逻辑坐标乘以 scale 换算成物理像素:
def logical_to_physical(x, y, w, h, scale): return int(x * scale), int(y * scale), int(w * scale), int(h * scale)反过来,如果 OCR 识别出来的文字带有坐标信息(比如某些 OCR 库会返回文字框位置),你想把它映射回逻辑坐标做界面标注,就要除以 scale。
这个换算看起来简单,但实际项目里最容易出错的地方是“混用”。比如截图用物理坐标,但鼠标点击用逻辑坐标,结果就是点偏了。我的建议是在程序里统一用一套坐标体系,要么全物理、要么全逻辑,在边界处做一次转换,中间不要来回换。
3. 截图与文字识别的完整实操流程
3.1 环境准备与依赖安装
先把环境搭起来。我用的组合是mss负责截图、Pillow负责图像处理、pytesseract负责 OCR。为什么选mss而不是Pillow的ImageGrab?因为mss抓的是物理像素,速度快,而且对多屏和 DPI 的处理更透明,配合前面说的 DPI 感知设置,坐标计算最不容易出错。
安装命令如下:
pip install mss pillow pytesseract opencv-python numpy这里要特别提醒:pytesseract只是一个 Python 封装,它本身不含 OCR 引擎,你还需要单独安装 Tesseract 引擎。Windows 下可以去它的官方发布页下载安装包,安装时记得勾选中文语言包(如果你要识别中文)。安装完后,要么把安装目录加到系统 PATH,要么在代码里用pytesseract.pytesseract.tesseract_cmd指定路径:
import pytesseract pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"如果你要识别中文,还需要确认语言包文件chi_sim.traineddata存在于 Tesseract 的tessdata目录下。没有的话,识别中文会直接报错或者返回空。
3.2 截图区域的动态计算
现在把前面的知识串起来,写一个能自适应的截图函数。核心思路是:先探测屏幕参数,再根据用户给的逻辑坐标换算成物理坐标,最后用mss抓取。
import mss import numpy as np from PIL import Image def capture_region(logical_x, logical_y, logical_w, logical_h): physical_w, physical_h, scale = get_screen_info() # 逻辑坐标转物理坐标 px = int(logical_x * scale) py = int(logical_y * scale) pw = int(logical_w * scale) ph = int(logical_h * scale) # 边界保护,防止超出屏幕 px = max(0, min(px, physical_w - 1)) py = max(0, min(py, physical_h - 1)) pw = min(pw, physical_w - px) ph = min(ph, physical_h - py) monitor = {"left": px, "top": py, "width": pw, "height": ph} with mss.mss() as sct: img = np.array(sct.grab(monitor)) # mss 返回的是 BGRA,转成 RGB img_rgb = img[:, :, :3][:, :, ::-1] return Image.fromarray(img_rgb)这段代码里有几个我特意加的细节。第一是边界保护,用户给的坐标可能超出屏幕范围,如果不做 clamp,mss会抛异常或者截出黑边。第二是颜色通道转换,mss抓出来的 numpy 数组是 BGRA 四通道,直接交给 OCR 会因为颜色通道顺序不对导致识别率下降,所以要转成 RGB。
提示:如果你要截取的是整个屏幕,直接把 monitor 设成
sct.monitors[1]就行,不用自己算坐标。但如果是局部区域,一定要走上面的换算流程。
3.3 图像预处理提升识别率
截图拿到手,别急着丢给 OCR。原始截图的识别率往往不理想,尤其是深色背景上的浅色文字、或者有抗锯齿的字体。做几步预处理,识别率能提升一大截。
我常用的预处理流程是这样的:
import cv2 def preprocess_for_ocr(pil_img): img = np.array(pil_img) # 转灰度 gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 放大图像,小字体识别率低,放大 2 倍 gray = cv2.resize(gray, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC) # 自适应二值化,应对不均匀光照 binary = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10 ) return Image.fromarray(binary)这里每一步都有讲究。转灰度是因为 OCR 引擎主要看形状和对比度,彩色信息反而是干扰。放大 2 倍是因为 Tesseract 对小于 20 像素高的文字识别率会明显下降,放大后相当于给它更多细节。自适应二值化比全局阈值好,因为截图里可能有渐变背景或者局部阴影,全局阈值会把某些区域整片变黑或变白。
不过二值化不是万能的。如果原图文字本身就是深色背景浅色字,二值化后可能变成白字黑底,Tesseract 对反色文字的识别率会下降。这时候可以加一步判断:如果二值化后白色像素占比超过 70%,说明可能是反色,再取一次反。
if np.mean(binary) > 178: # 白色偏多 binary = cv2.bitwise_not(binary)这个 178 是我实测下来比较稳的阈值,你也可以用binary.mean()动态判断。
3.4 调用 OCR 并解析结果
预处理完就可以识别了。pytesseract的基本调用很简单:
def ocr_image(pil_img, lang="chi_sim+eng"): config = "--oem 3 --psm 6" text = pytesseract.image_to_string(pil_img, lang=lang, config=config) return text.strip()这里的config参数值得展开说。--oem 3表示用默认的 OCR 引擎模式,兼容性最好。--psm 6表示“假设是一整块统一的文本”,这个模式对截图这种规整排版的文字最友好。如果你的截图是零散的文字(比如按钮上的字),可以试--psm 11(稀疏文本模式)。如果是一行字,用--psm 7。
语言参数chi_sim+eng表示同时加载简体中文和英文,中间用加号连接。如果你只识别英文,用eng就行,速度会快不少,因为不用加载中文模型。
识别结果是一个字符串,行与行之间用换行分隔。如果你需要每个词的位置信息,可以用image_to_data:
data = pytesseract.image_to_data(pil_img, lang=lang, output_type=pytesseract.Output.DICT) for i, word in enumerate(data["text"]): if word.strip(): print(f"文字: {word} 位置: ({data['left'][i]}, {data['top'][i]}) 置信度: {data['conf'][i]}")这个在需要定位文字位置时很有用,比如你想知道某个关键词出现在截图的哪个位置,就可以遍历这个结果去匹配。
3.5 把整个流程封装成可复用的类
零散的函数不利于维护,我习惯把它封装成一个类,把屏幕参数在初始化时探测一次,后续截图直接复用,避免每次都调系统 API。
class AdaptiveScreenOCR: def __init__(self, tesseract_path=None, lang="chi_sim+eng"): if tesseract_path: pytesseract.pytesseract.tesseract_cmd = tesseract_path self.lang = lang self.physical_w, self.physical_h, self.scale = get_screen_info() def capture(self, x, y, w, h): px, py, pw, ph = self._to_physical(x, y, w, h) monitor = {"left": px, "top": py, "width": pw, "height": ph} with mss.mss() as sct: img = np.array(sct.grab(monitor)) return Image.fromarray(img[:, :, :3][:, :, ::-1]) def _to_physical(self, x, y, w, h): px = max(0, min(int(x * self.scale), self.physical_w - 1)) py = max(0, min(int(y * self.scale), self.physical_h - 1)) pw = min(int(w * self.scale), self.physical_w - px) ph = min(int(h * self.scale), self.physical_h - py) return px, py, pw, ph def recognize(self, x, y, w, h, preprocess=True): img = self.capture(x, y, w, h) if preprocess: img = preprocess_for_ocr(img) return ocr_image(img, self.lang)用起来就很清爽:
ocr = AdaptiveScreenOCR(lang="chi_sim+eng") result = ocr.recognize(100, 200, 800, 600) print(result)这个类的好处是屏幕参数只探测一次,而且坐标换算逻辑集中在一处,改起来方便。如果以后要支持多屏,只需要在_to_physical里加上显示器偏移量就行。
4. 常见问题与排查技巧实录
4.1 识别结果乱码或为空
这是最常见的问题,原因通常有三类。第一类是语言包没装对,比如你要识别中文但只装了英文包,Tesseract 会把中文当成乱码。排查方法是命令行执行tesseract --list-langs,看看输出里有没有chi_sim。没有的话就是语言包缺失。
第二类是图像质量太差。截图区域太小、文字太模糊、对比度太低,都会导致识别失败。这时候先别急着调 OCR 参数,把预处理后的图像保存下来看一眼:
img.save("debug_preprocessed.png")我遇到过好几次是二值化参数不合适,把文字给“化”没了。看到图就一目了然。
第三类是--psm模式选错了。整块文本用了稀疏模式,或者单行文字用了整块模式,都会影响结果。这个只能多试几个模式对比。
4.2 截图区域偏移或截到黑边
偏移问题九成是缩放比例没处理对。排查步骤是:先打印出get_screen_info()返回的 scale,确认它和你系统设置里的缩放比例一致。如果系统是 150% 但代码返回 1.0,说明 DPI 感知没设置成功,检查SetProcessDpiAwareness是不是被其他库覆盖了。
截到黑边通常是坐标超出屏幕范围。前面代码里的边界保护能解决大部分情况,但如果你的目标区域本身就在屏幕边缘,可能需要调整截图区域,或者接受边缘有少量黑边后用裁剪去掉。
还有一种情况是副屏坐标为负。如果你在多屏环境下用虚拟大屏坐标,而副屏在主屏左边,那么副屏的 left 是负数。mss能处理负坐标,但你自己算边界保护时要注意别把负数 clamp 成 0,否则会截错区域。
4.3 识别速度慢
Tesseract 识别一张 800×600 的图,英文大概 0.3 到 0.5 秒,中英混合可能 1 秒以上。如果你要频繁截图识别,这个速度可能不够。优化方向有几个:
- 缩小截图区域,只截真正需要的部分,图像越小识别越快
- 如果只识别英文,把 lang 改成
eng,速度能快一倍 - 降低预处理里的放大倍数,从 2 倍改成 1.5 倍
- 如果对准确率要求不高,可以用
--psm 7单行模式,比整块模式快
如果速度还是不够,就得考虑换 OCR 引擎了。PaddleOCR 在中文场景下速度和准确率都比 Tesseract 好,但依赖更重,安装包大。EasyOCR 也不错,支持 GPU 加速。这个就看项目需求权衡了。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 识别结果为空 | 语言包缺失 | tesseract --list-langs | 安装对应语言包 |
| 识别结果乱码 | 语言参数错误 | 检查 lang 参数 | 改成正确的语言代码 |
| 截图区域偏移 | 缩放比例未处理 | 打印 scale 值 | 设置 DPI 感知并换算坐标 |
| 截图有黑边 | 坐标超范围 | 打印实际截图坐标 | 加边界保护 |
| 识别速度慢 | 图像过大或语言过多 | 计时各阶段耗时 | 缩小区域、精简语言 |
| 中文识别率低 | 未放大或未二值化 | 保存预处理图查看 | 放大 2 倍并自适应二值化 |
| 反色文字识别差 | 二值化后反色 | 检查白色像素占比 | 超过阈值时取反 |
4.5 几个我踩过的坑和独家技巧
第一个坑是mss的实例不能跨线程复用。我一开始想用多线程加速批量截图,结果发现多个线程共用一个mss.mss()实例会报错。后来改成每个线程自己创建实例,问题解决。所以如果你要做并发截图,记得每个线程独立创建mss对象。
第二个坑是 Tesseract 的临时文件。pytesseract在识别时会在系统临时目录生成中间文件,如果程序异常退出,这些文件可能残留。长时间运行的程序建议定期清理临时目录,或者在调用时指定--tessdata-dir到一个可控的目录。
第三个技巧是关于识别区域的微调。有时候截图区域稍微大一点或小一点,识别率差别很大。我的做法是先用一个宽松的区域截图,识别一次,然后根据识别出的文字位置反推精确区域,再截一次。虽然多了一次识别,但准确率提升明显。这个技巧在处理表格类界面时特别有用。
第四个技巧是缓存屏幕参数。get_screen_info()里的系统调用虽然不慢,但如果你每秒截图好几次,累积起来也是开销。我的做法是在类初始化时探测一次,如果用户中途改了缩放比例,提供一个手动刷新参数的方法。毕竟缩放比例不会频繁变,没必要每次都查。
5. 性能优化与扩展方向
5.1 减少不必要的全屏截图
很多人图省事,每次都截全屏再裁剪。全屏截图在 4K 屏上就是 800 多万像素,光是内存拷贝就要几十毫秒,再加上后续处理,整个流程会明显变慢。正确的做法是只截取目标区域,mss的grab方法支持直接指定区域,效率比全屏截图高得多。
如果你确实需要先看全屏再决定截哪里,可以截一次全屏,然后在内存里裁剪出多个区域分别识别,避免重复截图。这个在需要识别多个分散区域时特别有效。
5.2 用 OpenCV 做更精细的定位
前面说的都是“用户指定区域”,但有些场景下区域是动态的,比如你要识别一个会移动的弹窗。这时候可以用 OpenCV 做模板匹配或者轮廓检测,自动找到目标区域。
模板匹配的思路是:先截一张包含目标的图作为模板,然后在后续截图中用cv2.matchTemplate找到模板位置,再截取该位置附近的区域做识别。这个方法对固定样式的界面元素很有效,但对会变形的元素就不行了。
轮廓检测适合文字区域定位,思路是先二值化,再用cv2.findContours找到文字块的边界框,然后合并相邻的框得到文本区域。这个方法不需要模板,适应性更强,但参数调起来比较麻烦。
5.3 批量识别与结果结构化
如果截图里是表格数据,识别出来的文本是一堆散乱的行,还需要做结构化解析。我的做法是先用image_to_data拿到每个词的坐标,然后按 y 坐标聚类成行,按 x 坐标排序成列,最后拼成二维表格。这个逻辑写起来有点绕,但一旦写好,处理报表类截图就非常顺手。
def parse_table(pil_img, lang="chi_sim+eng"): data = pytesseract.image_to_data(pil_img, lang=lang, output_type=pytesseract.Output.DICT) items = [] for i, word in enumerate(data["text"]): if word.strip() and int(data["conf"][i]) > 30: items.append({ "text": word, "x": data["left"][i], "y": data["top"][i], "h": data["height"][i] }) # 按 y 坐标分行 items.sort(key=lambda d: d["y"]) rows = [] current_row = [] last_y = None for item in items: if last_y is None or abs(item["y"] - last_y) < item["h"] * 0.6: current_row.append(item) else: rows.append(sorted(current_row, key=lambda d: d["x"])) current_row = [item] last_y = item["y"] if current_row: rows.append(sorted(current_row, key=lambda d: d["x"])) return [[cell["text"] for cell in row] for row in rows]这段代码的核心是“按 y 坐标聚类成行”,阈值取行高的 0.6 倍是我实测下来比较稳的值。太小会把同一行拆开,太大会把相邻行合并。
5.4 定时任务与结果输出
如果这个工具是长期运行的,可以配合schedule库做定时截图识别:
import schedule import time def job(): result = ocr.recognize(100, 200, 800, 600) with open("output.txt", "a", encoding="utf-8") as f: f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')}\n{result}\n\n") schedule.every(10).minutes.do(job) while True: schedule.run_pending() time.sleep(1)输出格式可以根据需求改成 CSV、JSON 或者直接写数据库。如果要做成服务,可以用Flask包一层 HTTP 接口,让其他程序调用。
5.5 换用 PaddleOCR 的时机
Tesseract 在英文和规整印刷体上表现不错,但中文场景下,尤其是字体多样、背景复杂时,识别率会明显下降。如果你发现调参调到极限还是达不到要求,就该考虑换 PaddleOCR 了。
PaddleOCR 的调用方式和 Tesseract 不同,它返回的是带坐标的文本列表:
from paddleocr import PaddleOCR ocr_engine = PaddleOCR(use_angle_cls=True, lang="ch") result = ocr_engine.ocr(np.array(pil_img), cls=True) for line in result[0]: text = line[1][0] confidence = line[1][1] box = line[0] print(text, confidence, box)它的优势是中文识别率明显更高,而且自带方向分类,对倾斜文字也能处理。代价是安装包大、首次加载模型慢。我的建议是:如果项目以英文为主,Tesseract 够用;如果中文为主且对准确率要求高,直接上 PaddleOCR,别在 Tesseract 上死磕。
6. 一些实战中的经验体会
这个项目我从最初写死坐标的版本,到后来能自适应各种分辨率,中间改了好几版。最大的体会是:屏幕参数探测这一步绝对不能省。我见过太多人为了图快,直接把坐标写死,结果代码只能在作者自己的机器上跑。多花二十行代码做自适应,能省掉后面无数次的“在我电脑上明明可以”。
另一个体会是关于预处理的。一开始我觉得 OCR 引擎应该足够智能,不需要预处理。但实测下来,同样的图,预处理前后识别率能差 30% 以上。尤其是放大这一步,对小于 20 像素的文字效果立竿见影。所以别偷懒,该做的预处理一步都别少。
还有一点是关于错误处理。截图和 OCR 都是容易出异常的环节,比如截图时目标窗口被最小化了,或者 OCR 引擎崩溃了。如果不做异常捕获,程序跑着跑着就挂了。我的做法是在关键步骤外面包一层 try-except,出错时记录日志并返回空结果,让程序能继续跑下去。对于长期运行的定时任务,这个尤其重要。
最后分享一个小技巧:如果你不确定某个区域的识别效果,可以写一个调试模式,把截图、预处理后的图、识别结果三样东西一起输出到一个文件夹里。这样每次识别不理想时,你能快速定位是截图问题、预处理问题还是 OCR 问题。这个调试模式在我调参阶段帮了大忙,强烈建议你也加上。