车载静态手势控制系统 —— “车懂手语”

 

一、实际应用场景描述

 

场景:嘈杂环境下的驾驶员快捷操作

 

想象这样一个画面:你在雨天驾车,雨刷高速摆动,车窗紧闭,车内音响播放着导航语音。此时你想临时停车查看路况或接听电话,但你不想喊出“停车”或“启动”,因为:

 

- 环境噪音大,语音识别率低

- 语音指令需唤醒词,反应慢

- 公共场合喊指令不优雅

- 驾驶中说话分心,影响安全

 

创新方案:静态手势识别控制

 

受人类日常交流启发,我们设计一套基于视觉的静态手势识别系统,让车辆“看懂”驾驶员的手势,实现启停控制:

 

- 握拳 → 停车

- 手掌展开 → 启动/继续

- 比“OK” → 确认

- 五指张开 → 加速/特殊功能

 

颠覆点:不依赖语音,不依赖按钮,不依赖触摸屏,纯视觉+ML,毫秒级响应,无接触控制。

 

二、痛点分析

 

痛点 传统语音/按钮方案 本方案(手势)

环境噪音 语音识别率下降 不受噪音影响

唤醒延迟 需唤醒词+处理时间 实时检测

分心操作 需看按钮/想指令 余光即可

卫生问题 触摸按钮交叉感染 无接触

可扩展性 指令需预定义 手势可组合扩展

 

三、核心逻辑讲解

 

1. 系统架构

 

┌─────────────┐

│ 摄像头输入 │

└──────┬──────┘

       ▼

┌─────────────┐

│ 图像预处理 │ (Resize, Grayscale, Normalization)

└──────┬──────┘

       ▼

┌─────────────┐

│ 手部检测 │ (MediaPipe Hands / YOLOv5-Hands)

└──────┬──────┘

       ▼

┌─────────────┐

│ 关键点提取 │ (21个Landmark)

└──────┬──────┘

       ▼

┌─────────────┐

│ 特征工程 │ (角度、距离、方向)

└──────┬──────┘

       ▼

┌─────────────┐

│ 分类模型 │ (CNN / SVM / Random Forest)

└──────┬──────┘

       ▼

┌─────────────┐

│ 指令映射 │ (手势→启停/确认/加速)

└──────┬──────┘

       ▼

┌─────────────┐

│ 车辆控制 │ (CAN Bus / 模拟接口)

└─────────────┘

 

2. 核心算法流程

 

1. 数据采集:摄像头捕获驾驶舱内驾驶员右手区域

2. 手部检测:使用轻量级模型定位手部区域

3. 关键点提取:获取21个手部关节点坐标

4. 特征计算:

   - 手指弯曲角度

   - 指尖间距离

   - 手掌法向量方向

5. 分类识别:ML模型判断手势类别

6. 指令执行:映射为车辆控制命令(如 

"STOP", 

"START")

7. 反馈机制:LED/屏幕提示识别结果

 

四、完整代码实现

 

项目结构

 

gesture_control_vehicle/

├── README.md

├── requirements.txt

├── config.py

├── main.py

├── hand_detector.py # 手部检测模块

├── gesture_classifier.py # 手势分类模型

├── command_mapper.py # 手势-指令映射

├── vehicle_controller.py # 车辆控制接口

├── data_collector.py # 数据采集工具

├── trainer.py # 模型训练脚本

├── utils.py # 工具函数

└── demo/

    └── test_video.mp4

 

1. requirements.txt

 

opencv-python>=4.5.0

mediapipe>=0.8.0

numpy>=1.19.0

scikit-learn>=0.24.0

joblib>=1.0.0

torch>=1.9.0

pillow>=8.0.0

 

2. config.py

 

"""

配置参数模块

包含手势类别、模型路径、控制参数等

"""

 

from dataclasses import dataclass, field

from typing import List, Tuple

 

@dataclass

class GestureConfig:

    """手势配置"""

    gesture_classes: List[str] = field(default_factory=lambda: [

        "FIST", # 握拳 - 停车

        "OPEN_PALM", # 手掌展开 - 启动

        "OK_SIGN", # OK手势 - 确认

        "FIVE_FINGERS", # 五指张开 - 加速/特殊

        "NONE" # 无手势/未识别

    ])

    num_landmarks: int = 21 # MediaPipe手部关键点数量

    min_detection_confidence: float = 0.7

    min_tracking_confidence: float = 0.5

 

@dataclass

