智能舌診系統(tǒng)畢設實戰(zhàn):Python+Vue全棧實現(xiàn)與避坑指南)
簡介這是一套面向計算機相關專業(yè)大四學生與項目實戰(zhàn)學習者的中醫(yī)智能舌診系統(tǒng)畢業(yè)設計源碼由導師指導并獲評審99分認可適合作為畢設參考、課程設計或期末大作業(yè)。項目采用Python后端與Vue前端分離架構涵蓋舌象采集、智能分析與診斷結果展示等核心模塊代碼完整可運行零基礎也能按文檔逐步跑通。壓縮包共85個文件約2.51MB其中24個vue組件與22個py腳本構成前后端主體另有10個js、7個json配置、2個sql建表腳本及db數(shù)據庫文件配合md說明文檔、png與jpg界面截圖目錄結構清晰便于按模塊檢索學習。目前已有219人學習下載。讀者可獲得完整可運行的工程源碼、數(shù)據庫腳本與文檔說明直接用于畢設答辯、課程設計或二次開發(fā)省去從零搭建的時間成本。1. 從一張舌頭照片到一份診斷報告中醫(yī)智能舌診系統(tǒng)到底在做什么把手機鏡頭對準舌頭拍一張照片幾秒鐘后屏幕上彈出「舌質偏紅、苔薄黃、裂紋明顯傾向陰虛火旺」——這就是中醫(yī)智能舌診系統(tǒng)想干的事。它本質上是把「望舌」這個依賴老中醫(yī)經驗的動作拆成圖像采集、顏色與紋理特征提取、證型分類、結果可視化四段流水線再用 Python 做算法、Vue 做界面、數(shù)據庫存病歷。對做畢設的人來說它的價值在于選題有中醫(yī)文化背書、技術棧覆蓋前后端和機器學習、數(shù)據可自采、演示效果直觀答辯時一張對比圖就能講清楚。適合誰適合已經會一點 Python、想找一個「有真實業(yè)務場景又不至于爛大街」的計算機或軟件工程畢設方向的人。這篇筆記不講空話直接按我實際搭過一遍的順序把環(huán)境、數(shù)據、模型、前后端、數(shù)據庫和踩過的坑講透讓你能照著復現(xiàn)。2. 技術選型與整體架構為什么是 Python Vue 這套組合2.1 三段式架構的拆解邏輯一個能跑通、能答辯的舌診系統(tǒng)我一般會拆成三層算法層、服務層、展示層。算法層用 Python因為 OpenCV、scikit-image、PyTorch 這些庫在圖像處理和深度學習上生態(tài)最全舌象的顏色空間轉換、區(qū)域分割、特征提取幾乎都能找到現(xiàn)成實現(xiàn)。服務層用 Flask 或 FastAPI 把模型包成 HTTP 接口前端不關心模型怎么跑只發(fā)圖片收 JSON。展示層用 Vue因為它是組件化的舌象上傳、結果卡片、歷史記錄這些模塊可以拆開寫配合 Element Plus 能快速出效果答辯演示時界面不寒酸。數(shù)據庫選 MySQL 而不是 SQLite原因是畢設通常要體現(xiàn)「數(shù)據庫設計」這一章MySQL 有完整的建表、索引、外鍵、連接池概念可以寫SQLite 雖然輕但答辯老師容易覺得「太簡單」。如果你只是本地跑 demoSQLite 也夠但既然標題里帶了「數(shù)據庫」我建議直接上 MySQL把用戶表、舌象記錄表、診斷結果表、證型字典表設計清楚。2.2 環(huán)境搭建Python 與 Vue 的最小可跑配置先把 Python 環(huán)境弄干凈。不要用系統(tǒng)自帶的 Python用 conda 或 venv 建獨立環(huán)境否則后面裝 PyTorch 和 OpenCV 版本沖突會讓你懷疑人生。# 創(chuàng)建并激活虛擬環(huán)境以 conda 為例 conda create -n tongue python3.9 -y conda activate tongue # 安裝核心依賴版本是我實測能跑通的組合 pip install opencv-python4.8.1.78 pip install scikit-image0.22.0 pip install torch2.1.0 torchvision0.16.0 pip install flask3.0.0 pip install flask-cors4.0.0 pip install pymysql1.1.0 pip install numpy1.24.3這里 Python 選 3.9 是因為 PyTorch 2.1 對 3.9 支持最穩(wěn)3.11 以上有些舊庫會編譯失敗。OpenCV 用 4.8 版本舌象分割里用到的cv2.cvtColor和cv2.inRange行為一致。Flask 負責起服務flask-cors 解決 Vue 開發(fā)時跨域問題pymysql 連 MySQL。Vue 這邊用 Vue 3 Vite比 Vue 2 webpack 快很多配置也簡單。# 創(chuàng)建 Vue 3 項目 npm create vitelatest tongue-frontend -- --template vue cd tongue-frontend npm install npm install element-plus axios vue-router npm run develement-plus提供上傳組件和卡片布局axios發(fā)請求vue-router管頁面跳轉。裝完后npm run dev能起來就說明前端環(huán)境 OK。注意 Node 版本建議 18 LTS16 也能跑但 Vite 4 會警告。2.3 數(shù)據庫表結構設計四張表撐起整個系統(tǒng)數(shù)據庫不用設計得太復雜四張表足夠。用戶表存賬號舌象記錄表存每次上傳的圖片路徑和采集時間診斷結果表存模型輸出的證型和置信度證型字典表存中醫(yī)證型名稱和對應的調理建議。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這張字典表很關鍵它把模型輸出的英文標簽映射成中醫(yī)術語和調理建議答辯時老師問「你的診斷依據是什么」你可以指著這張表說「證型與建議是結構化存儲的可維護可擴展」。confidence字段存置信度前端可以按置信度高低用不同顏色顯示低于 0.6 的標灰提示「僅供參考」。3. 舌象圖像處理與特征提取從原始照片到可訓練數(shù)據3.1 舌體分割把舌頭從背景里摳出來用戶拍的照片背景五花八門直接送進模型準確率會崩。第一步必須做舌體分割。常見做法是轉 HSV 空間后用顏色閾值分割因為舌頭在 HSV 里的 H 分量集中在紅色到粉色區(qū)間。import cv2 import numpy as np def segment_tongue(image_path): img cv2.imread(image_path) # 統(tǒng)一縮放到 512 寬減少計算量 h, w img.shape[:2] scale 512 / w img cv2.resize(img, (512, int(h * scale))) # 轉 HSV舌體 H 分量大致在 0-20 和 160-180 兩個區(qū)間 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) # 形態(tài)學閉運算填掉小孔開運算去噪點 kernel np.ones((7, 7), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) # 取最大連通區(qū)域排除嘴唇等干擾 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None, None largest max(contours, keycv2.contourArea) clean_mask np.zeros_like(mask) cv2.drawContours(clean_mask, [largest], -1, 255, -1) result cv2.bitwise_and(img, img, maskclean_mask) return result, clean_maskHSV 閾值里的(0, 40, 50)分別對應 H、S、V 的下限S 設 40 是為了排除灰白色背景V 設 50 排除過暗區(qū)域。形態(tài)學核用 7×7 是因為舌體邊緣不規(guī)則核太小填不滿孔洞太大又會把舌體邊緣腐蝕掉。取最大連通區(qū)域這一步是血淚經驗不做的話嘴唇和部分臉頰會被誤判成舌體后面特征全亂。3.2 顏色與紋理特征舌質、舌苔的量化分割完的舌體圖要提取兩類特征顏色特征和紋理特征。顏色特征反映舌質和舌苔的色澤紋理特征反映裂紋、齒痕、點刺。from skimage.feature import graycomatrix, graycoprops def extract_features(tongue_img, mask): # 只統(tǒng)計 mask 內的像素 pixels tongue_img[mask 255] if len(pixels) 0: return None # 顏色特征RGB 和 HSV 各通道均值與標準差 mean_bgr np.mean(pixels, axis0) std_bgr np.std(pixels, axis0) hsv cv2.cvtColor(tongue_img, cv2.COLOR_BGR2HSV) hsv_pixels hsv[mask 255] mean_hsv np.mean(hsv_pixels, axis0) # 紋理特征灰度共生矩陣 gray cv2.cvtColor(tongue_img, cv2.COLOR_BGR2GRAY) gray_masked cv2.bitwise_and(gray, gray, maskmask) glcm graycomatrix(gray_masked, distances[1], angles[0], levels256, symmetricTrue, normedTrue) 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_vectorgraycomatrix的distances[1]表示相鄰像素對angles[0]只取水平方向實際做的時候可以取四個方向再平均但畢設階段一個方向夠用。contrast越大說明紋理越粗糙對應裂紋或厚苔homogeneity越大說明紋理越均勻對應正常舌象。這些特征拼成一個 12 維向量可以直接喂給傳統(tǒng)分類器也可以作為深度模型的輔助輸入。3.3 數(shù)據增強小樣本下的救命手段自己采集的舌象數(shù)據通常只有幾百張直接訓練必過擬合。我一般用在線增強在訓練循環(huán)里隨機做旋轉、翻轉、亮度調整。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. 證型分類模型從特征向量到中醫(yī)結論4.1 模型選型傳統(tǒng)機器學習還是深度學習這是畢設里最容易被問的點。如果你的數(shù)據量在 500 張以下我建議用 SVM 或隨機森林跑特征向量準確率不一定比深度學習差而且訓練快、可解釋性強答辯時能畫出特征重要性圖。如果數(shù)據量上千或者你想在論文里體現(xiàn)「深度學習」那就用 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_size0.2, stratifyy, random_state42) # SVM 方案 svm SVC(kernelrbf, C10, gammascale, probabilityTrue) svm.fit(X_train, y_train) y_pred svm.predict(X_test) print(classification_report(y_test, y_pred)) # 隨機森林方案順便看特征重要性 rf RandomForestClassifier(n_estimators200, max_depth10, random_state42) rf.fit(X_train, y_train) print(rf.feature_importances_) joblib.dump(svm, svm_model.pkl)stratifyy保證訓練測試集里各類證型比例一致否則小類別可能全被分到測試集。SVM 的C10是我調過的C 太小欠擬合太大過擬合10 左右在舌象特征上比較穩(wěn)。隨機森林的n_estimators200是精度和速度的折中max_depth10防止樹太深記住噪聲。feature_importances_打印出來你會發(fā)現(xiàn)顏色均值權重最高這符合中醫(yī)「舌色主病」的經驗答辯時是個好論據。4.2 深度學習方案ResNet 遷移學習如果堅持用深度模型別從零訓用預訓練權重微調。import torch import torch.nn as nn from torchvision import models, transforms class TongueNet(nn.Module): def __init__(self, num_classes6): super().__init__() self.backbone models.resnet18(weightsmodels.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_classes6對應六種常見證型你可以按自己的數(shù)據調整。Normalize 的均值方差是 ImageNet 的標準值用預訓練權重就必須用這套參數(shù)否則輸入分布對不上。4.3 訓練循環(huán)與早停別讓模型跑飛def train_model(model, train_loader, val_loader, epochs30, lr1e-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()), lrlr) 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(fEpoch {epoch1}, 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_accfilter(lambda p: p.requires_grad, ...)只把需要更新的參數(shù)傳給優(yōu)化器凍結的層不參與更新省顯存也防過擬合。patience5表示驗證準確率連續(xù) 5 輪不提升就停這是防止跑飛的關鍵。學習率1e-4是微調常用值比從頭訓練的1e-3小一個量級避免破壞預訓練權重。5. Flask 接口與 Vue 前端聯(lián)調把模型變成能點的按鈕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(hostlocalhost, userroot, passwordyourpwd, databasetongue_db, charsetutf8mb4) app.route(/api/diagnose, methods[POST]) def diagnose(): file request.files[image] user_id request.form[user_id] save_path fuploads/{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 開發(fā)服務器 5173 端口訪問 Flask 5000 端口的跨域。request.files[image]對應前端 FormData 里的字段名必須一致。predict_proba返回置信度前端可以據此顯示「可信度 85%」。注意conn.commit()別漏否則數(shù)據不落庫查歷史時一片空白。5.2 Vue 上傳組件把圖片發(fā)給后端template el-upload action# :before-uploadhandleUpload :show-file-listfalse acceptimage/* el-button typeprimary上傳舌象照片/el-button /el-upload el-card v-ifresult stylemargin-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 } /scriptbefore-upload返回false阻止 el-upload 自己發(fā)請求改由 axios 手動發(fā)這樣能控制字段名和錯誤處理。Content-Type設成multipart/form-data是文件上傳必須的axios 會自動加 boundary。user_id這里寫死 1 是 demo 簡化實際應該從登錄態(tài)取。5.3 聯(lián)調排查跨域和路徑兩個高頻問題聯(lián)調時最常見的報錯是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_okTrue)。前端顯示圖片時路徑要用后端能訪問的 URL不能直接用本地文件路徑需要再加一個/api/image/filename路由返回圖片。6. 避坑與常見問題那些讓我返工三次的細節(jié)6.1 現(xiàn)象模型在測試集準確率 95%實際用手機拍的照片全錯原因訓練數(shù)據是用固定設備、固定光源采集的模型學到了光源和背景的偏差而不是舌象本身的特征。解決訓練時必須做顏色恒常性處理或者用大量不同光源增強的數(shù)據。我一般會在預處理里加一步白平衡用cv2.xphoto.createSimpleWB()校正再送進模型。另外測試集要留一部分「臟數(shù)據」別全用清洗過的圖。6.2 現(xiàn)象Flask 接口第一次請求要等 10 秒后面就快了原因模型加載寫在路由函數(shù)里每次請求都重新joblib.load。解決把模型加載提到模塊頂層Flask 啟動時就加載一次全局復用。深度學習模型同理model.eval()也放頂層。這個坑很隱蔽因為功能是對的只是慢答辯演示時第一張圖卡 10 秒很尷尬。6.3 現(xiàn)象MySQL 插入中文證型名變成問號原因連接字符集不是 utf8mb4或者建表時沒指定字符集。解決pymysql.connect里加charsetutf8mb4建表語句末尾加DEFAULT CHARSETutf8mb4。中醫(yī)證型名里有「陰虛火旺」「脾虛濕盛」這些詞用 latin1 或 utf8 三字節(jié)都可能出問題必須 utf8mb4。6.4 現(xiàn)象Vue 打包后部署到服務器刷新頁面 404原因Vue Router 默認 history 模式服務器沒配 fallback。解決要么改用 hash 模式URL 帶 #要么在 Nginx 里加try_files $uri $uri/ /index.html;。畢設演示如果只在本地npm run dev不會遇到這個問題但一旦要部署到服務器答辯必踩。6.5 現(xiàn)象舌體分割把牙齒也框進去了原因HSV 閾值范圍太寬牙齒在強光下偏黃白S 分量低但 V 分量高可能落進閾值。解決在 mask 后加一步面積和長寬比過濾舌體面積通常占圖像 15% 到 40%長寬比在 0.8 到 1.5 之間超出范圍的連通域丟棄。另外可以在分割前先做人臉檢測只取下半臉區(qū)域再分割減少干擾。7. 讓系統(tǒng)更像「智能」的兩個進階技巧第一個技巧是置信度分級展示。模型輸出的predict_proba不要只取最大值把 top-3 證型和概率都返回給前端用進度條展示。這樣即使主證型判斷有偏差老師也能看到模型「知道自己在猶豫」比硬報一個結果可信得多。實現(xiàn)上把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, 圖像模糊請保持穩(wěn)定 if ratio 0.1: return False, 舌體太小請靠近拍攝 return True, OKcv2.Laplacian的方差反映圖像清晰度值越小越模糊100 這個閾值對手機拍攝的舌象比較合適。ratio用 mask 像素數(shù)除以總像素數(shù)低于 0.1 說明舌頭在畫面里太小特征提取會不準。這三個檢查放在診斷接口最前面不通過就直接返回省得白跑模型。我自己做這個系統(tǒng)時最大的教訓是別一上來就堆深度學習。先用 SVM 跑通全流程把前后端和數(shù)據庫串起來再考慮換模型。很多畢設翻車不是因為模型不夠強而是因為接口調不通、數(shù)據存不進、頁面刷不出來。把工程鏈路走通比追求那兩三個點的準確率重要得多。希望幫到你。本文還有配套的精品資源點擊獲取