上睑下垂分类与分割:医学图像数据集构建与双任务训练实践 简介这套深度学习数据集聚焦眼睛及虹膜区域的上睑下垂疾病分类与分割适用于医学图像处理、计算机视觉方向的研究者与开发者可帮助快速搭建医疗影像分析实验。所有图像来自真实采集并自行标注类别涵盖轻度、中度、重度、正常四类数据集按类别分目录存放每张原始图像均配有对应的分割标注文件既可直接训练分类模型也能展开眼睛、虹膜结构的精细分割研究。资源包共1568个文件主体为786张JPG原图与782个JSON标注文件压缩包仅约92MB目录结构清晰图像与标注一一对应便于直接加载使用。目前已有267人学习下载数据集同时覆盖分类与分割两条研究路径省去自行采集、筛选和标注的繁琐环节为模型训练、调参对比和指标验证提供了现成素材适合课程设计、论文实验或项目预研等场景。1. 上睑下垂分类与分割这个眼睛图像数据集要解决的两类任务眼科门诊里医生拿到一张眼睑特写照片需要在几秒内判断患者是否属于上睑下垂并粗略评估睑缘遮盖角膜的程度。这个判断本身依赖经验而深度学习模型可以辅助完成初筛和量化测量——前提是有一批质量可靠的眼睛图像数据集。标题里的「上睑下垂疾病分类、分割」实际上定义了两种不同的监督任务分类告诉模型「这张图有没有病、属于哪个等级」分割告诉模型「病变区域和关键解剖结构在像素级位于哪里」。两类任务共享同一批原始图像但需要完全不同的标注。这篇博文围绕这类数据集展开它的构成和格式、预处理与增强、双任务模型训练、评估与落地时容易踩的坑。适合正在做医学图像分析、需要从零构建或调用类似数据集的工程师。2. 数据集成因分类标签与分割掩膜的规范、结构和质量控制2.1 分类任务和分割任务分别需要什么样的标签上睑下垂的分类标签通常是序数型ordinal的常见做法是分成正常、轻度、中度、重度四个等级或者按临床量化指标 MRD-1边缘反射距离的数值区间切分。由于等级之间存在连续性分类模型输出的概率分布比单一硬标签更有用——你可以从 softmax 分布里看出模型在「轻度和中度」之间的犹豫程度。分割任务的标签则是像素级掩膜需要标注的区域一般包括上眼睑边缘、睑裂区域和虹膜暴露区域。虹膜暴露程度是判断上睑下垂严重度的关键视觉线索所以掩膜标注通常以虹膜轮廓为基准。这两类标签的关系值得注意分割掩膜不是分类标签的「附带产物」而是分类判断的可解释依据。一个常见做法是把分割结果作为中间特征计算虹膜暴露比例或睑裂高度再映射到分类等级。这比直接端到端分类更接近临床推理路径也方便医生理解模型为什么给出某个结论。2.1.1 同源数据、两种标注的协调策略同源图像做双重标注时需要协调两个标注任务的关系。一般流程是先让标注医生完成分割掩膜再基于分割结果填写分类标签——这样分类标签的边界会更一致因为「轻度」和「中度」的判定标准虹膜暴露比例已经通过掩膜量化了。反过来做容易出问题先标分类等级再画掩膜标注者会被分类标签锚定画出符合分类预期而非真实边界的掩膜。2.2 数据集目录结构与文件组织方式图像数据集的目录结构直接影响训练代码的复杂度。我一般会按任务拆分子目录而不是把分类和分割混合在同一个遍历逻辑里ptosis_dataset/ ├── images/ │ ├── train/ │ │ ├── ptosis_001.jpg │ │ ├── ptosis_002.jpg │ │ └── ... │ ├── val/ │ └── test/ ├── masks/ │ ├── train/ │ │ ├── ptosis_001_mask.png │ │ └── ... │ ├── val/ │ └── test/ ├── labels/ │ ├── train.csv │ ├── val.csv │ └── test.csv └── metadata.json掩膜文件用 PNG 格式存储8-bit 单通道像素值为 0背景、1虹膜、2眼睑边缘。PNG 是无损压缩不像 JPEG 那样在边缘产生伪影——医学分割任务里这种伪影会影响 Dice 指标的准确性。CSV 里每一行对应一张图像包含patient_id患者编号、image_path、mask_path、severity分类标签、mrd1_mm如果有临床测量值字段。patient_id这个字段不是可有可无的。数据划分必须按患者进行不能按图像随机切分——同一患者的左右眼图像高度相似如果左眼在训练集、右眼在测试集评估结果会虚高。这在后续章节会展开讲。2.3 标注质量控制的三个层2.3.1 标注者间一致性上睑下垂的边界判定存在主观性睑缘位置、虹膜上缘的弧线在低对比度图像上并不总是清晰。常见做法是让两位眼科医生独立标注同一批图像计算分割掩膜的 Dice 一致性系数和分类标签的 Cohens Kappa。Kappa 值低于 0.8 的样本需要第三方仲裁重新标注。分割任务里标注者间 Dice 达到 0.85 以上通常认为标注规范足够稳定。2.3.2 排除标准以下几类图像需要从数据集中剔除眼睑严重闭合导致虹膜被完全遮挡的图像能拍到头但无法判断虹膜边界、佩戴美瞳或强反光导致虹膜纹理不可辨识的图像、图像经过美颜滤镜处理的边缘被虚化像素级标注没有意义。这些排除标准在数据采集阶段就要记录避免后期花大量时间筛选。2.3.3 边界细化如果目标是精确分割上睑缘边缘所有人都要遵守同一个约定标注的是「实际睑缘线」而非「可见皮肤褶皱」。这两者在重度上睑下垂患者身上差距很大。标注工具里放大到 200% 再逐点打点是最慢但最稳的方式高级点的做法是用 SAMSegment Anything Model预标注后让医生修正——眼科医生普遍认为修 SAM 的 mask 比从零画快得多但要用低灰度阈值避免 SAM 把虹膜纹理也吞进去。提示拿到任何公开的眼睛图像数据集先跑一段数据探索脚本统计掩膜像素分布。如果掩膜值不是 0/1/2 三值而是带有中间灰度说明文件被压缩或标注工具保存了抗锯齿边缘需要重新进行阈值化处理。3. 预处理与数据增强从原始眼部图像到模型输入的标准化流程3.1 尺寸标准化与图像归一化眼科图像的原始分辨率差异很大裂隙灯显微镜拍摄的图像可能高达 2048×1536手机拍摄的眼部特写则只有 720×1080。分割任务通常统一到 512×512这个尺寸是 U-Net 系模型的常用默认选择——4 次下采样后特征图是 32×32保留了足够空间细节又不会让显存压力过大。分类任务可以接受更小的 224×224 输入因为 ImageNet 预训练模型的权重就是以这个尺寸设计的。归一化不能简单套用 ImageNet 的 mean/std。医学图像尤其是裂隙灯图像的亮度分布与自然图像差异明显直接用 [0.485, 0.456, 0.406] 这套参数往往导致对比度偏低。我一般会先对训练集的每个通道统计均值方差然后做标准化import cv2 import numpy as np from albumentations import Compose, CLAHE, Resize # 统计训练集各通道的均值和标准差一次性脚本 means, stds [], [] for path in train_image_paths: img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) / 255.0 means.append(img.reshape(-1, 3).mean(axis0)) stds.append(img.reshape(-1, 3).std(axis0)) mean np.mean(means, axis0) std np.mean(stds, axis0) print(RGB mean:, mean) # 例0.412, 0.388, 0.402 print(RGB std:, std) # 例0.214, 0.203, 0.198这里把图像从 BGR 转成 RGB 是因为 PyTorch 预训练模型的权重是按 RGB 顺序加载的。统计训练集而非整个数据集的均值和标准差避免测试集信息污染训练过程——虽然影响很小但严谨的 benchmark 会这样做。注意统计均值时把像素值缩放到 01 再算这与后续用 imagenet-stats 还是 dataset-stats 的用法保持一致。如果你直接用 0255 的整数算均值得到的 mean 要除以 255 后输入模型。3.2 针对眼部图像的增强策略通用增强随机翻转、裁剪、亮度抖动够用但不充分。上睑下垂图像有几个特定问题需要针对性增强。3.2.1 对比度受限自适应直方图均衡化裂隙灯图像的中心亮区与边缘暗区对比度差异大。CLAHE 在局部区域做直方图均衡化能提升虹膜纹理与眼睑边缘的可见度——这是处理这类图像最值得优先尝试的预处理步骤import cv2 def apply_clahe(img, clip_limit2.0, grid_size(8, 8)): 对亮度通道做 CLAHE保留色彩信息。 clip_limit 越大对比度提升越明显过大会带来噪声放大。 lab cv2.cvtColor(img, cv2.COLOR_RGB2LAB) l_channel, a_channel, b_channel cv2.split(lab) clahe cv2.createCLAHE(clipLimitclip_limit, tileGridSizegrid_size) l_channel_clahe clahe.apply(l_channel) lab_clahe cv2.merge((l_channel_clahe, a_channel, b_channel)) return cv2.cvtColor(lab_clahe, cv2.COLOR_LAB2RGB)参数含义clip_limit限制了对比度放大的上限2.0 是保守值grid_size决定局部块大小对于包含大面积虹膜纹理的图像8×8 比 4×4 更不容易产生块状伪影。注意 CLAHE 只对 LAB 空间的 L明度通道做保留 AB 彩色通道避免颜色偏移——这对后续分类判断「眼睑是否发红」这类线索很关键。3.2.2 模拟拍摄条件扰动的增强裂隙灯拍摄时患者头部微小移动会产生模糊和几何畸变。在增强阶段加入高斯模糊和轻微透视变换可以提高模型对采集设备差异的鲁棒性。但注意不要对分割掩膜做相同强度的模糊——掩膜是二值语义标签模糊会破坏边缘的硬边界。正确的做法是让图像和掩膜共享几何变换旋转、缩放、翻转但跳过像素级变换模糊、噪声、CLAHEfrom albumentations import ( Compose, HorizontalFlip, ShiftScaleRotate, RandomBrightnessContrast, GaussianBlur, OpticalDistortion ) train_transform Compose([ HorizontalFlip(p0.5), # 左右眼镜像天然对称 ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit15, p0.7), # 小角度旋转 轻微缩放 RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), GaussianBlur(blur_limit(3, 5), p0.3), # 模拟失焦 CLAHE(clip_limit(1, 3), tile_grid_size(8, 8), p0.5), ]) train_transform_mask Compose([ HorizontalFlip(p0.5), ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit15, p0.7), ])这两个Compose对象必须使用固定的随机种子同步调用先给图像做第一个 transform再把同一个 seed 传入第二个 transform 处理掩膜。用albumentations的ReplayCompose或者给Compose传入additional_targets参数都能实现这种同步后一种更简洁——直接在additional_targets里声明mask目标图像和掩膜共享所有几何增强参数。rotate_limit15这个值不能更大。眼睑和虹膜的解剖结构有固定朝向旋转太多会让模型学到错误的几何先验——真实拍摄时患者头部倾斜很少超过 15 度。另外不建议使用ElasticTransform弹性形变因为眼睑组织虽然有柔韧性但不会产生随机的局部扭曲弹性形变反而会让分割边缘失去临床意义。3.2.3 为什么不做随机裁剪通用目标检测任务里随机裁剪是标配但上睑下垂分割任务里裁剪位置如果偏离了虹膜中心掩膜和图像的对应关系虽然不变模型的注意力会被引导到错误区域。我通常只在「先检测出眼睛区域再裁剪」的流水线里用固定比例的中心裁剪而不是随机裁剪。如果数据集里眼睛并非始终居中优先用目标检测框定位后再裁剪到统一尺寸。3.3 类不平衡的采样层处理上睑下垂数据集中正常样本通常远多于中度和重度样本。这种分布反映真实门诊比例但直接训练会让模型偏向预测「正常」。处理方式分两层。第一层是采样器层面from torch.utils.data import WeightedRandomSampler # 假设 labels 是长度为 N 的分类标签数组classes 是类名列表 class_counts np.bincount(labels, minlengthlen(classes)) class_weights 1.0 / class_counts sample_weights class_weights[labels] sampler WeightedRandomSampler( weightssample_weights, num_sampleslen(sample_weights), replacementTrue )这段逻辑的核心是给少数类样本更高的采样概率。replacementTrue表示放回抽样少数类会在每个 epoch 中重复出现。num_samples设置为样本总数保证每个 epoch 的迭代步数与均匀采样一致方便与学习率调度器配合。第二层是损失函数层面的权重。两个层面可以同时使用但要注意采样器已经改变了有效类别分布损失函数里再加权重会双重放大少数类的梯度。我倾向于在采样器修正分布后损失函数只加一个很小的类别权重0.5 到 0.8 之间主要用于缓解残余偏差。4. 分类与分割双任务训练网络选型、损失函数与评估指标4.1 双头网络结构的选择两种主流方案一种是两个独立模型分别做分类和分割另一种是共享 encoder 的双头模型。小数据集上用两个独立模型更稳——先训练分割模型再用分割结果提取特征去辅助分类这样即使分割效果不完美分类模型也不会被连带拖累。数据集足够大或需要端到端优化时双头模型是更经济的选择。4.1.1 Encoder 的选型分割任务推荐 U-Net 结构其中 encoder 直接用 EfficientNet 或者 ResNet 的预训练权重PyTorch 生态下用segmentation_models_pytorch这个库很省事但你要理解它背后的原理不能只当黑盒。在医学小数据集上ResNet34 比 ResNet50 更合适——深度更浅、参数量更少不容易在小数据上过拟合。分类任务单独用 EfficientNet-B3 即可。4.1.2 双头模型的话梯度怎么回传双头模型一个常见的坑是分类头与分割头的梯度量级差距很大。分割头的损失通常在 0.51.0 这个尺度分类头的交叉熵损失则在 0.11.5 之间但梯度范数可能差一个数量级。解决方式是在两个头之间使用梯度缩放# 双头模型前向传播与加权损失的简化示意 class PtosisModel(nn.Module): def __init__(self, n_classes4, n_mask_classes3): super().__init__() self.encoder timm.create_model(efficientnet_b3, pretrainedTrue, features_onlyTrue) self.seg_head nn.Sequential( nn.Conv2d(64, 32, 3, padding1), nn.ReLU(), nn.Conv2d(32, n_mask_classes, 1) ) self.cls_head nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, n_classes) ) def forward(self, x, return_maskTrue): # features_onlyTrue 时取最后一层特征64 通道 feat self.encoder(x)[-1] seg_logits self.seg_head(feat) # 上采样到原始分辨率 cls_logits self.cls_head(feat) if return_mask: seg_logits F.interpolate(seg_logits, sizex.shape[2:], modebilinear, align_cornersFalse) return cls_logits, seg_logits注意timm的features_onlyTrue返回多尺度特征列表这里只取最后一层。双任务的核心是让 encoder 学到既对纹理敏感分割又对全局结构敏感分类的表征。实际训练时对两个 head 的损失加权分割损失权重设为 0.7、分类损失 0.3 是常见起点分割任务难度更高、能提供更强的梯度信号。4.2 损失函数组合与训练关键参数分类头用带标签平滑的交叉熵。上睑下垂分级是序数任务预测 1 和 2 的差异比预测 0 和 3 的差异小标签平滑可以软化硬标签让模型更少「过度自信」。分割头用 Dice Loss 和交叉熵的组合提示分割头的损失不要全程用纯 Dice Loss。Dice Loss 在小目标分割虹膜区域的边缘区域上梯度不稳定容易震荡。Dice CE 的组合Dice 权重 0.6、CE 权重 0.4收敛更平稳。训练参数参考下表这批参数在 1000 到 5000 张图像规模的眼科数据集上表现稳定参数推荐值说明输入尺寸512 × 512分割任务默认分类任务可降到 256Batch size8512 尺寸 U-Net 结构下 16GB 显存的极限优化器AdamWweight decay 设 1e-4比 Adam 更稳学习率1e-4主模型/ 1e-5分类头encoder 共享时不区分 head双头模型可给分类头更小 lr调度器CosineAnnealingLRT_max 设为 epoch 数Epochs60医学图像小数据集 commonly 用早停不需要很多 epoch类别权重分类[0.5, 0.8, 1.0, 1.5]对应正常/轻/中/重少数类权重更大Dice 权重 vs CE 权重0.6 / 0.4分割损失内部的子权重数据增强的强度要随着训练进度衰减。前 20 个 epoch 用强增强旋转 15 度、亮度扰动 ±20%后 20 个 epoch 逐渐减弱到旋转 5 度、亮度扰动 ±10%。原因强增强在训练初期提供正则化但在后期会阻挠模型收敛到更精确的边界——增强带来的噪声使模型无法精修边缘。这个「增强退火」技巧在分割任务里效果显著很多框架没有内置需要手动实现。4.3 评估指标体系与阈值选择分类任务的评估不能只看 Accuracy。上睑下垂数据集中正常样本占比可能超过 70%一个全预测「正常」的模型就能拿到 0.7 的 Accuracy但这毫无临床价值。需要同时报告Sensitivity敏感性/召回率所有真实患者中被检出的比例Specificity特异性所有正常者中被正确判为正常的比例AUC不依赖阈值的排序能力Macro-F1各类别 F1 的算术平均对少数类更敏感分割任务用 Dice 和 IoU。注意两者的数值关系IoU Dice / (2 - Dice)Dice 永远不小于 IoU。例如 Dice 0.85 对应 IoU 约 0.74。报告指标时两个都列出不必二选一。4.3.1 阈值选择与校准多分类阈值的默认做法是取概率最大的类别但这在序数任务里有问题。两种更符合实用场景的方式第一使用 Youden 指数J Sensitivity Specificity - 1在每个类别上寻找最优阈值第二因为类别有序直接对概率做加权累加得到「严重度得分」按医生的就诊决策阈值划分。第二种方式更贴近临床——医生不关心「度」的边界关心的是「要不要手术」而手术标准通常与严重度得分相关。5. 从分割结果到可解释特征、跨数据集的迁移验证5.1 把分割掩膜变成定量特征分类任务知道「有没有病」还远远不够。上睑下垂的临床决策依赖量化指标比如虹膜暴露比例眼睑遮盖了多少角膜和睑裂高度。分割掩膜可以直接计算这些特征这正是分割任务与分类任务形成闭环的地方import numpy as np def compute_iridial_exposure(mask): mask: shape (H, W)1 代表虹膜2 代表眼睑覆盖区 iris_area np.sum(mask 1) eyelid_area np.sum(mask 2) total_area iris_area eyelid_area return iris_area / max(total_area, 1) # 暴露比例值越小表示遮盖越严重这个暴露比例可以当作一个生物学上的连续特征与分类 logits 拼接也可以单独用来做决策。有了连续特征模型不再只输出一个离散等级而是能回答「这个患者的 MRD 大约是多少毫米」——前提是你有一批带有 MRD 临床测量值的标注。5.2 标注不一致的排错方法训练时 loss 降不下去第一步先检查标注而不是调模型。把训练集的掩膜和原始图像叠加可视化检查以下典型问题左右眼标注是否统一有的标注者可能把虹膜和眼睑像素值互换重度上睑下垂样本中虹膜区域被大面积遮盖时标注者是否仍然标出了不完整的虹膜边界图像方向未统一时模型需要额外学习「左眼右眼镜像」这一与疾病无关的变换操作方法是用 matplotlib 把每个样本的图、掩膜、预测结果并排拼成一张大图按 loss 降序排列直接看 loss 最高的那批样本的标注质量。常常会发现某个标注员在某个时间段标注的样本有明显边界偏移——这是标注疲劳的典型表现。5.3 跨数据集迁移时的验证策略上睑下垂数据集之间拍摄设备、光线条件和患者群体差异可能很大。如果用公开数据集训练、在自己的临床数据上微调需要先做一次层冻结实验冻结 encoder 的前几层只微调后几层和分割头对比全量微调的效果。医学小数据集的共识是「用 ImageNet 做初始化、用同领域数据微调高维语义层」因为底层边缘纹理特征已经由 ImageNet 学好了同领域数据更适合微调语义层面。层冻结实验最直接的做法是设置requires_gradFalse并记录验证集 Dice 的变化曲线如果冻结层数增加到某一层之后 Dice 断崖式下跌说明数据域差异集中在那几层后续可以针对性微调。细节上训练完成后对分割掩膜做一次后处理去掉小于阈值的连通区域消除噪声、对掩膜边缘做一次形态学闭运算闭合细微空洞这两个操作能让 Dice 指标提升 0.010.03。先处理再提交测试集不要为了数据好看在评估阶段就做清洗——一行的差异都值得抠因为模型上线后面对的就是这样不干净的预测结果。本文还有配套的精品资源点击获取