見出し画像

自分が一万円の顔に!?Vision Proで新一万円札AR作ってみた(前編)

※本内容は、visionOS 2.0のリリース日程が決定する以前に書かれたものになりますのでご注意ください。開発環境等は、記事内に記載しております。

Vision Proには、Personaという機能が存在します。これを使って、自分が新一万円札の肖像と入れ替われるアプリを作ってみました。試作アプリなのでリリースしていないのですが、visionOS 2.0の機能も利用して面白いものにできたと思います。

実際の動画の前に、Personaとはなんぞや

PersonaはVision Proを装着している人がFaceTimeなどの会話アプリを利用する際に、事前にVision Proで生成した3Dモデルを実写の代わりに表示できる公式の機能です。この3Dモデルはリアルタイムで表情が変わるため、実際に面と向かっているかのようにコミュニケーションできます。公式のビデオガイド動画の6:00あたりを見ていただくとどういうものかわかると思います。また、この機能もvisionOS 2.0以降でさらに改善されており、より立体的な映像として表示できるようになっています。(こちらの公式記事内の動画がわかり良いです。)

実際の動画をどうぞ

とりあえず最終版がどのようなものなのか実際の動画をご覧ください!

★クリックして動画を見る★

リアルタイムで、新一万円札をトラッキングして、ある程度の動きにも追従して重ね合わせができていると思います。

実装のワークフロー

実装のワークフローとしては、

  1. Personaの画像をリアルタイムで取得

  2. Personaの画像を加工する

  3. リファレンスファイルの作成

  4. Tracking Providerから検出したAnchorを取得

  5. アンカーの座標に画像や3Dモデルを位置合わせする

の5つの工程があります。簡易的に図示すると下のようなワークフローです。

ここからは、それぞれの工程についての解説とつまずいたところなどについて話していきたいと思います。少し記事が長くなるのでそれぞれの工程で利用した技術をざっくりと提示しておきます。知りたい情報にアクセスする際の手がかりにしていただけたらと思います。

  1. Personaの画像をリアルタイムで取得

    • AVFundation

      • AVCaptureSession(ペルソナの動画取得)

  2. Personaの画像を加工する

    • Visionフレームワークを利用した画像加工

      • VNGenerateForegroundInstanceMaskRequest(背景削除)

      • VNGeneratePersonInstanceMaskRequest(背景削除)

    • CIFilter(画像の色味加工)

  3. リファレンスファイルの作成

    • CreateML

      • Spatialテンプレート

  4. Tracking Providerから検出したAnchorを取得(この章からパート2)

    • ARKit

      • ImageTrackingProvider

      • ObjectTrackingProvider

  5. アンカーの座標に画像や3Dモデルを位置合わせする

    • RealityKit

      • Attachments(Viewの重ね合わせ)

      • ShaderGraphMaterial(画像の反映)

開発環境

今回開発した環境は基本的には下の通りです。開発時に環境をアップデートしているので複数あるものもありますが、リファレンスファイル作成以外の部分において、制約はないので大体VvsionOS 2.0で動かす環境なら動くという認識で問題ないと思います。また、今回のアプリは3Dモデル以外全てをSwiftUIおよびXcodeのツールを利用して開発しました。

Vision Pro: visionOS 2.0 beta4
macOS: 15.0beta,14.5
Xcode: 16.0beta2,16.0beta4
(RealityComposerPro:2.0)

1. Personaの画像をリアルタイムで取得

PersonaはvisionOS 1.1以降で対応になっているため、もし対応していないバージョンの場合はOSのアップデートをこなう必要があります。また、Personaを利用するための初期設定が必要になりますのでアップル公式の記事を参考にしながら設定を行なってください。

Personaの動画の取得方法は、こちらの方の記事を参考にさせていただきました。大半はこの記事と同じになるため、詳細はこちらの方の記事を参考いただければと思います。

基本的には、iOSでカメラの動画情報を取得する方法と同じです。記事内にもありますが、AVCaptureDevice.systemPreferredCamera のカメラの映像がvisionOSではPersonaの映像を取得できるカメラになっています。

注意点として、アプリを動かすためにカメラの利用パーミッションが必要です。Xcode内でCameraUsageDescriptionのパーミッション設定を行い、利用用途をinfo.plistに加筆する必要があります。

