基于 OpenCV 的校园课堂行为识别与智能考勤分析系统实战
项目目标与运行结果课堂考勤如果只记录“是否签到”很难反映课堂现场的真实状态。实际教学管理更关心的是学生是否在座、课堂互动是否活跃、是否出现低头或趴桌等注意力下降行为以及这些信息能否沉淀为可复盘的表格和报告。本项目实现了一个固定机位课堂图片下的行为识别与智能考勤分析系统。基础版不依赖训练权重也不要求 GPU使用 OpenCV、NumPy、Pandas、Matplotlib 和 Pillow 完成图像分析、行为判断、统计汇总和报告生成。运行python main.py后系统会读取内置真实课堂图片和座位 ROI 配置生成标注图、行为分布图、考勤趋势图、CSV 表格、HTML 报告和系统看板。项目默认样例是一张真实课堂举手互动图片来源说明保存在docs/sources/real_classroom_sources.md。这张图片对应 5 个可见学生区域当前运行结果为5 人到位3 人举手互动1 人侧身讨论1 人认真听讲。识别结果会画回原图。每个座位框、头部候选框、行为标签和置信度都来自程序生成的座位级结果表而不是静态效果图。工程结构与数据流项目目录按输入数据、核心识别、报告输出和说明材料拆分classroom_behavior_attendance_system/ ├── main.py ├── configs/config.json ├── demo_data/real_classroom/ │ ├── real_classroom_hands_raised.jpg │ └── real_classroom_seat_map.json ├── src/ │ ├── classroom_analyzer.py │ ├── report_generator.py │ ├── demo_generator.py │ └── vision_utils.py ├── images/ │ ├── figures/ │ ├── results/ │ └── sources/ ├── outputs/ ├── tests/ └── docs/main.py负责串联完整流程src/classroom_analyzer.py负责 ROI 裁剪、肤色分割、连通域分析和行为判定src/report_generator.py负责统计图、系统看板和 HTML 报告src/vision_utils.py处理中文字体、目录创建和 OpenCV/PIL 图像转换。系统数据流如下课堂图片 座位表 - 按 ROI 裁剪每个学生区域 - 提取肤色候选像素 - 形态学去噪与连通域分析 - 选择头部候选区域 - 根据头部、手部和座位中心关系判断行为 - 生成座位级 DataFrame - 导出 CSV、标注图、统计图、HTML 报告和看板项目有一个重要设计所有后续输出都从同一张 DataFrame 派生。标注图用它画框行为分布图用它统计类别CSV 直接保存它HTML 报告把它转成表格。这样可以避免图表、表格和看板各算一套逻辑导致数字不一致。算法方案固定机位 ROI 肤色连通域 行为规则当前版本采用“固定机位座位区域校准算法”。在固定摄像头场景下每个学生在画面中的活动范围相对稳定可以先用 ROI 将全图切成多个座位区域再在每个 ROI 内独立判断是否到位和处于哪种课堂行为。算法输入包括两部分输入 1课堂图像 I格式为 BGR/RGB 图像 输入 2座位表 S {s1, s2, ..., sn}每个座位si至少包含以下信息roi_i [x1, y1, x2, y2] center_x_i head_down_threshold_y_i side_turn_threshold_x_i head_hint_i可选算法输出是一张座位级结果表R {r1, r2, ..., rn}每条结果ri包含座位编号、学生编号、是否到位、行为类别、中文行为名、头部框、头部中心、肤色连通域数量和展示置信度。后面的标注图、CSV、统计图、HTML 报告都只依赖这张结果表。整体算法拆成 6 个阶段阶段目标关键数据ROI 裁剪把整张课堂图拆成多个座位区域roi_i肤色分割找出 ROI 内可能属于脸、手、手臂的像素skin_mask形态学处理去噪并连接局部断裂区域开运算、闭运算连通域分析提取候选脸部或手部区域bbox、area、center头部选择从多个候选块中确定头部候选head_hint、面积排序行为判定根据头部位置、手部位置和座位偏移输出行为present、behavior伪代码如下Algorithm: ClassroomBehaviorAnalysis Input: I: classroom image S: seat map list Output: R: seat-level behavior table for each seat s in S: roi crop(I, s.roi) if roi is empty: append absent record continue mask skin_threshold(roi) mask morphology_open(mask) mask morphology_close(mask) components find_contours(mask) components filter_by_area(components) if components is empty: append absent record continue head select_head_component(components, s.head_hint) hand_candidates components - head if exists raised_hand(hand_candidates, head): behavior hand_raise else if head.center_y s.head_down_threshold_y: behavior head_down else if abs(head.center_x - s.center_x) s.side_turn_threshold_x: behavior turned else: behavior attentive append present record with behavior, head box and confidence return R这套方法的重点不只是“检测肤色”。它会结合多个几何关系头部候选在哪里、其他肤色块是否高于头部、头部是否低于阈值、头部是否偏离座位中心。对于课程实践和软著材料整理来说这类规则方案的优势是可解释、可运行、可调参也便于后续替换成姿态估计模型。座位表设计与 ROI 标定固定机位课堂分析的第一步是定义“画面中的哪些区域对应哪些学生”。项目使用 JSON 座位表描述每个座位的 ROI、学生信息和行为判断阈值。默认文件为demo_data/real_classroom/real_classroom_seat_map.json真实样例中的一个座位配置如下{seat_id:S01,row:1,col:1,student_id:2026001,student_name:学生01,center_x:95,desk_y:1015,roi:[0,590,255,1060],head_down_threshold_y:850,side_turn_threshold_x:70,head_hint:[72,760]}核心字段如下字段作用开发时的使用方式roi座位检测区域格式为[x1, y1, x2, y2]程序只分析该矩形区域减少黑板、桌面、墙面等背景干扰center_x座位中心横坐标用头部中心与座位中心的横向偏移判断侧身讨论head_down_threshold_y低头阈值头部中心纵坐标超过该值时认为头部位置偏低side_turn_threshold_x侧身偏移阈值控制侧身讨论判定的灵敏度head_hint头部参考点在真实图片中帮助程序从多个肤色连通域里选出头部制作自己的座位表时建议按这个顺序进行固定输入图片分辨率避免后续坐标全部失效。用图片查看工具或标注工具读取每个学生区域的左上角、右下角坐标写入roi。根据人体中心附近位置设置center_x不要简单取 ROI 中点。观察正常抬头时的头部位置设置略低于正常头部中心的head_down_threshold_y。如果画面中有举手、遮挡或反光区域补充head_hint避免把手掌或手臂选成头部。ROI 标定完成后系统就从“分析整张图片”变成“逐个分析座位区域”。这也是规则版能够稳定复现结果的关键。核心实现从 ROI 到行为标签识别入口在ClassroomBehaviorAnalyzer.analyze_image()。程序读取图片后遍历座位表每个座位调用_classify_seat()forseatinself.seat_map:records.append(self._classify_seat(image_bgr,seat))ROI 裁剪与边界保护_classify_seat()先读取 ROI 坐标并把坐标限制在图像范围内x1,y1,x2,y2[int(v)forvinseat[roi]]h,wimage_bgr.shape[:2]x1,y1max(0,x1),max(0,y1)x2,y2min(w-1,x2),min(h-1,y2)ifx2x1ory2y1:returnself._empty_record(seat)roiimage_bgr[y1:y2,x1:x2]这段边界保护很有必要。自定义图片时ROI 坐标很容易因为分辨率变化或标注错误而越界程序遇到无效 ROI 时返回缺勤记录而不是直接报错退出。对应逻辑在tests/test_real_pipeline.py中也有测试覆盖。肤色阈值分割当前项目在 ROI 内使用 BGR 通道阈值提取候选肤色区域b,g,rcv2.split(roi_bgr)mask((r185)(g120)(g220)(b70)(b180)(rg)(gb*0.85)).astype(np.uint8)*255换成算法表达就是对 ROI 内每个像素p(x, y)读取 B、G、R 三个通道。只要满足这些条件就将该像素记为候选肤色像素R 185 120 G 220 70 B 180 R G G 0.85 * B二值 mask 的定义为mask(x, y) 255, if p(x, y) satisfies skin rule 0, otherwise该阈值用于当前固定机位样例的轻量检测不等价于通用人脸检测。它的作用是把可能属于脸、手、手臂的像素从背景中分离出来作为后续连通域分析的候选前景。形态学处理与连通域分析直接使用阈值 mask 容易得到零散噪声因此项目在进入轮廓提取前加入开运算和闭运算kernelnp.ones((5,5),np.uint8)maskcv2.morphologyEx(mask,cv2.MORPH_OPEN,kernel,iterations1)maskcv2.morphologyEx(mask,cv2.MORPH_CLOSE,kernel,iterations2)开运算可以理解为“先腐蚀再膨胀”主要去掉小白点闭运算可以理解为“先膨胀再腐蚀”主要连接局部断裂。课堂图片里手指、脸部边缘和手臂区域可能受光照影响被切断闭运算可以让这些区域形成更稳定的候选块。之后使用 OpenCV 轮廓函数提取候选区域contours,_cv2.findContours(mask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)每个候选区域记录四类信息bbox全图坐标下的外接框 area轮廓面积 center轮廓中心点 local_bboxROI 内部坐标bbox来自cv2.boundingRect()格式为bbox [x_min, y_min, x_max, y_max]候选区域中心点使用轮廓矩计算center_x m10 / m00 center_y m01 / m00如果轮廓矩m00为 0就退回外接框中心点。程序还会把 ROI 内坐标加上偏移量(roi_x1, roi_y1)转换成全图坐标后续绘制标注图时可以直接使用。头部候选选择如果只处理普通头像图最大肤色连通域通常可以当作头部。但真实课堂图片中举手时的手掌或手臂可能比脸部更大最大连通域策略容易把手选成头部。项目因此在座位表中加入可选字段head_hint。头部选择逻辑为没有候选区域时返回缺勤。如果座位配置了head_hint优先选择距离head_hint最近、且在半径范围内的连通域。如果没有head_hint或没有命中再退回最大连通域。距离计算公式为dist(c, h) sqrt((center_x - hint_x)^2 (center_y - hint_y)^2)候选块只有在dist head_hint_radius时才进入头部候选集合。候选集合中优先选择距离更近的区域如果距离相近再倾向选择面积更大的区域。这个设计保留了规则算法的轻量性也通过一次性人工标定提升了真实课堂样例下的稳定性。行为规则判定项目支持 5 类状态attentive认真听讲 hand_raise举手互动 head_down低头/趴桌 turned侧身讨论 absent缺勤第一层先判断缺勤ROI 内没有有效肤色连通域就认为该座位没有检测到学生。ifnotcomponents:returnself._empty_record(seat)第二层判断举手互动。程序把头部以外的肤色连通域当作候选手部区域如果该区域明显位于头部侧上方或向上延伸足够明显就判定为举手互动if(component[area]120andside_offset25and(by1head_cy-25or(comp_height70andcyhead_cy15))):has_raised_handTrue展开后可以理解为area(hand) 120 abs(hand_center_x - head_center_x) 25 hand_top_y head_center_y - 25对于手臂和手掌形成竖向长条的情况项目还加入补充条件component_height 70 component_center_y head_center_y 15第三层判断低头。系统不做人脸姿态估计而是比较头部中心纵坐标和座位表中的低头阈值elifhead_cyfloat(seat[head_down_threshold_y]):behaviorhead_down第四层判断侧身讨论。做法是比较头部中心和座位中心的横向偏移elifabs(head_cx-float(seat[center_x]))float(seat[side_turn_threshold_x]):behaviorturned对应公式为abs(head_center_x - seat_center_x) side_turn_threshold_x如果以上条件都不触发只要检测到学生就判为认真听讲behaviorattentive行为优先级为无有效肤色区域 - 缺勤 有高位手部区域 - 举手互动 头部中心低于阈值 - 低头/趴桌 头部横向偏移过大 - 侧身讨论 其他到位情况 - 认真听讲这个顺序会影响结果。举手时头部可能同时发生偏移如果先判断侧身举手容易被归为侧身讨论低头和侧身也可能同时出现当前版本按展示需求优先输出更直观的课堂行为类别。结果表、统计指标与报告生成识别完成后每个座位生成一行记录最终拼成 DataFrame。当前真实样例 CSV 字段如下image_name seat_id row col student_id student_name present behavior behavior_cn confidence head_bbox head_center_x head_center_y skin_component_count真实运行输出摘要如下S01 举手互动 0.96 head_bbox41,796,112,877 skin_component_count7 S02 侧身讨论 0.94 head_bbox215,675,290,901 skin_component_count1 S03 举手互动 0.96 head_bbox567,803,582,815 skin_component_count6 S04 举手互动 0.96 head_bbox609,681,662,859 skin_component_count6 S05 认真听讲 0.665 head_bbox727,730,765,767 skin_component_count1confidence不是深度学习模型的 softmax 概率而是规则版根据肤色面积和行为类别估算出的展示置信度。代码中的基础分数为base min(0.94, 0.52 total_skin_area / 6500)举手互动会在基础分上增加 0.05低头/趴桌会增加 0.03缺勤固定为 0.91。这个字段适合用于报告展示和结果排序不应解释成严格的模型概率。汇总统计由summarize()完成presentint(df[present].sum())attentiveint((df[behavior]attentive).sum())hand_raiseint((df[behavior]hand_raise).sum())head_downint((df[behavior]head_down).sum())turnedint((df[behavior]turned).sum())attendance_ratepresent/totaliftotalelse0attention_rateattentive/presentifpresentelse0当前样例结果为座位数5 实到人数5 缺勤人数0 考勤率100.0% 专注率20.0% 认真听讲1 举手互动3 低头/趴桌0 侧身讨论1行为分布图由behavior_distribution_chart()生成固定按 5 类行为统计人数order[attentive,hand_raise,head_down,turned,absent]values[int((single[behavior]item).sum())foriteminorder]考勤与专注趋势图由attendance_trend_chart()生成。默认真实样例只有一张课堂图片因此趋势图只有一个采样点后续接入视频帧或多张课堂图片后同一函数可以扩展为多时间点曲线。系统看板由dashboard_image()合成将顶部指标卡、标注图、行为分布图和趋势图整合到一张图片中适合作为项目报告或软著说明书中的运行效果图。HTML 报告由html_report()生成路径为outputs/report.html报告中包含系统指标、运行截图、标注图、统计图和座位级表格。自动化测试会检查报告内容不包含本机绝对路径避免公开发布时泄漏本地目录信息。运行方式与自定义图片安装依赖后在项目根目录执行pipinstall-rrequirements.txt python main.py默认输入为demo_data/real_classroom/real_classroom_hands_raised.jpg demo_data/real_classroom/real_classroom_seat_map.json主要输出文件包括images/results/classroom_01_annotated.png images/results/behavior_distribution.png images/results/attendance_attention_trend.png images/results/system_dashboard.png outputs/classroom_analysis_results.csv outputs/classroom_batch_results.csv outputs/report.html如果要分析自己的课堂图片不建议只替换图片后直接运行。固定机位方案依赖座位表正确做法是重新标定 ROI将课堂图片放入项目目录例如demo_data/my_classroom/classroom_a.jpg。按图片实际分辨率重新制作座位 ROI不要沿用默认样例坐标。给每个座位填写seat_id、student_id、student_name、roi、center_x、head_down_threshold_y、side_turn_threshold_x。如果图片中存在举手、遮挡或反光建议补充head_hint。运行时显式指定图片和座位表。命令示例python main.py--inputdemo_data/my_classroom/classroom_a.jpg --seat-map demo_data/my_classroom/seat_map.json调参时建议先配置 2 到 3 个座位确认标注框和行为类别正确后再补齐全班座位。一次性配置几十个 ROI 后再排查容易分不清问题来自坐标、阈值还是图片本身。常见问题可以按这个顺序排查现象优先检查框没有画在学生身上roi坐标是否匹配当前图片分辨率举手被识别成认真听讲手部连通域是否在 ROI 内head_hint是否选错头部侧身误判较多center_x是否取到人体中心side_turn_threshold_x是否过小低头误判较多head_down_threshold_y是否高于正常抬头位置中文显示异常系统是否安装中文字体或vision_utils.py是否找到可用字体测试与可复现性项目提供了最小自动化测试文件tests/test_real_pipeline.py测试覆盖两类关键场景。第一类验证真实课堂主流程能生成看板、标注图和 HTML 报告resultrun_pipeline(args)self.assertTrue(Path(result[dashboard_path]).exists())self.assertTrue(Path(result[annotated_path]).exists())self.assertGreater(result[summary][present_count],0)第二类验证 ROI 越界时不会抛异常而是返回缺勤tiny_imagenp.zeros((80,80,3),dtypenp.uint8)recordanalyzer._classify_seat(tiny_image,analyzer.seat_map[0])self.assertEqual(record[behavior],absent)self.assertEqual(record[present],0)验证命令如下python main.py python-munittest tests.test_real_pipeline文章中展示的结果来自默认真实课堂图片和默认座位表。只要图片、座位表和依赖环境保持一致读者可以复现同样的 CSV、标注图、统计图和报告。扩展方向与边界当前版本适合固定机位演示、课程设计、软著材料整理和小范围二次开发不应直接当作通用真实课堂监控系统。复杂教室环境会受到遮挡、背影、光照变化、肤色差异、摄像机角度变化等因素影响。后续升级时建议保持输出接口不变只替换识别模块在classroom_analyzer.py中新增模型版分析器例如PoseClassroomAnalyzer。使用 YOLO-Pose 或 MediaPipe Pose 输出人体关键点。用关键点判断举手、低头和侧身替换当前肤色连通域规则。继续输出同样字段的 DataFrame例如seat_id、present、behavior、confidence。保持report_generator.py不变继续生成 CSV、图表、看板和 HTML 报告。这样做可以把 V1.0 的工程闭环保留下来。后续升级重点放在识别算法而不是推倒重写报告、图表和导出逻辑。参考资料真实课堂图片 Hands raisedWikimedia Commonshttps://commons.wikimedia.org/wiki/File:Hands_raised_(7645867).jpgOpenCV 轮廓处理文档https://docs.opencv.org/3.4/de/d09/tutorial_table_of_contents_contours.htmlopencv-python 项目页面https://pypi.org/project/opencv-python/MediaPipe Pose Landmarker Python 文档https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/python