ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+Shell构建茶叶枯萎病检测系统:从模型训练到部署全流程

Python+Shell构建茶叶枯萎病检测系统:从模型训练到部署全流程 简介一套基于Python和Shell的茶叶枯萎病检测系统源码包面向茶叶种植者、农业科研人员及计算机视觉开发者用于解决人工识别耗时耗力、准确率不稳的问题。系统覆盖图像采集、预处理和基于YOLO的病害识别等完整流程并提供Dockerfile便于容器化部署。包体共51个文件以23个Python源文件为核心负责算法与数据处理16个YAML配置用于调整训练与推理参数另有Shell自动化脚本、Dockerfile、Jupyter Notebook及Markdown说明等辅助文件压缩包约745KB。目录结构涵盖模型定义、训练测试、工具函数及部署配置便于按模块阅读和二次开发。项目开源共享且已有272人浏览学习可帮助用户快速搭建茶叶病害检测环境理解YOLO系列模型在农业场景中的落地流程也可作为目标检测项目改造的参考模板。1. 一套茶叶枯萎病检测系统为什么同时要Python和Shell茶园里拍回来的照片不会自己说话。判断一片茶叶是否开始枯萎靠人眼翻几千张图不现实常见做法是用Python训练一个分类模型让新照片进来时直接给出枯萎/健康的判断。这是算法侧的事也是基于Python的检测系统这条主线。但一套能上线的检测系统源码里七成工作量不在模型。照片从相机同步到服务器、按日期归档、滤掉拍糊的残图、把最优权重替换上线这些环节用Shell处理比Python更直接也是很多刚接触linux系统安装python的人容易低估的部分。这套Python加Shell的组合适合正在做农业视觉项目、或想验证模型之外还有多少活的工程师。下面按数据集、训练、自动化、部署的主线把源码设计讲透结构取舍比单个模型文件更能决定系统能否在真实茶园里跑起来。2. 用Python搭建枯萎病识别核心数据集、迁移学习与推理2.1 先解决图片标准化预处理函数是所有样本的统一入口实际采集的茶叶照片渠道很杂——手机拍的、固定摄像头截的、甚至翻拍旧照片。尺寸、亮度、叶片朝向都不一样直接喂进网络会让训练loss持续震荡。所以第一步是把所有路径的图片压到统一尺寸并归一化。常见做法是OpenCV读图它对EXIF旋转和色彩通道的处理比PIL更省心。import cv2 import numpy as np def load_leaf_image(src, size(224, 224)): # 兼容 str 路径与文件流两种输入 if isinstance(src, str): img cv2.imread(src) else: data np.frombuffer(src.read(), dtypenp.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) if img is None: raise ValueError(f无法读取图片: {src}) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, size, interpolationcv2.INTER_AREA) return img.astype(np.float32) / 255.0这个函数同时兼容文件路径和文件流两种输入前一种给训练脚本用后一种给第四章的Flask接口用一处修改两端生效。imdecode把字节流直接解码成图像不需要先在磁盘上落地临时文件。INTER_AREA在缩小图片时保留叶片纹理骨架、减少锯齿换成INTER_LINEAR则适合放大场景。0-1归一化对小规模数据集够用ImageNet的均值方差归一化在枯萎病这种单一对象识别上收益不明显却会引入对mean/std参数的依赖调试时多一个变量。数据集目录建议按train/val/test分三层每层内部再按类别分子目录PyTorch的ImageFolder可以直接读这种结构不用手写Dataset。/data/tea_images/ ├── train/ │ ├── wilted/ │ └── healthy/ ├── val/ └── test/子目录名的命名要一致训练脚本、增强脚本、告警脚本都会引用这两个类别名中途改名会连带改一批脚本。样本量少时test集至少要留30张健康、30张枯萎否则最终指标波动很大不好判断模型真实水平。2.2 训练脚本用迁移学习把ResNet18改造成二分类器枯萎病公开数据集很少自己标注几百张到一千张已是极限。从零训练一个CNN效果远不如加载ImageNet预训练权重、只替换最后一层全连接的做法。PyTorch里这个改造只要几行。import torch.nn as nn from torchvision import models def build_classifier(num_classes2): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_f model.fc.in_features model.fc nn.Linear(in_f, num_classes) # 只替换最后一层保留骨干特征 return model model build_classifier() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) # 冻结骨干只训分类头 criterion nn.CrossEntropyLoss()关键在optimizer只接收model.fc.parameters()骨干网络的梯度不会更新相当于只训练一个分类头。提示如果误传model.parameters()前面所有层都会参与训练几百张样本几乎必然过拟合。想微调时再解冻最后两个stage并把学习率降到1e-4。训练循环里要单独维护一个best_state字典每个epoch结束后用验证集算accuracy只有超过历史最优时才保存权重。最后部署的必须是这份best权重而不是最后一个epoch的权重。类别不均衡时accuracy会失真比如枯萎样本占90%时模型全猜枯萎也有90%准确率所以要同时记录precision和recall枯萎类的recall偏低说明漏报变多比总准确率下降更危险。如果枯萎样本确实远多于健康样本可以在CrossEntropyLoss里传weight参数给少数类更高权重。weight值先用样本数的反比估算再根据验证集recall微调一般不会偏离超过两倍。表枯萎病二分类训练参数建议参数建议值说明image_size224与ResNet输入匹配显存紧张先减batchbatch_size16~32单卡建议32OOM时降到16epochs20~30迁移学习收敛快太多反而过拟合lr1e-3只训分类头解冻后降到1e-4数据划分7:2:1训练/验证/测试划分时固定随机种子2.3 推理脚本识别结果不能只看argmax训练结束后导出的.pth文件由推理脚本加载输出枯萎/健康标签。这里建议返回softmax概率而不是直接argmax。茶园的图片类别不可控一张模糊的叶子、一只虫子、甚至镜头误触的照片都会被强行分到某一类置信度低时应该拒识而不是乱报。def predict(model, tensor, threshold0.85): import torch model.eval() with torch.no_grad(): logits model(tensor.unsqueeze(0)) prob torch.softmax(logits, dim1)[0] conf, idx torch.max(prob, dim0) if conf.item() threshold: return -1, conf.item() # 低于阈值返回-1交给人工复核 return idx.item(), conf.item()返回-1表示不确定告警模块对这类样本一律转人工复核。threshold不能拍脑袋要统计验证集全部样本的置信度分布后决定第五章会讲具体做法。这段逻辑在源码里只有几行却是误报率控制的核心也是能demo和能落地的分水岭。3. Shell脚本包圆采集与训练流程照片归档、一键训练与定时任务3.1 数据侧为什么用Shell而不是再写一个Python脚本入行时我也习惯所有逻辑都用python写直到发现每天凌晨要从边缘盒子同步照片到训练服务器。SSH拉取、按日期建目录、删除损坏文件、统计数量生成manifest这些操作原生Shell命令就能完成。rsync断点续传、find筛选、shuf随机抽样写起来比Python短得多还不需要关心虚拟环境是否存在。对一台只装了基础组件的Linux服务器shell脚本是零依赖的任务编排方式正好是shell脚本入门阶段能立刻上手的实战场景。边缘设备拍照、rsync同步、归档建目录、生成训练清单、触发训练这条流水线每一环都是独立可重跑的命令出问题只看退出码就能定位到环节排查成本远低于维护一个几百行的Python编排器。3.2 按日期归档并生成训练清单#!/bin/bash # archive_images.sh set -euo pipefail BASE_DIR/data/tea_images DAY$(date %Y%m%d) SRC_DIR/mnt/edge_capture mkdir -p $BASE_DIR/$DAY rsync -avz --partial $SRC_DIR/ $BASE_DIR/$DAY/ find $BASE_DIR/$DAY -name *.jpg -size -20k -delete find $BASE_DIR/$DAY -name *.jpg $BASE_DIR/$DAY/manifest.txtrsync的--partial保证网线抖动中断后重传不用从头开始-a保留时间戳方便核对拍摄时刻。find用-size -20k过滤掉压缩失败的残图这类文件在移动网络上传时很常见留着会污染训练集。最后的manifest.txt是训练脚本的输入清单后续人工筛样只编辑清单不删原图原图是排查误判的第一手证据。训练集和验证集也可以用Shell从当天新样本里随机抽shuf配合head/tail就够了shuf $BASE_DIR/$DAY/manifest.txt | head -n 20 $BASE_DIR/$DAY/val_extra.txtshuf打乱行序后取前20行做验证样本剩余行留在manifest里参与训练。这种做法的好处是新样本随采随用坏处是每天的验证样本都在变跨天指标不可比。想对比训练效果时验证集必须固定一个目录不参与当天的shuf。归档时文件名里避免中文和空格边缘盒子的固件日期格式通常像20250110_083000.jpg这样纯数字训练脚本解析时间就简单。如果上游文件名带中文先mv重命名再归档否则python脚本里的open和cv2.imread在部分语言环境的容器里会报编码错误。3.3 训练封装脚本环境、日志、退出码三位一体python训练脚本写得再完整直接扔进crontab也会遇到PATH问题——cron环境里找不到python和虚拟环境脚本静默失败后只有一行日志。训练入口必须用一层shell封装把环境变量和日志路径固定下来。#!/bin/bash # train.sh set -euo pipefail export PATH/opt/python3/bin:$PATH source /opt/tea_env/bin/activate LOG_DIR/data/logs mkdir -p $LOG_DIR LOG_FILE$LOG_DIR/train_$(date %Y%m%d_%H%M%S).log python train.py --data /data/tea_images \ --out /data/models/tea_wilt.pth \ $LOG_FILE 21 echo train exit: $?set -euo pipefail让脚本在任一步失败时立即退出训练失败后不会继续执行后续动作。source activate显式激活虚拟环境不依赖用户bashrc。日志重定向到带时间戳的文件训练现场完整保留。输出train exit给上层调度系统判断成败。注意cron运行时的PATH只有/bin和/usr/binpython3路径必须写绝对路径或用export声明否则会在cron日志里留一行command not found找问题特别费时。3.4 定时任务节奏凌晨采集、每周增量训练生产方式下没人手动执行cron是shell脚本最常见的挂载点。30 2 * * * /opt/tea/bin/archive_images.sh /data/logs/cron.log 21 0 6 * * 1 /opt/tea/bin/train.sh /data/logs/cron.log 21 */5 * * * * /opt/tea/bin/watch_detect.sh /data/logs/watch.log 21第一行每天凌晨2:30拉取前一天的拍摄照片避开白天网络高峰第二行每周一早上6点跑增量训练第三行每5分钟探测一次推理服务对应第四章的守护脚本。表cron任务规划任务表达式作用照片归档30 2 * * *拉取并归档前一天照片增量训练0 6 * * 1每周一早训练服务探活*/5 * * * *推理服务异常自动重启增量训练周期不建议短于一周。样本积累不足时模型反复学习同一批图验证集acc虚高但不涨这是典型的假象。判断标准是训练集和验证集的acc差距差距超过10个百分点就是过拟合信号该回采集端补样本而不是继续加epoch。4. 部署联调Flask推理服务与Shell守护脚本怎么分工4.1 把模型封装成HTTP接口边缘端只负责传图片边缘采集程序通常是C或Go写的没法直接加载PyTorch权重。最可靠的对接方式是模型服务化Python起一个轻量HTTP服务接收multipart图片返回JSON结果。Flask在这里够用瓶颈在图像解码和推理不在框架本身。生产环境不用app.run()用gunicorn起多workergunicorn -w 2 -b 0.0.0.0:8000 app:appworker数是2还是4用压测工具测P99延迟决定不是拍脑袋。from flask import Flask, request, jsonify import torch from data_utils import load_leaf_image from inference import predict app Flask(__name__) model torch.load(/data/models/latest.pth, map_locationcpu) app.route(/detect, methods[POST]) def detect(): f request.files[image] tensor load_leaf_image(f.stream) label, conf predict(model, tensor) return jsonify({label: int(label), confidence: round(float(conf), 4)}) app.run(host0.0.0.0, port8000, threadedTrue)回应2.1的伏笔这里传的是f.stream一个file-like对象load_leaf_image走imdecode分支不需要把上传文件先写盘再读盘减少一次IO。map_locationcpu保证没有CUDA的服务器也能起服务有GPU再改成cuda。threadedTrue允许并发请求单卡推理时注意请求排队显存不够时优先限流而不是加worker。端口8000对外网卡开放前建议在Nginx层限制来源IP边缘盒子的地址是固定的白名单比任何鉴权都直接。4.2 守护脚本探活、自动重启、保留现场进程挂在服务器上一定会死。OOM、端口被误占、模型文件被cp覆盖瞬间句柄失效每周总有一两次。Shell守护脚本是兜底方案它不处理根因但保证服务在分钟级恢复。#!/bin/bash # watch_detect.sh URLhttp://127.0.0.1:8000/detect CODE$(curl -s -o /dev/null -w %{http_code} \ -X POST $URL -F image/tmp/health.jpg --max-time 10) if [ $CODE ! 200 ]; then pkill -f app.py || true sleep 2 nohup /opt/tea_env/bin/python /opt/tea/app.py \ /data/logs/app.log 21 echo $(date %F %T) restart reason code$CODE /data/logs/watch.log fi健康检查用的是真实推理请求传一张固定的健康样本图返回200说明服务和模型都活着比单纯测TCP端口可靠。pkill后跟|| true进程不存在时pkill返回非零没有这个容错set -e会直接让脚本静默退出。sleep 2等端口释放端口占用是重启失败的第一大原因。nohup配合让服务脱离SSH会话终端断开不影响运行。4.3 联调阶段最常踩的三个点现象最常见原因处理办法接口报504推理超时检查是否误用CPU跑大模型或上传图分辨率过大启动即退出8000端口被占ss -lntp | grep 8000kill旧进程再起全天输出同一类加载了过拟合权重确认部署的是best_model.pth不是最后一个epoch第三条在源码里最隐蔽。如果train.sh里没有单独保存best权重部署脚本引用的往往是最终epoch的模型这类模型在验证集上表现可能很差。建议训练循环内维护best_state_dict训练结束后另存best_model.pth部署侧永远引用这个固定文件名。另外一个容易忽略的细节是Python和Shell两侧日志时间要统一训练日志用北京时间守护脚本用UTC两个时间线对不上时排查故障要多绕一圈。统一在脚本开头export TZAsia/Shanghaicron的时区设置也一起核对。5. 让枯萎病检测在真实茶园里更可靠的三个落地技巧5.1 用置信度分布校准阈值而不是拍脑袋定0.85第2.3节里的threshold0.85只是初值。上线前把验证集全部样本过一次推理记录每张图的最大置信度分别画出枯萎类和健康类的分布。两个分布如果在0.7到0.9区间重叠严重说明类间区分度不够调阈值治标不治本要回训练侧加样本和增强。重叠小但靠阈值切分时取分布波谷位置的值比固定0.85更合理。阈值确定后写进配置文件不要在代码里硬编码后续换季样本分布变了改配置比重发代码快。5.2 样本不足时先做增强别急着换大模型几百张样本时ResNet18加随机增强就够用。换ResNet50或ViT参数量更大样本量喂不起过拟合后验证集acc反而更低。torchvision里三个增强组合效果最直接train_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), ])RandomHorizontalFlip对叶片这类对称对象很有效RandomResizedCrop模拟不同拍摄距离scale控制在0.8到1.0之间避免裁掉病斑区域。增强只加在训练集验证集和测试集保持原图否则指标虚高部署后对不上。5.3 模型产物的命名约定与一键回滚推荐命名格式model_YYYYMMDD_HHMM.pth训练脚本维护一个latest软链接指向当前最优版本。回滚时不需要重新训练改软链接再重启守护脚本ln -sfn /data/models/model_20250110_1402.pth /data/models/latest.pth /opt/tea/bin/watch_detect.shln -sfn的n参数很关键它把latest当成符号链接本身而不是链接目标目录缺了n会在/data/models/下多套一层目录。回滚后调一次健康检查接口确认推理结果正常。系统上线后每周翻一遍误报日志比反复调网络结构收获更大误报样本攒起来按周回填训练集模型就跟着茶园的真实状态走比任何参数微调都可靠。本文还有配套的精品资源点击获取
返回列表