首页 / 资讯中心 / 文章详情

基于YOLOv11与Django的课堂行为识别系统设计与实现

基于YOLOv11与Django的课堂行为识别系统设计与实现 ★ FEATURED ARTICLE
我最初拿到这个题目时脑子里第一反应是教育场景里的行为分析听着像安防监控的副产品实际上要落地的坑比普通目标检测多得多。学生行为不像交通流量那样尺度固定、形态单一课堂上有遮挡、有密集座位排列、有大幅度姿态变化还有隐私合规要求这决定了你不能直接拿一个预训练权重就跑生产。我做的这套系统基于YOLOv11配合Python和Django搭Web服务覆盖图片检测、视频文件检测、摄像头实时检测三种输入方式配套完整技术文档算是把课堂行为识别从模型训练到Web部署打通了。这篇总结就把我的设计思路、训练细节、集成方案和踩坑记录完整写出来希望能帮到正在做同类项目的开发者。这个项目本质上解决的是“课堂中自动识别学生外观行为”的问题典型行为类别包括举手、阅读、写字、趴桌、玩手机、起立、行走、交头接耳等。传统做法是人工巡课或者看监控回放效率低且无法实时干预。用深度学习模型做自动识别再通过Web系统提供检测服务是当前智慧校园落地比较成熟的技术路径。适合的读者包括做教育信息化项目的开发者、高校里做计算机视觉研究的同学、以及想将YOLO模型集成到Web应用中的后端工程师。读完你可以得到一套可以复用的架构参考以及我在训练和部署过程中总结的实操参数。1. 系统整体设计与技术选型思路1.1 为什么选YOLOv11而不是其他目标检测模型先说明一点我接手项目时也对比过YOLOv5、YOLOv8和YOLOv11。YOLOv5社区生态好资料多但模型结构相对老对密集小目标的特征提取能力在课堂场景下有点吃力。YOLOv8引入了C2f结构和无锚框检测头训练收敛更快但v8在部署时的灵活性一般特别是需要自定义检测头做行为分类时代码侵入性较大。YOLOv11在v8基础上进一步改进了Backbone的特征融合方式增加了一些可训练的注意力模块同时保持了单阶段检测的推理速度在实际测试中对课堂里遮挡严重的“趴桌”行为平均精度比v8高2到3个百分点推理速度在同样GPU上几乎持平。图像、视频、摄像头实时检测三种模式要同时支持这要求模型不能太大TensorRT加速后需要稳定跑在30FPS以上否则实时监控就是空话。YOLOv11提供了n、s、m、l、x五个尺寸我最后选用的是YOLOv11n和YOLOv11s两个版本边缘设备或低配服务器用n测试环境用s。如果你只做离线图片分析可以用m或l提升精度但实时流建议别超过s否则延迟很难看。1.2 Web框架选型为什么用Django模型是核心但用户不可能拿着命令行去跑检测必须有一个Web界面。选Django而不是Flask或FastAPI主要三个原因第一Django自带Admin后台和ORM检测记录、学生信息、课程表这些结构化数据的管理成本极低第二Django的项目结构适合这种“管理后台检测展示”的业务场景模板系统能快速做出上传页面和结果展示页面第三Django的中间件和CSRF防护相对完善做教育类系统要对外演示时安全性观感更好。当然Django也有短板它的WSGI模型是同步阻塞的直接放模型推理会导致并发能力很差。我的方案是Django只负责接收请求、管理任务、展示结果实际推理交给独立的模型服务进程通过Redis任务队列异步通信。这个架构在后面第3章详细讲。1.3 系统总架构模型、后端、前端与数据流整个系统按模块拆成四层层级技术组件职责表现层Django模板 JavaScript Video.js上传图片/视频、展示检测结果、实时画面监控应用层Django View Django REST Framework请求路由、权限控制、任务调度、结果查询模型服务层YOLOv11 PyTorch ONNX Runtime / TensorRT图像预处理、推理、后处理、行为标签生成数据层PostgreSQL Redis检测记录持久化、任务队列、缓存热数据数据流分两条链路。离线检测链路用户在Web上传图片或视频Django生成任务ID把文件路径写入Redis队列模型服务进程从队列消费检测完成后把结果写入数据库前端轮询任务状态并展示。实时检测链路摄像头RTSP流推送到系统模型服务按帧抽帧检测检测结果通过WebSocket推给浏览器同时按时间窗口聚合行为统计。这种分层最大的好处是模型推理和Web应用解耦模型升级、换权重、加行为类别都不需要重启Django服务。我一开始图省事把模型直接加载在Django进程里并发一上来CPU直接打满后来拆开才稳定下来。2. 数据准备与模型训练实操2.1 行为类别定义与标注策略数据决定模型上限。我定义的行为类别经过了三轮调整最初模仿通用行为识别论文列了12类比如“举手”“低头”“抬头”“写字”“阅读”“站立”“走动”“玩手机”“趴桌子”“交流”“遮挡”“其他”训练后发现“低头”和“写字”高度重叠标注人员也很难判断模型学得一团糟。最后压缩成7类举手、阅读、写字、玩手机、趴桌、站立、行走。这7类在课堂场景中外观可区分度足够高也方便后续做教学干预。标注策略上重点提一点不要只看在线公开数据集。课堂场景的数据非常特殊摄像头通常安装在教室前方或角落学生是后排排列的天然存在大量小目标和遮挡。我采集了三个教室、不同时段、不同光线条件下的视频按每5秒抽一帧标注总计2.1万张图像。标注工具用LabelImg输出YOLO格式的txt文件。每张图的标注原则是目标被遮挡超过50%就不标避免引入噪声同一行为只标注可见人体边界框不标头部和手部细节。这样做是为了让模型专注学习“人行为”的整体模式而不是被局部干扰。2.2 YOLOv11训练的初始化与超参数配置YOLOv11的官方仓库提供了非常完整的训练入口我用的是Ultralytics封装。关键超参数如下# dataset.yaml path: ./datasets/classroom train: images/train val: images/val names: 0: raise_hand 1: reading 2: writing 3: phone 4: lying 5: standing 6: walking训练命令yolo detect train dataclassroom.yaml modelyolo11s.pt epochs200 imgsz640 batch16 device0 lr00.01 lrf0.01 cos_lrTrue选yolo11s.pt做预训练权重而不是从零训练因为COCO预训练权重已经学到了通用物体特征尤其是人体的轮廓、姿态、遮挡关系迁移到课堂行为场景可以大幅缩短收敛时间。我从实际训练曲线看如果不加载预训练前50轮loss下降很慢加载预训练后第20轮就已经能出可用的结果。训练参数里我重点改了三个值imgsz用640而不是1280课堂小目标多按理说大分辨率更好但实时性和显存占用会指数上升640配合yolo11s在8G显存上batch_size调到16已经是极限cos_lr打开让学习率周期下降行为分类这种细粒度任务比通用检测需要更平滑的收敛epochs设200配合早停机制我大概在145轮左右就触发了早停后续没有再提升。2.3 数据增强与过拟合应对课堂数据很容易过拟合因为同一个教室的背景、座位排列、光线颜色高度相似模型往往学到的是“背景固定位置”而不是行为本身。我做了两类处理Mosaic增强Ultralytics默认开启Mosaic和MixUp我把Mosaic概率设为1.0MixUp设为0.2。Mosaic把四张图拼成一张模型被迫在不同背景下学习目标特征削弱对单一场景的记忆。随机擦除和HSV扰动对颜色空间做增强模仿不同时间教室灯光变化随机擦除部分区域能提高被遮挡目标的鲁棒性但擦除比例不能太高否则趴桌和低头这种关联细节都被擦没了我设了erase_scale0.2测试下来对验证集精度提升约1.5个点。过拟合的判断指标不能只看mAP。我每周跑一次真实视频样例观察模型在未见过的教室里的动态表现发现如果只在原场景测试mAP可能连续几次都在80以上但换了新教室后掉到60这就是典型的过拟合。后来加入了同一硬件环境不同座位模式的跨场景数据情况才明显缓解。2.4 模型评估与剪枝训练完需要用独立测试集评估。我按视频来源分配训练集、验证集和测试集保证同一摄像头同一时段的视频不会同时出现在训练和测试中这样评估结果才真实反映泛化能力。最终yolo11s在测试集上的mAP50约为0.87mAP50-95约为0.65其中“趴桌”精度最低约0.79“站立”和“行走”精度最高接近0.93。精度低的类别往往是样本数量少或形态受限我在数据准备时额外补充了趴桌在不同位置、不同光照下的样本比其他类别多了30%。部署到生产环境时我对模型做了剪枝和INT8量化。剪枝使用结构化稀疏去掉通道贡献低于阈值的卷积核模型大小从22MB降到14MB然后转ONNX再用TensorRT进行INT8量化需要校准数据集我从验证集随机抽500张图做校准。量化后mAP掉了1.8个百分点但推理速度从12ms提升到5ms对实时摄像头检测来说完全值得。3. Django集成与三种检测模式实现3.1 Django项目结构与模型服务进程解耦Django工程目录我按功能模块划分classroom_system/ ├── manage.py ├── config/ │ ├── settings.py │ └── urls.py └── apps/ ├── accounts/ # 用户登录与权限 ├── detection/ # 检测任务、上传、结果展示 ├── camera/ # 摄像头管理与实时流 └── reports/ # 行为统计报表模型服务单独放一个包不依赖Django的settings也不使用ORM只通过Redis交互。这样模型进程可以在独立GPU机器上运行Django纯CPU也能撑起高并发访问。模型服务代码核心结构# model_worker.py import torch import cv2 import numpy as np from ultralytics import YOLO import redis import json import time class YOLOWorker: def __init__(self, weights_path, devicecuda:0): self.model YOLO(weights_path) self.model.to(device) self.model.eval() self.redis redis.Redis(hostlocalhost, port6379, db0) def preprocess_image(self, img): # 统一缩放到模型输入尺寸保留比例不足部分填充灰色 h, w img.shape[:2] scale min(640 / h, 640 / w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(img, (new_w, new_h)) canvas np.full((640, 640, 3), 114, dtypenp.uint8) canvas[:new_h, :new_w] resized return canvas def predict_image(self, img): results self.model(img, conf0.45, iou0.5, imgsz640, verboseFalse) boxes results[0].boxes detections [] if boxes is not None: xyxy boxes.xyxy.cpu().numpy() conf boxes.conf.cpu().numpy() cls boxes.cls.cpu().numpy().astype(int) for i in range(len(cls)): detections.append({ bbox: xyxy[i].tolist(), confidence: round(float(conf[i]), 4), class_id: int(cls[i]), class_name: results[0].names[int(cls[i])] }) return detections def loop(self): while True: # 从Redis队列取任务 task self.redis.blpop(detect_tasks, timeout0) if not task: continue task_data json.loads(task[1]) task_type task_data.get(type) start time.time() if task_type image: img cv2.imread(task_data[path]) detections self.predict_image(img) self.redis.set(fresult:{task_data[task_id]}, json.dumps(detections)) elif task_type video: self.process_video(task_data) self.redis.lpush(task_logs, json.dumps({ task_id: task_data[task_id], inference_time: round(time.time() - start, 2) }))这个Worker用blpop阻塞读取任务队列避免轮询CPU空转。任务执行完把结果写入Redis带过期时间的key前端查询到后落库存为历史记录。3.2 图片上传检测接口实现Django端上传图片接口接收multipart文件保存到MEDIA_ROOT生成task_id把文件路径push到Redis然后立即返回task_id。前端轮询任务状态。这里关键点在于不要让Django同步等待模型结果否则一个检测任务耗时2秒Django进程就被占用其他请求全部排队。视图代码# views.py import uuid import redis from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .models import DetectTask r redis.Redis(hostlocalhost, port6379, db0) csrf_exempt def upload_image(request): if request.method ! POST: return JsonResponse({error: POST only}, status405) f request.FILES.get(image) if not f: return JsonResponse({error: missing image}, status400) task_id str(uuid.uuid4()) ext f.name.split(.)[-1].lower() save_path fuploads/{task_id}.{ext} with open(save_path, wb) as fp: for chunk in f.chunks(): fp.write(chunk) DetectTask.objects.create(task_idtask_id, input_typeimage, statuspending) r.lpush(detect_tasks, json.dumps({ task_id: task_id, type: image, path: save_path })) return JsonResponse({task_id: task_id, status: pending}) def query_result(request, task_id): task DetectTask.objects.get(task_idtask_id) if task.status pending: result r.get(fresult:{task_id}) if result: detections json.loads(result) task.status done task.result_json detections task.save() r.delete(fresult:{task_id}) else: return JsonResponse({status: pending}) return JsonResponse({status: task.status, detections: task.result_json})实际测试中单张640x640图片从上传到返回结果大约1.2秒其中模型推理仅占0.4秒其余时间花在文件IO和小图渲染上。如果部署环境用SSD延迟可以压缩到0.9秒以内。3.3 视频文件检测抽帧策略与结果合成视频检测不能逐帧推理否则一个10分钟视频会跑出1.5万次推理耗时过长且结果冗余。我采用“关键帧抽帧目标跟踪”的思路。每隔5帧抽一次检测相邻检测结果用IoU匹配如果同一目标在连续帧中被检测到且类别一致就合并为一段行为记录避免同一行为被重复框出几十次。具体来说视频按30FPS读取每5帧取1帧执行YOLO推理。检测到目标后用简单的IoU匹配关联到上一关键帧的目标ID。如果目标在超过1秒的时间内保持同一类别则记录一条有效行为并生成该行为起始和结束时间。最终在视频上叠加绘制不同颜色框和标签抽出每个行为类别的典型帧生成预览图。这个策略比纯逐帧检测节省90%计算量同时保留了行为时长信息。我在代码里直接用OpenCV的VideoCapture读取视频def process_video(task_data): cap cv2.VideoCapture(task_data[path]) fps cap.get(cv2.CAP_PROP_FPS) if fps 0: fps 30 sample_gap 5 frame_count 0 detections_buffer [] while True: ret, frame cap.read() if not ret: break if frame_count % sample_gap 0: dets worker.predict_image(frame) detections_buffer.append({ frame: frame_count, time: frame_count / fps, detections: dets }) frame_count 1 cap.release() # 汇总结果按目标关联生成行为时长 behaviors merge_detections(detections_buffer, fps, gap_threshold15) # 保存结果这里要注意frame_count可能非常大不能用普通列表无限存我在merge之前用流式聚合只在内存中保留最近两个关键帧的目标框其余直接写入临时数据库。否则一部1080P长视频能把内存吃满。3.4 摄像头实时检测RTSP拉流与WebSocket推送摄像头实时检测是要求最高的一块涉及网络流、多线程和前端播放。我的摄像头模块支持两类来源USB摄像头教室一体机和普通摄像头和RTSP网络摄像头。USB摄像头直接用opencv打开设备号或V4L2通道RTSP地址格式如rtsp://user:passwordip:554/stream1。实时检测的处理线程模型主线程Django后端启动一个CameraManager为每个摄像头创建一个CaptureThread。CaptureThread循环读取帧放入一个长度有限的队列比如5帧避免帧堆积导致延迟无限变大。推理线程从队列取最新帧丢弃旧帧执行YOLO推理把叠加了标注框的帧编码成JPEG通过WebSocket推送给前端。WebSocket用Django Channels实现。在前端页面上用img标签不断更新base64图片这种方式比播放video标签加canvas覆盖标注框简单得多兼容性也好。实测在局域网内720P分辨率下从摄像头采集到画面推送到浏览器约350毫秒1080P在此基础上增加150毫秒基本满足实时监看需求。推流代码核心# consumers.py import asyncio import base64 import json from channels.generic.websocket import AsyncWebsocketConsumer class CameraConsumer(AsyncWebsocketConsumer): async def connect(self): self.camera_id self.scope[url_route][kwargs][camera_id] await self.accept() # 注册到CameraManager的订阅列表 CameraManager.add_subscriber(self.camera_id, self) async def disconnect(self, code): CameraManager.remove_subscriber(self.camera_id, self) async def receive(self, text_data): data json.loads(text_data) if data.get(action) stop: await self.close() async def send_frame(self, frame_bytes): b64 base64.b64encode(frame_bytes).decode(ascii) await self.send(text_datajson.dumps({frame: b64}))CameraManager每隔200ms向所有订阅者推送一帧如果某个客户端网络慢就直接跳过该帧保证延迟优先而不是流畅优先。监控场景里管理者宁可看到轻微掉帧也不能接受画面延迟超过1秒因为实时干预需要低延迟。前端接收推送每帧刷新img的src配合行数统计面板。数据格式我额外推送了每个行为类别的人数浏览器端用简单的表格渲染当前课堂行为统计比如“举手3人、阅读12人、写写18人”这类实时统计对老师决策很有用。4. 常见问题与排查技巧实录4.1 YOLOv11推理速度不稳定时快时慢表现摄像头检测画面时偶尔掉到个位数FPSGPU利用率不高但CPU持续高负载。原因排查训练默认开启的很多预处理线程在我们自定义Worker环境中没有释放OpenCV的imread和BGR转RGB都占CPU。后来发现GIL也是罪魁祸首虽然PyTorch底层释放了GIL但Python业务代码之间的锁竞争仍然存在。解决方案把预处理、推理、后处理的每个阶段拆分成独立的Popen子进程或使用multiprocessing进程池而不是线程确保模型加载在主进程推理时只传张量数据。我最终用torch.multiprocessing的fork方式启动Worker推理进程独立完全绕开GIL。调整后单线程吞吐从18FPS提升到27FPS。4.2 小目标和密集人群漏检课堂后排学生目标尺寸可能只有60x30像素即便原图是1080P下采样到640后后排学生宽度只有20像素左右模型几乎无法识别。我在训练时把分辨率提高到960x960同时开启SAHI切片辅助推理对于图片和视频检测模式把大图切成多个512x512块每块独立检测再合并结果。SAHI对小目标提升显著后排学生召回率从0.65提升到0.82。但切片推理耗时翻了三倍在实时摄像头链路上不能直接用所以实时摄像头我保持640分辨率只对离线图片和视频使用SAHI。4.3 行为误报把“站立回答问题”识别成“行走”这是因为数据标注时没有区分定点站立和走动的边界。模型对运动目标会综合位置变化判断但单帧检测本质上没时序信息只能靠姿态判断。我增加了后处理规则如果同一目标连续5秒的位置坐标变化小于一个阈值例如身体中心移动距离小于50像素即使模型输出“行走”也会被修正为“站立”。这种规则补丁在纯目标检测模型上做时序平滑成本极低但效果明显。4.4 Django上报错“Worker timed out”或视频上传超时很多部署环境用默认的同步WSGI服务器比如Gunicorn单个worker处理视频上传和长轮询查询容易超过30秒超时。我的做法是Gunicorn配置成3个workertimeout120同时把大文件上传走Chunked传输或改用nginx上传模块。另外不要让Django直接返回检测好的视频文件正确做法是任务完成后生成结果视频路径前端单独通过静态文件服务下载。4.5 模型结果写入数据库时的类型问题YOLO输出bbox是tensor直接转json会报TypeError。我统一在Worker端将所有的numpy类型转成Python原生float/int并用round限定小数位。还有一个坑是class_id和class_name顺序如果本地训练时names顺序和部署时加载的权重不一致会出现标签错乱。我建议把names写死在权重文件对应的yaml中并在Worker初始化时打印一次类别映射人工核对一遍处理线上事故时能省半小时。4.6 技术文档编写的实际经验这个项目要求有技术文档我踩过最深的坑是文档和代码不匹配。模型参数、类别名、接口地址在迭代中改了好几次文档没有同步导致某公司按旧文档集成时调不通接口。后来我总结了一套文档维护方法接口文档用OpenAPI格式从Django REST Framework自动生成保证精确匹配代码。模型训练文档单独记录超参数、训练集统计数据和数据增强策略这部分作为“实验报告”不放入使用手册。部署文档必须包含从裸机开始的完整命令包括环境变量、模型权重位置、Redis/PostgreSQL初始化语句。文档版本跟随代码仓库的标签每次发布时同步更新不允许“先改代码后补文档”。技术文档不仅给使用者看也给自己留档。三个月后想重新训练一个新行为类别我当时记录的数据统计和分析结论仍然可以直接参考这点是很多开发团队忽略的。4.7 常见问题速查表问题现象可能原因解决方案GPU显存不足batch_size过大或imgsz过高降低batch到8imgsz用640图片检测结果为空conf阈值太高将conf从0.5降到0.35~0.45视频检测卡顿逐帧推理太慢每5帧抽1帧或改用RTSP硬解码Django接口500模型服务未启动或Redis连不上检查celery worker和redis-cli ping摄像头画面黑屏RTSP鉴权失败确认用户名密码使用VLC验证流地址某类行为完全检测不到训练样本不平衡为该类别补充数据并重训或开启数据增强WebSocket连接频繁断开nginx代理缓冲关闭配置proxy_buffering off并设置正确的Upgrade头模型推理和Web应用进程互相抢占资源同机部署无隔离设置docker资源限制或用K8s调度分开部署5. 项目扩展建议与个人经验系统做到这个程度其实已经覆盖了课堂巡课和远程督导的基本需求。在此基础上还能继续扩展的方向很多接入教室多媒体一体机的自动开关机策略基于行为统计结果做教学干预提醒利用OpenPose或姿态估计模型配合YOLO行为框做二次姿态校验减少趴桌和埋头阅读的混淆把行为序列按时序模型建模预测学生注意力曲线的拐点这些都属于“行为识别”的高阶玩法。我个人在实际操作中最大的体会是深度学习项目里模型训练只占30%工作量剩下的70%都在数据清洗、服务封装和规避边缘case。YOLOv11本身足够好用Ultralytics的封装对这些细节的托底也相当友好但你千万不要把开源仓库的demo一键运行当成生产系统。真的要部署到学校机房你必须认真对待并发、延迟、模型更新、数据隐私这几个问题。最后再分享一个小技巧调试实时摄像头时先用VLC或者ffplay确认RTSP流能够直接播放。很多摄像头在浏览器取流时会因为编解码格式不通导致黑屏不是代码问题是H.265编码不支持。遇到这类情况统一转成H.264或者用后端硬解码之后再送模型。另一个技巧是在Django项目里加一个简单的健康检查接口返回当前模型服务的实时状态和最近1分钟平均推理耗时。这个接口看起来不起眼但在演示活动现场能帮你提前发现GPU掉线、模型没有加载完毕这些意外状况避免在校长和老师面前翻车。如果这套系统的思路对你有帮助建议先拿一个教室的真实录制视频把数据集跑通再考虑整合Django。不要一上来就做实时摄像头先把离线视频检测的准确率和稳定性做到能接受的程度实时部分只是把离线流程缩短到一个推理周期内而已。数据、模型、服务一步一步来这个项目没有想象中那么难但也没有捷径可走。
阅读完成 · 觉得有帮助?
咨询建站