CT岩心裂缝语义分割源码实战:数据增强、U-Net训练与避坑指南
发布时间:2026/10/1 3:23:06来源:尧图网络
简介这份资源面向计算机视觉入门者、地质图像分析方向的学生以及需要完成期末大作业或课程设计的学习者提供一套基于Python的岩石裂缝与CT岩心裂缝语义分割完整方案。包内共15个文件以jpg示例图像、py源码脚本、zbak备份文件及md说明文档为主压缩包约1.15MB涵盖数据增强、均值计算与训练推理等环节并附带岩石、混凝土、CT岩心等多组原图与标注掩码便于直接对照调试。已有72人学习下载。读者可借此掌握从Pillow、OpenCV基础处理到TensorFlow或PyTorch搭建分割网络的端到端流程理解像素级分类在断层扫描图像裂隙识别中的落地方式同时获得可复用的数据组织与脚本结构为油气勘探、地质构造研究等场景的定量分析提供参考。1. 拿到这套 CT 岩心裂缝分割源码先搞清楚它能跑出什么结果地质和油气勘探方向的朋友应该都有体会CT 岩芯扫描能无损拿到岩样内部结构但扫描出来的灰度图里裂缝、孔隙、基质混在一起靠人眼在几百张切片上逐张勾裂缝既慢又容易漏。这套基于 Python 的岩石裂缝与 CT 岩心裂缝语义分割源码加数据集解决的就是这件事——把 CT 切片和岩石表面照片里的裂缝区域按像素级分类出来输出一张和原图同尺寸的掩膜图。资源包里给的东西很实在dataset-kit目录下是数据组织脚本amplifyData.py和amplifyData-16.py负责数据增强calc-mean.py用来算数据集的均值和标准差example目录里放了rock.jpg、rock_gt.jpg、concrete.jpg、concrete_gt.jpg、CT.jpg、CT_gt.jpg六张配对样本_gt后缀的就是标注掩膜。README 和几个.zbak备份文件说明这套代码被反复改过不是一次成型的玩具。它适合谁做课程设计、期末大作业的学生能直接拿现成的配对数据和训练脚本改做地质图像分析的工程师可以把它当成一个 baseline换成自己的岩心数据继续训。不适合指望开箱即出论文级精度的人——数据量摆在那后面我会讲清楚它的边界在哪。2. 数据管线拆解从 CT.jpg 到可训练张量的四步2.1 先看懂 example 里的配对逻辑example目录是整个资源里最值得先花时间看的部分。六张图其实是三组配对rock.jpg配rock_gt.jpgconcrete.jpg配concrete_gt.jpgCT.jpg配CT_gt.jpg。原图是 RGB 三通道掩膜是单通道灰度图裂缝区域是白色像素值 255背景是黑色0。这种二值掩膜是语义分割里最基础的格式训练时通常把白区当正类、黑区当背景。常见做法是先用 Pillow 或 OpenCV 把每对图读进来确认尺寸一致、掩膜确实是二值。我一般会先跑一段检查脚本避免后面训练时才发现某张掩膜是三通道或者尺寸对不上那种问题排查起来很费时间。import cv2 import numpy as np import os # 检查 example 目录下原图与掩膜的配对情况 pairs [(rock.jpg, rock_gt.jpg), (concrete.jpg, concrete_gt.jpg), (CT.jpg, CT_gt.jpg)] for img_name, gt_name in pairs: img cv2.imread(os.path.join(example, img_name)) gt cv2.imread(os.path.join(example, gt_name), cv2.IMREAD_GRAYSCALE) # 原图尺寸与掩膜尺寸必须一致否则无法做像素级对齐 print(img_name, img shape:, img.shape, gt shape:, gt.shape) # 统计掩膜里的唯一值正常应该只有 0 和 255 unique_vals np.unique(gt) print(gt_name, unique values:, unique_vals) # 计算裂缝像素占比占比过低说明样本不均衡训练时要考虑加权 crack_ratio np.sum(gt 255) / gt.size print(gt_name, crack pixel ratio: {:.4f}.format(crack_ratio))这段代码做了三件事读图、验证尺寸、统计正类占比。cv2.IMREAD_GRAYSCALE这个参数很关键不加的话掩膜会被读成三通道后面算 loss 时维度对不上直接报错。crack_ratio这个值如果低于 0.05说明裂缝像素很少训练时正负样本严重不均衡得在 loss 里加权重或者用 dice loss不然模型会倾向于全预测成背景accuracy 看着很高但裂缝一个都分不出来。2.2 amplifyData.py 到底增强了什么数据增强是这套资源里比较实用的部分。amplifyData.py和amplifyData-16.py两个脚本从命名看一个是通用版、一个是针对 16 位 CT 图像的版本。CT 扫描原始数据常见是 16 位灰度动态范围比 8 位大得多直接当 8 位处理会丢很多细节所以单独出一个 16 位版本是合理的。增强手段一般跑不出这几类旋转、翻转、裁剪、亮度对比度扰动。对岩石裂缝这种任务旋转和翻转是安全的因为裂缝方向没有固定语义但亮度扰动要小心CT 图像的灰度值和岩石密度直接相关乱调亮度可能把基质调成裂缝的灰度反而制造错误标签。import cv2 import numpy as np import os import random def augment_pair(img, gt): 对原图和掩膜做同步增强保证几何变换一致 # 随机水平翻转 if random.random() 0.5: img cv2.flip(img, 1) gt cv2.flip(gt, 1) # 随机垂直翻转 if random.random() 0.5: img cv2.flip(img, 0) gt cv2.flip(gt, 0) # 随机旋转 90 度的整数倍避免插值引入虚假边缘 k random.randint(0, 3) if k 0: img np.rot90(img, k) gt np.rot90(gt, k) return img.copy(), gt.copy() # 对 example 里的 CT 图做一轮增强演示 img cv2.imread(example/CT.jpg) gt cv2.imread(example/CT_gt.jpg, cv2.IMREAD_GRAYSCALE) aug_img, aug_gt augment_pair(img, gt) cv2.imwrite(example/CT_aug.jpg, aug_img) cv2.imwrite(example/CT_aug_gt.png, aug_gt)这里刻意只用了翻转和 90 度整数倍旋转没用任意角度旋转。原因是任意角度旋转需要插值原图插值后像素值会变掩膜插值后边缘会出现 0 到 255 之间的中间值二值掩膜就不纯了。如果非要用任意角度掩膜得用最近邻插值原图用双线性两个插值方式要分开设。np.rot90返回的是视图后面.copy()是为了避免写入时影响原数组。2.3 calc-mean.py 算的均值标准差怎么用calc-mean.py这个脚本的作用是遍历训练集算出所有图像的均值和标准差。这两个值在训练前归一化时要用把像素值从 0 到 255 映射到均值 0、标准差 1 的分布能让网络收敛更稳。import numpy as np import cv2 import os def calc_mean_std(img_dir, img_list): 遍历图像列表累加均值和方差最后开方得到标准差 mean np.zeros(3) std np.zeros(3) count 0 for name in img_list: path os.path.join(img_dir, name) img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 统一成 RGB 顺序 img img.astype(np.float32) / 255.0 mean img.mean(axis(0, 1)) std img.std(axis(0, 1)) count 1 mean / count std / count return mean, std # 假设训练图都在 example 目录实际用时换成自己的训练集列表 train_list [rock.jpg, concrete.jpg, CT.jpg] mean, std calc_mean_std(example, train_list) print(mean:, mean) print(std:, std)注意这里算的是逐通道的均值和标准差返回三个值对应 R、G、B。cv2.cvtColor那一步不能省OpenCV 默认读进来是 BGR 顺序如果直接算均值的通道顺序是反的归一化时通道对不上训练初期 loss 会异常。算出来的值填到训练脚本的Normalize(mean..., std...)里就行。如果数据集小这个值波动会比较大常见做法是直接用 ImageNet 的均值标准差先跑起来等数据量上来了再换。2.4 dataset-kit 目录的组织约定dataset-kit从名字看是数据集的脚手架一般包含划分训练集、验证集、测试集的脚本以及把图像和掩膜组织成模型可读格式的逻辑。语义分割的数据组织常见两种一种是图像和掩膜分两个文件夹文件名一一对应另一种是图像和掩膜同目录靠后缀区分。这套资源用的是后者_gt后缀就是掩膜。组织数据时我一般会按 7:2:1 划分训练、验证、测试划分前先打乱避免同一块岩样的切片同时出现在训练和测试里造成数据泄漏。如果 CT 切片是连续扫描的相邻切片高度相似随机划分会让验证集精度虚高这种情况得按岩样编号划分整块岩样要么全在训练集要么全在测试集。3. 模型选型与训练U-Net 为什么是这套数据的合理起点3.1 语义分割和实例分割别搞混热搜里有人问 yolo26 里实例分割和语义分割的区别这个问题在这套资源里很关键。语义分割是给每个像素打一个类别标签所有裂缝像素都是同一类不区分是哪条裂缝实例分割要把不同裂缝个体分开输出的是「裂缝 1、裂缝 2、裂缝 3」。这套资源做的是语义分割掩膜里所有裂缝都是白色没有个体编号。为什么选语义分割而不是实例分割因为岩心裂缝分析关心的是裂缝总面积、走向分布、密度这些统计量不需要区分具体哪条裂缝。而且实例分割需要每个裂缝单独标注标注成本高得多这套数据集的标注格式就是二值掩膜直接对应语义分割。选型上没必要上更复杂的实例分割框架U-Net 这类编码器-解码器结构就够了。3.2 U-Net 结构在这套数据上的适配点U-Net 的核心是编码器逐层下采样提特征、解码器逐层上采样恢复分辨率中间用跳跃连接把编码器的高分辨率特征拼到解码器上。对裂缝分割这个任务跳跃连接特别重要因为裂缝是细长结构下采样几次之后细裂缝在特征图上就消失了跳跃连接能把浅层的边缘信息带回来。import torch import torch.nn as nn class DoubleConv(nn.Module): 两层卷积加 BN 加 ReLUU-Net 的基础模块 def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch3, out_ch1): super().__init__() # 编码器通道数 64 - 128 - 256 - 512 self.enc1 DoubleConv(in_ch, 64) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.enc4 DoubleConv(256, 512) self.pool nn.MaxPool2d(2) # 解码器上采样后与编码器特征拼接 self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 DoubleConv(128, 64) self.out nn.Conv2d(64, out_ch, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) d3 self.dec3(torch.cat([self.up3(e4), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.out(d1)in_ch3对应 RGB 输入如果处理 16 位 CT 单通道图改成 1。out_ch1是二分类输出最后接 sigmoid 得到每个像素属于裂缝的概率。torch.cat里的dim1是通道维度拼接这是 U-Net 跳跃连接的关键操作。编码器每下采样一次特征图边长减半、通道翻倍解码器反过来。如果显存不够把 64 改成 32通道数减半显存大概降到四分之一精度会掉一些但能跑起来。3.3 训练循环与损失函数选择裂缝像素占比低用普通交叉熵会让模型偏向背景。常见做法是交叉熵加 dice loss 组合dice loss 直接优化预测和真值的重叠度对不均衡数据更友好。import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, pred, target): # pred 是 sigmoid 后的概率target 是 0/1 掩膜 pred pred.view(-1) target target.view(-1) intersection (pred * target).sum() dice (2. * intersection self.smooth) / (pred.sum() target.sum() self.smooth) return 1 - dice # 组合损失交叉熵稳定梯度dice 处理不均衡 bce nn.BCEWithLogitsLoss() dice DiceLoss() def criterion(logits, target): return bce(logits, target) dice(torch.sigmoid(logits), target)BCEWithLogitsLoss内部带了 sigmoid所以传进去的是 logits 不是概率别在外面再 sigmoid 一次否则梯度会出问题。dice loss 里smooth是防止分母为零的平滑项设 1.0 是常规值。两个 loss 直接相加权重各占一半如果裂缝特别少可以把 dice 权重调高。训练时学习率从 1e-3 起步用 Adam 优化器跑几十个 epoch 看验证集 dice 系数有没有上升。4. 避坑与排查这套源码跑不起来时先查这五处4.1 掩膜读成三通道导致 loss 维度报错现象训练时报Target size must be the same as input size或者维度不匹配。原因用cv2.imread读掩膜时没加IMREAD_GRAYSCALE默认读成三通道和模型输出的单通道对不上。解决所有读掩膜的地方统一加cv2.imread(path, cv2.IMREAD_GRAYSCALE)或者在 Dataset 类里读完后加一步gt gt[:, :, 0]取单通道。4.2 原图和掩膜增强不同步现象训练 loss 一直不降预测结果和原图对不上。原因数据增强时原图和掩膜用了不同的随机参数比如原图翻转了掩膜没翻转标签和图像错位。解决增强函数必须同时接收原图和掩膜用同一组随机数控制几何变换像 2.2 节那样把两个图一起传进去一起变换。4.3 16 位 CT 图当 8 位处理丢细节现象CT 图像训练出来的模型对低对比度裂缝完全不敏感。原因16 位 CT 动态范围是 0 到 65535直接除以 255 会截断大量细节丢失。解决用amplifyData-16.py处理 16 位数据读图时用cv2.IMREAD_UNCHANGED保留位深归一化时除以 65535 而不是 255或者先做窗宽窗位映射到 8 位再训练。4.4 数据泄漏导致验证集精度虚高现象验证集 dice 系数 0.9 以上换一批新数据掉到 0.5。原因CT 连续切片相邻帧高度相似随机划分时相似切片同时进了训练集和验证集。解决按岩样编号划分同一块岩样的所有切片只进一个集合。划分前先按文件名前缀分组再对组做划分。4.5 显存不足误以为是代码 bug现象跑几个 batch 后报 CUDA out of memory。原因U-Net 第一层 64 通道输入 512x512 时显存占用不小batch size 设大了直接爆。解决先把 batch size 降到 2 或 4输入裁剪到 256x256或者把第一层通道数从 64 降到 32。混合精度训练也能省一半显存加torch.cuda.amp就行。5. 把 example 跑通之后换自己数据的迁移技巧和验证方法example 里那三组图跑通只是验证代码没坏真正要用得换成自己的岩心数据。换数据时最容易翻车的地方是标注格式。这套代码期望的掩膜是单通道二值图白 255 黑 0但很多人用标注工具导出的是彩色索引图或者灰度值不纯的图。我一般会先写个转换脚本把所有掩膜统一成二值。import cv2 import numpy as np import os def binarize_mask(src_dir, dst_dir, threshold127): 把各种格式的掩膜统一成 0/255 二值图 os.makedirs(dst_dir, exist_okTrue) for name in os.listdir(src_dir): if not name.lower().endswith((.png, .jpg, .bmp)): continue path os.path.join(src_dir, name) mask cv2.imread(path, cv2.IMREAD_GRAYSCALE) # 大于阈值的算裂缝其余算背景 binary np.where(mask threshold, 255, 0).astype(np.uint8) cv2.imwrite(os.path.join(dst_dir, name), binary) binarize_mask(raw_masks, clean_masks, threshold127)threshold这个参数要看标注工具的输出。如果标注是纯黑白127 没问题如果标注边缘有抗锯齿产生的灰边阈值设高一点比如 200能把灰边归到背景避免训练时把模糊边缘当裂缝学。转换完一定要抽查几张用np.unique确认只剩 0 和 255。验证模型有没有真的学到东西不能只看 loss 曲线。我习惯在验证集上算三个指标dice 系数、IoU、以及裂缝像素的召回率。dice 和 IoU 反映整体重叠度召回率反映漏检情况。地质分析里漏检一条大裂缝比误检几条小裂缝严重得多所以召回率比精确率更值得盯。如果召回率低把 dice loss 权重调高或者在 loss 里给正类加权重。还有一个实用技巧把预测掩膜叠加到原图上用红色半透明显示预测裂缝和真值掩膜并排看。肉眼扫一遍就能发现模型是漏了细裂缝还是把噪声当成了裂缝。这种可视化比看数字直观得多调参阶段我基本每几个 epoch 就存一批对比图。从那以后我每次换新数据集都强制先跑一遍掩膜二值化检查和叠加可视化确认标签没问题再开训。很多所谓的模型不收敛根子都在标签上。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网