
简介基于Python的岩石裂缝与CT岩心裂缝语义分割项目面向岩土工程与计算机视觉交叉领域的毕业设计、课程设计学习者也适合需要快速上手的初学者资源将岩石表面裂缝与CT岩心内部裂缝统一处理解决裂缝目标提取、标注数据不足和模型训练门槛等问题。压缩包共11个文件、大小约1.13MB主要包含3个Python脚本承担数据增强、均值计算等预处理功能、6张岩石/CT裂缝原始图像与对应语义标注图以及项目说明文档和Git版本管理配置。代码注释详实从数据准备、脚本运行到分割结果展示都有说明新手也能看懂解压后稍作环境配置即可运行便于对照实际图像检验分割效果。目前已有226人学习参考适合需要搭建高分毕业设计或期末大作业方案以及希望借助真实裂缝图像数据集熟悉语义分割完整流程的研究者与开发者。1. 岩石裂缝与CT岩心裂缝语义分割源码和数据集能解决什么问题岩石裂缝与CT岩心裂缝语义分割就是把岩心样本的CT横切片喂给Python训练的分割模型让模型把每一条裂缝的像素轮廓从岩石基质里剥离出来。做这件事的现实意义很直接裂缝的位置、宽度和连通关系一旦变成像素级输出就可以被下游程序量化渗透率估算、裂缝密度统计分析乃至三维裂缝网络重建都依赖这个输入质量。做这个方向最常见的痛苦不是缺模型而是缺一份能直接用的数据组织和一套能跑通的源码。很多人拿到CT图像后发现原始TIFF是16位灰度标注工具导出却是8位JPEG光是对齐就翻了车。适合两类人一类是手里有CT切片、需要把裂缝从基质里定量分出来的地质岩土工程师另一类是刚开始学语义分割想找一个真实场景的数据和代码落地的CV新手。2. 用Python准备CT岩心与岩石裂缝数据集预处理与标注格式2.1 灰度分布决定预处理方式先用numpy读一遍直方图CT岩心图像不是一张普通的自然照片。常见来源是微米级CT扫描仪输出的16位TIFF序列或DICOM栈单张图像宽度往往在1024到2048像素之间灰度层级比普通8位图片多得多。如果你直接把cv2.imread读回来的数组丢给模型大多数情况下图像会是漆黑一片因为有效灰度值只占了整个uint16区间里很小的一段。我处理这类数据的第一步从来不是写增强函数而是先跑一段numpy统计把灰度分布看明白。这一步能省掉后面大量返工尤其是当你拿到的是别人重新导出的数据集时位深和范围往往和原始扫描不一致。import numpy as np import cv2 from pathlib import Path img_path Path(data/ct_core/slice_0042.tiff) img cv2.imread(str(img_path), cv2.IMREAD_UNCHANGED) print(dtype:, img.dtype, shape:, img.shape, unique:, np.unique(img).size) lo, hi np.percentile(img, (1, 99.9)) print(1%分位点, lo, 99.9%分位点, hi)代码逻辑说明cv2.IMREAD_UNCHANGED保证读到的不是被库强制转成8位的数组从而避免16位像素值被截断。之后percentile用于观察全局灰度分布两个分位数的跨度是后面做截断归一化的依据。若分位点跨度很窄比如只有几百到几千说明大多数像素的灰度聚集在一个小范围内这时候不能做min-max归一化否则裂缝这种极暗结构会被整体抹平。参数说明这一节有两处关键点。一是读取时的IMREAD_UNCHANGED标志二是观察直方图所用的分位数。分位数的选择直接影响训练时的对比度控制我默认用1%和99.9%在裂缝数据集上比较稳原因是裂缝属于极小占比结构如果分位数取到5%以下会把裂缝分布的尾巴和噪声一起截掉如果取到99%以上又会把CT环状伪影和孔隙噪声放大训练时背景差距被压缩分割结果反而变差。拿到灰度统计后归一化有两种落地习惯。第一种是线性截断后映射到0到255再把结果存成8位PNG好处是数据规模小、读起来快第二种是直接在训练阶段按分位数实时归一化不落盘。当图像数量过万时第一种更省内存但我一般倾向实时归一化因为落盘转换会引入一次重新保存时的位深损失而这种损失对裂缝这种弱对比结构是致命的。还需要注意图像的对齐和伪影问题。CT序列在扫描时会有运动伪影和环状伪影预处理时最好加一步可选的高斯低通滤波来抑制环形噪声。但滤波核大小不要超过3×3因为裂缝宽度往往只有2到5个像素大核会把裂缝直接抹平这个参数在我的项目里是踩过坑后才定下来的。2.2 标注格式与数据集目录让每张图配好一份mask岩石裂缝标注和普通目标检测标注有明显差别标注结果是像素级的不能用矩形框代替。常见落地工具是LabelMe它导出JSON文件把多边形坐标记录在文件里随后通过脚本转换成单通道mask。另一种来自医学影像领域的习惯是直接用ImageJ标注把裂缝区域打上ROI后导出为8位mask。两种工具我都在用只要最终落盘格式统一就行。无论标注工具是什么最终落到硬盘上的数据集格式务必要方便Dataset加载器直接读取。我惯用的目录组织方式是这样data/ train/ images/ core_0001.png core_0002.png masks/ core_0001.png core_0002.png val/ images/ core_0043.png masks/ core_0043.png test/ images/ core_0097.png masks/ core_0097.png在这个结构里train/val/test按样本划分而不是按扫描序列划分避免同一根岩心内部的相邻切片出现在训练和验证两个集合里防止验证指标虚高。每个图像和mask文件名严格一致mask使用PNG而不是JPEG因为JPEG压缩会在裂缝边缘产生伪影这些伪影会作为噪声进入分割输出。这里有一个非常容易忽略的细节mask的像素值必须检查。许多标注工具导出时背景是255、目标区域是0或者标签值不连续若不先做一次像素值统计就直接进入训练最后的损失值会怎么看怎么不对。import numpy as np import cv2 from pathlib import Path for mask_file in Path(data/train/masks).glob(*.png): mask cv2.imread(str(mask_file), cv2.IMREAD_UNCHANGED) cls, counts np.unique(mask, return_countsTrue) count_dict dict(zip(cls, counts)) if len(count_dict) 3: raise ValueError(%s 类别超预期需检查标注 % mask_file.name)这段代码跑一次能发现标注导出时背景和前景颠倒的问题也能发现是否存在中间值。比如某个项目里标注工具的橡皮擦会留下255的纯白像素明明每张图只该有两个类别结果硬是出现第三类不处理的话模型会学着把白色区域也算成一类。数据集划分比例我一般取8:1:1但如果裂缝样本太少验证集和测试集会遇到完全没有裂缝像素的情况这时候验证指标会退化成一个全背景预测都能拿高分的假结果需要人为保证每个集合里都有含裂缝的样本。一个简单做法是在划分时统计各集合mask中的正样本数让三个集合的正样本比例落在合理区间。提示拿到任何别人分享的裂缝分割数据集第一件事是检查图像位深、mask类别数和图像mask是否同名对齐。三样都没问题再谈训练。3. 用Python搭起裂缝分割源码U-Net结构选择与训练参数3.1 为什么裂缝分割优先选U-Net而不是DeepLabV3裂缝语义分割在CV领域属于像素级稠密预测任务可用模型有FCN、U-Net、DeepLab系列、SegNet和近期基于Transformer的架构。在CT岩心这种弱对比、细线结构的场景里我一般先推U-Net而不是一上来就上更大的DeepLabV3。原因有三个。第一裂缝宽度只有几个像素这种细线结构需要浅层高分辨率特征来保持边缘定位精度而U-Net的编码器-解码器结构会把每层的特征图与对应尺寸的解码端拼接把浅层的位置信息直接传回解码端细线不容易丢。第二U-Net对训练样本量不敏感在几百张切片的数据规模下就能收敛到可用结果DeepLabV3的ASPP模块虽然对多尺度语义更友好但需要更多数据才能发挥出威力。第三U-Net的推理速度快可解释性强后处理改造灵活对工业落地和科研复现都友好。相比之下遥感图像语义分割里常用的那些大模型到了CT岩心这种前景占比极低、背景高度均匀的图像上效果未必比U-Net好多少显存消耗却是成倍增加。要从源码层面快速验证方案U-Net是最短路径。3.2 最小可跑的训练源码与关键参数说明在工程里我用PyTorch实现原因很简单torch的Dataset类写起来清晰灵活性高。下面先展示U-Net编码器的核心结构帮助理解为什么它适合裂缝分割。import torch import torch.nn as nn import torch.nn.functional as F class UNetEncoder(nn.Module): def __init__(self, in_ch1, base_ch64): super().__init__() self.conv1 nn.Conv2d(in_ch, base_ch, 3, padding1) self.conv2 nn.Conv2d(base_ch, base_ch, 3, padding1) self.pool nn.MaxPool2d(2) self.conv3 nn.Conv2d(base_ch, base_ch * 2, 3, padding1) self.conv4 nn.Conv2d(base_ch * 2, base_ch * 2, 3, padding1) self.conv5 nn.Conv2d(base_ch * 2, base_ch * 4, 3, padding1) self.conv6 nn.Conv2d(base_ch * 4, base_ch * 4, 3, padding1) def forward(self, x): e1 F.relu(self.conv1(x)) e1 F.relu(self.conv2(e1)) e2 self.pool(e1) e2 F.relu(self.conv3(e2)) e2 F.relu(self.conv4(e2)) e3 self.pool(e2) e3 F.relu(self.conv5(e3)) e3 F.relu(self.conv6(e3)) return e1, e2, e3代码逻辑说明这个编码器就是卷积池化的交替堆叠每次池化后特征图尺寸减半、通道数翻倍e1、e2、e3分别保存三层不同分辨率的特征。U-Net解码时会把这些特征通过跳跃连接与解码端对应尺寸的特征图拼接从而把高分辨率空间信息保留下来这也是裂缝边缘定位准的根本原因。写这个示例是为了讲清原理实际落地我推荐直接用开源U-Net实现避免手写结构出错。训练源码里最核心的参数包括输入裁剪尺寸、批大小、初始学习率、损失函数类型。以下是我在裂缝分割上稳定跑通的一组基线参数建议值说明输入尺寸512×512CT切片过大时用随机裁剪去中心采样批大小8视显卡显存增减8GB显存时不要超过8学习率1e-4Adam优化器下比较保守损失Dice BCE组合裂缝像素占比低时必须用组合损失迭代轮数150-300验证IoU连续不升时提前停止class DiceBCELoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, pred, target): pred torch.sigmoid(pred) bce F.binary_cross_entropy(pred, target, reductionmean) pred_flat pred.view(pred.size(0), -1) target_flat target.view(target.size(0), -1) intersection (pred_flat * target_flat).sum(dim1) dice 1 - (2 * intersection self.smooth) / ( pred_flat.sum(dim1) target_flat.sum(dim1) self.smooth ) return bce dice.mean()代码逻辑说明组合损失把BCE和Dice同时作为学习信号。BCE对每个像素做独立判断即使裂缝像素很少也能提供梯度Dice损失则直接以预测mask与真值mask的重叠程度为优化目标防止网络在裂缝像素占比极低时直接预测全背景。smooth参数是为了避免分母为0默认取1.0在交集很小的小样本场景里也能稳定求导。为什么不用纯Dice损失呢纯Dice损失在裂缝目标占0.5%以下时会因为梯度被少数像素主导而收敛波折训练重启好几次结果都不一样这在分割训练里算一个典型的玄学问题其实背后就是正负样本梯度失衡。4. 裂缝语义分割常见问题与避坑翻车现象、根因与修法4.1 训练loss下降但IoU几乎为零现象训练时loss从0.7降到0.2看着一切正常但每个epoch结束跑验证集IoU一直在0.05以下预测结果全是背景。原因这里往往不是网络结构问题而是数据对齐问题。常见原因是预处理时只对灰度图像做了旋转增强却没对mask做同角度旋转或者训练集和标签来源于不同的切片编号。另一种情况是mask在存盘时被保存软件以有损压缩重写裂缝边缘的标注区域被过度平滑真实像素也发生了偏移。解决在训练集里随机挑10张图把image和mask用numpy横向拼接输出成一张核查图肉眼比对。更稳妥的办法是在数据集加载器里对image和mask做相同的随机变换并把验证集的变换固定到确定性状态。4.2 裂缝像素太少导致loss震荡不收敛现象有几张图裂缝面积占整幅图像不到0.3%训练loss曲线在0.4到0.7之间反复震荡预览预测结果全是背景怎么调学习率都没用。原因像素级正样本占比太低BCE loss被背景像素主导网络更新方向被背景梯度吃掉。这是小目标分割的通病裂缝这类线性结构尤其明显因为裂缝在CT切片里是一条连续细线像素占比极低。解决把loss换成DiceBCE组合同时控制负样本占比。此外可以做一个裂缝patch采样在训练数据加载时优先裁剪包含裂缝像素区域附近的图块而不仅仅是全图随机裁剪。这样每个训练批次里裂缝像素比例从0.3%提升到3%到10%收敛稳定性立刻改善。4.3 灰度归一化范围错误让裂缝消失现象明明同一批数据换了台机器或者换了个读图库之后预测结果变成一片白或者一片黑全图和mask貌似对不上。原因根源在CT数据的位深和灰度范围。16位TIFF里的有效值可能只在1000到4000之间如果代码里直接用了图像的最大最小值做归一化两张切片各自的灰度范围不同模型学习到的映射就完全不可靠。解决固定归一化参数。在训练集上先统计全局的第1和99.9分位数把这两个数字写死到配置里推理时也使用同一组统计值保证训练和推理的预处理完全一致。4.4 推理阶段出现大量碎片化裂缝噪声现象预测出的mask里裂缝旁边散布着很多孤立亮点形态学处理后还是不干净面积不大但数量极多。原因裂缝在CT图像中是低对比度目标网络推理时对每个像素独立输出概率单看某个点的邻域上下文就会把部分噪声点误判成裂缝。另一个来源是训练样本过少网络没有学到裂缝的连续性先验。解决在推理后接一个基于连通域的后处理。将预测概率经过阈值0.5后做一次连通域标记把面积小于设定阈值的连通区域剔除因为真实裂缝几乎不可能只有几个孤立像素构成。面积阈值通常设置为总像素数的0.1%。5. 裂缝分割推理与评估IoU、Dice与滑动窗口后处理5.1 像素级评估指标的正确打开方式分割任务里最常用的指标是IoU和Dice系数但裂缝场景里单纯的IoU有时会骗人。裂缝只有一个类别且面积占比太低如果模型把裂缝预测得稍微宽几个像素IoU会因为交集像素变化剧烈而骤降即使裂缝轮廓的主观效果看起来完全可用。所以我一般不只盯IoU还会同时看边界IoU和裂缝召回率。边界IoU只看边缘像素附近的预测正确率如果预测比真值宽一个像素普通IoU可能掉到0.5以下边界IoU会更敏感地反映出边缘偏移。裂缝召回率则回答一个问题真裂缝里被模型找到的比例是多少。三者的组合才能判断模型到底是位置错、宽度错还是完全漏检。import numpy as np def compute_iou(pred, target, eps1e-6): pred (pred 0.5).astype(np.uint8) inter np.logical_and(pred, target).sum() union np.logical_or(pred, target).sum() return (inter eps) / (union eps) def compute_dice(pred, target, eps1e-6): pred (pred 0.5).astype(np.uint8) inter np.logical_and(pred, target).sum() return (2 * inter eps) / (pred.sum() target.sum() eps)代码逻辑说明先做阈值二值化再统计交集和并集。eps的引入是为了避免空预测或空真值导致分母为0典型的除零保护写法。边界IoU计算时需要先提取边界像素常见做法是用形态学膨胀把边界带留出来再做交集运算这一项我给的权重比普通IoU更高因为裂缝识别最值钱的是位置准确性而不是完整覆盖。5.2 大尺寸CT切片的推理滑动窗口与后处理CT切片原始尺寸往往超过模型输入尺寸比如2048×2048没法直接用整图推理。常规做法是滑动窗口推理以步长裁剪固定大小的图块单独推理再把概率图拼回去。裁剪时必须考虑相邻图块的重叠程度如果重叠为0落在窗口边缘的裂缝会被截断丢失。我的实际建议如下窗口大小固定为训练时的输入尺寸512×512步长设置为窗口大小的1/2即相邻窗口重叠50%保证裂缝连续性。推理完成后再把概率图按位置拼接重叠区域取多次推理的平均值避免边缘处的不连续感。如果显存允许可以用FP16半精度推理显存缩小一半批量处理更多图块。对典型的2560×2048岩心CT切片我用512窗口、步长256、批大小4推理拼回的概率图边界平滑度比不重叠版本好不少。注意推理阶段必须使用与训练阶段完全相同的归一化方式。训练时统计的分位数要写死在配置里推理时不能再重新算一遍全图分布否则两个阶段的输入分布不一致预测结果会系统性偏移。6. 验证分割效果的技巧随机切块可视化与三元检查最常见的验证方式是把预测mask和标注mask按像素叠加成一张三通道图红色通道是预测绿色通道是标注两者重叠则显示为黄色。这张图一眼能看出模型是偏保守还是偏激进是预测多了还是预测少了比看IoU数字直观得多。我每次跑完模型都会出一批这样的对比图数量保持20到30张覆盖不同深度、不同岩性和不同裂缝密度。更进一步的验证是量化单条裂缝的连续性指标。从预测mask的连通域列表里计算最大连通区域的长度、面积、以及它和真值的重合程度这一步可以直接排查模型是否把长裂缝断成几段。若存在这样的问题后处理里增加一步基于裂缝方向的关键点连接往往就能修复。另一个习惯是把验证集切块分类统计。我会抽出一部分随机图块按包含裂缝像素的占比分成0到0.1%、0.1%到1%和1%以上三档逐一统计IoU。这样你会发现模型在低裂缝占比的图块上表现远低于高占比图块如果想继续优化就针对低占比区做patch采样和增强就够了方向比盲目换骨干网络清晰得多。我以前也迷信过换一个更强的解码器就能解决一切后来发现对裂缝这种弱对比细线结构归一化一致性、patch采样的裂缝比例均衡和推理后处理带来的提升要远远大于换backbone。每拿到一批新CT数据我先跑完前几轮检查上面的验证图再谈训练调整这个流程至今还在用。希望帮到你。本文还有配套的精品资源点击获取