iOS Vision framework x WWDC 24 Vision frameworkのSwift強化を発見するセッション
Vision framework の機能レビュー & iOS 18 新 Swift API 試用

写真提供: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 と合わせて、新しい Swift の特徴を活かしつつ改めて復習します。
CoreML
Apple にはもう一つの Framework である CoreML があります。これはオンデバイスチップを使った機械学習フレームワークで、自分で認識したい物体やドキュメントのモデルを訓練し、モデルをアプリに組み込んで直接使うことができます。興味がある方はぜひ試してみてください。(例:リアルタイム記事分類 、リアルタイムの 迷惑メッセージ検出 …)
p.s.
Vision :主に顔認識、バーコード検出、テキスト認識などの画像解析タスクに使用されます。静止画や動画の視覚コンテンツを処理・分析する強力なAPIを提供します。
VisionKit :ドキュメントスキャンに特化したフレームワークです。スキャナービューコントローラーを提供し、文書をスキャンして高品質なPDFや画像を生成できます。
Vision framework は M1 機種のシミュレーター上で動作せず、実機でのみテスト可能です。シミュレーター環境で実行すると Could not create Espresso context エラーが発生します。公式フォーラムの議論を調べましたが、解決策は見つかりませんでした。(公式フォーラム)
実機の iOS 18 デバイスが手元にないため、本記事のすべての実行結果は旧(iOS 18 以前)の書き方によるものです;新しい書き方でエラーが出た場合はコメントでお知らせください。
WWDC 2024 — VisionフレームワークにおけるSwiftの強化を発見する

VisionフレームワークにおけるSwiftの強化を発見する
本記事は WWDC 24 — Discover Swift enhancements in the Vision framework セッションの共有ノートと、自分で試した感想です。
はじめに — Vision framework の特徴
顔認識、輪郭認識


画像内テキスト認識
iOS 18 までに、18言語をサポートしています。

// サポートされている言語リスト
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で言及されたスウェーデン語は見当たらず、まだ対応していないかデバイスの地域と言語設定に依存している可能性があります
動的な動作キャプチャ


-
人や物体の動的追跡が可能です
-
ジェスチャーキャプチャによる空中サイン機能を実現
Visionの新機能(iOS 18)— 画像評価機能(品質、特徴点)
-
入力画像にスコアを計算し、優れた写真を選びやすくします。
-
スコアの計算には複数の要素が含まれ、画質だけでなく、照明、角度、被写体、記憶に残るかどうかも考慮されます。



WWDC では、同じ画質で以下の3枚の画像を使って説明がありました:
-
高評価の画像:構図、光、印象に残るポイント
-
低評価の画像:主題がなく、気軽に撮ったり不注意で撮ったようなもの
-
素材の画像:技術的にはよく撮れているが、印象に残らず、素材ライブラリ用の画像のようなもの
iOS ≥ 18 新API: CalculateImageAestheticsScoresRequest
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)
Visionの新機能(iOS 18)— 体と手のジェスチャー姿勢の同時検出

