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