class ModelConfig:

    """模型配置"""

    classifier_type: str = "SVM" # SVM / RF / CNN

    model_path: str = "models/gesture_classifier.joblib"

    feature_method: str = "angles_distances" # angles_distances / raw_coords

 

@dataclass

class CameraConfig:

    """摄像头配置"""

    camera_id: int = 0

    frame_width: int = 640

    frame_height: int = 480

    fps: int = 30

 

@dataclass

class VehicleControlConfig:

    """车辆控制配置"""

    stop_command: str = "STOP"

    start_command: str = "START"

    confirm_command: str = "CONFIRM"

    accelerate_command: str = "ACCELERATE"

    command_delay_ms: int = 500 # 防止误触发的延迟

 

# 全局配置实例

GESTURE_CONFIG = GestureConfig()

MODEL_CONFIG = ModelConfig()

CAMERA_CONFIG = CameraConfig()

CONTROL_CONFIG = VehicleControlConfig()

 

3. hand_detector.py

 

"""

手部检测模块

基于MediaPipe实现实时手部关键点检测

"""

 

import cv2

import mediapipe as mp

import numpy as np

from typing import Optional, Tuple, List

from config import GESTURE_CONFIG, CAMERA_CONFIG

 

class HandDetector:

    """

    手部检测器类

    使用MediaPipe Hands进行手部检测和关键点提取

    """

    

    def __init__(self):

        """初始化MediaPipe Hands"""

        self.mp_hands = mp.solutions.hands

        self.mp_drawing = mp.solutions.drawing_utils

        self.mp_drawing_styles = mp.solutions.drawing_styles

        

        self.hands = self.mp_hands.Hands(

            static_image_mode=False,

            max_num_hands=1, # 只检测一只手

            min_detection_confidence=GESTURE_CONFIG.min_detection_confidence,

            min_tracking_confidence=GESTURE_CONFIG.min_tracking_confidence

        )

        

    def detect(self, frame: np.ndarray) -> Tuple[Optional[object], np.ndarray]:

        """

        检测手部并提取关键点

        

        Args:

            frame: 输入图像 (BGR格式)

            

        Returns:

            (检测结果对象, 关键点坐标数组 shape=(21, 3))

        """

        # 转换BGR到RGB

        rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        

        # 检测手部

        results = self.hands.process(rgb_frame)

        

        landmarks = None

        

        if results.multi_hand_landmarks:

            # 取第一只手

            hand_landmarks = results.multi_hand_landmarks[0]

            

            # 提取关键点坐标

            landmarks = np.array([

                [lm.x, lm.y, lm.z] 

                for lm in hand_landmarks.landmark

            ])

            

            # 绘制关键点和连接线

            self.mp_drawing.draw_landmarks(

                frame,

                hand_landmarks,

                self.mp_hands.HAND_CONNECTIONS,

                self.mp_drawing_styles.get_default_hand_landmarks_style(),

                self.mp_drawing_styles.get_default_hand_connections_style()

            )

            

        return results, landmarks

    

    def get_finger_states(self, landmarks: np.ndarray) -> List[bool]:

        """

        根据关键点判断各手指是否伸直

        

        Args:

            landmarks: 21个关键点坐标

            

        Returns:

            各手指状态列表 [拇指, 食指, 中指, 无名指, 小指]

        """

        finger_tips = [4, 8, 12, 16, 20] # 指尖关键点索引

        finger_pips = [3, 6, 10, 14, 18] # 近端指节关键点索引

        

        finger_states = []

        

        # 拇指特殊处理(横向移动为主)

        thumb_tip = landmarks[4]

        thumb_ip = landmarks[3]

        thumb_mcp = landmarks[2]

        # 拇指伸直判断:指尖与IP关节的x差值方向与MCP一致

        thumb_extended = (thumb_tip[0] - thumb_ip[0]) * (thumb_ip[0] - thumb_mcp[0]) > 0

        finger_states.append(thumb_extended)

        

        # 其他四指

        for i in range(1, 5):

            tip_idx = finger_tips[i]

            pip_idx = finger_pips[i]

            mcp_idx = pip_idx - 1

            

            # 计算指尖到PIP关节的距离 vs MCP到PIP关节的距离

            tip_to_pip = np.linalg.norm(landmarks[tip_idx] - landmarks[pip_idx])

            mcp_to_pip = np.linalg.norm(landmarks[mcp_idx] - landmarks[pip_idx])

            

            # 如果指尖离PIP较远,认为手指伸直

            finger_extended = tip_to_pip > 1.2 * mcp_to_pip

            finger_states.append(finger_extended)

            

        return finger_states

    

    def release(self):

        """释放资源"""

        self.hands.close()

 

4. gesture_classifier.py

 

"""

手势分类模块

使用机器学习模型识别静态手势

"""

 

import numpy as np

import joblib

import os

from typing import Tuple, List, Optional

from sklearn.svm import SVC

from sklearn.ensemble import RandomForestClassifier

from sklearn.preprocessing import StandardScaler

from config import GESTURE_CONFIG, MODEL_CONFIG

import warnings

warnings.filterwarnings('ignore')

 

class GestureClassifier:

    """

    手势分类器类

    支持SVM、随机森林等分类器

    """

    

    def __init__(self, model_path: str = None):

        """

        初始化分类器

        

        Args:

            model_path: 预训练模型路径

        """

        self.model_path = model_path or MODEL_CONFIG.model_path

        self.classifier_type = MODEL_CONFIG.classifier_type

        self.feature_method = MODEL_CONFIG.feature_method

        

        self.scaler = StandardScaler()

        self.model = self._create_classifier()

        self.is_trained = False

        

    def _create_classifier(self):

        """创建分类器实例"""

        if self.classifier_type == "SVM":

            return SVC(kernel='rbf', C=10, gamma='scale', probability=True)

        elif self.classifier_type == "RF":

            return RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42)

        else:

            raise ValueError(f"Unsupported classifier: {self.classifier_type}")

    

    def extract_features(self, landmarks: np.ndarray) -> np.ndarray:

        """

        从关键点提取特征

        

        Args:

            landmarks: 21个关键点坐标 (21, 3)

            

        Returns:

            特征向量

        """

        if self.feature_method == "raw_coords":

            # 直接使用归一化坐标

            coords = landmarks.flatten()

            # 归一化到原点

            centroid = np.mean(coords.reshape(-1, 3), axis=0)

            coords = (coords.reshape(-1, 3) - centroid).flatten()

            return coords

            

        elif self.feature_method == "angles_distances":

            # 计算手指角度和指尖距离

            features = []

            

            # 1. 各手指弯曲角度

            finger_angles = self._compute_finger_angles(landmarks)

            features.extend(finger_angles)

            

            # 2. 指尖间距离

            fingertip_indices = [4, 8, 12, 16, 20]

            for i in range(len(fingertip_indices)):

                for j in range(i+1, len(fingertip_indices)):

                    dist = np.linalg.norm(

                        landmarks[fingertip_indices[i]] - landmarks[fingertip_indices[j]]

                    )

                    features.append(dist)

            

            # 3. 手掌尺寸(手腕到中指MCP的距离)

            palm_size = np.linalg.norm(landmarks[0] - landmarks[9])

            features.append(palm_size)

            

            # 4. 手掌方向(法向量)

            wrist = landmarks[0]

            index_mcp = landmarks[5]

            pinky_mcp = landmarks[17]

            v1 = index_mcp - wrist

            v2 = pinky_mcp - wrist

            normal = np.cross(v1, v2)

            normal_norm = np.linalg.norm(normal)

            if normal_norm > 0:

                normal = normal / normal_norm

            features.extend(normal[:2]) # 取x, y分量

            

            return np.array(features)

        

        else:

            raise ValueError(f"Unknown feature method: {self.feature_method}")

    

    def _compute_finger_angles(self, landmarks: np.ndarray) -> List[float]:

        """

        计算各手指的弯曲角度

        

        Args:

            landmarks: 21个关键点

            

        Returns:

            各手指角度列表

        """

        angles = []

        

        # 定义各手指的三个关键点(MCP, PIP, DIP/TIP)

        finger_joints = [

            (1, 2, 3), # 拇指

            (5, 6, 7), # 食指

            (9, 10, 11), # 中指

            (13, 14, 15),# 无名指

            (17, 18, 19) # 小指

        ]

        

        for mcp, pip, dip in finger_joints:

            # 计算向量

            v1 = landmarks[pip] - landmarks[mcp]

            v2 = landmarks[dip] - landmarks[pip]

            

            # 计算夹角

            cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6)

            angle = np.arccos(np.clip(cos_angle, -1.0, 1.0))

            angles.append(np.degrees(angle))

            

        return angles

    

    def train(self, X: np.ndarray, y: np.ndarray) -> None:

        """

        训练分类器

        

        Args:

            X: 特征矩阵

            y: 标签

        """

        # 特征标准化

        X_scaled = self.scaler.fit_transform(X)

        

        # 训练模型

        self.model.fit(X_scaled, y)

        self.is_trained = True

        

        # 保存模型

        self.save_model(self.model_path)

        

    def predict(self, landmarks: np.ndarray) -> Tuple[str, float]:

        """

        预测手势

        

        Args:

            landmarks: 21个关键点坐标

            

        Returns:

            (手势类别, 置信度)

        """

        if not self.is_trained:

            # 尝试加载预训练模型

            if os.path.exists(self.model_path):

                self.load_model(self.model_path)

            else:

                # 使用简单规则作为后备

                return self._rule_based_classify(landmarks)

        

        # 提取特征

        features = self.extract_features(landmarks)

        features = features.reshape(1, -1)

        features_scaled = self.scaler.transform(features)

        

        # 预测

        if hasattr(self.model, "predict_proba"):

            probs = self.model.predict_proba(features_scaled)[0]

            pred_idx = np.argmax(probs)

            confidence = probs[pred_idx]

        else:

            pred_idx = self.model.predict(features_scaled)[0]

            confidence = 0.8 # 默认置信度

        

        gesture = GESTURE_CONFIG.gesture_classes[pred_idx]

        return gesture, float(confidence)

    

    def _rule_based_classify(self, landmarks: np.ndarray) -> Tuple[str, float]:

        """

        基于规则的简单分类(后备方案)

        

        Args:

            landmarks: 21个关键点

            

        Returns:

            (手势类别, 置信度)

        """

        finger_states = self._get_finger_states_from_landmarks(landmarks)

        

        # 握拳:所有手指弯曲

        if not any(finger_states):

            return "FIST", 0.7

        

        # 手掌展开:所有手指伸直

        if all(finger_states):

            return "OPEN_PALM", 0.7

        

        # OK手势:拇指食指成圈,其他伸直

        if (finger_states[0] and finger_states[1] and 

            not finger_states[2] and not finger_states[3] and not finger_states[4]):

            # 检查拇指和食指是否接近

            dist = np.linalg.norm(landmarks[4] - landmarks[8])

            if dist < 0.05:

                return "OK_SIGN", 0.7

        

        # 五指张开:同手掌展开

        if all(finger_states):

            return "FIVE_FINGERS", 0.7

        

        return "NONE", 0.5

    

    def _get_finger_states_from_landmarks(self, landmarks: np.ndarray) -> List[bool]:

        """从关键点直接计算手指状态"""

        finger_tips = [4, 8, 12, 16, 20]

        finger_pips = [3, 6, 10, 14, 18]

        

        finger_states = []

        

        # 拇指

        thumb_tip = landmarks[4]

        thumb_ip = landmarks[3]

        thumb_mcp = landmarks[2]

        thumb_extended = (thumb_tip[0] - thumb_ip[0]) * (thumb_ip[0] - thumb_mcp[0]) > 0

        finger_states.append(thumb_extended)

        

        # 其他四指

        for i in range(1, 5):

            tip_idx = finger_tips[i]

            pip_idx = finger_pips[i]

            mcp_idx = pip_idx - 1

            

            tip_to_pip = np.linalg.norm(landmarks[tip_idx] - landmarks[pip_idx])

            mcp_to_pip = np.linalg.norm(landmarks[mcp_idx] - landmarks[pip_idx])

            finger_extended = tip_to_pip > 1.2 * mcp_to_pip

            finger_states.append(finger_extended)

            

        return finger_states

    

    def save_model(self, path: str) -> None:

        """保存模型"""

        os.makedirs(os.path.dirname(path), exist_ok=True)

        joblib.dump({

            'model': self.model,

            'scaler': self.scaler,

            'classifier_type': self.classifier_type,

            'feature_method': self.feature_method

        }, path)

        

    def load_model(self, path: str) -> None:

        """加载模型"""

        if os.path.exists(path):

            data = joblib.load(path)

            self.model = data['model']

            self.scaler = data['scaler']

            self.classifier_type = data.get('classifier_type', MODEL_CONFIG.classifier_type)

            self.feature_method = data.get('feature_method', MODEL_CONFIG.feature_method)

            self.is_trained = True

            print(f"Model loaded from {path}")

 

