WWDC Quick Look 💓 By SwiftGGTeam
Explore 3D body pose and person segmentation in Vision

Explore 3D body pose and person segmentation in Vision

观看原视频

Highlight

Vision 框架新增 3D 人体姿态检测(VNDetectHumanBodyPose3DRequest),无需 ARKit 即可从普通照片提取 17 个关节的 3D 位置(以米为单位);同时支持深度数据输入和最多四人的实例分割,让人体理解从 2D 平面走向真实空间。

核心内容

从 2D 到 3D:人体姿态检测的进化

Vision 框架之前提供的是 2D 人体姿态检测,返回的是归一化像素坐标。WWDC23 引入了 VNDetectHumanBodyPose3DRequest,直接从图像中提取 3D 关节位置。

3D 姿态返回 17 个关节,按组组织:

  • 头部:头顶、头部中心
  • 躯干:左肩、右肩、脊柱、髋部中心(root joint)、左髋、右髋
  • 左臂:左腕、左肩、左肘
  • 右臂:右腕、右肩、右肘
  • 左腿:左髋、左膝、左踝
  • 右腿:右髋、右膝、右踝

左右是相对于被检测者而言,不是图像的左右。

00:51

与 2D 检测不同,3D 关节位置以米为单位返回,原点位于髋部中心(root joint)。这让你可以直接测量真实世界中的距离,比如一个人的手腕离地面多高。

深度数据:让 3D 更精确

Vision 现在接受深度数据作为输入。VNImageRequestHandler 新增了支持 AVDepthData 的初始化方法,适用于 CVPixelBufferCMSampleBuffer

如果图像文件本身包含深度数据(如人像模式照片),Vision 会自动提取,无需修改现有 API。

深度数据包含:

  • 深度图:以视差(Disparity)或深度(Depth)格式存储
  • 相机标定数据:内参、外参、镜头畸变参数

有深度数据时,bodyHeight 返回实测身高;没有时返回参考值 1.8 米。你可以通过 heightEstimation 属性判断使用了哪种方式。

10:41

多人实例分割:从”所有人”到”每个人”

之前的 GeneratePersonSegmentation 请求返回一个包含所有人的单一 mask。新引入的 person instance mask 请求可以输出最多四个独立的人的 mask,每个都带置信度分数。

你可以单独选择、提取图像中的某一个人。如果只需要背景,选择 instance 0 即可。

对于超过四个人的场景,建议先用 Vision 的人脸检测 API 计数,超过四人时回退到传统的单人分割请求。

12:20

详细内容

3D 人体姿态检测的工作流程

使用 VNDetectHumanBodyPose3DRequest 的流程与 2D 请求一致:

import Vision
import UIKit

func detect3DBodyPose(in image: UIImage) {
    // 1. 创建 3D 人体姿态检测请求
    let request = VNDetectHumanBodyPose3DRequest()
    
    // 2. 初始化图像请求处理器
    guard let cgImage = image.cgImage else { return }
    let handler = VNImageRequestHandler(cgImage: cgImage)
    
    // 3. 执行请求
    do {
        try handler.perform([request])
        
        // 4. 获取结果
        if let observation = request.results?.first as? VNHumanBodyPose3DObservation {
            process3DPose(observation)
        }
    } catch {
        print("3D pose detection failed: \(error)")
    }
}

关键点:

  • VNDetectHumanBodyPose3DRequest 是新的 3D 姿态检测请求类
  • VNImageRequestHandler 的用法与 2D 请求完全一致
  • results 返回 VNHumanBodyPose3DObservation 数组,当前版本只返回最突出人物的一个结果
  • 错误处理使用标准的 do-catch 模式

获取关节位置数据

import Vision
import simd

