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 米

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui