ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

瞳孔虹膜分割数据集实战:从标注验证到Unet/YOLO-seg训练全流程解析

瞳孔虹膜分割数据集实战:从标注验证到Unet/YOLO-seg训练全流程解析 简介面向医学图像分割与计算机视觉研究者提供一套高分辨率瞳孔与虹膜分割数据集。数据集中于人眼区域图像统一为640×640分辨率标注图采用灰度mask格式像素值0、1、2分别对应背景、瞳孔和虹膜可直接用于训练语义分割或实例分割模型。资源完整划分为训练集与测试集训练集含394张原图及对应mask测试集含112张原图及对应mask便于模型训练与效果评估。压缩包共1014个文件以PNG图像、JPG原图和Python脚本为主整体约20.72MB下载和部署都很便捷。另附可视化脚本可随机抽取样本并同时展示原始图、GT图像及GT在原图上的蒙版效果帮助直观核对标注质量。目前已有266人学习适合需要标准瞳孔虹膜分割数据用于算法验证、论文实验或课程设计的开发者。1. 瞳孔虹膜分割数据集一份能直接开训的三分类分割数据做图像分割的人都有个共识模型结构可以抄损失函数可以调但一份标注干净的数据集才是真正的稀缺品。这份瞳孔虹膜分割数据集来自人眼图像统一分辨率 640×640mask 是 0/1/2 三分类灰度图训练集 394 张、测试集 112 张文件结构清晰还带一个可视化验证脚本。我拆完第一感觉是这不像随手导出的半成品而是可以直接接进 Unet 或 YOLO-seg 训练管线的成熟资源。尤其适合做医学图像分割、眼动追踪、活体检测方向的从业者——你不需要花几天时间去清洗标注省下的是数据预处理和格式对齐的重复劳动。不过标注语义和文件命名上有些细节容易被误读下面按我的复现过程一步步拆。2. 数据解剖目录结构、mask 语义与像素分布验证拿到一份分割数据集我习惯先做三件事看目录、验证 mask 取值、统计类别像素占比。这三步做完数据能不能用、训练时要特别注意什么心里基本就有数了。2.1 目录结构与文件名里的 .rf. 指纹解压后目录分两段训练集和测试集各自独立互不交叉train/ images/ 394 张 .jpg masks/ 394 张 .png test/ images/ 112 张 .jpg masks/ 112 张 .png训练集和测试集的 images 目录与 masks 目录一一对应同名文件即配对。文件名形如5f88420adb41b5d5_jpg.rf.b2feee328e6a5ba8ee09bf83ecc6b975.jpg注意这里有两个.jpg中间夹着一段.rf.加 32 位哈希。这段.rf.是 Roboflow 平台导出的命名指纹不是文件名写错了。5f88420adb41b5d5是原始图片名b2feee...是标注会话的哈希。如果你后续要写数据加载器用完整文件名配对没问题但如果你从其他来源补充数据别用jpg.rf.做分隔符切割文件名这样会把原始名截断。我一般直接用os.path.basename取全名做 key 匹配。2.2 mask 真的是 0/1/2 三分类吗先验证再信 README摘要里说 mask 是前景为 0/1/2 的灰度图这个表述容易让人误读。实际分割任务里0 通常代表背景1 和 2 才是前景类别。具体到这份数据合理推测是 1瞳孔、2虹膜但我不建议盲信写代码实测一遍最稳import cv2 import numpy as np mask_path train/masks/5f88420adb41b5d5_jpg.rf.b2feee328e6a5ba8ee09bf83ecc6b975.png mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) print(mask shape:, mask.shape) print(unique values:, np.unique(mask)) print(pixel counts per class:) for v in np.unique(mask): print(f class {v}: {np.sum(mask v)} pixels)这段代码用IMREAD_GRAYSCALE强制单通道读取避免把 PNG 的调色板或 alpha 通道读进来。np.unique直接列出 mask 里实际出现的灰度值正常情况下应该只有[0, 1, 2]三个值。提示如果np.unique返回了 255说明这张 mask 可能是 0/255/128 之类的调色板存储需要做重映射如果数值范围是 0-255 连续分布那就是把浮点图存成了 PNG属于标注导出错误这类图要剔掉。像素占比统计在分割任务里非常关键。瞳孔在整张 640×640 图里通常只占很小面积背景占比可能超过 70%这意味着如果直接跑交叉熵损失模型会倾向把所有像素预测为背景。我看到的结果基本符合预期背景占比最高虹膜次之瞳孔占比最小。这个结论直接决定损失函数要不要加权后面第 5 章会展开。2.3 分辨率统一是优点但要注意原图裁剪方式所有图像统一 640×640这对训练是好事免去了训练时随机 reszie 带来的标注偏移风险。但统一分辨率有两种来源一种是相机原生输出就是 640×640另一种是标注平台做了居中裁剪或 letterbox。两者的区别在于——如果是裁剪瞳孔和虹膜在图像中的相对位置分布会受裁剪框影响测试集如果裁剪边界不一致模型泛化会打折扣。我没有看到图像裁剪边界的额外标注信息所以这里给一个自查方法随机抽十几张图用边缘检测看四边是否存在规则的裁切痕迹或者直接看图像四边是否出现大量同色像素带。如果确认是 letterbox训练时建议把 letterbox 区域在 mask 里标记为 ignore_index不做损失计算否则模型会学到「预测边缘为背景」的偏置。3. 可视化脚本复现三张图看清标注质量这份资源附带的可视化脚本核心功能是随机抽一张图把原始图、GT mask、GT 蒙版叠加图并排展示并保存。这个脚本的价值被很多人低估——它不只是「看一眼长什么样」而是校验标注质量的最快路径。3.1 可视化脚本的关键实现原脚本的完整逻辑我不逐行搬了按它的行为重写一份等价的加了些注释方便你改import cv2 import numpy as np import glob import os import random img_paths sorted(glob.glob(train/images/*.jpg)) chosen random.choice(img_paths) mask_path chosen.replace(/images/, /masks/).replace(.jpg, .png) img cv2.imread(chosen) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # OpenCV 读入为 BGR展示前转 RGB mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 构建彩色蒙版1瞳孔(红)2虹膜(绿)背景保持透明 colored_mask np.zeros((*mask.shape, 3), dtypenp.uint8) colored_mask[mask 1] [255, 0, 0] colored_mask[mask 2] [0, 255, 0] # 叠加原图 60% 蒙版 40% overlay cv2.addWeighted(img, 0.6, colored_mask, 0.4, 0) canvas np.hstack([img, colored_mask, overlay]) cv2.imwrite(visual_check.png, cv2.cvtColor(canvas, cv2.COLOR_RGB2BGR)) print(saved to visual_check.png)这段代码里有两个容易翻车的点。第一mask_path的路径替换用了.replace(.jpg, .png)但原文件名里有两个.jpgreplace默认替换所有匹配项会把xxx.jpg.rf.xxx.jpg里的两处.jpg同时替换导致路径错误。我前面用.replace(/images/, /masks/)先换目录再用os.path.splitext处理扩展名才安全mask_path chosen.replace(/images/, /masks/) mask_path os.path.splitext(mask_path)[0] .png第二cv2.addWeighted要求两幅图尺寸和通道数完全一致mask 转成三通道彩色后才能叠加否则直接报维度错误。3.2 从可视化结果里能看出什么运行脚本得到三张并排图我建议每张图盯三个位置第一是瞳孔边缘的贴合度。瞳孔是圆形深色区域边缘应当是一条平滑曲线。如果 GT 的瞳孔边缘呈锯齿状说明标注时逐像素抠图精度不够模型训练时会在边界上反复震荡。第二是虹膜的外边界是否包含眼白区域。虹膜和巩膜的交界是半透明的标注员手动勾描时容易把虹膜外圈多包一圈或漏一圈。第三是瞳孔内部的反光点。眼球表面有高光反射这些反光点落在瞳孔区域内有的标注会把反光点标成背景形成「空洞」。如果 mask 里瞳孔区域出现了大量空洞训练时模型会学着在瞳孔中心预测背景这是标注不一致的信号——同一批数据里部分图标了反光点、部分没标模型会很困惑。可视化脚本我跑完后发现整体标注质量是能用的瞳孔与虹膜的边界基本贴合反光点问题不严重。4. 从 mask 到 YOLO-seg我在转换和训练里踩过的坑大多数人拿到分割数据集不只是为了跑 Unet还会想接进 YOLO-seg 这类目标检测框架。这一步的核心工作是把像素级 mask 转成多边形坐标格式对齐的过程中坑非常多。4.1 坑一mask 读进来是 3 通道类别数直接爆炸现象np.unique(mask)返回几十个值根本不是 0/1/2。原因cv2.imread默认以 BGR 三通道读取 PNG如果源 mask 是调色板 PNG读进来后每个通道的值不同组合出来的类别数远超 3。解决读取时强制cv2.IMREAD_GRAYSCALE或者用 PIL 的Image.open(mask_path).convert(L)。这是最基础的坑但确实有人在这个问题上浪费半天。随后 validate灰度读入后统计每张 mask 的实际类别数如果出现第三类别的游离像素比如灰度值 3 或 255要单独拎出来查看多半是标注工具自动填充的边缘抗锯齿像素需要二值化归一到最近类别。4.2 坑二OpenCV findContours 把瞳孔内部的孔洞也包了进来YOLO-seg 需要的是多边形顶点通常做法是对每个类别做二值化提取轮廓import cv2 import numpy as np def mask_to_polygon(mask_path, class_id, min_area50): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) binary (mask class_id).astype(np.uint8) # RETR_EXTERNAL 只取最外层轮廓避免孔洞被包进来 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) polygons [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: continue # 点数太多会导致训练慢用 approxPolyDP 压缩 epsilon 0.001 * cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, epsilon, True) polygons.append(approx.reshape(-1, 2)) return polygons这里的关键参数是RETR_EXTERNAL。如果用默认的RETR_LIST或RETR_TREE瞳孔内部的反射光斑空洞也会被提取成独立轮廓YOLO-seg 会把它们当成另一个实例。min_area50是过滤噪声的最小面积阈值我试过 10 和 5050 更稳能滤掉单像素毛刺。approxPolyDP的epsilon参数控制了压缩力度。0.001倍周长是比较保守的压缩比一个瞳孔轮廓压缩后大约 40~60 个点精度损失肉眼不可见。如果你追求更高的推理速度可以放宽到0.005但人会明显看到多边形出现折角。4.3 坑三坐标归一化容易漏掉边界值YOLO-seg 的标注格式要求坐标值归一化到 0~1并且可以等于 0 但不能等于 1严格小于 1。当瞳孔边缘贴合图像边缘时轮廓点的像素坐标除以图像宽度 640 后可能得到 1.0这个值会被 YOLO 训练框架判定为非法。解决坐标计算后做一次np.clip(coords, 0, 0.9999)。这不是钻空子而是 YOLO 系列解析标注时的硬性约束。类似的问题在边界框标注里也存在做过目标检测的人应该不陌生。4.4 坑四翻转增强让瞳孔和虹膜的语义产生歧义YOLO-seg 训练通常默认开启flip_lr0.5即一半概率水平翻转。对瞳孔虹膜分割来说水平翻转只是把左眼变成右眼像素级标注随图翻转语义不变没问题。但如果你在下游任务里要区分左眼和右眼翻转增强会让模型学到「左右眼对称」的错误先验此时必须关闭翻转或只做垂直翻转。另外还有一个容易被忽略的点虹膜内圈的瞳孔边界与外圈虹膜边界在翻转后相对位置不变所以不需要担心类别相对关系错乱。这个坑主要是任务语义层面的不是数据格式层面的提前想清楚能省很多返工时间。4.5 坑五测试集标注里存在个别缺失或错位我在遍历测试集时发现并不是所有文件名都能在images和masks两个目录里完全对齐。原因通常是标注平台导出时漏图或者某张图只有 image 没有 mask。这不是这份数据独有的问题Roboflow 导出的数据集偶尔就有这种疏漏。处理方式是在加载前做一次文件名交集检查import os img_dir test/images mask_dir test/masks img_names {os.path.basename(p) for p in os.listdir(img_dir)} mask_names {os.path.basename(p).replace(.png, .jpg) for p in os.listdir(mask_dir)} valid img_names mask_names missing_mask img_names - mask_names missing_img mask_names - img_names print(valid pairs:, len(valid)) print(img without mask:, len(missing_mask)) print(mask without img:, len(missing_img))要注意 mask 是.png后缀比较前先替换成.jpg再取交集。跑完后如果valid pairs不等于图片总数说明有脏数据直接过滤掉这批文件再训练别让 DataLoader 在训练中途报 FileNotFoundError。5. 把这份数据接到 Unet 训练管线DataLoader、损失函数与验证指标最后这一步我说说怎么把这份瞳孔虹膜分割数据真正变成一条能出指标的训练闭环。我以 Unet 为例这套思路同样适用于 DeepLabV3 等编码器-解码器结构。5.1 DataLoader 的关键写法数据加载器的核心是重写__getitem__返回原图和 mask 张量并做同步变换import torch from torch.utils.data import Dataset import cv2 import numpy as np import os class IrisSegDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size640): self.img_dir img_dir self.mask_dir mask_dir self.img_size img_size valid self._get_valid_pairs() self.pairs valid def _get_valid_pairs(self): img_names {p for p in os.listdir(self.img_dir) if p.endswith(.jpg)} pairs [] for name in img_names: mask_name os.path.splitext(name)[0] .png if os.path.exists(os.path.join(self.mask_dir, mask_name)): pairs.append((name, mask_name)) return pairs def __len__(self): return len(self.pairs) def __getitem__(self, idx): img_name, mask_name self.pairs[idx] img cv2.imread(os.path.join(self.img_dir, img_name)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, mask_name), cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (self.img_size, self.img_size)) mask cv2.resize(mask, (self.img_size, self.img_size), interpolationcv2.INTER_NEAREST) img torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask torch.from_numpy(mask).long() return img, mask两个参数值得说明。cv2.resize的interpolation参数mask 必须用INTER_NEAREST最近邻插值用双线性会把类别边界模糊成中间灰度值引入不存在的类别。img归一化用255.0把像素值压到 0~1这是 Unet 的常见输入范围。5.2 损失函数Dice 损失加类别权重前面提到瞳孔类别占比很小直接交叉熵会让模型忽略瞳孔。推荐组合损失CrossEntropyLoss DiceLoss其中 Dice 损失按类别分别计算再取平均。实际操作上我会给背景、虹膜、瞳孔设置[0.1, 1.0, 2.0]的类别权重抑制背景梯度让模型把注意力集中在虹膜和瞳孔边界。5.3 验证指标分类别看 Dice别看 mIoU 一个数训练完成后的验证阶段除了看整体 mIoU我会单独打印每个类别的 Dice 系数。为什么背景面积大背景 Dice 很容易到 0.98 以上把整体均值拉得很高瞳孔 Dice 低到 0.7 都看不出来。分开看三个数瞳孔 Dice 才是真正体现模型能力的关键指标——瞳孔边界精细是最难学好的类别。我第一次跑这份数据时整体 mIoU 到了 0.94看着不错但类别 Dice 一拆开瞳孔只有 0.81虹膜 0.92。后来加了瞳孔类别权重和边界像素加权瞳孔 Dice 才提到 0.88。从那以后我每次拿到分割数据集都强制先跑一遍可视化脚本和类别分布统计再决定损失函数怎么配而不是直接套默认配置。希望这份瞳孔虹膜分割数据的拆解和复现过程能帮你省掉我走过的这些弯路。本文还有配套的精品资源点击获取
返回列表