func process3DPose(_ observation: VNHumanBodyPose3DObservation) {
    // 获取特定关节的 3D 位置
    do {
        let leftWrist = try observation.recognizedPoint(.leftWrist)
        print("Left wrist position: \(leftWrist.position)")
        
        // position 是 simd_float4x4 矩阵,第三列包含平移值
        let translation = leftWrist.position.columns.3
        print("Translation - x: \(translation.x), y: \(translation.y), z: \(translation.z)")
    } catch {
        print("Failed to get left wrist point: \(error)")
    }
    
    // 获取一组关节
    do {
        let torsoPoints = try observation.recognizedPoints(.torso)
        for (jointName, point) in torsoPoints {
            print("\(jointName): \(point.position)")
        }
    } catch {
        print("Failed to get torso points: \(error)")
    }
    
    // 获取估计身高
    print("Estimated body height: \(observation.bodyHeight) meters")
    print("Height estimation method: \(observation.heightEstimation)")
}

关键点:

  • recognizedPoint(_:) 通过关节名称获取单个关节,返回 VNHumanBodyRecognizedPoint3D
  • recognizedPoints(_:) 通过组名获取一组关节,返回字典
  • positionsimd_float4x4 矩阵,与 ARKit 兼容,第三列(columns.3)包含 x/y/z 平移值
  • bodyHeight 返回估计身高(米)
  • heightEstimation 指示身高是实测值还是参考值

理解 3D 几何类层级

Vision 引入了新的 3D 几何基类:

  • VNPoint3D:基类,定义 simd_float4x4 矩阵存储 3D 位置
  • VNRecognizedPoint3D:继承位置信息,添加标识符(如关节名称)
  • VNHumanBodyRecognizedPoint3D:添加局部位置(localPosition)和父关节引用
func analyzeJointHierarchy(_ observation: VNHumanBodyPose3DObservation) {
    do {
        let leftWrist = try observation.recognizedPoint(.leftWrist)
        
        // model position: 相对于 root joint(髋部中心)的全局位置
        let globalPosition = leftWrist.position
        print("Global position relative to root: \(globalPosition)")
        
        // local position: 相对于父关节(这里是左肘)的局部位置
        let localPosition = leftWrist.localPosition
        print("Local position relative to parent: \(localPosition)")
        
        // 从 local position 计算关节角度
        let angle = calculateLocalAngleToParent(localPosition)
        print("Joint angle: \(angle)")
        
    } catch {
        print("Error: \(error)")
    }
}

func calculateLocalAngleToParent(_ localPosition: simd_float4x4) -> (pitch: Float, yaw: Float, roll: Float) {
    let pos = localPosition.columns.3
    let vectorLength = sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z)
    
    // pitch: 90度旋转将几何体从默认朝下的方向调整到骨骼方向
    let pitch = Float.pi / 2
    
    // yaw: 使用反余弦计算
    let yaw = acos(pos.z / vectorLength)
    
    // roll: 使用反正切计算
    let roll = atan2(pos.y, pos.x)
    
    return (pitch, yaw, roll)
}

关键点:

  • position 始终相对于 skeleton 的 root joint(髋部中心)
  • localPosition 相对于父关节,适合分析身体局部运动
  • simd_float4x4 与 ARKit 和 SceneKit 的坐标系统一致
  • 通过 localPosition 可以计算关节之间的角度(pitch/yaw/roll)

将 3D 关节投影回 2D 图像

func projectJointsToImage(_ observation: VNHumanBodyPose3DObservation, imageSize: CGSize) {
    do {
        // 获取 root joint 在 2D 图像中的位置
        let rootJointInImage = try observation.pointInImage(.root)
        print("Root joint in image: \(rootJointInImage)")
        
        // 获取左肩在 2D 图像中的位置
        let leftShoulderInImage = try observation.pointInImage(.leftShoulder)
        print("Left shoulder in image: \(leftShoulderInImage)")
        
        // 将 3D 骨架与原始图像对齐时,需要:
        // 1. 缩放:根据已知关节的 3D 和 2D 距离比例缩放图像平面
        // 2. 平移:使用 root joint 的 2D 位置确定偏移量
        
    } catch {
        print("Projection failed: \(error)")
    }
}

