この記事は、Pythonista3 Advent Calendar 2022 の21日目の記事です。
一方的な偏った目線で、Pythonista3 を紹介していきます。
ほぼ毎日iPhone(Pythonista3)で、コーディングをしている者です。よろしくお願いします。
以下、私の2022年12月時点の環境です。
--- 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
ファイル等の準備も不要です。
平面取得の流れと、ほぼ変わりなく実装ができます。
- リアカメラ取得をフロントカメラ取得へ
- 顔情報のジオメトリを張り加工
- 色付け
- ワイヤー
前回のコードの書き換えのみで実装が可能です。早速コードを見てみましょう。
実装コード
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 の呼び出しと:
ARFaceTrackingConfiguration = ObjCClass('ARFaceTrackingConfiguration')
ARSCNFaceGeometry = ObjCClass('ARSCNFaceGeometry')
delegate
生成のdidUpdateNode
のメソッドです:
def renderer_didUpdateNode_forAnchor_(_self, _cmd, _renderer, _node,
_anchor):
node = ObjCInstance(_node)
faceAnchor = ObjCInstance(_anchor)
faceGeometry = node.geometry()
faceGeometry.updateFromFaceGeometry_(faceAnchor.geometry())
また、背景を黒ベタにしています。コメントアウトすれば、カメラの取得映像が出ます。
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
と、renderer:didAddNode:forAnchor:
で代用可能性の説明があったのでrenderer_didAddNode_forAnchor_
で甘んじています。
renderer_didAddNode_forAnchor_
メソッド
ARSCNFaceGeometry.faceGeometryWithDevice_(renderer.device())
で、顔の情報Geometry を取得しています。
Geometry ですので、Material で色を付けたりとSceneKit 同様のGeometry 操作で調整が可能です。
# ワイヤー表現
faceGeometry.firstMaterial().fillMode = 1
# 青色に
faceGeometry.firstMaterial().diffuse().contents = UIColor.blueColor()
鼻にあるball は、fix で位置を指定し配置しています。
ballNode.position = (0.0, 0.0, 0.08) # 手動で微調整
実装方法によっては、顔のTracking 情報より鼻の位置を割り出して設置することも可能です。
renderer_didUpdateNode_forAnchor_
メソッド
renderer:didUpdateNode:forAnchor: | Apple Developer Documentation
前回の平面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 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。
書籍
iPhone/iPad でプログラミングする最強の本。
その他
- サンプルコード
Pythonista3 Advent Calendar 2022 でのコードをまとめているリポジトリがあります。
コードのエラーや変なところや改善点など。ご指摘やPR お待ちしておりますー
なんしかガチャガチャしていますが、お気兼ねなくお声がけくださいませー
やれるか、やれないか。ではなく、やるんだけども、紹介説明することは尽きないと思うけど、締め切り守れるか?って話よ!(クズ)
— pome-ta (@pome_ta93) November 4, 2022
Pythonista3 Advent Calendar 2022 https://t.co/JKUxA525Pt #Qiita
- GitHub
基本的にGitHub にコードをあげているので、何にハマって何を実装しているのか観測できると思います。