thumb

目录

项目背景

项目概览

功能实现

分类器选择

数据收集与处理

数据增强与傅里叶描述子计算

SVM训练

GUI设计

未来展望


项目概览

本项目旨在实现静态手势识别,其核心在于通过分类器区分不同的手势。围绕这一点,代码分为三个部分,数据采集与处理,模型训练以及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设计

使用时,首先需要点击打开相机,此时会显示摄像头捕捉到的画面以及经过处理后的图像。然后在摄像头前展示待检测手势,接着点击开始预测,预测结果会显示在右下角预测结果栏中。需要注意的是,检测并不是实时进行的而是检测按下后瞬间的手势


未来展望

·动态手势识别

本项目专注于静态手势识别,未来可以扩展到动态手势识别,探索使用时间序列分析或深度学习方法来处理动态手势数据。

·实时手势识别

将项目应用于实时手势识别系统,结合摄像头输入,实现即时的手势识别和反馈。


参考文献:

支持向量机 — 维基百科

支持向量机SVM理论基础

基于OpenCV的手势识别完整项目(Python3.7)

putText()显示中文问题

肤色检测

轮廓傅里叶描述子

Opencv中文教程

scnscnscn 个人主页

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