☰
岩石裂缝与CT岩心语义分割Python实战:源码、数据集与避坑指南
2026/10/12 12:08:04 网站建设 项目流程

简介:基于Python的岩石裂缝与CT岩心裂缝语义分割项目,面向需要完成毕业设计、期末大作业或课程设计的学生,解决岩石与CT图像中裂缝像素级分割及标注数据预处理问题,代码注释到位,新手也能快速上手实践。压缩包共11个文件,以Python脚本、JPG样例图像与Markdown说明为主,整体仅1.13MB;其中脚本负责训练与推理,图像样本覆盖岩石、混凝土、CT岩心等场景及对应标签图,另有数据增强和均值计算工具脚本,辅助理解数据准备与训练流程。解压后按说明简单配置即可运行,注释覆盖关键步骤,可帮助初学者理解数据准备、模型训练与结果评估的完整流程,适合答辩演示、课程展示或二次开发。该项目已有226人学习下载,是导师认可的高分项目,结构清晰、复用性强,是课程设计与毕业作品的实用参考模板。

1. 岩石裂缝与 CT 岩心语义分割:这套 Python 源码与数据集能直接复现什么

岩石裂缝与 CT 岩心裂缝的语义分割,是岩土、地质方向做毕业设计时最高频的一类题目:数据要自己找,裂缝在图上又细又少,模型稍不注意就学偏。这套基于 Python 的源码加数据集,把这条链路里最耗时间的部分直接打包好了。

压缩包里有 rock、concrete、CT 三类样本图和对应 GT 标注图,还有两个数据增强脚本 amplifyData.py、amplifyData-16.py,以及一个算数据集均值的 calc-mean.py。等于从数据准备到训练输入的前半程都铺好了,你只需要把训练跑起来,再按自己的数据做迁移。

代码带注释,README 里写清楚了每个文件的用途,新手按顺序跑就能把增强、归一化、训练这套流程走通。这个完成度拿来交毕业设计、期末大作业或课程设计,导师验收时很难挑出硬伤。下面我按实际拆包的顺序,把文件关系、脚本逻辑和训练阶段的坑逐条拆开讲。

2. 数据集与标签配对:rock_gt、concrete_gt、CT_gt 的格式与对应关系

2.1 文件清单:三类样本图与标注图的对应关系

先看压缩包根目录,文件不多,但每个都有明确用途:

文件 / 目录作用
dataset-kit/训练素材目录,图片和标签都在里面
rock.jpg / rock_gt.jpg岩石表面裂缝样本图与对应标注图
concrete.jpg / concrete_gt.jpg混凝土裂缝样本图与对应标注图
CT.jpg / CT_gt.jpgCT 岩心切片图与对应标注图
calc-mean.py统计数据集 RGB 均值的脚本
amplifyData.py单组数据增强脚本,图与标签联动变换
amplifyData-16.py增强脚本的另一个版本,偏批量与 16 倍增强
README.md说明文档,建议先读再动手

命名规律一眼能看出来:原图叫 rock.jpg,标注图就叫 rock_gt.jpg,_gt 是 ground truth 的缩写,concrete 和 CT 同理。训练代码只需要在原图文件名上拼 _gt 后缀就能同时读入图和标签,不用维护额外的映射表。这是裂缝分割数据集最常见的组织方式,比 CSV 记录路径的方式省事,也不容易漏配对。

我拿到这种压缩包,第一步从来不是急着跑训练,而是先写一个检查脚本,逐张确认原图和 _gt 图分辨率完全一致、都能正常读取。裂缝标注要求像素级对齐,如果有一张图尺寸对不上,DataLoader 直接报错还算小事,怕的是错误被静默跳过,模型在残缺数据上训练完,指标虚高,可视化一放大就露馅。

import cv2 import os base = "dataset-kit" # 按实际目录调整 for name in sorted(os.listdir(base)): if not name.endswith(".jpg") or "_gt" in name: continue img_path = os.path.join(base, name) gt_path = os.path.join(base, name.replace(".jpg", "_gt.jpg")) img = cv2.imread(img_path) gt = cv2.imread(gt_path) if img is None or gt is None: print(f"文件缺失或无法读取: {name}") continue print(f"{name}: img {img.shape}, gt {gt.shape}")