これまでは人体のポーズと手のポーズを個別に検出していましたが、今回のアップデートで開発者は身体のポーズと手のポーズを同時に検出し、同じリクエストと結果として統合できるようになりました。これにより、より多様なアプリケーション開発が容易になります。
iOS ≥ 18 新API: DetectHumanBodyPoseRequest
var request = DetectHumanBodyPoseRequest()
// 手のポーズも同時に検出する
request.detectsHands = true
guard let bodyPose = try await request.perform(on: image).first else { return }
// 体のポーズの関節
let bodyJoints = bodyPose.allJoints()
// 左手のポーズの関節
let leftHandJoints = bodyPose.leftHand.allJoints()
// 右手のポーズの関節
let rightHandJoints = bodyPose.rightHand.allJoints()
新しい Vision API
Apple は今回のアップデートで、新しい Swift Vision API を開発者向けに提供しました。基本的な既存機能のサポートに加え、主に Swift 6 と Swift Concurrency の特性を強化し、より高性能でより Swift らしい API 操作方法を提供しています。
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コード)を返します。 \|
\| DetectContoursRequest
画像内の輪郭を検出します。 \| ContoursObservation
検出された輪郭線を返します。 \|
\| DetectDocumentSegmentationRequest
画像内のドキュメントを検出・分割します。 \| RectangleObservation
ドキュメントの境界矩形を返します。 \|
\| DetectFaceCaptureQualityRequest
顔のキャプチャ品質を評価します。 \| FaceObservation
顔画像の品質評価スコアを返します。 \|
\| DetectFaceLandmarksRequest
顔の特徴点を検出します。 \| FaceObservation
目や鼻などの顔の特徴点の詳細な位置を返します。 \|
\| DetectFaceRectanglesRequest
画像内の顔を検出します。 \| FaceObservation
顔の境界矩形を返します。 \|
\| DetectHorizonRequest
画像内の地平線を検出します。 \| HorizonObservation
地平線の角度と位置を返します。 \|
\| DetectHumanBodyPose3DRequest
画像内の3D人体姿勢を検出します。 \| RecognizedPointsObservation
3D人体の骨格点と空間座標を返します。 \|
\| DetectHumanBodyPoseRequest
画像内の人体姿勢を検出します。 \| RecognizedPointsObservation
人体の骨格点と座標を返します。 \|
\| DetectHumanHandPoseRequest
画像内の手の姿勢を検出します。 \| RecognizedPointsObservation
手の骨格点と位置を返します。 \|
\| DetectHumanRectanglesRequest
画像内の人体を検出します。 \| HumanObservation
人体の境界矩形を返します。 \|
\| DetectRectanglesRequest
画像内の矩形を検出します。 \| RectangleObservation
矩形の4つの頂点座標を返します。 \|
\| 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 以前のバージョン)
講演者は以下のようなよく使われるリクエストについて言及しました。
ClassifyImageRequest
入力画像を認識し、ラベル分類と信頼度を取得します。

![[旅行記] 2024年 九州再訪 9日間自由旅行、釜山経由→博多クルーズ入国](/assets/755509180ca8/1*f1rNoOIQbE33M9F9NmoTXg.webp)
[旅行記] 2024年 九州再訪 9日間自由旅行、釜山経由→博多クルーズ入国
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("リクエストに失敗しました: \(error)")
}
}
} else {
// 旧APIの書き方
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("リクエストに失敗しました: \(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("リクエストに失敗しました: \(error)")
}
}
}
分析結果:
• 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
画像内の文字を認識する。(別名:画像からテキストへ)
![[[旅行記] 2023年 東京 5日間自由旅行](/posts/z-度旅行記/東京自由行-5日間の観光-グルメ-宿泊情報を徹底解説-9da2c51fa4f2/)](/assets/755509180ca8/1*XL40lLT774PfO60rCIfnxA.webp)
if #available(iOS 18.0, *) {
// Swiftの新機能を使った新しいAPI
var request = RecognizeTextRequest()
request.recognitionLevel = .accurate
request.recognitionLanguages = [.init(identifier: "ja-JP"), .init(identifier: "en-US")] // 言語コードを指定、例:繁体字中国語
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 ?? "テキストが認識されませんでした")
}
}
catch {
print("リクエストに失敗しました: \(error)")
}
}
} else {
// 旧APIの書き方
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("リクエストに失敗しました: \(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 ?? "テキストが認識されませんでした")
}
}
let request = VNRecognizeTextRequest(completionHandler: completionHandler)
request.recognitionLevel = .accurate
request.recognitionLanguages = ["ja-JP", "en-US"] // 言語コードを指定、例:繁体字中国語
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("リクエストに失敗しました: \(error)")
}
}
}
分析結果:
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
画像内のバーコードやQRコードのデータを検出します。


