☰
YOLOv11+Django+DeepSeek:课堂行为检测系统完整实现
2026/10/19 3:10:03 网站建设 项目流程

我最早是在给某课程试点项目搭课堂行为监测功能时接触到这个组合的。课堂场景里老师真正想知道的不是“模型输出了多少种框”,而是“这节课哪些学生在低头、哪些在趴桌、哪些在频繁玩手机”。标题里的 YOLOv11 + Django + DeepSeek 这套搭配,恰好把“看到行为”“管理数据”“组织语言”三件事各管一摊,还带登录界面的精美 Web 前端。这篇文章我按实际开发顺序,把整套系统拆开讲清楚——从模型推理、后端接口、前端展示到报告生成,每一步为什么这么做、卡住了怎么办,尽量一次性讲到位,适合想拿这套技术栈快速落地课堂行为检测系统的开发者做参考。

1. 行为检测系统最容易被低估的需求:不是模型,而是“从模型到老师看到报告”的链路

很多人听到“学生课堂行为检测系统”,第一反应就是“训练一个 YOLOv11 模型,检测睡觉、玩手机、举手”。实际上模型能顺利跑出框,只是这个项目里最简单的一层。真正让系统可用,要解决的是行为数据从哪来、往哪去、老师怎么看、异常怎么提醒这一整套链路问题。

1.1 课堂场景里,逐帧全量推理并不划算

课堂监控和普通的安防识别不太一样。学生在座位上长时间保持相似姿态,教室摄像头一节课动辄上万帧,如果每帧都送进模型推理,GPU 确实费、卡顿也明显,但老师要看的只是几个关键指标:这节课有多少次低头、总共睡了多久、举手几次、答题互动是否频繁。我在实践里的做法是降低采样频率,摄像头画面按每秒 1~2 帧抽帧推理,其余时间用来做跟踪和状态累积。课堂行为本身是慢变化过程,1 秒抽 1 帧已经能捕捉到够用的行为变化。

对应的行为类别,我选的是这几种常用组合:

类别标签检测难点
抬头听讲focusing摄像头俯拍时和低头难区分
低头looking_down遮挡多,角度敏感
书写writing手部动作快
举手hand_raising遮挡严重
玩手机phone手机目标小
趴桌sleeping头肩特征变化大

这套类别列表不用贪多,类别越多,单个类别的精度和稳定性越难兼顾,和课堂老师真正关心的指标直接相关就够用了。

1.2 定位练习 DeepSeek 和 Django 的分工

在这个系统里,YOLOv11 负责“看”:从一帧画面中框出学生个体,给出行为类别和置信度。Django 负责“管”:接收推理结果、存储行为数据、管理登录用户、向浏览器推送实时告警。DeepSeek 负责“说”:检测数据是零散的,老师不可能一节课盯着一堆数字看,于是把聚合后的行为统计抛给大模型,让它生成像“这节课整体专注度偏低,后排第三列玩手机次数较多,建议增加提问互动”这样的报告。

三者的边界划分很重要。不要让 Django 去做模型推理,也不要让前端直接请求大模型接口。检测、后端、生成报告各司其职,整个系统在出现问题的时候,单点排查会容易很多。

2. 项目整体架构与数据流设计

下面这张是我最终采用的模块布局,不画复杂架构图,直接文字说明每个模块之间怎么流转,这样更容易对应到代码结构。

2.1 Django 工程的分层结构

我建议把工程分成 apps 分层的模式,便于扩展。model 推理部分单独抽成一个 service,不塞在 views.py 里;行为聚合逻辑独立成 service;Web 前端使用静态页面加一套轻量后台模板;DeepSeek API 调用封装成独立 client。Django 工程的核心目录大致如下:

project_root/ ├── manage.py ├── requirements.txt ├── config/ │ ├── settings.py │ └── urls.py ├── behavior/ │ ├── camera.py # 视频源采集与抽帧 │ ├── detector.py # ONNX 模型推理 │ ├── tracker.py # 目标跟踪与ID维护 │ ├── aggregation.py # 行为统计聚合 │ └── report_client.py # DeepSeek 接口封装 └── web/ ├── views.py # 登录、页面、状态接口 ├── urls.py └── templates/