关键点:

  • pointInImage(_:) 将 3D 关节坐标投影回 2D 图像坐标
  • 返回值使用 lower-left 原点的 VNPoint 坐标系
  • 在渲染环境中需要转换到以图像中心为原点的坐标系
  • 结合 cameraOriginMatrix 可以从相机视角渲染场景

使用相机原点矩阵

func setupCameraPerspective(_ observation: VNHumanBodyPose3DObservation) {
    // cameraOriginMatrix 包含相机相对于被检测者的位置和旋转
    let cameraMatrix = observation.cameraOriginMatrix
    print("Camera origin matrix: \(cameraMatrix)")
    
    // 使用旋转信息将图像平面朝向相机
    // 只使用旋转部分(3x3 子矩阵),忽略平移(最后一列)
    let rotationMatrix = simd_float3x3(
        columns: (
            simd_float3(cameraMatrix.columns.0.x, cameraMatrix.columns.0.y, cameraMatrix.columns.0.z),
            simd_float3(cameraMatrix.columns.1.x, cameraMatrix.columns.1.y, cameraMatrix.columns.1.z),
            simd_float3(cameraMatrix.columns.2.x, cameraMatrix.columns.2.y, cameraMatrix.columns.2.z)
        )
    )
    
    // 计算逆旋转,让图像平面正对相机
    let inverseRotation = rotationMatrix.inverse
    print("Inverse rotation: \(inverseRotation)")
}

关键点:

  • cameraOriginMatrix 返回 simd_float4x4 矩阵,表示相机在 3D 空间中的位置和朝向
  • 相机可能不是正对被检测者,这个矩阵帮助理解相对位置
  • 从相机视角渲染时,使用矩阵的旋转部分(忽略平移)
  • 通过逆变换让图像平面正确朝向相机

带深度数据的请求处理

import Vision
import AVFoundation

func detectPoseWithDepth(image: CGImage, depthData: AVDepthData) {
    // 使用带深度数据的初始化方法
    let handler = VNImageRequestHandler(
        cgImage: image,
        depthData: depthData,
        options: [:]
    )
    
    let request = VNDetectHumanBodyPose3DRequest()
    
    do {
        try handler.perform([request])
        if let observation = request.results?.first as? VNHumanBodyPose3DObservation {
            // 有深度数据时,bodyHeight 返回实测值
            print("Measured height: \(observation.bodyHeight)")
            print("Estimation: \(observation.heightEstimation)") // .measured
        }
    } catch {
        print("Detection failed: \(error)")
    }
}

// 从人像照片自动提取深度
func detectPoseFromPortraitPhoto(imageURL: URL) {
    // 如果文件包含深度数据,Vision 会自动提取
    let handler = VNImageRequestHandler(url: imageURL)
    let request = VNDetectHumanBodyPose3DRequest()
    
    do {
        try handler.perform([request])
        // 处理结果...
    } catch {
        print("Detection failed: \(error)")
    }
}

关键点:

  • VNImageRequestHandler 新增接受 AVDepthData 参数的重载方法
  • AVDepthData 是 Apple SDK 中深度元数据的统一容器
  • 人像模式照片自动包含深度数据(以视差图形式存储)
  • 有深度数据时 heightEstimation 返回 .measured,否则返回 .reference
  • LiDAR 设备在实时捕获时可提供更精确的场景测量

多人实例分割

import Vision

