☰
中医智能舌诊系统毕设实战:Python+Vue全栈实现与避坑指南
2026/10/1 2:55:17 网站建设 项目流程

简介:这是一套面向计算机相关专业大四学生与项目实战学习者的中医智能舌诊系统毕业设计源码,由导师指导并获评审99分认可,适合作为毕设参考、课程设计或期末大作业。项目采用Python后端与Vue前端分离架构,涵盖舌象采集、智能分析与诊断结果展示等核心模块,代码完整可运行,零基础也能按文档逐步跑通。压缩包共85个文件,约2.51MB,其中24个vue组件与22个py脚本构成前后端主体,另有10个js、7个json配置、2个sql建表脚本及db数据库文件,配合md说明文档、png与jpg界面截图,目录结构清晰,便于按模块检索学习。目前已有219人学习下载。读者可获得完整可运行的工程源码、数据库脚本与文档说明,直接用于毕设答辩、课程设计或二次开发,省去从零搭建的时间成本。

1. 从一张舌头照片到一份诊断报告:中医智能舌诊系统到底在做什么

把手机镜头对准舌头拍一张照片,几秒钟后屏幕上弹出「舌质偏红、苔薄黄、裂纹明显,倾向阴虚火旺」——这就是中医智能舌诊系统想干的事。它本质上是把「望舌」这个依赖老中医经验的动作,拆成图像采集、颜色与纹理特征提取、证型分类、结果可视化四段流水线,再用 Python 做算法、Vue 做界面、数据库存病历。对做毕设的人来说,它的价值在于:选题有中医文化背书、技术栈覆盖前后端和机器学习、数据可自采、演示效果直观,答辩时一张对比图就能讲清楚。适合谁?适合已经会一点 Python、想找一个「有真实业务场景又不至于烂大街」的计算机或软件工程毕设方向的人。这篇笔记不讲空话,直接按我实际搭过一遍的顺序,把环境、数据、模型、前后端、数据库和踩过的坑讲透,让你能照着复现。

2. 技术选型与整体架构:为什么是 Python + Vue 这套组合

2.1 三段式架构的拆解逻辑

一个能跑通、能答辩的舌诊系统,我一般会拆成三层:算法层、服务层、展示层。算法层用 Python,因为 OpenCV、scikit-image、PyTorch 这些库在图像处理和深度学习上生态最全,舌象的颜色空间转换、区域分割、特征提取几乎都能找到现成实现。服务层用 Flask 或 FastAPI 把模型包成 HTTP 接口,前端不关心模型怎么跑,只发图片收 JSON。展示层用 Vue,因为它是组件化的,舌象上传、结果卡片、历史记录这些模块可以拆开写,配合 Element Plus 能快速出效果,答辩演示时界面不寒酸。

数据库选 MySQL 而不是 SQLite,原因是毕设通常要体现「数据库设计」这一章,MySQL 有完整的建表、索引、外键、连接池概念可以写,SQLite 虽然轻但答辩老师容易觉得「太简单」。如果你只是本地跑 demo,SQLite 也够,但既然标题里带了「数据库」,我建议直接上 MySQL,把用户表、舌象记录表、诊断结果表、证型字典表设计清楚。

2.2 环境搭建:Python 与 Vue 的最小可跑配置

先把 Python 环境弄干净。不要用系统自带的 Python,用 conda 或 venv 建独立环境,否则后面装 PyTorch 和 OpenCV 版本冲突会让你怀疑人生。

# 创建并激活虚拟环境(以 conda 为例) conda create -n tongue python=3.9 -y conda activate tongue # 安装核心依赖,版本是我实测能跑通的组合 pip install opencv-python==4.8.1.78 pip install scikit-image==0.22.0 pip install torch==2.1.0 torchvision==0.16.0 pip install flask==3.0.0 pip install flask-cors==4.0.0 pip install pymysql==1.1.0 pip install numpy==1.24.3

这里 Python 选 3.9 是因为 PyTorch 2.1 对 3.9 支持最稳,3.11 以上有些旧库会编译失败。OpenCV 用 4.8 版本,舌象分割里用到的cv2.cvtColor和cv2.inRange行为一致。Flask 负责起服务,flask-cors 解决 Vue 开发时跨域问题,pymysql 连 MySQL。

Vue 这边用 Vue 3 + Vite,比 Vue 2 + webpack 快很多,配置也简单。

# 创建 Vue 3 项目 npm create vite@latest tongue-frontend -- --template vue cd tongue-frontend npm install npm install element-plus axios vue-router npm run dev

element-plus提供上传组件和卡片布局,axios发请求,vue-router管页面跳转。装完后npm run dev能起来就说明前端环境 OK。注意 Node 版本建议 18 LTS,16 也能跑但 Vite 4 会警告。

2.3 数据库表结构设计:四张表撑起整个系统

数据库不用设计得太复杂,四张表足够。用户表存账号,舌象记录表存每次上传的图片路径和采集时间,诊断结果表存模型输出的证型和置信度,证型字典表存中医证型名称和对应的调理建议。

CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(128) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE tongue_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, image_path VARCHAR(255) NOT NULL, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ); CREATE TABLE diagnosis ( id INT PRIMARY KEY AUTO_INCREMENT, record_id INT NOT NULL, syndrome VARCHAR(50) NOT NULL, confidence FLOAT NOT NULL, advice TEXT, FOREIGN KEY (record_id) REFERENCES tongue_record(id) ); CREATE TABLE syndrome_dict ( id INT PRIMARY KEY AUTO_INCREMENT, syndrome_name VARCHAR(50) UNIQUE NOT NULL, description VARCHAR(255), advice TEXT );

syndrome_dict这张字典表很关键,它把模型输出的英文标签映射成中医术语和调理建议,答辩时老师问「你的诊断依据是什么」,你可以指着这张表说「证型与建议是结构化存储的,可维护可扩展」。confidence字段存置信度,前端可以按置信度高低用不同颜色显示,低于 0.6 的标灰提示「仅供参考」。

3. 舌象图像处理与特征提取:从原始照片到可训练数据

3.1 舌体分割:把舌头从背景里抠出来

用户拍的照片背景五花八门,直接送进模型准确率会崩。第一步必须做舌体分割。常见做法是转 HSV 空间后用颜色阈值分割,因为舌头在 HSV 里的 H 分量集中在红色到粉色区间。

import cv2 import numpy as np def segment_tongue(image_path): img = cv2.imread(image_path) # 统一缩放到 512 宽,减少计算量 h, w = img.shape[:2] scale = 512 / w img = cv2.resize(img, (512, int(h * scale))) # 转 HSV,舌体 H 分量大致在 0-20 和 160-180 两个区间 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask1 = cv2.inRange(hsv, (0, 40, 50), (20, 255, 255)) mask2 = cv2.inRange(hsv, (160, 40, 50), (180, 255, 255)) mask = cv2.bitwise_or(mask1, mask2) # 形态学闭运算填掉小孔,开运算去噪点 kernel = np.ones((7, 7), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 取最大连通区域,排除嘴唇等干扰 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, None largest = max(contours, key=cv2.contourArea) clean_mask = np.zeros_like(mask) cv2.drawContours(clean_mask, [largest], -1, 255, -1) result = cv2.bitwise_and(img, img, mask=clean_mask) return result, clean_mask

HSV 阈值里的(0, 40, 50)分别对应 H、S、V 的下限,S 设 40 是为了排除灰白色背景,V 设 50 排除过暗区域。形态学核用 7×7 是因为舌体边缘不规则,核太小填不满孔洞,太大又会把舌体边缘腐蚀掉。取最大连通区域这一步是血泪经验:不做的话,嘴唇和部分脸颊会被误判成舌体,后面特征全乱。

3.2 颜色与纹理特征:舌质、舌苔的量化

分割完的舌体图,要提取两类特征:颜色特征和纹理特征。颜色特征反映舌质和舌苔的色泽,纹理特征反映裂纹、齿痕、点刺。

from skimage.feature import graycomatrix, graycoprops def extract_features(tongue_img, mask): # 只统计 mask 内的像素 pixels = tongue_img[mask == 255] if len(pixels) == 0: return None # 颜色特征:RGB 和 HSV 各通道均值与标准差 mean_bgr = np.mean(pixels, axis=0) std_bgr = np.std(pixels, axis=0) hsv = cv2.cvtColor(tongue_img, cv2.COLOR_BGR2HSV) hsv_pixels = hsv[mask == 255] mean_hsv = np.mean(hsv_pixels, axis=0) # 纹理特征:灰度共生矩阵 gray = cv2.cvtColor(tongue_img, cv2.COLOR_BGR2GRAY) gray_masked = cv2.bitwise_and(gray, gray, mask=mask) glcm = graycomatrix(gray_masked, distances=[1], angles=[0], levels=256, symmetric=True, normed=True) contrast = graycoprops(glcm, 'contrast')[0, 0] homogeneity = graycoprops(glcm, 'homogeneity')[0, 0] energy = graycoprops(glcm, 'energy')[0, 0] feature_vector = np.concatenate([ mean_bgr, std_bgr, mean_hsv, [contrast, homogeneity, energy] ]) return feature_vector

graycomatrix的distances=[1]表示相邻像素对,angles=[0]只取水平方向,实际做的时候可以取四个方向再平均,但毕设阶段一个方向够用。contrast越大说明纹理越粗糙,对应裂纹或厚苔;homogeneity越大说明纹理越均匀,对应正常舌象。这些特征拼成一个 12 维向量,可以直接喂给传统分类器,也可以作为深度模型的辅助输入。

3.3 数据增强:小样本下的救命手段

自己采集的舌象数据通常只有几百张,直接训练必过拟合。我一般用在线增强,在训练循环里随机做旋转、翻转、亮度调整。

import random def augment(image, mask): # 随机水平翻转 if random.random() > 0.5: image = cv2.flip(image, 1) mask = cv2.flip(mask, 1) # 随机旋转 -15 到 15 度 angle = random.uniform(-15, 15) h, w = image.shape[:2] M = cv2.getRotationMatrix2D((w/2, h/2), angle, 1.0) image = cv2.warpAffine(image, M, (w, h)) mask = cv2.warpAffine(mask, M, (w, h)) # 随机亮度调整 factor = random.uniform(0.8, 1.2) image = np.clip(image * factor, 0, 255).astype(np.uint8) return image, mask

旋转角度控制在 ±15 度,因为舌象方向本身有语义,转 90 度就不合理了。亮度因子 0.8 到 1.2 是模拟不同拍摄光线,范围再大颜色特征会失真。增强只在训练时做,验证和测试用原图,否则评估结果不可信。

4. 证型分类模型:从特征向量到中医结论

4.1 模型选型:传统机器学习还是深度学习

这是毕设里最容易被问的点。如果你的数据量在 500 张以下,我建议用 SVM 或随机森林跑特征向量,准确率不一定比深度学习差,而且训练快、可解释性强,答辩时能画出特征重要性图。如果数据量上千,或者你想在论文里体现「深度学习」,那就用 ResNet 做迁移学习。

from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report import joblib # X 是 N×12 的特征矩阵,y 是证型标签 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42) # SVM 方案 svm = SVC(kernel='rbf', C=10, gamma='scale', probability=True) svm.fit(X_train, y_train) y_pred = svm.predict(X_test) print(classification_report(y_test, y_pred)) # 随机森林方案,顺便看特征重要性 rf = RandomForestClassifier(n_estimators=200, max_depth=10, random_state=42) rf.fit(X_train, y_train) print(rf.feature_importances_) joblib.dump(svm, 'svm_model.pkl')

stratify=y保证训练测试集里各类证型比例一致,否则小类别可能全被分到测试集。SVM 的C=10是我调过的,C 太小欠拟合,太大过拟合,10 左右在舌象特征上比较稳。随机森林的n_estimators=200是精度和速度的折中,max_depth=10防止树太深记住噪声。feature_importances_打印出来你会发现颜色均值权重最高,这符合中医「舌色主病」的经验,答辩时是个好论据。

4.2 深度学习方案:ResNet 迁移学习

如果坚持用深度模型,别从零训,用预训练权重微调。

import torch import torch.nn as nn from torchvision import models, transforms class TongueNet(nn.Module): def __init__(self, num_classes=6): super().__init__() self.backbone = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 冻结前几层,只微调后面的 for name, param in self.backbone.named_parameters(): if 'layer3' not in name and 'layer4' not in name and 'fc' not in name: param.requires_grad = False self.backbone.fc = nn.Linear(512, num_classes) def forward(self, x): return self.backbone(x) transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

ResNet18_Weights.IMAGENET1K_V1是 torchvision 自带的预训练权重,不用手动下载。冻结 layer1 和 layer2 是因为底层特征(边缘、颜色块)通用,没必要重训;微调 layer3、layer4 和 fc 让它适应舌象。num_classes=6对应六种常见证型,你可以按自己的数据调整。Normalize 的均值方差是 ImageNet 的标准值,用预训练权重就必须用这套参数,否则输入分布对不上。

