:區(qū)域提議與候選框生成全解析)
簡介一份面向Python開發(fā)者與計算機視覺初學者的選擇性搜索實現(xiàn)示例聚焦如何通過超像素分割、區(qū)域生長與特征排序生成目標候選區(qū)域為后續(xù)接入RCNN、Fast RCNN等檢測模型打下基礎(chǔ)。壓縮包共19個文件包含3個Python腳本分別對應(yīng)搜索、過濾、提取步驟、14張示例圖像12張png與2張jpg、1份markdown說明文檔及l(fā)icense文件整體僅53KB結(jié)構(gòu)精簡且目錄層級清晰適合快速上手。已有283人學習代碼中附有可視化中間結(jié)果可直觀看到超像素合并與候選框生成過程腳本還演示了圖像預處理、SLIC超像素分割及顏色直方圖等特征提取方法。通過閱讀搜索、過濾、提取三個腳本能學習如何用PIL和numpy實現(xiàn)完整流程并進一步理解候選區(qū)域輸入到檢測模型進行集成與推理的思路是入門目標檢測區(qū)域提議的實用示例。1. 選擇性搜索到底是什么區(qū)域提議算法為什么值得自己動手寫一遍做目標檢測的人遲早會遇到這個詞selective_search選擇性搜索。它的歷史地位很明確——在 Faster R-CNN 把區(qū)域提議網(wǎng)絡(luò)RPN做進模型之前R-CNN 系列全靠它來生成候選區(qū)域先找出圖上“可能含有目標”的幾千個框再逐個交給分類器確認。今天雖然端到端檢測器是主流但選擇性搜索依然是理解檢測流程、做無類別目標定位、或者快速驗證一個分類器能不能用于檢測任務(wù)時最順手的起點工具。這篇筆記想解決的不只是“跑通一個示例”而是幫你在本機把候選框真正生成出來、畫到圖上、接到后續(xù)檢測管線里順帶告訴你參數(shù)該怎么調(diào)、坐標系為什么總翻車、同一張圖為什么不同庫跑出來的框不一樣。適合正在入門檢測算法的人也適合需要區(qū)域提議但不想依賴重型檢測框架的工程同學。一句話它解決的是“目標可能在哪兒”的問題接下來才輪到“這是什么”。2. 算法核心拆解相似度度量與層次分組的選擇邏輯2.1 從分割到分組Felzenszwalb初始區(qū)域的生成邏輯選擇性搜索的第一步不是滑動窗口而是先用基于圖的圖像分割算法生成一批初始小區(qū)域。這一步常用的就是 Felzenszwalb 的分割方法它把像素看作圖上的節(jié)點相鄰像素之間的顏色差異作為邊的權(quán)重然后按最小生成樹的思路合并相似區(qū)域。合并的關(guān)鍵是一個判斷準則兩個區(qū)域是否能合并取決于它們內(nèi)部差異和邊界差異的相對大小。這一設(shè)計讓初始分割對噪聲有一定容忍度。參數(shù) scale 會直接影響合并的寬松程度——scale 越大越傾向于合并成大塊區(qū)域scale 越小碎片越多。sigma 則是分割前高斯平滑的標準差平滑越多小紋理越容易被抹掉。一般我用 scale 在 300 到 600 之間起步sigma 取 0.8 左右min_size 設(shè) 10 到 20過濾掉像素數(shù)極少的碎片。# 用 skimage 感受一下 Felzenszwalb 分割的粒度變化 import cv2 import numpy as np from skimage.segmentation import felzenszwalb img cv2.imread(sample.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) segments felzenszwalb( img_rgb, scale500, sigma0.8, min_size20 ) print(分割出的獨立區(qū)域數(shù)量:, segments.max() 1)分割后的每個整數(shù)標簽就是一個初始區(qū)域選擇性搜索之后做的所有層次分組都是在這批小區(qū)域上進行的。注意 felzenszwalb 返回的標簽并不保證滿足物體邊界它更偏“超像素式”的顏色一致塊所以后面才需要用相似度度量反復合并。2.2 貪心層次分組相似度函數(shù)怎么把碎片合成語義塊拿到初始區(qū)域之后選擇性搜索進入核心階段貪心層次分組。它不斷計算相鄰兩個區(qū)域的相似度把最相似的一對合并更新鄰接關(guān)系重復直到整張圖只剩一個區(qū)域。這個過程會把碎小的塊逐步聚成能對應(yīng)“一只貓”“一輛車”的中等尺度塊也會保留從單個超像素到全圖的各種尺度最終形成候選區(qū)域的金字塔。相似度由四個分量加權(quán)求和得到。顏色相似度比較兩個區(qū)域在 Lab 空間下的顏色直方圖每通道 25 個 bin用直方圖交集衡量重合程度紋理相似度用局部二值模式或 SIFT 風格的高斯導數(shù)響應(yīng)直方圖做同樣比較。尺寸相似度鼓勵小區(qū)域先合并兩個區(qū)域的面積之和越小得分越高這避免某個大塊吞掉周圍所有小塊。填充相似度衡量合并后外接框能多大程度包住內(nèi)部空洞獎勵那些形狀貼合目標的合并。# 偽代碼層次分組主循環(huán)理解結(jié)構(gòu)比背庫函數(shù)重要 def hierarchical_grouping(regions): adjacency build_adjacency(regions) while len(adjacency) 1: # 找到相似度最高的相鄰區(qū)域?qū)?i, j, max_score find_most_similar(regions, adjacency) if max_score 0: break # 合并并更新特征與鄰接表 merged merge_features(regions[i], regions[j]) adjacency rebuild_adjacency(adjacency, i, j, merged) regions replace(regions, i, j, merged) return regions這里的 merge_features 需要同步更新顏色直方圖、紋理直方圖、尺寸和外接框信息。實際工程中很少有人手寫這層但理解主循環(huán)后你才能明白為什么提示圖一旦出現(xiàn)一條被切斷的車燈候選框質(zhì)量差時問題多半出在相似度權(quán)重或初始分割上。2.3 為什么選選擇性搜索而非滑動窗口或暴力網(wǎng)格滑動窗口在圖像金字塔上暴力滑理論上窮盡但候選框數(shù)量爆炸、速度慢、且大量窗口落在背景上。選擇性搜索的意義是把“可能的位置”先篩到一兩千個質(zhì)量還明顯好于隨機采樣。相比后來 Faster R-CNN 的 RPN它不依賴訓練好的網(wǎng)絡(luò)也就意味著你可以在沒有任何檢測模型的情況下只靠圖像自身結(jié)構(gòu)生成候選框。這在冷啟動、數(shù)據(jù)探索、或者給弱監(jiān)督任務(wù)找初始框時非常實用。它的主要代價是速度CPU 上單張圖約 0.5 到 2 秒視參數(shù)與圖片尺寸而定。好在多數(shù)場景下候選框只離線生成一次存下來后續(xù)復用。另一個代價是候選框質(zhì)量上限受初始分割制約小到像遠處一只鳥那么小的目標如果初始分割直接把它并入背景后面層次分組基本撈不回來。這點在后文參數(shù)調(diào)優(yōu)里我會詳細說怎么規(guī)避。3. 跑通第一個示例環(huán)境準備、最小代碼與結(jié)果可視化3.1 環(huán)境準備與實現(xiàn)選擇兩種常用實現(xiàn)庫的取舍目前最容易上手的實現(xiàn)有兩個Python 的 selectivesearch 庫以及 OpenCV contrib 模塊里的 ximgproc 選擇性搜索接口。selectivesearch 完全基于 skimage 和 numpy純 Python 實現(xiàn)代碼短、可讀性好、參數(shù)直觀適合學習與定制缺點是慢。OpenCV 版本底層用 C速度快不少適合批量處理但 API 較繞且 OpenCV 版本之間行為不完全一致。我的建議是學習階段用 selectivesearch跑通邏輯、看懂參數(shù)進入批量生產(chǎn)再換成 OpenCV。兩個版本都要求圖像通道順序是 RGB 還是 BGR要特別小心。selectivesearch 按 skimage 慣例吃 RGB你把 OpenCV 讀出來的 BGR 圖直接丟進去候選框方向性差異可能不大但顏色相似度分量的統(tǒng)計就會對不上。# 最小安裝清單 pip install selectivesearch scikit-image opencv-python# 驗證環(huán)境 import selectivesearch import skimage import cv2 print(selectivesearch OK:, selectivesearch.__version__ if hasattr(selectivesearch, __version__) else unknown) print(skimage OK:, skimage.__version__) print(OpenCV OK:, cv2.__version__)3.2 最小示例代碼用 selectivesearch 庫跑出候選框下面這段是完整可跑的示例讀圖、轉(zhuǎn) RGB、執(zhí)行選擇性搜索、把結(jié)果整理成 Nx4 數(shù)組。注意 selectivesearch 返回的 regions每個元素是一個字典包含 rect、labels 和 size。rect 的格式是 [x, y, w, h]x 和 y 是左上角坐標這個格式非常容易在后續(xù)忘掉。import cv2 from selectivesearch import selective_search # 1. 讀圖并轉(zhuǎn) RGB img cv2.imread(sample.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 2. 執(zhí)行選擇性搜索 _, regions selective_search( img_rgb, scale500, sigma0.9, min_size20 ) # 3. 轉(zhuǎn)化為 [x1, y1, x2, y2] 格式候選框 candidates [] for r in regions: x, y, w, h r[rect] if w * h 500: # 過濾過小區(qū)域避免全是噪聲 continue candidates.append([x, y, x w, y h]) print(f候選框數(shù)量: {len(candidates)}) print(前 5 個候選框:, candidates[:5])scale 控制初始分割的粗糙度值越大初始區(qū)域越少最終候選框也越少適合圖中目標大而少的情況。sigma 影響分割前的平滑程度0.9 是常用起步值。min_size 會過濾掉分割時的小塊這里設(shè) 20。第三個返回值 regions 里還帶 label 信息如果后面想做區(qū)域特征提取可以按 label 取掩膜但大多數(shù)檢測任務(wù)只用到 rect。3.3 可視化候選框把區(qū)域畫在原圖上確認效果光有數(shù)值沒法判斷質(zhì)量畫框是確認效果的第一步。把前 100 個候選框畫在原圖上你立刻能看出框的尺度分布是否合理。好的效果是多個不同尺度框圍繞目標分布背景區(qū)域也有所覆蓋但不會占滿全圖。# 畫前 100 個候選框直觀確認覆蓋效果 vis img.copy() for x1, y1, x2, y2 in candidates[:150]: cv2.rectangle(vis, (x1, y1), (x2, y2), (0, 255, 0), 1) cv2.imwrite(candidates_top150.jpg, vis) # 再畫一張全量候選框圖感受數(shù)量規(guī)模 vis_all img.copy() for x1, y1, x2, y2 in candidates: cv2.rectangle(vis_all, (x1, y1), (x2, y2), (255, 0, 0), 1) cv2.imwrite(candidates_all.jpg, vis_all)如果畫出來的框幾乎全是整張圖的外接框加幾個隨機碎片多半是初始分割沒拆開需要調(diào)大 min_size 或者降低 scale。如果大量極端瘦長條框出現(xiàn)說明紋理相似度權(quán)重過強或者平滑不足??梢暬皇强蛇x項它是你后續(xù)調(diào)參數(shù)的“眼睛”每次改參都要存一張對比圖。4. 四個必調(diào)參數(shù)從候選框數(shù)量到質(zhì)量的工程取舍4.1 scale 與 sigma先調(diào)整哪一檔影響最大多數(shù)人第一次跑完示例看到幾千個候選框第一反應(yīng)是懷疑模型寫錯了。其實候選框數(shù)量正常區(qū)間就是 1000 到 3000關(guān)鍵看你要喂給后續(xù)的模型還是僅做定位探索。scale 是影響最直接的一檔它不直接限制候選框數(shù)量而是決定初始分割的尺度進而影響整棵合并樹的結(jié)構(gòu)。我常用的調(diào)參順序是先固定 sigma0.8、min_size20只動 scale。用 300、500、800 各生成一遍候選框打印數(shù)量并可視化觀察目標上的覆蓋。scale 過低時初始碎片太多候選框數(shù)量暴漲但大量是小碎塊scale 過高時初始區(qū)域已經(jīng)貼近整個目標合并樹深度不夠候選框數(shù)量驟降但誤檢率也降低。sigma 的作用相對溫和主要影響邊緣粘連問題對線條狀物體更明顯一般不需要頻繁動它。# 對同一張圖測試多個 scale for scale in 300 500 800; do python selectivesearch_demo.py --scale $scale --out scale_${scale}.jpg done不同 scale 生成的結(jié)果甚至可以合并起來把兩次或三次不同 scale 的候選框做并集能提升多尺度目標的召回。這是選擇性搜索的“多策略”思想官方論文里也提到組合不同顏色空間和參數(shù)能得到更多樣化的候選框但代價是候選框數(shù)量線性增長后續(xù) NMS 壓力更大。4.2 min_size 與 min_region_size候選框數(shù)量控制的關(guān)鍵min_size 在分割階段起作用過濾掉初始分割中像素少于該值的區(qū)域直接決定了碎片總量。min_region_size 是 selectivesearch 庫的特有參數(shù)它過濾合并結(jié)果中過小的最終區(qū)域這個值設(shè)得太大會丟掉真正的小目標。經(jīng)驗上對常規(guī) 800x600 圖片min_size 取 20 到 50min_region_size 取 500 到 2000 都算合理區(qū)間。這兩個參數(shù)影響最大的是邊界把 min_size 設(shè)成 200邊緣的小目標、遠處行人、小動物都會因為初始分割階段就被并掉而無法出現(xiàn)在任何候選框里。反過來設(shè)得太小會讓顏色直方圖稀疏相似度計算不穩(wěn)定候選框大量重疊。我的習慣是先看目標面積占圖面積比例目標約占 5% 到 10% 時min_size 取 20 左右目標較大時再提高。參數(shù)作用階段典型范圍調(diào)大后的影響scale初始分割300-800候選框變少大目標更完整sigma分割前平滑0.4-1.0弱化紋理邊緣更鈍min_size初始分割過濾10-50碎片減少小目標丟失min_region_size合并結(jié)果過濾500-2500最終候選框總數(shù)下降4.3 候選框數(shù)量壓縮NMS閾值與坐標歸一化的配合幾千個候選框直接喂給分類器每張圖跑一次前向幾百毫秒數(shù)據(jù)集大一點就等不起。壓縮數(shù)量主要有兩步先做 NMS 去掉高度重疊的框再做得分或尺寸篩選保留 top-N。NMS 的 IoU 閾值一般取 0.5 到 0.7閾值越低框越少但可能漏目標閾值越高保留的框越接近原始分布。import numpy as np def nms(rects, thresh0.5): rects np.asarray(rects, dtypenp.float32) if len(rects) 0: return [] x1, y1, x2, y2 rects[:, 0], rects[:, 1], rects[:, 2], rects[:, 3] areas (x2 - x1 1) * (y2 - y1 1) order areas.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter np.maximum(0.0, xx2 - xx1 1) * np.maximum(0.0, yy2 - yy1 1) iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou thresh)[0] order order[inds 1] return rects[keep]執(zhí)行完 NMS 后再按面積排序取前 1000 到 1500 個框基本能滿足 R-CNN 風格訓練的數(shù)據(jù)需求。坐標歸一化這一步容易忽略如果 GT 框的坐標已經(jīng)除以了圖像寬高候選框還是像素坐標后面算 IoU 就會徹底亂掉。統(tǒng)一辦法是候選框也除以原圖寬高存成 0 到 1 的相對坐標邏輯上更安全。5. 避坑指南選擇性搜索實戰(zhàn)中的常見鍋與排查辦法5.1 坐標格式翻車bbox 到底是左上角寬高還是中心點現(xiàn)象用候選框去算 IoU結(jié)果數(shù)值小得離譜或者畫框時框整體錯位到左下角。 原因selectivesearch 返回的 rect 是 [left, top, width, height]而 OpenCV 的 rectangle 需要 [x1, y1, x2, y2]很多人直接拿 w、h 當 x2、y2 用框自然歪了。更隱蔽的是有些教程把 rect 轉(zhuǎn)成了中心點格式混用時全部錯位。 解決寫一個統(tǒng)一的正反向轉(zhuǎn)換函數(shù)入口和出口都走同一個在代碼里加上斷言確保 x2 x1 且 y2 y1數(shù)值異常時立刻打印排查。def xywh_to_xyxy(rect): x, y, w, h rect return [x, y, x w, y h]5.2 候選框質(zhì)量差目標只出一半框怎么辦現(xiàn)象可視化時發(fā)現(xiàn)目標被一個框框住了上半身下半身被另一個更大框包含或者目標整體沒被任何框完整覆蓋。 原因最常見的是 scale 太大初始分割已經(jīng)把目標分成兩塊合并階段又沒有某個相鄰對能讓它們合在一起另一種是目標和背景顏色接近顏色相似度誤判導致過早與背景合并。 解決先降低 scale 到 300 試試觀察是否出現(xiàn)更完整的包圍盒再把 sigma 調(diào)到 0.6 到 0.8 減少邊緣干擾若仍是老樣子把圖片長邊縮到 500 左右再跑很多分割問題在大尺寸圖上反而更明顯。5.3 內(nèi)存與速度告急大批量圖片如何避免卡死現(xiàn)象幾百張圖跑下來越來越慢內(nèi)存從幾百 MB 漲到幾 GB甚至直接 OOM。 原因selectivesearch 把每張圖的所有中間區(qū)域全部保留返回數(shù)據(jù)集一大Python 對象開銷疊加內(nèi)存就爆了。另一個隱性原因是沒有及時釋放過大的原始圖循環(huán)里不斷讀圖疊加內(nèi)存引用。 解決用 OpenCV 版替代純 Python 版速度提升明顯圖片先縮放到長邊 1000 以內(nèi)候選框生成后立即轉(zhuǎn)成緊湊的 numpy 數(shù)組并丟棄 regions每個進程處理完一批就 gc.collect()。import gc def process_batch(image_paths): result [] for path in image_paths: img cv2.imread(path) img cv2.resize(img, (800, int(800 * img.shape[0] / img.shape[1]))) # 執(zhí)行 selective search 并提取候選框 result.append(np.asarray(candidates, dtypenp.float32)) gc.collect() return result5.4 OpenCV版與selectivesearch庫結(jié)果對不上現(xiàn)象同一張圖OpenCV 跑出來 800 個候選框selectivesearch 跑出來 2600 個目標覆蓋情況也不一致以為是哪個庫裝錯了。 原因兩個實現(xiàn)的相似度度量細節(jié)不同OpenCV 的 ximgproc 模塊使用了不同的特征初化與合并策略行為并不保證完全一致。 解決選一個版本固定下來訓練和推理全程用同一個。對結(jié)果做對比時只比較“數(shù)量級”和“是否覆蓋目標”這兩個層面不要逐框?qū)R。如果一定要在論文或報告中保持一致性建議在代碼里封裝統(tǒng)一接口換庫時只改一行。5.5 IoU匹配奇怪正負樣本數(shù)量異?,F(xiàn)象用候選框和 GT 做正負樣本分配時正樣本數(shù)幾乎為 0或者負樣本全是背景垃圾框。 原因坐標系統(tǒng)不統(tǒng)一候選框像素坐標對 GT 歸一化坐標、IoU 閾值設(shè)得太高、候選框數(shù)量不夠?qū)е聸]有框與 GT 達到閾值。 解決先確認兩邊坐標都在同一體系中正樣本 IoU 閾值建議從 0.5 起步如果候選框覆蓋不到 GT優(yōu)先檢查 scale 和 min_size而不是調(diào)閾值掩蓋問題。# 快速驗證一條 GT 是否被候選框覆蓋 gt np.array([120, 80, 320, 280], dtypenp.float32) for cand in candidates: iou compute_iou(gt, cand) if iou 0.5: print(找到覆蓋框:, cand, IoU:, iou) break else: print(該 GT 沒有被任何候選框覆蓋請調(diào)整 scale)6. 進階一步把候選框接到目標檢測管線里的驗證技巧6.1 用召回率曲線驗證候選框質(zhì)量候選框數(shù)量只是表面指標真正該看的是“目標有沒有被框到”。正確做法是取二十張帶 GT 的驗證圖對每張圖生成候選框計算每個 GT 與所有候選框的最大 IoU統(tǒng)計達到 0.5 和 0.7 的 GT 比例。這個數(shù)字就是區(qū)域提議召回率它能直觀反映參數(shù)調(diào)優(yōu)的價值。def proposal_recall(gts, candidates, iou_thresh0.5): recalled 0 for gt in gts: ious [compute_iou(gt, cand) for cand in candidates] if max(ious) iou_thresh: recalled 1 return recalled / len(gts)我一般會分別打印候選框數(shù)量與召回率畫一張表格scale 300、500、800 各對應(yīng)多少框、多少召回。召回率到 0.9 以上時候選框數(shù)量通常在 2000 附近再多就對檢測精度幫助不大了。6.2 把候選框接到分類器做一個最簡 R-CNN 驗證如果只到“生成框”為止你沒法確認這些框是否真的有用。最常見的驗證方式是把候選框剪裁出來縮放成 224x224送進一個預訓練分類器比如 ResNet取置信度最高的類別作為框標簽再 NMS 一下你就得到了一個不訓練的簡易檢測器。這個驗證能快速暴露兩個問題候選框是否對目標提供了足夠的上下文以及分類器對小目標的敏感度。from torchvision import models, transforms from PIL import Image model models.resnet18(pretrainedTrue) model.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) for x1, y1, x2, y2 in candidates[:50]: crop Image.open(sample.jpg).crop((x1, y1, x2, y2)) tensor transform(crop).unsqueeze(0) with torch.no_grad(): probs torch.softmax(model(tensor), dim1) print(f框 {x1},{y1},{x2},{y2}: {probs.max().item():.2f})這種驗證不用訓練任何檢測頭半小時就能跑完是檢驗區(qū)域提議質(zhì)量性價比最高的一步。我自己的習慣是永遠先把候選框可視化打印到本地再檢查召回率最后才引入分類器——因為候選框階段出的錯誤到了分類階段會被放大成“假正例”到時候查錯成本高得多。最后一個血淚經(jīng)驗不要相信“調(diào)好一組參數(shù)就能跑全部數(shù)據(jù)”這種話。換數(shù)據(jù)集、換分辨率、甚至換攝像頭角度候選框分布都會變。每次換數(shù)據(jù)先跑一遍召回率和可視化確認沒問題再往下走。這樣做雖然慢幾步但能省下大量排錯時間。希望幫到你。本文還有配套的精品資源點擊獲取