☰
基于YOLOv8的阳台花盆自动灌溉系统:从目标检测到联动控制
2026/10/1 10:30:42 网站建设 项目流程

简介:基于YOLOv8的智能家居阳台花盆自动灌溉监测系统,是一套融合目标检测、可视化管理与部署指导的完整项目方案,面向计算机、人工智能等专业学生及毕业设计、课程设计开发者。压缩包内共8个文件,以3个py源码脚本、3个pt模型权重文件和2个txt说明文档为主,整体大小15.91MB,结构简洁,便于直接运行。其中py脚本分别覆盖界面交互、视频检测与模型训练,pt权重包含预训练模型与最佳模型,txt则提供部署说明与数据配套信息。项目代码均经过运行验证,并提供完整数据集、可视化页面及部署教程,支持生成核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果和标签分布图,便于答辩展示与效果评估。目前已有36人学习下载,适合需要快速落地实验、完成课设或毕设演示的开发者。

1. 先看这个系统在做什么:从花盆画面到水泵动作的完整闭环

同样一盆绿萝,早上八点和下午两点的颜色在摄像头里完全不同:顺光时叶片油亮,逆光时发灰发暗。阳台花盆自动灌溉的难点其实不在水泵和继电器,而在“怎么让程序像人一样判断这盆花现在缺不缺水”。土壤湿度传感器插久了会盐碱化失效,埋深了又感应不到表层干湿,所以这个项目的思路是用 YOLOv8 做目标检测:摄像头对着阳台,模型框出花盆的同时把叶片分成正常、发黄、枯萎,连续多帧判定缺水后通过继电器驱动水泵;配套可视化界面实时显示画面、检测框和浇水记录。一句话概括:这是一套用视觉判断代替传感器触发的自动灌溉闭环,源码、完整数据集和部署教程齐全,适合拿来做毕设或课程设计。

2. 数据与模型准备:给 YOLOv8 造一个能用的花盆检测数据集

训练一个能被答辩老师认可的模型,功夫七成花在数据上,三成花在训练参数上。这个项目的检测对象是阳台场景下的花盆和叶片,环境比公开数据集里的普通物体复杂得多,所以先别急着下载现成的权重,老老实实从自己的数据集开始。

2.1 选型:为什么用 YOLOv8 而不是 OpenCV 阈值分割

在做这个系统之前,最容易走的老路是 OpenCV 加颜色阈值:把 BGR 转到 HSV,找绿色区域,统计占比,低于某个值就浇水。这套方案在实验室固定灯光下能跑通,一上阳台就翻车。原因是光照强度、色温、阴影范围每半小时都在变,同一种绿色在不同帧里的 HSV 区间根本不是固定值;再加上花盆、土壤、叶片的边界在阴影里会糊在一起,轮廓检测拿到的面积忽大忽小。

换用 YOLOv8 之后,这些“玄学”就变成了训练问题。网络结构上,YOLOv8 用 C2f 模块做多尺度特征复用,检测头是 Anchor-Free 的解耦结构,框的位置和类别得分分开预测,对叶片这种形状不规则、边缘不整齐的目标反而友好。看网络结构图时会发现它的 Backbone 和 Neck 比前代更精简,这也是参数量下降但精度不降的主要原因。

选哪个尺寸的模型也值得想一下。毕设演示现场大概率没有独立显卡,用 YOLOv8n 或 YOLOv8s 就够:n 在 CPU 上跑 640 分辨率一帧大概几十毫秒,能跟上实时预览。m 以上的模型精度高一点,但 CPU 推理会明显掉帧,“实时监控”就名存实亡了。

类别设计我一般会这样定:flowerpot、healthy_leaf、yellow_leaf、wilted_leaf 四类。flowerpot 让模型学会约束整体区域,避免把阳台外的树也框进来;yellow_leaf 是早中期缺水信号,wilted_leaf 是严重缺水。检测到 yellow 进入待浇水状态,wilted 立即触发,这比只分“正常/缺水”两个类抗光照变化能力强很多,因为中间类别给了模型一个缓冲。

