macOS原生OCR:用Vision框架快速实现屏幕文字识别提取
在 macOS 上做 OCR,最容易想到的方案有两种:把图片上传到云端识别接口,或者在本地安装 Tesseract。而这个发布在 Hacker News Show HN 板块的项目给出了第三种做法:完全依赖 macOS 自带的原生 OCR 能力,把屏幕上看到的一段文字原样复现成可编辑文本,也就是项目名 Monkey see, monkey do 表达的意思——看到什么,就把什么复刻出来。这个思路的吸引力在于:不依赖网络,不把截图送出设备,不需要额外安装识别引擎,而且可以直接通过系统能力抓取任意窗口内容。
实际开发中,这种“看到但不能复制”的场景非常多,比如只读文档、视频字幕、老旧软件面板、禁止复制网页。与其手打,不如用一个几十行的 Swift 脚本完成从屏幕抓图到文字输出的完整链路。下面从 Vision 框架的核心概念讲起,逐步实现一个可运行的 macOS 屏幕 OCR 工具,并覆盖权限、精度、性能和扩展方向。跑通后,你会得到两个命令行工具:一个输入图片路径输出文本,另一个输入窗口编号输出该窗口内的文本。
1. 为什么说 macOS 原生 OCR 是常被忽略的现成能力
1.1 这个项目要解决的场景:可见但不可选
Monkey see, monkey do 这类工具的核心使用场景很明确:屏幕上明明显示着一行文字,但系统不让你选中、复制,或者根本没有对应的文本层。最常见的场景有几类:
- 只读 PDF 和扫描文档,文字以图片形式存在,无法直接拷贝。
- 视频里的字幕、会议录屏上的提示信息,想记录却只能暂停截图。
- 老旧企业软件界面,文案写死在资源里,既不能复制也没有文本导出。
- 网页开发者故意禁用文本选择,或者内容本身是图片验证码、图表。
这些场景如果手动重打一遍,效率低还容易出错。OCR 工具的作用就是让“眼睛看到的”直接变成“剪贴板里的”。项目名里的 monkey see,指截获屏幕内容;monkey do,指把识别出的文本交还给用户使用。所以这不是一个复杂的识别算法项目,而是一个典型的系统能力集成项目。
1.2 原生 OCR 的技术基础:Vision 框架
macOS 从 10.15 开始,在系统自带的 Vision 框架中加入了基于深度学习的文本识别能力,核心 API 是VNRecognizeTextRequest。OCR 的全称是 Optical Character Recognition,即光学字符识别,目标是从图像中提取可编辑的字符序列。Vision 的实现运行在设备本地,识别过程不需要联网,识别结果按文字行返回,每一行包含对应的文本框坐标和候选字符串。
到 macOS 13 之后,Vision 的文本识别准确率进一步提升,新的识别 revision 对中文、英文混排、表格数字都有更好的处理。从开发者角度看,macOS 的“实况文本”功能本质上也是基于这一套识别能力构建的。也就是说,系统已经内置了高质量 OCR 引擎,开发者的工作不是重新发明识别算法,而是把系统能力接入自己的业务场景。
这里有一个容易误解的地方:原生 OCR 不等于“随便拿一张模糊图片就能完美识别”。它擅长的是常见界面文本、截图、文档扫描件;对于复杂背景、艺术字体、低分辨率图片,仍然需要配合裁剪、放大和参数调整。
1.3 它和 Tesseract、云 OCR 的差异
选择 OCR 方案时,不能只看准确率一个维度。下面这张表可以帮助判断什么时候该用原生 Vision:
| 维度 | Vision 原生 OCR | Tesseract | 云 OCR |
|---|---|---|---|
| 网络依赖 | 无 | 无 | 需要联网 |
| 图片是否离开设备 | 不会 | 不会 | 会上传 |
| 中文/多语言支持 | 系统内置,按系统版本支持 | 需要手动下载语言包 | 一般支持较全 |
| 安装成本 | 系统自带,无需安装 | 需要 brew 或源码编译 | 需要 SDK 和密钥 |
| 调用成本 | 无按量费用 | 无 | 通常按次计费 |
| 集成方式 | Swift/ObjC API | C/C++/Python/REST | HTTP API |
| 适合场景 | macOS 工具、快捷脚本、隐私敏感场景 | 跨平台离线批处理 | 高精度多语种、复杂图像 |
选择原生 OCR 的最大理由不是“准确率一定最高”,而是集成成本最低、隐私边界最清晰。云 OCR 在复杂图像上准确率通常更高,但每次调用都要把截图传出去;对于这种抓取本地屏幕文字的小工具,隐私问题往往比精度更致命。Tesseract 的优点是跨平台,但中文识别需要额外语言包,且对 UI 字号较小的截图效果通常不如系统方案。
2. 理解 Vision 的识别链路,才知道参数该调哪里
2.1 一次识别请求的处理流程
Vision 的文本识别链路可以拆成四步:
- 准备图像。把图片文件转成
CGImage,或者直接使用屏幕抓取的CGImage。 - 创建请求。创建
VNRecognizeTextRequest实例,设置识别精度、语言等参数。 - 交给处理器。用
VNImageRequestHandler(cgImage:options:)承载图片,调用perform([request])。 - 取回结果。从
request.results拿到VNRecognizedTextObservation数组,每个观察对象对应一行文字。
用代码表达就是下面几行:
let request = VNRecognizeTextRequest() let handler = VNImageRequestHandler(cgImage: image, options: [:]) try handler.perform([request]) for case let observation as VNRecognizedTextObservation in request.results ?? [] { if let candidate = observation.topCandidates(1).first { print(candidate.string) } }这个链路和 Core ML 的用法很像:handler 负责读图,request 负责描述任务,perform 负责执行。理解这一点后,后续所有参数调整都围绕 request 进行,抓图逻辑和识别逻辑可以完全分离。这也是为什么先写图片版脚本、再接入屏幕抓图是更合理的开发顺序。
2.2 请求参数与结果对象速查
VNRecognizeTextRequest的常用参数如下:
| 参数 | 默认值 | 作用 |
|---|---|---|
recognitionLevel | .accurate | 识别精度,可选.fast或.accurate |
usesLanguageCorrection | true | 是否用语言模型修正拼写 |
recognitionLanguages | 空数组 | 识别语言列表,空则按系统语言推断 |
minimumTextHeight | 0.0 | 低于该高度比例的文字会被忽略 |
regionOfInterest | 全图 | 只识别图像中的指定区域 |
结果对象VNRecognizedTextObservation包含两个重要成员:
boundingBox:识别文字行在图像中的位置,使用归一化坐标。topCandidates(_:):按置信度排序的候选字符串数组。
实际项目中几乎只用topCandidates(1).first?.string取出最可能的字符串。少数场景会用前三个候选做纠错或人工确认,但普通抓字工具不需要。
2.3 坐标系统是最大的认知坑
boundingBox使用 Vision 归一化坐标系:原点 (0,0) 在图像左下角,(1,1) 在右上角。而 macOS 在 Core Graphics 里的常用显示坐标是原点在左上角。如果不做转换,直接把boundingBox画到图片上,会发现识别框整体上下颠倒。
转换公式很简单:
let normalized = observation.boundingBox let topLeftX = normalized.origin.x let topLeftY = 1.0 - normalized.origin.y - normalized.height如果只是输出纯文本、不画框,这个坐标问题可以暂时忽略。但只要你想在截图上用红框标出识别到的文字,这一步就绕不开。另外,输出顺序也需要靠坐标排序,这一点在后文单独展开。
3. 从屏幕到文本:搭建最小抓字工具
3.1 环境准备:版本和工程形态
先确认本机环境,否则会在识别阶段无谓踩兼容性问题:
| 依赖 | 最低要求 | 建议 |
|---|---|---|
| macOS | 10.15 以上 | 13 以上,中文识别效果更好 |
| Xcode Command Line Tools | 已安装 | 保持最新 |
| 屏幕录制权限 | 抓取其他 App 窗口时需要 | 在系统设置中授权 |
可以用下面的代码查询当前系统支持的识别语言列表和可用 revision:
import Vision let supported = try VNRecognizeTextRequest.supportedRecognitionLanguages( for: .accurate, revision: VNRecognizeTextRequestRevision3 ) print(supported)VNRecognizeTextRequestRevision3需要 macOS 13 及以上。低版本系统要把revision参数换成系统默认版本,或者直接调用不带 revision 参数的版本。如果原始项目没有给出明确版本要求,落地前要先确认自己系统的实际支持情况。
注意:
swift直接运行脚本时,第一次会先编译,耗时比执行编译好的二进制慢不少,这是正常现象。
3.2 先写一个图片 OCR 脚本
为了让问题边界清晰,先实现输入图片、输出文本的最小脚本ocr.swift:
#!/usr/bin/env swift import Foundation import AppKit import Vision guard CommandLine.arguments.count > 1 else { print("用法: swift ocr.swift <图片路径>") exit(1) } let path = CommandLine.arguments[1] guard let image = NSImage(contentsOfFile: path) else { print("无法读取图片: \(path)") exit(1) } var rect = NSRect(origin: .zero, size: image.size) guard let cgImage = image.cgImage(forProposedRect: &rect, context: nil, hints: nil) else { print("无法转换为 CGImage") exit(1) } let request = VNRecognizeTextRequest() request.recognitionLevel = .accurate request.recognitionLanguages = ["zh-Hans", "en-US"] request.usesLanguageCorrection = true let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) try handler.perform([request]) var lines: [String] = [] for case let observation as VNRecognizedTextObservation in request.results ?? [] { if let candidate = observation.topCandidates(1).first { lines.append(candidate.string) } } print(lines.joined(separator: "\n"))这个脚本有几个关键点:
- 用
NSImage加载图片后再转CGImage,兼容常见图片格式;也可以直接用CGImageSource读取,更贴近底层但代码更长。 recognitionLanguages显式设置了["zh-Hans", "en-US"],避免系统语言环境不同导致的识别差异。- 输出顺序直接使用
request.results的返回顺序,严格场景需要按坐标排序。
3.3 接入屏幕抓图:真正实现“看到什么复刻什么”
上面的脚本只解决了“图片到文字”,真正的 monkey see 还需要拿到屏幕内容。最简单的方式是用系统自带的screencapture命令:
screencapture -x -o /tmp/screen.png swift ocr.swift /tmp/screen.png-x表示不播放抓图声音,-o表示不包含鼠标指针。这样可以得到全屏截图,再交给 OCR 脚本。如果想只抓一个区域,可以用-R x,y,w,h:
screencapture -x -o -R 200,200,1000,600 /tmp/region.png这种方式优点是零代码接入,缺点是每次都要落盘一次图片,而且无法精确指定某个窗口。如果要做成 Mac App,或者想对一个窗口