这段逻辑很简单,但换任何数据集都值得跑一遍。cv2.imread 读不出来会返回 None 而不是抛异常,所以必须显式判断。gt 这里先用彩色模式读,只是为了看形状,真正进模型时要改成 IMREAD_GRAYSCALE 读成单通道。

提示:检查脚本跑完后,顺手看一眼输出里有没有 img 和 gt 尺寸不一致的行。裂缝分割对像素对齐极度敏感,这一点在后面的避坑章节会反复出现。

2.2 裂缝标注的本质:二值 mask 与类别不平衡

这类裂缝 GT 图的本质是二值图:裂缝像素是白色(255),背景是黑色(0)。语义分割任务里把它当两类问题处理,裂缝是一类、背景是一类,不需要做实例级标注,也不需要区分裂缝的走向或宽度。模型的输出层只需要一个通道,最后用 sigmoid 出概率。

这里有个关键点容易被新手忽略:裂缝像素占整张图的比例极低。岩石表面裂缝通常只占 2% 到 5%,CT 岩心切片里的裂缝占比可能更低。这就是典型的类别不平衡问题,如果直接用普通交叉熵,模型很快就会学会「全预测背景」——因为那样 loss 已经很低了,它根本没必要冒险去预测裂缝。

理解了这一点,再看这个资源里几个脚本的设计意图就很清晰:增强脚本在扩大样本量的同时,相当于把裂缝以不同角度和亮度反复暴露给模型;calc-mean 在保证输入分布一致;后面训练部分还要搭配 Dice 系数的损失函数。整条链路都是围绕「让少量裂缝像素被充分放大」这个目标转的。

2.3 训练集与验证集的划分思路

数据集体量不大,划分方式要特别小心。我见过不少人先做数据增强、再划分训练验证集,结果同一张原始图的 8 个增强变体,一半进了训练集、一半进了验证集,验证分数虚高得离谱,换真实数据立刻打回原形。

正确做法是先按原始样本划分,再只对训练集做增强。比如手头原始样本 30 张,按 8:2 随机分出 24 张训练、6 张验证,然后只对这 24 张调用 amplifyData.py 做增强,验证集保持原样。这样验证集全部来自训练没见过的原始样本,IoU 和 Dice 才有参考价值,导师问起来也讲得清楚。

另一个细节是随机种子。每次跑都换一批验证集,调参时对比的就不是同一个基准。我一般会在划分脚本里固定 random.seed(42) 和 numpy.random.seed(42),保证实验可复现。这个习惯在交毕业设计时尤其重要,导师要复现实验,结果对不上会非常被动。

3. 均值计算与数据增强:calc-mean.py 和 amplifyData.py 的运行顺序与参数

先确认环境:Python 3.8 以上,装好 opencv-python 和 numpy 就能跑这两个脚本;要训练模型再补 PyTorch。新手上路建议先装 CPU 版把流程跑通,再用 GPU 训练,代码层面的差异几乎为零,只是速度快慢的问题。

3.1 calc-mean.py:为什么先算均值再进模型

calc-mean.py 的功能从名字就能看出来:统计整个数据集每个通道的像素均值。它的输出不是给人看的,而是给训练和推理做归一化用的。

PyTorch 里常见做法是先把图像缩放到 [0,1],再减均值、除标准差。如果训练和推理用的均值不一致,相当于输入分布变了,模型输出会忽高忽低。很多裂缝项目翻车就翻在这:训练时用了通用 ImageNet 均值,推理时换成自己数据集的均值,同一张图预测两次,结果差不少,而且说不清哪个是对的。

脚本逻辑参考这段:

import os import cv2 import numpy as np img_dir = "dataset-kit" sums = np.zeros(3, dtype=np.float64) pixels = 0 for name in os.listdir(img_dir): if not name.endswith(".jpg") or "_gt" in name: continue img = cv2.imread(os.path.join(img_dir, name)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR -> RGB,通道顺序对齐 sums += img.reshape(-1, 3).sum(axis=0) pixels += img.shape[0] * img.shape[1] mean = sums / pixels print("RGB mean:", np.round(mean, 4))

两个细节值得说。cv2.imread 读进来是 BGR 顺序,PyTorch 训练一般用 RGB,必须先转换,否则算出的均值通道错位,模型学到的颜色语义是错的。累加用 float64,样本量到几百张时 float32 累加会丢精度,而裂缝图的暗像素特别多,误差会被放大。

跑完会得到类似 [0.4821, 0.4512, 0.4124] 的三通道均值,保存下来。训练脚本、验证脚本、推理脚本必须用同一个值,这是后面第 6 章批量推理的基准,也是整个项目输入标准化的锚点。

3.2 amplifyData.py:旋转翻转的增强逻辑与标签联动

amplifyData.py 解决的是数据集太小的问题。手工标注几十张裂缝图直接喂给 UNet,几个 epoch 后就过拟合了,验证集上 IoU 上不去。增强的核心原则是:图像怎么变,标签图就怎么变,两者必须用同一套几何变换参数。

常见实现里,旋转 90/180/270 度加水平翻转、垂直翻转,一张图能扩出 8 张。参考写法如下:

import cv2 import os def augment_one(img_path, gt_path, out_dir): img = cv2.imread(img_path) gt = cv2.imread(gt_path, cv2.IMREAD_GRAYSCALE) base = os.path.splitext(os.path.basename(img_path))[0] rot_ops = { "rot90": cv2.ROTATE_90_CLOCKWISE, "rot180": cv2.ROTATE_180, "rot270": cv2.ROTATE_90_COUNTERCLOCKWISE, } for tag, code in rot_ops.items(): img_r = cv2.rotate(img, code) gt_r = cv2.rotate(gt, code) cv2.imwrite(f"{out_dir}/{base}_{tag}.jpg", img_r) cv2.imwrite(f"{out_dir}/{base}_{tag}_gt.jpg", gt_r) img_h = cv2.flip(img, 1) # 1 = 水平翻转 gt_h = cv2.flip(gt, 1) cv2.imwrite(f"{out_dir}/{base}_hflip.jpg", img_h) cv2.imwrite(f"{out_dir}/{base}_hflip_gt.jpg", gt_h)

关键在两点。cv2.rotate 和 cv2.flip 在图像、标签上各调用一次,参数完全相同,保证变换同步;输出文件名继续沿用 _gt 后缀约定,训练代码的读取逻辑一行不用改。

cv2.flip 的第二个参数:0 是垂直翻转,1 是水平翻转,-1 是双向翻转。写错不会报错,但标签和图像的关系就错了,这种错非常隐蔽,loss 正常下降,预测出来却完全对不上原因。我会在增强脚本跑完后抽几张增强结果,把图和标签叠加看一眼再继续。

3.3 amplifyData-16.py:批量版本与增强倍数的选择

amplifyData-16.py 从命名习惯看,应该是 16 倍增强版本:在旋转翻转的基础上,再加亮度、对比度、平移等扰动,把一张原始图扩展到 16 张。拆包后建议两个脚本都打开对比一下,看差异是增强变换列表更长,还是循环逻辑不同,这能帮你快速判断它的设计意图。

如果确实加了亮度扰动,注意只对图像做,不要对标签做,标签是二值图,变了就失真。裂缝在暗光下的可见度差异很大,模型见过多种亮度之后,泛化会明显改善,尤其适合岩石表面那种光照不均匀的样本。

增强倍数不是越大越好。裂缝是细长结构,过度的旋转翻转和亮度扰动,容易让模型去记住纹理统计规律而不是裂缝本身。我一般控制在 8 到 16 倍,再往上提升有限。如果增强后发现验证集上裂缝出现断裂,先怀疑增强伪影,而不是急着加深网络。

整体运行顺序固定为:先 calc-mean.py 算原始数据均值,再 amplifyData.py 做增强,最后把增强输出目录交给训练脚本。增强前后均值理论上接近,但加了亮度扰动之后,重新算一遍更稳妥。换成自己的数据集时,把脚本里的路径改掉就能直接用,这是这套资源迁移成本最低的部分。

4. 裂缝语义分割常见问题与避坑:五个真实踩坑记录

这一章我把跑这套资源时最容易翻车的五个点按「现象 → 原因 → 解决」拆开讲,这几个坑我在其他裂缝项目里也反复见到,属于典型的高频事故现场。

4.1 标签没跟着图像一起增强

现象:loss 正常下降,训练很顺,但预测出来的 mask 和原图对不上,裂缝明显偏移或形状扭曲。

原因:增强脚本里图像做了旋转,标签读取漏了 IMREAD_GRAYSCALE 参数,读成三通道后变换逻辑分叉;或者代码报错后有人直接把标签变换那行注释掉了,图转了标签没转。

解决:标签读取强制用灰度模式,并在增强后抽检,把增强图和标签图叠加保存,肉眼确认裂缝轮廓重合再进训练。参考:

vis = cv2.addWeighted(img_r, 0.6, cv2.cvtColor(gt_r, cv2.COLOR_GRAY2BGR), 0.4, 0) cv2.imwrite("check_aug.jpg", vis)

这一步 30 秒,能省掉后面一整个星期的排错时间。我后来养成的习惯是每次增强脚本改完必查一次叠加图,不改也定期抽查。

4.2 JPEG 压缩伪影被学成裂缝

现象:验证集 IoU 不低,但把预测结果放大看,裂缝周围多出一圈细碎的白色噪点,而且这些噪点在每张图上都规律地出现在物体边缘。

原因:示例图是 jpg 格式,压缩时边缘会出现方块伪影,裂缝的锐利边界恰恰是伪影最严重的位置。网络很容易把这种高频噪声当成裂缝特征,学到的边界比真实裂缝宽一圈。

解决:训练前对输入图做一次轻微高斯模糊,或者统一缩放到 512×512 以下来削弱伪影。注意不要用中值滤波,它会把细小裂缝一起抹掉。这一步在数据加载函数里做就行,不用改动保存的文件。

4.3 灰度 CT 图被当成三通道导致均值错乱

现象:CT.jpg 训练效果明显比岩石图差,loss 下降得慢,而且学习率怎么调都救不回来。

原因:CT 岩心切片本质是灰度图,很多读取脚本复制成三通道,calc-mean 算出来的均值三个通道几乎相同,和预训练权重假设的分布差很远,等于喂错了分布给模型。

解决:CT 图单独算均值、单独归一化,或者干脆降采样后当成单通道输入。我一般会把 CT 样本和 rock、concrete 样本分开建两个训练集,各算各的均值、各训各的模型,互不污染。如果硬要放一起训练,至少要把归一化参数拆开。

4.4 显存 OOM 与 batch size 的取舍

现象:训练一开始就报 CUDA out of memory,或者跑几个 epoch 之后爆掉。

原因:裂缝图为了保留细节常用 512×512 输入,UNet 浅层特征图通道数大,显存占用比普通分类网络高一个量级,直接照搬分类任务的 batch size 必然出事。

解决:先降 batch size 到 2 或 4,还不行就缩输入到 256×256。裂缝分割对分辨率敏感,但 256×256 足够把流程跑通,确认代码没问题再逐步调大。每个 epoch 验证前加一句 torch.cuda.empty_cache(),避免验证阶段显存碎片累积。

4.5 指标高但可视化裂缝断裂

现象:Dice 到了 0.85 以上,平均 IoU 也好看,但单张图放大看裂缝被切成几段,断的地方正好是最细的部分。

原因:预测输出是概率图,用 0.5 阈值一刀切,细裂缝处的概率值在 0.4 到 0.6 之间波动,阈值稍高就断。这是裂缝分割的后处理问题,不是模型结构问题,改网络结构没用。

解决:预测后先做形态学闭运算补断口,再做开运算清噪点。具体参数在第 6 章给,这里先记住结论:固定 0.5 阈值是最懒也最容易翻车的做法,后处理至少要做一步闭运算。

5. 训练参数与验证方法:裂缝分割模型怎么调才不虚高

5.1 模型选型:为什么裂缝优先选 UNet 结构

裂缝是细长结构,边界信息比语义信息重要。UNet 的编码器逐层下采样提取语义,解码器通过跳跃连接把各层的空间细节拼回来,输出边缘比纯下采样网络锐利很多,这是它长期霸榜裂缝分割的根本原因。

DeepLabV3 这类空洞卷积模型在自然场景分割上很强,但它高度依赖 ImageNet 预训练权重,裂缝数据的分布和 ImageNet 差太远,直接迁移效果往往不如从零训练的 UNet。百张级别的小数据集,UNet 从零训练反而更可控,显存占用也友好。

做毕业设计的话,建议先用 UNet 把 pipeline 跑通、拿到指标,再换 DeepLabV3 做对比实验。论文里既能交代 baseline,又有明确的改进点,导师会认为你对模型选型有实际思考,而不是随手抄了个网络。

5.2 损失函数:Dice Loss 与 BCE 的组合

裂缝像素占比低,单用 BCE 会让模型倾向全预测背景。Dice Loss 直接优化目标区域的交并比,对类别不平衡天然不敏感。常见组合是 BCE 和 Dice 各占一半:

def dice_loss(pred, gt, eps=1e-6): pred = torch.sigmoid(pred) # logit -> 概率 inter = (pred * gt).sum(dim=(1, 2, 3)) union = pred.sum(dim=(1, 2, 3)) + gt.sum(dim=(1, 2, 3)) dice = (2 * inter + eps) / (union + eps) return 1 - dice.mean() loss = 0.5 * torch.nn.functional.binary_cross_entropy(pred.sigmoid(), gt) \ + 0.5 * dice_loss(pred, gt)

两个容易错的位置:pred 是 logit,进 dice_loss 前先过 sigmoid;gt 必须是 0/1 的 float 张量,如果标注读进来是 0/255,记得先除 255。把 255 直接喂进去,dice 的分子分母都会膨胀,loss 直接变 NaN,训练白跑。

顺便说一句,dice_loss 里那个 eps 不能省。裂缝区域太小,遇到某张图完全没有裂缝的极端情况,交并集都可能为 0,不加 eps 会出现除零,数值计算上就是玄学问题,加了就稳。

5.3 训练超参数与评估指标

直接抄这个初始参数表,跑通后再微调:

参数推荐值说明
输入分辨率256×256 或 512×512先小后大,确认问题再提分辨率
batch size4~8显存优先,OOM 就减半
优化器Adamlr 1e-4 起步
学习率策略ReduceLROnPlateau验证 loss 不降时减半
epoch50~100配 early stopping,防过拟合
预测阈值0.5后处理里再按效果微调

评估指标用 IoU 和 Dice,不要只看 pixel accuracy。裂缝占比 2% 时,全预测背景的准确率也有 98%,数字漂亮但模型什么都没学到。交实验报告时只贴准确率,导师基本一眼就能看出水分。

def calc_iou(pred, gt, eps=1e-6): pred = (pred > 0.5).astype(np.uint8) gt = (gt > 0.5).astype(np.uint8) inter = np.logical_and(pred, gt).sum() union = np.logical_or(pred, gt).sum() return (inter + eps) / (union + eps)

训练完把每张验证图的 IoU 单独打印,看看是不是固定某几张特别低。如果是,先查那几张图的标注有没有漏标、和训练样本的形态差异是不是太大,再决定调参还是补数据,顺序不能反。调参解决不了数据问题,这个优先级搞错了会浪费大量时间。

6. 进阶用法:批量推理时把 calc-mean 和形态学后处理组合起来

训练完成后批量推理,最容易翻车的就是归一化参数不一致。我现在会把 calc-mean.py 的输出固定到一个 JSON 文件,推理脚本启动时读进来,保证训练、验证、推理三段用同一个均值,彻底杜绝「训练一个均值、推理一个均值」的隐性问题。

6.1 推理脚本的输入标准化

把均值保存下来的方式很随意,重定向输出即可:

python calc-mean.py > mean.json

推理时对每张图:转 RGB、缩放到 [0,1]、减均值,送入模型,得到概率图。注意减均值前图像的缩放范围要和训练时完全一致,这是最容易被忽略的一环。很多人只记得减均值,忘了前面的除法,预测效果差但又找不到原因。

6.2 形态学后处理把断裂裂缝接回来

后处理我固定用两步:

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=1) # 先补断口 mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1) # 再清噪点

闭运算在前,把细裂缝的断口接上;开运算在后,清掉孤立白点。kernel 别超过 3×3,裂缝往往只有一到两个像素宽,kernel 大了会把真裂缝一起吞掉。之后再用连通域统计,把面积小于阈值的碎块直接丢弃:

n, labels, stats, _ = cv2.connectedComponentsWithStats(mask.astype(np.uint8)) for i in range(1, n): if stats[i, cv2.CC_STAT_AREA] < 20: mask[labels == i] = 0

面积阈值 20 是按像素算的,图分辨率变了要重新调。这个后处理组合做完,可视化效果通常能明显上一个台阶,断裂的裂缝接回来,误检的噪点被清掉,交上去的分割结果图干净很多。

从那以后我每次做完裂缝分割项目,都会强制走一遍「算均值 → 增强 → 训练 → 同一均值推理 → 形态学后处理」的闭环,后处理参数单独记录,不再拍脑袋调。这套资源能不能出好的可视化结果,往往不在网络结构,而在这些管线细节。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询