☰
基于YOLOv8的校园能耗检测:数据集、训练与部署全流程
2026/9/26 16:22:19 网站建设 项目流程

简介:这份资源面向计算机、人工智能、自动化等专业的在校学生与教师,提供一套可直接运行的YOLOv8校园能耗智能检测项目,适合作为毕业设计、课程设计或大作业的完整方案,也便于初学者进阶学习。压缩包共8个文件,包含3个Python脚本、3个模型权重文件与2个说明文本,整体约15.91MB,涵盖模型训练、可视化界面与视频检测等核心模块,部署流程简单,按说明操作即可跑通。项目已产出核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图,便于答辩时直观展示实验效果。目前已有28人学习下载,代码均经测试运行成功,拿来即可使用,也可在此基础上修改扩展实现其他功能,是毕设与课设场景下省时省力的参考资源。

1. 从一份校园能耗数据集说起:YOLOv8 做能耗智能到底在做什么

校园能耗智能这个词听起来像物联网平台的事,但真正落到毕设或课程设计里,绝大多数人卡在第一步:数据从哪来、怎么让模型"看见"能耗异常。我拿到这个标题时第一反应不是去搭什么大平台,而是先确认一件事——它用的是 YOLOv8,说明核心是视觉检测,不是时序预测。也就是说,所谓"能耗智能",实际做的是从摄像头或巡检图像里识别出空调外机、照明灯具、电表箱、水管阀门这些设备的状态,再据此判断哪些区域在非工作时段仍在耗电。

这个定位很关键。如果你按传统能耗监测系统去做,得接电表、传数据、搭后台,一套下来没有两三个月根本跑不通。但基于 YOLOv8 的方案把问题转化成了目标检测:数据集里标注好设备类别和开关状态,训练一个模型,推理时框出设备并给出状态标签,前端可视化界面把结果叠加到画面上,能耗异常就变成了"该关没关"的视觉告警。适合谁?适合毕设周期只有八到十二周、手头没有真实电表数据、但能拍到校园设备照片的本科生和课程设计学生。源码、可视化界面、完整数据集、部署教程这四样东西凑齐,意味着你不需要从零标注,改改类别就能跑自己的场景。

2. YOLOv8 校园能耗检测的选型逻辑与数据准备

2.1 为什么是 YOLOv8 而不是分类网络或时序模型

能耗异常检测有两条路:一条是时序异常检测,用 LSTM 或 Transformer 分析电表读数;另一条是视觉检测,用目标检测网络识别设备状态。前者精度高但依赖硬件采集,后者精度稍逊但部署门槛极低。毕设场景下,你没有真实的智能电表,也没有历史用电曲线,时序模型根本无从训练。而 YOLOv8 只需要你有一批标注好的设备图片,就能训出一个可用的检测器。

YOLOv8 相比 YOLOv5 的优势在于:一是 anchor-free 解耦头,小目标检测更稳,校园场景里远处的空调外机、走廊尽头的灯管都属于小目标;二是训练脚本更简洁,ultralytics 库把数据加载、增强、学习率调度都封装好了,你只需要准备 YAML 配置文件;三是导出格式丰富,onnx、tensorrt、openvino 都支持,后面想部署到 RK3588 或边缘盒子也有路可走。GTX 1660 Ti 这种级别的显卡跑 YOLOv8n 或 YOLOv8s,batch size 设 8 到 16,训练 100 轮大概两三个小时,完全在毕设可接受范围内。

2.2 校园能耗数据集的类别设计与标注规范

数据集的质量直接决定模型能不能用。我见过太多人拿网上的通用数据集硬套,结果模型连电表箱和配电柜都分不清。校园能耗场景的类别设计要围绕"能耗相关设备 + 状态"来定,常见做法是分两大类:设备类别和状态类别。设备类别包括空调外机、照明灯具、电表箱、水管阀门、插座排插;状态类别包括开启、关闭、异常闪烁。但这样类别数会膨胀,更实用的做法是把状态作为类别的一部分,比如"空调_开启""空调_关闭""灯具_开启""灯具_关闭",总共八到十二个类。

标注用 Labelme 或 LabelImg 都行,导出 YOLO 格式的 txt。每张图对应一个 txt,每行格式是class_id x_center y_center width height,坐标全部归一化到 0 到 1。这里有个血泪经验:标注框不要贴边,留出至少两个像素的余量,否则 YOLOv8 在 mosaic 增强时会把框裁掉一半,训练出来的模型对边缘目标特别不敏感。另外,同一张图里如果设备密集,标注顺序不影响训练,但类别 ID 必须和 YAML 里的 names 列表严格对应,错一个位模型就全乱。