behavior 是纯算法侧,不依赖 Django 的 ORM;web 是展示和对外接口层。这样做的好处是,将来如果要把同样的推理能力换成一个独立的服务进程,behavior 目录可以直接搬走,不用改业务代码。

2.2 数据流:从视频帧到行为报告

摄像头连续采集画面,在抽帧后交给 detector 推理,YOLOv11 输出检测框、类别和置信度。检测框接着进入跟踪模块,给每个框分配稳定的学生 ID——这一步很关键,如果不做跟踪,前一秒检测到的“张三”和后一秒的“张三”,模型无法确定是不是同一个人,也就没法累计“某位学生低头了多久”。因此我用了一种轻量跟踪策略,给每个学生分配一个持续存在的 ID,按帧更新其位置和行为状态。

行为统计会每隔 N 帧聚合一次,写入数据库,同时通过 Django 接口把实时状态推给 Web 页面。整节课结束后,聚合器把班级级别的行为分布整理成结构化的 JSON 数据。report_client 读取这份 JSON,组装成 prompt 后请求 DeepSeek,把返回的课堂总结文本存入库中,老师登录系统后在报告页看到。

整个链路最核心的原则是:模型推理的输出要尽量结构化,避免直接把原始检测结果丢给 LLM。原始检测里一帧可能产生 20 条框输出,老师要的却是“这 20 个框对应 6 个学生,其中 3 个总低头”。结构化要付出额外的聚合代码,但从使用体验来看,值。

3. YOLOv11 模型导出与 ONNX 推理实操细节

YOLOv11 训练时的产物通常是 .pt 权重文件,但在这个项目里,我实际部署用的是 ONNX 版本。Django 侧不依赖 PyTorch 的完整训练环境,推理速度快,也更容易做 CPU 部署,在没 GPU 的教室环境里CPU也能艰难跑起来。

3.1 从训练权重到 ONNX 的导出命令

导出命令本身不复杂,关键是参数别选错。使用 ultralytics 框架导出 ONNX 时,以下几处参数需要注意。

yolo export model=yolov11n-behavior.pt format=onnx opset=12 imgsz=640 simplify=True dynamic=False

我解释下这几个参数的用意。imgsz 设为 640,是因为训练时的输入尺寸就是 640,导出推理尺寸和训练尺寸保持一致,能避免尺寸变化导致的精度下降。dynamic=False 意味输入形状固定为 [1, 3, 640, 640],固定形状在 ONNX Runtime 下的优化更充分,速度更稳定。opset 使用 12,兼容性比较好,新老环境都能跑。

simplify 使用 onnxsim 做简化,可以把一些冗余算子合并,减少推理时的计算开销。建议导出后顺手验证一次模型精度:

python -c " import onnx m = onnx.load('yolov11n-behavior.onnx') onnx.checker.check_model(m) print('ONNX model OK') "

这一步很多人忽略,但 ONNX 模型有时会包含训练阶段才用到的算子,部署时直接报错,提前 check model 可以省掉大量排查时间。

3.2 ONNX Runtime 推理代码的标准化写法

写推理代码时,最需要注意的是预处理和后处理和 PyTorch 推理不太一样。ONNX Runtime 读进来的图像要完成 BGR 转 RGB、归一化到 0~1、resize 到 640、以及 HWC 到 CHW 的维度变换,否则模型输出结果会明显异常。

