WWDC Quick Look 💓 By SwiftGGTeam
Build an Action Classifier with Create ML

Build an Action Classifier with Create ML

观看原视频

Highlight

Create ML 在 2020 年新增了 Action Classification(动作分类)模板。它基于 Vision framework 的 Body Pose Estimation(身体姿态估计)提取视频中的骨骼关键点,然后用这些关键点训练一个分类模型。


核心内容

以前,Create ML 已经能用运动传感器数据训练 Activity Classification(活动分类)模型。问题是,很多动作并没有手表或传感器输入,只有手机、摄像头或一段视频。健身动作、体操训练、舞蹈动作都属于这类场景。

Action Classification(动作分类)把输入换成视频里的身体姿态序列。Vision framework 先从每一帧提取人体姿态,Create ML 再把一段时间窗口内的姿态序列训练成分类模型。模型的输出是预定义动作标签,例如 jumping jacks、lunges、squats。

这套流程分成两段。训练阶段,开发者在 Create ML app 里准备按动作分类的视频,设置 Action Duration(动作持续时间)和 Augmentations(数据增强),然后导出 .mlmodel。运行阶段,app 用 Vision 从相机帧或视频帧提取姿态,把连续 60 帧拼成模型输入,再得到动作标签和置信度。

Session 也明确了边界:这个模型由 Vision 的人体姿态估计驱动,适合人的身体动作;它不适合识别动物或物体动作。训练数据也要围绕一个人、完整身体、稳定镜头和一致帧率来准备。

详细内容

用文件夹或标注文件准备训练数据

03:45)训练数据的基本形态是一组视频文件。每个视频只包含一种动作,文件夹名就是模型要预测的标签。Session 的例子收集了 jumping jacks、squats 等五类运动,还加入了 othernone 两个负类。

05:28)如果一段 montage video(混剪视频)里包含多个动作,就用 CSV 或 JSON 标出每个动作的起止时间。官方代码片段给了 JSON 结构:

[
    {
        "file_name": "Montage1.mov",
        "label": "Squats",
        "start_time": 4.5,
        "end_time": 8
    }
]

关键点:

  • file_name 指向包含动作片段的视频文件。
  • label 是这一段视频对应的动作类别,会成为模型预测的标签。
  • start_timeend_time 标出动作在视频中的时间范围。
  • 这种方式适合别人已经剪好或从网上取得的混剪素材。

在 Create ML 里设置动作窗口

07:52)Action Duration 是训练时最关键的参数之一。复杂动作需要更长窗口,例如舞蹈动作可能需要十秒;短而重复的健身动作可以用两秒窗口。Create ML 会根据视频帧率和动作持续时间算出帧数,Session 的健身模型使用 60 帧作为 prediction window(预测窗口)。

08:39)Augmentations 用现有视频生成训练变化。Session 演示了 horizontal flip(水平翻转):如果训练视频里人物都朝左,水平翻转会生成镜像样本,让模型覆盖两个方向。

09:40)训练分成两步。第一步是 feature extraction(特征提取),Vision 会分析每一帧,并编码身体上 18 个 landmarks(关键点)的位置。每个关键点记录 x 坐标、y 坐标和 confidence(置信度)。第二步才是用这些姿态特征训练分类模型。

用 Vision 从视频或图片中提取姿态

14:05)模型接收 Vision 提取出来的人体姿态,原始视频先经过姿态提取。最小入口是 VNDetectHumanBodyPoseRequest

import Vision
let request = VNDetectHumanBodyPoseRequest()

关键点:

  • import Vision 引入人体姿态估计所在的框架。
  • VNDetectHumanBodyPoseRequest 创建身体姿态请求,用来检测视频帧或图片中的人体关键点。

14:10)处理一个视频文件时,可以把这个 request 交给 VNVideoProcessor

import Vision
let videoURL = URL(fileURLWithPath: "your-video-file.MOV")
let startTime = CMTime.zero
let endTime = CMTime.indefinite

let request = VNDetectHumanBodyPoseRequest(completionHandler: { request, error in
    let poses = request.results as! [VNRecognizedPointsObservation]
})

let processor = VNVideoProcessor(url: videoURL)
try processor.add(request)
try processor.analyze(with: CMTimeRange(start: startTime, end: endTime))

关键点:

  • videoURL 指向要分析的视频文件。
  • startTimeendTime 定义分析范围;这里从开头分析到视频结束。
  • completion handler 里从 request.results 取得 [VNRecognizedPointsObservation]
  • VNVideoProcessor 负责把 request 应用到视频帧。
  • analyze(with:) 按给定时间范围执行分析。

14:26)处理图片或相机帧时,Session 使用 VNImageRequestHandler

import Vision
let request = VNDetectHumanBodyPoseRequest()
// Use either one from image URL, CVPixelBuffer, CMSampleBuffer, CGImage, CIImage, etc. in image request handler, based on the context.
let handler = VNImageRequestHandler(url: URL(fileURLWithPath: "your-image.jpg"))

try handler.perform([request])
let poses = request.results as! [VNRecognizedPointsObservation]

关键点:

  • 同一个 VNDetectHumanBodyPoseRequest 可以用于图片输入。
  • VNImageRequestHandler 的输入可以来自 image URL,也可以来自 CVPixelBufferCMSampleBufferCGImageCIImage
  • perform([request]) 执行姿态检测。
  • 结果同样是 [VNRecognizedPointsObservation]

把连续姿态拼成模型输入

14:57)Create ML 导出的动作分类模型接收的是一个三维 multi-array。每一帧姿态先转成 [1, 3, 18],60 帧再拼成 [60, 3, 18]

import Vision
import CoreML

