一、项目介绍
手势识别是一种常见的计算机视觉应用,可以通过摄像头获取手部图像,再根据手部的关键点位置判断当前手势。本次项目使用 OpenCV 获取摄像头画面,使用 MediaPipe 检测手部关键点,然后将关键点的三维坐标转换成机器学习模型可以处理的特征,最后使用传统机器学习算法完成手势分类和实时识别。
整个项目主要分为两个部分:第一部分负责采集手势数据,第二部分负责训练分类模型并进行实时识别。
程序中设置了 6 种手势类别,分别是拳头、张开手、指向、剪刀手、OK 手势。数据采集程序会将每个手势的关键点信息保存为 JSON 文件,为后续模型训练提供数据。
二、MediaPipe手部关键点检测
程序首先导入 OpenCV、MediaPipe、NumPy、os 和 json 等库,其中 OpenCV 主要负责摄像头和图像处理,MediaPipe 用于手部检测,NumPy 用于数据处理,json 用于保存采集的数据。
MediaPipe 手部模型可以检测手部的 21 个关键点。程序设置:
with mp_hands.Hands(
static_image_mode=False,
max_num_hands=1,
min_detection_confidence=0.7,
min_tracking_confidence=0.7) as hands:
其中 max_num_hands=1 表示一次最多检测一只手,min_detection_confidence=0.7 是手部检测置信度阈值,min_tracking_confidence=0.7 是手部跟踪置信度阈值。
摄像头获取的图像默认是 BGR 格式,而 MediaPipe 处理图像时使用 RGB 格式,因此程序先使用:
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = hands.process(image)
完成颜色空间转换和手部检测,之后再转换回 BGR 格式用于 OpenCV 显示。
当检测到手部以后,可以通过 hand_landmarks.landmark 获取 21 个关键点。每一个关键点都包含 x、y 和 z 三个坐标值:
landmarks = []
for lm in hand_landmarks.landmark:
landmarks.append([lm.x, lm.y, lm.z])
因此,一只手最终可以得到:
21 × 3 = 63 个特征值。
同时,程序使用 draw_landmarks() 将关键点以及手部连接线绘制到摄像头画面上,方便观察检测效果。
三、手势数据采集
为了训练机器学习模型,需要先建立手势数据集。程序根据不同手势创建对应的文件夹:
DATA_DIR = "gesture_data"
for cls in GESTURE_CLASSES.values():
os.makedirs(os.path.join(DATA_DIR, cls), exist_ok=True)
这样会形成不同的手势类别目录,每个目录中保存对应手势的 JSON 数据。
程序通过键盘数字控制数据采集。例如按下对应数字键后,如果摄像头检测到了手部,就会把当前手势的关键点保存下来。
保存的数据主要包括四部分:
data = {
"class": cls_name,
"class_index": cls_idx,
"landmarks": landmarks,
"timestamp": str(np.datetime64('now'))
}
其中 class 表示手势名称,class_index 表示手势对应的数字编号,landmarks 保存 21 个关键点的三维坐标,timestamp 保存数据采集时间。
使用 JSON 保存数据的好处是结构比较清晰,而且后续读取时比较方便。这样,原始的手部图像最终被转换成了结构化的关键点数据。
四、将关键点转换为机器学习特征
第二个程序首先遍历不同手势类别对应的文件夹,然后读取其中的 JSON 文件:
for cls_idx, cls_name in GESTURE_CLASSES.items():
cls_dir = os.path.join(DATA_DIR, cls_name)
读取 JSON 后,将其中的 21 个关键点展开成一个一维特征向量:
landmarks = data["landmarks"]
feature_vector = []
for lm in landmarks:
feature_vector.extend(lm)
每个关键点包含三个数据,因此最终一个样本具有 63 维特征:
21 个关键点 × 3 个坐标 = 63 维。
所有样本组成特征矩阵 X,对应的手势类别组成标签数组 y。
这种方法相比直接使用原始图片进行训练更加简单,因为模型不需要直接处理大量像素,而是直接学习手部关键点之间的空间关系。
五、划分数据集和特征标准化
得到数据之后,程序使用 train_test_split() 将数据划分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=y
)
其中 test_size=0.2 表示 20% 的数据作为测试集,剩余 80% 用于训练。random_state=42 用于保证每次划分结果具有一定的可重复性,stratify=y 可以让训练集和测试集中各类别的比例保持相对一致。
之后使用 StandardScaler 对特征进行标准化:
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
这里需要注意,标准化器只在训练集上执行 fit,然后使用相同的标准化规则处理测试集。这可以保证训练和测试时的数据处理方式一致。
程序还使用 joblib.dump() 将标准化器保存成 scaler.pkl,方便实时识别时使用相同的特征处理方式。
六、多种传统机器学习模型
为了比较不同算法在手势分类任务上的表现,程序同时训练了四种传统机器学习模型:
models = {
"K近邻分类器": KNeighborsClassifier(n_neighbors=5),
"支持向量机": SVC(kernel='rbf', gamma='scale'),
"决策树": DecisionTreeClassifier(max_depth=10),
"随机森林": RandomForestClassifier(n_estimators=100)
}
分别是 K 近邻、支持向量机、决策树和随机森林。
K 近邻分类器通过寻找距离较近的样本进行分类;支持向量机通过寻找合适的分类边界完成分类;决策树通过不断进行条件判断进行分类;随机森林则通过多棵决策树共同完成预测。
程序依次训练这些模型,并通过测试集计算准确率:
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
accuracy = accuracy_score(y_test, y_pred)
同时使用 classification_report() 输出分类报告,从多个指标观察模型的分类情况。
七、选择并保存模型
程序通过比较每个模型的测试集准确率,记录当前表现较好的模型:
if accuracy > best_accuracy:
best_accuracy = accuracy
best_model = model
best_model_name = name
最终使用 joblib.dump() 保存模型:
joblib.dump(best_model, "best_gesture_model.pkl")
因此训练完成后会得到两个重要文件:
-
best_gesture_model.pkl:保存训练好的手势分类模型 -
scaler.pkl:保存特征标准化器
这样在之后进行实时识别时,就不需要重新训练模型,直接加载这两个文件即可。
八、使用混淆矩阵分析分类结果
除了准确率和分类报告,程序还使用混淆矩阵对模型进行进一步分析:
cm = confusion_matrix(y_test, y_pred_best)
然后通过 Seaborn 绘制热力图:
sns.heatmap(
cm,
annot=True,
fmt='d',
cmap='Blues',
xticklabels=GESTURE_CLASSES.values(),
yticklabels=GESTURE_CLASSES.values()
)
混淆矩阵可以观察不同手势之间的预测情况。例如某一种手势经常被预测成另一种手势,就可以从矩阵中比较直观地发现。
最终图像保存为:
confusion_matrix.png
因此,模型评估不仅可以看整体准确率,还可以进一步分析具体类别之间的识别情况。
九、实时手势识别
训练完成以后,程序可以进入实时识别模式。首先加载训练好的模型和标准化器:
model = joblib.load("best_gesture_model.pkl")
scaler = joblib.load("scaler.pkl")
然后重新启动摄像头和 MediaPipe 手部检测。实时检测到手部后,同样提取 21 个关键点的三维坐标,并转换为 63 维特征向量:
landmarks = []
for lm in hand_landmarks.landmark:
landmarks.extend([lm.x, lm.y, lm.z])
接着使用训练阶段保存的标准化器处理数据:
landmarks_scaled = scaler.transform([landmarks])
最后将处理后的特征输入模型:
prediction = model.predict(landmarks_scaled)
predicted_class = GESTURE_CLASSES[prediction[0]]
这样就可以得到当前摄像头画面中的手势类别。
如果模型支持 predict_proba(),程序还会获取各类别的预测概率,并把预测结果和概率显示在摄像头画面上:
display_text = f"{predicted_class} ({max_prob:.1f}%)"
最终通过 cv2.putText() 将识别结果显示出来,实现实时手势识别。
十、命令行参数
程序最后使用 argparse 实现训练和实时识别两种运行方式:
parser.add_argument('--train', action='store_true', help='训练模型')
parser.add_argument('--recognize', action='store_true', help='实时识别')
运行:
python 程序.py --train
可以进行模型训练。
运行:
python 程序.py --recognize
可以进入实时手势识别模式。
十一、项目流程总结
整个手势识别系统可以概括为以下流程:
摄像头采集图像 → MediaPipe检测手部 → 提取21个关键点 → 获取63维三维坐标特征 → 保存JSON数据 → 划分训练集和测试集 → 特征标准化 → 训练多种机器学习模型 → 测试模型 → 保存模型和标准化器 → 摄像头实时检测 → 提取关键点 → 标准化 → 模型预测 → 显示识别结果。