2026/10/11 0:43:44

PyTorch柑橘成熟度识别:从数据流水线到PyQt部署实战

PyTorch柑橘成熟度识别:从数据流水线到PyQt部署实战 简介资源包围绕柑橘成熟度识别任务提供基于PyTorch深度学习框架的卷积神经网络完整工程适合图像分类初学者及农业智能化项目开发者参考。包内共一百二十个文件以一百一十三张柑橘成熟度图片为核心另含三个Python脚本及三个文本文件整体约十点八兆。脚本依次实现数据集文本生成、卷积神经网络模型训练和界面展示其中数据集预处理涵盖短边灰边补正方形与随机旋转扩增可有效提升模型泛化能力。训练完成后模型保存至本地便于后续调用环境依赖通过依赖清单说明上手路径清晰。目前已有二百三十七人学习下载对于希望快速搭建柑橘成熟度识别方案、了解数据增强与基础训练流程的用户具有直接参考价值。1. 柑橘成熟度识别不是调参是先把一条数据流水线跑通拿到这个“柑橘成熟度识别”项目时我第一反应是去看它的数据集目录而不是训练代码。原因很简单做图像分类的卷积神经网络项目90% 的坑都藏在数据怎么整理、怎么预处理、怎么喂进模型里。这个项目恰好把整条流水线拆成了三个脚本——先做数据集文本生成再做模型训练最后套一个 PyQt 界面每个步骤都能单独跑、单独验证。它解决的是“从一堆原始照片到能弹窗识别柑橘成熟度”的完整闭环问题适合做课程设计、毕业设计或者第一次想完整跑通一个 PyTorch 图像分类项目的人。复现的价值不在于网络结构多新颖而在于数据集生成脚本和训练脚本之间的衔接方式值得照着写一遍。2. 数据集与预处理灰度边补正方形、旋转扩增与 txt 生成2.1 数据集结构分析与类别映射打开压缩包里的数据集文件夹能看到两类图片Healthy和Greening对应的就是健康成熟柑橘和青绿未熟柑橘。文件命名里藏着预处理痕迹比如Healthy (9_rotated45.jpg是旋转 45 度后的扩增样本Healthy (9_flip.jpg是水平翻转后的样本原始图不带这些后缀。这意味着数据集不是“干干净净”的原始照片而是已经预先做过一次增强的版本。类别映射很简单按文件夹名走文件夹名含义观察特征Healthy健康/成熟柑橘表皮偏黄、色泽均匀Greening未熟/返青柑橘表皮带明显绿色有一个需要提前注意的问题Greening类别的原始样本在文件名列表中只出现了一张。如果训练集和验证集按常规比例切分验证集里可能只有一两条 Greening 样本评估指标会很虚。后面避坑章节我会专门说这个问题。2.2 01 脚本核心逻辑遍历文件夹、按比例生成训练与验证 txt01 脚本的名字叫“数据集文本生成制作”它的作用是把图片路径和标签写成 txt供训练脚本读取。常见实现思路是这样import os import random from glob import glob # 数据集根目录改成你自己的实际路径 data_root ./dataset train_ratio 0.8 random_seed 42 class_names [Healthy, Greening] class_to_idx {name: i for i, name in enumerate(class_names)} random.seed(random_seed) train_lines, val_lines [], [] for class_name in class_names: class_dir os.path.join(data_root, class_name) # 只筛选 jpg/jpeg/png 后缀避免把缩略图或系统文件混进来 img_paths glob(os.path.join(class_dir, *.jpg)) \ glob(os.path.join(class_dir, *.jpeg)) \ glob(os.path.join(class_dir, *.png)) img_paths sorted(img_paths) random.shuffle(img_paths) split_idx int(len(img_paths) * train_ratio) for p in img_paths[:split_idx]: train_lines.append(f{p} {class_to_idx[class_name]}\n) for p in img_paths[split_idx:]: val_lines.append(f{p} {class_to_idx[class_name]}\n) with open(train.txt, w, encodingutf-8) as f: f.writelines(train_lines) with open(val.txt, w, encodingutf-8) as f: f.writelines(val_lines) print(f训练集样本数: {len(train_lines)}, 验证集样本数: {len(val_lines)})这段代码做的事情很直接遍历每个类别目录收集图片路径打乱后按 8:2 切分训练集和验证集写成两行一组的路径 标签格式。train_ratio控制切分比例小数据集建议 0.8图片多的时候可以提高到 0.85 或 0.9。random_seed保证每次运行切分结果一致这样复现实验时不会因为随机性导致结果对不上。值得留意的点是后缀筛选。你实际运行时系统可能生成Thumbs.db或.DS_Store不显式限制后缀的话这些文件会被 glob 收集进列表训练时读图直接报错。这属于常见翻车点后面避坑章我会展开。2.3 灰度边补正方形的原理与 OpenCV 实现摘要里明确提到预处理方式是“通过在较短边增加灰边使图片变为正方形”。这一步的目的是让输入图片尺寸统一。CNN 的池化层和全连接层通常要求固定输入尺寸直接用resize拉伸会把柑橘压成椭圆破坏形状特征。补边则能保留原始比例只扩展背景区域。补黑边还是灰边我建议不要补黑边。黑色像素的 RGB 值全是 0经过归一化后会变成很大的负数在网络前几层产生强烈的虚假边缘响应。补灰边是更稳的做法YOLO 系列里填充默认值 114 也是同样的思路。常见代码是这样import cv2 import numpy as np def letterbox_square(img, size224, fill_value114): h, w img.shape[:2] # 短边补到和长边一致凑成正方形 if h w: top 0 bottom 0 pad (h - w) // 2 left pad right h - w - pad else: left 0 right 0 pad (w - h) // 2 top pad bottom w - h - pad # 用常数填充112 或 114 都可以关键是固定 img_padded cv2.copyMakeBorder( img, top, bottom, left, right, borderTypecv2.BORDER_CONSTANT, value(fill_value, fill_value, fill_value) ) img_resized cv2.resize(img_padded, (size, size)) return img_resized这里cv2.copyMakeBorder在短边两侧对称补边value是填充灰度值。选 224 是因为它是 ResNet、VGG 这类常见网络的默认输入尺寸能直接套用 ImageNet 预训练权重。如果你后面要换 EfficientNet可能要改成 240 或 260这个数字要和模型输入层匹配。2.4 旋转与翻转扩增的标注一致性扩增的逻辑是“旋转角度 翻转让数据集增大”。注意旋转 45 度后图片四个角会露出黑区域。如果你的训练脚本没有对扩增图重新做描边处理模型就会看到大量黑色角块这会在特征图里形成固定模式干扰识别。我的习惯是旋转前先补灰边成正方形旋转后再用cv2.BORDER_REPLICATE或继续常量填充补一次角。扩增只对训练集生效验证集永远用原始图和原始尺寸做同样预处理不额外扩增。一句话总结数据增强的目标是让模型在每个 epoch 看到不同的图而不是让验证集看起来更“华丽”。3. 训练流程PyTorch DataLoader、迁移学习与关键参数3.1 自定义 Dataset 读取 txt01 脚本生成train.txt和val.txt后02 脚本就要负责读它。PyTorch 里通过自定义 Dataset 把文本内容转为可迭代的训练样本import torch from torch.utils.data import Dataset from PIL import Image class CitrusDataset(Dataset): def __init__(self, txt_path, transformNone): self.samples [] self.transform transform with open(txt_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue img_path, label line.split() self.samples.append((img_path, int(label))) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] img Image.open(img_path).convert(RGB) if self.transform: img self.transform(img) return img, label读 txt 的好处是训练和验证的划分逻辑在 01 脚本里已经定死02 脚本不用关心数据集目录结构换数据集时只要重新生成 txt 就行。需要特别说明的是.convert(RGB)如果数据集中混入了灰度图或 RGBA 图不加这一行会在torch.stack时报通道数不一致错误。这是实际复现时最常见的翻车原因之一。DataLoader 的参数也有讲究。batch_size小数据集建议 8 或 16过大容易显存溢出num_workers在 Windows 上设置为 0 更稳设置为大于 0 可能触发多进程读取的 RuntimeError。3.2 模型选型自定义 CNN 还是迁移学习02 脚本里的模型结构以自带代码为准但如果你在复现后觉得训练收敛太慢我的建议是直接用 PyTorch 内置的 ResNet18 做迁移学习import torchvision.models as models def build_model(num_classes2): model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 冻结前四层只微调后面的层 for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad False # 替换最后一层全连接 model.fc torch.nn.Linear(model.fc.in_features, num_classes) return model为什么这么做这个数据集的样本量撑不起从零训练一个深层 CNN。ImageNet 预训练权重已经在海量自然图片上学过基础纹理、颜色和形状特征柑橘表面的斑点、颜色渐变对网络来说不是陌生概念所以只需微调最后几层。如果你的机器没有 GPU可以把weights参数去掉但训练时间会变长且准确率大概率上不去。num_classes2对应 Healthy 和 Greening 两类后面改成多分类时这里要同步调整。3.3 训练循环与模型保存策略训练主体循环是标准的 PyTorch 写法关键在 checkpoint 保存策略和优化器选择import torch.optim as optim from torch.optim.lr_scheduler import ReduceLROnPlateau device torch.device(cuda:0 if torch.cuda.is_available() else cpu) model build_model().to(device) criterion torch.nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler ReduceLROnPlateau(optimizer, modemin, patience5, factor0.5) best_val_acc 0.0 for epoch in range(50): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * imgs.size(0) # 每个 epoch 结束后在验证集上评估 model.eval() correct 0 total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) val_acc correct / total # 只保存验证集准确率最高的权重 if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth) scheduler.step(val_acc) print(fEpoch {epoch1}: loss{running_loss/len(train_loader.dataset):.4f}, val_acc{val_acc:.4f})ReduceLROnPlateau会在验证集准确率连续 5 个 epoch 不升时把学习率减半这是小数据集上最实用的调度策略。保存最优模型而非最后一个模型能避免最后的随机波动毁掉整个训练成果。代码里用with torch.no_grad()包裹验证过程不计算梯度能省大量显存。如果你不想在训练时盯日志可以在循环里加一个early_stop计数器连续 10 个 epoch 验证集没有提升就提前终止。4. 复现避坑预处理不一致、类别不平衡与显存不足4.1 训练 loss 震荡验证集准确率忽高忽低现象loss 整体在下降但验证集准确率每个 epoch 波动超过 5%甚至出现不升反降。原因一是旋转扩增后的图片无规律地同时出现在训练集和验证集导致验证集分布不稳定二是在 01 脚本里随机切分时没有按类别分层采样Greening 类样本本来就少验证集可能分不到或只分到一条。解决01 脚本切分时要按类别分别做 8:2 切分而不是把所有图片混在一起随机分。扩增图只放训练集验证集只用原始图。具体做法是在split_idx计算前先过滤出不带rotated、flip后缀的样本作为验证集候选。4.2 模型精度高但看热力图发现它学的是灰边不是果实现象训练时验证集准确率能到 90% 以上但把模型部署到 PyQt 界面后随便拍一张带背景的照片准确率骤降。原因训练前预处理固定用短边补灰边图片中柑橘周围全是均匀灰色块。网络很可能学到“边缘灰色区域大 Healthy”这类伪特征而不是果实本身的颜色纹理。解决训练和推理必须走完全一致的前处理链路。更稳的做法是在补灰边后加一步随机裁剪让柑橘在画面中的位置有轻微变化打破灰边和类别的强关联。我复现时是先把图片从 224 放大到 256补灰边后随机裁剪回 224效果比纯补边好很多。4.3 01 脚本跑完训练时报图片读取失败现象train.txt生成正常但 02 脚本训练到一半抛PIL.UnidentifiedImageError或FileNotFoundError。原因txt 里写的是相对路径当前工作目录和生成 txt 时的目录不一致或者 glob 把Thumbs.db、.DS_Store混进去了。解决01 脚本里改成os.path.abspath绝对路径写入 txt后缀筛选必须同时排除*.db和*.store。如果数据集是从网盘下载解压的还要检查是不是有嵌套目录有的压缩包会多出一层父文件夹导致路径前缀对不上。4.4 GPU 显存不足训练直接崩现象运行 02 脚本时出现CUDA out of memory代码当场终止。原因batch_size设置过大或者num_workers开太多导致内存碎片化。小显存显卡跑 ResNet18 时batch_size16加input_size224就可能满。解决先把batch_size降到 8num_workers设 0去掉pin_memoryTrue。要是还崩就把输入尺寸从 224 缩到 160这会牺牲一点精度但能稳定跑通。跑通之后再逐步调大明确边界之后才不慌。4.5 Greening 类只有一张原始图模型偏向 Healthy现象训练结束后打印混淆矩阵Healthy 类 F1 很好Greening 类查全率很低模型总是把青果判成健康果。原因类别严重不平衡Greening 原始样本只有 1 张即使旋转翻转扩增信息量也远小于 Healthy。单靠增强解决不了因为增强只是把同一张图变换视角并没有增加真实的绿色果皮纹理变化。解决对这个类别的扩增不能只用旋转翻转建议叠加亮度扰动、对比度扰动和 HSV 色相微调模拟不同光照条件下的青果外观。如果还是不行就去网上补 10 到 20 张未熟柑橘的图片放到 Greening 文件夹重新跑 01 脚本。数据增强救不了信息缺失补样本才是根治办法。5. PyQt 界面推理加载权重、统一前处理与结果展示5.1 界面结构选择图片、模型推理、置信度显示03 脚本是 PyQt 界面基本组成是三块一个显示图片的 QLabel、一个“选择图片”按钮、一个显示推理结果的文本框。界面逻辑不复杂核心是把训练好的best_model.pth加载进来对用户选的图片走一遍前处理然后输出类别和置信度。这里要复用训练时的前处理代码。不要重新写一套 resize 逻辑否则很容易出现训练用 224、推理用 256 这种低级错配。5.2 推理前处理函数与置信度输出下面是一个可以和 PyQt 槽函数直接对接的推理函数import torch import cv2 import numpy as np from torchvision import transforms device torch.device(cuda:0 if torch.cuda.is_available() else cpu) def preprocess_for_inference(img_bgr, size224, fill_value114): # 和前文 letterbox_square 完全一致保持训练推理前处理统一 img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) img_square letterbox_square(img_rgb, sizesize, fill_valuefill_value) img_normalized img_square.astype(np.float32) / 255.0 # 用 ImageNet 均值和标准差归一化 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img_normalized (img_normalized - mean) / std tensor torch.from_numpy(img_normalized.transpose(2, 0, 1)).unsqueeze(0) return tensor.to(device) def predict(image_path, model, class_names): img_bgr cv2.imread(image_path) if img_bgr is None: return 无法读取图片检查路径或文件格式, 0.0 input_tensor preprocess_for_inference(img_bgr) model.eval() with torch.no_grad(): outputs model(input_tensor) probs torch.softmax(outputs, dim1)[0] conf, pred_idx torch.max(probs, 0) return class_names[pred_idx.item()], conf.item()归一化用的mean和std是 ImageNet 的标准值。如果你的训练脚本里面没有这段标准化而是在 Dataset 里用transforms.ToTensor()直接归一化到 0 到 1那推理时也要保持一致否则输入分布错位置信度会变得毫无意义。一个排查技巧是跑一张训练集里的图走推理如果输出的置信度在 0.9 以上说明前处理链路通了如果连训练集图片都判错第一件事检查前处理是否和训练时一致而不是怀疑模型权重有问题。5.3 置信度阈值与实际使用界面里通常要加一个置信度判断当conf低于某个阈值时显示“无法判断请重新拍摄”。常见做法是阈值设在 0.6 到 0.7 之间低于阈值就提示用户换一张更清晰、果实居中的照片。这个阈值不能写死在模型里要在界面上留一个可调的输入框因为不同光照条件下模型输出的校准程度不一样。加了这一层界面的容错率会明显提升不会出现“拿着一张糊图也硬给结论”的情况。6. 扩展验证多分类改造、Grad-CAM 与结果核对如果你复现完这个二分类项目想让它更有说服力可以做三个方向的扩展。第一个是把二分类改成多分类例如把 Healthy 细分为“未熟”“半熟”“成熟”。操作步骤不复杂在数据集文件夹里新建三个子目录并把图片归类01 脚本里把class_names改成三个类别03 脚本里把model.fc的输出维度改成 3重新跑一遍训练即可。注意多分类时训练集数量要尽量均衡否则模型会偏向样本多的类别。第二个方向是验证模型到底在看什么。用 Grad-CAM 或 PyTorch 自带的torchcam库把最后卷积层的梯度响应叠到原图上能直观看到模型分类时关注的区域。如果高亮区域集中在果皮纹理而不是灰边背景说明训练是健康的如果高亮区全在边缘填充区就回到第 4 章讲的问题重新审视预处理。这一步在答辩或写报告的时候非常加分比贴准确率曲线更有说服力。第三个方向是跨目录泛化测试。把训练好的best_model.pth放到另一台电脑随便找几张没参与训练的真实柑橘照片跑一遍记录准确率和置信度。这能暴露过拟合问题特别是 Greening 类如果只依赖增强图真实照片大概率会翻车。核对的指标不只看准确率还要看两类各自的召回率。从那以后我每次拿到一个新的图像分类项目都强制先走一遍完整的数据流先跑 01 脚本看生成的 txt 内容确认路径无缺失、无混入文件、类别平衡再跑训练看 val_acc 是否合理不急着调模型结构最后才碰界面。数据流是这条链路的底盘底盘不稳后面调什么都像玄学。希望这个项目的复现过程能帮你把 PyTorch 图像分类的整套流程真正跑通。本文还有配套的精品资源点击获取