2.2 用 labelme 标注并转成 YOLO 格式:转换脚本与四个边界坑

数据集质量决定这个毕设的上限。采集时别只在中午拍,要在早晨、正午、傍晚各拍一组,把窗帘半遮、阳光直射、阴天三种光照都覆盖进去。叶片有重叠、部分被花盆遮挡都没关系,但同一株花的正反面都要有。数量上,每类标注实例至少 150 个,整体图像量 500 张以上,训练才不容易过拟合。

标注工具用 labelme 就行,画矩形或多边形都可以,转换脚本按外接矩形处理。下面是毕设项目里最常用的 labelme 转 YOLO 格式脚本:

import json import os from glob import glob def convert_labelme_json(json_path, out_dir, class_dict): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w = data["imageWidth"] img_h = data["imageHeight"] txt_name = os.path.basename(json_path).replace(".json", ".txt") out_path = os.path.join(out_dir, txt_name) lines = [] for shape in data["shapes"]: label = shape["label"] if label not in class_dict: continue cls_id = class_dict[label] # labelme 矩形标两个角点,多边形标一串点,统一取外接矩形 xs = [p[0] for p in shape["points"]] ys = [p[1] for p in shape["points"]] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) cx = (x_min + x_max) / 2 / img_w cy = (y_min + y_max) / 2 / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) class_dict = { "flowerpot": 0, "healthy_leaf": 1, "yellow_leaf": 2, "wilted_leaf": 3 } for json_path in glob("labelme/*.json"): convert_labelme_json(json_path, "datasets/leaf/labels/train", class_dict)

这段脚本有三个容易踩的细节。第一,用 min/max 求外接矩形而不是直接取 points[0] 和 points[1],因为你标注时可能随手画了多边形,只有矩形标注才能保证两个点就是左上和右下。第二,归一化时除以的是 imageWidth 和 imageHeight,这两个字段在 json 顶层,不是某个 shape 里的小宽高。第三,class_dict 的顺序就是类别的 ID,它必须和后面 data.yaml 里的 names 列表顺序完全一致,否则训练出来的模型拿到的类别标签全是错位的。

转换完一定要抽查 txt 文件,所有坐标值都应该在 0 到 1 之间。如果出现 1.2 这种超界值,多半是 json 里的 points 单位不是像素,或者图片被压缩过但 json 没同步更新。

提示:不要把标注任务留到最后一天做。500 张图平均每张 5 个框,一个人连续标注要 4 到 6 小时,手一酸就容易把类标错,后面排查更费时间。

2.3 训练参数含义与损失曲线判读:epochs、imgsz、batch 怎么调

数据和标签就位后,写 data.yaml。这是给 YOLOv8 喂数据的入口文件:

path: datasets/leaf train: images/train val: images/val nc: 4 names: ["flowerpot", "healthy_leaf", "yellow_leaf", "wilted_leaf"]

path 建议写相对路径,训练命令在项目根目录执行就不会找不到目录。train 和 val 不能指向同一个目录,否则验证结果没有参考意义。然后跑训练命令:

yolo detect train data=datasets/leaf/data.yaml model=yolov8n.pt epochs=150 imgsz=640 batch=16 patience=30 project=runs/leaf name=exp seed=42

第一次执行会自动下载 yolov8n.pt 预训练权重,网络不稳就手动下载后放到当前目录。要是电脑内存紧张,把 batch 降到 4,imgsz 降到 480 也能跑,只是精度会掉一点。

每个训练参数的含义直接决定你调参的方向,整理成表格方便对照:

参数建议值含义与调法
epochs150小数据集 150 轮足够,量少时跑多轮容易过拟合
imgsz640训练输入尺寸,与推理时保持一致,改小省内存但降精度
batch16显存或内存不够报 OOM 时降到 4 或 8
lr00.01默认值;loss 曲线震荡不降时试 0.001
patience30验证指标连续 30 轮不涨自动停,省时间
seed42固定随机种子,保证重跑结果可复现