5. command_mapper.py

 

"""

指令映射模块

将识别出的手势映射为车辆控制命令

"""

 

from dataclasses import dataclass, field

from typing import Dict, Optional, Callable

from enum import Enum, auto

from config import GESTURE_CONFIG, CONTROL_CONFIG

import time

import logging

 

logging.basicConfig(level=logging.INFO)

logger = logging.getLogger(__name__)

 

class CommandType(Enum):

    """车辆控制命令类型"""

    STOP = auto()

    START = auto()

    CONFIRM = auto()

    ACCELERATE = auto()

    NONE = auto()

 

@dataclass

class Command:

    """控制命令类"""

    cmd_type: CommandType

    command_str: str

    description: str

    priority: int = 0

    requires_confirmation: bool = False

 

class CommandMapper:

    """

    指令映射器

    管理手势到车辆控制命令的映射关系

    """

    

    def __init__(self):

        self.gesture_to_command: Dict[str, Command] = {}

        self.last_command_time: float = 0.0

        self.command_cooldown_ms: int = CONTROL_CONFIG.command_delay_ms

        self.pending_confirmation: Optional[Command] = None

        self.confirmation_timeout_ms: int = 3000

        

        self._setup_default_mapping()

        

    def _setup_default_mapping(self) -> None:

        """设置默认手势-命令映射"""

        self.gesture_to_command = {

            "FIST": Command(

                cmd_type=CommandType.STOP,

                command_str=CONTROL_CONFIG.stop_command,

                description="停车",

                priority=10,

                requires_confirmation=False

            ),

            "OPEN_PALM": Command(

                cmd_type=CommandType.START,

                command_str=CONTROL_CONFIG.start_command,

                description="启动/继续行驶",

                priority=10,

                requires_confirmation=False

            ),

            "OK_SIGN": Command(

                cmd_type=CommandType.CONFIRM,

                command_str=CONTROL_CONFIG.confirm_command,

                description="确认操作",

                priority=5,

                requires_confirmation=False

            ),

            "FIVE_FINGERS": Command(

                cmd_type=CommandType.ACCELERATE,

                command_str=CONTROL_CONFIG.accelerate_command,

                description="加速/特殊功能",

                priority=8,

                requires_confirmation=True

            ),

            "NONE": Command(

                cmd_type=CommandType.NONE,

                command_str="NONE",

                description="无操作",

                priority=0,

                requires_confirmation=False

            )

        }

        

    def map_gesture(self, gesture: str) -> Optional[Command]:

        """

        将手势映射为命令

        

        Args:

            gesture: 手势名称

            

        Returns:

            控制命令或None

        """

        if gesture not in self.gesture_to_command:

            logger.warning(f"Unknown gesture: {gesture}")

            return None

            

        command = self.gesture_to_command[gesture]

        

        # 检查冷却时间

        current_time = time.time() * 1000 # 转换为毫秒

        if current_time - self.last_command_time < self.command_cooldown_ms:

            logger.debug(f"Command cooldown active, ignoring {gesture}")

            return None

            

        # 处理需要确认的命令

        if command.requires_confirmation:

            if self.pending_confirmation is None:

                self.pending_confirmation = command

                logger.info(f"Command pending confirmation: {command.description}")

                return None

            elif self.pending_confirmation.cmd_type == command.cmd_type:

                # 同一命令再次确认

                self.pending_confirmation = None

                self.last_command_time = current_time

                logger.info(f"Confirmed command: {command.description}")

                return command

            else:

                # 不同命令,清除待确认状态

                self.pending_confirmation = None

                

        self.last_command_time = current_time

        return command

    

    def handle_confirmation(self, gesture: str) -> Optional[Command]:

        """

        处理确认手势(OK手势)

        

        Args:

            gesture: 当前手势

            

        Returns:

            确认后的命令或None

        """

        if gesture == "OK_SIGN" and self.pending_confirmation:

            command = self.pending_confirmation

            self.pending_confirmation = None

            self.last_command_time = time.time() * 1000

            logger.info(f"Confirmed via OK sign: {command.description}")

            return command

        return None

    

    def register_custom_mapping(self, gesture: str, command: Command) -> None:

        """

        注册自定义手势-命令映射

        

        Args:

            gesture: 手势名称

            command: 控制命令

        """

        if gesture in GESTURE_CONFIG.gesture_classes:

            self.gesture_to_command[gesture] = command

            logger.info(f"Registered custom mapping: {gesture} -> {command.description}")

        else:

            logger.error(f"Invalid gesture: {gesture}")

    

    def get_pending_command(self) -> Optional[Command]:

        """获取待确认的命令"""

        return self.pending_confirmation

    

    def cancel_pending(self) -> None:

        """取消待确认命令"""

        self.pending_confirmation = None

利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!

    

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