ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

相机归一化全解析:像素坐标、内参焦距与深度图可视化

相机归一化全解析:像素坐标、内参焦距与深度图可视化 做过视觉测量或者相机标定的朋友应该都遇到过这种场景从标定程序里拿到内参矩阵fx、fy都是上千甚至几千的数值cx、cy也常常跟图像中心差不少心里犯嘀咕——这些数到底是什么单位怎么换算成毫米又或者在调 OpenCV 里的undistortPoints明明输入的是像素坐标输出的却不是像素也不是真实物理单位而是一组看不懂的小数。如果你在这个问题上卡过大概率是被“相机中的归一化”这个概念绕住了。这篇文章我想从工程角度把相机里的几个“归一化”彻底拆开像素坐标怎么归一化成相机坐标、内参矩阵里的归一化焦距是什么、畸变校正和深度图可视化里的归一化又是另一回事。比较适合正在做相机标定、双目视觉、工业外观检测或者刚开始折腾 D435i、奥比中光这类深度相机的朋友。读完你至少能回答那几个常见疑问fx为什么是几千undistortPoints输出的是什么坐标深度图的 0 到 255 到底是怎么映射出来的1. 先理清楚相机里的“归一化”不是一个意思1.1 三种常见的归一化我见过不少人在同一个项目里把“归一化”这个词用混了最后代码能跑但换一组数据就出问题。在相机这个上下文里至少有三种完全不同的归一化像素灰度归一化把图像从 0~4095、0~65535 之类的原始灰度范围映射到 0~255 或者 0.0~1.0 的范围内。这是图像预处理里最常见的一种和硬件无关纯粹是数据处理。坐标归一化把图像上的像素坐标单位是像素通过内参矩阵转换到相机前方的归一化平面上得到的坐标单位可以理解为“无量纲的相机坐标”。这是三维视觉、相机标定的核心。深度值归一化把深度相机输出的毫米/米为单位、16位存储的原始深度数据映射成 8 位灰度图用于可视化。它不改变真实物理距离只是解决“显示出来一片黑”的问题。如果把这三种归一化当成同一种东西去套公式很容易在标定、去畸变、深度显示各种环节里绕晕。下文分别展开先说最容易被忽视、但也最核心的坐标归一化。1.2 一条完整的成像链路一个三维空间点是怎么变成图像上像素坐标的简单说走四步世界坐标系里的点通过旋转和平移变换到相机坐标系。这里相机坐标系的 Z 轴指向相机正前方。相机坐标系里的点P [X_c, Y_c, Z_c]投影到归一化平面上得到归一化坐标x_n X_c / Z_cy_n Y_c / Z_c。归一化坐标通过内参映射到像素坐标u fx * x_n cxv fy * y_n cy。像素坐标 (u, v) 就是你在图像里看到的那个点。注意第 2 步里的“归一化平面”其实是一个虚拟的、位于相机前方Z 1处的平面。可以把相机想象成一个小孔所有光线穿过光心后在感光元件上成像不管物体在 1 米外还是 100 米外只要它相对于光心的方向一致它在归一化平面上的坐标就是一致的区别只在于像素坐标是fx*x_n cx而不是别的。用一个生活化的类比理解“去掉距离的信息”你站在笔直的马路中间看远处的路牌路牌在你视网膜上成像的位置只取决于它相对你的方向和距离关系不大。坐标归一化就是只保留这个方向信息丢掉距离信息。等后续要重建三维点时再引入深度Z_c把它还原出来。2. 内参矩阵里的 fx、fy为什么叫归一化焦距2.1 从物理焦距到像素焦距相机内参矩阵长这样K [fx 0 cx] [ 0 fy cy] [ 0 0 1]很多人一开始不理解fx为什么动不动就是 1000 多。它不是镜头的物理焦距而是物理焦距除以像元尺寸得到的“像素焦距”fx f / dx fy f / dy其中f是镜头的物理焦距单位毫米dx、dy是单个像素在感光元件上的物理尺寸单位是毫米/像素。比如一颗 6mm 镜头配了一个像元尺寸 3.45μm 的相机fx ≈ 6 / 0.00345 ≈ 1739。你看这个数值量级跟标定出来的结果对得上。那为什么叫“归一化焦距”因为它干了这么一件事把归一化平面上的一个无量纲坐标乘以这个焦距就得到了像素坐标里的偏移量。换个说法在归一化坐标和像素坐标之间的换算关系里fx、fy起着“尺度因子”的作用。它把虚拟的、没有物理单位的归一化平面映射到真实感光元件的像素坐标系上。2.2 像素差和角度的关系fx的另一个实用性理解每个像素对应的归一化平面上的角度宽度约为1/fx弧度。比如fx 1000那么一个像素大约对应 1/1000 弧度也就是约 0.057 度。反过来如果你想计算某个物体偏离光轴多少角度可以用角度 atan((u - cx) / fx)这个公式在视觉伺服里很常用。比如目标在图像上的像素坐标是 (840, 480)图像分辨率是 1280x720主点约 (640, 360)fx是 800那么它相对光轴的水平夹角就是atan((840-640)/800) atan(0.25) ≈ 14 度。这个计算本质上就是在做坐标归一化只不过最后又转成了角度。2.3 主点 cx、cy 也不是图像中心cx、cy是主点光轴穿过像平面的位置的像素坐标它不一定在图像正中心。装配误差、镜头安装偏移都会让它偏离中心几十个甚至上百个像素。标定出内参后最好的做法就是直接使用标定值而不是硬编码成(width/2, height/2)。我做过一个小孔成像模型的自写代码一开始图省事把cx设成图像中心结果做单目测距误差在画面边角处达到 3% 以上后来改成标定值误差就压下来了。这个差异在中心区域不明显越往边缘越明显尤其是广角镜头。3. 归一化坐标在畸变校正里的实际位置3.1 畸变模型是在归一化坐标上做的很多人在 OpenCV 里去畸变只知道调undistort函数不清楚它内部到底在干什么。其实畸变模型作用在归一化坐标上不是在像素坐标上直接加减。假设一个三维点投影到归一化平面得到归一化坐标(x_n, y_n)然后计算半径r^2 x_n^2 y_n^2。切向畸变和径向畸变公式如下x_distorted x_n * (1 k1*r^2 k2*r^4 k3*r^6) 2*p1*x_n*y_n p2*(r^2 2*x_n^2) y_distorted y_n * (1 k1*r^2 k2*r^4 k3*r^6) p1*(r^2 2*y_n^2) 2*p2*x_n*y_n注意公式里的x_n、y_n就是归一化坐标。畸变参数k1、k2、k3、p1、p2也是在归一化坐标这个尺度上标定出来的。如果在像素坐标层面直接做非线性变形公式会复杂得多在归一化平面上做等于剥掉内参尺度只保留镜头的纯几何畸变。这点非常关键做去畸变时要先把像素坐标转成归一化坐标在归一化平面上做畸变校正然后如果需要再映射回像素坐标。顺序错了结果就是边缘更奇怪。3.2 OpenCV 的 undistortPoints 里面发生了什么OpenCV 的undistortPoints输出是什么很多人被它坑过。直接看文档undistortPoints(src, dst, cameraMatrix, distCoeffs)如果只传输入像素点、内参、畸变系数输出是去畸变后的归一化坐标不是像素坐标。也就是说输出值通常是小数值和你的内参fx、fy无关。如果想要去畸变后的像素坐标需要加上参数P并把P设为内参矩阵这样函数会在最后一步把归一化坐标通过P映射回像素坐标。Python 示例import cv2 import numpy as np K np.array([[800, 0, 640], [0, 800, 360], [0, 0, 1]], dtypenp.float64) dist np.array([-0.2, 0.1, 0.01, 0.005, 0.0], dtypenp.float64) pts np.array([[[650, 380]]], dtypenp.float64) # 只做去畸变输出归一化坐标 undist_norm cv2.undistortPoints(pts, K, dist, PNone) print(归一化坐标:, undist_norm) # 映射回像素坐标 undist_pix cv2.undistortPoints(pts, K, dist, PK) print(像素坐标:, undist_pix)我刚开始用这个函数时不加P拿结果去算真实距离怎么都不对后来才意识到输出是归一化坐标不是像素坐标。这个函数在不同版本的 OpenCV 里行为也略有差异最稳妥的方式是显式传入PK。3.3 工业视觉标定里的归一化坐标在工业场景里VisionMaster、Halcon 这些软件里做 9 点标定或者手眼标定内部也普遍走归一化坐标。以 9 点标定为例机械手带着相机在不同位置拍同一个 Mark 点或者在固定位姿下移动 Mark 点记录像素坐标和机械手坐标。如果直接用带畸变的原始像素坐标去拟合仿射变换边缘点的误差会被放大。正确的做法是先用标定出的内参和畸变系数把像素坐标去畸变本质上就是转化到归一化平面再做反向映射再去拟合机械手坐标和图像坐标之间的变换矩阵。我之前在一个项目里因为偷懒没用去畸变直接拿原始像素坐标做了 9 点标定结果在视场边缘误差超过 2mm后来改成先去畸变再标定整体误差压到了 0.3mm 以内。建议所有做视觉引导定位的朋友在标定流程里都保留“先归一化去畸变”这一步。4. 图像灰度归一化工业视觉里避不开的预处理4.1 为什么要做灰度归一化和坐标归一化不同图像灰度归一化解决的是“亮度尺度不一致”的问题。同一台工业相机在不同曝光时间、不同光源强度下拍同一块工件灰度均值可能差 30% 以上。模板匹配算法对绝对灰度很敏感如果不做归一化同一个工件的匹配分数可能从 0.98 掉到 0.6甚至匹配失败。4.2 常见的几种实现方式1. 固定范围缩放最直接的方法是把原始灰度线性映射到目标范围import cv2 import numpy as np def normalize_fixed(img, src_min0, src_max4095): # 假设是12位相机0~4095 img_norm (img.astype(np.float32) - src_min) / (src_max - src_min) img_norm np.clip(img_norm, 0.0, 1.0) return (img_norm * 255).astype(np.uint8)2. 均值方差归一化如果图像的灰度整体漂移比如光线缓慢变化可以用均值方差归一化def normalize_mean_std(img): img img.astype(np.float32) mean img.mean() std img.std() if std 1e-6: return np.zeros_like(img, dtypenp.uint8) img_norm (img - mean) / std img_norm np.clip(img_norm * 50 128, 0, 255) # 缩放到0~255 return img_norm.astype(np.uint8)这里*50128是个人经验值把标准正态分布映射到大约 0~255 的范围内。如果你做深度学习输入预处理通常不转成 0~255直接归一化到 0.0~1.0。3. 百分位截断归一化工业图像经常有高光、反光直接按最大最小值缩放会把整体亮度压得很暗。更稳的做法是取灰度直方图的 2% 和 98% 分位作为上下界再做线性映射def normalize_percentile(img, low_perc2, high_perc98): low np.percentile(img, low_perc) high np.percentile(img, high_perc) if high low: return np.zeros_like(img, dtypenp.uint8) img_norm (img.astype(np.float32) - low) / (high - low) img_norm np.clip(img_norm * 255, 0, 255) return img_norm.astype(np.uint8)实测下来在金属表面缺陷检测场景里百分位截断比固定范围缩放更稳因为金属反光导致直方图尾巴很长固定范围很容易把暗部细节压没。4.3 灰度归一化的边界灰度归一化不是万能的有三点需要注意过曝和欠曝是无法靠归一化救回来的。归一化本质是线性拉伸如果传感器已经饱和了信息已经丢失拉伸没用。做特征匹配时灰度归一化和坐标归一化是两码事不要混用。一个在图像处理层面一个在几何计算层面。不是所有算法都需要灰度归一化。基于边缘的匹配算法对灰度变化不敏感除非你是做灰度模板匹配或者像素级缺陷检测否则可以先不做归一化省一点处理时间。5. 深度图的归一化D435i、奥比中光这类深度相机输出怎么处理5.1 原始深度图不是 0~255很多玩 D435i 或者奥比中光深度相机的朋友第一次读取深度数据时发现图像几乎全黑第一反应是相机坏了。其实没坏而是深度图的数据格式和你想的不一样。深度图的原始数据一般是uint16单位是毫米有的相机是 0.1mm。比如距离相机 1 米的物体它的像素值是 1000距离 3 米的物体像素值是 3000。把 1000 这个数值直接显示在 8 位灰度图上最高 255自然就是全白或近乎全黑。要显示成能看的深度图必须把深度值“归一化”到一个合理的可视范围。5.2 深度可视化的三种方案方案一近远距离线性映射这是最直观的。设定一个最大显示距离max_dist超过它的值直接截断def depth_to_8bit(depth, max_dist_mm3000): depth np.clip(depth, 0, max_dist_mm) img_8bit (depth.astype(np.float32) / max_dist_mm * 255).astype(np.uint8) return img_8bit这种方法适合近距离场景比如桌面抓取、人脸识别因为目标距离比较集中线性映射可以保留细节。方案二逆深度归一化逆深度1/depth更接近人类对距离的感知也能拉开近距离物体的差异。适合做 SLAM 或者避障因为近距离障碍物才是最重要的def depth_to_inverse(depth, eps1e-6): depth depth.astype(np.float32) inv np.zeros_like(depth) mask depth 0 inv[mask] 1.0 / depth[mask] # 逆深度范围通常是0.001到1左右再做归一化显示 inv_norm np.clip(inv * 1000, 0, 255) return inv_norm.astype(np.uint8)方案三对数映射对数映射可以压缩动态范围让远近距离都能看到一定细节def depth_to_log(depth): depth depth.astype(np.float32) depth_safe np.where(depth 0, depth, 1) log_depth np.log1p(depth_safe) # log(1depth) log_depth np.clip(log_depth, 0, np.log1p(65535)) return (log_depth / np.log1p(65535) * 255).astype(np.uint8)具体用哪种要看场景。我的经验是如果只是给现场调试人员看的近距离线性映射最直观如果要作为后续分割、抓取算法的输入预处理逆深度往往效果更好。5.3 深度归一化容易踩的坑第一不要直接取深度图的uint16高字节当 8 位图。很多人图省事把深度图astype(np.uint8)发现图像里全是条纹和碎点这是因为高字节和低字节的二进制分布不代表距离渐变关系。第二深度图里的 0 是无效值不管是无效像素还是测不到距离的位置直接归一化会把 0 变成黑色和近距离的黑色混在一起必须先用 mask 处理。第三深度相机是主动光测量室内环境下光照变化对深度值影响不大所以深度图不需要做灰度归一化加了反而会引入伪影。6. 特征点坐标归一化容易被忽略的精度隐形杀手6.1 为什么八点法之前要先归一化特征点做双目视觉或者运动恢复结构时经常要估计基础矩阵 F 或者本质矩阵 E最经典的方法是八点法。但你知道为什么教材里都强调“先归一化坐标再做八点法”吗因为像素坐标的数量级通常在上千而构造线性方程组时这些大数值会造成矩阵条件数很差。Hartley 在 1997 年的论文里给出了明确结论先对特征点做平移、缩放归一化再求解数值稳定性会大幅提升估计精度可以提升几个数量级。这不是可有可无的优化而是必要的预处理。6.2 常用的归一化方法具体做法是对一组二维点集先减去质心坐标再缩放到所有点到质心的平均距离为sqrt(2)import numpy as np def normalize_points(pts): # pts: (N, 2) 像素坐标 centroid np.mean(pts, axis0) pts_centered pts - centroid mean_dist np.mean(np.linalg.norm(pts_centered, axis1)) scale np.sqrt(2.0) / mean_dist T np.array([[scale, 0, -scale * centroid[0]], [0, scale, -scale * centroid[1]], [0, 0, 1]], dtypenp.float64) pts_norm (T np.vstack([pts.T, np.ones(len(pts))])).T return pts_norm[:, :2], T之后求解出的基础矩阵F_norm再还原回去F T2^T * F_norm * T1这里的T1、T2分别是两组特征点各自对应的归一化矩阵。顺序别搞反。6.3 实测对比我之前用一段仿真数据做过对比生成一组带噪声的匹配点添加高斯噪声分别用不归一化的八点法和归一化八点法估计基础矩阵。用极线距离均方误差做评价结果很明显方法极线距离误差像素说明原始像素直接八点法1.87数值不稳定容易受噪声影响归一化八点法0.42精度提升约 4 倍在真实场景里如果图像分辨率是 1920x1080不归一化直接算甚至可能求出错误的 F 矩阵极线方向都不对。这不是算法不行是数值条件太差了。6.4 PnP 求解里的归一化相机位姿估计里常见的 PnP 问题也是一样。用直接线性变换求解时如果 3D 点和 2D 像素坐标都没有归一化会严重偏向数值大的方向。OpenCV 的solvePnP内部做了归一化处理但如果你自己在 MATLAB 或者 NumPy 里实现 DLT一定记得先归一化坐标。3D 点可以做相似变换归一化质心移到原点尺度缩放到平均距离为 12D 点按照上一节的方法归一化。求解完成后再把旋转平移还原到原坐标系下。这个细节在“相机 IMU 联合标定”、“手眼标定”这类多步求解的工程里尤其重要。累积起来的一点数值误差经过多次变换后会被放大最终导致标定外参误差超标。最后再分享一个我个人养成的习惯在视觉工程里凡是输入到几何计算模块的坐标都先通过内参把像素坐标归一化在归一化坐标层面做几何运算凡是输入到图像处理模块的灰度值都在确认无过曝的前提下做灰度归一化。两个“归一化”分清楚很多玄学问题都能在调试阶段直接消灭掉。如果你接下来要碰相机标定、双目立体匹配或者手眼标定建议把代码里所有坐标转换的中间量都打印出来看一眼你会对“归一化坐标”这个词有更直观的感受。
返回列表