import cv2 import numpy as np import onnxruntime as ort class YOLOv11ONNX: def __init__(self, model_path, conf_thres=0.35, iou_thres=0.45): self.session = ort.InferenceSession( model_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'] ) self.conf_thres = conf_thres self.iou_thres = iou_thres self.input_name = self.session.get_inputs()[0].name self.output_names = [o.name for o in self.session.get_outputs()] self.class_names = ["focusing", "looking_down", "writing", "hand_raising", "phone", "sleeping"] def preprocess(self, image): img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) img = img.astype(np.float32) / 255.0 img = cv2.resize(img, (640, 640)) img = img.transpose(2, 0, 1) return np.expand_dims(img, axis=0).astype(np.float32) def infer(self, image): input_tensor = self.preprocess(image) outputs = self.session.run(self.output_names, { self.input_name: input_tensor }) return self.postprocess(outputs[0])

后处理时要把模型的输出从中心点坐标格式转换成 xyxy,再做置信度过滤和 NMS。这部分代码每个版本输出格式略有差异,我这里的经验是:YOLOv11 导出的 ONNX 输出为 [1, 84, 8400],84 是 4 个坐标 + 80 个 COCO 类别对应的置信度,如果是自定义类别,要把对应的维度改成 4 + 类别数。

3.3 推理性能实测与稳定性的平衡

我分别跑过 CPU 和 GPU 环境。i5 级别 CPU 单帧 640 推理约为 150ms~200ms,RTX 3060 上能做到 15ms~25ms。要是按每秒 1 帧抽帧,CPU 也能支撑起一个教室的单路摄像头;如果需要多路摄像头,建议上 GPU 或者把推理放到独立进程里,避免阻塞 Django 请求线程。

最影响体验的地方其实是 NMS 参数。conf_thres 不建议设太低,课堂场景下 0.35 左右比较合适。设成 0.1,会出现大量低质量重复框,跟踪 ID 频繁切换,行为统计反而乱;设太高,虽然框少,但漏检多,像玩手机这类小目标更容易漏。

4. Django 后端与精美 Web 界面的集成思路

后端部分我重点讲两件事:直播监视页面怎么做、行为数据怎么实时刷新到前端。登录界面和权限体系相对常规,只简单轻点一下。

4.1 摄像头画面和检测结果如何推送到浏览器

常见的方案有两种:MJPEG 流或多线程帧缓存轮询。课堂行为检测系统对实时性要求没那么极端,我用的是多线程帧缓存方案:一个后台线程持续抓帧并推理,把最新的带框画面保存成 JPEG 编码的内存帧,前端通过一个定时刷新的接口拿到静止图片,实现类似“准直播”的效果。

import threading import cv2 _camera_lock = threading.Lock() _global_frames = {} def capture_worker(camera_id, camera_url): cap = cv2.VideoCapture(camera_url) while True: ret, frame = cap.read() if not ret: continue frame = detector.infer_and_draw(frame) _, jpeg = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 80]) with _camera_lock: _global_frames[camera_id] = jpeg.tobytes()

前端如果直接拿这个接口返回的图片刷新,能简化很多 WebSocket 的配置。每一帧大约几十 KB,一秒刷 1~2 次,带宽完全够用。唯一要提醒的是,Django 的 runserver 自带开发服务器不适合长时间跑视频流线程,部署时得用 gunicorn 或者 uWSGI,并且要把抓帧任务独立到后台进程管理工具里,避免工作进程重启时重复启动线程。

4.2 实时行为统计图表的实现

实时统计部分,我采用一个 JSON 接口定期拉取最近 5 分钟的班级行为分布:每名学生的当前状态、最近一段时间内各类行为的次数,以及趴桌、玩手机的告警提示。前端基于这些数据绘制行为趋势折线图和状态分布饼图。

接口返回结构可以设计成下面这种:

{ "timestamp": "2025-04-01 10:23:00", "students": 42, "class_aggregate": { "focusing": 28, "looking_down": 7, "writing": 5, "sleeping": 1, "phone": 1 }, "alerts": [ {"student_id": 17, "behavior": "phone", "duration_seconds": 180} ] }

前端拿到这份数据后,不需要理解模型输出,只需要渲染键值。给老师看的页面重点突出“异常”和“整体趋势”,而不是一屏幕的检测框。这也是 Web 界面好不好用的分水岭——框花花绿绿,可不代表老师觉得好用;一眼能看出“谁在睡觉,谁在玩手机,全班整体状态如何”,才叫好用。

