戰(zhàn):從大氣散射模型到最小實(shí)現(xiàn))
簡介一套基于Vision Transformer的圖像去霧算法研究與實(shí)現(xiàn)資料包內(nèi)含源碼與文檔說明適合計算機(jī)視覺方向研究生、算法工程師以及圖像復(fù)原愛好者用于學(xué)習(xí)與二次開發(fā)。項目采用Uformer架構(gòu)覆蓋從NH-HAZE數(shù)據(jù)集預(yù)處理到模型訓(xùn)練的全流程包含將訓(xùn)練圖像切分為256×256小圖的generate_patches_SIDD.py預(yù)處理腳本以及支持--arch Uformer、--nepoch 270、--batch_size 32等參數(shù)配置的My_train.py訓(xùn)練代碼并配套YAML配置文件、CSV損失記錄與結(jié)果圖可幫助復(fù)現(xiàn)去霧實(shí)驗并分析模型收斂情況。壓縮包共338個文件包含204個Python腳本、16個YAML配置、12個CSV記錄、9個IPython筆記及8個Markdown文檔另有PNG/GIF結(jié)果圖與TXT日志整體約156MB目錄結(jié)構(gòu)清晰。目前已有291人學(xué)習(xí)下載。配套文檔說明詳細(xì)解讀Vision Transformer在圖像去霧中的實(shí)現(xiàn)思路、數(shù)據(jù)組織方式及訓(xùn)練調(diào)參要點(diǎn)適合作為畢業(yè)設(shè)計、課程項目或科研工作的參考基線。1. 圖像去霧為什么從卷積轉(zhuǎn)向 Transformer一次視覺任務(wù)的分水嶺霧天拍攝的監(jiān)控畫面和自動駕駛感知圖像往往整體發(fā)灰、對比度極低這類圖像的共同特征是清晰度損失并不只發(fā)生在局部而是整幅圖像都被大氣散射壓暗。傳統(tǒng)去霧算法如暗通道先驗在天空區(qū)域和白色物體上經(jīng)常翻車原因是它假設(shè)“每個局部patch里總有一個通道的強(qiáng)度趨近于零”這個假設(shè)在亮度均勻的區(qū)域根本不成立。而基于 Vision Transformer 的圖像去霧算法把整幅霧圖切成一串視覺詞元patch通過自注意力機(jī)制建立遠(yuǎn)距離像素之間的聯(lián)系正好補(bǔ)上了卷積局部感受野的短板。本文會從大氣散射模型講起逐步給出可運(yùn)行的最小實(shí)現(xiàn)、參數(shù)配置、評估方法和踩坑記錄適合正在做圖像去霧課題或用 Transformer 做底層視覺任務(wù)的同學(xué)照著復(fù)現(xiàn)。2. 從大氣散射模型到 Vision Transformer去霧任務(wù)為什么吃這套2.1 大氣散射模型與 ViT 的對應(yīng)關(guān)系圖像去霧的理論基礎(chǔ)是大氣散射模型Atmospheric Scattering ModelI(x) J(x) * t(x) A * (1 - t(x))其中 I(x) 是觀測到的霧圖J(x) 是我們要恢復(fù)的清晰圖像t(x) 是透射率A 是全局大氣光。這個式子展開來看去霧任務(wù)的核心就是估計兩個未知量逐像素的透射率 t(x) 和全局大氣光 A。一旦這兩項估計出來清晰圖像可以通過 J(x) (I(x) - A) / t(x) A 直接算出。關(guān)鍵細(xì)節(jié)在于透射率 t(x) 的空間分布。在同一個場景里近處的物體透射率高、遠(yuǎn)處的山體透射率低t(x) 的變化往往跨越整個圖像平面。卷積神經(jīng)網(wǎng)絡(luò)受限于卷積核的大小通常只能看到 3x3 或 7x7 的局部鄰域要讓它學(xué)到“遠(yuǎn)山和近景之間的透射率漸變關(guān)系”必須堆很多層才能把感受野擴(kuò)到足夠大。Vision Transformer 天然沒有這個問題——它把圖像分成 patch 之后每個 patch 的 token 會與全圖所有其他 patch 的 token 做注意力計算一步就建立了全局依賴。這意味著在估計透射率時模型可以直接比較圖像左上角和右下角的亮度差異從而判斷深度梯度。2.2 為什么選擇 Vision Transformer 而不是堆更大的卷積核做去霧之前我也試過把普通卷積網(wǎng)絡(luò)加寬加深比如把 UNet 的 encoder 部分替換成 ResNet-50效果在均勻霧天情況下還行但遇到濃霧和薄霧同時出現(xiàn)的場景就明顯不夠用。深層卷積網(wǎng)絡(luò)在訓(xùn)練時還有個常見問題梯度在跨層傳播過程中會逐漸消失深層特征容易丟失淺層的細(xì)節(jié)紋理。Transformer 的殘差連接和 LayerNorm 機(jī)制在一定程度上緩解了這個問題更重要的是它的注意力頭可以并行地關(guān)注不同的空間關(guān)系——某些頭關(guān)注全局亮度分布某些頭關(guān)注邊緣細(xì)節(jié)這種多樣化的表征能力恰好對應(yīng)去霧問題中“估計透射率需要全局信息、恢復(fù)紋理需要局部信息”的雙重需求。另外值得注意的一點(diǎn)是Vision Transformer 在圖像分類任務(wù)上的成功并不能直接遷移到去霧任務(wù)上來因為去霧是稠密預(yù)測任務(wù)輸出要求與輸入同分辨率的逐像素結(jié)果。學(xué)術(shù)界通常的做法是在 ViT 后端接入一個解碼器decoder結(jié)構(gòu)或者直接把 Transformer 作為特征提取器再配合卷積上采樣層把特征圖恢復(fù)到原始尺寸。本文的實(shí)現(xiàn)就是走這條路ViT 負(fù)責(zé)全局建模后接輕量級卷積解碼器恢復(fù)細(xì)節(jié)。3. 基于 Vision Transformer 的圖像去霧最小可跑實(shí)現(xiàn)從數(shù)據(jù)到模型逐段拆解3.1 數(shù)據(jù)準(zhǔn)備先造霧再學(xué)去霧公開去霧數(shù)據(jù)集如 RESIDE的下載和預(yù)處理需要較大的網(wǎng)絡(luò)成本日常實(shí)驗調(diào)試階段最常用的做法是先用清晰圖像合成霧圖。合成公式嚴(yán)格按大氣散射模型來霧圖 清晰圖 x 透射率圖 大氣光 x (1 - 透射率圖)透射率圖通常用一個隨深度變化的平滑張量來表示深度信息可以用隨機(jī)生成的分層梯度圖近似。import torch import torch.nn.functional as F import numpy as np def generate_haze(clean_image, min_t0.3, max_t1.0, airlight0.8): 按大氣散射模型合成霧圖 clean_image: (B, C, H, W)取值范圍 0~1 的清晰圖像 min_t: 透射率下限越小代表霧越濃 airlight: 大氣光強(qiáng)度0~1 之間越高整體越偏灰白 batch_size, _, h, w clean_image.shape # 生成平滑的透射率圖先隨機(jī)生成粗略深度圖再做高斯模糊 depth torch.rand(batch_size, 1, h // 8, w // 8) depth F.interpolate(depth, size(h, w), modebilinear, align_cornersFalse) depth F.avg_pool2d(depth, kernel_size31, stride1, padding15) # 平滑 # 把深度歸一化到 [min_t, max_t] 區(qū)間 t min_t (max_t - min_t) * (1.0 - depth) # 大氣光按全局常數(shù)處理真實(shí)場景多為 (0.7~0.9) 的灰度值 a torch.full_like(clean_image, airlight) haze clean_image * t a * (1.0 - t) return haze, t這段代碼里最難調(diào)的參數(shù)是 min_t。取值越小霧越濃訓(xùn)練出的模型對濃霧的適應(yīng)能力越強(qiáng)但訓(xùn)練難度也越大因為透射率過低時清晰圖像的信號衰減嚴(yán)重模型容易把輸出直接推向“全圖抹平”。我一般調(diào)試階段用 min_t0.3跑通流程后用 0.2 加強(qiáng)魯棒性。avg_pool2d 的 kernel 大小決定透射率圖的平滑程度kernel 越大深度漸變越自然但如果設(shè)得太大近景和遠(yuǎn)景之間就沒有層次感了。3.2 Patch Embedding把霧圖切成視覺詞元Vision Transformer 的第一步是把輸入圖像切分成固定大小的 patch然后通過線性映射把每個 patch 壓成一個向量。這一步和 NLP 里把句子切成詞元再映射成 embedding 是一個道理只是在圖像里我們用的是二維 patch 而非一維詞元。import torch import torch.nn as nn class PatchEmbed(nn.Module): def __init__(self, in_channels3, patch_size4, embed_dim256): super().__init__() self.proj nn.Conv2d(in_channels, embed_dim, kernel_sizepatch_size, stridepatch_size) def forward(self, x): x: (B, 3, H, W) 輸出: (B, N, embed_dim)N H/4 * W/4 x self.proj(x) # (B, embed_dim, H/4, W/4) b, c, h, w x.shape x x.flatten(2) # (B, embed_dim, N) x x.transpose(1, 2) # (B, N, embed_dim) return xpatch_size 的選取直接影響計算量和感知粒度。patch4 時一張 256x256 的輸入會被切成 64x644096 個 patchTransformer 的注意力計算量是 O(N^2)N4096 時自注意力矩陣是 4096x4096顯存壓力會顯著上升。patch8 時 N1024計算量小了四倍但每個 patch 內(nèi)部的信息混合更粗邊緣細(xì)節(jié)的恢復(fù)能力變?nèi)?。?shí)驗對比下來256x256 輸入配 patch4 是效果和顯存的平衡點(diǎn)如果你的 GPU 顯存只有 8G 左右建議把輸入降到 192x192 或直接用 patch8。3.3 Transformer Encoder 加透射率預(yù)測頭Transformer Encoder 是整條網(wǎng)絡(luò)的核心由多頭自注意力層和前饋網(wǎng)絡(luò)交替堆疊而成。和 ViT 分類模型不同的是去霧任務(wù)的輸出需要保留空間結(jié)構(gòu)所以我要在 encoder 后面再接一個輕量卷積解碼器把特征圖上采樣回原分辨率。import torch import torch.nn as nn import math class TransformerBlock(nn.Module): def __init__(self, embed_dim, num_heads, mlp_ratio4.0, dropout0.1): super().__init__() self.norm1 nn.LayerNorm(embed_dim) self.attn nn.MultiheadAttention(embed_dim, num_heads, dropoutdropout, batch_firstTrue) self.norm2 nn.LayerNorm(embed_dim) self.mlp nn.Sequential( nn.Linear(embed_dim, int(embed_dim * mlp_ratio)), nn.GELU(), nn.Dropout(dropout), nn.Linear(int(embed_dim * mlp_ratio), embed_dim), nn.Dropout(dropout), ) def forward(self, x): x x self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[0] x x self.mlp(self.norm2(x)) return x class ViTDehaze(nn.Module): def __init__(self, img_size256, patch_size4, embed_dim256, num_heads8, depth6): super().__init__() self.patch_embed PatchEmbed(in_channels3, patch_sizepatch_size, embed_dimembed_dim) self.cls_token nn.Parameter(torch.zeros(1, 1, embed_dim)) self.pos_embed nn.Parameter(torch.randn(1, (img_size // patch_size) ** 2 1, embed_dim)) self.blocks nn.Sequential(*[ TransformerBlock(embed_dim, num_heads) for _ in range(depth) ]) self.norm nn.LayerNorm(embed_dim) # 解碼器從序列恢復(fù)到圖像尺寸 self.decoder nn.Sequential( nn.Conv2d(embed_dim, embed_dim // 2, kernel_size3, padding1), nn.GELU(), nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), nn.Conv2d(embed_dim // 2, embed_dim // 4, kernel_size3, padding1), nn.GELU(), nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), nn.Conv2d(embed_dim // 4, 3, kernel_size3, padding1), ) def forward(self, x): B x.shape[0] tokens self.patch_embed(x) # (B, N, embed_dim) # 前置 cls_token 和位置編碼 cls_tokens self.cls_token.expand(B, -1, -1) tokens torch.cat([cls_tokens, tokens], dim1) tokens tokens self.pos_embed tokens self.blocks(tokens) tokens self.norm(tokens) # 去掉 cls_token恢復(fù)成特征圖并送入解碼器 tokens tokens[:, 1:, :] feat_h feat_w int(math.sqrt(tokens.shape[1])) feat tokens.transpose(1, 2).reshape(B, -1, feat_h, feat_w) out self.decoder(feat) return out代碼里兩個容易忽略的細(xì)節(jié)一是 cls_token 初值用了零初始化位置編碼用了 randn 而不是固定正弦編碼因為圖像 patch 的空間關(guān)系比較復(fù)雜隨機(jī)位置編碼配合訓(xùn)練中學(xué)習(xí)比手工設(shè)計的三角函數(shù)編碼更靈活。二是 decoder 的上采樣直接用 bilinear沒用轉(zhuǎn)置卷積。轉(zhuǎn)置卷積在 stride2 時容易產(chǎn)生棋盤偽影bilinear 插值雖然理論上更“樸素”但在去霧這種需要平滑輸出的任務(wù)里反而能減少高頻噪聲的引入。3.4 訓(xùn)練腳本與損失函數(shù)組合去霧任務(wù)的損失函數(shù)不能只用 L1 或 MSE。L1 損失對整體亮度恢復(fù)比較穩(wěn)健但容易讓輸出圖像紋理過于平滑MSE 損失對異常值敏感。業(yè)界最常用的做法是 L1 損失 SSIM 損失的組合有條件的話再加上感知損失Perceptual Loss。import torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import DataLoader def ssim_loss(pred, target): 簡化版 SSIM 損失window 大小 11 c1 (0.01 * 1.0) ** 2 c2 (0.03 * 1.0) ** 2 kernel torch.ones(1, 1, 11, 11) / 121.0 kernel kernel.to(pred.device) mu_x F.conv2d(pred, kernel, padding5) mu_y F.conv2d(target, kernel, padding5) mu_xx F.conv2d(pred * pred, kernel, padding5) mu_yy F.conv2d(target * target, kernel, padding5) mu_xy F.conv2d(pred * target, kernel, padding5) sigma_x mu_xx - mu_x * mu_x sigma_y mu_yy - mu_y * mu_y sigma_xy mu_xy - mu_x * mu_y ssim_map ((2 * mu_x * mu_y c1) * (2 * sigma_xy c2)) / \ ((mu_x * mu_x mu_y * mu_y c1) * (sigma_x sigma_y c2)) return 1.0 - ssim_map.mean() def dehaze_loss(pred, target, alpha0.8): l1 F.l1_loss(pred, target) ssim ssim_loss(pred, target) return alpha * l1 (1 - alpha) * ssim # 訓(xùn)練循環(huán)關(guān)鍵片段 model ViTDehaze(img_size256, patch_size4, embed_dim256, num_heads8, depth6) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): for batch in dataloader: haze_img batch[haze].cuda() clean_img batch[clean].cuda() pred model(haze_img) loss dehaze_loss(pred, clean_img) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step()alpha 參數(shù)控制 L1 和 SSIM 的權(quán)重。alpha 越大訓(xùn)練過程越穩(wěn)定早期收斂越快但圖像邊緣容易發(fā)糊alpha 調(diào)小后 SSIM 主導(dǎo)紋理保持更清晰但在霧很濃的區(qū)域訓(xùn)練容易波動。我試過不同組合后alpha0.8 是省心的默認(rèn)值先讓它收斂到比較低的 loss再隔 20 個 epoch 把 alpha 降到 0.6 做微調(diào)效果會比固定權(quán)重好一些。clip_grad_norm_ 這行不能省Transformer 的梯度范數(shù)波動遠(yuǎn)大于卷積網(wǎng)絡(luò)不裁剪梯度的話偶爾一個 loss 突刺就能讓訓(xùn)練發(fā)散。4. 訓(xùn)練配置與評估PSNR、SSIM 之外還要看什么4.1 一組能直接用的超參數(shù)超參數(shù)組合決定了訓(xùn)練能否收斂以及收斂后效果的上限下面這組參數(shù)是我在單張 12G 顯存顯卡上調(diào)試出來的平衡點(diǎn)。參數(shù)推薦值調(diào)整方向輸入尺寸256x256顯存不夠時降到 192但 patch_size 要同步保持整除關(guān)系patch_size4越大速度越快但細(xì)節(jié)恢復(fù)越差最大不建議超過 8embed_dim256調(diào)大能提升表征能力多頭注意力的顯存占用會指數(shù)上升num_heads8需要讓 embed_dim 能被 num_heads 整除transformer depth6數(shù)據(jù)集小時 depth 超 8 容易出現(xiàn)過擬合batch_size8單卡 12G 上限超過 8 容易 OOM初始學(xué)習(xí)率1e-4AdamW 配 1e-4 比較穩(wěn)比 3e-4 更不容易出現(xiàn) loss 震蕩訓(xùn)練 epoch50 20 微調(diào)前 50 個 epoch 用 alpha0.8后 20 個 epoch 用 alpha0.6這里有個容易誤判的點(diǎn)不是 embed_dim 越大越好。embed_dim 從 256 調(diào)到 512 后PSNR 提升通常只有 0.3~0.5dB但顯存占用幾乎翻倍訓(xùn)練時間也肉眼可見地變長。如果數(shù)據(jù)集規(guī)模不大幾千張圖embed_dim256 已經(jīng)能覆蓋絕大多數(shù)去霧場景的表達(dá)需求。4.2 評估流程先看量化指標(biāo)再做人眼判斷去霧任務(wù)最常用的兩個指標(biāo)是 PSNR峰值信噪比和 SSIM結(jié)構(gòu)相似性。PSNR 衡量像素級差異值越高越好SSIM 衡量結(jié)構(gòu)一致性值越接近 1 越好。這兩個指標(biāo)在完全合成霧的數(shù)據(jù)上表現(xiàn)穩(wěn)定但做對比實(shí)驗時只報這兩個數(shù)是不夠的因為它們對“整體亮度偏移”非常敏感——把輸出整體調(diào)暗 5%PSNR 會掉一大截但人眼看起來可能并不覺得多難受。import numpy as np from skimage.metrics import peak_signal_noise_ratio, structural_similarity def eval_dehaze(pred, target): # pred 和 target 都是 0~1 的 float 數(shù)組HWC 格式 pred np.clip(pred, 0.0, 1.0) target np.clip(target, 0.0, 1.0) psnr peak_signal_noise_ratio(target, pred, data_range1.0) ssim structural_similarity( target, pred, data_range1.0, channel_axis-1 ) return psnr, ssim評估時我一般跑三組對比原始霧圖直接算指標(biāo)作為 baseline、暗通道先驗算法作為傳統(tǒng)方法代表、ViT 去霧模型。這也是一篇論文或一份課程設(shè)計報告里最基礎(chǔ)的三行對比表。但建議額外統(tǒng)計一個“天空區(qū)域占比”指標(biāo)對霧圖按亮度做閾值分割統(tǒng)計高亮區(qū)域在輸出中的細(xì)節(jié)保留程度。很多時候整體 PSNR 很好看天空區(qū)域卻被處理成一塊平板視覺上非常出戲。4.3 視覺檢查清單評估環(huán)節(jié)除了跑指標(biāo)我習(xí)慣固定抽 10 張不同場景的圖做肉眼檢查重點(diǎn)看三個位置一是天空和物體交界處有沒有白邊二是遠(yuǎn)處低對比度區(qū)域有沒有出現(xiàn)色塊或噪聲三是近景紋理有沒有被過度銳化導(dǎo)致邊緣發(fā)黑。這三個位置是去霧模型最容易出問題的地方量化指標(biāo)很難完全反映。5. 避坑ViT 去霧最容易翻車的四個位置5.1 透射率越界導(dǎo)致輸出黑斑現(xiàn)象訓(xùn)練到中途驗證集上偶爾會出現(xiàn)全黑或帶大片黑斑的輸出圖單張圖 loss 正常但整體訓(xùn)練曲線開始抖動。原因模型預(yù)測的透射率沒有做數(shù)值約束直接進(jìn)入 J(x) (I(x) - A) / t(x) 的反演公式當(dāng) t(x) 預(yù)測值接近 0 時除法會把噪聲無限放大得到接近全黑的圖像。Transformer 在訓(xùn)練初期特別容易在某些 patch 上輸出極小的透射率值。解決在解碼器最后一層卷積后面加一個 sigmoid 激活把透射率輸出限制在 (0, 1) 區(qū)間并且在使用時對透射率做下限截斷例如 t max(t, 0.1)。這比在 loss 里加透射率平滑項更直接有效。5.2 patch 邊界偽影現(xiàn)象輸出圖像能隱約看到網(wǎng)格狀痕跡尤其在 256x256 輸入、patch_size8 時特別明顯放大后像馬賽克。原因Patch Embedding 用 stridepatch_size 的卷積做切分相鄰 patch 之間沒有重疊區(qū)域Decoder 在上采樣恢復(fù)時每個 patch 內(nèi)部的特征獨(dú)立性太強(qiáng)patch 之間缺少平滑過渡。解決第一個方法是把 patch_size 從 8 降到 4網(wǎng)格痕跡會明顯減輕第二個方法是在 Decoder 的第一層卷積改用 kernel_size5、padding2讓卷積核有機(jī)會跨 patch 混合信息第三個更徹底的做法是在 Patch Embedding 前加一層 stride1、kernel_size3 的預(yù)處理卷積把相鄰像素的信息預(yù)先融合。5.3 小數(shù)據(jù)集上訓(xùn)練發(fā)散現(xiàn)象訓(xùn)練集只有幾百張合成霧圖時前幾個 epoch 的 loss 在 0.1~0.3 之間反復(fù)跳怎么調(diào)學(xué)習(xí)率都壓不住甚至出現(xiàn) loss 越訓(xùn)越高的反直覺情況。原因Vision Transformer 的參數(shù)空間比同體量卷積網(wǎng)絡(luò)更大對數(shù)據(jù)量的要求相應(yīng)更高。幾百張圖不夠喂飽 6 層 Transformer 的注意力權(quán)重模型容易把一些 patch 的注意力分?jǐn)?shù)學(xué)到極端位置直接記住訓(xùn)練集噪聲。解決我在這種情況下的首選是加數(shù)據(jù)增強(qiáng)把訓(xùn)練集做隨機(jī)裁剪尺寸范圍 0.8~1.0、水平翻轉(zhuǎn)、隨機(jī)亮度擾動乘子 0.85~1.15。注意不要在增強(qiáng)里加高斯噪聲因為去霧任務(wù)本身就要從低信噪比的霧圖中恢復(fù)信號額外噪聲會讓任務(wù)更難。5.4 評估指標(biāo)和視覺感受脫節(jié)現(xiàn)象PSNR 均值 28dB、SSIM 0.92數(shù)字很漂亮但放大單張輸出圖時發(fā)現(xiàn)天空部分有肉眼可見的帶狀條紋columns 方向上顏色不連續(xù)。原因PSNR 是逐像素差異的均值它的懲罰機(jī)制對高頻噪聲不算敏感——一條細(xì)帶子只占整圖的百分之一像素不足以顯著拉低 PSNR。而 SSIM 在局部窗口內(nèi)計算結(jié)構(gòu)相似度帶狀條紋如果寬度小于窗口尺寸同樣很難被發(fā)現(xiàn)。解決在評估腳本里加一個高頻分量統(tǒng)計把輸出圖像減去高斯模糊版本得到高頻殘差圖統(tǒng)計殘差圖的能量分布。如果個別局部區(qū)域的高頻能量遠(yuǎn)高于全圖中位數(shù)基本可以斷定有偽影或帶狀條紋。這個指標(biāo)雖然不能寫進(jìn)實(shí)驗對比表但對排查模型問題很有用處。6. 進(jìn)階用引導(dǎo)濾波把 ViT 輸出邊緣救回來ViT 去霧模型在整體亮度和色彩恢復(fù)上表現(xiàn)不錯但輸出圖像的邊緣細(xì)節(jié)有時不如基于卷積的傳統(tǒng)方法銳利。這是注意力機(jī)制的固有取舍自注意力擅長捕捉全局依賴對高頻局部結(jié)構(gòu)的響應(yīng)天然弱于卷積。一個成熟的工程做法是把 ViT 的輸出和經(jīng)典引導(dǎo)濾波結(jié)合起來——把 ViT 預(yù)測的透射率圖作為引導(dǎo)圖對去霧結(jié)果做邊緣保持的濾波處理。import cv2 import numpy as np def guided_filter_dehaze(dehazed_img, guidance_img, radius16, eps1e-3): 用透射率圖為引導(dǎo)對去霧輸出做邊緣保持平滑 dehazed_img: (H, W, 3) float 0~1 guidance_img: 單通道灰度圖常用透射率估計 t 或霧圖亮度 # 轉(zhuǎn)成 float32 滿足 OpenCV 輸入要求 guidance guidance_img.astype(np.float32) dehazed dehazed_img.astype(np.float32) # 對每個顏色通道做引導(dǎo)濾波 result np.zeros_like(dehazed) for c in range(3): result[:, :, c] cv2.ximgproc.guidedFilter( guidance, dehazed[:, :, c], radius, eps ) return np.clip(result, 0.0, 1.0)radius 和 eps 這兩個參數(shù)直接影響平滑程度。radius 太小小于 8時濾波窗口覆蓋不了紋理區(qū)域邊緣保留效果不明顯radius 太大大于 32會把真正的邊緣也磨平。eps 是正則項控制“什么程度的差異算邊緣”——取值越小濾波越盲目相信引導(dǎo)圖的邊緣結(jié)構(gòu)但引導(dǎo)圖透射率圖本身的誤差也會被放大取值過大則退化成普通均值濾波。我通常從 radius16、eps1e-3 起步量化指標(biāo)不會改善太多人眼觀感上邊緣更利落。如果想要更極致的邊緣保持效果可以把引導(dǎo)圖換成原霧圖的亮度通道。原理是原霧圖在有紋理的位置天然具備高頻信息這些位置也正好是恢復(fù)清晰圖像時最該保留細(xì)節(jié)的位置。最后說一個我的習(xí)慣每次訓(xùn)練完不急著收工把五六張典型測試圖單獨(dú)存下來分別用輕霧、中霧、濃霧三種強(qiáng)度合成看模型在三種退化程度下的表現(xiàn)差異。經(jīng)常出現(xiàn)的情況是輕霧場景下 ViT 去霧不如簡單直方圖均衡因為輕霧圖的對比度損失本身很小復(fù)雜模型的過度處理反而引入偽影。如果遇到這種現(xiàn)象可以把輕霧樣本單獨(dú)篩出來用更小的損失權(quán)重參與訓(xùn)練或者干脆在推理時對預(yù)測透射率做一個置信度閾值判斷。希望這一篇能幫你在 ViT 去霧這條路上少走一些我走過的彎路。本文還有配套的精品資源點(diǎn)擊獲取