出来らあっ！　え！！Pythonista3 でAR Face Trackingを！？

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

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 はじまるよー

前回はリアカメラを使い、Tracking で平面を取得しパネルを設置しました。

私のiPhoneは、11なのでLiDAR センサーは無いのですが、それなりの精度でパネルが出てきました。

今回はフロントのカメラを使って、顔の情報を取得して遊んでみましょう。

### 顔の情報を取得する手順

マスクも取得情報から構築が可能なので、事前に`.obj` ファイル等の準備も不要です。

平面取得の流れと、ほぼ変わりなく実装ができます。

- リアカメラ取得をフロントカメラ取得へ
- 顔情報のジオメトリを張り加工
  - 色付け
  - ワイヤー

前回のコードの書き換えのみで実装が可能です。早速コードを見てみましょう。

## 実装コード

```py
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')
ARFaceTrackingConfiguration = ObjCClass('ARFaceTrackingConfiguration')
ARSCNFaceGeometry = ObjCClass('ARSCNFaceGeometry')

SCNNode = ObjCClass('SCNNode')
SCNSphere = ObjCClass('SCNSphere')


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

  def viewDidLoad(self):
    _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 << 5) | (1 << 30) | (1 << 32)
    sceneView.debugOptions = _debugOptions

    sceneView.autorelease()
    sceneView.scene().background().contents = UIColor.blackColor()
    self.sceneView = sceneView

  def viewWillAppear(self):
    self.resetTracking()

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

  def resetTracking(self):
    _configuration = ARFaceTrackingConfiguration.new()
    _configuration.isLightEstimationEnabled = True
    '''ARSessionRunOptions
    ARSessionRunOptionResetTracking = (1 << 0)
    ARSessionRunOptionRemoveExistingAnchors = (1 << 1)
    '''
    _options = (1 << 0) | (1 << 1)
    self.sceneView.session().runWithConfiguration_options_(
      _configuration, _options)

  def create_delegate(self):
    # --- /delegate
    def renderer_didAddNode_forAnchor_(_self, _cmd, _renderer, _node, _anchor):
      renderer = ObjCInstance(_renderer)
      node = ObjCInstance(_node)

      faceGeometry = ARSCNFaceGeometry.faceGeometryWithDevice_(
        renderer.device())
      #faceGeometry.firstMaterial().fillMode = 1
      faceGeometry.firstMaterial().diffuse().contents = UIColor.blueColor()
      faceGeometry.material(
      ).lightingModelName = 'SCNLightingModelPhysicallyBased'

      ball = SCNSphere.sphereWithRadius_(0.03)
      ball.segmentCount = 8
      ball.geodesic = True

      ball.firstMaterial().diffuse().contents = UIColor.redColor()

      ball.material().lightingModelName = 'SCNLightingModelPhysicallyBased'

      ballNode = SCNNode.nodeWithGeometry_(ball)
      ballNode.position = (0.0, 0.0, 0.08)
      node.addChildNode_(ballNode)

      node.geometry = faceGeometry

    def renderer_didUpdateNode_forAnchor_(_self, _cmd, _renderer, _node,
                                          _anchor):
      node = ObjCInstance(_node)
      faceAnchor = ObjCInstance(_anchor)

      faceGeometry = node.geometry()
      faceGeometry.updateFromFaceGeometry_(faceAnchor.geometry())

    # --- 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'])

```

`GameScene` class すら消えてしまいました。

追加事項は、`ObjCClass` でのclass の呼び出しと:

```py
ARFaceTrackingConfiguration = ObjCClass('ARFaceTrackingConfiguration')
ARSCNFaceGeometry = ObjCClass('ARSCNFaceGeometry')
```

`delegate` 生成の`didUpdateNode` のメソッドです:

```py
def renderer_didUpdateNode_forAnchor_(_self, _cmd, _renderer, _node,
                                      _anchor):
  node = ObjCInstance(_node)
  faceAnchor = ObjCInstance(_anchor)

  faceGeometry = node.geometry()
  faceGeometry.updateFromFaceGeometry_(faceAnchor.geometry())

```

また、背景を黒ベタにしています。コメントアウトすれば、カメラの取得映像が出ます。

```py
sceneView.scene().background().contents = UIColor.blackColor()
```

### フロントカメラでの取得

`_configuration` へ、`ARFaceTrackingConfiguration` のインスタンスを格納しています。

前回は`ARWorldTrackingConfiguration` でした。

この変更により、リアカメラからフロントカメラ取得へ変わります。

### `delegate` 処理

`objc_util` としての実装方法は、前回と変わりありません。

ただネット上にあるサンプルのコードたちでは、`renderer:nodeForAnchor:` のメソッドから`node` を返しています。

しかし、Pythonista3 上では動きが掴めず（`print` での出力がなかった = 処理がされていない可能性）、`renderer_didAddNode_forAnchor_` にて、処理を置き換えています。

Documentation にも、

> 代わりに renderer:didAddNode:forAnchor: メソッドを実装し、そのノードにビジュアルコンテンツをアタッチして提供することができます。

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

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

と、`renderer:didAddNode:forAnchor:` で代用可能性の説明があったので`renderer_didAddNode_forAnchor_` で甘んじています。

#### `renderer_didAddNode_forAnchor_` メソッド

`ARSCNFaceGeometry.faceGeometryWithDevice_(renderer.device())` で、顔の情報Geometry を取得しています。

Geometry ですので、Material で色を付けたりとSceneKit 同様のGeometry 操作で調整が可能です。

```py
# ワイヤー表現
faceGeometry.firstMaterial().fillMode = 1

# 青色に
faceGeometry.firstMaterial().diffuse().contents = UIColor.blueColor()
```

鼻にあるball は、fix で位置を指定し配置しています。

```py
ballNode.position = (0.0, 0.0, 0.08)  # 手動で微調整
```

実装方法によっては、顔のTracking 情報より鼻の位置を割り出して設置することも可能です。

#### `renderer_didUpdateNode_forAnchor_` メソッド

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

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

前回の平面Tracking では、`pass` でスルーをしていたメソッドです。

`anchor`（今回ですと、顔の情報）の動き・変化をキャッチした際に作動します。

`.updateFromFaceGeometry_` にて、顔のTracking 情報をマスク上のGeometry へ更新をかけてくれます。

## 次回は

平面Tracking に続き、比較的少量のコードで実装ができますね。

マスクの色をShader で出してみたり、目玉表現として`SCNSphere` を仕込んでみたり。色々な表現の可能性がありそうです。

AR は、リアルな世界が画面上に出るので、キャプチャが少し恥ずかしいですね。。。

気になるARKit のコードがあれば、Pythonista3 に移植してみてはいかがでしょうか。

書き換えに際し、気をつける点としては、

- サンプルで呼び出しているオブジェクトを用意できるか
- Matrix, simd の演算処理があるか？
  - 他の計算で対応できるか？
  - Pythonista3 上で実装して対応させるか？
    - SciPy がないので苦しいとこですね。。。
- RealityKit ではないか？

そんなことを頭の片隅に置きながら、コードを確認してみると道筋が見えてくると思います。

iPhone の中で実装実行ができる3DCG の世界。ARKit をSceneKit の派生と考えると、5回に渡り紹介をしてきました。

完全に数学的要素を排除し、Framework としても薄い表層の部分のみでしたが「こんなことができるのか！」というワクワク感がありますよね。

今回は、カメラからリアルタイムに顔の情報を取得しました。

次回より機械学習のCore ML を使った顔情報の取得を紹介したいと思います。

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

## せんでん

### 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