4. 基于OpenCV的实时手势识别

任务目标

在嵌入式设备(如树莓派4B)上,使用OpenCV调用轻量级深度学习模型(MobileNetV3-SSD),实现摄像头实时画面中的手势识别(0-9数字手势),并能在复杂背景(非纯色、光照变化、轻微遮挡)下保持可用精度。

环境与依赖

  • 硬件:树莓派4B(2GB+),USB摄像头或CSI摄像头
  • 系统:Raspberry Pi OS (64-bit)
  • Python 3.9+,OpenCV 4.5+(含dnn模块),NumPy
  • 模型文件:hand_model.pbtxt(网络结构)与 hand_model.pb(权重),基于SSD MobileNetV3,输入尺寸320×320,输出为7个坐标点(食指到小指共4个指尖+4个指根+手腕,实际取指尖与指根共8点用于计数)

模型文件需预先下载并放置于models/目录。若使用其他手势检测模型,需对应调整输出层解析逻辑。

核心实现

以下代码实现完整闭环:读取摄像头帧 → 模型推理 → 解析关键点 → 计算伸展手指数量 → 显示识别结果。针对复杂背景,采用以下策略:

  • 使用深度学习模型而非肤色分割,天然抵抗复杂背景干扰
  • 对关键点做平滑滤波(指数移动平均),减少抖动
  • 手指计数基于几何关系(指尖与指根距离/角度),不依赖背景分割
import cv2
import numpy as np

# 模型路径
PROTO = "models/hand_model.pbtxt"
WEIGHTS = "models/hand_model.pb"
CONF_THRESH = 0.5

# 加载网络
net = cv2.dnn.readNetFromTensorflow(WEIGHTS, PROTO)
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)

# 关键点索引(根据模型输出定义)
# 输出为 [x1,y1,...,x21,y21] 共21点,此处取:4-指尖(8,12,16,20),指根(5,9,13,17)
TIP_IDS = [8, 12, 16, 20]
BASE_IDS = [5, 9, 13, 17]
WRIST_ID = 0

# 平滑滤波参数
alpha = 0.4
smoothed_pts = None

def count_fingers(pts):
    """根据指尖与指根距离判断手指是否伸展"""
    wrist = pts[WRIST_ID]
    count = 0
    for tip_id, base_id in zip(TIP_IDS, BASE_IDS):
        tip = pts[tip_id]
        base = pts[base_id]
        # 指尖到指根距离
        d_tb = np.linalg.norm(tip - base)
        # 指根到手腕距离
        d_bw = np.linalg.norm(base - wrist)
        # 指尖到手腕距离
        d_tw = np.linalg.norm(tip - wrist)
        # 若指尖明显远离手腕且指尖-指根距离大于指根-手腕距离的0.6倍,判定为伸展
        if d_tw > d_bw * 1.2 and d_tb > d_bw * 0.6:
            count += 1
    return count

def process_frame(frame):
    global smoothed_pts
    h, w = frame.shape[:2]
    # 模型输入
    blob = cv2.dnn.blobFromImage(frame, 1.0/255.0, (320, 320), (0,0,0), swapRB=True, crop=False)
    net.setInput(blob)
    output = net.forward()  # shape: [1, 1, N, 7] 或 [1, N, 7]
    
    # 解析输出
    if output.ndim == 4:
        detections = output[0, 0]
    else:
        detections = output[0]
    
    best_conf = 0
    best_pts = None
    for i in range(detections.shape[0]):
        conf = detections[i, 2]
        if conf > best_conf:
            best_conf = conf
            # 21点坐标从第3列开始,每点2个值
            raw = detections[i, 3:]
            pts = []
            for j in range(0, len(raw), 2):
                x = raw[j] * w
                y = raw[j+1] * h
                pts.append([x, y])
            best_pts = np.array(pts, dtype=np.float32)
    
    if best_pts is None or best_conf < CONF_THRESH:
        return frame, None
    
    # 平滑处理
    if smoothed_pts is None:
        smoothed_pts = best_pts
    else:
        smoothed_pts = alpha * best_pts + (1 - alpha) * smoothed_pts
    
    # 绘制关键点
    for pt in smoothed_pts:
        cv2.circle(frame, (int(pt[0]), int(pt[1])), 4, (0, 255, 0), -1)
    
    # 手指计数
    fingers = count_fingers(smoothed_pts)
    cv2.putText(frame, f"Fingers: {fingers}", (10, 40), 
                cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 2)
    return frame, fingers

# 主循环
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    result, fingers = process_frame(frame)
    cv2.imshow("Hand Gesture Recognition", result)
    
    key = cv2.waitKey(1) & 0xFF
    if key == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

运行与验证

  1. 将上述代码保存为gesture_recognition.py,模型文件置于models/目录
  2. 运行:python3 gesture_recognition.py
  3. 摄像头自动开启,窗口实时显示画面

验证方法:

  • 在摄像头前分别伸出1~5根手指,观察窗口左上角数字是否与预期一致
  • 在复杂背景(如室内杂物、窗外光线变化)下测试,若检测到的手部关键点稳定跟随且计数正确率≥80%,即判定通过
  • 按q键退出程序

常见问题

  • 检测不到手:确认模型文件路径正确,光照充足,手距摄像头30~80cm
  • 计数不稳定:平滑系数alpha可调小(如0.2)增加稳定性,但响应会变慢
  • 帧率低:降低输入分辨率至320×240,或改用CSI摄像头