Java案例如何实现手势识别?

wen python案例 1

本文目录导读:

Java案例如何实现手势识别?

  1. 场景一:基于摄像头视频流的桌面手势识别(Java + OpenCV + 媒体库)
  2. 场景二:Android触摸屏手势识别(Java + MotionEvent + GestureDetector)
  3. 总结与选型建议

在Java中实现手势识别,通常有两种主流场景:桌面端(摄像头视频流)移动端(Android触摸屏)

以下是针对这两种场景的完整实现案例及技术方案。


基于摄像头视频流的桌面手势识别(Java + OpenCV + 媒体库)

这是最通用的方案,适用于Windows/Mac/Linux,通过摄像头捕捉手部轮廓或关键点,并基于几何关系判断手势(如:握拳、五指张开、比耶等)。

技术栈

  • JDK:8+
  • OpenCV:4.5+(用于图像处理)
  • 视觉库:MediaPipe(Google出品,推荐,自带手部21个关键点检测)或 传统OpenCV轮廓法。

方案A:使用MediaPipe(推荐,更简单、准确)

MediaPipe提供了Java Native接口,可以实时检测21个手部关键点坐标,然后通过这些坐标计算手势。

依赖与配置 下载MediaPipe的Java Jar包和对应的Native库(.dll/.so/.dylib),或通过Maven引入(目前MediaPipe官方Java库较复杂,建议直接下载预编译包)。

核心代码逻辑

import com.google.mediapipe.formats.proto.LandmarkProto.NormalizedLandmark;
import com.google.mediapipe.solution.hands.Hands;
// ... 其他导入
public class GestureRecognizer {
    // 1. 加载手部检测模型
    private Hands hands;
    public void init() {
        HandsOptions options = HandsOptions.builder()
                .setStaticImageMode(false)
                .setMaxNumHands(2)
                .setModelComplexity(1)
                .setMinDetectionConfidence(0.5f)
                .setMinTrackingConfidence(0.5f)
                .build();
        hands = new Hands(options);
    }
    // 2. 从摄像头帧中检测
    public void processFrame(Mat frame) {
        // 将Mat转为MediaPipe需要的格式
        Frame mpFrame = convertMatToMediaPipeFrame(frame);
        // 运行检测
        HandsResult result = hands.process(mpFrame);
        if (result == null || result.getHandLandmarksList().isEmpty()) {
            System.out.println("未检测到手");
            return;
        }
        // 遍历每个检测到的手
        for (List<NormalizedLandmark> landmarks : result.getHandLandmarksList()) {
            Gesture gesture = classifyGesture(landmarks);
            System.out.println("识别手势: " + gesture);
        }
    }
    // 3. 手势分类逻辑(基于关键点几何关系)
    private Gesture classifyGesture(List<NormalizedLandmark> landmarks) {
        // 关键点索引(MediaPipe标准)
        int THUMB_TIP = 4;
        int INDEX_FINGER_TIP = 8;
        int MIDDLE_FINGER_TIP = 12;
        int RING_FINGER_TIP = 16;
        int PINKY_TIP = 20;
        int WRIST = 0;
        // 计算指端到手腕的距离作为归一化基准
        float baseDist = distance(landmarks.get(WRIST), landmarks.get(INDEX_FINGER_TIP));
        // 获取各手指指尖到对应根部(或手腕)的相对距离
        float thumbDist = distance(landmarks.get(THUMB_TIP), landmarks.get(1)); // 拇指根部
        float indexDist = distance(landmarks.get(INDEX_FINGER_TIP), landmarks.get(5));
        float middleDist = distance(landmarks.get(MIDDLE_FINGER_TIP), landmarks.get(9));
        float ringDist = distance(landmarks.get(RING_FINGER_TIP), landmarks.get(13));
        float pinkyDist = distance(landmarks.get(PINKY_TIP), landmarks.get(17));
        // 简单阈值判断:如果手指伸展距离超过基准的0.5倍,认为该手指竖起
        boolean thumbUp = thumbDist > baseDist * 0.5;
        boolean indexUp = indexDist > baseDist * 0.5;
        boolean middleUp = middleDist > baseDist * 0.5;
        boolean ringUp = ringDist > baseDist * 0.5;
        boolean pinkyUp = pinkyDist > baseDist * 0.5;
        // 组合判断
        if (!thumbUp && !indexUp && !middleUp && !ringUp && !pinkyUp) return Gesture.FIST; // 握拳
        if (thumbUp && indexUp && middleUp && ringUp && pinkyUp) return Gesture.FIVE;    // 五指张开
        if (thumbUp && indexUp && !middleUp && !ringUp && !pinkyUp) return Gesture.GUN;   // 手枪(比数字8)
        if (thumbUp && indexUp && middleUp && !ringUp && !pinkyUp) return Gesture.OK;     // 比心
        if (!thumbUp && indexUp && middleUp && !ringUp && !pinkyUp) return Gesture.PEACE; // 比耶
        if (thumbUp && !indexUp && !middleUp && !ringUp && pinkyUp) return Gesture.METAL; // 摇滚手势
        return Gesture.UNKNOWN;
    }
    private float distance(NormalizedLandmark a, NormalizedLandmark b) {
        float dx = a.getX() - b.getX();
        float dy = a.getY() - b.getY();
        return (float) Math.sqrt(dx*dx + dy*dy);
    }
    enum Gesture {
        FIST, FIVE, GUN, OK, PEACE, METAL, UNKNOWN
    }
}

方案B:传统OpenCV + 轮廓法(无外部AI库,适合学习原理)

需要肤色检测、二值化、凸包计算、凸缺陷分析。