4.3 登录界面与权限控制的实现策略

登录界面用 Django 自带的认证模块就够,但有两个细节值得处理:

第一,登录成功后,系统会创建与用户绑定的会话,教师的会话里可以配置所管理的班级;管理员可以查看所有班级的数据。第二,课堂画面属于敏感信息,任何页面在展示摄像头内容之前,都要做登录校验。我用了一个简单的 LoginRequiredMiddleware,在请求入口统一拦截,而不是在每个视图函数里重复写装饰器。

课堂行为系统的权限不太复杂,做成“教师可看自己负责的教室,管理员可看全局”就能覆盖绝大多数需求。真正要留神的是登录失败尝试次数限制,防止课堂上被外界暴力尝试密码,简单加个 IP 维度限流就够了。

5. DeepSeek 接入与课堂报告生成的工程化做法

这是这套系统相对有亮点的部分。检测数据本身只是一种客观行为统计,但老师需要的可能是“为什么这节课堂氛围偏低”“应该怎么调整教学节奏”,这些判断交给 DeepSeek 去做性价比很高。

5.1 为什么把结构化统计而不是原始检测交给大模型

原始检测输出是密集的框和置信度,直接发给大模型,既浪费 token,又很容易让大模型淹没在无用细节里。我实际做法是先聚合,再传入。聚合后的数据形如“总人数 42,俯视低头累计 12 人次,平均单次低头时长 65 秒,后排靠窗位置出现连续趴桌”。这种数据量小、语义清晰,大模型能准确抓住重点,生成的报告也更贴近教学语言。

5.2 Prompt 模板设计

DeepSeek 入口调用时,我设计了一个固定格式的 prompt,把班级行为统计、检测时间、学科类型作为变量传入,并要求输出定向结构。一个大约可用的 prompt 如下:

你是一名教学观察助手。以下是某课堂内摄像头行为识别系统统计到的班级行为数据: - 上课时长:45分钟 - 学生人数:42 - 各行为累计时长:书写210分钟,抬头听讲850分钟,低头360分钟,玩手机45分钟,趴桌18分钟 - 偶发事件:后排3名学生频繁玩手机 请根据以上数据生成一份300字以内的课堂行为反馈,包含: 1. 班级整体专注度评价 2. 需要关注的异常行为 3. 可操作的教学建议 要求语言客观,不做人身评价,输出为markdown格式。

注意 prompt 里的一个重要边界:要求大模型“不做人身评价”,这样既能克制输出风险,也符合教师端工具的产品伦理。检测系统只是辅助工具,最终判断权一定留在老师手里。

5.3 DeepSeek 调用的超时、重试与降级

任何大模型接口都可能超时或返回异常,工程上要设计三件事:

  • 超时控制:requests 库设置 timeout,我这里设置为 10 秒,避免阻塞报告页长时间白屏;
  • 重试策略:对 5xx 和超时做最多 2 次重试,每次间隔递增;
  • 降级策略:调用失败时,不弹错误页,而是展示本地预先写好的模板报告,并在页面上提示“AI 分析暂不可用,已展示基础统计”。
import json import requests API_KEY = "your_deepseek_api_key" API_URL = "https://your_deepseek_endpoint" def generate_report(structured_data: dict) -> str: prompt = build_prompt(structured_data) headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} payload = { "model": "your_model_name", "messages": [ {"role": "user", "content": prompt} ], "temperature": 0.4, "max_tokens": 600 } for attempt in range(3): try: resp = requests.post(API_URL, json=payload, headers=headers, timeout=10) if resp.status_code == 200: content = resp.json()["choices"][0]["message"]["content"] return content except Exception: if attempt == 2: break time.sleep(2 * (attempt + 1)) return load_fallback_report()

