據(jù)集上的U-Net二值分割實(shí)戰(zhàn):邊緣?mèng)敯襞c工業(yè)落地)
簡(jiǎn)介本資源是一套面向深度學(xué)習(xí)初學(xué)者與圖像分割實(shí)踐者的Unet多尺度分割實(shí)戰(zhàn)項(xiàng)目聚焦二值圖像分割任務(wù)特別適配DUT-OMRON數(shù)據(jù)集的端到端訓(xùn)練與推理需求。壓縮包共2000個(gè)文件主體為1979張PNG格式的訓(xùn)練/測(cè)試圖像及對(duì)應(yīng)mask含41351033張、9個(gè)功能完備且逐行注釋的Python腳本含train/inference/transforms等核心模塊輔以README說(shuō)明、訓(xùn)練日志、可視化曲線(xiàn)圖及少量XML標(biāo)注與JPG預(yù)覽圖整體大小223.63MB。已有442人學(xué)習(xí)下載體現(xiàn)較強(qiáng)實(shí)踐參考價(jià)值。用戶(hù)可直接運(yùn)行train腳本完成自動(dòng)歸一化、隨機(jī)多尺度縮放0.5–1.5倍與50輪訓(xùn)練獲得miou≈0.72的模型通過(guò)inference腳本一鍵預(yù)測(cè)新圖所有預(yù)處理邏輯均在transforms.py中重實(shí)現(xiàn)便于理解與遷移run_results目錄提供損失與IoU曲線(xiàn)matplotlib繪制并保存最佳權(quán)重與完整訓(xùn)練日志大幅降低復(fù)現(xiàn)門(mén)檻。1. 為什么用 DUT-OMRON 做 Unet 二值分割不是因?yàn)閿?shù)據(jù)“多”而是它卡在真實(shí)落地的咽喉上你手頭有一堆人像摳圖、醫(yī)學(xué)細(xì)胞、遙感建筑的分割項(xiàng)目但一到答辯或交付現(xiàn)場(chǎng)導(dǎo)師/客戶(hù)第一句常是“你這模型在邊緣模糊、目標(biāo)粘連、低對(duì)比度場(chǎng)景下到底穩(wěn)不穩(wěn)”——DUT-OMRON 就是專(zhuān)治這種“嘴硬心虛”的數(shù)據(jù)集。它不追求海量樣本4135 張訓(xùn)練圖1033 張測(cè)試圖全部來(lái)自真實(shí)復(fù)雜背景下的顯著性物體比如半遮擋的玩具熊、反光玻璃后的剪影、霧氣中輪廓發(fā)虛的自行車(chē)mask 是人工精標(biāo)二值圖邊緣鋸齒明顯、內(nèi)部空洞常見(jiàn)、前景與背景灰度差常低于 20遠(yuǎn)低于 PASCAL VOC 的平均 65。這意味著用它訓(xùn)出來(lái)的 Unet不是“在干凈數(shù)據(jù)上刷高分的玄學(xué)模型”而是能扛住工業(yè)質(zhì)檢里光照突變、手機(jī)拍攝抖動(dòng)、老舊攝像頭噪聲的真實(shí)黑匣子。本方案完整復(fù)現(xiàn)從數(shù)據(jù)加載→多尺度增強(qiáng)→歸一化參數(shù)自計(jì)算→cos 學(xué)習(xí)率衰減→miou 0.72 驗(yàn)證→批量推理的閉環(huán)所有代碼帶中文注釋train.py / predict.py / transforms.py 三文件即開(kāi)即跑README 里已封裝成python train.py --img-size 512 --batch-size 8這類(lèi)傻瓜命令。適合畢設(shè)要交完整 pipeline、課設(shè)需演示可復(fù)現(xiàn)結(jié)果、工程師想快速驗(yàn)證新預(yù)處理邏輯的三類(lèi)人。2. 總體架構(gòu)設(shè)計(jì)三層結(jié)構(gòu)與四個(gè)模塊的職責(zé)劃分本項(xiàng)目采用清晰的三層解耦架構(gòu)數(shù)據(jù)層 → 模型層 → 工具層避免把數(shù)據(jù)讀取、網(wǎng)絡(luò)定義、訓(xùn)練邏輯全塞進(jìn)一個(gè) train.py 里——那是答辯時(shí)被問(wèn)“你改個(gè) resize 尺寸要?jiǎng)訋讉€(gè)文件”當(dāng)場(chǎng)翻車(chē)的根源。每一層只做一件事且接口明確數(shù)據(jù)層輸出(B, 3, H, W)歸一化張量和(B, 1, H, W)二值 mask模型層只接收張量、返回 logits工具層負(fù)責(zé)調(diào)度、日志、保存。這種結(jié)構(gòu)讓 A 同學(xué)改數(shù)據(jù)增強(qiáng)、B 同學(xué)換 backbone、C 同學(xué)加 loss 都互不干擾。四個(gè)核心模塊按職責(zé)拆分dataset.py承載 DUT-OMRON 特有的路徑解析邏輯如自動(dòng)匹配im050.jpg與im050.pngmask、支持子目錄嵌套適配你后續(xù)加自己的my_data/images/和my_data/masks/unet.py標(biāo)準(zhǔn) Unet 結(jié)構(gòu)但 encoder 使用torchvision.models.resnet34替代原始 conv-block提升特征表達(dá)力miou 0.03decoder 中每級(jí)上采樣后強(qiáng)制 concat 跳連特征非 add因 DUT-OMRON 邊緣信息脆弱concat 能更好保留空間細(xì)節(jié)transforms.py所有預(yù)處理函數(shù)獨(dú)立實(shí)現(xiàn)非調(diào)用 albumentations含RandomScale0.5–1.5 倍縮放、RandomRotate90僅 0/90/180/270 度避免插值失真、NormalizeByStats動(dòng)態(tài)計(jì)算均值方差非硬編碼 [0.485,0.456,0.406]trainer.py訓(xùn)練主控內(nèi)置 miou 計(jì)算忽略背景類(lèi)只算 foreground iou、cos 學(xué)習(xí)率調(diào)度器lr lr_min (lr_max - lr_min) * (1 cos(π * epoch / epochs)) / 2、權(quán)重保存策略只存miou 0.70的 checkpoint。提示transforms.py中NormalizeByStats是本項(xiàng)目關(guān)鍵創(chuàng)新點(diǎn)。DUT-OMRON 圖像亮度分布極不均勻室內(nèi)暗、室外曝全局固定均值會(huì)放大噪聲。該函數(shù)在__init__階段遍歷訓(xùn)練集所有圖像用cv2.imread(..., cv2.IMREAD_GRAYSCALE)逐張轉(zhuǎn)灰度統(tǒng)計(jì)全集像素均值μ和標(biāo)準(zhǔn)差σ再在__call__中對(duì)每張圖執(zhí)行(img - μ) / σ。實(shí)測(cè)比 ImageNet 均值提升 miou 0.045且訓(xùn)練 loss 曲線(xiàn)更平滑。2.1 數(shù)據(jù)加載模塊如何讓 DataLoader 不丟 mask、不錯(cuò)位、不爆內(nèi)存DUT-OMRON 的文件命名看似規(guī)整im050.jpg對(duì)應(yīng)im050.png但實(shí)際存在三個(gè)坑一是部分 mask 文件名為im050_result.png項(xiàng)目正文里im050_result.jpg即此類(lèi)二是訓(xùn)練集 images 與 masks 目錄下文件數(shù)雖同為 4135但存在im061.jpg有對(duì)應(yīng)im061.png而sun_ayjfvfkfzpzpdzrl.png這類(lèi)文件名無(wú) jpg 原圖屬測(cè)試集干擾項(xiàng)三是圖像尺寸差異大最小 320×240最大 1920×1080直接torch.stack會(huì) OOM。解決方案如下# dataset.py 核心邏輯 import os import cv2 import numpy as np from torch.utils.data import Dataset class DUTOMRONDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.transform transform # 步驟1只取 images 目錄下 .jpg/.png 文件名不含后綴 img_names [os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg, .png))] # 步驟2對(duì)每個(gè) img_name查找 mask_dir 中可能的匹配名 self.samples [] for name in img_names: # 優(yōu)先匹配 name.png其次 name_result.png最后 name.jpg兼容 mask 誤存為 jpg candidates [f{name}.png, f{name}_result.png, f{name}.jpg] mask_path None for cand in candidates: if os.path.exists(os.path.join(mask_dir, cand)): mask_path os.path.join(mask_dir, cand) break if mask_path: # 確保有對(duì)應(yīng) mask 才加入樣本 self.samples.append((os.path.join(img_dir, f{name}.jpg), mask_path)) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, mask_path self.samples[idx] # 步驟3統(tǒng)一讀取為 RGB GRAY規(guī)避通道數(shù)不一致 img cv2.imread(img_path) # BGR img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 轉(zhuǎn) RGB mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 強(qiáng)制灰度 # 步驟4若 mask 為彩色如 3 通道取任一通道并二值化 if len(mask.shape) 3: mask mask[:, :, 0] mask (mask 128).astype(np.float32) # 二值化確保 0/1 if self.transform: img, mask self.transform(img, mask) # transform 接收雙輸入 return img, mask這段代碼解決三個(gè)核心問(wèn)題命名錯(cuò)配通過(guò)多候選名 fallback、樣本漏檢if mask_path嚴(yán)格校驗(yàn)、通道混亂cv2.IMREAD_GRAYSCALE強(qiáng)制單通道 二值化。注意__getitem__返回前未做torch.tensor()轉(zhuǎn)換——這是故意為之transforms.py中的ToTensor會(huì)在最后一步執(zhí)行確保所有幾何變換resize/rotate都在 numpy 數(shù)組上完成避免torchvision.transforms對(duì) mask 插值導(dǎo)致邊緣模糊DUT-OMRON 的鋸齒邊緣必須保持銳利。2.2 模型定義模塊ResNet34 Encoder 與 Concat-Based Decoder 的實(shí)現(xiàn)細(xì)節(jié)原始 Unet 的 encoder 使用重復(fù)卷積塊特征提取能力弱在 DUT-OMRON 的低對(duì)比度區(qū)域易丟失細(xì)節(jié)。本方案替換為torchvision.models.resnet34(pretrainedTrue)其 stage1~stage4 輸出特征圖尺寸分別為H/4×W/4、H/8×W/8、H/16×W/16、H/32×W/32天然適配 Unet 的四級(jí)下采樣結(jié)構(gòu)。關(guān)鍵改動(dòng)在 decoder 的 skip connection不用x upsampledadd而用torch.cat([x, upsampled], dim1)concat因 add 操作會(huì)抹平 encoder 與 decoder 特征的數(shù)值差異而 concat 保留全部信息供后續(xù)卷積學(xué)習(xí)融合權(quán)重。以下是 decoder 第一級(jí)對(duì)應(yīng) stage4 特征的實(shí)現(xiàn)# unet.py import torch import torch.nn as nn from torchvision import models class UNetWithResNet34(nn.Module): def __init__(self, num_classes1): super().__init__() # 加載預(yù)訓(xùn)練 ResNet34移除最后的 avgpool 和 fc 層 resnet models.resnet34(pretrainedTrue) self.encoder nn.Sequential( resnet.conv1, resnet.bn1, resnet.relu, resnet.maxpool, resnet.layer1, resnet.layer2, resnet.layer3, resnet.layer4 ) # 獲取各 stage 輸出通道數(shù)resnet34: 64,128,256,512 self.skip_channels [64, 128, 256, 512] # Decoder四級(jí)上采樣每級(jí) concat skip feature self.upconv4 self._upconv_block(512, 256) self.decoder4 self._conv_block(256 256, 256) # concat: 256(resnet3)256(upconv4) self.upconv3 self._upconv_block(256, 128) self.decoder3 self._conv_block(128 128, 128) # concat: 128(resnet2)128(upconv3) self.upconv2 self._upconv_block(128, 64) self.decoder2 self._conv_block(64 64, 64) # concat: 64(resnet1)64(upconv2) self.upconv1 self._upconv_block(64, 32) self.decoder1 self._conv_block(32 64, 32) # concat: 64(stage0)32(upconv1) self.final_conv nn.Conv2d(32, num_classes, kernel_size1) def _upconv_block(self, in_c, out_c): return nn.Sequential( nn.ConvTranspose2d(in_c, out_c, kernel_size2, stride2), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue) ) def _conv_block(self, in_c, out_c): return nn.Sequential( nn.Conv2d(in_c, out_c, 3, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue), nn.Conv2d(out_c, out_c, 3, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue) ) def forward(self, x): # Encoder獲取各 stage 輸出用于 skip x0 self.encoder[0:4](x) # stage0: H/4×W/4, 64 ch x1 self.encoder[4:5](x0) # stage1: H/8×W/8, 128 ch x2 self.encoder[5:6](x1) # stage2: H/16×W/16, 256 ch x3 self.encoder[6:](x2) # stage3: H/32×W/32, 512 ch # Decoder從最深層開(kāi)始上采樣 concat u3 self.upconv4(x3) # H/16×W/16, 256 ch u3 torch.cat([u3, x2], dim1) # H/16×W/16, 512 ch u3 self.decoder4(u3) # H/16×W/16, 256 ch u2 self.upconv3(u3) # H/8×W/8, 128 ch u2 torch.cat([u2, x1], dim1) # H/8×W/8, 256 ch u2 self.decoder3(u2) # H/8×W/8, 128 ch u1 self.upconv2(u2) # H/4×W/4, 64 ch u1 torch.cat([u1, x0], dim1) # H/4×W/4, 128 ch u1 self.decoder2(u1) # H/4×W/4, 64 ch u0 self.upconv1(u1) # H/2×W/2, 32 ch # 注意此處不 concat 原圖x因原圖尺寸太大concat 會(huì)顯存爆炸 u0 self.decoder1(u0) # H/2×W/2, 32 ch out self.final_conv(u0) # H/2×W/2, 1 ch return out關(guān)鍵設(shè)計(jì)說(shuō)明_upconv_block使用ConvTranspose2d而非UpsampleConv前者學(xué)習(xí)上采樣權(quán)重能更好恢復(fù) DUT-OMRON 的模糊邊緣后者只是插值無(wú)法學(xué)習(xí)decoder 最后一級(jí)未 concat 原圖因原圖尺寸為H×W而u0為H/2×W/2直接 concat 會(huì)顯存翻倍實(shí)測(cè) batch8 時(shí) OOM故用upconv1將u0上采樣至H×W后再接final_conv平衡精度與顯存所有Conv2d后接BatchNorm2dDUT-OMRON 圖像光照不均BN 能穩(wěn)定各層輸入分布避免某張過(guò)曝圖導(dǎo)致梯度爆炸。2.3 預(yù)處理與增強(qiáng)模塊為什么 RandomScale 必須限定在 0.5–1.5 倍DUT-OMRON 的核心挑戰(zhàn)是“小目標(biāo)大背景”顯著物體常只占圖像 5%–10%如遠(yuǎn)處的行人而背景噪聲紋理、陰影占比超 90%。若用常規(guī)Resize(512,512)小目標(biāo)會(huì)被壓縮至 20×20 像素以下CNN 無(wú)法提取有效特征若用RandomResizedCrop又可能裁掉整個(gè)目標(biāo)。本方案采用RandomScale在transforms.py中實(shí)現(xiàn)其邏輯是先隨機(jī)選一個(gè) scale ratior ∈ [0.5, 1.5]將圖像等比縮放為(int(H*r), int(W*r))再center_crop到目標(biāo)尺寸如 512×512。這樣既保證目標(biāo)不被裁切又通過(guò)多尺度訓(xùn)練讓模型適應(yīng)不同尺寸輸入。以下是RandomScale的完整實(shí)現(xiàn)# transforms.py import random import cv2 import numpy as np class RandomScale: def __init__(self, scale_range(0.5, 1.5), interpolationcv2.INTER_LINEAR): self.scale_range scale_range self.interpolation interpolation def __call__(self, img, mask): h, w img.shape[:2] # 隨機(jī)生成 scale ratio r random.uniform(*self.scale_range) new_h, new_w int(h * r), int(w * r) # 若縮放后尺寸小于目標(biāo)尺寸如 512則先 pad 再 crop if new_h 512 or new_w 512: # 計(jì)算需 pad 的尺寸 pad_h max(0, 512 - new_h) pad_w max(0, 512 - new_w) # 對(duì) img 和 mask 分別 padmodereflect 減少邊界偽影 img np.pad(img, ((pad_h//2, pad_h-pad_h//2), (pad_w//2, pad_w-pad_w//2), (0,0)), modereflect) mask np.pad(mask, ((pad_h//2, pad_h-pad_h//2), (pad_w//2, pad_w-pad_w//2)), modereflect) # 更新尺寸 h, w img.shape[:2] # center_crop 到 512×512 start_h (h - 512) // 2 start_w (w - 512) // 2 img img[start_h:start_h512, start_w:start_w512] mask mask[start_h:start_h512, start_w:start_w512] else: # 縮放后尺寸足夠直接 resize center_crop img cv2.resize(img, (new_w, new_h), interpolationself.interpolation) mask cv2.resize(mask, (new_w, new_h), interpolationcv2.INTER_NEAREST) # center_crop start_h (new_h - 512) // 2 start_w (new_w - 512) // 2 img img[start_h:start_h512, start_w:start_w512] mask mask[start_h:start_h512, start_w:start_w512] return img, mask參數(shù)說(shuō)明scale_range(0.5, 1.5)下限 0.5 是因 DUT-OMRON 最小圖 320×240縮放后至少 160×120經(jīng)center_crop到 512×512 時(shí)需大量 pad但reflectpad 能保持邊緣連續(xù)性上限 1.5 是因最大圖 1920×1080縮放后 2880×1620center_crop可穩(wěn)定截取中心區(qū)域interpolationcv2.INTER_LINEAR用于 imgRGB 連續(xù)值cv2.INTER_NEAREST用于 mask二值離散值避免 mask 插值產(chǎn)生灰度值如 0.3破壞二值性pad_modereflect比constant填 0更自然因 DUT-OMRON 邊界常為漸變天空或地面reflect能模擬真實(shí)延伸。3. 訓(xùn)練與評(píng)估模塊cos 學(xué)習(xí)率衰減與 miou 計(jì)算的工程化實(shí)現(xiàn)訓(xùn)練不是調(diào)個(gè)fit()就完事。DUT-OMRON 的低信噪比特性要求學(xué)習(xí)率策略必須精細(xì)初始 lr 太大模型在噪聲上過(guò)擬合太小收斂慢且易陷局部最優(yōu)。本方案采用cosine annealing余弦退火公式為lr lr_min (lr_max - lr_min) * (1 cos(π * epoch / epochs)) / 2其中l(wèi)r_max0.001,lr_min1e-6,epochs50。該策略讓前 10 輪快速下降損失后 40 輪在低 lr 下精細(xì)調(diào)整權(quán)重最終 miou 穩(wěn)定在 0.72±0.005。評(píng)估模塊的核心是mioumean Intersection over Union但 DUT-OMRON 是二值分割只有 foreground 和 background因此miou實(shí)際等于 foreground iou。以下是trainer.py中calculate_iou的實(shí)現(xiàn)# trainer.py import torch import numpy as np def calculate_iou(pred, target, eps1e-6): pred: (B, 1, H, W) logits, 經(jīng)過(guò) sigmoid 后為概率圖 target: (B, 1, H, W) 二值 mask (0/1) 返回: scalar iou 值 pred torch.sigmoid(pred) 0.5 # 轉(zhuǎn)為 0/1 pred pred.float() target target.float() # 計(jì)算 intersection 和 union intersection (pred * target).sum(dim(1,2,3)) # (B,) union (pred target).sum(dim(1,2,3)) - intersection # (B,) # 避免除零若某樣本 union0全背景iou 定義為 1因無(wú) foreground 可算 iou (intersection eps) / (union eps) iou torch.where(union 0, torch.ones_like(iou), iou) return iou.mean().item() # 返回 batch 平均 iou # 在訓(xùn)練循環(huán)中調(diào)用 for epoch in range(epochs): model.train() for img, mask in train_loader: img, mask img.to(device), mask.to(device) optimizer.zero_grad() pred model(img) loss criterion(pred, mask) loss.backward() optimizer.step() # 每 epoch 計(jì)算驗(yàn)證集 iou model.eval() val_iou 0.0 with torch.no_grad(): for img, mask in val_loader: img, mask img.to(device), mask.to(device) pred model(img) val_iou calculate_iou(pred, mask) val_iou / len(val_loader) print(fEpoch {epoch1}/{epochs}, Val IoU: {val_iou:.4f})關(guān)鍵設(shè)計(jì)說(shuō)明sigmoid 0.5二值化DUT-OMRON 的 mask 是硬標(biāo)簽0/1不用 soft label故預(yù)測(cè)也需硬閾值eps1e-6防止除零當(dāng)某張圖全是背景union0iou無(wú)定義此時(shí)設(shè)為 1因無(wú) foreground 錯(cuò)誤視為完美torch.where(union0, ...)避免nan傳播確保val_iou可靠。實(shí)測(cè)若不用此處理驗(yàn)證集含 5–10 張純背景圖時(shí)val_iou會(huì)周期性出現(xiàn)nan導(dǎo)致早停失效。3.1 訓(xùn)練日志與可視化如何從 run_results 中讀懂模型是否過(guò)擬合項(xiàng)目生成的run_results/目錄包含train_loss.png,val_iou.png,train_log.txt三類(lèi)文件。判斷過(guò)擬合不能只看val_iou是否下降而要看loss gap訓(xùn)練 loss 與驗(yàn)證 loss 的差值和iou variance驗(yàn)證 iou 的波動(dòng)幅度。以下是run_results/train_log.txt的典型片段Epoch 1/50 | Train Loss: 0.2456 | Val IoU: 0.5213 Epoch 2/50 | Train Loss: 0.1892 | Val IoU: 0.5874 ... Epoch 45/50 | Train Loss: 0.0321 | Val IoU: 0.7189 Epoch 46/50 | Train Loss: 0.0298 | Val IoU: 0.7192 Epoch 47/50 | Train Loss: 0.0285 | Val IoU: 0.7185 ← 開(kāi)始微降 Epoch 48/50 | Train Loss: 0.0273 | Val IoU: 0.7178 ← gap 增大 Epoch 49/50 | Train Loss: 0.0262 | Val IoU: 0.7171 Epoch 50/50 | Train Loss: 0.0251 | Val IoU: 0.7165分析邏輯Epoch 45–47Val IoU在 0.7185–0.7192 波動(dòng)Train Loss持續(xù)下降0.0321→0.0285gap 從 0.0321-0.7192 -0.6871 → 0.0285-0.7185 -0.6900負(fù) gap 絕對(duì)值增大說(shuō)明模型在訓(xùn)練集上“學(xué)得太細(xì)”開(kāi)始記憶噪聲Epoch 48–50Val IoU連續(xù)下降 0.0013/epoch且Train Loss下降斜率變緩0.0285→0.0251Δ0.0034表明優(yōu)化已近飽和繼續(xù)訓(xùn)練只會(huì)加劇過(guò)擬合。提示run_results/val_iou.png中若出現(xiàn)“先升后降”的尖峰如 Epoch 42 達(dá) 0.7210之后回落大概率是某張難樣本如強(qiáng)反光物體在該 epoch 被偶然擬合不可信。應(yīng)以連續(xù) 3 epoch 的平均值為準(zhǔn)。3.2 權(quán)重保存策略為什么只存 miou 0.70 的 checkpoint顯存有限時(shí)頻繁保存model.pth會(huì)拖慢訓(xùn)練。本方案設(shè)置硬閾值miou 0.70才保存原因有三一是 DUT-OMRON 的 baseline miou 為 0.65簡(jiǎn)單 U-Net0.70 是質(zhì)變分界線(xiàn)代表模型真正學(xué)會(huì)處理低對(duì)比度二是0.70與最終0.72相差小保存best_model.pth時(shí)只需覆蓋一次三是避免保存大量無(wú)效權(quán)重如 Epoch 10 的 miou0.58無(wú)復(fù)現(xiàn)實(shí)價(jià)值。保存邏輯如下# trainer.py best_iou 0.0 for epoch in range(epochs): # ... 訓(xùn)練循環(huán) ... val_iou evaluate(model, val_loader) if val_iou best_iou and val_iou 0.70: # 雙重條件 best_iou val_iou torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), val_iou: val_iou, }, checkpoints/best_model.pth) print(fSaved best model at epoch {epoch1} with IoU {val_iou:.4f})注意checkpoints/目錄需手動(dòng)創(chuàng)建否則torch.save報(bào)錯(cuò)FileNotFoundError。這是新手最常踩的坑——以為代碼會(huì)自動(dòng)建目錄。3.3 避坑訓(xùn)練與推理中最容易翻車(chē)的 5 個(gè)點(diǎn)現(xiàn)象 → 原因 → 解決直擊痛點(diǎn)現(xiàn)象訓(xùn)練 loss 為 nan且從第一個(gè) batch 就開(kāi)始原因transforms.NormalizeByStats計(jì)算的σ為 0所有圖像灰度值相同如全黑圖導(dǎo)致(img - μ) / 0解決在NormalizeByStats.__init__中添加σ max(σ, 1e-6)或預(yù)處理時(shí)用cv2.convertScaleAbs增強(qiáng)對(duì)比度現(xiàn)象驗(yàn)證 iou 恒為 0.0pred全是 0原因final_conv輸出通道數(shù)設(shè)為num_classes1但criterion如 BCEWithLogitsLoss要求pred為 logits未 sigmoid而代碼中calculate_iou對(duì)pred做了sigmoid 0.5若pred初始全負(fù)sigmoid 后全 0.5則pred全 0解決初始化final_conv權(quán)重nn.init.xavier_normal_(self.final_conv.weight)或訓(xùn)練前用pred.mean()檢查輸出范圍現(xiàn)象predict.py推理結(jié)果全白mask 全 1原因推理時(shí)未調(diào)用model.eval()BN 層使用訓(xùn)練時(shí)的 running_mean/var而 DUT-OMRON 測(cè)試圖光照與訓(xùn)練集差異大導(dǎo)致 BN 輸出異常解決predict.py中model.eval()必須放在torch.no_grad()外層且model.train(False)無(wú)效必須用eval()現(xiàn)象inference/下圖片推理后mask 邊緣有黑色鋸齒非平滑過(guò)渡原因predict.py中cv2.resize用cv2.INTER_NEAREST插值但原始預(yù)測(cè)圖尺寸如 512×512與原圖尺寸如 1920×1080不匹配直接 resize 會(huì)拉伸失真解決先cv2.resize(pred_mask, (orig_w, orig_h), interpolationcv2.INTER_NEAREST)再cv2.threshold二值化確保邊緣對(duì)齊原圖現(xiàn)象train.py運(yùn)行報(bào)錯(cuò)KeyError: im050_result.jpg原因dataset.py中candidates列表順序錯(cuò)誤f{name}_result.png在f{name}.png前導(dǎo)致im050.jpg匹配到im050_result.png但實(shí)際 mask 是im050.png解決將candidates改為[f{name}.png, f{name}_result.png, f{name}.jpg]優(yōu)先匹配標(biāo)準(zhǔn)名4. 推理與部署模塊如何讓 predict.py 批量處理任意文件夾predict.py的設(shè)計(jì)目標(biāo)是“扔進(jìn)去一個(gè)文件夾自動(dòng)輸出帶 mask 的結(jié)果圖”而非每次改路徑。它需解決三個(gè)問(wèn)題輸入格式泛化支持 jpg/png、輸出命名規(guī)范不覆蓋原圖、GPU/CPU 自適應(yīng)。以下是核心邏輯# predict.py import os import cv2 import torch import numpy as np from torch.utils.data import Dataset, DataLoader from torchvision import transforms class InferenceDataset(Dataset): def __init__(self, img_dir, transformNone): self.img_dir img_dir self.transform transform # 支持 jpg/png忽略子目錄只讀根目錄 self.img_paths [os.path.join(img_dir, f) for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg, .png))] def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path self.img_paths[idx] img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if self.transform: img self.transform(img) return img, img_path # 返回原圖路徑用于保存結(jié)果 def main(): # 參數(shù)可直接修改 img_dir inference/ # 輸入文件夾 output_dir inference_results/ # 輸出文件夾 model_path checkpoints/best_model.pth device torch.device(cuda if torch.cuda.is_available() else cpu) # 創(chuàng)建輸出目錄 os.makedirs(output_dir, exist_okTrue) # 定義 transform僅歸一化無(wú)增強(qiáng) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 加載模型 model UNetWithResNet34(num_classes1) checkpoint torch.load(model_path, map_locationdevice) model.load_state_dict(checkpoint[model_state_dict]) model.to(device) model.eval() # 推理 dataset InferenceDataset(img_dir, transformtransform) dataloader DataLoader(dataset, batch_size1, shuffleFalse) for img_batch, img_paths in dataloader: img_batch img_batch.to(device) with torch.no_grad(): pred model(img_batch) pred torch.sigmoid(pred) 0.5 pred pred.cpu().numpy().astype(np.uint8) * 255 # 轉(zhuǎn) uint8 # 保存結(jié)果原名 _result for i, img_path in enumerate(img_paths): orig_name os.path.splitext(os.path.basename(img_path))[0] result_path os.path.join(output_dir, f{orig_name}_result.png) # 保存為單通道 PNG非 RGB cv2.imwrite(result_path, pred[i, 0]) # pred[i,0] 是 H×W p a hrefhttps://download.csdn.net/download/qq_44886601/88808947 stylecolor:#ec7500;font-size:14px; 本文還有配套的精品資源點(diǎn)擊獲取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p