基于深度学习与摄像头的坐姿检测系统设计与实现
简介一份基于深度学习的智能坐姿检测系统完整项目适合课程设计、期末大作业或毕业设计场景。项目通过摄像头或图像输入利用姿态估计与分类模型实时判断人体坐姿是否规范可扩展至学习提醒、健康监测等应用。压缩包内共15个文件主要包括Python源码、训练数据、模型权重和音频文件其中多个py文件分别负责数据预处理、模型训练、姿态检测主流程和简单演示界面“.pth”为预训练模型data目录存放标注数据mp3可用于不良坐姿语音提醒。整体资源包仅48KB结构紧凑便于快速部署与二次开发。当前已有1059人学习浏览。项目经严格调试下载后按说明配置环境即可直接运行尤其适合需要快速搭建完整方案、深入理解深度学习落地的初学者或毕业生参考。1. 为什么坐姿检测要选深度学习加摄像头以前做坐姿提醒直觉反应是上硬件压力坐垫、毫米波雷达、带传感器的椅子一套做下来成本几百还要考虑供电和蓝牙连接。换个思路用普通 USB 摄像头加深度学习模型把画面里的人对应到“坐姿好不好”这个判断上整套代码加数据集、训练好的权重一起跑通成本就剩一台电脑。这个项目就是这种实现PyTorch 训练出网络权重 net.pthCore 目录下的 dataSet.py、process.py、pose.py 等模块完成读帧、预处理、推理、音频提醒的完整闭环附训练集数据目录。适合三类人做毕业设计需要完整落地链路的学生想在公司内部做久坐提醒原型的工程师以及研究姿态估计想找可对照基线的人。2. 训练数据怎么组织dataSet.py 与 process.py 的前置工作2.1 Data/Train 的目录结构与标签方式解压后 Data/Train 目录下是按类别划分的子文件夹每个子文件夹名就是标签里面放对应坐姿类别的图片。这种组织方式在图像分类项目里最常见PyTorch 的torchvision.datasets.ImageFolder可以直接读但项目里自己写了 dataSet.py说明训练时还做了定制逻辑比如按 8:2 切训练集和验证集或者对样本做加权采样。标签建议控制在 3 到 5 类good、forward、lean_left、lean_right、slouch。类别太少模型学不到区分度类别太多标注成本和误判率都会上升。我在类似项目里见过有人把类别拆成 8 类训练集只有几百张结果验证集准确率上不去这种问题多半出在类别粒度而不是模型结构上。dataSet_test.py对应测试集评估入口如果测试图片是单张拷进来的路径写错会报 FileNotFoundError注意看异常信息里是文件名还是目录名。2.2 dataSet.py 里自定义 Dataset 的关键实现dataSet.py 的核心是继承torch.utils.data.Dataset重写__len__和__getitem__。常见做法是扫描目录把每个样本的路径和整数标签存成列表__getitem__里用 Pillow 或 OpenCV 读图再套 transform。参考骨架如下import os import torch from torch.utils.data import Dataset from PIL import Image class PostureDataset(Dataset): def __init__(self, root, label_map, transformNone): self.samples [] self.transform transform for label_name in os.listdir(root): label_dir os.path.join(root, label_name) if not os.path.isdir(label_dir): continue label label_map[label_name] for img_name in os.listdir(label_dir): img_path os.path.join(label_dir, img_name) self.samples.append((img_path, label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] img Image.open(img_path).convert(RGB) if self.transform: img self.transform(img) return img, torch.tensor(label, dtypetorch.long)代码逻辑__init__里把目录名映射成整数标签__getitem__返回的是(tensor图片, tensor标签)而不是(路径, 标签)这样 DataLoader 才能直接 batching。label_map 建议写成{good: 0, forward: 1, ...}而不是用os.listdir的顺序自动编号原因在于推理端比如 simple_demo.py也要用同一份映射顺序一致才不会出现“训练准确率 95%一跑摄像头全乱判”的情况。提示如果图片命名带中文或空格Windows 下用Image.open可能报 UnicodeDecodeError。统一改成img_0001.jpg这类命名能省掉很多麻烦。2.3 process.py 预处理流水线与参数process.py 在训练和推理两条链路里都要用到。训练链路里它是 transform 工厂推理链路里它把摄像头帧转成模型输入。两处共用一份预处理逻辑避免出现 train loss 很低、摄像头推理却一塌糊涂的经典坑。import cv2 import torch from torchvision import transforms IMG_SIZE 224 NORMALIZE_MEAN [0.485, 0.456, 0.406] NORMALIZE_STD [0.229, 0.224, 0.225] def build_train_transform(): return transforms.Compose([ transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(NORMALIZE_MEAN, NORMALIZE_STD), ]) def build_infer_transform(): return transforms.Compose([ transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.ToTensor(), transforms.Normalize(NORMALIZE_MEAN, NORMALIZE_STD), ]) def preprocess_frame(frame_bgr): rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) # numpy(H,W,C) 转 PIL 再走统一 transform pil_img Image.fromarray(rgb) tensor build_infer_transform()(pil_img) return tensor.unsqueeze(0) # (1,3,224,224)参数说明IMG_SIZE取 224是因为 backbone 用 ResNet 系列时最后是 7x7 或 1x1 池化224 是约定输入尺寸如果你换成 EfficientNet224 也兼容。归一化的 mean/std 用的是 ImageNet 统计值因为加载预训练权重时模型假设输入服从这个分布。preprocess_frame里unsqueeze(0)补了 batch 维推理时不用再手动 reshape。2.4 数据增强怎么选和样本均衡怎么处理坐姿检测的增强策略跟通用分类不完全一样。垂直翻转不能用上下翻转后“人坐在椅子上”的语义直接崩掉随机裁剪也不宜太狠裁剪过头会把头部和肩部裁掉模型学的特征是残肢而不是坐姿。表格里是我常用的一组配置增强手段参数建议理由RandomHorizontalFlipp0.5左右倾斜天然对称能翻倍利用样本ColorJitterbrightness0.2, contrast0.2模拟不同光照环境RandomRotation5 度以内摄像头角度轻微抖动RandomErasingp0.2模拟遮挡提升鲁棒性样本不均衡在坐姿项目里很常见good 类好拍forward 和 slouch 类样本少。处理办法是给 DataLoader 传WeightedRandomSampler权重按类别样本数的倒数算。我一般会把 logits 输出维度跟len(label_map)保持一致然后在代码里打印每个 epoch 的类别准确率而不是只看整体 acc这样能发现“good 类 98%forward 类 40%”的偏科问题。3. 训练脚本与权重产出train.py 的超参数设计和 net.pth 生成3.1 模型选型ResNet 还是轻量 CNN训练阶段用 torchvision 提供的预训练 ResNet18 是比较稳妥的起步方案。坐姿图像和 ImageNet 的分布差距不算大预训练权重能显著加快收敛几十个 epoch 就能达到可用水平。如果部署机器没有 GPU可以把pretrainedTrue改成False或者换 MobileNetV3 这种轻量结构代价是收敛速度变慢。素材里 Model 目录只有一个 net.pth说明训练脚本把state_dict存成了单文件加载时得配合模型结构定义才能用这一点在第 4 章会展开说。如果以后想从“分类整张图”升级到“框出人体再判断”可以迁移到目标检测模型。参考 yolov8 训练自己数据集的流程把标注从目录分类改成检测框格式本质上就是把任务从图像分类换成目标分类但训练脚本、推理链路都要重写不建议毕设初期就上检测方案。3.2 超参数表与典型取值超参数典型值调整方向batch_size32显存不够就 16 或 8过小会导致 BN 统计不稳定learning_rate1e-3换大模型降到 1e-4预训练骨干通常用更小 lrweight_decay1e-4数据量小时加大到 1e-3 抑制过拟合epochs4060看验证集 acc 是否连续 10 轮不涨lr_schedulerStepLR step15 gamma0.5也可以用 CosineAnnealinglossCrossEntropyLoss类别不均衡时加 class_weight这些参数在 train.py 里一般是集中定义在文件顶部或一个 config dict 里。做课设时不要在代码里散落魔法数字验收答辩时被问到“这个 15 是什么意思”会很狼狈统一收口在超参数区是基本习惯。3.3 train.py 训练骨架下面是一段精简过的训练主循环保留关键逻辑import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) num_classes 5 model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size15, gamma0.5) for epoch in range(50): model.train() running_loss 0.0 for images, labels in train_loader: images images.to(device) labels labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) scheduler.step() model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images images.to(device) labels labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fepoch{epoch1} loss{running_loss/len(train_loader.dataset):.4f} val_acc{val_acc:.4f}) # 保存 state_dict文件名对应 Model/net.pth torch.save(model.state_dict(), Model/net.pth)逻辑说明model.fc被替换成输出num_classes个类别的全连接层这是迁移学习最常用的改法optimizer.zero_grad()每组 batch 清一次梯度loss.backward()反向传播optimizer.step()更新权重这个三步顺序不能乱。model.eval()和torch.no_grad()在验证时必须成对出现否则 BN 和 Dropout 会沿用训练行为val_acc 会虚高。保存权重用state_dict()而不是整个 model文件更小加载时也更灵活。3.4 训练监控与 net.pth 保存的坑训练日志里的 loss 下降速度和 val_acc 曲线要一起看。如果 train loss 持续下降、val_acc 徘徊不动优先怀疑过拟合可以加数据增强强度或增大 weight_decay如果 val_acc 最近几个 epoch 一直在震荡先观察到尾不要急着调参。项目里 dataSet_test.py 对应测试集评估训练完在这个脚本上跑一遍再发布权重我一般会确认测试集 acc 不低于验证集 acc 的 3% 以内差距大了说明某个子集有数据泄漏或者分布不一致。保存 net.pth 时有个细节如果后面想把模型换回完整 checkpoint建议存成 dicttorch.save({ arch: resnet18, state_dict: model.state_dict(), label_map: label_map, }, Model/net.pth)这样加载时可以先用arch重建结构再载入权重。如果直接保存state_dict加载的人必须知道模型的类名和fc替换方式否则报unexpected key(s)是必然的。素材里的 net.pth 我估计走的是第一种省事路线所以 main.py 里module.py一定写死了 resnet18 和fc输出维度改网络结构时记得同步改 module.py。4. 从摄像头到声音提醒main.py、pose.py 与 Audio 的完整链路4.1 各模块职责边界模块职责输入输出module.py定义模型结构并加载 net.pth 权重图片 tensor分类 logitsprocess.py图像预处理BGR 帧归一化后的 tensorpose.py做坐姿几何判断模型输出的分类或关键点坐姿标签view.py可视化与画面标注原始帧 判断结果带标注的帧main.py串联整个流程摄像头/图片无或窗口画面这个分层的好处是每一层都能单独替换。比如把 module.py 里的 ResNet18 换成 MobileNetV3只要输入输出 shape 不变其他模块不用动pose.py 里如果发现角度阈值不准也只改一个文件。4.2 主循环数据流与代码main.py 的主循环典型实现如下里面关键的是用cv2.VideoCapture拿摄像头帧经过预处理和 forward 后交给 pose.py 判读import cv2 import torch import time from Core.module import load_model from Core.process import preprocess_frame from Core.pose import judge_posture from Core.view import draw_result from Core.audio import play_alert model load_model(Model/net.pth) model.eval() cap cv2.VideoCapture(0) # 0 表示默认摄像头 bad_frames 0 last_alert_time 0.0 ALERT_COOLDOWN 30 # 秒 while True: ret, frame cap.read() if not ret: break input_tensor preprocess_frame(frame).cuda() # 无 GPU 时去掉 .cuda() with torch.no_grad(): logits model(input_tensor) posture judge_posture(logits) # 返回 good / forward / lean_left / ... # 连续帧判断 冷却时间避免频繁播报 if posture ! good: bad_frames 1 else: bad_frames 0 if bad_frames 15 and (time.time() - last_alert_time) ALERT_COOLDOWN: play_alert(Audio/audio.mp3) last_alert_time time.time() show_frame draw_result(frame, posture) cv2.imshow(Posture Monitor, show_frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明bad_frames起到时序平滑作用防止单帧误检导致播报乱响。ALERT_COOLDOWN设定两次提醒的最小间隔这个参数在真实办公场景中很关键否则一整天下来的提醒次数会让人直接把程序关掉。4.3 pose.py 的坐姿几何判断是怎么回事如果模型输出的是类别pose.py 直接查表映射就行如果模型输出的是人体关键点坐标pose.py 就要算角度。项目里单独把 pose.py 拎出来说明它至少承担了“从模型输出到可解释坐姿标签”的转换。常见做法是用左右肩和头部三个关键点做几何判断import math def angle_between_points(a, b): 返回 ab 连线与水平线的夹角单位度 dx b[0] - a[0] dy b[1] - a[1] if dx 0: return 90.0 return math.degrees(math.atan2(abs(dy), abs(dx))) def judge_posture_from_keypoints(left_shoulder, right_shoulder, nose): shoulder_angle angle_between_points(left_shoulder, right_shoulder) # 肩线倾角大于阈值则认为左右歪斜 if shoulder_angle 15: if left_shoulder[1] right_shoulder[1]: return lean_left return lean_right # 鼻子在左右肩连线垂直投影上的偏移比例 shoulder_width abs(right_shoulder[0] - left_shoulder[0]) 1e-6 offset_ratio abs(nose[0] - (left_shoulder[0] right_shoulder[0]) / 2) / shoulder_width if offset_ratio 0.25: return forward_or_slouch return good参数说明15 度和 0.25 不是拍脑袋定的我一般会先采集用户保持正确坐姿 10 秒把肩线倾角和偏移比做平均分别得到正常值再以正常值加减两倍标准差作为阈值写入配置文件。不同身高、不同摄像头安装高度的用户这套几何参数差异非常大固定阈值只能保证 demo 可用距离产品化还差一个标定环节。4.4 Audio 音频模块的触发与防打扰audio.mp3 的播放如果自己用 winsound 实现只能在 Windows 跑跨平台用 pygame 或 pyaudio 更稳。pygame 的 mixer 初始化开销不小不要在每次提醒时都重新 init应该在 main.py 启动时初始化一次。import pygame _pygame_inited False def play_alert(audio_path): global _pygame_inited if not _pygame_inited: pygame.mixer.init() _pygame_inited True pygame.mixer.music.load(audio_path) pygame.mixer.music.play()提示pygame 播放完立刻 load 同一文件没问题但如果在音乐还没播完时再次调用会直接切断前一次播放。配合 main.py 里的bad_frames 15和 30 秒冷却这个情况基本不会出现。无头服务器上没有音频设备pygame.mixer.init()会抛异常建议用 try/except 包一层失败时降级成打印日志。5. simple_demo.py 快速验证与排错技巧5.1 simple_demo.py 在部署链路里的位置simple_demo.py 的作用是脱离摄像头和 GUI 跑推理。它的输入可以是一张图片或一段视频文件运行完直接退出不进入主循环这样在做环境验证时非常方便。推荐调用方式是命令带参数而不是改代码python simple_demo.py --input test.jpg python simple_demo.py --input bad_case.mp4在服务器这种无桌面环境里main.py 的cv2.imshow会直接报错simple_demo.py 反而是主力验证入口我一般先跑通它再回去查摄像头问题。它也承担回归测试的角色把之前误判的图片攒成一个目录每次改完模型或改完阈值全量跑一遍数一下误判数有没有变多。5.2 最常见的五个运行问题定位现象定位方向处理方式RuntimeError: CUDA out of memory视频帧分辨率过高或 batch 设置过大把帧 resize 到 640 再送模型或者强制用 CPUKeyError或unexpected key加载 net.pth 报错模型结构和保存权重时不一致确认 module.py 里 backbone 是 resnet18且fc输出维度跟训练时一致cant open camera by index摄像头被其他程序占用把VideoCapture(0)改成 1 或 2先杀掉占用摄像头的软件推理结果明显不对预处理和训练时不一致检查是否用了build_infer_transform()重点看 Normalize 参数中文路径报 UnicodeDecodeErrorWindows 下 Python 默认编码问题图片和数据集路径全部改英文这是最省事的一种5.3 把双阈值和告警渠道改成可配置坐姿检测这类项目交付后用户一定会有“提醒太频繁”“我在喝水也算前倾”这类反馈。改代码不是解决办法把阈值和提醒间隔全部搬进一个 JSON 配置文件才算真正收尾。下面是可以直接用的结构import json with open(config.json, r, encodingutf-8) as f: cfg json.load(f) THRESHOLD_ANGLE cfg[pose][shoulder_angle_deg] THRESHOLD_OFFSET cfg[pose][head_offset_ratio] ALERT_COOLDOWN cfg[alert][cooldown_sec]对应 config.json 里是{pose: {shoulder_angle_deg: 15, head_offset_ratio: 0.25}, alert: {cooldown_sec: 30}}。改动阈值后重启进程即可不用动一行 Python 代码。在此基础上再做一步play_alert里除了播 audio.mp3同时调用一个 webhook 通知函数把“什么时候检测到异常坐姿”推送到企业微信或钉钉机器人就具备了给多台办公电脑做统一久坐监控的雏形。本文还有配套的精品资源点击获取