:從大圖到有效特征的關(guān)鍵步驟)
說句實在話圖像類項目的成敗往往不是死在模型結(jié)構(gòu)上而是死在這些最基礎(chǔ)的數(shù)據(jù)環(huán)節(jié)上。這第10篇我終于把主題從表格數(shù)據(jù)轉(zhuǎn)到圖像來了。今天要聊的“圖像切割與特征提取”屬于數(shù)據(jù)預(yù)處理和特征工程里最容易翻車、也最值得摳細節(jié)的板塊。很多人拿到的圖像不是網(wǎng)上現(xiàn)成的小尺寸圖庫而是相機采集的高分辨率大圖——工業(yè)零件、遙感影像、醫(yī)學(xué)切片、街景監(jiān)控幾千萬像素是常態(tài)。這時候你要是把原圖直接喂給模型顯存第一個不同意你要是暴力縮放成224×224那些細小目標和關(guān)鍵紋理基本就廢了。所以怎么做切割、切完之后怎么提取特征是每個視覺項目都躲不開的第一道坎。這篇文章適合兩類人一類是剛接觸計算機視覺、想知道圖像數(shù)據(jù)該怎么下手的初學(xué)者另一類是模型已經(jīng)能跑通、但準確率和魯棒性一直上不去的工程師。我會結(jié)合實際項目經(jīng)驗把切割策略、特征設(shè)計、常見坑一次講透。1. 圖像切割為什么要動刀怎么動刀才不傷數(shù)據(jù)1.1 切割在圖像項目里的三個真實價值你可能會想切圖不就是把大圖裁小嗎有什么好講的。實際項目里這一步的核心價值至少有三個而且每個都直接關(guān)系到你的模型能不能訓(xùn)練起來、能不能訓(xùn)好。第一個價值是適配算力。工業(yè)相機的圖像動不動就是6000×4000像素一張單通道灰度圖按uint8算就要24MB左右三通道RGB直接翻三倍。你以batch size 32往里塞光是輸入數(shù)據(jù)就是幾個GBGPU顯存直接爆炸。就算你勉強塞進去了訓(xùn)練時的數(shù)據(jù)讀取和預(yù)處理也會變成瓶頸GPU吃不滿整個訓(xùn)練流程慢得像蝸牛。切塊之后比如切成512×512一張大圖變成幾十個小塊顯存壓力瞬間緩解。第二個價值是保留小目標信息。這個更致命。很多任務(wù)里關(guān)鍵目標在整幅圖中只占很小一塊面積。比如一張零件圖上有一條幾十像素寬的劃痕你把整張圖縮放到224×224這條劃痕可能就變成了一兩個像素的模糊噪聲什么特征都提取不出來。切塊相當于讓模型用原始分辨率去觀察局部區(qū)域“一整張圖看不清”變成了“每一塊都能看清細節(jié)”這是切割最本質(zhì)的收益。第三個價值是樣本量擴充。一個數(shù)據(jù)集中如果有1000張大圖切成16塊就是16000個樣本切成49塊就是49000個樣本。對深度學(xué)習(xí)或者對數(shù)據(jù)量敏感的模型來說這等于免費多了一批訓(xùn)練數(shù)據(jù)。當然樣本量變大不代表樣本獨立這里面的坑我后面會專門講。1.2 三種常用切割策略與適用場景我平時用到的切分方式主要有三種它們不是互斥的很多時候要組合著用。第一種是固定網(wǎng)格切分。就是把大圖按行列均勻分成固定大小的塊比如2048×2048的圖按512切成4×4共16塊。優(yōu)點是實現(xiàn)最簡單、覆蓋完整不會漏掉任何區(qū)域缺點是如果目標恰好落在切分邊界上目標會被硬生生劈成兩半兩塊圖里各留一半特征都不完整。這種切法適合目標分布相對均勻、圖幅尺寸規(guī)整的場景比如把整幅遙感影像分成標準瓦片。第二種是滑動窗口切分。設(shè)定窗口大小和滑動步長窗口按步長在圖上移動。關(guān)鍵參數(shù)就是步長與窗口大小的比值。舉個例子2048×2048圖像窗口512步長256那x方向和y方向各能得到7個起點位置一共49個patch。相比網(wǎng)格切法的16塊重疊率50%讓樣本量直接翻了3倍多。重疊的意義在于目標如果恰好跨在邊界上相鄰窗口會把它完整包含進去至少有一張patch能看清全貌。步長越小重疊越大目標被切碎的概率越低但樣本量膨脹也越厲害訓(xùn)練耗時同步上漲。第三種是隨機裁剪。在訓(xùn)練階段從原圖像上隨機取一個子區(qū)域配合隨機翻轉(zhuǎn)、旋轉(zhuǎn)、亮度擾動一起用這是深度學(xué)習(xí)中非常常見的數(shù)據(jù)增強手段。它的優(yōu)勢是每次epoch看到的樣本都在變化模型泛化能力更好缺點是非確定性的覆蓋推理時需要用全圖或多窗口預(yù)測來保證不漏檢。三種策略的取舍我整理成了表策略覆蓋完整性樣本量增幅適用場景固定網(wǎng)格切分完整但邊界無保護適中目標分布均勻、圖幅規(guī)整滑動窗口切分完整且邊界有重疊保護較大目標大小不定、對召回要求高隨機裁剪訓(xùn)練時不保證完整靈活控制數(shù)據(jù)增強、樣本量充足時實際項目里我常用的組合是訓(xùn)練階段用滑動窗口切塊重疊率設(shè)在50%上下如果數(shù)據(jù)仍然不夠再疊加隨機裁剪做增強。1.3 切分時必須守住的三個底線切圖看似簡單但我見過太多項目在這里埋雷后面怎么調(diào)都救不回來。三個底線必須守住。底線一是標簽必須跟著patch走。大圖的標簽不能直接復(fù)制給每一塊。比如大圖上有缺陷切成49塊可能只有3塊包含這個缺陷你把缺陷標簽給全部49塊模型直接學(xué)出一堆噪聲。正確做法是切割時記錄patch在大圖中的坐標與標注框求交疊交疊比例超過閾值的patch才繼承正樣本標簽或者按實際包含目標的比例來標。底線二是切分之后立即檢查樣本分布。大圖上目標占比可能只有1%切塊后如果不做篩選你的訓(xùn)練集里99%都是純背景patch正負樣本比例極端不平衡模型會傾向于把所有東西都預(yù)測成負樣本。先統(tǒng)計一下各類別patch數(shù)量不平就要在特征工程之前處理掉。底線三是按原圖劃分數(shù)據(jù)集而不是按patch劃分。同一張大圖切出來的patch之間有很強的相關(guān)性如果隨機把這些patch分進訓(xùn)練集和測試集模型等于提前見過“同一場景的親戚”驗證分數(shù)虛高上線后立刻現(xiàn)原形。正確做法是先把大圖按比例分成訓(xùn)練、驗證、測試組再對組內(nèi)圖像切塊保證同一個來源的大圖不會同時出現(xiàn)在兩個集合里。2. 特征提取從切割圖里撈什么怎么撈才劃算2.1 不訓(xùn)練網(wǎng)絡(luò)也能用的三組手工特征切完圖之后每個patch還是一張像素圖直接扔給模型不是不行但如果你要走的是經(jīng)典機器學(xué)習(xí)路線或者想在深度學(xué)習(xí)之前先建一個baseline手工特征是最快的路徑。我在做圖像分類、缺陷檢測項目時最常撈的是三組特征顏色灰度特征、紋理特征、形狀特征。顏色特征最簡單的做法是直方圖。要注意的是直接在RGB空間做直方圖對光照變化非常敏感同一個物體打光不同RGB值可能差出一大截。更穩(wěn)的做法是把圖像轉(zhuǎn)到HSV或LAB空間對H通道或ab通道做統(tǒng)計把色相和亮度剝離開?;叶葓D更簡單直接統(tǒng)計灰度直方圖、均值、標準差成本極低信息量卻不小。紋理特征里L(fēng)BP局部二值模式和GLCM灰度共生矩陣是我最常用的兩個。LBP的基本思路很樸素把一個像素和它的鄰居比較鄰居值比中心大記1否則記0一圈下來得到一個二進制數(shù)這個數(shù)字就描述了局部的紋理模式。統(tǒng)計整張圖里所有模式的出現(xiàn)頻率得到直方圖就是LBP特征。GLCM則統(tǒng)計兩個相距一定距離的像素在不同灰度組合下出現(xiàn)的次數(shù)再從這個矩陣抽象出對比度、能量、均勻性、相關(guān)性等標量。這兩個特征擅長抓“表面質(zhì)感”比如金屬拉絲、木頭紋路、布料織法這類人類覺得“手感不同”但顏色相近的區(qū)域恰恰是它們的用武之地。形狀特征的代表是HOG方向梯度直方圖。思路是先把圖像分成小格子在每個格子里統(tǒng)計梯度方向的分布再把鄰近格子的直方圖拼接起來。梯度大的地方往往就是邊緣和輪廓HOG等于把“長了什么樣、輪廓清不清楚”數(shù)值化。早期行人檢測就是靠HOGSVM撐起來的到今天用在零件輪廓、字符識別這類場景依然有效。這三組特征不是互斥的實際項目里我通常把它們拼起來用。特征之間互補性越強分類器的上限越高。2.2 手工特征里最容易被忽視的維度與量綱問題手工特征拼起來有個直接的麻煩維度爆炸。一個patch上LBP直方圖256維GLCM五個統(tǒng)計量灰度統(tǒng)計四五項HOG按常見參數(shù)輕松上千維。加在一起一個patch輕松有幾千維而你的有效樣本可能只有幾千到幾萬。高維度小樣本直接喂線性模型或者SVM過擬合幾乎是必然的。所以流程上我是這么走的。第一步先做低方差過濾那些幾乎所有樣本上取值都差不多的特征列對區(qū)分類別沒有貢獻直接刪掉。第二步做標準化顏色直方圖的取值可能是幾百GLCM的對比度可能是零點幾HOG的值可能到幾千這些量綱差異會讓距離類模型完全被大數(shù)值特征主導(dǎo)。常見做法是z-score標準化把每列變成均值0、方差1。第三步做PCA降維保留95%方差對應(yīng)的主成分數(shù)量把幾千維壓到幾十上百維。這一步在去掉共線性冗余的同時也讓后續(xù)分類器更穩(wěn)。這里有一個特別常見的錯誤我必須單獨拎出來說標準化和PCA的擬合只能用訓(xùn)練集驗證集和測試集要復(fù)用訓(xùn)練集擬合好的轉(zhuǎn)換器來transform。有人圖省事把全部數(shù)據(jù)一起fit測試集的信息就在這一步泄漏進了訓(xùn)練過程驗證分數(shù)虛高沒參考意義。2.3 什么時候別折騰手工特征直接上深度特征手工特征雖然穩(wěn)定但天花板確實存在。復(fù)雜的語義信息——比如“這個缺陷是焊接時留下的氣孔”還是“是磕碰出來的凹痕”——靠直方圖和梯度很難描述清楚。這種情況下深度特征才是正解。做法也很簡單拿一個在ImageNet上預(yù)訓(xùn)練過的CNN模型比如ResNet18或ResNet50把最后用于分類的全連接層去掉只保留前面的卷積特征提取部分。輸入一張224×224的圖輸出一個幾百維的向量這個向量就是模型理解的“圖像語義壓縮包”。你不再需要手工設(shè)計任何特征需要做的只是把每張patch套進模型過一遍拿到特征向量存成npy后面該訓(xùn)練什么分類器就訓(xùn)練什么分類器。我在項目里會先看數(shù)據(jù)量和任務(wù)復(fù)雜度。數(shù)據(jù)量只有幾千張、類別差異又比較表象比如不同顏色的零件手工特征往往更快更穩(wěn)。數(shù)據(jù)量達到幾萬張、類別差異很抽象比如不同類型的產(chǎn)品缺陷或者場景里有大量光照和角度變化深度特征的優(yōu)勢就非常明顯了。兩者也不沖突深度特征為主再拼接上灰度均值方差這類基礎(chǔ)統(tǒng)計特征偶爾會有驚喜。3. 實操在一塊表面缺陷檢測數(shù)據(jù)上走通切割特征提取3.1 案例背景與整體流程我拿一個做過的表面缺陷檢測項目舉例細節(jié)做了脫敏但流程完全真實。背景是某工廠要對零件表面做自動質(zhì)檢相機采集到的灰度大圖尺寸為2048×2048標簽分三類正常、劃痕、表面污漬。缺陷在整圖中占比很小直接縮小整圖訓(xùn)練不現(xiàn)實所以整個流程定為大圖切割生成patch對每個patch提取特征用特征矩陣訓(xùn)練分類器完成檢測。流程拆開就是“讀圖→切塊→特征提取→標準化→降維→分類訓(xùn)練→評估”看著平平無奇但每一步都在前面講的坑里蹚過一遍。下面直接上代碼。3.2 圖像切割實現(xiàn)與邊界處理切割我選了滑動窗口窗口大小512步長256重疊率50%。這個組合怎么定的一是512×512在顯卡上可以開一個不算小的batch訓(xùn)練效率能接受二是步長256給了足夠重疊細小劃痕跨塊被切碎的概率大幅降低?;A(chǔ)切分用OpenCV就能寫import cv2 import numpy as np def split_into_patches(image, patch_size512, stride256): h, w image.shape[:2] patches [] coords [] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patches.append(image[y:y patch_size, x:x patch_size]) coords.append((x, y)) return patches, coords img cv2.imread(part_surface.png, cv2.IMREAD_GRAYSCALE) patches, coords split_into_patches(img) print(f共生成 {len(patches)} 個patch) # 2048x2048 圖像窗口512步長256理論上是 7x7 49 個這個寫法在圖像尺寸剛好匹配時沒問題但真實場景里圖像尺寸經(jīng)常是2047、2049這種“差一點”的怪數(shù)字。for range到不了最后一行/列尾部像素就被丟棄了。我實際生產(chǎn)里會換成while循環(huán)保證最后一個窗口緊貼邊界收尾寧可尾部多一次重疊也不能丟失圖像內(nèi)容。還有一個細節(jié)2048×2048、窗口512、步長256x方向能取到的起始點是0,256,512,768,1024,1280,1536一共7個位置7×7得到49個patch。如果你用固定網(wǎng)格512無重疊只能得到16個patch。重疊切法讓樣本量翻了三倍還多對于小樣本項目這是一個非??捎^的增幅。3.3 特征提取與特征矩陣構(gòu)建特征提取我選了LBP直方圖、GLCM統(tǒng)計特征、灰度與梯度統(tǒng)計三組。LBP參數(shù)上我直接用24個采樣點、半徑3uniform模式。這里有個細節(jié)要提醒uniform模式下LBP的取值只有0到P1也就是0到25共26個可能值所以直方圖bins要設(shè)成n_points 2而不是想當然的16或256否則高段位的模式會被直接丟掉。from skimage.feature import local_binary_pattern def lbp_histogram(patch, n_points24, radius3): lbp local_binary_pattern(patch, Pn_points, Rradius, methoduniform) n_bins n_points 2 hist, _ np.histogram(lbp.ravel(), binsn_bins, range(0, n_bins)) hist hist.astype(np.float32) hist / (hist.sum() 1e-6) return histGLCM部分灰度共生矩陣對灰度級數(shù)量很敏感直接拿0到255的原始灰度做矩陣矩陣會非常大而且稀疏統(tǒng)計量反而失穩(wěn)。我的做法是先量化到32個灰度級距離取1個像素方向取0度、45度、90度、135度四個方向然后對每個方向的統(tǒng)計量取均值。from skimage.feature import graycomatrix, graycoprops def glcm_features(patch, levels32): patch_q (patch / 256.0 * levels).astype(np.uint8) glcm graycomatrix(patch_q, distances[1], angles[0, np.pi/4, np.pi/2, 3*np.pi/4], levelslevels, symmetricTrue, normedTrue) props [] for prop in [contrast, dissimilarity, homogeneity, energy, correlation]: vals graycoprops(glcm, prop) props.append(vals.mean()) return np.array(props, dtypenp.float32)最后是一組最樸素的灰度與梯度統(tǒng)計特征?;叶染岛蜆藴什畈蹲秸w明暗Sobel梯度幅值的均值與標準差捕捉邊緣強度。別小看這幾個數(shù)字它們經(jīng)常能在分類器里貢獻意想不到的區(qū)分度。def gray_stats(patch): mean patch.mean() std patch.std() gx cv2.Sobel(patch, cv2.CV_64F, 1, 0, ksize3) gy cv2.Sobel(patch, cv2.CV_64F, 0, 1, ksize3) grad np.sqrt(gx ** 2 gy ** 2) return np.array([mean, std, grad.mean(), grad.std()], dtypenp.float32)三個函數(shù)提取的結(jié)果拼成一行特征def build_feature_matrix(patches): rows [] for p in patches: lbp lbp_histogram(p) glcm glcm_features(p) stats gray_stats(p) row np.concatenate([lbp, glcm, stats]) rows.append(row) return np.vstack(rows) X build_feature_matrix(patches) print(特征矩陣形狀:, X.shape) # 26 5 4 35 維這里你可能會說不是說特征幾千維嗎這個例子我刻意選了較輕量的特征組合35維對于小樣本也足夠了。實戰(zhàn)中如果疊加HOG維度會明顯變大到時候就必須加上PCA。3.4 標準化、降維與分類評估特征矩陣建好后面的處理鏈是標準三步標準化、PCA、分類器。為了展示信息泄漏的坑代碼里特意把scaler和pca只fit在訓(xùn)練集上from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.svm import LinearSVC from sklearn.model_selection import train_test_split # 假設(shè) X 是特征矩陣y 是對應(yīng)的patch標簽 X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42) scaler StandardScaler().fit(X_train) X_train_s scaler.transform(X_train) X_val_s scaler.transform(X_val) # 關(guān)鍵只準transform不準fit pca PCA(n_components0.95).fit(X_train_s) X_train_pca pca.transform(X_train_s) X_val_pca pca.transform(X_val_s) clf LinearSVC(max_iter5000) clf.fit(X_train_pca, y_train) acc clf.score(X_val_pca, y_val) print(f驗證集準確率: {acc:.4f})分類器選了LinearSVC原因是特征維度不高、樣本量有限時線性SVM在小樣本分類上非常穩(wěn)而且訓(xùn)練速度快。如果你的數(shù)據(jù)量更大、特征和類別關(guān)系更復(fù)雜換成隨機森林或梯度提升樹也可以但前處理邏輯完全不變。跑完之后我還會看一眼混淆矩陣特別是劃痕和污漬這兩類是否互相混淆這往往能指引下一步該怎么補特征。4. 圖像特征工程翻車實錄高頻問題與排查清單4.1 切完片后樣本不平衡反而更嚴重切塊后最典型的問題就是正負樣本比例急劇惡化。原始大圖里缺陷面積可能只占1%按512窗口、50%重疊切完含缺陷的patch可能只有幾個剩下全是背景。模型訓(xùn)練出來永遠只預(yù)測“正常”準確率看著還有99%——因為正常類本身就占99%一點意義都沒有。我常用的解法有兩路。一路從數(shù)據(jù)層入手對包含缺陷的patch做擴充旋轉(zhuǎn)、翻轉(zhuǎn)、小幅縮放、調(diào)整對比度都行讓模型在同一個缺陷上看到更多變化同時對純正常的patch做下采樣把比例控制到能接受的范圍。另一路從模型層入手改用帶類別權(quán)重的損失函數(shù)或者簡單一點在SVM或樹模型里把class_weight設(shè)成balanced。實踐經(jīng)驗是兩個手段一起用比單用一個穩(wěn)得多。4.2 目標跨塊被切碎模型怎么都認不出滑動窗口重疊50%之后跨塊問題好了很多但依然存在。劃痕這種細長形態(tài)的目標可能橫跨四個patch每個patch里只有一小段弧線單看哪一塊都像噪聲。我踩過這個坑后的改進方式是訓(xùn)練時只要該缺陷與當前patch有實質(zhì)性交疊就把這個patch標成正樣本讓模型見過“半個缺陷”的樣子推理時用更小的步長再做一次滑動窗口讓同一個缺陷出現(xiàn)在多個窗口里最后把多個窗口的預(yù)測結(jié)果做投票或平均。這相當于把單個patch的判斷升級成“多窗口協(xié)同判斷”誤檢率明顯下降。4.3 特征維度太高導(dǎo)致過擬合手工特征最誘惑人的地方就是可以不斷往上堆顏色直方圖加完加紋理紋理加完加關(guān)鍵點特征最后一個patch上萬維。但維度一旦超過樣本量能支撐的范圍分類器就會開始背樣本而不是學(xué)規(guī)律訓(xùn)練集分數(shù)極高、驗證集崩盤。我的排查思路是分三步。第一步看每個特征列的方差方差接近0的直接刪這種列在樣本間沒有區(qū)分信息。第二步看相關(guān)性把相關(guān)性極高的特征列保留其中之一LBP直方圖的相鄰bin之間有時高度冗余GLCM的dissimilarity和contrast也常常接近線性相關(guān)。第三步PCA壓到90%到95%的方差保留量。如果你用的是樹模型也可以直接看feature_importance砍掉那些重要性長期墊底的特征。4.4 測評指標虛高數(shù)據(jù)泄漏藏在前處理里這個坑最隱蔽也最坑。前面我反復(fù)強調(diào)Scaler和PCA只能在訓(xùn)練集上fit但更隱蔽的是數(shù)據(jù)集劃分層面的泄漏。假如一張大圖切成49個patch隨機按patch劃分訓(xùn)練集和測試集那么同一張大圖的patch很可能同時出現(xiàn)在兩邊。這些patch共享同一光照、同一個背景、甚至同一個缺陷形態(tài)相當于模型在測試時遇到了“訓(xùn)練時見過的相似場景”指標自然漂亮。但如果換一批全新的大圖效果立刻打回原形。我的做法是周一上來就按大圖路徑做分組劃分把每張大圖對應(yīng)的所有patch綁定在一起要么全在訓(xùn)練集要么全在測試集。有時候更嚴格一點按采集批次切分比如前三個批次做訓(xùn)練、后一個批次做驗證模擬“模型上線后見到新環(huán)境”的真實場景。4.5 切圖太慢IO卡死數(shù)據(jù)量大時切圖環(huán)節(jié)很容易被低估。幾千張2048×2048的圖逐張讀取、逐張切塊、再逐張?zhí)崛√卣鲉尉€程跑起來可能要好幾個小時GPU基本在空轉(zhuǎn)。我踩過幾次之后改成離線預(yù)處理模式先把所有大圖一次性切成patch存成npy或者小尺寸文件訓(xùn)練時直接讀已經(jīng)切好、提取好特征的矩陣而不是訓(xùn)練循環(huán)里現(xiàn)切現(xiàn)算。切分和特征提取過程用multiprocessing并行按CPU核心數(shù)切任務(wù)。實測下來同樣的數(shù)據(jù)量離線并行預(yù)處理比邊訓(xùn)練邊處理快一個數(shù)量級。這里再給一個與LBP有關(guān)的冷門提示local_binary_pattern的計算其實不便宜radius和n_points越大越明顯。如果你有幾萬個patch先用小參數(shù)如P8、R1跑通整個流程確認有效再上大參數(shù)不然調(diào)試一次要等半天。上面這些坑有些是我一開始就意識到的有些是上線后被實際效果打臉才回頭補的。做圖像特征工程最核心的體會就是每一步都要問一句“這個操作有沒有讓信息在訓(xùn)練和測試之間偷偷流動”。切塊大小、重疊率、特征組合這三個東西沒有一勞永逸的黃金參數(shù)。我現(xiàn)在的習(xí)慣是固定一套代碼先在小批量數(shù)據(jù)上把整個流程串起來用結(jié)果反推參數(shù)怎么調(diào)目標很小就縮小窗口目標細長就加大重疊率特征區(qū)分度不夠就加一組深度向量再比一把。這個流程跑順之后你手里的數(shù)據(jù)會從一堆原始像素變成干凈、穩(wěn)定、有區(qū)分度的特征矩陣后續(xù)做什么模型都順手得多。