また、参考記事内ではPersonaのデータをUIImageとして出力していますが、今回Personaの画像を加工する必要があるため、フィルターなどの加工を行うときに取り回しが良いCIImageを最終出力しとして利用しています。

// CIImageを受けるコールバック    
var callback: ((CIImage)->Void)?

// カメラ映像はAVCaptureVideoDataOutputSampleBufferDelegateを使って取得
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
            let cameraCIImage = CIImage(cvPixelBuffer: pixelBuffer)
            callback?(cameraCIImage)
        }
}

これでPersonaの画像が取得できました。PersonaをViewに表示すると下の動画のようになります。

★クリックして動画を見る★

この動画で注目したい点は、2点あります。
一つ目は、Viewを移動した際にPersonaの顔の角度が変わっている点です。visionOS 1.0系でこの仕様であるのか把握していないのですが、visionOS 2.0では生成されたWindowGroup内のViewがPersonaのカメラになっており、Viewを移動させることでPersonaの角度が変更できます。View以外で明示的にカメラ位置を変更できるかは調査中ですが、WWDC23のSharePlayに関する講演でSharePlayを行う場合のテンプレートについて言及があり、Viewの位置によってPersonaの位置が変えられることが言及されているので、Personaの角度についてはこのSharePlayの影響によるものかと思っています。

二つ目は、背景が存在する点です。この背景は設定アプリのPersonaの項目で、変更することができ、デフォルトでは絵画がある部屋が背景画像として選択されています。Persona一万円札ARでは背景を削除する加工を行うため、できる限り背景が単純なもの選択して撮影しています。

2. Personaの画像を加工する

画像の加工は、背景の削除とセピア色にフィルターする二つを行いました。
セピア色のフィルターについては、CIFilterのCISepiaToneを利用してフィルターをかけています。

let ciFilter = CIFilter(name: "CISepiaTone")!
ciFilter.setValue(source, forKey: kCIInputImageKey)
ciFilter.setValue(0.8, forKey: kCIInputIntensityKey)
if let image = ciFilter.outputImage {
   return image
}else{
  print("dont have image")
}

VisionフレームワークAPI

背景画像の削除にはVisionフレームワークを利用しました。試したAPIは2種類になります。一つは、人間を検出してマスクを生成するVNGeneratePersonInstanceMaskRequest もう一つは、前景(画像内の主要オブジェクト)を検出してマスクを生成するVNGenerateForegroundInstanceMaskRequestです。WWDC23の動画を参考にして作成しました。この二つを行うと下のような画像が得られます。

Personaの画像に二つを適用すると、ほぼほぼ同じような結果になるのですが、VNGeneratePersonInstanceMaskRequestの方は人間っぽいものを検出するので胴体部分を過剰に検出したマスクを生成する傾向にあり、VNGenerateForegroundInstanceMaskRequestは後ろの背景を前景として認識する場合があるというデメリットがそれぞれ存在しています。最終的には、Personaの背景を絵画などがある複雑な背景ではなく単純な背景にすることでVNGenerateForegroundInstanceMaskRequestでのデメリットが起き辛くできたので、こちらを採用しました。

    // 前景のみのマスクを生成し、背景削除
    func vnRemoveBackground(ciImage: CIImage) async -> CIImage?{

        // CIImageを取り込む
        let image = ciImage
        
        let request = VNGenerateForegroundInstanceMaskRequest()
        
        let allDevices = MLComputeDevice.allComputeDevices
        
        if computeDevice == nil{
            for device in allDevices {
                print("device.description\(device.description)")
                if(device.description.contains("MLGPUComputeDevice")){
                    computeDevice = device
                    break
                }
            }
        }
            
        guard let device = computeDevice else {
            print("dont find computeDevice")
            return nil
        }
        request.setComputeDevice(.some(device), for: .main)
        
        let handler = VNImageRequestHandler(ciImage: image)
        do{
            try handler.perform([request])
        }catch{
            print("NoSubject Observations found.\(error)")
            return nil
        }
        
        guard let result = request.results?.first else{
            print("results not found.")
            return nil
        }

        guard let output = try? result.generateMaskedImage(
            ofInstances: result.allInstances,
            from: handler,
            croppedToInstancesExtent: false) else{
            print("cant generate mask")
            return nil
        }
        
        let maskedImage = CIImage(cvPixelBuffer: output)
        return maskedImage
    }
    // 人間のみのマスクを生成し、背景削除
    func humanDetect(ciImage: CIImage) async -> CIImage?{

        // CIImageを取り込む
        let image = ciImage

        let request = VNGeneratePersonInstanceMaskRequest()

        let allDevices = MLComputeDevice.allComputeDevices

        if computeDevice == nil{
            for device in allDevices {
                if(device.description.contains("MLGPUComputeDevice")){
                    computeDevice = device
                    break
                }
            }
        }

        guard let device = computeDevice else {
            print("dont find computeDevice")
            return nil
        }
        request.setComputeDevice(.some(device), for: .main)

        let handler = VNImageRequestHandler(ciImage: image)
        do{
            try handler.perform([request])
        }catch{
            print("NoSubject Observations found.\(error)")
            return nil
        }

        guard let result = request.results?.first else{
            print("results not found.")
            return nil
        }

        guard let output = try? result.generateMaskedImage(
            ofInstances: result.allInstances,
            from: handler,
            croppedToInstancesExtent: false) else{
            print("cant generate mask")
            return nil
        }

        let maskedImage = CIImage(cvPixelBuffer: output)
        return maskedImage
    }

