简介:这份资源面向计算机视觉与地质工程方向的本科生、研究生及课程设计开发者,提供一套基于Python的CT岩芯与岩石裂缝语义分割完整方案,可用于期末大作业、课程设计或相关课题的快速复现与二次开发。压缩包共15个文件,约1.15MB,包含3个py源码脚本、6张jpg示例图像与标注图、若干zbak备份文件及README说明文档,覆盖数据增强、均值计算与训练推理等环节,并附带岩石、混凝土、CT岩芯等多组样本,便于直接跑通流程。目前已有73人学习下载。读者可从中获得端到端的像素级分割实现思路,理解Pillow、OpenCV与TensorFlow/PyTorch在图像预处理和网络构建中的分工,掌握CT断层扫描图像的裂隙识别与量化分析方法,为油气勘探、地质构造研究等场景的定量分析提供可借鉴的技术支撑。
1. CT岩心裂缝语义分割:从灰度切片到可训练掩码的完整链路
拿到一批工业 CT 扫描的岩心切片,想自动把裂缝、孔隙、基质分开,这件事在石油地质和岩石力学圈子里需求很实在。CT 岩心图像的特点是灰度对比弱、裂缝细且走向随机、还常伴随环状伪影和束硬化,人眼标注一张 1024×1024 的切片动辄十几分钟,几百张就是灾难。语义分割模型能把这件事从"逐像素手描"变成"训练一次、批量推理",而 Python 生态里的 PyTorch + 分割网络正好是当前最成熟的落地路径。这个标题讲的就是:用 Python 搭一套从数据准备、模型训练到裂缝掩码输出的语义分割系统,配套岩心 CT 数据集。适合做岩石物理实验、数字岩心分析、地质图像处理的工程师,也适合刚学完 Python 想找一个真实分割项目练手的人。下面按数据、模型、训练、避坑、进阶的顺序把整条链路拆开。
2. 岩心 CT 数据集怎么准备:从原始切片到可训练掩码
2.1 先搞清楚你的 CT 数据是什么形态
工业 CT 输出的常见格式有 DICOM、TIFF 序列、RAW 体数据,还有部分设备直接给 PNG 切片。做语义分割前必须确认三件事:位深、灰度范围、切片间距。位深决定你要不要做归一化,8 位图直接除以 255,16 位图得先看直方图再决定窗宽窗位。灰度范围影响裂缝和基质的可分性,很多岩心 CT 里裂缝灰度只比基质低 10 到 30 个灰度级,直接送进网络会被当成噪声。切片间距决定你能不能做三维上下文,间距大于 1mm 时层间相关性弱,强行上 3D 网络收益不大,不如老老实实做 2D 分割再堆叠。
我一般会先写个小脚本把一批切片的统计量打出来,确认没有异常切片混进来。这一步不做,后面训练 loss 不降你会怀疑人生。
import numpy as np from pathlib import Path from PIL import Image def scan_slices(folder, sample_n=20): files = sorted(Path(folder).glob("*.png"))[:sample_n] for f in files: img = np.array(Image.open(f)) print(f"{f.name} | shape={img.shape} | dtype={img.dtype} " f"| min={img.min()} max={img.max()} mean={img.mean():.1f} " f"| p1={np.percentile(img,1):.0f} p99={np.percentile(img,99):.0f}") scan_slices("./ct_slices")这段代码做的是抽样统计,sample_n控制抽样数量,先看 20 张足够判断整体分布。重点看p1和p99,如果两者差距很小说明对比度极低,需要做 CLAHE 或者直方图拉伸。dtype是 uint16 的话,后面归一化不能直接除 255,要按实际位深处理。
2.2 标注格式选型:PNG 掩码还是 COCO JSON
语义分割的标注格式主流有两种:单通道 PNG 掩码(像素值即类别 id)和 COCO JSON(多边形坐标)。岩心裂缝这种细长目标,我强烈建议用 PNG 掩码。原因是裂缝宽度经常只有 1 到 3 个像素,多边形标注在细长区域容易产生自交和漏点,转成掩码后边缘锯齿严重。PNG 掩码用 LabelMe 或者 CVAT 画完多边形导出即可,也可以直接用 ITK-SNAP 在体数据上刷。
类别定义建议从简:0 背景(基质)、1 裂缝、2 孔隙。类别超过 4 个时,岩心 CT 的标注一致性会急剧下降,不同标注员对"微裂隙"和"孔隙"的边界理解不一致,训练出来的模型在验证集上看着好,换一批岩心就崩。常见做法是先做二分类(裂缝 vs 非裂缝),跑通后再考虑细分。
| 格式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| PNG 掩码 | 细长目标、像素级精度 | 无损、直接对应类别 id | 需要统一尺寸 |
| COCO JSON | 目标检测+分割联合 | 生态工具多 | 细长目标多边形易自交 |
| NPY 数组 | 科研快速迭代 | 加载快、支持 float | 不通用、难可视化 |
2.3 数据增强:岩心 CT 不能随便翻转
通用分割增强里的随机旋转、翻转,在岩心 CT 上要谨慎。岩心是有层理方向的,水平翻转会破坏沉积构造的物理意义,垂直翻转更离谱。我一般只开三类增强:小角度旋转(±10 度以内)、亮度对比度扰动、弹性形变(幅度调小)。弹性形变对裂缝这种细结构有帮助,但alpha和sigma设大了会把裂缝拉断,反而制造错误标签。
import albumentations as A train_tf = A.Compose([ A.RandomRotate90(p=0.0), # 岩心禁用90度旋转 A.Rotate(limit=10, border_mode=0, p=0.5), # 小角度旋转 A.RandomBrightnessContrast(0.2, 0.2, p=0.5), # 灰度扰动 A.ElasticTransform(alpha=30, sigma=5, p=0.3), # 小幅弹性形变 A.Resize(512, 512), ]) val_tf = A.Compose([A.Resize(512, 512)])limit=10是旋转角度上限,超过 15 度岩心纹理方向就失真了。alpha=30, sigma=5是弹性形变的位移幅度和光滑度,这两个值是我在几批砂岩 CT 上试出来的,再大裂缝会被扯断。border_mode=0表示旋转后边界填 0,和背景类别一致,避免引入假边缘。
3. 分割模型选型与训练:U-Net、DeepLabV3+ 还是 SegFormer
3.1 岩心裂缝分割为什么 U-Net 仍然是首选
裂缝分割的核心难点是细结构保持。Transformer 类模型(SegFormer、Swin-UNet)在全局上下文上强,但下采样倍率高,1 到 2 像素宽的裂缝在 1/16 特征图上直接消失。U-Net 的跳跃连接把编码器的高分辨率特征直接送到解码器,对细结构友好,这是它在医学和地质分割里长期占位的根本原因。DeepLabV3+ 的空洞卷积能扩大感受野又不丢分辨率,理论上也适合,但 ASPP 模块参数量大,小数据集上容易过拟合。
我的选型逻辑是:数据量小于 500 张,用 U-Net + ResNet34 编码器;500 到 2000 张,可以上 U-Net++ 或者 DeepLabV3+;超过 2000 张且算力充足,再考虑 SegFormer。岩心 CT 公开数据集普遍不大,所以 U-Net 系是稳妥起点。
3.2 用 segmentation_models_pytorch 搭一个最小可训练版本
不重复造轮子,segmentation_models_pytorch(smp)把主流编码器和解码头都封装好了,改几行就能换骨干。下面是一个完整的最小训练脚本骨架。
import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader import segmentation_models_pytorch as smp import albumentations as A from PIL import Image import numpy as np class CoreDataset(Dataset): def __init__(self, img_dir, mask_dir, tf): self.imgs = sorted(Path(img_dir).glob("*.png")) self.mask_dir = Path(mask_dir) self.tf = tf def __len__(self): return len(self.imgs) def __getitem__(self, idx): img = np.array(Image.open(self.imgs[idx]).convert("L")) img = np.stack([img]*3, axis=-1) # 单通道转3通道喂给预训练编码器 mask = np.array(Image.open(self.mask_dir / self.imgs[idx].name)) aug = self.tf(image=img, mask=mask) img = aug["image"].transpose(2, 0, 1).astype(np.float32) / 255.0 mask = aug["mask"].astype(np.int64) return torch.from_numpy(img), torch.from_numpy(mask) model = smp.Unet( encoder_name="resnet34", encoder_weights="imagenet", in_channels=3, classes=2, # 背景 + 裂缝 )encoder_weights="imagenet"用预训练权重,小数据集上比从头训收敛快得多。classes=2对应二分类,如果做背景/裂缝/孔隙三分类就改成 3。单通道 CT 转 3 通道是为了适配 ImageNet 预训练编码器的输入要求,这是常见做法,不是必须,但省事。
3.3 损失函数与评估指标:裂缝分割别只看 IoU
裂缝像素占比通常不到 5%,交叉熵会被背景主导,模型学会全预测背景就能拿到 95% 准确率。必须用组合损失:Dice + BCE,或者 Focal + Dice。Dice 直接优化重叠度,对类别不平衡鲁棒。
dice_loss = smp.losses.DiceLoss(mode="multiclass") ce_loss = nn.CrossEntropyLoss() def criterion(pred, target): return 0.5 * dice_loss(pred, target) + 0.5 * ce_loss(pred, target) # 评估用 IoU 和 F1,按类别算 iou_metric = smp.metrics.get_stats评估时一定要分类别看 IoU。裂缝类的 IoU 才是真正反映模型能不能用的指标,背景 IoU 再高也没意义。我见过背景 IoU 0.98、裂缝 IoU 0.15 的模型,报告里只写平均 IoU 0.56,实际完全不能用。
3.4 训练参数怎么设:学习率、batch size、迭代轮数
学习率用 1e-4 起步,配合 CosineAnnealing 或者 ReduceLROnPlateau。batch size 受显存限制,512×512 输入下 8GB 显存大概能跑 batch 4 到 8。迭代轮数看数据量,500 张图训 100 到 150 epoch 通常够,早停 patience 设 20。优化器 AdamW 比 SGD 在小数据集上更稳,weight decay 设 1e-4。
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)T_max=100是余弦退火的周期,和总 epoch 对齐。如果中途发现验证集裂缝 IoU 连续 20 轮不涨,直接停,继续训只会过拟合。
4. 裂缝分割避坑:那些让模型"看着能跑其实没用"的坑
4.1 现象:训练 loss 正常下降,裂缝 IoU 始终低于 0.2
原因通常是类别极度不平衡加上损失函数选错。纯交叉熵下模型倾向于预测背景,loss 看着降其实是背景拟合好了。解决方法是换 Dice + CE 组合损失,同时在数据采样时对含裂缝的切片做加权,让每个 batch 里裂缝样本占比不低于 30%。
4.2 现象:验证集效果好,换一批岩心就崩
这是域偏移问题。不同设备、不同扫描参数、不同岩性(砂岩 vs 碳酸盐岩)的 CT 灰度分布差异很大。解决方法是训练时加入强灰度增强(Gamma 变换、CLAHE),推理前对输入做直方图匹配,把目标域图像的灰度分布对齐到训练域。如果条件允许,在新岩心上标注 20 到 30 张做微调,效果立竿见影。
4.3 现象:裂缝断断续续,细的地方直接消失
原因是下采样丢信息。U-Net 编码器一般下采样 5 次,1 像素宽的裂缝在 1/32 特征图上就没了。解决办法有三个:把输入分辨率从 256 提到 512 或 1024;减少下采样次数(用 ResNet18 代替 ResNet34);在损失里加边界加权,让模型更关注裂缝边缘像素。我一般先提分辨率,成本最低。
4.4 现象:推理结果有大量孤立小斑点
这是后处理没做。模型输出的概率图直接取 argmax 会产生很多假阳性小区域。用连通域分析过滤掉面积小于阈值的区域,阈值按实际裂缝最小面积设,一般 20 到 50 像素。
import cv2 def remove_small(mask, min_area=30): n, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8) out = np.zeros_like(mask) for i in range(1, n): if stats[i, cv2.CC_STAT_AREA] >= min_area: out[labels == i] = 1 return outmin_area=30是经验值,岩心 CT 里真实裂缝连通域通常大于这个数,小于的基本是噪声。这个值要根据你的像素分辨率调,分辨率高就调大。
4.5 现象:显存够但训练速度极慢
常见原因是数据加载成了瓶颈。num_workers设 0 时数据在主进程加载,GPU 大量时间在等数据。设成 4 到 8,配合pin_memory=True。另外检查增强里有没有特别慢的操作,弹性形变在大图上很吃 CPU,可以降低触发概率。
5. 从掩码到定量分析:裂缝参数提取与模型验证
5.1 用骨架化提取裂缝长度和走向
分割出掩码只是第一步,地质分析要的是裂缝长度、开度、走向玫瑰图。用skimage.morphology.skeletonize把裂缝掩码细化成单像素骨架,再统计骨架像素数得到总长度,用连通域的主成分方向得到走向。
from skimage.morphology import skeletonize from skimage.measure import label, regionprops import numpy as np def crack_stats(mask, pixel_size_mm=0.05): skel = skeletonize(mask > 0) length_px = skel.sum() length_mm = length_px * pixel_size_mm lbl = label(mask > 0) props = regionprops(lbl) angles = [] for p in props: if p.area < 30: continue # 用区域协方差矩阵的主方向近似走向 y, x = np.where(lbl == p.label) coords = np.stack([x - x.mean(), y - y.mean()]) cov = np.cov(coords) eigvals, eigvecs = np.linalg.eigh(cov) v = eigvecs[:, -1] angles.append(np.degrees(np.arctan2(v[1], v[0])) % 180) return length_mm, anglespixel_size_mm是 CT 体素的实际物理尺寸,必须从扫描参数里拿到,否则长度没有物理意义。angles是每个裂缝连通域的走向角,画成玫瑰图就是地质上要的裂缝方位分布。area < 30过滤掉噪声连通域,和前面后处理的阈值保持一致。
5.2 验证模型是否可信:三个必做检查
第一,拿几张训练集里没见过的切片,人工标注后和模型输出做像素级对比,算裂缝类 IoU,低于 0.5 就别急着上生产。第二,检查模型在裂缝交叉点和端点处的表现,这些位置最容易断。第三,把模型输出和原始 CT 叠加显示,看有没有系统性偏移,比如整体往一个方向偏一两个像素,那可能是数据增强里旋转引入的插值误差。
我自己的习惯是每训完一个模型,先挑 5 张最难的切片(裂缝最细、对比度最低的)单独跑一遍,肉眼过一遍再决定要不要继续调。这个习惯帮我省了很多次"指标好看但实际不能用"的返工。裂缝分割这件事,指标是参考,肉眼验证才是后悔药。希望帮到你。
本文还有配套的精品资源,点击获取