果合并的完整實踐指南)
做目標檢測的人遲早會遇到這么一類項目圖特別大目標特別小。你拿著一張四千乘三千的航拍圖里面要找的是十幾個像素的車輛或屋頂直接丟進檢測器要么顯存不夠跑不起來要么跑起來了但漏檢漏到你懷疑人生。這個時候大多數(shù)人第一反應(yīng)都一樣把圖拆開拆成四份、九份甚至更多每塊單獨訓(xùn)練、單獨檢測最后再把檢測結(jié)果拼回原圖坐標。這個思路看起來很樸素但真正落地的時候問題比想象中多得多子圖切多大合適要不要重疊標注文件怎么跟著轉(zhuǎn)換檢測結(jié)果怎么映射回大圖兩個子圖同時檢測到同一個目標時要不要合并這些細節(jié)沒有處理好切塊不僅不會帶來提升反而會讓檢測結(jié)果亂成一團。這篇內(nèi)容我就把“大圖拆塊 小目標檢測”這條路線從頭到尾講清楚包括方案設(shè)計、切圖腳本、標注轉(zhuǎn)換、推理合并、常見坑位以及什么時候該換用更進階的做法。這些都是我實際跑過項目之后總結(jié)出來的不繞彎子直接說重點。1. 先弄明白為什么大圖里的小目標總是漏檢1.1 小目標到底有多“小”“小目標”在學(xué)術(shù)圈有個常用參考口徑COCO數(shù)據(jù)集里把像素面積小于32×32的目標歸為小目標。但實際項目里這個定義遠遠不夠用。大圖場景下的目標往往不是32×32而是更極端的情況一張5000×4000的遙感影像目標可能只有20×20像素甚至更小。按照“面積占整張圖比例”來算這類目標大概只占全圖面積的十萬分之一。落到檢測任務(wù)里小目標的核心難點有三層第一是像素信息少能用來區(qū)分的特征本身就非常有限第二是經(jīng)過網(wǎng)絡(luò)下采樣之后特征進一步丟失到了高層特征圖上可能只剩一兩個像素點第三是正負樣本比例懸殊一張大圖里背景區(qū)域占絕大多數(shù)模型很容易被背景帶偏。1.2 整圖縮放對小目標很不友好很多人第一個想法是大圖沒法直接訓(xùn)練那就先縮放到模型輸入尺寸比如640×640。這個操作的副作用非常直接。假設(shè)原圖是4000×3000模型輸入是640×480那么縮放比例大約是0.16。原圖里一個20×20的目標縮放后變成大約3×3像素。3×3像素的物體任何一個檢測器都很難區(qū)分它到底是目標還是噪點。更不用說網(wǎng)絡(luò)內(nèi)部還有多層stride為2的下采樣經(jīng)過幾次池化或卷積步長之后這個小目標幾乎完全從特征圖里消失。這就像你把一張城市地圖縮到手機屏幕大小想找到某條街道上的一個報亭根本不可能。但如果你把地圖裁成四塊分別放大顯示報亭的細節(jié)就能看清楚。切塊的本質(zhì)就是“局部放大”人眼這種視覺機制放在檢測器上同樣成立。1.3 切塊到底解決了什么切塊不是玄學(xué)它本質(zhì)上解決了三個問題。第一保留了目標的原始像素分辨率。目標在大圖里是20×20切成子圖后依然是20×20沒有因為統(tǒng)一縮放而變小。第二提高了目標在輸入圖像中的相對占比。同樣一個20×20的目標放在4000×3000的大圖里占比極小但放在1000×750的子圖里就清晰多了。第三把大圖對顯存和算力的壓力拆散。單張4K圖可能根本塞不進顯卡但切出來的小塊可以輕松進入模型前向計算。所以切塊方案的核心價值在于通過空間劃分把“大圖小目標”問題轉(zhuǎn)化成“小圖相對大目標”問題。這個思路本身不難難的是轉(zhuǎn)化過程中那些數(shù)據(jù)加工和結(jié)果合并的細節(jié)。2. 切塊方案的整體設(shè)計先想清楚再動手2.1 切四塊還是切N塊標題里說的是“拆成四個”這也是最自然的直覺2×2網(wǎng)格簡單干脆。但真上線前必須想清楚一個問題切四塊之后子圖尺寸是不是仍然遠超模型輸入尺寸。舉個例子一塊4000×3000的圖切成2×2四份每份是2000×1500。如果你用的是640×480輸入的檢測器子圖依然要縮放到640×480這輪縮放比例是0.32。相比整圖縮放的0.16目標像素從3.2×3.2變成6.4×6.4確實有提升但對很多項目來說6.4像素依然不夠。這種情況下切成3×3每塊約1333×1000或4×4每塊約1000×750反而更合理目標在縮放后的尺寸能達到9.6甚至12.8像素明顯更利于檢測。所以我的判斷標準很簡單先統(tǒng)計目標在原圖中的典型尺寸再倒推子圖尺寸。假設(shè)模型輸入長邊是S你希望小目標在經(jīng)過切塊和縮放后至少有8到16像素那么子圖長邊可以粗略按“S × 目標尺寸 / 期望像素”來估算。實際項目里不需要算得那么精確但“根據(jù)目標大小決定切塊數(shù)量”這個思路一定要有而不是無腦四等分。2.2 子圖尺寸和模型輸入怎么對齊子圖尺寸不要拍腦袋定至少要滿足兩個約束。第一個約束是模型有最佳輸入范圍。大多數(shù)檢測器在某個分辨率區(qū)間內(nèi)效果穩(wěn)定比如YOLO系列常用640Faster RCNN常用800×800或1333×800。子圖長邊建議控制在模型輸入尺寸的1到2倍之間。太大了縮放損失依然嚴重太小了又失去了切塊的意義。第二個約束是子圖的長寬比不要和模型輸入差太多。比如把一張超長全景圖切成1×4的長條每個長條寬度可能只有高度的四分之一resize進正方形輸入時會發(fā)生嚴重形變或者為了保持比例而大量填充黑邊這兩種情況都會損傷檢測精度。對于這類圖應(yīng)該按長寬比靈活調(diào)整網(wǎng)格而不是機械地切成四份。另外工程上的經(jīng)驗是如果原圖尺寸只是模型輸入的兩倍以內(nèi)而且目標不算太小可以先試不切塊直接縮放訓(xùn)練很多時候也沒問題。切塊不是越多越好而是“剛好讓目標在輸入圖上達到可檢測尺寸”最好同時還要盡量保留目標周圍的上下文信息。子圖太碎目標周邊環(huán)境信息丟失反而會增加誤檢。2.3 重疊率是很容易被忽略的參數(shù)完全沒有重疊的切塊會帶來一個非常具體的問題目標正好落在切分邊界上。比如一個寬40像素的目標橫跨兩個子圖左半邊在塊A右半邊在塊B兩塊里都只有一個殘缺的目標模型很難正確識別。更極端的情況是目標的位置恰好落在四條邊界交匯點四個子圖各占一小塊哪邊都認不出來。解決辦法就是切塊時留重疊區(qū)域。重疊率通常取子圖尺寸的10%到25%同時考慮目標最大尺寸重疊寬度至少覆蓋目標最大邊的一半以上。我的經(jīng)驗是普通場景下重疊64到128像素比較穩(wěn)妥目標較大的場景重疊區(qū)域要更大。比如目標最大尺寸是100像素那重疊至少留50到100像素否則一個目標很可能同時出現(xiàn)在兩個子圖里或者在所有子圖里都是殘缺的。訓(xùn)練階段和推理階段的重疊處理方式稍有不同。訓(xùn)練時建議一個目標的標簽只分配給目標中心點所在的子圖避免同一個目標在多個子圖里被重復(fù)學(xué)習(xí)推理時則不同所有子圖都正常檢測重疊區(qū)域里的同一個目標可能被多個子圖同時檢出來這時候靠后處理NMS去合并。3. 完整實操從切圖到合并檢測結(jié)果3.1 一次講清切圖腳本我這里按最常見的YOLO格式來演示切圖流程因為目前工程里YOLO系列的普及率最高。假設(shè)每張原圖對應(yīng)一個同名的txt標簽文件每行是“類別 cx cy w h”坐標都是相對于原圖寬高的歸一化值。切圖腳本的核心邏輯如下確定網(wǎng)格行列數(shù)、重疊像素然后遍歷每個網(wǎng)格位置用OpenCV切出子圖同時把落在該子圖內(nèi)的目標標簽一并轉(zhuǎn)換寫入新文件。import cv2 import os import numpy as np def slice_image(img_path, label_path, out_img_dir, out_label_dir, grid_rows2, grid_cols2, overlap64): img cv2.imread(img_path) H, W img.shape[:2] # 計算步長最后一行/列做邊界對齊 step_h max(1, (H - overlap) // grid_rows) step_w max(1, (W - overlap) // grid_cols) # 讀取標簽 boxes [] with open(label_path, r) as f: for line in f.readlines(): parts line.strip().split() cls int(parts[0]) cx, cy, w, h map(float, parts[1:]) # 轉(zhuǎn)成絕對像素坐標 x1 (cx - w / 2) * W y1 (cy - h / 2) * H x2 (cx w / 2) * W y2 (cy h / 2) * H boxes.append([cls, x1, y1, x2, y2]) for r in range(grid_rows): for c in range(grid_cols): y1 int(r * step_h) x1 int(c * step_w) y2 min(y1 step_h overlap, H) x2 min(x1 step_w overlap, W) # 邊界塊向左上對齊保證大小基本一致 y1 max(0, y2 - (step_h overlap)) x1 max(0, x2 - (step_w overlap)) sub_img img[y1:y2, x1:x2] sub_h, sub_w sub_img.shape[:2] # 保存子圖 base_name f{os.path.splitext(os.path.basename(img_path))[0]}_{r}_{c} cv2.imwrite(os.path.join(out_img_dir, base_name .jpg), sub_img) # 過濾落在當前子圖內(nèi)的目標 new_lines [] for cls, bx1, by1, bx2, by2 in boxes: # 中心點是否在該子圖內(nèi) bcx (bx1 bx2) / 2 bcy (by1 by2) / 2 if x1 bcx x2 and y1 bcy y2: # 裁剪到子圖范圍內(nèi) nx1 (bx1 - x1) / sub_w ny1 (by1 - y1) / sub_h nx2 (bx2 - x1) / sub_w ny2 (by2 - y1) / sub_h nx1 min(max(nx1, 0), 1) ny1 min(max(ny1, 0), 1) nx2 min(max(nx2, 0), 1) ny2 min(max(ny2, 0), 1) ncx (nx1 nx2) / 2 ncy (ny1 ny2) / 2 nw nx2 - nx1 nh ny2 - ny1 new_lines.append(f{cls} {ncx:.6f} {ncy:.6f} {nw:.6f} {nh:.6f}) with open(os.path.join(out_label_dir, base_name .txt), w) as f: f.write(\n.join(new_lines))上面代碼里最關(guān)鍵的一步是“目標中心點在該子圖內(nèi)”這個條件。中心點落在哪個塊就把目標完整分配給那個塊。這樣能避免同一個目標被多個子圖重復(fù)學(xué)習(xí)也能保證至少有一個子圖包含完整目標。當然如果你的場景里目標特別密集每個目標都可能跨多個塊也可以按“與子圖的交并比大于閾值”來分配但那種情況下標簽信息會出現(xiàn)冗余需要更小心的后處理。3.2 標注轉(zhuǎn)換千萬別算錯標注轉(zhuǎn)換是切塊里最容易出錯的一步坐標算錯會導(dǎo)致檢測框偏移、尺寸異常甚至模型訓(xùn)練直接崩掉。出錯的原因幾乎都是“歸一化坐標”和“絕對像素坐標”混用。轉(zhuǎn)換邏輯其實很清晰。第一步把原圖標簽里的歸一化坐標還原成原圖上的絕對像素坐標第二步判斷目標中心是否落在當前子圖內(nèi)第三步把絕對像素坐標減去子圖的左上角偏移量變成子圖內(nèi)的絕對坐標第四步再除以子圖的寬高轉(zhuǎn)成子圖內(nèi)的歸一化坐標。舉個例子原圖寬4000一個目標的中心cx是0.5那么原圖上絕對中心x是2000。如果子圖左上角x偏移是1000子圖寬是2000那么該目標在子圖內(nèi)的歸一化cx就是(2000-1000)/2000 0.5。這個數(shù)看起來好像沒變但如果你忘了減偏移量直接拿2000除以2000就會得到1.0檢測框被硬生生推算到子圖最右邊位置就錯了。切圖完成之后我強烈建議做一次可視化校驗。用腳本把轉(zhuǎn)換后的標簽畫到子圖上人工抽查幾十張看看框和目標是否對齊。這一步花不了幾分鐘但能避免你在訓(xùn)練了好幾天之后才發(fā)現(xiàn)數(shù)據(jù)有問題。我見過太多項目模型效果起不來最后查了半天原因就是標簽坐標映射錯了一個偏移量。3.3 訓(xùn)練階段的配置調(diào)整切塊完成后訓(xùn)練數(shù)據(jù)集的大小會變成原來的N倍切N塊就近似N倍。這種情況下有幾個參數(shù)需要調(diào)整。首先是迭代輪次。數(shù)據(jù)量翻了幾倍如果還保持原來的epoch數(shù)訓(xùn)練步數(shù)會大增。一般可以適當降低epoch或者用步數(shù)來控制訓(xùn)練長度。其次是學(xué)習(xí)率數(shù)據(jù)量變大后一個epoch內(nèi)的step數(shù)量也變多了學(xué)習(xí)率調(diào)度策略要跟著調(diào)整尤其是余弦退火的周期。更重要的是切塊后的子圖可能有很多完全沒有目標。這些空塊需要處理策略。如果全部保留負樣本比例會嚴重失衡模型容易偏向“什么都檢測不到”。我的做法是過濾掉大部分空塊但保留一小部分比如總量的10%到20%作為難負例尤其在推理階段會檢測全圖的場景下保留一定空塊能讓模型學(xué)會抑制背景區(qū)域的誤檢。還有一個很容易踩的坑是數(shù)據(jù)增強的適配。常規(guī)的隨機裁剪增強在切塊場景下要小心因為子圖本身已經(jīng)很小再隨機裁剪可能把唯一的目標裁掉。我建議優(yōu)先使用翻轉(zhuǎn)、旋轉(zhuǎn)、色彩擾動、馬賽克增強裁剪類增強要克制或者使用帶目標約束的裁剪。3.4 推理階段坐標映射與NMS合并推理時先對測試大圖做和訓(xùn)練時完全相同的切塊處理相同網(wǎng)格數(shù)、相同重疊率、相同子圖尺寸。然后對每塊子圖單獨做檢測拿到檢測結(jié)果后把所有框映射回原圖坐標。假設(shè)模型在子圖上輸出的歸一化框為(cx, cy, w, h)子圖左上角在原圖中的坐標是(off_x, off_y)子圖寬高是(sub_w, sub_h)原圖寬高是(orig_w, orig_h)那么映射公式如下def map_box_to_original(cx, cy, w, h, off_x, off_y, sub_w, sub_h, orig_w, orig_h): abs_cx cx * sub_w off_x abs_cy cy * sub_h off_y abs_w w * sub_w abs_h h * sub_h orig_cx abs_cx / orig_w orig_cy abs_cy / orig_h orig_w_ratio abs_w / orig_w orig_h_ratio abs_h / orig_h return orig_cx, orig_cy, orig_w_ratio, orig_h_ratio映射完之后所有子圖的框都到了同一張原圖坐標系里這時候需要做全局NMS。因為重疊區(qū)域的存在同一個目標可能被相鄰兩個子圖各檢測一次產(chǎn)生兩個高度重疊的框。NMS的作用就是保留置信度更高的框抑制重復(fù)框。NMS要注意兩點一是按類別做不同類別的框不要互相抑制二是建議加一個比較溫和的IOU閾值比如0.5到0.6。如果場景里目標本身密集閾值可以放寬到0.4避免把相鄰的不同目標誤合并。實際項目里還有一種特殊情況目標在切分邊界上中心點所在的子圖檢測出了完整框但隔壁子圖由于看到了殘缺目標也可能輸出一個位置偏移的半截框。這兩個框的IOU不一定很高普通NMS可能合并不掉。遇到這種情況可以先檢查重疊率是否足夠如果問題頻繁出現(xiàn)可以考慮放寬合并條件比如對同一類別且中心距離小于某個像素閾值的框做合并處理。4. 常見問題與避坑實錄4.1 檢測框位置偏移、尺寸不對這類問題十有八九出在標注轉(zhuǎn)換或推理映射環(huán)節(jié)。最常見的錯誤包括減偏移量時減錯方向、歸一化時除以了原圖尺寸而不是子圖尺寸、推理映射時忘了加上子圖偏移。排查方法很簡單寫一個可視化腳本把檢測結(jié)果畫在原圖上對著問題圖看。如果所有框都偏移到右上方向那基本是偏移量計算錯誤如果框的寬高整體偏大或偏小那多半是縮放系數(shù)弄混了。我自己的習(xí)慣是每次改完切圖或推理腳本先跑一張圖把“子圖檢測框”和“原圖映射框”同時畫出來核對一遍確認無誤再批量跑。4.2 目標被切到邊界漏檢或重復(fù)如果訓(xùn)練后模型對跨邊界目標的檢測效果明顯偏差先檢查切圖重疊率。沒有重疊或重疊太少跨邊界目標就會變成“兩邊都只剩一半”的殘缺樣本。增大重疊率讓任何目標至少完整出現(xiàn)在一個子圖里是最直接的解決辦法。重復(fù)檢測的處理主要靠NMS但如果發(fā)現(xiàn)同一目標穩(wěn)定地被輸出兩個不完全重疊的框說明目標經(jīng)常橫跨多個子圖并且多個子圖都檢測到了。這時可以回到標簽分配邏輯檢查訓(xùn)練時是否也應(yīng)該采用“每個目標只分配給中心點所在塊”的策略。另外對邊界框的置信度做位置補償也是一種思路給距離子圖中心越近的框越高的置信度權(quán)重但這會引入額外超參數(shù)一般不作為首選。4.3 空塊太多導(dǎo)致訓(xùn)練失衡大圖場景通常目標稀疏切塊后一半以上的子圖可能都是空塊。把空塊全部喂進訓(xùn)練集會導(dǎo)致模型訓(xùn)練時大部分時間都在看背景整體收斂變慢甚至可能出現(xiàn)“全部輸出零檢測框”的退化結(jié)果。我的建議是先統(tǒng)計塊內(nèi)目標數(shù)量保留所有含目標的正樣本塊對于空塊隨機保留一部分數(shù)量控制在正樣本塊數(shù)量的三分之一以內(nèi)。如果任務(wù)本身誤檢率要求很高可以適當多留如果更看重召回率可以少留甚至不留。這個比例需要根據(jù)驗證集的實際表現(xiàn)微調(diào)。4.4 訓(xùn)練時間、顯存與I/O成本切塊后數(shù)據(jù)量膨脹訓(xùn)練I/O會成為新的瓶頸。幾千張原圖切成幾萬個子圖如果每次都從磁盤隨機讀小文件訓(xùn)練速度會慢得讓人崩潰??尚械姆桨赣袃蓚€。第一個是離線切圖把切好的子圖直接保存到本地再配合數(shù)據(jù)加載器做緩存或預(yù)取簡單直接缺點是占用磁盤空間。第二個是在線切圖在數(shù)據(jù)加載階段動態(tài)從原圖切塊優(yōu)點是靈活可以隨機調(diào)整切塊偏移來增加數(shù)據(jù)多樣性缺點是每次訓(xùn)練都可能重復(fù)計算需要做好緩存。顯存方面切塊后的子圖尺寸變小單張進入顯存基本沒問題瓶頸反而轉(zhuǎn)移到了數(shù)據(jù)讀取和增強的CPU占用上建議加大num_workers或者用內(nèi)存映射方式讀取大圖。5. 什么時候切塊不夠用進階擴展思路5.1 推理時切片也是一條路訓(xùn)練時切塊是最穩(wěn)的方案但不是唯一的選擇。還有一種做法是訓(xùn)練時整圖訓(xùn)練推理時再把大圖切片分別檢測業(yè)內(nèi)叫SAHI切片輔助推理已經(jīng)有現(xiàn)成的開源庫可以快速實驗。訓(xùn)練時切片的好處是讓模型在訓(xùn)練階段就適應(yīng)小圖視角推理時切片的好處是訓(xùn)練數(shù)據(jù)可以保持原始上下文模型看到的目標更完整。實際操作中前者通常更穩(wěn)因為訓(xùn)練和推理的數(shù)據(jù)分布一致不容易出現(xiàn)尺度不一致導(dǎo)致的性能下降。如果你暫時不想改動訓(xùn)練流程直接拿SAHI在測試階段做切片推理也是一個低成本驗證方案。5.2 切塊、多尺度與高分辨率特征層互補切塊本質(zhì)上是數(shù)據(jù)層面的尺度放大它和多尺度訓(xùn)練、特征金字塔這些策略并不沖突可以組合使用。多尺度訓(xùn)練能增加模型對目標尺寸變化的魯棒性但不會解決“目標在輸入圖上只有3個像素”的根本問題。切塊先解決了這個底層問題多尺度再在子圖基礎(chǔ)上去增強多樣性兩者配合效果更好。另外很多檢測器的特征金字塔默認從P3層開始小目標特征到了P3已經(jīng)損失不少可以考慮使用帶P2層的檢測頭讓高分辨率特征圖直接參與到小目標預(yù)測。這類改進在切塊方案之后通常還能帶來幾個點的提升。5.3 超分重建與切塊結(jié)合如果目標實在太小切塊加放大之后仍然不夠識別可以考慮對局部區(qū)域做超分辨率重建先放大再檢測。比如先通過一個快速篩選器找到可能包含目標的子圖區(qū)域?qū)@些區(qū)域做超分再進入精細檢測器。這個方案的代價是推理鏈路變長、耗時增加適合離線處理或?qū)φ倩芈室髽O高但對實時性要求不高的場景。我嘗試過在部分遙感項目里用這個組合小目標的召回率確實有提升但速度會慢好幾倍是否值得需要根據(jù)業(yè)務(wù)具體權(quán)衡。5.4 部署階段的性能注意點切塊方案上線部署時計算量會變成原來的N倍這是一個經(jīng)常被忽略的成本。比如4張子圖推理次數(shù)就是原來的4倍如果再疊加多尺度或TTA計算量可能膨脹到十幾倍。部署時可以做兩件事優(yōu)化第一重疊區(qū)域的卷積特征可以緩存復(fù)用但通常實現(xiàn)復(fù)雜收益不一定明顯第二多個子圖可以放到同一個batch里并行推理比逐張推理更高效。另外前后處理的邏輯要封裝成獨立模塊輸入一張大圖輸出完整坐標的檢測結(jié)果對上層應(yīng)用屏蔽切塊細節(jié)這樣后續(xù)切換算法或調(diào)整切塊參數(shù)時不會影響業(yè)務(wù)流程。最后幾句實在話切塊訓(xùn)練不是銀彈但在大圖小目標場景里它確實是性價比最高、最容易落地的一步。我做過的一個遙感影像檢測項目目標像素只有二三十像素原圖四千多像素寬最初直接縮放訓(xùn)練mAP慘不忍睹后來改成切塊加重疊什么網(wǎng)絡(luò)結(jié)構(gòu)都沒動mAP直接漲了十幾個點。這個提升幾乎全部來自數(shù)據(jù)預(yù)處理層面的尺度還原。如果你正準備上手這類項目我的建議是動手前先花半天統(tǒng)計目標尺寸和分布想清楚切多少塊、子圖多大、重疊多少切完圖做好可視化校驗確認標簽沒有出錯訓(xùn)練和推理的切塊參數(shù)保持一致后處理NMS別偷懶。這幾件事做扎實了大圖小目標的檢測基本就成功了一大半。