VisionフレームワークのVisonOS2.0での不具合

今回の開発では、Visionフレームワーク周りで2つの不具合に当たり、最終的にこの形になっています。この記事が公開されているときには修正されている可能性もありますが、共有しておきます。

一つ目の不具合としては、WWDC24で発表されたbeta版の新しいVisionフレームワークが利用できないことです。こちらのドキュメントにはvisionOS 2.0から利用可能という事が書かれていますが、エラーが出てしまいます。Forum上にissue立てをして議論されていますが、現状betaのVisionフレームワークを利用できていません。(もし、利用できた方いたら利用方法を教えて欲しいです)

二つ目は、従来のVisionフレームワーク(VNがpreffixにあるもの)も、通常通り利用するとエラーが出る点です。こちらについては解決方法がわかっており、先ほど挙げたForumでworkaroundが提示されています。解決策の該当する部分は下記のコードの部分になります。利用するComputeDeviceをGPUにしてrequestを作成するとエラーなく利用できました。

        let allDevices = MLComputeDevice.allComputeDevices
        
        if computeDevice == nil{
            for device in allDevices {
                print("device.description\(device.description)")
                if(device.description.contains("MLGPUComputeDevice")){
                    computeDevice = device
                    break
                }
            }
        }
            
        guard let device = computeDevice else {
            print("dont find computeDevice")
            return nil
        }
        request.setComputeDevice(.some(device), for: .main)
        

ここからは定かではない推測ですが、visionOS 2.0から機械学習を行うためのComputeDeviceとしてNeural Engineというものが追加されており、この利用するためにはEnterpriseAPIが必要になります。
EnterpriseAPIを利用するにはAppleに申請して許可を取る必要があるため、今回のこの不具合もそれに関係するものかなーと思っています。最終的にリリースされるタイミングでは直っていると思うので、あまり気にする必要はないかもしれません。

3. リファレンスファイルの作成

今回、位置合わせにはImageTrackingとvisionOS 2.0から追加されたObjectTrackingの二つを試し、ObjectTrackingを採用しました。本来であれば、紙のトラッキングなのでImageTrackingを利用すべき場面だと思うのですが、ImageTrackingのfpsや精度が現状ではそこまで良くなかったのとObjectTrackingについてはEnterpriseAPIがあれば精度の問題が解決できるため、将来性を見越してObjectTrackingを利用しました。

ImageTrackingとObjectTrackingの精度について

ImageTrackingはvisionOS 1.0から利用できる画像をリファレンスファイルとして、画像認識して空間アンカーを生成するAPIです。対して、ObjectTrackingはvisionOS 2.0から導入されたusdzファイルから生成したリファレンスファイルを利用して、立体物を認識して空間アンカーを生成するAPIです。
従来のiOSでのARKitであれば、カメラのフレームレート毎に処理を行うところだと思うのですが、ImageTrackingおよびObjectTrackingにおいては、それぞれでカメラのフレームレートよりも低い値でトラッキングを行なっています。

