赛博探案集:用 Vision 框架在像素迷宫中“揪”出文字真凶
![]()
这里是后厂村阴影中最神秘的“全栈侦探事务所”。当你的 if-else 走到尽头,当你的 Bug 堆积如山,资深探长“老司机”就是你最后的救命稻草。本期案卷记录了一次关于“像素与文字”的离奇遭遇:实习生阿强因“人肉 OCR”识别截图密码失败,险些引发上线事故。面对这起“视力危机”,我们拒绝蛮力,祭出了 Apple 强大的 Vision 框架。这不仅是一篇关于如何用 Swift 实现 OCR(文字识别)的硬核教程,更是一场从构建“文字捕手”到破解“坐标迷宫”的技术探险。准备好了吗?泡好你的枸杞咖啡,跟随老司机的代码,一起揭开隐藏在图片像素背后的真相。
🕵️♂️ 引子
在一个雷雨交加的周五深夜,位于后厂村的“全栈侦探事务所”依然灯火通明。传说中,这里有一位代号为“老司机”的资深工程师,他不仅能用汇编语言写情书,还能在没有任何文档的遗留代码(Legacy Code)中自由穿梭。
就在刚刚,事务所的大门被撞开了。实习生阿强跌跌撞撞地跑进来,手里挥舞着一张模糊不清的截图,脸上写满了被产品经理折磨后的绝望。“老大!出大事了!这图片里藏着服务器的 Root 密码,但我手抄了三次都提示错误!现在上线倒计时只剩 30 分钟了!”
![]()
老司机缓缓放下手中早已凉透的黑咖啡,推了推鼻梁上那副防蓝光眼镜,嘴角勾起一抹神秘的微笑。“阿强,把你的‘人肉 OCR’停一停吧。在 Apple 的地盘上,我们有更优雅的武器——Vision 框架。”
在本次探案之旅中,您将学到如下内容:
- 🕵️♂️ 引子
- 🤖 第一章:不仅是扫码工具人的 Vision
- 🛠️ 第二章:打造“文字捕手” (The Text Recognizer)
- ⚠️ 老司机的技术批注:
- 🎯 第三章:给真相画个圈 (Highlighting Found Text)
- 🤝 终章:真相大白
他指尖在机械键盘上飞舞,屏幕上开始跳动起绿色的代码符文。“坐好,今晚带你见识一下,如何用机器学习的‘天眼’,让图片里的文字自己‘招供’。”
![]()
🤖 第一章:不仅是扫码工具人的 Vision
听好了,阿强。大多数人对 Apple Vision 框架的印象,还停留在扫个二维码或者条形码这种“小儿科”的阶段。这就好比你拿着一把激光剑去切西瓜——简直是暴殄天物!
实际上,Vision 就像是给你的 App 装上了一双“写轮眼”。它不仅能从图片中识别并定位文字(Text Detection),还能把图片里的特定区域剥离出来、在连续的视频帧里追踪物体、甚至检测你那僵硬的手势和坐姿!
![]()
我第一次跟 Vision 打交道的时候,是写了一个 Swift 命令行工具来移除图片背景 ✂️。那时候我就意识到,这玩意儿简直是修图师的噩梦,程序员的福音。但今天,我们要用它来做点更硬核的——文字识别。
![]()
🛠️ 第二章:打造“文字捕手” (The Text Recognizer)
要在茫茫像素中提取文字,我们得先组装一个名为 TextRecognizer 的“审讯室”。在这个环节,我们要用到 Vision 的核心组件:RecognizeTextRequest。
这就好比我们向系统提交一份“搜查令”,告诉它:“嘿,帮我把这张图里的字儿都给我找出来,而且要准(Accurate)!”
![]()
来看看这段代码,这可是我们的核心武器:
import Foundation
import SwiftUI
import Vision
struct TextRecognizer {
var recognizedText = ""
// 保存识别到的所有“线索”(观察结果)
var observations: [RecognizedTextObservation] = []
// 这个初始化器是异步的,因为查案需要时间,急不得
init(imageResource: ImageResource) async {
// 1. 创建搜查令:RecognizeTextRequest
var request = RecognizeTextRequest()
// 2. 将识别精度设置为 .accurate(我们要的是精准打击,不是瞎猜)
request.recognitionLevel = .accurate
// 3. 将 ImageResource 转换为 UIImage
let image = UIImage(resource: imageResource)
// 4. 重点来了!Vision 不吃 UIImage 这一套,它只认二进制数据 Data
// 所以我们必须把图片“粉碎”成 PNG 数据
if let imageData = image.pngData(),
// 执行搜查任务(perform)。这一步可能会失败,所以用了 try? 来“掩耳盗铃”
// 注意:这里是异步等待结果
let results = try? await request.perform(on: imageData) {
// 5. 将抓获的嫌疑人(观察结果)关进 observations 数组
observations = results
}
// 6. 审讯环节:遍历每一个观察结果
for observation in observations {
// 获取可能性最高的那个“候选词”(topCandidates(1))
// 就像指认现场,我们通常只信最像的那个
let candidate = observation.topCandidates(1)
if let observedText = candidate.first?.string {
// 把招供的文字拼接到结果字符串里
recognizedText += "\n\(observedText) "
}
}
}
}
![]()
⚠️ 老司机的技术批注:
这里有个坑你要注意,阿强。RecognizeTextRequest 是个挑剔的家伙,它不能直接处理 Swift 的 Image 或 UIImage 对象,它需要生肉——也就是 Image Data。
![]()
所以我们必须先把图片转成 Data 格式。另外,整个过程是 async(异步)的,毕竟机器学习这玩意儿虽然快,但也没快到能超越光速,我们得给 CPU 一点“思考”的时间。
![]()
接下来,我们把这个“文字捕手”集成到 SwiftUI 的视图里,让你亲眼看看效果:
import SwiftUI
struct TextRecognitionView: View {
let imageResource: ImageResource
// 状态变量,一旦侦探有了结果,界面就会刷新
@State private var textRecognizer: TextRecognizer?
var body: some View {
List {
// 展示嫌疑图片
Section {
Image(imageResource)
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
.listRowBackground(Color.clear)
// 展示审讯结果(识别出的文字)
Section {
// 如果 textRecognizer 还没初始化好,就先显示空字符串
Text(textRecognizer?.recognizedText ?? "")
} header: {
Text("从图片中提取的证词")
}
}
.navigationTitle("文字侦探")
.task {
// 重点:在 .task 修饰符里调用异步初始化器
// 就像在后台偷偷干活,不阻塞主线程 UI 的渲染
textRecognizer = await TextRecognizer(imageResource: imageResource)
}
}
}
这时候,阿强凑过来看着模拟器屏幕,只见原本模糊的截图下方,整整齐齐地列出了识别出来的文字。“卧槽,神了!连那个像‘1’又像‘l’的字符都分清了!”
![]()
🎯 第三章:给真相画个圈 (Highlighting Found Text)
“别急着庆祝,阿强。”我敲了敲桌子,“光把字认出来还不够,我们要做到按图索骥。既然 Vision 已经告诉了我们文字在哪里,我们就得在图片上把它们圈出来,就像犯罪现场的粉笔线一样。”
![]()
这里涉及到一个让很多新手头秃的概念:坐标系转换。
Vision 返回的坐标是归一化的(Normalized),也就是说,它的 x 和 y 都在 0.0 到 1.0 之间。左下角是 (0,0),右上角是 (1,1)。但我们的屏幕图片是按像素画的,而且 UIKit/SwiftUI 的坐标原点通常在左上角。这就好比火星人给地球人指路,如果不好好翻译一下坐标,你画的框可能会飞到姥姥家去。
我们需要定义一个 Shape,专门用来画框:
import Foundation
import SwiftUI
import Vision
struct BoundsRect: Shape {
// 这里存的是 Vision 给我们的“火星坐标”(归一化矩形)
let normalizedRect: NormalizedRect
func path(in rect: CGRect) -> Path {
// 关键时刻!将归一化坐标转换为图片的实际像素坐标
// origin: .upperLeft 是为了适配 SwiftUI 的坐标系习惯
let imageCoordinatesRect = normalizedRect
.toImageCoordinates(rect.size, origin: .upperLeft)
return Path(imageCoordinatesRect)
}
}
![]()
🔍 技术扩展: toImageCoordinates 这个方法虽然原文没细说,但它大概率是一个扩展方法(Extension),用于把 0~1 的小数映射到图片的 width 和 height 上,并处理坐标原点的翻转。这一步至关重要,不做这一步,你的框框就会像没头苍蝇一样乱撞。
![]()
![]()
现在,我们把这个“现形符”贴到图片上:
struct TextRecognitionView: View {
// ... 前面的代码 ...
// 定义一个深红色的框,充满了悬疑感
let boundingColor = Color(red: 0.31, green: 0.11, blue: 0.11)
var body: some View {
List {
Section {
Image(imageResource)
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay {
// 如果侦探已经有了观察结果
if let observations = textRecognizer?.observations {
ForEach(observations, id: \.uuid) { observation in
// 遍历每一个观察点,画个圈圈诅咒...啊不,标记它
// observation.boundingBox 就是那个归一化的坐标
BoundsRect(normalizedRect: observation.boundingBox)
.stroke(boundingColor, lineWidth: 3) // 描边
}
}
}
}
// ... 后面的代码 ...
}
}
}
![]()
随着代码重新编译运行,屏幕上的截图发生了变化。每一个单词周围都被套上了一个暗红色的方框,就像是被狙击手锁定的目标。
![]()
![]()
🤝 终章:真相大白
“看到了吗?”我指着屏幕上被红框圈出的一串字符,“那根本不是 Root 密码。”
阿强瞪大了眼睛,盯着那行被 Vision 精准识别出的文字:WIFI_PASSWORD: 12345678。
“这……这就是隔壁会议室的 WiFi 密码?”阿强瘫软在椅子上,“我为了这个通宵了两天?”
![]()
我拍了拍他的肩膀,语重心长地说道:“虽然你是个笨蛋,但好在 Vision 框架足够聪明。记住,Vision 不仅仅能找字,它还能做更多事情——从视频里追踪隔壁老王的身影,到检测你是不是在偷偷抠脚(Body Pose Detection)。今天我们学的只是冰山一角,但也足够你在这个充满像素迷雾的开发世界里防身了。”
就这样,Vision 框架再次拯救了一个无知的灵魂(虽然并没有拯救他的加班费)。
![]()
希望宝子们喜欢这个故事,以及它背后的技术,但对于小伙伴们来说,利用 Apple 强大的 ML 能力去探索未知的旅程,才刚刚开始。
![]()
保持好奇,保持代码整洁,我们下个案子见。👋🙂 8-)