タイの現地の人がおすすめするガチョウ膏
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 ?? "ペイロードなし")")
print("Symbology: \(observation.symbology)")
}
}
catch {
print("リクエスト失敗: \(error)")
}
}
} else {
// 旧APIの書き方
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("リクエスト失敗: \(String(describing: error))")
return
}
guard let observations = request.results as? [VNBarcodeObservation] else {
return
}
observations.forEach {
observation in
print("Payload: \(observation.payloadStringValue ?? "ペイロードなし")")
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("リクエスト失敗: \(error)")
}
}
}
分析結果:
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
画像内の動物と信頼度を識別します。


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("検出された動物: \(label.identifier)、信頼度: \(label.confidence)")
}
}
}
catch {
print("リクエスト失敗: \(error)")
}
}
} else {
// 旧APIの書き方
let completionHandler: VNRequestCompletionHandler = {
request, error in
guard error == nil else {
print("リクエスト失敗: \(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("検出された動物: \(label.identifier)、信頼度: \(label.confidence)")
}
}
}
let request = VNRecognizeAnimalsRequest(completionHandler: completionHandler)
DispatchQueue.global().async {
let handler = VNImageRequestHandler(url: fileURL, options: [:])
do {
try handler.perform([request])
}
catch {
print("リクエスト失敗: \(error)")
}
}
}
分析結果:
Detected animal: Cat with confidence: 0.77245045
その他:
-
画像内の人体検出:DetectHumanRectanglesRequest
-
人や動物のポーズ検出(3Dまたは2D対応):DetectAnimalBodyPoseRequest、DetectHumanBodyPose3DRequest、DetectHumanBodyPoseRequest、DetectHumanHandPoseRequest
-
物体の動きの軌跡を検出および追跡(動画やアニメーションの異なるフレームで):DetectTrajectoriesRequest、TrackObjectRequest、TrackRectangleRequest
iOS ≥ 18 アップデートのハイライト:
VN*Request -> *Request(例:VNDetectBarcodesRequest -> DetectBarcodesRequest)
VN*Observation -> *Observation(例:VNRecognizedObjectObservation -> RecognizedObjectObservation)
VNRequestCompletionHandler -> async/await
VNImageRequestHandler.perform([VN*Request]) -> *Request.perform()
WWDCの例
WWDC公式動画ではスーパーマーケットの商品スキャナーを例に挙げています。
まずほとんどの商品にはスキャン可能なバーコードがあります



observation.boundingBox からバーコードの位置を取得できますが、一般的な UIView の座標系とは異なり、BoundingBox の相対位置の起点は左下で、値の範囲は0〜1の間です。
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バーコードのみをスキャンする場合は直接指定してパフォーマンス向上
Task {
do {
let observations = try await request.perform(on: fileURL)
if let observation = observations.first {
DispatchQueue.main.async {
self.infoLabel.text = observation.payloadString
// マーク用の色レイヤー
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 {
// 旧APIの書き方
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
// マーク用の色レイヤー
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バーコードのみをスキャンする場合は直接指定してパフォーマンス向上
DispatchQueue.global().async {
let handler = VNImageRequestHandler(url: fileURL, options: [:])
do {
try handler.perform([request])
}
catch {
print("Request failed: \(error)")
}
}
}
iOS ≥ 18 アップデートのハイライト:
// iOS >=18 新しい座標変換API toImageCoordinates
observation.boundingBox.toImageCoordinates(CGSize, origin: .upperLeft)
// https://developer.apple.com/documentation/vision/normalizedpoint/toimagecoordinates(from:imagesize:origin:)
ヘルパー:
// 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)
}
出力結果
BoundingBox: (0.5295758928571429, 0.21408638121589782, 0.0943080357142857, 0.21254415360708087)
Payload: 4710018183805
Symbology: VNBarcodeSymbologyEAN13
一部の商品はバーコードがなく、例えばばら売りの果物には商品ラベルのみが付いています


したがって、私たちのスキャナーもテキストラベルのスキャンに対応する必要があります。
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バーコードのみスキャンする場合は指定してパフォーマンス向上
var textRequest = RecognizeTextRequest()
textRequest.recognitionLanguages = [.init(identifier: "zh-Hnat"), .init(identifier: "en-US")]
Task {
do {
let handler = ImageRequestHandler(fileURL)
// パラメータパック構文で全リクエストの完了を待つ必要がある
// 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
// マーク用の色レイヤー
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 {
// 旧APIの書き方
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
// マーク用の色レイヤー
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バーコードのみスキャンする場合は指定してパフォーマンス向上
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)")
}
}
}
出力結果:
94128s
オーガニック
ピンクレディ®
USh産
iOS ≥ 18 アップデートのハイライト:
let handler = ImageRequestHandler(fileURL)
// パラメータパック構文で、すべてのリクエストが完了するまで結果を使用できません。
// 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) はバーコードスキャンとテキストスキャンの処理が両方のリクエスト完了まで待つ必要がありましたが、iOS 18からは新しい performAll() メソッドが提供され、レスポンス方式がストリームに変わりました。これにより、どちらかのリクエスト結果を受け取った時点で対応処理が可能になり、例えばバーコードをスキャンしたらすぐに反応できます。
if #available(iOS 18.0, *) {
// Swiftの新機能を使ったAPI
var barcodesRequest = DetectBarcodesRequest()
barcodesRequest.symbologies = [.ean13] // EAN13バーコードのみスキャンする場合は指定して性能向上
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
// マーク用の色レイヤー
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 ?? "テキスト認識なし")
}
default:
print("認識できない結果: \(result)")
}
}
}
}
Swift Concurrency で最適化する


