ARKit を使ってPythonista3 で拡張現実世界の平面をトラッキングしたい

この記事は、[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) の20日目の記事です。

https://qiita.com/advent-calendar/2022/pythonista3

一方的な偏った目線で、Pythonista3 を紹介していきます。

ほぼ毎日iPhone（Pythonista3）で、コーディングをしている者です。よろしくお願いします。

以下、私の2022年12月時点の環境です。

```sysInfo.log
--- SYSTEM INFORMATION ---
* Pythonista 3.3 (330025), Default interpreter 3.6.1
* iOS 16.1.1, model iPhone12,1, resolution (portrait) 828.0 x 1792.0 @ 2.0
```

他の環境(iPad や端末の種類、iOS のバージョン違い)では、意図としない挙動(エラーになる)なる場合もあります。ご了承ください。

ちなみに、`model iPhone12,1` は、iPhone11 です。

## この記事でわかること

- ARKit のAR World Tracking 処理
- Tracking 情報から、パネルを張る
- `objc_util` の`delegate` 実装

## 現実世界の情報を掴む

前回は、ARKit の最小の実装方法を紹介しました。

最小の実装では、SceneKit で呼び出した`box` は、その場で回転し続けるのみでした。

今回は、カメラからの現実世界の平面を見つけた際に、平面のパネルを呼び出すようにしてみましょう。

SceneKit 側の処理は通常通りなのですが、`objc_util` で呼び出す`delegate` 処理が面倒かつ特殊です。

`objc_util` の`delegate` を中心に見ていきましょう。

### `delegate` ？

「指定の事柄が発生したら呼び出される」処理です。めちゃくちゃざっくりですが。。。

`AVAudioEngine` 回で`Block` 処理を使いましたが、`objc_util` 的には似た面倒さで似た処理です（私的には）。

今回ですと「平面情報を取得したら」の条件下で処理が実行されます。