训练结束后,在 runs/leaf/exp/ 下有 weights/best.pt 和 last.pt,best.pt 是验证集 mAP 最高的权重,后面推理全部用它。关于 yolov8 画损失函数曲线图,项目里已经生成了 results.csv,直接用 pandas 画:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/leaf/exp/results.csv") fig, ax1 = plt.subplots(figsize=(10, 5)) ax1.plot(df["epoch"], df["train/box_loss"], label="train/box_loss") ax1.plot(df["epoch"], df["val/box_loss"], label="val/box_loss") ax1.set_ylabel("box loss") ax2 = ax1.twinx() ax2.plot(df["epoch"], df["metrics/mAP50(B)"], color="green", label="mAP50") ax2.set_ylabel("mAP50") fig.legend() plt.savefig("loss_curve.png")

如果 csv 里找不到这些列名,先 print(df.columns) 看一眼,不同 ultralytics 小版本的字段名会有细微差别。判断训练是否正常看三个信号:train/box_loss 应该一路下降不再反弹,val/box_loss 要和 train 保持贴近,mAP50 平稳上升。val 和 train 的 loss 拉开 20% 以上,就是过拟合,解决方向是增加数据量、加数据增强,而不是继续加 epochs。

3. 把模型变成系统:推理、可视化界面与灌溉联动

训练出 best.pt 只是完成了 30%。毕设要交付的是“打开界面就能看到实时画面、有检测框、能自动浇水”的一整套系统。这一章把推理、界面、控制三段串起来,每一段都有可以直接抄的代码结构。

3.1 最小推理代码:让摄像头画面实时出现检测框

先不碰界面,用命令行脚本验证模型能不能在真实摄像头画面里稳定工作。在项目目录下新建 detect_demo.py:

from ultralytics import YOLO import cv2 model = YOLO("runs/leaf/exp/weights/best.pt") cap = cv2.VideoCapture(0) if not cap.isOpened(): raise IOError("camera 0 cannot open, try index 1 or 2") while True: ret, frame = cap.read() if not ret: break # conf 低于 0.45 的预测框大多是阳台外的干扰目标 results = model.predict(frame, conf=0.45, imgsz=640, verbose=False) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) if cls_id >= 2: # yellow_leaf / wilted_leaf print(f"{model.names[cls_id]}: {conf:.2f} at frame") annotated = results[0].plot() cv2.imshow("balcony monitor", annotated) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

这段代码把整个系统最核心的一件事做了:读帧、推理、在图上画出检测框并显示。model.predict 的 conf 参数是置信度阈值,阳台上低于 0.45 的框绝大多数是远处的行人、隔壁阳台的植物或者窗框反光,过滤掉能让演示画面干净很多。imgsz 保持 640 和训练一致,改成 320 会明显掉精度。results[0].plot() 方法内部已经帮你把框、类别名、置信度画好了,不需要手动画矩形。verbose=False 是关掉每帧的日志输出,否则终端会被刷屏。

另外注意 cv2.waitKey(1) 不能省,OpenCV 在高位 GUI 环境下不调用它,窗口不会刷新,看起来就像画面卡死了。CPU 机器上跑 yolov8n 的 640 模型,一帧推理时间在几十毫秒量级,配合 waitKey(1) 足以维持近似实时的预览。

3.2 可视化界面:PyQt5 界面怎么接模型而不是各跑各的

可视化界面最常见的翻车姿势是把 while 推理循环直接写在按钮的槽函数里。这样做的结果是界面窗口一开始检测就转圈、拖不动,几秒后系统提示未响应。原因很简单:推理阻塞了 UI 的事件循环,窗口的消息处理停摆。