2.3 用 Python 脚本把 Labelme 标注转成 YOLOv8 可读格式

Labelme 默认导出 JSON,需要转成 YOLO 的 txt。下面这个脚本我用了很多次,处理过几千张图,稳定可用。

import json import os import glob # 类别映射,必须和 data.yaml 里的 names 顺序一致 class_mapping = { "air_conditioner_on": 0, "air_conditioner_off": 1, "light_on": 2, "light_off": 3, "meter_box": 4, "water_valve": 5 } def labelme_to_yolo(json_path, output_dir): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) img_w = data['imageWidth'] img_h = data['imageHeight'] lines = [] for shape in data['shapes']: label = shape['label'] if label not in class_mapping: continue # 跳过未定义类别,避免训练时报错 points = shape['points'] x_coords = [p[0] for p in points] y_coords = [p[1] for p in points] # 计算归一化中心点和宽高 x_center = (min(x_coords) + max(x_coords)) / 2.0 / img_w y_center = (min(y_coords) + max(y_coords)) / 2.0 / img_h width = (max(x_coords) - min(x_coords)) / img_w height = (max(y_coords) - min(y_coords)) / img_h # 裁剪到 0-1 范围,防止标注越界导致训练崩溃 x_center = max(0, min(1, x_center)) y_center = max(0, min(1, y_center)) width = max(0, min(1, width)) height = max(0, min(1, height)) lines.append(f"{class_mapping[label]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") base_name = os.path.splitext(os.path.basename(json_path))[0] out_path = os.path.join(output_dir, base_name + '.txt') with open(out_path, 'w') as f: f.write('\n'.join(lines)) # 批量转换 json_files = glob.glob('./labelme_jsons/*.json') os.makedirs('./labels', exist_ok=True) for jf in json_files: labelme_to_yolo(jf, './labels') print(f"转换完成,共处理 {len(json_files)} 个文件")

这段代码的逻辑很直白:读 JSON、取多边形顶点、算外接矩形、归一化、写 txt。关键参数是class_mapping,它决定了类别 ID 的分配,必须和后续data.yaml里的names列表一一对应。另一个注意点是坐标裁剪,Labelme 有时候会因为图片旋转或标注手抖导致坐标超出图像边界,不裁剪的话 YOLOv8 在计算损失时会出 NaN。转换完成后,建议随机抽十张图用cv2.rectangle画出来肉眼检查一遍,确认框的位置和类别都对。

2.4 data.yaml 的写法与数据集目录结构

YOLOv8 靠一个 YAML 文件找到图片和标签。目录结构建议这样组织:

campus_energy/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

data.yaml内容如下:

path: ./campus_energy train: images/train val: images/val test: images/test nc: 6 names: 0: air_conditioner_on 1: air_conditioner_off 2: light_on 3: light_off 4: meter_box 5: water_valve

nc是类别数,必须和 names 的长度一致。train、val、test 的路径是相对于path的。划分比例我一般按 7:2:1,如果数据量少于 500 张,就 8:1:1,验证集太小会导致 mAP 波动大,看不出真实效果。

3. 训练 YOLOv8 校园能耗检测模型:参数、命令与可视化

3.1 环境配置与 ultralytics 安装的版本坑

环境配置是第一个翻车点。网上教程有的让你装ultralytics,有的让你克隆 YOLOv8 仓库,两种方式都能跑,但依赖冲突的概率不一样。我一般直接用 pip 装 ultralytics,命令如下:

conda create -n yolov8_energy python=3.9 -y conda activate yolov8_energy pip install ultralytics==8.0.200 pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117

这里锁版本是有原因的。ultralytics 8.0.200 之后的版本对data.yaml的路径解析做过调整,如果你用相对路径,新版可能找不到文件。torch 1.13.1 配 cu117 在 GTX 1660 Ti 上实测稳定,再新的 torch 2.x 在部分老驱动上会报 CUDA 初始化失败。装完之后跑yolo checks确认环境,输出里 CUDA 那一行显示可用就行。

3.2 训练命令与关键参数怎么调

训练命令本身很短:

yolo detect train \ data=./campus_energy/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=12 \ lr0=0.01 \ patience=30 \ device=0 \ project=./runs_energy \ name=exp1

