スポンサーリンク

ARKit2の画像トラッキングを試してみた

ちょっと出遅れた感はありますが、ARKit2 で新たに追加された機能である画像トラッキング機能を試してみました。

題材は、パッと思いついたポケモンにしています(笑)

動画ではディスプレイ上の画像を認識していますが、もちろん紙であっても認識します。

とりあえずコード全体を載せるとこんな感じです。

class ViewController: UIViewController {
    
    private var textNode001: SCNNode?
    private var textNode002: SCNNode?
    private var textNode003: SCNNode?

    @IBOutlet var sceneView: ARSCNView!
    
    let imageConfiguration: ARImageTrackingConfiguration = {
        let configuration = ARImageTrackingConfiguration()
        
        let images = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil)
        configuration.trackingImages = images!
        configuration.maximumNumberOfTrackedImages = 3
        return configuration
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        sceneView.delegate = self
        textNode001 = makeLabelNode(text: "フシギダネ")
        textNode002 = makeLabelNode(text: "フシギソウ")
        textNode003 = makeLabelNode(text: "フシギバナ")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        sceneView.session.run(imageConfiguration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        sceneView.session.pause()
    }
    
    private func makeLabelNode(text: String) -> SCNNode {
        let depth: CGFloat = 0.001
        let font = UIFont(name: "HiraKakuProN-W3", size: 0.5);
        
        let textGeometory = SCNText(string: text, extrusionDepth: depth)
        textGeometory.flatness = 0
        textGeometory.font = font
        let textNode = SCNNode(geometry: textGeometory)
        let (min, max) = (textNode.boundingBox)
        let x = CGFloat(max.x - min.x)
        textNode.position = SCNVector3(-(x/2), -1, 0.1)
        
        return textNode
    }
    
    private func makeImagePlaneNode(imageAnchor: ARImageAnchor) -> SCNNode {
        let plane = SCNPlane(width: imageAnchor.referenceImage.physicalSize.width, height: imageAnchor.referenceImage.physicalSize.height)
        plane.firstMaterial?.diffuse.contents = UIColor.black
        plane.firstMaterial?.transparency = 0.5
        let planeNode = SCNNode(geometry: plane)
        planeNode.eulerAngles.x = -.pi / 2
        return planeNode
    }
    
    private func makeLabelPlaneNode(imageAnchor: ARImageAnchor) -> SCNNode {
        let plane = SCNPlane(width: imageAnchor.referenceImage.physicalSize.width, height: imageAnchor.referenceImage.physicalSize.height / 3)
        plane.firstMaterial?.diffuse.contents = UIColor.black
        plane.firstMaterial?.transparency = 0.9
        let planeNode = SCNNode(geometry: plane)
        planeNode.position = SCNVector3(0, 0, imageAnchor.referenceImage.physicalSize.height * 2 / 3)
        planeNode.eulerAngles.x = -.pi / 2
        return planeNode
    }
}

extension ViewController: ARSCNViewDelegate {
    
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        guard let imageAnchor = anchor as? ARImageAnchor else {
            return nil
        }
        
        switch imageAnchor.referenceImage.name {
        case "001":
            let node = SCNNode()
            let imagePlaneNode = makeImagePlaneNode(imageAnchor: imageAnchor)
            let labelPlaneNode = makeLabelPlaneNode(imageAnchor: imageAnchor)
            node.addChildNode(imagePlaneNode)
            node.addChildNode(labelPlaneNode)
            if let textNode = textNode001 {
                labelPlaneNode.addChildNode(textNode)
            }
            return node
        case "002":
            let node = SCNNode()
            let imagePlaneNode = makeImagePlaneNode(imageAnchor: imageAnchor)
            let labelPlaneNode = makeLabelPlaneNode(imageAnchor: imageAnchor)
            node.addChildNode(imagePlaneNode)
            node.addChildNode(labelPlaneNode)
            if let textNode = textNode002 {
                labelPlaneNode.addChildNode(textNode)
            }
            return node
        case "003":
            let node = SCNNode()
            let imagePlaneNode = makeImagePlaneNode(imageAnchor: imageAnchor)
            let labelPlaneNode = makeLabelPlaneNode(imageAnchor: imageAnchor)
            node.addChildNode(imagePlaneNode)
            node.addChildNode(labelPlaneNode)
            if let textNode = textNode003 {
                labelPlaneNode.addChildNode(textNode)
            }
            return node
        default:
            return nil
        }
    }
}

これとは別に storyboard 上で ARSCNView を作成し紐づけています。
また、認識する画像は Assets.xcassets にこんな感じで登録しています。

※画像データは ポケモンだいすきクラブ さんからお借りしました。

ソースコードを軽く解説していくと、 viewDidLoad で表示するラベルのノードを作成しています。
そこで呼び出している makeLabelNode ではラベルの表示位置や向きの調整を行っています。

viewWillAppear で AR のセッションを開始しています。
ARImageTrackingConfiguration では認識する対象の画像や、同時に認識できる画像の最大数を設定します。
同時に認識できる画像の最大数 (maximumNumberOfTrackedImages) はどうやら上限があるらしいのですが、詳しくはわかっていません。

セッションがスタートし、対象の画像を認識したら renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) が呼び出されます。
ここで認識した画像を可視化するために半透明の黒のマスクをかけ、その下にラベルでその画像のポケモンの名前を表示しています。
ラベルの表示位置は

planeNode.position = SCNVector3(0, 0, imageAnchor.referenceImage.physicalSize.height * 2 / 3)

で設定しているのですが、認識する画像にあわせて調整しています。

以上です。
たったこれだけのコードで画像を認識して、それに対してオブジェクトを配置するプログラムをかけるなんて ARKit2 すごすぎです。

※この記事の内容は https://3jino-oyatsu.com/blog/99/ からお引越ししたものです