金属表面缺陷检测这个方向搞过的人都知道数据准备有多磨人。谢韦尔钢铁那份公开数据集标注信息全塞在一个CSV里图像文件名、缺陷类别、缺陷位置全挤在几列文本中而YOLO那边要的是每张图配一个txt、每行五个归一化数值。这两套格式之间的鸿沟说大不大说小也绝对不小——我第一次转的时候光是把CSV里的像素坐标换算成归一化中心点加宽高就来回折腾了大半天中间还因为几个空缺陷样本的处理方式不对训练时loss直接飙到nan。这篇就把整个转换链路从头到尾捋一遍包括CSV结构怎么读、坐标怎么算、空标注怎么处理、目录怎么组织以及我踩过的那些坑。不管你是刚拿到这份数据集准备做缺陷检测的新手还是想找个靠谱转换脚本的老手下面这些内容应该都能直接用上。1. 先搞清楚谢韦尔CSV到底长什么样动手写转换脚本之前必须先把原始数据的组织方式摸透。谢韦尔这份数据集的标注文件是一个CSV每行对应一张图像列的结构大致是这样的第一列是ImageId也就是图像文件名去掉扩展名后的标识第二列是ClassId用整数编码缺陷类别常见的有1到4四类分别对应不同的表面缺陷类型第三列是EncodedPixels这一列是整个转换的核心它用游程编码run-length encoding的方式把缺陷区域的像素位置压成了一串空格分隔的数字。这里有个特别容易忽略的点一张图可能对应多行。什么意思如果一张图像上存在多个缺陷实例CSV里就会有多个行共享同一个ImageId每行描述一个缺陷实例的ClassId和EncodedPixels。而如果某张图完全没有缺陷它的EncodedPixels列是空的ClassId通常也是空的。这个一图多行和空缺陷行的结构直接决定了后面转换逻辑不能简单地一行对一图处理。游程编码的读法也得说清楚。EncodedPixels里的数字是成对出现的第一个数字表示起始像素的线性索引从1开始计数按行优先展开第二个数字表示从这个起点开始连续多少个像素属于缺陷。比如100 5就表示从第100个像素开始连续5个像素是缺陷区域。这里有个坑索引是从1开始的而我们在Python里处理数组是从0开始转换时要么统一减1要么在还原掩码时注意偏移搞错了整个缺陷区域就会整体错位一个像素虽然看着不明显但对小目标缺陷来说影响不小。我建议在正式转换前先写几行代码把CSV读进来做个统计总共有多少行、多少个唯一ImageId、每个ClassId各有多少实例、有多少ImageId对应的全是空EncodedPixels。这一步花不了几分钟但能让你对数据分布心里有数后面处理空标注和类别不平衡时就有依据了。用pandas的话df.groupby(ImageId)[EncodedPixels].apply(lambda x: x.isna().sum())这类操作就能快速看出哪些图是纯背景。2. 从游程编码还原掩码转换的地基整个转换流程里最不能出错的就是把EncodedPixels还原成二值掩码这一步。因为YOLO需要的边界框是从掩码的像素范围推出来的掩码错了框就全错。还原的逻辑本身不复杂但细节多。先确定图像尺寸。谢韦尔数据集的图像统一是1600×256这个宽高必须和CSV里的线性索引对应上否则还原出来的掩码形状对不上。线性索引的展开方式是行优先也就是第r行第c列的像素其线性索引是r×width c 1注意那个加1是因为编码从1开始。还原时我们创建一个全零的(height, width)数组然后按对遍历编码串每两个数字一组第一个是起点start第二个是长度length把数组中从start-1到start-1length的位置置为1。用numpy实现的话可以先把编码串split成整数列表然后步长为2切片取出起点和长度再用一个循环或者向量化方式填充。向量化写法性能更好尤其是面对上千张图的时候。我一般会写一个函数输入编码串和图像尺寸输出一个uint8的掩码数组。这里提醒一句如果EncodedPixels是空值NaN或者空字符串直接返回全零掩码不要让它进填充逻辑否则split会报错。还原出掩码之后别急着往下走先做一次可视化抽查。随机挑几张有缺陷的图把掩码叠加到原图上看看位置对不对。我当初就是靠这一步发现索引偏移问题的——有一批图的缺陷框整体偏了一行原因就是我在某个分支里忘了减1。这种错误光看代码很难发现但叠图一看就露馅。抽查不用多十来张覆盖不同类别就够了。还有一点值得说一张图有多个缺陷实例时每个实例单独还原成一个掩码然后分别求边界框。不要把所有实例的掩码合并成一个大掩码再求一个框那样会把多个分散的缺陷框成一个巨大的区域训练出来的模型根本学不到单实例的特征。YOLO的标注是一行一个目标所以每个实例必须独立成行。3. 掩码到YOLO边界框的坐标换算掩码有了接下来是把掩码转成YOLO格式的边界框。YOLO的标注格式是每行五个值类别索引、中心点x、中心点y、宽度、高度后四个都是相对于图像宽高的归一化值范围在0到1之间。注意类别索引是从0开始的而谢韦尔的ClassId是从1开始的所以转换时要减1这个映射关系千万别搞反否则类别全错位。从掩码求边界框的逻辑是找到掩码中所有值为1的像素的行列坐标取列的最小值和最大值作为x_min和x_max取行的最小值和最大值作为y_min和y_max。然后宽度w x_max - x_min 1高度h y_max - y_min 1中心点cx x_min w/2cy y_min h/2。这里加1是因为像素坐标是离散的一个像素本身占一个单位宽度不加1会让框比实际小一圈对小缺陷尤其明显。归一化的时候cx除以图像宽度、cy除以图像高度、w除以图像宽度、h除以图像高度。算完之后建议做个范围检查所有值都应该在0到1之间如果出现负数或者大于1说明前面的坐标计算出问题了得回头查。我习惯在脚本里加一个断言一旦越界就打印出问题的ImageId和具体数值方便定位。还有一个边界情况要处理如果某个缺陷实例的掩码还原出来全是零可能是编码有问题或者数据本身异常那它求不出边界框这种行要跳过并记录日志不要硬算否则min/max会报错或者产生无意义的框。实际数据里这种情况不多但一旦遇到不处理整个脚本就崩了。换算完成后每张图对应一个txt文件文件名和图像文件名一致扩展名换成txt里面每行是一个缺陷实例的标注。如果一张图有多个实例就有多行如果一张图是纯背景所有EncodedPixels为空那txt文件应该是空的——注意是空文件不是不生成文件。这一点下一节详细说。4. 空标注样本留还是不留这是个问题谢韦尔数据集里有一批图像是完全无缺陷的它们的EncodedPixels为空。这批样本怎么处理直接影响到训练效果也是很多人转换时纠结的地方。先说结论空标注样本要保留对应的txt文件要生成但内容为空。为什么因为YOLO训练时一张图如果没有对应的标注文件或者标注文件不存在有些框架会直接跳过这张图不参与训练而如果标注文件存在但为空框架会把它当作负样本纯背景来用这对抑制误检很有帮助。缺陷检测场景里背景样本能教会模型什么不是缺陷缺了它们模型容易把正常纹理也判成缺陷误报率会明显上升。具体操作上对于ImageId对应的所有行EncodedPixels都为空的情况创建一个同名的空txt文件即可。注意判断逻辑不能只看某一行是否为空要看这个ImageId下所有行是不是都为空。因为一张图可能有多行其中部分行有缺陷、部分行为空这种混合情况在实际数据里存在这种图是有缺陷的要正常生成标注不能当背景处理。判断时按ImageId分组检查该组内是否存在非空的EncodedPixels存在就是正样本全空才是负样本。我踩过的一个坑是早期图省事直接把空EncodedPixels的行drop掉结果那些纯背景图在CSV里彻底消失了转换出来的数据集里一张负样本都没有。训练出来的模型在验证集上召回率看着还行但一放到实际场景里正常区域被框得满天飞。后来把负样本补回去重新训练误检明显下降。所以这一步别偷懒空文件该建就建。另外提一句负样本的比例也要心里有数。如果负样本占比过高比如超过一半训练时可以考虑适当下采样避免正负样本过度失衡如果占比很低那就全留着。具体比例在第一节做统计的时候就能算出来根据实际情况调整。5. 目录结构与数据集划分的落地细节YOLO训练对目录结构有约定俗成的组织方式转换的时候顺手把目录建好后面省事。标准的组织大概是这样的一个images目录下分train和val两个子目录放图像一个labels目录下同样分train和val放标注txt图像和标注的文件名不含扩展名必须一一对应。有些框架还要求一个data.yaml配置文件里面写明训练集和验证集的路径、类别数量、类别名称。划分训练集和验证集的时候建议按ImageId划分而不是按行划分。什么意思因为一张图可能对应多行标注如果按行随机划分同一张图的不同缺陷实例可能被分到训练集和验证集两边造成数据泄漏验证集指标会虚高。正确做法是先拿到所有唯一ImageId的列表打乱后按比例切分比如8:2然后根据ImageId把对应的图像和标注整体分到对应目录。这个细节很多人不注意但对评估的可靠性影响很大。类别名称这块谢韦尔的四类缺陷在原始文档里有对应描述转换时把类别索引和名称的映射写进data.yaml就行。类别数量填4名称列表按索引顺序排列确保和txt里的类别索引一致。如果后面要合并类别或者只检测其中某几类改yaml和过滤标注就行不用重新转一遍。文件复制还是移动我一般用复制保留原始数据不动转换过程可重复。图像复制过去的时候保持原文件名标注文件用同样的名字但扩展名是txt。复制完做一次完整性校验遍历images下的每个文件检查labels下是否有同名txt数量对不对有没有漏。这个校验脚本很简单但能避免训练时报找不到标注的低级错误。6. 转换脚本的实操拆解与性能考量把前面几节的逻辑串起来就是一个完整的转换脚本。结构上我习惯分成几个函数读CSV并分组、还原掩码、掩码转框、写标注文件、组织目录。每个函数职责单一方便调试和复用。读CSV用pandas最省事pd.read_csv之后按ImageId分组。这里有个性能提示如果数据量大groupby之后逐组处理可能慢可以考虑先把需要的列取出来减少内存占用。谢韦尔这份数据规模中等普通机器跑下来几分钟的事不用过度优化。还原掩码的函数前面说过重点是空值判断和向量化填充。掩码转框的函数里用np.where(mask 0)拿到行列坐标然后min/max。写标注文件时用f.write逐行写每行格式化保留六位小数就够了太多位数没必要还增大文件。目录组织用os.makedirs加exist_okTrue避免重复运行时目录已存在的报错。跑脚本的时候建议加进度条tqdm和日志。进度条让你知道跑到哪了日志记录跳过了哪些异常样本、哪些图是负样本、各类别实例数统计。这些日志在后续排查问题时很有用。我第一次跑完没留日志后来发现验证集里某类缺陷特别少想回头查是哪一步过滤掉的结果无从查起只能重跑。还有一个实操心得先拿一小批数据比如前50个ImageId跑通全流程人工检查生成的txt内容和对应图像是否吻合确认无误后再全量跑。全量跑之前把输出目录清空避免上次的残留文件混进来。这种小步验证再全量的习惯能帮你省下大量返工时间。7. 转换后必须做的几项校验脚本跑完不等于完事转换结果必须校验否则带着错误的数据去训练浪费的是几小时的训练时间和一堆算力。第一项校验是数量对齐images下train和val的图像总数应该等于labels下对应txt的总数且每个图像都能找到同名txt。第二项是标注内容合法性遍历所有txt检查每行是不是五个值、类别索引在0到3之间、后四个值在0到1之间、宽高大于零。第三项是可视化抽查随机抽若干张图把txt里的框画回原图上看框是否贴合缺陷区域。这三项做完基本能排除绝大多数转换错误。我还会额外做一项分布校验统计训练集和验证集里各类别的实例数看看比例是否接近。如果某个类别在验证集里几乎没有那验证指标就不可信得调整划分策略或者用分层抽样。分层抽样按ImageId的主类别一张图可能有多个类别取其中一个或按多标签处理来分能保证各类别在两个集合里都有代表。校验脚本不用写得多复杂几十行足够但一定要跑。我见过不少人转换完直接开训训到一半发现loss不降回头查才发现标注全错位了白白浪费半天。花十分钟校验省几小时排查这笔账怎么算都划算。8. 几个容易翻车的细节与我的处理习惯最后聊几个零散但很关键的细节都是实际转换中容易翻车的地方。坐标取整的问题。掩码转框算出来的x_min、y_min这些是整数像素坐标但归一化之后是浮点数写文件时保留六位小数足够。不要提前取整否则小目标的框会失真。另外如果缺陷区域紧贴图像边缘算出来的框可能刚好在边界上归一化后是0或1这是合法的不用特殊处理但校验时要允许边界值。类别映射的一致性。谢韦尔的ClassId是1到4YOLO要0到3这个减1的操作要在写标注时统一做别在某个分支忘了。我习惯在脚本开头定义一个映射字典所有地方都查这个字典避免硬编码散落各处。多实例图的处理顺序。一张图多个缺陷实例时写进同一个txt的多行顺序无所谓但每行对应一个实例不能合并。如果两个实例的框有重叠也照常各写各的YOLO训练时能处理重叠框。文件名的大小写和特殊字符。谢韦尔的ImageId是十六进制字符串一般没有特殊字符但复制文件时注意扩展名统一原图可能是jpg或pngtxt文件名要和图像去掉扩展名后完全一致大小写敏感的系统上尤其要注意。重复运行的安全性。脚本设计成幂等的重复跑不会产生重复文件或覆盖错误。输出目录每次跑之前清空或者写到带时间戳的新目录看个人习惯。我倾向后者保留历史结果方便对比。这些细节单看都不起眼但凑在一起任何一个没处理好都可能让整个转换白做。把上面这些环节都走一遍谢韦尔数据集转YOLO格式这件事就算彻底跑通了后面拿去训练检测模型数据这块不会再拖后腿。
阅读完成 · 觉得有帮助?