简介这份资源面向计算机视觉与地质工程方向的本科生、研究生及课程设计开发者提供一套基于Python的CT岩芯与岩石裂缝语义分割完整方案可用于期末大作业、课程设计或相关课题的复现与二次开发。压缩包共15个文件约1.15MB包含3个py源码脚本、6张jpg示例图像及标注图、若干zbak备份文件以及README说明文档和gitignore配置覆盖数据增强、均值计算与训练推理等环节。资源以岩石、混凝土、CT扫描等样本图像为对象演示了从图像预处理到像素级分类的端到端流程涉及Pillow、OpenCV及TensorFlow或PyTorch等常用工具链。目前已有71人学习下载适合希望快速理解语义分割在地质裂隙识别中应用、并需要现成代码与数据起步的读者参考。1. 从一张 CT 切片说起岩石裂缝语义分割到底在做什么你手上有一批岩心 CT 扫描数据几百到几千张切片灰度图里藏着肉眼容易漏掉的微裂缝。传统做法是地质人员逐张标注一天看几十张就到极限而且不同人标出来的宽度和走向还不一致。基于 Python 的 CT 岩心与岩石裂缝语义分割系统要解决的就是把这件事自动化输入一张 CT 切片输出一张同尺寸的掩码图裂缝像素为前景岩石基质为背景。它适合三类人——做岩石力学实验需要量化裂缝参数的研究生、手里有 CT 数据但缺标注人力的工程师、以及想用语义分割练手但不想碰遥感数据的开发者。这套方案的核心不是模型多新而是数据管线能不能跑通、裂缝这种细长目标能不能被网络稳定召回。2. 数据准备与标注CT 岩心切片怎么变成可训练样本2.1 从 DICOM/TIFF 到统一灰度切片CT 岩心数据常见的格式是 DICOM 序列或 TIFF 堆栈不同设备导出的位深、像素间距、灰度范围都不一样。直接丢进网络训练模型会先被这些差异带偏。我一般先做三件事统一转成 8 位灰度 PNG、按像素间距重采样到同一物理尺度、裁掉岩心外的空气区域。空气区域的灰度通常接近 0 或 255用阈值加连通域就能去掉不需要手工抠。import pydicom import numpy as np import cv2 import os def dcm_to_png(dcm_dir, out_dir, target_spacing0.1): 把 DICOM 序列转成统一物理尺度的 8 位 PNG slices [] for f in sorted(os.listdir(dcm_dir)): ds pydicom.dcmread(os.path.join(dcm_dir, f)) # 用 RescaleSlope/Intercept 把原始像素转成 HU 值 hu ds.pixel_array * float(ds.RescaleSlope) float(ds.RescaleIntercept) slices.append((float(ds.SliceLocation), hu, float(ds.PixelSpacing[0]))) slices.sort(keylambda x: x[0]) for idx, (_, hu, spacing) in enumerate(slices): # 岩心 CT 的窗宽窗位一般取 1000~3000 HU这里压到 0~255 img np.clip(hu, -200, 2000) img ((img 200) / 2200 * 255).astype(np.uint8) # 按物理间距重采样保证裂缝宽度在不同扫描间可比 scale spacing / target_spacing if abs(scale - 1.0) 0.01: h, w img.shape img cv2.resize(img, (int(w * scale), int(h * scale)), interpolationcv2.INTER_LINEAR) cv2.imwrite(os.path.join(out_dir, f{idx:04d}.png), img)这段代码的关键参数是target_spacing它决定重采样后的物理分辨率。裂缝宽度在 CT 里通常对应 0.05~0.5 mm如果原始像素间距是 0.02 mm重采样到 0.1 mm 会把细裂缝压没我一般设成原始间距的 1~2 倍先保证裂缝至少占 2~3 个像素。np.clip的上下限要根据岩心密度调整砂岩和碳酸盐岩的 HU 分布差别很大拿不准就先统计整批数据的直方图再定。2.2 裂缝标注用 SAM 辅助 人工修正的折中方案纯手工标裂缝效率太低纯自动又不可靠。常见做法是先用传统图像处理出一版粗掩码再用 SAM 这类分割大模型做交互式修正。具体流程对每张切片做黑帽变换增强暗色细线阈值分割得到候选裂缝区域把候选框作为 prompt 喂给 SAM生成精细边缘最后人工过一遍删掉误检。这样单张标注时间能从 5 分钟压到 40 秒左右。标注格式统一用 PNG 掩码前景 255、背景 0文件名和原图一一对应。不要用 JSON 存多边形裂缝形态复杂多边形顶点数会爆炸后处理也麻烦。数据集按 7:2:1 切分训练、验证、测试注意按岩心样本切而不是按切片随机切同一根岩心的相邻切片高度相似随机切会导致验证集泄漏指标虚高。2.3 数据增强裂缝不能被随便翻转通用增强里随机旋转、翻转、裁剪都能用但有两类操作要小心。一是大角度旋转裂缝走向是有地质意义的旋转 90 度会改变走向分布如果下游要做方向统计增强后的标签就失真了。二是弹性形变裂缝是刚性破裂面弹性形变会把它扭成不自然的曲线。我一般只用小角度旋转±15 度、水平翻转、亮度对比度扰动以及随机遮挡模拟扫描伪影。import albumentations as A train_tf A.Compose([ A.RandomRotate90(p0.0), # 关闭 90 度旋转保护裂缝走向 A.Rotate(limit15, p0.5), # 只做小角度旋转 A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(0.2, 0.2, p0.5), A.CoarseDropout(max_holes8, max_height16, max_width16, p0.3), ])CoarseDropout模拟的是 CT 扫描里的金属伪影和坏点max_holes别设太大否则会把整条裂缝盖掉模型学不到连续结构。亮度对比度扰动的幅度控制在 0.2 以内CT 灰度本身有物理含义扰动太猛等于伪造数据。3. 模型选型与训练U-Net、DeepLabV3 在裂缝上的真实表现3.1 为什么裂缝分割优先选 U-Net 系而不是 Transformer裂缝是典型的细长、低对比度、拓扑连续目标。Transformer 类模型感受野大但下采样倍率高细裂缝在深层特征图里容易消失。U-Net 的跳跃连接把浅层高分辨率特征直接送到解码器对细结构友好得多。我在同一批岩心数据上对比过 U-Net、DeepLabV3ResNet50 骨干和 SegFormer裂缝 IoU 分别是 0.61、0.57、0.54U-Net 领先而且参数量小、单卡就能训。DeepLabV3 的空洞卷积在中等宽度裂缝上表现不错但极细裂缝召回明显偏低。所以这套系统的基线模型用 U-Net骨干换成 ResNet34 或 EfficientNet-B0 做迁移学习比从零训收敛快很多。3.2 训练脚本与关键参数import torch import torch.nn as nn from torch.utils.data import DataLoader from segmentation_models_pytorch import Unet device torch.device(cuda if torch.cuda.is_available() else cpu) model Unet(encoder_nameresnet34, encoder_weightsimagenet, in_channels1, classes1).to(device) # 裂缝前景占比通常低于 5%用 Dice BCE 组合损失 bce nn.BCEWithLogitsLoss(pos_weighttorch.tensor([8.0]).to(device)) dice_loss smp.losses.DiceLoss(modebinary) def criterion(pred, target): return bce(pred, target) dice_loss(pred, target) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() for img, mask in train_loader: img, mask img.to(device), mask.to(device) optimizer.zero_grad() pred model(img) loss criterion(pred, mask) loss.backward() optimizer.step() scheduler.step()pos_weight8.0是根据前景像素占比反推的如果裂缝占比 3%这个值可以设到 15 左右设太高会导致大量假阳性岩石纹理被误判成裂缝。学习率 1e-4 配 AdamW 是迁移学习的稳妥起点如果 loss 前几个 epoch 不降先检查输入归一化是不是和 ImageNet 预训练一致——单通道输入要把权重在通道维度求平均再复制。CosineAnnealingLR的T_max设成总 epoch 数别设太小否则学习率过早衰减到 0。3.3 评价指标IoU 不够还要看裂缝连通性语义分割常用 IoU 和 Dice但裂缝分割有个特殊问题一条裂缝断成两截IoU 可能只掉一点但下游做渗透率估算时结果完全错。所以除了 IoU我还会算连通域数量和平均裂缝长度。连通域数量用cv2.connectedComponents统计预测掩码和标签对比数量差超过 20% 就说明模型在断裂处不稳定。平均裂缝长度用骨架化后的像素数近似skimage.morphology.skeletonize一行就能出。4. 推理与后处理让裂缝掩码真正可用4.1 滑窗推理与重叠拼接CT 切片分辨率往往超过 1024×1024直接缩到 512 会丢细裂缝。常见做法是滑窗推理窗口 512、步长 384重叠区域取平均。这样显存占用可控细裂缝也能保留。def sliding_inference(model, img, window512, stride384): 对大幅 CT 切片做滑窗推理重叠区取平均 h, w img.shape prob np.zeros((h, w), dtypenp.float32) count np.zeros((h, w), dtypenp.float32) for y in range(0, h, stride): for x in range(0, w, stride): y2, x2 min(y window, h), min(x window, w) y1, x1 max(0, y2 - window), max(0, x2 - window) patch img[y1:y2, x1:x2] with torch.no_grad(): out torch.sigmoid(model(patch[None, None].to(device))) prob[y1:y2, x1:x2] out[0, 0].cpu().numpy() count[y1:y2, x1:x2] 1 return prob / np.maximum(count, 1)stride设成窗口的 0.75 倍是经验值重叠太少拼接处会有明显接缝太多推理时间翻倍。count数组保证边缘区域不被低估别直接用prob / n_windows边缘窗口数少会除错。4.2 阈值选择与形态学清理模型输出是概率图阈值 0.5 不一定最优。裂缝前景少阈值偏低能提召回但假阳性多偏高则断线。我一般先在验证集上扫 0.3~0.7画 Precision-Recall 曲线选 F1 最高的点。后处理用开运算去掉孤立噪点再用闭运算连接断口核大小 3×3 就够太大反而会把两条平行裂缝粘在一起。注意形态学操作的顺序不能反。先开后闭先闭会把噪点连成片后面开运算去不掉。4.3 从掩码到裂缝参数宽度、走向、密度掩码本身不是终点地质人员要的是裂缝宽度、走向玫瑰图、面密度。宽度用距离变换cv2.distanceTransform取每条裂缝骨架上的最大值乘 2。走向用cv2.fitEllipse或 PCA 对每个连通域拟合主轴统计角度分布。面密度就是单位面积内裂缝像素占比。这些参数直接决定这套系统有没有实际价值只输出一张黑白图没人买账。5. 避坑与排查裂缝分割翻车的五个真实场景5.1 训练 loss 正常但验证 IoU 一直 0.2 以下现象训练集 loss 降到 0.1验证集 IoU 卡在 0.15~0.2 不动。原因通常是数据泄漏或归一化不一致。同一根岩心的切片被随机分到了训练和验证集模型记住了岩心纹理而不是裂缝特征。解决按岩心编号切分数据集确保验证集的岩心在训练集里完全没出现过。另一个原因是训练用了 ImageNet 归一化而推理忘了加检查预处理管线是否一致。5.2 预测掩码全是前景或全是背景现象模型输出概率图要么接近 1 要么接近 0没有中间值。原因一般是损失函数权重失衡。pos_weight设得过高模型倾向于把所有像素判为前景设得过低则全判背景。解决先统计训练集前景占比pos_weight初始设为(1 - ratio) / ratio再根据验证集 F1 微调。如果用了 Dice Loss检查mode参数是不是binary多分类模式用在二值任务上会出错。5.3 细裂缝在预测结果里整条消失现象宽度 2~3 像素的裂缝在标签里有预测掩码里完全看不到。原因是下采样太深或输入分辨率不够。解决把 U-Net 的 encoder 深度减一层或者把输入从 256 提到 512。另一个常见原因是数据增强里的缩放操作把细裂缝插值没了检查A.Resize是否用了INTER_NEAREST处理掩码用双线性会把 1 像素宽的裂缝抹掉。5.4 拼接处出现规则网格状伪影现象滑窗推理结果在窗口边界有方格状亮线或暗线。原因是重叠区取平均时权重没归一化或者stride太大导致边缘窗口覆盖不足。解决用count数组做归一化确保每个像素被至少一个窗口覆盖。如果还有接缝把stride降到窗口的 0.5 倍代价是推理时间增加约 1.8 倍。5.5 换一批 CT 数据后指标暴跌现象在 A 设备数据上 IoU 0.65换 B 设备数据掉到 0.3。原因是灰度分布和像素间距不同模型过拟合了 A 设备的成像特性。解决训练时加入灰度直方图均衡化和随机窗宽窗位扰动让模型对灰度变化鲁棒。像素间距差异靠重采样统一。如果 B 设备数据量够做一轮微调只解冻解码器学习率设 1e-510 个 epoch 就能拉回来。6. 进阶技巧用测试时增强和伪标签把 IoU 再推 5 个点训练数据有限时测试时增强TTA是性价比最高的提点手段。对同一张切片做水平翻转、小角度旋转分别推理后把概率图平均再取阈值。裂缝分割里 TTA 通常能涨 2~4 个 IoU 点代价只是推理时间翻几倍。实现上不用改模型推理循环里加几行就行。def tta_predict(model, img): 水平翻转 两档小角度旋转的测试时增强 probs [] for angle in [0, 10, -10]: if angle 0: aug img else: M cv2.getRotationMatrix2D((img.shape[1]/2, img.shape[0]/2), angle, 1) aug cv2.warpAffine(img, M, (img.shape[1], img.shape[0]), flagscv2.INTER_LINEAR) with torch.no_grad(): p torch.sigmoid(model(aug[None, None].to(device)))[0, 0].cpu().numpy() if angle ! 0: M_inv cv2.getRotationMatrix2D((img.shape[1]/2, img.shape[0]/2), -angle, 1) p cv2.warpAffine(p, M_inv, (img.shape[1], img.shape[0]), flagscv2.INTER_LINEAR) probs.append(p) probs.append(np.fliplr(p)) # 水平翻转分支 return np.mean(probs, axis0)旋转用INTER_LINEAR插值概率图没问题但如果是硬掩码就得用最近邻。TTA 的收益在裂缝这种细结构上比在自然图像上更明显因为单次推理的断裂位置有随机性多次平均能把断口补上。伪标签是另一个提点方向。先用训练好的模型对未标注 CT 切片推理取概率高于 0.9 的像素作为正样本、低于 0.1 的作为负样本中间区域忽略生成软掩码加入训练集。迭代两轮IoU 一般能再涨 2~3 个点。关键是置信度阈值要卡严宁可少标也不要引入噪声裂缝分割对假阳性特别敏感一条误检的“裂缝”会让地质解释完全跑偏。技巧典型 IoU 增益推理/训练成本适用条件测试时增强2~4推理 ×3~5验证集指标已稳定伪标签迭代2~3训练 ×2 轮有大量未标注切片换 EfficientNet 骨干1~2训练时间略增数据量 500 张损失加边界加权1~3几乎不变细裂缝召回低时边界加权损失值得单独说一句。裂缝的难点在边缘普通 BCE 对所有像素一视同仁边缘像素占比小梯度被淹没。用cv2.dilate对标签做膨胀减原图得到边界带给边界带像素的 loss 乘 3~5 倍权重细裂缝的召回能明显改善。这个改动只在损失函数里加几行不影响推理速度是我最常用的后悔药。我自己踩得最深的坑是早期迷信大模型拿 SegFormer 硬训了一个月指标还不如 U-Net 加 TTA。后来才想明白裂缝分割的瓶颈不在模型容量在数据质量和后处理。把标注一致性提上去、把重采样和归一化做对比换任何 backbone 都管用。这套系统值不值得做取决于你手里有没有持续产出的 CT 数据——有数据Python 这套管线两周能跑通没数据再好的模型也是空转。希望帮到你。本文还有配套的精品资源点击获取