// Assume pose1, pose2, ..., have been obtained from a video file or camera stream.
let pose1: VNRecognizedPointsObservation
let pose2: VNRecognizedPointsObservation
// ...

// Get a [1, 3, 18] dimension multi-array for each frame
let poseArray1 = try pose1.keypointsMultiArray()
let poseArray2 = try pose2.keypointsMultiArray()
// ...

// Get a [60, 3, 18] dimension prediction window from 60 frames
let modelInput = MLMultiArray(concatenating: [poseArray1, poseArray2], axis: 0, dataType: .float)

关键点:

  • keypointsMultiArray() 是 Vision 提供的便捷 API,把一帧姿态转成模型需要的 multi-array。
  • 单帧数组维度是 [1, 3, 18],对应一个时间点、三个数值通道和 18 个身体关键点。
  • MLMultiArray(concatenating:axis:dataType:) 沿时间轴拼接多帧姿态。
  • 训练时窗口大小是 60,运行时也要准备 60 帧窗口。

在 app 里维护窗口并预测

16:27)Xcode demo 把姿态提取、窗口缓存和预测封装在 Predictor 里:

import Foundation
import CoreML
import Vision

@available(iOS 14.0, *)
class Predictor {
    /// Fitness classifier model.
    let fitnessClassifier = FitnessClassifier()

    /// Vision body pose request.
    let humanBodyPoseRequest = VNDetectHumanBodyPoseRequest()

    /// A rotation window to save the last 60 poses from past 2 seconds.
    var posesWindow: [VNRecognizedPointsObservation?] = []
    init() {
        posesWindow.reserveCapacity(predictionWindowSize)
    }

    /// Extracts poses from a frame.
    func processFrame(_ samplebuffer: CMSampleBuffer) throws -> [VNRecognizedPointsObservation] {
        // Perform Vision body pose request
        let framePoses = extractPoses(from: samplebuffer)

        // Select the most promiment person.
        let pose = try selectMostProminentPerson(from: framePoses)

        // Add the pose to window
        posesWindow.append(pose)

        return framePoses
    }

    // Make a prediction when window is full, periodically
    var isReadyToMakePrediction: Bool {
        posesWindow.count == predictionWindowSize
    }

    /// Make a model prediction on a window.
    func makePrediction() throws -> PredictionOutput {
        // Prepare model input: convert each pose to a multi-array, and concatenate multi-arrays.
        let poseMultiArrays: [MLMultiArray] = try posesWindow.map { person in
            guard let person = person else {
                // Pad 0s when no person detected.
                return zeroPaddedMultiArray()
            }
            return try person.keypointsMultiArray()
        }

        let modelInput = MLMultiArray(concatenating: poseMultiArrays, axis: 0, dataType: .float)

        // Perform prediction
        let predictions = try fitnessClassifier.prediction(poses: modelInput)

        // Reset poses window
        posesWindow.removeFirst(predictionInterval)

        return (
            label: predictions.label,
            confidence: predictions.labelProbabilities[predictions.label]!
        )
    }
}

关键点:

  • FitnessClassifier() 是从 Create ML 导出的 Core ML 模型。
  • humanBodyPoseRequest 只初始化一次,Session 建议避免每帧重复创建。
  • posesWindow 保存最近 60 帧姿态,对应训练时的两秒窗口。
  • processFrame(_:)CMSampleBuffer 中提取姿态,并选出一个最显著的人。
  • Action Classifier 只接收单人输入;多人画面需要自己实现选择逻辑。
  • 没检测到人时,示例用 zeroPaddedMultiArray() 补零。
  • fitnessClassifier.prediction(poses:) 返回预测标签和各类别置信度。
  • removeFirst(predictionInterval) 让窗口向前滑动,方便连续预测。

训练和使用时的限制

22:00)模型需要 repetition and variety(重复和多样性)。Session 建议每个动作大约准备 50 个视频,并覆盖不同人的风格、能力和速度。如果用户会从侧面或背面运动,训练数据也要包含这些角度。

23:35)Action Duration 要匹配动作长度,各类动作时长也要尽量接近。视频帧率会影响实际窗口长度,所以训练和测试视频的平均帧率需要保持一致。

24:21)运行时只选择一个人。如果 Vision 检测到多个人,app 可以提醒用户只保留一个人在画面内,也可以根据姿态关键点坐标按大小或位置选择一个人。

24:52)如果要计数重复动作,每个训练视频最好只包含一次单个动作。预测时还需要可靠的触发点,或用平滑逻辑更新计数器。

核心启发

  • 做一个无需触屏的居家健身计时器:当模型识别到 jumping jacks、lunges 或 squats 时自动启动计时,停止动作后回到 other 状态。Session 的 demo 就用连续预测驱动挑战计时,适合把手机放远后训练。
  • 做一个动作质量评分工具:用预测结果里的 confidence value(置信度)给动作质量打分。Session 明确提到可以把置信度用于评估动作质量,并与训练视频中的示例动作比较。
  • 做一个重复次数计数器:把训练视频整理成“一段视频只包含一次单个动作”,运行时用 prediction window 和 smoothing logic 更新计数。这个方案适合深蹲、弓步、开合跳这类重复动作。
  • 做一个训练数据检查清单:在用户导入视频前检查是否只有一个人、全身是否入镜、镜头是否稳定、衣服是否遮挡动作。Session 把这些都列为影响模型效果的关键条件。
  • 做一个混剪视频标注小工具:让用户在长视频上标出 file_namelabelstart_timeend_time,再导出 JSON 或 CSV 给 Create ML 训练。这样可以复用已有素材,不必先把每段动作剪成独立视频。

关联 Session

评论

GitHub Issues · utterances