【アプリ開発】Swift Playgroundsを使ったアイトラッキングアプリに挑戦

Swift

アイトラッキング技術は、ユーザーの視線を追跡し、その情報を活用することで、より直感的なユーザーインターフェースやアクセシビリティの向上など、さまざまな可能性を秘めています。この記事では、iPadとSwift Playgroundsを使用して基本的なアイトラッキングアプリを作成する過程を通じて、私が学んだことを共有します。

前提条件

SwiftというiOSアプリを開発する言語を用いてアイトラッキングを実装しようとする場合、「ARKit」というものを使用します。ARKitを使用してiOSデバイス上でアイトラッキング機能を利用するためには、いくつかの要件を満たす必要があります。これらの要件には、以下のようなものが含まれます。

対応するiOSデバイス

アイトラッキング機能を利用するためには、TrueDepthカメラを備えたiOSデバイスでのみ使用可能です。TrueDepthカメラとは、iPhone X以降のモデルに搭載されている特殊なカメラシステムです。顔認識、表情追跡、その他の拡張現実(AR)機能に使用されています。アイトラッキングの場合、このカメラはユーザーの目の位置、視線方向、まばたきなどを検出するのに使われます。

iOSバージョン

アイトラッキング機能をサポートするためには、最新のiOSバージョンが必要な場合があります。ARKitは定期的にアップデートされ、新しい機能が追加されるため、最新のiOSバージョンを使用することが重要です。

Swift playgroundsの基本的な使い方

下記のアプリをストアからダウンロードし、ダウンロードが完了したらSwift Playgroundsのアプリを開いてください。

アプリを開くと、下記のような画面が表示されますので、+アイコンをタッチして、新しいアプリを作成するためのエディタを開いてください。

下記のように操作することで、ファイルを増やすことができます。

今回は「MyApp.swift」「ContentView.swift」「ARViewContainer.swift」「ViewController.swift」という4つのファイルに分けて作成していこうと思うので、その名前のファイルを作成しました。プロジェクトや作りたいものによって、ファイルの数も名前も適したものにする必要がありますが、とりあえず今回はこれで準備完了です!

実際のコード

ここからは先ほど作成した4つのファイルそれぞれに実際にコードを書いていきます。

まず「MyApp.swift」に書き込むコードの中身はこちらです↓

// MyApp.swift
import SwiftUI

@main
struct YourAppName: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

このファイルは、アプリのスタート地点です。アプリを開いたときに何が表示されるかを決めます。今回のコードでは、アプリが始まるときに最初に表示する画面(ContentView)を設定します。

次に「ContentView.swift」に書き込むコードの中身はこちらです↓

// ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        ARViewContainer()
            .edgesIgnoringSafeArea(.all)
    }
}

このファイルは、アプリの主要な見た目を作ります。ここで、ARを使った特別な画面を表示するように設定します。今回の場合、ここで特別なAR画面(ARViewContainerから来る)をアプリの画面いっぱいに表示します。

次に「ARViewContainer.swift」に書き込むコードの中身はこちらです↓

// ARViewContainer.swift
import SwiftUI

struct ARViewContainer: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> ViewController {
        return ViewController()
    }
    
    func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}

このファイルは、あなたのアプリで特別な画面(ARを使った画面)を表示するための箱のようなものです。新しい特別な画面を作るための指示を含み、特別な画面が更新されるたびに何をすべきかを指示していますが、今回の場合は特に何もしません。

最後に「ViewController.swift」に書き込むコードの中身はこちらです↓

// ViewController.swift
import UIKit
import ARKit

// ViewControllerクラス: UIViewControllerを継承し、画面の制御を行います。
final class ViewController: UIViewController {
    
    // ARセッション: ARKitの機能を使用するためのセッションを管理します。
    private let session = ARSession()

    // 視線追跡点を表示するUIImageView: ここでは目のアイコンを使用しています。
    private var lookAtPointView: UIImageView = {
        let image = UIImageView(image: .init(systemName: "eye")) // アイコンの種類を変更する場合、"eye"を他のシステム名に変更します。
        image.frame = .init(origin: .zero, size: CGSize(width: 30, height: 30)) // アイコンのサイズを変更する場合、ここのwidthとheightを調整します。
        image.contentMode = .scaleAspectFit
        return image
    }()
    
    // 最後に追跡された視線のポイントを保持する変数。
    private var lastLookAtPoint: CGPoint?
    
    // 視線のドットの色: 初期値は赤色です。
    private var dotColor: UIColor = .red // ドットの色を変更する場合は、ここを任意のUIColorに変更します。
    
    // 最後に検出された顔のアンカーを保持する変数。
    private var lastFaceAnchor: ARFaceAnchor?
    
    // ビューがメモリにロードされた後に呼び出されるメソッド。
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(lookAtPointView) // 視線追跡点のビューを画面に追加します。
        session.delegate = self // ARセッションのデリゲートを自身に設定します。
    }
    
    // ビューが画面に表示される直前に呼び出されるメソッド。
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let configuration = ARFaceTrackingConfiguration() // 顔追跡用のARセッション設定を作成します。
        configuration.isLightEstimationEnabled = true // 環境光の推定を有効にします(必要に応じて変更可能)。
        session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) // ARセッションを開始します。
    }
}