正确的结构是把采集和推理丢到 QThread 子线程里,通过信号把结果帧和报警信息发射回主线程。下面是可以直接用的工作线程骨架:

from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage import cv2 from ultralytics import YOLO class DetectWorker(QThread): frame_ready = pyqtSignal(QImage) # 主界面用这个信号刷新视频帧 alert = pyqtSignal(str, float) # 类别名 + 置信度 def __init__(self, model_path, camera_index=0): super().__init__() self.model = YOLO(model_path) self.cam_index = camera_index self.running = True def run(self): cap = cv2.VideoCapture(self.cam_index) while self.running: ok, frame = cap.read() if not ok: continue results = self.model.predict(frame, conf=0.45, imgsz=640, verbose=False) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) if cls_id >= 2: self.alert.emit(self.model.names[cls_id], conf) annotated = results[0].plot() rgb = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(qimg) self.msleep(30) # 控制线程循环频率,约 33 帧/秒 cap.release()

主界面里把 worker 的 frame_ready 信号连到一个槽函数,槽函数里只用一行 label.setPixmap(QPixmap.fromImage(qimg)) 刷新 QLabel 就完成了。整个界面线程不碰推理,自然不会卡死。

界面布局建议做成左边视频区、右侧三个功能区:检测统计区显示当前各类别目标数量;灌溉日志区滚动记录触发时间、类别和置信度;底部放一个“手动浇水”按钮。手动按钮一定要保留,答辩演示时这比干等自动触发直观得多,也可以准备一盆明显缺水的花当演示道具。

3.3 灌溉联动:连续帧判定与继电器控制

模型说“这帧有黄叶”不代表就该浇水。单帧判断在阳台场景下极不可靠,一阵风把叶子吹翻、一只飞虫掠过都可能导致误报。我一般会用一个滑动窗口做连续判定:窗口大小 10 帧,其中至少 5 帧命中“缺水”才触发浇水,这样既能滤掉噪声,又不会延迟太久。

from collections import deque import time class Irrigator: def __init__(self, window_size=10, trigger_frames=5, min_conf=0.5, cooldown=600, max_seconds=10): self.history = deque(maxlen=window_size) self.trigger_frames = trigger_frames self.min_conf = min_conf self.cooldown = cooldown self.max_seconds = max_seconds self.last_water = 0 def update(self, results): # 当前帧是否有缺水信号:yellow_leaf 或 wilted_leaf hit = False for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) if cls_id >= 2 and conf >= self.min_conf: hit = True self.history.append(hit) hit_count = sum(self.history) if hit_count >= self.trigger_frames and time.time() - self.last_water > self.cooldown: self.last_water = time.time() return True return False

两个参数值得展开说。cooldown 设为 600 秒(10 分钟),作用是即使叶片状态没恢复,也不能短时间内反复浇水,一是保护继电器和水泵,二是防止土壤积水烂根。max_seconds 是单次浇水时长上限,下面的水泵函数会用到它做硬保护。

继电器和水泵的接线,选 5V/12V 光耦继电器模块最稳妥,绝大多数电磁阀是 12V 供电,不能直接拿开发板的 GPIO 去驱动,GPIO 只负责给继电器信号线一个高低电平。控制代码按树莓派 GPIO 写法示意:

import RPi.GPIO as GPIO import time WATER_PIN = 18 GPIO.setmode(GPIO.BCM) GPIO.setup(WATER_PIN, GPIO.OUT) GPIO.output(WATER_PIN, False) # 低电平触发,初始不动作 def water(seconds=5): GPIO.output(WATER_PIN, True) time.sleep(min(seconds, 10)) # 硬上限 10 秒,防止继电器粘连漫水 GPIO.output(WATER_PIN, False)

这里有个血泪教训:继电器模块一定要选低电平触发,也就是 GPIO 输出低电平时继电器吸合。原因是开发板上电瞬间引脚默认状态多为高电平,如果继电器是高电平触发,一插电就开始浇水,阳台直接变湿地。低电平触发的话,上电默认高电平反而是安全的断开状态。