[create_objc_class | objc_util — Utilities for bridging Objective-C APIs — Python 3.6.1 documentation](https://omz-software.com/pythonista/docs/ios/objc_util.html#objc_util.create_objc_class)

https://omz-software.com/pythonista/docs/ios/objc_util.html#objc_util.create_objc_class

`objc_util.create_objc_class` で、新たに`objc_util` オブジェクトのclass を作ることで`delegate` が実装できます。

参照している実装コードで、Delegate 処理が出てきた場合には、Documentation で検索をし必要な情報を探していきます。
Objective-C、Swift のDelegate に関しては、以下の記事から掘っていくと理解が深まるかもしれません。

[SwiftにおけるDelegateとは何か、なぜ使うのか - Qiita](https://qiita.com/st43/items/9f9990d76cefa1909ef4)

https://qiita.com/st43/items/9f9990d76cefa1909ef4

#### `methods`

`methods` に、`delegate` で処理されるメソッドを指定します。

このメソッドは、`delegate` に存在するメソッド名を定義して、その中で処理を書いていくことになります。

引数には、実在するメソッドの引数の他に`_self, _cmd` を先頭に設置する必要があります。

[renderer:didAddNode:forAnchor: | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arscnviewdelegate/2865794-renderer?language=objc)

https://developer.apple.com/documentation/arkit/arscnviewdelegate/2865794-renderer?language=objc

配列で格納されているように、複数のメソッドを指定できます。

#### `protocols`

`delegate` は、Protocol として定義されているので、呼び出したい`delegate` を指定します。

今回は`ARSCNView` の`delegate` なので、`ARSCNViewDelegate` です。

[ARSCNViewDelegate | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arscnviewdelegate?language=objc)

https://developer.apple.com/documentation/arkit/arscnviewdelegate?language=objc

#### `def create_delegate(self):` メソッド

Pythonista3 の他の`delegate` 実装は、メソッドを外に書いて、メソッド内の変数をグローバル化しています。

実装上は、何の問題もありません。ただ私のこだわりとして、class 内で定義をし、スコープをclass 内に収めています。

処理上で、面倒になってしまった場合には素直に`global` でいいと思います。。。。

## 実装

前回のコードから、コードを追加して実装していきます。

`ViewController` class を定義したのは、この処理から`ARSCNView` でコネコネすることが多くなるので、コードの見通しの観点から`View` class と分けています。

```py
from math import pi

from objc_util import load_framework, ObjCClass, ObjCInstance, create_objc_class
from objc_util import UIColor
import ui

import pdbg

load_framework('ARKit')

ARSCNView = ObjCClass('ARSCNView')
ARWorldTrackingConfiguration = ObjCClass('ARWorldTrackingConfiguration')

SCNScene = ObjCClass('SCNScene')
SCNNode = ObjCClass('SCNNode')
SCNPlane = ObjCClass('SCNPlane')


class GameScene:
  def __init__(self):
    self.scene: SCNScene
    self.setUpScene()

  def setUpScene(self):
    scene = SCNScene.scene()
    self.scene = scene


class ViewController:
  def __init__(self):
    self.sceneView: ARSCNView
    self.scene: GameScene
    self.viewDidLoad()
    self.viewWillAppear()

  def viewDidLoad(self):
    scene = GameScene()

    _frame = ((0, 0), (100, 100))
    sceneView = ARSCNView.alloc().initWithFrame_(_frame)
    sceneView.autoresizingMask = (1 << 1) | (1 << 4)

    _delegate = self.create_delegate()
    sceneView.delegate = _delegate

    sceneView.autoenablesDefaultLighting = True
    sceneView.showsStatistics = True
    ''' debugOptions
    OptionNone = 0
    ShowPhysicsShapes = (1 << 0)
    ShowBoundingBoxes = (1 << 1)
    ShowLightInfluences = (1 << 2)
    ShowLightExtents = (1 << 3)
    ShowPhysicsFields = (1 << 4)
    ShowWireframe = (1 << 5)
    RenderAsWireframe = (1 << 6)
    ShowSkeletons = (1 << 7)
    ShowCreases = (1 << 8)
    ShowConstraints = (1 << 9)
    ShowCameras = (1 << 10)
    ARSCNDebugOptionShowFeaturePoints = (1 << 30)
    ARSCNDebugOptionShowWorldOrigin = (1 << 32)
    '''
    _debugOptions = (1 << 1) | (1 << 30) | (1 << 32)
    sceneView.debugOptions = _debugOptions
    sceneView.scene = scene.scene
    sceneView.autorelease()
    self.scene = scene
    self.sceneView = sceneView

  def viewWillAppear(self):
    self.resetTracking()

  def viewWillDisappear(self):
    self.sceneView.session().pause()

  def resetTracking(self):
    _configuration = ARWorldTrackingConfiguration.new()
    _configuration.planeDetection = (1 << 0)
    self.sceneView.session().runWithConfiguration_(_configuration)

  def create_delegate(self):
    # --- /delegate
    def renderer_didAddNode_forAnchor_(_self, _cmd, renderer, _node, _anchor):
      node = ObjCInstance(_node)
      planeAnchor = ObjCInstance(_anchor)
      _width = planeAnchor.planeExtent().width()
      _height = planeAnchor.planeExtent().height()
      geometry = SCNPlane.planeWithWidth_height_(_width, _height)
      geometry.firstMaterial().diffuse().contents = UIColor.color(
        red=1.0, green=0.0, blue=0.0, alpha=0.8)
      planeNode = SCNNode.nodeWithGeometry_(geometry)
      planeNode.eulerAngles = (-pi / 2.0, 0.0, 0.0)
      node.addChildNode_(planeNode)

    def renderer_didUpdateNode_forAnchor_(_self, _cmd, renderer, node, anchor):
      pass

    # --- delegate/

    _methods = [
      renderer_didAddNode_forAnchor_,
      renderer_didUpdateNode_forAnchor_,
    ]
    _protocols = ['ARSCNViewDelegate']

    renderer_delegate = create_objc_class(
      'renderer_delegate', methods=_methods, protocols=_protocols)
    return renderer_delegate.new()


class View(ui.View):
  def __init__(self, *args, **kwargs):
    ui.View.__init__(self, *args, **kwargs)
    self.bg_color = 'maroon'
    self.vc = ViewController()
    self.objc_instance.addSubview_(self.vc.sceneView)

  def will_close(self):
    self.vc.viewWillDisappear()


if __name__ == '__main__':
  view = View()
  view.present(style='fullscreen', orientations=['portrait'])

```

先に、`objc_util` 側の処理（`delegate`）から見ていきます。

### `delegate` の実装

class `ViewController` に、`create_delegate` メソッドを生やしています。

メソッド内で、Delegate として呼び出すメソッドを定義しています。

`objc_util.create_objc_class` で、class 化し`.new()` でインスタンス化したものを返しています。

`ViewController.viewDidLoad` 上で`ARSCNView.delegate` へ格納することでDelegate として機能します。

```py

def create_delegate(self):
  # objc_util でDelegate を実装するための、メソッド準備
  # --- /delegate 
  # インスタンスメソッド内で、関数を定義する
  # 1つめ
  def renderer_didAddNode_forAnchor_(_self, _cmd, renderer, _node, _anchor):
    node = ObjCInstance(_node)  # 引数の変数を、インスタンス化する

    planeAnchor = ObjCInstance(_anchor) # tracking で得た情報
    _width = planeAnchor.planeExtent().width()
    _height = planeAnchor.planeExtent().height()

    # SceneKit での生成方法と同じ
    geometry = SCNPlane.planeWithWidth_height_(_width, _height)
    geometry.firstMaterial().diffuse().contents = UIColor.color(
      red=1.0, green=0.0, blue=0.0, alpha=0.8)
    planeNode = SCNNode.nodeWithGeometry_(geometry)
    planeNode.eulerAngles = (-pi / 2.0, 0.0, 0.0)
    node.addChildNode_(planeNode) # rootNode は、引数_node をインスタンス化したもの

  # 2つめ
  def renderer_didUpdateNode_forAnchor_(_self, _cmd, renderer, node, anchor):
    # 情報が変更（アップデート） された時の処理を指定できる。
    # add したパネルのNode の面積情報を更新したりなど
    pass
  # --- delegate/ delegate 生成時のメソッド作りおわり

  # 上記で定義した関数を格納
  _methods = [
    renderer_didAddNode_forAnchor_,
    renderer_didUpdateNode_forAnchor_,
  ]
  _protocols = ['ARSCNViewDelegate']

  # 第一引数は、変数名を文字列で指定する
  renderer_delegate = create_objc_class(
    'renderer_delegate', methods=_methods, protocols=_protocols)
  return renderer_delegate.new() # インスタンス化して返す

```

#### `renderer_didAddNode_forAnchor_` の引数

`objc_util` の`delegate` 処理の引数は、往々にしてそのままでは使えません。`ObjCInstance(引数)` とインスタンス化してから、処理をすることになります。

`print` で実体を確認するなどして、変数の状態を確認し適宜対応を変えていきます。

引数の`_node` は`rootNode`。`_anchor` にはトラッキングで取得した情報が入っています。

それぞれ`ObjCInstance` でインスタンス化し、平面情報を取得した際にパネルを呼び出す処理をしています。

### `renderer_didAddNode_forAnchor_` と`renderer_didUpdateNode_forAnchor_` の処理

サンプルで散見する`extent` は非推奨となっていたため、`planeExtent` で縦横幅の情報を取得しています。

[extent | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arplaneanchor/2882055-extent?language=objc)

https://developer.apple.com/documentation/arkit/arplaneanchor/2882055-extent?language=objc

[planeExtent | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arplaneanchor/3950858-planeextent?language=objc)

https://developer.apple.com/documentation/arkit/arplaneanchor/3950858-planeextent?language=objc

パネルは、X軸に面して生成されるので、Z軸に向ける処理をしています。

```py
planeNode.eulerAngles = (-pi / 2.0, 0.0, 0.0)
```

本来は`SCNMatrix4MakeRotation` 等の行列処理をおこないたいところですが、`objc_util` とMatrix やsimd 関係の構造体の相性があまりよくなく、違う方法で回転をさせています。

まだまだ調査不足の部分ですので、新しい発見がありましたら、追って報告をします。

[SCNMatrix4MakeRotation | Apple Developer Documentation](https://developer.apple.com/documentation/scenekit/1409686-scnmatrix4makerotation?language=objc)

https://developer.apple.com/documentation/scenekit/1409686-scnmatrix4makerotation?language=objc

[SCNMatrix4 | Apple Developer Documentation](https://developer.apple.com/documentation/scenekit/scnmatrix4?language=objc)

https://developer.apple.com/documentation/scenekit/scnmatrix4?language=objc

ちなみに`ARSCNViewDelegate` には、複数のDelegate がありますが、今回は仮に2つ呼び出しをして1つを実装に使っています。

（`renderer_didAddNode_forAnchor_` のみでも問題ありません）

### `.planeDetection = (1 << 0)` 指定

```py
def resetTracking(self):
  _configuration = ARWorldTrackingConfiguration.new()
  _configuration.planeDetection = (1 << 0)
  self.sceneView.session().runWithConfiguration_(_configuration)
```

Tracking の取得をHorizontal（水平）、Vertical（垂直）で指定します。

直接、値（文字列などで）を指定したいところですが、Enumeration Case となっています。

- [ARPlaneDetectionHorizontal | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arplanedetection/arplanedetectionhorizontal?language=objc)
  - `ARPlaneDetectionHorizontal = (1 << 0)`
- [ARPlaneDetectionVertical | Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arplanedetection/arplanedetectionvertical?language=objc)
  - `ARPlaneDetectionVertical = (1 << 1)`

今回は、平面の取得をしたいので水平を選択します。

`_configuration.planeDetection = (1 << 0)`

https://developer.apple.com/documentation/arkit/arplanedetection/arplanedetectionhorizontal?language=objc

https://developer.apple.com/documentation/arkit/arplanedetection/arplanedetectionvertical?language=objc

## 次回は

`delegate` の事前処理を終えたら、その他は簡単に（`delegate` と比較して）実装ができましたかね？

Pythonista3 で「できなさそうなこと」 もお伝えができた点が、個人的に良かったと感じています。

Matrix やsimd に関して、独自実装ではなく`objc_util` でしれっと使う方法を知っている方！ご連絡お待ちしております。

今回までリアカメラを使ったAR でしたので、次回はフロントカメラを使ったフェイストラッキングを紹介したいと思います。

ここまで、読んでいただきありがとうございました。

## 参考文献

- [Apple ARKit by Example. With the beta release of ARKit by… | by Mark Dawson | Mark Dawson](https://blog.markdaws.net/apple-arkit-by-example-ef1c8578fb59)
- [動かして学ぶARKit ~平面検出~ | DevelopersIO](https://dev.classmethod.jp/articles/plane_detection_arkit/)
- [ARKit入門 / 平面の検出(1)｜npaka｜note](https://note.com/npaka/n/n5e05236dbd17)

## せんでん

### Discord

Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。

https://t.co/0IOW2hn1VC

### 書籍

iPhone/iPad でプログラミングする最強の本。

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「活用篇」：hirax](https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99)

https://techbookfest.org/product/1WiyV4LEev3f4YrzesRKWV?productVariantID=axX32srsSgtLtZWUtjkj99

- [Python や Jupyter で iPhone/iPad 先端機能を簡単･自由にプログラミング！「土台篇」：hirax](https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r)

https://techbookfest.org/product/wTZTyeibm5GQ5XgdfMrEBV?productVariantID=kRDmN1udbEYZUWbETdwL8r

### その他

- サンプルコード

[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) でのコードをまとめているリポジトリがあります。

コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー

https://github.com/pome-ta/Pythonista3AdventCalendar2022sampleCode

- Twitter

なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー

https://twitter.com/pome_ta93

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか？って話よ！（クズ）<br><br>Pythonista3 Advent Calendar 2022 <a href="https://t.co/JKUxA525Pt">https://t.co/JKUxA525Pt</a> <a href="https://twitter.com/hashtag/Qiita?src=hash&amp;ref_src=twsrc%5Etfw">#Qiita</a></p>&mdash; pome-ta (@pome_ta93) <a href="https://twitter.com/pome_ta93/status/1588570697777152006?ref_src=twsrc%5Etfw">November 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- GitHub

基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。

https://github.com/pome-ta