📷

SwiftUIとVision Framework、Google Gemini APIで作る名刺読み取りアプリ

に公開

はじめに

名刺の情報を手動で連絡先に入力するのは面倒な作業ですよね。そこで今回は、SwiftUIを使って、Vision FrameworkとGoogle Gemini APIを組み合わせた高精度な名刺読み取りアプリを開発しました。

単純なOCRだけでなく、AIによる情報の構造化まで行うことで、実用的な名刺読み取り機能を実現しています。

完成したアプリの特徴

主な機能

  • リアルタイム名刺検出: カメラでリアルタイムに名刺の輪郭を認識
  • 自動フォーカス: 名刺が適切な位置に来ると自動で撮影
  • 高精度OCR: Vision Frameworkによる日本語文字認識
  • AI構造化: Google Gemini APIで抽出した文字列を構造化データ(JSON)に変換
  • 白飛び検出: 画像品質チェックで読み取り精度を向上

UX/UI設計のポイント

  • フルスクリーンカメラで没入感のある体験
  • オーバーレイ表示で読み取り領域を明確化
  • 視覚的フィードバック(赤→水色のライン変化)
  • 触覚フィードバック(撮影完了時のバイブレーション)

技術スタック

SwiftUI + Vision Framework + Google Generative AI
  • フレームワーク: SwiftUI
  • 画像処理: Vision Framework(Rectangle Detection + OCR)
  • AI連携: Google Gemini API (gemini-1.5-flash)
  • 開発言語: Swift
  • 対応OS: iOS 14.0以上

アーキテクチャ設計

データフロー

MVVMパターンの採用

ContentView.swift
// ViewModelでデータ管理
class CardViewModel: ObservableObject, MyDataReceiverDelegate {
    @Published var cards: [Card] = [...] // 名刺情報
    @Published var image: Image = Image("Download") // 読み取った画像
    
    func imageReceived(data: UIImage) { ... }
    func mapReceived(data: [Card]) { ... }
}

// Viewは状態変化に自動反応
struct ContentView: View {
    @StateObject private var viewModel = CardViewModel()
    // ...
}

実装のポイント

1. リアルタイム名刺検出

Vision FrameworkのVNDetectRectanglesRequestを使用して、カメラフレームから名刺型の四角形を検出します。

MedicalCardScannerViewController.swift
private func findBestRectangleMatch(_ rectangles: [VNRectangleObservation]) -> VNRectangleObservation? {
    let idealAspectRatio: CGFloat = 91.0 / 55.0 // 名刺の標準的なアスペクト比
    
    return rectangles.min(by: { rect1, rect2 in
        let aspectRatio1 = rect1.boundingBox.width / rect1.boundingBox.height
        let aspectRatio2 = rect2.boundingBox.width / rect2.boundingBox.height
        
        return abs(aspectRatio1 - idealAspectRatio) < abs(aspectRatio2 - idealAspectRatio)
    })
}

2. 安定した自動撮影

名刺が適切な位置に1秒間維持されたときに自動撮影する仕組みを実装。

MedicalCardScannerViewController.swift
private func checkForMatch(_ rectangle: VNRectangleObservation, pixelBuffer: CVPixelBuffer) {
    // サイズ比較で位置の安定性をチェック
    let isWithinSizeThreshold = widthDifference <= matchPercentageThreshold ||
                               heightDifference <= matchPercentageThreshold
    
    if isWithinSizeThreshold {
        if matchingStartTime == nil {
            matchingStartTime = Date()
            overlayView.setMatchStatus(true) // 水色に変更
        }
        
        // 1秒間安定したら撮影実行
        if let startTime = matchingStartTime,
           Date().timeIntervalSince(startTime) >= matchThresholdSeconds {
            // 撮影処理...
        }
    }
}

3. 高精度OCR設定

日本語に最適化されたOCR設定で文字認識精度を向上。

MedicalCardScannerViewController.swift
func ocrRequest(_ image: UIImage) {
    let request = VNRecognizeTextRequest { ... }
    
    request.recognitionLanguages = ["ja-jp"]
    request.recognitionLevel = .accurate  // 高精度モード
    request.usesLanguageCorrection = true // 言語補正を有効化
}

4. Gemini APIでの構造化

OCRで抽出した文字列をGoogle Gemini APIで構造化データに変換。

