AI-Фітнес-тренер з корекцією техніки вправ мобільного додатку
Різниця між підсчітом повторень та корекцією техніки—принципіальна. Повторення—"скільки". Техніка—"як": кут сгибання колена в приседанні (має бути ≥ 90°, не виходити за носок), нейтральне положення поясниці, контроль лопаток. Все це потребує точного геометричного аналізу позы на кожному кадрі—та зрозумілої обратної зв'язки в реальному часі.
Pose Estimation: точність важлива
Для корекції техніки MediaPipe BlazePose Full (33 точки включаючи кисті та стопи) точніша за Vision VNDetectHumanBodyPoseRequest (19 точок). Критична різниця: MediaPipe дає 3D координати (x, y, z) у нормалізованому просторі—це дозволяє рахувати кути в реальному 3D, а не тільки в площині камери.
// MediaPipe Tasks iOS SDK
import MediaPipeTasksVision
class FormAnalyzer: PoseLandmarkerLiveStreamDelegate {
private var poseLandmarker: PoseLandmarker?
func setup() throws {
let options = PoseLandmarkerOptions()
options.baseOptions.modelAssetPath = Bundle.main.path(
forResource: "pose_landmarker_full",
ofType: "task"
)!
options.runningMode = .liveStream
options.numPoses = 1
options.minPoseDetectionConfidence = 0.7
options.minPosePresenceConfidence = 0.7
options.minTrackingConfidence = 0.7
options.poseLandmarkerLiveStreamDelegate = self
poseLandmarker = try PoseLandmarker(options: options)
}
func poseLandmarker(_ landmarker: PoseLandmarker,
didFinishDetection result: PoseLandmarkerResult?,
timestampInMilliseconds: Int,
error: Error?) {
guard let landmarks = result?.landmarks.first else { return }
analyzeSquatForm(landmarks: landmarks)
}
}
Геометричний аналіз техніки: приседання як приклад
Приседання—найбільш аналітично розібране вправа. Ключові метрики:
Кут сгибання колена
func kneeFlexionAngle(landmarks: [NormalizedLandmark]) -> Double {
// Точки: бедро (23/24), колено (25/26), голеностоп (27/28)
let hip = landmarks[23] // leftHip
let knee = landmarks[25] // leftKnee
let ankle = landmarks[27] // leftAnkle
// Вектори від колена до бедра та від колена до голеностопа
let vecToHip = SIMD2<Double>(
Double(hip.x - knee.x),
Double(hip.y - knee.y)
)
let vecToAnkle = SIMD2<Double>(
Double(ankle.x - knee.x),
Double(ankle.y - knee.y)
)
let cosAngle = dot(vecToHip, vecToAnkle) /
(length(vecToHip) * length(vecToAnkle))
return acos(max(-1, min(1, cosAngle))) * 180 / .pi
}
Норма в нижній точці приседання: 80–100°. Менше 80°—занадто глибоко для початківців, більше 100°—неповна амплітуда. Зворотний зв'язок: "Чуть глибше—согніть колени ще на 10–15°".
Колено виходить за носок
func kneeOverToe(landmarks: [NormalizedLandmark]) -> Bool {
let knee = landmarks[25]
let toe = landmarks[31] // leftFootIndex
// Якщо X колена (горизонталь) значительно дальше X носка при виді сбоку
return Double(knee.z - toe.z) > 0.05 // z у MediaPipe—глубина
}
MediaPipe 3D координата z—глубина від камери. При виді сбоку це дає проекцію вперед-назад. Для фронтального виду потрібна бокова установка або 3D-реконструкція з фронтальних даних (складніше).
Спина: нейтральне положення
func spineAngle(landmarks: [NormalizedLandmark]) -> Double {
let shoulder = landmarks[11] // leftShoulder
let hip = landmarks[23] // leftHip
// Кут лінії плечо-бедро до вертикалі
let dx = Double(shoulder.x - hip.x)
let dy = Double(shoulder.y - hip.y)
return atan2(dx, -dy) * 180 / .pi
}
30° від вертикалі при нижній точці = сутулість / "вываливання" корпуса вперед. Коррекція: "Поднимите грудь, сведіть лопатки".
Голосова обратна зв'язок в реальному часі
Текстові підказки на екрані—неручно: користувач дивиться на вправу, не на телефон. Голосові підказки працюють краще.
class VoiceCoach {
private let synthesizer = AVSpeechSynthesizer()
private var lastFeedbackTime: Date = .distantPast
private let feedbackCooldown: TimeInterval = 3.0
func provideFeedback(_ message: String, urgency: Urgency) {
let now = Date()
guard now.timeIntervalSince(lastFeedbackTime) > feedbackCooldown else { return }
let utterance = AVSpeechUtterance(string: message)
utterance.voice = AVSpeechSynthesisVoice(language: "uk-UA")
utterance.rate = urgency == .critical ? 0.55 : 0.48
utterance.pitchMultiplier = urgency == .critical ? 1.1 : 1.0
utterance.volume = 0.9
synthesizer.speak(utterance)
lastFeedbackTime = now
}
}
feedbackCooldown = 3.0—критично. Без нього система долбить одне й те ж повідомлення 30 разів в секунду. Користувач вирубає звук.
Пріоритизація: кілька помилок одночасно→вибираємо найбільш критичну. Ієрархія: безпека (колено усередину→ризик травми) > техніка (неповна амплітуда) > рекомендація (дихайте рівномірно).
Аналіз фази вправи
Коррекція техніки релевантна тільки в потрібній фазі:
- Фаза еквцентрики (опускання в присед): перевіряємо спину та положення коліней
- Нижня точка: перевіряємо кут сгибання, положення коліней над носком
- Фаза концентрики (вставання): перевіряємо, чи не "складується" користувач
Детекція фази—через похідну tracking-точки (бедра). Похідна Y-координати: від'ємна (опускання) = еквцентрика, мінімум при нижній точці, позитивна = концентрика.
Підтримувані вправи та масштабування
Кожна вправа потребує власного набору метрик та правил. Реалізуємо через протокол:
protocol ExerciseFormAnalyzer {
var exerciseType: ExerciseType { get }
func analyze(landmarks: [NormalizedLandmark], phase: ExercisePhase) -> [FormFeedback]
func detectPhase(landmarks: [NormalizedLandmark], history: [[NormalizedLandmark]]) -> ExercisePhase
}
Кожна вправа—окремий conforming type. Легко додавати нові без змін ядра системи.
Набір для старту: присед, выпад, віджимання, становая тяга, планка, берпі. Це покриває більшість домашніх тренувань без обладнання.
Процес розробки
Вибір та інтеграція MediaPipe / Vision. Розробка геометричних метрик для кожної вправи (разом з методистом/тренером). Система пріоритизації та cooldown обратної зв'язки. Голосові підказки через AVSpeechSynthesizer. UI: skeleton overlay, метрики в реальному часі, постсесійний аналіз. Тестування на людях різної комплекції та зросту.
Орієнтири за часом
AI-тренер для 3–5 вправ з голосовими підказками—2–4 тижні. Розширена система з автоопределением вправи, постсесійним звітом та прогресом по часу—5–8 тижнів.







