ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

富士康金属件自动化尺寸测量:OpenCV像素到毫米的产线替换逻辑

富士康金属件自动化尺寸测量:OpenCV像素到毫米的产线替换逻辑

简介:本资源面向计算机视觉与工业自动化方向的开发者及学生,提供一套基于 OpenCV 与深度学习实现富士康金属件自动化尺寸测量的完整项目源码与配套数据。项目核心思路是先定位目标位置,再统计其像素面积,结合比例尺换算实际尺寸,涵盖图像匹配、特征提取、角度测量等典型难点,并附有 0/4/7 号位置的识别测量结果供对照验证。压缩包共 88 个文件,以 44 个 png 与 9 个 jpg 图像样本、20 个 py 源码脚本为主,另含 json 配置、mov/mp4 演示视频、md 说明文档及 ipynb 实验笔记,整体约 47.56MB,目录按 code、data、docs、dataset 等模块组织,便于按流程检索。目前已有 147 人学习下载。读者可获取从图像预处理、目标定位到尺寸换算的完整算法链路,以及针对小尺寸实物、工业相机放大受限等问题的排错思路与实验记录,适合作为工业视觉测量项目的参考方案。

1. 富士康金属件尺寸测量:从卡尺到相机的产线替换逻辑

一条手机中框从冲压线下来,质检工位上的老手会拿数显卡尺量长宽、用塞尺测段差、再拿投影仪抽检几个关键孔位。这套流程单件耗时几十秒,抽检率上不去,漏检的件流到组装段才被发现,返工成本直接翻倍。基于计算机视觉的富士康金属件自动化尺寸测量算法,要解决的就是把这道工位从「人眼加卡尺」换成「相机加算法」——用工业相机拍一张金属件正投影图,通过 OpenCV 做边缘提取和亚像素定位,把像素坐标换算成毫米,输出长、宽、孔径、孔距、段差这些尺寸,和公差带比对后判定 OK/NG。

这套方案适合三类人:一是做 3C 结构件质检的自动化工程师,手上有产线但抽检率卡在瓶颈;二是计算机视觉方向的学生或转行者,想找一个有明确物理意义、能跑通全流程的实战项目,而不是只调分类模型;三是产线设备集成商,需要一套可复现的 Python 尺寸测量基线,再往 Halcon 或专用测量机方向做精度升级。它不追求微米级计量室精度,目标是在产线节拍内做到 ±0.02mm 级别的重复性,把全检变成可能。

2. 像素到毫米:尺寸测量算法的四个核心环节

2.1 标定先行:为什么没有标定就没有尺寸

视觉测量的第一性原理是:相机看到的是像素,不是毫米。像素和毫米之间的换算关系,必须通过标定建立。最常见的做法是使用棋盘格标定板,用cv2.findChessboardCorners找到角点,再用cv2.calibrateCamera解出内参矩阵和畸变系数。内参矩阵里的焦距fx, fy和主点cx, cy决定了像素与物理尺寸的映射比例,畸变系数则用来校正镜头带来的桶形或枕形变形。

对于产线测量,我一般会做两步标定:先做相机内参标定,消除镜头畸变;再在测量平面上放一个已知尺寸的标准件(比如 50mm 量块),拍图后计算「像素/毫米」比例,这个比例就是后续所有尺寸换算的基准。如果只做内参标定不做平面比例标定,测量结果会随工件在视野中的位置漂移,这是新手最容易翻车的地方。

import cv2 import numpy as np # 棋盘格内角点数量,例如 9x6 pattern_size = (9, 6) # 棋盘格方格实际边长,单位 mm square_size = 5.0 # 生成棋盘格世界坐标 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 点 img_points = [] # 2D 点 # 读取多张不同角度的标定图 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: # 亚像素角点优化,提升标定精度 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) # 相机标定 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print("内参矩阵:\n", mtx) print("畸变系数:\n", dist)

这段代码的关键参数是pattern_size和square_size,必须和实际标定板一致,填错会导致标定结果整体缩放错误。cornerSubPix的窗口大小(11, 11)是经验值,太小对噪声敏感,太大在角点密集时容易把邻近角点算进来。标定图建议拍 10 到 15 张,覆盖视野的四个角和中心,标定重投影误差控制在 0.1 像素以内再进入下一步。

2.2 边缘提取与亚像素定位:Canny 之后还要做什么

金属件在背光或同轴光下成像,边缘对比度高,Canny 是最常用的边缘检测算子。但 Canny 输出的是像素级边缘,直接拿来做尺寸测量,重复性大概在 ±1 像素,换算到 0.05mm/像素的分辨率就是 ±0.05mm,达不到产线要求。所以必须做亚像素边缘定位。

