智能舌診系統(tǒng)畢設(shè)實(shí)戰(zhàn):Python+Vue全棧實(shí)現(xiàn)與避坑指南)
簡(jiǎn)介這是一套面向計(jì)算機(jī)相關(guān)專(zhuān)業(yè)大四學(xué)生與項(xiàng)目實(shí)戰(zhàn)學(xué)習(xí)者的中醫(yī)智能舌診系統(tǒng)畢業(yè)設(shè)計(jì)源碼由導(dǎo)師指導(dǎo)并獲評(píng)審99分認(rèn)可適合作為畢設(shè)參考、課程設(shè)計(jì)或期末大作業(yè)。項(xiàng)目采用Python后端與Vue前端分離架構(gòu)涵蓋舌象采集、智能分析與診斷結(jié)果展示等核心模塊代碼完整可運(yùn)行零基礎(chǔ)也能按文檔逐步跑通。壓縮包共85個(gè)文件約2.51MB其中24個(gè)vue組件與22個(gè)py腳本構(gòu)成前后端主體另有10個(gè)js、7個(gè)json配置、2個(gè)sql建表腳本及db數(shù)據(jù)庫(kù)文件配合md說(shuō)明文檔、png與jpg界面截圖目錄結(jié)構(gòu)清晰便于按模塊檢索學(xué)習(xí)。目前已有219人學(xué)習(xí)下載。讀者可獲得完整可運(yùn)行的工程源碼、數(shù)據(jù)庫(kù)腳本與文檔說(shuō)明直接用于畢設(shè)答辯、課程設(shè)計(jì)或二次開(kāi)發(fā)省去從零搭建的時(shí)間成本。1. 從一張舌頭照片到一份診斷報(bào)告中醫(yī)智能舌診系統(tǒng)到底在做什么把手機(jī)鏡頭對(duì)準(zhǔn)舌頭拍一張照片幾秒鐘后屏幕上彈出「舌質(zhì)偏紅、苔薄黃、裂紋明顯傾向陰虛火旺」——這就是中醫(yī)智能舌診系統(tǒng)想干的事。它本質(zhì)上是把「望舌」這個(gè)依賴(lài)?yán)现嗅t(yī)經(jīng)驗(yàn)的動(dòng)作拆成圖像采集、顏色與紋理特征提取、證型分類(lèi)、結(jié)果可視化四段流水線再用 Python 做算法、Vue 做界面、數(shù)據(jù)庫(kù)存病歷。對(duì)做畢設(shè)的人來(lái)說(shuō)它的價(jià)值在于選題有中醫(yī)文化背書(shū)、技術(shù)棧覆蓋前后端和機(jī)器學(xué)習(xí)、數(shù)據(jù)可自采、演示效果直觀答辯時(shí)一張對(duì)比圖就能講清楚。適合誰(shuí)適合已經(jīng)會(huì)一點(diǎn) Python、想找一個(gè)「有真實(shí)業(yè)務(wù)場(chǎng)景又不至于爛大街」的計(jì)算機(jī)或軟件工程畢設(shè)方向的人。這篇筆記不講空話(huà)直接按我實(shí)際搭過(guò)一遍的順序把環(huán)境、數(shù)據(jù)、模型、前后端、數(shù)據(jù)庫(kù)和踩過(guò)的坑講透讓你能照著復(fù)現(xiàn)。2. 技術(shù)選型與整體架構(gòu)為什么是 Python Vue 這套組合2.1 三段式架構(gòu)的拆解邏輯一個(gè)能跑通、能答辯的舌診系統(tǒng)我一般會(huì)拆成三層算法層、服務(wù)層、展示層。算法層用 Python因?yàn)?OpenCV、scikit-image、PyTorch 這些庫(kù)在圖像處理和深度學(xué)習(xí)上生態(tài)最全舌象的顏色空間轉(zhuǎn)換、區(qū)域分割、特征提取幾乎都能找到現(xiàn)成實(shí)現(xiàn)。服務(wù)層用 Flask 或 FastAPI 把模型包成 HTTP 接口前端不關(guān)心模型怎么跑只發(fā)圖片收 JSON。展示層用 Vue因?yàn)樗墙M件化的舌象上傳、結(jié)果卡片、歷史記錄這些模塊可以拆開(kāi)寫(xiě)配合 Element Plus 能快速出效果答辯演示時(shí)界面不寒酸。數(shù)據(jù)庫(kù)選 MySQL 而不是 SQLite原因是畢設(shè)通常要體現(xiàn)「數(shù)據(jù)庫(kù)設(shè)計(jì)」這一章MySQL 有完整的建表、索引、外鍵、連接池概念可以寫(xiě)SQLite 雖然輕但答辯老師容易覺(jué)得「太簡(jiǎn)單」。如果你只是本地跑 demoSQLite 也夠但既然標(biāo)題里帶了「數(shù)據(jù)庫(kù)」我建議直接上 MySQL把用戶(hù)表、舌象記錄表、診斷結(jié)果表、證型字典表設(shè)計(jì)清楚。2.2 環(huán)境搭建Python 與 Vue 的最小可跑配置先把 Python 環(huán)境弄干凈。不要用系統(tǒng)自帶的 Python用 conda 或 venv 建獨(dú)立環(huán)境否則后面裝 PyTorch 和 OpenCV 版本沖突會(huì)讓你懷疑人生。# 創(chuàng)建并激活虛擬環(huán)境以 conda 為例 conda create -n tongue python3.9 -y conda activate tongue # 安裝核心依賴(lài)版本是我實(shí)測(cè)能跑通的組合 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 是因?yàn)?PyTorch 2.1 對(duì) 3.9 支持最穩(wěn)3.11 以上有些舊庫(kù)會(huì)編譯失敗。OpenCV 用 4.8 版本舌象分割里用到的cv2.cvtColor和cv2.inRange行為一致。Flask 負(fù)責(zé)起服務(wù)flask-cors 解決 Vue 開(kāi)發(fā)時(shí)跨域問(wèn)題pymysql 連 MySQL。Vue 這邊用 Vue 3 Vite比 Vue 2 webpack 快很多配置也簡(jiǎn)單。# 創(chuàng)建 Vue 3 項(xiàng)目 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ā)請(qǐng)求vue-router管頁(yè)面跳轉(zhuǎn)。裝完后npm run dev能起來(lái)就說(shuō)明前端環(huán)境 OK。注意 Node 版本建議 18 LTS16 也能跑但 Vite 4 會(huì)警告。2.3 數(shù)據(jù)庫(kù)表結(jié)構(gòu)設(shè)計(jì)四張表?yè)纹鹫麄€(gè)系統(tǒng)數(shù)據(jù)庫(kù)不用設(shè)計(jì)得太復(fù)雜四張表足夠。用戶(hù)表存賬號(hào)舌象記錄表存每次上傳的圖片路徑和采集時(shí)間診斷結(jié)果表存模型輸出的證型和置信度證型字典表存中醫(yī)證型名稱(chēng)和對(duì)應(yīng)的調(diào)理建議。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這張字典表很關(guān)鍵它把模型輸出的英文標(biāo)簽映射成中醫(yī)術(shù)語(yǔ)和調(diào)理建議答辯時(shí)老師問(wèn)「你的診斷依據(jù)是什么」你可以指著這張表說(shuō)「證型與建議是結(jié)構(gòu)化存儲(chǔ)的可維護(hù)可擴(kuò)展」。confidence字段存置信度前端可以按置信度高低用不同顏色顯示低于 0.6 的標(biāo)灰提示「僅供參考」。3. 舌象圖像處理與特征提取從原始照片到可訓(xùn)練數(shù)據(jù)3.1 舌體分割把舌頭從背景里摳出來(lái)用戶(hù)拍的照片背景五花八門(mén)直接送進(jìn)模型準(zhǔn)確率會(huì)崩。第一步必須做舌體分割。常見(jiàn)做法是轉(zhuǎn) HSV 空間后用顏色閾值分割因?yàn)樯囝^在 HSV 里的 H 分量集中在紅色到粉色區(qū)間。import cv2 import numpy as np def segment_tongue(image_path): img cv2.imread(image_path) # 統(tǒng)一縮放到 512 寬減少計(jì)算量 h, w img.shape[:2] scale 512 / w img cv2.resize(img, (512, int(h * scale))) # 轉(zhuǎn) HSV舌體 H 分量大致在 0-20 和 160-180 兩個(gè)區(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)學(xué)閉運(yùn)算填掉小孔開(kāi)運(yùn)算去噪點(diǎn) 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)分別對(duì)應(yīng) H、S、V 的下限S 設(shè) 40 是為了排除灰白色背景V 設(shè) 50 排除過(guò)暗區(qū)域。形態(tài)學(xué)核用 7×7 是因?yàn)樯囿w邊緣不規(guī)則核太小填不滿(mǎn)孔洞太大又會(huì)把舌體邊緣腐蝕掉。取最大連通區(qū)域這一步是血淚經(jīng)驗(yàn)不做的話(huà)嘴唇和部分臉頰會(huì)被誤判成舌體后面特征全亂。3.2 顏色與紋理特征舌質(zhì)、舌苔的量化分割完的舌體圖要提取兩類(lèi)特征顏色特征和紋理特征。顏色特征反映舌質(zhì)和舌苔的色澤紋理特征反映裂紋、齒痕、點(diǎn)刺。from skimage.feature import graycomatrix, graycoprops def extract_features(tongue_img, mask): # 只統(tǒng)計(jì) mask 內(nèi)的像素 pixels tongue_img[mask 255] if len(pixels) 0: return None # 顏色特征RGB 和 HSV 各通道均值與標(biāo)準(zhǔn)差 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]表示相鄰像素對(duì)angles[0]只取水平方向?qū)嶋H做的時(shí)候可以取四個(gè)方向再平均但畢設(shè)階段一個(gè)方向夠用。contrast越大說(shuō)明紋理越粗糙對(duì)應(yīng)裂紋或厚苔homogeneity越大說(shuō)明紋理越均勻?qū)?yīng)正常舌象。這些特征拼成一個(gè) 12 維向量可以直接喂給傳統(tǒng)分類(lèi)器也可以作為深度模型的輔助輸入。3.3 數(shù)據(jù)增強(qiáng)小樣本下的救命手段自己采集的舌象數(shù)據(jù)通常只有幾百?gòu)堉苯佑?xùn)練必過(guò)擬合。我一般用在線增強(qiáng)在訓(xùn)練循環(huán)里隨機(jī)做旋轉(zhuǎn)、翻轉(zhuǎn)、亮度調(diào)整。import random def augment(image, mask): # 隨機(jī)水平翻轉(zhuǎn) if random.random() 0.5: image cv2.flip(image, 1) mask cv2.flip(mask, 1) # 隨機(jī)旋轉(zhuǎn) -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)) # 隨機(jī)亮度調(diào)整 factor random.uniform(0.8, 1.2) image np.clip(image * factor, 0, 255).astype(np.uint8) return image, mask旋轉(zhuǎn)角度控制在 ±15 度因?yàn)樯嘞蠓较虮旧碛姓Z(yǔ)義轉(zhuǎn) 90 度就不合理了。亮度因子 0.8 到 1.2 是模擬不同拍攝光線范圍再大顏色特征會(huì)失真。增強(qiáng)只在訓(xùn)練時(shí)做驗(yàn)證和測(cè)試用原圖否則評(píng)估結(jié)果不可信。4. 證型分類(lèi)模型從特征向量到中醫(yī)結(jié)論4.1 模型選型傳統(tǒng)機(jī)器學(xué)習(xí)還是深度學(xué)習(xí)這是畢設(shè)里最容易被問(wèn)的點(diǎn)。如果你的數(shù)據(jù)量在 500 張以下我建議用 SVM 或隨機(jī)森林跑特征向量準(zhǔn)確率不一定比深度學(xué)習(xí)差而且訓(xùn)練快、可解釋性強(qiáng)答辯時(shí)能畫(huà)出特征重要性圖。如果數(shù)據(jù)量上千或者你想在論文里體現(xiàn)「深度學(xué)習(xí)」那就用 ResNet 做遷移學(xué)習(xí)。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 是證型標(biāo)簽 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)) # 隨機(jī)森林方案順便看特征重要性 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保證訓(xùn)練測(cè)試集里各類(lèi)證型比例一致否則小類(lèi)別可能全被分到測(cè)試集。SVM 的C10是我調(diào)過(guò)的C 太小欠擬合太大過(guò)擬合10 左右在舌象特征上比較穩(wěn)。隨機(jī)森林的n_estimators200是精度和速度的折中max_depth10防止樹(shù)太深記住噪聲。feature_importances_打印出來(lái)你會(huì)發(fā)現(xiàn)顏色均值權(quán)重最高這符合中醫(yī)「舌色主病」的經(jīng)驗(yàn)答辯時(shí)是個(gè)好論據(jù)。4.2 深度學(xué)習(xí)方案ResNet 遷移學(xué)習(xí)如果堅(jiān)持用深度模型別從零訓(xùn)用預(yù)訓(xùn)練權(quán)重微調(diào)。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) # 凍結(jié)前幾層只微調(diào)后面的 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 自帶的預(yù)訓(xùn)練權(quán)重不用手動(dòng)下載。凍結(jié) layer1 和 layer2 是因?yàn)榈讓犹卣鬟吘?、顏色塊通用沒(méi)必要重訓(xùn)微調(diào) layer3、layer4 和 fc 讓它適應(yīng)舌象。num_classes6對(duì)應(yīng)六種常見(jiàn)證型你可以按自己的數(shù)據(jù)調(diào)整。Normalize 的均值方差是 ImageNet 的標(biāo)準(zhǔn)值用預(yù)訓(xùn)練權(quán)重就必須用這套參數(shù)否則輸入分布對(duì)不上。4.3 訓(xùn)練循環(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() # 驗(yàn)證 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)化器凍結(jié)的層不參與更新省顯存也防過(guò)擬合。patience5表示驗(yàn)證準(zhǔn)確率連續(xù) 5 輪不提升就停這是防止跑飛的關(guān)鍵。學(xué)習(xí)率1e-4是微調(diào)常用值比從頭訓(xùn)練的1e-3小一個(gè)量級(jí)避免破壞預(yù)訓(xùn)練權(quán)重。5. Flask 接口與 Vue 前端聯(lián)調(diào)把模型變成能點(diǎn)的按鈕5.1 Flask 接口三個(gè)路由撐起后端后端不需要復(fù)雜三個(gè)接口夠用上傳圖片并診斷、查歷史記錄、查證型字典。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: 未檢測(cè)到舌體請(qǐng)重新拍攝}) 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 開(kāi)發(fā)服務(wù)器 5173 端口訪問(wèn) Flask 5000 端口的跨域。request.files[image]對(duì)應(yīng)前端 FormData 里的字段名必須一致。predict_proba返回置信度前端可以據(jù)此顯示「可信度 85%」。注意conn.commit()別漏否則數(shù)據(jù)不落庫(kù)查歷史時(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診斷結(jié)果{{ 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(請(qǐng)求失敗檢查后端是否啟動(dòng)) } return false } /scriptbefore-upload返回false阻止 el-upload 自己發(fā)請(qǐng)求改由 axios 手動(dòng)發(fā)這樣能控制字段名和錯(cuò)誤處理。Content-Type設(shè)成multipart/form-data是文件上傳必須的axios 會(huì)自動(dòng)加 boundary。user_id這里寫(xiě)死 1 是 demo 簡(jiǎn)化實(shí)際應(yīng)該從登錄態(tài)取。5.3 聯(lián)調(diào)排查跨域和路徑兩個(gè)高頻問(wèn)題聯(lián)調(diào)時(shí)最常見(jiàn)的報(bào)錯(cuò)是Access to XMLHttpRequest has been blocked by CORS policy說(shuō)明 flask-cors 沒(méi)生效或沒(méi)裝。檢查CORS(app)是否在app Flask(__name__)之后立刻調(diào)用。第二個(gè)坑是圖片路徑Flask 保存到uploads/目錄但這個(gè)目錄必須提前建好否則file.save會(huì)拋FileNotFoundError。我一般在啟動(dòng)腳本里加os.makedirs(uploads, exist_okTrue)。前端顯示圖片時(shí)路徑要用后端能訪問(wèn)的 URL不能直接用本地文件路徑需要再加一個(gè)/api/image/filename路由返回圖片。6. 避坑與常見(jiàn)問(wèn)題那些讓我返工三次的細(xì)節(jié)6.1 現(xiàn)象模型在測(cè)試集準(zhǔn)確率 95%實(shí)際用手機(jī)拍的照片全錯(cuò)原因訓(xùn)練數(shù)據(jù)是用固定設(shè)備、固定光源采集的模型學(xué)到了光源和背景的偏差而不是舌象本身的特征。解決訓(xùn)練時(shí)必須做顏色恒常性處理或者用大量不同光源增強(qiáng)的數(shù)據(jù)。我一般會(huì)在預(yù)處理里加一步白平衡用cv2.xphoto.createSimpleWB()校正再送進(jìn)模型。另外測(cè)試集要留一部分「臟數(shù)據(jù)」別全用清洗過(guò)的圖。6.2 現(xiàn)象Flask 接口第一次請(qǐng)求要等 10 秒后面就快了原因模型加載寫(xiě)在路由函數(shù)里每次請(qǐng)求都重新joblib.load。解決把模型加載提到模塊頂層Flask 啟動(dòng)時(shí)就加載一次全局復(fù)用。深度學(xué)習(xí)模型同理model.eval()也放頂層。這個(gè)坑很隱蔽因?yàn)楣δ苁菍?duì)的只是慢答辯演示時(shí)第一張圖卡 10 秒很尷尬。6.3 現(xiàn)象MySQL 插入中文證型名變成問(wèn)號(hào)原因連接字符集不是 utf8mb4或者建表時(shí)沒(méi)指定字符集。解決pymysql.connect里加charsetutf8mb4建表語(yǔ)句末尾加DEFAULT CHARSETutf8mb4。中醫(yī)證型名里有「陰虛火旺」「脾虛濕盛」這些詞用 latin1 或 utf8 三字節(jié)都可能出問(wèn)題必須 utf8mb4。6.4 現(xiàn)象Vue 打包后部署到服務(wù)器刷新頁(yè)面 404原因Vue Router 默認(rèn) history 模式服務(wù)器沒(méi)配 fallback。解決要么改用 hash 模式URL 帶 #要么在 Nginx 里加try_files $uri $uri/ /index.html;。畢設(shè)演示如果只在本地npm run dev不會(huì)遇到這個(gè)問(wèn)題但一旦要部署到服務(wù)器答辯必踩。6.5 現(xiàn)象舌體分割把牙齒也框進(jìn)去了原因HSV 閾值范圍太寬牙齒在強(qiáng)光下偏黃白S 分量低但 V 分量高可能落進(jìn)閾值。解決在 mask 后加一步面積和長(zhǎng)寬比過(guò)濾舌體面積通常占圖像 15% 到 40%長(zhǎng)寬比在 0.8 到 1.5 之間超出范圍的連通域丟棄。另外可以在分割前先做人臉檢測(cè)只取下半臉區(qū)域再分割減少干擾。7. 讓系統(tǒng)更像「智能」的兩個(gè)進(jìn)階技巧第一個(gè)技巧是置信度分級(jí)展示。模型輸出的predict_proba不要只取最大值把 top-3 證型和概率都返回給前端用進(jìn)度條展示。這樣即使主證型判斷有偏差老師也能看到模型「知道自己在猶豫」比硬報(bào)一個(gè)結(jié)果可信得多。實(shí)現(xiàn)上把model.predict_proba(features)[0]排序取前三配合syndrome_dict表查描述前端用el-progress渲染。第二個(gè)技巧是加一個(gè)「舌象質(zhì)量檢測(cè)」前置接口。用戶(hù)拍的照片如果太暗、太模糊、舌體占比太小直接返回「請(qǐng)重新拍攝」而不是硬跑模型。質(zhì)量檢測(cè)用三個(gè)指標(biāo)圖像平均亮度低于 60 判太暗拉普拉斯方差低于 100 判模糊舌體 mask 面積占比低于 10% 判太遠(yuǎn)。這三個(gè)閾值是我調(diào)出來(lái)的能擋掉大部分廢片避免模型在垃圾輸入上給出垃圾輸出答辯時(shí)這個(gè)設(shè)計(jì)很加分。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, 光線太暗請(qǐng)?jiān)诿髁撂幣臄z if blur 100: return False, 圖像模糊請(qǐng)保持穩(wěn)定 if ratio 0.1: return False, 舌體太小請(qǐng)靠近拍攝 return True, OKcv2.Laplacian的方差反映圖像清晰度值越小越模糊100 這個(gè)閾值對(duì)手機(jī)拍攝的舌象比較合適。ratio用 mask 像素?cái)?shù)除以總像素?cái)?shù)低于 0.1 說(shuō)明舌頭在畫(huà)面里太小特征提取會(huì)不準(zhǔn)。這三個(gè)檢查放在診斷接口最前面不通過(guò)就直接返回省得白跑模型。我自己做這個(gè)系統(tǒng)時(shí)最大的教訓(xùn)是別一上來(lái)就堆深度學(xué)習(xí)。先用 SVM 跑通全流程把前后端和數(shù)據(jù)庫(kù)串起來(lái)再考慮換模型。很多畢設(shè)翻車(chē)不是因?yàn)槟P筒粔驈?qiáng)而是因?yàn)榻涌谡{(diào)不通、數(shù)據(jù)存不進(jìn)、頁(yè)面刷不出來(lái)。把工程鏈路走通比追求那兩三個(gè)點(diǎn)的準(zhǔn)確率重要得多。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取