简介:一份完整的机器视觉教室照明控制系统工程源码包,面向嵌入式视觉、智能硬件方向学习者及课设/毕设开发者。系统基于YOLO算法识别教室内人体位置,并将画面划分为A、B、C、D四个区域,结合环境亮度与开放时间自动控制对应区域灯光,软件采用Qt开发。整个压缩包内共146个文件,涵盖C++源码、Qt工程(pro/ui资源)、YOLO模型配置与权重(含完整版与轻量版)、上位机可执行程序、设计文档PDF及界面图片素材,整体约491.91MB;按文档完成环境配置、编译后即可获得可运行的照明控制项目。已有436人学习下载。除上下位机完整源码外,还附带区域划分、灯光联动等关键逻辑说明,适合快速复现项目或作为毕业设计、竞赛方案参考,能显著提升从算法到落地的综合实操能力。
1. 基于机器视觉的教室照明控制系统:从"整室亮灭"到"按人按区"
一所普通高校的教室,早八点前灯全亮、午休时没人灯还亮、晚自习走了大半学生但整排灯依然开着……这些场景背后是同一类需求:照明控制的颗粒度太粗。基于机器视觉的教室照明控制系统,就是用摄像头替代红外传感器和人体感应开关,通过识别画面里有没有人、人在哪个区域、大概几个人,再把结果映射成对灯光的区域级控制。它既是一个机器视觉工程项目,也是一套完整的物联网控制链路,适合做智慧校园、楼宇自控、节能改造的方向。这篇按我自己做项目的顺序,把检测选型、区域划分、控制策略、源码包落地和踩坑一次说透。
2. 机器视觉人员检测怎么选型:传统视觉 vs 深度学习,曝光是隐形成本
2.1 检测方案对比:帧差法、HOG+SVM、YOLO 的取舍
机器视觉项目里人员检测不是一个新问题。常见做法有三条路线。
帧差法/背景建模:把摄像头的画面按帧做差,运动区域就是目标。实现最简单,OpenCV 几十行就能跑。但教室场景最大的问题是"人不动"——学生上课时大部分时间是静止的,帧差法会把人漏掉。这套逻辑更适合走廊这种人流量大的场所,用在教室里基本是自废武功。
HOG+SVM:用梯度直方图描述人体轮廓,配合滑动窗口做分类。对静态人有效,对遮挡和密集排座效果差,而且标注工作量大、泛化能力弱,换一个教室的座椅颜色和背景就要重新调参数。
基于深度学习的检测(YOLO、SSD、CenterNet):训练好的模型直接输出目标框和置信度。YOLO 系在边缘设备上还能跑到实时,是目前教室照明这类项目的默认选择。我一般把 YOLO 作为首选,但不会一上来就上大模型。教室摄像头装在高处俯拍,人体目标在画面里占比不大,用轻量级模型(yolov5s、yolov8n)配合合适分辨率,检测帧率就能维持在 15~30 FPS,对照明控制完全够用。
选型判断的核心不是"哪个算法准",而是"在这个安装位置、这个光照条件下谁更可靠"。检测不是越快越好——照明控制的响应时间在秒级就合格,帧率低一点换 CPU/GPU 占用低,系统才适合部署到普通工控机甚至树莓派上。真到了部署阶段,摄像头画质、安装角度、区域映射这些工程细节对效果的影响,往往比模型本身更大。
2.2 摄像头安装与图像坐标映射:区域划分的前提
照明要按区控制,前提是把图像里的像素坐标映射到教室的实际物理区域。常见做法是把教室沿纵深方向分成 2~3 个照明区(前排、中排、后排),每区对应一组灯,摄像头装在后墙或前墙高处,视角覆盖全场。
这里的关键是图像坐标到地面坐标的映射。摄像头俯视视角下,画面里的点并不均匀对应地面的点——离摄像头近的地方 1 像素对应地面几厘米,远的地方 1 像素可能对应几十厘米。直接按像素坐标画矩形区域,后排区域的划分会被严重切错。正规做法是标定四个角点,用透视变换(getPerspectiveTransform)把画面投影到一个虚拟的俯视平面上,再在这个平面里划区。
我做过的一个部署案例里,摄像头装在教室后墙 2.8 米高处,1080p 画面,覆盖 12 米长的教室。标定过程是:在教室地面四个角落摆四个标记物,记录它们的像素坐标;再量出它们相对某个原点的物理坐标,得到变换矩阵 H。之后每一帧的检测框中心点用 H 变换到地面坐标,再判断落在哪个区。
这个环节最容易被低估。很多同学直接拿检测框的 center_x、center_y 跟图像像素阈值比较,判断人在前排还是后排——图像近大远小导致的前后排划分误差能到 30%。如果是低位摄像头(比如装在前墙 3 米处平视),问题更严重,后排的框只有 20 像素高,几乎没法分辨。坐标系映射不是可选项,是做按区照明的前提。
2.3 曝光调整原理:教室光照变化为什么是检测的隐形杀手
教室是最典型的"光照动态变化"场景:上午靠窗一侧强逆光、拉窗帘后突然变暗、下午西晒、傍晚日光灯与自然光混叠、投影仪把幕布打亮。这些变化直接影响摄像头传感器的曝光,进而影响整个检测链路的可靠性。
曝光调整原理不难说清楚:传感器通过调整曝光时间(shutter)和增益(gain),把场景亮度映射到合理的灰度范围。机器视觉里常见的是分区测光,也就是把画面拆成若干区域分别计算亮度贡献。教室画面里,如果一侧窗户强烈过曝,测光算法可能为了让整帧不过曝,把暗部压得更暗,坐在暗区的人就跟桌椅融到一起了。
实际项目中我建议别依赖摄像头默认的自动曝光,而是做两件事。
固定曝光:在白天正常光照下,把曝光时间、增益、白平衡固定下来,避免摄像头在黄昏时自己拉高增益导致噪声放大、或者突然压暗导致人物轮廓丢失。在 Linux 下可以用 v4l2-ctl 设置,Windows 下走 SDK 接口。
亮度自适应:在检测前先算一下帧的平均亮度,低于某个阈值就认为进入低照度模式,把检测置信度阈值从 0.45 降到 0.3,同时启用更宽松的 NMS。这个逻辑比单纯调摄像头参数要稳。
不要指望一个模型搞定所有光照。做机器视觉应用工程师这几年,我最深的体会是:摄像头出来的原始画面质量,决定了算法的上限。算法再漂亮,画面过曝或欠曝,YOLO 的输出都是废的。所以工程包里一定要留出"画面诊断"模块——实时打印当前帧亮度、对比度和目标框数量,方便现场调试。这一步做好,后面所有控制逻辑才有意义。
3. 照明控制策略与触发逻辑:检测结果如何变成灯的开关
3.1 按区独立的控制状态机:延时、抖动与区域合并
人员检测结果出来后,不能直接送开关。最典型的问题是抖动:一个人在区边界来回走动,或者检测器在几帧之间出现一次丢检,就会导致灯光咔咔地开关。照明控制必须是一个带延时的状态机,而不是简单的 if 语句。
我的状态机设计是三态:每区有"无人、等待确认、有人"三个状态。检测到人进入某区,并不会立刻开灯,而是进入"等待确认",持续 N 秒内仍然检测到人(不要求每一帧都在,允许中间丢几帧),才正式切到"有人"并开灯。反过来,人离开区域后也不是立刻关灯,而是进入"无人"倒计时,比如 5 分钟——避免课间出去上厕所、走到后排拿书这种短时间离开导致灯灭。
三个关键参数值得单独拿出来说。
开灯确认延时(detect_hold_on):一般 3~5 秒。太短会因误检闪灯,太长体验差,学生走进教室到坐下,灯还没亮,这就说不过去了。
关灯延时(turn_off_delay):一般 180~300 秒。学校教室课间 10 分钟,如果设置少于课间时长,学生出去一圈回来灯已经灭了,体验很差。
丢帧容忍(lost_frames):连续丢失多少帧后判定离开。我一般设为 10~15 帧,相当于 0.5 秒左右,能过滤掉检测器单帧抖动。
另一个容易被忽略的点是区域合并。教室后排中间两个区之间,如果人坐在交界处,检测框中心可能在两个区之间反复横跳。处理办法是:以检测框中心点为准,但中心点落在边界 ±0.3 米范围时,同时点亮两个区的灯(贪心策略),人稳定后连续三帧里有两帧落在同一个区,再收敛到单区。这样既避免了临界抖动,又不会长期把两个区都点亮。
3.2 控制链路怎么落地:继电器、Modbus 还是 MQTT
照明控制的执行端有三种常见做法,各有适应面。
继电器组直控:用 GPIO 直接控制继电器模块,点对点控制每组灯的交流接触器。适合实验室原型演示,代码最简单,但走线多、不隔离、维修麻烦,不适合真正的教室改造。
Modbus RTU 继电器:工业上最通用。树莓派或工控机通过 USB 转 485 接一组 Modbus 继电器模块,用 CRC16 + 寄存器地址写线圈。优点是可寻址、抗干扰、接线少,一套 485 总线可以挂 32 个模块。适合没有现成智能照明平台的改造项目。
MQTT 接智能照明网关:校园里如果已有智能照明平台,最优雅的做法是检测模块只负责计算,把"某区有人/无人"发布到 MQTT topic,由下游网关做开灯关灯。这样视觉检测和控制解耦,多间教室可以共用一套平台。
从工程交付角度看,完整系统应该把这三条链路做成可配置的驱动接口,而不是写死一种。我建议默认跑 MQTT,因为调试时可以在电脑上直接订阅 topic 看状态变化,比用万用表量继电器触点直观得多。
MQTT 的 topic 设计也有讲究。不要用单个 topic 发布"教室有人",而是按区域发布:classroom/light/front、classroom/light/middle、classroom/light/rear,payload 用简单的 ON/OFF,再加一个独立的状态 topic 上报每区的小时运行时长,用于节能统计。这样下游无论是脚本、Node-RED 还是第三方网关,都能直接消费。
3.3 与课程表、自然光、紧急照明的联动参数
照明系统不是孤立的系统,需要跟周边环境协调。三个最常见的联动场景。
第一,课程表联动。教室在非上课时间(比如深夜)应该强制进入"无课模式",此时检测到人才开灯,且关灯延时缩短到 60 秒;上课时间则优先按课表常开,检测只做辅助。这个逻辑在代码里是一张 schedule 配置表,按星期几和节次匹配。否则晚自习结束后保洁阿姨进场,灯会自动亮,也会被系统当作"有人"记录。
第二,自然光补偿。如果装了光照度传感器,可以设置一个 illuminance_threshold:某一区自然光超过 400 lux 时,即使检测到人也不开灯,或只开一半灯。这里要注意传感器装的位置——装到窗边会被局部阳光骗到,我一般往教室中线靠内装,并且做 1 分钟均值滤波。
第三,紧急照明联动。消毒灯、应急照明、安防布防这几类不能用同一套自动控制逻辑。应急照明必须物理旁路,自动控制不能接入;消毒灯则要单独时控,绝对不能用"检测到人开灯"的逻辑去控制紫外线灯,这是安全问题。工程包里应该有一个 master kill switch 配置项,一键把系统切成"纯手动"模式。
联动逻辑看起来是业务层的事,但它是这套系统从实验室 demo 走向可交付系统的分水岭。人员检测只能解决"教室有没有人",照明控制系统的价值在于"该不该亮、亮多久、跟什么协调"。
4. 从 zip 工程包到可运行系统:机器视觉照明项目的解压、配置与最小跑通
4.1 源码包的目录结构与配置入口
拿到一个工程源码包.zip,第一件事不是解压就开跑,而是先看目录结构和文档,确认它是不是你当前环境能跑的。一个规范的机器视觉照明控制工程包,目录里至少应该有这几块:
- src/ 或者 app/:主程序,包含摄像头采集、检测推理、区域映射、控制决策四条链路。
- models/:训练好的权重文件,比如 best.pt 或 openvino 导出的 IR 模型。
- config/:YAML 或 JSON 配置文件,所有可调参数集中在这里。
- tools/:辅助脚本,比如区域标定工具、离线视频回放脚本、数据集标注转换脚本。
- requirements.txt:Python 依赖清单。
用 zip 方式分发工程包有个 Windows 上的老坑。工程包如果在 Linux 下压缩,文件名的编码是 UTF-8,Windows 自带的资源管理器解压时如果检测不到 UTF-8 标志,就会把中文文件名解压成一堆乱码。这不是包坏了,是编码问题。解决方法是不要用资源管理器"全部解压缩",改用 7-Zip 的"以 UTF-8 编码解压"选项,或者直接在 WSL 或 Git Bash 里用 unzip 解压。
另一个是关于 zip 伪加密的提醒。如果你拿到的是一个"伪加密"的 zip——压缩包的文件头标记成加密但实际数据没加密——常见的表现是解压时提示需要密码。这类包用 7-Zip 强制解压或者修改本地文件头标记位就能绕过,因为数据本身没加密,所以并不是真正的密码保护。但正规的工程源码包不会用这个技巧分发;真加密的包没有密码就是打不开,任何"密码移除"工具都无能为力。遇到伪加密的压缩包先杀毒再解压,保平安。
4.2 第一遍跑通的最小步骤:环境、模型、摄像头
假设你已解压完成、看到了 src/ 和 models/,下面按最小步骤跑通。
# 1. 创建虚拟环境并安装依赖 python -m venv venv source venv/bin/activate pip install -r requirements.txt # 2. 验证模型文件能正常加载 python -c "from ultralytics import YOLO; m = YOLO('models/best.pt'); print(m.names)"第一段代码里,venv 是为了不污染系统 Python;requirements.txt 里一般包含 opencv-python、ultralytics、pyserial、paho-mqtt 这几个核心包。第二步加载模型后打印 m.names,能看到这个模型训练时定义的类别名列表。如果输出不是 {'0': 'person'} 或者有多个类别,说明模型不是纯人检测模型,相应的置信度阈值和 NMS 参数要按模型实际类别来调。
# detect_and_control.py —— 单帧推理 + 区域映射 + 控制决策的最小闭环 import cv2 import numpy as np from ultralytics import YOLO model = YOLO("models/best.pt") def map_to_zone(center_x, center_y, homography_matrix, zones): # 将检测框中心点用透视变换矩阵映射到地面坐标 point = np.array([[[center_x, center_y]]], dtype=np.float32) ground = cv2.perspectiveTransform(point, homography_matrix) gx, gy = ground[0][0] for zone_name, (x_min, y_min, x_max, y_max) in zones.items(): if x_min <= gx <= x_max and y_min <= gy <= y_max: return zone_name return None cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break results = model(frame, conf=0.4, iou=0.5, verbose=False) for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cx, cy = (x1 + x2) / 2.0, (y1 + y2) / 2.0 zone = map_to_zone(cx, cy, H, zones) if zone: print(zone, "occupied", round(conf, 2))这段代码的核心逻辑是:每一帧先做 YOLO 推理,得到所有人目标的框和置信度;取每个检测框的中心点,用预先标定好的透视变换矩阵 H 映射到地面坐标;再判断地面坐标落在哪个照明分区。
参数说明:conf=0.4 是置信度阈值,低于该值的检测框会被丢弃;iou=0.5 是 NMS 的 IoU 阈值,两个重叠的框大于 50% 时会合并成一个。H 和 zones 应该从 config 文件里加载,实际工程里不会在代码里硬编码。跑通这一步之后,终端会持续打印每个区是否有人。这一步验证的是"检测→映射"链路是否正确。先不要接真实的灯光控制,用屏幕上的输出当模拟开关,确认人走到哪个区就打印哪个区,再往下接 MQTT 或继电器。
4.3 参数调优:置信度、区域阈值、延时秒数的具体取值
我把一套工程里最值得调的参数列成一个表,这些参数直接决定了系统的手感。
| 参数 | 建议范围 | 影响 |
|---|---|---|
| conf(检测置信度阈值) | 0.30~0.55 | 越高漏检越多,越低误检越多;教室场景建议 0.4 起步 |
| iou(NMS 阈值) | 0.40~0.60 | 重叠目标合并的宽松度,0.5 是通用值 |
| detect_hold_on | 3~5 秒 | 开灯前的确认时间,防误检闪灯 |
| turn_off_delay | 180~300 秒 | 人离开后的关灯延时,配合课间时长设定 |
| lost_frames | 10~15 帧 | 连续丢帧后的离开判定 |
| img_size | 640 或 1280 | 推理分辨率;教室大景深场景可以试 1280,帧率降一半但小目标检出率明显提升 |
| zone_border_margin | 0.3 米 | 区域边界附近的双区点亮策略范围 |
置信度阈值 conf 是最需要现场调的。模型在白天、光线好时框出的目标置信度普遍在 0.6 以上,这时 conf=0.55 都行;但到了傍晚或逆光,目标框置信度普遍掉到 0.3 附近,如果阈值还卡在 0.5,大量真实的人会被丢掉。这就是前面说的"亮暗判断 + 动态阈值"的意思:低照度时自动把阈值降到 0.3。
img_size 是另一个副作用明显的参数。YOLO 在 640x640 下推理,一个后排的小个子目标可能只有 60x120 像素,特征不足;升到 1280 后,小目标在特征图上的响应明显变强,但推理耗时可能从 30ms 涨到 120ms。照明控制不需要那么高帧率,所以我的经验是:宁可接受 8 FPS 也要上 1280,只要不低于 5 FPS,控制体验都是连续的。
提示:以上参数值不能靠拍脑袋定,要基于你自己的安装位置和光照条件标定。先把后面讲的离线回放数据集跑起来,再按统计结果反推阈值区间,这比在现场对着实时画面瞎试要快得多。
5. 避坑指南:机器视觉教室照明系统最容易翻车的 5 个地方
5.1 逆光与黄昏时检测率骤降
现象:上午靠窗一侧出大太阳,坐在窗边的学生检测不到;傍晚 17:30~18:30 之间,整个画面发暗,所有目标置信度跌破 0.3,系统判定教室无人,灯全灭了。
原因:窗户的高亮区域主导了自动测光,传感器为了压住窗外天空的过曝,把室内曝光时间缩短,暗部整体被压暗;黄昏时环境光快速下降,自动增益拉满后噪声显著,小目标被噪声淹没。
解决:把摄像头固定在手动曝光模式,以教室中部正常照度为基准设定曝光参数;在代码里加入帧亮度实时统计(cv2.mean(frame)[0]),亮度低于阈值时自动切低置信度模式。还有一个辅助手段是给镜头装遮光罩,避免阳光直射镜头导致内部散射。这个环节靠调算法不如先调画面,画面正常了算法自然正常。
5.2 静止听课的学生被过滤掉
现象:学生坐定 5 分钟后,检测框开始时有时无,10 分钟后几乎全部丢检,系统判定后排无人并关灯。
原因:很多工程包默认启用了 OpenCV 的 MOG2 背景建模或者运动检测辅助逻辑,用来过滤静态目标、降低误报。但这个逻辑把"静止的学生"也过滤了。另一个原因是摄像头采集帧率太低,GPU 资源被占满后推理间隔拉长到几秒,检测结果在时间维度上时好时坏。
解决:检查代码里是否混入了背景或运动检测逻辑,关闭一切"只在目标移动时才输出"的过滤。纯 YOLO 检测本身对静态目标没有衰减问题。如果推理速度不足,把采集帧率降到 5 FPS 恒定抽取,而不是让采集和推理互相积压。
5.3 投影仪幕布被误判为人
现象:教室前排灯无故每节课亮几分钟、灭几分钟,查看日志发现系统持续检测到"人"在前排区域,但现场确认教室前几排实际是空的,也不是保洁或管理员路过。
原因:投影仪开启时,幕布区域亮度高且呈现为亮色矩形,幕布边缘的黑色边框和白色幕面形成强烈的梯度边缘,在特定角度和光照下容易被检测器当成人形目标。这种现象在浅色幕布配深色黑板墙的教室里尤其常见。
解决:先看日志里目标框中心和尺寸,确认误检位置;然后在区域映射里把幕布对应的物理坐标区域加入遮挡列表(ignore_mask),检测框中心落在该区域内时直接丢弃。注意遮挡列表不能覆盖到幕布前方的第一排学生座位区,通常只遮幕布本身那一条带状区域。
5.4 远程控制延迟导致"人走了灯还亮着"
现象:MQTT 指令发出后,下游灯光网关要等 3 秒才执行,加上关灯延时默认 5 分钟,学生 22:00 离开教室,22:08 灯才灭,物业来查发现整夜亮灯。
原因:不是单一故障,是三个延迟叠加:检测端丢帧缓冲区延迟、MQTT 重连往返延迟、网关轮询间隔。任何一个环节卡住,下游都不知道当前真实状态,指令变成了"早晚会执行"而不是"立即执行"。
解决:把检测端设计成只发状态变化事件,而不是周期发全量状态。人从"有人"变"无人"时,立即发一条 force_off 指令并绕过 turn_off_delay。网关侧要把指令标记为"直控模式",不走轮询队列。工程包里如果没做这个 force_off 分支,自己加也不难:在状态机离开"有人"态时,除了常规延时,再提供一个可选的 immediate_off 配置。
5.5 zip 包解压后路径与中文编码问题
现象:工程包在 Windows 下解压后运行报 ModuleNotFoundError,检查目录发现文件名为乱码,zipfile 解压出来的路径带有非法字符,程序根本进不到正确的目录。
原因:工程包在 Linux 用 ZIP 默认编码打包,中文文件名(如"配置文件.yaml")在 Windows 的 GBK 环境下被错误解码;另一种情况是包内用一级目录套一级目录,直接把整个工程目录又包了一层,用户解压后没有进入正确的工作目录,导致 import 全崩。
解决:统一在项目根目录放一个 start.sh 或 start.bat,脚本内部用 cd 切到脚本所在目录再运行 python main.py,避免路径问题;README 第一行写明"用 7-Zip 以 UTF-8 模式解压"。对交付方来说,工程包最好把文件名改成 ASCII 安全命名(如 config_final.yaml),中文只出现在文档里,这是最稳妥的做法。
另外提一个源头上避免的细节:在 Linux 下打包时用zip -r project.zip project/ --no-symlinks,不要用 Windows 自带的"发送到压缩文件夹",那个带 ADS 流和短路径名,在 Linux 服务器上解压时会冒出奇怪的临时文件。
6. 进阶验证:用录制的离线视频回放去验收整套系统
6.1 搭建回放-复判的数据集
现场直接调系统,最大的问题是"今天测试时天气好、光线正,看不出系统上限"。我在交付前都会做一步离线回放:把摄像头录制的 3~5 段不同时段的视频(早课、午后、黄昏、晚间开灯、投影仪开启)存成 mp4,然后在离线模式下跑一遍检测,把每一帧的检测结果落盘。
回放的工程价值在于可复现。现场测试时改一个参数,要等第二天同一光照条件才能验证,离线视频不存在这个问题。具体做法是写一个复检脚本,把视频按每秒 5 帧抽帧,跑检测框架后输出检测框和判定区域到 CSV,再导入到标注工具里跟真实情况对比。这一步能把你从"在现场等太阳"里解放出来。
6.2 计算虚警率、漏检率与控制准确度
回放验收的最后指标,我会算三个数。
漏检率:应该有人但没有输出"有人"事件的帧占比,目标不超过 5%。虚警率:没人却输出"有人"事件的帧占比,目标不超过 2%。控制准确度:把系统输出的开灯/关灯事件和人工标注的"真实应开/应关"对比,计算匹配率。
这里有个我自己的习惯:自动化指标只做筛选,真正的验收动作是随机抽 3 段长视频,每段 10 分钟,人工标注一遍,然后和系统日志比对,找出所有不一致的时间点。原因是 CSV 统计会把"人坐在区域边界导致双区同亮"这种单帧抖动算成虚警,但真实体验里双区同亮是可以接受的。人工复判能区分出"不能忍的错误"和"设计内的妥协"。
结尾说点实在的。做这套系统最深的教训是:一半以上的问题不是出在算法,而是出在画面质量、区域映射和控制链路的可靠性上。曝光不修、坐标系不标,YOLO 调得再细也是白搭。环境、参数、状态机这些偏工程的细节,反而决定了系统能不能从 demo 变成真正在教室里跑一年的产品。如果你正准备照着这个方向做,先把摄像头装好、画面调好,再把控制延时配好,最后才回头精细调模型。希望帮到你。
本文还有配套的精品资源,点击获取