多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

图像块分类实战:切块策略、模型训练与工程落地避坑指南

图像块分类实战:切块策略、模型训练与工程落地避坑指南 简介一套完整的“图像块分类”Matlab实现方案已经整理打包适合计算机视觉初学者、高年级本科生和需要做局部特征分类的研究人员。内容围绕图像预处理灰度化、去噪、分块、特征提取GLCM、Gabor等、特征选择PCA/LDA以及SVM/CNN分类器训练的全流程提供从脚本到测试图的闭环代码可帮助理解“光滑块/非光滑块”二分类任务并直接复用提供中间数据以供对比调试。压缩包共30个文件以m源码为主配合mat数据、png测试图和doc说明文档整体大小27.8MB目录按功能拆分清晰便于按步骤运行和查阅。已有321人学习下载。运行main.m等脚本可复现图像分块、遗传算法寻优、PSNR/SSIM误差计算等实验结合“程序说明.doc”和“实验结果.doc”能系统掌握图像块分类的工程细节适合课程设计、毕业设计或算法预研等场景。1. 图像块分类把一张大图拆成小方块来识别到底解决什么问题在工业表面缺陷检测、病理切片分析这类场景里你想识别的是“图里哪个位置有问题”而不是“整张图有没有问题”。直接拿整图分类跑目标通常只占几十个像素经过池化层后信息被稀释模型近乎盲猜想做像素级分割标注成本又高得吓人。图像块分类patch classification的折中方案是按固定大小切块逐块判断类别再把结果拼回原图。它靠块级标签训练不需要目标框不需要像素级掩膜就能输出“位置类别”的粗粒度结果。这篇笔记适合手里正好有几千张带块级标注的图、想快速验证这个方案能不能用的工程师。我会把切块、训练、后处理和几个高频翻车点一次讲透。2. 为什么整图分类做不了图像块分类三种切块策略与模型选型图像块分类不是简单把图缩小它的第一步是“决定怎么拆图”。拆法直接决定样本数量、标签难度和推理性能。先看它和相邻技术方案的分界再选切块方式最后落到模型结构上。2.1 图像块分类与整图分类、语义分割的真正边界整图分类假设“一张图一个语义”ImageNet 的数据就是这么组织的。但真实业务里一张 5000×5000 的卫星影像可能同时包含农田、屋顶、水体每个类别都重要。强行贴一个主标签会丢掉空间信息语义分割又需要逐像素标注成本在病理和遥感场景里极不现实。图像块分类刚好卡在中间输出单元是固定大小的 patch比图小、比像素大。判断一个项目是否适合用图像块分类我会看三条规则。第一目标尺度基本一致比如细胞核、微小裂纹它们的直径在某个固定倍数下有稳定像素宽第二大图背景占比高真正有意义的块可能只有 10%这时把整图直接下采样会导致目标消失第三输出“在哪个区域有问题”就够用后续有更高层模块做精确定位。如果这三条都满足图像块分类通常是投入产出比最高的方案。2.2 切块策略网格切块、滑动窗口和语义引导切块网格切块是最朴素的策略从坐标 (0,0) 开始按patch_size为步长切块与块之间没有重叠。它适合图内容分布比较均匀的场景比如扫描件版面分析、正射遥感影像参数也最简单。缺点是目标一旦跨到两块交界处就会变成“谁都不完整”标签归属非常模糊。滑动窗口更常用块的大小固定步长小于块宽相邻块有重叠。重叠带来两个好处一个目标大概率以完整形态出现在某一块内推理时同一个位置被多个窗口覆盖可以通过投票消除单次误判。代价是计算量按照重叠率近似上涨步长设为块宽的一半时样本量是网格切块的四倍。我通常先用 50% 重叠跑通模型之后再为了提速把重叠降到 25% 左右。语义引导切块适合背景主导的图像比如一整张病理切片 80% 以上是空白区域。先用阈值或简单模型筛掉空白块只在有组织残留的区域切块训练和推理都能省大量时间。实现上不复杂统计每个候选块内灰度均值、方差或者梯度强度低于阈值的直接丢弃。切块策略重叠方式适用场景计算开销后处理难度网格切块无重叠地物均匀、背景干净低中滑动窗口通常 50% 重叠小目标密集、缺陷多样高低语义引导切块无固定要求背景占比大于 80%低高2.3 模型选型ResNet、ViT 还是轻量 CNN模型选型是跟着 patch 尺寸走的不是拍脑袋。如果 patch 是 32 或 64 像素信息量有限上大模型没有意义也容易过拟合这时用三层卷积加一个全局池化再加全连接头效果往往很好。如果 patch 是 224 像素直接用 ImageNet 预训练的 ResNet18 或 EfficientNet 做特征提取器把最后一层分类头换成自己的线性层收敛快得多。块分类本质还是图像分类预训练权重的收益非常明显。ViT 在图像块分类里概念上很自然因为 ViT 本身就先切 patch 再编码。但要注意如果你的每个输入 patch 是 64×64ViT 的 patch size 设为 16序列够长效果能出来如果输入本身只有 32×32ViT 的结构优势就发挥不出来。另外 ViT 需要更多数据和更久训练时间业务初期我不建议直接上。轻量 CNN 在 CPU 推理优势很大像 MobileNetV3 这样的小模型即便没有 GPU 也能同时处理几十个块适合实时漏检场景。3. 用 PyTorch 跑通图像块分类切块到训练的最小闭环理论讲完直接上代码。下面的流程以一张或多张大图为输入最终输出一个训练好的块分类模型。我会把切块、网络、训练三个环节拆开每段代码后面都会把参数讲清楚。3.1 把大图切成块滑动窗口切图脚本与参数说明切块这一步最容易被低估切出来的样本质量直接决定训练效果。下面的函数对一张二维或三维图像做滑动窗口切块同时返回每个块在原图中的左上角坐标坐标在推理拼图阶段必须用到。import numpy as np def slice_image(img, patch_size256, stride128): h, w img.shape[:2] patches [] coords [] if img.ndim 2: # 灰度图统一转成三维 img img[:, :, None] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): p img[y:y patch_size, x:x patch_size] patches.append(p[:, :, 0] if p.shape[-1] 1 else p) coords.append((x, y)) return patches, coords这里patch_size256是块的大小stride128是步长。当 stride 小于 patch_size 时相邻块有一半区域重叠适合小目标当 stride 等于 patch_size 时上面的函数就退化成了网格切块。注意循环条件是h - patch_size 1如果图片宽度不能被步长整除最后一行或一列会被直接丢弃。对大多数任务这不是问题但若边界信息重要可以在调用前用np.pad做边缘补零或者步长改为奇数让窗口能覆盖到边界。切块返回的是 Python 列表大图几百兆时列表会占内存。更稳健的工程做法是把切块写成一个生成器或者保存成 LMDB/WebDataset避免加载整张大图时内存被瞬间打满。这里贴的是最小实现便于理解坐标关系。3.2 轻量 CNN 分类器结构与标签设计块分类的标签是一个整数类别号。我一般会把训练数据按类别名/图片名.jpg的目录结构组织这样torchvision.datasets.ImageFolder可以直接读取。下面这个网络是针对 32 到 64 像素输入设计的参数量小不容易在小块数据上过拟合。import torch.nn as nn class PatchClassifier(nn.Module): def __init__(self, in_channels3, num_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, num_classes), ) def forward(self, x): x self.features(x) return self.classifier(x)输入张量形状是(batch, channels, height, width)。以 64×64 输入为例两次最大池化后空间尺寸变成 16×16通道数变成 64最后一层全局平均池化把每个通道压缩成一个数再进入全连接分类头。num_classes改成你的实际类别数二分类写 2多分类就写对应数字。几个参数需要说明。第一个卷积层通道数 32在 patch 很小的情况下已经够用如果 patch 是 224×224 或者你需要更强拟合能力可以把通道数翻倍到 64/128。BatchNorm2d 放在卷积和 ReLU 之间对小 batch 要谨慎batch size 低于 8 时它会让训练不稳定建议 batch size 至少 16。分类头用AdaptiveAvgPool2d(1)而不是Flatten加全连接是为了让网络能适应不同尺寸的输入验证时偶尔切出非正方形块也跑得通。3.3 训练脚本损失函数、优化器与关键超参训练逻辑本身和普通图像分类没有区别重点是选择对块数据友好的超参数。下面这段脚本可以直接套用。import torch from torchvision import transforms, datasets from torch.utils.data import DataLoader import torch.optim as optim train_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_data datasets.ImageFolder(data/train, transformtrain_transform) train_loader DataLoader(train_data, batch_size64, shuffleTrue, num_workers4, drop_lastTrue) model PatchClassifier(in_channels3, num_classes2) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr3e-4, weight_decay5e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(30): model.train() for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() optimizer.step() scheduler.step()这里损失函数用普通交叉熵适合类别均衡的起步任务。如果正负样本比例超过 3:1建议先把CrossEntropyLoss换成带权重版本criterion nn.CrossEntropyLoss(weighttorch.tensor([1.0, 3.0]))权重按“多数类权重小、少数类权重大”来设置路径是先统计训练集各类别的数量把少数类的 weight 设为多数类数量的比值。优化器选择 AdamW学习率 3e-4 是块分类相对稳的起步值如果你的 patch 数量很多、模型较深可以直接降到 1e-4。CosineAnnealing 调度在 30 epoch 内能平滑收尾。还有一个细节drop_lastTrue保证最后一个 batch 不丢在 BatchNorm 前避免单样本 batch 导致归一化统计量偏移。4. 让图像块分类真正落地难负样本挖掘与邻域投票后处理训练完基线模型只是开始。真实业务里块分类最大的敌人是“绝大数块都是背景”以及“独立预测结果拼回去像马赛克”。这一章讲怎么从这些块里挖出真正有价值的样本以及如何让最终输出更接近人类标注的连续区域。4.1 难负样本挖掘当背景块占绝大多数时的训练策略假设表面缺陷检测任务里正常块占 95%带缺陷的块只有 5%。一轮训练后模型可能达到 95% 准确率但漏检几乎为零——把每个块都预测成正常就完事了。准确率虚高F1 分数却很难看。解决办法之一是在训练过程中对难例加权最常见的做法是按 loss 排序取 top-k。loss criterion(logits, y, reductionnone) k max(1, int(loss.numel() * 0.25)) _, hard_indices loss.topk(k) loss loss[hard_indices].mean()这段代码的逻辑是每个样本先算一个独立 loss取出最高的 25% 的 loss只用这些难样本的反向传播更新梯度。参数 0.25 可以调节难例比例越高模型越聚焦于模糊样本但也越容易过拟合那些噪声标签。一般先从 25% 开始验证集 F1 反而下降时再把比例降低到 10%。离线难例挖掘更可控用当前模型对训练集所有块做一次推理把所有预测错误或置信度在 0.5 到 0.9 之间的块挑出来和原始训练数据混合再微调模型。这个“Hard Negative Mining”的做法在目标检测领域已经很成熟块分类同样适用。我的经验是训练集里难例比例不要超过 30%否则模型会忘记简单样本长什么样。4.2 邻域投票与空间平滑把块结果拼回大图切块时用滑动窗口同一个像素会被多个块覆盖天然适合做投票。最简单的做法是记录每个块预测出来的类别在块对应区域内进行多数投票。实际工程中我用得更多的是一条捷径——先把每个块预测结果画到一张类别图上再对类别图做中值滤波效果接近邻域投票但速度快一个数量级。from scipy.ndimage import median_filter # label_map 是由块预测拼合而成的整数图 # 每个像素的类别等于覆盖它的块类别重叠区域取最后一个块的值 smoothed_label_map median_filter(label_map, size7)中值滤波窗口 size7 对应 7×7 邻域内的众数替换中心值可以有效去除孤立错分点。要注意块重叠严重时高频边缘会被当成类别“接缝”这个操作能压掉大部分接缝噪声。如果后续需要像素级轮廓可以在平滑后加一次边缘保留滤波比如bilateral_filter。块结果拼图时一定要保留坐标原图否则后面做区域统计和可视化全部对不上。4.3 置信度阈值与多尺度推理块分类模型给出的软概率可以直接当置信度用。训练完后我会在验证集上搜索一个阈值置信度低于阈值的结果标记为“不确定”而不是硬归到概率最大的类别。这个做法在缺陷检测里尤其有用可以避免把模棱两可的块强行塞进“正常”为下一级复检留出空间。选择阈值的方法是画 PR 曲线取精确率和召回率交叉处或者按业务成本选点。比如漏检代价高就把阈值调低让更多低置信度块被标记为疑似误报代价高就把阈值调高。至于多尺度推理我一般把同一个块缩放 0.8 倍和 1.25 倍三个尺度的概率做平均再取类别。它能提升 1 到 3 个百分点的 F1代价是推理时间近乎三倍只适合精度优先的离线分析场景。5. 图像块分类避坑五个高频翻车点与定位思路块分类看起来简单实际落地时坑藏得比较深。这一章把最常见到的五个问题按“现象、原因、解决”写出来都是我实际踩过的翻车点不是理论推演。5.1 显存不够不是模型大是切块和 Batch 搭配出了问题现象是CUDA out of memory频繁出现但模型本身明明很小。常见原因是我把大图切成块后全部加载到内存再一次性转成 tensor 塞进 GPU或者 batch size 设置和块大小不匹配。比如 256×256 的块batch size 开到 128一张卡肯定扛不住。解决方式先对切块函数做内存估算把训练 batch size 调到 16 或 32训练时用pin_memoryTrue加快数据搬运如果还是爆显存启用 PyTorch 的混合精度训练显存占用几乎减半。5.2 正负样本失衡准确率很高但召回率惨不忍睹现象是训练集准确率过 95%验证集 F1 只有 0.3。原因是背景块太多模型用“全部预测为背景”的偷懒策略就能拿到高准确率。解决思路有两个方向第一在 DataLoader 里对背景块做下采样让每个 epoch 的正负比例不超过 1:3第二把交叉熵改成带类别权重的版本或加到 Focal Loss。我先用“背景下采样 类别权重”的组合简单而且稳定难例挖掘在中后程再加直接上来就挖容易把噪声放大。5.3 切块边界把目标一分为二标签归属变得模糊现象是同一个缺陷在相邻两块里表现完全不同模型对其中一块预测为正常对另一块预测为异常拼回原图后目标被劈成两半。原因是固定网格切块不考虑目标边界目标在窗口边缘时信息不全。解决把滑动窗口重叠率从 0 提升到 50%或者采样时不以网格左上角为锚点而是以目标中心为锚点再截取固定大小块。后一种做法更像目标检测的“Center Crop”适合带弱目标位置标注的数据。5.4 推理时重复计算太多一张大图预测慢到不可用现象是训练时 GPU 跑得很快但推理一张 10000×10000 的大图要几十秒比训练一个 epoch 还慢。原因是滑动窗口重叠率设得过高或者没有过滤空白块。解决先算一下理论推理次数公式是ceil((W - patch_size) / stride 1) * ceil((H - patch_size) / stride 1)把重叠率从 50% 降到 25%推理时间基本会减少三分之一到一半。如果大图背景多先在低分辨率或灰度统计上做粗筛只对候选区域做块分类。5.5 验证集指标不错拼出的大图却全是棋盘状噪声现象是独立预测每个块的准确率都合格但整图可视化后出现很规则的块状网格像打了马赛克。原因有两个一是训练时归一化用的统计量只有均值方差切块之间曝光不一致模型在不同块上输出了不同的偏移二是模型只看到了局部缺少上下文紧挨着的类似区域被分成两类。解决推理时用训练集的全局均值方差做归一化不要用每个小图自己的统计数据另外用 50% 重叠切块并把重叠区域概率平均棋盘效应基本能消除。如果仍旧存在就加一层条件随机场做空间平滑。6. 验证图像块分类效果CAM 定位检查与推理调优技巧模型训练完、阈值也调完之后我建议先别急着接流做一次 CAM 可视化检查。常见做法是取验证集里预测正确的 50 个块用 Grad-CAM 看模型关注的区域是否落在真实目标上。如果热点集中在图像边缘、固定噪点或背景纹理上说明模型学到了数据集伪影而不是目标特征。这时优先检查预处理和切块方式不要盲目调网络结构。推理调优有一个性价比很高的技巧保留软输出而不是硬类别。把模型最后一个全连接层输出的 logits 过 softmax保存每个类的概率在拼接阶段按概率加权融合。比如相邻块对同一位置给出两个不同预测用置信度更高的那个比少数服从多数更准确。如果业务允许延迟多尺度推理的收益放在第二位。我的习惯是离线分析必开多尺度和软输出线上实时检测只开重叠 25% 并设置置信度阈值 0.7。最后列一张验收检查清单方便你在每次迭代后快速定位问题检查项通过标准块大小与目标尺寸比目标宽高约占块的 1/2 到 2/3训练集正负比在 1:1 到 1:3 之间验证集 F1不低于 0.8且漏检率可接受CAM 热点集中在目标区域而非边缘伪影推理重叠率与实际业务延迟匹配无明显棋盘噪声低置信度结果有明确“未知”或“疑似”策略上面这几项里CAM 和低置信度策略经常被忽略但它们是线上翻车的主要来源。我吃过一次亏模型 F1 到 0.93满怀信心部署到现场结果切片染色批次不同许多正常块被预测成缺陷。原因是验证集来自同一个批次的图像模型学到的是染色偏移而不是缺陷本身。从那以后我把“跨批次验证”加进了验收清单并且对置信度低于 0.85 的结果一律转人工复判绝对不硬分类。每一个网络都不会完美但把不可信的边界行为暴露出来是块分类方案能稳定落地的关键。希望这些习惯能帮到你。本文还有配套的精品资源点击获取
返回列表