深度学习图像分类实战:基于TensorFlow与Flask的红枣识别系统部署

深度学习图像分类实战:基于TensorFlow与Flask的红枣识别系统部署 简介基于深度学习的红枣识别算法设计完整源码包面向高校毕业设计与课程设计使用TensorFlow实现新疆红枣与灰枣分类识别。系统划分为图像数据处理、模型构建与可视化界面三大模块完整覆盖原始图片采集、数据集划分、图像增强、卷积神经网络搭建、超参数调优以及基于Flask的交互页面。全套资源共905个文件核心代码为18个Python源文件同时包含MySQL建库SQL脚本、环境依赖whl、PyCharm工程配置、答辩PPT与说明文档图像素材含305个PNG和82个JPG前端部分提供Bootstrap、LayUI等CSS/JS组件压缩包约428.92MB已有54人学习下载。借助完整源码可掌握TensorFlow数据流水线、CNN训练评估与模型保存加载的完整流程并能在浏览器中实时上传图片完成类别识别。项目目录结构清晰数据库脚本和运行说明齐全适合直接作为毕业设计参考或课程设计升级练手对理解图像分类算法与Web端部署也有明显帮助。1. 红枣识别项目从数据集到Flask部署的完整链路这套基于深度学习的红枣识别系统解决的是新疆红枣与灰枣两类干果的自动分类问题。项目采用Python 3.6.8 TensorFlow构建卷积神经网络模型训练完成后通过Flask框架提供Web可视化交互界面。相比网上零散的图像分类教程这组源码把数据预处理、模型训练、参数对比、Web部署串成了一条完整链路适合作为毕业设计或课程设计的基线工程。选Flask而不是Django是因为Flask轻量、路由配置直观模型推理服务不需要重型ORM支撑选TensorFlow而不是PyTorch是因为TF 1.x/2.x在模型导出和Keras高层API上更省事Python 3.6环境下兼容性也更好。你拿到源码后不需要改动模型主干只要替换自己的图像数据集就能迁移到其他农产品分类场景。下面从最耗时的数据处理部分拆起。2. 图像数据预处理与数据集划分TensorFlow输入管道的搭建2.1 数据集采集与类别平衡处理源码中红枣图像采集自公开数据集和实地拍摄包含两类新疆红枣大枣和灰枣小枣。采集阶段最容易犯的错误是类别不均衡——灰枣图像数量可能是新疆红枣的两倍直接训练会让模型偏向多数类。处理办法有两个一是采样平衡对少数类进行过采样复制或对多数类进行欠采样随机剔除二是在损失函数中设置类别权重让少数类的误判惩罚更大。实际工程中我一般先用脚本统计每类图像数量输出分布情况。源码里的data_preprocess.py已经实现了这个功能核心逻辑如下import os from collections import Counter def count_images(data_dir): 统计每个子目录下的图片数量返回类别分布字典 categories {} for root, dirs, files in os.walk(data_dir): # 只处理包含图片文件的目录忽略根目录 if len(files) 0 and any(f.endswith((.jpg, .png)) for f in files): # 目录名即类别名如xingjiang_hongzao / huizao category os.path.basename(root) img_count sum(1 for f in files if f.endswith((.jpg, .png))) categories[category] img_count return categories distribution count_images(data/raw) print(distribution)这段代码遍历data/raw下的子目录每个子目录名当作类别名统计.jpg和.png文件数量。参数data_dir换成你的实际路径即可。如果发现分布差异超过1:1.5就应当做平衡处理如果差异不大直接进入下一步。2.2 图像增强与归一化参数设置原始图像分辨率不等需要统一尺寸。源码使用cv2.resize将图像缩放为224x224和VGG16、ResNet50的输入尺寸一致。但直接resize会带来两个问题第一长宽比失真导致几何特征变形第二光照、角度差异导致模型泛化差。因此源码里做了两件事先按比例resize再中心裁剪同时使用随机翻转、亮度调整做数据增强。下面是源码中增强部分的精简版本import cv2 import numpy as np import tensorflow as tf def preprocess_image(img_path, target_size(224, 224)): 读取图像进行缩放、裁剪和归一化 # 读取BGR图像 img cv2.imread(img_path) h, w, _ img.shape # 短边缩放到target_size保持长宽比 scale target_size[0] / min(h, w) new_w, new_h int(w * scale), int(h * scale) img cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) # 中心裁剪到target_size start_x (new_w - target_size[1]) // 2 start_y (new_h - target_size[0]) // 2 img img[start_y:start_ytarget_size[0], start_x:start_xtarget_size[1]] # BGR转RGB归一化到[0,1] img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) / 255.0 return img def augment_image(img): 随机水平翻转和亮度调整返回增强后的图像 if np.random.rand() 0.5: img img[:, ::-1] # 水平翻转 brightness_factor 0.8 0.4 * np.random.random() # 0.8~1.2 img np.clip(img * brightness_factor, 0.0, 1.0) return img参数说明target_size设为(224,224)匹配预训练模型要求INTER_LINEAR是双线性插值比最近邻插值效果好归一化除以255是为了匹配TensorFlow图像输入习惯。brightness_factor控制在0.8到1.2之间避免亮度过激破坏红枣表面纹理。数据增强只在训练阶段使用验证和测试阶段不做否则会干扰真实准确率评估。2.3 数据集划分与TensorFlow数据管道源码按6:2:2的比例划分训练集、验证集、测试集。划分时要保证同一批次图像的随机性同时让每类图像在各集合中的比例一致否则验证集可能只包含某一类。这里使用train_test_split的分层参数import random from sklearn.model_selection import train_test_split def split_dataset(file_paths, labels, val_ratio0.2, test_ratio0.2, seed42): 分层划分数据集返回训练、验证、测试的文件路径和标签 train_paths, temp_paths, train_labels, temp_labels train_test_split( file_paths, labels, test_sizeval_ratio test_ratio, stratifylabels, random_stateseed ) # 从临时集合中再按比例切出验证集和测试集 val_ratio_adjusted val_ratio / (val_ratio test_ratio) val_paths, test_paths, val_labels, test_labels train_test_split( temp_paths, temp_labels, test_size1 - val_ratio_adjusted, stratifytemp_labels, random_stateseed 1 ) return (train_paths, train_labels), (val_paths, val_labels), (test_paths, test_labels)stratify参数确保划分后各类别占比与原始数据集一致random_state固定随机种子让实验可复现。源码中后续用TensorFlow的Dataset.from_tensor_slices构造输入管道设置batch_size32shuffle缓冲区大小设为len(train_paths)这样每个epoch都能打乱数据顺序。注意验证集和测试集不需要shuffle和repeat。3. CNN模型构建与参数优化从LeNet到可部署的识别模型3.1 卷积神经网络结构选型红枣识别是典型的细粒度图像分类任务新疆红枣和灰枣在颜色、纹理上差异并不显著——前者偏大偏长表皮褶皱较浅后者较小较圆褶皱密集。传统特征提取颜色直方图、纹理LBP很难区分因此源码选用深度学习卷积网络。项目最初尝试了类似LeNet的浅层结构但准确率只有82%左右。后来改用带Batch Normalization和Dropout的自定义CNN验证集准确率提升到94%。网络结构设计遵循三个原则浅层卷积核数量少、深层卷积核数量多每两个卷积层后接一个最大池化全连接层之前用全局平均池化减少参数量。3.2 模型构建代码与关键参数说明源码使用TensorFlow的Keras API构建模型核心代码如下import tensorflow as tf from tensorflow.keras import layers, models def build_cnn_model(input_shape(224, 224, 3), num_classes2): 构建自定义卷积神经网络分类模型 model models.Sequential([ # 第一卷积块提取边缘和颜色特征 layers.Conv2D(32, (3, 3), activationrelu, paddingsame, input_shapeinput_shape), layers.BatchNormalization(), layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), # 第二卷积块提取局部纹理特征 layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), # 第三卷积块提取整体形状特征 layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.BatchNormalization(), layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), # 全局平均池化替代Flatten减少参数量 layers.GlobalAveragePooling2D(), layers.Dropout(0.5), layers.Dense(128, activationrelu), layers.Dropout(0.3), layers.Dense(num_classes, activationsoftmax) ]) return model model build_cnn_model() model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) model.summary()参数说明Conv2D的paddingsame保持特征图尺寸不变避免边缘信息过快丢失BatchNormalization放在激活函数之前加速收敛并减少内部协变量偏移GlobalAveragePooling2D强制特征图与类别建立对应关系防止全连接层过拟合Dropout比例设置为0.5和0.3训练时随机丢弃神经元测试时自动缩放权重。优化器选择adam而不是SGD是因为Adam自适应学习率在超参数不敏感的情况下更容易收敛。损失函数用categorical_crossentropy要求标签为one-hot编码如果标签是整数则改用sparse_categorical_crossentropy。源码里两种都封装了通过参数切换。3.3 参数对比实验与调优路径源码的train.py中内置了几组对比实验用来选择最优参数组合。下表是作者跑出的典型结果卷积核数量学习率是否使用BN验证集准确率训练时间epoch5032-64-1280.001是94.6%约18分钟32-64-1280.01是89.1%约17分钟16-32-640.001否86.3%约12分钟64-128-2560.001是93.8%约35分钟从表中可以看到学习率从0.001升到0.01准确率下降5个百分点说明学习率过大导致损失震荡去掉BN后准确率下降8个百分点证明BN在浅层网络中作用明显。增加卷积核数量并不能带来持续提升反而因参数量增大导致过拟合风险。调参时我会先固定batch_size32然后按“学习率 - 卷积核数量 - Dropout比例”的顺序搜索。train.py里使用ReduceLROnPlateau回调当验证集准确率连续3个epoch不提升时学习率自动乘以0.1这比手动调低更平滑。如果发现训练准确率远高于验证准确率优先增大Dropout比例或加入L2正则化如果两者都低检查数据预处理是否出错比如图像标签错位。4. Flask可视化界面设计与模型加载4.1 Flask应用结构设计训练完成后的模型保存为.h5文件Flask负责将其加载到内存对外提供HTTP接口。源码的app.py采用单文件Flask应用模板目录templates存放HTML页面静态目录static存放CSS和JS。需要明确一点Flask只做推理服务不重新训练模型所以CPU环境也能运行。应用包含三个核心路由GET /返回上传页面POST /predict处理上传的图像并返回分类结果GET /history展示预测历史记录。模型在应用启动时加载一次避免每次请求都重新读取磁盘文件。4.2 模型加载与预测接口实现模型加载和预测的代码是部署的关键源码如下import os import numpy as np import tensorflow as tf from flask import Flask, request, jsonify, render_template from werkzeug.utils import secure_filename from PIL import Image app Flask(__name__) model None # 类别索引到名称的映射顺序必须与训练时一致 CLASS_NAMES [新疆红枣, 灰枣] # 允许上传的图片格式 ALLOWED_EXTENSIONS {jpg, jpeg, png} def allowed_file(filename): 检查文件扩展名是否合法 return . in filename and filename.rsplit(., 1)[1].lower() in ALLOWED_EXTENSIONS def load_model(): 全局加载一次模型避免重复IO开销 global model model tf.keras.models.load_model(models/best_model.h5) def preprocess_for_inference(image_path): 对上传图片做与训练时相同的预处理 img Image.open(image_path).convert(RGB) img img.resize((224, 224), Image.LANCZOS) img_array np.array(img, dtypenp.float32) / 255.0 # 增加batch维度 img_array np.expand_dims(img_array, axis0) return img_array app.route(/) def index(): 渲染上传页面 return render_template(index.html) app.route(/predict, methods[POST]) def predict(): 处理上传图像返回分类结果和置信度 file request.files.get(file) if file is None or file.filename : return jsonify({error: 未选择文件}), 400 if not allowed_file(file.filename): return jsonify({error: 图片格式不支持}), 400 filename secure_filename(file.filename) save_path os.path.join(uploads, filename) file.save(save_path) img_array preprocess_for_inference(save_path) pred model.predict(img_array)[0] # 输出形如 [0.12, 0.88] class_index int(np.argmax(pred)) confidence float(pred[class_index]) result { class: CLASS_NAMES[class_index], confidence: round(confidence, 4) } return jsonify(result) # 启动时加载模型 load_model() if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)代码逻辑说明allowed_file用来限制扩展名防止上传可执行文件secure_filename清理文件名中的特殊字符避免路径穿越攻击preprocess_for_inference中使用Image.LANCZOS高质量缩放替代训练时的OpenCV resize效果更平滑。预测后np.argmax取出置信度最大的类别索引再映射到中文名称。参数说明model.predict返回的形状是(1, num_classes)取[0]得到二维数组的样本维度confidence保留四位小数前端展示时直接使用。host0.0.0.0允许局域网内其他设备访问如果你只想本机调试改为127.0.0.1。4.3 前端页面与AJAX交互源码的index.html使用Bootstrap和layui美化样式通过AJAX异步上传图片避免整页刷新。关键交互逻辑在static/js/main.js中$(#uploadBtn).on(click, function () { var formData new FormData(); var fileInput document.getElementById(imageInput); if (fileInput.files.length 0) { alert(请先选择图片); return; } formData.append(file, fileInput.files[0]); $.ajax({ url: /predict, type: POST, data: formData, processData: false, contentType: false, success: function (response) { if (response.error) { alert(response.error); return; } $(#resultClass).text(识别类别 response.class); $(#resultConfidence).text(置信度 response.confidence * 100 %); }, error: function (xhr) { alert(服务器错误 xhr.status); } }); });processData:false告诉jQuery不要转换FormData对象contentType:false让浏览器自动设置multipart/form-data边界。后端返回的JSON包含class和confidence两个字段前端直接渲染到指定DOM元素。注意生产环境下建议增加Token校验和文件大小限制Flask自带的开发服务器并发能力有限部署时用Gunicorn或uWSGI托管。5. 训练验证与部署排错损失曲线、过拟合与MySQL存储这一章把最容易让新手翻车的三个环节单独拿出来说。第一个是训练时如何判断模型是否收敛。源码里用TensorBoard记录训练过程启动命令是tensorboard --logdirlogs然后浏览器打开6006端口。看两条曲线训练损失和验证损失。正常情况两者同步下降最后平稳如果训练损失持续下降但验证损失先降后升就是过拟合。此时马上停止训练回退到验证损失最低的那个epoch对应的权重然后增大Dropout或减小学习率。第二个坑是模型保存与加载不匹配。训练时使用model.save(model.h5)保存完整模型包含网络结构和权重。但加载时如果代码里预处理的图像尺寸、归一化方式与训练不一致预测结果会完全错乱。我用一个简单方法验证找一张训练集里置信度最高的图保存后重新加载模型预测看结果是否与原训练日志一致。不一致就是预处理链路出差了重点检查cv2和PIL在缩放时的插值差异。第三个是关于MySQL存储预测记录。虽然Flask本身不需要数据库但毕业设计为了体现完整性源码用SQLAlchemy连接MySQL 5.7在predict接口里插入一条记录包含文件名、预测类别、置信度和时间戳。这里容易遇到两类问题一是MySQL驱动未安装报ModuleNotFoundError: No module named MySQLdb需要pip install PyMySQL并在__init__里执行pymysql.install_as_MySQLdb()二是数据库编码不一致导致中文乱码建库时指定utf8mb4字符集连接字符串加上charsetutf8mb4。最后给你一个可复用的排查思路当预测结果异常时先打印preprocess_for_inference得到的img_array的均值和标准差正常应该在0到1之间再看model.summary()最后一层的输出形状确认类别数与你的分类数一致。这套红枣识别项目经过以上步骤调通后替换数据和类别名就可以复用到苹果分级、橙子品质分类等场景。如果只是做演示可以把uploads目录改成临时目录每次请求后删除图片避免磁盘堆积。本文还有配套的精品资源点击获取