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