如何将MLKit Vision3DPoint转换为UV坐标或AVFoundation坐标?
把MLKit Vision3DPoint转换为UV/AVFoundation 2D坐标
要解决这个问题,核心是利用相机的内参矩阵(Intrinsic Matrix)把3D空间点做透视投影,映射到2D图像平面上。MLKit返回的3D关键点是基于相机坐标系的(右手系:X向右,Y向上,Z轴沿相机光轴向前),而我们需要把它转换成图像的归一化UV坐标(0-1范围)或者直接的像素坐标。
核心原理
相机内参包含了焦距(fx, fy)和主点(cx, cy),这两个参数是将3D点投影到2D图像的关键,公式如下:
u = (fx * X / Z) + cx v = (fy * Y / Z) + cy
其中:
- (X,Y,Z) 是MLKit返回的Vision3DPoint的坐标
- fx/fy 是相机在X/Y方向的焦距
- cx/cy 是图像主点的像素坐标(通常接近图像中心)
计算出u和v后,除以图像的宽高就能得到归一化的UV坐标;如果要直接得到屏幕像素坐标,再乘以屏幕的宽高即可。
具体实现步骤&代码修改
首先,我们需要从采样缓冲(sampleBuffer)中获取相机的校准数据(包含内参),然后编写转换函数处理每个3D关键点:
1. 编写3D点转2D坐标的函数
func project3DPointToUV(_ point: Vision3DPoint, calibrationData: AVCameraCalibrationData, imageSize: CGSize) -> CGPoint? { // 过滤掉相机后方的点(Z<=0时投影无效) guard point.z > 0 else { return nil } let intrinsics = calibrationData.intrinsicMatrix // 提取内参的焦距和主点 let fx = intrinsics[0][0] let fy = intrinsics[1][1] let cx = intrinsics[2][0] let cy = intrinsics[2][1] // 执行透视投影计算像素坐标 let pixelX = (fx * point.x / point.z) + cx let pixelY = (fy * point.y / point.z) + cy // 转换为归一化UV坐标(0-1范围) let uvX = pixelX / imageSize.width let uvY = pixelY / imageSize.height // 处理前置摄像头的镜像问题(如果使用前置相机) // if camera.position == .front { // return CGPoint(x: 1 - uvX, y: uvY) // } return CGPoint(x: uvX, y: uvY) }
2. 修改你的CaptureOutput代码
在处理姿态检测结果前,先获取相机校准数据和图像尺寸,然后逐个转换关键点:
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // 准备输入图像 let image = VisionImage(buffer: sampleBuffer) image.orientation = imageOrientation(deviceOrientation: UIDevice.current.orientation, cameraPosition: camera.position) // 获取相机校准数据和图像尺寸 guard let formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer), let calibrationData = AVCameraCalibrationData(formatDescription: formatDesc), let imageDimensions = CMVideoFormatDescriptionGetDimensions(formatDesc) else { print("无法获取相机校准数据或图像尺寸") return } let imageSize = CGSize(width: CGFloat(imageDimensions.width), height: CGFloat(imageDimensions.height)) // 异步检测姿态 poseDetector.process(image) { [unowned self] (detectedPoses, error) in guard error == nil else { print("姿态检测出错: \(error!)") return } guard let detectedPoses = detectedPoses, !detectedPoses.isEmpty else { return } var displayPoints = [(CGPoint, UIColor)]() for pose in detectedPoses { for landmark in pose.landmarks { let position = landmark.position // 转换3D点到UV坐标 if let uvPoint = project3DPointToUV(position, calibrationData: calibrationData, imageSize: imageSize) { // 如果需要屏幕像素坐标,替换成下面一行: // let screenPoint = CGPoint(x: uvPoint.x * UIScreen.main.bounds.width, y: uvPoint.y * UIScreen.main.bounds.height) displayPoints.append((uvPoint, UIColor.red)) } } } owner.pointView.points = displayPoints owner.pointView.setNeedsDisplay() } }
关键注意事项
- Z值过滤:如果Z<=0,说明关键点在相机后方,投影出来的坐标没有意义,直接忽略即可。
- 图像方向&镜像:前置摄像头的图像是镜像的,如果你发现UV坐标左右颠倒,可以取消注释代码里的前置摄像头镜像处理逻辑。
- 坐标系统对齐:如果投影出来的Y坐标上下颠倒,可能需要调整公式中的Y符号,比如改成
let pixelY = (fy * (-point.y) / point.z) + cy,这取决于MLKit的Y轴方向和你图像坐标系的匹配情况,可以根据实际测试调整。 - 内参准确性:AVCapture自动提供的校准数据是设备专属的,确保你使用的是当前捕获设备的校准数据,不要硬编码内参值。
内容的提问来源于stack exchange,提问作者Chat Dp




