用YOLOv8构建校园能耗识别系统:从训练到部署全流程
简介一套基于YOLOv8的校园能耗智能检测项目面向计算机相关专业学生、毕业设计及课程设计开发者。项目代码已测试通过包含完整源码、数据集、可视化界面和部署说明可直接运行并生成核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图功能完善且操作简单。资源包共8个文件压缩包约15.91MB。其中3个Python脚本分别对应模型训练、视频检测与可视化页面3个pt文件为预训练权重与训练成果另有2个txt文件提供部署说明与运行指引目录结构清晰便于按需调用。项目源自作者个人毕业设计代码运行成功后才上传可帮助答辩评审直观理解实现效果作为完整参考方案具有较强说服力。目前已有28人学习下载适合需要快速搭建目标检测项目的在校学生、老师或企业员工参考学习也可在现有代码基础上二次开发以扩展其他功能。资源仅供学习参考请勿用于商业用途。1. 校园能耗识别是什么一个识别 统计 面板的三层系统很多人第一次看到这类项目名第一反应是“又要装一堆传感器”。实际做完你会发现这套方案的核心并不是硬件而是让 基于YOLOv8 的目标检测模型去自动识别教室、实验室、办公室里的用电设备——电脑显示器、空调、打印机、投影仪、照明灯——然后把这些识别结果变成一张张可直接汇报的图表。它适合的场景很明确你在做毕设或课程设计需要“能跑、能演示、能写进论文”的完整系统而手头没有真实传感器数据只有摄像头或视频文件。这套系统的价值在于把三个原本割裂的东西串成一条链路模型负责“看到”设备脚本负责“统计”设备运行状态Web 面板负责“展示”能耗趋势。整个链路不用改电表、不用接线视频进去就能出报表。本文按这条链路往下拆先讲清楚 YOLOv8 为什么适合做这件事再带你从头训练一个设备识别模型接入可视化界面最后把所有部署时容易翻车的地方集中排一遍。2. YOLOv8 为什么适合这个项目从模型结构到推理链路2.1 一个模型文件就能扛起整个识别任务YOLOv8 属于一阶段目标检测器一次前向传播同时输出目标的类别和位置。它把输入图片划分成网格每个网格预测若干个候选框再经过非极大值抑制NMS去掉重复框。相比两阶段检测器它在 CPU 上的推理速度更友好这对毕设演示环境很重要——很多人的电脑根本没有独立显卡。模型结构上值得关注的两个设计是 C2f 模块和 Anchor-Free 检测头。C2f 把输入特征拆成多条分支做密集连接在增加感受野的同时控制了计算量比老版本的 C3 模块更适合实时场景。检测头则从 Anchor-Based 改成了 Anchor-Free直接回归目标中心点到四条边的距离省去了设计锚框尺寸的超参数。换句话说你不必针对“空调比显示器大很多”这种尺寸差异手工调锚框模型会自动适应。一个典型的 YOLOv8 配置文件中网络结构大概长这样# yolo8s.yaml 关键段 backbone: - [-1, 1, Conv, [64, 3, 2]] # 下采样 - [-1, 1, Conv, [128, 3, 2]] # 下采样 - [-1, 3, C2f, [128, True]] # 主干基础块 - [-1, 1, Conv, [256, 3, 2]] - [-1, 6, C2f, [256, True]] - [-1, 1, Conv, [512, 3, 2]] - [-1, 6, C2f, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] - [-1, 3, C2f, [1024, True]] # 最深层特征 - [-1, 1, SPPF, [1024, 5]] # 空间金字塔池化 head: - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 6], 1, Concat, [1]] - [-1, 3, C2f, [512]] # PAN-FPN 路径聚合 - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 4], 1, Concat, [1]] - [-1, 3, C2f, [256]] - [-1, 1, Detect, [nc]] # nc 是类别数参数说明C2f 中的 True 表示是否使用残差连接数字代表该层输出的通道数SPPF 的 5 代表池化核尺寸Detect 后面的 nc 是数据集类别总数训练时由数据配置自动填入。这段配置一般不需要手动改你只需要决定用 n、s、m、l、x 哪个规格——对校园能耗识别这种类别少、目标相对固定的任务s 规格在速度和精度上最均衡。2.2 从摄像头到能耗报表的四层架构把模型放进系统里整体是四层结构。第一层是输入源可以是摄像头、视频文件或图片目录第二层是推理层YOLO 模型对每一帧做检测第三层是统计层把检测结果按设备类别计数、累加在线时长第四层是展示层用 Web 面板输出实时画面和统计图表。这是一条很常见的做法——把检测和统计解耦。为什么不能直接在模型输出层做统计因为模型输出的是“某一帧里有什么设备”而能耗管理要的是“这台设备开了多久、今天一共运行了几小时”这需要跨帧累积。如果混在一起你每次改统计口径都要重新跑模型实际开发中非常痛苦。我一般会把推理代码写成独立模块对外只暴露一个函数输入一帧图像返回检测框列表和类别列表。统计逻辑单独写这样两边都能单独调试。下面是推理模块的最小实现也是整个项目的地基from ultralytics import YOLO # 加载训练好的权重设备可选 cpu 或 cuda:0 model YOLO(runs/detect/train/weights/best.pt) def detect_frame(frame, conf_thres0.5, iou_thres0.6): 对单帧图像做检测。 参数 frame: BGR 格式的图像numpy 数组 conf_thres: 置信度阈值低于该值的框会被丢弃 iou_thres: NMS 的 IoU 阈值重叠超过该值则保留置信度更高的框 返回 boxes: 每个检测框的 xyxy 坐标 labels: 每个检测框对应的类别 id results model.predict( frame, confconf_thres, iouiou_thres, verboseFalse # 关闭命令行日志方便界面展示 ) boxes results[0].boxes.xyxy.cpu().numpy() labels results[0].boxes.cls.cpu().numpy().astype(int) return boxes, labels逻辑说明predict 方法内部完成了预处理、推理、NMS 全部流程返回的 Results 对象里boxes.xyxy 是归一化后的坐标cls 是类别编号。这里显式指定 conf 和 iou 而不是用默认值是为了防止在界面演示时把低置信度的误检框也画上去。conf_thres 设得越高误报越少但漏检可能增加对空调这种大面积目标可以设 0.5对小目标灯具建议降到 0.4 再观察。参数调整建议如果你的摄像头画面里有大量远处的小设备先别急着降 conf优先检查训练数据里是否包含了“小目标样本”。模型对小目标漏检通常是训练数据里小目标占比不够而不是阈值设置问题。2.3 推理性能与运行环境的基本判断一个毕设项目能不能现场演示流畅很大程度取决于推理帧率而不是模型理论精度。YOLOv8s 在普通 CPU 上处理 640x640 的图片单帧耗时大约在 200 到 500 毫秒之间。如果只是对视频文件离线处理这个速度完全够用如果是摄像头实时预览建议把推理帧率限制在每秒 1 到 2 帧否则界面会卡到无法操作。限制帧率的常见做法是丢帧——即检测线程只处理最新的一帧跳过中间帧。这个策略比用 time.sleep 更合理因为摄像头缓存里的旧帧会被及时清空画面延迟不会累积。从统计角度说一秒检测 1 次已经足够判断设备是否在线因为设备开关状态的变化频率远低于这个水平。实测经验是把输入分辨率从 640 降到 416推理时间可以减少约四成而能耗设备这类大目标的 mAP 下降不到 1%这是一个值得做的取舍。3. 准备一个能用的设备识别模型数据标注、格式与训练参数3.1 数据采集原则先拍够再标注很多人在公开数据集上直接训练完就去跑界面结果换到自己校园摄像头画面里几乎什么都识别不出来这是场景差异导致的——公开数据里的会议室和你教室的灯光角度、摄像头高度完全不同。所以正确的顺序是先采集自己场景的图片再训练最后再考虑用公开数据做预训练权重。一个可用的校园设备数据集至少要覆盖 4 类显示器、空调室内机、灯具、投影仪。采集时有三条原则。第一同一个设备至少从三个角度拍正面、侧面、斜上方因为巡检摄像头通常是俯视角度。第二把一天中不同时间的光照都采样到傍晚和拉窗帘时的光线差异很大模型很容易因为阴影翻车。第三每类设备样本量不要低于 300 张如果是多实例场景一张图里有三台显示器一张图能贡献多个标注框总体样本会更充足。标注时不要只框设备的“核心区域”。显示器的整个屏幕区域都应该框进去而不是只框 logo 或边框空调室内机要框完整的长条形状不要截掉一半。标注框边界过紧或过松都会让模型学到的目标形状出现偏差这也是新手最容易忽略的细节。3.2 把标注结果转成 YOLO 格式转换脚本与四个边界坑常见标注工具导出的是 COCO 格式的 JSON 文件而 YOLO 训练需要每个图片对应一个同名 txt 文件每行是“类别id 中心点x 中心点y 框宽 框高”所有值都归一化到 0 到 1。转换脚本并不复杂但边界坑不少。import json import os def coco_to_yolo(coco_json_path, output_dir): 将 COCO 格式标注转换为 YOLO 格式。 coco_json_path: 标注文件路径 output_dir: 输出的 label txt 目录 with open(coco_json_path, r, encodingutf-8) as f: coco json.load(f) os.makedirs(output_dir, exist_okTrue) # 建立 image_id 到图片信息的映射 img_info {img[id]: img for img in coco[images]} # 建立 image_id 到标注列表的映射 annos {} for ann in coco[annotations]: annos.setdefault(ann[image_id], []).append(ann) for image_id, img in img_info.items(): width img[width] height img[height] txt_path os.path.join(output_dir, img[file_name].replace(.jpg, .txt).replace(.png, .txt)) lines [] for ann in annos.get(image_id, []): x, y, w, h ann[bbox] # COCO 的 bbox 是左上角坐标加宽高 cx x w / 2.0 cy y h / 2.0 # 归一化到 [0, 1] cx / width cy / height w / width h / height # 坑1框越界导致训练报错 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) lines.append(f{ann[category_id] - 1} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) print(f转换完成共处理 {len(img_info)} 张图片)逻辑说明COCO 的 bbox 是绝对像素坐标YOLO 需要的是相对坐标所以必须除以图片宽高。坑点集中在四个地方。第一category_id 从 1 开始编号但 YOLO 类别 id 从 0 开始记得减 1。第二标注框可能略微超出图片边界转换时要把坐标裁剪到 0 到 1 之间否则训练时数据加载器会报“标签越界”错误。第三txt 文件名必须与图片文件名完全一致包括大小写。第四空的 txt 文件要删除YOLO 会把“没有标注文件的图片”当成背景图参与训练而不是报错这会导致模型莫名其妙学到错误特征。转换完成后用下面这条命令快速检查一遍每个 txt 的行数是否合理如果绝大多数 txt 都是空的说明 JSON 里 categories 和 images 的字段名匹配出了问题find labels -name *.txt | xargs wc -l | sort -n | head -203.3 训练命令与配置文件参数表数据集目录按“images / labels train / val”的结构组织然后写一个数据配置文件YOLOv8 会按这个文件去寻找图片和标注# campus_energy.yaml path: /data/campus_energy # 数据集根目录 train: images/train val: images/val names: 0: monitor # 显示器 1: aircon # 空调室内机 2: lamp # 灯具 3: projector # 投影仪训练命令本身非常短真正的门道在超参设置上yolo detect train \ datacampus_energy.yaml \ modelyolov8s.pt \ epochs100 \ batch16 \ imgsz640 \ patience20 \ devicecpu参数说明model 指定预训练权重yolov8s.pt 是从公开数据集上预训练好的初始权重能大幅缩短收敛时间。epochs 设 100 是合理起点但 patience20 意味着连续 20 轮验证集精度没提升就提前停止防止过拟合。batch16 是显存或内存的折中值如果你的机器内存小于 16GB改成 8 更稳妥。imgsz640 保持默认不需要为了追求精度调到 1280能耗设备目标是“大的大、小的小”640 对这类场景是性价比最高的分辨率。训练过程中重点观察两个文件results.png 里的 loss 曲线和 confusion_matrix.png 里的混淆矩阵。如果 val 损失在 40 轮后开始上升而 train 损失还在下降就说明过拟合了此时应该减少 epochs 或增加数据增强。混淆矩阵里如果“显示器”经常被误认为“灯具”大概率是标注时两类设备在小图里视觉特征过于接近需要补充更多侧视角度的数据而不是调整训练参数能解决的。4. 在可视化界面里接上模型检测框、统计卡片与趋势图4.1 为什么用 Web 面板而不是桌面程序毕设项目里可视化界面最常见的两个选择是 PyQt5 和 Streamlit。我的建议是无脑选 Streamlit理由是基于实操效率PyQt5 要处理窗口线程、信号槽、布局管理器写一个带视频预览和图表更新的窗口至少要多花三天Streamlit 用纯 Python 脚本写界面刷新机制由框架管理部署时只需要一条命令启动浏览器访问即可。更关键的是Streamlit 自带组件生态图表可以直接用 Plotly 渲染不需要额外写前端代码。这对“简单部署即可运行”的定位非常契合——评审老师打开浏览器就能看到界面不需要装任何额外软件。采用 Streamlit 时要注意它的运行机制是“脚本从上到下重新执行”所以视频流对象不能直接存在全局变量里要放进 st.session_state 才能跨刷新保持摄像头会话。4.2 实时检测流与统计卡片的核心代码下面这段代码是实现“视频文件 / 摄像头输入 → 检测 → 展示”的最小闭环也是整个可视化界面的主干import streamlit as st import cv2 from collections import defaultdict from ultralytics import YOLO # 加载模型使用 st.cache_resource 避免每次刷新重复加载 st.cache_resource def load_model(): return YOLO(runs/detect/train/weights/best.pt) model load_model() st.title(校园能耗智能识别系统) # 设备类别映射必须与训练配置一致 class_names [显示器, 空调, 灯具, 投影仪] cap cv2.VideoCapture(0) # 0 为默认摄像头视频文件则填路径 # 用 session_state 跨刷新保存统计结果 if device_counter not in st.session_state: st.session_state.device_counter defaultdict(int) placeholder st.empty() while True: ret, frame cap.read() if not ret: break frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 推理conf 设 0.4 兼顾小目标灯具 results model.predict(frame_rgb, conf0.4, verboseFalse)[0] # 统计当前帧各类别数量 for cls_id in results.boxes.cls.cpu().numpy().astype(int): st.session_state.device_counter[cls_id] 1 # 画检测框和类别标签 annotated results.plot() placeholder.image(annotated, use_container_widthTrue)逻辑说明st.cache_resource 是关键——模型加载是耗时操作如果不加这个装饰器每次页面刷新都会重新加载模型界面会卡到像死机一样。设备计数器放在 st.session_state 里是为了让计数在页面刷新后依然保留。results.plot() 是 YOLO 自带的可视化方法会直接在原图上画框、写类别和置信度省去自己调 cv2.rectangle 的工作。循环里的 placeholder 每次刷新用新的画面替换旧画面实现视频预览效果。这里有一个性能和显示的逻辑Streamlit 的刷新机制无法做到 30 帧流畅视频所以这个循环里必须加一个“限流”操作。常见做法是每推理 3 帧才更新一次画面或者固定 sleep 0.5 秒。不要试图跑满 fps画面会变得极其卡顿而且 CPU 占用飙高后鼠标操作页面都会失灵。4.3 能耗趋势图与设备运行时长统计检测计数只解决了“现场有哪些设备”而能耗报表需要的是“这些设备今天累计运行了多久”。这个统计逻辑要落在时间维度上。下面这段代码把每一帧的检测结果按分钟归组再聚合成设备运行时长曲线import pandas as pd import plotly.express as px import time # 记录当前帧时间按分钟聚合 def update_device_runtime(counter_dict, timestampNone): 将按帧累计的设备出现次数折算成运行时长。 假设每秒检测2帧则每2帧代表1秒在线时长。 if timestamp is None: timestamp time.strftime(%Y-%m-%d %H:%M:%S) # frame_based_runtime: 单位为“帧数 / 2” runtime_minutes {k: v / 2 / 60 for k, v in counter_dict.items()} df pd.DataFrame([ {设备: class_names[k], 运行时长(分钟): round(v, 1)} for k, v in runtime_minutes.items() ]) return df df update_device_runtime(st.session_state.device_counter) fig px.bar(df, x设备, y运行时长(分钟), color设备) st.plotly_chart(fig)逻辑说明这个统计是近似值前提是你固定了每秒检测帧数。如果你用前面丢帧方案每秒检测帧数是告诉了多少帧但“视频帧到底对应现实中多久”这个问题很难精确回答。我一般会再做一层简化处理只统计“设备是否连续出现在连续的 N 次检测中”如果连续超过阈值才认为设备在线。这个逻辑能滤掉误检抖动——一个显示器偶尔闪出两帧不应算作开机连续稳定出现 30 秒以上才是可信的在线状态。阈值通常设 15 次连续检测对应约 15 秒的真实时间。趋势图展示时有一个小细节Plotly 默认的配色在投影仪上会很淡建议提前用颜色序列指定“显示器蓝色、空调红色、灯具黄色、投影仪紫色”这样现场演示时远处的观众也能分清不同设备。这类细节虽然不影响功能但直接影响答辩观感。5. 部署排雷手记界面跑不起来的五个真实原因5.1 摄像头一打开界面就卡住或黑屏现象Streamlit 页面一直转圈或者打开后是黑屏终端里没有任何报错。原因摄像头索引 0 被系统其他进程占用或者笔记本摄像头实际索引是 1。另一种常见情况是 OpenCV 在 Linux 下读摄像头需要 v4l2 驱动缺少依赖时会静默返回空帧而不是抛异常。解决把摄像头的打开方式改成“先探测、后使用”def open_camera(index0): cap cv2.VideoCapture(index) if not cap.isOpened(): cap cv2.VideoCapture(1) # 尝试第二个索引 if not cap.isOpened(): # 退回视频文件确保演示不中断 cap cv2.VideoCapture(demo.mp4) return cap视频文件作为兜底非常实用现场设备一旦连不上至少能播放一段预录好的检测演示视频不会冷场。5.2 检测速度慢到本地视频都跑不动现象页面能显示画面但画面更新间隔长达数秒设备状态变化完全跟不上。原因最常见的是三件事同时叠加——输入分辨率 640、用的是 yolov8m 或更大规格模型、没有做帧率限制。也可能是在 CPU 上跑却加载了为 GPU 优化的版本导致线程开销变大。解决三个手段并行。第一步换 yolov8s.pt 权重精度下降有限、速度提升明显。第二步把推理分辨率降到 416。第三步加一个明显的丢帧逻辑frame_interval 0.5 # 每 0.5 秒处理一帧 while True: ret, frame cap.read() if not ret: break if time.time() - last_time frame_interval: continue # 跳过中间帧 last_time time.time() # 其余检测逻辑不变这段代码的坑在于 ret 读取必须始终执行不能因为丢帧就不读取视频流否则缓存堆满后视频源会断流。也就是说“读帧”和“检测帧”必须分离——帧要读只是不送去检测。5.3 界面里中文显示成方块现象标题和设备名称在网页里全变成方块或乱码但英文正常。原因Streamlit 网页本身支持中文但 Plotly 默认字体在部分 Linux 部署环境里缺少中文字形渲染中文就直接 fallback 成方块。解决在绘图前显式指定中文字体。代码里加一行px.defaults.template plotly px.defaults.width 800 px.defaults.height 400如果仍然乱码则不用中文字段做图表标签改用英文标签Monitor、AC、Lamp、Projector页面标题保留中文。这个折中方案对答辩展示完全够用且不会因为字体问题打乱部署节奏。5.4 换一台电脑后模型预测结果变差现象在自己电脑上训练测试都很理想换一台电脑运行界面后检测框明显变少或者置信度普遍下降。原因不同摄像头的位置、角度、分辨率与训练数据不一致模型对“新场景”的泛化能力不足。这不是代码问题是数据集覆盖不够。另一个隐藏原因是新电脑上没有安装与训练时相同的 YOLO 版本推理代码行为出现细微差异。解决如果是数据集问题快速补救办法是收集新电脑摄像头的 30 到 50 张画面用已有权重做伪标注人工修正后微调 20 轮。如果只是为了演示我一般建议把部署环境固定——训练和演示用同一台电脑、同一个摄像头方向这也是毕设 demo 最稳妥的方式。关于版本问题部署时写死依赖版本是基本功不能图省事装最新版。5.5 训练时准确率很高但统计的能耗数据明显不对现象模型对每一帧检测都准确但仪表盘上空调“运行时长”显示为几分钟甚至几秒和实际开了一小时严重不符。原因统计逻辑只数了“设备出现的帧次数”没有和真实时间建立换算关系。你按每秒 2 帧检测但实际可能因为机器卡顿掉帧每秒只处理了 0.5 帧时长就多算了一倍。解决不要用帧数折算时间改用真实时间戳。每次检测时记录当前系统时间统计从“第一次检测到该设备”到“最后一次检测到该设备”的时间差这才是设备在线时长。我的习惯是每次检测后只更新“该设备最近一次出现时间”每 30 秒做一次判断如果此时间距超过 60 秒就认为设备离线离线前的持续时间累加进报表。用时间戳不仅更准确还天然不受机器卡顿影响。6. 把它做成可信的能耗数据两个验证技巧与一个提速习惯先聊验证技巧。第一个技巧是给检测结果设“双阈值审核”机制——识别阈值和统计阈值分开。视觉界面上你可以用 conf0.3 让检测框尽量多显示但统计模块必须用 conf0.6 以上才记入运行时长。这么做的原因是界面是用来给人看的多一点误检框没关系人眼能分辨统计结果是要写进论文的必须尽量只用高置信度样本。实测下来把统计阈值从 0.3 提到 0.6一类设备的误统计次数能减少约八成而漏统计的设备基本都是与摄像头距离过远的小目标对整体趋势影响有限。第二个技巧是引入“连续帧确认”逻辑设备对象只有连续出现 3 帧以上才被记为在线单帧的闪烁检测直接忽略。这个逻辑可以过滤掉画面抖动和短暂遮挡带来的误检。再聊提速习惯。我做完这个项目后形成了一个固定习惯无论如何都会把模型导出成 ONNX 格式再做部署。YOLOv8 导出 ONNX 只需要一行命令yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640导出后用 ONNX Runtime 推理CPU 上的速度比直接跑 PyTorch 模型快约 30% 到 50%。更重要的是导出的 ONNX 文件是纯推理图不依赖训练框架版本换一台部署机器时不用担心 PyTorch 版本不匹配导致的 API 变动问题。推理代码也简洁很多import onnxruntime as ort sess ort.InferenceSession(best.onnx) input_name sess.get_inputs()[0].name outputs sess.run(None, {input_name: preprocessed_frame})这段代码的输出格式与训练框架输出略有差异需要做一次坐标映射但换来的是部署侧极大的稳定度。对于以“简单部署即可运行”为卖点的项目这一步几乎是我推荐的必做项。最后说一个踩出来的教训统一的设备类别字典要在项目一开始就定好训练配置文件、界面显示、统计报表三处永远共用同一个类名列表。我曾经在统计模块里自己手写了一遍类别名结果训练时顺序是“显示器、空调、灯具、投影仪”统计模块写成了“空调、显示器、投影仪、灯具”一整个星期数据全是错的。别相信记忆力哪怕只有四个类别也建立一个共享配置类。希望这套从模型到界面再到排错的完整链路对你有帮助按这个流程走你的毕设或课设项目能扎实跑起来。本文还有配套的精品资源点击获取