ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

岩石裂缝语义分割实战:从CT岩心到像素级标注的完整链路

岩石裂缝语义分割实战:从CT岩心到像素级标注的完整链路 简介这份资源面向地质、石油工程与计算机视觉方向的学习者提供基于Python的岩石裂缝与CT岩心裂缝语义分割完整实践素材帮助读者掌握从数据读取到模型训练、评估的全流程。包内共10个文件以6张jpg示例图像、3个py脚本和1份md说明为主压缩包约1.12MB其中图像涵盖岩石、混凝土与CT岩心及其对应标注脚本涉及数据增强与均值计算等预处理环节。已有207人学习下载适合希望将深度学习应用于地质图像分析的入门与进阶读者。通过该资源读者可了解U-Net等编码器-解码器结构在裂缝分割中的实现思路熟悉训练集与验证集划分、IoU等指标评估方法并借助数据增强脚本扩充样本多样性为储层渗透率与储油能力研究提供可复用的代码与数据参考。1. 岩石裂缝语义分割资源包从 CT 岩心到像素级标注的完整链路拿到一份岩石裂缝语义分割的源码加数据集最怕的不是跑不通而是跑通了却不知道每一步在干什么。这个资源包围绕 Python 语义分割展开核心任务是把岩石、混凝土、CT 岩心三类图像里的裂缝逐像素抠出来属于典型的二分类语义分割场景。包里给了rock.jpg、concrete.jpg、CT.jpg三组原图以及对应的rock_gt.jpg、concrete_gt.jpg、CT_gt.jpg标注图还有amplifyData.py、amplifyData-16.py、calc-mean.py三个脚本和一份README.md。它适合想入门语义分割但缺真实数据的人也适合做地质、石油工程、材料检测方向、需要把裂缝识别自动化的从业者。下面按数据、增强、训练、评估、避坑的顺序拆开讲。2. 数据组织与标注格式三组图像怎么读、怎么对齐2.1 目录结构与文件职责资源包解压后根目录下大致是code、dataset-kit、example三个部分外加README.md。example里放的是示例图像dataset-kit是数据工具集code是训练和推理脚本。三组图像命名规律很清晰原图不带后缀标注图带_gt。rock.jpg对应rock_gt.jpgconcrete.jpg对应concrete_gt.jpgCT.jpg对应CT_gt.jpg。这种命名方式在语义分割里很常见好处是写 DataLoader 时可以直接用字符串替换生成标注路径不用额外维护映射表。常见做法是把原图和标注图分目录存放比如images/和masks/文件名保持一致。这个包目前是平铺的正式训练前建议先整理成标准结构否则数据量一多容易乱。整理脚本可以这样写import os import shutil # 原始平铺目录整理成 images / masks 两个子目录 src_dir ./dataset-kit img_dir ./dataset/images mask_dir ./dataset/masks os.makedirs(img_dir, exist_okTrue) os.makedirs(mask_dir, exist_okTrue) for fname in os.listdir(src_dir): if not fname.lower().endswith((.jpg, .png)): continue src_path os.path.join(src_dir, fname) if _gt in fname: shutil.copy(src_path, os.path.join(mask_dir, fname)) else: shutil.copy(src_path, os.path.join(img_dir, fname)) print(整理完成原图与标注已分离)这段逻辑很直白遍历源目录凡是文件名含_gt的归到masks其余归到images。参数上唯一要注意的是扩展名过滤.jpg和.png都要覆盖因为不同来源的标注图格式可能不统一。整理完之后训练脚本里读数据就只需要按文件名配对不用再关心路径拼接的玄学问题。2.2 标注图的读取陷阱灰度图不是单通道语义分割的标注图有两种常见存法一种是灰度图像素值 0 表示背景、255 表示裂缝另一种是调色板图每个类别对应一个 RGB 颜色。这个包里的_gt.jpg从命名看是 JPEG 格式JPEG 是有损压缩边缘像素值可能被轻微改变导致原本 255 的裂缝边缘变成 254 或 253。如果训练时直接用label 255做判断边缘就会丢像素。我一般会先把标注图转成灰度再做阈值化而不是直接拿原始像素值当类别。代码示例如下import cv2 import numpy as np def load_mask(mask_path, threshold128): # 以灰度模式读取避免三通道干扰 mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: raise FileNotFoundError(mask_path) # 二值化大于阈值视为裂缝否则背景 _, binary cv2.threshold(mask, threshold, 1, cv2.THRESH_BINARY) return binary.astype(np.uint8) m load_mask(./dataset/masks/rock_gt.jpg) print(裂缝像素占比:, m.mean())这里threshold128是经验值因为裂缝标注通常是高亮区域。cv2.IMREAD_GRAYSCALE保证读进来就是单通道省去后续转换。m.mean()输出的是裂缝像素占全图的比例如果这个值接近 0 或接近 1说明标注可能反了或者阈值不对。参数上如果标注图背景是白色、裂缝是黑色阈值判断要反过来这个在换数据集时一定要先可视化确认。2.3 训练集与验证集的划分策略三组图像数量很少直接按比例随机划分意义不大。更合理的做法是按图像类型划分比如用rock和concrete做训练用CT做验证这样能检验模型在不同成像模态之间的泛化能力。如果非要随机划分建议固定随机种子并且保证每类图像至少有一张进验证集。import random random.seed(42) all_images [rock.jpg, concrete.jpg, CT.jpg] random.shuffle(all_images) split int(len(all_images) * 0.67) train_list all_images[:split] val_list all_images[split:] print(训练集:, train_list) print(验证集:, val_list)random.seed(42)是为了让每次运行划分结果一致方便复现。0.67的划分比例在极小数据集上比较常见但样本少于 10 张时这种划分的统计意义很弱验证指标波动会很大。更稳妥的方式是做交叉验证或者把amplifyData.py增强后的图像也纳入训练集扩大样本量。3. 数据增强脚本 amplifyData.py参数怎么设、增强后怎么存3.1 amplifyData.py 与 amplifyData-16.py 的分工包里有两个增强脚本amplifyData.py和amplifyData-16.py。从命名推测前者是通用增强后者可能针对 16 位图像或者 16 倍增强。CT 图像常见 16 位灰度动态范围比 8 位大得多如果直接按 8 位方式做增强像素值会被截断。所以amplifyData-16.py很可能是处理 16 位 CT 图像的版本读取时要指定cv2.IMREAD_UNCHANGED或IMREAD_ANYDEPTH。常见做法是8 位图像用amplifyData.py16 位 CT 用amplifyData-16.py。如果混用16 位图会被当成 8 位读高亮度区域全部饱和成 255裂缝细节直接丢失。这个坑我在处理医学 CT 时踩过血泪经验就是先看img.dtype确认是uint8还是uint16。3.2 几何增强与像素增强的参数选择语义分割的数据增强必须保证原图和标注图同步变换否则图像和标签对不上训练直接翻车。几何增强包括翻转、旋转、缩放像素增强包括亮度、对比度、噪声。对于裂缝检测水平翻转和垂直翻转通常是安全的因为裂缝方向没有严格的上下左右语义。旋转要小心90 度整数倍旋转安全任意角度旋转会引入插值标注图的边缘会变模糊。import cv2 import numpy as np import os def augment_pair(img, mask): # 水平翻转 img_h cv2.flip(img, 1) mask_h cv2.flip(mask, 1) # 垂直翻转 img_v cv2.flip(img, 0) mask_v cv2.flip(mask, 0) # 90 度旋转 img_r cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) mask_r cv2.rotate(mask, cv2.ROTATE_90_CLOCKWISE) return [(img_h, mask_h), (img_v, mask_v), (img_r, mask_r)] img cv2.imread(./dataset/images/rock.jpg) mask cv2.imread(./dataset/masks/rock_gt.jpg, cv2.IMREAD_GRAYSCALE) pairs augment_pair(img, mask) for i, (im, ma) in enumerate(pairs): cv2.imwrite(f./dataset/images/rock_aug_{i}.jpg, im) cv2.imwrite(f./dataset/masks/rock_aug_{i}.jpg, ma) print(增强完成新增, len(pairs), 对样本)cv2.flip的第二个参数1 是水平翻转0 是垂直翻转-1 是同时翻转。cv2.rotate用ROTATE_90_CLOCKWISE保证不引入插值。增强后的文件名加_aug_前缀避免覆盖原图。参数上增强倍数不要盲目调大三组原图增强后变成 12 张已经能缓解小样本问题再多了容易过拟合到增强后的特定模式。3.3 增强后的数据校验增强完必须做一次校验确认图像和标注仍然对齐。最简单的办法是把标注叠加到原图上看裂缝位置是否吻合。overlay img.copy() overlay[mask 128] [0, 0, 255] # 裂缝标红 cv2.imwrite(./check_overlay.jpg, overlay)如果红色区域和肉眼看到的裂缝位置一致说明增强没出问题。如果红色区域偏移或者镜像反了说明翻转时只处理了图像没处理标注或者旋转方向不一致。这个校验步骤花不了几秒钟但能省掉后面训练几小时才发现标签错位的后悔药。4. calc-mean.py 与训练配置均值计算、损失函数与评估指标4.1 calc-mean.py 在做什么calc-mean.py从名字看是计算数据集均值和标准差的脚本。图像归一化时常用(x - mean) / std其中 mean 和 std 通常取 ImageNet 的[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]。但岩石和 CT 图像的分布和自然图像差别很大用 ImageNet 的统计量不一定最优所以这个脚本很可能是遍历训练集计算每个通道的均值和标准差。import cv2 import numpy as np import os img_dir ./dataset/images means, stds [], [] for fname in os.listdir(img_dir): img cv2.imread(os.path.join(img_dir, fname)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) / 255.0 means.append(img.reshape(-1, 3).mean(axis0)) stds.append(img.reshape(-1, 3).std(axis0)) mean np.mean(means, axis0) std np.mean(stds, axis0) print(均值:, mean) print(标准差:, std)cv2.cvtColor把 BGR 转成 RGB因为大多数预训练模型按 RGB 顺序训练。除以 255 是把像素缩到 0 到 1。reshape(-1, 3)把图像拉成像素列表每行一个 RGB 值。算出来的 mean 和 std 直接填进训练脚本的归一化层即可。如果数据集很小这个统计量本身波动大可以结合 ImageNet 的值做加权平均或者干脆不做减均值只做除以 255。4.2 损失函数选择BCE 还是 Dice裂缝分割是典型的类别不平衡问题裂缝像素通常只占全图的百分之几甚至千分之几。如果只用二元交叉熵BCE模型会倾向于全部预测为背景因为这样损失也很低。常见做法是 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) intersection (probs * targets).sum() dice_loss 1 - (2 * intersection 1e-6) / (probs.sum() targets.sum() 1e-6) return self.bce_weight * bce_loss (1 - self.bce_weight) * dice_lossBCEWithLogitsLoss内部做了 sigmoid数值更稳定。Dice 计算里加1e-6是防止分母为零。bce_weight0.5是起点如果训练时发现召回率低、漏检多可以调低 bce_weight让 Dice 占更大比重。参数上Dice 的平滑项不要设太大否则小目标会被平滑掉。4.3 评估指标 IoU 的计算与解读IoUIntersection over Union是分割任务最常用的指标计算的是预测区域和真实区域的交集除以并集。对于二分类IoU 可以按前景算也可以按背景算通常报告的是前景 IoU也就是裂缝的 IoU。def compute_iou(pred, target, threshold0.5): pred_bin (pred threshold).astype(np.uint8) target_bin (target 0.5).astype(np.uint8) intersection (pred_bin target_bin).sum() union (pred_bin | target_bin).sum() if union 0: return 1.0 if intersection 0 else 0.0 return intersection / unionthreshold0.5是把概率图转成二值图。union 0表示预测和真实都没有裂缝这种情况按惯例返回 1.0。如果返回 0.0会把“正确预测无裂缝”也当成错误指标会失真。实际报告时建议同时给出 precision 和 recall因为 IoU 高不代表漏检少裂缝检测里漏检往往比误检更致命。5. 避坑与排查裂缝分割训练中最容易翻车的五件事5.1 现象训练 loss 一直降但验证 IoU 始终接近 0原因通常是标注图读取时通道顺序或阈值反了。JPEG 标注图读进来可能是三通道mask 128对三通道做判断会得到三维布尔数组后续和单通道预测做 loss 时广播出错或者被错误地当成全背景。解决方法是强制用cv2.IMREAD_GRAYSCALE读取标注并在训练前可视化几张标注图确认裂缝区域是高亮而不是背景。5.2 现象增强后图像和标注错位裂缝跑到岩石外面原因是几何变换只作用在原图上标注图没有同步变换或者旋转角度不一致。解决方法是把原图和标注封装成 pair所有变换函数同时接收两个输入返回两个输出。写增强代码时养成习惯先写img_t, mask_t transform(img, mask)再分别保存不要分开调用两次随机变换。5.3 现象CT 图像训练时 loss 突然变成 NaN原因是 16 位 CT 图像没有归一化就直接送进网络像素值范围 0 到 65535经过卷积和激活后数值爆炸。解决方法是读取时用cv2.IMREAD_UNCHANGED保留位深然后除以 65535 缩到 0 到 1或者用calc-mean.py算出的统计量做标准化。如果已经出现 NaN检查输入数据的max()和min()确认范围在合理区间。5.4 现象模型在 rock 上表现好在 CT 上 IoU 掉一半原因是不同成像模态的纹理和对比度差异大模型过拟合到了岩石的表面纹理而不是裂缝的几何形状。解决方法是混合训练把三类图像都放进训练集或者用更强的颜色抖动和灰度变换做增强。如果 CT 样本极少可以考虑先在 rock 和 concrete 上预训练再在 CT 上微调但微调时学习率要调小否则预训练学到的特征会被覆盖。5.5 现象推理时输出全黑或全白原因是推理脚本没有做 sigmoid或者阈值设成了 0 或 1。模型输出的是 logits需要先过torch.sigmoid再阈值化。如果输出全黑检查阈值是不是太高如果全白检查阈值是不是太低。另外推理时的归一化参数必须和训练时一致训练用了calc-mean.py的均值方差推理也要用同一组否则输入分布偏移会导致输出异常。6. 从单张推理到批量评估一个可复用的验证脚本训练完之后最实用的技巧是写一个批量验证脚本把验证集里所有图像跑一遍输出每张的 IoU 和可视化叠加图。这样不用逐张打开看一眼就能定位哪张翻车。import os import cv2 import numpy as np import torch def batch_evaluate(model, img_dir, mask_dir, devicecpu, threshold0.5): model.eval() results [] for fname in sorted(os.listdir(img_dir)): img_path os.path.join(img_dir, fname) mask_path os.path.join(mask_dir, fname.replace(.jpg, _gt.jpg)) if not os.path.exists(mask_path): continue img cv2.imread(img_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) / 255.0 tensor torch.from_numpy(img_rgb).permute(2, 0, 1).float().unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor) prob torch.sigmoid(logits).squeeze().cpu().numpy() gt cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) gt_bin (gt 128).astype(np.uint8) pred_bin (prob threshold).astype(np.uint8) inter (pred_bin gt_bin).sum() union (pred_bin | gt_bin).sum() iou inter / union if union 0 else 1.0 results.append((fname, iou)) # 保存叠加图红色为预测绿色为真实黄色为重合 overlay img.copy() overlay[pred_bin 0] [0, 0, 255] overlay[gt_bin 0] [0, 255, 0] overlay[(pred_bin 0) (gt_bin 0)] [0, 255, 255] cv2.imwrite(f./vis_{fname}, overlay) return results # 使用示例 # results batch_evaluate(model, ./dataset/images, ./dataset/masks) # for name, iou in results: # print(f{name}: IoU{iou:.4f})这段脚本的关键点有三个。第一model.eval()和torch.no_grad()必须加否则 BatchNorm 和 Dropout 会干扰推理结果显存也会爆。第二permute(2, 0, 1)把 HWC 转成 CHW这是 PyTorch 的标准输入格式。第三叠加图用三种颜色区分预测、真实和重合黄色越多说明预测越准。参数上threshold0.5是默认值如果发现漏检多可以降到 0.3误检多可以升到 0.7具体看业务对漏检和误检的容忍度。跑完批量评估后我会习惯性地把 IoU 最低的那张叠加图打开看通常能发现一些共性问题比如某类纹理被误判成裂缝或者裂缝太细导致断裂。这些问题回头改增强策略或者损失函数权重比盲目调参有效得多。从那以后我每次训练完都强制走一遍批量可视化再决定要不要继续调模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表