You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将SCNNode正确放置在ARKit识别出的二维码顶部?

问题排查及修复方案

二维码检测场景
偏移问题由4个核心错误导致,逐一修复即可解决:

  • 坐标系转换错误
    Vision框架返回的boundingBox是左下角为原点的归一化坐标,而ARKit的hitTest接口使用的是左上角为原点的视图坐标系,Y轴方向完全相反,直接用原始坐标做hitTest必然偏移。
  • 未指定图像方向
    创建VNImageRequestHandler时没有传入当前摄像头的朝向参数,会导致Vision识别结果的坐标与实际摄像头采集画面的方向不匹配,竖屏使用时偏移尤其明显。
  • 未开启竖直平面检测
    当前的ARKit配置没有开启竖直平面识别,hitTest时使用的.featurePoint类型是对环境随机特征点的采样,位置本身就存在漂移,无法稳定锚定在竖直的二维码平面上。
  • 未做结果防抖
    每次检测到二维码就直接更新节点位置,频繁的识别误差会导致节点持续偏移抖动。

修复后的完整代码

class ViewController: UIViewController, ARSCNViewDelegate,ARSessionDelegate{

    @IBOutlet var sceneView: ARSCNView!

    var qrRequests = [VNRequest]()
    var detectedDataAnchor: ARAnchor?
    var processing = false

    let configuration = ARWorldTrackingConfiguration()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.sceneView.delegate = self
        self.sceneView.session.delegate = self
        // 开启竖直平面检测
        configuration.planeDetection = [.vertical]
        self.sceneView.session.run(configuration)
        startQrCodeDetection()
    }

    func startQrCodeDetection() {
        let request = VNDetectBarcodesRequest(completionHandler: self.requestHandler)
        self.qrRequests = [request]
    }

    public func session(_ session: ARSession, didUpdate frame: ARFrame) {
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                if self.processing {
                  return
                }
                self.processing = true
                // 传入摄像头朝向,修正图像方向
                let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: frame.capturedImage,
                                                                orientation: frame.camera.imageOrientation,
                                                                options: [:])
                try imageRequestHandler.perform(self.qrRequests)
            } catch {
                self.processing = false
            }
        }
    }

    func requestHandler(request: VNRequest, error: Error?) {
        if let results = request.results, let result = results.first as? VNBarcodeObservation {
            guard let payload = result.payloadStringValue else {
                self.processing = false
                return
            }
            let rect = result.boundingBox
            // 翻转Y轴,适配ARKit坐标系
            let convertedCenter = CGPoint(x: rect.midX, y: 1 - rect.midY)
            DispatchQueue.main.async {
                self.hitTestQrCode(center: convertedCenter)
                self.processing = false
            }
        } else {
            self.processing = false
        }
    }

    func hitTestQrCode(center: CGPoint) {
        // 仅匹配已识别的平面,去掉随机特征点采样
        guard let hitTestResults = self.sceneView?.hitTest(center, types: [.existingPlaneUsingExtent]),
              let hitTestResult = hitTestResults.first else {
            self.processing = false
            return
        }
        // 加防抖:已存在锚点时,仅当位置偏差超过1cm再更新
        if let detectedDataAnchor = self.detectedDataAnchor,
           let node = self.sceneView.node(for: detectedDataAnchor) {
            let currentPosition = SCNVector3FromMatrix4(hitTestResult.worldTransform)
            let oldPosition = node.position
            let distance = sqrt(pow(currentPosition.x - oldPosition.x, 2) + pow(currentPosition.y - oldPosition.y, 2) + pow(currentPosition.z - oldPosition.z, 2))
            if distance > 0.01 {
                node.transform = SCNMatrix4(hitTestResult.worldTransform)
                // 节点放在二维码上方2cm处
                node.position.y += 0.02
            }
        } else {
            self.detectedDataAnchor = ARAnchor(transform: hitTestResult.worldTransform)
            self.sceneView.session.add(anchor: self.detectedDataAnchor!)
        }
    }

    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        if self.detectedDataAnchor?.identifier == anchor.identifier {
            let sphere = SCNSphere(radius: 0.02)
            sphere.firstMaterial?.diffuse.contents = UIColor.red
            let sphereNode = SCNNode(geometry: sphere)
            // 自动上浮2cm,ARKit会自动同步锚点变换无需手动设置
            sphereNode.position.y += 0.02
            return sphereNode
        }
        return nil
    }
    
    // 辅助工具方法:从变换矩阵提取位置向量
    func SCNVector3FromMatrix4(_ matrix: matrix_float4x4) -> SCNVector3 {
        return SCNVector3(matrix.columns.3.x, matrix.columns.3.y, matrix.columns.3.z)
    }
}

内容的提问来源于stack exchange,提问作者Hashan Kahandugoda

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 12:51:03