axiom-vision
被写体のセグメンテーション、VNGenerateForegroundInstanceMaskRequest、手からのオブジェクト分離、VisionKit被写体リフティング、画像前景検出、インスタンスマスク、クラス非依存セグメンテーション、VNRecognizeTextRequest、OCR、VNDetectBarcodesRequest、DataScannerViewController、ドキュメントスキャニング、RecognizeDocumentsRequestを使用して実装できます。これらのAPIを活用することで、画像から被写体を自動検出・分離したり、手の領域を除外したりすることが可能です。また、テキスト認識、バーコード検出、ドキュメントスキャンなど、様々なコンピュータビジョンタスクに対応できます。
description の原文を見る
subject segmentation, VNGenerateForegroundInstanceMaskRequest, isolate object from hand, VisionKit subject lifting, image foreground detection, instance masks, class-agnostic segmentation, VNRecognizeTextRequest, OCR, VNDetectBarcodesRequest, DataScannerViewController, document scanning, RecognizeDocumentsRequest
SKILL.md 本文
Vision Framework コンピュータビジョン
コンピュータビジョンの実装を段階的に解説します。被写体の分割、手や体のポーズ検出、人物検出、テキスト認識、バーコード検出、ドキュメントスキャン、複雑な問題を解決するための Vision API の組み合わせ方法を学べます。
このスキルを使う場合
以下が必要な場合に使用してください:
- ☑ 背景から被写体を分離する(被写体の抽出)
- ☑ ジェスチャーの手ポーズを検出・追跡する
- ☑ フィットネス・アクション分類用に体ポーズを検出・追跡する
- ☑ 複数人を個別に分割する
- ☑ オブジェクトのバウンディングボックスから手を除外する(API の組み合わせ)
- ☑ VisionKit と Vision framework の選択
- ☑ Vision と CoreImage を組み合わせてコンポジットする
- ☑ どの Vision API で問題を解決するか判断する
- ☑ 画像内のテキストを認識する(OCR)
- ☑ バーコードと QR コードを検出する
- ☑ パースペクティブ補正でドキュメントをスキャンする
- ☑ ドキュメントから構造化データを抽出する(iOS 26+)
- ☑ ライブスキャン体験を構築する(DataScannerViewController)
プロンプト例
「背景から被写体を分離するにはどうしたらいいですか?」 「ピンチなどの手ジェスチャーを検出する必要があります」 「手に持たれているオブジェクトの周りにバウンディングボックスを描画するには?手を含めないで」 「被写体の抽出に VisionKit と Vision framework のどちらを使うべきですか?」 「複数人を個別に分割するにはどうしたらいいですか?」 「フィットネスアプリの体ポーズ検出が必要です」 「新しい背景に被写体をコンポジットするときに HDR を保持するにはどうしたらいいですか?」 「画像内のテキストを認識するにはどうしたらいいですか?」 「カメラから QR コードをスキャンしたいです」 「レシートからデータを抽出するにはどうしたらいいですか?」 「DataScannerViewController と Vision を直接使うどちらを選ぶべきですか?」 「ドキュメントをスキャンしてパースペクティブを補正するにはどうしたらいいですか?」 「ドキュメントからテーブルデータを抽出する必要があります」
危険な兆候
これをもっと複雑にしているサイン:
- ❌ CoreML モデルで手動で被写体分割を実装している
- ❌ 体ポーズだけのために ARKit を使用している(Vision はオフラインで動く)
- ❌ ジェスチャー認識をゼロから書いている(手ポーズ + シンプルな距離チェックを使う)
- ❌ メインスレッドで処理している(UI がブロックされる - Vision はリソース集約的)
- ❌ Vision API が既に存在するのにカスタムモデルを学習している
- ❌ 信頼度スコアをチェックしていない(信頼度が低い = ランドマークが信頼できない)
- ❌ 座標の変換を忘れている(左下原点と UIKit の左上原点)
- ❌ VNRecognizeTextRequest が存在するのにカスタムテキスト認識器を構築している
- ❌ DataScannerViewController で十分なのに AVFoundation + Vision を使っている
- ❌ スキャン用にすべてのカメラフレームを処理している(フレームをスキップして関心領域を使う)
- ❌ 1 つだけ必要なのにすべてのバーコード記号体系を有効にしている(パフォーマンス低下)
- ❌ テーブル・リスト構造が必要なのに RecognizeDocumentsRequest を無視している(iOS 26+)
必須の最初のステップ
Vision 機能を実装する前に:
1. 適切な API を選ぶ(決定木)
何をする必要がありますか?
┌─ 背景から被写体を分離しますか?
│ ├─ システム UI + プロセス外が必要 → VisionKit
│ │ └─ ImageAnalysisInteraction(iOS/iPadOS)
│ │ └─ ImageAnalysisOverlayView(macOS)
│ ├─ カスタムパイプライン / HDR / 大きい画像が必要 → Vision
│ │ └─ VNGenerateForegroundInstanceMaskRequest
│ └─ オブジェクトから手を除外する必要がある → API を組み合わせる
│ └─ 被写体マスク + 手ポーズ + カスタムマスキング(パターン 1 参照)
│
├─ 人物を分割しますか?
│ ├─ 1 つのマスク内のすべての人物 → VNGeneratePersonSegmentationRequest
│ └─ 人物ごとに個別マスク(最大 4 人) → VNGeneratePersonInstanceMaskRequest
│
├─ 手ポーズ・ジェスチャーを検出しますか?
│ ├─ 手の位置のみ → VNDetectHumanRectanglesRequest
│ └─ 21 個の手ランドマーク → VNDetectHumanHandPoseRequest
│ └─ ジェスチャー認識 → 手ポーズ + 距離チェック
│
├─ 体ポーズを検出しますか?
│ ├─ 2D 正規化ランドマーク → VNDetectHumanBodyPoseRequest
│ ├─ 3D 実世界座標 → VNDetectHumanBodyPose3DRequest
│ └─ アクション分類 → 体ポーズ + CreateML モデル
│
├─ 顔を検出しますか?
│ ├─ バウンディングボックスだけ → VNDetectFaceRectanglesRequest
│ └─ 詳細なランドマーク → VNDetectFaceLandmarksRequest
│
├─ 人物検出(位置のみ)ですか?
│ └─ VNDetectHumanRectanglesRequest
│
├─ 画像内のテキストを認識しますか?
│ ├─ カメラからのリアルタイム + UI が必要 → DataScannerViewController(iOS 16+)
│ ├─ キャプチャした画像を処理 → VNRecognizeTextRequest
│ │ ├─ 速度が必要(リアルタイムカメラ) → recognitionLevel = .fast
│ │ └─ 精度が必要(ドキュメント) → recognitionLevel = .accurate
│ └─ 構造化ドキュメント が必要(iOS 26+) → RecognizeDocumentsRequest
│
├─ バーコード・QR コードを検出しますか?
│ ├─ リアルタイムカメラ + UI が必要 → DataScannerViewController(iOS 16+)
│ └─ 画像を処理 → VNDetectBarcodesRequest
│
└─ ドキュメントをスキャンしますか?
├─ 組み込み UI + パースペクティブ補正が必要 → VNDocumentCameraViewController
├─ 構造化データ(テーブル、リスト)が必要 → RecognizeDocumentsRequest(iOS 26+)
└─ カスタムパイプライン → VNDetectDocumentSegmentationRequest + パースペクティブ補正
2. バックグラウンド処理をセットアップする
絶対にメインスレッドで Vision を実行しないでください:
let processingQueue = DispatchQueue(label: "com.yourapp.vision", qos: .userInitiated)
processingQueue.async {
do {
let request = VNGenerateForegroundInstanceMaskRequest()
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
// 観測を処理...
DispatchQueue.main.async {
// UI を更新
}
} catch {
// エラーを処理
}
}
3. 適切なリクエストハンドラーを選ぶ
ビデオフレームを処理しますか? VNSequenceRequestHandler を使用してください(フレーム間の状態を保持して時間的スムージングを行う)。単一の画像の場合は VNImageRequestHandler を使用します。フレームごとに新しい VNImageRequestHandler を作成するとフレーム間コンテキストが破棄され、結果がちらつきます。詳細な比較とコード例については axiom-vision-ref を参照してください。
4. プラットフォーム対応状況を確認する
| API | 最小バージョン |
|---|---|
| 被写体分割(インスタンスマスク) | iOS 17+ |
| VisionKit 被写体抽出 | iOS 16+ |
| 手ポーズ | iOS 14+ |
| 体ポーズ(2D) | iOS 14+ |
| 体ポーズ(3D) | iOS 17+ |
| 人物インスタンス分割 | iOS 17+ |
| VNRecognizeTextRequest(基本) | iOS 13+ |
| VNRecognizeTextRequest(高精度、多言語) | iOS 14+ |
| VNDetectBarcodesRequest | iOS 11+ |
| VNDetectBarcodesRequest(リビジョン 2: Codabar、MicroQR) | iOS 15+ |
| VNDetectBarcodesRequest(リビジョン 3: ML ベース) | iOS 16+ |
| DataScannerViewController | iOS 16+ |
| VNDocumentCameraViewController | iOS 13+ |
| VNDetectDocumentSegmentationRequest | iOS 15+ |
| RecognizeDocumentsRequest | iOS 26+ |
一般的なパターン
パターン 1: 手を除外しながらオブジェクトを分離する
ユーザーの元の問題: 手に持たれているオブジェクトの周りにバウンディングボックスを描画し、手を含めない。
根本原因: VNGenerateForegroundInstanceMaskRequest はクラスに依存しないため、手+オブジェクトを 1 つの被写体として扱います。
解決方法: 被写体マスクと手ポーズを組み合わせて除外マスクを作成します。
// 1. 被写体インスタンスマスクを取得
let subjectRequest = VNGenerateForegroundInstanceMaskRequest()
let handler = VNImageRequestHandler(cgImage: sourceImage)
try handler.perform([subjectRequest])
guard let subjectObservation = subjectRequest.results?.first as? VNInstanceMaskObservation else {
fatalError("被写体が検出されません")
}
// 2. 手ポーズランドマークを取得
let handRequest = VNDetectHumanHandPoseRequest()
handRequest.maximumHandCount = 2
try handler.perform([handRequest])
guard let handObservation = handRequest.results?.first as? VNHumanHandPoseObservation else {
// 手が検出されない - 完全な被写体マスクを使用
let mask = try subjectObservation.createScaledMask(
for: subjectObservation.allInstances,
croppedToInstancesContent: false
)
return mask
}
// 3. ランドマークから手の除外領域を作成
let handPoints = try handObservation.recognizedPoints(.all)
let handBounds = calculateConvexHull(from: handPoints) // あなたの実装
// 4. CoreImage を使用して被写体マスクから手領域を減算
let subjectMask = try subjectObservation.createScaledMask(
for: subjectObservation.allInstances,
croppedToInstancesContent: false
)
let subjectCIMask = CIImage(cvPixelBuffer: subjectMask)
let handMask = createMaskFromRegion(handBounds, size: sourceImage.size)
let finalMask = subtractMasks(handMask: handMask, from: subjectCIMask)
// 5. 最終マスクからバウンディングボックスを計算
let objectBounds = calculateBoundingBox(from: finalMask)
ヘルパー:凸包
func calculateConvexHull(from points: [VNRecognizedPointKey: VNRecognizedPoint]) -> CGRect {
// 信頼度が高いポイントを取得
let validPoints = points.values.filter { $0.confidence > 0.5 }
guard !validPoints.isEmpty else { return .zero }
// シンプルなバウンディング矩形(より正確には実際の凸包アルゴリズムを使用)
let xs = validPoints.map { $0.location.x }
let ys = validPoints.map { $0.location.y }
let minX = xs.min()!
let maxX = xs.max()!
let minY = ys.min()!
let maxY = ys.max()!
return CGRect(
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
)
}
コスト: 初期実装 2~5 時間、継続メンテナンス 30 分
パターン 2: VisionKit シンプル被写体抽出
ユースケース: 最小限のコードでシステムのような被写体抽出 UI を追加します。
// iOS
let interaction = ImageAnalysisInteraction()
interaction.preferredInteractionTypes = .imageSubject
imageView.addInteraction(interaction)
// macOS
let overlayView = ImageAnalysisOverlayView()
overlayView.preferredInteractionTypes = .imageSubject
nsView.addSubview(overlayView)
使う場合:
- ✓ システムの動作が必要(長押しで選択、ドラッグで共有)
- ✓ カスタム処理パイプラインが不要
- ✓ 画像サイズが VisionKit の制限内(プロセス外)
コスト: 実装 15 分、継続メンテナンス 5 分
パターン 3: プログラム的な被写体アクセス(VisionKit)
ユースケース: UI インタラクションなしで被写体画像・バウンズを取得します。
let analyzer = ImageAnalyzer()
let configuration = ImageAnalyzer.Configuration([.text, .visualLookUp])
let analysis = try await analyzer.analyze(sourceImage, configuration: configuration)
// すべての被写体を取得
for subject in analysis.subjects {
let subjectImage = subject.image
let subjectBounds = subject.bounds
// 被写体を処理...
}
// タップベースのルックアップ
if let subject = try await analysis.subject(at: tapPoint) {
let compositeImage = try await analysis.image(for: [subject])
}
コスト: 実装 30 分、継続メンテナンス 10 分
パターン 4: Vision インスタンスマスクのカスタムパイプライン
ユースケース: HDR 保持、大きい画像、カスタムコンポジット。
let request = VNGenerateForegroundInstanceMaskRequest()
let handler = VNImageRequestHandler(cgImage: sourceImage)
try handler.perform([request])
guard let observation = request.results?.first as? VNInstanceMaskObservation else {
return
}
// ソフトセグメンテーションマスクを取得
let mask = try observation.createScaledMask(
for: observation.allInstances,
croppedToInstancesContent: false // コンポジット用に完全な解像度
)
// CoreImage で HDR 保持を使用
let filter = CIFilter(name: "CIBlendWithMask")!
filter.setValue(CIImage(cgImage: sourceImage), forKey: kCIInputImageKey)
filter.setValue(CIImage(cvPixelBuffer: mask), forKey: kCIInputMaskImageKey)
filter.setValue(newBackground, forKey: kCIInputBackgroundImageKey)
let compositedImage = filter.outputImage
コスト: 実装 1 時間、継続メンテナンス 15 分
パターン 5: タップして選択するインスタンス
ユースケース: ユーザーがタップして、どの被写体・人物を抽出するかを選択します。
// タップポイントのインスタンスを取得
let instance = observation.instanceAtPoint(tapPoint)
if instance == 0 {
// 背景がタップされた - すべてのインスタンスを選択
let mask = try observation.createScaledMask(
for: observation.allInstances,
croppedToInstancesContent: false
)
} else {
// 特定のインスタンスがタップされた
let mask = try observation.createScaledMask(
for: IndexSet(integer: instance),
croppedToInstancesContent: true
)
}
別の方法: ピクセルバッファへの直接アクセス
let instanceMask = observation.instanceMask
CVPixelBufferLockBaseAddress(instanceMask, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(instanceMask, .readOnly) }
let baseAddress = CVPixelBufferGetBaseAddress(instanceMask)
let bytesPerRow = CVPixelBufferGetBytesPerRow(instanceMask)
// 正規化されたタップをピクセル座標に変換
let pixelPoint = VNImagePointForNormalizedPoint(
tapPoint,
width: imageWidth,
height: imageHeight
)
let offset = Int(pixelPoint.y) * bytesPerRow + Int(pixelPoint.x)
let label = UnsafeRawPointer(baseAddress!).load(
fromByteOffset: offset,
as: UInt8.self
)
コスト: 実装 45 分、継続メンテナンス 10 分
パターン 6: ハンドジェスチャー認識(ピンチ)
ユースケース: カスタムカメラトリガーまたは UI コントロール用のピンチジェスチャーを検出します。
let request = VNDetectHumanHandPoseRequest()
request.maximumHandCount = 1
try handler.perform([request])
guard let observation = request.results?.first as? VNHumanHandPoseObservation else {
return
}
let thumbTip = try observation.recognizedPoint(.thumbTip)
let indexTip = try observation.recognizedPoint(.indexTip)
// 信頼度をチェック
guard thumbTip.confidence > 0.5, indexTip.confidence > 0.5 else {
return
}
// 距離を計算(正規化座標)
let dx = thumbTip.location.x - indexTip.location.x
let dy = thumbTip.location.y - indexTip.location.y
let distance = sqrt(dx * dx + dy * dy)
let isPinching = distance < 0.05 // しきい値を調整
// 証拠を蓄積するための状態機械
if isPinching {
pinchFrameCount += 1
if pinchFrameCount >= 3 {
state = .pinched
}
} else {
pinchFrameCount = max(0, pinchFrameCount - 1)
if pinchFrameCount == 0 {
state = .apart
}
}
コスト: 実装 2 時間、継続メンテナンス 20 分
パターン 7: 複数人を個別に分割する
ユースケース: 各人に異なるエフェクトを適用するか、人数をカウントします。
let request = VNGeneratePersonInstanceMaskRequest()
try handler.perform([request])
guard let observation = request.results?.first as? VNInstanceMaskObservation else {
return
}
let peopleCount = observation.allInstances.count // 最大 4 人
for personIndex in observation.allInstances {
let personMask = try observation.createScaledMask(
for: IndexSet(integer: personIndex),
croppedToInstancesContent: false
)
// この人物のみにエフェクトを適用
applyEffect(to: personMask, personIndex: personIndex)
}
混雑したシーン(4 人以上):
// 顔の数を数えて混雑を検出
let faceRequest = VNDetectFaceRectanglesRequest()
try handler.perform([faceRequest])
let faceCount = faceRequest.results?.count ?? 0
if faceCount > 4 {
// フォールバック: すべての人物に単一マスクを使用
let singleMaskRequest = VNGeneratePersonSegmentationRequest()
try handler.perform([singleMaskRequest])
}
コスト: 実装 1.5 時間、継続メンテナンス 15 分
パターン 8: アクション分類用の体ポーズ
ユースケース: ジャンプジャックやスクワットなどのエクササイズを認識するフィットネスアプリ。
// 1. 体ポーズ観測を収集
var poseObservations: [VNHumanBodyPoseObservation] = []
let request = VNDetectHumanBodyPoseRequest()
try handler.perform([request])
if let observation = request.results?.first as? VNHumanBodyPoseObservation {
poseObservations.append(observation)
}
// 2. 60 フレームのポーズがある場合、CreateML モデル用に準備
if poseObservations.count == 60 {
var multiArray = try MLMultiArray(
shape: [60, 18, 3], // 60 フレーム、18 関節、(x, y, 信頼度)
dataType: .double
)
for (frameIndex, observation) in poseObservations.enumerated() {
let allPoints = try observation.recognizedPoints(.all)
for (jointIndex, (_, point)) in allPoints.enumerated() {
multiArray[[frameIndex, jointIndex, 0] as [NSNumber]] = NSNumber(value: point.location.x)
multiArray[[frameIndex, jointIndex, 1] as [NSNumber]] = NSNumber(value: point.location.y)
multiArray[[frameIndex, jointIndex, 2] as [NSNumber]] = NSNumber(value: point.confidence)
}
}
// 3. CreateML モデルで推論を実行
let input = YourActionClassifierInput(poses: multiArray)
let output = try actionClassifier.prediction(input: input)
let action = output.label // "jumping_jacks"、"squats" など
}
コスト: 実装 3~4 時間、継続メンテナンス 1 時間
パターン 9: テキスト認識(OCR)
ユースケース: 画像、レシート、標識、ドキュメントからテキストを抽出します。
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate // または .fast リアルタイム用
request.recognitionLanguages = ["en-US"] // 既知の言語を指定
request.usesLanguageCorrection = true // 精度を改善
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
guard let observations = request.results as? [VNRecognizedTextObservation] else {
return
}
for observation in observations {
// トップ候補を取得(最も可能性が高い)
guard let candidate = observation.topCandidates(1).first else { continue }
let text = candidate.string
let confidence = candidate.confidence
// 特定の部分文字列のバウンディングボックスを取得
if let range = text.range(of: searchTerm) {
if let boundingBox = try? candidate.boundingBox(for: range) {
// ハイライト用に使用
}
}
}
高速 vs 高精度:
- 高速: リアルタイムカメラ、大きな判読可能テキスト(標識、広告看板)、文字ごと
- 高精度: ドキュメント、レシート、小さいテキスト、手書き、ML ベースの単語・行認識
言語のヒント:
- 順序が重要: 最初の言語が高精度パスの ML モデルを決定します
automaticallyDetectsLanguage = trueは言語が不明な場合のみ使用してください- 現在のリビジョンで
supportedRecognitionLanguagesを照会してください
コスト: 基本的な実装 30 分、言語処理付きで 2 時間
パターン 10: バーコード・QR コード検出
ユースケース: 製品バーコード、QR コード、ヘルスケアコードをスキャンします。
let request = VNDetectBarcodesRequest()
request.revision = VNDetectBarcodesRequestRevision3 // ML ベース、iOS 16+
request.symbologies = [.qr, .ean13] // 必要なもののみを指定!
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
guard let observations = request.results as? [VNBarcodeObservation] else {
return
}
for barcode in observations {
let payload = barcode.payloadStringValue // デコード済みコンテンツ
let symbology = barcode.symbology // バーコードのタイプ
let bounds = barcode.boundingBox // 位置(正規化)
print("\(symbology) を見つけました: \(payload ?? "文字列なし")")
}
パフォーマンスのヒント: より少ない記号体系を指定する = より速いスキャン
リビジョンの違い:
- リビジョン 1: 一度に 1 つのコード、1D コードは行を返す
- リビジョン 2: Codabar、GS1Databar、MicroPDF、MicroQR、ROI を使用するとより良い
- リビジョン 3: ML ベース、一度に複数コード、バウンディングボックスが改良、重複が少ない
コスト: 実装 15 分
パターン 11: DataScannerViewController(ライブスキャン)
ユースケース: 組み込み UI でテキスト・バーコードをカメラからスキャンします(iOS 16+)。
import VisionKit
// サポートを確認
guard DataScannerViewController.isSupported,
DataScannerViewController.isAvailable else {
// サポートされていないか、カメラアクセスが拒否されている
return
}
// スキャンする内容を設定
let recognizedDataTypes: Set<DataScannerViewController.RecognizedDataType> = [
.barcode(symbologies: [.qr]),
.text(textContentType: .URL) // または nil ですべてのテキスト
]
// 作成してプレゼント
let scanner = DataScannerViewController(
recognizedDataTypes: recognizedDataTypes,
qualityLevel: .balanced, // または .fast、.accurate
recognizesMultipleItems: false, // false の場合は中央のみ
isHighFrameRateTrackingEnabled: true, // スムーズなハイライト用
isPinchToZoomEnabled: true,
isGuidanceEnabled: true,
isHighlightingEnabled: true
)
scanner.delegate = self
present(scanner, animated: true) {
try? scanner.startScanning()
}
デリゲートメソッド:
func dataScanner(_ scanner: DataScannerViewController,
didTapOn item: RecognizedItem) {
switch item {
case .text(let text):
print("タップされたテキスト: \(text.transcript)")
case .barcode(let barcode):
print("タップされたバーコード: \(barcode.payloadStringValue ?? "")")
@unknown default: break
}
}
// カスタムハイライト用
func dataScanner(_ scanner: DataScannerViewController,
didAdd addedItems: [RecognizedItem],
allItems: [RecognizedItem]) {
for item in addedItems {
let highlight = createHighlight(for: item)
scanner.overlayContainerView.addSubview(highlight)
}
}
非同期ストリーム別の方法:
for await items in scanner.recognizedItems {
// 現在のアイテムを処理
}
コスト: カスタムハイライト付き実装 45 分
パターン 12: VNDocumentCameraViewController でドキュメントをスキャンする
ユースケース: 自動エッジ検出とパースペクティブ補正を使用して紙のドキュメントをスキャンします。
import VisionKit
let documentCamera = VNDocumentCameraViewController()
documentCamera.delegate = self
present(documentCamera, animated: true)
// デリゲート内
func documentCameraViewController(_ controller: VNDocumentCameraViewController,
didFinishWith scan: VNDocumentCameraScan) {
controller.dismiss(animated: true)
// 各ページを処理
for pageIndex in 0..<scan.pageCount {
let image = scan.imageOfPage(at: pageIndex)
// 補正された画像でテキスト認識を実行
let handler = VNImageRequestHandler(cgImage: image.cgImage!)
let textRequest = VNRecognizeTextRequest()
try? handler.perform([textRequest])
}
}
コスト: 実装 30 分
パターン 13: ドキュメント分割(カスタムパイプライン)
ユースケース: カスタムカメラ UI 用にプログラムでドキュメントのエッジを検出します。
let request = VNDetectDocumentSegmentationRequest()
let handler = VNImageRequestHandler(ciImage: inputImage)
try handler.perform([request])
guard let observation = request.results?.first,
let document = observation as? VNRectangleObservation else {
return
}
// コーナーポイントを取得(正規化座標)
let topLeft = document.topLeft
let topRight = document.topRight
let bottomLeft = document.bottomLeft
let bottomRight = document.bottomRight
// CoreImage でパースペクティブ補正を適用
let correctedImage = inputImage
.cropped(to: document.boundingBox.scaled(to: imageSize))
.applyingFilter("CIPerspectiveCorrection", parameters: [
"inputTopLeft": CIVector(cgPoint: topLeft.scaled(to: imageSize)),
"inputTopRight": CIVector(cgPoint: topRight.scaled(to: imageSize)),
"inputBottomLeft": CIVector(cgPoint: bottomLeft.scaled(to: imageSize)),
"inputBottomRight": CIVector(cgPoint: bottomRight.scaled(to: imageSize))
])
VNDetectDocumentSegmentationRequest vs VNDetectRectanglesRequest:
- ドキュメント: ML ベース、ドキュメント用に学習、非矩形を処理、1 つのドキュメントを返す
- 矩形: エッジベース、任意の四角形を見つける、複数を返す、CPU のみ
コスト: 実装 1~2 時間
パターン 14: 構造化ドキュメント抽出(iOS 26+)
ユースケース: テーブル、リスト、段落をセマンティック理解で抽出します。
// iOS 26+
let request = RecognizeDocumentsRequest()
let observations = try await request.perform(on: imageData)
guard let document = observations.first?.document else {
return
}
// テーブルを抽出
for table in document.tables {
for row in table.rows {
for cell in row {
let text = cell.content.text.transcript
print("セル: \(text)")
}
}
}
// 検出されたデータを取得(メール、電話、URL、日付)
let allDetectedData = document.text.detectedData
for data in allDetectedData {
switch data.match.details {
case .emailAddress(let email):
print("メール: \(email.emailAddress)")
case .phoneNumber(let phone):
print("電話: \(phone.phoneNumber)")
case .link(let url):
print("URL: \(url)")
default: break
}
}
ドキュメント階層:
- Document → コンテナ(テキスト、テーブル、リスト、バーコード)
- Table → 行 → セル → コンテンツ
- Content → テキスト(文字起こし、行、段落、単語、detectedData)
コスト: 実装 1 時間
パターン 15: リアルタイム電話番号スキャナー
ユースケース: バーコードスキャナーのようにカメラから電話番号をスキャンします(WWDC 2019 より)。
// 1. ユーザーをガイドするために関心領域を使用
let textRequest = VNRecognizeTextRequest { request, error in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
for observation in observations {
guard let candidate = observation.topCandidates(1).first else { continue }
// ドメイン知識を使用して検出
if let phoneNumber = self.extractPhoneNumber(from: candidate.string) {
self.stringTracker.add(phoneNumber)
}
}
// フレーム全体で証拠を構築
if let stableNumber = self.stringTracker.getStableString(threshold: 10) {
self.foundPhoneNumber(stableNumber)
}
}
textRequest.recognitionLevel = .fast // リアルタイム
textRequest.usesLanguageCorrection = false // 自然言語ではなくコード
textRequest.regionOfInterest = guidanceBox // ユーザーの焦点領域にトリミング
// 2. 安定性用の文字列トラッカー
class StringTracker {
private var seenStrings: [String: Int] = [:]
func add(_ string: String) {
seenStrings[string, default: 0] += 1
}
func getStableString(threshold: Int) -> String? {
seenStrings.first { $0.value >= threshold }?.key
}
}
WWDC 2019 からの重要なテクニック:
- リアルタイム用に
.fast認識レベルを使用 - コード・番号用に言語補正を無効化
- 速度と焦点を改善するために関心領域を使用
- 複数フレーム全体で証拠を構築(文字列トラッカー)
- ドメイン知識を適用(電話番号の正規表現)
コスト: 実装 2 時間
アンチパターン
アンチパターン 1: メインスレッドでの処理
間違い:
let request = VNGenerateForegroundInstanceMaskRequest()
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request]) // UI をブロック!
正解:
DispatchQueue.global(qos: .userInitiated).async {
let request = VNGenerateForegroundInstanceMaskRequest()
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
DispatchQueue.main.async {
// UI を更新
}
}
重要な理由: Vision はリソース集約的です。メインスレッドをブロックすると UI がフリーズします。
アンチパターン 2: 信頼度スコアを無視する
間違い:
let thumbTip = try observation.recognizedPoint(.thumbTip)
let location = thumbTip.location // 信頼できないかもしれません!
正解:
let thumbTip = try observation.recognizedPoint(.thumbTip)
guard thumbTip.confidence > 0.5 else {
// 低信頼度 - ランドマークは信頼できない
return
}
let location = thumbTip.location
重要な理由: 低信頼度ポイントは不正確です(オクルージョン、ブラー、フレームの端)。
アンチパターン 3: 座標変換を忘れている
間違い(座標系を混合):
// Vision は左下原点を使用
let axiom-visionPoint = recognizedPoint.location // (0, 0) = 左下
// UIKit は左上原点を使用
let uiPoint = CGPoint(x: axiom-visionPoint.x, y: axiom-visionPoint.y) // 間違い!
正解:
let axiom-visionPoint = recognizedPoint.location
// UIKit 座標に変換
let uiPoint = CGPoint(
x: axiom-visionPoint.x * imageWidth,
y: (1 - axiom-visionPoint.y) * imageHeight // Y 軸を反転
)
重要な理由: 座標系の不一致により、UI オーバーレイが間違った位置に表示されます。
アンチパターン 4: maximumHandCount を高く設定する
間違い:
let request = VNDetectHumanHandPoseRequest()
request.maximumHandCount = 10 // 「念のため」
正解:
let request = VNDetectHumanHandPoseRequest()
request.maximumHandCount = 2 // 必要なものだけ
重要な理由: パフォーマンスは maximumHandCount でスケールします。ポーズはすべての検出された手(≤ 最大)に対して計算されます。
アンチパターン 5: 不要な ARKit を使用している
間違い(ARKit が不要な場合):
// 単に体ポーズのために AR セッションが必要
let arSession = ARBodyTrackingConfiguration()
正解:
// Vision は静止画像でオフラインで動作
let request = VNDetectHumanBodyPoseRequest()
重要な理由: ARKit 体ポーズはリアカメラが必要、AR セッション、対応デバイスが限定。Vision はどこでも動作(オフラインも)。
圧力シナリオ
シナリオ 1: 「機能をリリースするだけ」
状況: プロダクト マネージャーが金曜日までに Photos アプリのような「被写体抽出」をリリースするよう要求。バックグラウンド処理をスキップしようと検討中。
圧力: 「iPhone 15 Pro で動作しています。リリースしましょう。」
現実: Vision は古いデバイスでは UI をブロックします。iPhone 12 以前のユーザーはアプリがフリーズするのを経験し、1 つ星レビューを書きます。
正しい行動:
- バックグラウンドキューを実装(15 分)
- ロードインジケータを追加(10 分)
- iPhone 12 以前でテスト(5 分)
反論テンプレート: 「被写体抽出は動作しますが、古いデバイスで UI がフリーズします。30 分でバックグラウンド処理を追加して、1 つ星レビューを防ぐ必要があります。」
シナリオ 2: 「独自のモデルを学習する」
状況: デザイナーが手からオブジェクトのバウンディングボックスを除外したい。エンジニアはカスタム CoreML モデルの学習を提案。
圧力: 「完璧なバウンズが必要。モデルを学習しましょう。」
現実: 学習にはラベル付きデータセット(数週間)が必要。継続メンテナンスも必要。新しいオブジェクトに一般化しません。組み込み Vision API + 手ポーズで 2~5 時間で解決できます。
正しい行動:
- パターン 1 を説明(被写体マスク + 手ポーズの組み合わせ)
- 1 時間でプロトタイプを実装してデモンストレーション
- 学習のタイムラインと比較(週 vs 時間)
反論テンプレート: 「モデル学習には数週間かかり、特定のオブジェクトにしか対応しません。Vision API を組み合わせて数時間で解決でき、あらゆるオブジェクトで動作します。」
シナリオ 3: 「iOS 17 を待つことはできない」
状況: インスタンスマスクが必要だが、アプリは iOS 15+ をサポート。
圧力: 「iOS 15 の人物分割で リリースしましょう。」
現実: VNGeneratePersonSegmentationRequest(iOS 15)はすべての人物を 1 つのマスクに結合します。マルチ人物ユースケースを解決しません。
正しい行動:
- 最小デプロイメント ターゲットを iOS 17 に上げる(最良の UX)
- または iOS 15 API にフォールバック: iOS 15 で複数人物機能を無効化
- または
@availableを使用して条件付きで機能を有効化
反論テンプレート: 「iOS 15 の人物分割はすべての人物を 1 つのマスクに結合します。iOS 17 に要件を上げるか、古い OS では複数人物機能を無効にするかを選んでください。」
チェックリスト
Vision 機能をリリース前に:
パフォーマンス:
- ☑ すべての Vision リクエストがバックグラウンドキューで実行
- ☑ UI がロード中にインジケータを表示
- ☑ iPhone 12 以前でテスト(最新デバイスだけではなく)
- ☑
maximumHandCountを最小必要値に設定
精度:
- ☑ ランドマーク使用前に信頼度スコアをチェック
- ☑ 低信頼度観測の フォールバック動作
- ☑ 被写体・手・人物が検出されない場合を処理
座標:
- ☑ Vision 座標(左下原点)を UIKit(左上)に変換
- ☑ 正規化座標をピクセル寸法にスケール
- ☑ UI オーバーレイが画像に正しく配置されている
プラットフォーム対応:
- ☑ iOS 17+ API に
@availableチェック(インスタンスマスク) - ☑ iOS 14-16 のフォールバック(またはデプロイメント ターゲットを上げる)
- ☑ シミュレータだけでなく実際のデバイスでテスト
エッジケース:
- ☑ 検出可能な被写体がない画像を処理
- ☑ 部分的にオクルージョンされた手・体を処理
- ☑ 画像エッジ近くの手・体を処理
- ☑ 人物インスタンス分割で 4 人以上を処理
CoreImage インテグレーション(該当する場合):
- ☑ 高動的範囲画像で HDR 保持を確認
- ☑ マスク解像度がソース画像に一致
- ☑
croppedToInstancesContentが適切に設定(コンポジット用に false)
テキスト・バーコード認識(該当する場合):
- ☑ 認識レベルがユースケースに一致(リアルタイムは高速、ドキュメントは高精度)
- ☑ コード・シリアル番号用に言語補正を無効化
- ☑ バーコード記号体系を実際の必要に制限(パフォーマンス)
- ☑ スキャンエリアをフォーカスするために関心領域を使用
- ☑ 複数の候補をチェック(トップ候補だけでなく)
- ☑ リアルタイムでフレーム全体で証拠を蓄積(文字列トラッカー)
- ☑ プレゼント前に DataScannerViewController 対応状況をチェック
リソース
WWDC: 2019-234、2021-10041、2022-10024、2022-10025、2025-272、2023-10176、2023-111241、2020-10653
ドキュメント: /vision、/visionkit、/vision/vnrecognizetextrequest、/vision/vndetectbarcodesrequest
スキル: axiom-vision-ref、axiom-vision-diag
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- ComeOnOliver
- ライセンス
- MIT
- 最終更新
- 2026/5/11
Source: https://github.com/ComeOnOliver/skillshub / ライセンス: MIT
関連スキル
listenhub
あらゆることを説明できます。アイデアをポッドキャスト、解説動画、または音声ナレーションに変換します。 ユーザーが「ポッドキャストを作りたい」「解説動画を作成したい」「これを読み上げてほしい」「画像を生成したい」、または知識を音声・映像形式で共有したいときに使用します。トピックの説明、YouTubeリンク、記事URL、プレーンテキスト、画像プロンプトに対応しています。
best-youtube-video-editor
ClawHub上の「best-youtube-video-editor」スキルは、YouTube クリエイターのコンテンツ制作を革新します。タイムラインや複雑なソフトウェアを必要とせず、会話形式のAI駆動型ビデオ編集が可能です。無音部分のカット、チャプターマーカーの追加、字幕の挿入、ペーシングの調整、エクスポートの最適化——すべてが自然言語の指示で実現します。初回使用時には NemoVideo API を通じて認証情報を自動設定するため、有効化後数秒で編集を開始できます。YouTuber、教育関係者、ポッドキャスター、ブランドチャネル向けに開発され、品質を損なわず高速な納期対応が必要な方に最適です。mp4、mov、avi、webm、mkv 形式に対応しています。
video
ユーザーがAIツールやプログラマティックフレームワークを使用してビデオコンテンツを作成、生成、または制作したい場合に使用します。また、ユーザーが「ビデオ制作」「AIビデオ」「Remotion」「Hyperframes」「HeyGen」「Synthesia」「Veo」「Runway」「Kling」「Pika」「ビデオ生成」「AIアバター」「トーキングヘッドビデオ」「プログラマティックビデオ」「ビデオテンプレート」「解説ビデオ」「プロダクトデモビデオ」「ビデオパイプライン」または「ビデオを作ってほしい」と言及している場合にも使用します。ビデオ作成、生成、制作のワークフロー全般に対応できます。ビデオコンテンツの戦略や投稿内容については「social-content」を、有料ビデオ広告クリエイティブについては「ad-creative」をご参照ください。
clipify
ビデオから最も面白い瞬間を検出し、スタンドアロンクリップとしてカットできます。オプションで16:9から9:16へのリフォーマット(フェイスパンまたはスプリットスクリーン)に対応し、Opus風の単語ごとのキャプションを焼き込みます。ユーザーが「clipify」「このビデオからクリップをカットして」「これからショーツを作って」「面白い瞬間を見つけて」「9:16にリフレーミングして」「縦型クリップ」と言及したり、ビデオファイルパスを貼り付けてSNS対応のクリップを求める場合に使用します。
speech
ユーザーが音声生成、ナレーション、アクセシビリティ対応の読み上げ、音声プロンプト、またはOpenAI Audio APIによるバッチ音声生成をリクエストした場合に使用します。組み込みボイスを備えたバンドルCLI(`scripts/text_to_speech.py`)を実行でき、ライブ呼び出しには`OPENAI_API_KEY`が必要です。カスタムボイスの作成には対応していません。
depth-estimation
Depth Anything v2を使用したリアルタイム深度マップのプライバシー変換(CoreML + PyTorch対応) このスキルは、Depth Anything v2モデルを活用して、画像やビデオから取得した深度情報をリアルタイムで処理し、プライバシーを保護しながら変換します。CoreMLとPyTorchの両方に対応しており、エッジデバイスでの高速処理とクラウド環境での柔軟な運用が可能です。顔認識データのぼかしや背景の匿名化など、プライバシー関連の処理を効率的に実行できます。