android mlkit 实现仰卧起坐和俯卧撑识别

**用 Android ML Kit 姿势检测 (Pose Detection) 完全可以实现 俯卧撑、仰卧起坐的自动计数与动作标准度检测。

核心原理:追踪 33 个骨骼点 → 计算关键角度 → 判断动作阶段(上 / 下)→ 状态机计数。

关键骨骼点(健身用):

NOSE, LEFT_SHOULDER, RIGHT_SHOULDER

LEFT_ELBOW, RIGHT_ELBOW

LEFT_WRIST, RIGHT_WRIST

LEFT_HIP, RIGHT_HIP

LEFT_KNEE, RIGHT_KNEE

LEFT_ANKLE, RIGHT_ANKLE

**

1. 依赖

java 复制代码
implementation "androidx.camera:camera-core:1.3.0"
implementation "androidx.camera:camera-camera2:1.3.0"
implementation "androidx.camera:camera-lifecycle:1.3.0"
implementation "androidx.camera:camera-view:1.3.0"

implementation 'com.google.mlkit:pose-detection:18.0.0-beta3'

2. 权限 AndroidManifest.xml

java 复制代码
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

3. 布局 activity_main.xml

java 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_marginTop="30dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <TextView
            android:id="@+id/tvPushUp"
            android:text="俯卧撑: 0"
            android:textSize="24sp"
            android:textColor="#ffffff"/>

        <TextView
            android:id="@+id/tvSitUp"
            android:text="仰卧起坐: 0"
            android:textSize="24sp"
            android:textColor="#ffffff"
            android:layout_marginTop="8dp"/>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

4. 角度计算工具类(Java)

工具:计算三点夹角

java 复制代码
import com.google.mlkit.vision.pose.PoseLandmark;
import static java.lang.Math.acos;
import static java.lang.Math.sqrt;

public class AngleUtil {
    public static float calculateAngle(PoseLandmark a, PoseLandmark b, PoseLandmark c) {
        float baX = a.getX() - b.getX();
        float baY = a.getY() - b.getY();
        float bcX = c.getX() - b.getX();
        float bcY = c.getY() - b.getY();

        float dot = baX * bcX + baY * bcY;
        float mag1 = sqrt(baX * baX + baY * baY);
        float mag2 = sqrt(bcX * bcX + bcY * bcY);

        if (mag1 == 0 || mag2 == 0) return 0;

        float cosAngle = dot / (mag1 * mag2);
        cosAngle = Math.max(Math.min(cosAngle, 1.0f), -1.0f);
        return (float) Math.toDegrees(acos(cosAngle));
    }
}

5. 俯卧撑计数器 PushUpCounter.java

俯卧撑计数逻辑(核心)
判定标准
高位(撑起):手肘角 > 160°(手臂接近伸直)
低位(下压):手肘角 < 100°(胸部接近地面)
计数:高 → 低 → 高 算一次

java 复制代码
import com.google.mlkit.vision.pose.Pose;
import com.google.mlkit.vision.pose.PoseLandmark;

public class PushUpCounter {
    private String state = "HIGH";
    public int count = 0;

    public void update(Pose pose) {
        PoseLandmark shoulder = pose.getPoseLandmark(PoseLandmark.LEFT_SHOULDER);
        PoseLandmark elbow = pose.getPoseLandmark(PoseLandmark.LEFT_ELBOW);
        PoseLandmark wrist = pose.getPoseLandmark(PoseLandmark.LEFT_WRIST);

        if (shoulder.getInFrameLikelihood() < 0.5f) return;
        if (elbow.getInFrameLikelihood() < 0.5f) return;
        if (wrist.getInFrameLikelihood() < 0.5f) return;

        float angle = AngleUtil.calculateAngle(shoulder, elbow, wrist);

        if (state.equals("HIGH")) {
            if (angle < 100) {
                state = "LOW";
            }
        } else if (state.equals("LOW")) {
            if (angle > 160) {
                state = "HIGH";
                count++;
            }
        }
    }
}

6. 仰卧起坐计数器 SitUpCounter.java

仰卧起坐计数逻辑(核心)
判定标准
平躺:髋 - 背 - 膝角度 > 150°(身体平直)
坐起:髋 - 背 - 膝角度 < 80°(上身抬起)
计数:躺 → 起 → 躺 算一次

java 复制代码
import com.google.mlkit.vision.pose.Pose;
import com.google.mlkit.vision.pose.PoseLandmark;

public class SitUpCounter {
    private String state = "LAYING";
    public int count = 0;

