Skip to content

aaronjohn2/AR-Ruler

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 

Repository files navigation

AR-Ruler

I made a precise virtual (AR) Ruler compared to the real one by implementing ARKit in Swift to calculate the distance of real world objects.

This project relies on the built in sensors and A9 processor (Available through iPhone 6S and upwards.) The measurements are based on the plane detection's capabilities of ARKit.

Here’s how I built the iOS app:

  1. First import the necessary libraries: UIKit, ARKit, and SceneKit. Here, the ARKit library handles all the tracking and analyzing(tracks the phone's position in the real world), while the SceneKit library renders 3D virtual objects on top of the camera's image. But if you have used Unity or Unreal Engine then it would be nice to use those libraries as your rendering engine.
import UIKit
import ARKit
import SceneKit
  1. I used ARSCNViewDelegate to interact with SceneKit to use the augmented reality features of ARKit. The SceneKit I chose (between ARSCNView and SCNView) for this project was ARSCNView. This ARSCNView renders the scenes UI objects' session(to start a new session when the app is opened) and configuration(config,scene,etc.) Here, we use ARWorldTrackingConfiguration to track the devices movement.
class ViewController: UIViewController, ARSCNViewDelegate {
    
    @IBOutlet var indicator: UIImageView!       //Outlets that are connected to StoryBoard
    @IBOutlet var placeButton: UIButton!
    @IBOutlet var trashButton: UIButton!
    @IBOutlet var sceneView: ARSCNView!
    
    var center : CGPoint!

    override func viewDidLoad() {
        super.viewDidLoad()
        sceneView.delegate = self
        center = view.center
        sceneView.scene.rootNode.addChildNode(arrow)
        sceneView.autoenablesDefaultLighting = true
    }

    override func didRotate(from fromInterfaceOrientation: UIInterfaceOrientation) {
        center = view.center
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let configuration = ARWorldTrackingConfiguration()
        sceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        sceneView.session.pause()
    }
  1. The Next important part of the project is the SCNNode object. Everything inside the scene, including the scene is represented as an instance of this object. These instances can have other properties which may include geometry (meaning 3D models), position of object and and its children, etc. Here, the core concept is that we descirbe objects as nodes(This is a data structure used to manage all the content inside SceneKit.)
    var startNode: SCNNode!         //variables for setting up Nodes
    var endNode: SCNNode!
    var lineNode: SCNNode?
    var textNode: SCNNode!
    var textWrapNode: SCNNode!
    var positions = [SCNVector3]()
    var isFirstPoint = true
    var points = [SCNNode]()
  1. The function renderer is called when ARKit detects a surface in the scene view which could be used as an "anchor" and added to the scene view. (An "anchor" is a special type of node which could be used to position a virtual object in a way that it will make the object seem like it is actually in the real world. example: Wall, floor, chair, table etc.) Here, ARKit and SceneKit work seamlessly together. When ARKit finds an anchor in the realworld, it adds it to the scene and SceneKit calls the renderer function to see if there are more instructions to be added to the anchor node.
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        let hitTest = sceneView.hitTest(center, types: .featurePoint)
        let result = hitTest.last
        guard let transform = result?.worldTransform else {return}
        let thirdColumn = transform.columns.3
        let position = SCNVector3Make(thirdColumn.x, thirdColumn.y, thirdColumn.z)
        positions.append(position)
        let lastTenPositions = positions.suffix(10)
        arrow.position = getAveragePosition(from: lastTenPositions)
        
    }
  1. Here, I wrote a function to get the average center position as to where the camera is pointing.
  func getAveragePosition(from positions : ArraySlice<SCNVector3>) -> SCNVector3 {
        var averageX : Float = 0
        var averageY : Float = 0
        var averageZ : Float = 0

        for position in positions {
            averageX += position.x
            averageY += position.y
            averageZ += position.z
        }
        let count = Float(positions.count)
        return SCNVector3Make(averageX / count , averageY / count, averageZ / count)
    } 
  1. Here, I move on to add buttons for the user view. I started off with a "plus" button to enable measuring. So, when the user taps on this button it starts the measuremernt process by adding a small white sphere to the scene view (3D space) as the starting position (point A.) Next, when the user moves the camera to the end position, they should tap on the "plus" button again to end measurement. By doing so, another small white sphere is added to the scene view (3D space) as the end position (point B.) Following this, a line connects these two spheres in the scene view and it provides a red sphere at the midpoint of this line, while also showing the distance between the two spheres in cm above this midpoint. (Note: This function is connected to step 9.)
@IBAction func placeAction(_ sender: UIButton) {
        
        let sphereGeometry = SCNSphere(radius: 0.005)
        let sphereNode = SCNNode(geometry: sphereGeometry)
        sphereNode.position = arrow.position
        sceneView.scene.rootNode.addChildNode(sphereNode)
        points.append(sphereNode)
        
        if isFirstPoint {
            isFirstPoint = false
        } else {
            //calculate the distance
            let pointA = points[points.count - 2]
            guard let pointB = points.last else {return}
            
            let d = distance(float3(pointA.position), float3(pointB.position))
            
            //add line
                let line = SCNGeometry.lined(from: pointA.position, to: pointB.position)
                print(d.description)
                let lineNode = SCNNode(geometry: line)
                sceneView.scene.rootNode.addChildNode(lineNode)
            
            
            // add midPoint
            let midPoint = (float3(pointA.position) + float3(pointB.position)) / 2
            let midPointGeometry = SCNSphere(radius: 0.003)
            midPointGeometry.firstMaterial?.diffuse.contents = UIColor.red
            let midPointNode = SCNNode(geometry: midPointGeometry)
            midPointNode.position = SCNVector3Make(midPoint.x, midPoint.y, midPoint.z)
            sceneView.scene.rootNode.addChildNode(midPointNode)
            
            // add text
            
            let textGeometry = SCNText(string: String(format: "%.0f", d * 100) + "cm" , extrusionDepth: 1)
            let textNode = SCNNode(geometry: textGeometry)
            textNode.scale = SCNVector3Make(0.005, 0.005, 0.01)
            textGeometry.flatness = 0.2
            midPointNode.addChildNode(textNode)
            
            
            // Billboard contraints
            let contraints = SCNBillboardConstraint()
            contraints.freeAxes = .all
            midPointNode.constraints = [contraints]
            
            
            isFirstPoint = true   
            
        }
        
    }
  1. Next, I added a button to delete all AR nodes that were created on screen, if the user wanted to clear scene view.
 @IBAction func deleteAction(_ sender: UIButton) {
       
        sceneView.scene.rootNode.enumerateChildNodes { (node, stop) in
            node.removeFromParentNode()
        }
        
    }
  1. Next, I added a button to toggle a flashlight, so that the user can use this app to take measurements in the dark.
@IBAction func toggleTorch(_ sender: UIButton) {
        
        guard let device = AVCaptureDevice.default(for: AVMediaType.video)
            else {return}
        
        if device.hasTorch {
            do {
                try device.lockForConfiguration()
                
                if device.torchMode == .on {
                    device.torchMode = .off
                } else {
                    device.torchMode = .on
                }
                
                device.unlockForConfiguration()
            } catch {
                print("Torch could not be used")
            }
        } else {
            print("Torch is not available")
        }
    }
  1. Here, I extended a function called lined to my class (used in step 6.) The function is what helps create the line geometry between Point A and Point B spheres.
extension SCNGeometry {
    class func lined(from vectorA : SCNVector3, to vectorB : SCNVector3) -> SCNGeometry {
        let indices : [Int32] = [0,1]
        let source = SCNGeometrySource(vertices: [vectorA, vectorB])
        let element = SCNGeometryElement(indices: indices, primitiveType: .line)
        return SCNGeometry(sources: [source], elements: [element])
    }
}

In conclusion the AR Ruler works pretty well. It is not perfect in some situations, such as in low lighting or when a surface is not entirely flat. Hence, the results wont be completely accurate all the time and since ARkit is still in its Beta Phase you're better off using a real ruler for now, to measure anything requiring high accuracy.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages