習(xí)的舌苔識(shí)別系統(tǒng):從源碼到畢業(yè)設(shè)計(jì)全流程)
簡(jiǎn)介本資源為基于深度學(xué)習(xí)的舌苔識(shí)別檢測(cè)鑒定系統(tǒng)完整項(xiàng)目包面向計(jì)算機(jī)相關(guān)專業(yè)正在做畢業(yè)設(shè)計(jì)的學(xué)生、需要項(xiàng)目實(shí)戰(zhàn)練習(xí)的學(xué)習(xí)者也可作為課程設(shè)計(jì)或期末大作業(yè)參考。項(xiàng)目經(jīng)導(dǎo)師指導(dǎo)并認(rèn)可通過評(píng)審分98分涵蓋舌苔檢測(cè)、體質(zhì)辨識(shí)等核心功能配套PyQt5圖形界面與訓(xùn)練好的模型可直接運(yùn)行演示。壓縮包整體約105.47MB內(nèi)含Python源碼、PyQt5界面文件、模型權(quán)重及畢業(yè)論文文檔源碼負(fù)責(zé)算法與界面邏輯論文則完整梳理課題背景、機(jī)器學(xué)習(xí)理論、需求分析與舌象數(shù)據(jù)集構(gòu)建等章節(jié)便于對(duì)照理解系統(tǒng)設(shè)計(jì)思路。目前已有93人學(xué)習(xí)關(guān)注。讀者可借此獲得一套結(jié)構(gòu)清晰的畢設(shè)方案掌握從數(shù)據(jù)集構(gòu)建、卷積神經(jīng)網(wǎng)絡(luò)訓(xùn)練到GUI集成的完整流程并參考論文中的可行性分析與功能模塊劃分快速完成自己的項(xiàng)目搭建與答辯準(zhǔn)備。1. 舌苔識(shí)別系統(tǒng)到底在做什么從一張手機(jī)照片到一份體質(zhì)報(bào)告很多人第一次聽到「舌苔識(shí)別檢測(cè)鑒定系統(tǒng)」會(huì)以為是個(gè)噱頭其實(shí)它解決的是一個(gè)很具體的問題中醫(yī)舌診依賴經(jīng)驗(yàn)同一個(gè)舌頭不同醫(yī)生看結(jié)論可能不一樣。把這件事交給深度學(xué)習(xí)模型核心目標(biāo)不是替代醫(yī)生而是把「舌質(zhì)顏色、舌苔厚薄、苔色黃白、齒痕有無」這幾個(gè)可量化的維度標(biāo)準(zhǔn)化。你手上如果有一份 Python 源碼、一個(gè) PyQt5 界面、一個(gè)訓(xùn)練好的模型權(quán)重再加上一篇畢業(yè)論文那這套東西的完整鏈路就是圖像采集 → 預(yù)處理 → 分類/分割 → 結(jié)果展示 → 報(bào)告導(dǎo)出。適合誰適合做畢設(shè)的本科生、想入門醫(yī)學(xué)圖像分類的算法新手、以及需要快速搭一個(gè)桌面端 AI 演示工具的產(chǎn)品驗(yàn)證者。熱搜里「python」「pyqt5」「深度學(xué)習(xí)」「舌苔識(shí)別」「源碼」這幾個(gè)詞恰好對(duì)應(yīng)了這套系統(tǒng)的四層語言、界面、算法、數(shù)據(jù)。下面我按實(shí)際能跑起來的順序把每一層拆開講。2. 環(huán)境與數(shù)據(jù)把「python安裝」到「舌苔數(shù)據(jù)集」這條鏈路先打通2.1 為什么選 PyQt5 PyTorch 而不是 Web 方案做桌面端舌苔識(shí)別最常見的翻車點(diǎn)是選了 Web 框架結(jié)果攝像頭調(diào)用、本地文件批量處理、離線演示三件事互相打架。PyQt5 的優(yōu)勢(shì)在于它是真正的本地窗口程序QCamera 或 OpenCV 直接讀攝像頭QFileDialog 批量選圖推理結(jié)果用 QLabel 貼圖整個(gè)鏈路不依賴瀏覽器權(quán)限。深度學(xué)習(xí)框架這邊PyTorch 比 TensorFlow 更適合畢設(shè)場(chǎng)景動(dòng)態(tài)圖調(diào)試直觀torchvision里 ResNet、EfficientNet 的預(yù)訓(xùn)練權(quán)重直接能加載改幾行就能做遷移學(xué)習(xí)。熱搜里「pyqt5安裝」「python安裝numpy庫的方法」之所以高頻就是因?yàn)檫@兩步卡住的人最多。我一般會(huì)建議用 conda 建獨(dú)立環(huán)境避免和系統(tǒng) Python 混在一起。conda create -n tongue python3.8 -y conda activate tongue pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python pillow numpy pyqt5邏輯說明Python 3.8 是 PyQt5 和多數(shù)預(yù)訓(xùn)練模型兼容性最穩(wěn)的版本不建議上 3.11部分舊版 torchvision 會(huì)報(bào)DLL load failed。--index-url指定 CPU 版輪子如果你有 NVIDIA 顯卡換成對(duì)應(yīng) CUDA 版本的命令即可。參數(shù)說明opencv-python負(fù)責(zé)圖像讀取和顏色空間轉(zhuǎn)換pillow用于 PyQt5 顯示時(shí)的格式橋接numpy是所有矩陣運(yùn)算的底座。裝完后用python -c import cv2, torch; print(torch.__version__)驗(yàn)證能打印版本號(hào)才算通。2.2 舌苔數(shù)據(jù)集的采集與標(biāo)注口徑舌苔識(shí)別不是通用圖像分類數(shù)據(jù)集質(zhì)量決定上限。常見做法是分兩類任務(wù)一是舌體分割把舌頭從嘴唇、牙齒、背景里摳出來二是舌苔屬性分類薄白、厚白、薄黃、厚黃、剝落等。如果你拿不到醫(yī)院級(jí)數(shù)據(jù)可以用公開的中醫(yī)舌象數(shù)據(jù)集做預(yù)訓(xùn)練再用自己的手機(jī)拍攝數(shù)據(jù)微調(diào)。拍攝時(shí)固定三個(gè)條件自然光或環(huán)形補(bǔ)光燈、舌頭自然伸出不超過 5 秒、鏡頭距離 20–30 cm。標(biāo)注用 LabelImg 或 LabelMe分割任務(wù)存 PNG 掩碼分類任務(wù)按文件夾分目錄。import os, cv2, numpy as np from sklearn.model_selection import train_test_split def load_dataset(root): images, labels [], [] classes sorted(os.listdir(root)) for idx, cls in enumerate(classes): cls_dir os.path.join(root, cls) for fname in os.listdir(cls_dir): img cv2.imread(os.path.join(cls_dir, fname)) img cv2.resize(img, (224, 224)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) images.append(img) labels.append(idx) return np.array(images), np.array(labels), classes X, y, class_names load_dataset(data/tongue_coat) X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, stratifyy, random_state42) print(class_names, X_train.shape, X_val.shape)邏輯說明sorted(os.listdir)保證類別順序固定否則訓(xùn)練和推理時(shí)標(biāo)簽對(duì)不上這是血淚經(jīng)驗(yàn)。stratifyy讓訓(xùn)練集和驗(yàn)證集類別比例一致舌苔數(shù)據(jù)往往黃苔樣本少不分層會(huì)導(dǎo)致驗(yàn)證集里沒有黃苔。參數(shù)說明輸入尺寸 224×224 是 ResNet 系列的標(biāo)準(zhǔn)輸入如果你用 EfficientNet-B0 也是 224用 B4 就要改 380。random_state42固定劃分方便復(fù)現(xiàn)。跑完打印出類別名和形狀確認(rèn)沒有讀成空數(shù)組。3. 模型訓(xùn)練從遷移學(xué)習(xí)到舌苔分類的四個(gè)必調(diào)參數(shù)3.1 用 ResNet18 做遷移學(xué)習(xí)的最小訓(xùn)練腳本舌苔識(shí)別數(shù)據(jù)量通常不大幾千張就算多的從零訓(xùn)練 CNN 必然過擬合。遷移學(xué)習(xí)是標(biāo)準(zhǔn)解法加載 ImageNet 預(yù)訓(xùn)練權(quán)重替換最后一層全連接先凍結(jié)主干訓(xùn)分類頭再解凍后幾層微調(diào)。熱搜里「深度學(xué)習(xí)cnn」「動(dòng)手深度學(xué)習(xí)」對(duì)應(yīng)的就是這一塊。我一般用 ResNet18 起步參數(shù)量 11MCPU 也能跑畢設(shè)演示足夠。import torch, torch.nn as nn, torch.optim as optim from torch.utils.data import DataLoader, TensorDataset from torchvision import models, transforms transform transforms.Compose([ transforms.ToPILImage(), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds TensorDataset(torch.stack([transform(x) for x in X_train]), torch.tensor(y_train)) val_ds TensorDataset(torch.stack([transform(x) for x in X_val]), torch.tensor(y_val)) train_loader DataLoader(train_ds, batch_size32, shuffleTrue) val_loader DataLoader(val_ds, batch_size32) model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) for p in model.parameters(): p.requires_grad False model.fc nn.Linear(512, len(class_names)) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.fc.parameters(), lr1e-3) for epoch in range(15): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() optimizer.step() model.eval() correct total 0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb).argmax(1) correct (pred yb).sum().item() total yb.size(0) print(fepoch {epoch1} val_acc {correct/total:.4f})邏輯說明先凍結(jié)全部參數(shù)只訓(xùn)fc是因?yàn)殡S機(jī)初始化的分類頭梯度太大會(huì)破壞預(yù)訓(xùn)練特征。跑 5 個(gè) epoch 后如果驗(yàn)證準(zhǔn)確率上不去再解凍layer4用更小學(xué)習(xí)率微調(diào)。參數(shù)說明batch_size32在 8GB 內(nèi)存的機(jī)器上安全顯存不夠就降到 16。lr1e-3是分類頭專用微調(diào)階段要降到 1e-4。RandomRotation(10)模擬拍攝角度偏差ColorJitter模擬不同光源這兩個(gè)增強(qiáng)對(duì)舌苔顏色分類特別關(guān)鍵但亮度抖動(dòng)別超過 0.3否則黃苔白苔會(huì)被混淆。3.2 舌苔分類的四個(gè)必調(diào)參數(shù)與驗(yàn)證指標(biāo)訓(xùn)練舌苔模型有四個(gè)參數(shù)你不動(dòng)它準(zhǔn)確率就是上不去。第一個(gè)是輸入分辨率224 對(duì)舌體整體夠用但如果你要分「薄厚」這種紋理細(xì)節(jié)建議 320 或 384。第二個(gè)是類別權(quán)重黃苔樣本少就設(shè)weighttorch.tensor([1.0, 1.0, 2.5, 2.5])傳給 CrossEntropyLoss。第三個(gè)是學(xué)習(xí)率調(diào)度用CosineAnnealingLR比固定 lr 穩(wěn)。第四個(gè)是早停 patience驗(yàn)證損失連續(xù) 5 輪不降就停省時(shí)間。參數(shù)推薦值作用調(diào)錯(cuò)后果輸入尺寸224 / 320控制紋理細(xì)節(jié)太小分不清薄厚類別權(quán)重按樣本反比緩解不平衡少數(shù)類全錯(cuò)初始 lr1e-3頭/1e-4微調(diào)收斂速度太大震蕩太小不動(dòng)早停 patience5防過擬合訓(xùn)到過擬合才停驗(yàn)證指標(biāo)別只看 accuracy。舌苔識(shí)別里「厚苔漏判」比「薄苔誤判」嚴(yán)重所以要看每類的 recall 和混淆矩陣。用sklearn.metrics.classification_report打印重點(diǎn)看黃苔那一行的 recall。如果黃苔 recall 低于 0.7先查數(shù)據(jù)增強(qiáng)是不是把黃色洗掉了再查類別權(quán)重。4. PyQt5 界面把模型推理塞進(jìn)桌面窗口的三個(gè)關(guān)鍵點(diǎn)4.1 界面布局與信號(hào)槽的最小可用結(jié)構(gòu)PyQt5 做 AI 演示界面核心就三塊左邊顯示原圖右邊顯示結(jié)果和置信度底部一排按鈕打開圖片、開始識(shí)別、保存報(bào)告。熱搜里「pyqt5教程」「pyqt5顯示html」說明很多人卡在控件聯(lián)動(dòng)上。記住一個(gè)原則推理必須放在 QThread 里不能直接寫在按鈕的槽函數(shù)里否則界面會(huì)假死這是最常見的翻車點(diǎn)。from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QFileDialog from PyQt5.QtGui import QPixmap, QImage from PyQt5.QtCore import QThread, pyqtSignal import cv2, numpy as np, sys class InferThread(QThread): finished pyqtSignal(str, float) def __init__(self, img_path, model, class_names): super().__init__() self.img_path, self.model, self.class_names img_path, model, class_names def run(self): img cv2.imread(self.img_path) img cv2.resize(img, (224, 224)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) / 255.0 tensor torch.tensor(img).permute(2,0,1).unsqueeze(0).float() with torch.no_grad(): prob torch.softmax(self.model(tensor), 1) conf, idx prob.max(1) self.finished.emit(self.class_names[idx.item()], conf.item()) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(舌苔識(shí)別檢測(cè)系統(tǒng)) self.img_label QLabel(請(qǐng)選擇舌象圖片) self.result_label QLabel(等待識(shí)別) btn_open QPushButton(打開圖片) btn_run QPushButton(開始識(shí)別) btn_open.clicked.connect(self.open_image) btn_run.clicked.connect(self.run_infer) layout QVBoxLayout() layout.addWidget(self.img_label) layout.addWidget(self.result_label) h QHBoxLayout(); h.addWidget(btn_open); h.addWidget(btn_run) layout.addLayout(h) container QWidget(); container.setLayout(layout) self.setCentralWidget(container) self.img_path None def open_image(self): self.img_path, _ QFileDialog.getOpenFileName(self, 選擇圖片, , Images (*.png *.jpg *.jpeg)) if self.img_path: pix QPixmap(self.img_path).scaled(400, 400) self.img_label.setPixmap(pix) def run_infer(self): if not self.img_path: return self.thread InferThread(self.img_path, model, class_names) self.thread.finished.connect(lambda name, conf: self.result_label.setText(f結(jié)果{name} 置信度{conf:.2%})) self.thread.start() app QApplication(sys.argv) win MainWindow(); win.show() sys.exit(app.exec_())邏輯說明InferThread繼承 QThread把推理放在run里通過pyqtSignal把結(jié)果傳回主線程更新 UI這樣界面不會(huì)卡。finished信號(hào)帶兩個(gè)參數(shù)槽函數(shù)用 lambda 接收。參數(shù)說明scaled(400, 400)保持顯示比例實(shí)際項(xiàng)目里應(yīng)該用Qt.KeepAspectRatio。permute(2,0,1)把 HWC 轉(zhuǎn) CHWunsqueeze(0)加 batch 維度這兩步順序錯(cuò)了模型直接報(bào)維度錯(cuò)誤。torch.softmax把 logits 轉(zhuǎn)概率置信度低于 0.6 時(shí)建議界面提示「建議重新拍攝」。4.2 圖像預(yù)處理與界面顯示的格式橋接OpenCV 讀進(jìn)來是 BGRPyQt5 的 QImage 要 RGB模型訓(xùn)練時(shí)又是 RGB 歸一化三個(gè)色彩空間來回轉(zhuǎn)不統(tǒng)一就會(huì)出現(xiàn)「訓(xùn)練準(zhǔn)、界面錯(cuò)」的玄學(xué)問題。我一般寫一個(gè)統(tǒng)一函數(shù)所有入口都走它。另外舌體分割如果單獨(dú)做了界面要顯示掩碼疊加圖用cv2.addWeighted把原圖和掩碼按 0.7:0.3 混合。def preprocess_for_model(img_path): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (224, 224)) img img.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img (img - mean) / std return torch.tensor(img).permute(2,0,1).unsqueeze(0).float() def cvimg_to_qpixmap(cv_img): rgb cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) return QPixmap.fromImage(qimg)邏輯說明preprocess_for_model里的 mean/std 必須和訓(xùn)練時(shí)transforms.Normalize完全一致不一致模型輸出會(huì)整體偏移。cvimg_to_qpixmap里ch * w是每行字節(jié)數(shù)寫錯(cuò)會(huì)花屏。參數(shù)說明如果你的模型訓(xùn)練時(shí)沒做 Normalize這里也要去掉否則顏色全變。分割掩碼疊加時(shí)掩碼要先轉(zhuǎn)成三通道再addWeighted。5. 避坑與排查舌苔識(shí)別系統(tǒng)最常見的五條血淚記錄5.1 模型訓(xùn)練準(zhǔn)確率 99% 但界面識(shí)別全錯(cuò)現(xiàn)象訓(xùn)練腳本打印 val_acc 0.99PyQt5 界面里同一張圖識(shí)別成隨機(jī)類別。原因訓(xùn)練時(shí)用了transforms.Normalize界面推理時(shí)只做了/255.0沒減均值除標(biāo)準(zhǔn)差。解決把預(yù)處理封裝成一個(gè)函數(shù)訓(xùn)練和推理共用別寫兩份。這是最高頻的翻車沒有之一。5.2 攝像頭拍出來偏藍(lán)黃苔被判成白苔現(xiàn)象白天自然光下識(shí)別正常晚上開臺(tái)燈就全錯(cuò)。原因白平衡沒鎖不同色溫下舌苔顏色偏移模型沒見過這種分布。解決采集數(shù)據(jù)時(shí)就要覆蓋多種光源訓(xùn)練時(shí)加ColorJitter推理端加一個(gè)簡(jiǎn)單白平衡校正用灰度世界算法把 RGB 均值拉平。5.3 PyQt5 點(diǎn)擊識(shí)別按鈕后界面卡死現(xiàn)象按鈕按下去窗口無響應(yīng)幾秒后才出結(jié)果。原因推理寫在主線程槽函數(shù)里阻塞了 Qt 事件循環(huán)。解決必須用 QThread 或 QThreadPool把模型推理放子線程通過信號(hào)回傳結(jié)果。注意模型對(duì)象不要跨線程共享每個(gè)線程加載一份或加鎖。5.4 打包成 exe 后模型文件找不到現(xiàn)象PyCharm 里跑得好好的PyInstaller 打包后報(bào)FileNotFoundError: model.pth。原因打包后工作目錄變了相對(duì)路徑失效。解決用sys._MEIPASS判斷是否打包環(huán)境把模型路徑拼成絕對(duì)路徑或者用--add-data把模型文件打進(jìn)去。5.5 舌體分割掩碼邊緣有鋸齒影響分類現(xiàn)象分割出來的舌頭邊緣毛糙分類模型把嘴唇像素也學(xué)進(jìn)去了。原因分割網(wǎng)絡(luò)輸出是低分辨率直接上采樣沒做后處理。解決分割后做一次形態(tài)學(xué)開運(yùn)算去噪再用cv2.GaussianBlur平滑邊緣最后按掩碼裁剪再送分類模型。別小看這一步邊緣干凈后分類準(zhǔn)確率能漲 3–5 個(gè)點(diǎn)。6. 進(jìn)階技巧用 Grad-CAM 讓舌苔識(shí)別結(jié)果可解釋畢設(shè)答辯時(shí)老師最愛問的一句話是「模型憑什么這么判」。你光給一個(gè)置信度不夠得讓評(píng)委看到模型關(guān)注的是舌苔區(qū)域而不是背景。Grad-CAM 是最容易落地的可解釋性方法原理是拿目標(biāo)類別的梯度對(duì)最后一個(gè)卷積層的特征圖加權(quán)生成熱力圖疊加在原圖上。熱搜里「深度學(xué)習(xí)l2正則化pytorch代碼」這類詞說明大家關(guān)心訓(xùn)練細(xì)節(jié)但答辯現(xiàn)場(chǎng)一張熱力圖比十行正則化代碼更有說服力。import torch.nn.functional as F class GradCAM: def __init__(self, model, target_layer): self.model model self.gradients None self.activations None target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, input_tensor, class_idxNone): output self.model(input_tensor) if class_idx is None: class_idx output.argmax(1).item() self.model.zero_grad() output[0, class_idx].backward() weights self.gradients.mean(dim(2, 3), keepdimTrue) cam F.relu((weights * self.activations).sum(1, keepdimTrue)) cam F.interpolate(cam, sizeinput_tensor.shape[2:], modebilinear, align_cornersFalse) cam cam.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) return cam cam_gen GradCAM(model, model.layer4[-1]) heatmap cam_gen.generate(preprocess_for_model(test.jpg))邏輯說明register_forward_hook抓前向特征圖register_full_backward_hook抓反向梯度weights是梯度在空間維度上的均值代表每個(gè)通道對(duì)目標(biāo)類的重要程度。F.relu只保留正向貢獻(xiàn)F.interpolate把熱力圖放大回原圖尺寸。參數(shù)說明target_layer選model.layer4[-1]是 ResNet 的最后一個(gè)殘差塊選太淺熱力圖會(huì)糊選太深分辨率不夠。生成后把熱力圖用cv2.applyColorMap轉(zhuǎn)成偽彩色再和原圖addWeighted疊加存成報(bào)告插圖。我自己的習(xí)慣是每次改完模型結(jié)構(gòu)或預(yù)處理先跑一張固定測(cè)試圖看熱力圖有沒有落在舌體上如果熱區(qū)跑到背景或牙齒上說明模型學(xué)偏了這時(shí)候調(diào)數(shù)據(jù)比調(diào)參有用。舌苔識(shí)別這個(gè)方向數(shù)據(jù)質(zhì)量占七成模型結(jié)構(gòu)占兩成界面和報(bào)告占一成。別在界面上花太多時(shí)間把預(yù)處理和數(shù)據(jù)集做扎實(shí)結(jié)果自然穩(wěn)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取