You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何将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

火山引擎 最新活动