合開發(fā)機器視覺檢測系統(tǒng):架構(gòu)、模塊與驗收實踐)
簡介圍繞C#與HALCON構(gòu)建的機器視覺檢測系統(tǒng)是一套面向機器視覺初學(xué)者與中高級開發(fā)者的完整工程源碼聚焦工件表面質(zhì)量檢測場景。系統(tǒng)按圖像采集、預(yù)處理、特征提取、決策反饋和圖形界面等模塊組織展示相機取圖、去噪增強、形狀與模板匹配、缺陷判定及報警聯(lián)動的實現(xiàn)路徑適合用于課程實踐、項目原型驗證和二次開發(fā)參考。壓縮包共245個文件、約45.48MB以80個C#源碼文件為核心配有35張BMP測試圖像、29個DLL與23個PDB依賴調(diào)試文件以及10個resx/resources界面資源、6個csproj工程文件、3個exe和sln解決方案等目錄結(jié)構(gòu)較完整便于直接打開工程梳理調(diào)用關(guān)系。已有7066人學(xué)習(xí)下載。讀者可據(jù)此理解C#調(diào)用HALCON算子、視覺流程編排、檢測結(jié)果展示與工程部署方式并借助測試圖復(fù)現(xiàn)實驗、定位問題積累可遷移的機器視覺系統(tǒng)開發(fā)經(jīng)驗。1. 一條外觀檢測線的真實取舍C# 與 HALCON 到底誰管哪一段某產(chǎn)線要做零件外觀檢測最初的想法很樸素用 C# 把相機采到的圖讀成 Bitmap自己寫閾值、找輪廓、算尺寸。真跑到現(xiàn)場就卡住了——換一批來料光照差兩檔閾值全廢亞像素定位沒有測量值跳動比公差還大模板匹配要從頭造輪子。后來改成 C# 寫上位機、HALCON 做算法內(nèi)核機器視覺里最費勁的那部分交給成熟算子庫視覺檢測系統(tǒng)里界面、流程、數(shù)據(jù)、報警、配方這些工程活留在 C# 里。這份 C# 與 HALCON 聯(lián)合開發(fā)的機器視覺軟件系統(tǒng)源代碼就是按這個分工搭起來的完整視覺檢測系統(tǒng)相機采集、模板定位、尺寸測量、缺陷判定、結(jié)果落盤、配方切換一條鏈走完。它適合正在做機器視覺方向畢設(shè)或課程設(shè)計的學(xué)生也適合要把一套能跑的 C# 視覺軟件直接搬到自己設(shè)備上改的工程師。2. 需求拆解與選型把「能看見缺陷」翻譯成節(jié)拍、精度和軟件棧2.1 檢測項怎么落成可量化的指標需求溝通里最容易翻車的一句話是「要能檢出劃傷」。這句話沒法驗收也沒法寫代碼。負責(zé)的做法是先把它拆成四列測什么、用什么成像方式測、精度要到多少、判定閾值是多少。檢測項一般分三類——尺寸類長度、直徑、間距、角度、位置類裝配偏移、缺件、錯件、表面類劃傷、臟污、毛刺、壓痕。三類在算法上的實現(xiàn)路徑完全不同尺寸類靠測量工具位置類靠模板匹配加區(qū)域比對表面類多半要靠 Blob 分析、頻域濾波或者差異圖。精度這一欄要用像素當量倒推不能拍腦袋。視場 40mm、相機 2448×2048像素當量約 0.016mm/pixelHALCON 的亞像素邊緣通常能做到 1/4 像素以內(nèi)折算下來重復(fù)性在 0.004mm 量級。所以一個標稱 ±0.02mm 的尺寸項在這個配置下是有余量的要是公差要求 ±0.005mm那就該換遠心鏡頭、提高分辨率或者上背光而不是指望算法硬扛。我一般會先算這筆賬再談選型省得后面相機都裝好了才發(fā)現(xiàn)精度不夠。檢測項成像方式精度要求判定方式外形長寬背光、遠心鏡頭±0.02mm卡尺工具測邊緣對距離孔位偏移同軸光±0.03mm模板匹配定位后測圓心表面劃傷低角度環(huán)形光最小 0.1mm×2mmBlob 面積長寬比篩選缺件/錯件正面均勻光有無判斷區(qū)域灰度均值與模板比對裝配間隙側(cè)向條形光±0.05mm測量矩形內(nèi)雙邊緣間距節(jié)拍是另一條硬約束。單件 300ms 的產(chǎn)線一次曝光加傳輸大概吃掉 6080ms算法預(yù)算只剩 200ms 出頭還得留出通訊和界面刷新的余量。所以算法流程要做成「先粗后精」先用降采樣圖像做快速定位把 ROI 縮到工件附近再做全分辨率的測量和判定。這套系統(tǒng)的流程編排就是按這個思路排的每個工具只在自己關(guān)心的 ROI 里跑不整圖掃。2.2 C# 做上位機、HALCON 做算法核的分層依據(jù)有人會問HDevelop 也能拖界面為什么還要 C#答案是工程化。產(chǎn)線上的軟件不只是跑算法它要連 PLC、要存追溯數(shù)據(jù)、要做用戶權(quán)限、要支持十幾個料號的配方切換、要能在客戶現(xiàn)場遠程看日志。這些事 HDevelop 做得吃力.NET 生態(tài)里是現(xiàn)成的。反過來純 C# 加 OpenCVSharp 也能做視覺但形狀模板匹配的抗遮擋能力、亞像素邊緣的穩(wěn)定性、標定工具鏈的完整度調(diào)起來的隱性成本很高。所以分工線畫在這里C# 負責(zé)「殼」——界面、流程調(diào)度、多線程、數(shù)據(jù)庫、通訊協(xié)議、配置管理、日志與報警HALCON 負責(zé)「眼」——圖像預(yù)處理、模板匹配、邊緣提取、測量、Blob 分析、標定換算。兩邊通過 HALCON 的 .NET 接口HObject / HTuple交接邊界清晰。這條線畫對了后面加檢測項就是加一個工具類不用動主流程。分層還有個現(xiàn)實好處算法參數(shù)可以外置成配方工藝工程師改參數(shù)不用重新編譯。這套系統(tǒng)里所有閾值、ROI 偏移、匹配分數(shù)下限、灰度上下限都放在配方文件里程序啟動時加載切換料號時熱更新。2.3 算子與相機選型的對照取舍選型時最常糾結(jié)的幾組我把自己會怎么選列出來場景常見選擇 A常見選擇 B我一般選工件有旋轉(zhuǎn)、部分遮擋NCC 灰度匹配形狀模板匹配B對光照變化和遮擋更穩(wěn)邊緣位置測量手寫梯度找極值measure_pos 測量工具B亞像素和抗噪現(xiàn)成表面缺陷提取固定閾值 Blob動態(tài)閾值 形態(tài)學(xué)B適應(yīng)光照漂移大幅面高速采集USB3 面陣GigE 面陣看線長超過 5m 用 GigE精密尺寸普通 FA 鏡頭雙遠心鏡頭B視場內(nèi)的放大倍率一致性更好相機接口這塊GigE 的優(yōu)點是線纜長、多相機組網(wǎng)方便缺點是延遲比 USB3 略高USB3 帶寬足、延遲低但線長限制在 35m工業(yè)現(xiàn)場走線要提前規(guī)劃。這套系統(tǒng)里采集層做了接口抽象換相機只改一個配置項算法層完全不用動——這也是分層帶來的直接收益。提示選型階段就把「精度、節(jié)拍、視場、景深」四個數(shù)寫進需求文檔后面所有爭論都回到這四個數(shù)上比爭論用哪個算子有效得多。3. 總體架構(gòu)設(shè)計三層結(jié)構(gòu)與六個模塊的職責(zé)邊界3.1 表現(xiàn)層、業(yè)務(wù)層、算法層的劃分整套軟件按三層切。表現(xiàn)層是 WinForm 或 WPF 的主界面負責(zé)實時圖像顯示、結(jié)果列表、參數(shù)編輯、報警提示它不碰任何 HALCON 算子只消費業(yè)務(wù)層給出來的結(jié)果對象。業(yè)務(wù)層是流程引擎和調(diào)度中心管著「取圖 → 定位 → 測量 → 判定 → 存檔 → 通訊」這條流水線同時管配方加載、權(quán)限校驗、異常上報。算法層是 HALCON 的封裝每個檢測能力包裝成一個工具類對外只暴露「輸入圖像 參數(shù) → 輸出測量值」這樣的簽名。這樣切的好處是測試好做。算法層的每個工具可以單獨拿一批離線圖片跑回歸業(yè)務(wù)層可以用假的圖像源做流程測試界面層改動不影響算法正確性。做畢設(shè)答辯時這一點特別加分——老師問「你怎么驗證算法是對的」你能拿出分層測試的記錄比說「我跑了一遍看著沒問題」有說服力得多。六個模塊分別是設(shè)備接入相機、光源、PLC/IO、圖像預(yù)處理、定位與測量、判定與分類、數(shù)據(jù)與追溯、配方與權(quán)限。每個模塊內(nèi)部的類只依賴下一層的接口不允許跨層直接調(diào)。比如判定模塊不能自己去讀相機它只接收測量模塊的結(jié)果數(shù)組。3.2 圖像從相機到算法再到數(shù)據(jù)庫的流轉(zhuǎn)通路高節(jié)拍下最容易出問題的是圖像流轉(zhuǎn)。做法是采集與處理分離相機回調(diào)線程只做一件事——把 HObject 塞進一個有界隊列然后立刻返回絕不在回調(diào)里跑算法。算法線程從隊列里取圖處理處理完把結(jié)果對象推到結(jié)果隊列界面線程和存盤線程各自消費。隊列要有界滿了就丟最舊的一幀并記一條丟幀日志避免內(nèi)存無限堆積。// 采集線程與算法線程之間的有界緩沖容量按節(jié)拍和算法耗時估算 private readonly BlockingCollectionHObject _imageQueue new BlockingCollectionHObject(8); // 相機回調(diào)只負責(zé)入隊禁止在這里做任何耗時運算 private void OnImageGrabbed(HObject image) { HObject copy; HOperatorSet.CopyImage(image, out copy); // 回調(diào)緩沖會被復(fù)用必須拷貝 if (!_imageQueue.TryAdd(copy)) { // 隊列滿說明算法跟不上節(jié)拍丟棄并計數(shù)事后看日志定位瓶頸 copy.Dispose(); Interlocked.Increment(ref _dropCount); } } // 算法線程主循環(huán) private void ProcessLoop() { foreach (var img in _imageQueue.GetConsumingEnumerable(_cts.Token)) { using (img) { var result _pipeline.Run(img); // 內(nèi)部依次調(diào)用各工具 _resultQueue.Add(result); } } }這里的關(guān)鍵點是CopyImage。很多相機 SDK 的回調(diào)緩沖區(qū)是復(fù)用的你把引用直接傳出去下一幀到來時圖像內(nèi)容就被覆蓋了現(xiàn)象是「偶爾檢測到上一件的缺陷」這種問題查起來極其折磨。拷貝一次的代價遠小于排查成本。隊列容量 8 是按「算法單幀 80ms、節(jié)拍 300ms」估的實際項目里按實測的算法耗時乘 23 倍來定太大反而掩蓋性能問題。3.3 模塊之間的接口約定與異常傳遞工具層統(tǒng)一接口是為了讓流程配置化。每個檢測工具實現(xiàn)同一個契約流程引擎按配方里配置的順序依次執(zhí)行public interface IVisionTool { string Name { get; } // 工具標識配方里按名字引用 ToolResult Execute(HObject image, JObject param); // param 來自配方文件的節(jié)點 } public class ToolResult { public bool Success { get; set; } // 算法是否正常執(zhí)行完 public int ErrorCode { get; set; } // 非 0 表示執(zhí)行失敗界面按碼查提示 public double[] Values { get; set; } // 測量值數(shù)組單位統(tǒng)一為毫米 public bool IsOk { get; set; } // 判定結(jié)論與 Success 分開 public string DefectCode { get; set; } // NG 時的缺陷編碼用于統(tǒng)計 }Success和IsOk必須分開。算法沒跑通找不到模板、圖像過曝、邊緣數(shù)量不對是執(zhí)行異常產(chǎn)品合格與否是判定結(jié)論兩者混在一起會導(dǎo)致「算法失敗被當成 NG 品流出去」這在追溯時是災(zāi)難。執(zhí)行異常要走獨立的報警通道讓設(shè)備停下來等人處理而不是默默計數(shù)。結(jié)果落盤用一張主表加一張明細表就夠了主表存單件結(jié)論明細存每個測量項的值方便后面按項做趨勢分析CREATE TABLE InspectionRecord ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Sn TEXT NOT NULL, -- 產(chǎn)品條碼/流水號 RecipeCode TEXT NOT NULL, -- 料號用于分料號統(tǒng)計 Result INTEGER NOT NULL, -- 1OK, 0NG DefectCode TEXT, -- NG 缺陷編碼OK 時為空 ImagePath TEXT, -- NG 圖像存檔路徑 CreateTime TEXT NOT NULL ); CREATE INDEX idx_record_sn ON InspectionRecord(Sn); CREATE INDEX idx_record_time ON InspectionRecord(CreateTime); CREATE TABLE MeasureDetail ( Id INTEGER PRIMARY KEY AUTOINCREMENT, RecordId INTEGER NOT NULL, ItemName TEXT NOT NULL, -- 測量項名稱與配方節(jié)點對應(yīng) Value REAL NOT NULL, LowerLimit REAL, UpperLimit REAL, IsOk INTEGER NOT NULL );索引這兩條不是湊數(shù)。按 Sn 查是為了追溯按時間查是為了做日報和趨勢圖產(chǎn)線上這兩個查詢是每天都跑的。明細表拆出來是因為一個工件可能有十幾二十個測量項塞進主表的 JSON 字段里查起來費勁做 SPC 分析時更難受。4. 核心模塊實現(xiàn)采集、定位、測量、判級四段代碼落地4.1 相機接入與 HObject 的生命周期相機接入層要做三件事打開設(shè)備、配置參數(shù)、穩(wěn)定取圖。HALCON 用open_framegrabber統(tǒng)一封裝各家接口參數(shù)隨接口類型不同而不同具體字段要按相機廠商的接口文檔對齊不能照抄。public class CameraDevice : IDisposable { private HTuple _acqHandle null; private readonly object _lock new object(); public void Open(string interfaceName, string deviceId) { // 參數(shù)順序固定含義依次為接口名、水平/垂直分辨率、起始位置、 // 圖像寬高、場序、位深、色彩空間、泛型參數(shù)、外部觸發(fā)、相機類型、設(shè)備標識 HOperatorSet.OpenFramegrabber( interfaceName, 0, 0, 0, 0, 0, 0, progressive, -1, default, -1, false, default, deviceId, -1, -1, out _acqHandle); // 關(guān)掉自動曝光和自動增益產(chǎn)線上這兩個必須鎖死否則測量值會漂 HOperatorSet.SetFramegrabberParam(_acqHandle, ExposureTime, 8000); HOperatorSet.SetFramegrabberParam(_acqHandle, Gain, 0); } public HObject GrabOnce(int timeoutMs) { lock (_lock) // 同一句柄不能并發(fā)調(diào)用加鎖是最省事的做法 { HOperatorSet.GrabImageAsync(out HObject image, _acqHandle, timeoutMs); return image; } } public void Dispose() { if (_acqHandle ! null) { HOperatorSet.CloseFramegrabber(_acqHandle); _acqHandle null; } } }自動曝光這一條值得單獨說。調(diào)試時開著自動曝光畫面好看上線后光源老化、環(huán)境光變化相機會自己調(diào)增益灰度整體漂移閾值型檢測項集體失效。血淚經(jīng)驗是調(diào)試階段就用固定曝光固定增益把光源亮度調(diào)到能拿到穩(wěn)定灰度的位置之后不再動它。真要適應(yīng)不同料號的反光差異靠配方里的曝光參數(shù)切換不靠相機自動。HObject 是托管包裝的非托管資源必須顯式釋放。用using或者 try/finally 里的Dispose()不能指望 GC。這一點在第 5 章展開。4.2 形狀模板匹配與亞像素定位定位是整個流程的地基定位偏了后面所有測量都偏。做法分兩步建模階段從一張標準件圖像里摳出特征明顯的區(qū)域生成形狀模板運行階段用模板在當前圖里找位姿。// 建模在一張合格品圖像上框出特征區(qū)生成形狀模板 public HTuple CreateTemplate(HObject image, double row1, double col1, double row2, double col2) { HOperatorSet.GenRectangle1(out HObject roi, row1, col1, row2, col2); HOperatorSet.ReduceDomain(image, roi, out HObject domain); roi.Dispose(); // 角度范圍 -0.39~0.39 rad約 ±22.5°覆蓋來料旋轉(zhuǎn) // 金字塔層數(shù)、對比度、最小對比度都交給 auto讓算子按圖像自選 HOperatorSet.CreateShapeModel( domain, auto, -0.39, 0.79, auto, auto, use_polarity, auto, auto, out HTuple modelId); domain.Dispose(); return modelId; } // 運行在當前圖中查找返回行、列、角度、得分 public bool Find(HTuple modelId, HObject image, out double row, out double col, out double angle) { HOperatorSet.FindShapeModel( image, modelId, -0.39, 0.79, 0.7, // 最小得分低于它認為沒找到 1, // 最多找?guī)讉€匹配單件檢測取 1 0.5, // 重疊度 least_squares, // 亞像素模式精度最高的那一檔 0, // 金字塔層數(shù)0 表示用建模時的設(shè)置 0.9, // 貪婪度越大越快越可能漏 out HTuple r, out HTuple c, out HTuple a, out HTuple score); row r.Length 0 ? r.D.ToArray()[0] : 0; col c.Length 0 ? c.D.ToArray()[0] : 0; angle a.Length 0 ? a.D.ToArray()[0] : 0; return score.Length 0 score.D[0] 0.7; }參數(shù)里最需要現(xiàn)場調(diào)的是MinScore和Greediness。MinScore 定太低0.5 以下會把相似結(jié)構(gòu)誤認成目標定太高0.85 以上則在來料表面有輕微臟污時找不到。經(jīng)驗值是先設(shè) 0.7拿一批包含邊界樣本的圖跑看得分分布取一個能把 OK 品和誤匹配分開的閾值。Greediness 默認 0.9如果發(fā)現(xiàn)偶爾漏檢先往 0.8 降一檔試試速度會慢一些但更穩(wěn)。建模區(qū)域的選擇也有講究不要選大面積的均勻區(qū)域那種區(qū)域沒有梯度信息模板會退化成靠邊緣支撐抗噪能力差要選帶角點、圓孔、缺口這類強特征的位置。模板圖像盡量用實際產(chǎn)線的相機和光源拍別拿實驗室的樣品圖湊合光學(xué)條件不一樣模板的對比度參數(shù)就不對了。4.3 測量與判級從像素值到 OK/NG 結(jié)論有了位姿測量就好辦了。思路是把模板坐標系里定義好的測量位置通過剛性變換映射到當前圖像的實際位置再用測量工具在映射后的位置取邊緣。這樣來料有平移旋轉(zhuǎn)也不影響。// 模板坐標系下的基準點與測量矩形參數(shù)建模時在標準件上確定 private const double TplRow 512.0; private const double TplCol 640.0; private const double TplAngle 0.0; public double MeasureGap(HObject image, double mRow, double mCol, double mAngle, JObject param) { // 1. 用匹配位姿構(gòu)造模板坐標 - 當前圖像坐標的剛性變換 HOperatorSet.VectorAngleToRigid(TplRow, TplCol, TplAngle, mRow, mCol, mAngle, out HTuple homMat2D); // 2. 把模板里定義好的測量中心映射過去測量框跟著工件一起轉(zhuǎn) HOperatorSet.AffineTransPoint2d(homMat2D, TplRow, TplCol - 80, out HTuple row, out HTuple col); HTuple phi TplAngle mAngle; // 3. 生成測量矩形長 60 寬 12方向沿工件邊插值選雙線性 HOperatorSet.GenMeasureRectangle2(row, col, phi, 30, 6, 60, 12, bilinear, out HTuple handle); // 4. 在矩形內(nèi)找邊緣對sigma1.0 抑制噪聲threshold30 控制靈敏度 HOperatorSet.MeasurePos(image, handle, 1.0, 30, all, all, out HTuple edgeRow1, out HTuple edgeCol1, out HTuple amp1, out HTuple edgeRow2, out HTuple edgeCol2, out HTuple amp2, out HTuple intraDist, out HTuple interDist); HOperatorSet.CloseMeasure(handle); if (intraDist.Length 1) throw new VisionException(1001, 測量框內(nèi)未找到成對邊緣檢查光照或測量框位置); // interDist 是相鄰邊緣間距像素乘像素當量換成毫米 double pixelScale param.Valuedouble(PixelScale); // 由標定得到如 0.0163 return interDist.D[0] * pixelScale; }Sigma和Threshold這兩個參數(shù)是測量的手感所在。Sigma 是高斯平滑的系數(shù)來料表面粗糙、圖像噪點大就調(diào)大到 1.52.0代價是邊緣位置會被輕微模糊精度略降圖像干凈就保持 1.0 甚至 0.8。Threshold 是邊緣幅度閾值它決定什么算作邊緣——太低會把噪點當邊緣太高會漏掉對比度弱的真實邊緣。穩(wěn)妥的做法是打開邊緣幅度輸出看一批圖的幅度分布把閾值定在噪聲幅度和真實邊緣幅度之間。判級就是拿測量值和配方里的上下限比public (bool ok, string defectCode) Judge(double value, JObject param) { double lower param.Valuedouble(LowerLimit); double upper param.Valuedouble(UpperLimit); double tol param.Valuedouble(JudgeTolerance); // 判定死區(qū)默認 0 // 死區(qū)的作用剛好卡在邊界上的值做二次確認避免 OK/NG 反復(fù)橫跳 if (value lower - tol) return (false, param.Valuestring(DefectCodeLow)); if (value upper tol) return (false, param.Valuestring(DefectCodeHigh)); return (true, null); }判定死區(qū)這個小設(shè)計在批量生產(chǎn)時很關(guān)鍵。沒有它的時候一個值在上下限附近抖同一個工件可能這次 OK 下次 NG操作員會懷疑設(shè)備有問題。加了死區(qū)之后邊界附近的值統(tǒng)一往寬松方向靠配合趨勢圖看反而更容易發(fā)現(xiàn)工藝漂移。NG 圖像一定要存。存圖策略按「日期 / 料號 / 時間戳_缺陷碼」的目錄結(jié)構(gòu)落盤保留最近 30 天超期自動清理。這張圖在后續(xù)分析缺陷成因、和供應(yīng)商扯皮的時候是唯一的證據(jù)鏈。4.4 配方管理換料號不改代碼配方是這套系統(tǒng)里最能體現(xiàn)工程成熟度的部分。十幾個料號每個料號的 ROI、閾值、模板、上下限都不同如果寫死在代碼里每換一次料就要改代碼重新發(fā)版現(xiàn)場根本沒法用。配方的組織方式是一棵 JSON 樹根節(jié)點是料號下面是工具鏈數(shù)組每個工具節(jié)點帶自己的參數(shù){ RecipeCode: P-1024, PixelScale: 0.0163, Tools: [ { Name: Locate, Type: ShapeMatch, TemplateFile: tpl_1024.shm, MinScore: 0.72, AngleStart: -0.39, AngleExtent: 0.79 }, { Name: GapA, Type: MeasurePair, RefRow: 512, RefCol: 560, LowerLimit: 1.98, UpperLimit: 2.06, JudgeTolerance: 0.005, DefectCodeLow: GAP_SMALL, DefectCodeHigh: GAP_LARGE }, { Name: Scratch, Type: BlobDefect, MinArea: 120, MaxAspect: 8.0, GrayLow: 40, DefectCode: SCRATCH } ] }流程引擎按Tools數(shù)組順序執(zhí)行遇到Locate就把位姿寫進上下文后面的測量工具從上下文里取。新增一個檢測項只是在數(shù)組里加一個節(jié)點代碼零改動。配方的加載和熱切換要注意兩點一是切換時必須在流程空閑的間隙做不能在一件工件處理到一半時換參數(shù)二是每次切換記一條日志包含料號、操作人、時間出問題時能對上是哪次切換引入的偏差。注意配方文件建議加版本號和校驗值現(xiàn)場出現(xiàn)過配置文件被人手工改壞、程序啟動直接崩的情況加個校驗?zāi)芴崆皵r住。5. 避坑與常見問題排查內(nèi)存、線程、標定三處最容易翻車的地方5.1 內(nèi)存只漲不落現(xiàn)象程序跑兩三個小時內(nèi)存從 200MB 漲到 2GB最后卡死或者被系統(tǒng)殺掉。原因幾乎都是 HObject 沒釋放。HALCON 的圖像和區(qū)域?qū)ο蟮讓邮欠峭泄軆?nèi)存C# 的 GC 只回收包裝對象不會主動去釋放底層句柄峰值節(jié)拍下對象創(chuàng)建速度遠超終結(jié)器回收速度句柄就堆起來了。解決方法是所有 HObject 都走using或者在方法末尾顯式Dispose()尤其注意異常分支——try 塊里創(chuàng)建了對象catch 里直接 return那個對象就漏了。// 反面寫法異常一拋img 和 region 都不會被釋放 HObject img; HOperatorSet.ReadImage(out img, path); HObject region; HOperatorSet.Threshold(img, out region, 0, 128); DoSomething(region); // 若這里拋異常上面兩個對象泄漏 // 正確寫法用 using 包住無論走哪條分支都釋放 using (HObject img new HObject()) using (HObject region new HObject()) { HOperatorSet.ReadImage(out img, path); HOperatorSet.Threshold(img, out region, 0, 128); DoSomething(region); }排查手段任務(wù)管理器看句柄數(shù)比看內(nèi)存更準句柄只漲不落就是泄漏。也可以在每個流程節(jié)點前后打一條get_system(handle_count)的日志幾個循環(huán)就能定位到是哪個工具漏了。5.2 多線程下的偶發(fā)崩潰現(xiàn)象單線程跑一天沒事改成多線程后隨機崩報的是訪問沖突或者算子內(nèi)部錯誤日志里看不出規(guī)律。原因是 HALCON 的 HTuple 和 HObject 不是線程安全的兩個線程同時操作同一個對象或者在 A 線程創(chuàng)建、B 線程釋放都可能出問題。解法是「誰創(chuàng)建誰釋放跨線程只傳值不傳引用」算法線程處理完把測量值double 數(shù)組和結(jié)果碼傳出去不要把 HObject 傳來傳去確實要傳圖像就在目標線程里CopyImage一份。另外 HALCON 的算子在內(nèi)部已經(jīng)做了并行外層再開一堆線程搶 CPU 反而是負優(yōu)化算法線程一般開 12 個就夠。5.3 標定與像素當量換算翻車現(xiàn)象測量值整體偏大或偏小百分之幾重復(fù)性很好但準確度不對。原因多半在像素當量上。像素當量不是拿標稱視場除分辨率算出來的那個值忽略了鏡頭畸變和實際放大倍率偏差。正確做法是拿一塊標準量塊或者標定板在同一工作距離下拍圖量出已知長度對應(yīng)的像素數(shù)兩者相除。更嚴謹?shù)淖龇ㄊ亲龆帱c標定把鏡頭畸變一起擬合掉視場邊緣的測量值才不會系統(tǒng)性偏大。還有個容易忽略的點更換鏡頭、調(diào)整工作距離、甚至相機被撞了一下像素當量都要重新標。我見過一個項目調(diào)試時標定過后來調(diào)焦換了鏡頭沒重標整批產(chǎn)品尺寸超差卻判 OK流到客戶那邊才發(fā)現(xiàn)?,F(xiàn)在我的習(xí)慣是每次動過光學(xué)系統(tǒng)就在配方里改標定日期并在開機自檢里加一條「標定有效期提醒」。5.4 觸發(fā)時序錯位與丟幀現(xiàn)象檢測結(jié)果和產(chǎn)品對不上號或者偶爾漏檢一件。這類問題多半在觸發(fā)鏈路上。相機是硬觸發(fā)還是軟觸發(fā)、PLC 給信號到相機曝光的延遲多少、編碼器分頻對不對、光源頻閃和曝光是否同步任何一環(huán)錯位都會導(dǎo)致「拍到了相鄰的一件」。排查方法是拿一張帶標記的樣件連續(xù)跑 100 件把每件的圖像和條碼一一對應(yīng)看錯位會立刻暴露。修復(fù)上硬觸發(fā)優(yōu)先于軟觸發(fā)觸發(fā)信號走光耦隔離別用普通 IO 直連曝光時間和光源延時要在配方里成組配置不能分開調(diào)。5.5 部署環(huán)境缺運行庫現(xiàn)象開發(fā)機上跑得好好的拷到產(chǎn)線電腦上雙擊就報錯或者一打開相機就崩。原因是目標機缺 HALCON 運行時或者 .NET 運行時版本不匹配也可能是相機廠商的驅(qū)動沒裝、網(wǎng)卡巨幀沒開。部署前要列一張環(huán)境清單.NET 運行時版本、HALCON 運行庫位數(shù)32/64 位必須和主程序一致、相機驅(qū)動版本、網(wǎng)卡巨幀和中斷親和性設(shè)置、系統(tǒng)電源計劃改成高性能別讓 CPU 降頻。這張清單每次部署都過一遍比現(xiàn)場抓瞎強。6. 驗收與答辯用一組可復(fù)現(xiàn)的數(shù)據(jù)證明系統(tǒng)「測得準、跑得穩(wěn)」驗收這一步很多人栽在「我說它準」上。合格的做法是拿數(shù)據(jù)說話而且數(shù)據(jù)要可復(fù)現(xiàn)。我一般準備三組測試準確性測試、重復(fù)性測試、穩(wěn)定性測試。準確性用標準件。拿 10 件已知尺寸的標準件或者三坐標測過的樣件每件測 10 次算每個測量項的平均值和標準值的偏差。偏差要小于公差的三分之一超了就得回去查標定或者算法。重復(fù)性用同一件產(chǎn)品連續(xù)測 30 次算標準差這個數(shù)反映的是系統(tǒng)自身的抖動一般要求小于公差的十分之一。穩(wěn)定性是連續(xù)跑 48 小時統(tǒng)計總檢數(shù)、誤檢數(shù)、漏檢數(shù)、算法異常次數(shù)同時記錄內(nèi)存和句柄曲線。驗收項指標測試方法記錄內(nèi)容測量準確性偏差 ≤ 公差/310 件標準件 × 10 次均值、偏差、最大值重復(fù)性標準差 ≤ 公差/10同件連續(xù) 30 次標準差、極差檢出率≥ 99.5%200 件含已知缺陷樣本漏檢數(shù)、漏檢類型誤檢率≤ 0.5%200 件合格品誤檢數(shù)、誤檢原因節(jié)拍≤ 設(shè)計值連續(xù) 500 件計時平均、P95、最大值長時間穩(wěn)定性8 小時無異常連續(xù)運行內(nèi)存曲線、異常次數(shù)節(jié)拍統(tǒng)計最好在程序里內(nèi)建別拿秒表掐// 每個流程節(jié)點前后打點落庫后直接出 P95 報表 var sw Stopwatch.StartNew(); var result _pipeline.Run(image); sw.Stop(); _repo.SaveTiming(result.Sn, result.RecipeCode, sw.Elapsed.TotalMilliseconds);答辯時的講法我的建議是倒過來講先講誤檢漏檢的數(shù)據(jù)和你是怎么把這兩個數(shù)壓下去的再講架構(gòu)和代碼。老師最想聽的不是「我用了什么技術(shù)」而是「你怎么知道它是對的」。你可以拿出那張 200 件樣本的混淆矩陣講某一類缺陷一開始漏檢率 8%查下來是 Blob 面積下限設(shè)太大把下限從 200 調(diào)到 120 之后降到 0.5%——這種帶數(shù)字的調(diào)試過程比任何架構(gòu)圖都有說服力。還有一個常被問到的點如果換個料號怎么辦。這時候現(xiàn)場演示配方切換30 秒內(nèi)完成切換后連續(xù)跑 20 件全部正確這一下就把「課程作業(yè)」和「能交付的系統(tǒng)」區(qū)分開了。從那以后我每次做完一套視覺系統(tǒng)驗收前都強制走一遍「標準件 重復(fù)性 8 小時連續(xù)跑」這三關(guān)哪怕時間再緊也不跳。跳過一次后面在現(xiàn)場補的功夫是省下來那點時間的好幾倍。希望這套結(jié)構(gòu)和記錄方法能幫到你需要對照完整實現(xiàn)的話倉庫里的源代碼可以直接拿去做參考。本文還有配套的精品資源點擊獲取