资讯动态

口腔疾病检测:基于YOLO的目标检测与置信度融合实践

发布时间:2026/9/15 1:57:36 来源:尧图企业网站定制
简介该项目面向AI医疗与计算机视觉的学习者和从业者提供一套基于卷积神经网络CNN的口腔疾病检测方案。它通过分析口腔图像自动识别龋齿、牙龈疾病等常见健康问题并输出分类诊断结果适用于疾病早期筛查、辅助诊断与医学教学同时利用大规模口腔图像数据集训练对不同病变类别均有覆盖支持后续针对新样本进行微调和优化。压缩包为zip格式共5个文件大小仅19.75MB核心包括Python主程序app.py、预训练模型权重best.pt、依赖清单文本requirements.txt与packages.txt以及项目说明文档README.md结构清晰解压配置环境后即可运行。其中best.pt可直接加载使用免去从零训练的漫长过程app.py内置图像上传与结果展示界面方便快速完成单张口腔图像的检测演示说明文档提供环境搭建与运行指引便于学习者复现及二次开发。目前已有378人学习下载对于希望快速搭建口腔影像识别原型、研究CNN在医疗场景中迁移应用的开发者具有直接的落地参考价值。1. 为口腔门诊准备一套可落地的深度学习检测基线拿到这份Oral-Disease-Detection-main工程第一反应不是在 IDE 里直接点运行而是先看它的推断链路是否对得起“检测”这两个字。解压后目录干净得很有代表性app.py负责入口weights/best.pt是已经训练好的权重requirements.txt和packages.txt把依赖拆成了运行与部署两份。这种结构在真实项目里很常见也意味着你不需要从零训练而是先把检测跑通再决定后续续训还是微调。口腔图像的特殊性在于病灶区域小、颜色对比低、牙齿和牙龈边界经常被唾液反光干扰。直接拿整图训练一个十几类的图像分类网络通常效果很差反过来用目标检测先定位到可疑区域再对裁剪出来的局部图做细粒度分类收敛速度更快泛化也更稳。这套工程提供的正是后一种思路适合想快速搭建口腔 AI 辅助筛查能力、又不想被繁琐训练流程拖住的开发者。2. 口腔图像检测从 best.pt 到推理链路设计2.1 两级推理结构为什么检测比整图分类更稳口腔图像里牙齿和软组织的空间分布相对固定但同一颗牙在不同光照、不同开口角度下局部纹理差异很大。如果直接用一个 CNN 分类整个口腔图像模型必须同时处理背景、舌头、牙龈和牙齿真正能用于判别病灶的特征很容易被稀释。常见做法是先做目标检测把疑似问题区域从大图中抠出来再做 ROI 分类。这样训练出的分类网络聚焦在局部特征上比如颊侧牙面的白色斑块是否属于早期龋蚀龈缘附近的暗红色区域是牙结石还是牙龈炎。这类两级流水线在医疗影像深度学习里已经很成熟best.pt作为目标检测权重其训练任务大概率是“定位每一颗牙齿或者定位可疑病灶”。如果训练时标注的是病灶框那么检测输出直接对应病种候选区如果标注的是牙齿框分类网络就负责判断这颗牙是否健康。判断哪种情况查看README.md中 class 定义即可。2.2 模型加载与单张图片推理项目入口app.py加载权重的方式和常规 YOLO 推理脚本差别不大。下面这段代码覆盖了从加载模型到输出检测框的完整链路兼容 PyTorch 生态里两种常见写法。import cv2 import torch # 方式一适用于 yolov5 系列的 torch.hub 加载 model torch.hub.load(ultralytics/yolov5, custom, pathweights/best.pt, force_reloadFalse) # 方式二如果训练配置用的是新版 ultralytics 包则改为 # from ultralytics import YOLO # model YOLO(weights/best.pt) # 推理参数conf 阈值为 0.25NMS 的 IoU 阈值为 0.45 model.conf 0.25 model.iou 0.45 # 读取并统一送入 640x640 image cv2.imread(test_01.jpg) image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results model(image_rgb, size640) boxes results.xyxy[0].cpu().numpy() for b in boxes: x1, y1, x2, y2, conf, cls b print(fbbox({int(x1)}, {int(y1)}) - ({int(x2)}, {int(y2)}), conf{conf:.2f}, cls{int(cls)})这段代码里有几个值得注意的地方。model.conf和model.iou分别控制检出灵敏度和框重叠的抑制程度口腔图像里相邻牙齿经常出现部分重叠IoU 阈值太高会保留两个高度重合的框太低又可能把相邻牙齿合并成一个框0.45 是一个比较平衡的起点。size640是推理分辨率不需要和训练分辨率完全一致但偏差过大会影响定位精度。代码里先转 RGB 是因为 OpenCV 默认读入的是 BGR 通道YOLO 推理内部对通道顺序敏感。2.3 检测框解析到质量过滤拿到xyxy格式的检测框之后第一件要做的事不是立刻裁剪而是先做质量过滤。口腔图像经常出现多个重叠的检测框尤其当模型对同一颗牙产生两次响应时两个框的 IoU 可能超过 0.8。只依赖 NMS 还不够建议在 NMS 之外再加一层基于尺寸和位置的过滤规则。下面这段逻辑可以挂到app.py的处理函数里过滤掉明显不合理的框。def filter_boxes(boxes, min_side12, max_side512): filtered [] for b in boxes: x1, y1, x2, y2, conf, cls b w, h x2 - x1, y2 - y1 if w min_side or h min_side: continue if w max_side or h max_side: continue # 口腔图像中检测框的宽高比不应过于极端 aspect w / max(h, 1e-4) if aspect 0.3 or aspect 3.3: continue filtered.append(b) return np.array(filtered)这里设置的宽高比范围与口腔解剖结构直接相关切牙和尖牙的框通常接近正方形磨牙的框可能略宽但不会出现非常细长的条形框。把min_side设在 1015 像素是为了去掉那些由反光产生的小噪声框口腔镜近景拍摄的图像分辨率通常很高小于 12 像素的检测框基本不是病灶更可能是背景噪声。3. 病灶 ROI 提取与预处理别让背景干扰分类3.1 动态扩充检测框保留牙周上下文检测框只是定位到牙体边缘但很多口腔疾病需要靠周围软组织信息辅助判断。比如牙周炎要看牙龈乳头的红肿程度龋齿判断需要看到牙釉质表面与相邻牙的接触点。如果直接把检测框内区域全部截下来盒子边缘会把病灶切开丢失上下文。成熟工程里通常会把原检测框外扩一定比例把牙龈和牙缝信息也包含进去。我这里实现是加一个pad_ratio按检测框宽高动态计算扩充量。def extract_roi(image, box, pad_ratio0.15, target_size(224, 224)): x1, y1, x2, y2 [int(v) for v in box[:4]] h, w image.shape[:2] pw int((x2 - x1) * pad_ratio) ph int((y2 - y1) * pad_ratio) x1 max(0, x1 - pw) y1 max(0, y1 - ph) x2 min(w, x2 pw) y2 min(h, y2 ph) roi image[y1:y2, x1:x2] roi_resized cv2.resize(roi, target_size, interpolationcv2.INTER_LINEAR) return roi_resized, (x1, y1, x2, y2)pad_ratio0.15意味着向外扩展检测框宽高的 15%。这个值的设置不建议照抄它取决于你的数据标注方式如果标注框本身就是沿着病损边缘画的比如标注的是龋坏面积那么pad_ratio建议 0.20.3如果标注的是整颗牙外扩 0.1 就够了外扩太多会把邻牙带进来。target_size要与分类网络的输入尺寸一致224×224 是轻量级网络的常见入参如果后续改用 320 或 384 的输入ROI 内的纹理细节会保留得更多但代价是推理耗时上升。3.2 归一化参数与分类网络输入对齐检测网络内部会自己处理归一化但分类网络往往需要显式归一化。口腔内窥镜图像的亮度分布不稳定不同设备拍出来的色温差异也很大常见做法是用 ImageNet 统计均值做标准化再配合随机亮度抖动削弱这种设备差异。下面这段代码把 ROI 从 OpenCV 的 BGR 格式转成 PyTorch 需要的 Tensor。def roi_to_tensor(roi_resized): roi_rgb cv2.cvtColor(roi_resized, cv2.COLOR_BGR2RGB) roi_norm roi_rgb.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) roi_norm (roi_norm - mean) / std tensor torch.from_numpy(roi_norm).permute(2, 0, 1).unsqueeze(0) return tensor注意permute(2, 0, 1)这一步把维度从 HWC 转成 CHW这是 PyTorch 卷积网络的标准输入格式。很多第一次跑这类工程的开发者容易在这里报维度错误报错信息通常是Expected 3D or 4D tensor。ROI 的宽高在上一节已经被 resize 成 224×224所以这里不需要再做尺寸对齐。分类权重如果是自己训练的口腔数据模型均值方差可能和 ImageNet 不同最好去README.md里确认训练阶段的预处理参数不要默认沿用 ImageNet 数值。3.3 多尺度预测与辅助特征在 ROI 分类过程中单纯依赖单一尺度会漏掉一些特征。比如早期龋齿在 224×224 分辨率下可能只占十几个像素此时把 ROI 同时缩放到 128、160、224 三个尺度分别预测再把结果做加权平均是一个工程上很实用的稳定化技巧。def multi_scale_predict(model_cls, roi, scales[128, 160, 224]): logits [] for s in scales: resized cv2.resize(roi, (s, s)) tensor roi_to_tensor(resized) with torch.no_grad(): logit model_cls(tensor) logits.append(logit) # 三个尺度的 logits 先求平均再走 softmax avg_logits torch.mean(torch.stack(logits), dim0) prob torch.softmax(avg_logits, dim1) return prob多尺度预测提升精度的原理在于大尺度保留纹理细节小尺度抑制局部噪声。对于口腔内窥镜的高光反射小尺度预测会把亮点直接模糊成小片区域减少误判。但注意这会带来约 3 倍的分类推理开销如果部署机器是 CPU建议去掉中间尺度只用 160 和 224 两个尺度。4. 疾病分类的置信度融合与诊断输出4.1 检测置信度和分类置信度的合成策略两级流水线有一个容易被忽略的问题检测阶段每个框都有一个置信度分类网络对每个抠出来的 ROI 也会输出一个概率最终你对用户展示的“这个区域有问题的概率”该取哪个最直接的做法是取分类概率但这样没有惩罚检测框本身的模糊性。一个检测置信度只有 0.3 的框即使分类网络给出 0.95 的高分也不应该直接判定为阳性。工程上常见的合成策略是加权几何平均把两个置信度压缩成一个 0 到 1 之间的值。def combine_conf(det_conf, cls_conf, alpha0.5): # alpha 控制检测置信度的权重 return det_conf ** alpha * cls_conf ** (1 - alpha)这里用乘法而不是加法是因为乘法对低置信度更敏感检测置信度降到 0.2 时即使分类置信度是 0.99合成结果也只有 0.2 的 0.5 次方乘以 0.99整体被明显压低。口腔检测场景更适合这种保守策略宁可把一个可疑区域标记为“需复核”也不能让低质量检测框顶着高分类概率误导医生。alpha默认取 0.5如果检测框质量不稳定可以调到 0.6。4.2 类间混淆与决策阈值不同口腔疾病类别之间的混淆规律比较固定。下表列了最常见的三类混淆及推荐处理参数。混淆类别典型诱因推荐辅助判断方式阈值建议龋齿 vs 牙渍两者都表现为牙面颜色异常结合检测框内纹理梯度强度判断分类概率低于 0.7 时输出“可能存在龋坏”牙龈炎 vs 正常牙龈轻度炎症仅有颜色变化对 ROI 做 HSV 颜色空间分析红通道均值升高红色通道均值超过阈值时提升类别权重牙结石 vs 牙体阴影光照不均造成暗色区域与邻牙亮度做相对比较不依赖绝对灰度邻牙亮度差低于 15% 时不视为病灶这些阈值不是为了追求学术意义上的最优而是为了减少漏诊。口腔疾病检测这类辅助工具漏报的代价高于误报所以把决策边界往“倾向于阳性”的方向偏移是合理的。实际操作时cls_conf在 0.50.7 之间的样本被称为“灰区”应当单独显示不能直接合并到阴性或阳性。4.3 面向医生的可解释性输出app.py提供的用户界面不应该只显示“正常”或“异常”这样一个结论医生需要知道模型为什么做出了判断。一个可落地的输出方案是同时展示原图、检测框叠加图、ROI 局部放大图和分类概率条形图。下面这段用 Gradio 搭建的界面可以直接替换app.py里的演示部分。import gradio as gr def predict(image): # 检测网络前向 results model(image, size640) boxes results.xyxy[0].cpu().numpy() annotated image.copy() for b in boxes: x1, y1, x2, y2, conf, cls b cv2.rectangle(annotated, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) # 分类网络前向代码略详见第 3.1 节 extract_roi label_probs {健康: 0.62, 龋齿可能: 0.21, 牙龈炎可能: 0.17} return annotated, label_probs gr.Interface( fnpredict, inputsgr.Image(typenumpy), outputs[gr.Image(), gr.Label(num_top_classes3)] ).launch(server_name0.0.0.0, server_port7860)gr.Label组件会直接把分类概率画成横向柱状图这个形式对临床医生相对友好。需要说明的是这类界面输出的只是辅助筛查信息不能作为诊断依据界面底部建议固定展示“AI 辅助筛查结果需由口腔医生复核确认”。“0.0.0.0”绑定之后局域网内其他终端也能访问服务内网部署时很方便但不建议直接暴露到公网。5. 批量验证、评估指标与视频序列平滑5.1 batch 推理脚本结果落盘单张图片跑通不代表模型可以上线至少得在几十到上百张测试图上跑一遍统计漏检率和误检率。我不建议在app.py里直接加循环因为 Gradio 的阻塞模型和批处理逻辑搅在一起不好排查问题。单独建一个batch_infer.py更合理。from pathlib import Path import json import cv2 test_dir Path(test_images) output_data [] for img_path in sorted(test_dir.glob(*.jpg)): image cv2.imread(str(img_path)) results model(image, size640) for b in results.xyxy[0].cpu().numpy(): x1, y1, x2, y2, conf, cls b output_data.append({ image: img_path.name, bbox: [float(x1), float(y1), float(x2), float(y2)], conf: float(conf), cls: int(cls) }) with open(batch_results.json, w, encodingutf-8) as f: json.dump(output_data, f, ensure_asciiFalse, indent2)输出结果用 JSON 而不是直接打印在终端是因为后续算指标时你需要按图片名和真实标注做 join。口腔图像数据集的标注粒度会直接影响评估方式如果标注在牙齿级别那么检测框和真实框算 IoU 时按标准目标检测指标来如果标注在病种级别需要把同一颗牙上的多个检测框合并成单类输出。5.2 检测层与分类层指标解读对batch_results.json做统计分析重点关注以下四个指标。指标计算方式口腔场景下的合理目标mAP0.5检测框 IoU 0.5 视为命中0.75 以上Recall敏感度真实阳性样本中被检出的比例0.9 以上宁可误检不可漏检Precision特异度检出框中真实阳性的比例0.8 左右即可F1-score精确率与召回率调和平均0.85 以上前两个指标决定了辅助工具能不能帮医生发现问题后两个指标决定医生愿不愿意信任这个工具因为太多的误报会让医生直接关掉界面。检测层的 IoU 阈值不用一开始就卡死可以同时输出 0.3、0.5、0.7 三档结果观察检测框偏移是整体性的还是随机性的。5.3 对连拍帧做序列平滑很多口腔设备支持连续抓拍同一颗牙会出现在连续 3 到 5 帧中。单帧检测抖动难以完全避免但时序信息可以显著消减噪声。常见做法是做一个简单的状态机连续 N 帧检测到同一类别时才输出阳性结论。FRAME_COOLDOWN 3 class FrameSmoother: def __init__(self, cooldownFRAME_COOLDOWN): self.cooldown cooldown self.last_cls None self.counter 0 def update(self, cls_id): if cls_id self.last_cls: self.counter 1 else: self.last_cls cls_id self.counter 1 return self.counter self.cooldown and self.last_cls is not Nonecooldown不宜设得太大否则某些转瞬即逝、需要在早期介入的病灶会被过滤掉。口腔连拍一般 2 到 3 帧内就能稳定检出同一个区域取 3 作为阈值覆盖了最典型的抖动场景。update返回的逻辑结果可以直接接入app.py的诊断输出也可以作为批量视频离线分析的过滤条件。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价