简介:这份资源面向计算机视觉与机器学习入门者,提供基于傅里叶算子的手势识别完整实现方案,可用于课程设计、毕业设计或算法练手。代码在Win10与Python3.7环境下开发,覆盖图像平滑、OTSU阈值肤色分割、八邻域搜索轮廓检测,并提取傅里叶描述子与椭圆傅里叶描述子做归一化处理,再分别用KNN和SVM训练模型,最后基于PyQt5搭建简易交互界面。压缩包共约2000个文件,以txt样本数据、png手势图片为主,另含m脚本、py源码、pyc缓存及少量xml、docx等配置与说明文档,整体约142.52MB,样本库与训练脚本齐备。目前已有13868人学习下载,读者可据此复现从采集到轮廓曲线提取的完整流程,对比两种描述子与两种分类器的效果,并借助界面快速验证模型,适合想系统掌握手势识别工程链路的学习者。
1. 手势识别项目拆解:傅里叶算子加 SVM 的完整源码能跑出什么
很多人做手势识别第一反应是上深度学习,但真到了嵌入式或者算力吃紧的场景,传统方案反而更稳。这份源码走的就是傅里叶描述子加 SVM 的路线,Python 实现,自带样本库,拿到手就能跑通训练和预测全流程。它解决的核心问题是:把手势轮廓从图像里抠出来,用傅里叶算子把形状转成特征向量,再喂给 SVM 做分类。适合谁?一是想入门计算机视觉传统方法的学生,二是需要在低算力设备上做手势交互的开发者,三是想拿一个完整闭环项目练手的人。整份代码不依赖大模型,纯 OpenCV 加 scikit-learn 就能撑起来,样本库也一并给了,省去自己采集的麻烦。
2. 技术选型与项目结构:为什么用傅里叶描述子而不是 CNN
2.1 傅里叶描述子的数学直觉
手势轮廓是一个闭合曲线,把它看成复平面上的点序列,做离散傅里叶变换后,低频分量描述整体形状,高频分量描述细节。只保留前若干个低频系数,就得到一个对旋转、平移、缩放都不敏感的特征向量。这个特性在手势识别里非常关键——同一个人做同一个手势,手离摄像头远近不同、角度不同,轮廓大小和位置都会变,但傅里叶描述子的低频部分基本稳定。
常见做法是取前 10 到 20 个系数,归一化之后拼成一个定长向量。这份源码里用的是归一化后的傅里叶描述子,具体取多少个系数可以在配置里改。相比 CNN 需要大量标注数据和 GPU 训练,傅里叶描述子加 SVM 在小样本下表现更可控,训练时间以秒计。
2.2 项目目录结构
拿到源码后先别急着跑,把目录翻一遍心里有数。典型结构如下:
gesture_recognition/ ├── data/ │ ├── train/ # 训练样本,按手势类别分文件夹 │ └── test/ # 测试样本 ├── src/ │ ├── preprocess.py # 图像预处理:去噪、二值化、轮廓提取 │ ├── fourier.py # 傅里叶描述子计算与归一化 │ ├── train.py # SVM 训练脚本 │ ├── predict.py # 单张图片预测 │ └── config.py # 参数配置 ├── models/ # 训练好的模型保存路径 ├── requirements.txt └── README.mddata/train下每个子文件夹代表一个手势类别,文件夹名就是标签。src/config.py集中管理傅里叶系数个数、SVM 核函数类型、图像尺寸等参数,改配置不用翻代码。models/用来存训练完的.pkl文件,预测时直接加载。
2.3 依赖环境与安装
依赖不多,核心就三个:
pip install opencv-python numpy scikit-learn matplotlibOpenCV 负责图像读写和轮廓提取,numpy 做傅里叶变换和数组运算,scikit-learn 提供 SVM 实现和模型持久化。matplotlib 可选,用来可视化轮廓和傅里叶重建效果,调试阶段很有用。
提示:OpenCV 安装时如果遇到 GUI 相关报错,换成
opencv-python-headless即可,服务器环境不需要图形界面。
3. 核心模块拆解:从图像到特征向量的完整链路
3.1 图像预处理与轮廓提取
预处理的目标是把彩色图像变成干净的二值轮廓图。步骤是灰度化、高斯模糊去噪、自适应二值化、形态学闭运算补空洞,最后用findContours提取最大轮廓。
import cv2 import numpy as np def extract_contour(image_path, target_size=(128, 128)): # 读取图像并统一尺寸 img = cv2.imread(image_path) img = cv2.resize(img, target_size) # 灰度化 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊,核大小 5x5 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 自适应二值化,反转使手势为白色 binary = cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2 ) # 形态学闭运算,填补轮廓内部小孔 kernel = np.ones((3, 3), np.uint8) binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 提取轮廓,只取面积最大的那个 contours, _ = cv2.findContours( binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE ) if not contours: return None contour = max(contours, key=cv2.contourArea) return contourtarget_size控制输入图像尺寸,统一到 128x128 是为了后续傅里叶变换的点数一致。adaptiveThreshold的11是邻域块大小,2是常数 C,这两个参数对二值化效果影响很大,光照不均匀时可以调大块大小。RETR_EXTERNAL只取外轮廓,避免内部噪声干扰。max(contours, key=cv2.contourArea)取面积最大的轮廓,假设手势是画面主体。
3.2 傅里叶描述子计算与归一化
拿到轮廓点序列后,把它映射到复平面,做 FFT,取低频系数,再做归一化。
def fourier_descriptor(contour, num_coeffs=15): # 将轮廓点转为复数序列 contour = contour.squeeze() complex_seq = contour[:, 0] + 1j * contour[:, 1] # 离散傅里叶变换 fft_result = np.fft.fft(complex_seq) # 取前 num_coeffs 个低频系数(含直流分量) coeffs = fft_result[:num_coeffs] # 归一化:除以直流分量幅值,消除尺度影响 if abs(coeffs[0]) > 1e-6: coeffs = coeffs / abs(coeffs[0]) # 取模值作为特征,消除起始点影响 feature = np.abs(coeffs[1:]) # 去掉直流分量 return featurenum_coeffs决定保留多少个频率分量,太小会丢失形状细节,太大会引入噪声。源码默认 15,实际调参时可以从 10 试到 25。除以abs(coeffs[0])是尺度归一化,让不同大小的手势轮廓得到相同特征。取模值np.abs是消除旋转带来的相位变化,因为手势旋转后轮廓起点变了,但形状没变。最终特征向量长度是num_coeffs - 1,因为直流分量被去掉了。
3.3 SVM 训练与模型保存
特征准备好之后,把所有训练样本的特征向量堆成矩阵,标签对应好,直接喂给 SVM。
from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline import joblib import os def train_model(train_dir, model_path, num_coeffs=15): features = [] labels = [] # 遍历每个类别文件夹 for label in os.listdir(train_dir): class_dir = os.path.join(train_dir, label) if not os.path.isdir(class_dir): continue for fname in os.listdir(class_dir): fpath = os.path.join(class_dir, fname) contour = extract_contour(fpath) if contour is None: continue feat = fourier_descriptor(contour, num_coeffs) features.append(feat) labels.append(label) X = np.array(features) y = np.array(labels) # 构建管道:标准化 + SVM pipeline = Pipeline([ ('scaler', StandardScaler()), ('svm', SVC(kernel='rbf', C=10, gamma='scale', probability=True)) ]) pipeline.fit(X, y) # 保存模型 joblib.dump(pipeline, model_path) print(f"模型已保存至 {model_path},共 {len(X)} 个样本") return pipelineStandardScaler把特征各维度标准化,避免某些频率分量数值过大主导距离计算。SVC用 RBF 核,C=10控制惩罚力度,gamma='scale'自动根据特征方差设核宽度。probability=True开启概率输出,预测时可以拿到置信度。joblib.dump把整个管道保存下来,预测时直接加载,不用重新标准化。
3.4 预测与结果输出
预测脚本加载模型,对单张图片走同样的预处理和特征提取流程,输出类别和置信度。
def predict(image_path, model_path, num_coeffs=15): pipeline = joblib.load(model_path) contour = extract_contour(image_path) if contour is None: return "未检测到手势", 0.0 feat = fourier_descriptor(contour, num_coeffs).reshape(1, -1) label = pipeline.predict(feat)[0] proba = pipeline.predict_proba(feat).max() return label, probareshape(1, -1)把单样本特征变成二维数组,符合 sklearn 输入要求。predict_proba返回各类别概率,取最大值作为置信度。如果轮廓提取失败,直接返回提示,避免后续报错。
4. 跑通全流程:从零开始训练到预测
4.1 数据准备与目录规范
样本库已经按类别分好文件夹,直接放到data/train下即可。如果自己采集,注意每个类别至少 30 张,背景尽量干净,手势居中。图片格式统一为 jpg 或 png,尺寸不要求一致,预处理会统一 resize。
注意:类别文件夹名不要用中文,避免编码问题。建议用
gesture_01、gesture_02这种命名。
4.2 训练命令与参数调整
在项目根目录执行:
python src/train.py --data_dir data/train --model_path models/svm_model.pkl --num_coeffs 15如果train.py没有加命令行参数解析,直接改config.py里的配置也行。训练完成后会打印样本数量和模型保存路径。常见做法是在config.py里把num_coeffs、C、gamma都暴露出来,方便做网格搜索。
4.3 预测验证与效果评估
python src/predict.py --image data/test/gesture_01/sample.jpg --model models/svm_model.pkl输出类似预测类别: gesture_01, 置信度: 0.93。如果置信度普遍偏低,检查预处理阶段轮廓是否提取正确,可以用 matplotlib 把轮廓画出来看一眼。另一个排查方向是num_coeffs是否太小,导致不同类别特征区分度不够。
4.4 批量测试与混淆矩阵
想系统评估,写个脚本遍历测试集,统计准确率和混淆矩阵:
from sklearn.metrics import classification_report, confusion_matrix def evaluate(test_dir, model_path, num_coeffs=15): pipeline = joblib.load(model_path) y_true, y_pred = [], [] for label in os.listdir(test_dir): class_dir = os.path.join(test_dir, label) if not os.path.isdir(class_dir): continue for fname in os.listdir(class_dir): fpath = os.path.join(class_dir, fname) contour = extract_contour(fpath) if contour is None: continue feat = fourier_descriptor(contour, num_coeffs).reshape(1, -1) pred = pipeline.predict(feat)[0] y_true.append(label) y_pred.append(pred) print(classification_report(y_true, y_pred)) print(confusion_matrix(y_true, y_pred))classification_report给出每个类别的精确率、召回率和 F1,confusion_matrix看哪些类别容易混。如果某两个类别互相误判多,说明它们的傅里叶描述子太接近,可以考虑增加num_coeffs或者换更复杂的特征。
5. 避坑与常见问题排查
5.1 轮廓提取为空或提取到错误区域
现象:预测时返回“未检测到手势”,或者轮廓明显不是手势。原因通常是二值化阈值不合适,或者背景有比手势更大的高亮区域。解决:把adaptiveThreshold的块大小从 11 调到 21 或 31,常数 C 从 2 调到 5 或 10。如果背景干扰严重,先做背景减除或者用肤色检测限定区域。
5.2 不同类别特征区分度低
现象:混淆矩阵显示多个类别互相误判,准确率卡在 60% 左右上不去。原因可能是num_coeffs太小,低频系数不足以区分相似手势。解决:把num_coeffs从 15 逐步加到 25 或 30,重新训练看效果。另一个原因是样本量太少,每个类别不到 20 张,SVM 学不到足够边界。
5.3 训练时报数组维度不一致
现象:ValueError: setting an array element with a sequence。原因通常是某些图片轮廓提取失败返回 None,被跳过后特征列表长度不一致,或者不同图片提取的轮廓点数不同导致傅里叶系数长度不同。解决:确保fourier_descriptor返回固定长度向量,num_coeffs固定不变。轮廓提取失败的直接跳过,不要塞 None 进去。
5.4 模型加载后预测结果全为同一类
现象:不管输入什么图片,预测结果都是同一个标签。原因可能是训练时标签和特征没对齐,或者标准化参数在保存和加载时不一致。解决:检查train_model里features和labels的 append 顺序是否一一对应。用Pipeline保存整个流程可以避免标准化参数丢失,不要单独保存SVC对象。
5.5 傅里叶描述子对旋转敏感
现象:同一个手势旋转 90 度后识别错误。原因:虽然取了模值消除相位,但如果轮廓起点变化太大,低频系数的模值也会有波动。解决:在计算傅里叶描述子之前,先对轮廓做重采样,固定点数,并且以轮廓质心为原点做极坐标展开,这样旋转不变性更好。源码里如果没做这一步,可以自己加上。
6. 二次开发与改造思路:从 Demo 到可用模块
这份源码跑通之后,最值钱的不是准确率数字,而是那条从图像到特征的流水线。我一般会从三个方向改:第一,把extract_contour和fourier_descriptor抽成独立模块,输入输出定义清楚,方便替换成其他特征提取方法,比如 Hu 矩或者 Zernike 矩。第二,加一个实时摄像头循环,用cv2.VideoCapture逐帧读取,每 N 帧做一次预测,中间帧用跟踪算法补上,这样能做成实时手势交互。第三,把 SVM 换成轻量级分类器,比如线性 SVM 或者决策树,导出成 ONNX 格式,部署到嵌入式设备上。
改造时有个细节容易翻车:实时场景下每帧都做完整预处理和 FFT,帧率会掉到个位数。我的习惯是隔帧预测,中间帧用光流或者简单的质心跟踪维持手势位置,预测帧才走完整流程。另外,样本库如果不够大,可以做数据增强,对训练图片做随机旋转、缩放、加噪声,再提取傅里叶描述子,这样 SVM 的泛化能力会明显提升。
验证改造效果的方法很简单:固定一个测试集,每次改完跑一遍evaluate,看混淆矩阵和 F1 变化。不要凭感觉调参,血泪经验是调了num_coeffs觉得“应该好了”,结果一跑测试集发现某两个类别误判更多了。从那以后我每次改特征提取或者分类器参数,都强制走一遍批量评估,把classification_report打出来对比。希望帮到你。
本文还有配套的精品资源,点击获取