如何将人脸检测获取的CGPoint转换为SceneKit中的SCNVector3?
把Vision检测的鼻子CGPoint转换成SceneKit的SCNVector3
嘿,刚好做过类似的人脸跟踪+3D物体挂载需求,咱们一步步把2D的鼻子坐标映射到3D空间里去~
首先得搞清楚两个坐标系的核心差异:
- Vision返回的
CGPoint是基于摄像头捕获图像的坐标系,原点在图像左上角,X向右,Y向下; - SceneKit的
SCNVector3是3D右手坐标系,默认原点在视图中心,X向右,Y向上,Z轴向外(相机默认在Z=0,看向-Z方向)。
步骤1:把图像坐标转成视图坐标
Vision的坐标是相对于摄像头的原始分辨率(比如1920x1080),而你的SceneKit视图尺寸可能和图像不一样,再加上前置摄像头是镜像的,所以第一步要做适配:
// 替换成你的摄像头实际捕获尺寸(可从AVCaptureDevice的activeFormat里获取) let cameraImageSize = CGSize(width: 1920, height: 1080) // visionNosePoint是Vision返回的鼻子部位CGPoint let normalizedX = 1.0 - visionNosePoint.x / cameraImageSize.width // 前置摄像头镜像反转X let normalizedY = visionNosePoint.y / cameraImageSize.height // 映射到SceneKit视图的坐标系 let scnViewPoint = CGPoint( x: normalizedX * yourSCNView.bounds.width, y: normalizedY * yourSCNView.bounds.height )
步骤2:把视图2D点转成3D空间的SCNVector3
SceneKit提供了unprojectPoint(_:)方法,可以把视图平面上的点转换成3D空间坐标。因为2D点对应3D里的一条射线,我们需要指定物体的Z位置(比如人脸大概在相机前方-Z方向0.5~0.8的位置,根据你的相机设置调整):
// 创建视图近平面(Z=0)和远平面(Z=1)上的点 let nearPlanePoint = SCNVector3(scnViewPoint.x, scnViewPoint.y, 0) let farPlanePoint = SCNVector3(scnViewPoint.x, scnViewPoint.y, 1) // 转换成3D空间坐标 let unprojectedNear = yourSCNView.unprojectPoint(nearPlanePoint) let unprojectedFar = yourSCNView.unprojectPoint(farPlanePoint) // 计算射线方向向量 let direction = SCNVector3( unprojectedFar.x - unprojectedNear.x, unprojectedFar.y - unprojectedNear.y, unprojectedFar.z - unprojectedNear.z ) // 设定目标Z位置(比如人脸在Z=-0.6处,根据实际场景调整) let targetZ: Float = -0.6 // 计算射线到目标Z位置的参数t let t = (targetZ - unprojectedNear.z) / direction.z // 最终的3D位置 let final3DPosition = SCNVector3( unprojectedNear.x + t * direction.x, unprojectedNear.y + t * direction.y, targetZ )
额外注意事项
- 设备方向适配:如果你的APP支持横竖屏,摄像头捕获的图像方向可能和视图方向不一致,需要先对Vision返回的坐标做旋转处理(比如用
CGAffineTransform旋转90度)。 - 性能优化:逐帧处理时,坐标转换尽量放在后台线程完成,只把最终的
SCNVector3传到主线程更新节点位置,避免阻塞UI。 - 相机参数调整:如果你的SceneKit相机不是默认设置(比如调整了FOV、位置),需要对应修改
targetZ的值,确保3D物体刚好落在人脸区域。
完整示例代码
把这些逻辑整合到Vision的回调里:
func processFaceObservations(_ observations: [VNFaceObservation]) { guard let face = observations.first, let noseLandmark = face.landmarks?.nose?.points.first?.location else { return } DispatchQueue.main.async { [weak self] in guard let self = self, let scnView = self.sceneView else { return } // 步骤1:图像坐标转视图坐标 let cameraSize = CGSize(width: 1920, height: 1080) let normalizedX = 1.0 - noseLandmark.x / cameraSize.width let normalizedY = noseLandmark.y / cameraSize.height let viewPoint = CGPoint( x: normalizedX * scnView.bounds.width, y: normalizedY * scnView.bounds.height ) // 步骤2:视图坐标转3D坐标 let near = SCNVector3(viewPoint.x, viewPoint.y, 0) let far = SCNVector3(viewPoint.x, viewPoint.y, 1) let unprojectedNear = scnView.unprojectPoint(near) let unprojectedFar = scnView.unprojectPoint(far) let dirX = unprojectedFar.x - unprojectedNear.x let dirY = unprojectedFar.y - unprojectedNear.y let dirZ = unprojectedFar.z - unprojectedNear.z let targetZ: Float = -0.6 let t = (targetZ - unprojectedNear.z) / dirZ self.nose3DNode.position = SCNVector3( unprojectedNear.x + t * dirX, unprojectedNear.y + t * dirY, targetZ ) } }
这样就能让3D物体精准跟着鼻子移动啦!
内容的提问来源于stack exchange,提问作者Himanshu Ahuja
相关产品推荐
相关产品推荐

