首页 / 资讯中心 / 文章详情

水果识别毕业设计实战:ResNet18全链路项目

水果识别毕业设计实战:ResNet18全链路项目 ★ FEATURED ARTICLE
简介这是一套面向计算机专业本科生的毕业设计级水果识别实战项目基于Python与深度学习技术构建端到端识别系统适用于大作业、毕设开发及AI入门实践。资源包含完整可运行源码、预训练模型、标注数据集及配套前端界面所有代码均经本地编译调试通过评审得分98分内容由助教审定难度适中、结构清晰、工程规范。压缩包共277个文件约17.53MB其中8个Python脚本构成核心训练与推理逻辑114个JS和84个GIF支撑Web交互界面26个CSS与多个字体/图标文件woff、ttf、svg等保障前端渲染效果另有HTML、PNG、JPG等用于页面展示与样本示例。目前已有142人下载学习读者可直接部署运行获得从数据加载、模型训练、Web服务封装到可视化识别的全流程实现参考并掌握LayuiBootstrap前端集成、Flask后端接口开发及CNN图像分类典型实践路径。1. 水果识别不是调个cv2.imread就完事一个能跑通、能答辩、能改参数的毕业设计级深度学习项目你是不是也试过网上搜“水果识别 Python”结果下了一堆只有train.py和三行注释的压缩包运行报错ModuleNotFoundError: No module named torchvision查半天发现作者用的是 PyTorch 1.7而你装的是 2.1或者model.pth加载失败提示unexpected key in state_dict更别提数据集目录结构乱成一锅粥/data/train/apple/下混着.jpg和.JPGtest文件夹里还塞了.DS_Store—— 这些都不是玄学是毕业设计答辩前夜真实发生的翻车现场。这个项目不一样它是一套经过本地全链路验证、含完整训练-验证-推理闭环、适配主流显卡GTX 1060 / RTX 3050、带 Web 前端交互界面的水果识别系统。核心不是炫技而是解决三个硬需求① 导师一眼看懂模型结构和评估逻辑② 答辩时能现场换图、实时出结果③ 你能在 2 小时内改出「识别香蕉成熟度」或「区分苹果品种」的新分支。它用 ResNet18 作 backbone但关键在dataset.py里封装了自动大小写归一、路径容错、标签映射缓存在inference.py中内置了 OpenCV PIL 双后端 fallbackWeb 界面不依赖 Flask 复杂路由而是用轻量http.server 静态资源直出——所有这些都为一个目标服务让你把时间花在理解 CNN 特征提取逻辑上而不是 debug 路径拼写错误。适合计算机、软件工程、人工智能方向本科毕设也适合作为深度学习入门后的第一个可交付实战项目。2. 从数据加载到模型训练为什么 ResNet18 是这个项目的“甜点级”选择2.1 数据集结构与预处理不是放对文件夹就叫“有数据集”这个项目附带的数据集共 12 类常见水果苹果、香蕉、橙子、葡萄、草莓、梨、桃子、李子、芒果、菠萝、猕猴桃、西瓜每类 300–500 张实拍图分辨率集中在 640×480 到 1920×1080 区间。但真正决定训练成败的不是图片数量而是目录组织方式与加载器健壮性。项目采用标准 ImageFolder 结构dataset/ ├── train/ │ ├── apple/ # 所有小写无空格 │ │ ├── 001.jpg │ │ └── 002.jpg │ ├── banana/ │ └── ... ├── val/ │ ├── apple/ │ └── ... └── test/ ├── apple/ └── ...注意train/val/test三级目录必须严格存在且子目录名即类别名全部小写、无中文、无特殊字符。项目dataset.py中的FruitDataset类会自动扫描train/下所有子目录生成class_to_idx映射并缓存到dataset/classes.json避免每次初始化都遍历磁盘——这是防止答辩演示时卡顿的关键细节。加载器核心代码如下dataset.pyimport os import torch from torch.utils.data import Dataset from PIL import Image import json class FruitDataset(Dataset): def __init__(self, root_dir, transformNone, cache_filedataset/classes.json): self.root_dir root_dir self.transform transform self.classes sorted([d for d in os.listdir(root_dir) if os.path.isdir(os.path.join(root_dir, d))]) self.class_to_idx {cls: idx for idx, cls in enumerate(self.classes)} # 缓存 class_to_idx避免重复计算 os.makedirs(os.path.dirname(cache_file), exist_okTrue) with open(cache_file, w) as f: json.dump(self.class_to_idx, f) self.samples [] for cls_name in self.classes: cls_path os.path.join(root_dir, cls_name) for img_name in os.listdir(cls_path): if img_name.lower().endswith((.jpg, .jpeg, .png)): self.samples.append((os.path.join(cls_path, img_name), self.class_to_idx[cls_name])) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] try: img Image.open(img_path).convert(RGB) # 强制转 RGB兼容灰度图 except Exception as e: # 关键容错跳过损坏图片记录日志但不中断训练 print(fWarning: corrupted image {img_path}, skipped. Error: {e}) # 返回一个占位图保持 batch size 不变 img Image.new(RGB, (224, 224), colorgray) if self.transform: img self.transform(img) return img, label这段代码的实战价值在于三点img_name.lower().endswith(...)解决.JPG和.jpg混存问题Image.open(...).convert(RGB)统一通道数避免torch.nn.functional.interpolate对单通道图报错try-except中返回灰色占位图而非raise保证 DataLoader 不因单张坏图崩溃——这在你临时加图测试时救过多少次命自己心里清楚。2.2 模型选型ResNet18 不是“凑合”而是精度-速度-可解释性的黄金平衡点为什么不用 ViT 或 EfficientNet不是技术落后而是毕业设计场景下的务实选择显存友好ResNet18 在 batch_size32、输入 224×224 下RTX 3060 显存占用约 3.2GB远低于 ResNet50 的 5.8GB避免学生笔记本显存不足反复调参特征可追溯model.layer2[0].conv1.weight的梯度热力图能清晰对应到水果轮廓如香蕉弧线、苹果高光区答辩时导师问“模型学到什么”你能指着 Grad-CAM 图说清楚迁移学习友好项目默认加载torchvision.models.resnet18(weightsResNet18_Weights.IMAGENET1K_V1)仅替换最后全连接层nn.Linear(512, 12)冻结前 4 个 block 的权重微调仅需 15 分钟——比从头训快 8 倍且 top-1 准确率稳定在 92.3%±0.5%见results/val_acc_epoch_XX.txt。模型定义代码model.pyimport torch import torch.nn as nn from torchvision.models import resnet18, ResNet18_Weights def build_fruit_classifier(num_classes12, pretrainedTrue, freeze_backboneTrue): model resnet18(weightsResNet18_Weights.IMAGENET1K_V1 if pretrained else None) # 替换最后全连接层 model.fc nn.Sequential( nn.Dropout(0.3), # 防止过拟合尤其小数据集 nn.Linear(model.fc.in_features, 256), nn.ReLU(), nn.Dropout(0.2), nn.Linear(256, num_classes) ) if freeze_backbone: # 冻结前4个block只微调 fc 和 layer4 for param in model.parameters(): param.requires_grad False for param in model.layer4.parameters(): param.requires_grad True for param in model.fc.parameters(): param.requires_grad True return model关键参数说明pretrainedTrue加载 ImageNet 预训练权重收敛更快freeze_backboneTrue默认冻结大部分层适合小样本每类仅 300 图若你收集了 2000 张/类可设为False并调大学习率Dropout(0.3)放在 fc 前比只在最后加一层更有效——这是我在 3 个毕设项目中验证过的血泪经验。2.3 训练脚本train.py里的 5 个隐藏开关train.py表面是 80 行脚本实则埋了 5 个影响结果的关键开关全部通过命令行参数控制参数默认值作用推荐场景--lr0.001学习率微调时用 0.001全训用 0.01--batch-size32Batch 大小GTX 1060 用 16RTX 3060 用 32--epochs30总轮数首次运行设 20观察 loss 曲线再决定是否加--save-freq5每 N 轮保存一次模型答辩前务必设为 1留多个 checkpoint 防翻车--augmentTrue是否启用增强必开否则验证集准确率波动超 ±3%执行示例python train.py --lr 0.001 --batch-size 32 --epochs 25 --save-freq 1 --augment True增强策略在transforms.py中定义包含RandomHorizontalFlip(p0.5)解决水果摆放方向随机问题ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.1)模拟不同光照条件RandomRotation(degrees15)应对拍摄角度倾斜RandomAffine(degrees0, translate(0.1, 0.1), scale(0.9, 1.1))模拟轻微缩放和平移。提示ColorJitter的hue0.1是关键——水果色差大青苹果 vs 红苹果饱和度和色相扰动比单纯亮度调整更能提升泛化性。3. 模型推理与 Web 交互让答辩老师亲手上传一张图3 秒看到结果3.1 单图推理inference.py的双后端设计很多项目只提供predict.py但实际答辩时老师可能用手机拍张图发给你你得现场跑。inference.py为此做了两套后端OpenCV 后端cv2.imread()→cv2.cvtColor()→cv2.resize()速度快兼容性好适合 Windows 无 GPU 环境PIL 后端Image.open()→convert(RGB)→resize()色彩保真度高适合 Mac/Linux代码自动 fallbackdef load_image_cv2(image_path): try: img cv2.imread(image_path) if img is None: raise ValueError(cv2.imread returned None) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR→RGB return img except: return None def load_image_pil(image_path): try: img Image.open(image_path).convert(RGB) return np.array(img) except: return None def load_image_safe(image_path): img load_image_cv2(image_path) if img is None: img load_image_pil(image_path) if img is None: raise RuntimeError(fFailed to load image: {image_path}) return img推理主函数predict_single_image()支持三种输入本地路径predict_single_image(test/banana.jpg)URLpredict_single_image(https://example.com/fruit.jpg)自动下载缓存numpy arraypredict_single_image(cv2.imread(...))供 Web API 调用输出格式统一为字典{ class: banana, confidence: 0.962, top3: [(banana, 0.962), (plantain, 0.021), (mango, 0.008)], inference_time_ms: 42.3 }3.2 Web 前端为什么用http.server而不是 FlaskFlask 需要pip install flask还要配app.run(host0.0.0.0)学生环境常因端口冲突或防火墙失败。本项目用 Python 内置http.server 静态文件直出零依赖cd web/ python -m http.server 8000访问http://localhost:8000即可打开界面。前端核心逻辑在web/main.jsdocument.getElementById(upload-btn).addEventListener(click, function() { const fileInput document.getElementById(file-input); const formData new FormData(); formData.append(image, fileInput.files[0]); fetch(/predict, { method: POST, body: formData }) .then(response response.json()) .then(data { document.getElementById(result).innerHTML h3识别结果${data.class}/h3 p置信度${(data.confidence * 100).toFixed(1)}%/p pTop3br${data.top3.map(([c, p]) ${c} (${(p*100).toFixed(1)}%)).join(br)}/p p耗时${data.inference_time_ms.toFixed(1)} ms/p ; }) .catch(err { document.getElementById(result).innerHTML 错误${err.message}; }); });后端web/server.py极简from http.server import HTTPServer, BaseHTTPRequestHandler from inference import predict_single_image import json import tempfile import os class FruitHandler(BaseHTTPRequestHandler): def do_POST(self): if self.path /predict: # 解析 multipart/form-data content_length int(self.headers.get(Content-Length, 0)) post_data self.rfile.read(content_length) # 提取文件简化版生产环境请用 werkzeug boundary self.headers[Content-Type].split(boundary)[1].strip() parts post_data.split(b-- boundary.encode()) for part in parts: if bfilename in part: # 提取文件名和内容 filename part.split(bfilename)[1].split(b)[0].decode() file_content part.split(b\r\n\r\n)[1].split(b\r\n--)[0] # 临时保存并预测 tmp_path os.path.join(tempfile.gettempdir(), filename) with open(tmp_path, wb) as f: f.write(file_content) result predict_single_image(tmp_path) os.remove(tmp_path) # 立即清理 self.send_response(200) self.send_header(Content-type, application/json) self.end_headers() self.wfile.write(json.dumps(result).encode()) return注意此 server 仅用于本地演示不可用于公网部署。答辩时关掉 WiFi 热点纯局域网运行即可。3.3 模型导出与 ONNX 兼容为后续部署留后路项目提供export_onnx.py将训练好的.pth模型转 ONNX 格式便于后续部署到 Jetson 或 OpenVINOimport torch import torch.onnx from model import build_fruit_classifier model build_fruit_classifier(num_classes12) model.load_state_dict(torch.load(checkpoints/best_model.pth)) model.eval() dummy_input torch.randn(1, 3, 224, 224) # 注意尺寸必须与训练一致 torch.onnx.export( model, dummy_input, fruit_classifier.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 )导出后可用 Netron 查看结构确认Gemm全连接和Conv层顺序正确。ONNX 模型体积约 45MB比原始.pth~52MB略小且跨平台兼容性更好。4. 避坑指南答辩前夜最常踩的 4 个坑以及我怎么把它变成加分项4.1 现象train.py运行到第 3 轮突然卡住GPU 显存占用 100%但nvidia-smi显示 no running process原因DataLoader的num_workers 0在 Windows 上与某些杀毒软件尤其是 360、腾讯电脑管家冲突导致 worker 进程僵死。解决在train.py中强制设num_workers0或升级到 PyTorch 2.0已修复。临时方案# train.py 第 42 行附近 train_loader DataLoader( train_dataset, batch_sizeargs.batch_size, shuffleTrue, num_workers0, # 关键Windows 下必须为 0 pin_memoryTrue )4.2 现象Web 页面上传图片后返回{error: KeyError: image}原因前端FormData附加字段名是image但后端解析时误以为是request.files[file]Flask 风格。解决检查web/server.py中的filename提取逻辑。正确写法是匹配Content-Disposition: form-data; nameimage; filenamexxx.jpg而非硬编码namefile。项目已修正但如果你合并了其他代码请核对boundary解析部分。4.3 现象predict_single_image(test/apple.jpg)返回classunknownconfidence0.001原因模型加载时未指定map_location导致 CPU 环境下加载 GPU 训练的模型失败state_dict键名不匹配。解决在inference.py的load_model()函数中加入device torch.device(cuda if torch.cuda.is_available() else cpu) model.load_state_dict(torch.load(model_path, map_locationdevice))4.4 现象答辩时老师用 iPad 拍照上传Web 界面显示模糊图识别结果错误原因iPad 拍照默认开启 HEIC 格式Image.open()无法直接读取。解决在web/server.py的文件保存前增加格式转换# 检测是否为 HEIC if filename.lower().endswith(.heic): from PIL import Image import pillow_heif heif_file pillow_heif.read_heif(file_content) img Image.frombytes( heif_file.mode, heif_file.size, heif_file.data, raw, heif_file.mode, heif_file.stride ) # 转为 JPEG 再保存 jpeg_bytes io.BytesIO() img.save(jpeg_bytes, formatJPEG) file_content jpeg_bytes.getvalue() filename filename.replace(.heic, .jpg)需pip install pillow-heif5. 毕设进阶技巧如何在 1 小时内扩展成「水果新鲜度分级」系统5.1 从分类到回归修改损失函数与输出头原项目是 12 分类若要识别「苹果新鲜度1-5 级」需将任务转为回归。改动集中在三处数据集标签将train/apple/下的图片按新鲜度打分1腐烂5新鲜重命名apple_3_001.jpg3 表示新鲜度并在FruitDataset.__getitem__()中解析数字# dataset.py 中 __getitem__ 修改 label_str img_name.split(_)[1] # apple_3_001.jpg → 3 label float(label_str) # 转为 float return img, label # 返回标量非整数模型输出头model.fc最后一层改为单输出# model.py model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(model.fc.in_features, 128), nn.ReLU(), nn.Dropout(0.2), nn.Linear(128, 1) # 输出 1 个数值 )损失函数train.py中替换nn.CrossEntropyLoss()为nn.MSELoss()并关闭torch.nn.functional.softmaxcriterion nn.MSELoss() # 预测时直接取 output.item() pred_score output.item() # 不再 softmax5.2 评估指标用 Spearman 相关系数替代 Accuracy分类任务用 Accuracy回归任务必须用相关性指标。在validate.py中添加from scipy.stats import spearmanr def evaluate_regression(model, data_loader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in data_loader: images, labels images.to(device), labels.to(device) outputs model(images).squeeze() all_preds.extend(outputs.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # Spearman 相关系数衡量排序一致性 rho, p_val spearmanr(all_preds, all_labels) mse ((np.array(all_preds) - np.array(all_labels)) ** 2).mean() return {spearman_rho: rho, mse: mse, p_value: p_val}Spearman ρ 0.85 即说明模型能正确排序新鲜度比 MSE 更反映业务价值。5.3 可视化加分项Grad-CAM 热力图叠加原图答辩时展示「模型关注哪里」比只说准确率更有说服力。gradcam.py提供一键生成from gradcam import GradCAM from model import build_fruit_classifier model build_fruit_classifier(12) model.load_state_dict(torch.load(checkpoints/best_model.pth)) target_layer model.layer4[-1] # ResNet18 最后一个残差块 cam GradCAM(modelmodel, target_layertarget_layer) img Image.open(test/apple.jpg).convert(RGB) input_tensor transform(img).unsqueeze(0) # transform 来自 train.py # 生成热力图针对预测类别 cam_output cam(input_tensor, target_categoryNone) # None 表示用模型预测的类别 save_heatmap(cam_output, apple_cam.jpg, test/apple.jpg)生成的apple_cam.jpg会显示红色高亮区域模型认为的判别依据如苹果果梗、表皮斑点——这能直观证明模型没瞎猜。从那以后我每次帮学生改毕设都强制走一遍python inference.py --demopython web/server.pypython gradcam.py三连测确保答辩当天不因环境差异掉链子。这套流程跑下来98 分不是运气是每个环节都经得起推敲。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站