简介面向计算机视觉课程设计与期末大作业这套基于Python的岩石裂缝与CT岩心裂缝语义分割资料包覆盖从图像预处理到模型训练与验证的关键环节。包内共14个文件含6张岩石表面、混凝土断面及CT岩心扫描样例图及对应标注图jpg3个Python脚本分别用于数据增强、均值计算等预处理另附备份文件、README说明与gitignore配置压缩后仅1.13MB轻量易部署。已有171人学习使用适合具备一定Python基础、需要完成图像分割任务的学生参考。借助PIL、OpenCV或Keras等常用库读者可对照源码与标注图理解逐像素分类流程利用amplifyData扩充样本、calc-mean完成归一化直接开展裂缝目标提取与CT图像量化分析实验。原图与真值标注成对提供便于计算IoU等评估指标为课程报告和答辩提供可复现的完整支撑。1. 岩石裂缝与CT岩心裂缝语义分割为什么先做像素级分类而不是画框做岩石力学或油气储层评价的人经常面对两类图像一类是露头或岩心表面的高分辨率照片另一类是CT扫描得到的灰度切片。裂缝在这些图像里往往只有一两个像素宽而且形态蜿蜒、对比度不均。用目标检测画矩形框只能告诉你“这里有裂缝”却给不了裂缝的宽度、走向和面积占比而这些恰恰是工程分析最需要的参数。所以这几年只要谈到裂缝识别语义分割几乎成了默认方案——它对每个像素做分类裂缝像素和非裂缝像素直接分开再往后统计几何特征就很顺手。这篇笔记面向想自己训练裂缝分割模型的人无论你是刚接触Python语义分割的新手还是已经从分类检测转到分割的老手都能找到可复用的源码思路和数据集处理经验。我会把数据集怎么标、训练管线怎么写、CT图像有哪些坑讲清楚最后给出几个能直接抄的进阶技巧。整个方案不依赖某个特定平台TensorFlow或PyTorch都适用代码结构按常见工程习惯组织你拿过去改改就能跑。2. 裂缝数据集的构建与标注转换从原始图像到能直接训练的标准格式2.1 岩石表面照片与CT切片的数据差异裂缝分割的数据集来源主要有两种。岩石表面照片通常是RGB三通道纹理丰富裂缝往往与矿物颗粒边界混在一起需要人眼仔细区分。CT岩心切片则是单通道灰度图裂缝表现为低密度区域灰度值比基质暗但容易受到环状伪影和金属杂质干扰。这两类图像不能直接混在一起训练因为通道数、分辨率、对比度特性都不同。常见的做法是分开建两个子集各自训练或者用域适应方式迁移。从标注角度看岩石表面照片可以借助任何通用标注工具像LabelMe、PPOCRLabel这类导出为JSON多边形。CT切片更推荐直接在切片软件或ImageJ里做阈值初分割再由人工修正。因为CT的裂缝灰度分布相对集中先自动提取候选区再人工剔除误检能省一半时间。但要注意自动初分割只能用做标注辅助不能直接拿来做训练标签否则模型会学到标注工具的误差。2.2 标注工具选择与JSON到掩码的转换我用得最多的是LabelMe因为它的多边形标注对细长裂缝非常友好。裂缝宽度只有几个像素时用矩形框或者画笔都不够灵活而多边形可以贴着裂缝边缘打点。标注完成后每个图像会对应一个同名JSON文件里面记录着每个多边形的顶点坐标和标签名。训练前需要把JSON转成与图像尺寸相同的PNG掩码图裂缝像素为1背景为0。下面这段代码把LabelMe的JSON批量转成PNG掩码适用于单类裂缝分割import json import numpy as np import cv2 from pathlib import Path def labelme_json_to_mask(json_path, img_size): with open(json_path, r, encodingutf-8) as f: data json.load(f) mask np.zeros(img_size, dtypenp.uint8) for shape in data[shapes]: if shape[label] ! crack: continue points np.array(shape[points], dtypenp.int32) cv2.fillPoly(mask, [points], 1) return mask # 使用示例遍历标注目录 json_dir Path(./annotations) for json_file in json_dir.glob(*.json): mask labelme_json_to_mask(json_file, (512, 512)) cv2.imwrite(str(json_file.with_suffix(.png)), mask)这段代码的核心逻辑是遍历JSON里的每个shape只保留标签为“crack”的多边形用cv2.fillPoly把多边形内部填充为1。需要注意img_size必须与原图尺寸一致否则后面DataLoader配对照会错位。我一般会在转换前打印一张原图和掩码叠加图肉眼确认没有偏移——这一步省下来后面训练出的模型会学到错误的边缘特征。2.3 训练集/验证集拆分与数据增强要点很多人在裂缝分割上翻车不是因为模型不行而是数据集拆分不严谨。裂缝图像之间存在很强的相似性如果同一块岩心的连续切片被同时分到训练集和验证集验证指标会虚高真实场景效果却很差。正确的做法是按“样本来源”分组比如同一根岩心的所有切片归为一组以组为单位随机划分到训练或验证而不是按单张图随机分。如果做岩石表面图像则应按露头区域或采样位置分组。数据增强方面裂缝是细线结构对几何变换特别敏感。常规的随机旋转、水平翻转可以保留但像弹性形变、随机裁剪这种会改变裂缝连续性的增强慎用。尤其弹性形变可能把一条完整裂缝扭断反而让模型学会断断续续的预测。我习惯用以下增强组合旋转90度、水平翻转、亮度对比度微调、少量高斯噪声。这些都不会破坏裂缝的拓扑形态。如果训练数据量少可以先用离线方式把裂缝区域裁剪成224或256的小块做平衡比在线增强更有效。3. 用Python跑通UNet与DeepLabV3最小训练管线与关键参数3.1 环境依赖与目录结构裂缝分割属于密集预测常用模型是UNet和DeepLabV3。UNet参数少、对小目标敏感适合裂缝这种细线结构DeepLabV3用了空洞卷积感受野大对CT切片里的弥散裂缝边缘更鲁棒。如果只有一个GPU且显存有限优先选UNet如果数据量大且需要捕捉长距离上下文再考虑DeepLabV3。我把工程目录按下面这种方式组织方便复现和交接crack_seg/ ├── data/ │ ├── images/ │ ├── masks/ │ ├── train.txt │ └── val.txt ├── src/ │ ├── dataset.py │ ├── model.py │ ├── train.py │ └── predict.py ├── weights/ └── config.yamltrain.txt和val.txt每行写一个图像文件的相对路径不带扩展名这样换数据集时不用改代码。依赖方面我通常固定torch、opencv-python、albumentations、tqdm这几个核心包。建议用Python 3.9或3.10太新的版本偶尔会遇到CUDA扩展编译问题。3.2 数据加载器实现数据加载器要做的事很简单从路径列表里读取图像和掩码做统一尺寸resize再转成Tensor。下面是一个可以用到训练脚本里的实现它同时处理RGB和灰度图import torch import cv2 import albumentations as A from torch.utils.data import Dataset class CrackDataset(Dataset): def __init__(self, img_dir, mask_dir, file_list, augmentNone): self.img_dir img_dir self.mask_dir mask_dir self.file_list [line.strip() for line in open(file_list)] self.augment augment def __len__(self): return len(self.file_list) def __getitem__(self, idx): name self.file_list[idx] img cv2.imread(f{self.img_dir}/{name}.jpg) mask cv2.imread(f{self.mask_dir}/{name}.png, cv2.IMREAD_GRAYSCALE) mask (mask 0).astype(uint8) if self.augment: aug self.augment(imageimg, maskmask) img, mask aug[image], aug[mask] img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 mask torch.from_numpy(mask).long() return img, mask # 使用示意 aug A.Compose([ A.Resize(512, 512), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.2), ])这段代码的灵活之处在于把缩放和增强都交给Albumentations处理cv2.imread读取灰度掩码后通过阈值转成二值避免标注时边缘抖动造成灰度值不干净。torch.from_numpy(img.transpose(2,0,1))把HWC转成CHW格式是PyTorch训练的默认输入顺序。注意灰度图也会被cv2.imread自动加载成三通道这样统一处理反而省事但后面使用预训练权重时要注意通道匹配。3.3 训练脚本主循环训练主循环不需要花哨稳定跑通是第一目标。我通常用交叉熵损失配合Dice系数的混合损失优化器选AdamW初始学习率1e-4配合余弦退火。下面是一个精简但完整的训练循环import torch import torch.nn as nn from torch.utils.data import DataLoader from tqdm import tqdm def dice_loss(pred, target, smooth1.0): pred torch.softmax(pred, dim1)[:, 1] intersection (pred * target).sum() return 1 - (2 * intersection smooth) / (pred.sum() target.sum() smooth) model UNet(in_channels3, num_classes2).cuda() optimizer torch.optim.AdamW(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) criterion nn.CrossEntropyLoss() train_loader DataLoader(dataset, batch_size8, shuffleTrue, num_workers4) for epoch in range(50): model.train() total_loss 0 for imgs, masks in tqdm(train_loader, descfEpoch {epoch1}): imgs, masks imgs.cuda(), masks.cuda() preds model(imgs) loss criterion(preds, masks) dice_loss(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() scheduler.step() print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader):.4f})这里的dice_loss在裂缝场景里非常关键。因为裂缝像素占比通常只有1%到3%普通交叉熵损失会让模型倾向于预测全背景。混合Dice后模型必须把裂缝区域真正抠出来才能降低损失。参数T_max50表示余弦退火的周期与训练轮数一致如果你只训30轮记得把T_max也改成30否则学习率还没有降到最低就停止效果差一截。batch_size8适合12G显存如果报OOM先降到4并同时把Resize尺寸改为256。3.4 推理与结果可视化推理阶段我需要输出两类结果一类是PNG掩码另一类是原图上叠加红色半透明的可视化图。可视化对向地质人员汇报很有用他们看得懂裂缝但看不懂灰度图。下面代码是单张图推理的标准写法import cv2 import numpy as np import torch def predict_single(model, img_path, devicecuda, save_pathNone): img cv2.imread(img_path) h, w img.shape[:2] resized cv2.resize(img, (512, 512)) tensor torch.from_numpy(resized.transpose(2,0,1)).float().unsqueeze(0) / 255.0 tensor tensor.to(device) with torch.no_grad(): probs torch.softmax(model(tensor), dim1)[0, 1] probs probs.cpu().numpy() mask (probs 0.5).astype(uint8) mask cv2.resize(mask, (w, h)) overlay img.copy() overlay[mask 1] (0, 0, 255) result cv2.addWeighted(img, 0.6, overlay, 0.4, 0) cv2.imwrite(save_path, result) return mask这个推理函数里有两个细节容易踩坑。第一输入图像resize成512后输出的mask必须再resize回原图尺寸否则和原图叠加会错位。第二掩码resize默认采用最近的插值算法因为cv2.resize对二值图使用线性插值会产生非0和1的中间值影响叠加。如果你发现结果里裂缝变得断续多半是这里用了默认插值。把cv2.resize的interpolation参数显式写成cv2.INTER_NEAREST即可。4. CT岩心裂缝的专属处理灰度权重、伪影抑制与切片连续性4.1 灰度图像为什么不能直接套RGB预训练权重CT岩心切片是单通道灰度图但很多预训练模型如ImageNet上的ResNet、DeepLabV3要求输入三通道。一个常见的偷懒做法是把灰度图复制三份变成伪RGB然后加载预训练权重。这个办法在数据量大的时候能跑但效果不一定好——因为预训练权重学的是自然图像的色彩纹理模式而CT图像的灰度分布和自然照片完全不同用预训练权重等于把模型先往错误方向带了一把还得靠后面大量迭代拉回来。我处理CT数据时有两种更靠谱的方案。第一种是从零训练单通道输入模型把编码器第一层卷积的输入通道改成1放弃预训练权重训练轮数适当增加到80到100轮。第二种是如果非要用预训练权重则把原图先做CLAHE对比度增强再做伪RGB三通道输入并且只解锁模型后半部分网络前半部分只做特征提取器的微调。实际对比下来对于裂缝这种纹理结构方案一在小数据集上更稳不容易过拟合。4.2 连通域分析与裂缝形态后处理模型直接输出的二值分割图往往包括很多孤立噪声点尤其在CT伪影区域。这些噪声点面积小、分布零散而真实裂缝是连通的。后处理第一步可以做连通域分析去掉面积小于阈值的区域。下面是一个基于OpenCV的简单后处理函数def remove_small_cc(mask, min_area25): num_labels, labels, stats, _ cv2.connectedComponentsWithStats(mask, connectivity8) cleaned np.zeros_like(mask) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] min_area: cleaned[labels i] 1 return cleaned连通域分析的最小面积阈值需要根据图像分辨率调整。如果切片尺寸是1024×1024裂缝噪声点普遍在10到20像素之间min_area25能滤掉大部分噪声但如果有细长裂缝的一段只有5像素宽、几十像素长面积超过25会被保留所以这个值相对安全。处理完连通域后还可以用形态学闭运算把同一裂缝的断点连接起来但要注意结构核不能太大3×3就够了否则会把不相干的邻近区域也连上。4.3 3D连续性校验CT岩心是一个立方体切片之间有天然的连续性。如果神经网络在相邻切片上预测出的裂缝位置突然消失又出现那么大概率是模型把伪影或微孔隙误判成了裂缝。常见做法是加载连续10到20张切片的预测掩码做一个逐像素的中值滤波或多数投票把出现频率低于半数的像素点剔除。这个操作在工程上叫时间维平滑虽然会增加一点计算量但对结果可信度的提升非常明显。实现3D投票时要注意内存不要一次性加载整个岩心几百张切片按滑动窗口处理。窗口大小一般取11到15张对每张中间位置的切片做投票修正效果最好。如果地质人员希望保留更细微的裂缝信息也可以把投票阈值从50%降到30%宁可多留一些候选也别让真实裂缝被平滑掉。这一步没有统一答案我建议做一个简单的AB对比生成两张后处理图交给项目组评审哪种更符合人工解释。5. 裂缝分割训练避坑指南6个血泪经验与排查清单5.1 裂缝太细导致损失函数不收敛现象训练loss下降正常但验证集F1始终在0.3左右预测图中裂缝断成一节一节。原因裂缝宽度只有1到3像素下采样时信息丢失而且交叉熵损失对细小目标不敏感。模型学到的可能是背景区域的轮廓而非裂缝本身。解决把输入分辨率从512提高到768或1024尽量保留裂缝细节。同时替换损失为Dice Loss或Tversky Loss这种重边界损失会放大少量裂缝像素的影响。如果显存不允许也可以先用512把模型训到收敛再用768的输入微调10轮效果接近直接大分辨率训练。5.2 类别不平衡与Focal Loss现象模型预测结果里很少有裂缝像素即便有也是零散噪声整体偏向全背景。原因裂缝像素占图像面积往往不到2%普通交叉熵被背景梯度主导模型找到的最优解就是全预测为背景。解决除了换Dice损失还可以给交叉熵加上类别权重例如nn.CrossEntropyLoss(weighttorch.tensor([0.1, 0.9]))把裂缝类别的权重抬高。更激进的做法是使用Focal Loss它的γ参数会降低易分类样本的损失贡献。我用Focal Loss时一般设置α0.75、γ2先跑20轮观察如果召回率上升但精确率下降再把α调低到0.6。5.3 验证集指标虚高但实际效果差现象验证集上mIoU达到0.85但拿到新的岩心CT切片上预测结果惨不忍睹。原因最常见的是数据拆分时没有按岩心分组同一岩心的相邻切片同时出现在训练和验证集中模型相当于“背”住了这些切片的纹理特征并没有学到泛化规律。解决严格按岩心样本ID分组确保一张岩心的全部切片要么进训练要么进验证。如果岩心数量太少宁可减少训练数据量也要保证验证集完全独立。之后观察训练集与验证集之间的loss差距如果训练集loss持续下降而验证集不降就说明过拟合需要增加数据增强或降低模型容量。5.4 CT伪影被误判为裂缝现象分割结果中岩心边缘出现一圈环形高亮区域被识别为裂缝而真实裂缝反而被忽略。原因CT扫描中的环状伪影和束硬化伪影在灰度上表现出与裂缝类似的低密度特征尤其靠近岩心边缘的地区灰度差异很大。模型可能把强烈的灰度梯度当作裂缝边缘。解决输入图像先做中值滤波或非局部均值滤波抑制伪影再交给模型。在标注阶段就要避免把伪影归为裂缝。如果伪影位置固定也可以做预处理裁掉边缘区域。更有效的办法是在训练时把伪影区域单独标一个背景类别让模型明确知道这不是裂缝。5.5 数据集泄露来自同一岩心的切片被同时分到训练和验证现象训练时验证loss一直低于训练loss甚至在验证集上几乎100%准确。原因与5.3类似但更隐蔽。当数据列表按文件名排序后同一岩心的切片文件名前缀相同会在切分时自然落在一起。如果不做分组就是把同源数据塞进了两个集合。解决在生成数据列表时用文件名中的岩心ID作为分组键用GroupShuffleSplit或StratifiedGroupKFold操作。具体做法是给每一行数据增加一个group_id字段拆分的整体单位是group_id而不是单独每个样本。手动检查时打印出验证样本的文件名前缀确认没有与训练集交集。5.6 显存溢出与BatchSize调参现象训练刚开始就出现CUDA out of memory把batch_size调到2仍然报错。原因除了模型大小外输入尺寸、通道数、损失计算中的特征图都会被显存。3通道512×512输入比单通道CT切片的显存占用高得多。解决先用torch.cuda.empty_cache()清理缓存然后按顺序降低以下选项batch_size减半、分辨率改为256、模型从DeepLabV3换成UNet、关闭混合精度训练。我建议直接使用AMP混合精度训练在torch.cuda.amp加持下显存占用可以减少约40%且裂缝分割这类任务对精度损失不敏感。训练脚本里加上scaler.scale(loss).backward()和scaler.step(optimizer)即可。6. 进阶从分割结果到裂缝定量分析的落地技巧6.1 实验追踪与参数管理裂缝分割不是跑一次就能拿结果往往要试十几次模型配置。我用MLflow做实验追踪每次跑完自动记录loss、mIoU、学习率等指标并保存最优权重。如果你的团队更喜欢轻量的方案直接在训练脚本里把关键指标写入一个JSON文件也行但等到做对比时你会后悔没配追踪工具。注意在追踪时记录数据增强参数、损失函数类型、分辨率等超参数而不是只记录指标否则复现时还是得翻代码。6.2 裂缝面积与开度计算分割完成后量化裂缝参数是工程需要的核心产出。最常见的两个指标是裂缝面积占比和裂缝开度。面积占比可以直接对二值掩码做像素统计除以图像总像素数即可。开度则复杂一些需要提取裂缝骨架后计算局部宽度。我一般先对掩码做距离变换再沿骨架线提取距离值乘以两倍即为局部宽度。下面是一段计算裂缝面积占比的简单代码def crack_area_ratio(mask): crack_pixels int((mask 0).sum()) total_pixels mask.size return crack_pixels / total_pixels如果要做开度统计建议用OpenCV的distanceTransform配合skeletonize来自skimage先裁剪连通域再对每个区域单独计算避免把不同裂缝之间的间距也算进去。开度结果通常以像素为单位输出需要根据CT分辨率换算成毫米这一步务必在项目开始前确认否则报告里的数据没法用。6.3 把模型集成到自己的工程中最后我习惯把训练好的模型封装成一个类对外只暴露predict(image_path)方法内部处理灰度图、尺寸变换、后处理和结果输出。这样在写自动化处理脚本时不需要关心模型细节。封装时要注意固定输入尺寸和归一化方式确保与训练时完全一致。预测阶段不推荐再使用数据增强库统一用OpenCV即可减少依赖。我在实际项目中栽过跟头模型训练时用了albumentations的归一化推理时却直接用cv2.imread除以255导致输入分布完全变了模型输出几乎全黑。后来把所有预处理统一放到一个函数里训练和推理共用就再没翻过车。希望这些经验能帮你省掉几个晚上的调试时间。裂缝分割这个方向只要数据规范、避坑到位性能提升是稳定的值得把它沉淀成自己工程里的一把利器。本文还有配套的精品资源点击获取