简介这是一套面向计算机视觉目标检测任务的全景牙齿X光片牙位标注数据集主要供算法工程师、医学科研人员与AI初学者用于训练牙齿检测和定位模型解决口腔影像自动化分析中的高质量标注数据需求。全套共210个文件包含105张精选JPG全景片与105个配套XML标注文件压缩包约28.49MBXML由LabelImg工具生成记录每颗牙齿的边界框坐标及类别信息可直接对接Faster R-CNN、YOLO、SSD等主流目标检测框架省去自行标注的繁琐流程。页面累计已有642人浏览学习。牙位标注精细、图片经过筛选能显著减少数据清洗与预处理工作量适合目标检测入门实战也可支撑辅助诊断、牙齿矫正规划、口腔疾病筛查等医疗AI应用的模型迭代是医疗影像算法研究的基础数据素材。 先说一个让我印象特别深的经历。第一次拿全景牙片训练牙位检测模型时我图省事从一个公开小样本集里拉了八百张片子标注也是东拼西凑的。训练出来的模型在门牙上看着还行一碰到磨牙区就乱套验证集mAP一直卡在0.6上下。后来把标注框可视化出来一看问题全在数据本身同一颗第一磨牙有人框牙冠有人拉到牙根有人干脆把两颗邻牙框在了一个框里。从那之后我彻底信了一件事——牙位检测模型的上限从你落笔拉框的那一刻就定死了。后来我重新做了一整套全景牙齿X光片的牙位标注数据集用labelimg逐张手标输出的是PASCAL VOC格式的精细xml标注文件。这篇就把这套数据集的搭建过程、牙位规则、xml结构、质检方法一次讲清楚适合准备做口腔影像目标检测的算法工程师、需要自建医学影像数据集的研究生以及刚接触labelimg和xml标注格式的入门者。1. 全景牙片为什么是目标检测里的“硬骨头”1.1 全景片成像机制决定了“框不准”是常态全景片也叫曲面断层片它的成像原理是X射线源与影像接收器围绕患者头部做旋转扫描把上颌骨、下颌骨、颞下颌关节展开到一张二维图像里。这个“展开”不是数学上的拉直而是弧线轨迹的投影叠加。结果就是上颌和下颌、牙体和牙槽骨、左右结构都会在二维平面上产生不同程度的重叠前牙区相对清晰双尖牙和磨牙区受颈椎、颞下颌关节的影响容易出现形变和伪影。这些成像特点对目标检测的直接后果是常见的“紧密贴合”式标注思路在全景片上并不好用。一张普通自然图像里目标的边界是清晰的灰度跳变而全景片上牙冠和邻牙之间是小面积接触、低对比度过渡很多区域根本没有明确的边界线。所以做这类数据集第一步是接受一个现实标注框不可能像自然图像那样紧密贴合它更多的是一种“可重复的近似”。1.2 牙位标注的难处不在“画框”而在“认牙”成人恒牙正常情况下有28到32颗每一颗都有独立的牙位编号本质上就是一个极其细粒度的多分类目标检测任务。难处集中在三点目标小且密集在全景片上单颗牙齿可能只占几十乘几十像素的矩形区域而全口牙齿密密麻麻排在一起框选时最容易把邻牙包进来。牙冠形态差异大切牙窄而薄尖牙长而尖前磨牙咬合面扁平磨牙粗壮且可能有分叉。用同一个矩形框去逼近这些不同形态边界定义必须足够清晰。伪影和重叠干扰金属冠、银汞充填体、根管治疗后的高密度影像会形成亮斑局部放大后甚至看不清边缘上下颌咬合时上牙和下牙在咬合面附近会重叠。我在标注规范里定了一条硬规则只标注牙冠部分下边界到釉牙骨质界也就是牙冠和牙根交界的位置上边界到切端或咬合面左右边界放在邻面接触点外侧一点点宁小勿大。这样规则明确之后不同标注员之间的差异能明显缩小。1.3 “精心挑选”的具体挑选标准标题里强调“精心挑选的图片”这真不是客套话。一张全景片合不合格直接决定标注出来的框有没有意义。我在做这个数据集时对原图做了一道比较严格的筛选基本放射质控得过关患者没有吐舌、没有佩戴活动义齿、咬合关系正常、没有明显颈椎重影叠在下颌切牙区域。金属耳环、项链等体外伪影尽量排除这些会在重建中产生大片放射状伪影干扰牙位判断。牙列要有代表性但不极端牙列严重不齐且邻牙重叠超过两颗的、全口重度牙周炎导致牙齿明显移位的、混合牙列期儿童片这些都会让标注边界变得不可控训练出来也难用。换句话说图片筛选的核心目的是保证“标签的真值稳定”。一条模棱两可的样本标进数据集模型学到的不只是错误边界还有对整个牙位分布的怀疑。2. 选labelimg而不上CVAT或在线工具我的真实考虑2.1 本地工具在医学影像场景下的合规优势做医疗影像标注隐私问题躲不开。全景牙片属于患者影像数据拿到网上的makesense.ai这种在线标注工具里传一遍很多医院和课题组在合规性上根本过不了关。CVAT虽然功能强但要部署一套Docker环境对只想做几百上千张数据的团队来说学习成本和维护成本明显偏高。labelimg的优势恰恰是“零部署”本地安装、本地运行、图片不出机器、导出格式直接就是VOC xml对于千张量级的数据集完全够用。当时还有一个考虑是团队协作方式。小团队做医学数据集通常是一到三个人标注靠的是统一规范和互相抽检而不是一个复杂的在线标注系统。labelimg虽然不支持多人实时协同但配合一个简单的Excel进度表和命名规范效率并不低。2.2 labelimg安装与配置里容易被忽略的几个点安装本身很简单。我推荐在conda里新建一个Python 3.9环境避免污染已有环境conda create -n labelimg python3.9 -y conda activate labelimg pip install pyqt5 lxml git clone https://github.com/tzutalin/labelImg.git cd labelImg python labelImg.py安装这件事上有一个非常容易踩的坑图片路径和xml保存路径里不要出现中文和空格否则有些版本的lxml在保存时会报编码错误或者保存出来的xml文件路径字段是乱码。另外在启动界面打开View - Auto Saving模式这样每切到下一张图时自动保存能避免标完忘存。使用上有两个设置值得注意。第一保存格式选PascalVOC不要选YOLO。虽然labelimg可以直接存YOLO格式的txt但VOC xml保留了更多信息比如图片尺寸、对象级的状态标记后续转换和校验都更加方便。第二如果你有提前写好的类别列表文件classes.txt可以通过界面上的“打开标签文件”按钮导入这样每标注一颗牙只需要从下拉列表里选而不是每次手输牙位名。2.3 牙齿矩形框的操作规范用labelimg画牙齿框和画行人、画汽车完全是两回事。行人和汽车有明确的外轮廓牙齿的全景片影像却是一团灰度接近的区域。我常用的操作方法如下先在左下角工具栏里调整缩放把要标的那颗牙放大到屏幕的清晰可见状态。拉框时按住鼠标沿着牙长轴方向取外接矩形前牙倾斜时也不要去旋转框就取一个能包住牙冠的竖直矩形保持一致性比追求单颗贴合更重要。如果两颗牙在影像上重叠只框目标牙的可见轮廓不要想着“带一点邻牙没关系”模型会因为这个“带一点”彻底分不清边界。对于全景片边缘被截断的牙齿要求牙冠可见度超过50%才标注否则不标并在xml里通过truncated字段记录状态。这套规则看着死板但对训练极其友好。模型在推理阶段给出的预测框会更稳定而不是在“多框一半少框一半”之间随机游走。3. 牙位标注规范类别命名与xml文件逐字段拆解3.1 FDI牙位编号规则与图像左右映射牙位标注最关键的是类别定义。我采用的是国际通用的FDI两位数字系统十位代表象限1是右上、2是左上、3是左下、4是右下个位是从中线往后的牙位顺序1是中切牙2是侧切牙3是尖牙4和5是前磨牙6、7、8是磨牙。所以一颗完整的右上中切牙就是tooth_11左下第二磨牙就是tooth_37这样全套恒牙一共32个类别。这里有一个新手特别容易翻车的点全景片的左右方位。牙科影像通常按患者解剖位置摆位也就是说图像上的左侧对应的是患者右侧。标注时如果只是凭“我看到的左边”来定牙位全口牙位都会左右颠倒。我在标注规范里明确要求打开一张片子先找解剖标志比如鼻中隔、下颌中线确认患者的左和右在图像上的位置如果窗口里方向不明确宁可先用边框标记一下再开工。3.2 一份标准xml文件长什么样labelimg保存的VOC xml本质是一个纯文本的XML树一个典型的牙位标注文件长这样annotation folderpanoramic_dataset/folder filenamePANO_1023.jpg/filename pathdata/images/PANO_1023.jpg/path source databaseprivate/database /source size width2400/width height1250/height depth3/depth /size segmented0/segmented object nametooth_11/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin1024/xmin ymin388/ymin xmax1142/xmax ymax578/ymax /bndbox /object /annotation逐字段说下含义。filename是图片文件名path是相对路径或保存时的图片路径size里的width和height是像素宽高depth通常是3对应彩色图segmented表示有没有像素级分割掩码牙位检测数据集里固定为0。后面每个object就是一颗牙的标注name是类别名bndbox是边界框的左上角坐标xmin/ymin和右下角坐标xmax/ymax坐标系原点在图片左上角单位是像素。需要注意VOC的坐标是绝对值不是归一化值。这意味着如果你后续对图片做了resize、裁剪或旋转必须同步更新xml里的size和所有bndbox坐标否则训练时坐标和图像内容会对不上。3.3 多人标注时的统一规则与边界情况小团队做标注最大的风险不是工具不会用而是同一个边界每个人理解不同。我最后落地的边界规则可以归纳成几条每颗牙只允许一个外接矩形框。下边界画到釉牙骨质界不包括牙根。左右边界到邻面接触区外侧接触区无法判断时把图像放大到800%以上以牙冠颊侧轮廓的灰度极小值位置作为分界。金属冠高密度亮斑不作为边界参考。缺失牙不画框完全未萌出或埋伏牙不画框。重叠超过70%的牙齿不标注因为即使标了框人类自己都难重复机器学了只会更乱。这些规则写完之后我让两个人在50张片子上一人标一遍算了比较直接的一致性指标同一个牙位标注框的IoU平均值。第一次测出来只有0.72经过两轮规则修订讨论后普遍能到0.85以上。所以不要嫌定规则麻烦这一道工序直接影响标注精度。4. 精标数据的质检链路回放画框、脚本统计与格式转换4.1 可视化回放是质检的第一道防线标注完了不等于数据能用。我强烈建议至少做一轮完整的人工抽检但别直接在labelimg里逐张翻效率太低。我写了一个简单的Python脚本把xml里的框回放到原图上批量输出成带标注的jpg存到review目录里然后快速翻看这些图重点关注左右牙位有没有标反、框有没有框到邻牙、类别名有没有写错。import cv2 from pathlib import Path import xml.etree.ElementTree as ET img_dir Path(images) xml_dir Path(annotations) out_dir Path(review) out_dir.mkdir(exist_okTrue) for xml_path in xml_dir.glob(*.xml): root ET.parse(xml_path).getroot() img_path img_dir / root.find(filename).text img cv2.imread(str(img_path)) if img is None: print(fmissing image: {img_path}) continue for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(ymin - 5, 20)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 1) cv2.imwrite(str(out_dir / f{xml_path.stem}_review.jpg), img)这个脚本本身两分钟就能写出来但价值非常大。回放图一旦生成很多问题一眼就能看出来某个区域连续几颗牙的头文字是反的、某张图只标了半口、某些框莫名其妙跑到牙槽骨上这些问题仅靠盯屏幕是盯不出来的。4.2 常见xml错误与修复方式xml文件最常见的几个错误我在这里集中说一下path字段存了绝对路径换电脑后路径失效。解决办法是保存时改成相对路径或者在解析时忽略path直接用filename到同名图片目录去拼接。坐标越界比如xmax大于图片宽度或ymin小于0。labelimg本身不太会生成越界框但如果你用脚本批量改过标注就有可能出现统计时要专门检测。文件名不一致。xml里filename叫PANO_1023.jpg实际文件叫pano_1023.jpg大小写不一致在Linux里直接读不到图。同一个牙位在一张图里出现了两个框。这往往是标注员中途改框没删干净需要回放检查个别人工删除。xml文件的打开和编辑大多数人第一反应是用浏览器或文本编辑器。其实日常维护用Python的xml.etree.ElementTree就够解析、改字段、批量替换都很方便。如果你只是想快速浏览格式VS Code装一个XML插件也能看得很清楚。但我不建议直接用文本编辑器大改xml很容易破坏标签闭合结构导致后续解析报错。4.3 VOC转YOLO格式的细节与坑数据集做完之后还是要喂给模型。现在主流检测框架像YOLOv8、YOLOv5默认读的是txt格式的标注所以需要把VOC xml转成YOLO格式。YOLO格式每一行是一个目标类别ID、中心点x、中心点y、宽、高所有值都归一化到0到1之间。import xml.etree.ElementTree as ET classes [tooth_11, tooth_12, tooth_13, tooth_14, tooth_15, tooth_16, tooth_17, tooth_18, tooth_21, tooth_22, tooth_23, tooth_24, tooth_25, tooth_26, tooth_27, tooth_28, tooth_31, tooth_32, tooth_33, tooth_34, tooth_35, tooth_36, tooth_37, tooth_38, tooth_41, tooth_42, tooth_43, tooth_44, tooth_45, tooth_46, tooth_47, tooth_48] def convert(size, box): dw 1.0 / size[0] dh 1.0 / size[1] x (box[0] box[1]) / 2.0 y (box[2] box[3]) / 2.0 w box[1] - box[0] h box[3] - box[2] return x * dw, y * dh, w * dw, h * dh for xml_file in xml_dir.glob(*.xml): root ET.parse(xml_file).getroot() size_node root.find(size) img_w int(size_node.find(width).text) img_h int(size_node.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: continue cls_id classes.index(name) box_node obj.find(bndbox) xmin float(box_node.find(xmin).text) ymin float(box_node.find(ymin).text) xmax float(box_node.find(xmax).text) ymax float(box_node.find(ymax).text) cx, cy, w, h convert((img_w, img_h), (xmin, xmax, ymin, ymax)) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_txt txt_dir / f{xml_file.stem}.txt out_txt.write_text(\n.join(lines))这里最关键的坑是classes列表的顺序必须和训练时的data.yaml里的类顺序完全一致一旦固定下来就不要随意增删否则所有txt的类别ID都会错位。我见过有人中途在中间插了一个类别结果整个模型训练到一半全乱了。另外转换之前先确认图片没有被resize过如果训练时会在data loader里做resize那并不需要改txt里的归一化坐标因为归一化坐标对缩放是天然的鲁棒但前提是图片的长宽比不能被单独拉伸。5. 数据集完成后还能怎么走扩展路线与我的经验教训5.1 从牙位检测到分割、病变分类的扩展方向牙位检测数据集做完之后很多人会问下一步能做什么。常见路线有三条病变辅助识别在现有牙位框内再标注龋病、根尖周病变、牙周膜间隙异常等属性把单纯的目标检测升级为多标签检测。注意此时类别数量会成倍增加比如tooth_14_caries这种组合类别标注前要设计好标签体系避免类别爆炸。牙齿分割如果后续要做牙冠形态测量、根尖距离分析矩形框就不够了要用polygon标注工具也建议换到labelme或CVAT的分割模式导出COCO格式或VOC格式的polygon字段。3D扩展全景片是二维影像很多三维信息已经丢失。如果要做正畸头影测量或者种植体规划最终还是要回到CBCT的3D标注但全套牙位检测可以作为3D模型的2D先验输入。5.2 最想分享的三条经验第一标注规则不要等到标了200张再定先选20张典型片子让每个人试标一遍然后拿着回放图逐条讨论规则把“看起来像”变成“边界在哪里”。这一步省下来的返工时间是最多的。第二划分训练集和测试集时按患者维度划分而不是按图片随机分。虽然全景片本身是一人一片但如果你的数据集后续扩展了多视角或不同时期的片源同一患者的片子一旦出现在测试集里评估指标会因为图像相似性而虚高看起来mAP很漂亮实际临床效果远没有那么好。第三标注完不要当天复核。我自己吃过这个亏当天标完当天审眼睛已经疲劳到自动忽略掉同一类错误隔一天再回看错误明显得多。如果项目时间紧也至少在几个关键区域做全量复核比如左右象限对称位置往往最容易发现牙位偏移。最后再分享一个我养成的检查习惯每次拿到新一批标注结果不急着可视化先跑一个按类别统计数量的脚本。如果某张图里tooth_11的数量异常增多而tooth_41明显减少先别怀疑模型大概率是标注员把左右牙位整体搞反了。这种错误靠人眼逐张查很难发现但靠统计脚本一眼就能看出来。数据集的构建从来不是光鲜的工作它琐碎、重复、需要大量耐心但对医疗AI来说它就是所有模型效果的地基。这套全景牙齿X光牙位标注数据集的做法希望能给你省下几个月的试错时间。本文还有配套的精品资源点击获取