iOS Vision framework|WWDC 24 Swift Concurrency 与新 API 深度解析与实作指南
iOS 开发者面对 Vision framework 复杂图像辨识需求,透过 WWDC 24 Swift Concurrency 新 API,掌握 31 种图像辨识请求与身体手势追踪,提升辨识效率与安全性,实现高效图像分析与即时应用。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
iOS Vision framework x WWDC 24 Discover Swift enhancements in the Vision framework Session
Vision framework 功能回顾 & iOS 18 新 Swift API 试玩
Photo by BoliviaInteligente
主题
跟 Vision Pro 的关系就跟热狗跟狗的关系一样,毫无关系。
Vision framework
Vision framework 是 Apple 整合机器学习的图像辨识框架,让开发者可以简单快速地实现常见的图像辨识功能;Vision framework 早在 iOS 11.0+ (2017/ iPhone 8) 就已推出,期间不断地迭代优化,并完善与 Swift Concurrency 的特性整合提升执行效能,并且从 iOS 18.0 提供全新的 Swift Vision framework API 发挥 Swift Concurrency 最大效果。
Vision framework 特色
内建众多图片辨识、动态追踪方法 (iOS 18 为止一共 31 种)
On-Device 单纯使用手机晶片运算,辨识过程不依赖云端服务,快速又安全
API 简单好操作
Apple 全平台均支援 iOS 11.0+, iPadOS 11.0+, Mac Catalyst 13.0+, macOS 10.13+, tvOS 11.0+, visionOS 1.0+
已发布多年 (2017~今) 且不断更新
整合 Swift 语言特性提升运算效能
6 年前曾经小玩过: Vision 初探 — APP 头像上传 自动识别人脸裁图 (Swift)
这次搭配 WWDC 24 Discover Swift enhancements in the Vision framework Session 重新回顾并结合新的 Swfit 特性再玩一次。
CoreML
Apple 还有另外一个 Framework 叫 CoreML ,也是基于 On-Device 晶片的机器学习框架;但他可以让你自己训练想辨识的物件、文件模型,并将模型放到 App 中直接使用,有兴趣的朋友也可以玩看看。(e.g. 即时文章分类 、即时 垃圾讯息检测 …)
p.s.
Vision _:主要用于图像分析任务,如脸部识别、条码检测、文本识别等。它提供了强大的 API 来处理和分析静态图像或视频中的视觉内容。*
VisionKit _:专门用于处理与文件扫描相关的任务。它提供了一个扫描仪视图控制器,可以用来扫描文档,并生成高质量的 PDF 或图像。*
Vision framework 在 M1 机型上无法跑在模拟器,只能接实体手机测试;在模拟器环境执行会抛出 Could not create Espresso context
Error,查 官方论坛讨论,没找到解答 。
因手边没有实体 iOS 18 装置进行测试,所以本文中的所有执行结果都是使用旧的 (iOS 18 以前) 的写法结果; 如新写法有出现错误再麻烦留言指教 。
WWDC 2024 — Discover Swift enhancements in the Vision framework
Discover Swift enhancements in the Vision framework
本文是针对 WWDC 24 — Discover Swift enhancements in the Vision framework Session 的分享笔记,跟一些自己实验的心得。
Introduction — Vision framework Features
人脸辨识、轮廓识别
图像内容文字辨识
截至 iOS 18 为止,支援 18 种语言。
1
2
3
4
5
6
7
8
9
10
11
// 支援的语系列表
if #available(iOS 18.0, *) {
print(RecognizeTextRequest().supportedRecognitionLanguages.map { "\($0.languageCode!)-\(($0.region?.identifier ?? $0.script?.identifier)!)" })
} else {
print(try! VNRecognizeTextRequest().supportedRecognitionLanguages())
}
// 实际可用辨识语言以这为主。
// 实测 iOS 18 输出以下结果:
// ["en-US", "fr-FR", "it-IT", "de-DE", "es-ES", "pt-BR", "zh-Hans", "zh-Hant", "yue-Hans", "yue-Hant", "ko-KR", "ja-JP", "ru-RU", "uk-UA", "th-TH", "vi-VT", "ar-SA", "ars-SA"]
// 未看到 WWDC 提到的 Swedish 语言,不确定是还没推出还是跟装置地区、语系有关联
动态动作捕捉
可以实现人、物件动态捕捉
手势补捉实现隔空签名功能
What’s new in Vision? (iOS 18)— 图片评分功能 (品质、记忆点)
可对输入图片得计算出分数,方便筛选出优质照片
分数计算方式包含多个维度,不只是画质,还有光线、角度、拍摄主体、 是否有让人感到的记忆点 …等等
WWDC 中给了以上三张图片做说明(相同画质之下),分别是:
高分的图片:取景、光线、有记忆点
低分的图片:没有主体、像是随手或不小心拍的
素材的图片:技术上拍的很好但是没有记忆点,像是作为素材图库用的图片
iOS ≥ 18 New API: CalculateImageAestheticsScoresRequest
1
2
3
4
5
6
7
8
let request = CalculateImageAestheticsScoresRequest()
let result = try await request.perform(on: URL(string: "https://zhgchg.li/assets/cb65fd5ab770/1*yL3vI1ADzwlovctW5WQgJw.jpeg")!)
// 照片分数
print(result.overallScore)
// 是否被判定为素材图片
print(result.isUtility)
What’s new in Vision? (iOS 18) — 身体+手势姿势同时侦测
以往只能个别侦测人体 Pose 和 手部 Pose,这次更新可以让开发者同时侦测身体 Pose + 手部 Pose,合成同一个请求跟结果,方便我们做更多应用功能开发。
iOS ≥ 18 New API: DetectHumanBodyPoseRequest
1
2
3
4
5
6
7
8
9
10
11
12
var request = DetectHumanBodyPoseRequest()
// 一并侦测手部 Pose
request.detectsHands = true
guard let bodyPose = try await request.perform(on: image). first else { return }
// 身体 Pose Joints
let bodyJoints = bodyPose.allJoints()
// 左手 Pose Joints
let leftHandJoints = bodyPose.leftHand.allJoints()
// 右手 Pose Joints
let rightHandJoints = bodyPose.rightHand.allJoints()
New Vision API
Apple 在这次的更新当中提供了新的 Swift Vision API 封装给开发者使用,除了基本的包含原本的功能支援之外,主要针对加强 Swift 6 / Swift Concurrency 的特性,提供效能更优、写起来更 Swift 的 API 操作方式。
Get started with Vision
这边讲者又重新介绍了一次 Vision framework 的基础使用方式,Apple 已经封装好了 31 种 (截至 iOS 18)常见的图像辨识请求「Request」与对应回传的「Observation」物件。
Request: DetectFaceRectanglesRequest 人脸区域识别请求 Result: FaceObservation 之前的文章「 Vision 初探 — APP 头像上传 自动识别人脸裁图 (Swift) 」就是用这对请求。
Request: RecognizeTextRequest 文字辨识请求 Result: RecognizedTextObservation
Request: GenerateObjectnessBasedSaliencyImageRequest 主体物件辨识请求 Result: SaliencyImageObservation
全部 31 种请求 Request:
\| Request 用途 \| Observation 说明 \| \|———————————————–\|——————————————————————\| \| CalculateImageAestheticsScoresRequest
计算图像的美学分数。 \| AestheticsObservation
返回图像的美学评分,如构图、色彩等因素。 \| \| ClassifyImageRequest
分类图像内容。 \| ClassificationObservation
返回图像中物体或场景的分类标签及置信度。 \| \| CoreMLRequest
使用 Core ML 模型分析图像。 \| CoreMLFeatureValueObservation
根据 Core ML 模型的输出结果生成观察值。 \| \| DetectAnimalBodyPoseRequest
检测图像中的动物姿势。 \| RecognizedPointsObservation
返回动物的骨架点及其位置。 \| \| DetectBarcodesRequest
检测图像中的条码。 \| BarcodeObservation
返回条码数据及类型(如 QR code)。 \| \| DetectContoursRequest
检测图像中的轮廓。 \| ContoursObservation
返回图像中检测到的轮廓线。 \| \| DetectDocumentSegmentationRequest
检测并分割图像中的文件。 \| RectangleObservation
返回文件边界的矩形框位置。 \| \| DetectFaceCaptureQualityRequest
评估面部捕捉质量。 \| FaceObservation
返回面部图像的质量评估分数。 \| \| DetectFaceLandmarksRequest
检测面部特征点。 \| FaceObservation
返回面部特征点(如眼睛、鼻子等)的详细位置。 \| \| DetectFaceRectanglesRequest
检测图像中的面部。 \| FaceObservation
返回人脸的边界框位置。 \| \| DetectHorizonRequest
检测图像中的地平线。 \| HorizonObservation
返回地平线的角度和位置。 \| \| DetectHumanBodyPose3DRequest
检测图像中的 3D 人体姿势。 \| RecognizedPointsObservation
返回 3D 人体骨架点及其空间坐标。 \| \| DetectHumanBodyPoseRequest
检测图像中的人体姿势。 \| RecognizedPointsObservation
返回人体骨架点及其坐标。 \| \| DetectHumanHandPoseRequest
检测图像中的手部姿势。 \| RecognizedPointsObservation
返回手部骨架点及其位置。 \| \| DetectHumanRectanglesRequest
检测图像中的人体。 \| HumanObservation
返回人体的边界框位置。 \| \| DetectRectanglesRequest
检测图像中的矩形。 \| RectangleObservation
返回矩形的四个顶点坐标。 \| \| DetectTextRectanglesRequest
检测图像中的文本区域。 \| TextObservation
返回文本区域的位置和边界框。 \| \| DetectTrajectoriesRequest
检测并分析物体运动轨迹。 \| TrajectoryObservation
返回运动轨迹点及其时间序列。 \| \| GenerateAttentionBasedSaliencyImageRequest
生成基于注意力的显著性图像。 \| SaliencyImageObservation
返回图像中最具吸引力区域的显著性地图。 \| \| GenerateForegroundInstanceMaskRequest
生成前景实例掩膜图像。 \| InstanceMaskObservation
返回前景物体的掩膜。 \| \| GenerateImageFeaturePrintRequest
生成图像特征指纹以进行比较。 \| FeaturePrintObservation
返回图像的特征指纹数据,用于相似度比较。 \| \| GenerateObjectnessBasedSaliencyImageRequest
生成基于物体显著性的图像。 \| SaliencyImageObservation
返回物体显著性区域的显著性地图。 \| \| GeneratePersonInstanceMaskRequest
生成人物实例掩膜图像。 \| InstanceMaskObservation
返回人物实例的掩膜。 \| \| GeneratePersonSegmentationRequest
生成人物分割图像。 \| SegmentationObservation
返回人物分割的二值图。 \| \| RecognizeAnimalsRequest
检测并识别图像中的动物。 \| RecognizedObjectObservation
返回动物类型及其置信度。 \| \| RecognizeTextRequest
检测并识别图像中的文本。 \| RecognizedTextObservation
返回检测到的文本内容及其区域位置。 \| \| TrackHomographicImageRegistrationRequest
跟踪图像的同位影像配准。 \| ImageAlignmentObservation
返回图像间的同位变换矩阵,用于影像配准。 \| \| TrackObjectRequest
跟踪图像中的物体。 \| DetectedObjectObservation
返回物体在影像中的位置和速度信息。 \| \| TrackOpticalFlowRequest
跟踪图像中的光流。 \| OpticalFlowObservation
返回光流矢量场,用于描述像素移动情况。 \| \| TrackRectangleRequest
跟踪图像中的矩形。 \| RectangleObservation
返回矩形在影像中的位置、大小和旋转角度。 \| \| TrackTranslationalImageRegistrationRequest
跟踪图像的平移影像配准。 \| ImageAlignmentObservation
返回图像间的平移变换矩阵,用于影像配准。 \|
- 前面补上 VN 就是旧的 API 写法 (iOS 18 以前的版本)
讲者提到了几个常用的 Request,如下。
ClassifyImageRequest
辨识输入的图片,得到标签分类与置信度。
[游记] 2024 二访九州 9 日自由行,经釜山→博多邮轮入境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
if #available(iOS 18.0, *) {
// 新的使用 Swift 特性的 API
let request = ClassifyImageRequest()
Task {
do {
let observations = try await request.perform(on: URL(string: "https://zhgchg.li/assets/cb65fd5ab770/1*yL3vI1ADzwlovctW5WQgJw.jpeg")!)
observations.forEach {
observation in
print("\(observation.identifier): \(observation.confidence)")
}
}
catch {
print("Request failed: \(error)")
}
}
} else {
// 旧的写法
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("Request failed: \(String(describing: error))")
return
}
guard let observations = request.results as? [VNClassificationObservation] else {
return
}
observations.forEach {
observation in
print("\(observation.identifier): \(observation.confidence)")
}
}
let request = VNClassifyImageRequest(completionHandler: completionHandler)
DispatchQueue.global().async {
let handler = VNImageRequestHandler(url: URL(string: "https://zhgchg.li/assets/cb65fd5ab770/1*3_jdrLurFuUfNdW4BJaRww.jpeg")!, options: [:])
do {
try handler.perform([request])
}
catch {
print("Request failed: \(error)")
}
}
}
分析结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
• outdoor(户外): 0.75392926
• sky(天空): 0.75392926
• blue_sky(蓝天): 0.7519531
• machine(机器): 0.6958008
• cloudy(多云): 0.26538086
• structure(结构): 0.15728651
• sign(标志): 0.14224191
• fence(栅栏): 0.118652344
• banner(横幅): 0.0793457
• material(材料): 0.075975396
• plant(植物): 0.054406323
• foliage(树叶): 0.05029297
• light(光): 0.048126098
• lamppost(灯柱): 0.048095703
• billboards(广告牌): 0.040039062
• art(艺术): 0.03977703
• branch(树枝): 0.03930664
• decoration(装饰): 0.036868922
• flag(旗帜): 0.036865234
....略
RecognizeTextRequest
辨识图片中的文字内容。(a.k.a 图片转文字)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
if #available(iOS 18.0, *) {
// 新的使用 Swift 特性的 API
var request = RecognizeTextRequest()
request.recognitionLevel = .accurate
request.recognitionLanguages = [.init(identifier: "ja-JP"), .init(identifier: "en-US")] // Specify language code, e.g., Traditional Chinese
Task {
do {
let observations = try await request.perform(on: URL(string: "https://zhgchg.li/assets/9da2c51fa4f2/1*fBbNbDepYioQ-3-0XUkF6Q.jpeg")!)
observations.forEach {
observation in
let topCandidate = observation.topCandidates(1).first
print(topCandidate?.string ?? "No text recognized")
}
}
catch {
print("Request failed: \(error)")
}
}
} else {
// 旧的写法
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("Request failed: \(String(describing: error))")
return
}
guard let observations = request.results as? [VNRecognizedTextObservation] else {
return
}
observations.forEach {
observation in
let topCandidate = observation.topCandidates(1).first
print(topCandidate?.string ?? "No text recognized")
}
}
let request = VNRecognizeTextRequest(completionHandler: completionHandler)
request.recognitionLevel = .accurate
request.recognitionLanguages = ["ja-JP", "en-US"] // Specify language code, e.g., Traditional Chinese
DispatchQueue.global().async {
let handler = VNImageRequestHandler(url: URL(string: "https://zhgchg.li/assets/9da2c51fa4f2/1*fBbNbDepYioQ-3-0XUkF6Q.jpeg")!, options: [:])
do {
try handler.perform([request])
}
catch {
print("Request failed: \(error)")
}
}
}
分析结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
LE LABO 青山店
TEL:03-6419-7167
*お买い上げありがとうございます*
No: 21347
日付:2023/06/10 14.14.57
担当:
1690370
レジ:008A 1
商品名
税込上代数量税込合计
カイアック 10 EDP FB 15ML
J1P7010000S
16,800
16,800
アナザー 13 EDP FB 15ML
J1PJ010000S
10,700
10,700
リップパーム 15ML
JOWC010000S
2,000
1
合计金额
(内税额)
CARD
2,000
3点御买上げ
29,500
0
29,500
29,500
DetectBarcodesRequest
侦测图片中的条码、QRCode 数据。
泰国当地人推荐鹅牌清凉膏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
let filePath = Bundle.main.path(forResource: "IMG_6777", ofType: "png")! // 本地测试图片
let fileURL = URL(filePath: filePath)
if #available(iOS 18.0, *) {
// 新的使用 Swift 特性的 API
let request = DetectBarcodesRequest()
Task {
do {
let observations = try await request.perform(on: fileURL)
observations.forEach {
observation in
print("Payload: \(observation.payloadString ?? "No payload")")
print("Symbology: \(observation.symbology)")
}
}
catch {
print("Request failed: \(error)")
}
}
} else {
// 旧的写法
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("Request failed: \(String(describing: error))")
return
}
guard let observations = request.results as? [VNBarcodeObservation] else {
return
}
observations.forEach {
observation in
print("Payload: \(observation.payloadStringValue ?? "No payload")")
print("Symbology: \(observation.symbology.rawValue)")
}
}
let request = VNDetectBarcodesRequest(completionHandler: completionHandler)
DispatchQueue.global().async {
let handler = VNImageRequestHandler(url: fileURL, options: [:])
do {
try handler.perform([request])
}
catch {
print("Request failed: \(error)")
}
}
}
分析结果:
1
2
3
4
5
6
7
8
Payload: 8859126000911
Symbology: VNBarcodeSymbologyEAN13
Payload: https://lin.ee/hGynbVM
Symbology: VNBarcodeSymbologyQR
Payload: http://www.hongthaipanich.com/
Symbology: VNBarcodeSymbologyQR
Payload: https://www.facebook.com/qr?id=100063856061714
Symbology: VNBarcodeSymbologyQR
RecognizeAnimalsRequest
辨识图片中的动物与置信度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
let filePath = Bundle.main.path(forResource: "IMG_5026", ofType: "png")! // 本地测试图片
let fileURL = URL(filePath: filePath)
if #available(iOS 18.0, *) {
// 新的使用 Swift 特性的 API
let request = RecognizeAnimalsRequest()
Task {
do {
let observations = try await request.perform(on: fileURL)
observations.forEach {
observation in
let labels = observation.labels
labels.forEach {
label in
print("Detected animal: \(label.identifier) with confidence: \(label.confidence)")
}
}
}
catch {
print("Request failed: \(error)")
}
}
} else {
// 旧的写法
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("Request failed: \(String(describing: error))")
return
}
guard let observations = request.results as? [VNRecognizedObjectObservation] else {
return
}
observations.forEach {
observation in
let labels = observation.labels
labels.forEach {
label in
print("Detected animal: \(label.identifier) with confidence: \(label.confidence)")
}
}
}
let request = VNRecognizeAnimalsRequest(completionHandler: completionHandler)
DispatchQueue.global().async {
let handler = VNImageRequestHandler(url: fileURL, options: [:])
do {
try handler.perform([request])
}
catch {
print("Request failed: \(error)")
}
}
}
分析结果:
1
Detected animal: Cat with confidence: 0.77245045
其他:
侦测图像中的人体:DetectHumanRectanglesRequest
侦测人、动物的 Pose 动作 (3D or 2D 都可以):DetectAnimalBodyPoseRequest、DetectHumanBodyPose3DRequest、DetectHumanBodyPoseRequest、DetectHumanHandPoseRequest
检测并追踪物件的运动轨迹(在影片、动画不同的侦中):DetectTrajectoriesRequest、TrackObjectRequest、TrackRectangleRequest
iOS ≥ 18 Update Highlight:
1
2
3
4
VN*Request -> *Request (e.g. VNDetectBarcodesRequest -> DetectBarcodesRequest)
VN*Observation -> *Observation (e.g. VNRecognizedObjectObservation -> RecognizedObjectObservation)
VNRequestCompletionHandler -> async/await
VNImageRequestHandler.perform([VN*Request]) -> *Request.perform()
WWDC Example
WWDC 官方影片以超市商品扫描器为例。
首先大多数的商品都有 Barcode 可供扫描
我们可以从 observation.boundingBox
取得 Barcode 所在位置,但不同于常见 UIView 座标系, BoundingBox
的相对位置起点是从左下角,值的范围落在 0~1 之间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
let filePath = Bundle.main.path(forResource: "IMG_6785", ofType: "png")! // 本地测试图片
let fileURL = URL(filePath: filePath)
if #available(iOS 18.0, *) {
// 新的使用 Swift 特性的 API
var request = DetectBarcodesRequest()
request.symbologies = [.ean13] // 如果只要扫描 EAN13 Barcode,可直接指定,提升效能
Task {
do {
let observations = try await request.perform(on: fileURL)
if let observation = observations.first {
DispatchQueue.main.async {
self.infoLabel.text = observation.payloadString
// 标记颜色 Layer
let colorLayer = CALayer()
// iOS >=18 新的座标转换 API toImageCoordinates
// 未经测试,实际可能还需要计算 ContentMode = AspectFit 的位移:
colorLayer.frame = observation.boundingBox.toImageCoordinates(self.baseImageView.frame.size, origin: .upperLeft)
colorLayer.backgroundColor = UIColor.red.withAlphaComponent(0.5).cgColor
self.baseImageView.layer.addSublayer(colorLayer)
}
print("BoundingBox: \(observation.boundingBox.cgRect)")
print("Payload: \(observation.payloadString ?? "No payload")")
print("Symbology: \(observation.symbology)")
}
}
catch {
print("Request failed: \(error)")
}
}
} else {
// 旧的写法
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("Request failed: \(String(describing: error))")
return
}
guard let observations = request.results as? [VNBarcodeObservation] else {
return
}
if let observation = observations.first {
DispatchQueue.main.async {
self.infoLabel.text = observation.payloadStringValue
// 标记颜色 Layer
let colorLayer = CALayer()
colorLayer.frame = self.convertBoundingBox(observation.boundingBox, to: self.baseImageView)
colorLayer.backgroundColor = UIColor.red.withAlphaComponent(0.5).cgColor
self.baseImageView.layer.addSublayer(colorLayer)
}
print("BoundingBox: \(observation.boundingBox)")
print("Payload: \(observation.payloadStringValue ?? "No payload")")
print("Symbology: \(observation.symbology.rawValue)")
}
}
let request = VNDetectBarcodesRequest(completionHandler: completionHandler)
request.symbologies = [.ean13] // 如果只要扫描 EAN13 Barcode,可直接指定,提升效能
DispatchQueue.global().async {
let handler = VNImageRequestHandler(url: fileURL, options: [:])
do {
try handler.perform([request])
}
catch {
print("Request failed: \(error)")
}
}
}
iOS ≥ 18 Update Highlight:
// iOS >=18 新的座标转换 API toImageCoordinates
observation.boundingBox.toImageCoordinates(CGSize, origin: .upperLeft)
// https://developer.apple.com/documentation/vision/normalizedpoint/toimagecoordinates(from:imagesize:origin:)
Helper:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Gen by ChatGPT 4o
// 因为照片在 ImageView 是设定 ContentMode = AspectFit
// 所以要多计算上下因 Fit 造成的空白位移
func convertBoundingBox(_ boundingBox: CGRect, to view: UIImageView) -> CGRect {
guard let image = view.image else {
return .zero
}
let imageSize = image.size
let viewSize = view.bounds.size
let imageRatio = imageSize.width / imageSize.height
let viewRatio = viewSize.width / viewSize.height
var scaleFactor: CGFloat
var offsetX: CGFloat = 0
var offsetY: CGFloat = 0
if imageRatio > viewRatio {
// 图像在宽度方向上适配
scaleFactor = viewSize.width / imageSize.width
offsetY = (viewSize.height - imageSize.height * scaleFactor) / 2
}
else {
// 图像在高度方向上适配
scaleFactor = viewSize.height / imageSize.height
offsetX = (viewSize.width - imageSize.width * scaleFactor) / 2
}
let x = boundingBox.minX * imageSize.width * scaleFactor + offsetX
let y = (1 - boundingBox.maxY) * imageSize.height * scaleFactor + offsetY
let width = boundingBox.width * imageSize.width * scaleFactor
let height = boundingBox.height * imageSize.height * scaleFactor
return CGRect(x: x, y: y, width: width, height: height)
}
输出结果
1
2
3
BoundingBox: (0.5295758928571429, 0.21408638121589782, 0.0943080357142857, 0.21254415360708087)
Payload: 4710018183805
Symbology: VNBarcodeSymbologyEAN13
部分商品无 Barcode,如散装水果只有商品标签
因此我们的扫瞄器也需要同时支援扫描纯文字标签。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
let filePath = Bundle.main.path(forResource: "apple", ofType: "jpg")! // 本地测试图片
let fileURL = URL(filePath: filePath)
if #available(iOS 18.0, *) {
// 新的使用 Swift 特性的 API
var barcodesRequest = DetectBarcodesRequest()
barcodesRequest.symbologies = [.ean13] // 如果只要扫描 EAN13 Barcode,可直接指定,提升效能
var textRequest = RecognizeTextRequest()
textRequest.recognitionLanguages = [.init(identifier: "zh-Hnat"), .init(identifier: "en-US")]
Task {
do {
let handler = ImageRequestHandler(fileURL)
// parameter pack syntax and we must wait for all requests to finish before we can use their results.
// let (barcodesObservation, textObservation, ...) = try await handler.perform(barcodesRequest, textRequest, ...)
let (barcodesObservation, textObservation) = try await handler.perform(barcodesRequest, textRequest)
if let observation = barcodesObservation.first {
DispatchQueue.main.async {
self.infoLabel.text = observation.payloadString
// 标记颜色 Layer
let colorLayer = CALayer()
// iOS >=18 新的座标转换 API toImageCoordinates
// 未经测试,实际可能还需要计算 ContentMode = AspectFit 的位移:
colorLayer.frame = observation.boundingBox.toImageCoordinates(self.baseImageView.frame.size, origin: .upperLeft)
colorLayer.backgroundColor = UIColor.red.withAlphaComponent(0.5).cgColor
self.baseImageView.layer.addSublayer(colorLayer)
}
print("BoundingBox: \(observation.boundingBox.cgRect)")
print("Payload: \(observation.payloadString ?? "No payload")")
print("Symbology: \(observation.symbology)")
}
textObservation.forEach {
observation in
let topCandidate = observation.topCandidates(1).first
print(topCandidate?.string ?? "No text recognized")
}
}
catch {
print("Request failed: \(error)")
}
}
} else {
// 旧的写法
let barcodesCompletionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("Request failed: \(String(describing: error))")
return
}
guard let observations = request.results as? [VNBarcodeObservation] else {
return
}
if let observation = observations.first {
DispatchQueue.main.async {
self.infoLabel.text = observation.payloadStringValue
// 标记颜色 Layer
let colorLayer = CALayer()
colorLayer.frame = self.convertBoundingBox(observation.boundingBox, to: self.baseImageView)
colorLayer.backgroundColor = UIColor.red.withAlphaComponent(0.5).cgColor
self.baseImageView.layer.addSublayer(colorLayer)
}
print("BoundingBox: \(observation.boundingBox)")
print("Payload: \(observation.payloadStringValue ?? "No payload")")
print("Symbology: \(observation.symbology.rawValue)")
}
}
let textCompletionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("Request failed: \(String(describing: error))")
return
}
guard let observations = request.results as? [VNRecognizedTextObservation] else {
return
}
observations.forEach {
observation in
let topCandidate = observation.topCandidates(1).first
print(topCandidate?.string ?? "No text recognized")
}
}
let barcodesRequest = VNDetectBarcodesRequest(completionHandler: barcodesCompletionHandler)
barcodesRequest.symbologies = [.ean13] // 如果只要扫描 EAN13 Barcode,可直接指定,提升效能
let textRequest = VNRecognizeTextRequest(completionHandler: textCompletionHandler)
textRequest.recognitionLevel = .accurate
textRequest.recognitionLanguages = ["en-US"]
DispatchQueue.global().async {
let handler = VNImageRequestHandler(url: fileURL, options: [:])
do {
try handler.perform([barcodesRequest, textRequest])
}
catch {
print("Request failed: \(error)")
}
}
}
输出结果:
1
2
3
4
94128s
ORGANIC
Pink Lady®
Produce of USh
iOS ≥ 18 Update Highlight:
1
2
3
4
let handler = ImageRequestHandler(fileURL)
// parameter pack syntax and we must wait for all requests to finish before we can use their results.
// let (barcodesObservation, textObservation, ...) = try await handler.perform(barcodesRequest, textRequest, ...)
let (barcodesObservation, textObservation) = try await handler.perform(barcodesRequest, textRequest)
iOS ≥ 18 performAll( )?changes=latest_minor){:target=”_blank”} 方法
前面的 perform(barcodesRequest, textRequest)
处理 Barcode 扫描跟文字扫描的方式需要等到两个 Request 都完成才能继续执行;iOS 18 开始提供新的 performAll()
方法,将回应方式改为串流,在收到其中一个 Reqeust 结果是就能做对应处理,例如扫描到 Barcode 就直接响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
if #available(iOS 18.0, *) {
// 新的使用 Swift 特性的 API
var barcodesRequest = DetectBarcodesRequest()
barcodesRequest.symbologies = [.ean13] // 如果只要扫描 EAN13 Barcode,可直接指定,提升效能
var textRequest = RecognizeTextRequest()
textRequest.recognitionLanguages = [.init(identifier: "zh-Hnat"), .init(identifier: "en-US")]
Task {
let handler = ImageRequestHandler(fileURL)
let observation = handler.performAll([barcodesRequest, textRequest] as [any VisionRequest])
for try await result in observation {
switch result {
case .detectBarcodes(_, let barcodesObservation):
if let observation = barcodesObservation.first {
DispatchQueue.main.async {
self.infoLabel.text = observation.payloadString
// 标记颜色 Layer
let colorLayer = CALayer()
// iOS >=18 新的座标转换 API toImageCoordinates
// 未经测试,实际可能还需要计算 ContentMode = AspectFit 的位移:
colorLayer.frame = observation.boundingBox.toImageCoordinates(self.baseImageView.frame.size, origin: .upperLeft)
colorLayer.backgroundColor = UIColor.red.withAlphaComponent(0.5).cgColor
self.baseImageView.layer.addSublayer(colorLayer)
}
print("BoundingBox: \(observation.boundingBox.cgRect)")
print("Payload: \(observation.payloadString ?? "No payload")")
print("Symbology: \(observation.symbology)")
}
case .recognizeText(_, let textObservation):
textObservation.forEach {
observation in
let topCandidate = observation.topCandidates(1).first
print(topCandidate?.string ?? "No text recognized")
}
default:
print("Unrecongnized result: \(result)")
}
}
}
}
Optimize with Swift Concurrency
假设我们有一个图片墙列表,每张图片都需要自动裁切出物件主体;这时候可以善用 Swift Concurrency 增加载入效率。
原始写法
1
2
3
4
5
6
7
8
9
10
11
func generateThumbnail(url: URL) async throws -> UIImage {
let request = GenerateAttentionBasedSaliencyImageRequest()
let saliencyObservation = try await request.perform(on: url)
return cropImage(url, to: saliencyObservation.salientObjects)
}
func generateAllThumbnails() async throws {
for image in images {
image.thumbnail = try await generateThumbnail(url: image.url)
}
}
一次只执行一个,效率、效能缓慢。
优化 (1) — TaskGroup Concurrency
1
2
3
4
5
6
7
8
func generateAllThumbnails() async throws {
try await withThrowingDiscardingTaskGroup { taskGroup in
for image in images {
image.thumbnail = try await generateThumbnail(url: image.url)
}
}
}
将每个 Task 都加入 TaskGroup Concurrency 执行。
问题:图片辨识、截图操作非常消耗记忆体性能,如果无节制狂加并行任务,可能造成使用者卡顿、OOM 闪退问题。
优化 (2) — TaskGroup Concurrency + 限制并行数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func generateAllThumbnails() async throws {
try await withThrowingDiscardingTaskGroup {
taskGroup in
// 最多执行数量不得超过 5
let maxImageTasks = min(5, images.count)
// 先填充 5 个 Task
for index in 0..<maxImageTasks {
taskGroup.addTask {
image[index].thumbnail = try await generateThumbnail(url: image[index].url)
}
}
var nextIndex = maxImageTasks
for try await _ in taskGroup {
// taskGroup 里 Task await 完成时...
// 检查 Index 是否到尾部
if nextIndex < images.count {
let image = images[nextIndex]
// 继续逐个填充 Task (将维持在最多 5 个)
taskGroup.addTask {
image.thumbnail = try await generateThumbnail(url: image.url)
}
nextIndex += 1
}
}
}
}
Update an existing Vision app
Vision 将在具备神经引擎的设备上移除对部分请求的 CPU 和 GPU 支持。在这些设备上,神经引擎是性能最好的选择。 可以使用
supportedComputeDevices()
API 进行检查移除所有 VN 前缀
VNXXRequest
,VNXXXObservation
->Reqeust
,Observation
使用 async/await 取代原本的 VNRequestCompletionHandler
直接使用
*Request.perform()
取代原本的VNImageRequestHandler.perform([VN*Request])
Wrap-up
为 Swift 语言特性新设计的 API
新的功能、方法都为 Swift Only, iOS ≥ 18 可用
新的图片评分功能、身体+手部动作追踪
Thanks!
KKday 招募工商
👉👉👉本次读书会分享源于 KKday App Team 组内每周技术分享活动, 目前团队也正在热情招募 Senior iOS Engineer ,有兴趣的朋友欢迎投递履历 。👈👈👈
参考资料
Discover Swift enhancements in the Vision framework
The Vision Framework API has been redesigned to leverage modern Swift features like concurrency, making it easier and faster to integrate a wide array of Vision algorithms into your app. We’ll tour the updated API and share sample code, along with best practices, to help you get the benefits of this framework with less coding effort. We’ll also demonstrate two new features: image aesthetics and holistic body pose.
Chapters
0:00 — Introduction
1:07 — New Vision API
1:47 — Get started with Vision
11:05 — Update an existing Vision app
13:46 — What’s new in Vision?
Vision framework Apple Developer Documentation
-
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。