简介这是一套面向高校学生与开发者的手语识别系统完整项目源码基于Python、OpenCV、JavaScript与MySQL构建适合作为毕业设计或课程设计参考。系统通过摄像头实时捕获手势并转换为文字支持用户上传手势视频训练个性化神经网络模型并借助降噪算法提升识别准确率可应用于教育、社交与公共服务等场景。压缩包共85个文件约30.17MB包含11个Python脚本负责模型训练与后端逻辑、16个HTML页面与5个CSS样式搭建前端界面、4个JavaScript文件实现交互与识别脚本另含1个pth模型文件及项目说明文档目录结构清晰。已有73人学习关注。读者可获得从数据预处理、模型训练到实时识别的完整实现方案以及自定义字典、模型选择与手势提示等模块的代码参考便于快速理解系统流程并在此基础上二次开发。1. 手语识别系统到底在识别什么从一帧画面到一句可读文本很多人第一次听到“手语识别系统”脑子里浮现的是摄像头对着人屏幕上直接蹦出一句完整的话。真做起来会发现这件事被拆成了好几段手部区域怎么从画面里抠出来、静态手势怎么分类、连续动作怎么切分、识别结果怎么存进数据库、前端怎么把结果展示出来。标题里这套 Python OpenCV JavaScript MySQL 的组合恰好对应了这条链路上的四个环节——Python 做模型推理OpenCV 做图像预处理JavaScript 做前端交互MySQL 存识别记录和词条映射。它适合谁做毕设或课设的开发者手里有一份模型文件想把它跑成一个能演示、能录入、能查询的完整系统而不是只丢一个准确率数字。难点不在模型本身而在“模型输出怎么变成一条数据库记录再变成页面上的一句话”。这篇笔记就按这条链路把每一步的参数、代码和坑讲清楚。2. 环境搭建与模型文件加载让推理脚本先跑起来2.1 为什么选 Python 做推理、JavaScript 做展示常见做法是把识别逻辑全部放在 Python 侧用 Flask 或 FastAPI 起一个本地服务前端用 JavaScript 发请求拿结果。这样做的理由是模型文件通常是 PyTorch 或 TensorFlow 格式JavaScript 直接加载要么转 ONNX要么用 TensorFlow.js 重写转换过程容易掉精度调试成本高。而 Python 侧加载模型只需要几行代码OpenCV 的摄像头读取和图像处理也在同一进程里省去跨语言传图像的麻烦。我一般会这样分工Python 负责“摄像头 → 预处理 → 模型推理 → 返回 JSON”JavaScript 负责“调用接口 → 渲染文本 → 写入 MySQL 的请求转发”。MySQL 不直接连前端所有写库操作走 Python 接口避免把数据库账号暴露在浏览器里。2.2 用 conda 建环境并锁定依赖版本模型文件能不能加载成功一半取决于依赖版本对不对。血泪经验是不要用最新版的 torch 去加载旧模型报错信息往往指向 pickle 反序列化失败看起来像文件损坏其实是版本不匹配。# 创建独立环境Python 版本按模型训练时的版本选常见是 3.8 或 3.9 conda create -n sign_recog python3.9 -y conda activate sign_recog # 安装核心依赖版本号按模型文件实际需要调整 pip install opencv-python4.8.1.78 pip install torch1.13.1 torchvision0.14.1 pip install flask2.3.3 pip install mysql-connector-python8.1.0 pip install numpy1.24.3参数说明opencv-python选 4.8 系列是因为 4.9 之后部分摄像头后端在 Windows 上行为有变化torch的版本必须和模型保存时一致如果不确定先用torch.load试报错里会提示版本线索。mysql-connector-python用纯 Python 实现不需要额外装 C 库适合课设环境。2.3 加载模型文件并做一次最小推理验证在写任何接口之前先单独跑通一次推理。这一步的目的是确认模型文件没坏、输入尺寸对得上、输出类别数和你预期一致。import torch import cv2 import numpy as np # 加载模型map_location 保证在无 GPU 的机器上也能跑 model torch.load(model/sign_model.pth, map_locationcpu) model.eval() # 读取一张测试图转成模型需要的输入格式 img cv2.imread(test/sample_01.jpg) img cv2.resize(img, (224, 224)) # 输入尺寸按模型定义改 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 训练时若用 RGB这里必须转 img img.astype(np.float32) / 255.0 # 归一化到 0-1 img np.transpose(img, (2, 0, 1)) # HWC 转 CHW img np.expand_dims(img, axis0) # 增加 batch 维度 tensor torch.from_numpy(img) with torch.no_grad(): output model(tensor) pred torch.argmax(output, dim1).item() print(预测类别索引:, pred)逻辑说明map_locationcpu是后悔药防止模型在 GPU 上保存后拿到没显卡的机器上加载失败。cv2.cvtColor这一步最容易翻车——OpenCV 默认读进来是 BGR而大多数模型训练时用的是 RGB不转的话准确率会莫名其妙掉一大截。归一化系数和输入尺寸必须和训练时完全一致这两个参数错了模型输出就是随机数。3. OpenCV 预处理与手部区域提取把无关背景挡在模型外面3.1 肤色阈值和背景减除怎么选手语识别的输入如果直接是整张图背景里的桌椅、衣服颜色都会干扰分类。常见做法有两种一种是基于 YCrCb 肤色阈值做手部区域分割另一种是用背景减除MOG2提取运动前景。前者适合背景复杂但手部肤色明显的场景后者适合摄像头固定、背景基本不动的场景。我一般会先试肤色阈值因为课设环境里摄像头位置经常变背景减除需要重新建模。肤色阈值的缺点是光照变化敏感所以后面要加形态学操作把噪点去掉。3.2 用 YCrCb 阈值加形态学提取手部轮廓import cv2 import numpy as np def extract_hand_region(frame): # 转 YCrCb 空间Cr 通道对肤色更敏感 ycrcb cv2.cvtColor(frame, cv2.COLOR_BGR2YCrCb) # 肤色范围可按实际光照微调 lower np.array([0, 133, 77], dtypenp.uint8) upper np.array([255, 173, 127], dtypenp.uint8) mask cv2.inRange(ycrcb, lower, upper) # 开运算去噪闭运算补空洞 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 找最大轮廓假设画面里只有一只手 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None hand max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(hand) return frame[y:yh, x:xw]参数说明lower和upper里的 Cr 范围 133-173、Cb 范围 77-127 是常见经验值实际用的时候拿一张手部图打印 Cr 通道直方图再定。kernel大小 5x5 适合 640x480 分辨率分辨率更高要相应加大。max(contours, keycv2.contourArea)假设画面里只有一只手如果有多只手或手臂大面积露出会取到错误区域这时候要加面积上限过滤。3.3 预处理结果怎么和模型输入对齐提取出来的手部区域尺寸不固定直接送模型会报错。常见做法是缩放到模型输入尺寸但缩放会改变长宽比对某些手势有影响。我一般会先做 padding 到正方形再缩放保持手势不变形。def pad_to_square(img): h, w img.shape[:2] size max(h, w) # 用黑色填充到正方形避免拉伸变形 canvas np.zeros((size, size, 3), dtypenp.uint8) top (size - h) // 2 left (size - w) // 2 canvas[top:toph, left:leftw] img return canvas这一步的逻辑是先补成正方形再统一 resize 到 224x224这样手势的宽高比不会被破坏。如果跳过 padding 直接 resize握拳和伸掌的区分度会下降模型容易混淆。4. 识别结果落库与前端展示从 JSON 到页面文本4.1 MySQL 表结构怎么设计才够用课设级别的系统不需要太复杂的表结构但至少要分两张表一张存识别记录一张存手势词条映射。识别记录表存每次识别的类别索引、置信度、时间戳词条映射表存类别索引对应的中文文本。这样模型输出变了只需要改映射表不用动代码。CREATE TABLE sign_record ( id INT AUTO_INCREMENT PRIMARY KEY, class_index INT NOT NULL, confidence FLOAT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sign_mapping ( class_index INT PRIMARY KEY, label_text VARCHAR(64) NOT NULL ); -- 初始化几条映射按实际类别数补充 INSERT INTO sign_mapping VALUES (0, 你好), (1, 谢谢), (2, 再见);参数说明confidence用 FLOAT 够用不需要 DOUBLE。created_at用数据库默认时间避免前端传时间被篡改。sign_mapping的主键就是类别索引查询时直接 JOIN 或二次查询。4.2 Flask 接口把推理结果写进数据库from flask import Flask, request, jsonify import mysql.connector import torch import cv2 import numpy as np app Flask(__name__) model torch.load(model/sign_model.pth, map_locationcpu) model.eval() def get_db(): return mysql.connector.connect( hostlocalhost, userroot, passwordyour_password, databasesign_db ) app.route(/predict, methods[POST]) def predict(): file request.files[frame] img cv2.imdecode(np.frombuffer(file.read(), np.uint8), cv2.IMREAD_COLOR) # 预处理流程同上此处省略重复代码 tensor preprocess(img) with torch.no_grad(): output model(tensor) prob torch.softmax(output, dim1) conf, pred torch.max(prob, dim1) class_index pred.item() confidence conf.item() # 写库 db get_db() cursor db.cursor() cursor.execute( INSERT INTO sign_record (class_index, confidence) VALUES (%s, %s), (class_index, confidence) ) db.commit() cursor.execute(SELECT label_text FROM sign_mapping WHERE class_index%s, (class_index,)) row cursor.fetchone() cursor.close() db.close() return jsonify({ class_index: class_index, label: row[0] if row else 未知, confidence: round(confidence, 4) })逻辑说明request.files[frame]接收前端传来的图像帧用cv2.imdecode解码避免写临时文件。torch.softmax把输出转成概率torch.max同时拿到置信度和类别。写库和查询映射放在同一个连接里减少连接开销。注意db.commit()必须在 INSERT 之后否则记录不会真正写入。4.3 前端 JavaScript 定时抓帧并调用接口const video document.getElementById(camera); const canvas document.createElement(canvas); const ctx canvas.getContext(2d); async function captureAndPredict() { canvas.width video.videoWidth; canvas.height video.videoHeight; ctx.drawImage(video, 0, 0); // 转成 blob按 multipart 发送 canvas.toBlob(async (blob) { const formData new FormData(); formData.append(frame, blob, frame.jpg); const resp await fetch(/predict, { method: POST, body: formData }); const data await resp.json(); document.getElementById(result).innerText ${data.label} (${(data.confidence * 100).toFixed(1)}%); }, image/jpeg, 0.8); } // 每 500ms 抓一帧间隔太短会堆积请求 setInterval(captureAndPredict, 500);参数说明toBlob的第三个参数 0.8 是 JPEG 质量太低会糊掉手部细节太高传输慢。setInterval间隔 500ms 是折中值低于 300ms 容易在前端堆积未完成的请求导致页面卡顿。如果识别延迟明显先把间隔调大再考虑优化模型推理速度。5. 避坑与排查那些让系统跑不起来的细节5.1 摄像头读到的第一帧总是黑的现象OpenCV 打开摄像头后立刻read()拿到的画面全黑或全绿。原因是摄像头硬件初始化需要时间第一帧往往还没准备好。解决办法是在循环开始前先空读几帧或者加 0.5 秒延时。cap cv2.VideoCapture(0) for _ in range(5): # 丢弃前 5 帧 cap.read()5.2 模型预测结果永远偏向同一个类别现象不管做什么手势输出都是同一个类别置信度还很高。原因通常是预处理和训练时不一致最常见的是 BGR/RGB 没转或者归一化系数用错。排查方法是拿一张训练集里的图跑推理如果训练图也预测错说明预处理有问题如果训练图对、摄像头图错说明摄像头画面和训练数据分布差异大。5.3 MySQL 写入中文变成问号现象label_text存进去是???。原因是数据库或表的字符集不是 utf8mb4。建库时就要指定后期改表只能改新数据。CREATE DATABASE sign_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接时也要加charsetutf8mb4否则 Python 侧编码和数据库侧对不上。5.4 前端请求跨域被浏览器拦截现象前端页面和 Flask 不在同一个端口fetch 报 CORS 错误。课设环境最简单的方法是让 Flask 同时托管前端静态文件或者装flask-cors加一行CORS(app)。不要用浏览器插件绕过演示环境换台机器就失效。5.5 连续识别时数据库连接数暴涨现象跑一段时间后 MySQL 报连接数超限。原因是每次请求都新建连接没有关闭或复用。解决办法是用连接池或者确保每个请求结束时cursor.close()和db.close()都执行。更稳妥的做法是用mysql.connector.pooling.MySQLConnectionPool初始化时建固定数量连接。6. 让识别更稳的几个进阶技巧6.1 用滑动窗口平滑连续帧的预测结果单帧识别容易受手部抖动影响出现“你好”“谢谢”“你好”这样跳变的结果。我一般会维护一个长度为 5 的队列取众数作为最终输出。这样即使某一帧预测错也不会立刻反映到页面上。from collections import deque, Counter pred_queue deque(maxlen5) def smooth_predict(class_index): pred_queue.append(class_index) most_common Counter(pred_queue).most_common(1)[0][0] return most_common参数说明maxlen5是经验值太小平滑效果不明显太大延迟高。如果手势切换频繁可以降到 3。6.2 置信度阈值过滤低质量帧不是每一帧都值得写库。手部区域太小、置信度低于阈值时直接跳过避免数据库里堆满垃圾记录。CONF_THRESHOLD 0.6 if confidence CONF_THRESHOLD: return jsonify({label: 未识别, confidence: confidence})阈值 0.6 是起点实际按模型在测试集上的表现调。如果模型整体置信度偏低先检查预处理而不是一味降阈值。6.3 用类别索引和文本分离的方式支持多语言如果后面要加英文或方言展示不需要重新训练模型只需要在sign_mapping表里加一列label_text_en前端根据语言参数取不同列。这样模型和展示解耦扩展成本低。字段类型说明class_indexINT模型输出索引主键label_textVARCHAR(64)中文展示文本label_text_enVARCHAR(64)英文展示文本可空6.4 模型文件加载失败时的排查顺序先看报错类型如果是FileNotFoundError检查路径和文件名大小写如果是pickle.UnpicklingError检查 torch 版本如果是RuntimeError: Error(s) in loading state_dict说明模型结构定义和保存时不一致需要找到训练时的模型类定义文件。我习惯在加载模型前打印一次torch.__version__和模型文件大小确认文件没被截断。6.5 演示前把摄像头分辨率和帧率固定下来不同机器上摄像头默认分辨率不一样导致预处理参数失效。演示前用cap.set固定分辨率和帧率减少环境差异。cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30)固定之后肤色阈值和形态学核大小都不用再改。这个习惯帮我省掉了很多次“在我机器上好好的”的尴尬。希望帮到你。本文还有配套的精品资源点击获取