常见做法有两种:一是用cv2.cornerSubPix的思路,对边缘点做梯度方向的亚像素插值;二是用卡尺工具(Caliper),沿一条搜索线找灰度梯度极值点,再做抛物线拟合。OpenCV 本身没有现成的卡尺函数,但可以用cv2.Sobel算梯度,再沿搜索线找极值。

def subpixel_edge(gray, p1, p2, search_width=20): """沿 p1->p2 方向搜索亚像素边缘点""" # 在 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, dtype=np.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 # 映射回坐标 t = sub_idx / (num - 1) return p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])

search_width控制搜索范围,太宽会把邻近边缘算进来,太窄可能找不到真实边缘。抛物线拟合的假设是边缘附近梯度分布近似二次曲线,对高斯型边缘成立,对阶跃型边缘会有偏差。如果金属件边缘有毛刺或反光,建议先做高斯滤波再算梯度,滤波核大小取 3 或 5,太大边缘会糊。

2.3 尺寸换算与公差判定:像素比例怎么用

拿到亚像素边缘点后,两个边缘点之间的像素距离乘以「毫米/像素」比例,就是物理尺寸。这个比例来自 2.1 节的平面标定。如果工件有厚度,正投影测量的是上表面轮廓,段差测量需要另做方案,比如用激光三角测量或结构光,纯 2D 视觉测不了 Z 向尺寸。

公差判定就是拿测量值和图纸公差带比对。这里有个细节:测量值本身有不确定度,如果公差带很窄(比如 ±0.01mm),而你的测量重复性是 ±0.02mm,那判定结果没有意义。所以先做 GR&R(量具重复性与再现性)分析,确认测量系统能力足够,再上线判定逻辑。

# 假设已标定出 mm_per_pixel mm_per_pixel = 0.05 def measure_distance(edge1, edge2): """计算两个亚像素边缘点之间的物理距离""" 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 "NG"

mm_per_pixel的精度直接决定测量精度,标定时标准件的尺寸要用更高一级的量具测准,比如用千分尺测 50mm 量块的实际值,而不是信标称值。公差判定函数里tol_plus和tol_minus分开传,是因为很多金属件公差不对称,比如 +0.02/-0.01。

2.4 多工件批量处理:从单张图到产线节拍

产线上相机是连续采图的,算法要能批量处理并输出结构化结果。常见做法是用一个主循环读图、测量、写结果,结果存 CSV 或直接通过串口/PLC 发给执行机构。如果节拍要求高,可以把测量逻辑封装成函数,用多进程或 C++ 加速,Python 层只做调度。

import glob import csv results = [] for fname in sorted(glob.glob("prod_*.jpg")): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 这里调用前面的边缘提取和测量函数 # 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, nominal=50.0, tol_plus=0.02, tol_minus=0.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)

批量处理时要注意图像命名排序,sorted按字符串排,如果文件名是prod_1.jpg到prod_10.jpg,会出现prod_10排在prod_2前面,建议文件名补零。结果 CSV 的字段设计要包含原始文件名、测量值、判定结果和测量时间戳,方便追溯。

3. 环境搭建与最小可跑通流程

3.1 Python 与 OpenCV 安装:避开 ModuleNotFoundError

新手最常遇到的报错是ModuleNotFoundError: No module named 'opencv'和cv2.error: OpenCV(4.4.0) ...。前者是没装 opencv-python,后者多半是版本不匹配或安装不完整。我一般用 conda 建虚拟环境,再 pip 装 opencv-python 和 opencv-contrib-python,contrib 包里有一些额外算法,虽然尺寸测量用不到,但后续扩展方便。

conda create -n measure python=3.9 -y conda activate measure pip install opencv-python==4.8.0.74 opencv-contrib-python==4.8.0.74 pip install numpy pandas

装完后验证:

import cv2 print(cv2.__version__)

如果import cv2报错但 pip 显示已安装,检查是不是在错误的 Python 环境里执行。VS Code 里要选对解释器,PyCharm 里要确认项目解释器路径。树莓派上装 OpenCV 建议用pip install opencv-python-headless,省掉 GUI 依赖,编译时间也短。

3.2 从一张图到一组尺寸:最小测量脚本

下面是一个最小可跑通的测量脚本,假设图像里有一个矩形金属件,背景是背光板,边缘清晰。脚本做三件事:读图、Canny 找边缘、霍夫直线拟合出四条边,再算长宽。

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 边缘检测,阈值根据实际图像调整 edges = cv2.Canny(blur, 50, 150) # 霍夫直线检测 lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=100, minLineLength=200, maxLineGap=10) # 按角度分组,找水平和垂直方向的主边 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]) # 取最外侧的边计算尺寸(简化处理) 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 # 需根据标定替换 print(f"宽度: {width_px * mm_per_pixel:.3f} mm") print(f"高度: {height_px * mm_per_pixel:.3f} mm")