例えば、画像ウォールのリストがあり、各画像から自動的に物体の主体を切り抜く必要がある場合、Swift Concurrency を活用して読み込み効率を向上させることができます。
元の書き方
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 コンカレンシー
func generateAllThumbnails() async throws {
try await withThrowingDiscardingTaskGroup { taskGroup in
for image in images {
image.thumbnail = try await generateThumbnail(url: image.url)
}
}
}
各タスクを TaskGroup Concurrency に追加して実行する。
問題:画像認識やスクリーンショットの処理は非常にメモリとパフォーマンスを消費します。無制限に並列タスクを増やすと、ユーザー操作が重くなったり、OOM(メモリ不足)によるクラッシュが発生する可能性があります。
最適化 (2) — TaskGroup Concurrency + 並行数の制限
func generateAllThumbnails() async throws {
try await withThrowingDiscardingTaskGroup {
taskGroup in
// 最大実行数は5を超えない
let maxImageTasks = min(5, images.count)
// まず5つのタスクを追加
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内のタスクが完了した時...
// インデックスが終端か確認
if nextIndex < images.count {
let image = images[nextIndex]
// タスクを順次追加(最大5つを維持)
taskGroup.addTask {
image.thumbnail = try await generateThumbnail(url: image.url)
}
nextIndex += 1
}
}
}
}
既存の Vision アプリを更新する


-
Vision は、ニューラルエンジン搭載デバイスで一部のリクエストに対する CPU と GPU のサポートを廃止します。これらのデバイスでは、ニューラルエンジンが最も高性能な選択肢です。
supportedComputeDevices()API を使って確認できます。 -
すべての VN プレフィックスを削除
VNXXRequest,VNXXXObservation→Request,Observation -
元の VNRequestCompletionHandler を async/await で置き換える
-
直接
*Request.perform()を使い、従来のVNImageRequestHandler.perform([VN*Request])を置き換える
まとめ
-
Swift 言語機能のために新たに設計された API
-
新しい機能やメソッドはすべて Swift 専用で、iOS 18 以上で利用可能です。
-
新しい画像の美的評価機能、身体+手の動作追跡
ありがとうございます!

KKday 募集広告

👉👉👉本読書会の共有は KKday App Team 内の週次技術共有活動に由来しています。現在チームでは熱心に Senior iOS Engineer を募集しています。興味のある方はぜひ応募してください。 👈👈👈
参考資料
VisionフレームワークのSwift強化を発見する
Vision Framework API は、Swift の最新機能である並行処理を活用するように再設計され、多様な Vision アルゴリズムをアプリにより簡単かつ高速に統合できるようになりました。この記事では、更新された API を紹介し、サンプルコードやベストプラクティスを共有して、より少ないコーディングでこのフレームワークの利点を活用できるようにします。また、新機能の画像美学評価と全身姿勢検出についても解説します。
チャプター
-
0:00 — イントロダクション
-
1:07 — 新しい Vision API
-
1:47 — Visionの使い始め
-
8:59 — Swift Concurrency を使った最適化
-
11:05 — 既存のVisionアプリを更新する
-
13:46 — Visionの新機能は?
Vision framework Apple Developer Documentation
-
Post は ZMediumToMarkdown によって Medium から変換されました。



コメント