4. 部署避坑:CPU 环境的常见问题与排查路径

这套系统最常见的运行环境是 ubuntu20.04 搭建 yolov8 环境 cpu 版本,也就是去掉了 GPU 依赖的纯 CPU 环境。这一章把联调阶段的高频问题按现象、原因、解决写清楚,每一条都来自真实翻车现场。

4.1 环境崩在 torch 上:Illegal instruction 与版本玄学

现象:pip install ultralytics 一切正常,但 import torch 直接报 Illegal instruction (core dumped),或者训练慢到无法接受。

原因:ultralytics 依赖 torch,pip 在 Linux 默认环境下会拉取 CUDA 版 torch。在没有 N 卡的机器上,CUDA 版代码执行到不支持的指令集就会崩;即使不崩,也会因为反复尝试加载 GPU 上下文而慢得离谱。

解决:先建一个干净的 Python 3.10 虚拟环境,用官方 CPU 轮子装 torch,再装 ultralytics。这一套顺序不能乱:

python3.10 -m venv venv source venv/bin/activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install "ultralytics>=8.2,<9" yolo predict model=yolov8n.pt source=test.jpg

最后一行是验证命令,它会自动下载 yolov8n.pt 并跑一张测试图。如果这一步能输出类别和坐标,环境就算通了。如果还报错,注意看错误信息里有没有 libopenblas 字样,有的话先 apt install libopenblas-dev 再重试。yolov8 下载权重这一步卡住的人很多,基本都是网络问题,手动下载后放在当前目录就能跳过自动下载。

4.2 训练自己的数据集:标签错位导致 mAP 一直为 0

现象:训练能正常跑完,loss 从 2.0 降到 0.8,但 mAP50 始终是 0,验证集 confidence 曲线全贴在地板上。

原因:最大概率是类别 ID 错位。转换脚本里的 class_dict 顺序和 data.yaml 的 names 列表不一致,模型学的 “yellow_leaf” 实际在 yaml 里对应的是 “flowerpot”,类别被整个带偏。还有一种情况是标注时手滑把 label 打错,某个类别在训练集里一个样本都没有。

解决:训练前先做一次标签体检。用一段脚本统计每个类别在训练集中的框数量:

from glob import glob counts = {0: 0, 1: 0, 2: 0, 3: 0} for txt in glob("datasets/leaf/labels/train/*.txt"): for line in open(txt): cls = int(line.split()[0]) counts[cls] += 1 print(counts)

counts 里任何一类是 0,训练出来的模型必然对那个类没有识别能力。其次打开一个 txt 文件人工核对,逐行看 class_id 和归一化坐标是否在 0 到 1 之间。这两步做完再重训,能解决 90% 的 mAP 为 0 问题。

4.3 可视化界面卡死:推理塞进 UI 线程

现象:点击“开始检测”后窗口转圈,标题栏显示“未响应”,过几秒 Windows 弹出强行关闭提示;Linux 下则是窗口假死,拖不动。

原因:把 while 读帧推理循环写在了按钮的 clicked 槽函数里。UI 线程被无限循环占住,Qt 的事件循环完全停摆,窗口自然无法响应。

解决:严格按 3.2 的结构,把采集和推理放进 QThread,信号槽传帧。需要特别注意的是 QThread 里不能再访问界面的控件,所有 UI 刷新都通过信号槽切回主线程;如果发现画面能显示但文字日志不刷新,检查是不是在子线程里直接调用了 label.setText 之类的方法,应该改为 emit 一个字符串信号。

4.4 灌溉误触发:单帧判定付出的代价

现象:晴天下午总是浇完水没过几分钟又开始浇;或者人从摄像头前走过也会触发浇水。

原因:单帧 conf 大于等于 0.5 就直接触发,设定太激进。午后光线变化会让绿色叶片局部发黄,模型在个别帧产生误判;人影识别成叶片则说明置信度阈值太低,或者模型没有收敛干净。