HoughLinesP的threshold是累加器阈值,值越大检出的线越少但越可靠;minLineLength过滤短线段,金属件边缘长,可以设大一点;maxLineGap允许线段中间断开,适合有倒角或圆角的边。这个脚本只是基线,实际产线要用亚像素边缘和卡尺工具替换霍夫直线,精度才能上去。

3.3 标定文件与测量脚本的对接

标定得到的mtx和dist要存成文件,测量脚本启动时加载,避免每次重新标定。常见做法是存成.npz或.yaml。

# 保存标定结果 np.savez("calib.npz", mtx=mtx, dist=dist, mm_per_pixel=0.05) # 测量脚本加载 calib = np.load("calib.npz") mtx = calib["mtx"] dist = calib["dist"] mm_per_pixel = float(calib["mm_per_pixel"])

mm_per_pixel建议单独存一个标量,不要从内参矩阵反推,因为平面比例还和相机高度、镜头焦距有关,直接标定更可靠。如果产线换镜头或调了相机高度,必须重新标定并更新这个文件。

4. 避坑与排查:金属件测量常见的五个翻车点

4.1 边缘反光导致测量值跳变

现象:同一工件连续拍十张,宽度测量值在 0.05mm 范围内跳动,偶尔跳 0.1mm 以上。原因:金属件表面反光,边缘附近灰度梯度不稳定,Canny 阈值固定时,反光区域会产生伪边缘或漏检。解决:改用背光或同轴光,让工件呈剪影;如果必须用表面光,加偏振片压反光,同时把 Canny 阈值改成自适应,用cv2.threshold的 Otsu 方法先二值化再找轮廓。

4.2 镜头畸变没校正,边缘位置随工件移动变化

现象:工件放在视野中心时测量准,移到边缘就偏 0.1mm 以上。原因:镜头畸变没校正,或者只做了内参标定没做畸变校正。解决:用cv2.undistort对每张图先做畸变校正,再测量。校正后的图会损失一部分视野边缘,相机选型时要把这个余量算进去。

4.3 标定板不平或标定图太少

现象:标定重投影误差看着很小,但实际测量就是不准。原因:标定板没放平,或者标定图角度太单一,解出的参数过拟合。解决:标定板用陶瓷或玻璃基板,别用打印纸;标定图至少 10 张,覆盖视野四角和中心,倾斜角度在 ±30 度内变化。

4.4 像素比例用错,尺寸整体缩放

现象:所有测量值都偏大或偏小同一个比例。原因:mm_per_pixel算错,比如标定件实际 50mm 但代码里写了 5mm,或者单位混了。解决:标定件尺寸用千分尺实测,代码里单位统一用毫米,标定后拿一个已知尺寸的工件验证,偏差超过 0.01mm 就查比例。

4.5 多线程读图导致结果错乱

现象:批量处理时结果和文件名对不上,或者程序偶尔崩溃。原因:多线程读图时共享了全局变量,或者 OpenCV 的imread在多线程下不是线程安全的。解决:读图用单线程,测量逻辑可以并行,但每个线程用独立的图像副本,别共享numpy数组。

5. 精度再往上走:亚像素卡尺工具与 GR&R 验证

如果产线要求 ±0.01mm 的重复性,前面那套 Canny 加霍夫直线的方案就不够了。我一般会自己写一个卡尺工具:在边缘附近沿搜索线取灰度剖面,做高斯拟合求极值点,再把多个搜索线的结果取平均。这样单边边缘的定位重复性能到 0.1 像素以内,换算到 0.02mm/像素的分辨率就是 ±0.002mm。

def caliper_edge(gray, start, end, num_lines=10, search_len=30): """卡尺工具:多条搜索线取平均""" 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越多,平均效果越好,但计算量也越大。产线节拍允许的话,取 20 到 30 条。search_len要覆盖边缘可能的漂移范围,一般取 20 到 50 像素。

验证测量系统能力,用 GR&R:选 10 个工件,每个工件测 3 次,换 2 个操作员各测一轮,算重复性和再现性。%GR&R 低于 10% 说明测量系统合格,10% 到 30% 可接受但要看应用场景,超过 30% 就得改方案。这个分析用 Python 的pandas和numpy就能算,不需要专门软件。

我自己踩过的坑是:标定完觉得万事大吉,直接上线,结果夜班灯光变化导致边缘阈值失效,一批件全判 NG。后来养成习惯,每次换班先拿标准件跑一遍,确认测量值在公差中值附近再开线。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进