ARTICLE DETAIL

资讯详情

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

基于卷积神经网络的鲜茶叶分选:从数据集构建到产线部署的工程实践

基于卷积神经网络的鲜茶叶分选:从数据集构建到产线部署的工程实践 简介这份PDF文献面向从事茶叶加工、智能装备研发及计算机视觉应用的研究人员与工程技术人员针对机采鲜茶叶中风选、筛选难以精确细分等级的问题提出结合计算机视觉与深度学习的智能分选方案。资源包内仅含1个PDF文件大小约2.31MB即《基于卷积神经网络的鲜茶叶智能分选系统研究》全文便于直接查阅与引用。文中搭建了7层结构的卷积神经网络识别模型通过共享权值与逐渐下降的学习速率提升训练性能并借助图像分割与尺度变换完成输入归一化避免繁琐的人工特征提取。实验表明系统可对单芽、一芽一叶、一芽二叶、一芽三叶、单片叶及叶梗进行有效类别分选识别正确率不低于90%。目前已有172人学习适合作为智能系统开发、人工智能与图像识别方向的参考文献与专业指导材料。1. 鲜茶叶分选这道题卷积神经网络到底能解到什么程度鲜茶叶分选是个典型的「看着简单、做起来全是坑」的工业视觉问题。一斤鲜叶里混着单芽、一芽一叶、一芽二叶、老叶、茶梗、虫伤叶传统做法靠振动筛加风选按尺寸和重量粗分遇到颜色相近但品级不同的芽叶就抓瞎。人工拣剔效率低、标准漂移大旺季招不到人是常态。卷积神经网络CNN进这个场景核心价值不是「AI 概念」而是把「芽叶形态 色泽 纹理」这三类人眼判据变成可复现的特征提取让分选标准从老师傅的手感变成可标定、可回滚的模型参数。这篇笔记面向两类人一类是茶机厂或茶企里想上视觉分选但不知道从哪下手的工程师另一类是做过通用图像分类、想把这套东西迁到农产品分选上的算法同学。我会按「数据怎么采、模型怎么选、产线怎么落、坑在哪」的顺序讲参数和命令都给到能直接抄的程度。2. 从鲜叶图像到可训练数据集采集、标注与增强的完整链路2.1 为什么鲜茶叶的数据集比通用数据集难做鲜茶叶分选的数据集难点不在量在「一致性」。通用图像分类数据集比如 ImageNet的拍摄条件相对可控而鲜叶从茶园到分选机之间光照、含水率、堆放厚度、传送带速度全在变。同一批鲜叶早上采的和下午采的颜色能差出一个色阶摊放两小时和摊放半小时叶片挺度和反光完全不同。如果采集时不把这些变量记录下来后面模型在产线上翻车你连原因都找不到。我一般的做法是采集阶段就按「变量矩阵」来组织而不是随手拍。具体来说至少控制四个维度——光照顺光/逆光/补光、鲜叶状态刚采/摊放1h/摊放2h、品级单芽/一芽一叶/一芽二叶/老叶/茶梗/杂质、背景传送带空载/薄层/厚层。每个组合拍 50100 张这样出来的数据集才有「抗干扰」的底子。常见做法是用工业相机加环形补光固定在传送带正上方 3040cm视场覆盖 20cm×20cm 左右分辨率至少 1280×960保证单芽的芽尖在图像里占 40 像素以上。标注环节鲜茶叶分选有个特殊点类别边界模糊。「一芽一叶」和「一芽二叶」在图像上可能只差一片刚冒头的小叶标注员之间的一致性Inter-Annotator Agreement往往只有 0.7 左右。我的经验是标注规范里必须写死「以芽为基准数展开叶数未展开的不计」并且每类给 20 张「标准参考图」贴在标注工具里标注员拿不准就对照。标注格式用 YOLO 的 txt 或 COCO 的 json 都行但建议同时存一份「原始采集元数据」光照、摊放时间、批次后面做误差分析时能按维度切片。2.2 用 Python 做鲜叶数据集的清洗与增强数据拿到手第一步不是直接喂网络而是清洗。鲜叶图像里常见的废片包括运动模糊传送带速度没匹配好、过曝补光太强、叶片重叠超过 70%标注不可靠、背景杂物非茶叶的梗、虫体。下面这段脚本做三件事按清晰度过滤、按亮度过滤、按标注框面积过滤。import cv2 import numpy as np import os import json def laplacian_variance(img_gray): 拉普拉斯方差值越低越模糊鲜叶建议阈值 80 return cv2.Laplacian(img_gray, cv2.CV_64F).var() def mean_brightness(img_gray): 平均亮度过曝或过暗都筛掉鲜叶建议 60~200 return np.mean(img_gray) def filter_dataset(img_dir, label_dir, out_txt, blur_th80, bright_lo60, bright_hi200): kept, dropped [], [] for fname in os.listdir(img_dir): if not fname.lower().endswith((.jpg, .png)): continue img_path os.path.join(img_dir, fname) img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur laplacian_variance(gray) bright mean_brightness(gray) # 标注文件同名 .txt label_path os.path.join(label_dir, os.path.splitext(fname)[0] .txt) if not os.path.exists(label_path): dropped.append((fname, no_label)) continue with open(label_path) as f: lines [l.strip() for l in f if l.strip()] if len(lines) 0: dropped.append((fname, empty_label)) continue if blur blur_th: dropped.append((fname, fblur_{blur:.1f})) continue if bright bright_lo or bright bright_hi: dropped.append((fname, fbright_{bright:.1f})) continue kept.append(fname) with open(out_txt, w) as f: f.write(\n.join(kept)) print(fkept{len(kept)}, dropped{len(dropped)}) for d in dropped[:10]: print(drop:, d) return kept filter_dataset(./images, ./labels, ./clean_list.txt)这段代码的逻辑很直白拉普拉斯方差衡量图像高频分量模糊图像的高频被抹掉方差会明显偏低鲜叶因为纹理细正常清晰图的方差通常在 100 以上低于 80 基本是糊了。亮度阈值 60200 是经验值过暗说明补光不足或叶片堆太厚过曝说明补光直射导致高光溢出这两类都会让颜色特征失真。参数怎么改如果你的相机是黑白相机亮度范围要重新标如果传送带速度很快模糊阈值可以降到 60但要在增强阶段补运动模糊模拟。清洗完做增强。鲜叶分选的增强不能乱用通用策略——水平翻转可以芽叶左右对称垂直翻转不行芽尖朝上的方向是品级判据之一颜色抖动要克制因为色泽本身就是分类依据抖动太猛会把「一芽一叶」的嫩绿抖成「老叶」的暗绿。我一般用 Albumentations 配一套「温和增强」import albumentations as A import cv2 train_transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.5), A.HueSaturationValue(hue_shift_limit5, sat_shift_limit10, val_shift_limit10, p0.4), A.GaussNoise(var_limit(5.0, 20.0), p0.3), A.MotionBlur(blur_limit3, p0.2), # 模拟传送带轻微运动模糊 A.Resize(448, 448), ], bbox_paramsA.BboxParams(formatyolo, label_fields[class_labels])) # 使用示例 img cv2.imread(sample.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) aug train_transform(imageimg, bboxes[[0.5, 0.5, 0.2, 0.3]], class_labels[1])这里的关键参数brightness_limit0.1和hue_shift_limit5是刻意压小的目的只是让模型对采集时的轻微波动鲁棒而不是改变类别语义。MotionBlur(blur_limit3)模拟传送带速度波动带来的轻微模糊让模型不至于在产线提速时崩掉。Resize(448, 448)是输入尺寸后面选模型时会对应调整。如果你的鲜叶目标特别小单芽在图中占比低建议用 640 甚至 800别为了省显存牺牲小目标。提示增强后的图像一定要抽样人工看 50 张确认没有把「一芽二叶」增强成「一芽一叶」的歧义样本。我见过有人用 RandAugment 默认强度结果颜色抖动把嫩芽抖成了老叶色模型学出来的决策边界完全是错的。3. 选 CNN 还是上 Transformer鲜叶分选的模型选型与训练参数3.1 鲜茶叶分选该选哪种 CNN 结构标题里写的是「基于卷积神经网络」但 CNN 是个大类从 ResNet、EfficientNet 到 MobileNet、ConvNeXt 都算。鲜叶分选选型的核心约束有三个目标尺寸小单芽可能只占 40×40 像素、类别间差异细一芽一叶 vs 一芽二叶、产线要实时至少 1530 FPS。这三个约束决定了你不能直接拿 ImageNet 上刷分的最大模型往上堆。我的选型逻辑是先看目标尺寸如果单芽在输入图里小于 64×64优先选保留高分辨率特征图的骨干比如 EfficientNet-B0 的 stride 配置比 ResNet-50 更友好或者直接用 YOLOv8n/YOLOv8s 这类检测框架做「检测 分类」一体。如果类别差异主要在纹理和色泽ConvNeXt-Tiny 在细粒度任务上通常比同量级 ResNet 好一截因为它的 7×7 大核卷积更擅长抓纹理。如果产线算力只有 Jetson Nano 级别MobileNetV3-Small 加一个自定义分类头是稳妥选择精度掉 23 个点但帧率能翻倍。下面给一个基于 timm 的 EfficientNet-B0 微调模板这是我在鲜叶分选里用得最多的 baselineimport torch import torch.nn as nn import timm from torch.utils.data import DataLoader from torchvision import transforms, datasets NUM_CLASSES 6 # 单芽/一芽一叶/一芽二叶/老叶/茶梗/杂质 BATCH_SIZE 32 EPOCHS 40 LR 3e-4 DEVICE cuda if torch.cuda.is_available() else cpu # 鲜叶专用增强训练用温和增强验证只做 Resize Normalize train_tf transforms.Compose([ transforms.Resize((448, 448)), transforms.RandomHorizontalFlip(0.5), transforms.ColorJitter(brightness0.1, contrast0.1, saturation0.1, hue0.02), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize((448, 448)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(./data/train, transformtrain_tf) val_ds datasets.ImageFolder(./data/val, transformval_tf) train_loader DataLoader(train_ds, batch_sizeBATCH_SIZE, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_sizeBATCH_SIZE, shuffleFalse, num_workers4) # 加载预训练骨干替换分类头 model timm.create_model(efficientnet_b0, pretrainedTrue, num_classesNUM_CLASSES) model model.to(DEVICE) # 分层学习率骨干小学习率分类头大学习率 backbone_params [p for n, p in model.named_parameters() if classifier not in n] head_params [p for n, p in model.named_parameters() if classifier in n] optimizer torch.optim.AdamW([ {params: backbone_params, lr: LR * 0.1}, {params: head_params, lr: LR}, ], weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxEPOCHS) criterion nn.CrossEntropyLoss(label_smoothing0.1) for epoch in range(EPOCHS): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(DEVICE), labels.to(DEVICE) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 验证略按需加 accuracy / confusion matrix print(fepoch {epoch} done)这段代码有几个鲜叶场景特有的设计。第一ColorJitter的 hue 只给 0.02比通用设置的 0.1 小一个量级原因前面说过——色泽是分类依据不能大改。第二分层学习率把骨干压到 3e-5分类头用 3e-4因为鲜叶数据集通常只有几千到几万张骨干全量微调容易过拟合小学习率让它「轻微适配」就够了。第三label_smoothing0.1是必须的鲜叶类别边界模糊硬标签会让模型对「一芽一叶/一芽二叶」的边界过度自信平滑后泛化更好。参数怎么调如果显存不够把 batch 降到 16学习率同比降到 1.5e-4如果类别极不平衡茶梗样本远少于单芽把CrossEntropyLoss换成带 weight 的版本weight 按类别频率的倒数设。3.2 训练过程中必须盯住的三个指标鲜叶分选模型训练光看 accuracy 会被骗。我一般同时盯三个指标每类的 recall、混淆矩阵里相邻类别的误分率、以及「高置信度错误」的数量。为什么因为产线上最怕的不是「不确定」而是「自信地分错」——一个一芽二叶被以 0.95 置信度分成一芽一叶下游拼配就全乱了。具体做法每个 epoch 在验证集上算混淆矩阵重点看「一芽一叶→一芽二叶」和「一芽二叶→老叶」这两对相邻类。如果这两对的误分率超过 8%说明模型没学到「叶片展开程度」这个关键特征要么是分辨率不够要么是增强把叶片边缘糊掉了。这时候的排查顺序是先把输入分辨率提到 640 重训一轮如果没改善检查标注里这两类的边界是否一致最后才考虑换更大的骨干。「高置信度错误」的统计方式是取验证集里预测置信度 0.9 但预测错误的样本单独存成一个文件夹人工看。我做过统计鲜叶分选里这类样本 70% 以上是「标注本身就模棱两可」的剩下 30% 是「光照极端」或「叶片严重重叠」。前者要回去修标注后者要在增强里补对应场景。这个习惯帮我省了大量盲目调参的时间。注意不要用测试集调参。鲜叶分选的测试集应该按「批次」划分而不是随机划分。同一批鲜叶的图像高度相似随机划分会让训练集和测试集「泄漏」测出来的精度虚高 510 个点上线就露馅。4. 从实验室到产线鲜叶分选系统的部署与推理优化4.1 推理速度与精度的平衡怎么定实验室里模型跑 95% 精度到了产线可能只有 80%原因通常不是模型本身而是「推理链路」没对齐。鲜叶分选系统的推理链路是相机采图 → 预处理去噪、白平衡→ 模型推理 → 后处理NMS、置信度过滤→ 执行机构气阀/挡板。这条链路里预处理和后处理的时间经常被忽略但它们可能占掉一半的延迟。我的做法是先测「端到端延迟」再拆解优化。用下面这段代码测单张推理时间注意要包含预处理import time import torch import numpy as np import cv2 def benchmark(model, img_path, devicecuda, warmup10, runs100): model.eval() img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 预处理 img_resized cv2.resize(img, (448, 448)) img_norm (img_resized / 255.0 - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) tensor torch.from_numpy(img_norm).permute(2, 0, 1).float().unsqueeze(0).to(device) # warmup with torch.no_grad(): for _ in range(warmup): _ model(tensor) if device cuda: torch.cuda.synchronize() # 计时 t0 time.time() with torch.no_grad(): for _ in range(runs): _ model(tensor) if device cuda: torch.cuda.synchronize() t1 time.time() print(f单张推理: {(t1-t0)/runs*1000:.2f} ms, FPS: {runs/(t1-t0):.1f}) benchmark(model, test.jpg)测出来如果单张超过 30ms对应 33 FPS就要优化。优化顺序我一般是这样先转 TensorRT 或 ONNX RuntimeEfficientNet-B0 在 Jetson Xavier NX 上从 PyTorch 的 25ms 能压到 8ms 左右如果还不够把输入从 448 降到 320精度通常掉 12 个点但速度能再快一倍最后才考虑换 MobileNet 这类轻量骨干。注意转 TensorRT 时要把预处理也放进引擎里用--fp16和自定义插件否则预处理在 CPU 上反而成瓶颈。4.2 产线集成的四个接口约定模型部署不是把 .pt 文件丢给产线就完事接口约定不清楚联调能拖两周。我一般会和产线工程师先定死四件事第一图像传输格式。相机出的是 Bayer 还是 RGB分辨率多少传输用 GigE 还是 USB3这些决定了预处理代码怎么写。常见坑是相机出 Bayer 图算法端忘了做去马赛克颜色全偏。第二触发信号。是相机硬触发还是软件轮询硬触发延迟低但需要接线软触发灵活但抖动大。鲜叶分选建议硬触发因为传送带速度稳定时固定间隔采图比轮询可靠。第三结果输出格式。分类结果是一个类别 ID 还是「类别 置信度 位置」如果下游要按位置吹气就必须输出检测框如果只是整图分类输出类别 ID 就够。我一般输出 JSON{class_id: 2, confidence: 0.93, bbox: [x, y, w, h]}方便下游解析。第四异常处理。模型置信度低于阈值怎么办图像采集失败怎么办我的约定是置信度低于 0.6 的样本走「默认通道」比如全部归入待人工复检采集失败重试 3 次后报警停机。这些逻辑要写进部署代码不能靠产线工人临场判断。5. 鲜叶分选落地时最容易翻车的五个坑5.1 坑一训练集和产线光照不一致模型「见光死」现象实验室验证集 94%上线第一天精度掉到 75%且错误集中在下午时段。原因训练集是上午顺光拍的产线补光是顶部环形光下午环境光变化后叶片反光模式完全不同。模型学到的是「上午光照下的颜色分布」不是「鲜叶本身的颜色」。解决采集阶段就覆盖多时段、多光照训练时加RandomBrightnessContrast和GaussNoise部署前在产线现场拍 200 张做「现场验证集」如果掉点超过 5%用现场数据做 510 个 epoch 的微调小学习率 1e-5。5.2 坑二类别不平衡导致「茶梗」被吞现象茶梗识别 recall 只有 0.5大量茶梗被分进「老叶」。原因茶梗样本天然少可能只占总量的 3%交叉熵损失被多数类主导模型倾向于把茶梗预测成老叶。解决损失函数加类别权重weight 1 / class_freq或者用 Focal Loss。同时做「过采样」——把茶梗样本复制到和其他类同量级但要注意复制时用不同增强否则过拟合。我一般两者结合权重调到 35 倍过采样到 10% 占比。5.3 坑三输入分辨率压缩太狠小目标特征丢失现象单芽和「一芽一叶」的误分率高达 15%混淆矩阵显示两类互相串。原因为了提速把输入从 448 压到 224单芽在特征图上只剩 20×20 像素芽尖和第一片叶的边界特征被池化掉了。解决要么保持 448 输入要么用「裁剪 分类」两阶段——先检测芽叶位置再对每个目标裁剪出来单独分类。后者速度可能更快因为分类网络输入可以小但检测网络要额外训练。我的经验是如果单芽在 448 输入下小于 48×48直接上两阶段。5.4 坑四数据增强把类别语义改掉了现象训练 loss 正常下降但验证集精度卡在 80% 上不去混淆矩阵里「一芽二叶」大量被分成「老叶」。原因用了ColorJitter(hue0.1)或RandAugment默认强度把嫩叶的黄绿色抖成了老叶的暗绿色模型学到的决策边界和真实语义相反。解决鲜叶分选的增强必须「保守」hue 抖动不超过 0.03saturation 不超过 0.15禁用Equalize、Posterize这类会改变颜色分布的算子。增强后抽样人工检查确认没有语义歧义。5.5 坑五模型更新没有回滚机制一次翻车全批报废现象某次用新数据微调后上线精度不升反降但已经分选了 200 斤鲜叶只能全部返工。原因没有版本管理和 A/B 验证新模型直接替换旧模型出问题才发现。解决模型文件按「日期 训练集版本 精度」命名部署时保留上一个版本新模型先跑「影子模式」只记录预测不执行分选24 小时对比新旧模型的预测差异差异超过 5% 的样本人工复核。确认无误再切换。这个习惯看起来麻烦但能避免「一次翻车全批报废」的血泪教训。6. 把鲜叶分选模型压到 8ms 以内TensorRT 量化与现场验证的一个具体技巧模型训练完只是半成品真正决定产线能不能用的是推理延迟。我拿 EfficientNet-B0448 输入6 分类在 Jetson Xavier NX 上做过一轮完整优化路径是 PyTorch → ONNX → TensorRT FP16 → INT8 校准延迟从 25ms 压到 7.8ms精度只掉 0.6 个点。这里把关键步骤和参数写清楚。第一步导出 ONNX。注意opset用 12 以上动态轴只留 batchimport torch model.eval() dummy torch.randn(1, 3, 448, 448).cuda() torch.onnx.export( model, dummy, fresh_tea.onnx, opset_version13, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )第二步用trtexec转 TensorRT FP16先看延迟trtexec --onnxfresh_tea.onnx --fp16 --saveEnginefresh_tea_fp16.engine \ --shapesinput:1x3x448x448 --workspace2048第三步INT8 量化。鲜叶分选做 INT8 有个坑校准集必须用产线现场图不能用训练集。因为训练集和现场的光照分布有差异用训练集校准出来的 scale 在现场会偏。校准集准备 500 张现场图覆盖不同时段和品级trtexec --onnxfresh_tea.onnx --int8 --fp16 --saveEnginefresh_tea_int8.engine \ --shapesinput:1x3x448x448 --workspace2048 \ --calibfresh_tea_calib.cache第四步现场验证。这一步最容易被跳过但最关键。把 FP16 和 INT8 两个引擎同时部署跑同一批现场图对比预测差异。如果 INT8 和 FP16 的预测不一致率超过 2%说明量化损失太大要么增加校准集要么回退 FP16。我一般会做一个「差异样本池」把两个引擎预测不同的样本存下来人工看如果差异集中在「一芽一叶/一芽二叶」边界说明量化把细粒度特征压没了这时候可以考虑只量化骨干、保留分类头 FP16混合精度。最后给一个我自己的习惯每次模型上线前我会在产线旁蹲半天拿一个笔记本记录「模型分错的样本长什么样」。这个习惯看起来笨但比看任何指标都管用——因为产线上的错误分布和验证集永远不一样只有亲眼看过才知道下一个版本该补什么数据。希望帮到你。本文还有配套的精品资源点击获取
返回列表