简介:一套基于机器学习实现的语音+图像双模态水果分拣系统源代码与数据集,聚焦香蕉、苹果、桃子三类水果的自动识别与分拣,适合人工智能学习者、算法工程师以及农业、物流、零售等需要自动化分拣的场景。系统从数据采集与预处理起步,包含图像裁剪归一化、语音去噪分割,并采用CNN进行图像识别、RNN/LSTM完成语音指令解析,覆盖模型训练、优化、系统集成与用户界面展示的完整工程链路。压缩包共502个文件,其中455个jpg图像样本和10个wav语音段用于训练验证,10个py源码实现核心算法与接口逻辑,另有模型权重、前端页面、接口文档及配置文件,总量约62.82MB。该资源已有122人学习,借助自带数据集与预训练模型可快速复现分拣流程,也能通过接口文档将识别功能嵌入其他系统,是毕业设计、课程项目或轻量级分拣原型的实用参考。
1. 语音与图像双模态水果分拣:这个 Python 项目到底拆了什么
拿到这套源码时,我先翻了目录,发现它并不是一个「纯图像分类」的玩具项目——目录里同时躺着 checkpoint 模型文件、一整套 HTML/CSS 前端配置页,以及一份图片识别接口文档。换句话说,这更像一个「能跑通全链路」的工程骨架:摄像头拍一张香蕉,模型输出类别;操作员说一句「苹果」,语音链路参与复核或切换指令。语音分拣和图像分拣在这里是两条并行又耦合的数据流,而不是通常意义上「听声音识别水果」——物理上靠撞击声分辨香蕉和桃子并不现实,项目里语音的真正角色是操作指令输入和低置信度兜底。这套代码适合三类人:做课程设计/毕设需要完整 Demo 的学生、想在产线上快速验证视觉分拣的工程师、以及想拆一份带前端配置页的机器学习源码来参考接口设计的人。
2. 从 checkpoint 到完整系统:图像与语音两条数据链路怎么搭
2.1 项目文件结构与运行路径:config.html、CSS 与 checkpoint 的关系
把源码包展开后,文件可以分成四组:模型文件(checkpoint、model.ckpt.data-00000-of-00001)、前端页面(config.html、config.css、index.css、test.css、common.css)、接口文档(.docx)、版本管理文件(.gitignore)。存放模型的文件是 TensorFlow 旧版 checkpoint 格式,model.ckpt.data-00000-of-00001存储权重,checkpoint文本文件则记录最新模型路径。项目里没有直接给出训练脚本,checkpoint是训练产物,运行时靠它恢复计算图。
# 典型启动顺序:先看 checkpoint 文件内容 cat checkpoint # 输出示例:model_checkpoint_path: "model.ckpt" # 再确认 Python 环境里有没有对应框架 python -c "import tensorflow as tf; print(tf.__version__)"提示:
model.ckpt.data-00000-of-00001里的00000-of-00001表示单个分片文件。如果模型超过单文件上限(默认 2GB),会出现多个分片,恢复时 TensorFlow 会根据checkpoint自动拼接。
2.2 图像数据预处理:归一化、尺寸对齐与 label 映射
图像进入模型前必须做与训练时一致的预处理,否则识别率会断崖式下跌。香蕉、苹果、桃子三分类,训练时若用 ImageNet 风格(RGB 均值归一化)还是一般 Min-Max 归一化,直接影响推理代码怎么写。常见做法是:读取图片文件 → 解码为 RGB → 缩放到模型输入尺寸(如 224×224)→ 归一化到 [0,1] 或减去均值 → 转成 batch 维度。
import cv2 import numpy as np def preprocess_image(image_path, input_size=(224, 224)): # 读取图片,OpenCV 默认通道顺序是 BGR img = cv2.imread(image_path) if img is None: raise ValueError(f"无法读取图片: {image_path}") # 转成 RGB,避免后续特征提取时通道语义错位 img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 缩放时用 INTER_LINEAR,训练时若用其他插值法推理要一致 img = cv2.resize(img, input_size, interpolation=cv2.INTER_LINEAR) # 归一化到 [0,1],模型最后一层通常是 Softmax,输出才是概率 img = img.astype(np.float32) / 255.0 # 增加 batch 维度,shape: (1, 224, 224, 3) return np.expand_dims(img, axis=0)这段代码里最容易被忽略的是cv2.cvtColor。如果训练管线用 PIL 读图,推理用 OpenCV,BGR 和 RGB 的差异会让同一张图输出概率完全不同;归一化系数 255.0 对应的是 0-255 的整型图像。如果训练时用的是 TensorFlow 的tf.keras.preprocessing.image,默认归一化也是 /255,这组参数可以对齐。label 映射建议硬编码为有序字典,避免类别和索引错位:
{"banana": 0, "apple": 1, "peach": 2}2.3 语音指令链路:转写与意图解析
语音部分在项目里通过麦克风采集操作员指令,经过语音转文本(ASR)后解析出水果类别关键词。采集端常见做法是把音频切成 1 秒左右的帧做端点检测(VAD),避免静音段挤占识别资源。
import speech_recognition as sr def transcribe_audio(timeout=3): recognizer = sr.Recognizer() with sr.Microphone() as source: # 环境噪声校准,能显著降低误识别率 recognizer.adjust_for_ambient_noise(source, duration=0.5) print("请说出水果名称:香蕉 / 苹果 / 桃子") try: audio = recognizer.listen(source, timeout=timeout, phrase_time_limit=2) except sr.WaitTimeoutError: return None # 简体中文需要指定语言,否则默认英文识别结果不可用 try: text = recognizer.recognize_google(audio, language="zh-CN") return text except sr.UnknownValueError: return None参数里phrase_time_limit=2是关键:它限制单次语音输入最多 2 秒,防止操作员说完话后不停止,把环境杂音也录进去,导致 ASR 返回无意义字符串。adjust_for_ambient_noise会采集前 0.5 秒作为底噪基线,在工厂产线这类固定噪声场景下能明显提高识别率。识别出的文本接着做关键词匹配,映射到banana/apple/peach三选一;如果文本里同时出现两个水果名,取置信度更高的视觉模型结果为准。
3. CNN 分类与语音复核:训练、推理与接口封装
3.1 模型结构与 checkpoint 加载
图像侧的模型是典型的卷积神经网络分类器:若干 Conv+ReLU+MaxPool 块提取纹理特征,展平后接全连接层,最后的 Softmax 输出 3 个类别的概率。语音侧更轻量——本质上不训练「水果声学模型」,而是把 ASR 文本当成一个短序列来做意图分类,所以用 LSTM 或简单词袋模型就够。加载 checkpoint 时最容易踩的坑是 TensorFlow 1.x 与 2.x 的兼容问题:
import tensorflow.compat.v1 as tf tf.disable_v2_behavior() # 从 checkpoint 恢复计算图和权重 sess = tf.Session() saver = tf.train.import_meta_graph("model.ckpt.meta") saver.restore(sess, "model.ckpt") graph = tf.get_default_graph() # 从图里取出输入输出节点,注意节点名要和训练时保存的一致 input_tensor = graph.get_tensor_by_name("input_1:0") output_tensor = graph.get_tensor_by_name("dense_2/Softmax:0")import_meta_graph需要.meta文件,如果源码包里只有checkpoint和.data-00000-of-00001,说明图结构在代码里重新构建后再恢复权重。我一般建议改用tf.keras.models.load_model加载整个目录,省去节点名匹配的麻烦,但这要求训练时保存的是完整 SavedModel 格式;对当前这种裸 checkpoint,最稳妥的是重新定义模型结构,把权重导入同名变量。
3.2 用 Flask 封装图片识别接口
项目配套的接口文档描述的就是典型的「图片识别接口」。生产里不会直接把模型推理代码暴露给浏览器,而是封装成 HTTP 服务。用 Flask 起一个最小服务,接收 multipart 图片,返回 JSON.
from flask import Flask, request, jsonify import numpy as np app = Flask(__name__) # 伪代码:load_model() 内部完成 checkpoint 恢复或 keras 模型加载 model = load_model("model.ckpt") LABELS = ["banana", "apple", "peach"] @app.route("/recognize", methods=["POST"]) def recognize(): # 前端传文件字段名为 image;若为空则返回 400 方便前端做错误态 f = request.files.get("image") if f is None: return jsonify({"error": "缺少 image 字段"}), 400 # 从文件流直接解码,避免中间落盘产生权限和临时文件问题 img = preprocess_image_from_bytes(f.read()) preds = model.predict(img)[0] # shape: (3,) idx = int(np.argmax(preds)) confidence = float(preds[idx]) # 低于阈值返回 uncertain,由语音链路二次判断 if confidence < 0.6: return jsonify({"label": "uncertain", "confidence": confidence}) return jsonify({"label": LABELS[idx], "confidence": confidence}) if __name__ == "__main__": # 局域网调试不要开 reloader,会重复加载模型吃内存 app.run(host="0.0.0.0", port=5000, debug=False)confidence < 0.6这个阈值写成可配置项更合理,因为产线光照变化会导致整体置信度偏移。接口返回uncertain而不是硬给一个类别,给上层系统留出了人工干预或语音复核的入口,这是工程化做法——纯分类 Demo 通常不这么设计。debug=False也是必须项,Flask 的 reloader 会创建两个进程加载两次模型,小机器上直接内存溢出。
3.3 语音文本如何参与分拣决策
双模态融合在项目里是「视觉为主、语音为辅」的投票策略。具体规则:视觉模型输出最大概率超过阈值(如 0.85)时直接分拣;低于阈值但高于 0.6 时,把语音识别出的候选词拿出来做加权投票;低于 0.6 则转人工确认。
def decide(image_pred, voice_text, voice_weight=0.4): label, conf = image_pred["label"], image_pred["confidence"] # 高置信度场景不等待语音,保证产线节拍 if conf >= 0.85: return label # 低置信度时语音成为第二证据源 voice_label = extract_fruit_keyword(voice_text) if voice_label is None: return label if conf >= 0.6 else "reject" # 视觉置信度与语音权重做线性融合,简单且可解释 scores = {"banana": 0.0, "apple": 0.0, "peach": 0.0} scores[label] += conf * (1 - voice_weight) scores[voice_label] += voice_weight return max(scores, key=scores.get)voice_weight=0.4表示语音在争议场景下拥有 40% 的决策权重,这个值一般在步测中调:操作员发音准确率高就调高,产线噪声大就调低。融合决策的收益在于绕过纯 CNN 在遮挡、反光场景下的「强分类」缺陷,让低质图像帧也能被分拣而不是被机械剔除。
4. config.html 与 CSS 配置台:本地联调和实时反馈
4.1 config 页与 CSS 文件的职责划分
前端目录里的四个 CSS 文件不是随意的:common.css是全局基础样式;index.css对应主识别页面;test.css大概率服务于调试页(单张图片上传测试);config.css则匹配config.html配置页。这种按页面拆 CSS 的方式,在小型工具类项目中比组件库更轻量,改一个页面样式不会影响其他页面。config.html是本地控制台入口,通常包含以下配置项:
| 配置项 | 作用 | 建议值 |
|---|---|---|
| 模型路径 | checkpoint 所在目录 | ./checkpoint |
| 置信度阈值 | 视觉分类的最低置信度 | 0.60 |
| 语音权重 | 低置信度时语音的投票权重 | 0.40 |
| 摄像头地址 | 视频流或 RTSP 地址 | /dev/video0 |
| 接口地址 | 后端识别服务地址 | http://127.0.0.1:5000 |
config.html作为纯静态页面,修改配置后通过localStorage或后端保存,刷新主页面即可生效。这里的「前后端语音控制事件流程图」逻辑也很直白:麦克风采集 → ASR 转文本 → 关键词提取 → 触发分拣结果播报或参数调整,整个链路不依赖第三方调度框架。
4.2 配置下发与识别请求的交互时序
前端页面不直接接触模型,所有推理请求都走后端接口。一次完整的交互是:主页面启动时拉取配置 → 摄像头采集帧 → 定时或按键触发识别 → 渲染结果。
// 主识别页的核心请求逻辑 async function sendFrame(imageBlob) { const form = new FormData(); form.append("image", imageBlob, "frame.jpg"); // AbortController 用来做超时控制,避免摄像头断流时请求挂死 const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); try { const resp = await fetch("/recognize", { method: "POST", body: form, signal: controller.signal, }); const data = await resp.json(); // 渲染类别与置信度,uncertain 时调用语音复核 renderResult(data); } catch (err) { if (err.name === "AbortError") { showToast("识别超时,请检查后端服务"); } } finally { clearTimeout(timer); } }AbortController加 5 秒超时在实时图像场景下特别重要:摄像头取流偶尔会丢帧,后端若同时处理多个请求排队,前端会越积越多,最终浏览器内存被 Blob 占满。轮流检测页的建议间隔是 200ms 一帧,既能感知分拣结果变化,又不至于压垮 Flask 的单线程服务。
4.3 实时分拣结果的语音反馈
识别结果除了写进页面 DOM,还可以通过语音播报读出来。项目里有语音控制的能力,反过来做语音反馈是顺手的事——用 Web Speech API 的speechSynthesis在识别完成时读出类别。
function speakResult(label, confidence) { const texts = { banana: "香蕉", apple: "苹果", peach: "桃子" }; const msg = new SpeechSynthesisUtterance(); msg.lang = "zh-CN"; msg.text = `识别结果为${texts[label]},置信度${(confidence * 100).toFixed(1)}%`; // 打断上一句播报,防止连续识别时播报声音叠在一起 speechSynthesis.cancel(); speechSynthesis.speak(msg); }speechSynthesis.cancel()的细节值得注意:不打断的情况下,连续帧识别会产生数十条播报队列,后面全在复读旧结果。配合一个「仅当类别变化时才播报」的条件,产线工位上的声音提示才不会变成噪声。这个模块独立于后端,当地理位置在浏览器端,离线也能跑。
5. 三分类扩展到多分类:改网络层、校验 checkpoint 与验证闭环
5.1 换数据集时检查哪些文件
把香蕉、苹果、桃子扩展到橘子、梨之前,先要做一次文件体检。三个文件必须改:数据集的目录结构(按类建子目录)、label 映射表、模型最后一层的神经元数。源码包里的.gitignore排除了*.ckpt*,说明模型文件是训练后单独拷贝的,改分类数意味着要重新训练并手动覆盖 checkpoint——不能只改前端 label。
| 改动点 | 操作 | 示例 |
|---|---|---|
| 数据集目录 | 新增类别的图片子目录 | dataset/orange/ |
| label 映射 | 扩展 JSON 数组 | ["banana","apple","peach","orange"] |
| 网络输出层 | 全连接层神经元数 3 → 4 | Dense(4, activation="softmax") |
| 配置阈值 | 新类别易混淆时可调高 | confidence_threshold: 0.65 |
5.2 上线前的验证命令与常见报错
改完后先做接口级验证,不要直接上摄像头。用 curl 模拟前端上传,确认返回结果符合预期:
# 上传本地图片到识别接口,观察 JSON 输出 curl -X POST -F "image=@test_apple.jpg" http://127.0.0.1:5000/recognize # 期望输出:{"label":"apple","confidence":0.923}这一步能快速暴露三类高频问题:Checkpoint was not found多半是模型路径写错或checkpoint文件里的相对路径失效,把model_checkpoint_path改成绝对路径即可;Shape mismatch说明修复后的网络层与 checkpoint 权重不一致,最常见的就是最后一层神经元的数量没改;CUDA out of memory出现在推理阶段时,可以直接把config.gpu_options.allow_growth设为 True,别一开口就上大数据集重训。加载 checkpoint 后可以用一个确定性的测试样本打印预测值和argmax,如果输出 NaN,先检查model.ckpt.data-00000-of-00001的 md5 是否在传输中被截断——大文件分片下载损坏是 source code 包最容易出现的问题。
本文还有配套的精品资源,点击获取