参数逐个说。model=yolov8s.pt是预训练权重,校园能耗数据集通常不大,用 s 版本比 n 版本精度高,比 m 版本训练快。epochs=150配合patience=30,意思是 30 轮验证集 mAP 不提升就早停,避免过拟合。imgsz=640是标准输入尺寸,如果你的图片里设备特别小,可以提到 1280,但显存占用会翻四倍,1660 Ti 的 6G 显存扛不住,得降到 batch=4。lr0=0.01是初始学习率,YOLOv8 默认用 SGD 加余弦退火,这个值对大多数场景都适用,如果 loss 震荡厉害就降到 0.005。batch=12是 6G 显存下的安全值,再大就 OOM。

训练过程中终端会实时打印每轮的 box_loss、cls_loss、dfl_loss 和 mAP50。box_loss 负责框的位置,cls_loss 负责类别,dfl_loss 是分布焦点损失,三个都下降才说明模型在正常学习。如果 cls_loss 一直不降,大概率是类别标注有问题,回去检查 txt 里的 class_id 有没有越界。

3.3 用训练日志画损失曲线和 mAP 曲线

YOLOv8 训练完会在runs_energy/exp1/下生成results.csv,里面记录了每轮的损失和指标。画曲线用 pandas 加 matplotlib 就行:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('./runs_energy/exp1/results.csv') df.columns = df.columns.str.strip() # 列名可能有空格,必须去掉 fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 损失曲线 axes[0].plot(df['epoch'], df['train/box_loss'], label='box_loss') axes[0].plot(df['epoch'], df['train/cls_loss'], label='cls_loss') axes[0].plot(df['epoch'], df['train/dfl_loss'], label='dfl_loss') axes[0].set_xlabel('Epoch') axes[0].set_ylabel('Loss') axes[0].legend() axes[0].set_title('Training Loss') # mAP 曲线 axes[1].plot(df['epoch'], df['metrics/mAP50(B)'], label='mAP50') axes[1].plot(df['epoch'], df['metrics/mAP50-95(B)'], label='mAP50-95') axes[1].set_xlabel('Epoch') axes[1].set_ylabel('mAP') axes[1].legend() axes[1].set_title('Validation mAP') plt.tight_layout() plt.savefig('./training_curves.png', dpi=150)

这段代码的关键是df.columns.str.strip(),YOLOv8 导出的 CSV 列名有时带前导空格,不处理的话KeyError直接报错。画出来的图里,正常收敛的曲线应该是 box_loss 快速下降后趋于平缓,mAP50 在 50 轮左右达到 0.8 以上。如果 mAP50 卡在 0.3 不动,先别怀疑模型,去检查验证集的标注是不是和训练集用了不同的类别映射。

3.4 推理验证与可视化界面怎么接

训练完先用命令行推理几张图看看效果:

yolo detect predict \ model=./runs_energy/exp1/weights/best.pt \ source=./campus_energy/images/test \ conf=0.4 \ save=True

conf=0.4是置信度阈值,低于这个值的框不显示。校园能耗场景里,我一般设 0.35 到 0.45,太低会误检,太高会漏检远处的设备。推理结果存在runs_energy/exp1/下,打开图片看框的位置和标签对不对。

可视化界面用 Gradio 或 Streamlit 都行,Gradio 更简单,十几行代码就能搭一个上传图片返回检测结果的页面:

import gradio as gr from ultralytics import YOLO model = YOLO('./runs_energy/exp1/weights/best.pt') def detect(image): results = model(image, conf=0.4) return results[0].plot() # 返回画好框的 numpy 数组 demo = gr.Interface( fn=detect, inputs=gr.Image(type='numpy'), outputs=gr.Image(type='numpy'), title='校园能耗设备检测' ) demo.launch(server_name='0.0.0.0', server_port=7860)

results[0].plot()返回的是带框和标签的 BGR 图像数组,Gradio 会自动转成 RGB 显示。server_name='0.0.0.0'让局域网内其他设备也能访问,答辩时用手机拍一张教室照片上传,界面直接出检测结果,比放 PPT 有说服力。

4. 部署与避坑:从训练环境到边缘设备的常见问题

4.1 模型导出 ONNX 与 RK3588 部署的注意点

毕设如果要求"部署",通常指的是把 PyTorch 权重转成 ONNX 或 RKNN,再放到边缘设备上跑。导出 ONNX 的命令:

yolo export model=./runs_energy/exp1/weights/best.pt format=onnx opset=12 simplify=True

opset=12是兼容性最好的版本,simplify=True会调用 onnx-simplifier 去掉冗余节点。导出后得到一个.onnx文件,用onnxruntime加载推理,速度比 PyTorch 快百分之二三十。如果要部署到 RK3588,还得用 RKNN-Toolkit2 把 ONNX 转成 RKNN,这一步的坑在于量化。默认的 int8 量化需要提供校准数据集,校准集里的图片要覆盖所有类别和光照条件,否则量化后精度掉得厉害。我一般先用do_quantization=False导一版浮点 RKNN 验证精度,确认无误再开量化。

