简介:这份资源面向地质、石油工程与计算机视觉方向的学习者,提供基于Python的岩石裂缝与CT岩心裂缝语义分割完整实践方案,帮助读者掌握从CT图像中自动识别与量化裂缝的深度学习流程。压缩包共10个文件,包含6张jpg示例图像、3个py脚本与1个md说明文档,整体约1.12MB,其中脚本覆盖数据增强、均值计算等预处理环节,图像则提供岩石、混凝土与CT岩心的原图及对应标注,便于直接开展训练与验证。目前已有207人学习下载。资源以U-Net等编码器-解码器结构为核心,涉及数据读取与归一化、模型定义、训练循环、预测评估及IoU指标计算等关键模块,读者可据此搭建可复现的分割基线,并在此基础上调整超参数、更换损失函数或引入跳跃连接与模型融合策略,从而提升裂缝分割精度,适合作为地质图像智能分析的入门与进阶参考。
1. 拿到一份岩石裂缝语义分割包,先别急着跑 train.py
地质和石油工程方向的朋友对 CT 岩心裂缝识别应该不陌生:一块岩心扫完 CT,几十上百张切片,靠人眼在灰度图里描裂缝,一天下来眼睛发花还容易漏掉细微裂隙。这份「基于 Python 的岩石裂缝与 CT 岩心裂缝语义分割源码 + 数据集」压缩包,干的就是把这件事自动化——用语义分割模型把每张岩心图像里的裂缝像素单独抠出来。包里的结构很直白:code目录放脚本,dataset-kit放数据,根目录散着rock.jpg、rock_gt.jpg、CT.jpg、CT_gt.jpg、concrete.jpg、concrete_gt.jpg这几组原图与标注对照,外加amplifyData.py、amplifyData-16.py、calc-mean.py三个工具脚本和一份README.md。它适合两类人:一是做岩石力学、储层表征、材料缺陷检测的研究生和工程师,想快速搭一个能跑通的裂缝分割 baseline;二是刚接触语义分割、想拿真实工业图像练手的开发者。不适合指望开箱即用、直接上生产的人——这份资源是学习和二次开发的起点,不是成品软件。
2. 拆开压缩包:数据组织、标注格式与三个脚本的真实分工
2.1 目录结构与文件角色
先把包解开,按功能把文件归位。dataset-kit是数据主目录,code是代码主目录,根目录那几张 jpg 是给你快速验证的样例对。命名规律很关键:xxx.jpg是原图,xxx_gt.jpg是对应的 ground truth 标注图。rock是岩石表面裂缝,CT是 CT 岩心切片,concrete是混凝土裂缝——三类场景共用一套分割流程,说明作者想让你看到方法的泛化性。
| 文件/目录 | 类型 | 作用 |
|---|---|---|
code/ | 目录 | 训练、预测、评估脚本 |
dataset-kit/ | 目录 | 图像与标注数据 |
amplifyData.py | 脚本 | 数据增强(基础版) |
amplifyData-16.py | 脚本 | 数据增强(16 位/特定尺寸版) |
calc-mean.py | 脚本 | 计算数据集均值,用于归一化 |
rock.jpg/rock_gt.jpg | 样例 | 岩石裂缝原图与标注 |
CT.jpg/CT_gt.jpg | 样例 | CT 岩心原图与标注 |
concrete.jpg/concrete_gt.jpg | 样例 | 混凝土裂缝原图与标注 |
README.md | 文档 | 环境与运行说明 |
calc-mean.py的存在说明作者在意归一化——语义分割对输入分布敏感,用数据集自身的均值和标准差做标准化,比拍脑袋用 ImageNet 的均值更稳。amplifyData.py和amplifyData-16.py分开,通常是因为 CT 图像位深和普通 RGB 照片不同,增强时的插值方式、像素范围处理要区别对待。
2.2 标注格式与类别定义
语义分割的标注是逐像素的掩膜(mask)。这份资源里标注以_gt.jpg形式给出,意味着标注被存成了图像。这里有个必须确认的点:标注图是单通道灰度还是三通道彩色。常见做法是裂缝像素为 255(白),背景为 0(黑),但有些数据集会用 1/0 或特定调色板。动手前先跑一段检查代码:
import numpy as np from PIL import Image # 读取一张标注图,确认它的通道数和像素取值分布 gt = np.array(Image.open('rock_gt.jpg')) print('shape:', gt.shape) # (H, W) 或 (H, W, 3) print('dtype:', gt.dtype) # uint8 常见 print('unique values:', np.unique(gt)[:10]) # 看是不是 0/255 或 0/1逻辑说明:shape是二维说明是单通道掩膜,三维就要判断是否三通道等值。unique values直接告诉你类别编码方式。如果只有[0, 255],训练时把标注除以 255 变成 0/1 即可;如果出现[0, 1],说明已经是二值,别再除。参数上,np.unique只取前 10 个值避免大图打印爆炸。这一步不做,后面 loss 算出 NaN 你都不知道为什么。
2.3 三个脚本的调用顺序
合理的使用顺序是:先calc-mean.py算归一化参数,再amplifyData.py扩增数据,最后进训练。calc-mean.py典型实现是遍历训练集所有图像累加像素和:
import os import numpy as np from PIL import Image # 遍历数据目录,累加每个通道的像素值,最后求均值 data_dir = 'dataset-kit' pixel_sum = np.zeros(3) pixel_count = 0 for fname in os.listdir(data_dir): if fname.endswith('.jpg') and '_gt' not in fname: img = np.array(Image.open(os.path.join(data_dir, fname))).astype(np.float64) if img.ndim == 2: # 灰度图补成三通道统一处理 img = np.stack([img]*3, axis=-1) pixel_sum += img.reshape(-1, 3).sum(axis=0) pixel_count += img.shape[0] * img.shape[1] mean = pixel_sum / pixel_count print('mean per channel:', mean / 255.0) # 归一化到 0~1 区间逻辑说明:pixel_sum按通道累加,pixel_count统计总像素数,相除得到均值。灰度图用np.stack补成三通道,保证后续统一。参数上,astype(np.float64)防止累加溢出,最后除以 255 是因为多数框架输入期望 0~1。把打印出的均值填进训练脚本的归一化配置里,这一步省了,模型收敛会明显变慢。
3. 从原图到训练张量:数据增强与 U-Net 输入管线的搭建
3.1 为什么裂缝分割必须做增强
裂缝在图像里是细长、低对比度的结构,样本量往往只有几十到几百张。不做增强,模型很快过拟合到某几张图的纹理上。amplifyData.py和amplifyData-16.py就是干这个的。裂缝分割的增强有个讲究:几何变换(翻转、旋转)对裂缝形态是安全的,但颜色抖动要克制——CT 图像的灰度直接反映密度,乱调亮度会破坏物理意义。常见做法是水平翻转、垂直翻转、90 度旋转、小角度仿射,再加轻微高斯噪声。
3.2 增强脚本的参数与调用
以amplifyData.py为例,典型结构是读原图和标注、同步做相同变换、写出新文件。关键是原图和标注必须用同一组随机参数,否则图和 mask 对不上,这是新手最容易翻车的地方。
import os import random import numpy as np from PIL import Image # 对原图和标注同步做翻转/旋转,保证几何一致 def augment_pair(img, gt): if random.random() < 0.5: img, gt = np.fliplr(img), np.fliplr(gt) # 水平翻转 if random.random() < 0.5: img, gt = np.flipud(img), np.flipud(gt) # 垂直翻转 k = random.choice([0, 1, 2, 3]) img, gt = np.rot90(img, k), np.rot90(gt, k) # 90度整数倍旋转 return img.copy(), gt.copy() src_dir, out_dir = 'dataset-kit', 'dataset-kit/aug' os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(src_dir): if fname.endswith('.jpg') and '_gt' not in fname: base = fname[:-4] img = np.array(Image.open(os.path.join(src_dir, fname))) gt = np.array(Image.open(os.path.join(src_dir, base + '_gt.jpg'))) for i in range(4): # 每张图扩增4份 ai, ag = augment_pair(img, gt) Image.fromarray(ai).save(os.path.join(out_dir, f'{base}_aug{i}.jpg')) Image.fromarray(ag).save(os.path.join(out_dir, f'{base}_aug{i}_gt.jpg'))逻辑说明:augment_pair里所有随机操作同时作用于img和gt,这是同步增强的核心。np.rot90用整数倍旋转避免插值引入的像素值漂移,对 mask 尤其重要——旋转 45 度会让标注边缘产生非 0/255 的中间值,污染标签。参数上,range(4)控制扩增倍数,样本极少时可以调到 8 或 16,但要注意磁盘和后续训练时间。amplifyData-16.py大概率是处理 16 位 CT 图的版本,读图时要用Image.open(...).convert('I')或指定模式,别默认按 8 位读,否则灰度动态范围被压掉一半。
3.3 搭一个最小可跑的 U-Net 输入管线
数据准备好后,进模型前要确认张量形状。U-Net 输入通常是(N, C, H, W),标注是(N, 1, H, W)或(N, H, W)。用 PyTorch 写一个 Dataset 验证管线通不通:
import torch from torch.utils.data import Dataset, DataLoader import numpy as np from PIL import Image class CrackDataset(Dataset): def __init__(self, img_dir, size=256): self.files = [f for f in os.listdir(img_dir) if f.endswith('.jpg') and '_gt' not in f] self.dir = img_dir self.size = size def __len__(self): return len(self.files) def __getitem__(self, idx): base = self.files[idx][:-4] img = Image.open(f'{self.dir}/{base}.jpg').convert('L').resize((self.size, self.size)) gt = Image.open(f'{self.dir}/{base}_gt.jpg').convert('L').resize((self.size, self.size)) img = np.array(img, dtype=np.float32) / 255.0 gt = (np.array(gt, dtype=np.float32) > 127).astype(np.float32) # 二值化 return torch.from_numpy(img).unsqueeze(0), torch.from_numpy(gt).unsqueeze(0) ds = CrackDataset('dataset-kit/aug') loader = DataLoader(ds, batch_size=4, shuffle=True) x, y = next(iter(loader)) print('input:', x.shape, 'label:', y.shape) # 期望 (4,1,256,256)逻辑说明:convert('L')统一转灰度,因为裂缝分割不依赖颜色。resize到固定尺寸是 batch 训练的前提。标注用> 127二值化,把可能的抗锯齿灰边归到背景,避免标签不纯。参数上,size=256是显存和细节的折中,CT 图裂缝细,可以试 512,但 batch_size 要相应降到 2 或 1。unsqueeze(0)补通道维,让输入符合卷积要求。跑通这段,说明数据侧没问题,可以接模型了。
4. 训练裂缝分割模型的避坑清单:从 loss 不降到指标虚高
4.1 现象:loss 一直卡在 0.6 附近不降
原因通常是类别极度不平衡。裂缝像素占比可能只有百分之几,模型全预测背景就能拿到很低的 loss,但 IoU 接近 0。解决:换损失函数。二分类分割常用 Dice Loss 或 BCE + Dice 组合,Dice 直接优化重叠度,对不平衡更鲁棒。
import torch import torch.nn as nn # Dice Loss:直接优化预测与标注的重叠度,缓解类别不平衡 class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super().__init__() self.smooth = smooth def forward(self, logits, targets): probs = torch.sigmoid(logits) probs = probs.view(-1) targets = targets.view(-1) inter = (probs * targets).sum() dice = (2 * inter + self.smooth) / (probs.sum() + targets.sum() + self.smooth) return 1 - dice逻辑说明:smooth防止分母为 0。view(-1)把整批拉平算全局 Dice,比逐样本算再平均更稳。参数上,smooth=1.0是常规起点,样本极少时可调小到 1e-5。配合BCEWithLogitsLoss按 0.5:0.5 加权,通常比单用 BCE 收敛快得多。
4.2 现象:验证集 IoU 很高,实际预测全是噪点
原因多半是标注泄漏或数据划分不当。如果增强后的图和原图被分到了训练集和验证集两侧,验证集里其实有训练样本的近亲,指标虚高。解决:先划分再增强,或者按原图分组划分,保证同一张原图的增强版本只出现在一侧。另一个可能是标注本身有噪声,_gt.jpg经过 JPEG 压缩后边缘出现灰色过渡带,二值化阈值设 127 会把一部分裂缝边缘判成背景。可以试阈值 100 或 150,看验证指标变化。
4.3 现象:CT 图和普通照片混训,模型两边都不讨好
原因:CT 图像是灰度、高动态范围、纹理平滑;岩石和混凝土照片是 RGB、纹理丰富。直接混在一起,归一化参数打架。解决:要么分开训练两个模型,要么在输入层做场景归一化——对 CT 图单独做直方图均衡或 CLAHE 增强对比度后再统一到同一分布。calc-mean.py如果是对全数据集算一个均值,混训场景下建议按子集分别算。
4.4 现象:训练显存爆了,报 CUDA out of memory
原因:输入尺寸 512 且 batch_size 设太大,或者 U-Net 下采样层数多、特征图占显存。解决:先把 batch_size 降到 1 确认能跑,再用梯度累积模拟大 batch。或者把输入裁成 256 的 patch 训练,推理时滑窗拼接。别一上来就上 1024,裂缝细节虽重要,但显存是硬约束。
4.5 现象:推理结果边缘锯齿严重
原因:上采样用了最近邻插值,或者输出没有做后处理。解决:U-Net 解码器用双线性插值,最后输出用sigmoid后按 0.5 阈值二值化,再做一次形态学开运算去掉孤立小点。OpenCV 的morphologyEx几行就能搞定,但注意别把细裂缝也腐蚀没了,核大小用 3x3 起步。
5. 评估与调优:IoU 之外,裂缝分割还该看什么
5.1 别只盯 IoU,裂缝是细结构
IoU 对大面积目标友好,裂缝这种细长目标,预测断成几截和整体偏移,IoU 可能差不多,但实际可用性差很远。建议补两个指标:一是连通域数量比,预测的裂缝连通域个数和标注比,差太多说明断裂严重;二是骨架重合度,把预测和标注都细化成单像素骨架再算距离。常见做法是用skimage.morphology.skeletonize提取骨架,再算平均对称距离。
import numpy as np from skimage.morphology import skeletonize from scipy.ndimage import distance_transform_edt # 计算预测骨架到标注骨架的平均距离,衡量细结构贴合度 def skeleton_distance(pred, gt): sk_pred = skeletonize(pred > 0) sk_gt = skeletonize(gt > 0) dt = distance_transform_edt(~sk_gt) # 每个像素到最近标注骨架点的距离 return dt[sk_pred].mean()逻辑说明:distance_transform_edt对标注骨架的反掩膜做距离变换,得到每个位置到最近骨架点的距离。dt[sk_pred]取出预测骨架位置的距离值,均值越小说明贴合越好。参数上,输入需是二值布尔数组。这个指标比 IoU 更能反映裂缝是否连续、是否偏移。
5.2 后处理:把碎裂缝连起来
模型输出常有断裂。一个实用技巧是形态学闭运算加连通域过滤:先闭运算连接邻近断裂,再删掉面积小于阈值的连通域。阈值按图像分辨率定,256 尺寸下面积小于 20 像素的连通域基本是噪声。
import cv2 import numpy as np # 闭运算连接断裂,再按面积过滤小噪点 def postprocess(mask, kernel_size=3, min_area=20): mask = mask.astype(np.uint8) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) n, labels, stats, _ = cv2.connectedComponentsWithStats(closed) out = np.zeros_like(closed) for i in range(1, n): if stats[i, cv2.CC_STAT_AREA] >= min_area: out[labels == i] = 1 return out逻辑说明:MORPH_CLOSE先膨胀后腐蚀,填补小断裂。connectedComponentsWithStats拿到每个连通域面积,小于min_area的丢弃。参数上,kernel_size越大连接越强但可能粘连相邻裂缝,3 或 5 是常用值;min_area按你的图像分辨率和裂缝最小尺度调。
5.3 一个我踩过的坑:验证集指标好,换一批 CT 就崩
有次我用岩石照片训练的模型直接去预测 CT 岩心,IoU 从 0.7 掉到 0.2。原因是两类图像的灰度分布和纹理尺度差异太大,模型学到的是照片的纹理特征,不是裂缝的几何本质。从那以后我每次跨场景用模型前,都强制先跑一遍目标域的可视化对比,确认输入分布没跑偏,再决定是微调还是重训。这份资源把 rock、CT、concrete 三类放在一起,其实就是在提醒你:方法通用,但权重不通用。想省事就做域适应,想稳就分场景各训一个。希望这份拆解帮到你,少走点我当年走过的弯路。
本文还有配套的精品资源,点击获取