U-Net实现岩石裂缝与CT岩心图像语义分割实战

发布时间:2026/9/14 7:38:50
U-Net实现岩石裂缝与CT岩心图像语义分割实战 简介面向岩石裂缝与CT岩心裂缝的语义分割任务这份Python源码与数据集包可帮助地质工程、石油工程及计算机视觉学习者快速入手深度学习方法。资源围绕U型卷积网络即U-Net展开覆盖图像读取、数据预处理与增强、模型搭建、训练循环、预测评估等关键环节能直接用于裂缝像素级识别场景。压缩包共10个文件、1.12MB包含6张JPG示例图像原始岩心图与裂缝标注真值各占一部分、3个Python脚本分别实现图像扩增、批量数据均值计算、自定义增强策略等以及1个Markdown说明文档目录结构简洁便于按功能定位代码。已有206人学习下载。借助源码和样例读者可掌握数据集划分、归一化、扩增方法理解编码器-解码器结构并能使用标注图像计算IoU等分割指标说明文档和脚本注释进一步降低了上手门槛为迁移到其他岩石或CT影像分析任务提供可执行的参考。1. 裂缝在 CT 里没有颜色只有梯度把 CT 岩心扫描图交给一个只看过自然图像的语义分割模型第一轮训练就会崩。原因很简单岩石裂缝在 CT 切片里没有颜色只有密度值对应的灰度梯度裂缝与孔隙背景之间的边界经常只有 3 到 4 个像素宽。传统的阈值分割或边缘检测在这种低信噪比图像上会把矿物颗粒边界误认成裂缝而语义分割网络能把每个像素归类到“裂缝”或“背景”从而保留裂缝拓扑。这套基于 Python 的岩石裂缝与 CT 岩心裂缝语义分割源码加数据集正好覆盖了从原始 CT 图像、标注图到数据增强脚本、均值统计脚本和 U-Net 训练链路的完整闭环。适合正在做油气储层裂缝量化、混凝土裂缝检测或者需要把语义分割落地到灰度工业图像上的工程师和研究人员。我下面会把每个脚本怎么用、参数怎么调、输出怎么验证拆开讲。2. U-Net 与岩石裂缝分割的任务边界2.1 为什么是语义分割而不是传统阈值法CT 岩心图像中裂缝的灰度特征与岩石基质的差异并不稳定。由于矿物成分不同同一套灰度阈值可能在某段岩心有效换一个位置就会把高密度矿物也划进来。岩石裂缝占比往往小于全图面积的 2%属于典型的极端类别不平衡问题。语义分割模型通过卷积核学习局部灰度梯度组合而非单一阈值能够区分“裂缝”和“矿物边界”的结构差异。更重要的是语义分割输出的是每个像素的类别概率图后续可以直接做裂缝连通域分析、方位统计和密度测算。传统方法比如 Canny 或自适应阈值只能给出二值边缘无法判断边缘内部是否属于裂缝填充。而基于深度学习的语义分割模型例如本项目重点用到的 U-Net它的下采样过程能把整块区域的上下文压缩成高维特征上采样过程再把特征映射回原图分辨率。对 CT 这类灰度图像来说U-Net 的输入可以保持单通道不需要预热成三通道彩色图这反而减少了参数计算量。2.2 编码器-解码器结构如何保留裂缝细节U-Net 之所以适合岩石裂缝核心原因是它的跳跃连接结构。编码器每一层做完下采样后会把对应尺寸的 feature map 直接拼接到解码器的同层输入上。这样低层特征里保存的高频边缘信息不会在多次卷积和池化中被抹掉。裂缝宽度通常只有 1 到 2 个像素如果网络只靠深层特征恢复细节缝隙会被平滑成一条粗线甚至直接消失。跳跃连接相当于给解码器一条“抄近路”让浅层的位置信息也能参与最终像素级预测。在实际使用中U-Net 的输入尺寸对 CT 切片非常敏感。我一般把切片裁剪或缩放到 512x512再送给模型。太大如 1024x1024 会增加显存压力太小如 256x256 则会让亚毫米级裂缝在缩放时丢失。项目里 example 目录提供的 CT.jpg 刚好是 CT 切片标注 CT_gt.jpg 是黑色背景、白色裂缝的标注图可以直接用来验证这套结构。2.3 标签数据与读取预处理的常见坑项目里的标注图是 JPG 格式。如何读取直接决定训练是否正常。这里给出一个通用的读取函数适用于 rock.jpg 与 rock_gt.jpg、concrete.jpg 与 concrete_gt.jpg 这组成对样本。import cv2 import numpy as np def load_image_and_label(img_path, gt_path, target_size(512, 512)): # 原图按灰度读入保持单通道 img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 标注图同样按灰度读入 gt cv2.imread(gt_path, cv2.IMREAD_GRAYSCALE) if img is None or gt is None: raise ValueError(f读取失败: {img_path} 或 {gt_path}) img cv2.resize(img, target_size, interpolationcv2.INTER_AREA) # 标注图缩放必须用最近邻避免产生伪裂缝灰度 gt cv2.resize(gt, target_size, interpolationcv2.INTER_NEAREST) # 归一化到 0~1保持 float32 img img.astype(np.float32) / 255.0 # 将标注统一到 1 表示裂缝 gt_bin (gt 127).astype(np.float32) return np.expand_dims(img, axis-1), np.expand_dims(gt_bin, axis-1)这里的参数 target_size 控制输入分辨率INTER_AREA 适合缩小灰度图像时避免摩尔纹INTER_NEAREST 用于标注图缩放时保持硬边界。如果标注图在缩放时用了线性插值裂缝边缘会出现灰色过渡像素模型会学到不存在的软边界最终预测结果边缘会出现一圈阈值漂移区。gt_bin 的阈值取 127是因为原始标注图用 255 标注裂缝用 1 标注会落到浮点精度损失区域统一二值化更稳。3. 数据集拆解与 augmented 预处理管线3.1 资源里的文件结构这套源码包里的 dataset-kit 目录和根目录几个 Python 脚本构成了一个最小可用的训练数据生产链路。我把每个文件的用途整理成下表。文件名作用说明datasest-kit/数据集目录组织好训练集、验证集的原图与标注图amplifyData.py基础增强脚本对 every 图像执行旋转、翻转、对比度变换amplifyData-16.py16 倍增强脚本在同一份数据上生成 16 种变化副本calc-mean.py均值统计脚本计算原图全局均值和标准差用于归一化example/示例样本rock.jpg 与 rock_gt.jpg、CT.jpg 与 CT_gt.jpg、concrete.jpg 与 concrete_gt.jpgREADME.md项目说明标注格式与训练流程从 example 目录的命名能看出这个项目覆盖了三类典型输入岩石表面裂缝图像、CT 岩心扫描切片、混凝土裂缝图像。前两者属于地质样本混凝土图像则更接近建材领域的检测需求。如果你的任务点落在其中某一类可以直接用对应图像训练。如果三种都要覆盖建议把三组样本混合增强脚本要按类别区分避免混凝土图像放大过多把岩石裂缝特征挤掉。3.2 手工增强脚本的写法amplifyData.py 是最核心的数据增产工具。它做的事很简单将原图和标注图做相同的几何变换保证像素对齐。下面这段代码是常见做法使用 PIL 自带接口不需要额外安装第三个库。import os from PIL import Image, ImageEnhance def augment_pair(img_path, gt_path, output_dir, prefixaug): os.makedirs(output_dir, exist_okTrue) img Image.open(img_path).convert(L) gt Image.open(gt_path).convert(L) # 1. 原样保存 img.save(os.path.join(output_dir, f{prefix}_0_img.jpg)) gt.save(os.path.join(output_dir, f{prefix}_0_gt.jpg)) # 2. 水平翻转 垂直翻转 for i, t in enumerate([Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM], start1): img.transpose(t).save(os.path.join(output_dir, f{prefix}_{i}_img.jpg)) gt.transpose(t).save(os.path.join(output_dir, f{prefix}_{i}_gt.jpg)) # 3. 旋转 90 / 180 / 270 for angle in [90, 180, 270]: img.rotate(angle, expandTrue).save(os.path.join(output_dir, f{prefix}_{angle}_img.jpg)) gt.rotate(angle, expandTrue).save(os.path.join(output_dir, f{prefix}_{angle}_gt.jpg)) # 4. 对比度增强只作用于原图标注保持原样 enhancer ImageEnhance.Contrast(img) enhancer.enhance(1.5).save(os.path.join(output_dir, f{prefix}_contrast_img.jpg)) gt.save(os.path.join(output_dir, f{prefix}_contrast_gt.jpg))这个脚本的逻辑顺序是先保存原图作为基线再分别做水平、垂直翻转然后做 90 度整数倍旋转最后做对比度增强。旋转角度使用expandTrue会改变画布尺寸因此增强后的图必须和标注图同步裁剪回原尺寸否则模型输入尺寸会不统一。我一般在 rotate 之后加一步center_crop到 512x512。实际上 amplifyData-16.py 就是在上述基础上把每个变换再叠加两种对比度等级达到 16 倍数据量。使用时要根据原始样本数量设置增强倍率样本少于 50 张时直接上 16 倍多于 200 张且包含多组岩心时 8 倍就够了。3.3 计算均值和标准差避免 BatchNorm 前期震荡模型训练前常用的归一化方式是先减均值再除标准差。对普通三通道图像大部分开源数据集直接给 RGB 均值。但 CT 岩心图像是单通道密度图不同扫描设备输出的像素分布差异很大必须用 calc-mean.py 自己统计。import cv2 import glob import numpy as np image_paths glob.glob(dataset-kit/train/images/*.jpg) pixel_sum np.zeros(1) pixel_sq_sum np.zeros(1) count 0 for path in image_paths: img cv2.imread(path, cv2.IMREAD_GRAYSCALE).astype(np.float32) pixel_sum img.sum() pixel_sq_sum (img**2).sum() count img.size mean pixel_sum / count std np.sqrt(pixel_sq_sum / count - mean**2) print(fmean: {mean:.4f}, std: {std:.4f})这段代码的输入是图像路径通配符输出是全局均值和标准差。如果你看到 std 接近 70 左右说明图像对比度比较大建议在模型输入层加一个标准化层。如果 std 小于 30说明图像整体偏灰直接训练会让 U-Net 的 BatchNorm 层反复调整尺度前 20 个 epoch 震荡明显。CT 图像经过 JPEG 保存后原始 HU 值已经被映射到 0~255此时计算的均值可以直接作为网络输入归一化参数。4. Keras 下的 U-Net 训练与边界损失设置4.1 模型定义与关键参数项目没有限制深度学习框架但从摘要描述来看最接近的实现是 Keras 后端调用 TensorFlow。下面是一个标准 U-Net 定义输入为 512x512x1输出为 512x512x1采用 sigmoid 激活。import tensorflow as tf from tensorflow.keras import layers def conv_block(x, filters): x layers.Conv2D(filters, 3, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.Conv2D(filters, 3, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) return x def unet(input_size(512, 512, 1)): inputs tf.keras.Input(input_size) # Encoder e1 conv_block(inputs, 64) p1 layers.MaxPooling2D(2)(e1) e2 conv_block(p1, 128) p2 layers.MaxPooling2D(2)(e2) e3 conv_block(p2, 256) p3 layers.MaxPooling2D(2)(e3) e4 conv_block(p3, 512) p4 layers.MaxPooling2D(2)(e4) # Bridge b conv_block(p4, 1024) # Decoder d4 layers.UpSampling2D(2)(b) d4 layers.Concatenate()([d4, e4]) d4 conv_block(d4, 512) d3 layers.UpSampling2D(2)(d4) d3 layers.Concatenate()([d3, e3]) d3 conv_block(d3, 256) d2 layers.UpSampling2D(2)(d3) d2 layers.Concatenate()([d2, e2]) d2 conv_block(d2, 128) d1 layers.UpSampling2D(2)(d2) d1 layers.Concatenate()([d1, e1]) d1 conv_block(d1, 64) output layers.Conv2D(1, 1, activationsigmoid, paddingsame)(d1) return tf.keras.Model(inputs, output)模型定义里编码器每层卷积后都接 BatchNorm 和 ReLU。在地质图像上BatchNorm 尤其重要因为不同岩心样本的灰度分布不一致BatchNorm 能减少内部协变量偏移。解码器使用 UpSampling2D 而不是 Conv2DTranspose原因在于裂缝语义分割的样本量通常不大转置卷积容易在裂缝边缘产生棋盘伪影而最近邻上采样配合后续卷积可以降低这种风险。Concatenate 操作对应 U-Net 的跳跃连接把同层下采样前的特征图与上采样后的特征图拼接让网络在恢复分辨率时保留边界信息。4.2 损失函数与类不平衡岩石裂缝在整个图像中占比非常小使用二元交叉熵作为唯一损失会出现严重的类别偏向模型会学成输出全图背景。常见做法是叠加 Dice Loss。下面给出一个在 Keras 中可用的组合损失。def dice_loss(y_true, y_pred, smooth1e-6): y_true_f tf.reshape(y_true, [-1]) y_pred_f tf.reshape(y_pred, [-1]) intersection tf.reduce_sum(y_true_f * y_pred_f) return 1 - (2.0 * intersection smooth) / ( tf.reduce_sum(y_true_f) tf.reduce_sum(y_pred_f) smooth ) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), losslambda y_true, y_pred: tf.keras.losses.binary_crossentropy(y_true, y_pred) dice_loss(y_true, y_pred), metrics[accuracy, tf.keras.metrics.MeanIoU(num_classes2)] )Dice Loss 的 smooth 参数用来防止分母为零在训练初期建议设置 1e-6后期如果 loss 抖动明显可以升到 1e-3。组合损失里二元交叉熵保证梯度在概率偏离时依然有效Dice Loss 则直接优化目标指标。注意 MeanIoU 这个指标在 Keras 里计算的是按类别平均的 IoU对裂缝这种小目标它的分数会被背景的大 IoU 抬升因此还是要单独记录裂缝类别的 IoU。超参数推荐值说明batch_size8~16CT 切片单通道512x512 约 1MB8 张在 8GB 显存可跑learning_rate1e-4如果使用 AdamW可尝试 2e-4epochs100~200配合早停重点观察验证集 Diceearly_stoppingpatience20监控验证 loss避免过拟合数据增强rotate90, flip, contrast使用 amplifyData-16.py 提前增强而不是 on-the-fly训练时如果使用提前增强建议将增强后的图像打乱顺序并确保同一张原图增强出的多个副本不落在同一个 batch 内。如果 batch size 太小同一副本会主导梯度方向。更稳妥的做法是在 tf.keras.preprocessing.image.ImageDataGenerator 里设置 rotation_range90, horizontal_flipTrue让每轮 epoch 数据动态变化。4.3 训练脚本与模型保存以下是整合训练流程的骨架脚本。import tensorflow as tf from sklearn.model_selection import train_test_split img_paths glob.glob(dataset-kit/train/images/*.jpg) gt_paths [p.replace(images, masks).replace(.jpg, _gt.jpg) for p in img_paths] train_imgs, val_imgs, train_gts, val_gts train_test_split( img_paths, gt_paths, test_size0.2, random_state42 ) train_ds tf.data.Dataset.from_tensor_slices((train_imgs, train_gts)) train_ds train_ds.map( lambda x, y: tf.numpy_function(load_image_and_label, [x, y], [tf.float32, tf.float32]), num_parallel_callstf.data.AUTOTUNE ).batch(8).prefetch(tf.data.AUTOTUNE) callbacks [ tf.keras.callbacks.ModelCheckpoint(unet_rock_ct.h5, save_best_onlyTrue, monitorval_loss), tf.keras.callbacks.EarlyStopping(patience20, restore_best_weightsTrue) ] model unet() history model.fit(train_ds, validation_dataval_ds, epochs150, callbackscallbacks)这段逻辑里map中使用的是第 2 章定义的加载函数tf.numpy_function把 Python 函数包装成 TensorFlow 图节点。这里的 train_test_split 按 8:2 划分数据注意要固定 random_state否则每次跑模型会得到不同验证集划分导致评测结果不可比。ModelCheckpoint 的 save_best_only 会在验证 loss 下降时保存权重保证最终拿到的是验证集上表现最好的模型。5. 预测脚本里的灰度陷阱与 IoU 验证5.1 加载模型并处理 CT 灰度图训练完成后用户通常会拿新 CT 图像做预测。这里最容易踩的坑是输入灰度值范围不一致。训练时我们做了img.astype(np.float32) / 255.0预测时如果忘记同样的归一化CT 图像原始像素值在 0~255 内分布可能偏暗导致预测结果几乎全黑。另一个坑是图像尺寸U-Net 的输入是 512x512使用 OpenCV 的 resize 时缩小用 INTER_AREA放大用 INTER_LINEAR。下面是可直接运行的预测脚本。import cv2 import numpy as np import tensorflow as tf model tf.keras.models.load_model(unet_rock_ct.h5, compileFalse) def predict_ct(model, ct_path, save_path, size(512, 512)): img cv2.imread(ct_path, cv2.IMREAD_GRAYSCALE) original_size img.shape[::-1] img_resized cv2.resize(img, size, interpolationcv2.INTER_AREA).astype(np.float32) img_resized img_resized / 255.0 img_input np.expand_dims(np.expand_dims(img_resized, axis-1), axis0) pred model.predict(img_input, verbose0)[0, :, :, 0] # 上采样回原图尺寸使用线性插值保持概率场连续 pred_original cv2.resize(pred, original_size, interpolationcv2.INTER_LINEAR) # 按阈值二值化 mask (pred_original 0.5).astype(np.uint8) * 255 cv2.imwrite(save_path, mask)这里的original_size记住的是宽高顺序OpenCV 的 resize 参数是 (width, height)所以要用img.shape[::-1]。阈值固定为 0.5 不一定合适我一般会在验证集上扫描 0.3~0.7 步长 0.05选 IoU 最高的阈值保存为最佳阈值。如果预测结果裂缝过于破碎可以把输出概率图做一次中值滤波核大小 3在二值化之前操作能够去掉单像素椒盐噪声。5.2 IoU 与裂缝长度统计验证分割效果最常用的指标是 IoU对岩石裂缝还应该单独看裂缝类别的 IoU。如果只计算全局 accuracy模型把裂缝全预测成背景也能得到 99% 以上没有参考意义。下面给出一个标准计算方式。def compute_iou(mask_pred, mask_true, foreground_idx255): pred (mask_pred foreground_idx) true (mask_true foreground_idx) intersection np.logical_and(pred, true).sum() union np.logical_or(pred, true).sum() iou intersection / union if union 0 else 0.0 precision intersection / pred.sum() if pred.sum() 0 else 0.0 recall intersection / true.sum() if true.sum() 0 else 0.0 return iou, precision, recall这段代码里接口需要把模型输出的概率图先二值化到 255 和 0。如果 mask_pred 的元素值全为 0precision 会返回 0而不是报错。在裂缝分割里单纯 IoU 还不够因为裂缝宽度小一个像素的偏移就会让 IoU 大幅下降。更实用的补充是骨架线命中率先对预测掩码做形态学骨架化再计算骨架线在真实标注范围内的比例。这个比例反映了裂缝的连通性还原程度。5.3 迁移到其他岩石数据的三个入手点如果手头有新的岩心 CT 数据不建议直接拿这个模型硬推理。先用 amplifyData.py 将新数据中一眼能认出的裂缝标 20~30 张把已有模型在新数据上预测人工修正错误区域这样得到初始训练集的速度比从零标注快得多。这是许多工业语义分割项目的通用冷启动方案。另外CT 图像中矿物颗粒边缘的灰度突变与裂缝边缘高度相似增强脚本里可以额外加入高斯噪声和模糊模拟不同扫描噪声级别。把噪声水平作为增强参数写入 amplifyData-16.py 中每次训练能提高模型的鲁棒性。若显存不足将 512x512 缩到 256x256 会让裂缝宽度缩小到 1 像素以下此时可以把解码器深度从 4 层降到 3 层避免过多下采样丢失裂缝响应。处理自己的数据时这三个方向比盲目堆数据集大小更容易见效。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询