基于Opencv的手势识别
目录
项目概览
本项目旨在实现静态手势识别,其核心在于通过分类器区分不同的手势。围绕这一点,代码分为三个部分,数据采集与处理,模型训练以及GUI设计。
该项目已在GitHub开源,遵循MIT License。
项目结构如下:
├─gesture_record
│ │ image_processor.py(图片处理)
│ │ roi_save.py(手势照片收集)
│ │ __init__.py
│ │
│ ├─data(手势照片目录)
│ └─dataset(处理后的轮廓图片目录)
│
│
├─gui
│ │ classify.py(调用模型)
│ │ qt_gui.py(gui实现)
│ │
│ ├─model(模型目录)
│ └─ svm_efd_train_model.m
│
└─training
├─dataenhance(数据增强)
│ │ calculate.py(计算傅里叶描述子)
│ │ rotate.py(扩充数据)
│ │ __init__.py
│ │
│ ├─feature(描述子txt文件目录)
│ └─image(轮廓图片目录)
└─svm
│ SVM_training.py(svm训练文件)
│
├─feature(描述子txt文件目录)
├─model(模型目录)
│ svm_efd_train_model.m
│
└─test_feature(测试集目录)
功能实现
分类器选择
在选择分类器时,我综合考虑了以下因素:
数据集规模有限
由于是个人项目,数据集规模相对较小。深度学习方法(CNN)通常需要大量数据以避免过拟合。鉴于时间和资源限制,收集大量数据并不现实,因此需要一种能在小数据集上表现良好的分类器。
特征维度高
手势特征(如傅里叶描述子)通常是高维的。部分分类器在处理高维数据时可能会遇到维度灾难,导致性能下降。因此,需要一种能有效处理高维数据的分类器。
项目目标是静态手势识别
本项目目标是实现静态手势识别,不涉及动态手势识别。因此,适用于动态手势识别的方法(如 HMM)并不合适。
基于上述因素,支持向量机(SVM)成为理想选择。SVM 通过核技巧有效处理高维数据,避免维度灾难导致的性能下降。此外,SVM 通过最大化间隔提高模型泛化能力,即使在训练数据较少的情况下,也能较好地适应新数据。同时,SVM 参数相对较少,调参过程简单,使其能在小数据集上实现高精度识别。
数据收集与处理
要训练分类器,首先需要收集数据并进行处理。数据收集包括拍摄不同手势的照片,使用 OpenCV 进行图像预处理,提取手势特征,然后划分为训练集和测试集,最后利用 SVM 进行模型训练。
数据采集
拍摄不同手势的照片,每种手势采集 20 份样本。提供用户友好的界面,方便用户进行手势拍摄。由于电脑摄像头摄影范围较大,需设定一个可移动的 ROI(感兴趣区域)进行框选。为简化录制流程,采用自动保存机制,确保每次拍摄的手势图片即时存储,并可连续拍摄多组手势。
图像预处理
将图像从 BGR 颜色空间转换为 YCrCb 颜色空间,对 Cr 通道进行高斯模糊,使用 Otsu 算法进行阈值分割得到肤色掩码,然后进行腐蚀和膨胀操作,以去除噪声并增强轮廓。
·关于肤色检测
在肤色检测上,简单的 RGB 空间阈值设定容易受光照变化影响,导致检测结果不稳定。因此,采用 HSV 颜色空间进行肤色检测更为有效,因其对光照变化具有较好的鲁棒性。然而,在复杂背景下,肤色与背景颜色相近时,误检率较高。综合考虑,本项目采用 YCrCb + Otsu 的肤色检测方法,结合形态学操作,有效减少背景干扰。
def preprocess_image(self, img):
"""图像预处理:肤色检测和形态学操作。"""
ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCR_CB)
cr = cv2.split(ycrcb)[1]
cr_blurred = cv2.GaussianBlur(cr, (5, 5), 0)
_, skin = cv2.threshold(cr_blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
skin_eroded = cv2.erode(skin, kernel, iterations=1)
skin_dilated = cv2.dilate(skin_eroded, kernel, iterations=2)
self.logger.info("图像预处理完成")
return skin_dilated
特征提取
从预处理后的二值图像中提取轮廓,选择面积最大的轮廓,将其转换为复数形式,计算傅里叶变换,并截断保留低频成分,得到傅里叶描述子。返回重建的轮廓图像和傅里叶描述子。截断傅里叶变换结果,保留中心附近的 MIN_DESCRIPTOR(32)个系数,以减少数据量并保留主要特征。
最后通过 GestureProcessor 类实例将处理后的图像存储在指定位置。当然,也可以采用 OpenCV 自带的 AdaptiveSkinDetector 函数,该函数专门用于肤色检测。
class FourierDescriptor:
MIN_DESCRIPTOR = 32 # 保留的傅里叶描述子数量
def __init__(self, image_processor):
self.image_processor = image_processor
self.logger = logging.getLogger(__name__)
def calculate(self, skin_dilated):
"""计算傅里叶描述子"""
contours = self.image_processor.find_contours(skin_dilated)
if not contours:
self.logger.warning("未检测到轮廓")
return None, None
# 取面积最大的轮廓
contour = max(contours, key=cv2.contourArea)
contour_array = contour[:, 0, :]
# 创建轮廓图像
ret_np = np.zeros(skin_dilated.shape, np.uint8)
ret = cv2.drawContours(ret_np, [contour], -1, 255, 1)
# 转换为复数形式
contours_complex = contour_array[:, 0] + 1j * contour_array[:, 1]
# 傅里叶变换并截断
fourier_result = np.fft.fft(contours_complex)
descriptor = self.truncate_descriptor(fourier_result)
self.logger.info("傅里叶描述子计算完成")
return ret, descriptor
def truncate_descriptor(self, fourier_result):
"""截断并保留低频成分"""
num_coeffs = len(fourier_result)
center = num_coeffs // 2
low, high = center - self.MIN_DESCRIPTOR // 2, center + self.MIN_DESCRIPTOR // 2
descriptor = np.fft.fftshift(fourier_result)
descriptor = descriptor[low:high]
return np.fft.ifftshift(descriptor)
def reconstruct(self, img, descriptor):
"""从傅里叶描述子重建轮廓"""
coeffs = np.fft.ifftshift(descriptor)
contour_reconstruct = np.fft.ifft(coeffs)
# 转换为坐标点
contour_points = np.array([
[np.real(pt), np.imag(pt)]
for pt in contour_reconstruct
], dtype=np.float32)
# 归一化到图像尺寸
contour_points -= np.min(contour_points, axis=0)
scale = min(img.shape[0] / np.max(contour_points[:, 1]),
img.shape[1] / np.max(contour_points[:, 0]))
contour_points = (contour_points * scale).astype(np.int32)
# 绘制轮廓
black_np = np.zeros(img.shape[:2], dtype=np.uint8)
cv2.polylines(black_np, [contour_points], isClosed=True, color=255, thickness=1)
self.logger.info("轮廓重建完成")
return black_np
数据增强与傅里叶描述子计算
由于样本数量较少,我们采用数据增强技术来扩充样本集。具体方法包括对图像进行翻转和缩放操作,以生成更多变体。在完成数据增强后,我们将调用 FourierDescriptor 类的 calculate 方法,计算每张图像的傅里叶描述子。
傅里叶描述子的归一化处理
为了确保后续分类器训练时数据的稳定性和可靠性,我们需要对傅里叶描述子进行归一化处理。具体步骤如下:
提取描述子的模:计算傅里叶描述子的绝对值,以获取其模(幅值)。
检查归一化基准值:以描述子模的第一个非零值作为归一化基准值。如果该基准值为零,则说明该描述子无效,我们将打印提示信息并跳过该图像的处理。
归一化处理:将每个描述子模除以基准值进行归一化,并将结果乘以 100 后取整。这样可以将描述子的值统一到一个相对稳定的范围内,便于后续的分类器训练。
for cat in categories: # 遍历每个类别
cat_path = os.path.join(path_img, cat)
for img_name in os.listdir(cat_path): # 遍历类别文件夹中的所有图像
img_path = os.path.join(cat_path, img_name)
roi = cv2.imread(img_path)
if roi is None:
print(f"图像 {img_path} 无法读取,跳过")
continue
# 将图像转换为灰度图像
roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
# 先创建 image_processor 实例
image_processor = ip.ImageProcessor()
# 创建 FourierDescriptor 实例
fd = ip.FourierDescriptor(image_processor)
# 计算傅里叶描述子
result = fd.calculate( roi_gray)
print("fourierDescriptor 返回值:", result) # 调试输出
# 提取傅里叶描述子
if isinstance(result, tuple) and len(result) > 1:
descriptor_in_use = result[1] # 傅里叶描述子
else:
print(f"图像 {img_path} 的傅里叶描述子无效,跳过")
continue
# 检查描述子是否有效
if len(descriptor_in_use) < 2:
print(f"图像 {img_path} 的傅里叶描述子无效,跳过")
continue
# 构造特征文件名
fd_name = os.path.join(path, f"{cat}_{os.path.splitext(img_name)[0]}.txt")
with open(fd_name, 'w', encoding='utf-8') as f:
# 取傅里叶描述子的绝对值(模)
descriptor_magnitude = np.abs(descriptor_in_use)
# 检查归一化基准值是否为零
temp = descriptor_magnitude[1]
if temp == 0:
print(f"图像 {img_path} 的归一化基准值为零,跳过")
continue
for k in range(1, len(descriptor_magnitude)):
x_record = int(100 * descriptor_magnitude[k] / temp)
f.write(str(x_record) + ' ')
f.write('\n')
SVM训练
数据准备与特征提取
提取类别标签:从特征文件的文件名中提取类别标签。文件名格式为 {类别}_{编号}.txt,其中类别标签是文件名的第一个部分。
特征文件读取:将每个特征文件读取为一维矩阵。通过读取 .txt 文件中的数据,并将其转换为整数形式存储到矩阵中。
构建训练矩阵:将所有特征矩阵组合成一个训练矩阵,同时将类别标签存储到一个列表中。这为后续的模型训练提供了输入数据。
# 加载训练数据
hwLabels = [] # 存放类别标签
trainingFileList = listdir(path)
m = len(trainingFileList)
trainingMat = np.zeros((m, N))
for i in range(m):
fileNameStr = trainingFileList[i]
classNumber = int(fileNameStr.split('_')[0])
hwLabels.append(classNumber)
trainingMat[i, :] = txtToVector(path + fileNameStr, N) # 将训练集改为矩阵格式
print("数据加载完成")
模型训练
使用 GridSearchCV 进行参数优化:定义 SVM 模型的参数网格,包括核函数类型(如 linear 和 rbf)、正则化参数 C 和核函数参数 gamma。然后使用 GridSearchCV 进行网格搜索,结合 5 折交叉验证来评估不同参数组合的性能。
训练模型:调用 fit 方法训练 SVM 模型。该方法会自动遍历参数网格中的所有组合,通过交叉验证找到最佳的参数组合。
保存最佳模型:训练完成后,保存最佳参数对应的模型,以便后续使用。
# 定义SVM模型参数
svc = SVC()
parameters = {
'kernel': ('linear', 'rbf'),
'C': [1, 3, 5, 7, 9, 11, 13, 15, 17, 19],
'gamma': [0.00001, 0.0001, 0.001, 0.1, 1, 10, 100, 1000]
}
# 使用网格搜索法进行参数优化
clf = GridSearchCV(svc, parameters, cv=5, n_jobs=8) # 设置5折交叉验证
clf.fit(trainingMat, hwLabels)
print("最佳参数:", clf.best_params_) # 打印出最好的结果
# 保存最佳模型
save_path = model_path + "svm_efd_" + "train_model.m"
os.makedirs(os.path.dirname(save_path), exist_ok=True)
joblib.dump(clf.best_estimator_, save_path)
print("SVM Model saved successfully.")
模型测试
最后对模型进行测试,验证其在测试集上的性能。通过计算错误率,评估模型的分类精度。
事实上由于傅里叶描述子匹配任务简单,模型的错误率普遍较低。然而,对于复杂场景下的识别任务,仍需进一步测试。
GUI设计
使用时,首先需要点击打开相机,此时会显示摄像头捕捉到的画面以及经过处理后的图像。然后在摄像头前展示待检测手势,接着点击开始预测,预测结果会显示在右下角预测结果栏中。需要注意的是,检测并不是实时进行的而是检测按下后瞬间的手势

未来展望
·动态手势识别:
本项目专注于静态手势识别,未来可以扩展到动态手势识别,探索使用时间序列分析或深度学习方法来处理动态手势数据。
·实时手势识别:
将项目应用于实时手势识别系统,结合摄像头输入,实现即时的手势识别和反馈。
参考文献:
scnscnscn 个人主页
更多推荐

所有评论(0)