
简介这份资源面向计算机视觉与地质工程方向的本科生、研究生及课程设计开发者提供一套基于Python的CT岩芯与岩石裂缝语义分割完整方案可用于期末大作业、课程设计或相关课题的快速复现与二次开发。压缩包共15个文件约1.15MB包含3个py源码脚本、6张jpg示例图像、3个zbak备份文件以及zip、gitignore、md等辅助文件源码覆盖数据增强、均值计算等训练前处理环节示例图则对应岩石、混凝土与CT岩芯的原图及标注掩码便于直接验证分割效果。资源采用Pillow、OpenCV与TensorFlow/PyTorch等主流工具链构建从图像预处理到像素级分类的端到端流程可帮助读者理解裂隙识别在地质构造与油气勘探中的量化分析思路。目前已有73人学习适合希望以较低成本获取可运行代码与配套数据、快速完成项目搭建的读者参考。1. 从一张 CT 岩心切片说起这套 Python 语义分割源码到底能干什么去年帮一个做地质数字化的朋友看岩心扫描图他手里有几百张 CT 切片想自动把裂缝区域抠出来量面积、算密度人工描边一张要十几分钟一天下来眼睛都花了。这类需求其实非常典型输入是一张岩石或 CT 岩心的灰度图输出是一张同尺寸的掩膜图裂缝像素标白、背景标黑这就是语义分割要干的事。这套「基于 Python 的 CT 岩心与岩石裂缝语义分割系统源码及数据集」正好卡在这个场景上它把训练数据、增强脚本、均值计算脚本和示例图打包在一起拿到手就能跑通一条从数据到可视化的最小闭环。它适合三类人一是做课程设计或期末大作业的学生需要一份结构完整、能讲清楚流程的工程二是地质、石油工程方向想入门图像分割的研究者拿它当 baseline 快速验证想法三是已经会 PyTorch 或 TensorFlow、但没接触过岩石裂缝这类低对比度目标的工程师想看看别人怎么处理数据。资源本身不绑定某个框架的完整训练代码重点在数据组织和预处理环节这也是我看重它的原因——分割任务里真正拉开差距的往往不是模型结构而是数据这一层。2. 拆开压缩包目录结构、文件职责与数据组织逻辑2.1 从文件清单反推工程意图先把压缩包里的东西按职责分个类这样后面动手时不会乱。清单里出现的文件大致可以归成四组文件/目录类型作用dataset-kit目录数据集主体通常放原图与对应掩膜amplifyData.py/amplifyData-16.py脚本数据增强扩充样本量calc-mean.py脚本计算数据集均值供归一化使用example目录示例图含原图与 gt 掩膜README.md文档使用说明.gitignore/.zbak/备份文件.zip杂项版本控制与备份可忽略example目录里的命名很规整rock.jpg配rock_gt.jpgconcrete.jpg配concrete_gt.jpgCT.jpg配CT_gt.jpg。_gt后缀是 ground truth 的惯例写法说明这是一套「原图 标注掩膜」成对出现的数据组织方式。岩石、混凝土、CT 三类样本放在一起意味着这套代码想覆盖的不仅是岩心还包括类岩石材料泛化场景更宽一些。2.2 数据增强脚本 amplifyData.py 的定位amplifyData.py和amplifyData-16.py两个版本从命名推测 16 可能是针对 16 位深图像的处理分支。CT 岩心图常见 16 位灰度直接按 8 位读会丢动态范围这个细节如果不注意后面训练时裂缝和基质的对比度会被压掉一大截。常见做法是用 OpenCV 的IMREAD_UNCHANGED标志读取再按需归一化到 0-255 或保留浮点。增强脚本一般做几何变换翻转、旋转、裁剪和像素变换亮度、对比度、噪声。对裂缝分割来说几何变换要特别小心旋转和翻转必须原图和掩膜同步做否则标签就错位了。下面是一段我常用的同步增强骨架思路和amplifyData.py应该是一致的import cv2 import numpy as np import os def augment_pair(img_path, gt_path, out_dir, idx): # IMREAD_UNCHANGED 保留 16 位深度避免 CT 图动态范围丢失 img cv2.imread(img_path, cv2.IMREAD_UNCHANGED) gt cv2.imread(gt_path, cv2.IMREAD_UNCHANGED) # 水平翻转原图与掩膜必须用同一个 flipCode img_f cv2.flip(img, 1) gt_f cv2.flip(gt, 1) # 旋转 90 度k1 表示逆时针一次 img_r np.rot90(img, 1) gt_r np.rot90(gt, 1) cv2.imwrite(os.path.join(out_dir, fimg_{idx}_f.png), img_f) cv2.imwrite(os.path.join(out_dir, fgt_{idx}_f.png), gt_f) cv2.imwrite(os.path.join(out_dir, fimg_{idx}_r.png), img_r) cv2.imwrite(os.path.join(out_dir, fgt_{idx}_r.png), gt_r)逻辑说明读图用IMREAD_UNCHANGED是关键普通IMREAD_COLOR会把 16 位压成 8 位。翻转和旋转对原图、掩膜用完全相同的参数保证像素级对齐。参数上flipCode1是水平翻转np.rot90的第二个参数控制旋转次数每次 90 度。输出文件名带后缀区分增强类型方便回溯。提示增强后的掩膜如果是二值图旋转插值要用最近邻否则边缘会出现 0.3、0.7 这种非 0/1 的灰度值训练时算 loss 会出问题。2.3 calc-mean.py 为什么不能省calc-mean.py负责算数据集的均值和标准差用于归一化。很多人做分割直接套 ImageNet 的均值方差但岩石 CT 图和自然图像分布差很远用错均值会让模型收敛慢甚至不收敛。这个脚本的存在说明作者是认真对待数据分布的。它的逻辑通常是遍历所有训练图累加像素值再求平均import os import cv2 import numpy as np img_dir dataset-kit/train/img means, stds [], [] for name in os.listdir(img_dir): img cv2.imread(os.path.join(img_dir, name), cv2.IMREAD_GRAYSCALE) img img.astype(np.float32) / 255.0 means.append(img.mean()) stds.append(img.std()) print(mean:, np.mean(means), std:, np.mean(stds))逻辑说明逐图算均值和标准差再取平均比把所有像素堆一起算更稳避免某张大图主导结果。参数上除以 255 是先把范围压到 0-1如果你保留 16 位这一步要改成除以 65535。算出来的值填进训练脚本的Normalize(mean[x], std[y])里灰度图只有一个通道别写成三个。3. 把数据喂进模型从掩膜格式到训练配置的完整链路3.1 掩膜的二值化与类别约定语义分割的掩膜有两种常见格式一种是灰度图像素值 0 表示背景、255 表示目标另一种是索引图像素值直接是类别 id0、1、2…。这套资源里的_gt.jpg大概率是前者。训练前要确认清楚因为损失函数的选择依赖它。如果是 0/255用BCEWithLogitsLoss前要把掩膜除以 255 变成 0/1如果是多类别索引图就得用CrossEntropyLoss。import cv2 import numpy as np gt cv2.imread(example/CT_gt.jpg, cv2.IMREAD_GRAYSCALE) # 查看掩膜里到底有哪些像素值 unique_vals np.unique(gt) print(unique_vals) # 期望看到 [0, 255] 或 [0, 1] # 统一转成 0/1 浮点供 BCE 使用 gt_bin (gt 127).astype(np.float32)逻辑说明np.unique是排查掩膜格式最快的手段一眼就能看出是二值还是多类。阈值 127 是 0/255 图转 0/1 的常用分界如果掩膜边缘有抗锯齿产生的中间值这个阈值能把它归到目标侧。参数上astype(np.float32)是为了和模型输出的浮点 logits 对齐。3.2 训练集与验证集的划分策略岩石裂缝数据往往样本量不大example里就几张图dataset-kit里估计也就几十到上百张。这种规模下划分要讲究不能纯随机否则同一块岩心的不同切片可能同时进训练和验证造成数据泄漏验证指标虚高。常见做法是按岩心编号或图像来源分组划分同一来源的图只进一个集合。import os import random all_imgs sorted(os.listdir(dataset-kit/img)) # 按文件名前缀分组假设前缀代表不同岩心来源 groups {} for name in all_imgs: prefix name.split(_)[0] groups.setdefault(prefix, []).append(name) keys list(groups.keys()) random.seed(42) random.shuffle(keys) split int(len(keys) * 0.8) train_keys, val_keys keys[:split], keys[split:] train_imgs [n for k in train_keys for n in groups[k]] val_imgs [n for k in val_keys for n in groups[k]] print(len(train_imgs), len(val_imgs))逻辑说明先按前缀分组再划分保证同源数据不跨集。random.seed(42)固定随机性方便复现。参数上 0.8 是训练比例样本少时可以调到 0.7 给验证留更多。如果前缀规则不明确退而求其次可以按文件名排序后隔几张取一张做验证但效果不如分组稳。3.3 损失函数与评价指标的搭配裂缝分割有个典型问题裂缝像素占比往往不到 5%正负样本极度不平衡。这时候纯 BCE 会让模型倾向于全预测背景准确率看着很高但 IoU 惨不忍睹。常见做法是 BCE 加 Dice Loss 组合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) # Dice 计算前把维度拉平避免 batch 维度干扰 probs_flat probs.view(-1) targets_flat targets.view(-1) intersection (probs_flat * targets_flat).sum() dice_loss 1 - (2 * intersection 1e-6) / (probs_flat.sum() targets_flat.sum() 1e-6) return self.bce_weight * bce_loss (1 - self.bce_weight) * dice_loss逻辑说明BCE 提供稳定的逐像素梯度Dice 直接优化重叠度。1e-6是防止分母为零的平滑项别省。参数bce_weight控制两者权重0.5 是起点如果发现前景召回低就调小它、让 Dice 占更大比重。评价指标上除了 IoU建议单独看裂缝类的 recall因为漏检一条裂缝比多标一块背景代价大得多。4. 避坑与排查跑这套源码时最容易翻车的五个地方4.1 现象训练 loss 一直不降输出全黑原因掩膜没做二值化或者归一化用了 ImageNet 的均值方差和岩石图分布不匹配。CT 图整体偏暗用自然图像的均值会把有效信号压没。解决先跑calc-mean.py拿到真实均值和标准差填进 Normalize。再用np.unique确认掩膜只有 0/1 或 0/255训练前统一转成 0/1 浮点。4.2 现象增强后图像和掩膜对不上边缘错位原因原图和掩膜用了不同的插值方式或者旋转时一个用了cv2.rotate一个用了np.rot90两者对边界的处理不一致。解决所有几何变换对原图和掩膜用同一套函数、同一组参数。掩膜旋转插值固定用INTER_NEAREST原图可以用INTER_LINEAR但坐标映射必须一致。4.3 现象16 位 CT 图读进来变成全白或全黑原因用cv2.imread默认的IMREAD_COLOR读 16 位图OpenCV 会做截断高动态范围信息丢失。解决读图时显式传cv2.IMREAD_UNCHANGED或cv2.IMREAD_ANYDEPTH拿到原始位深后再决定归一化策略。如果模型输入要求 8 位用cv2.normalize按百分位拉伸别直接除以 256。4.4 现象验证集 IoU 高得离谱实际预测一塌糊涂原因数据泄漏。同一块岩心的相邻切片被分到了训练集和验证集模型见过几乎一样的图。解决按岩心来源分组划分同源数据只进一个集合。如果文件名没有来源信息至少按采集批次或时间戳分组别用纯随机。4.5 现象推理时显存爆掉或 batch size 只能设 1原因CT 岩心图分辨率通常很大直接整图送进网络特征图占的显存随分辨率平方增长。解决训练时随机裁 patch比如 256×256 或 512×512推理时用滑窗加重叠拼接。重叠区域取平均能消除拼接缝。这套资源的example图可以先裁小跑通流程再上全图。5. 进阶玩法用滑窗推理把大图分割跑稳附一套验证习惯数据量小的时候除了增强还有一个提分手段是测试时增强TTA把图翻转、旋转几个版本分别推理再把结果平均回去。对裂缝这种方向性强的目标TTA 能明显稳住边缘。下面是一段滑窗加 TTA 的推理骨架import cv2 import numpy as np import torch def sliding_window_inference(model, img, patch256, stride128, devicecuda): model.eval() h, w img.shape[:2] prob_map np.zeros((h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) for y in range(0, h - patch 1, stride): for x in range(0, w - patch 1, stride): patch_img img[y:ypatch, x:xpatch] # 转 tensor 并加 batch、channel 维度 tensor torch.from_numpy(patch_img).float().unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): # TTA原图 水平翻转两次结果平均 out1 torch.sigmoid(model(tensor)) out2 torch.sigmoid(model(torch.flip(tensor, dims[3]))) out2 torch.flip(out2, dims[3]) prob ((out1 out2) / 2).squeeze().cpu().numpy() prob_map[y:ypatch, x:xpatch] prob count_map[y:ypatch, x:xpatch] 1 # 重叠区域取平均避免拼接缝 prob_map prob_map / np.maximum(count_map, 1e-6) return (prob_map 0.5).astype(np.uint8) * 255逻辑说明滑窗的stride小于patch是为了制造重叠重叠区靠count_map做平均消除边界突变。TTA 部分对水平翻转的预测再翻回来和原预测平均能压掉一部分随机噪声。参数上patch256、stride128是显存和精度的折中显存够可以调大 patch但 stride 建议保持 patch 的一半左右。阈值 0.5 是默认值如果发现漏检多就降到 0.4 试试。验证习惯上我一般会固定三件事一是每次改完预处理先拿example里那几张图跑一遍可视化肉眼看掩膜边缘对不对二是记录每次实验的均值方差、损失权重、学习率写在 README 里不然过两天就忘了哪组参数最好三是留一张完全不参与训练的图做最终测试别用验证集的结果下结论。这套源码给了一个能跑通的起点但真正决定效果的是你对数据的理解——裂缝在 CT 图里是暗线还是亮线、宽度大概几个像素、有没有矿物填充导致的灰度变化这些都得自己盯着图看几遍。从那以后我每次拿到新的岩石分割数据都强制先做一遍像素值分布统计和掩膜格式检查再动模型。希望帮到你。本文还有配套的精品资源点击获取