news 2026/9/20 20:40:27

基于深度学习与摄像头的坐姿检测系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于深度学习与摄像头的坐姿检测系统设计与实现

简介:一份基于深度学习的智能坐姿检测系统完整项目,适合课程设计、期末大作业或毕业设计场景。项目通过摄像头或图像输入,利用姿态估计与分类模型实时判断人体坐姿是否规范,可扩展至学习提醒、健康监测等应用。压缩包内共15个文件,主要包括Python源码、训练数据、模型权重和音频文件:其中多个py文件分别负责数据预处理、模型训练、姿态检测主流程和简单演示界面,“.pth”为预训练模型,data目录存放标注数据,mp3可用于不良坐姿语音提醒。整体资源包仅48KB,结构紧凑,便于快速部署与二次开发。当前已有1059人学习浏览。项目经严格调试,下载后按说明配置环境即可直接运行,尤其适合需要快速搭建完整方案、深入理解深度学习落地的初学者或毕业生参考。

1. 为什么坐姿检测要选深度学习加摄像头

以前做坐姿提醒,直觉反应是上硬件:压力坐垫、毫米波雷达、带传感器的椅子,一套做下来成本几百,还要考虑供电和蓝牙连接。换个思路,用普通 USB 摄像头加深度学习模型,把画面里的人对应到“坐姿好不好”这个判断上,整套代码加数据集、训练好的权重一起跑通,成本就剩一台电脑。这个项目就是这种实现:PyTorch 训练出网络权重 net.pth,Core 目录下的 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, transform=None): 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, dtype=torch.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(brightness=0.2, contrast=0.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 是约定输入尺寸;如果你换成 EfficientNet,224 也兼容。归一化的 mean/std 用的是 ImageNet 统计值,因为加载预训练权重时模型假设输入服从这个分布。preprocess_frameunsqueeze(0)补了 batch 维,推理时不用再手动 reshape。

2.4 数据增强怎么选和样本均衡怎么处理

坐姿检测的增强策略跟通用分类不完全一样。垂直翻转不能用,上下翻转后“人坐在椅子上”的语义直接崩掉;随机裁剪也不宜太狠,裁剪过头会把头部和肩部裁掉,模型学的特征是残肢而不是坐姿。表格里是我常用的一组配置:

增强手段参数建议理由
RandomHorizontalFlipp=0.5左右倾斜天然对称,能翻倍利用样本
ColorJitterbrightness=0.2, contrast=0.2模拟不同光照环境
RandomRotation5 度以内摄像头角度轻微抖动
RandomErasingp=0.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,可以把pretrained=True改成False,或者换 MobileNetV3 这种轻量结构,代价是收敛速度变慢。素材里 Model 目录只有一个 net.pth,说明训练脚本把state_dict存成了单文件,加载时得配合模型结构定义才能用,这一点在第 4 章会展开说。

如果以后想从“分类整张图”升级到“框出人体再判断”,可以迁移到目标检测模型。参考 yolov8 训练自己数据集的流程,把标注从目录分类改成检测框格式,本质上就是把任务从图像分类换成目标分类,但训练脚本、推理链路都要重写,不建议毕设初期就上检测方案。

3.2 超参数表与典型取值

超参数典型值调整方向
batch_size32显存不够就 16 或 8;过小会导致 BN 统计不稳定
learning_rate1e-3换大模型降到 1e-4;预训练骨干通常用更小 lr
weight_decay1e-4数据量小时加大到 1e-3 抑制过拟合
epochs40~60看验证集 acc 是否连续 10 轮不涨
lr_schedulerStepLR step=15 gamma=0.5也可以用 CosineAnnealing
lossCrossEntropyLoss类别不均衡时加 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(pretrained=True) model.fc = nn.Linear(model.fc.in_features, num_classes) model = model.to(device) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=15, gamma=0.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(f"epoch={epoch+1} 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,建议存成 dict:

torch.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分类 logits
process.py图像预处理BGR 帧归一化后的 tensor
pose.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 再送模型,或者强制用 CPU
KeyErrorunexpected key加载 net.pth 报错模型结构和保存权重时不一致确认 module.py 里 backbone 是 resnet18,且fc输出维度跟训练时一致
can't open camera by index摄像头被其他程序占用VideoCapture(0)改成 1 或 2,先杀掉占用摄像头的软件
推理结果明显不对预处理和训练时不一致检查是否用了build_infer_transform(),重点看 Normalize 参数
中文路径报 UnicodeDecodeErrorWindows 下 Python 默认编码问题图片和数据集路径全部改英文,这是最省事的一种

5.3 把双阈值和告警渠道改成可配置

坐姿检测这类项目,交付后用户一定会有“提醒太频繁”“我在喝水也算前倾”这类反馈。改代码不是解决办法,把阈值和提醒间隔全部搬进一个 JSON 配置文件,才算真正收尾。下面是可以直接用的结构:

import json with open("config.json", "r", encoding="utf-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 通知函数,把“什么时候检测到异常坐姿”推送到企业微信或钉钉机器人,就具备了给多台办公电脑做统一久坐监控的雏形。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 20:39:52

uniapp+Java多端淘宝客源码拆解:架构、部署与避坑指南

简介&#xff1a;面向电商导购与CPS推广场景的“省钱兄淘宝客”多端项目是一套完整的源码包&#xff0c;适合需要快速搭建返利/优惠券平台的开发者&#xff0c;也适合 Java 后端与 uniapp 前端学习者参考。资源内整合 APP 端、小程序、公众号及 H5 页面&#xff0c;对应 uniapp…

作者头像 李华
网站建设 2026/9/20 20:37:07

AI辅助红队评估:用Claude构建结构化安全技能库的实践指南

这两年安全圈里聊得最多的话题&#xff0c;大概就是“AI 到底能不能替代渗透测试工程师”。我的观点一直很明确&#xff1a;短期内不能完全替代&#xff0c;但 AI 绝对能在红队评估里把那些最磨人、最耗时的脏活累活接过去。前段时间我花了大量时间折腾 claude-red 这个思路——…

作者头像 李华
网站建设 2026/9/20 20:36:47

Python函数与模块化开发核心技术与实践

1. 为什么函数与模块是Python开发的基石刚接触Python时&#xff0c;我们往往习惯把所有代码写在一个文件里。但随着项目规模扩大&#xff0c;这种写法很快会变成难以维护的"面条代码"。三年前我接手过一个遗留项目&#xff0c;8000多行代码挤在单个.py文件里&#xf…

作者头像 李华
网站建设 2026/9/20 20:33:47

5 分钟上手 QuickRecorder:不到 10MB 的免费 macOS 录屏工具

5 分钟上手 QuickRecorder&#xff1a;不到 10MB 的免费 macOS 录屏工具 【免费下载链接】QuickRecorder A lightweight screen recorder based on ScreenCapture Kit for macOS / 基于 ScreenCapture Kit 的轻量化多功能 macOS 录屏工具 项目地址: https://gitcode.com/GitH…

作者头像 李华