简介:基于树莓派和Python的黄瓜病斑识别系统设计文档,面向农业信息化、嵌入式视觉方向的开发者与相关专业学生,系统讲解利用树莓派3B、摄像头及Python完成黄瓜叶片图像采集、预处理、OTSU分割、灰度化与中值滤波去噪,并依据病斑面积比例划分病害等级的整体方案。包体为1个docx文件,压缩包24KB,内容精简但覆盖硬件组成、系统架构、算法流程与等级判定标准,文档结构完整、层次清楚,尤其适合课程设计、毕业设计或项目方案参考。已有67人学习下载。文档结合锂电池供电、LCD显示、GPIO接口等硬件细节,以及面积计算、六等级划分等关键步骤,能帮助读者快速理解从图像采集到病害判别的完整链路,也可为其他作物病斑识别系统的设计提供可迁移思路。
1. 前言:病斑识别系统的定位与设计边界
黄瓜叶片的病斑,在温室里往往是“先出现一两点,两三天后整片叶角垮掉”。人工巡检只能看到发黄大斑块,看不见还没变色的早期组织;基于树莓派和Python的黄瓜病斑识别系统,就是用一块能长期开机的开发板把相机对准叶片,每隔几分钟采集一张图像,用轻量模型判断病斑是否开始出现。它解决的是“持续观察,而不是单次看病”,适合毕业设计、温室试点和农业机器人原型。
设计边界必须划清:树莓派4B的CPU没有独立GPU,很难照搬服务端重型分类网络,所以常见落地路径是在普通PC上训练轻量模型,导出TensorFlow Lite格式,再由树莓派做边缘推理。摄像头选择CSI接口而不是USB,区别在于CSI硬件链路更稳定、功耗更低,适合长时间运行。下面按模型选型、数据组织、相机采集、板上推理、部署调优和排错验证的顺序,把这套方案完整讲清楚。
2. 黄瓜病斑识别系统的模型选型与数据集设计
2.1 病斑识别先拆成“分类、检测还是分割”三种任务
用户说“识别病斑”时,实际需求并不相同。在树莓派这种边缘设备上,通常可以把任务分成三类:图片级分类只回答“这片叶子得病没有、得了哪类病”;病斑检测需要用矩形框标出每个病斑的位置;像素级分割则把病斑边界逐像素画出来。分类最容易落地,标注成本最低,一张图片配一个标签就能完成训练;分割最贴近农业病理分析,但标注成本高,小样本下还容易出现边界噪声。
我一般给的默认方案是“分类为主,检测辅助”:固定相机对着单张叶片时用整图分类,移动巡检时用颜色掩膜把异常区域框出来,再把每块裁切送进分类模型。这样避开了在树莓派上直接跑目标检测的延迟问题,又能得到病斑坐标。目标检测模型如YOLOv5n虽然只有约190万参数,但在树莓派4B上推理一帧需要2到3秒,连续巡检时CPU发热和延时都不划算。如果必须做检测,建议把图像分辨率固定在640x480,且只检测单一叶片,不要引入多目标跟踪,否则系统复杂度会迅速超过识别任务本身。
2.2 树莓派4B上跑得动的模型:MobileNetV2、EfficientNet-Lite与ResNet50
树莓派4B的CPU是四核Cortex-A72,没有GPU加速。TensorFlow Lite在CPU上运行卷积时,速度主要取决于乘加次数和算子实现,因此模型体积和输入分辨率比网络结构“新不新”更重要。常用候选模型对比如下:
| 模型 | 参数量 | 224x224推理耗时(树莓派4B) | TFLite支持 | 推荐场景 |
|---|---|---|---|---|
| MobileNetV2 | 3.4M | 0.3~0.6秒 | 极好 | 整图分类首选 |
| EfficientNet-Lite0 | 4.7M | 0.6~1.0秒 | 较好 | 精度优先时备用 |
| ResNet50 | 25.6M | 2~4秒 | 一般 | 不推荐 |
| YOLOv5n | 1.9M | 2~3秒 | 需要转换 | 多框检测再考虑 |
表中的耗时是动态范围量化后在四线程下的经验值,实际值会随温度变化。MobileNetV2的优势不只是快,更在于Keras内置实现可以平滑导出TFLite,算子和量化流程都很成熟;EfficientNet-Lite在部分TensorFlow 2.x版本中导出时可能遇到ResizeBilinear算子在旧版TFLite运行时上不支持的问题,需要额外处理。所以病斑识别系统的默认模型建议固定为MobileNetV2,后期如果精度不够,再换成EfficientNet-Lite并做算子兼容验证。
训练环境不建议放在树莓派上,而是放在普通PC或服务器上。温室场景下照片会受到背光、潮湿和阴影干扰,仅靠ImageNet预训练权重不够,必须用自建病斑数据集做迁移学习和数据增强,具体做法在2.4节展开。
2.3 数据集目录结构、切分比例与命名规范
数据集按“类别/文件”分层,是训练脚本可复用的基础。我习惯把原始图片按“标签_编号.jpg”命名,再把训练、验证、测试三组数据分别放进独立目录:
cucumber_leaf/ ├── train/ │ ├── healthy/ │ ├── anthracnose/ │ ├── downy_mildew/ │ └── powdery_mildew/ ├── val/ └── test/缺少test目录时,模型选型会失去客观依据;测试集里的照片最好来自另一天、另一株黄瓜,用来模拟真实工况。如果数据只有1000张,建议按7:2:1划分,数据增强只作用于训练集,验证集和测试集不增强。下面脚本用固定随机种子完成切分:
import shutil from pathlib import Path import random random.seed(42) # 固定种子,保证每次切分结果一致 src = Path("raw_images") out = Path("cucumber_leaf") for img_path in src.glob("*.jpg"): # 文件名示例:anthracnose_001.jpg,取下划线前的部分作为标签 tag = img_path.stem.split("_")[0] r = random.random() if r < 0.7: shutil.copy(img_path, out / "train" / tag / img_path.name) elif r < 0.9: shutil.copy(img_path, out / "val" / tag / img_path.name) else: shutil.copy(img_path, out / "test" / tag / img_path.name)这个脚本依赖文件名前缀作为标签,所以采集照片时就应该把类别写进文件名,而不是靠文件夹来记忆。r < 0.7对应训练集70%,r < 0.9表示验证集再加20%,剩余10%自动成为测试集。固定random.seed(42)很关键,否则每次运行切分都不一样,后续不同训练轮次的精度对比就没有意义。
2.4 用迁移学习训练MobileNetV2并导出TFLite
训练在PC上执行,Python环境安装TensorFlow即可。这里用ImageNet预训练权重做迁移学习,把模型顶层替换成4类输出:
import tensorflow as tf from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.layers import Dense, GlobalAveragePooling2D from tensorflow.keras.models import Model # 载入去掉顶层分类器的MobileNetV2,输入尺寸224x224 base = MobileNetV2(weights="imagenet", include_top=False, input_shape=(224, 224, 3)) outputs = Dense(4, activation="softmax")( GlobalAveragePooling2D()(base.output) ) model = Model(inputs=base.input, outputs=outputs) model.compile( loss="categorical_crossentropy", optimizer=tf.keras.optimizers.Adam(1e-4), metrics=["accuracy"] ) model.fit(train_ds, validation_data=val_ds, epochs=15) # 动态范围量化导出,体积约缩小3倍 converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] open("cucumber_model.tflite", "wb").write(converter.convert())include_top=False去掉ImageNet的1000类分类层,GlobalAveragePooling2D把特征图压缩成向量,比Flatten更不容易过拟合。Dense(4, activation="softmax")中的4必须和数据集类别数一致,输出顺序由训练数据加载顺序决定,部署时要在代码里与class_names一一对应。converter.optimizations使用动态范围量化,模型体积从约14MB降到约4.5MB,推理速度明显提升,且不需要额外校准数据。
如果还想用全整型量化,转换时就需要传入一个representative_dataset函数,用来提供验证集样本分布;否则量化校准后的精度可能突然掉到0.5以下。对于病斑这种颜色纹理敏感的识别任务,先跑动态范围量化,不要一开始就上整型量化。
3. 用Python和OpenCV在树莓派上实现黄瓜病斑识别
3.1 用Picamera2把CSI摄像头画面转成NumPy数组
树莓派摄像头模块常见的是OV5647或IMX219。在Raspberry Pi OS Bookworm之后,命令行拍照推荐libcamera-still,Python读写推荐python3-picamera2,旧库picamera已无法配合新libcamera框架。安装Picamera2后,最小采集代码如下:
from picamera2 import Picamera2 picam2 = Picamera2() # 主图像流设置为640x480,对分类任务够用 config = picam2.create_still_configuration(main={"size": (640, 480)}) picam2.configure(config) picam2.start() # 返回第一帧RGB数组 frame = picam2.capture_array() print(frame.shape, frame.dtype) # (480, 640, 3) uint8 picam2.stop()create_still_configuration中的main表示主流,(640, 480)是兼顾速度和精度的起点。capture_array()返回RGB顺序,这一点和OpenCV的BGR不同;直接交给TFLite模型没问题,但保存或显示前必须转成BGR。如果后面还要给病斑画定位框,建议把分辨率提高到1280x720,否则小病斑在降采样后容易被抹平,画框位置会偏移。使用USB摄像头时可以直接cv2.VideoCapture(0),但温室长期运行场景下,CSI摄像头更稳,不会因USB带宽波动而掉帧。
3.2 TensorFlow Lite推理脚本:输出病害类别与置信度
使用tflite-runtime包加载转换好的cucumber_model.tflite,循环采集并打印结果:
import cv2 import numpy as np import tflite_runtime.interpreter as tflite interpreter = tflite.Interpreter( model_path="cucumber_model.tflite", num_threads=4 ) interpreter.allocate_tensors() inp = interpreter.get_input_details()[0] out = interpreter.get_output_details()[0] class_names = ["healthy", "anthracnose", "downy_mildew", "powdery_mildew"] def infer(frame: np.ndarray) -> (str, float): # 使用模型输入尺寸,不要写死224 h, w = inp["shape"][1], inp["shape"][2] img = cv2.resize(frame, (w, h)) # 归一化到[-1,1],必须与训练时一致 img = img.astype(np.float32) / 127.5 - 1.0 interpreter.set_tensor(inp["index"], img.reshape(1, h, w, 3)) interpreter.invoke() scores = interpreter.get_tensor(out["index"])[0] idx = int(np.argmax(scores)) return class_names[idx], float(scores[idx])num_threads=4对应树莓派4B的四个核心,继续调大不会更快,反而可能因为内存带宽瓶颈变慢;长时间连续运行时建议改为3,以降低CPU温度。inp["shape"]的排列是[1, height, width, 3],所以resize尺寸应从shape索引取,而不是写死224,这样换192输入模型时无需改代码。归一化(x / 127.5 - 1)与Keras中MobileNetV2的前处理一致;如果训练时用了其他均值方差,部署脚本也要同步改,否则推理结果会出现大量误差。
3.3 用HSV掩膜定位病斑,再把候选区域送进分类器
整图分类结果不够直观,不少项目要求在画面上框出病斑。黄瓜病斑多为黄褐色或灰白色,与绿色叶片在HSV空间区分明显,因此可以先提取非绿色区域,再做轮廓分析:
hsv = cv2.cvtColor(frame, cv2.COLOR_RGB2HSV) # 绿色范围在HSV中约为H35~85, S40以上 green_mask = cv2.inRange(hsv, (35, 40, 40), (85, 255, 255)) not_green = cv2.bitwise_not(green_mask) mask = cv2.morphologyEx(not_green, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8)) cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes = [cv2.boundingRect(c) for c in cnts if cv2.contourArea(c) > 300] for (x, y, w, h) in boxes: patch = frame[y:y+h, x:x+w] label, prob = infer(patch) # 阈值0.6防止把叶片阴影或土粒当作病斑 if prob > 0.6: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 0, 255), 2)HSV阈值中的(35,40,40)到(85,255,255)覆盖中等光照下的绿色叶片,取反后得到疑似病斑区域。MORPH_OPEN用5x5卷积核先去噪点,contourArea > 300按像素面积过滤极小区域,避免把叶脉纹理误当成病斑。这里对每个候选块再次执行整图分类,置信度高于0.6才画框,能显著减少误报。该方法的局限是遇到叶片整体黄化或强阴影时阈值不稳定,因此把它当成辅助定位工具,不要当作分割结果直接用于病理统计。
3.4 把识别结果写入CSV,变成巡检记录系统
只输出命令行结果没有农业价值,识别系统应把时间、类别、置信度和病斑数量一起保存。追加写入CSV是最直接的做法:
from datetime import datetime # 追加模式,避免覆盖前一天数据 with open("recognition_log.csv", "a", encoding="utf-8") as fp: t = datetime.now().isoformat(timespec="seconds") fp.write(f"{t},{label},{prob:.3f},{len(boxes)}\n")timespec="seconds"把时间精确到秒,避免频繁的微秒无意义变化;prob:.3f限制小数位数,生成的文件可直接拖进Excel做趋势图。每次推理后打开一次文件,看似低效,但识别任务本身就是按分钟级巡检,IO开销可以忽略,反而能确保日志及时落盘。如果巡检间隔是5分钟,一年约10万条记录,CSV仍然够用;后续要接Web界面再换成SQLite。
4. 树莓派部署与运行优化:无屏幕启动、环境安装和性能调参
4.1 树莓派4B无屏幕启动:烧录、开启SSH与摄像头参数
先把Raspberry Pi OS 64位Lite镜像写入TF卡,用官方Raspberry Pi Imager完成自定义用户、启用SSH和配置Wi-Fi。这样不需要连接显示器,通电后就能在局域网里找到设备。如果准备装Ubuntu Server,步骤类似,但官方Raspberry Pi OS对CSI摄像头支持更好,农业长期运行项目更推荐后者。
第一次开机后,在SSH终端执行libcamera-hello --list-cameras确认摄像头驱动是否加载。如果看不到摄像头,再检查/boot/firmware/config.txt中是否缺少:
dtoverlay=ov5647dtoverlay=ov5647用于让内核加载OV5647模块;新版本libcamera通常能自动识别,所以这行只在排查摄像头不工作时加上。登录后还应安装libcamera-tools,其中libcamera-still可以直接拍照测试,排除应用程序层问题。对CSI摄像头和USB摄像头同时存在的设备,先执行v4l2-ctl --list-devices查看设备节点,避免把CSI图像读到USB设备上。
4.2 Python环境和OpenCV、tflite-runtime的安装顺序
树莓派上编译OpenCV代价很高,优先用apt安装带优化的版本,再用venv隔离Python依赖:
sudo apt update sudo apt install -y python3-venv python3-opencv libatlas-base-dev python3-picamera2 python3 -m venv --system-site-packages ~/cv-project source ~/cv-project/bin/activate pip install --upgrade pip pip install tflite-runtime numpy--system-site-packages让虚拟环境可以使用apt安装的cv2和picamera2,避免用pip从源码编译opencv-python这类重包。libatlas-base-dev提供底层BLAS加速,缺失时numpy矩阵运算会落入低效路径,图像预处理和数组归一化都会变慢。这里的pip只需要tflite-runtime和numpy两个包,不用安装完整TensorFlow;模型训练早已在PC完成,树莓派只做推理。
安装完成后,用python -c "import cv2, numpy"验证导入,再用python -c "import tflite_runtime.interpreter"确认TFLite运行时可用。如果后者报No module named 'tensorflow',说明装成了错误的tensorflow包,应先卸载再安装tflite-runtime,两者不能混用。
4.3 性能调优:分辨率、线程数与散热的关系
树莓派4B在低负载时CPU频率约600MHz,推理开始后会升到1.8GHz,温度超过80℃时会主动降频,推理时间可能从0.3秒跳回1秒以上。所以性能优化首先要解决散热,其次才是模型参数。推荐配置如下:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 摄像头分辨率 | 640x480 / 1280x720 | 分类用640,画框用1280 |
| 模型输入尺寸 | 192x192 / 224x224 | 192更快,病斑清晰时足够 |
| Interpreter线程数 | 3~4 | 4最快,连续运行建议3 |
| 散热方案 | 铝制散热片加小风扇 | 直接影响推理速度稳定性 |
| 巡检间隔 | 5到10分钟一张 | 识别系统不需要视频流实时处理 |
把输入从224降到192,计算量下降约26%,结合动态范围量化,整图分类可以做到0.25秒左右。如果病斑在叶片上只占很小比例,直接resize会丢失细节,正确做法是先按3.3节HSV掩膜裁切病斑区域,再缩放进模型。另外,不要让摄像头自动曝光与模型推理同时运行,否则户外光线变化会让画面忽明忽暗,模型容易把亮度差误判成病斑;在代码里固定曝光时间和ISO更方便。
树莓派5上这套方案依然成立,只是四核A76性能更强,线程数可以继续用4,散热条件比4B好一些,但摄像头排线接口和config.txt的dtoverlay有差异,不能直接把同一张卡跨型号共用。
4.4 用systemd让识别程序开机自启动
最后把巡检程序固化成系统服务。在/etc/systemd/system/cucumber.service写入:
[Unit] Description=Cucumber Disease Recognition After=network.target [Service] User=pi WorkingDirectory=/home/pi/cv-project ExecStart=/home/pi/cv-project/bin/python main.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.targetExecStart必须指向venv里的python绝对路径,不能用python main.py,否则systemd环境下找不到tflite_runtime。Restart=always应对摄像头掉线和进程死掉,但调试阶段建议先改成Restart=no,否则模型路径错误时系统会每5秒重启一次,日志很快被刷屏。启用服务的命令是:
sudo systemctl daemon-reload sudo systemctl enable --now cucumber journalctl -u cucumber -fjournalctl -u cucumber -f实时查看服务日志,是排查启动失败最直接的工具。无桌面环境下不要调用cv2.imshow,它没有GUI显示器会直接段错误;需要看画面时先cv2.imwrite再通过scp拉到电脑上检查。
5. 部署后的验证与提高预警能力的小技巧
5.1 先固定图片端到端验证,再接入摄像头
把模型、预处理、分类代码写好后,先准备一张带病斑的jpg图片,用命令python check_model.py test.jpg在树莓派上验证。这样把摄像头故障和模型故障分开:固定图片输出正常,问题就出在摄像头;固定图片输出乱码概率,则要检查归一化、类别顺序和量化参数。接入摄像头后,再用libcamera-still -o c.jpg手动拍一张图传回电脑检查颜色,排除CSI排线接触不良。
5.2 误检高或速度慢时优先检查三步
病斑误检通常来自三个地方:训练集不均衡,健康样本占比过大,模型会习惯性输出“健康”;HSV阈值把阴影当成病斑,需要按当天光照微调阈值;模型输入尺寸与训练不一致,导致病斑纹理被破坏。速度慢则先看散热,再降低线程数,最后确认是否用了未量化模型。这三点对应三个脚本参数:类别权重、HSV阈值和输入shape。启动时打印模型大小、输入shape和CPU温度,能把这些参数问题暴露在日志里。
5.3 用“非绿色面积占比”做早期预警
一个不常见但很实用的技巧是:不必等模型分类判定为某种病才开始记录,而是每次巡检时计算叶片上非绿色面积的比率。相同位置拍摄,非绿色面积从5%升到12%的过程,往往比模型输出转到病类别更早。在24行HSV掩膜代码后加一行:
# 计算非绿色面积占比,作为发病趋势特征 ratio = cv2.countNonZero(mask) / (frame.shape[0] * frame.shape[1])将ratio与分类结果一起写入CSV,连续3天的占比超过0.1时触发预警。这样在叶片还只是零星黄点时,已经能从面积曲线里看到上升趋势,比单纯等模型判定出具体病种更有提前量。
本文还有配套的精品资源,点击获取