简介:面向秋招求职者的上海柏楚机器视觉测试工程师岗笔试真题整理,涵盖15道在线题目与70分钟限时场景,适合准备视觉算法、工业相机与测试理论笔试的候选人使用。文档逐题梳理白盒测试、2D变换、相机参数、形态学运算、大津法、条件概率、CCD/CMOS对比、滤波与边缘检测等高频考点,并附有图像旋转编程题,可用于自查知识盲区与模拟答题节奏。资源为1个docx文档,约13KB,内容紧凑可直接阅读,已有362人学习下载。除客观题外,简答题部分还涉及过拟合/欠拟合解决方案、工业相机关键参数、小孔成像模型等,能帮助读者快速回顾机器视觉测试岗位的核心理论与典型题型,适合作为笔试前冲刺与查漏补缺的参考资料。
1. 机器视觉测试工程师笔试到底在筛什么:从柏楚24秋招真题看考察主线
如果你准备投递柏楚的机器视觉测试工程师岗,看到“笔试真题”几个字时,第一反应可能是到处搜题库。但真正把卷子做下来你会发现,这套题的核心不是在考你会不会背 API,而是在筛你有没有工业视觉系统的“全局观”。柏楚做的是激光切割控制系统和工业自动化软件,机器视觉测试工程师要面对的从来不是干净的标准图像,而是强光、反光、烟尘、抖动叠加出来的真实现场。笔试里反复出现的,是标定参数、坐标系变换、缺陷检测用例设计、图像预处理链路这类偏工程落地的问题。适合谁来考:有图像处理基础、对 OpenCV 熟悉、又想往工业检测方向转的测试开发。这篇就按笔试的真实考察主线,把算法考点、测试用例设计、编程避坑和场景题答题框架一条条拆开讲。
2. 机器视觉核心算法考点:标定、坐标系与图像处理基本功
2.1 相机标定为什么是必考题:从内参外参到畸变校正的参数链路
机器视觉测试工程师的笔试里,相机标定几乎是绕不开的第一关。柏楚这类做激光切割系统的公司,视觉模块的作用是引导切割头找准工件位置、识别轮廓、检测切割质量,而这一切的前提是知道像素坐标和物理坐标之间怎么换算。如果标定参数错了,后面的检测算法写得再漂亮也白搭——图像上偏差 1 个像素,到了实际加工坐标可能就是 0.05mm 的误差,这在切割场景里直接导致废料。
笔试对相机标定的考察方式通常是三个层次:第一层是概念,问你内参、外参、畸变系数各自描述什么;第二层是流程,给你一组棋盘格照片,让你写出从拍摄到得到标定结果的完整步骤;第三层是分析,给你一组标定结果的重投影误差数据,问你误差偏大时先查什么。前两层靠记忆,第三层靠理解。
import cv2 import numpy as np # 准备棋盘格的物理角点坐标,这里假设格子尺寸 25mm pattern_size = (9, 6) 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 *= 25.0 obj_points = [] # 世界坐标系下的角点 img_points = [] # 图像坐标系下的角点 for img_path in chessboard_images: img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret: obj_points.append(objp) img_points.append(corners) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None)这段代码是标定的标准流程骨架。pattern_size是棋盘格的内角点数,不是格子数,棋盘是 10x7 的格子,内角点就是 9x6,笔试里很容易在这里埋坑。objp *= 25.0这一步把角点坐标从“格数”换算成“毫米”,如果忘记乘格子尺寸,标定出来的平移向量 tvecs 会整体错乱。最后返回的mtx是内参矩阵,dist是畸变系数,rvecs和tvecs是每张图的外参。
笔试真正想看的,是你拿到这套结果之后怎么判断标定质量。最直接的指标是重投影误差,就是把标定得到的内外参代回去,把世界坐标点重新投影到图像上,算投影点和检测角点之间的距离。经验值上,平均重投影误差小于 0.1 像素算优秀,小于 0.3 像素可用,超过 0.5 像素基本要重新拍图。误差偏大的时候,优先检查三件事:格子是否贴平、拍摄角度是否过大、图片数量是否够(10~15 张是底线)。我一般会建议多拍一些不同角度和距离的图,而不是同一位置拍十几张——后者对提升标定精度几乎没有帮助。
2.2 坐标系变换与手眼标定:像素坐标到机构坐标的四步换算
柏楚这类工业控制场景里,相机看到的和机构要动的位置之间差了四个坐标系:像素坐标系、图像物理坐标系、相机坐标系、机器人/机构坐标系。笔试经常给一个场景:“相机检测到工件中心在像素坐标 (u, v),如何换算成切割头需要移动的目标坐标?”这道题看着像数学题,实际考的是你有没有做过手眼标定。
完整的换算链路是:像素坐标先通过内参矩阵转到归一化平面,得到相机坐标系下的坐标,再通过外参(旋转矩阵 R 和平移向量 t)转到机构坐标系。多数笔试不会让你手推公式,但会给你一个简化的场景:相机固定在 Z 轴上方不动,机构在 XY 平面运动,让你写出换算公式。这种场景叫“眼在手外”,是工业现场最常见的布局。
import numpy as np # 内参矩阵(来自标定)和手眼标定得到的外参 K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) # 相机到机构坐标系的变换 R = np.array([[r11, r12, r13], [r21, r22, r23], [r31, r32, r33]]) t = np.array([tx, ty, tz]) def pixel_to_robot(u, v, z_c=100.0): # 像素坐标转归一化坐标 p_cam = np.linalg.inv(K) @ np.array([u, v, 1.0]) # 乘以相机坐标系下的深度 Zc p_cam *= z_c # 通过外参转到机构坐标系 p_robot = R @ p_cam + t.reshape(3, 1) return p_robot.flatten()这里最容易出错的是z_c的取值。如果工件放在一个已知高度的平面上,z_c就是这个平面在相机坐标系下的 Z 值,不是随便填的。笔试如果没给这个值,通常意味着默认工件在一个固定高度的平面上,或者你需要用“测量高度”的方式先把它标出来。另一种常见错误是把 R 和 t 的方向搞反:手眼标定得到的是“相机坐标系到机构坐标系”的变换,如果你拿到的是机构到相机的变换,直接代进去就会得到完全错误的结果。
变形到机构坐标系之后还有个坑:机构坐标系的单位可能是毫米,但你算出来的值确实也是毫米,看起来一致,实际却对不上。原因在于手眼标定时用的棋盘格尺寸有没有写对,这个细节我反复在踩坑,笔试里面也经常以数值题的形式出现。养成习惯,任何坐标换算代码里都先确认棋盘格尺寸、像素当量、机构单位这三者的量纲一致,能省掉后面一整个下午的排查时间。
2.3 图像预处理高频考点:阈值分割、滤波与边缘检测的选型逻辑
视觉笔试里图像处理题目占比往往最高,但考得最多不是背 API,而是“给一个具体场景让你选处理方法”。柏楚的激光切割场景里,典型任务有:找工件轮廓、检测切割线宽度、判别切割断面是否挂渣。这些任务有一个共同难点:图像里有强烈的激光飞溅、烟尘、反光干扰,直接用固定阈值分割几乎必翻车。
import cv2 import numpy as np img = cv2.imread('workpiece.png', cv2.IMREAD_GRAYSCALE) # 高斯滤波去噪,核大小和标准差是选型关键 blur = cv2.GaussianBlur(img, (5, 5), 0) # 大津法自适应阈值,适合前景背景灰度差异明显但不均匀的场景 _, otsu = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 形态学闭运算,填补轮廓内部的孔洞 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) closed = cv2.morphologyEx(otsu, cv2.MORPH_CLOSE, kernel) # Canny 边缘检测,双阈值参数直接影响轮廓连续性 edges = cv2.Canny(closed, 50, 150)这段预处理链路是笔试里常见的“标准答案”,但真正拉开分数的是每一步选型的理由。为什么先高斯滤波而不是中值滤波?因为高斯滤波对高斯噪声(传感器热噪声)效果最好,而激光切割现场的椒盐噪声(飞溅颗粒造成的黑白点)更多,这种情况下中值滤波反而更合适。笔试里给你“图上有很多孤立的白色亮点”这个描述,你要能反应过来它说的是椒盐噪声,从而选择中值滤波。
大津法(Otsu)的适用条件是前景背景灰度分布呈双峰。切割现场因为强反光,灰度直方图经常是单峰或者多峰,这时候 Otsu 会失效。笔试如果问你“光照不均匀工件如何分割”,正确答案不是调阈值,而是先用顶帽变换或者均匀光照校正把背景抹平,再做全局阈值。Canny 的双阈值也有讲究:高阈值决定了哪些强边缘被保留,低阈值决定了弱边缘的延续长度。现场反光导致边缘断裂时,优先尝试降低低阈值,而不是调高阈值去“过滤噪声”---过滤噪声是滤波阶段的事,Canny 阶段再调阈值往往是把真边缘一起削掉了。
3. 测试理论考点:从笔试场景题反推用例设计方法论
3.1 视觉检测场景题:怎么把“检测工件表面划痕”拆成可执行的测试用例
柏楚机器视觉测试工程师的笔试里,一定会有一道“给你一个检测需求,请你设计测试用例”的大题。这类题占了卷面的 20~30 分,也是区分“会写代码的人”和“会做测试的人”的关键。常见的题目描述是:“一个视觉检测系统用于识别工件表面的划痕,划痕长度大于 5mm 判为缺陷,请设计测试用例覆盖各种情况。”
这种题大家都会写“正常工件通过、有划痕工件拦截”这种基础用例,但拿高分的差异体现在你有没有覆盖边界条件和干扰因素。我拿到这种题会先画一个输入空间:图像来源(不同相机、不同分辨率、不同光照)、工件状态(干净、油污、水渍、反光、划痕深浅不一)、环境因素(烟尘、振动、焦距偏差)。然后按等价类、边界值、场景法三个维度去拆。
一个完整的划痕检测测试用例集至少要有以下角度:划痕长度恰好 5mm 的边界样本(程序判断是大于还是大于等于);划痕方向从 0 度到 90 度旋转(检测算法是否对方向敏感);划痕位置在工件边缘和中心的表现(边缘有倒角反光干扰);多个短划痕拼接成长划痕的漏检风险;表面有油污时光照反射变化导致的误检。这些用例本质上是在验证一个视觉算法在不同成像条件下的稳定性。
下面是一个测试用例设计的示例表格,笔试答题时按这个结构写,阅卷老师最容易给分:
| 用例编号 | 前置条件 | 测试步骤 | 预期结果 | 优先级 |
|---|---|---|---|---|
| TC-01 | 光照 500lx,工件表面清洁 | 放置无缺陷工件,触发检测 | 输出 PASS,无缺陷标记 | P0 |
| TC-02 | 光照 500lx,工件表面清洁 | 放置长度 5mm 划痕工件 | 边界值:记录输出结果,确认逻辑为 ≥5mm | P0 |
| TC-03 | 光照变化 200~1000lx | 同一工件间隔 1 分钟检测 10 次 | 每次结果一致,无不稳定输出 | P0 |
| TC-04 | 工件表面涂薄油膜 | 放置有 3mm 划痕工件 | 输出 PASS,油膜不干扰判断 | P1 |
| TC-05 | 工件边缘 45 度倒角 | 放置边缘有 6mm 划痕工件 | 输出 FAIL,边缘反光不导致漏检 | P1 |
这种表格结构里,最容易忽略的是 TC-03 这类“稳定性测试”。视觉检测系统最怕的不是“测不准”,而是“同样输入两次结果不一致”。我见过太多现场问题,算法在实验室单张图上表现完美,一上线就时好时坏,最后定位出来是曝光时间抖动导致图像亮度轻微变化。笔试里只要你在用例里加入“同一工件多次检测结果一致性”这个用例,基本就能证明你有真实测试经验。
3.2 缺陷定位与回归策略:视觉系统失效时先查数据还是先查代码
除了设计用例,笔试还喜欢给一个“现场故障”让你做根因分析。典型描述:“产线反馈某型号工件经常被误判为缺陷,检测系统报警率从 1% 升到 10%,请问怎么排查。”这种题没有标准答案,但有一个靠谱的排查顺序,和“先查数据还是先查代码”这个问题密切相关。
我一般会先查数据再查代码,而且主张用“二分法”缩小范围。第一步确认误判的工件图片到底长什么样——从日志里捞 50 张误判样本,人眼扫一遍,看是否有共同特征(某个方向的光斑、特定位置的纹路、某种表面处理)。这一步不写一行代码,但能解决 50% 的问题,因为很多“算法问题”其实是“数据问题”。第二步把误判图片和正常图片的灰度直方图对比,确认是成像差异还是算法阈值失效。第三步才进代码,看预处理参数和检测算法里有没有写死的东西。
import cv2 import numpy as np # 从误判样本里统计共同特征:计算平均灰度与梯度方向直方图 false_positives = load_images('fp_samples/') # 误判样本目录 mean_intensity = [] grad_hist = [] for fp in false_positives: gray = cv2.cvtColor(fp, cv2.COLOR_BGR2GRAY) mean_intensity.append(gray.mean()) # Sobel 梯度方向,统计边缘方向分布 gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3) gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3) mag, angle = cv2.cartToPolar(gx, gy, angleInDegrees=True) hist = np.histogram(angle, bins=36, range=(0, 360))[0] grad_hist.append(hist) # 输出几个统计量,判断误判样本是否有方向性偏置 avg_angle = np.mean(grad_hist, axis=0) print("mean intensity:", np.mean(mean_intensity)) print("dominant edge angle:", np.argmax(avg_angle) * 10)这段代码做的事就是数据层面的快速验证。如果误判样本的梯度方向集中在某个角度,比如 90 度附近,说明算法对水平方向边缘特别敏感,可能是边缘检测的核(kernel)设置不对称导致的。如果平均灰度和正常样本差异很大,则要回头查曝光参数和光源状态。笔试答题时,把“先数据后代码”“用统计特征定位共性”这两个观点写出来,得分就稳了。
4. 笔试编程与脚本题的避坑指南:手写视觉算法常见的五个翻车点
4.1 手写高斯滤波与卷积边界处理:越界的三种处理方式
笔试编程题很少让你调现成的 OpenCV 函数,更多是让你手写一个简化版的高斯滤波、中值滤波或 Sobel 边缘检测,然后处理边界。这种题看着简单,翻车率却极高。考生最常见的错误是卷积核遍历到图像边界时不做处理,导致数组越界。
高斯滤波的核心是生成一个符合二维高斯分布的卷积核,然后逐个像素做加权求和。笔试给的标准差 sigma 通常是 1.0,核大小是 3x3 或 5x5。你需要自己算出核系数、归一化,然后处理边界。边界处理有三种常见方式,笔试里你选哪种都可以,但必须在代码里体现出你有这个意识。
import numpy as np def gaussian_kernel(size=3, sigma=1.0): """生成归一化的高斯卷积核""" ax = np.arange(size) - size // 2 xx, yy = np.meshgrid(ax, ax) kernel = np.exp(-(xx**2 + yy**2) / (2 * sigma**2)) return kernel / kernel.sum() def gaussian_blur_manual(img, kernel): h, w = img.shape k = kernel.shape[0] pad = k // 2 # 边界用 reflect 方式填充,避免图像边缘出现暗边 padded = np.pad(img, pad, mode='reflect') out = np.zeros_like(img, dtype=np.float32) for i in range(h): for j in range(w): region = padded[i:i+k, j:j+k] out[i, j] = (region * kernel).sum() return out这段代码里,np.pad(img, pad, mode='reflect')用的是反射填充,这是工业视觉里最推荐的边界处理方式。对比另外两种:constant(补零)会让边缘像素的加权平均偏暗,产生“黑边”;wrap(循环填充)在图像边缘不连续时会产生伪边缘。反射填充的效果介于两者之间,对后续边缘检测的影响最小。笔试里如果你写了边界处理,哪怕就一行np.pad,阅卷印象分都会高很多。
这里面还有一个常见的隐藏问题:滤波后的数据类型。out = np.zeros_like(img, dtype=np.float32)如果用np.zeros_like(img)默认继承 uint8,卷积结果小数部分会被截断,连续做几次滤波之后图像会出现肉眼可见的阶梯状色带。工业视觉里这种精度损失在阈值分割时会被放大,笔试代码里提前转float32是加分的细节。
另外,手写卷积的性能也是笔试会问的点。双层 for 循环在百万级像素图像上慢得离谱,一个 3x3 核的 1080p 图像要跑几百毫秒。我一般会顺手写一句优化思路:把二维卷积拆成水平方向和垂直方向两个一维卷积,时间复杂度从 O(n²k²) 降到 O(n²k)。笔试只要写出这个结论,说明你有工程化意识,比代码本身更值钱。
4.2 坐标变换与数据类型陷阱:笔试代码跑不通的常见原因
编程题里另一类高频翻车点是坐标变换和数据类型。工业视觉里坐标有个特别容易踩的坑:图像坐标系的原点在左上角,x 轴向右,y 轴向下;而机构坐标系通常是右手系,y 轴向上。笔试让你“把图像坐标转成机构坐标”,如果你直接拿像素坐标去乘像素当量,得到的 y 轴方向必然是反的。
我见过最典型的错误是 OpenCV 里img.shape返回(height, width),而很多人习惯性写成(width, height)。这个错误在标定、ROI 裁剪、坐标映射时都会出现。测试工程师写脚本时尤其要注意,因为你写的测试脚本要验证开发写的坐标转换逻辑,如果你的脚本本身坐标都搞反了,会得出完全相反的测试结论。
import cv2 import numpy as np # 正确解析图像尺寸:shape = (height, width) h, w = img.shape[:2] cx, cy = w / 2.0, h / 2.0 # 图像中心坐标,注意先宽后高 # 像素坐标转物理坐标(假设已知像素当量 scale,单位 mm/pixel) scale = 0.05 # 每个像素对应 0.05mm def pixel_to_physical(u, v): # 先把原点移到图像中心,再翻转 y 轴 x_mm = (u - cx) * scale y_mm = -(v - cy) * scale # 图像 y 向下,机构 y 向上 return x_mm, y_mm这段代码里的-(v - cy)就是坐标系翻转的关键。很多视觉算法工程师第一次写手眼标定程序时都会在这里卡半天,表现是“算出来的坐标 x 对、y 反”,然后各种怀疑标定矩阵有问题,最后才发现是图像坐标系和机构坐标系天然差了一个 y 轴翻转。笔试里遇到坐标换算题目,先停下来想清楚两个坐标系的方向关系,再写代码,能省掉大量返工。
还有一个数据类型陷阱:OpenCV 的projectPoints和solvePnP默认接受float64数组,如果你把坐标点做成float32,部分版本会报类型错误。跨语言调用时,Python 的float和 numpy 的float64在内存布局上一致,但 C++ 接口里cv::Mat的类型必须和 API 期望一致,否则会静默出错或直接断言失败。笔试里如果让你“查找坐标转换结果异常的原因”,数据类型不匹配是一个很容易被忽略的排查点。
4.3 自动化测试脚本设计:从测试数据生成到断言策略的最小闭环
笔试编程题除了算法题,还经常有一道测试脚本题。场景一般是:“给你一个图像处理函数detect_defect(img_path) -> bool,请编写测试脚本验证它的正确性。”这类题考的是测试工程师的基本功:测试数据怎么来、断言怎么写、结果怎么报告。
测试数据生成是第一个坑。很多人只准备两三张正常图片和缺陷图片,断言也写得很随意——“结果不是 True 就是 False”,这相当于没测。合理的做法是准备一个分级数据集:正常样本 10 张、缺陷样本 10 张、边界样本(划痕刚好到阈值长度)5 张、干扰样本(油污、反光、过曝)5 张,然后对每一类统计检测准确率。
import pytest import csv from pathlib import Path from vision import detect_defect # 测试数据目录结构:normal/ defect/ boundary/ noise/ CASES = [ ("normal/001.png", False), ("normal/002.png", False), ("defect/001.png", True), ("boundary/001.png", True), # 划痕刚好 5mm,预期判为缺陷 ("noise/001.png", False), # 强反光但不含缺陷,预期通过 ] @pytest.mark.parametrize("img_path,expected", CASES) def test_detect_defect(img_path, expected): result = detect_defect(str(Path("data") / img_path)) assert result == expected, f"{img_path}: expected {expected}, got {result}" def test_stability(): # 同一张图跑 5 次,结果必须一致(防抖动) results = [detect_defect("data/defect/001.png") for _ in range(5)] assert all(r == results[0] for r in results)parametrize是 pytest 里做数据驱动测试的标准写法,每一行 CASES 就是一个独立的测试用例,哪个失败一目了然。这个结构里最关键的是test_stability这个用例——很多笔试考生想不到要测稳定性,但视觉检测系统最怕的就是“同图不同结果”。工业现场出现偶发误检,很多时候不是算法逻辑错,而是图像采集本身有抖动(曝光时间波动、光源频闪、相机增益跳动),稳定性测试能把这类问题暴露出来。
断言的粒度也值得注意。assert result == expected是最低级的断言,只能告诉你“对不对”。如果笔试允许,我建议对输出做更细的结构化断言:检测函数返回的不只是 True/False,还应该有缺陷位置和置信度,断言里可以验证“缺陷中心是否落在工件的 ROI 区域内”“置信度是否超过 0.8”。这种断言会强迫你思考“检测结果的可信度”,而不是把算法当成一个黑匣子。测试工程师的活儿,本质上是给算法建立一道质量控制网。
5. 场景设计题通常压轴出现:用一套答题框架稳住基本面
笔试最后一道题往往是场景设计,分值高、开放性强,也是最容易拉开差距的题。常见描述是:“某工厂需要在传送带上检测金属工件表面缺陷,节拍 2 秒一件,检测项包括划痕、凹坑、锈斑,请设计完整的视觉检测方案。”这种题目没有唯一答案,但阅卷时有一套隐形的评分框架:需求澄清、成像方案、算法选型、验证方案,覆盖得越全分越高。
我一般按四步来答。第一步先写需求澄清:缺陷最小尺寸是多少、合格率要求多高、现场有没有振动物料是否定位 OK。第二步写成像方案:选什么光源(同轴光还是环形光)、什么相机(面阵还是线阵)、曝光时间怎么定。第三步写算法流程:预处理用什么滤波、分割用什么阈值、特征提取用什么算子。第四步写验证方案:拿多少张样本、准确率怎么算、误检率容许多高。
这四步的答题逻辑是一次完整的工业方案设计。第一、二步尤其容易被忽略,但恰恰是柏楚这类公司的重点考察方向——激光切割场景里,成像方案直接决定算法上限。光源角度不合适、曝光时间跟不上节拍,算法再强也白搭。我自己的习惯是答题时先写一句“本方案的关键约束是节拍 2 秒和最小缺陷尺寸”,把设计目标定死,后面所有参数都围绕这个目标展开,方案的整体感会强很多。
关于这套笔试准备,我个人的血泪教训是:刷题不如搭环境。把 OpenCV、pytest、一个简易的标定棋盘格打印出来,在本地跑通一遍标定-坐标转换-阈值分割-测试用例的完整链路,你对笔试里 80% 的考点都会有一种“这题我做过”的底气。祝你在柏楚的笔试里稳住基本面,把该拿的分都拿到。希望帮到你。
本文还有配套的精品资源,点击获取