4.3 训练循环与早停:别让模型跑飞

def train_model(model, train_loader, val_loader, epochs=30, lr=1e-4): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=lr) best_acc = 0.0 patience = 5 counter = 0 for epoch in range(epochs): model.train() 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() # 验证 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = correct / total print(f'Epoch {epoch+1}, Val Acc: {acc:.4f}') if acc > best_acc: best_acc = acc torch.save(model.state_dict(), 'best_model.pth') counter = 0 else: counter += 1 if counter >= patience: print('Early stopping') break return best_acc

filter(lambda p: p.requires_grad, ...)只把需要更新的参数传给优化器,冻结的层不参与更新,省显存也防过拟合。patience=5表示验证准确率连续 5 轮不提升就停,这是防止跑飞的关键。学习率1e-4是微调常用值,比从头训练的1e-3小一个量级,避免破坏预训练权重。

5. Flask 接口与 Vue 前端联调:把模型变成能点的按钮

5.1 Flask 接口:三个路由撑起后端

后端不需要复杂,三个接口够用:上传图片并诊断、查历史记录、查证型字典。

from flask import Flask, request, jsonify from flask_cors import CORS import pymysql import joblib import os app = Flask(__name__) CORS(app) model = joblib.load('svm_model.pkl') def get_db(): return pymysql.connect(host='localhost', user='root', password='yourpwd', database='tongue_db', charset='utf8mb4') @app.route('/api/diagnose', methods=['POST']) def diagnose(): file = request.files['image'] user_id = request.form['user_id'] save_path = f'uploads/{file.filename}' file.save(save_path) tongue_img, mask = segment_tongue(save_path) if tongue_img is None: return jsonify({'code': 400, 'msg': '未检测到舌体,请重新拍摄'}) features = extract_features(tongue_img, mask).reshape(1, -1) pred = model.predict(features)[0] prob = model.predict_proba(features).max() conn = get_db() cursor = conn.cursor() cursor.execute("INSERT INTO tongue_record (user_id, image_path) VALUES (%s, %s)", (user_id, save_path)) record_id = cursor.lastrowid cursor.execute("INSERT INTO diagnosis (record_id, syndrome, confidence) VALUES (%s, %s, %s)", (record_id, pred, float(prob))) conn.commit() conn.close() return jsonify({'code': 200, 'syndrome': pred, 'confidence': float(prob)})

CORS(app)解决 Vue 开发服务器 5173 端口访问 Flask 5000 端口的跨域。request.files['image']对应前端 FormData 里的字段名,必须一致。predict_proba返回置信度,前端可以据此显示「可信度 85%」。注意conn.commit()别漏,否则数据不落库,查历史时一片空白。

5.2 Vue 上传组件:把图片发给后端

<template> <el-upload action="#" :before-upload="handleUpload" :show-file-list="false" accept="image/*" > <el-button type="primary">上传舌象照片</el-button> </el-upload> <el-card v-if="result" style="margin-top: 20px"> <h3>诊断结果:{{ result.syndrome }}</h3> <p>置信度:{{ (result.confidence * 100).toFixed(1) }}%</p> </el-card> </template> <script setup> import { ref } from 'vue' import axios from 'axios' const result = ref(null) const handleUpload = async (file) => { const formData = new FormData() formData.append('image', file) formData.append('user_id', 1) try { const res = await axios.post('http://localhost:5000/api/diagnose', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) if (res.data.code === 200) { result.value = res.data } else { alert(res.data.msg) } } catch (e) { alert('请求失败,检查后端是否启动') } return false } </script>

before-upload返回false阻止 el-upload 自己发请求,改由 axios 手动发,这样能控制字段名和错误处理。Content-Type设成multipart/form-data是文件上传必须的,axios 会自动加 boundary。user_id这里写死 1 是 demo 简化,实际应该从登录态取。

5.3 联调排查:跨域和路径两个高频问题

联调时最常见的报错是Access to XMLHttpRequest has been blocked by CORS policy,说明 flask-cors 没生效或没装。检查CORS(app)是否在app = Flask(__name__)之后立刻调用。第二个坑是图片路径,Flask 保存到uploads/目录,但这个目录必须提前建好,否则file.save会抛FileNotFoundError。我一般在启动脚本里加os.makedirs('uploads', exist_ok=True)。前端显示图片时,路径要用后端能访问的 URL,不能直接用本地文件路径,需要再加一个/api/image/<filename>路由返回图片。