解决:套 3.3 的滑动窗口判定,10 帧里至少 5 帧命中才动作。同时把推理区域裁剪到花盆所在的 ROI,只保留画面中间一块,阳台边缘的干扰物就不进模型了。阈值方面,缺水信号 conf 提到 0.6,检测到人形类别时锁定自动浇灌 10 秒。另外预留一个总开关,调试阶段先关掉自动浇水,只做检测,等阈值调稳了再打开继电器。

4.5 演示现场翻车:摄像头索引与外接设备占用

现象:答辩现场打开界面,显示的是笔记本电脑的前置摄像头,而不是外接 USB 摄像头;或者摄像头画面黑屏,read() 一直返回 False。

原因:VideoCapture(0) 的索引不保证对应外接摄像头,Linux 下 /dev/video0 可能指向前置摄像头,不同机器对 USB 摄像头的枚举顺序也不一样。另一个常见原因是上次程序崩溃没有释放设备,摄像头被残留进程占住。

解决:写一个枚举脚本,把设备索引和分辨率打印出来,先确认再启动界面:

import cv2 for idx in range(4): cap = cv2.VideoCapture(idx) ok, frame = cap.read() print(f"index {idx}: opened={ok}, shape={None if not ok else frame.shape}") cap.release()

输出里能读到帧的就是可用的索引,把对应数字写进配置文件或界面下拉框里。演示前先跑一遍 3.1 的最小推理脚本,确认画面和检测都正常再开主界面,这个习惯能避开大部分现场事故。

5. 让系统更可靠:三个值得做的验证与进阶方向

模型训练完、界面能跑、水泵会动,这套系统就算立住了。但要拿去答辩或者自己长期用,还差一步:用数据证明它可靠,而不是只靠现场演示。

第一个建议是在阳台真实场景录一段 5 到 10 分钟的视频,用脚本离线回放,统计每一帧是否被判断为“缺水”。这样可以量化误报率,也能画出“缺水帧占比时间线”贴在论文实验章节里。简单的回放脚本如下:

import cv2 from ultralytics import YOLO model = YOLO("runs/leaf/exp/weights/best.pt") cap = cv2.VideoCapture("balcony_test.mp4") frames = 0 dry_hits = 0 while cap.isOpened(): ok, frame = cap.read() if not ok: break results = model.predict(frame, conf=0.45, verbose=False) hit = any(int(b.cls[0]) >= 2 for r in results for b in r.boxes) dry_hits += int(hit) frames += 1 print(f"water-needed frame ratio: {dry_hits}/{frames}") cap.release()

第二个方向是给系统加双保险。用一个雨滴传感器和模型做与逻辑:模型判断缺水且雨滴传感器显示晴天,才允许浇水;下雨天就算叶片发黄也跳过。模型负责“看表相”也就是叶片状态,传感器负责“看本质”也就是环境水分,两者互补,论文里的创新点和答辩加分点都在这。土壤湿度探头也可以再加一路,三个信号做多数表决,稳定性会好很多。

第三个方向是把模型从 YOLOv8n 换成 YOLOv8n-seg 做实例分割。分割能拿到叶片像素级轮廓,避免检测框里同时包含黄叶和绿叶导致的统计毛刺,但 CPU 推理帧率会下降一半,看演示机器性能取舍。

我自己做这套系统时栽过一次跟头:第一次联调只做了单帧判定,午后起风,叶子被吹得翻面,背面颜色偏灰,模型连续报了 9 帧黄叶,一个下午自动浇了 3 次水。后来把判定改成“10 帧窗口里至少 5 帧命中才浇水”,并加了 10 分钟冷却锁,这之后再没误触发过。如果你打算拿这套源码做毕设,建议先把上面的回放脚本跑一遍,再决定阈值写多少。希望帮到你。

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

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

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

立即咨询