探索 Vision 框架的 Swift 增强
Discover Swift enhancements in the Vision framework
2024年6月10日
一句话判断
Vision 框架今年用 Swift 原生 API 全面替换了旧的 OC 风格接口,配合 Swift Concurrency 和 parameter pack 批量请求,三行代码就能完成以前二十行才能做到的图像分析。
这场 Session 讲了什么
Vision 是 Apple 的计算机视觉框架,提供人脸检测、文字识别、人体姿态估计、条码扫描等 31 种不同的图像分析请求。今年 Apple 做了一件早该做的事:用 Swift 原生 API 替换了之前基于 completion handler 的 Objective-C 风格接口。
新 API 的设计遵循 Swift 的惯用模式:async/await 替代回调、throws 替代 NSError、结构体替代类。核心用法简化到三行:创建 request、await 执行、读取 observations。支持 Swift 6 的严格并发检查。
批量请求有两种模式:perform(parameter pack 语法,等待所有请求完成)和 performAll(AsyncStream,每个请求完成就返回)。还有新的坐标转换 API toImageCoordinates(),解决 Vision 归一化坐标系和其他框架坐标系不一致的老问题。
值得深挖的点
三行代码的哲学:从 20 行到 3 行的 API 演进
旧的 Vision API 用起来有多痛苦?创建 VNImageRequestHandler、创建 VNDetectBarcodesRequest、设置 completion handler、在 handler 里遍历 results、类型转换、回到主线程更新 UI——随随便便就二十多行,而且嵌套回调的可读性很差。
新 API 把这压缩到三行:
let request = DetectBarcodesRequest()
let observations = try await request.perform(on: image)
// 拿到结果了
怎么做到的?perform(on:) 是 async throws 的实例方法,直接返回强类型的 observations 数组。不再需要 VNImageRequestHandler 这个中间层来执行单个请求——它只在批量请求时才需要。
这种简化不是偷工减料。底层能力完全没变,31 种请求类型全部保留,每个请求的配置属性(比如 DetectBarcodesRequest.symbologies)也都在。Apple 做的是把”创建 handler → 创建 request → 设置回调 → 执行 → 处理回调”这个五步流程压缩成了”创建 request → 执行 → 用结果”三步。
坐标系转换:一个困扰开发者多年的小问题
Vision 的 observations 使用归一化坐标系,原点在左下角,坐标值在 0 到 1 之间。但 SwiftUI 的坐标系原点在左上角,UIKit 的也在左上角。每次拿到 Vision 的 bounding box,你都要手动翻转 Y 轴、乘以图片尺寸。这个小问题每年都在论坛上被反复提问。
新 API 提供 toImageCoordinates() 方法,一步完成坐标系转换和去归一化:
let box = observation.boundingBox.toImageCoordinates(
image.size, origin: .upperLeft
)
这个改动小到几乎不值得在 WWDC 上专门提,但它消除了一个反复出现的 bug 来源——手动坐标转换做错一次,人脸框就画到图片外面去了。
批量请求的两种策略
当你需要对同一张图片执行多个请求时,有两种模式。perform 使用 parameter pack 语法,等待所有请求完成后一起返回结果。适合”我需要所有结果才能继续”的场景。performAll 返回 AsyncStream,每个请求完成就立即回调。适合”哪个先完成我就先用哪个”的场景。
这个区分很重要。Session 举了一个例子:同时扫描条码和识别文字。如果条码扫描先完成,用户不用等文字识别结束就能看到结果。用 performAll 的话,条码结果可以立即处理,文字结果随后跟上。
代码片段
最简单的条码扫描
场景:扫描图片中的条码并获取 payload。
// 三行代码完成条码检测
let request = DetectBarcodesRequest()
let observations = try await request.perform(on: image)
let payload = observations.first?.payloadStringValue
坑:默认扫描所有条码类型,如果只需要特定类型,设置 symbologies 属性来优化性能。
批量请求并逐个处理结果
场景:同时检测条码和识别文字,结果异步返回。
let barcodeRequest = DetectBarcodesRequest()
let textRequest = RecognizeTextRequest()
let handler = ImageRequestHandler(image)
// performAll 返回 stream,每个请求完成就回调
for try await result in handler.performAll([barcodeRequest, textRequest]) {
switch result {
case .barcodes(let observations):
// 条码结果先到就先处理
processBarcodes(observations)
case .text(let observations):
// 文字结果后到就后处理
processText(observations)
}
}
坑:performAll 需要用 ImageRequestHandler,单个请求可以直接在 request 上调 perform(on:),别搞混。
用 Swift Concurrency 并行处理多张图片
场景:对相册中的多张图片进行裁剪,批量处理。
// 用 withTaskGroup 并行处理多张图片
await withTaskGroup(of: UIImage.self) { group in
for image in images {
group.addTask {
let request = GenerateObjectnessBasedSaliencyImageRequest()
let observation = try await request.perform(on: image)
// 基于 salient region 裁剪图片
return self.cropToSubject(image, using: observation)
}
}
for await cropped in group {
display(cropped)
}
}
坑:并发的任务数量要根据设备能力控制,不要一次性开太多。
最佳实践
新项目:直接用新 API,旧 API 不要碰。新 API 完全覆盖旧 API 的能力,而且和 Swift 6 严格并发检查兼容。如果你的项目只做单一类型的图像分析(比如只扫条码),用 request.perform(on:) 就够了。如果涉及多类型分析,用 performAll 拿 stream。
已有项目:迁移是渐进式的,新旧 API 可以共存。建议从最常用的几个请求开始迁移——DetectBarcodesRequest、RecognizeTextRequest、DetectFaceRectanglesRequest 这几个改动量最小、收益最明显。旧的 VNImageRequestHandler + completion handler 代码可以逐个文件替换,不需要一次性重写。坐标转换部分优先迁移到 toImageCoordinates(),能立刻消除一类常见 bug。
还有什么值得关注
- Vision 总共有 31 种请求类型,涵盖图像分类、物体检测、人体姿态(2D 和 3D)、运动追踪等。
- 新 API 完全兼容 Swift 6 严格并发模式,不需要任何
@Sendable标注的 workaround。 - 每个 request 的配置属性(如条码类型、文字识别语言)都可以 fine-tune 来优化特定场景的性能。