+1 (415) 993-7206

Measuring the Real World with ARKit: LiDAR Dimensioning, Accuracy You Can Claim, and Shipping a Measure Feature People Trust

Almost every AR project we get pulled into eventually grows a measurement feature. A furniture app needs the doorway width. A logistics app needs carton dimensions for cubing. A restoration contractor needs wall area for a quote. A medical device team needs a wound outline. The ask sounds small — "just like Apple's Measure app" — and then the first question from the business side is the one that actually matters: how accurate is it, and can we put that number in front of a customer?

None of our other tutorials cover this, so here is the whole picture: how to build a dimensioning feature on ARKit and RealityKit, what accuracy you can honestly claim, and the engineering that separates a demo from something a field crew trusts.

Where the numbers come from

An ARKit measurement is the distance between two points expressed in world-space metres. Everything hinges on how you obtain those points:

  1. Raycast against detected planes. Cheapest and most stable. ARSession.raycast with an existingPlaneGeometry target snaps to a plane ARKit has already fitted and smoothed over many frames. Good for floors, tabletops, walls.
  2. Raycast against the reconstructed mesh. On LiDAR devices, .estimatedPlane and scene-mesh hits let you place points on arbitrary surfaces — a sofa arm, a pipe, a crate corner. More flexible, noisier.
  3. Depth map sampling. Read ARFrame.sceneDepth directly and unproject a pixel. Maximum control, maximum responsibility: you own the confidence filtering.

For most products, start at (1), fall back to (2), and only reach for (3) when you need per-pixel work like outlining an irregular shape.

A minimal two-point measurement

import ARKit
import RealityKit

final class MeasureController {
    let arView: ARView
    private var firstPoint: SIMD3<Float>?

    init(arView: ARView) {
        self.arView = arView
        let config = ARWorldTrackingConfiguration()
        config.planeDetection = [.horizontal, .vertical]
        if ARWorldTrackingConfiguration.supportsSceneReconstruction(.meshWithClassification) {
            config.sceneReconstruction = .meshWithClassification
        }
        if ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) {
            config.frameSemantics.insert(.sceneDepth)
        }
        arView.session.run(config)
    }

    /// Returns a finished measurement in metres once the second point lands.
    func addPoint(atScreen point: CGPoint) -> Float? {
        guard let world = worldPosition(for: point) else { return nil }

        guard let start = firstPoint else {
            firstPoint = world
            return nil
        }
        firstPoint = nil
        return simd_distance(start, world)
    }

    private func worldPosition(for screenPoint: CGPoint) -> SIMD3<Float>? {
        // Prefer a fitted plane; fall back to the reconstructed mesh.
        let targets: [ARRaycastQuery.Target] = [.existingPlaneGeometry, .estimatedPlane]
        for target in targets {
            if let result = arView.raycast(from: screenPoint,
                                           allowing: target,
                                           alignment: .any).first {
                return result.worldTransform.translation
            }
        }
        return nil
    }
}

extension simd_float4x4 {
    var translation: SIMD3<Float> { SIMD3(columns.3.x, columns.3.y, columns.3.z) }
}

That is the easy 5%. The rest of this post is the other 95%.

Accuracy: what you can honestly claim

Our field numbers, across iPhone Pro models with LiDAR, on cooperative surfaces, after the session has been given a few seconds to converge:

ConditionTypical error on a 1–3 m span
Plane-snapped points, good light, textured floor0.5–1% (5–30 mm)
Mesh-snapped points on a matte object1–2%
Poor light, glossy or dark surfaces, glass3%+ or outright failure
Non-LiDAR device, plane-snapped only1–3%, degrades fast beyond 2 m

Two corollaries people miss:

  • Error is not purely proportional. There is a roughly fixed per-point placement error (where the user tapped, plus depth noise) plus a drift term that grows with how far the device has travelled between the two points. Measuring a 6 m room by walking the length of it compounds tracking drift into the result; measuring a 30 cm box from one standing position does not.
  • Small measurements are relatively worse. A 3 mm point error on a 20 mm feature is 15%. LiDAR-class depth sensing is not calipers, and no amount of UI polish makes it calipers.

So: claim "±1% on typical room-scale spans, under good conditions" in engineering docs, and in the product write something the user can act on — "Measurements are estimates. Verify critical dimensions with a tape measure." Every serious shipping measure app does this, and if your buyer needs tolerance-grade numbers, the honest answer is that ARKit is the wrong instrument.

The engineering that actually moves the number

Don't let users measure a cold session. Until tracking state is .normal and a plane or mesh exists near the target, every number is a guess. Gate the measure button on tracking quality and coach the user — the same pattern as our tracking states and placement UX post.

Snap to geometry, not to thin air. Plane-fitted points are averaged across dozens of frames and are dramatically steadier than a raw depth sample. When you must use the mesh, snap to plane classification (.floor, .wall, .table) where you can.

Filter depth by confidence. If you sample sceneDepth directly, read sceneDepth.confidenceMap and discard .low pixels outright. Then take a median — not a mean — of a small patch (say 5×5) around the target pixel so a single flyaway sample cannot move the result.

Snap corners with edge awareness. Cubing a box is not two taps in the middle of two faces; it is finding the corner. Fit planes to the visible faces and intersect them, or run a local plane fit on the mesh and take the intersection of the three dominant normals. Intersecting fitted planes is far more accurate than asking a human to tap a corner precisely.

Re-measure and show spread. Take the measurement three times a second while the user holds the second point, and display the running median plus the spread. If the spread is 4 cm, the user can see for themselves that the reading is not trustworthy — better than a confident single number that is wrong.

Give the user a units and precision policy. Rounding to the millimetre implies millimetre accuracy. Round to the nearest 5 mm (or ¼ inch) and you have quietly communicated the real resolution.

Areas, volumes, and the compounding problem

Area and volume multiply your errors. A 1% error on each of three edges is roughly a 3% error on the volume — which, for freight cubing or material estimation, is the difference between a profitable quote and a bad one. If you are computing volumes:

  • Measure the shortest spans you can and derive, rather than walking the tape across the room.
  • Use RoomPlan for room-scale layouts instead of hand measurement; it does the plane fitting, corner intersection, and smoothing for you. See our RoomPlan walkthrough.
  • Report a range, not a point value, wherever the number drives money.

Persisting and exporting measurements

A measurement that vanishes on app close is a demo. Anchor each endpoint (a WorldAnchor / ARAnchor pair), persist the anchors so the annotation returns on the next visit — the pattern in our persistent AR anchors post — and export the numbers with metadata: device model, whether LiDAR was used, tracking state at capture, depth confidence, and timestamp. When a customer disputes a dimension six weeks later, that metadata is the only thing that lets you answer.

Testing it without a tape measure in the office

Build a calibration rig: a printed target board with known distances, plus two or three reference objects you keep at fixed dimensions. Run it on every device class you support, in three lighting conditions, and record the error distribution as a fixture. That turns "measurement seems fine" into a regression test you can run before every release — see testing ARKit apps for the replay-based harness we use.

The short version

ARKit measurement is straightforward to prototype and genuinely hard to ship responsibly. The accuracy you can claim depends far more on geometry snapping, confidence filtering, session gating, and honest rounding than on which raycast API you call. Get those right and a 1% room-scale estimate is a legitimately useful business feature; skip them and you have built a random number generator with a nice reticle.

Need a measurement or dimensioning feature that holds up in the field — and a defensible accuracy statement to go with it? Talk to a RealityRogue ARKit consultant.