4.2 避坑:校园能耗检测训练与部署的五个翻车现场

现象一:训练到一半 loss 变成 NaN。原因通常是标注文件里有坐标超出 0 到 1 范围,或者某张图的 txt 是空的但图片还在。解决方法是写个脚本遍历所有 label 文件,检查每行第一个数字是否在 0 到 nc-1 之间,后四个数字是否在 0 到 1 之间,发现异常直接删掉对应图片和标签。

现象二:mAP50 很高但实际推理全是误检。原因是验证集和训练集来自同一批图片的相邻帧,数据泄漏了。解决方法是按拍摄时间段划分数据集,比如上午拍的做训练,下午拍的做验证,确保同一场景不出现在两个集合里。

现象三:Gradio 界面在服务器上跑但外部访问不了。原因是防火墙没开端口,或者server_name没设成0.0.0.0。解决方法是sudo ufw allow 7860,然后确认启动日志里显示的是Running on local URL: http://0.0.0.0:7860。

现象四:RK3588 上推理速度只有 5 FPS。原因是模型没量化,或者输入尺寸设成了 1280。解决方法是导出 RKNN 时开 int8 量化,输入尺寸降到 640,NPU 利用率能到 80% 以上,帧率提到 25 FPS 左右。

现象五:换了新场景后模型完全失效。原因是训练集里的设备外观和部署场景差异太大,比如训练用的是白色空调外机,部署场景是灰色。解决方法是收集新场景的图片,用yolo detect train的pretrained参数加载已有权重做微调,学习率降到 0.001,训 30 轮就能适应。

4.3 用混淆矩阵和 PR 曲线判断模型短板

训练完别只看 mAP,混淆矩阵能告诉你模型到底把什么认成了什么。YOLOv8 在验证时会自动生成confusion_matrix.png和PR_curve.png,在runs_energy/exp1/目录下。混淆矩阵的对角线越深越好,如果air_conditioner_on和air_conditioner_off之间有明显交叉,说明模型分不清开关状态,这时候要么增加这两类的样本量,要么在标注时把状态差异拍得更明显。PR 曲线看的是每个类别的查准率和查全率平衡点,曲线下的面积就是 AP,哪个类别的 AP 明显低,就针对性地补那个类别的数据。

5. 把检测结果变成能耗告警:一个可复现的后处理技巧

模型输出的是框和类别,但毕设答辩时老师会问:"检测到灯开着,然后呢?"你需要一个后处理逻辑把检测结果转成能耗告警。我的做法是定义一个"非工作时段设备开启"规则:如果当前时间在 22:00 到次日 6:00 之间,且检测到light_on或air_conditioner_on的框,就判定为能耗异常,在可视化界面上用红色边框标出并弹出提示。

实现上,在 Gradio 的detect函数里加时间判断:

from datetime import datetime def detect_with_alert(image): results = model(image, conf=0.4) boxes = results[0].boxes alert = False now = datetime.now().hour if now >= 22 or now < 6: for cls_id in boxes.cls: if int(cls_id) in [0, 2]: # 0 是空调开启,2 是灯开启 alert = True break annotated = results[0].plot() if alert: # 在图像顶部叠加红色告警条 annotated[0:40, :, :] = [0, 0, 255] return annotated

这段代码的逻辑是:先推理,再取检测框的类别 ID,如果当前是非工作时段且存在开启类设备,就把图像顶部四十行像素涂红作为告警。boxes.cls返回的是张量,用int()转成整数再比较。这个后处理虽然简单,但把"检测"和"能耗智能"之间的逻辑补上了,答辩时能讲清楚从视觉到业务的闭环。

还有一个进阶技巧是统计画面中开启设备的数量,如果超过阈值就触发二级告警。比如一间教室检测到三盏以上灯同时亮着且是深夜,就判定为"整间教室忘关灯",比单设备告警更有实际意义。阈值怎么定?我一般按场景面积来,小教室设 2,大教室设 4,走廊设 3。这个数没有标准答案,根据你数据集里的实际分布调就行。

最后说个我自己的习惯:每次训完模型,不管 mAP 多高,我都会拿手机去楼道里拍二十张新图跑一遍推理,肉眼数误检和漏检。这个动作花不了十分钟,但能暴露验证集上看不到的问题。毕设的模型不需要完美,但得让老师觉得你真的在校园里跑过、看过、改过。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询