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 的初始化方法,适用于 CVPixelBuffer 和 CMSampleBuffer。
如果图像文件本身包含深度数据(如人像模式照片),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(_:)通过关节名称获取单个关节,返回VNHumanBodyRecognizedPoint3DrecognizedPoints(_:)通过组名获取一组关节,返回字典position是simd_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
- Detect animal poses in Vision — Vision 的动物姿态检测,与人体姿态检测互补
- Lift subjects from images in your app — 从图像中提取主体,与 person segmentation 相关
- Discover advancements in iOS camera capture — iOS 相机捕获的进阶功能,包括深度数据获取
- Build spatial experiences with RealityKit — 将 3D 人体姿态数据与 RealityKit 结合创造空间体验
评论
GitHub Issues · utterances