Search code examples
swift3dqr-codearkitcore-motion

ARKit – How to put 3D Object on QRCode?


I'm actually trying to put a 3D Object on QRCode with ARKit For that I use a AVCaptureDevice to detect a QRCode and establish the area of the QRCode that gives me a CGRect. Then, I make a hitTest on every point of the CGRect to get the average 3D coordinates like so :

positionGiven = SCNVector3(0, 0, 0)

for column in Int(qrZone.origin.x)...2*Int(qrZone.origin.x + qrZone.width) {
    for row in Int(qrZone.origin.y)...2*Int(qrZone.origin.y + qrZone.height) {
        for result in sceneView.hitTest(CGPoint(x: CGFloat(column)/2,y:CGFloat(row)/2), types: [.existingPlaneUsingExtent,.featurePoint]) {

            positionGiven.x+=result.worldTransform.columns.3.x
            positionGiven.y+=result.worldTransform.columns.3.y
            positionGiven.z+=result.worldTransform.columns.3.z
            cpts += 1
        }
    }
}

positionGiven.x=positionGiven.x/cpts
positionGiven.y=positionGiven.y/cpts
positionGiven.z=positionGiven.z/cpts

But the hitTest doesn't detect any result and freeze the camera while when I make a hitTest with a touch on screen it works. Do you have any idea why it's not working ? Do you have an other idea that can help me to achieve what I want to do ?

I already thought about 3D translation with CoreMotion that can give me the tilt of the device but that seems really tedious. I also heard about ARWorldAlignmentCamera that can locked the scene coordinate to match the orientation of the camera but I don't know how to use it !

Edit : I try to move my 3D Object every time I touch the screen and the hitTest is positive, and it's pretty accurate ! I really don't understand why hitTest on an area of pixels doesn't work...

Edit 2 : Here is the code of the hitTest who works with 2-5 touches on the screen:

@objc func touch(sender : UITapGestureRecognizer) {

    for result in sceneView.hitTest(CGPoint(x: sender.location(in: view).x,y: sender.location(in: view).y), types: [.existingPlaneUsingExtent,.featurePoint]) {
        //Pop up message for testing
        alert("\(sender.location(in: view))", message: "\(result.worldTransform.columns.3)")

        //Moving the 3D Object to the new coordinates
        let objectList = sceneView.scene.rootNode.childNodes

        for object : SCNNode in objectList {
            object.removeFromParentNode()
        }
        addObject(SCNVector3(result.worldTransform.columns.3.x,result.worldTransform.columns.3.y,result.worldTransform.columns.3.z))
    }
}

Edit 3 : I manage to resolve my problem partially.

I take the transform matrix of the camera (session.currentFrame.camera.transform) so that the object is in front of the camera. Then I apply a translation on (x,y) with the position of the CGRect. However i can't translate the z-axis because i don't have enough informations. And I will probably need a estimation of z coordinate like the hitTest do..

Thanks in advance ! :)


Solution

  • You could use Apple's Vision API to detect the QR code and place an anchor.

    To start detecting QR codes, use:

     var qrRequests = [VNRequest]()
     var detectedDataAnchor: ARAnchor?
     var processing = false
    
     func startQrCodeDetection() {
        // Create a Barcode Detection Request
        let request = VNDetectBarcodesRequest(completionHandler: self.requestHandler)
        // Set it to recognize QR code only
        request.symbologies = [.QR]
        self.qrRequests = [request]
    }
    

    In ARSession's didUpdate Frame

    public func session(_ session: ARSession, didUpdate frame: ARFrame) {
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                if self.processing {
                  return
                }
                self.processing = true
                // Create a request handler using the captured image from the ARFrame
                let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: frame.capturedImage,
                                                                options: [:])
                // Process the request
                try imageRequestHandler.perform(self.qrRequests)
            } catch {
    
            }
        }
    }
    

    Handle the Vision QR request and trigger the hit test

    func requestHandler(request: VNRequest, error: Error?) {
        // Get the first result out of the results, if there are any
        if let results = request.results, let result = results.first as? VNBarcodeObservation {
            guard let payload = result.payloadStringValue else {return}
            // Get the bounding box for the bar code and find the center
            var rect = result.boundingBox
            // Flip coordinates
            rect = rect.applying(CGAffineTransform(scaleX: 1, y: -1))
            rect = rect.applying(CGAffineTransform(translationX: 0, y: 1))
            // Get center
            let center = CGPoint(x: rect.midX, y: rect.midY)
    
            DispatchQueue.main.async {
                self.hitTestQrCode(center: center)
                self.processing = false
            }
        } else {
            self.processing = false
        }
    }
    
     func hitTestQrCode(center: CGPoint) {
        if let hitTestResults = self.latestFrame?.hitTest(center, types: [.featurePoint] ),
            let hitTestResult = hitTestResults.first {
            if let detectedDataAnchor = self.detectedDataAnchor,
                let node = self.sceneView.node(for: detectedDataAnchor) {
                let previousQrPosition = node.position
                node.transform = SCNMatrix4(hitTestResult.worldTransform)
    
            } else {
                // Create an anchor. The node will be created in delegate methods
                self.detectedDataAnchor = ARAnchor(transform: hitTestResult.worldTransform)
                self.sceneView.session.add(anchor: self.detectedDataAnchor!)
            }
        }
    }
    

    Then handle adding the node when the anchor is added.

    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
    
        // If this is our anchor, create a node
        if self.detectedDataAnchor?.identifier == anchor.identifier {
            let sphere = SCNSphere(radius: 1.0)
            sphere.firstMaterial?.diffuse.contents = UIColor.redColor()
            let sphereNode = SCNNode(geometry: sphere)
            sphereNode.transform = SCNMatrix4(anchor.transform)
            return sphereNode
        }
        return nil
    }
    

    Source