字圖像處理中的表示與描述:從像素到可識(shí)別對(duì)象的完整指南)
1. 從“像素堆”到“可理解對(duì)象”表示與描述到底在解決什么問題很多人學(xué)數(shù)字圖像處理前面幾章做濾波、邊緣檢測、閾值分割跑出來的結(jié)果是一張二值圖或者一堆邊緣點(diǎn)看起來挺像那么回事。但一旦要回答“圖里有幾個(gè)目標(biāo)”“每個(gè)目標(biāo)是什么形狀”“這兩個(gè)目標(biāo)是不是同一個(gè)東西”就卡住了。原因很簡單分割只是把像素分成了“前景”和“背景”但計(jì)算機(jī)并不知道前景里那一坨像素到底代表什么。表示和描述這一章干的就是把“一坨像素”變成“一個(gè)可以被算法比較、匹配、識(shí)別的對(duì)象”這件事。我習(xí)慣把這一章理解成圖像處理流水線里的“翻譯層”。前面幾章是“把圖變干凈”這一章是“把干凈的東西說清楚”。說清楚分兩步走第一步叫表示解決“用什么數(shù)據(jù)結(jié)構(gòu)來存這個(gè)目標(biāo)”第二步叫描述解決“用什么特征向量來刻畫這個(gè)目標(biāo)”。表示是骨架描述是血肉兩者缺一不可。你只做表示不做描述那目標(biāo)就只是一串坐標(biāo)沒法比較你只做描述不做表示那特征沒有依附的載體算出來的數(shù)也不知道對(duì)應(yīng)哪個(gè)區(qū)域。這一章適合誰看如果你正在做缺陷檢測、字符識(shí)別、細(xì)胞計(jì)數(shù)、零件分揀、遙感地塊提取這類任務(wù)那這一章就是你的核心工具箱。如果你只是做圖像增強(qiáng)、去噪、超分那這一章可能暫時(shí)用不上但一旦你的任務(wù)從“改善圖像”變成“理解圖像”表示和描述就是繞不過去的坎。我見過太多項(xiàng)目分割做得漂漂亮亮最后卡在“怎么判斷這個(gè)缺陷是劃痕還是凹坑”上根子就在這一章沒吃透。提示這一章的內(nèi)容和“圖像分割”“形態(tài)學(xué)處理”是強(qiáng)耦合的。如果你的分割結(jié)果本身有毛刺、有孔洞、有粘連后面表示和描述做得再精細(xì)也是白搭。先把分割質(zhì)量穩(wěn)住再來談特征提取。2. 表示方法選型鏈碼、多邊形、骨架到底該用哪個(gè)2.1 鏈碼表示最輕量的邊界編碼方案鏈碼的核心思想特別樸素既然邊界是一串相鄰的像素點(diǎn)那我從某個(gè)起點(diǎn)出發(fā)每一步記錄“往哪個(gè)方向走”就能把整條邊界壓縮成一串方向數(shù)字。四方向鏈碼用0到3表示右、下、左、上八方向鏈碼用0到7表示東、東南、南、西南、西、西北、北、東北。舉個(gè)例子一條從(0,0)走到(2,0)再走到(2,2)的邊界八方向鏈碼就是0,0,2,2四個(gè)數(shù)字搞定比存六個(gè)坐標(biāo)點(diǎn)省事得多。鏈碼最大的好處是平移不變。你把目標(biāo)整體挪十個(gè)像素鏈碼一個(gè)數(shù)字都不變因?yàn)榉较驔]變。但它對(duì)旋轉(zhuǎn)和縮放敏感目標(biāo)轉(zhuǎn)45度鏈碼全亂。所以實(shí)際用的時(shí)候通常要做歸一化旋轉(zhuǎn)歸一化就是找鏈碼的循環(huán)最小值縮放歸一化就是按目標(biāo)周長重新采樣。我實(shí)測下來鏈碼適合做形狀檢索的粗篩速度快、存儲(chǔ)小但精細(xì)匹配還得靠后面的描述子。# 八方向鏈碼的簡單實(shí)現(xiàn)思路 # 方向編碼0東, 1東北, 2北, 3西北, 4西, 5西南, 6南, 7東南 directions [(1,0), (1,1), (0,1), (-1,1), (-1,0), (-1,-1), (0,-1), (1,-1)] def boundary_chain_code(boundary_points): code [] for i in range(len(boundary_points)-1): dx boundary_points[i1][0] - boundary_points[i][0] dy boundary_points[i1][1] - boundary_points[i][1] code.append(directions.index((dx, dy))) return code注意鏈碼的起點(diǎn)選擇會(huì)影響編碼結(jié)果。做匹配之前一定要做起點(diǎn)歸一化否則同一個(gè)形狀因?yàn)槠瘘c(diǎn)不同會(huì)得到完全不同的鏈碼序列。我一般用“鏈碼循環(huán)移位后字典序最小”作為歸一化標(biāo)準(zhǔn)。2.2 多邊形逼近用最少的頂點(diǎn)保住形狀鏈碼是逐像素走的粒度太細(xì)噪聲一多鏈碼就抖得厲害。多邊形逼近的思路是我用少量直線段去擬合邊界只要擬合誤差在容忍范圍內(nèi)就用這些線段的端點(diǎn)來代表整個(gè)邊界。最經(jīng)典的算法是Douglas-Peucker算法思路是遞歸找離當(dāng)前直線最遠(yuǎn)的點(diǎn)如果距離超過閾值就保留這個(gè)點(diǎn)把線段一分為二繼續(xù)遞歸。這個(gè)方法的參數(shù)就一個(gè)逼近精度閾值。閾值設(shè)大了頂點(diǎn)少但形狀失真閾值設(shè)小了頂點(diǎn)多但保留了細(xì)節(jié)。我的經(jīng)驗(yàn)是對(duì)于尺寸在200×200左右的目標(biāo)閾值取2到3個(gè)像素比較穩(wěn)。如果你做的是工業(yè)零件匹配閾值可以再小一點(diǎn)因?yàn)榱慵吘壉旧砭秃芤?guī)整如果你做的是自然場景分割閾值可以放寬反正邊緣本來就不規(guī)則。多邊形逼近的好處是抗噪因?yàn)樵肼曇鸬奈⑿⊥蛊饡?huì)被閾值過濾掉。而且頂點(diǎn)序列可以直接當(dāng)特征用比如計(jì)算相鄰邊的夾角、邊長比這些對(duì)旋轉(zhuǎn)和縮放都有一定的魯棒性。2.3 骨架表示把“粗”目標(biāo)壓成“細(xì)”線條骨架也叫中軸變換思路是我把一個(gè)有一定寬度的區(qū)域反復(fù)腐蝕直到只剩一條單像素寬的線這條線就是骨架。骨架保留了目標(biāo)的拓?fù)浣Y(jié)構(gòu)——有幾個(gè)分支、有沒有環(huán)、端點(diǎn)在哪——但丟掉了寬度信息。所以骨架特別適合做字符識(shí)別和血管提取這類任務(wù)因?yàn)槟汴P(guān)心的是“筆畫怎么走”而不是“筆畫有多粗”。骨架的坑在于毛刺。由于邊界的不規(guī)則腐蝕到最后經(jīng)常會(huì)在骨架末端長出很多小分支這些分支不是真實(shí)結(jié)構(gòu)是噪聲引起的。處理辦法是在骨架化之前先做平滑或者在骨架化之后做剪枝把長度小于某個(gè)閾值的分支砍掉。剪枝閾值一般取目標(biāo)主體長度的5%到10%具體看你的目標(biāo)形態(tài)。表示方法存儲(chǔ)開銷平移不變旋轉(zhuǎn)不變抗噪性適用場景鏈碼低是否弱形狀檢索粗篩多邊形逼近中是部分強(qiáng)工業(yè)零件匹配骨架中是否中字符、血管、筆畫邊界跟蹤點(diǎn)列高是否弱通用邊界分析3. 描述子實(shí)戰(zhàn)從邊界特征到區(qū)域特征的完整拆解3.1 邊界描述子周長、直徑、偏心率怎么算才靠譜邊界描述子是最直觀的一類特征。周長就是邊界像素的個(gè)數(shù)但這里有個(gè)坑八方向鏈碼里水平垂直走一步算1對(duì)角走一步算根號(hào)2如果你直接數(shù)像素個(gè)數(shù)周長會(huì)被高估。正確做法是按鏈碼方向加權(quán)求和。直徑是邊界上任意兩點(diǎn)間的最大距離暴力算法是O(n2)但實(shí)際用的時(shí)候可以先用凸包把邊界點(diǎn)壓縮再在凸包頂點(diǎn)上算能快一個(gè)數(shù)量級(jí)。偏心率描述的是邊界的“細(xì)長程度”通常用邊界擬合橢圓的短軸和長軸之比來算。偏心率接近1說明形狀接近圓接近0說明形狀很扁。這個(gè)特征在細(xì)胞識(shí)別里特別有用因?yàn)檎<?xì)胞和病變細(xì)胞的偏心率分布有明顯差異。我試過在同一個(gè)數(shù)據(jù)集上光靠偏心率一個(gè)特征就能把兩類細(xì)胞分開到85%的準(zhǔn)確率加上周長和面積后能到93%。3.2 區(qū)域描述子面積、緊湊度、不變矩的工程細(xì)節(jié)區(qū)域描述子比邊界描述子更穩(wěn)因?yàn)樗玫氖钦麄€(gè)區(qū)域的像素統(tǒng)計(jì)對(duì)邊界噪聲不敏感。面積就是區(qū)域內(nèi)的像素總數(shù)這個(gè)沒什么好說的但要注意如果你的區(qū)域是用多邊形逼近表示的面積要用鞋帶公式算不能數(shù)像素。緊湊度的定義是周長平方除以面積這個(gè)值對(duì)圓是最小的形狀越復(fù)雜值越大。緊湊度的好處是無量綱不受縮放影響。一個(gè)圓不管放大多少倍緊湊度都是4π。我經(jīng)常用緊湊度做第一道過濾如果緊湊度超過某個(gè)閾值直接判定為“非目標(biāo)”省得后面算復(fù)雜特征。不變矩是這一章的重頭戲。Hu在1962年提出的七個(gè)不變矩對(duì)平移、旋轉(zhuǎn)、縮放都保持不變是形狀匹配的經(jīng)典工具。但實(shí)際用的時(shí)候有幾個(gè)坑第一不變矩的數(shù)值范圍跨度極大直接拿來做距離計(jì)算會(huì)被大數(shù)值主導(dǎo)必須先做對(duì)數(shù)變換第二不變矩對(duì)噪聲敏感尤其是高階矩所以一般只用前四個(gè)第三不變矩描述的是全局形狀對(duì)局部形變不敏感所以做精細(xì)匹配時(shí)還得配合其他特征。import numpy as np import cv2 def hu_moments_features(binary_image): moments cv2.moments(binary_image, binaryImageTrue) hu cv2.HuMoments(moments).flatten() # 對(duì)數(shù)變換壓縮數(shù)值范圍 hu_log -np.sign(hu) * np.log10(np.abs(hu) 1e-30) return hu_log提示Hu不變矩的第七個(gè)矩在有些實(shí)現(xiàn)里符號(hào)不穩(wěn)定做匹配時(shí)建議只用前六個(gè)。另外不變矩是在連續(xù)域推導(dǎo)的離散圖像上只有近似不變性別指望它能做到完全不變。3.3 紋理描述子當(dāng)形狀特征不夠用時(shí)怎么辦有些目標(biāo)形狀差不多但紋理不一樣比如布匹上的不同織法、金屬表面的不同處理工藝。這時(shí)候光靠邊界和區(qū)域描述子分不開得上紋理描述子。最常用的三個(gè)灰度共生矩陣、局部二值模式、Gabor濾波響應(yīng)?;叶裙采仃嚨乃悸肥墙y(tǒng)計(jì)“相隔某個(gè)距離的兩個(gè)像素灰度分別是i和j的概率”從這個(gè)矩陣?yán)锟梢运愠鰧?duì)比度、相關(guān)性、能量、同質(zhì)性四個(gè)指標(biāo)。局部二值模式更簡單對(duì)每個(gè)像素比較它和周圍八個(gè)像素的大小大于等于記1小于記0得到一個(gè)八位二進(jìn)制數(shù)這個(gè)數(shù)的直方圖就是紋理特征。Gabor濾波則是用一組不同方向和頻率的濾波器去卷圖像響應(yīng)強(qiáng)度就是紋理特征。我的經(jīng)驗(yàn)是灰度共生矩陣適合周期性紋理比如織物、網(wǎng)格局部二值模式適合微觀紋理比如粗糙度、顆粒感Gabor適合方向性紋理比如木紋、指紋。選哪個(gè)取決于你的目標(biāo)紋理是什么類型沒有萬能方案。4. 主成分分析降維特征太多反而不好用4.1 為什么需要降維你辛辛苦苦提取了二十個(gè)特征結(jié)果發(fā)現(xiàn)分類器訓(xùn)練不動(dòng)或者訓(xùn)練出來了但在測試集上表現(xiàn)很差。這大概率是維度災(zāi)難特征太多樣本相對(duì)太少分類器在高維空間里找不到可靠的決策邊界。而且很多特征之間是相關(guān)的比如周長和面積高度相關(guān)偏心率和不變矩也有相關(guān)性冗余特征不僅不提供新信息還會(huì)引入噪聲。主成分分析的做法是把原始特征投影到一組新的正交基上讓投影后的第一個(gè)維度方差最大第二個(gè)維度方差次大以此類推。你保留前幾個(gè)主成分就能用很少的維度保住大部分信息。我一般看累計(jì)方差貢獻(xiàn)率達(dá)到90%到95%就截?cái)唷?.2 實(shí)操中的參數(shù)選擇與踩坑記錄主成分分析的參數(shù)就一個(gè)保留幾個(gè)主成分。但這個(gè)參數(shù)不能拍腦袋定。我的做法是畫碎石圖看方差貢獻(xiàn)率的下降曲線拐點(diǎn)在哪就取到哪。另一個(gè)坑是標(biāo)準(zhǔn)化如果你的特征量綱不一樣比如面積是幾千偏心率是零點(diǎn)幾不做標(biāo)準(zhǔn)化的話面積會(huì)主導(dǎo)主成分方向偏心率的信息就被淹沒了。所以做PCA之前一定要做z-score標(biāo)準(zhǔn)化。還有一個(gè)容易被忽略的點(diǎn)PCA是在訓(xùn)練集上擬合的測試集必須用同樣的均值和標(biāo)準(zhǔn)差做變換不能重新擬合。我見過有人把訓(xùn)練集和測試集拼在一起做PCA然后說效果特別好那是數(shù)據(jù)泄露實(shí)際部署的時(shí)候性能會(huì)崩。特征數(shù)量樣本數(shù)量建議保留主成分?jǐn)?shù)累計(jì)方差貢獻(xiàn)率102004-690%205006-1092%50100010-1595%5. 關(guān)系描述當(dāng)目標(biāo)不止一個(gè)時(shí)怎么組織信息5.1 字符串描述與樹結(jié)構(gòu)描述前面講的都是單個(gè)目標(biāo)的表示和描述。但實(shí)際場景里一張圖里往往有多個(gè)目標(biāo)而且目標(biāo)之間有空間關(guān)系。比如“A在B的左邊”“C在D的上面”“E被F包圍”。這些關(guān)系怎么編碼最常用的兩種結(jié)構(gòu)是字符串和樹。字符串描述適合線性排列的目標(biāo)比如一行文字、一列零件。你把每個(gè)目標(biāo)的特征按順序排成一串就得到了字符串描述。匹配的時(shí)候用字符串編輯距離能容忍目標(biāo)缺失和順序微調(diào)。樹結(jié)構(gòu)適合層次關(guān)系比如一個(gè)電路板上有多個(gè)芯片每個(gè)芯片有多個(gè)引腳引腳和芯片是父子關(guān)系芯片和電路板也是父子關(guān)系。樹匹配用子樹同構(gòu)算法能處理嵌套結(jié)構(gòu)。5.2 關(guān)系描述的適用邊界與性能權(quán)衡關(guān)系描述的好處是表達(dá)能力強(qiáng)能編碼復(fù)雜的空間約束。但代價(jià)是匹配復(fù)雜度高。字符串匹配是O(n2)樹匹配是指數(shù)級(jí)的目標(biāo)一多就跑不動(dòng)。所以實(shí)際用的時(shí)候我一般先用單目標(biāo)特征做粗篩把候選目標(biāo)壓到幾個(gè)再用關(guān)系描述做精匹配。這樣既保住了表達(dá)能力又控制了計(jì)算量。另一個(gè)坑是關(guān)系定義的模糊性?!白筮叀钡降资侵改繕?biāo)中心點(diǎn)的左邊還是目標(biāo)最右像素的左邊不同定義會(huì)導(dǎo)致完全不同的匹配結(jié)果。我的做法是在項(xiàng)目文檔里把每個(gè)關(guān)系的定義寫死比如“A的中心點(diǎn)x坐標(biāo)小于B的中心點(diǎn)x坐標(biāo)則稱A在B左邊”避免歧義。6. 常見問題與排查技巧實(shí)錄6.1 特征提取結(jié)果不穩(wěn)定怎么辦這是最常見的問題同一類目標(biāo)不同樣本提取出來的特征值波動(dòng)很大。排查思路分三步走。第一步檢查分割質(zhì)量把二值圖疊加到原圖上看邊界是不是貼合真實(shí)邊緣。如果分割本身就不穩(wěn)后面特征肯定不穩(wěn)。第二步檢查特征計(jì)算實(shí)現(xiàn)特別是周長和面積這種基礎(chǔ)特征用已知形狀比如一個(gè)標(biāo)準(zhǔn)圓驗(yàn)證一下數(shù)值對(duì)不對(duì)。第三步檢查歸一化如果你的目標(biāo)尺寸差異很大不做尺度歸一化的話面積特征會(huì)直接反映尺寸而不是形狀。6.2 不變矩?cái)?shù)值溢出或精度丟失Hu不變矩的高階矩在計(jì)算過程中會(huì)出現(xiàn)很大的中間值float64都可能溢出。解決辦法有兩個(gè)一是用對(duì)數(shù)域計(jì)算把乘法變成加法二是先對(duì)圖像做歸一化把像素坐標(biāo)縮放到0到1之間。另外不變矩的第七個(gè)矩在離散情況下符號(hào)不穩(wěn)定做匹配時(shí)建議只用前六個(gè)。6.3 主成分分析后分類效果反而變差這通常是因?yàn)楸A舻闹鞒煞痔侔延信袆e力的信息丟掉了。PCA是無監(jiān)督的它保留的是方差最大的方向但方差大不等于判別力強(qiáng)。如果兩類目標(biāo)的差異正好在方差小的方向上PCA會(huì)把這個(gè)方向丟掉。解決辦法是改用線性判別分析它是有監(jiān)督的會(huì)主動(dòng)找類間差異最大的方向?;蛘吒纱嗖唤稻S改用帶正則化的分類器。問題現(xiàn)象可能原因排查方法解決方案特征值波動(dòng)大分割不穩(wěn)疊加二值圖看邊界改進(jìn)分割算法不變矩溢出高階矩?cái)?shù)值大打印中間值對(duì)數(shù)域計(jì)算PCA后效果差丟失判別信息對(duì)比降維前后改用LDA匹配速度慢特征維度高統(tǒng)計(jì)計(jì)算耗時(shí)先粗篩再精匹配旋轉(zhuǎn)后匹配失敗特征不具旋轉(zhuǎn)不變性旋轉(zhuǎn)測試用不變矩或歸一化提示做形狀匹配時(shí)我習(xí)慣先用緊湊度和偏心率做第一道過濾這兩個(gè)特征計(jì)算快、魯棒性強(qiáng)能過濾掉大部分明顯不匹配的候選。剩下的再用不變矩做精細(xì)匹配這樣整體速度能提升三到五倍。7. 一個(gè)完整的形狀匹配流程從分割到識(shí)別7.1 流程設(shè)計(jì)與參數(shù)配置假設(shè)你要做一個(gè)零件分揀系統(tǒng)傳送帶上隨機(jī)擺放幾種零件相機(jī)拍圖后要判斷每個(gè)零件屬于哪一類。完整流程是這樣的第一步灰度化加高斯濾波去掉傳感器噪聲第二步Otsu閾值分割把零件從背景里分出來第三步形態(tài)學(xué)開閉運(yùn)算去掉毛刺和填平小孔第四步連通域分析給每個(gè)零件分配獨(dú)立標(biāo)簽第五步對(duì)每個(gè)連通域提取邊界計(jì)算鏈碼和多邊形逼近第六步計(jì)算區(qū)域特征包括面積、緊湊度、偏心率、前六個(gè)Hu不變矩第七步特征標(biāo)準(zhǔn)化后做PCA降維到五維第八步用最近鄰分類器匹配到已知類別。參數(shù)配置上高斯核取5×5sigma取1.5Otsu分割后做一次3×3的開運(yùn)算多邊形逼近閾值取2.5像素PCA保留累計(jì)方差95%的主成分。這套參數(shù)在我做過的幾個(gè)零件分揀項(xiàng)目里都跑得比較穩(wěn)但具體數(shù)值還得根據(jù)你的圖像分辨率和零件尺寸微調(diào)。7.2 效果驗(yàn)證與迭代優(yōu)化驗(yàn)證的時(shí)候不能只看準(zhǔn)確率還要看混淆矩陣。如果某兩類零件經(jīng)?;ハ嗾`判說明它們的特征空間有重疊需要加新特征或者換分類器。我遇到過一個(gè)案例兩種螺絲外形幾乎一樣只有螺紋間距不同形狀特征完全分不開最后加了紋理特征才解決。所以特征提取不是一勞永逸的得根據(jù)混淆矩陣反饋來迭代。迭代的方向有三個(gè)一是加特征比如加紋理、加顏色二是換分類器從最近鄰換成支持向量機(jī)或者隨機(jī)森林三是改分割有時(shí)候誤判的根源在分割階段兩個(gè)零件粘在一起被當(dāng)成一個(gè)目標(biāo)了。我的經(jīng)驗(yàn)是先排查分割再排查特征最后才動(dòng)分類器因?yàn)榍皟烧叩挠绊懜蟆?. 工程落地中的性能優(yōu)化與經(jīng)驗(yàn)沉淀8.1 計(jì)算加速的幾種實(shí)用手段鏈碼和多邊形逼近的計(jì)算量都不大真正耗時(shí)的是不變矩和PCA。不變矩的加速可以用積分圖把矩的計(jì)算從O(n2)降到O(1)。PCA的加速可以用隨機(jī)化SVD在樣本量大的時(shí)候比標(biāo)準(zhǔn)SVD快很多。另外如果你的目標(biāo)是實(shí)時(shí)處理建議把特征提取寫成C擴(kuò)展或者用CUDA加速Python循環(huán)算矩確實(shí)慢。還有一個(gè)容易被忽略的點(diǎn)邊界跟蹤的起點(diǎn)選擇。如果每次跟蹤都從最左上角的邊界點(diǎn)開始鏈碼的起點(diǎn)就是固定的省去了起點(diǎn)歸一化的步驟。這個(gè)技巧在批量處理時(shí)能省不少時(shí)間。8.2 從項(xiàng)目里攢下來的幾條硬經(jīng)驗(yàn)第一條特征不是越多越好。我見過有人提取了五十個(gè)特征結(jié)果分類器過擬合嚴(yán)重測試集準(zhǔn)確率還不如只用五個(gè)特征。特征選擇比特征提取更重要。第二條歸一化要統(tǒng)一。訓(xùn)練集和測試集必須用同一套歸一化參數(shù)這個(gè)參數(shù)要在訓(xùn)練集上算然后存下來給測試集用。我習(xí)慣把歸一化參數(shù)和分類器模型一起序列化保存部署的時(shí)候直接加載。第三條可視化調(diào)試。把提取到的邊界、多邊形、骨架畫到原圖上肉眼看一下對(duì)不對(duì)。很多bug肉眼一看就發(fā)現(xiàn)了比看代碼快得多。第四條留出驗(yàn)證集。不要用測試集調(diào)參數(shù)否則你調(diào)出來的參數(shù)只對(duì)測試集好換個(gè)數(shù)據(jù)集就崩。我一般按6:2:2劃分訓(xùn)練、驗(yàn)證、測試驗(yàn)證集用來調(diào)參測試集只在最后跑一次。這個(gè)內(nèi)容后續(xù)還可以這樣擴(kuò)展把表示和描述跟深度學(xué)習(xí)結(jié)合起來用卷積網(wǎng)絡(luò)自動(dòng)學(xué)特征替代手工設(shè)計(jì)的描述子。但手工特征在樣本少、可解釋性要求高的場景下依然有優(yōu)勢(shì)兩者不是替代關(guān)系是互補(bǔ)關(guān)系。