Discover Swift enhancements in the Vision framework
SwiftUI & UI Frameworks 进阶 20m

探索 Vision 框架的 Swift 增强

Discover Swift enhancements in the Vision framework

2024年6月10日

在 Apple 官方观看视频

一句话判断

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 可以共存。建议从最常用的几个请求开始迁移——DetectBarcodesRequestRecognizeTextRequestDetectFaceRectanglesRequest 这几个改动量最小、收益最明显。旧的 VNImageRequestHandler + completion handler 代码可以逐个文件替换,不需要一次性重写。坐标转换部分优先迁移到 toImageCoordinates(),能立刻消除一类常见 bug。

还有什么值得关注

  • Vision 总共有 31 种请求类型,涵盖图像分类、物体检测、人体姿态(2D 和 3D)、运动追踪等。
  • 新 API 完全兼容 Swift 6 严格并发模式,不需要任何 @Sendable 标注的 workaround。
  • 每个 request 的配置属性(如条码类型、文字识别语言)都可以 fine-tune 来优化特定场景的性能。
WWDC 2024