public class OpenCVHandGesture {
    public Gesture recognizeGesture(Mat frame) {
        // 1. 色彩空间转换:RGB -> YCrCb 或 HSV(更容易做肤色检测)
        Mat ycrcb = new Mat();
        Imgproc.cvtColor(frame, ycrcb, Imgproc.COLOR_BGR2YCrCb);
        // 2. 肤色阈值(Cr 约 133~173,Cb 约 77~127)
        Mat skinMask = new Mat();
        Core.inRange(ycrcb, new Scalar(0, 133, 77), new Scalar(255, 173, 127), skinMask);
        // 3. 形态学操作去噪(腐蚀+膨胀)
        Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(5, 5));
        Imgproc.morphologyEx(skinMask, skinMask, Imgproc.MORPH_CLOSE, kernel);
        // 4. 找轮廓
        List<MatOfPoint> contours = new ArrayList<>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(skinMask, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
        if (contours.isEmpty()) return Gesture.NONE;
        // 5. 取最大轮廓(假设是手)
        MatOfPoint handContour = Collections.max(contours, Comparator.comparingDouble(Imgproc::contourArea));
        // 6. 计算凸包和凸缺陷
        MatOfInt hull = new MatOfInt();
        Imgproc.convexHull(handContour, hull);
        MatOfInt4 convexityDefects = new MatOfInt4();
        Imgproc.convexityDefects(handContour, hull, convexityDefects);
        // 7. 基于凸缺陷数量判断手势
        // 凸缺陷数:0-握拳,1-食指伸出,2-比耶,3-三个手指...
        int defectCount = convexityDefects.rows();
        if (defectCount == 0) return Gesture.FIST;
        else if (defectCount == 1) return Gesture.ONE;
        else if (defectCount == 2) return Gesture.TWO;
        else return Gesture.UNKNOWN;
    }
}

Android触摸屏手势识别(Java + MotionEvent + GestureDetector)

适用于手机App,识别手指滑动、缩放、长按、旋转等触控手势。

技术栈

  • Android SDK
  • GestureDetector(系统内置)
  • ScaleGestureDetector(缩放)
  • 自定义触摸事件处理

核心实现

单一手势(滑屏、长按):使用GestureDetector

public class GestureActivity extends AppCompatActivity implements GestureDetector.OnGestureListener {
    private GestureDetector gestureDetector;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        gestureDetector = new GestureDetector(this, this);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 将触摸事件交给GestureDetector处理
        return gestureDetector.onTouchEvent(event);
    }
    @Override
    public boolean onDown(MotionEvent e) {
        return false; // 必须返回true才会继续接收后续事件
    }
    @Override
    public void onShowPress(MotionEvent e) { }
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        Log.d("Gesture", "单击");
        return true;
    }
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        Log.d("Gesture", "滑动: dx=" + distanceX + ", dy=" + distanceY);
        return true;
    }
    @Override
    public void onLongPress(MotionEvent e) {
        Log.d("Gesture", "长按");
    }
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        Log.d("Gesture", "快速甩动: vx=" + velocityX + ", vy=" + velocityY);
        return true;
    }
}

双指缩放(Pinch/Zoom):使用ScaleGestureDetector

public class PinchZoomView extends View {
    private ScaleGestureDetector scaleDetector;
    private float scaleFactor = 1.0f;
    public PinchZoomView(Context context) {
        super(context);
        scaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                scaleFactor *= detector.getScaleFactor(); // 获取缩放比例
                scaleFactor = Math.max(0.1f, Math.min(scaleFactor, 10.0f));
                invalidate(); // 重绘
                return true;
            }
        });
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        scaleDetector.onTouchEvent(event); // 交给缩放检测器
        return true; // 消费事件
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.scale(scaleFactor, scaleFactor, getWidth()/2f, getHeight()/2f);
        // 在这里绘制内容,会被缩放
    }
}

复杂多点手势(旋转、按压、自定义):直接分析MotionEvent

@Override
public boolean onTouchEvent(MotionEvent event) {
    int pointerCount = event.getPointerCount();
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_POINTER_DOWN:
            // 新的手指按下
            if (pointerCount == 2) {
                // 记录双指起始距离和角度
                initialDistance = spacing(event);
                initialAngle = angle(event);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            if (pointerCount == 2) {
                float currentDistance = spacing(event);
                float currentAngle = angle(event);
                // 判断缩放
                if (Math.abs(currentDistance - initialDistance) > THRESHOLD) {
                    // 执行缩放逻辑
                }
                // 判断旋转
                if (Math.abs(currentAngle - initialAngle) > ROTATION_THRESHOLD) {
                    // 执行旋转逻辑
                }
            }
            break;
    }
    return true;
}
// 计算两点间距
private float spacing(MotionEvent event) {
    float x = event.getX(0) - event.getX(1);
    float y = event.getY(0) - event.getY(1);
    return (float) Math.sqrt(x*x + y*y);
}
// 计算两点连线角度
private float angle(MotionEvent event) {
    double dx = event.getX(1) - event.getX(0);
    double dy = event.getY(1) - event.getY(0);
    return (float) Math.toDegrees(Math.atan2(dy, dx));
}

总结与选型建议

场景 推荐方案 难度 准确度
桌面端摄像头手势(复杂手势) MediaPipe (Java Native)
桌面端摄像头手势(简单、学习) OpenCV 轮廓法 低~中 低(受光照影响大)
Android触控手势(滑动、缩放) GestureDetector + ScaleGestureDetector 极高(系统自带)
Android触控手势(自定义双指旋转等) 直接解析 MotionEvent
移动端摄像头手势识别 MediaPipe for AndroidTensorFlow Lite

推荐入门路径:先学习Android的GestureDetector(最简单),然后基于MediaPipe体验桌面端AI手势识别(最有成就感),最后再尝试OpenCV传统方法理解原理。

抱歉,评论功能暂时关闭!