    public void update(Pose pose) {
        PoseLandmark hip = pose.getPoseLandmark(PoseLandmark.LEFT_HIP);
        PoseLandmark shoulder = pose.getPoseLandmark(PoseLandmark.LEFT_SHOULDER);
        PoseLandmark knee = pose.getPoseLandmark(PoseLandmark.LEFT_KNEE);

        if (hip.getInFrameLikelihood() < 0.5f) return;
        if (shoulder.getInFrameLikelihood() < 0.5f) return;
        if (knee.getInFrameLikelihood() < 0.5f) return;

        float angle = AngleUtil.calculateAngle(knee, hip, shoulder);

        if (state.equals("LAYING")) {
            if (angle < 85) {
                state = "SITTING";
            }
        } else if (state.equals("SITTING")) {
            if (angle > 140) {
                state = "LAYING";
                count++;
            }
        }
    }
}

7. 主页面 MainActivity.java(Java 完整版)

java 复制代码
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.*;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.common.InputImage;
import com.google.mlkit.vision.pose.Pose;
import com.google.mlkit.vision.pose.PoseDetection;
import com.google.mlkit.vision.pose.PoseDetector;
import com.google.mlkit.vision.pose.PoseDetectorOptions;
import android.widget.TextView;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {

    private PreviewView previewView;
    private TextView tvPushUp, tvSitUp;

    private ExecutorService cameraExecutor;
    private PushUpCounter pushUpCounter;
    private SitUpCounter sitUpCounter;
    private PoseDetector poseDetector;

    private static final int REQUEST_CAMERA = 10;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        previewView = findViewById(R.id.previewView);
        tvPushUp = findViewById(R.id.tvPushUp);
        tvSitUp = findViewById(R.id.tvSitUp);

        cameraExecutor = Executors.newSingleThreadExecutor();
        pushUpCounter = new PushUpCounter();
        sitUpCounter = new SitUpCounter();

        PoseDetectorOptions options = new PoseDetectorOptions.Builder()
                .setDetectorMode(PoseDetectorOptions.STREAM_MODE)
                .build();

        poseDetector = PoseDetection.getClient(options);

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED) {
            startCamera();
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
        }
    }

    private void startCamera() {
        ListenableFuture<ProcessCameraProvider> future
                = ProcessCameraProvider.getInstance(this);

        future.addListener(() -> {
            try {
                ProcessCameraProvider provider = future.get();

                Preview preview = new Preview.Builder().build();
                preview.setSurfaceProvider(previewView.getSurfaceProvider());

                ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
                        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                        .build();

                imageAnalysis.setAnalyzer(cameraExecutor, imageProxy -> {
                    if (imageProxy.getImage() == null) {
                        imageProxy.close();
                        return;
                    }

                    InputImage image = InputImage.fromMediaImage(
                            imageProxy.getImage(),
                            imageProxy.getImageInfo().getRotationDegrees()
                    );

                    poseDetector.process(image)
                            .addOnSuccessListener(poses -> {
                               if (poses!=null) {
                                    Pose pose = poses;
                                    pushUpCounter.update(pose);
                                    sitUpCounter.update(pose);

                                    runOnUiThread(() -> {
                                        tvPushUp.setText("俯卧撑: " + pushUpCounter.count);
                                        tvSitUp.setText("仰卧起坐: " + sitUpCounter.count);
                                    });
                                }
                                imageProxy.close();
                            })
                            .addOnFailureListener(e -> imageProxy.close());
                });

                CameraSelector selector = CameraSelector.DEFAULT_FRONT_CAMERA;
                provider.unbindAll();
                provider.bindToLifecycle(this, selector, preview, imageAnalysis);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, ContextCompat.getMainExecutor(this));
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CAMERA) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                startCamera();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        cameraExecutor.shutdown();
    }
}

8 新增:自定义绘制 View(画骨骼)

java 复制代码
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import com.google.mlkit.vision.pose.Pose;
import com.google.mlkit.vision.pose.PoseLandmark;
import java.util.List;

public class PoseOverlayView extends View {
    private Pose pose;
    private final Paint pointPaint;
    private final Paint linePaint;

    public PoseOverlayView(Context context, AttributeSet attrs) {
        super(context, attrs);
        pointPaint = new Paint();
        pointPaint.setColor(Color.GREEN);
        pointPaint.setStyle(Paint.Style.FILL);
        pointPaint.setAntiAlias(true);
        pointPaint.setStrokeWidth(8f);

        linePaint = new Paint();
        linePaint.setColor(Color.YELLOW);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setAntiAlias(true);
        linePaint.setStrokeWidth(4f);
    }

