ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python岩石裂缝CT岩心语义分割:从数据标注到U-Net训练与裂缝量化全流程

2026/9/23 20:15:39 拓冰建站 浏览量
Python岩石裂缝CT岩心语义分割:从数据标注到U-Net训练与裂缝量化全流程 简介这份资源面向地质、石油工程与计算机视觉方向的学习者和研究人员提供一套基于Python的岩石裂缝与CT岩心裂缝语义分割完整实践材料帮助读者掌握从数据读取、模型训练到分割评估的全流程。压缩包共10个文件约1.12MB包含6张jpg示例图像岩石、混凝土及CT岩心原图与对应标注图、3个py脚本数据增强与均值计算等预处理工具以及1个md说明文档结构紧凑、便于快速上手。目前已有207人学习下载。资源以U-Net等编码器-解码器架构为核心覆盖数据预处理、模型定义、训练循环、预测评估与IoU指标计算等关键环节读者可据此复现裂缝像素级分割实验理解CT岩心无损检测中裂缝识别对储层渗透率与储油能力分析的意义并在此基础上调整超参数、更换损失函数或引入跳跃连接与模型融合策略提升分割精度。1. 岩石裂缝语义分割从 CT 岩心切片到像素级裂缝掩码岩心 CT 扫描出来的切片灰度分布往往很窄裂缝和基质之间的对比度低到肉眼都要反复调窗宽窗位才能分辨。传统做法是阈值分割加形态学后处理参数一换岩性就崩换一批岩心又得重新调。基于 Python 的岩石裂缝与 CT 岩心裂缝语义分割要解决的就是把这条链路从「人工调参」换成「模型学特征」输入一张岩心切片输出一张同尺寸的二值掩码裂缝像素为 1基质为 0。这套方案适合三类人做岩石力学与地质表征、手里有 CT 切片但缺标注工具的研究人员想用语义分割练手、需要真实工业数据集的算法工程师以及要把裂缝识别嵌进岩心数字化流程的开发者。它不追求 SOTA 指标追求的是一套能跑通、能复现、能替换自己数据的完整源码加数据集。读完你应该能判断这套东西值不值得投入以及怎么在自己的岩心数据上落地。2. 语义分割在岩心裂缝上的选型为什么不是阈值和实例分割2.1 裂缝分割的本质是二分类像素任务岩石裂缝在 CT 切片里的形态有几个特点宽度从一两个像素到几十像素不等走向随机经常和层理、矿物条带、扫描伪影混在一起。阈值法Otsu、自适应阈值依赖灰度直方图的双峰性而岩心 CT 的裂缝填充物可能是高密度矿物也可能是空气灰度方向不固定双峰假设经常不成立。语义分割把这个问题建模成逐像素二分类网络学的是局部纹理和上下文而不是单一灰度阈值。裂缝不管亮还是暗只要纹理模式一致模型就能抓出来。这也是为什么语义分割算法在岩心这类低对比度、形态多变的场景里比传统图像处理稳。这里要区分一个高频混淆点yolo26 中实例分割与语义分割的区别。实例分割给每个裂缝对象单独编号适合数裂缝条数语义分割只标「是不是裂缝」适合算裂缝面积占比、做裂缝网络骨架提取。岩心裂缝经常连成网状实例边界本身就模糊硬做实例分割标注成本高、收益低。所以这套源码选语义分割是合理的如果你的下游任务是统计单条裂缝长度才需要考虑往实例方向改。2.2 U-Net 系结构为什么仍是岩心分割的默认起点岩心 CT 数据集通常不大几百到几千张切片量级标注还是像素级的成本极高。这种情况下deeplabv3 语义分割这类带空洞卷积和强预训练 backbone 的模型容易过拟合而 U-Net 的编码器-解码器加跳跃连接在小数据集上收敛更稳跳跃连接能把浅层的高频边缘信息直接送到解码端对细裂缝的定位帮助明显。常见做法是编码器用 ResNet34 或 VGG16 的预训练权重初始化解码器从零训练损失函数用 BCE 加 Dice 的组合。BCE 管像素级分类Dice 管整体重叠度裂缝像素占比低的时候 Dice 能缓解类别不平衡。如果你的数据里裂缝占比低于 2%可以再叠加 Focal Loss 或给正样本加权。2.3 从零搭一套可复现的训练环境先确认 Python 环境。python 安装教程满天飞但做分割建议直接上 3.9 到 3.10太新的版本某些 CUDA 轮子还没跟上。用 conda 建独立环境避免和系统包打架conda create -n rockseg python3.10 -y conda activate rockseg # 按本机 CUDA 版本装 torch下面以 cu118 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy pillow matplotlib tqdm scikit-learn装完验证一下 GPU 是否可用这步别省很多「训练慢」的问题其实是跑在 CPU 上import torch print(torch.__version__) print(torch.cuda.is_available()) # 必须是 True print(torch.cuda.get_device_name(0)) # 确认显卡型号参数说明--index-url指向 PyTorch 官方轮子源cu118 对应 CUDA 11.8如果你装的是 CUDA 12.x 就换成 cu121。torch.cuda.is_available()返回 False 时先查驱动版本和 CUDA 运行时是否匹配别急着改代码。2.4 数据集目录结构与标注格式约定语义分割数据集最常见的翻车点是路径和掩码格式不统一。这套方案按下面的结构组织源码里的 Dataset 类直接按这个读dataset/ images/ # 原图png 或 jpg slice_0001.png slice_0002.png masks/ # 掩码单通道 png裂缝255基质0 slice_0001.png slice_0002.png掩码必须是单通道、和原图同名、像素值只有 0 和 255 两种。如果你用 LabelMe 标注导出后是 JSON需要转成掩码转换时注意label到像素值的映射别把背景也标成 255。下面这段是常见的 JSON 转掩码逻辑import json import numpy as np import cv2 def labelme_to_mask(json_path, out_path, target_labelcrack): with open(json_path, r, encodingutf-8) as f: data json.load(f) h, w data[imageHeight], data[imageWidth] mask np.zeros((h, w), dtypenp.uint8) for shape in data[shapes]: if shape[label] ! target_label: continue pts np.array(shape[points], dtypenp.int32) cv2.fillPoly(mask, [pts], 255) # 裂缝区域填 255 cv2.imwrite(out_path, mask)逻辑说明fillPoly把多边形内部填成 255多个裂缝多边形会叠加。参数上target_label要和标注时的标签名完全一致大小写敏感。如果一张图里有多种裂缝类型张开缝、充填缝要么合并成一个类要么改成多通道掩码后者训练时输出通道数也要跟着改。3. 训练流程拆解从数据加载到裂缝掩码输出3.1 Dataset 与增强岩心切片不能随便翻转岩心 CT 切片有明确的方向性垂直裂缝和水平层理在空间上不等价。随机 90 度旋转和上下翻转会破坏这种先验模型可能学到错误的方向特征。我一般只用水平翻转、小角度旋转±15 度、亮度对比度扰动这几类增强对岩心数据是安全的。import cv2 import numpy as np import torch from torch.utils.data import Dataset class RockCrackDataset(Dataset): def __init__(self, img_dir, mask_dir, filenames, img_size512, augmentFalse): self.img_dir img_dir self.mask_dir mask_dir self.filenames filenames self.img_size img_size self.augment augment def __len__(self): return len(self.filenames) def __getitem__(self, idx): name self.filenames[idx] img cv2.imread(f{self.img_dir}/{name}, cv2.IMREAD_GRAYSCALE) mask cv2.imread(f{self.mask_dir}/{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) # 掩码必须最近邻 if self.augment: if np.random.rand() 0.5: img cv2.flip(img, 1) mask cv2.flip(mask, 1) angle np.random.uniform(-15, 15) M cv2.getRotationMatrix2D((self.img_size/2, self.img_size/2), angle, 1) img cv2.warpAffine(img, M, (self.img_size, self.img_size)) mask cv2.warpAffine(mask, M, (self.img_size, self.img_size), flagscv2.INTER_NEAREST) img img.astype(np.float32) / 255.0 mask (mask 127).astype(np.float32) return torch.from_numpy(img).unsqueeze(0), torch.from_numpy(mask).unsqueeze(0)逻辑说明掩码 resize 和旋转必须用INTER_NEAREST用双线性插值会在裂缝边缘造出 0 到 255 之间的灰值二值化后边缘会漂移。参数img_size建议 512岩心切片原始分辨率高的话先裁块再缩放直接缩到 256 会丢细裂缝。3.2 损失函数与训练循环的关键参数裂缝像素占比低纯 BCE 会让模型倾向全预测背景。用 BCE 加 Dice 的组合Dice 对前景占比不敏感import torch import torch.nn as nn class BCEDiceLoss(nn.Module): def __init__(self, bce_weight0.5): super().__init__() self.bce nn.BCEWithLogitsLoss() self.bce_weight bce_weight def forward(self, logits, targets): bce_loss self.bce(logits, targets) probs torch.sigmoid(logits) intersection (probs * targets).sum(dim(1, 2, 3)) union probs.sum(dim(1, 2, 3)) targets.sum(dim(1, 2, 3)) dice_loss 1 - (2 * intersection 1e-6) / (union 1e-6) return self.bce_weight * bce_loss (1 - self.bce_weight) * dice_loss.mean()参数说明bce_weight默认 0.5如果验证集上裂缝召回率明显偏低把它降到 0.3让 Dice 主导如果预测出大量假裂缝调回 0.6 以上。1e-6是防止除零的平滑项别去掉。训练循环里几个必调参数学习率用 1e-4 起步配合 CosineAnnealingLRbatch size 受显存限制512 尺寸下 8 到 16 比较常见epoch 数看数据量几百张切片通常 100 到 200 轮早停看验证集 Dice。from torch.optim import Adam from torch.optim.lr_scheduler import CosineAnnealingLR model UNet(in_channels1, out_channels1).cuda() criterion BCEDiceLoss(bce_weight0.5) optimizer Adam(model.parameters(), lr1e-4) scheduler CosineAnnealingLR(optimizer, T_max100) for epoch in range(150): model.train() for img, mask in train_loader: img, mask img.cuda(), mask.cuda() optimizer.zero_grad() logits model(img) loss criterion(logits, mask) loss.backward() optimizer.step() scheduler.step()逻辑说明logits是没经过 sigmoid 的原始输出损失函数内部自己处理别在外面先 sigmoid 再喂进去否则 BCEWithLogitsLoss 会算错。每轮结束记得在验证集上算 Dice 和 IoU只存验证指标最好的权重别存最后一轮。3.3 推理与后处理把概率图变成裂缝掩码推理输出是 0 到 1 的概率图阈值 0.5 是默认值但岩心数据上 0.4 到 0.6 之间调一调往往有惊喜。后处理做两件事去掉面积过小的连通域噪点填补裂缝内部的小孔洞。import cv2 import numpy as np import torch def predict_mask(model, img_path, img_size512, thresh0.5, min_area30): model.eval() img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) h, w img.shape inp cv2.resize(img, (img_size, img_size)).astype(np.float32) / 255.0 inp torch.from_numpy(inp).unsqueeze(0).unsqueeze(0).cuda() with torch.no_grad(): prob torch.sigmoid(model(inp))[0, 0].cpu().numpy() prob cv2.resize(prob, (w, h)) binary (prob thresh).astype(np.uint8) * 255 # 去小连通域 num, labels, stats, _ cv2.connectedComponentsWithStats(binary, connectivity8) clean np.zeros_like(binary) for i in range(1, num): if stats[i, cv2.CC_STAT_AREA] min_area: clean[labels i] 255 # 闭运算填小孔 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) clean cv2.morphologyEx(clean, cv2.MORPH_CLOSE, kernel) return clean参数说明thresh控制灵敏度调低召回高但假阳性多min_area按你的裂缝最小尺度设太小去不掉噪点太大会把细裂缝整条删掉。闭运算核别超过 5x5否则会把两条邻近裂缝粘成一条。4. 避坑与排查岩心裂缝分割最常见的 5 个翻车点4.1 掩码插值用了双线性裂缝边缘整体偏移现象训练 loss 正常下降但预测掩码比真实裂缝宽一圈或窄一圈Dice 卡在 0.6 上不去。原因数据加载时对掩码用了默认的双线性插值边缘产生了中间灰值二值化后边界漂移。解决所有对掩码的 resize、旋转、仿射变换统一用INTER_NEAREST检查一遍 Dataset 里每一处涉及掩码的几何变换。4.2 正负样本极度不平衡模型全预测背景现象训练几轮后 loss 降到很低但预测出来全是黑图Dice 接近 0。原因裂缝像素占比可能只有 1% 到 3%纯 BCE 下模型发现全预测 0 就能拿到很低的 loss。解决换成 BCE 加 Dice 组合损失或者给 BCE 加pos_weight参数值设成负样本数除以正样本数。验证时盯 Dice 和召回率别只看 loss。4.3 训练集和验证集来自同一块岩心指标虚高现象验证集 Dice 0.9换一批新岩心切片预测惨不忍睹。原因随机划分把同一块岩心的相邻切片分到了训练和验证两边切片之间高度相似等于变相泄漏。解决按岩心样本划分同一块岩心的所有切片要么全在训练集要么全在验证集。这是岩心数据最容易被忽略的坑指标掉 20 个点都正常。4.4 显存不够就无脑降分辨率细裂缝直接消失现象把 img_size 从 512 降到 256 后能训了但宽度一两个像素的细裂缝完全预测不出来。原因下采样把细裂缝的信息抹掉了U-Net 再强也救不回已经丢失的高频。解决优先用梯度累积或混合精度省显存实在要降分辨率就改成滑窗裁块训练保持原始分辨率推理时再拼回去。4.5 推理阈值照搬 0.5不同岩性表现差异大现象同一套权重砂岩岩心预测正常碳酸盐岩岩心裂缝断断续续。原因不同岩性的裂缝对比度不同模型输出的概率分布整体偏移固定 0.5 阈值不通用。解决在验证集上按岩性分组扫一遍阈值选每组 Dice 最高的值推理时按岩性选阈值。嫌麻烦就统一用 0.4召回优先后处理再去噪。5. 把裂缝掩码用起来面积统计、骨架提取与迁移到自己的数据训出掩码只是第一步真正体现价值的是下游量化。裂缝面积占比直接对掩码求和除以总像素但更有用的是裂缝骨架能算长度、走向和分叉。用skimage的 skeletonize 提取单像素骨架再统计骨架像素数近似裂缝总长import numpy as np from skimage.morphology import skeletonize def crack_metrics(mask): binary mask 127 area_ratio binary.sum() / binary.size # 裂缝面积占比 skeleton skeletonize(binary) length_px skeleton.sum() # 骨架像素数≈裂缝长度 # 用连通域数量近似裂缝条数 from scipy import ndimage num_cracks ndimage.label(binary)[1] return { area_ratio: round(float(area_ratio), 4), length_px: int(length_px), num_cracks: int(num_cracks), }参数说明skeletonize对二值图操作输入必须是 bool 数组。length_px是像素单位要换算成物理长度得乘上 CT 切片的像素分辨率mm/pixel这个值在扫描参数里查。num_cracks用连通域数量近似网状裂缝会偏少只做趋势参考。迁移到自己的数据按这个顺序走先把你的切片统一成灰度单通道、统一分辨率再用 LabelMe 标 20 到 30 张做小样本验证确认标注格式和源码对得上然后跑一遍训练看 Dice 能不能到 0.7 以上到不了就回头查标注质量和数据划分最后再扩标注量。别一上来就标几百张标注规范没定下来返工成本极高。我自己的习惯是每换一批岩心先拿 5 张切片做推理可视化把原图、概率热力图、二值掩码并排看一遍比盯指标直观得多。裂缝断在哪、假阳性出在哪一眼就能看出来。这套流程跑顺之后单块岩心的裂缝量化从半天缩到十几分钟值得投入。希望帮到你。本文还有配套的精品资源点击获取