化尺寸測(cè)量:OpenCV像素到毫米的產(chǎn)線替換邏輯)
簡(jiǎn)介本資源面向計(jì)算機(jī)視覺與工業(yè)自動(dòng)化方向的開發(fā)者及學(xué)生提供一套基于 OpenCV 與深度學(xué)習(xí)實(shí)現(xiàn)富士康金屬件自動(dòng)化尺寸測(cè)量的完整項(xiàng)目源碼與配套數(shù)據(jù)。項(xiàng)目核心思路是先定位目標(biāo)位置再統(tǒng)計(jì)其像素面積結(jié)合比例尺換算實(shí)際尺寸涵蓋圖像匹配、特征提取、角度測(cè)量等典型難點(diǎn)并附有 0/4/7 號(hào)位置的識(shí)別測(cè)量結(jié)果供對(duì)照驗(yàn)證。壓縮包共 88 個(gè)文件以 44 個(gè) png 與 9 個(gè) jpg 圖像樣本、20 個(gè) py 源碼腳本為主另含 json 配置、mov/mp4 演示視頻、md 說明文檔及 ipynb 實(shí)驗(yàn)筆記整體約 47.56MB目錄按 code、data、docs、dataset 等模塊組織便于按流程檢索。目前已有 147 人學(xué)習(xí)下載。讀者可獲取從圖像預(yù)處理、目標(biāo)定位到尺寸換算的完整算法鏈路以及針對(duì)小尺寸實(shí)物、工業(yè)相機(jī)放大受限等問題的排錯(cuò)思路與實(shí)驗(yàn)記錄適合作為工業(yè)視覺測(cè)量項(xiàng)目的參考方案。1. 富士康金屬件尺寸測(cè)量從卡尺到相機(jī)的產(chǎn)線替換邏輯一條手機(jī)中框從沖壓線下來質(zhì)檢工位上的老手會(huì)拿數(shù)顯卡尺量長(zhǎng)寬、用塞尺測(cè)段差、再拿投影儀抽檢幾個(gè)關(guān)鍵孔位。這套流程單件耗時(shí)幾十秒抽檢率上不去漏檢的件流到組裝段才被發(fā)現(xiàn)返工成本直接翻倍。基于計(jì)算機(jī)視覺的富士康金屬件自動(dòng)化尺寸測(cè)量算法要解決的就是把這道工位從「人眼加卡尺」換成「相機(jī)加算法」——用工業(yè)相機(jī)拍一張金屬件正投影圖通過 OpenCV 做邊緣提取和亞像素定位把像素坐標(biāo)換算成毫米輸出長(zhǎng)、寬、孔徑、孔距、段差這些尺寸和公差帶比對(duì)后判定 OK/NG。這套方案適合三類人一是做 3C 結(jié)構(gòu)件質(zhì)檢的自動(dòng)化工程師手上有產(chǎn)線但抽檢率卡在瓶頸二是計(jì)算機(jī)視覺方向的學(xué)生或轉(zhuǎn)行者想找一個(gè)有明確物理意義、能跑通全流程的實(shí)戰(zhàn)項(xiàng)目而不是只調(diào)分類模型三是產(chǎn)線設(shè)備集成商需要一套可復(fù)現(xiàn)的 Python 尺寸測(cè)量基線再往 Halcon 或?qū)S脺y(cè)量機(jī)方向做精度升級(jí)。它不追求微米級(jí)計(jì)量室精度目標(biāo)是在產(chǎn)線節(jié)拍內(nèi)做到 ±0.02mm 級(jí)別的重復(fù)性把全檢變成可能。2. 像素到毫米尺寸測(cè)量算法的四個(gè)核心環(huán)節(jié)2.1 標(biāo)定先行為什么沒有標(biāo)定就沒有尺寸視覺測(cè)量的第一性原理是相機(jī)看到的是像素不是毫米。像素和毫米之間的換算關(guān)系必須通過標(biāo)定建立。最常見的做法是使用棋盤格標(biāo)定板用cv2.findChessboardCorners找到角點(diǎn)再用cv2.calibrateCamera解出內(nèi)參矩陣和畸變系數(shù)。內(nèi)參矩陣?yán)锏慕咕鄁x, fy和主點(diǎn)cx, cy決定了像素與物理尺寸的映射比例畸變系數(shù)則用來校正鏡頭帶來的桶形或枕形變形。對(duì)于產(chǎn)線測(cè)量我一般會(huì)做兩步標(biāo)定先做相機(jī)內(nèi)參標(biāo)定消除鏡頭畸變?cè)僭跍y(cè)量平面上放一個(gè)已知尺寸的標(biāo)準(zhǔn)件比如 50mm 量塊拍圖后計(jì)算「像素/毫米」比例這個(gè)比例就是后續(xù)所有尺寸換算的基準(zhǔn)。如果只做內(nèi)參標(biāo)定不做平面比例標(biāo)定測(cè)量結(jié)果會(huì)隨工件在視野中的位置漂移這是新手最容易翻車的地方。import cv2 import numpy as np # 棋盤格內(nèi)角點(diǎn)數(shù)量例如 9x6 pattern_size (9, 6) # 棋盤格方格實(shí)際邊長(zhǎng)單位 mm square_size 5.0 # 生成棋盤格世界坐標(biāo) objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size obj_points [] # 3D 點(diǎn) img_points [] # 2D 點(diǎn) # 讀取多張不同角度的標(biāo)定圖 for fname in [calib_01.jpg, calib_02.jpg, calib_03.jpg]: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: # 亞像素角點(diǎn)優(yōu)化提升標(biāo)定精度 corners_refined cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ) obj_points.append(objp) img_points.append(corners_refined) # 相機(jī)標(biāo)定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(內(nèi)參矩陣:\n, mtx) print(畸變系數(shù):\n, dist)這段代碼的關(guān)鍵參數(shù)是pattern_size和square_size必須和實(shí)際標(biāo)定板一致填錯(cuò)會(huì)導(dǎo)致標(biāo)定結(jié)果整體縮放錯(cuò)誤。cornerSubPix的窗口大小(11, 11)是經(jīng)驗(yàn)值太小對(duì)噪聲敏感太大在角點(diǎn)密集時(shí)容易把鄰近角點(diǎn)算進(jìn)來。標(biāo)定圖建議拍 10 到 15 張覆蓋視野的四個(gè)角和中心標(biāo)定重投影誤差控制在 0.1 像素以內(nèi)再進(jìn)入下一步。2.2 邊緣提取與亞像素定位Canny 之后還要做什么金屬件在背光或同軸光下成像邊緣對(duì)比度高Canny 是最常用的邊緣檢測(cè)算子。但 Canny 輸出的是像素級(jí)邊緣直接拿來做尺寸測(cè)量重復(fù)性大概在 ±1 像素?fù)Q算到 0.05mm/像素的分辨率就是 ±0.05mm達(dá)不到產(chǎn)線要求。所以必須做亞像素邊緣定位。常見做法有兩種一是用cv2.cornerSubPix的思路對(duì)邊緣點(diǎn)做梯度方向的亞像素插值二是用卡尺工具Caliper沿一條搜索線找灰度梯度極值點(diǎn)再做拋物線擬合。OpenCV 本身沒有現(xiàn)成的卡尺函數(shù)但可以用cv2.Sobel算梯度再沿搜索線找極值。def subpixel_edge(gray, p1, p2, search_width20): 沿 p1-p2 方向搜索亞像素邊緣點(diǎn) # 在 p1-p2 連線上采樣 num int(np.linalg.norm(np.array(p2) - np.array(p1))) xs np.linspace(p1[0], p2[0], num) ys np.linspace(p1[1], p2[1], num) # 雙線性插值取灰度 vals [] for x, y in zip(xs, ys): vals.append(cv2.getRectSubPix(gray, (1, 1), (float(x), float(y)))[0, 0]) vals np.array(vals, dtypenp.float32) # 一階差分找梯度極值 grad np.abs(np.diff(vals)) idx np.argmax(grad) # 拋物線擬合求亞像素位置 if 0 idx len(grad) - 1: y0, y1, y2 grad[idx - 1], grad[idx], grad[idx 1] delta 0.5 * (y0 - y2) / (y0 - 2 * y1 y2) sub_idx idx delta else: sub_idx idx # 映射回坐標(biāo) t sub_idx / (num - 1) return p1[0] t * (p2[0] - p1[0]), p1[1] t * (p2[1] - p1[1])search_width控制搜索范圍太寬會(huì)把鄰近邊緣算進(jìn)來太窄可能找不到真實(shí)邊緣。拋物線擬合的假設(shè)是邊緣附近梯度分布近似二次曲線對(duì)高斯型邊緣成立對(duì)階躍型邊緣會(huì)有偏差。如果金屬件邊緣有毛刺或反光建議先做高斯濾波再算梯度濾波核大小取 3 或 5太大邊緣會(huì)糊。2.3 尺寸換算與公差判定像素比例怎么用拿到亞像素邊緣點(diǎn)后兩個(gè)邊緣點(diǎn)之間的像素距離乘以「毫米/像素」比例就是物理尺寸。這個(gè)比例來自 2.1 節(jié)的平面標(biāo)定。如果工件有厚度正投影測(cè)量的是上表面輪廓段差測(cè)量需要另做方案比如用激光三角測(cè)量或結(jié)構(gòu)光純 2D 視覺測(cè)不了 Z 向尺寸。公差判定就是拿測(cè)量值和圖紙公差帶比對(duì)。這里有個(gè)細(xì)節(jié)測(cè)量值本身有不確定度如果公差帶很窄比如 ±0.01mm而你的測(cè)量重復(fù)性是 ±0.02mm那判定結(jié)果沒有意義。所以先做 GRR量具重復(fù)性與再現(xiàn)性分析確認(rèn)測(cè)量系統(tǒng)能力足夠再上線判定邏輯。# 假設(shè)已標(biāo)定出 mm_per_pixel mm_per_pixel 0.05 def measure_distance(edge1, edge2): 計(jì)算兩個(gè)亞像素邊緣點(diǎn)之間的物理距離 px_dist np.sqrt((edge1[0] - edge2[0])**2 (edge1[1] - edge2[1])**2) return px_dist * mm_per_pixel def judge(value, nominal, tol_plus, tol_minus): 公差判定 if nominal - tol_minus value nominal tol_plus: return OK return NGmm_per_pixel的精度直接決定測(cè)量精度標(biāo)定時(shí)標(biāo)準(zhǔn)件的尺寸要用更高一級(jí)的量具測(cè)準(zhǔn)比如用千分尺測(cè) 50mm 量塊的實(shí)際值而不是信標(biāo)稱值。公差判定函數(shù)里tol_plus和tol_minus分開傳是因?yàn)楹芏嘟饘偌畈粚?duì)稱比如 0.02/-0.01。2.4 多工件批量處理從單張圖到產(chǎn)線節(jié)拍產(chǎn)線上相機(jī)是連續(xù)采圖的算法要能批量處理并輸出結(jié)構(gòu)化結(jié)果。常見做法是用一個(gè)主循環(huán)讀圖、測(cè)量、寫結(jié)果結(jié)果存 CSV 或直接通過串口/PLC 發(fā)給執(zhí)行機(jī)構(gòu)。如果節(jié)拍要求高可以把測(cè)量邏輯封裝成函數(shù)用多進(jìn)程或 C 加速Python 層只做調(diào)度。import glob import csv results [] for fname in sorted(glob.glob(prod_*.jpg)): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 這里調(diào)用前面的邊緣提取和測(cè)量函數(shù) # edge_left subpixel_edge(gray, (100, 300), (100, 500)) # edge_right subpixel_edge(gray, (900, 300), (900, 500)) # width measure_distance(edge_left, edge_right) # status judge(width, nominal50.0, tol_plus0.02, tol_minus0.01) # results.append([fname, width, status]) pass with open(measure_result.csv, w, newline) as f: writer csv.writer(f) writer.writerow([file, width_mm, status]) writer.writerows(results)批量處理時(shí)要注意圖像命名排序sorted按字符串排如果文件名是prod_1.jpg到prod_10.jpg會(huì)出現(xiàn)prod_10排在prod_2前面建議文件名補(bǔ)零。結(jié)果 CSV 的字段設(shè)計(jì)要包含原始文件名、測(cè)量值、判定結(jié)果和測(cè)量時(shí)間戳方便追溯。3. 環(huán)境搭建與最小可跑通流程3.1 Python 與 OpenCV 安裝避開 ModuleNotFoundError新手最常遇到的報(bào)錯(cuò)是ModuleNotFoundError: No module named opencv和cv2.error: OpenCV(4.4.0) ...。前者是沒裝 opencv-python后者多半是版本不匹配或安裝不完整。我一般用 conda 建虛擬環(huán)境再 pip 裝 opencv-python 和 opencv-contrib-pythoncontrib 包里有一些額外算法雖然尺寸測(cè)量用不到但后續(xù)擴(kuò)展方便。conda create -n measure python3.9 -y conda activate measure pip install opencv-python4.8.0.74 opencv-contrib-python4.8.0.74 pip install numpy pandas裝完后驗(yàn)證import cv2 print(cv2.__version__)如果import cv2報(bào)錯(cuò)但 pip 顯示已安裝檢查是不是在錯(cuò)誤的 Python 環(huán)境里執(zhí)行。VS Code 里要選對(duì)解釋器PyCharm 里要確認(rèn)項(xiàng)目解釋器路徑。樹莓派上裝 OpenCV 建議用pip install opencv-python-headless省掉 GUI 依賴編譯時(shí)間也短。3.2 從一張圖到一組尺寸最小測(cè)量腳本下面是一個(gè)最小可跑通的測(cè)量腳本假設(shè)圖像里有一個(gè)矩形金屬件背景是背光板邊緣清晰。腳本做三件事讀圖、Canny 找邊緣、霍夫直線擬合出四條邊再算長(zhǎng)寬。import cv2 import numpy as np img cv2.imread(metal_part.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯濾波降噪 blur cv2.GaussianBlur(gray, (5, 5), 0) # Canny 邊緣檢測(cè)閾值根據(jù)實(shí)際圖像調(diào)整 edges cv2.Canny(blur, 50, 150) # 霍夫直線檢測(cè) lines cv2.HoughLinesP(edges, 1, np.pi / 180, threshold100, minLineLength200, maxLineGap10) # 按角度分組找水平和垂直方向的主邊 horizontal, vertical [], [] for line in lines: x1, y1, x2, y2 line[0] angle np.degrees(np.arctan2(y2 - y1, x2 - x1)) if abs(angle) 10: horizontal.append(line[0]) elif abs(abs(angle) - 90) 10: vertical.append(line[0]) # 取最外側(cè)的邊計(jì)算尺寸簡(jiǎn)化處理 if horizontal and vertical: ys [p[1] for l in horizontal for p in [(l[0], l[1]), (l[2], l[3])]] xs [p[0] for l in vertical for p in [(l[0], l[1]), (l[2], l[3])]] height_px max(ys) - min(ys) width_px max(xs) - min(xs) mm_per_pixel 0.05 # 需根據(jù)標(biāo)定替換 print(f寬度: {width_px * mm_per_pixel:.3f} mm) print(f高度: {height_px * mm_per_pixel:.3f} mm)HoughLinesP的threshold是累加器閾值值越大檢出的線越少但越可靠minLineLength過濾短線段金屬件邊緣長(zhǎng)可以設(shè)大一點(diǎn)maxLineGap允許線段中間斷開適合有倒角或圓角的邊。這個(gè)腳本只是基線實(shí)際產(chǎn)線要用亞像素邊緣和卡尺工具替換霍夫直線精度才能上去。3.3 標(biāo)定文件與測(cè)量腳本的對(duì)接標(biāo)定得到的mtx和dist要存成文件測(cè)量腳本啟動(dòng)時(shí)加載避免每次重新標(biāo)定。常見做法是存成.npz或.yaml。# 保存標(biāo)定結(jié)果 np.savez(calib.npz, mtxmtx, distdist, mm_per_pixel0.05) # 測(cè)量腳本加載 calib np.load(calib.npz) mtx calib[mtx] dist calib[dist] mm_per_pixel float(calib[mm_per_pixel])mm_per_pixel建議單獨(dú)存一個(gè)標(biāo)量不要從內(nèi)參矩陣反推因?yàn)槠矫姹壤€和相機(jī)高度、鏡頭焦距有關(guān)直接標(biāo)定更可靠。如果產(chǎn)線換鏡頭或調(diào)了相機(jī)高度必須重新標(biāo)定并更新這個(gè)文件。4. 避坑與排查金屬件測(cè)量常見的五個(gè)翻車點(diǎn)4.1 邊緣反光導(dǎo)致測(cè)量值跳變現(xiàn)象同一工件連續(xù)拍十張寬度測(cè)量值在 0.05mm 范圍內(nèi)跳動(dòng)偶爾跳 0.1mm 以上。原因金屬件表面反光邊緣附近灰度梯度不穩(wěn)定Canny 閾值固定時(shí)反光區(qū)域會(huì)產(chǎn)生偽邊緣或漏檢。解決改用背光或同軸光讓工件呈剪影如果必須用表面光加偏振片壓反光同時(shí)把 Canny 閾值改成自適應(yīng)用cv2.threshold的 Otsu 方法先二值化再找輪廓。4.2 鏡頭畸變沒校正邊緣位置隨工件移動(dòng)變化現(xiàn)象工件放在視野中心時(shí)測(cè)量準(zhǔn)移到邊緣就偏 0.1mm 以上。原因鏡頭畸變沒校正或者只做了內(nèi)參標(biāo)定沒做畸變校正。解決用cv2.undistort對(duì)每張圖先做畸變校正再測(cè)量。校正后的圖會(huì)損失一部分視野邊緣相機(jī)選型時(shí)要把這個(gè)余量算進(jìn)去。4.3 標(biāo)定板不平或標(biāo)定圖太少現(xiàn)象標(biāo)定重投影誤差看著很小但實(shí)際測(cè)量就是不準(zhǔn)。原因標(biāo)定板沒放平或者標(biāo)定圖角度太單一解出的參數(shù)過擬合。解決標(biāo)定板用陶瓷或玻璃基板別用打印紙標(biāo)定圖至少 10 張覆蓋視野四角和中心傾斜角度在 ±30 度內(nèi)變化。4.4 像素比例用錯(cuò)尺寸整體縮放現(xiàn)象所有測(cè)量值都偏大或偏小同一個(gè)比例。原因mm_per_pixel算錯(cuò)比如標(biāo)定件實(shí)際 50mm 但代碼里寫了 5mm或者單位混了。解決標(biāo)定件尺寸用千分尺實(shí)測(cè)代碼里單位統(tǒng)一用毫米標(biāo)定后拿一個(gè)已知尺寸的工件驗(yàn)證偏差超過 0.01mm 就查比例。4.5 多線程讀圖導(dǎo)致結(jié)果錯(cuò)亂現(xiàn)象批量處理時(shí)結(jié)果和文件名對(duì)不上或者程序偶爾崩潰。原因多線程讀圖時(shí)共享了全局變量或者 OpenCV 的imread在多線程下不是線程安全的。解決讀圖用單線程測(cè)量邏輯可以并行但每個(gè)線程用獨(dú)立的圖像副本別共享numpy數(shù)組。5. 精度再往上走亞像素卡尺工具與 GRR 驗(yàn)證如果產(chǎn)線要求 ±0.01mm 的重復(fù)性前面那套 Canny 加霍夫直線的方案就不夠了。我一般會(huì)自己寫一個(gè)卡尺工具在邊緣附近沿搜索線取灰度剖面做高斯擬合求極值點(diǎn)再把多個(gè)搜索線的結(jié)果取平均。這樣單邊邊緣的定位重復(fù)性能到 0.1 像素以內(nèi)換算到 0.02mm/像素的分辨率就是 ±0.002mm。def caliper_edge(gray, start, end, num_lines10, search_len30): 卡尺工具多條搜索線取平均 points [] for i in range(num_lines): t i / (num_lines - 1) cx start[0] t * (end[0] - start[0]) cy start[1] t * (end[1] - start[1]) # 沿垂直方向搜索 p1 (cx - search_len / 2, cy) p2 (cx search_len / 2, cy) edge subpixel_edge(gray, p1, p2) points.append(edge) # 取平均 xs [p[0] for p in points] ys [p[1] for p in points] return np.mean(xs), np.mean(ys)num_lines越多平均效果越好但計(jì)算量也越大。產(chǎn)線節(jié)拍允許的話取 20 到 30 條。search_len要覆蓋邊緣可能的漂移范圍一般取 20 到 50 像素。驗(yàn)證測(cè)量系統(tǒng)能力用 GRR選 10 個(gè)工件每個(gè)工件測(cè) 3 次換 2 個(gè)操作員各測(cè)一輪算重復(fù)性和再現(xiàn)性。%GRR 低于 10% 說明測(cè)量系統(tǒng)合格10% 到 30% 可接受但要看應(yīng)用場(chǎng)景超過 30% 就得改方案。這個(gè)分析用 Python 的pandas和numpy就能算不需要專門軟件。我自己踩過的坑是標(biāo)定完覺得萬事大吉直接上線結(jié)果夜班燈光變化導(dǎo)致邊緣閾值失效一批件全判 NG。后來養(yǎng)成習(xí)慣每次換班先拿標(biāo)準(zhǔn)件跑一遍確認(rèn)測(cè)量值在公差中值附近再開線。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取