實(shí)戰(zhàn):暗場(chǎng)成像、Matlab處理與誤檢抑制)
玻璃瓶裂紋檢測(cè)這件事第一次上手的人幾乎都會(huì)在同一個(gè)地方卡住把相機(jī)對(duì)準(zhǔn)瓶子屏幕上看到的是一片慘白的反光裂紋在哪完全靠猜。我最初也以為是自己光源沒選對(duì)換了兩三種燈還是不理想后來才想明白——透明瓶體的裂紋檢測(cè)難的不是算法本身而是怎么讓裂紋在圖像里“顯形”以及怎么把它從合縫線、水痕、標(biāo)簽邊緣這一堆長得差不多的干擾里摘出來。這篇就圍繞機(jī)器視覺做玻璃瓶裂紋檢測(cè)這條主線把成像、Matlab 處理鏈路、特征判定、節(jié)拍優(yōu)化和源碼工程結(jié)構(gòu)完整拆一遍附的這套 Matlab 源碼可以直接拿來改參數(shù)跑自己的樣本。內(nèi)容偏工程實(shí)戰(zhàn)適合已經(jīng)會(huì)一點(diǎn) Matlab 圖像處理、但真正落到透明件檢測(cè)上就心里沒底的朋友也適合剛接觸視覺檢測(cè)、想搞明白“為什么這么設(shè)計(jì)”的入門讀者。1. 透明瓶體上的裂紋為什么“看不清”成像難點(diǎn)與誤檢來源拆解1.1 裂紋的物理特征決定了它不是普通的“邊緣”很多人做缺陷檢測(cè)的第一反應(yīng)是上 Canny 邊緣檢測(cè)因?yàn)榻坛汤锒际沁@么寫的。放到玻璃瓶上這個(gè)思路會(huì)立刻翻車。原因在于玻璃的成像特性和金屬、塑料完全不同可見光穿過玻璃時(shí)大部分直接透射只有遇到裂紋這種斷層結(jié)構(gòu)才會(huì)發(fā)生散射。也就是說在普通明場(chǎng)照明下裂紋不僅不暗反而可能比瓶體還亮一點(diǎn)點(diǎn)同時(shí)又和瓶身弧度帶來的高光混在一起邊緣檢測(cè)算子根本分不清哪條邊是裂紋、哪條邊是瓶壁輪廓。從物理上看裂紋有兩個(gè)可以利用的性質(zhì)。第一是尺度小工業(yè)上判定需要攔截的裂紋寬度通常在幾十微米量級(jí)長度從零點(diǎn)幾毫米到幾毫米不等在圖像上表現(xiàn)為一條細(xì)長的、寬度只有兩三個(gè)像素的線狀結(jié)構(gòu)。第二是方向隨機(jī)、邊界不規(guī)則裂紋沿著應(yīng)力方向擴(kuò)展走的是鋸齒狀路徑不像模具合縫線那樣筆直。這兩點(diǎn)決定了算法路線應(yīng)該往“提取細(xì)小線狀亮/暗結(jié)構(gòu)”上走而不是往“找物體輪廓”上走。我一般把這個(gè)問題拆成三層來看成像層要保證裂紋與背景有足夠的灰度差處理層要能把這種差放大成可分割的二值區(qū)域判定層要能用幾何特征把真裂紋和仿冒者區(qū)分開。三層里任何一層沒做好后面的努力都是白費(fèi)而大多數(shù)調(diào)參失敗其實(shí)卡在第一層——圖里根本沒有足夠的信噪比算法再花哨也救不回來。1.2 四類最常見的誤檢源真正跑起來之后你會(huì)發(fā)現(xiàn)漏檢反而不是最頭疼的誤檢業(yè)內(nèi)叫“過殺”才是。我在實(shí)際調(diào)試?yán)镉龅阶疃嗟氖窍旅孢@幾類幾乎每一種都值得單獨(dú)做一條規(guī)則去壓制。干擾類型圖像表現(xiàn)與真裂紋的差異常用壓制手段模具合縫線筆直的縱向亮線位置固定位置在瓶身圓周上固定角度方向幾乎不偏位置掩膜屏蔽或旋轉(zhuǎn)瓶體后多幀比對(duì)表面劃痕細(xì)長亮線方向隨機(jī)寬度略大、邊緣更平滑、對(duì)比度偏弱提高頂帽響應(yīng)閾值 長寬比組合判定水痕/油污不規(guī)則塊狀或弧形亮區(qū)面積大、長寬比接近 1、邊界圓潤面積上限 占空比Extent過濾標(biāo)簽與印刷邊緣強(qiáng)對(duì)比的邊緣線灰度跳變劇烈、位于固定區(qū)域ROI 裁剪檢測(cè)區(qū)限定在瓶肩/瓶底光滑段合縫線是最麻煩的一個(gè)。它天然滿足“細(xì)長、方向一致、對(duì)比度強(qiáng)”這三個(gè)條件用幾何特征幾乎打不掉。后來我采用的思路是合縫線在每個(gè)瓶上的角位置是固定的只要在標(biāo)定時(shí)把它所在的列范圍記錄下來做一張掩膜圖檢測(cè)前先把它“涂掉”就行。前提是你的瓶子在夾具里的定位重復(fù)精度夠好一般用機(jī)械定位加一個(gè)氣缸頂緊就能做到正負(fù)零點(diǎn)幾毫米。如果定位精度不夠就得靠瓶體旋轉(zhuǎn)配合多工位圖像做差分了那是另一套更重的方案。提示不建議用“面積小于多少就忽略”這種單一條件去過濾。裂紋本身就很小一刀切容易把真缺陷一起砍掉。多條弱條件組合打分的誤檢抑制效果通常好于一條強(qiáng)條件。2. 光源與相機(jī)選型把裂紋“打”到算法能吃的位置2.1 暗場(chǎng)照明為什么是首選如果只能記住一句話那就是裂紋檢測(cè)優(yōu)先考慮暗場(chǎng)照明。原理不復(fù)雜用低角度環(huán)形光或者從側(cè)面打光讓絕大部分光線以掠射角掠過瓶壁而不進(jìn)入鏡頭相機(jī)視野里背景是暗的一旦表面有裂紋這種微觀起伏光線在那里發(fā)生散射一部分散射光進(jìn)入鏡頭裂紋就以亮線的形式跳出來。這種“暗背景亮缺陷”的模式對(duì)比度比明場(chǎng)高出好幾倍圖像信噪比一好后面 Matlaab 里的閾值分割就不用那么極限。實(shí)際配置上我一般會(huì)用低角度環(huán)形光配一個(gè)漫射罩把直射的雜光打散避免瓶身弧面產(chǎn)生大塊高光。對(duì)于需要同時(shí)看清楚瓶底和瓶肩的場(chǎng)景可以用分區(qū)布光加多相機(jī)每個(gè)相機(jī)只管一個(gè)視場(chǎng)。另外偏振片值得一試在鏡頭前和光源前各加一片交叉偏振可以壓掉一部分表面反射對(duì)某些鍍膜瓶或者帶霧面的瓶型效果明顯。有一點(diǎn)經(jīng)常被忽略——瓶體是曲面的。同一個(gè)視場(chǎng)里中心區(qū)域和邊緣區(qū)域的入射角差異很大導(dǎo)致同一條裂紋在圖像中心的響應(yīng)可能只有邊緣的三分之一。解決辦法要么縮短工作距離、縮小視野要么在預(yù)處理階段做一次照度校正用一張標(biāo)準(zhǔn)白板圖做背景歸一化。這個(gè)步驟在源碼里對(duì)應(yīng)的是“背景模板減除”成本很低收益很高。2.2 分辨率與節(jié)拍的反向推算選相機(jī)不能憑感覺得從“最小待檢缺陷”倒推。舉個(gè)例子假定要檢出的裂紋寬度是 0.08 mm檢測(cè)視野寬度取 80 mm涵蓋瓶身主要檢測(cè)面那么需要的橫向像素?cái)?shù)至少是 80 / 0.08 1000但工程上通常要求一個(gè)特征至少占據(jù) 2 到 3 個(gè)像素才穩(wěn)定可分割所以實(shí)際需要 2000 到 3000 像素。這就落到 500 萬像素級(jí)約 2448×2048的相機(jī)上此時(shí)像素當(dāng)量約為 0.033 mm/pixel一條 0.08 mm 寬的裂紋在圖上約 2.4 個(gè)像素勉強(qiáng)夠用但不算富余如果再考慮鏡頭畸變和亞像素誤差建議把視野再收緊或者上更高分辨率的機(jī)型。節(jié)拍是另一半約束。假設(shè)某條產(chǎn)線每分鐘過 500 個(gè)瓶子也就是每 100 ms 一個(gè)這 100 ms 里要完成觸發(fā)、曝光、傳輸、處理、判定、輸出信號(hào)。曝光通常 2 到 5 ms千兆網(wǎng)傳一張 500 萬像素的 8 位灰度圖大概 20 到 30 ms留給 Matlab 算法的就只剩 50 ms 左右。這個(gè)預(yù)算相當(dāng)緊直接決定了后面代碼不能寫得太“教科書”。把這兩個(gè)約束放在一起看會(huì)發(fā)現(xiàn)很多選型其實(shí)是互相牽制的想提高分辨率就得犧牲幀率或者換更貴的接口想縮短處理時(shí)間就得限制 ROI 面積。我一般的做法是先按缺陷尺寸定分辨率再用 ROI 裁剪把有效處理面積壓到整幅圖的 30% 以內(nèi)這樣即使算法單幀跑 40 ms 也還有余量。源碼里的cfg.roi就是干這個(gè)用的別嫌它簡單產(chǎn)線上這一步能省掉一半時(shí)間。3. Matlab 圖像處理鏈路的逐環(huán)節(jié)拆解3.1 預(yù)處理頂帽/黑帽比直接 Canny 更可靠拿到灰度圖之后第一步不是邊緣檢測(cè)而是把小尺度結(jié)構(gòu)從大尺度背景里分離出來。形態(tài)學(xué)頂帽變換imtophat正好干這件事用原圖減去開運(yùn)算結(jié)果保留比結(jié)構(gòu)元小的亮細(xì)節(jié)把大面積的亮度漸變?nèi)磕ǖ簟A鸭y在暗場(chǎng)圖里是亮線用白頂帽就很合適如果某些瓶型上裂紋表現(xiàn)為暗線就換成黑帽imbothat邏輯對(duì)稱。結(jié)構(gòu)元的尺寸是有講究的不能瞎填。它必須比裂紋寬度大、比你要抑制的背景結(jié)構(gòu)小。裂紋寬度 2 到 3 個(gè)像素那結(jié)構(gòu)元半徑取 5 到 8 個(gè)像素比較合適取太小了細(xì)節(jié)提不出來取太大了會(huì)把瓶壁的亮度起伏一起當(dāng)成細(xì)節(jié)保留。我在源碼里把它做成cfg.topHatRadius參數(shù)默認(rèn) 6調(diào)試時(shí)按瓶型微調(diào)。頂帽之后跟著高斯濾波作用是壓掉傳感器噪聲和表面的細(xì)微麻點(diǎn)。這里有個(gè)小技巧imgaussfilt的 sigma 不要超過 1.5因?yàn)槟銊倓傎M(fèi)力提取出來的就是寬度兩三個(gè)像素的細(xì)線濾波太狠會(huì)把它糊掉。我吃過這個(gè)虧一開始用 sigma3 覺得圖看起來干凈多了結(jié)果裂紋的峰值響應(yīng)直接掉了一半閾值怎么調(diào)都檢不全。增強(qiáng)之后才是二值化。全局 Otsugraythresh在光照均勻時(shí)夠用但對(duì)曲面瓶體往往不穩(wěn)定因?yàn)椴煌瑓^(qū)域的背景亮度差異大。更穩(wěn)的做法有兩種一是用固定閾值前提是暗場(chǎng)照明很穩(wěn)定、相機(jī)增益鎖死我傾向于這種因?yàn)榭深A(yù)測(cè)二是用adaptthresh做局部自適應(yīng)閾值魯棒性好一點(diǎn)但計(jì)算量上去了。源碼默認(rèn)走固定閾值乘以一個(gè)縮放系數(shù)cfg.threshScale方便你在不同批次樣本上快速找平衡點(diǎn)。3.2 二值與形態(tài)學(xué)讓斷裂的裂紋連成一條閾值分割出來的二值圖通常不理想——裂紋會(huì)因?yàn)榫植糠垂獠痪鴶喑珊脦捉赝瑫r(shí)背景里冒出一堆零散噪點(diǎn)。這時(shí)候形態(tài)學(xué)的作用就體現(xiàn)出來了但順序很關(guān)鍵先開運(yùn)算去噪點(diǎn)再閉運(yùn)算連斷線最后做小面積剔除。開運(yùn)算imerode后imdilate或用bwareaopen等效實(shí)現(xiàn)用半徑 1 的圓盤結(jié)構(gòu)元專門清掉孤立的一兩個(gè)像素噪點(diǎn)。閉運(yùn)算用稍大一點(diǎn)的結(jié)構(gòu)元半徑 2 到 3把裂紋斷口之間的縫隙填上讓一整個(gè)缺陷變成同一個(gè)連通域避免后面統(tǒng)計(jì)時(shí)把一條裂紋拆成三四個(gè)小目標(biāo)每個(gè)都不夠閾值而被誤判為噪聲。這個(gè)順序如果反了先閉運(yùn)算會(huì)把噪點(diǎn)也連成塊后面反而更難處理。面積剔除用bwareaopen但這個(gè)閾值得小心設(shè)。一條 1 mm 長、0.08 mm 寬的裂紋在 0.033 mm/pixel 的當(dāng)量下面積大約是 (1/0.033)×(0.08/0.033) ≈ 30×2.4 ≈ 72 個(gè)像素。所以最小面積閾值設(shè)在 20 到 30 比較合適設(shè)到 100 就會(huì)開始漏檢短裂紋。我在實(shí)際項(xiàng)目里會(huì)把這個(gè)值設(shè)得偏低一點(diǎn)比如 15把“是不是真裂紋”的判斷完全交給后面的特征篩選而不是在這一步提前砍掉。還有一個(gè)容易被忽視的細(xì)節(jié)瓶子邊緣的輪廓線也會(huì)被頂帽提取出來。瓶壁與背景的交界處是強(qiáng)梯度形態(tài)學(xué)處理后會(huì)形成一條沿輪廓的長條亮區(qū)面積大、形狀細(xì)長非常容易被當(dāng)成裂紋。解決辦法是在處理前用一張“有效檢測(cè)區(qū)域掩膜”把瓶體輪廓附近若干像素排除掉只保留瓶壁中段的平滑區(qū)域。掩膜可以按瓶型固定生成也可以在每幀里用大津法分割瓶體輪廓后往里收縮 5 到 10 個(gè)像素得到。源碼里用的是固定掩膜加一次形態(tài)學(xué)收縮簡單夠用。4. 特征篩選把合縫線、水痕、劃痕從候選里摘出去4.1 連通域幾何特征表走到這一步二值圖里剩下的連通域就是“候選缺陷”。接下來要做的就是用regionprops把這堆候選的幾何屬性全算出來再用一組規(guī)則打分。單靠一個(gè)特征永遠(yuǎn)不夠必須組合。下面這張表是我在實(shí)際項(xiàng)目里反復(fù)調(diào)整后沉淀下來的判定邏輯參數(shù)值僅供參考不同瓶型肯定要重調(diào)。特征計(jì)算方式真裂紋的典型范圍主要攔截對(duì)象面積 Arearegionprops直接給30 ~ 2000 像素噪點(diǎn)過小、大片污漬過大長寬比長軸/短軸大于 4氣泡、結(jié)石接近 1占空比 Extent面積/外接框面積0.15 ~ 0.6塊狀水痕接近 1偏心率 Eccentricityregionprops直接給大于 0.95圓形/橢圓形干擾平均頂帽響應(yīng)候選區(qū)域內(nèi)頂帽圖均值高于全局均值 1.8 倍弱對(duì)比度的料紋、淺劃痕位置質(zhì)心坐標(biāo)不在合縫線掩膜內(nèi)合縫線打分方式我一般用“全部滿足才判為裂紋”的硬規(guī)則而不是加權(quán)求和。原因是工業(yè)檢測(cè)里漏檢的代價(jià)遠(yuǎn)高于過殺硬規(guī)則更容易追溯——出現(xiàn)誤判時(shí)能明確說是哪一條沒過、哪一條過了調(diào)起來有方向。加權(quán)打分雖然靈活但一旦出問題就是黑盒排查成本很高。4.2 合縫線的特殊處理前面提到合縫線靠幾何特征打不掉這里詳細(xì)說下我用的方法。思路是在標(biāo)定階段采集若干張良品圖統(tǒng)計(jì)出每一列或者某一個(gè)角度扇區(qū)內(nèi)的頂帽響應(yīng)均值找出響應(yīng)持續(xù)偏高的那一段列范圍把它標(biāo)記為“永久屏蔽區(qū)”。檢測(cè)時(shí)如果某個(gè)候選的質(zhì)心落在這個(gè)范圍內(nèi)直接丟棄。這個(gè)方法的有效性完全依賴于瓶子在夾具中的角向定位精度。如果產(chǎn)線用的是星輪加定位塊的機(jī)械結(jié)構(gòu)重復(fù)精度通常沒問題如果瓶子是自由落在輸送帶上的角位置會(huì)漂那就得換方案——比如在檢測(cè)工位前加一個(gè)旋轉(zhuǎn)機(jī)構(gòu)讓瓶子轉(zhuǎn)到一個(gè)固定角度再觸發(fā)拍照或者干脆用兩個(gè)相機(jī)從不同角度拍取“兩個(gè)視角都認(rèn)為是裂紋”的結(jié)果作為最終判定合縫線因?yàn)橹辉诠潭ń嵌瘸霈F(xiàn)自然就被排除了。注意屏蔽區(qū)的寬度不要設(shè)得太寬。有些裂紋恰好出現(xiàn)在合縫線附近屏蔽區(qū)一旦覆蓋過大就會(huì)造成系統(tǒng)性漏檢。我的經(jīng)驗(yàn)是把屏蔽區(qū)寬度控制在合縫線實(shí)際像素寬度的 1.5 倍以內(nèi)寧可留一點(diǎn)過殺。除了位置屏蔽還有一個(gè)輔助判據(jù)值得加合縫線的亮度沿縱向非常均勻而裂紋的亮度分布是起伏的中間亮兩頭暗。計(jì)算候選區(qū)域內(nèi)頂帽響應(yīng)的標(biāo)準(zhǔn)差裂紋通常明顯高于合縫線。這個(gè)特征加進(jìn)去之后合縫線的誤判率能再降一個(gè)檔次。5. 從跑通 Demo 到上產(chǎn)線耗時(shí)、標(biāo)定、打包5.1 耗時(shí)優(yōu)化先測(cè)量再動(dòng)手Matlab 寫圖像處理最大的坑不是寫不出來而是寫出來太慢。我的習(xí)慣是先加計(jì)時(shí)打點(diǎn)把整個(gè)流程拆成讀圖、預(yù)處理、二值化、形態(tài)學(xué)、連通域分析、判定六段看看到底哪一段吃時(shí)間。實(shí)測(cè)下來500 萬像素的圖耗時(shí)排名通常是讀圖和格式轉(zhuǎn)換 連通域分析 形態(tài)學(xué) 頂帽 閾值。這個(gè)排序和很多人的直覺不一樣但確實(shí)如此。針對(duì)性優(yōu)化手段有這幾個(gè)。第一ROI 裁剪要趁早在讀圖之后立刻裁不要等做完預(yù)處理再裁imread加imcrop的順序能省下大量內(nèi)存拷貝。第二避免在循環(huán)里調(diào)用imshow產(chǎn)線模式必須關(guān)掉所有可視化只在調(diào)試模式下開。第三regionprops盡量少要屬性特別是不需要PixelIdxList的時(shí)候就別要它返回的是元胞數(shù)組每個(gè)連通域一份像素索引數(shù)量一多就是災(zāi)難。第四能用向量化就別寫 for 循環(huán)Matlab 的循環(huán)雖然在改進(jìn)但圖像處理里逐像素循環(huán)仍然是性能殺手。如果做完這些還是不夠快可以考慮把核心算法用 Matlab Coder 生成 C 代碼或者把整個(gè)流程用 MATLAB Compiler 打包成獨(dú)立可執(zhí)行程序在目標(biāo)機(jī)上用運(yùn)行時(shí)跑。打包之后的啟動(dòng)開銷比在 IDE 里跑小很多這一點(diǎn)在產(chǎn)線環(huán)境里很關(guān)鍵——工控機(jī)開機(jī)就要能用不能依賴打開一個(gè)龐大的開發(fā)環(huán)境。5.2 標(biāo)定與驗(yàn)證漏檢率和過殺率要分開看算法調(diào)完之后必須做量化評(píng)估不能靠“我看著還行”。評(píng)估至少要準(zhǔn)備兩類樣本正樣本含真實(shí)裂紋最好覆蓋不同位置、不同長度、不同走向和負(fù)樣本良品但包含各種干擾。樣本量太小沒有意義我一般要求正樣本不少于 100 個(gè)缺陷實(shí)例負(fù)樣本不少于 300 個(gè)。評(píng)估指標(biāo)上漏檢率和過殺率一定要分開統(tǒng)計(jì)。漏檢率是缺陷被放過的比例這個(gè)在工業(yè)場(chǎng)景里通常是硬指標(biāo)要求接近零過殺率是良品被誤判的比例一般允許 3% 到 8%但太高會(huì)影響產(chǎn)線效率。兩者是一對(duì)矛盾把閾值調(diào)低能降漏檢但升過殺調(diào)高則相反。源碼里附了一個(gè)批量評(píng)估腳本跑完會(huì)輸出混淆矩陣和每個(gè)樣本的判定結(jié)果方便你定位到底是哪一類樣本被誤判了。還有一個(gè)很少被提及但很重要的點(diǎn)驗(yàn)證集不能和調(diào)參集重合。我見過有人拿同一批圖反復(fù)調(diào)閾值調(diào)到完美換一批新樣本立刻崩盤。正確做法是把樣本分成調(diào)參集和驗(yàn)證集調(diào)參只在調(diào)參集上做最終指標(biāo)只在驗(yàn)證集上跑。這個(gè)習(xí)慣能幫你避免大量的自我欺騙。6. 一套可以直接復(fù)現(xiàn)的 Matlab 代碼骨架與 GUI 入口6.1 主檢測(cè)函數(shù)下面這段是核心檢測(cè)函數(shù)參數(shù)全部集中在一個(gè)結(jié)構(gòu)體里方便批量調(diào)參。它把前面講的“頂帽增強(qiáng)→濾波→二值化→形態(tài)學(xué)→連通域→特征打分”整條鏈路串了起來返回判定結(jié)果和標(biāo)注圖。function out crackDetect(imgPath, cfg) % crackDetect 玻璃瓶裂紋檢測(cè)主函數(shù) % 輸入: imgPath 圖像路徑; cfg 參數(shù)結(jié)構(gòu)體 % 輸出: out.isCrack 是否含裂紋; out.marked 標(biāo)注圖; out.stats 特征表 % ---------- 1. 讀圖與 ROI ---------- I imread(imgPath); if size(I, 3) 3 I rgb2gray(I); end I im2double(I); if ~isempty(cfg.roi) I imcrop(I, cfg.roi); end % ---------- 2. 照度校正(可選) ---------- if ~isempty(cfg.bgTemplate) bg cfg.bgTemplate; I imdivide(I, bg 1e-6); I mat2gray(I); end % ---------- 3. 頂帽增強(qiáng) ---------- se strel(disk, cfg.topHatRadius); I_top imtophat(I, se); % 亮細(xì)節(jié)(暗場(chǎng)下的裂紋) I_top imgaussfilt(I_top, cfg.sigma); % ---------- 4. 二值化 ---------- if cfg.useOtsu lv graythresh(I_top); else lv cfg.fixedThresh; end BW imbinarize(I_top, lv * cfg.threshScale); % ---------- 5. 形態(tài)學(xué) ---------- BW bwareaopen(BW, cfg.minArea); % 去小噪點(diǎn) BW imclose(BW, strel(disk, cfg.closeRadius)); % 連接斷口 BW imopen(BW, strel(disk, 1)); % 屏蔽區(qū)(合縫線、瓶體輪廓)置零 if ~isempty(cfg.mask) BW BW cfg.mask; end % ---------- 6. 連通域特征 ---------- st regionprops(BW, I_top, ... Area, BoundingBox, MajorAxisLength, MinorAxisLength, ... Eccentricity, Extent, Centroid, MeanIntensity); n numel(st); keep false(n, 1); rec zeros(n, 6); for k 1:n elong st(k).MajorAxisLength / max(st(k).MinorAxisLength, eps); rec(k, :) [st(k).Area, elong, st(k).Extent, ... st(k).Eccentricity, st(k).MeanIntensity, st(k).Centroid(1)]; keep(k) st(k).Area cfg.minArea ... st(k).Area cfg.maxArea ... elong cfg.minElong ... st(k).Extent cfg.maxExtent ... st(k).Eccentricity cfg.minEcc ... st(k).MeanIntensity cfg.minResponse; end % ---------- 7. 輸出 ---------- out.isCrack any(keep); out.marked insertShape(im2uint8(I), Rectangle, ... vertcat(st(keep).BoundingBox), ... Color, red, LineWidth, 2); out.stats rec; out.keepIdx find(keep); end這段代碼里有幾個(gè)地方是刻意這么寫的。imtophat放在濾波之前是為了先提細(xì)節(jié)再平滑噪聲順序反了細(xì)節(jié)會(huì)被平滑掉。regionprops一次性把MeanIntensity也拿了這個(gè)屬性需要傳入頂帽圖I_top作為灰度參數(shù)它給的是候選區(qū)域的原始頂帽響應(yīng)均值直接對(duì)應(yīng)“這個(gè)候選夠不夠亮”是最有效的強(qiáng)弱判據(jù)。keep用的是逐條的硬規(guī)則出問題時(shí)打印rec矩陣一眼就能看出是哪個(gè)候選卡在哪一條上。6.2 批量評(píng)估與 GUI 入口單張跑通之后第一件事是寫批量評(píng)估腳本不然你永遠(yuǎn)不知道真實(shí)的漏檢率和過殺率。下面這個(gè)腳本掃兩個(gè)文件夾正樣本目錄裝含缺陷圖負(fù)樣本目錄裝良品圖跑完輸出混淆矩陣。function report batchEval(posDir, negDir, cfg) posList dir(fullfile(posDir, *.png)); negList dir(fullfile(negDir, *.png)); TP 0; FN 0; FP 0; TN 0; tic; for i 1:numel(posList) r crackDetect(fullfile(posDir, posList(i).name), cfg); if r.isCrack, TP TP 1; else, FN FN 1; end end for i 1:numel(negList) r crackDetect(fullfile(negDir, negList(i).name), cfg); if r.isCrack, FP FP 1; else, TN TN 1; end end elapsed toc; report.missRate FN / max(TP FN, 1); % 漏檢率 report.overkill FP / max(FP TN, 1); % 過殺率 report.avgTime elapsed / (numel(posList) numel(negList)) * 1000; report.confMat [TP FN; FP TN]; fprintf(漏檢率 %.2f%% 過殺率 %.2f%% 平均耗時(shí) %.1f ms\n, ... report.missRate*100, report.overkill*100, report.avgTime); endavgTime這個(gè)字段別嫌它多余它決定了你的算法能不能塞進(jìn)產(chǎn)線節(jié)拍。如果平均耗時(shí)超過預(yù)算就得回頭做 5.1 里的那些優(yōu)化。要給人看效果的話用 App Designer 搭個(gè)最小界面就夠了一個(gè)uiaxes顯示原圖一個(gè)uiaxes顯示標(biāo)注圖一個(gè)滑塊調(diào)閾值一個(gè)按鈕切圖?;卣{(diào)里直接調(diào)crackDetect把out.marked塞給坐標(biāo)區(qū)?;瑝K的作用很大——讓工藝人員能自己感受閾值變化對(duì)漏檢和過殺的影響比你在報(bào)告里寫十頁說明都管用。源碼包里帶了一個(gè)精簡版的界面腳本核心就三十來行可以直接改。7. 調(diào)試過程中最容易翻車的幾個(gè)地方第一個(gè)坑是拿實(shí)驗(yàn)室圖調(diào)完參數(shù)就直接上產(chǎn)線。實(shí)驗(yàn)室里的瓶子和產(chǎn)線上的瓶子狀態(tài)差很遠(yuǎn)——產(chǎn)線上的瓶子可能有冷凝水、有細(xì)微粉塵、有剛從退火爐出來帶著余溫導(dǎo)致的輕微形變這些都會(huì)改變圖像的灰度分布。我一般的做法是至少在產(chǎn)線連續(xù)采三種不同狀態(tài)的樣本剛下線、冷卻后、清洗后各幾十張全部納入調(diào)參集。第二個(gè)坑是把結(jié)構(gòu)元尺寸當(dāng)成萬能鑰匙。頂帽的半徑、閉運(yùn)算的半徑、面積閾值這三者之間有耦合關(guān)系改一個(gè)就得重看另外兩個(gè)。比較穩(wěn)妥的調(diào)法是把它們固定成一組基線參數(shù)先只動(dòng)閾值縮放到能分割出大部分缺陷再微調(diào)形態(tài)學(xué)參數(shù)讓斷線連上最后才動(dòng)面積和長寬比這些判定條件。一次只動(dòng)一個(gè)動(dòng)完立刻在固定驗(yàn)證集上跑一遍看指標(biāo)別憑感覺。第三個(gè)坑是忽略了瓶型的差異。同一個(gè)算法在淺色瓶和深色瓶上的表現(xiàn)可能天差地別深色瓶的透光率低暗場(chǎng)效果反而更好淺色瓶則容易出現(xiàn)大面積高光。如果你的產(chǎn)線會(huì)換瓶型參數(shù)必須做成配方化的按瓶型編號(hào)切換一整套cfg不要指望一套參數(shù)打天下。第四個(gè)坑是只做單幀判定。真實(shí)的漂移是緩慢發(fā)生的比如光源老化、鏡頭積灰、瓶體批次變化。代碼再穩(wěn)也建議加一層趨勢(shì)監(jiān)控統(tǒng)計(jì)最近 1000 個(gè)瓶子的候選缺陷數(shù)量均值一旦偏離基線超過閾值就報(bào)警提示檢查光源和鏡頭。這個(gè)功能實(shí)現(xiàn)起來不到二十行但能幫你提前幾天發(fā)現(xiàn)問題而不是等客戶投訴才知道。最后分享一個(gè)小技巧把每次判定的候選區(qū)域和特征值都寫進(jìn)日志哪怕只記前十個(gè)候選的面積、長寬比、響應(yīng)均值出了問題拿日志回溯比重新復(fù)現(xiàn)現(xiàn)場(chǎng)容易得多。我吃過沒有日志的虧一個(gè)偶發(fā)的過殺查了整整兩天后來加上日志第三次出現(xiàn)時(shí)五分鐘就定位到是某個(gè)角度的反光被誤判了。