// ARSessionDelegateプロトコルに準拠する拡張部分。
extension ViewController: ARSessionDelegate {
    
    // ARセッションが新しいフレームを受け取るたびに呼ばれるメソッド。
    func session(_ session: ARSession, didUpdate frame: ARFrame) {
        guard let faceAnchor = frame.anchors.first(where: { $0 is ARFaceAnchor }) as? ARFaceAnchor else {
            return // 顔のアンカーが検出されなかった場合、ここで処理を中断します。
        }
        lastFaceAnchor = faceAnchor // 検出された顔のアンカーを保存します。

        let orientation = currentDeviceOrientation() // デバイスの現在の向きを取得します。
        
        // 顔の視線ポイントをカメラ座標からビュー座標に変換します。
        let lookingPoint = frame.camera.projectPoint(faceAnchor.lookAtPoint,
                                                     orientation: orientation,
                                                     viewportSize: view.bounds.size)
        
        // 補間率を調整して視線の動きを滑らかにします。
        let interpolationRate: CGFloat = 0.95 // 補間率を高くすることで動きを滑らかにします(0.8から1.0の範囲で調整可能)。
        let smoothLookAtPoint = lastLookAtPoint.map { lerp(start: $0, end: lookingPoint, t: interpolationRate) } ?? lookingPoint
        lastLookAtPoint = smoothLookAtPoint // 新しい視線追跡点を保存します。
        
        DispatchQueue.main.async {
            // 視線追跡点をビューポート内に制限して、中心点を設定します。
            self.lookAtPointView.center = self.clampToViewport(point: smoothLookAtPoint)
        }
    }
    
    // 線形補間関数。2点間を滑らかに補間します。
    private func lerp(start: CGPoint, end: CGPoint, t: CGFloat) -> CGPoint {
        return CGPoint(x: start.x + (end.x - start.x) * t, y: start.y + (end.y - start.y) * t)
    }
    
    // ポイントをビューポート内に制限するための関数。
    private func clampToViewport(point: CGPoint) -> CGPoint {
        return CGPoint(
            x: min(max(point.x, 0), view.bounds.width),
            y: min(max(point.y, 0), view.bounds.height)
        )
    }
    
    // 現在のデバイスの向きを取得する関数。
    private func currentDeviceOrientation() -> UIInterfaceOrientation {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .unknown
    }
}

このファイルは、アプリの心臓部のようなものです。ここで、カメラを使ってどこを見ているかを追跡し、その情報に基づいて画面上に何を表示するかを決めます。アプリが始まるときに必要な設定を行い、カメラを使ってユーザーの顔の動きを追跡します。さらに、ユーザーがどこを見ているかに基づいて、小さなアイコン(目の形をしたアイコン)を動かして表示します

デバイスの画面サイズ

このコードではデバイスの画面サイズやデバイスと顔との距離について直接取得しているわけではありません。ただし、これらの要素は間接的にARKitの機能として扱われています。

今回のコードでは view.bounds.size を使用しています。これは現在のビュー(つまりアプリの画面)のサイズを表します。これはデバイスの画面サイズに等しいわけではありませんが、画面いっぱいにビューが表示されている場合、ビューのサイズは画面サイズと同じになります。ARKitが使用するビューのサイズは、視線追跡点を画面上の正しい位置に投影するために重要です。

視線追跡点の動きの補間

このコードにおける「補間」は、視線追跡点の動きを自然に見せるために非常に重要です。補間がないと、視線追跡点の動きは非常に突然かつ不自然になります。

lerp 関数について

  • 機能: lerp 関数は「線形補間」という手法を使って、2点間の中間点を計算します。簡単に言えば、2つの位置の間の滑らかな移動を作成するための計算を行います。
  • 補間がない場合: この機能がなければ、視線追跡点は一瞬で新しい位置にジャンプすることになり、この動きは非常に不自然に見えます。ユーザーが見ることになるのは、画面上を瞬時に動く点になります。

interpolationRate による補間

  • 機能: interpolationRate は、lerp 関数がどの程度前の点から新しい点に移動するかを決定します。この値が高いほど、点は新しい位置に近づきますが、完全には到達しません。これにより、点の移動が滑らかになります。
  • 補間がない場合: interpolationRate が低い(例えば0)場合、点は常に古い位置に留まり、新しい位置に移動しません。逆に、この値が高すぎる(例えば1)と、点は新しい位置にすぐにジャンプし、これもまた不自然な動きになります。

簡単に言うと、これらの機能は視線追跡点の動きを自然で滑らかに見せるために重要です。

実行結果の確認

このコードを実行してみると、下記のように、目線に合わせて目のアイコンが動くものを作成することができました。ただ、あまり視線の検知精度が良くなくて、どちらかというと視線の向きというより、顔の向きを検知しているような結果になりました。

まとめ

このプロジェクトを通じて、アイトラッキングの基本的な概念を学びました。技術の迅速な進歩により、これまで専門家の領域だったような高度な機能も、今では誰でも手軽に試すことができます。私の経験が、同じように新しい技術を学び、探求したいと思っている皆さんの一助となれば幸いです。

コメント