LoginSignup
1
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-19

この記事は、Pythonista3 Advent Calendar 2022 の20日目の記事です。

一方的な偏った目線で、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_utildelegate 実装

現実世界の情報を掴む

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

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

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

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

objc_utildelegate を中心に見ていきましょう。

delegate

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

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

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

create_objc_class | objc_util — Utilities for bridging Objective-C APIs — Python 3.6.1 documentation

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

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

SwiftにおけるDelegateとは何か、なぜ使うのか - Qiita

methods

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

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

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

renderer:didAddNode:forAnchor: | Apple Developer Documentation

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

protocols

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

今回はARSCNViewdelegate なので、ARSCNViewDelegate です。

ARSCNViewDelegate | Apple Developer Documentation

def create_delegate(self): メソッド

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

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

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

実装

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

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

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 として機能します。


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_utildelegate 処理の引数は、往々にしてそのままでは使えません。ObjCInstance(引数) とインスタンス化してから、処理をすることになります。

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

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

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

renderer_didAddNode_forAnchor_renderer_didUpdateNode_forAnchor_ の処理

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

extent | Apple Developer Documentation

planeExtent | Apple Developer Documentation

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

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

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

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

SCNMatrix4MakeRotation | Apple Developer Documentation

SCNMatrix4 | Apple Developer Documentation

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

renderer_didAddNode_forAnchor_ のみでも問題ありません)

.planeDetection = (1 << 0) 指定

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

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

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

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

_configuration.planeDetection = (1 << 0)

次回は

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

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

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

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

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

参考文献

せんでん

Discord

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

書籍

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

その他

  • サンプルコード

Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。

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

  • Twitter

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

  • GitHub

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2