资讯详情

资讯详情

基于Python+OpenCV的手语识别系统:从视频到数据库全链路实现

简介这是一套面向高校学生与开发者的手语识别系统完整项目源码基于Python、OpenCV、JavaScript与MySQL构建适合作为毕业设计或课程设计参考。系统通过摄像头实时捕获手势结合神经网络模型完成识别并转换为文字输出同时支持用户上传手势视频自定义训练模型覆盖数据预处理、模型训练、降噪优化到实时识别的完整流程。压缩包共85个文件约30.17MB包含11个Python脚本负责模型训练与后端服务、16个HTML页面搭建前端交互界面、4个JavaScript文件处理识别逻辑与上传功能另有1个pth模型文件可直接加载使用以及若干jpg、png图片与css样式资源。项目已吸引73人学习内含项目说明文档目录结构清晰便于快速理解系统架构与模块划分适合需要完整手语识别方案、模型文件及前后端代码的读者参考借鉴。1. 手语识别系统到底在识别什么从一段 3 秒视频到一条数据库记录很多人第一次听到「手语识别系统」脑子里浮现的是摄像头对着人屏幕上实时蹦出中文句子。真动手做才发现难点根本不在「识别」两个字而在把一段连续的手部动作切成机器能吃的样本再把它映射成一个稳定的标签。我做过一套基于 Python OpenCV JavaScript MySQL 的手语识别系统输入是摄像头或本地视频输出是识别结果落库、前端页面能查历史记录。它适合毕设课设也适合想跑通「视觉模型 后端 前端 数据库」全链路的人。这套系统真正解决的问题是把孤立词手语比如「你好」「谢谢」「帮助」这类单手势从视频流里检测出来、分类、存下来、展示出来。它不适合做连续手语句子翻译那是另一个量级的任务。下面我按「数据怎么来 → 模型怎么训 → 后端怎么接 → 前端怎么用 → 坑在哪」的顺序讲清楚每一步都给能直接抄的命令和参数。2. 数据采集与预处理为什么你的手语数据集总是不够用2.1 先定标签体系再开摄像头新手最容易翻车的地方是先拍了几百段视频回头发现标签乱成一锅粥。我一般会先定一个封闭词表比如 1020 个常用孤立词每个词对应一个英文标签避免中文路径在 OpenCV 里出玄学问题。目录结构固定成dataset/ ├── hello/ │ ├── 001.mp4 │ ├── 002.mp4 ├── thanks/ │ ├── 001.mp4 └── help/ ├── 001.mp4每个类别至少 30 段、每段 23 秒同一个人不同角度、不同光照各拍几段。别小看光照手语识别模型对背景亮度变化非常敏感这是血泪经验。2.2 用 OpenCV 抽帧并做手部区域裁剪视频不能直接喂给分类模型常见做法是抽帧 检测手部 裁剪成固定尺寸。下面这段脚本负责把 mp4 转成 224×224 的图片序列import cv2 import os import mediapipe as mp mp_hands mp.solutions.hands hands mp_hands.Hands( static_image_modeFalse, max_num_hands1, # 孤立词手语通常单手双手词可改 2 min_detection_confidence0.5, min_tracking_confidence0.5 ) def extract_frames(video_path, out_dir, step3, size224): os.makedirs(out_dir, exist_okTrue) cap cv2.VideoCapture(video_path) idx, saved 0, 0 while True: ret, frame cap.read() if not ret: break if idx % step 0: # 每 3 帧取 1 帧降冗余 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) res hands.process(rgb) if res.multi_hand_landmarks: h, w, _ frame.shape xs [lm.x for lm in res.multi_hand_landmarks[0].landmark] ys [lm.y for lm in res.multi_hand_landmarks[0].landmark] x1, x2 int(min(xs)*w), int(max(xs)*w) y1, y2 int(min(ys)*h), int(max(ys)*h) pad 30 # 外扩像素防止手势贴边被切 x1, y1 max(0, x1-pad), max(0, y1-pad) x2, y2 min(w, x2pad), min(h, y2pad) crop frame[y1:y2, x1:x2] if crop.size 0: idx 1 continue crop cv2.resize(crop, (size, size)) cv2.imwrite(os.path.join(out_dir, f{saved:04d}.jpg), crop) saved 1 idx 1 cap.release()逻辑说明step3是抽帧间隔视频 30fps 时相当于每秒留 10 帧既保留动作变化又不会让数据爆炸。max_num_hands1对应孤立词单手场景如果你做的是双手词改成 2 并分别裁剪。pad30是外扩防止手部关键点刚好在边缘导致裁剪区域过小。min_detection_confidence调到 0.5 是平衡漏检和误检光线差的环境可以降到 0.3但误检会变多。2.3 数据增强与划分别让模型记住背景抽完帧后按 8:1:1 划分训练/验证/测试。增强只对训练集做常见组合是随机旋转 ±15°、随机亮度 ±20%、随机水平翻转。注意水平翻转对手语要谨慎有些手势左右手含义不同翻完标签就错了。我一般只对明确对称的词做翻转。from torchvision import transforms train_tf transforms.Compose([ transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])Normalize用的 ImageNet 均值方差因为后面用迁移学习。如果你从零训可以换成自己数据集的统计值但迁移学习下这套参数最稳。3. 模型选型与训练MobileNetV3 为什么比 ResNet50 更适合课设3.1 选型理由精度够用、推理快、显存友好手语孤立词分类本质是图像分类。ResNet50 精度高但参数量 25M课设机器甚至笔记本 CPU推理一帧要几百毫秒前端体验直接崩。MobileNetV3-Small 参数量约 2.5MCPU 单帧 2040ms精度在 1020 类手语上通常只比 ResNet50 低 13 个百分点。我一般选 MobileNetV3-Small 做基线如果精度不够再换 MobileNetV3-Large 或 EfficientNet-B0。模型参数量CPU 单帧推理适合场景MobileNetV3-Small~2.5M2040ms课设、实时前端MobileNetV3-Large~5.4M5080ms精度优先ResNet50~25M200ms服务器端离线3.2 训练脚本与关键参数import torch import torch.nn as nn from torchvision import models, datasets, transforms from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model models.mobilenet_v3_small(weightsmodels.MobileNet_V3_Small_Weights.DEFAULT) model.classifier[3] nn.Linear(model.classifier[3].in_features, 10) # 10 类 model model.to(device) train_ds datasets.ImageFolder(dataset/train, transformtrain_tf) val_ds datasets.ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) criterion nn.CrossEntropyLoss(label_smoothing0.1) # 缓解过拟合 optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) for epoch in range(20): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在 val_loader 上算准确率保存最优权重 torch.save(model.state_dict(), best_handsign.pth)参数说明batch_size32在 8G 显存下安全显存小改 16。lr3e-4是迁移学习常用起点太大容易把预训练权重冲烂。label_smoothing0.1对小手语数据集很关键能明显压住过拟合。T_max20和总 epoch 对齐余弦退火让后期学习率平滑降到接近 0。3.3 训练过程要看什么指标别只盯准确率。手语数据集类别不平衡时准确率会骗人。我一般同时看混淆矩阵和每类 F1。如果某一类总是被误判成另一类大概率是这两个手势本身太像或者采集时背景没换。解决办法补采这两类的困难样本或者把这两个词合并成一个粗标签。4. 后端服务与数据库Flask MySQL 怎么接住模型输出4.1 接口设计一个上传接口 一个查询接口后端用 Flask 起两个核心接口/api/recognize接收图片或视频帧返回识别标签和置信度/api/history返回历史记录。模型在服务启动时加载一次不要每次请求都 load否则延迟爆炸。from flask import Flask, request, jsonify import torch, io from PIL import Image import pymysql app Flask(__name__) model models.mobilenet_v3_small() model.classifier[3] nn.Linear(model.classifier[3].in_features, 10) model.load_state_dict(torch.load(best_handsign.pth, map_locationcpu)) model.eval() LABELS [hello, thanks, help, yes, no, please, sorry, good, bad, ok] def db(): return pymysql.connect(hostlocalhost, userroot, passwordyour_pwd, databasesign_db, charsetutf8mb4) app.route(/api/recognize, methods[POST]) def recognize(): file request.files[frame] img Image.open(io.BytesIO(file.read())).convert(RGB) x val_tf(img).unsqueeze(0) with torch.no_grad(): prob torch.softmax(model(x), dim1)[0] idx int(prob.argmax()) label, conf LABELS[idx], float(prob[idx]) conn db() with conn.cursor() as cur: cur.execute( INSERT INTO records(label, confidence, created_at) VALUES (%s, %s, NOW()), (label, conf)) conn.commit(); conn.close() return jsonify({label: label, confidence: round(conf, 4)})逻辑说明model.eval()必须加否则 BatchNorm 和 Dropout 会按训练模式跑结果随机。torch.no_grad()省显存和计算。置信度低于 0.6 时我一般返回「不确定」而不是硬报一个标签前端体验会好很多。4.2 建表语句与索引CREATE TABLE records ( id INT AUTO_INCREMENT PRIMARY KEY, label VARCHAR(32) NOT NULL, confidence FLOAT NOT NULL, created_at DATETIME NOT NULL, INDEX idx_created (created_at), INDEX idx_label (label) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;idx_created让前端按时间倒序查历史时不用全表扫。idx_label用于按标签筛选。数据量上万后这两个索引差别很明显。4.3 前端 JavaScript 调用与摄像头采集前端用getUserMedia抓帧定时 POST 到后端。注意别每帧都发23 秒发一次就够否则后端压力大且识别结果抖动。const video document.getElementById(cam); navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }) .then(stream { video.srcObject stream; video.play(); }); setInterval(async () { const canvas document.createElement(canvas); canvas.width 224; canvas.height 224; canvas.getContext(2d).drawImage(video, 0, 0, 224, 224); canvas.toBlob(async blob { const fd new FormData(); fd.append(frame, blob, frame.jpg); const res await fetch(/api/recognize, { method: POST, body: fd }); const data await res.json(); document.getElementById(result).innerText ${data.label} (${data.confidence}); }, image/jpeg, 0.8); }, 2000);toBlob的 0.8 是 JPEG 质量太高传输慢太低模型看不清。setInterval2000ms 是识别节奏太快结果会跳。5. 避坑与排查这 5 个问题几乎每个人都会遇到5.1 现象训练准确率 99%一开摄像头就乱报原因数据集背景太单一模型学到了背景而不是手势。解决采集时换 3 种以上背景训练时加随机裁剪和颜色抖动验证集必须来自不同背景。5.2 现象OpenCV 读中文路径视频返回 None原因cv2.VideoCapture在部分平台对非 ASCII 路径支持差。解决路径全用英文或者用cv2.imdecode配合np.fromfile读。5.3 现象Flask 接口第一次请求特别慢后面正常原因模型懒加载或每次请求重新 load。解决在app.run之前全局加载模型用model.eval()固定推理模式。5.4 现象MySQL 插入中文标签变成问号原因连接字符集不是 utf8mb4。解决pymysql.connect加charsetutf8mb4建表也用 utf8mb4。5.5 现象前端识别结果疯狂闪烁原因每帧都发请求模型对相似帧输出不稳定。解决加滑动窗口投票连续 3 次结果一致才更新 UI或者降低发送频率到 2 秒一次。6. 进阶技巧把孤立词串成短句的轻量做法孤立词识别跑通后很多人会想再往前走一步能不能识别「你好 谢谢」这种两词短句完整连续手语翻译需要 CTC 或 Transformer 序列模型课设周期内不现实。我一般用一个轻量状态机做过渡维护一个长度为 5 的结果队列当检测到某个标签连续出现 3 次且与上一个稳定标签不同就把它追加到句子缓冲区超过 5 秒没有新词就截断成一句。from collections import deque, Counter class SentenceBuilder: def __init__(self, window5, stable3, timeout5.0): self.buf deque(maxlenwindow) self.stable stable self.timeout timeout self.last_label None self.last_time 0 self.sentence [] def push(self, label, conf, now): if conf 0.6: return self.buf.append(label) if len(self.buf) self.stable: return top, cnt Counter(self.buf).most_common(1)[0] if cnt self.stable and top ! self.last_label: if now - self.last_time self.timeout: self.sentence [] # 超时开新句 self.sentence.append(top) self.last_label top self.last_time nowwindow5是投票窗口stable3是判定稳定所需次数timeout5.0是句子截断秒数。这套逻辑不完美但比直接拼接稳定得多。验证方法是录 20 段两词组合视频看句子缓冲区输出和人工标注的一致率一般能到 70% 以上就算可用。我自己的习惯是任何识别类项目先把「单帧准确率」和「连续输出稳定性」分开测前者看模型后者看后处理。很多人模型训得不错却因为没做投票和超时演示时句子乱跳最后被当成模型不行。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →