简介:本资源是一套基于Python与深度学习技术实现的舌苔图像智能检测系统,面向计算机、人工智能、生物医学工程等专业的高校学生及科研人员,适用于毕业设计、课程实践与医学图像分析入门学习。项目包含完整可运行源码、UI交互界面、训练模型(.pth文件)、标注数据集及详细设计文档,支持端到端的舌象采集、预处理、特征提取与分类识别全流程。压缩包共109个文件,主体为26个核心Python脚本(含训练/推理/界面逻辑)、6个预训练模型文件、7张示例舌苔图像(.jpg)、2个Qt Designer生成的.ui界面文件,以及日志文件(.tfevents)和配置文件(.json/.md),整体大小105.04MB。目前已有67人下载学习,内容结构清晰、注释充分,附带TensorFlow/PyTorch训练过程日志,便于理解模型收敛性与调参思路,亦可作为医学AI方向的二次开发基础框架。
1. 舌苔不是“拍张照就能认出来”的黑匣子:为什么用Python做舌象识别,得先过数据、标注、UI三道坎?
你手头拿到一个叫“基于Python机器学习的舌苔检测系统(论文+源码+UI界面).zip”的压缩包,双击解压后看到main.py、model/、data/和ui/四个文件夹——别急着python main.py,这大概率会报错。真实项目里,舌苔检测根本不是“输入一张舌头照片→输出‘厚腻’或‘薄白’”这么干净的流程。它卡在三个地方:第一,临床舌象图极度不均衡——90%是室内白光下正脸平铺舌体,剩下10%是手机随手拍、侧光、反光、带牙印、有唾液反光点;第二,标注不是打个框就行,中医舌诊要求同时标出苔色(淡黄/焦黄/灰黑)、苔质(腐腻/滑润/燥裂)、苔布(满布/偏布/剥落),且不同医师标注一致性只有62%(《中国中医药信息杂志》2023年舌象标注信度报告);第三,UI界面不是PyQt拖几个按钮就完事,当用户用iPhone前置摄像头拍舌面,图像自动旋转90°、曝光过曝、边缘畸变,而你的模型训练时喂的全是校准过的标准图——这时候UI不是加分项,是灾难放大器。这个项目真正价值不在“能跑通”,而在它把舌象数据清洗 pipeline、多标签分类建模策略、以及面向真实终端设备的图像预处理嵌入UI层这三件事串成了一条可复现链路。适合正在做中医AI毕设、医院信息化科室想落地舌诊辅助模块、或者被“UI卡顿”“模型在测试集准、在手机上全错”问题反复折磨的工程师。
2. 从舌象图到特征向量:为什么不用ResNet直接finetune,而要自己搭CNN+Attention双支结构?
舌苔识别不是通用图像分类,不能简单套用ImageNet预训练模型。原因很实在:ResNet学到的“纹理”是毛衣纹、砖墙纹、豹斑纹,而舌苔的“腐腻感”是微米级菌膜堆叠+角化细胞脱落形成的亚像素级明暗跳变,传统CNN感受野太大,会把舌体边缘血管伪影当成关键特征。我们拆解原项目model/tongue_cnn_att.py里的核心设计,它用的是双支异构网络:一支走轻量CNN提取宏观区域特征(舌中/舌根/舌边分区),另一支走频域变换+通道注意力聚焦微观质地(对HSV空间的S通道做DCT变换,再用SE Block加权高频系数)。这种设计不是炫技,而是被数据逼出来的。
2.1 数据增强必须带“中医物理仿真”,不是加高斯噪声那么简单
原项目data/augment.py里最关键的不是RandomRotation,而是TongueLightSimulator类——它模拟三种典型误拍场景:
# data/augment.py class TongueLightSimulator: def __init__(self, light_type='side'): # side: 模拟手机侧光拍摄导致的半边过曝 # gloss: 模拟唾液反光形成的局部镜面高光 # shadow: 模拟舌体卷曲造成的根部阴影 self.light_type = light_type def __call__(self, img): if self.light_type == 'side': mask = np.zeros(img.shape[:2], dtype=np.float32) cv2.ellipse(mask, (img.shape[1]//4, img.shape[0]//2), (img.shape[1]//3, img.shape[0]//2), 0, 0, 360, 1, -1) img = cv2.addWeighted(img, 0.7, (mask[:,:,None] * [255,255,255]).astype(np.uint8), 0.3, 0) return img提示:这段代码的
cv2.ellipse参数不是随便写的。center=(img.shape[1]//4, img.shape[0]//2)把高光区故意偏移到舌体左侧——因为临床统计显示73%的非专业拍摄者习惯右手持机,自然形成左亮右暗。如果你直接用RandomBrightness,模型只会学“整体变亮=厚苔”,而真实场景里是“局部过曝+周边发暗=湿热证”。
2.2 标签体系必须解耦“苔色”“苔质”“苔布”,不能塞进一个softmax
原项目dataset/tongue_dataset.py把标签存成字典而非整数:
# dataset/tongue_dataset.py label_dict = { 'color': {'pale_yellow': 0, 'deep_yellow': 1, 'gray_black': 2}, 'texture': {'rotten': 0, 'greasy': 1, 'moist': 2, 'dry': 3}, 'distribution': {'full': 0, 'partial': 1, 'peeled': 2} } # 加载时返回三元组 return img, (color_idx, texture_idx, dist_idx)模型输出层对应三个独立全连接头,损失函数用加权多任务学习:
# model/tongue_cnn_att.py def multi_task_loss(pred_color, pred_texture, pred_dist, true_color, true_texture, true_dist): loss_color = F.cross_entropy(pred_color, true_color) loss_texture = F.cross_entropy(pred_texture, true_texture) loss_dist = F.cross_entropy(pred_dist, true_dist) # 权重按临床诊断权重设:苔质>苔色>苔布 return 0.4*loss_color + 0.45*loss_texture + 0.15*loss_dist参数说明:权重
0.45不是拍脑袋定的。翻原项目附的《舌诊专家共识(2022版)》第3.2条:“苔质润燥直接反映津液盈亏,为辨证首要依据”,所以给最高权重。如果你强行合并成单标签(如'pale_yellow_rotten_full'),类别数会爆炸(3×4×3=36类),而你的数据集才427张图——小样本下模型必然过拟合到“某张图的背景瓷砖纹路”。
3. UI不是摆设:为什么PyQt5主窗口要嵌入OpenCV实时流,而不是用QLabel加载静态图?
很多同学拿到源码后发现ui/main_window.py里有个QGraphicsView控件,但main.py启动后UI卡死。问题不在代码,而在没理解这个UI的设计哲学:它本质是个“舌象采集-分析-反馈”闭环终端,不是演示demo。原项目把OpenCV视频流直接喂进QGraphicsView的QGraphicsScene,每帧做三件事:1)用cv2.undistort()校正手机镜头畸变;2)调用cv2.createCLAHE()增强舌面纹理对比度;3)在GPU空闲时异步调用模型推理。这才是解决“UI卡顿”的正解——不是优化PyQt渲染,而是把耗时计算从GUI线程剥离。
3.1 手机直连方案:用VLC推流替代USB调试,绕过Android权限墙
原项目ui/camera_handler.py支持两种模式:
# ui/camera_handler.py class CameraHandler: def __init__(self, mode='usb'): # mode: 'usb' or 'rtsp' if mode == 'rtsp': # 手机端用VLC播放器开启RTSP服务(设置→工具→偏好→全部→串流→启用RTSP) self.cap = cv2.VideoCapture('rtsp://192.168.1.100:8554/') else: self.cap = cv2.VideoCapture(0) # 笔记本内置摄像头 def get_frame(self): ret, frame = self.cap.read() if not ret: return None # 关键:只在此处做畸变校正,避免重复计算 if hasattr(self, 'mtx') and hasattr(self, 'dist'): frame = cv2.undistort(frame, self.mtx, self.dist, None, self.new_mtx) return frame逻辑说明:
rtsp模式比usb模式更可靠。Android 12+对USB摄像头访问加了严格权限,而VLC RTSP服务只需打开WiFi并允许局域网访问,实测延迟<300ms。self.mtx和self.dist是预先用calibrate_camera.py标定好的相机内参——这个文件在utils/目录下,必须用项目提供的calibration_pattern.jpg(带舌形轮廓的棋盘格)打印出来贴在硬板上,从5个不同角度拍照生成,不能用OpenCV默认棋盘格。
3.2 实时反馈机制:用QPainter在QGraphicsView上画动态舌象热力图
原项目ui/visualizer.py没用matplotlib,而是用PyQt原生绘图:
# ui/visualizer.py def draw_heatmap(self, scene, heatmap_array): # heatmap_array shape: (H, W, 1),值范围0~1 h, w = heatmap_array.shape[:2] # 转成QImage(注意BGR->RGB转换) img_rgb = (heatmap_array * 255).astype(np.uint8) qimg = QImage(img_rgb.data, w, h, w, QImage.Format_Grayscale8) pixmap = QPixmap.fromImage(qimg) # 叠加到原图上,透明度30% item = QGraphicsPixmapItem(pixmap) item.setOpacity(0.3) scene.addItem(item)参数说明:
setOpacity(0.3)是经验值。低于0.2看不清,高于0.4会遮盖舌体真实颜色。热力图不是模型原始输出,而是对CNN最后一层特征图做Grad-CAM生成的——这部分代码在model/gradcam.py,它用torch.autograd.grad计算梯度,比直接取feature map更准。你如果删掉这个可视化,UI就退化成“拍照→等待→弹窗结果”,失去中医“望闻问切”中“望”的交互感。
4. 模型部署不是copy-paste:为什么要把PyTorch模型转ONNX再用ONNX Runtime推理?
原项目model/export_onnx.py里有一段看似多余的转换:
# model/export_onnx.py torch.onnx.export( model, dummy_input, "tongue_model.onnx", input_names=["input"], output_names=["color", "texture", "distribution"], dynamic_axes={ "input": {0: "batch_size"}, "color": {0: "batch_size"}, "texture": {0: "batch_size"}, "distribution": {0: "batch_size"} }, opset_version=12 )这不是为了装X,而是解决Windows平台PyTorch GPU推理的三大坑:1)PyTorch 1.12+在某些NVIDIA驱动版本下cuda.is_available()返回True但实际推理崩溃;2)打包成exe后,PyTorch动态链接库(cudnn64_8.dll等)路径错乱;3)模型加载耗时>2s,UI线程卡死。ONNX Runtime用C++写,体积小(仅12MB)、启动快(<200ms)、跨平台稳定。
4.1 ONNX模型必须做shape infer,否则PyQt里会报“tensor size mismatch”
原项目model/inference.py关键检查:
# model/inference.py import onnxruntime as ort session = ort.InferenceSession("tongue_model.onnx") # 必须显式获取输入shape,不能假设是(1,3,224,224) input_shape = session.get_inputs()[0].shape print(f"Model expects input shape: {input_shape}") # 输出: [1, 3, 256, 256] # 后续resize必须严格匹配 img_resized = cv2.resize(img, (input_shape[3], input_shape[2]))血泪经验:我第一次部署时没做这步,直接用
cv2.resize(img, (224,224)),结果模型输出color维度是[1,3]但texture是[1,4]——因为ONNX导出时dynamic_axes没对齐,导致不同输出分支的batch维度解析错乱。get_inputs()[0].shape是唯一可信来源。
4.2 多线程推理必须用ORT的SessionOptions,不能靠Python threading
原项目ui/inference_worker.py:
# ui/inference_worker.py class InferenceWorker(QObject): result_ready = pyqtSignal(dict) def __init__(self, model_path): super().__init__() self.model_path = model_path # 关键:设置intra_op_num_threads=1,避免线程竞争 self.sess_options = ort.SessionOptions() self.sess_options.intra_op_num_threads = 1 self.sess_options.inter_op_num_threads = 1 def run(self): session = ort.InferenceSession(self.model_path, self.sess_options) # 推理代码...避坑原理:ONNX Runtime默认用所有CPU核心并行算一个op(如MatMul),但在PyQt多线程环境下,多个Session实例会抢同一块内存缓存,导致
Access violation。设intra_op_num_threads=1强制单核运算,用QThreadPool管理多个Worker实例,才是安全方案。
5. 避坑指南:那些让舌苔检测系统在验收现场集体翻车的5个真实问题
现象、原因、解决方案必须一一对应,不讲虚的。
5.1 现象:UI界面点击“开始检测”后无响应,任务管理器显示Python进程CPU 100%、内存缓慢上涨
原因:ui/camera_handler.py里cv2.VideoCapture未设置缓冲区清空,手机RTSP流持续写入但未读取,OpenCV内部队列溢出。
解决:在get_frame()开头加强制清空:
# ui/camera_handler.py def get_frame(self): # 新增:清空缓冲区,防止积压 for _ in range(5): # 清5帧,足够应对网络抖动 self.cap.grab() ret, frame = self.cap.read() # ...后续代码5.2 现象:模型在验证集准确率92%,但用iPhone拍的真实舌图全部判错
原因:训练时用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转色,但iPhone相册图是sRGB色彩空间,OpenCV默认读取为BGR但未做gamma校正。
解决:在dataset/tongue_dataset.py的__getitem__里加色彩空间适配:
# dataset/tongue_dataset.py def __getitem__(self, idx): img = cv2.imread(self.img_paths[idx]) # 新增:iPhone图需先转sRGB再转RGB if 'iphone' in self.img_paths[idx].lower(): img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = np.clip(img ** (1/2.2) * 255, 0, 255).astype(np.uint8) else: img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # ...后续预处理5.3 现象:PyInstaller打包后exe双击闪退,日志显示ImportError: DLL load failed while importing cv2
原因:OpenCV的DLL依赖(如opencv_world455.dll)未被PyInstaller自动收集,尤其opencv-contrib-python的额外模块。
解决:用--add-binary手动指定路径:
pyinstaller --onefile --add-binary "C:\Python39\Lib\site-packages\cv2\opencv_videoio_ffmpeg455_64.dll;." main.py注意:DLL路径需根据你的OpenCV版本调整,用
pip show opencv-python查版本,再进site-packages/cv2/目录找对应DLL。
5.4 现象:舌苔“剥落”类别召回率始终低于30%,混淆矩阵显示大量被分到“薄白”
原因:数据集中“剥落苔”样本只有17张,且全为老年患者舌象(舌体瘦小、颜色偏红),模型学到的是“瘦小+偏红→剥落”,而非苔质特征。
解决:在data/augment.py里为剥落类加针对性增强:
# data/augment.py if label['distribution'] == 'peeled': # 强制添加舌体边缘模糊(模拟老年舌肌萎缩) kernel = np.ones((3,3), np.float32) / 9 img = cv2.filter2D(img, -1, kernel) # 降低饱和度,突出剥落区苍白感 hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) hsv[:,:,1] = hsv[:,:,1] * 0.7 img = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)5.5 现象:UI界面上舌象热力图位置偏移,总往右下方偏15像素
原因:ui/visualizer.py里QGraphicsPixmapItem的坐标原点默认在左上角,但QGraphicsView的sceneRect未重置,导致叠加时基准点错位。
解决:在UI初始化时重置scene坐标系:
# ui/main_window.py self.graphics_view.setScene(QGraphicsScene()) # 新增:强制scene原点在(0,0),宽高匹配view self.graphics_view.scene().setSceneRect(0, 0, self.graphics_view.width(), self.graphics_view.height())6. 把“舌苔检测”变成可交付产品:三个必须动手验证的硬指标与我的落地习惯
做完以上所有步骤,你得到的还是个实验室玩具。要让它真正在社区卫生站或中医馆用起来,必须验证三个硬指标,每个都对应一个可执行命令或脚本。别信“准确率95%”这种虚数,要看它在真实约束下的表现。
6.1 验证“端到端延迟”:从点击拍摄到结果显示,必须≤1.2秒
这是UI体验生死线。原项目utils/benchmark_latency.py提供测量脚本:
# utils/benchmark_latency.py import time from ui.inference_worker import InferenceWorker worker = InferenceWorker("model/tongue_model.onnx") # 预热 worker.run() latencies = [] for _ in range(20): start = time.time() # 模拟一帧舌象图输入(用data/test_sample.jpg) img = cv2.imread("data/test_sample.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) result = worker.infer(img) # 注意:infer()是worker的推理方法 latencies.append(time.time() - start) print(f"Mean latency: {np.mean(latencies)*1000:.1f}ms") print(f"P95 latency: {np.percentile(latencies, 95)*1000:.1f}ms")我的习惯:P95必须≤1200ms。如果超标,优先砍模型宽度(把CNN支路的channel数从64降到32),而不是降分辨率——舌苔细节在256×256已到极限,再小就丢失“裂纹”“剥落”等关键征。
6.2 验证“跨设备鲁棒性”:用5台不同品牌手机各拍10张舌图,准确率波动不能超±3%
原项目utils/test_cross_device.py生成对比报告:
# utils/test_cross_device.py devices = ['iphone13', 'huawei_p50', 'xiaomi_12', 'oppo_findx5', 'samsung_s22'] results = {} for device in devices: # 从data/device_test/{device}/目录加载该设备拍摄的10张图 device_imgs = glob(f"data/device_test/{device}/*.jpg") acc = test_model_on_images(device_imgs, "model/tongue_model.onnx") results[device] = acc df = pd.DataFrame(list(results.items()), columns=['Device', 'Accuracy']) print(df.to_markdown(index=False))关键动作:运行前必须用
calibrate_camera.py为每台手机单独标定内参。我见过最坑的是OPPO Find X5,它的超广角模式默认开启,标定时没关会导致畸变校正失效,10张图全错。
6.3 验证“临床一致性”:请3位主治中医师盲测50张图,模型结果与医师共识率≥78%
这不是技术活,是流程活。原项目utils/clinical_consensus.py生成医师打分表:
# utils/clinical_consensus.py # 导出Excel模板,含50张图路径、3个医师的独立标注列、模型预测列 template_df = pd.DataFrame({ 'image_path': [f'data/clinical_test/{i:03d}.jpg' for i in range(50)], 'doctor_a_color': [''] * 50, 'doctor_a_texture': [''] * 50, 'doctor_b_color': [''] * 50, # ...其他列 }) template_df.to_excel('clinical_consensus_template.xlsx', index=False)我的血泪经验:医师打分必须隔离进行,禁止讨论。曾有次让两位医师同室打分,他们互相看到对方选了“灰黑苔”,第二位就跟着选——表面一致率95%,实际Kappa系数仅0.32(弱一致)。真正的临床价值,是模型在医师犹豫时给出稳定参考,不是取代医师。
最后说句实在的:这个项目最值得你花时间的,不是调参或改UI,而是亲手拍100张真实舌象图,按data/README.md里的标注规范标一遍。你会突然明白为什么模型总把“薄白”和“剥落”搞混——因为你自己标的时候也在犹豫。技术只是工具,中医舌诊的魂,在于人眼对生命体征的敬畏。希望帮到你。
本文还有配套的精品资源,点击获取