    public void setPose(Pose pose) {
        this.pose = pose;
        invalidate(); // 重绘
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (pose == null) return;

        List<PoseLandmark> landmarks = pose.getAllPoseLandmarks();
        for (PoseLandmark landmark : landmarks) {
            if (landmark.getInFrameLikelihood() > 0.5f) {
                canvas.drawCircle(landmark.getX(), landmark.getY(), 8f, pointPaint);
            }
        }

        drawConnect(canvas, PoseLandmark.NOSE, PoseLandmark.LEFT_SHOULDER);
        drawConnect(canvas, PoseLandmark.NOSE, PoseLandmark.RIGHT_SHOULDER);
        drawConnect(canvas, PoseLandmark.LEFT_SHOULDER, PoseLandmark.LEFT_ELBOW);
        drawConnect(canvas, PoseLandmark.LEFT_ELBOW, PoseLandmark.LEFT_WRIST);
        drawConnect(canvas, PoseLandmark.RIGHT_SHOULDER, PoseLandmark.RIGHT_ELBOW);
        drawConnect(canvas, PoseLandmark.RIGHT_ELBOW, PoseLandmark.RIGHT_WRIST);
        drawConnect(canvas, PoseLandmark.LEFT_SHOULDER, PoseLandmark.LEFT_HIP);
        drawConnect(canvas, PoseLandmark.RIGHT_SHOULDER, PoseLandmark.RIGHT_HIP);
        drawConnect(canvas, PoseLandmark.LEFT_HIP, PoseLandmark.LEFT_KNEE);
        drawConnect(canvas, PoseLandmark.RIGHT_HIP, PoseLandmark.RIGHT_KNEE);
        drawConnect(canvas, PoseLandmark.LEFT_KNEE, PoseLandmark.LEFT_ANKLE);
        drawConnect(canvas, PoseLandmark.RIGHT_KNEE, PoseLandmark.RIGHT_ANKLE);
        drawConnect(canvas, PoseLandmark.LEFT_SHOULDER, PoseLandmark.RIGHT_SHOULDER);
        drawConnect(canvas, PoseLandmark.LEFT_HIP, PoseLandmark.RIGHT_HIP);
    }

    private void drawConnect(Canvas canvas, int from, int to) {
        PoseLandmark a = pose.getPoseLandmark(from);
        PoseLandmark b = pose.getPoseLandmark(to);
        if (a.getInFrameLikelihood() > 0.5f && b.getInFrameLikelihood() > 0.5f) {
            canvas.drawLine(a.getX(), a.getY(), b.getX(), b.getY(), linePaint);
        }
    }
}

修改布局:把绘制层盖在相机上 activity_main.xml 加入:

<com.yourpackage.PoseOverlayView

android:id="@+id/overlayView"

android:layout_width="match_parent"

android:layout_height="match_parent" />

MainActivity 里加入绘制

顶部声明:private PoseOverlayView overlayView;

overlayView = findViewById(R.id.overlayView);

在 poseDetector.process(image)

.addOnSuccessListener方法最后加一句:

复制代码
pushUpCounter.update(pose);
sitUpCounter.update(pose);
runOnUiThread(() -> {
    tvPushUp.setText("俯卧撑: " + pushUpCounter.count);
    tvSitUp.setText("仰卧起坐: " + sitUpCounter.count);
    overlayView.setPose(pose); // 绘制骨骼
});

效果

绿色圆点 = 关节点

黄色连线 = 骨骼

实时跟随人体运动

不影响计数

使用建议

光线充足

手机 侧放、全身入镜

距离 1.5~2 米

相关推荐
ameyume1 小时前
基于原生Android 16设置音量调用流程
android·audio
ii_best1 小时前
lua语言开发脚本基础、mql命令库开发、安卓/ios基础开发教程,按键精灵新手工具
android·ios·自动化·编辑器
BoomHe1 天前
Android AOSP13 原生 Launcher3 壁纸获取方式
android
Digitally1 天前
如何将联系人从 Android 转移到 Android
android
李小枫1 天前
webflux接收application/x-www-form-urlencoded参数
android·java·开发语言
爱丽_1 天前
MySQL `EXPLAIN`:看懂执行计划、判断索引是否生效与排错套路
android·数据库·mysql
NPE~1 天前
[App逆向]环境搭建下篇 — — 逆向源码+hook实战
android·javascript·python·教程·逆向·hook·逆向分析
yewq-cn1 天前
AOSP 下载
android
cch89181 天前
Laravel vs ThinkPHP:PHP框架终极对决
android·php·laravel
米码收割机1 天前
【Android】基于安卓app的汽车租赁管理系统(源码+部署方式+论文)[独一无二]
android·汽车