ImageTrackingは、ForumにおいてImageTrackingのフレームレートが低いことに対してのスレッドがいくつかあり、解決方法としてはフィードバックを上げて欲しいというものになっています。(該当Forum)
要望が届けば、今後のバージョンアップでフレームレートについても、改善があるかもしれません。

ObjectTrackingについては少し複雑です。まず、ObjectTrackingにはImageTrackingと異なり、
下記のようなTrackingConfigurationという構造体がプロパティとして存在しています。

   /// A structure containing parameters to change object tracking behavior.
    /// An enterprise license is required to modify the tracking configuration, and will be otherwise be a no-op.
    /// The app must include the following entitlement:
    ///  com.apple.developer.arkit.object-tracking-parameter-adjustment.allow
    public struct TrackingConfiguration : CustomStringConvertible {

        /// The total number of object instances that can be tracked at the same time.
        public var maximumTrackableInstances: Int

        /// How many instances of each reference object type to allow tracking at once.
        public var maximumInstancesPerReferenceObject: Int

        /// The frequency at which object detection runs, in Hz. Clamped between 0 and 30 Hz.
        public var detectionRate: Float

        /// The frequency at which object tracking runs for stationary objects, in Hz. Clamped between 0 and 30 Hz.
        public var stationaryObjectTrackingRate: Float

        /// The frequency at which object tracking runs for moving objects, in Hz. Clamped between 0 and 30 Hz.
        public var movingObjectTrackingRate: Float

        /// A textual representation of this tracking configuration.
        public var description: String { get }

        /// Initializes all parameters with default values.
        public init()
    }

この構造体では、detectionRate、stationaryObjectTrackingRate、movingObjectTrackingRateのフレームレートに関する三つの値を設定でき、それぞれが0-30Hzまで変更できることがコメントで書かれています。

しかし、TrackingConfigurationの上部のコメントを見るとenterprise license is required to modify the tracking configurationと書かれており、EnterpriseAPIがないとこの値が変更できないことがわかります。(実際に、EnterpriseAPIがない状態だと変更できなかったです。)

では、デフォルトならどのような値になっているかというと…コード上で確認するとdetectionRate: 2.0, stationaryObjectTrackingRate: 5.0, movingObjectTrackingRate: 5.0という設定になっていました。現状、動画等で visionOSのObjectTrackingを行なっているものは、この設定のものがほとんどだと思われます。今回のアプリもEnterpriseAPIを利用していない状態での実装になっているのでこのフレームレートでトラッキングが行われています。

ObjectTrackingのリファレンスファイルの作成

ObjectTrackingのリファレンスファイルの作成は、いくつかの工程に分かれています。詳しくは、下の記事に詳しく載っているのでそちらをまず参照してみてください。

今回のアプリで、上記で紹介した記事内容以外で行ったこととしては、紙上のものをリファレンスファイルとして生成するために、どのようにusdzファイルを用意するか?という部分です。

トライしたこととしては、blenderでCubeを平たく生成したusdzファイルを利用してCreateMLのSpatialテンプレートを利用する方法です。こちらは、何度か試みてインポートまではできるのですが、機械学習を行うタイミングでエラーが出て利用できませんでした。CreateMLのバージョンにもよるかもしれませんが、ObjectTrackingに関する公式動画内のようにiOSでObjectCaptureしたusdzファイルが必要なのかもしれません。

blenderで作成した新紙幣のusdzファイル

次に、下のような装置を作成し紙幣をどうにかしてObjectCaptureできないかトライしました。この装置で何度か試し、最終的にリファレンスファイルを作成する事ができました。

飛沫防止用のアクリルに紙幣を貼り付けて撮影


実際に使用したusdzファイル

これで、ObjectTrackingを行うための準備が整いました。
話が少し長くなってしまったため、次の記事で実際にObjectTrackingのアンカーを取り出して、重ね合わせを行う4,5の工程について話たいと思います。

↓続き
自分が一万円の顔に!?VisionProで新一万円札AR作ってみた(後編)

いいなと思ったら応援しよう!