本文目录导读:

- 场景一:基于摄像头视频流的桌面手势识别(Java + OpenCV + 媒体库)
- 场景二:Android触摸屏手势识别(Java + MotionEvent + GestureDetector)
- 总结与选型建议
在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 Android 或 TensorFlow Lite | 高 | 高 |
推荐入门路径:先学习Android的GestureDetector(最简单),然后基于MediaPipe体验桌面端AI手势识别(最有成就感),最后再尝试OpenCV传统方法理解原理。