ARKitを使いつつカメラの制御がどこまでできるか調べてみた

# はじめに

みなさま、あけましておめでとうございます:confetti_ball:

昨年末に[ARKitに関する記事を投稿しました](https://qiita.com/kotetuco/items/8324b1e5a49df68365a1)が、その続きとして、`ARKitを使いながらAVFoundationのようなカメラ制御も同時に行うことができるか？`というテーマで調査を行いました。


# 何をやりたいのか？

- AR処理を行いつつ、裏でカメラ画像を使って画像処理や画像認識処理を行いたい。
    - ARKitが提供する水平面検出や特徴点認識を併用できると面白いことができそう？
- AVFoundationで可能な解像度設定やフォーカス設定などの高度なカメラ設定も行いたい。

# 解決する必要のある課題

1. ARKitを使ってカメラのフレームバッファを取得する方法はあるか？
1. ARKit使用時にカメラ解像度の変更を行うことができるか？
1. ARKit使用時にオートフォーカス関連の設定を行うことができるか？
    - ARKitを使って端末を動かしていると、焦点が合わなくなることがあり、ここからARKit使用時のオートフォーカス設定がどうなっているのか気になったので、調べてみました。


# 調査結果

## ARKitを使ってカメラのフレームバッファを取得する方法はあるか？

- 結論 ： 可能
    - ただし、取得できる画像サイズは `1280x720` のみ。
        - 後述だが、画像サイズの変更は不可能。
    - フレームバッファは下記2種類の方法で取得可能。
        1. `session(_:didUpdate:)`の引数frameのプロパティ`capturedImage`(CVPixelBuffer)を使う。
        1. `ARSCNView.session.currentFrame`(ARFrame)を使う。

## ARKit使用時にカメラ解像度の変更を行うことができるか？

- 結論 : 不可能
    - ARSessionの内部でAVCaptureSessionを使っているので、ARSessionとは別にAVCaptureSessionを作るのは不可能。また、現時点でARSessionからカメラの設定にアクセスする手段が無い。よってカメラ設定の変更は不可能。
        - 参考 : https://forums.developer.apple.com/thread/81301
    - 別なアプローチとして、 `ARCamera.imageResolution` でカメラ解像度を取得することができるが、このプロパティはget-onlyなので、やはり設定はできない。
        - 余談だが、ARCameraは`ARSCNView.session.currentFrame.camera`で取得可能。

## ARKit使用時にオートフォーカス関連の設定を行うことができるか？

- 結論 : 不可能(オートフォーカスは動作していないように見える)
    - `水平面検出に悪影響を与えるため` とのこと。
    - https://forums.developer.apple.com/thread/78954


# まとめ

私の考えていた用途として、元々1280x720より大きなサイズの画像を使いたかったのと、オートフォーカスなどのカメラ制御を必要としていたため、現時点でARKitとカメラ制御・リアルタイム画像処理を両立させるのは難しいという結論に達しました。

ただ、用途的に1280x720サイズの画像でも問題ない場合や、フォーカス周りの設定がデフォルト設定でも問題ない場合には、AR処理と画像処理を併用することが可能ということも言えるのではないかと思います。

解像度固定でなおかつ長辺1280というのは、AR処理をスムーズに行うためにはこのくらい解像度を絞らないといけないのだとしたら、個人的には納得感はあります。もしこの仮説が正しいとすれば、ハードウェアのスペックが向上すれば、将来的にはより大きな解像度設定も可能になるかもしれないですね。


# おまけ : 取得したフレームバッファを使って画像処理を行ってARSCNViewに表示してみた。

ここまで調べて実装例を紹介しないのも寂しいので、取得したフレームバッファに対してCIFilterで画像処理を行って表示してみました。

詳細な実装方法については下記リンク先の記事が大変参考になります(・・・というか、これから書くことはリンク先の記事とほとんど同じです)。

http://appleengine.hatenablog.com/entry/advent20171215
(大変参考になりました。ありがとうございます！)

やりたいのは `定期的にフレームバッファを取得して表示を更新する`ですが、これを実現するために、 `renderer(_:updateAtTime time:)` メソッドを実装します。これは表示内容の更新処理を行うために使用するもので、フレーム毎に呼び出されます。
https://developer.apple.com/documentation/scenekit/scnscenerendererdelegate/1522937-renderer

今回は、renderer(_:updateAtTime time:)メソッド内でフレームバッファに`CIPhotoEffectProcess`フィルタをかけてみました。コードは次のとおりです(抜粋)。

```Swift
private let context = CIContext()

func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    guard let currentFrame = sceneView.session.currentFrame else {
        print("Error: Current frame is nil. [\(#function)]")
        return
    }

    // 表示時には90度回転する
    let ciImage = CIImage(cvPixelBuffer: currentFrame.capturedImage).oriented(.right)
    let imageFilter:CIFilter = CIFilter(name: "CIPhotoEffectProcess")!
    imageFilter.setValue(ciImage, forKey: kCIInputImageKey)

    // background.contentsはCGImageでセットする必要がある
    if let filtImage = imageFilter.outputImage,
        let cgImage = context.createCGImage(filtImage, from: ciImage.extent) {
        sceneView.scene.background.contents = cgImage
    }
}
```

動かしてみた結果は次のような感じになりました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/122081/00ee3ddd-dcee-4555-75dc-d9fedf153d1d.jpeg" width=50%>

紹介したサンプルコードを含むソース一式は下記リポジトリのブランチに置いてあります(リポジトリには他の検証で使用したソースもそれぞれブランチを切ってアップしてあります)。
https://github.com/kotetuco/ARKit-Sample/tree/frame-buffer