6. 避坑与常见问题:那些让我返工三次的细节

6.1 现象:模型在测试集准确率 95%,实际用手机拍的照片全错

原因:训练数据是用固定设备、固定光源采集的,模型学到了光源和背景的偏差,而不是舌象本身的特征。解决:训练时必须做颜色恒常性处理,或者用大量不同光源增强的数据。我一般会在预处理里加一步白平衡,用cv2.xphoto.createSimpleWB()校正,再送进模型。另外测试集要留一部分「脏数据」,别全用清洗过的图。

6.2 现象:Flask 接口第一次请求要等 10 秒,后面就快了

原因:模型加载写在路由函数里,每次请求都重新joblib.load。解决:把模型加载提到模块顶层,Flask 启动时就加载一次,全局复用。深度学习模型同理,model.eval()也放顶层。这个坑很隐蔽,因为功能是对的,只是慢,答辩演示时第一张图卡 10 秒很尴尬。

6.3 现象:MySQL 插入中文证型名变成问号

原因:连接字符集不是 utf8mb4,或者建表时没指定字符集。解决:pymysql.connect里加charset='utf8mb4',建表语句末尾加DEFAULT CHARSET=utf8mb4。中医证型名里有「阴虚火旺」「脾虚湿盛」这些词,用 latin1 或 utf8 三字节都可能出问题,必须 utf8mb4。

6.4 现象:Vue 打包后部署到服务器,刷新页面 404

原因:Vue Router 默认 history 模式,服务器没配 fallback。解决:要么改用 hash 模式(URL 带 #),要么在 Nginx 里加try_files $uri $uri/ /index.html;。毕设演示如果只在本地npm run dev,不会遇到这个问题,但一旦要部署到服务器答辩,必踩。

6.5 现象:舌体分割把牙齿也框进去了

原因:HSV 阈值范围太宽,牙齿在强光下偏黄白,S 分量低但 V 分量高,可能落进阈值。解决:在 mask 后加一步面积和长宽比过滤,舌体面积通常占图像 15% 到 40%,长宽比在 0.8 到 1.5 之间,超出范围的连通域丢弃。另外可以在分割前先做人脸检测,只取下半脸区域再分割,减少干扰。

7. 让系统更像「智能」的两个进阶技巧

第一个技巧是置信度分级展示。模型输出的predict_proba不要只取最大值,把 top-3 证型和概率都返回给前端,用进度条展示。这样即使主证型判断有偏差,老师也能看到模型「知道自己在犹豫」,比硬报一个结果可信得多。实现上把model.predict_proba(features)[0]排序取前三,配合syndrome_dict表查描述,前端用el-progress渲染。

第二个技巧是加一个「舌象质量检测」前置接口。用户拍的照片如果太暗、太模糊、舌体占比太小,直接返回「请重新拍摄」而不是硬跑模型。质量检测用三个指标:图像平均亮度低于 60 判太暗,拉普拉斯方差低于 100 判模糊,舌体 mask 面积占比低于 10% 判太远。这三个阈值是我调出来的,能挡掉大部分废片,避免模型在垃圾输入上给出垃圾输出,答辩时这个设计很加分。

def check_quality(image_path): img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) brightness = gray.mean() blur = cv2.Laplacian(gray, cv2.CV_64F).var() _, mask = segment_tongue(image_path) ratio = (mask == 255).sum() / mask.size if mask is not None else 0 if brightness < 60: return False, '光线太暗,请在明亮处拍摄' if blur < 100: return False, '图像模糊,请保持稳定' if ratio < 0.1: return False, '舌体太小,请靠近拍摄' return True, 'OK'

cv2.Laplacian的方差反映图像清晰度,值越小越模糊,100 这个阈值对手机拍摄的舌象比较合适。ratio用 mask 像素数除以总像素数,低于 0.1 说明舌头在画面里太小,特征提取会不准。这三个检查放在诊断接口最前面,不通过就直接返回,省得白跑模型。

我自己做这个系统时,最大的教训是别一上来就堆深度学习。先用 SVM 跑通全流程,把前后端和数据库串起来,再考虑换模型。很多毕设翻车不是因为模型不够强,而是因为接口调不通、数据存不进、页面刷不出来。把工程链路走通,比追求那两三个点的准确率重要得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询