资讯动态

相机标定与激光线扫标定实战:从内参到激光平面拟合

发布时间:2026/9/15 3:32:01 来源:尧图企业网站定制
简介面向计算机视觉、机器人导航与自动驾驶领域开发者这套基于MATLAB的线激光相机标定方案提供了从图像预处理到内外参计算的完整实现包含棋盘格与线激光投影数据的处理流程适合需要快速上手激光线扫标定的研究人员。压缩包共116个文件大小15.48MB以mat数据文件、m脚本、pcd点云文件、jpg图像和asc点坐标文本为主另有md说明文档与cpp参考代码便于对照算法理解数据结构。预编写的函数覆盖特征检测、几何校正等核心环节配合多组标定图像与点云数据可帮助用户直观复现标定过程并应用于定位测量通过标定可获得焦距、主点、畸变系数及相机与激光器相对位姿。资源中还提供不同角度的标定样本与预处理脚本支持自行验证算法精度和调整参数。已有618人学习下载对于正在搭建激光视觉系统或希望深入理解相机标定原理的开发者而言这是一份兼具代码参考与数据支撑的实用资料。1. 相机标定与激光线扫标定先把两个坐标系对齐再说一条一字线激光扫过传送带上的工件工业相机同步拍照机器视觉工程师经常在这个画面里做激光轮廓测量。激光线扫标定要解决的核心问题不是把激光调得更亮更细而是把像素、激光线和真实毫米坐标三者固定下来。相机标定负责输出内参和畸变系数回答“某个像素在相机坐标里是哪条射线”激光标定负责求出激光平面在相机坐标系下的方程回答“这条射线和多远处的一个平面相交”。两者都完成图像里每个激光条纹点才能换算成三维坐标。下面按“先相机内参、再激光平面、后联合验证”的顺序讲适合正在做激光线扫三维重建、结构光测量或快速转行机器视觉的工程师参考。2. 相机标定原理从内参矩阵到畸变模型这步错了后面全错激光条纹提取出来只是一串像素坐标像素本身没有物理意义。要变成三维射线必须先完成相机标定。这个环节的输出直接决定后续激光平面标定的可靠性所以不值得在采集阶段省时间。OpenCV 的calibrateCamera基于张正友标定法用棋盘格角点建立已知三维点和图像点的对应关系联合优化内参、畸变系数和每帧外参。对单目相机加线激光的结构标定内参不依赖激光可以在激光关闭状态下单独完成建议把它当成一道独立工序。2.1 内参矩阵和畸变模型先看像素、相机点、世界点怎么映射相机模型可以简化为像素坐标等于内参矩阵乘以外参矩阵再乘三维世界坐标。内参矩阵里fx、fy是归一化焦距cx、cy是主点畸变系数包含径向畸变k1,k2,k3和切向畸变p1,p2。工业镜头通常畸变不大但边缘区域仍会有可见形变。实际标定时要注意两个容易忽略的地方一是镜头光圈没锁死自动光圈会在不同亮度下改变焦距导致内参飘二是棋盘格尺寸填错格子实际边长影响的是后续三维点比例尺不是像素坐标。这里不建议一上来就套用双目相机标定或者雷达标定工具。单目相机加线激光的问题模型更简单只需要单目内参和激光平面不需要引入第二相机或激光雷达点云。即使以后系统里加了 IMU也应该先完成相机标定再做相机 IMU 联合标定顺序不要反。2.2 用 OpenCV 跑通相机内参标定的最小脚本下面这段脚本是完整的单目标定流程接受一个包含棋盘格图像的目录输出内参矩阵和畸变系数。import cv2 import glob import numpy as np pattern (11, 8) # 内角点数不是棋盘格方块数 square_size 30.0 # 方格实际边长单位毫米后续三维点也以此为尺度 # 构建棋盘格角点的三维坐标Z0 objp np.zeros((np.prod(pattern), 3), dtypenp.float32) objp[:, :2] np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) objp[:, :2] * square_size obj_points [] # 每张图对应的三维点 img_points [] # 每张图对应的图像角点 good_images [] for fname in sorted(glob.glob(calib_images/*.jpg)): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 检测棋盘格角点返回的是整数像素位置 ret, corners cv2.findChessboardCorners(gray, pattern, None) if not ret: print(跳过, fname) continue # 亚像素细化提高角点精度 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) corners cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners) good_images.append(fname) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(参与标定图像数:, len(good_images)) print(标定重投影误差:, ret) print(内参矩阵:\n, mtx) print(畸变系数:, dist.ravel())pattern必须写成棋盘格内部角点数例如 11x8 表示横向有 11 个角点、纵向有 8 个角点。square_size单位要和后面的激光标定三维重建单位保持一致建议统一用毫米。cornerSubPix是必要的亚像素细化步骤不做的话薄板边缘的角点位置误差会被畸变模型吸收导致畸变系数不准。如果是在 ROS 2 环境里也可以直接用ros2 run camera_calibration cameracalibrator在线标定它输出的 yaml 文件本质上就是 OpenCV 的相机参数。区别只是 GUI 交互底层算法和上面的脚本是同一套但批量脚本更容易排查哪张图不合格。2.3 相机标定输出的 5 个关键指标怎么看标定结果不是“能跑出来就行”要通过下面几个检查项确认数据可信度。检查项参考值不合格时处理重投影误差 RMS小于 0.15 像素去掉误差最大的一两张图像fx 与 fy 多次标定偏差小于 0.5%检查镜头是否松动或光圈漂移角点覆盖范围图像四角和中心都要出现补充对应区域的标定图主点 cx, cy 与图像中心偏差不超过 5% 图像宽度检查感光芯片是否倾斜参与标定图像数15 到 30 张太少时内参受单帧噪声影响大如果重投影误差 RMS 在 0.2 像素以上先不要急着往下做。常见的错误是棋盘格贴在不平的纸板上或者某几张图的角点被误检。可以打印出每一帧的角点检测图挑出角点连线异常的那几张删掉再重跑。3. 激光线扫标定提取激光条纹中心线拟合激光平面相机标定做完接下来处理激光。激光线扫标定的目标很明确求出一个在相机坐标系下的激光平面方程。之后相机图像中任意一个落在激光条纹上的像素点沿着相机射线反向投影到这个平面交点就是它的三维坐标。3.1 激光线扫标定的数学模型射线与平面求交一字线激光器发出的光在空间中近似形成一个平整的光平面平面方程可以写成aX bY cZ d 0这个平面在相机坐标系里是固定的。标定时让激光线投射到棋盘格表面棋盘格平面的位姿由相机标定得到的外参确定。激光条纹像素与棋盘格三维平面的交点就是激光平面上的一组采样点。采样点覆盖不同距离和角度后用平面拟合算法就能解出a,b,c,d。关键点在于激光条纹像素本身只是二维点但它对应相机光心发出的一条射线。射线先和棋盘格平面求交得到三维点这个三维点又落在激光平面上所以属于激光平面。只要采集足够多姿态比如标定板前后倾斜、左右移动就能拟合出完整激光平面。3.2 同一姿态拍两张图一张关激光一张开激光激光线打在棋盘格上会盖住角点直接在激光图像上找角点经常漏检。更稳的做法是同一个标定板位姿下拍两张图一张关闭激光一张打开激光。关闭激光的图像只用来检测棋盘格角点打开激光的图像只用来提取激光条纹中心。import cv2 import numpy as np # 同一个位姿下的两张图 laser_off cv2.imread(laser_off.png, cv2.IMREAD_GRAYSCALE) laser_on cv2.imread(laser_on.png, cv2.IMREAD_GRAYSCALE) # 在关闭激光的图像里找棋盘格角点 ret, corners cv2.findChessboardCorners(laser_off, (11, 8), None) if ret: criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) corners cv2.cornerSubPix(laser_off, corners, (11, 11), (-1, -1), criteria) # 差分后提取激光条纹背景留在原图里的部分会被减掉 diff cv2.absdiff(laser_on, laser_off) _, threshold_mask cv2.threshold(diff, 40, 255, cv2.THRESH_BINARY) # 按列计算灰度质心得到亚像素位置的条纹中心 pixel_points [] for col in range(diff.shape[1]): column_data diff[:, col] ys np.where(column_data 40)[0] if len(ys) 3: continue weights column_data[ys].astype(np.float32) center_y np.sum(weights * ys) / np.sum(weights) pixel_points.append([col, center_y]) pixel_points np.array(pixel_points, dtypenp.float32)差分的作用是把镜头固定噪声和背景弱反光去掉。阈值 40 需要根据实际图像灰度调整激光线在图像中通常会超过 150背景低于 60 时这个值比较安全。如果激光条纹被棋盘格黑格吸收导致中间断裂可以适当降低阈值但不能低到 10 以下否则残留边缘会混进中心计算。按列提取灰度质心对斜线同样有效。每一列只有一个候选亮带时质心位置能精确到亚像素同一列出现两条亮带说明存在镜面反射该列应该剔除。3.3 把激光条纹像素映射到棋盘格三维平面有了角点和激光条纹像素下一步用solvePnP求出当前棋盘格在相机坐标下的位姿再把每个像素沿射线投影到棋盘格平面得到三维点。# 棋盘格角点对应的三维坐标 objp np.zeros((11 * 8, 3), dtypenp.float64) objp[:, :2] np.mgrid[0:11, 0:8].T.reshape(-1, 2) * 30.0 # 求棋盘格位姿 ret, rvec, tvec cv2.solvePnP(objp, corners, mtx, dist, flagscv2.SOLVEPNP_ITERATIVE) R, _ cv2.Rodrigues(rvec) # 棋盘格平面在相机坐标下的法向量和到光心的距离 plane_normal R[:, 2] # 棋盘格坐标系的 Z 轴方向 plane_distance plane_normal.dot(tvec.reshape(3)) # 平面方程 normal·P distance fx, fy mtx[0, 0], mtx[1, 1] cx, cy mtx[0, 2], mtx[1, 2] laser_3d_points [] image_points_for_validation [] for u, v in pixel_points: # 像素反投影成相机射线方向 ray_dir np.array([(u - cx) / fx, (v - cy) / fy, 1.0]) ray_dir ray_dir / np.linalg.norm(ray_dir) # 射线与棋盘格平面求交t distance / (normal·dir) denom plane_normal.dot(ray_dir) if abs(denom) 1e-8: continue t plane_distance / denom if t 0: continue pt_cam t * ray_dir laser_3d_points.append(pt_cam) image_points_for_validation.append([u, v]) laser_3d_points np.array(laser_3d_points) image_points_for_validation np.array(image_points_for_validation)plane_distance的符号取决于棋盘格法线方向公式是plane_normal.dot(tvec)。只要棋盘格在相机前方计算出的t通常为正如果出现大量负数点说明标定板在相机后面或者外参解算不对需要回头检查objp的角点排序。denom接近 0 表示这条射线几乎平行于棋盘格平面交点非常不稳定应该直接丢弃。这种情况发生在激光线横着扫过标定板侧面时需要在采集阶段避免。3.4 用 RANSAC 拟合激光平面参数采集多个位姿后把所有帧得到的laser_3d_points合并成一个数组然后用 RANSAC 拟合平面。这一步不需要一次性解全局最小二乘先用 RANSAC 剔除镜面反射和体误差较大的外点。def fit_plane_ransac(points, distance_threshold0.2, iterations1000): best_plane None best_inliers 0 for _ in range(iterations): # 随机选 3 个点确定一个候选平面 idx np.random.choice(len(points), 3, replaceFalse) sample points[idx] centroid sample.mean(axis0) # 对样本做奇异值分解最小奇异值对应的向量就是平面法向量 _, _, vt np.linalg.svd(sample - centroid) normal vt[-1] d -normal.dot(centroid) # 计算所有点到候选平面的距离 dist np.abs(points normal d) inliers np.sum(dist distance_threshold) if inliers best_inliers: best_inliers inliers best_plane np.array([normal[0], normal[1], normal[2], d]) return best_plane, best_inliers plane, inliers fit_plane_ransac(laser_3d_points, distance_threshold0.2) print(激光平面方程系数:, plane) print(内点数量 / 总点数量:, inliers, /, len(laser_3d_points))代码里的distance_threshold0.2单位是毫米因为之前棋盘格尺寸用了毫米。这个阈值要和激光条纹中心提取精度匹配通常取 0.1 到 0.3 mm。如果明显大于这个范围先检查是不是标定板不平整而不是盲目放宽阈值。平面拟合后要重新解一遍最小二乘平面只用内点这样得到的系数更稳定。系数不做归一化也不影响后续使用只要在计算时保持同一尺度即可。4. 相机与激光联合标定的实操流程和参数设置从采集到拟合一次跑通理论方法和代码都齐了剩下的是把整个流程串起来并处理现场最容易翻车的几个问题。4.1 安装、曝光与滤光片先解决反光和过曝激光线扫标定最容易失败在图像质量。激光器、相机和标定板之间的相对位置一旦固定就要检查激光条纹在棋盘格上的亮度分布。金属标定板容易产生镜面反射图片中会出现第二道虚影这些区域会被 CFD 当作外点。优先用哑光白色陶瓷标定板或者给相机镜头加窄带滤光片只让激光波长通过。曝光时间要锁死不能打开自动增益和自动曝光。可以把曝光时间设到 2 到 5 ms让激光条纹灰度峰值在 180 到 220 之间背景棋盘格灰度低于 80。如果激光亮度太高导致中心饱和条纹中心会被拉偏可能偏出真实位置 1 个像素以上。4.2 批量采集用视频抽帧而不是一张张手工拍手动一张张拍标定图很费时还容易带入运动模糊。常见做法是录制一段标定视频关闭激光时拍一段棋盘格运动视频打开激光时再拍一段同样轨迹的视频最后按帧号对齐抽帧。# 安装 ffmpeg 后从视频中每 25 帧抽取一帧 mkdir -p calib_images # 激光关闭的视频 ffmpeg -i laser_off.mp4 -vf selectnot(mod(n\,25)) -vsync vfr calib_images/off_%04d.png # 激光打开的视频 ffmpeg -i laser_on.mp4 -vf selectnot(mod(n\,25)) -vsync vfr calib_images/on_%04d.png抽帧后用序号一一对应例如off_0001.png对应on_0001.png。视频帧率一旦确定25 帧抽一帧大约每秒抽 2 张标定板移动速度要慢确保抽出来的图没有运动模糊。对于帧率 30fps 的视频标定板快速转动反而会让角点亚像素定位失败。4.3 参数设置参考表下面的表格是实践中的经验值不是严格的物理公式适合绝大多数工业相机加红色一字激光器的组合。参数推荐区间说明棋盘格内角点数11x8 或 12x9角点数太少平面约束不足棋盘格方格边长10 到 30 mm单位与三维重建结果一致选毫米标定板位姿数量10 到 15 个覆盖近、远、左、右、前倾、后倾激光条纹宽度2 到 5 像素太窄对焦不准太宽质心抖动增大差分阈值30 到 60 灰度根据激光与背景亮度差调整RANSAC 阈值0.1 到 0.3 mm与三维点噪声水平匹配激光灰度峰值180 到 220避免过曝和半饱和标定板位姿是最容易被低估的变量。只把标定板放在正前方拟合出的激光平面参数在远端外推时会偏出去。必须至少有一组图让激光打在一半棋盘格上另外半边棋盘格仍然可见这样才能保证射线和激光平面都受到约束。4.4 常见失败现象和调整顺序角点检测失败占比高先关掉激光在laser_off图上检测如果仍然失败检查光照是否均匀棋盘格是否反光。激光条纹断裂调高曝光或降低差分阈值如果只有黑格处断裂说明标定板表面漫反射不足换浅色标定板。RANSAC 内点比例低于 60%大概率是激光反射或者棋盘格平面不平先查看外点分布位置不要直接改阈值。平面拟合误差在图像边缘偏大说明标定图没有覆盖对应视场回到采集步骤补充边缘位姿。重投影误差正常但三维尺寸不对检查square_size和objp的尺度单位毫米、厘米混用是常见原因。把这些问题按“图像质量、角点精度、平面模型、尺度单位”的顺序排查比随机调阈值有效得多。5. 标定完成后用重投影验证再对激光平面做联合优化激光平面标定完成的标志不是“跑出了系数”而是误差可量化。最常见的验证方法是把三维点重新投影回图像和最初的激光条纹像素做对比。代码逻辑很简单对某个激光条纹三维点用相机内参和零外参执行projectPoints得到重投影像素坐标再计算与提取像素坐标的欧氏距离。def reproject_3d_point(pt_cam, mtx, dist): rvec np.zeros((1, 3), dtypenp.float32) tvec np.zeros((1, 3), dtypenp.float32) image_pts, _ cv2.projectPoints( pt_cam.reshape(1, 1, 3), rvec, tvec, mtx, dist ) return image_pts.reshape(2) for pt_3d, pt_pixel in zip(laser_3d_points, image_points_for_validation): reproj reproject_3d_point(pt_3d, mtx, dist) error np.linalg.norm(reproj - pt_pixel) if error 0.8: print(像素:, pt_pixel, 重投影:, reproj, 误差:, error)单帧验证只能说明当前标定板位姿下拟合是自洽的。更可靠的做法是把所有帧的数据放在一起做一次全局优化优化变量是每帧棋盘格外参和激光平面四个系数目标函数是棋盘格角点重投影误差加激光条纹点到激光平面的距离。内参先固定迭代几轮后再放开相机内参精调。很多开源相机雷达联合标定工具也是这个思路只是把激光雷达点云换成了激光线扫描点。如果你的系统里还接了 IMU下一步是相机 IMU 联合标定把惯性坐标系和相机坐标系对齐。那和激光平面优化是两层问题不要混在一次求解里。我一般会在每次标定结束后把内参矩阵、畸变系数、激光平面方程、重投影误差和标定板尺寸全部写进一个 YAML 文件文件名按当天日期命名后续如果设备运输或碰撞后精度下降就可以拿这个文件做基准对比误差重新变大时只重新标定激光平面不必重复整条流水线。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价