func segmentMultiplePeople(in image: UIImage) {
    guard let cgImage = image.cgImage else { return }
    
    // 创建多人实例分割请求
    let request = VNGeneratePersonInstanceMaskRequest()
    
    // 可选:指定只返回特定实例
    // request.instanceNumber = 1  // 只返回第一个人
    
    let handler = VNImageRequestHandler(cgImage: cgImage)
    
    do {
        try handler.perform([request])
        
        if let observation = request.results?.first as? VNGeneratePersonInstanceMaskObservation {
            // 获取所有可用实例
            let instanceCount = observation.allInstances.count
            print("Detected \(instanceCount) people")
            
            // 提取每个人的 mask
            for instanceIndex in observation.allInstances {
                let mask = try observation.generateMaskedImage(
                    ofInstances: [instanceIndex],
                    from: handler,
                    croppedToInstancesExtent: false
                )
                print("Instance \(instanceIndex) mask generated")
            }
            
            // 提取背景(instance 0)
            let backgroundMask = try observation.generateMaskedImage(
                ofInstances: [0],
                from: handler,
                croppedToInstancesExtent: false
            )
        }
    } catch {
        print("Instance segmentation failed: \(error)")
    }
}

// 处理超过四人的场景
func handleCrowdedScene(image: UIImage) {
    // 先用人脸检测计数
    let faceRequest = VNDetectFaceRectanglesRequest()
    let handler = VNImageRequestHandler(cgImage: image.cgImage!)
    
    do {
        try handler.perform([faceRequest])
        let faceCount = faceRequest.results?.count ?? 0
        
        if faceCount > 4 {
            // 回退到传统的单人分割请求
            let segmentationRequest = VNGeneratePersonSegmentationRequest()
            try handler.perform([segmentationRequest])
            // 处理单一 mask...
        } else {
            // 使用实例分割
            let instanceRequest = VNGeneratePersonInstanceMaskRequest()
            try handler.perform([instanceRequest])
            // 处理独立 mask...
        }
    } catch {
        print("Error: \(error)")
    }
}

关键点:

  • VNGeneratePersonInstanceMaskRequest 是新的多人实例分割请求
  • 最多支持 4 个人,每人一个独立 mask
  • instanceNumber 属性可指定只返回特定实例(0 是背景)
  • allInstances 返回检测到的所有实例索引
  • generateMaskedImage(ofInstances:from:croppedToInstancesExtent:) 生成指定实例的 mask 图像
  • 超过 4 人的场景建议先用人脸检测计数,再决定使用哪种分割策略

核心启发

  • 做一个”姿势纠正”健身应用

    • 做什么:用户做瑜伽或健身动作时,实时检测 3D 姿态并与标准姿势对比,给出纠正建议
    • 为什么值得做:3D 关节位置以米为单位返回,可以精确测量关节角度,判断动作是否标准
    • 怎么开始:用 VNDetectHumanBodyPose3DRequest 获取关节位置,通过 localPosition 计算关节角度(pitch/yaw/roll),与预设的标准角度范围对比
  • 做一个”身高测量”工具

    • 做什么:从人像照片中自动测量人物身高
    • 为什么值得做:Vision 在有深度数据时返回实测身高,无需 ARKit 或专门的硬件
    • 怎么开始:加载人像模式照片(自动包含深度数据),执行 VNDetectHumanBodyPose3DRequest,读取 bodyHeight 属性,通过 heightEstimation 确认是实测值
  • 做一个”合影背景替换”应用

    • 做什么:从合影中单独提取某一个人,替换背景或合成到新场景
    • 为什么值得做:实例分割 API 可以独立提取最多 4 个人,每个人有单独的 mask
    • 怎么开始:用 VNGeneratePersonInstanceMaskRequest 获取各实例 mask,选择目标人物的 instance index,用 generateMaskedImage 提取,再与自定义背景合成
  • 做一个”3D 人体模型”预览工具

    • 做什么:从普通照片生成 3D 骨架可视化,支持旋转查看
    • 为什么值得做:无需 ARKit 或深度相机,普通照片就能获得 3D 关节位置
    • 怎么开始:用 VNDetectHumanBodyPose3DRequest 获取 3D 关节,用 cameraOriginMatrix 获取相机位置,在 SceneKit 或 RealityKit 中渲染骨架,支持视角切换

关联 Session

评论

GitHub Issues · utterances