温度参数我一般设到 0.3~0.5 之间。设太高,生成报告会充满推测性描述;课堂行为分析要的是客观、稳定、可复现的输出,不是天马行空的评语。

6. 部署上线过程中容易踩的坑与应对方案

这部分全是实际操作里趟出来的经验。项目看起来每个模块单独跑都没问题,一组合起来,问题就浮出来了。

6.1 多线程抓帧与 Django 请求的冲突

最开始抓帧逻辑写在视图函数内部,导致每打开一个页面,就多启动一个抓帧线程,时间一长整个机器卡死。后来换成独立的后台进程,由进程管理器拉起摄像头抓帧任务,Django 只负责读取内存中最新帧,问题才彻底解决。

线程安全也要注意。多个线程同时读写一个 JPEG 帧,不加锁会在极少数情况下读到半张图。写入和读取都加锁,代价很小,却能让系统稳定很多。

6.2 ONNX Runtime 的 GPU 版本安装陷阱

onnxruntime-gpu 版本和 CUDA 版本不对应,是最常见的问题。你很可能安装一个新版 onnxruntime-gpu,但机器上 CUDA 版本较老,推理时直接找不到 CUDA 环境,自动退化到 CPU,性能自然掉下来。部署前用一条命令确认是否真正调用 GPU:

import onnxruntime as ort print(ort.get_available_providers())

只有输出了 CUDAExecutionProvider 才说明 GPU 可用。如果没出现,要么换对应版本的 onnxruntime-gpu,要么升级 CUDA 工具链。考虑到教室边缘环境复杂,我的建议是优先做好 CPU 推理的性能优化,GPU 预留为可选增强。

6.3 行为统计的累积误差比单帧误差更难排查

单帧漏检一眼就能看到,不容易察觉的是统计链条里的误差累积。比如某学生被跟踪 ID 频繁切换,就会把同一段低头时长计成两个学生。我加了一个跟踪阈值:同一检测框与已有轨迹 IoU 超过 0.5 才更新 ID,小于阈值的暂时保留 3 帧待匹配,超过 3 帧没有匹配才判定为新 ID。这样能显著降低 ID 抖动。

另一个坑是视频流断流后,行为时长还在继续累计。抓帧线程要维护一个“最后更新时间”,超过设定阈值就自动终止该摄像头会话,直到视频流重新恢复。

6.4 前端显示时间戳与检测帧不同步

摄像头的视频流可能持续掉线,前端的“实时状态”如果直接取接口返回时刻作为时间戳,会掩盖掉视频流断流的真实现象。我在帧数据里额外写入帧原始采集时间,前端展示时以这个字段为准,一旦发现采集时间与当前系统时间差距超过 10 秒,就明确提示“画面延迟过高”,避免老师误判课堂实时状态。

7. 这套系统后续还可以扩展的三个方向

项目跑通后,我积累了三条很明确的扩展思路,这里一并分享。

一是多摄像头教室聚合。一个教室装多路摄像头,通过跟踪关联同一学生在不同机位下的检测结果,行为统计会更完整。实现时可以把 camera_id 加进统计数据表,前端按教室分组汇总。

二是把行为检测和课堂互动工具对接。检测到举手行为后自动标记对应学生;检测到低头频繁的时段,自动记录教学节点,老师课后复盘时直接定位时间轴。

三是引入边缘盒子部署。现在整套系统跑在一台带 GPU 的服务器上,但很多学校希望摄像头本地完成推理,再把结构化结果传到中心服务器。ONNX 模型已经具备部署到边缘的条件,只要把 detector 独立成子进程,上传过程中合并班级维度的聚合数据,带宽压力也不大。

做一个课堂行为检测系统,模型选型和训练只是开始,难度其实在后端的稳定性、前端的信息组织、大模型的语义概括。把这三样整合到一条顺畅的链路上,会比单纯追求几个点的 mAP 更有实际意义。只要链路完整、数据可靠,老师不用理解技术细节,就能轻松拿到“这节课谁在认真听、哪个环节需要调整”的可读结论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询