@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,736 +1,614 @@
|
|
|
1
|
-
# Vision
|
|
1
|
+
# Vision request recipes
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Longer examples for the requests summarized in `../SKILL.md`. Swift-native
|
|
4
|
+
examples need iOS 18 or later unless marked otherwise; legacy examples are
|
|
5
|
+
labelled.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
|
-
- Complete Text Recognition Pipeline
|
|
9
|
-
- Face Detection with Landmarks
|
|
10
|
-
- Barcode Detection with All Symbologies
|
|
11
|
-
- Person Segmentation with Mask Application
|
|
12
|
-
- Instance Segmentation (iOS 18+)
|
|
13
|
-
- Image Classification
|
|
14
|
-
- Saliency Detection
|
|
15
|
-
- Rectangle Detection
|
|
16
|
-
- Horizon Detection
|
|
17
|
-
- Batch Processing Multiple Requests
|
|
18
|
-
- Video Frame Processing with CMSampleBuffer
|
|
19
|
-
- Object Tracking Across Video Frames
|
|
20
|
-
- Coordinate Normalization Utilities
|
|
21
|
-
- Performance Considerations
|
|
22
|
-
|
|
23
|
-
## Complete Text Recognition Pipeline
|
|
24
|
-
|
|
25
|
-
Full pipeline from image loading through text extraction with coordinate mapping.
|
|
26
8
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
- [Text recognition pipeline](#text-recognition-pipeline)
|
|
10
|
+
- [Faces with landmarks and quality](#faces-with-landmarks-and-quality)
|
|
11
|
+
- [Barcodes](#barcodes)
|
|
12
|
+
- [Person segmentation and background blur](#person-segmentation-and-background-blur)
|
|
13
|
+
- [Classification](#classification)
|
|
14
|
+
- [Saliency](#saliency)
|
|
15
|
+
- [Rectangles](#rectangles)
|
|
16
|
+
- [Horizon](#horizon)
|
|
17
|
+
- [Several requests on one image](#several-requests-on-one-image)
|
|
18
|
+
- [Camera frames from AVFoundation](#camera-frames-from-avfoundation)
|
|
19
|
+
- [Tracking across frames](#tracking-across-frames)
|
|
20
|
+
- [Coordinate helpers](#coordinate-helpers)
|
|
21
|
+
- [Performance](#performance)
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
final class TextRecognizer {
|
|
33
|
-
func recognizeText(in image: UIImage) async throws -> [RecognizedTextBlock] {
|
|
34
|
-
guard let cgImage = image.cgImage else {
|
|
35
|
-
throw TextRecognitionError.invalidImage
|
|
36
|
-
}
|
|
23
|
+
## Text recognition pipeline
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
request.recognitionLevel = .accurate
|
|
40
|
-
request.recognitionLanguages = [
|
|
41
|
-
Locale.Language(identifier: "en-US"),
|
|
42
|
-
]
|
|
43
|
-
request.usesLanguageCorrection = true
|
|
25
|
+
A reusable reader that turns a `UIImage` into lines with pixel-space frames.
|
|
44
26
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
height: cgImage.height
|
|
49
|
-
)
|
|
27
|
+
```swift
|
|
28
|
+
import UIKit
|
|
29
|
+
import Vision
|
|
50
30
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
let imageRect = VNImageRectForNormalizedRect(
|
|
55
|
-
boundingBox,
|
|
56
|
-
Int(imageSize.width),
|
|
57
|
-
Int(imageSize.height)
|
|
58
|
-
)
|
|
59
|
-
return RecognizedTextBlock(
|
|
60
|
-
text: candidate.string,
|
|
61
|
-
confidence: candidate.confidence,
|
|
62
|
-
boundingBox: imageRect
|
|
63
|
-
)
|
|
64
|
-
}
|
|
31
|
+
extension RecognizedTextObservation {
|
|
32
|
+
var bestString: String? {
|
|
33
|
+
topCandidates(1).first?.string
|
|
65
34
|
}
|
|
66
35
|
}
|
|
67
36
|
|
|
68
|
-
struct
|
|
37
|
+
struct ScannedLine: Sendable {
|
|
69
38
|
let text: String
|
|
70
39
|
let confidence: Float
|
|
71
|
-
let
|
|
40
|
+
let frame: CGRect
|
|
72
41
|
}
|
|
73
42
|
|
|
74
|
-
enum
|
|
75
|
-
case
|
|
43
|
+
enum ReceiptReaderError: Error {
|
|
44
|
+
case noBitmap
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
struct ReceiptReader {
|
|
48
|
+
func lines(in picture: UIImage) async throws -> [ScannedLine] {
|
|
49
|
+
guard let bitmap = picture.cgImage else { throw ReceiptReaderError.noBitmap }
|
|
50
|
+
var request = RecognizeTextRequest()
|
|
51
|
+
request.recognitionLevel = .accurate
|
|
52
|
+
request.recognitionLanguages = [Locale.Language(identifier: "en-CA")]
|
|
53
|
+
request.usesLanguageCorrection = true
|
|
54
|
+
let found = try await request.perform(on: bitmap)
|
|
55
|
+
return found.compactMap { observation in
|
|
56
|
+
guard let best = observation.topCandidates(1).first else {
|
|
57
|
+
return nil
|
|
58
|
+
}
|
|
59
|
+
let frame = VNImageRectForNormalizedRect(
|
|
60
|
+
observation.boundingBox.cgRect, bitmap.width, bitmap.height
|
|
61
|
+
)
|
|
62
|
+
return ScannedLine(text: best.string, confidence: best.confidence, frame: frame)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
76
65
|
}
|
|
77
66
|
```
|
|
78
67
|
|
|
79
|
-
|
|
68
|
+
The `bestString` extension is reused by later examples. `boundingBox` is a
|
|
69
|
+
`NormalizedRect`; pass `.cgRect` to the C helper, or call
|
|
70
|
+
`boundingBox.toImageCoordinates(size, origin: .upperLeft)` to get a top-left
|
|
71
|
+
rectangle in one step.
|
|
72
|
+
|
|
73
|
+
### Several languages and domain words
|
|
80
74
|
|
|
81
75
|
```swift
|
|
82
|
-
|
|
76
|
+
import Vision
|
|
77
|
+
|
|
78
|
+
func multilingualRequest() -> RecognizeTextRequest {
|
|
83
79
|
var request = RecognizeTextRequest()
|
|
84
|
-
request.recognitionLevel = .accurate
|
|
85
80
|
request.recognitionLanguages = [
|
|
81
|
+
Locale.Language(identifier: "nl-NL"),
|
|
86
82
|
Locale.Language(identifier: "en-US"),
|
|
87
|
-
Locale.Language(identifier: "
|
|
88
|
-
Locale.Language(identifier: "de-DE"),
|
|
83
|
+
Locale.Language(identifier: "es-ES")
|
|
89
84
|
]
|
|
90
|
-
request.
|
|
91
|
-
request
|
|
92
|
-
|
|
93
|
-
let observations = try await request.perform(on: cgImage)
|
|
94
|
-
return observations.compactMap { $0.topCandidates(1).first?.string }
|
|
85
|
+
request.customWords = ["visionOS", "TestFlight", "Xcode"]
|
|
86
|
+
return request
|
|
95
87
|
}
|
|
96
88
|
```
|
|
97
89
|
|
|
98
|
-
|
|
90
|
+
List languages in priority order. `customWords` keeps the corrector from
|
|
91
|
+
"fixing" product names and jargon.
|
|
92
|
+
|
|
93
|
+
### Fast recognition on camera frames
|
|
99
94
|
|
|
100
95
|
```swift
|
|
101
|
-
|
|
96
|
+
import CoreMedia
|
|
97
|
+
import Vision
|
|
98
|
+
|
|
99
|
+
func quickLines(in frame: CMSampleBuffer) async throws -> [String] {
|
|
102
100
|
var request = RecognizeTextRequest()
|
|
103
101
|
request.recognitionLevel = .fast
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let observations = try await request.perform(on: sampleBuffer)
|
|
107
|
-
return observations.compactMap { $0.topCandidates(1).first?.string }
|
|
102
|
+
let found = try await request.perform(on: frame)
|
|
103
|
+
return found.compactMap(\.bestString)
|
|
108
104
|
}
|
|
109
105
|
```
|
|
110
106
|
|
|
111
|
-
|
|
107
|
+
A `CMSampleBuffer` goes straight into `perform(on:)`; no conversion needed.
|
|
108
|
+
|
|
109
|
+
### Legacy version with a completion wrapper
|
|
112
110
|
|
|
113
111
|
```swift
|
|
114
112
|
import Vision
|
|
115
113
|
|
|
116
|
-
func
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
func legacyLines(in bitmap: CGImage) async throws -> [String] {
|
|
115
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
116
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
117
|
+
let request = VNRecognizeTextRequest { finished, failure in
|
|
118
|
+
if let failure {
|
|
119
|
+
continuation.resume(throwing: failure)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
let found = finished.results as? [VNRecognizedTextObservation] ?? []
|
|
123
|
+
continuation.resume(returning: found.compactMap { $0.topCandidates(1).first }.map(\.string))
|
|
124
|
+
}
|
|
125
|
+
request.recognitionLevel = .accurate
|
|
126
|
+
request.recognitionLanguages = ["en-US", "pt-BR"]
|
|
127
|
+
do {
|
|
128
|
+
try VNImageRequestHandler(cgImage: bitmap).perform([request])
|
|
129
|
+
} catch {
|
|
130
|
+
continuation.resume(throwing: error)
|
|
131
|
+
}
|
|
129
132
|
}
|
|
130
|
-
completion(strings)
|
|
131
|
-
}
|
|
132
|
-
request.recognitionLevel = .accurate
|
|
133
|
-
request.recognitionLanguages = ["en-US"]
|
|
134
|
-
request.usesLanguageCorrection = true
|
|
135
|
-
|
|
136
|
-
let handler = VNImageRequestHandler(cgImage: cgImage)
|
|
137
|
-
DispatchQueue.global(qos: .userInitiated).async {
|
|
138
|
-
try? handler.perform([request])
|
|
139
133
|
}
|
|
140
134
|
}
|
|
141
135
|
```
|
|
142
136
|
|
|
143
|
-
|
|
137
|
+
`VNRecognizeTextRequest` needs iOS 13. When `perform` throws, the completion
|
|
138
|
+
does not run, so the continuation is resumed exactly once either way.
|
|
139
|
+
|
|
140
|
+
## Faces with landmarks and quality
|
|
141
|
+
|
|
142
|
+
Run the three requests and join the results by position. Guard every index,
|
|
143
|
+
because the arrays are not guaranteed to line up in length.
|
|
144
144
|
|
|
145
145
|
```swift
|
|
146
146
|
import Vision
|
|
147
147
|
|
|
148
|
-
struct
|
|
149
|
-
let
|
|
150
|
-
let
|
|
151
|
-
let
|
|
152
|
-
let
|
|
153
|
-
let
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
struct FaceLandmarkPoints: Sendable {
|
|
157
|
-
let leftEye: [CGPoint]
|
|
158
|
-
let rightEye: [CGPoint]
|
|
159
|
-
let nose: [CGPoint]
|
|
160
|
-
let outerLips: [CGPoint]
|
|
161
|
-
let faceContour: [CGPoint]
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
func detectFaces(in cgImage: CGImage) async throws -> [DetectedFace] {
|
|
165
|
-
// Detect face rectangles
|
|
166
|
-
let rectRequest = DetectFaceRectanglesRequest()
|
|
167
|
-
let faces = try await rectRequest.perform(on: cgImage)
|
|
168
|
-
|
|
169
|
-
// Detect landmarks for detailed features
|
|
170
|
-
let landmarkRequest = DetectFaceLandmarksRequest()
|
|
171
|
-
let landmarkFaces = try await landmarkRequest.perform(on: cgImage)
|
|
172
|
-
|
|
173
|
-
// Detect capture quality for photo selection
|
|
174
|
-
let qualityRequest = DetectFaceCaptureQualityRequest()
|
|
175
|
-
let qualityFaces = try await qualityRequest.perform(on: cgImage)
|
|
176
|
-
|
|
177
|
-
return faces.enumerated().map { index, face in
|
|
178
|
-
let landmarks: FaceLandmarkPoints?
|
|
179
|
-
if index < landmarkFaces.count,
|
|
180
|
-
let lm = landmarkFaces[index].landmarks {
|
|
181
|
-
landmarks = FaceLandmarkPoints(
|
|
182
|
-
leftEye: lm.leftEye?.normalizedPoints ?? [],
|
|
183
|
-
rightEye: lm.rightEye?.normalizedPoints ?? [],
|
|
184
|
-
nose: lm.nose?.normalizedPoints ?? [],
|
|
185
|
-
outerLips: lm.outerLips?.normalizedPoints ?? [],
|
|
186
|
-
faceContour: lm.faceContour?.normalizedPoints ?? []
|
|
187
|
-
)
|
|
188
|
-
} else {
|
|
189
|
-
landmarks = nil
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let quality: FaceObservation.CaptureQuality?
|
|
193
|
-
if index < qualityFaces.count {
|
|
194
|
-
quality = qualityFaces[index].captureQuality
|
|
195
|
-
} else {
|
|
196
|
-
quality = nil
|
|
197
|
-
}
|
|
148
|
+
struct FaceReport: Sendable {
|
|
149
|
+
let box: NormalizedRect
|
|
150
|
+
let eyes: [NormalizedPoint]
|
|
151
|
+
let nose: [NormalizedPoint]
|
|
152
|
+
let mouth: [NormalizedPoint]
|
|
153
|
+
let outline: [NormalizedPoint]
|
|
154
|
+
let quality: FaceObservation.CaptureQuality?
|
|
155
|
+
}
|
|
198
156
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
157
|
+
func faceReports(in portrait: CGImage) async throws -> [FaceReport] {
|
|
158
|
+
let boxes = try await DetectFaceRectanglesRequest().perform(on: portrait)
|
|
159
|
+
let marks = try await DetectFaceLandmarksRequest().perform(on: portrait)
|
|
160
|
+
let scores = try await DetectFaceCaptureQualityRequest().perform(on: portrait)
|
|
161
|
+
|
|
162
|
+
return boxes.indices.map { i in
|
|
163
|
+
let landmarks = i < marks.count ? marks[i].landmarks : nil
|
|
164
|
+
return FaceReport(
|
|
165
|
+
box: boxes[i].boundingBox,
|
|
166
|
+
eyes: (landmarks?.leftEye.points ?? []) + (landmarks?.rightEye.points ?? []),
|
|
167
|
+
nose: landmarks?.nose.points ?? [],
|
|
168
|
+
mouth: landmarks?.outerLips.points ?? [],
|
|
169
|
+
outline: landmarks?.faceContour.points ?? [],
|
|
170
|
+
quality: i < scores.count ? scores[i].captureQuality : nil
|
|
205
171
|
)
|
|
206
172
|
}
|
|
207
173
|
}
|
|
208
174
|
```
|
|
209
175
|
|
|
210
|
-
|
|
176
|
+
`FaceObservation.CaptureQuality` exposes `score`. To match faces reliably
|
|
177
|
+
instead of by index, set `inputFaceObservations` on the landmark or quality
|
|
178
|
+
request to the rectangles you already found.
|
|
179
|
+
|
|
180
|
+
## Barcodes
|
|
211
181
|
|
|
212
182
|
```swift
|
|
213
183
|
import Vision
|
|
214
184
|
|
|
215
|
-
struct
|
|
185
|
+
struct ScannedCode: Sendable {
|
|
216
186
|
let payload: String?
|
|
217
|
-
let
|
|
218
|
-
let
|
|
187
|
+
let kind: BarcodeSymbology
|
|
188
|
+
let box: NormalizedRect
|
|
219
189
|
}
|
|
220
190
|
|
|
221
|
-
func
|
|
222
|
-
|
|
223
|
-
symbologies: [VNBarcodeSymbology] = [.qr, .ean13, .code128]
|
|
224
|
-
) async throws -> [DetectedBarcode] {
|
|
191
|
+
func codes(in label: CGImage,
|
|
192
|
+
accepting kinds: [BarcodeSymbology] = [.qr, .ean13, .code128]) async throws -> [ScannedCode] {
|
|
225
193
|
var request = DetectBarcodesRequest()
|
|
226
|
-
request.symbologies =
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return observations.map { barcode in
|
|
230
|
-
DetectedBarcode(
|
|
231
|
-
payload: barcode.payloadString,
|
|
232
|
-
symbology: barcode.symbology,
|
|
233
|
-
boundingBox: barcode.boundingBox
|
|
234
|
-
)
|
|
194
|
+
request.symbologies = kinds
|
|
195
|
+
return try await request.perform(on: label).map {
|
|
196
|
+
ScannedCode(payload: $0.payloadString, kind: $0.symbology, box: $0.boundingBox)
|
|
235
197
|
}
|
|
236
198
|
}
|
|
237
199
|
|
|
238
|
-
|
|
239
|
-
func detectQRCodes(in cgImage: CGImage) async throws -> [URL] {
|
|
200
|
+
func firstLink(in poster: CGImage) async throws -> URL? {
|
|
240
201
|
var request = DetectBarcodesRequest()
|
|
241
202
|
request.symbologies = [.qr]
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return observations.compactMap { barcode in
|
|
245
|
-
guard let payload = barcode.payloadString else { return nil }
|
|
246
|
-
return URL(string: payload)
|
|
247
|
-
}
|
|
203
|
+
let payload = try await request.perform(on: poster).first?.payloadString
|
|
204
|
+
return payload.flatMap(URL.init(string:))
|
|
248
205
|
}
|
|
249
206
|
```
|
|
250
207
|
|
|
251
|
-
|
|
208
|
+
The Swift-native type is `BarcodeSymbology`; the legacy and VisionKit APIs use
|
|
209
|
+
`VNBarcodeSymbology`. Both carry the same names.
|
|
252
210
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let linearSymbologies: [VNBarcodeSymbology] = [
|
|
256
|
-
.codabar, .code39, .code39Checksum, .code39FullASCII,
|
|
257
|
-
.code39FullASCIIChecksum, .code93, .code93i, .code128,
|
|
258
|
-
.ean8, .ean13, .gs1DataBar, .gs1DataBarExpanded,
|
|
259
|
-
.gs1DataBarLimited, .i2of5, .i2of5Checksum, .itf14,
|
|
260
|
-
.msiPlessey, .upce,
|
|
261
|
-
]
|
|
262
|
-
|
|
263
|
-
// 2D barcodes
|
|
264
|
-
let matrixSymbologies: [VNBarcodeSymbology] = [
|
|
265
|
-
.qr, .aztec, .dataMatrix, .pdf417, .microPDF417, .microQR,
|
|
266
|
-
]
|
|
267
|
-
```
|
|
211
|
+
Two-dimensional: `.aztec`, `.dataMatrix`, `.microPDF417`, `.microQR`,
|
|
212
|
+
`.pdf417`, `.qr`.
|
|
268
213
|
|
|
269
|
-
|
|
214
|
+
One-dimensional: `.codabar`, `.code128`, `.code39`, `.code39Checksum`,
|
|
215
|
+
`.code39FullASCII`, `.code39FullASCIIChecksum`, `.code93`, `.code93i`,
|
|
216
|
+
`.ean13`, `.ean8`, `.gs1DataBar`, `.gs1DataBarExpanded`, `.gs1DataBarLimited`,
|
|
217
|
+
`.i2of5`, `.i2of5Checksum`, `.itf14`, `.msiPlessey`, `.upce`.
|
|
270
218
|
|
|
271
|
-
|
|
219
|
+
`supportedSymbologies` on the request lists what the current revision can read.
|
|
220
|
+
|
|
221
|
+
## Person segmentation and background blur
|
|
222
|
+
|
|
223
|
+
The mask comes back smaller than the photo, so scale it to the photo's extent
|
|
224
|
+
before blending.
|
|
272
225
|
|
|
273
226
|
```swift
|
|
274
|
-
import Vision
|
|
275
227
|
import CoreImage
|
|
276
228
|
import CoreImage.CIFilterBuiltins
|
|
229
|
+
import Vision
|
|
277
230
|
|
|
278
|
-
func
|
|
279
|
-
|
|
280
|
-
request.qualityLevel = .
|
|
281
|
-
|
|
282
|
-
let
|
|
283
|
-
let
|
|
231
|
+
func personMask(for photo: CGImage) async throws -> CIImage {
|
|
232
|
+
let request = GeneratePersonSegmentationRequest()
|
|
233
|
+
request.qualityLevel = .balanced
|
|
234
|
+
let segmentation = try await request.perform(on: photo)
|
|
235
|
+
let mask = CIImage(cgImage: try segmentation.cgImage)
|
|
236
|
+
let sx = CGFloat(photo.width) / mask.extent.width
|
|
237
|
+
let sy = CGFloat(photo.height) / mask.extent.height
|
|
238
|
+
return mask.transformed(by: CGAffineTransform(scaleX: sx, y: sy))
|
|
239
|
+
}
|
|
284
240
|
|
|
285
|
-
|
|
286
|
-
let
|
|
241
|
+
func portraitBlur(_ photo: CGImage, blurRadius: Float = 18) async throws -> CIImage {
|
|
242
|
+
let original = CIImage(cgImage: photo)
|
|
243
|
+
let mask = try await personMask(for: photo)
|
|
287
244
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
let
|
|
292
|
-
scaleX: scaleX, y: scaleY
|
|
293
|
-
))
|
|
245
|
+
let blur = CIFilter.gaussianBlur()
|
|
246
|
+
blur.inputImage = original
|
|
247
|
+
blur.radius = blurRadius
|
|
248
|
+
let softened = blur.outputImage?.cropped(to: original.extent) ?? original
|
|
294
249
|
|
|
295
|
-
|
|
250
|
+
let blend = CIFilter.blendWithMask()
|
|
251
|
+
blend.inputImage = original
|
|
252
|
+
blend.backgroundImage = softened
|
|
253
|
+
blend.maskImage = mask
|
|
254
|
+
return blend.outputImage ?? original
|
|
296
255
|
}
|
|
256
|
+
```
|
|
297
257
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
let mask = try await segmentPerson(in: cgImage)
|
|
301
|
-
let original = CIImage(cgImage: cgImage)
|
|
302
|
-
|
|
303
|
-
let blurFilter = CIFilter.gaussianBlur()
|
|
304
|
-
blurFilter.inputImage = original
|
|
305
|
-
blurFilter.radius = Float(blurRadius)
|
|
306
|
-
guard let blurredImage = blurFilter.outputImage else {
|
|
307
|
-
throw SegmentationError.noMask
|
|
308
|
-
}
|
|
258
|
+
The person stays sharp because the original is the input image and the blurred
|
|
259
|
+
copy is the background.
|
|
309
260
|
|
|
310
|
-
|
|
311
|
-
blendFilter.inputImage = original // foreground (person)
|
|
312
|
-
blendFilter.backgroundImage = blurredImage // blurred background
|
|
313
|
-
blendFilter.maskImage = mask
|
|
261
|
+
Legacy mask (iOS 15+):
|
|
314
262
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
return result
|
|
319
|
-
}
|
|
263
|
+
```swift
|
|
264
|
+
import Vision
|
|
320
265
|
|
|
321
|
-
enum
|
|
322
|
-
case
|
|
266
|
+
enum MaskError: Error {
|
|
267
|
+
case empty
|
|
323
268
|
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Legacy API (Pre-iOS 18)
|
|
327
269
|
|
|
328
|
-
|
|
329
|
-
func segmentPersonLegacy(in cgImage: CGImage) throws -> CVPixelBuffer {
|
|
270
|
+
func legacyPersonMask(for photo: CGImage) throws -> CVPixelBuffer {
|
|
330
271
|
let request = VNGeneratePersonSegmentationRequest()
|
|
331
272
|
request.qualityLevel = .accurate
|
|
332
273
|
request.outputPixelFormat = kCVPixelFormatType_OneComponent8
|
|
333
|
-
|
|
334
|
-
let
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
guard let maskBuffer = request.results?.first?.pixelBuffer else {
|
|
338
|
-
throw SegmentationError.noMask
|
|
274
|
+
try VNImageRequestHandler(cgImage: photo).perform([request])
|
|
275
|
+
guard let first = request.results?.first else {
|
|
276
|
+
throw MaskError.empty
|
|
339
277
|
}
|
|
340
|
-
return
|
|
278
|
+
return first.pixelBuffer
|
|
341
279
|
}
|
|
342
280
|
```
|
|
343
281
|
|
|
344
|
-
###
|
|
345
|
-
|
|
346
|
-
Separate masks per person for individual effects.
|
|
282
|
+
### One mask per person
|
|
347
283
|
|
|
348
284
|
```swift
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
let
|
|
353
|
-
|
|
354
|
-
let indices = observation.allInstances
|
|
355
|
-
return try indices.map { index in
|
|
356
|
-
try observation.generateMask(forInstances: IndexSet(integer: index))
|
|
357
|
-
}
|
|
285
|
+
import Vision
|
|
286
|
+
|
|
287
|
+
func individualMasks(in groupShot: CGImage) async throws -> [CVPixelBuffer] {
|
|
288
|
+
guard let result = try await GeneratePersonInstanceMaskRequest().perform(on: groupShot) else { return [] }
|
|
289
|
+
return try result.allInstances.map { try result.generateMask(for: IndexSet(integer: $0)) }
|
|
358
290
|
}
|
|
359
|
-
```
|
|
360
291
|
|
|
361
|
-
|
|
362
|
-
// Legacy API (iOS 17+)
|
|
363
|
-
func segmentIndividualPeopleLegacy(in cgImage: CGImage) throws -> [CVPixelBuffer] {
|
|
292
|
+
func legacyIndividualMasks(in groupShot: CGImage) throws -> [CVPixelBuffer] {
|
|
364
293
|
let request = VNGeneratePersonInstanceMaskRequest()
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
return try indices.map { index in
|
|
372
|
-
try result.generateMask(forInstances: IndexSet(integer: index))
|
|
294
|
+
try VNImageRequestHandler(cgImage: groupShot).perform([request])
|
|
295
|
+
guard let people = request.results?.first else {
|
|
296
|
+
return []
|
|
297
|
+
}
|
|
298
|
+
return try people.allInstances.map { person in
|
|
299
|
+
try people.generateMask(forInstances: [person])
|
|
373
300
|
}
|
|
374
301
|
}
|
|
375
302
|
```
|
|
376
303
|
|
|
377
|
-
|
|
304
|
+
The legacy request needs iOS 17. The Swift-native observation also offers
|
|
305
|
+
`generateMaskedImage(for:imageFrom:croppedToInstancesExtent:)` when you want the
|
|
306
|
+
cut-out pixels rather than a mask.
|
|
307
|
+
|
|
308
|
+
## Classification
|
|
378
309
|
|
|
379
310
|
```swift
|
|
380
311
|
import Vision
|
|
381
312
|
|
|
382
|
-
func
|
|
383
|
-
let
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return observations.prefix(maxResults).map { observation in
|
|
387
|
-
(observation.identifier, observation.confidence)
|
|
388
|
-
}
|
|
313
|
+
func sceneTags(for picture: CGImage, limit: Int = 5) async throws -> [(label: String, score: Float)] {
|
|
314
|
+
let labels = try await ClassifyImageRequest().perform(on: picture)
|
|
315
|
+
return labels.prefix(limit).map { ($0.identifier, $0.confidence) }
|
|
389
316
|
}
|
|
390
317
|
```
|
|
391
318
|
|
|
392
|
-
## Saliency
|
|
319
|
+
## Saliency
|
|
393
320
|
|
|
394
|
-
|
|
321
|
+
Attention-based saliency predicts where people look. Objectness-based saliency
|
|
322
|
+
marks things that look like distinct objects.
|
|
395
323
|
|
|
396
324
|
```swift
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
let
|
|
401
|
-
|
|
402
|
-
return saliency.salientObjects?.map(\.boundingBox) ?? []
|
|
325
|
+
import Vision
|
|
326
|
+
|
|
327
|
+
func focusRegions(in picture: CGImage) async throws -> [NormalizedRect] {
|
|
328
|
+
let heat = try await GenerateAttentionBasedSaliencyImageRequest().perform(on: picture)
|
|
329
|
+
return heat.salientObjects.map(\.boundingBox)
|
|
403
330
|
}
|
|
404
331
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
let results = try await request.perform(on: cgImage)
|
|
409
|
-
guard let saliency = results.first else { return [] }
|
|
410
|
-
return saliency.salientObjects?.map(\.boundingBox) ?? []
|
|
332
|
+
func objectRegions(in picture: CGImage) async throws -> [NormalizedRect] {
|
|
333
|
+
let heat = try await GenerateObjectnessBasedSaliencyImageRequest().perform(on: picture)
|
|
334
|
+
return heat.salientObjects.map(\.boundingBox)
|
|
411
335
|
}
|
|
412
336
|
```
|
|
413
337
|
|
|
414
|
-
|
|
338
|
+
`salientObjects` is a non-optional array in the Swift-native API; the legacy
|
|
339
|
+
`VNSaliencyImageObservation.salientObjects` is optional. The heat map itself is
|
|
340
|
+
`heat.heatMap`.
|
|
415
341
|
|
|
416
|
-
|
|
342
|
+
## Rectangles
|
|
417
343
|
|
|
418
344
|
```swift
|
|
419
|
-
|
|
420
|
-
var request = DetectRectanglesRequest()
|
|
421
|
-
request.minimumAspectRatio = 0.3
|
|
422
|
-
request.maximumAspectRatio = 1.0
|
|
423
|
-
request.minimumSize = 0.1
|
|
424
|
-
request.maximumObservations = 5
|
|
345
|
+
import Vision
|
|
425
346
|
|
|
426
|
-
|
|
427
|
-
|
|
347
|
+
func cardOutlines(in picture: CGImage) async throws -> [NormalizedRect] {
|
|
348
|
+
var request = DetectRectanglesRequest()
|
|
349
|
+
request.minimumAspectRatio = 0.5
|
|
350
|
+
request.maximumAspectRatio = 0.8
|
|
351
|
+
request.minimumSize = 0.2
|
|
352
|
+
request.maximumObservations = 3
|
|
353
|
+
return try await request.perform(on: picture).map(\.boundingBox)
|
|
428
354
|
}
|
|
429
355
|
```
|
|
430
356
|
|
|
431
|
-
|
|
357
|
+
Aspect-ratio bounds filter shapes (a payment card is about 0.63), `minimumSize`
|
|
358
|
+
is a fraction of the image, and `maximumObservations` caps the result count.
|
|
359
|
+
Each `RectangleObservation` also exposes its four corners.
|
|
432
360
|
|
|
433
|
-
|
|
361
|
+
## Horizon
|
|
434
362
|
|
|
435
363
|
```swift
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
364
|
+
import Vision
|
|
365
|
+
|
|
366
|
+
func tiltCorrection(for picture: CGImage) async throws -> CGFloat? {
|
|
367
|
+
guard let horizon = try await DetectHorizonRequest().perform(on: picture) else { return nil }
|
|
368
|
+
return CGFloat(-horizon.angle.converted(to: .radians).value)
|
|
440
369
|
}
|
|
441
370
|
```
|
|
442
371
|
|
|
443
|
-
|
|
372
|
+
`angle` is a `Measurement<UnitAngle>`; convert it before using it as a
|
|
373
|
+
rotation. `horizon.transform(for:)` gives a ready-made affine transform.
|
|
374
|
+
|
|
375
|
+
## Several requests on one image
|
|
444
376
|
|
|
445
|
-
|
|
377
|
+
Swift-native requests are independent values, so run them concurrently:
|
|
446
378
|
|
|
447
379
|
```swift
|
|
448
|
-
|
|
449
|
-
async let textResults = {
|
|
450
|
-
var req = RecognizeTextRequest()
|
|
451
|
-
req.recognitionLevel = .accurate
|
|
452
|
-
return try await req.perform(on: cgImage)
|
|
453
|
-
}()
|
|
454
|
-
|
|
455
|
-
async let faceResults = {
|
|
456
|
-
let req = DetectFaceRectanglesRequest()
|
|
457
|
-
return try await req.perform(on: cgImage)
|
|
458
|
-
}()
|
|
459
|
-
|
|
460
|
-
async let barcodeResults = {
|
|
461
|
-
var req = DetectBarcodesRequest()
|
|
462
|
-
req.symbologies = [.qr, .ean13]
|
|
463
|
-
return try await req.perform(on: cgImage)
|
|
464
|
-
}()
|
|
465
|
-
|
|
466
|
-
let text = try await textResults
|
|
467
|
-
let faces = try await faceResults
|
|
468
|
-
let barcodes = try await barcodeResults
|
|
469
|
-
|
|
470
|
-
return ImageAnalysisResult(
|
|
471
|
-
recognizedText: text.compactMap { $0.topCandidates(1).first?.string },
|
|
472
|
-
faceCount: faces.count,
|
|
473
|
-
barcodePayloads: barcodes.compactMap(\.payloadString)
|
|
474
|
-
)
|
|
475
|
-
}
|
|
380
|
+
import Vision
|
|
476
381
|
|
|
477
|
-
struct
|
|
478
|
-
let
|
|
382
|
+
struct PhotoDigest: Sendable {
|
|
383
|
+
let lines: [String]
|
|
479
384
|
let faceCount: Int
|
|
480
|
-
let
|
|
385
|
+
let links: [String]
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
func digest(of picture: CGImage) async throws -> PhotoDigest {
|
|
389
|
+
async let text = RecognizeTextRequest().perform(on: picture)
|
|
390
|
+
async let faces = DetectFaceRectanglesRequest().perform(on: picture)
|
|
391
|
+
async let codes = DetectBarcodesRequest().perform(on: picture)
|
|
392
|
+
|
|
393
|
+
return PhotoDigest(
|
|
394
|
+
lines: try await text.compactMap(\.bestString),
|
|
395
|
+
faceCount: try await faces.count,
|
|
396
|
+
links: try await codes.compactMap(\.payloadString)
|
|
397
|
+
)
|
|
481
398
|
}
|
|
482
399
|
```
|
|
483
400
|
|
|
484
|
-
|
|
401
|
+
`ImageRequestHandler(picture).perform(a, b, c)` is an alternative that shares
|
|
402
|
+
one decoded image across requests and returns a tuple of typed results.
|
|
485
403
|
|
|
486
|
-
With the legacy API,
|
|
404
|
+
With the legacy API, give one handler all requests at once:
|
|
487
405
|
|
|
488
406
|
```swift
|
|
489
|
-
|
|
490
|
-
let textRequest = VNRecognizeTextRequest { request, error in
|
|
491
|
-
// Handle text results
|
|
492
|
-
}
|
|
493
|
-
let faceRequest = VNDetectFaceRectanglesRequest { request, error in
|
|
494
|
-
// Handle face results
|
|
495
|
-
}
|
|
496
|
-
let barcodeRequest = VNDetectBarcodesRequest { request, error in
|
|
497
|
-
// Handle barcode results
|
|
498
|
-
}
|
|
407
|
+
import Vision
|
|
499
408
|
|
|
500
|
-
|
|
501
|
-
|
|
409
|
+
func legacyDigest(of picture: CGImage) throws -> (VNRecognizeTextRequest, VNDetectFaceRectanglesRequest, VNDetectBarcodesRequest) {
|
|
410
|
+
let text = VNRecognizeTextRequest()
|
|
411
|
+
let faces = VNDetectFaceRectanglesRequest()
|
|
412
|
+
let codes = VNDetectBarcodesRequest()
|
|
413
|
+
try VNImageRequestHandler(cgImage: picture).perform([text, faces, codes])
|
|
414
|
+
return (text, faces, codes)
|
|
502
415
|
}
|
|
503
416
|
```
|
|
504
417
|
|
|
505
|
-
##
|
|
418
|
+
## Camera frames from AVFoundation
|
|
506
419
|
|
|
507
|
-
|
|
420
|
+
The capture delegate runs on its own serial queue and hands out non-Sendable
|
|
421
|
+
`CMSampleBuffer` values. Forward the newest frame into an `AsyncStream` and do
|
|
422
|
+
the Vision work in a consumer task; the stream's buffering policy drops frames
|
|
423
|
+
while one is being processed.
|
|
508
424
|
|
|
509
425
|
```swift
|
|
510
426
|
import AVFoundation
|
|
511
427
|
import Vision
|
|
512
428
|
|
|
513
|
-
|
|
514
|
-
|
|
429
|
+
struct CameraFrame: @unchecked Sendable {
|
|
430
|
+
let sample: CMSampleBuffer
|
|
431
|
+
}
|
|
515
432
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
433
|
+
final class FramePump: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
434
|
+
let frames: AsyncStream<CameraFrame>
|
|
435
|
+
private let sink: AsyncStream<CameraFrame>.Continuation
|
|
436
|
+
private let output = AVCaptureVideoDataOutput()
|
|
437
|
+
private let queue = DispatchQueue(label: "camera.frames", qos: .userInitiated)
|
|
438
|
+
|
|
439
|
+
override init() {
|
|
440
|
+
let pair = AsyncStream.makeStream(of: CameraFrame.self, bufferingPolicy: .bufferingNewest(1))
|
|
441
|
+
frames = pair.stream
|
|
442
|
+
sink = pair.continuation
|
|
443
|
+
super.init()
|
|
444
|
+
}
|
|
520
445
|
|
|
446
|
+
func attach(to session: AVCaptureSession) {
|
|
447
|
+
output.setSampleBufferDelegate(self, queue: queue)
|
|
448
|
+
output.alwaysDiscardsLateVideoFrames = true
|
|
521
449
|
if session.canAddOutput(output) {
|
|
522
450
|
session.addOutput(output)
|
|
523
451
|
}
|
|
524
452
|
}
|
|
525
453
|
|
|
526
|
-
func captureOutput(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
await MainActor.run {
|
|
541
|
-
// Update UI with recognized strings
|
|
542
|
-
}
|
|
543
|
-
} catch {
|
|
544
|
-
// Handle error
|
|
545
|
-
}
|
|
546
|
-
}
|
|
454
|
+
func captureOutput(_ output: AVCaptureOutput,
|
|
455
|
+
didOutput sampleBuffer: CMSampleBuffer,
|
|
456
|
+
from connection: AVCaptureConnection) {
|
|
457
|
+
sink.yield(CameraFrame(sample: sampleBuffer))
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
func readLive(from pump: FramePump, show: @escaping @MainActor ([String]) -> Void) async {
|
|
462
|
+
var request = RecognizeTextRequest()
|
|
463
|
+
request.recognitionLevel = .fast
|
|
464
|
+
for await frame in pump.frames {
|
|
465
|
+
guard let found = try? await request.perform(on: frame.sample) else { continue }
|
|
466
|
+
let lines = found.compactMap(\.bestString)
|
|
467
|
+
await MainActor.run { show(lines) }
|
|
547
468
|
}
|
|
548
469
|
}
|
|
549
470
|
```
|
|
550
471
|
|
|
551
|
-
|
|
472
|
+
The `@unchecked Sendable` wrapper is acceptable here because the frame is
|
|
473
|
+
never touched again after it is yielded.
|
|
552
474
|
|
|
553
|
-
|
|
475
|
+
## Tracking across frames
|
|
554
476
|
|
|
555
|
-
|
|
556
|
-
|
|
477
|
+
The Swift-native tracker keeps its own history, so there is no sequence
|
|
478
|
+
handler to manage. Keep the request object for as long as you track.
|
|
557
479
|
|
|
558
480
|
```swift
|
|
559
481
|
import Vision
|
|
560
482
|
|
|
561
|
-
final class
|
|
483
|
+
final class SubjectFollower {
|
|
562
484
|
private var request: TrackObjectRequest?
|
|
485
|
+
private let minimumConfidence: Float = 0.3
|
|
563
486
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
let observation = DetectedObjectObservation(boundingBox: boundingBox)
|
|
567
|
-
var req = TrackObjectRequest(observation: observation)
|
|
568
|
-
req.trackingLevel = .accurate
|
|
569
|
-
request = req
|
|
487
|
+
func begin(at box: NormalizedRect) {
|
|
488
|
+
request = TrackObjectRequest(detectedObject: DetectedObjectObservation(boundingBox: box))
|
|
570
489
|
}
|
|
571
490
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
guard
|
|
575
|
-
|
|
576
|
-
let results = try await req.perform(on: pixelBuffer)
|
|
577
|
-
guard let tracked = results.first, tracked.confidence > 0.3 else {
|
|
578
|
-
request = nil
|
|
491
|
+
func next(_ frame: CVPixelBuffer) async throws -> NormalizedRect? {
|
|
492
|
+
guard let request else { return nil }
|
|
493
|
+
guard let hit = try await request.perform(on: frame), hit.confidence > minimumConfidence else {
|
|
494
|
+
end()
|
|
579
495
|
return nil
|
|
580
496
|
}
|
|
581
|
-
|
|
582
|
-
request = req // preserve stateful tracking context
|
|
583
|
-
return tracked.boundingBox
|
|
497
|
+
return hit.boundingBox
|
|
584
498
|
}
|
|
585
499
|
|
|
586
|
-
func
|
|
500
|
+
func end() {
|
|
587
501
|
request = nil
|
|
588
502
|
}
|
|
589
503
|
}
|
|
590
504
|
```
|
|
591
505
|
|
|
592
|
-
|
|
506
|
+
Legacy tracker with an explicit sequence handler:
|
|
593
507
|
|
|
594
508
|
```swift
|
|
595
|
-
|
|
596
|
-
private var sequenceHandler = VNSequenceRequestHandler()
|
|
597
|
-
private var currentObservation: VNDetectedObjectObservation?
|
|
598
|
-
|
|
599
|
-
func startTracking(boundingBox: CGRect) {
|
|
600
|
-
currentObservation = VNDetectedObjectObservation(boundingBox: boundingBox)
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
func track(in pixelBuffer: CVPixelBuffer) throws -> CGRect? {
|
|
604
|
-
guard let observation = currentObservation else { return nil }
|
|
509
|
+
import Vision
|
|
605
510
|
|
|
606
|
-
|
|
607
|
-
|
|
511
|
+
final class LegacySubjectFollower {
|
|
512
|
+
private let sequence = VNSequenceRequestHandler()
|
|
513
|
+
private var last: VNDetectedObjectObservation?
|
|
608
514
|
|
|
609
|
-
|
|
515
|
+
func begin(at box: CGRect) {
|
|
516
|
+
last = VNDetectedObjectObservation(boundingBox: box)
|
|
517
|
+
}
|
|
610
518
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
519
|
+
func next(_ frame: CVPixelBuffer) throws -> CGRect? {
|
|
520
|
+
guard let seed = last else { return nil }
|
|
521
|
+
let request = VNTrackObjectRequest(detectedObjectObservation: seed)
|
|
522
|
+
request.trackingLevel = .accurate
|
|
523
|
+
try sequence.perform([request], on: frame)
|
|
524
|
+
guard let hit = request.results?.first as? VNDetectedObjectObservation, hit.confidence > 0.3 else {
|
|
525
|
+
last = nil
|
|
614
526
|
return nil
|
|
615
527
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
return result.boundingBox
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
func stopTracking() {
|
|
622
|
-
currentObservation = nil
|
|
528
|
+
last = hit
|
|
529
|
+
return hit.boundingBox
|
|
623
530
|
}
|
|
624
531
|
}
|
|
625
532
|
```
|
|
626
533
|
|
|
627
|
-
## Coordinate
|
|
534
|
+
## Coordinate helpers
|
|
535
|
+
|
|
536
|
+
Normalized values run from 0 to 1 with the origin at the lower left.
|
|
628
537
|
|
|
629
|
-
|
|
630
|
-
|
|
538
|
+
- `VNImageRectForNormalizedRect(rect, width, height)` scales a normalized
|
|
539
|
+
rectangle to pixels.
|
|
540
|
+
- `VNImagePointForNormalizedPoint(point, width, height)` does the same for a
|
|
541
|
+
point.
|
|
542
|
+
- Neither flips the y axis; UIKit drawing still needs a flip.
|
|
543
|
+
- Swift-native `NormalizedRect` and `NormalizedPoint` have
|
|
544
|
+
`toImageCoordinates(_:origin:)` which scales and, with `.upperLeft`, flips.
|
|
631
545
|
|
|
632
546
|
```swift
|
|
633
547
|
import Vision
|
|
634
|
-
import UIKit
|
|
635
|
-
|
|
636
|
-
enum VisionCoordinateConverter {
|
|
637
|
-
/// Convert normalized Vision rect to image-pixel coordinates
|
|
638
|
-
static func toImageCoordinates(
|
|
639
|
-
_ normalizedRect: CGRect,
|
|
640
|
-
imageWidth: Int,
|
|
641
|
-
imageHeight: Int
|
|
642
|
-
) -> CGRect {
|
|
643
|
-
VNImageRectForNormalizedRect(normalizedRect, imageWidth, imageHeight)
|
|
644
|
-
}
|
|
645
548
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
/// Convert Vision rect (bottom-left origin) to UIKit rect (top-left origin)
|
|
656
|
-
static func toUIKitCoordinates(
|
|
657
|
-
_ normalizedRect: CGRect,
|
|
658
|
-
viewSize: CGSize
|
|
659
|
-
) -> CGRect {
|
|
660
|
-
let imageRect = VNImageRectForNormalizedRect(
|
|
661
|
-
normalizedRect,
|
|
662
|
-
Int(viewSize.width),
|
|
663
|
-
Int(viewSize.height)
|
|
664
|
-
)
|
|
665
|
-
// Flip Y axis: Vision origin is bottom-left, UIKit is top-left
|
|
666
|
-
return CGRect(
|
|
667
|
-
x: imageRect.origin.x,
|
|
668
|
-
y: viewSize.height - imageRect.origin.y - imageRect.height,
|
|
669
|
-
width: imageRect.width,
|
|
670
|
-
height: imageRect.height
|
|
671
|
-
)
|
|
672
|
-
}
|
|
549
|
+
func viewRect(for normalized: CGRect, in viewSize: CGSize) -> CGRect {
|
|
550
|
+
let pixelWidth = Int(viewSize.width)
|
|
551
|
+
let pixelHeight = Int(viewSize.height)
|
|
552
|
+
let scaled = VNImageRectForNormalizedRect(normalized, pixelWidth, pixelHeight)
|
|
553
|
+
return CGRect(x: scaled.minX,
|
|
554
|
+
y: viewSize.height - scaled.minY - scaled.height,
|
|
555
|
+
width: scaled.width,
|
|
556
|
+
height: scaled.height)
|
|
557
|
+
}
|
|
673
558
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
viewSize: CGSize
|
|
678
|
-
) -> [CGPoint] {
|
|
679
|
-
normalizedPoints.map { point in
|
|
680
|
-
CGPoint(
|
|
681
|
-
x: point.x * viewSize.width,
|
|
682
|
-
y: (1.0 - point.y) * viewSize.height // flip Y
|
|
683
|
-
)
|
|
684
|
-
}
|
|
559
|
+
func viewPoints(for normalized: [CGPoint], in viewSize: CGSize) -> [CGPoint] {
|
|
560
|
+
normalized.map { p in
|
|
561
|
+
CGPoint(x: p.x * viewSize.width, y: viewSize.height - p.y * viewSize.height)
|
|
685
562
|
}
|
|
686
563
|
}
|
|
687
564
|
```
|
|
688
565
|
|
|
689
|
-
## Performance
|
|
566
|
+
## Performance
|
|
690
567
|
|
|
691
|
-
|
|
568
|
+
Ballpark costs; measure on the oldest device you support.
|
|
692
569
|
|
|
693
|
-
|
|
|
570
|
+
| Scenario | Level | Typical cost |
|
|
694
571
|
|---|---|---|
|
|
695
|
-
| Live camera
|
|
696
|
-
| Photo library scan | `.accurate` |
|
|
697
|
-
| Batch document OCR | `.accurate` |
|
|
698
|
-
| Barcode
|
|
572
|
+
| Live camera text | `.fast` | about 30 ms per frame |
|
|
573
|
+
| Photo library scan | `.accurate` | 200 to 500 ms per image |
|
|
574
|
+
| Batch document OCR | `.accurate` | 200 to 500 ms per page |
|
|
575
|
+
| Barcode scanning | `.fast` or `.balanced` | 15 to 50 ms per frame |
|
|
699
576
|
|
|
700
|
-
|
|
577
|
+
Memory:
|
|
701
578
|
|
|
702
|
-
- Reuse `VNSequenceRequestHandler`
|
|
703
|
-
-
|
|
704
|
-
- Release `CVPixelBuffer` references promptly after processing
|
|
705
|
-
- Use `autoreleasepool` in tight loops processing many images
|
|
579
|
+
- Reuse a single `VNSequenceRequestHandler` for a whole frame sequence.
|
|
580
|
+
- Work through large photo sets one image at a time so memory does not spike:
|
|
706
581
|
|
|
707
582
|
```swift
|
|
708
|
-
|
|
709
|
-
var allResults: [[String]] = []
|
|
583
|
+
import Vision
|
|
710
584
|
|
|
711
|
-
|
|
585
|
+
func transcribeAll(_ pages: [CGImage]) async throws -> [[String]] {
|
|
586
|
+
var output: [[String]] = []
|
|
587
|
+
for page in pages {
|
|
712
588
|
var request = RecognizeTextRequest()
|
|
713
589
|
request.recognitionLevel = .accurate
|
|
714
|
-
let
|
|
715
|
-
|
|
716
|
-
allResults.append(result)
|
|
590
|
+
let found = try await request.perform(on: page)
|
|
591
|
+
output.append(found.compactMap(\.bestString))
|
|
717
592
|
}
|
|
718
|
-
return
|
|
593
|
+
return output
|
|
719
594
|
}
|
|
720
595
|
```
|
|
721
596
|
|
|
722
|
-
|
|
597
|
+
- Let go of `CVPixelBuffer` references as soon as you are done; wrap tight
|
|
598
|
+
synchronous loops over legacy handlers in `autoreleasepool`.
|
|
723
599
|
|
|
724
|
-
|
|
725
|
-
- Legacy API: create `VNImageRequestHandler` and call `perform` on a background queue
|
|
726
|
-
- Never block the main thread with Vision requests
|
|
727
|
-
- `VNSequenceRequestHandler` is not thread-safe -- use from a single serial queue
|
|
600
|
+
Threading:
|
|
728
601
|
|
|
729
|
-
|
|
602
|
+
- Swift-native `perform(on:)` is async and can be awaited from any context.
|
|
603
|
+
- Legacy `perform` is synchronous; call it from a background queue, never the
|
|
604
|
+
main thread.
|
|
605
|
+
- `VNSequenceRequestHandler` has no internal locking; confine each instance to
|
|
606
|
+
a single serial queue.
|
|
730
607
|
|
|
731
|
-
|
|
732
|
-
and reuse them across calls -- just create a fresh one each time.
|
|
608
|
+
Reuse:
|
|
733
609
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
610
|
+
- Swift-native request structs are cheap values; build a fresh one per job
|
|
611
|
+
rather than caching it. Stateful request classes are the exception: keep
|
|
612
|
+
them for the sequence they track.
|
|
613
|
+
- A `VNImageRequestHandler` belongs to one image; create one per image. A
|
|
614
|
+
`VNSequenceRequestHandler` is meant to be reused across a sequence.
|