MedicalCardScannerViewController.swift
func callGemini(original: String) async -> String {
    let model = GenerativeModel(name: "gemini-1.5-flash", apiKey: APIKey.default)
    
    let prompt = """
    以下の文字列は日本語の名刺をOCRで読み取った結果です。
    文字列には、'会社名'、'部署名'、'役職名'、'郵便番号'、'住所'、'電話番号'、'内線番号'、'携帯番号'、'FAX番号'、'URL'、'メールアドレス'が含まれています。
    '郵便番号'は先頭に'〒'がついている場合があります。
    '電話番号'、'携帯番号'、'FAX番号'は数字で、区切り記号として' '、'-'、'('、')'が使われることがあります。
    '電話番号'で検出された'b'は'6'に変換してください。
    '電話番号'で検出された'D'は'0'に変換してください。
    '電話番号'で検出された'S'は'5'に変換してください。
    
    抽出された項目をJSON文字列に変換してください。
    JSONの項目名を、'会社名'は'company'、'部署名'は'division'、'役職名'は'title'、'郵便番号'は'zipcode'、'住所'は'address'、'電話番号'は'tel'、'内線番号'は'ext'、'携帯番号'は'mobile'、'FAX番号'は'fax'、'URL'は'url'、'メールアドレス'は'email'にしてください。
    
    '\(original)'
    """
    
    let response = try await model.generateContent(prompt)
    return response.text ?? ""
}

5. 画像品質チェック

白飛びを検出して、読み取り精度の低下を防ぐ機能も実装。

MedicalCardScannerViewController.swift
func detectBlownHighlightsByLuminance(in image: UIImage, luminanceThreshold: Double = 250.0) -> [CGPoint] {
    // RGBから輝度計算
    let luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
    
    if luminance >= luminanceThreshold {
        blownOutPoints.append(CGPoint(x: x, y: y))
    }
}

SwiftUIとUIKitの連携

カメラ機能はUIKitで実装し、SwiftUIから呼び出す構成になっています。

ContentView.swift
struct NameCardViewControllerWrapper: UIViewControllerRepresentable {
    var delegate: MyDataReceiverDelegate

    func makeUIViewController(context: Context) -> MedicalCardScannerViewController {
        return MedicalCardScannerViewController(delegate: delegate)
    }

    func updateUIViewController(_ uiViewController: MedicalCardScannerViewController, context: Context) {}
}

データの受け渡しはDelegateパターンで実装し、ViewModelが受け取った結果をSwiftUIのViewに反映します。

パフォーマンス最適化

1. 非同期処理の活用

  • カメラ処理:バックグラウンドキューで実行
  • OCR処理:グローバルキューで実行
  • UI更新:メインキューで実行

2. メモリ管理

  • CVPixelBufferの適切な管理
  • Core Imageのコンテキスト再利用

セットアップ方法

1. Google Gemini API キーの設定

2. カメラ権限設定

Info.plist
<key>NSCameraUsageDescription</key>
<string>名刺を読み取るためにカメラを使用します</string>

アプリ画面の紹介

画面名 説明 画面
アプリ起動時 起動直後の画面 アプリ起動時
カメラ画面(スキャン中) フルスクリーンカメラビューに名刺フレームオーバーレイ
名刺検出中は赤いラインで表示
リアルタイム矩形検出の様子を示す
スキャン中
カメラ画面(検出完了) 名刺が適切な位置に配置された状態
水色ラインで撮影準備完了を表示
1秒後の自動撮影前の瞬間
検出完了
検出結果表示 名刺画像と抽出された11項目のデータが一覧表示
読み取り結果の確認とデータの活用が可能
結果表示

今後の改善点

  1. CoreMLモデルの導入: オフラインでの文字認識
  2. 連絡先アプリとの連携: 読み取り結果の直接保存
  3. 複数言語対応: 英語名刺の読み取り
  4. バッチ処理: 複数枚の名刺一括処理

まとめ

Vision FrameworkとGoogle Gemini APIを組み合わせることで、高精度な名刺読み取りアプリを開発できました。

特に以下の点が実用性向上に寄与しています:

  • リアルタイム検出による直感的なUX
  • AI構造化による高精度なデータ抽出
  • 品質チェックによる読み取りエラーの削減

SwiftUIとVision Frameworkの組み合わせは、このような画像処理アプリの開発において非常に強力です。ぜひ参考にして、独自の画像認識アプリを開発してみてください。

参考リンク

https://github.com/masaaki-hori/NameCardDemo
https://developer.apple.com/documentation/vision
https://ai.google.dev/tutorials/swift_quickstart


この記事が少しでもお役に立てば幸いです。質問やフィードバックがあれば、お気軽にコメントください!

株式会社BALEEN STUDIO

Discussion