先看两台传感器对同一场景的读数差别。相机拍下一帧,一个像素只能说明"这个方向上有个东西", 说不出它有多远;LiDAR 扫一圈,每个读数自带距离,一个读数就对应一个确定的 3D 点。 差别可以收成一条判据:这个传感器的输出是不是「降维且不可逆」的。
相机的读数: (u, v) 像素
→ 一个像素 = 一条射线 = 无穷多个 3D 点
→ 【不可逆】,丢掉了深度
→ 必须靠"投影参数"才能把像素还原成射线
LiDAR 的读数: (ring, azimuth, range)
→ range 就是深度!
→ 一个读数 = 一个确定的 3D 点
→ 【可逆】,只需要坐标变换参数,不需要"投影参数"
「投影参数」存在的前提,是传感器测不到深度。
相机把 3D 世界"拍扁"成 2D 图像,压掉了深度这一维。 投影参数就是用来描述"这次压扁是怎么压的",补足降维损失的那部分信息。
| 传感器 | 输出 | 测深度? | 可逆? | 有投影参数? |
|---|---|---|---|---|
| 针孔相机 | (u,v) | ❌ | ❌ | ✅ 必须有 |
| 鱼眼 / 全景相机 | (u,v) | ❌ | ❌ | ✅ 必须有,还要指定投影模型 |
| 深度相机 RGB-D | (u,v) + depth | ✅(自己测的) | ✅ | ✅ 有(用于反投影) |
| LiDAR (3D) | 角度 + 距离 | ✅ | ✅ | ❌ 只有坐标变换参数 |
| LiDAR (2D) | 角度 + 距离 | ✅ | ✅ | ❌ |
| 毫米波雷达 | 距离+方位+多普勒 | ✅ | ✅ | ❌(但有角度标定) |
| 超声波 | 只有距离 | ✅ | ❌ 连方向都没有 | ❌ |
| IMU / 编码器 / 力 / 触觉 | 标量 / 向量 | — | — | ❌ 与投影无关 |
LiDAR 的原始测量:
(ring_index, azimuth_index) → range
↑ ↑
垂直索引 水平索引
= 一个【2D 数组】! // 结构上和一个图像一模一样
区别在于:
图像的 2D 数组: (u, v) → 【没有】深度 → 不可逆 → 需要投影参数
LiDAR 的 2D 数组: (ring, az) → 【有】range → 可逆 → 不需要
所以 LiDAR 的输出也叫 "range image"(距离图像),它长得像图,但每个像素自带深度。 这就是为什么相机需要标定一堆参数、LiDAR 只需要一个外参。
P,一个 3×4 矩阵Foxglove 的 CameraCalibration schema 里,有个字段的名字就叫"投影矩阵":
P : float64[12] —— "Projection/camera matrix (3x4 row-major matrix)"
⌊ fx' 0 cx' Tx ⌋
P = | 0 fy' cy' Ty |
⌊ 0 0 1 0 ⌋
给定 3D 点 [X Y Z]¹,投影到(校正后)图像的 (x, y):
[u v w]¹ = P · [X Y Z 1]¹ x = u/w y = v/w
P 把内参和外参一次性合成了一个矩阵,这就是 KITTI 里的 P_rect。
K / R / P 三个矩阵,最容易搞混| 字段 | 名字 | 是什么 |
|---|---|---|
K | Intrinsic camera matrix (3×3) | 原始(带畸变的)图像的内参 |
R | Rectification matrix (3×3) | 立体校正旋转,让两台相机的极线平行 |
P | Projection matrix (3×4) | 校正后图像的内参 + 立体基线平移 |
D | Distortion parameters | 畸变系数 |
官方原文说得很清楚:K 是 raw (distorted) 图像的内参;
P 的左边 3×3 部分是校正后(rectified)图像的内参,
"这些值可能与 K 中的不同"。单目相机通常 Tx = Ty = 0,
R = 单位阵,P[:, :3] = K。
物理镜头 → K + D 描述(真实存在的那个相机)
↓ 立体校正(把图像重投影到理想平行极线的虚拟像平面)
虚拟相机 → P 描述(你实际拿去算几何的那个)
K + DP这正是"投影参数"和"内参"不是一回事的原因,P 描述的是一个可能不存在的虚拟相机。
① 相机坐标系约定(Foxglove 官方原文):frame_id 字段说明里明确写了
"The origin of the frame is the optical center of the camera. +x points to the right in the image,
+y points down, and +z points into the plane of the image.",原点在光心,x 右、y 下、z 指进画面。
② K[0] == 0 表示"未标定":Foxglove 沿用了 ROS 的约定
"Foxglove also treats K[0] == 0.0 as indicating an uncalibrated camera, and calibration data will be ignored."
这是个真实会咬人的坑:如果你的投影怎么都不对,先看一眼 K 是不是全零,被忽略了,不报错。
Foxglove 支持的畸变模型(很好的证据):
distortion_model | 参数 | 适用 |
|---|---|---|
plumb_bob | k1, k2, p1, p2, k3(5 个) | 普通镜头 |
rational_polynomial | k1…k8(8 个) | 超广角 |
kannala_brandt | k1, k2, k3, k4(4 个) | 鱼眼,支持到 180° FOV |
fisheye62 | k0…k3, p0, p1 (+crit_theta) | 超过 180° 的鱼眼(Project Aria 的模型) |
注意最后两个是"投影模型",不是"畸变模型",这是两回事:
普通镜头: 先用针孔模型投影(x = X/Z),再叠加小畸变修正
鱼眼镜头: 针孔模型【根本不成立】(180° 的视场没法用 x=X/Z 表示)
→ 必须换一个"球面 → 平面"的映射函数本身
"投影"(projection)这个词在相机领域,和"地图投影"是同一个词。
地球是球面 → 地图是平面 → 必须选一种"投影方式"
墨卡托 / 等积 / 等距 …
方向的球面 → 图像是平面 → 也必须选一种"投影方式"
等距 / 等积 / 等立体角 / 立体投影 …
鱼眼镜头的几种模型就是不同的球面→平面映射:
等距投影 (equidistant): r = f·θ
等立体角 (equisolid): r = 2f·sin(θ/2)
正交投影 (orthographic): r = f·sin(θ)
立体投影 (stereographic): r = 2f·tan(θ/2)
每种都是一个不同的 r(θ) 函数,这就是"投影模型"。
所以鱼眼镜头的"投影参数"里,选对模型比调数字更重要。
具体画面:一辆车装了四台鱼眼镜头做 360° 环视,标定文件里躺着一串数字。 这串数字分成三堆,描述镜头自己怎么成像的(内参)、 描述它相对车身装在哪、朝向如何(外参)、 以及把像素还原成射线要用的那套规则(投影参数)。 下面这张表先给总览,后面逐项展开。
| 是什么 | 具体内容 | 数量 | |
|---|---|---|---|
| 内参 | 相机自身的映射 | fx, fy, cx, cy + k1, k2, k3, k4 + "kannala_brandt" | 8 个数 + 1 个字符串 |
| 外参 | 相机相对于谁 | [R | t](4×4),例如 T_cam_lidar | 6 自由度 |
| 投影参数 | ⚠️ 鱼眼没有单一的投影矩阵,只能拆成「内参 + 模型名 + 外参」三件套 | — | |
fx = 600.0 fy = 600.0 // 焦距(像素)
cx = 960.0 cy = 960.0 // 主点(光心在图像上落在哪)
k1 = -0.05 k2 = 0.01
k3 = -0.002 k4 = 0.0005 // 投影/畸变系数
distortion_model = "kannala_brandt" // 必须一起给,不能省
| 针孔 | 鱼眼 | |
|---|---|---|
| 系数个数 | k1 k2 p1 p2 k3(5 个,含切向 p1 p2) | k1 k2 k3 k4(4 个,纯径向) |
| skew | 有时有 | 没有 |
| 模型名 | 一般默认 plumb_bob | 必须显式给 |
fx 和针孔的 fx 含义不一样针孔: u - cx = fx · (X/Z) // fx 是【像素 / 单位斜率】
鱼眼: u - cx = fx · θ_d // fx 是【像素 / 弧度】
同一个符号,物理意义不同。小角度时两者几乎相等,所以看不出来;大角度时分道扬镳:
θ(离轴角) 鱼眼 针孔 比值
------------------------------------------------------
5° 52.3px 52.5px 0.997 ← 几乎一样
10° 104.6px 105.8px 0.988
20° 208.2px 218.4px 0.953
30° 310.1px 346.4px 0.895 ← 开始分道
45° 458.3px 600.0px 0.764
60° 600.2px 1039.2px 0.578
75° 736.7px 2239.2px 0.329
89° 863.4px 34374.0px 0.025
90° 872.7px ∞ ← 针孔发散
------------------------------------------------------
所以:把鱼眼的 fx 塞进针孔公式,中心附近看不出问题,画面边缘会错得离谱。
鱼眼在外参上的特别之处不在"定义",而在"使用场景":
| 场景 | 外参需求 |
|---|---|
| 单目鱼眼 + LiDAR | 1 个外参 |
| 车载环视(4~6 个鱼眼) | 每个相机一个外参,且需要统一的参考系 |
| 全景拼接 | 所有相机外参 + 重叠区对齐 |
车载环视是鱼眼的主场,因为要用尽量少的相机覆盖 360°,必须用超广角。
用一个定义性质就能证明:3×4 投影矩阵的本质是「射影变换」, 而射影变换的定义性质是把直线映射成直线。所以只要找到一条 3D 直线, 投影后变弯了,就证明它不可能是任何 3×4 矩阵。
【偏离光轴的直线 (y=1.5, z=3, x:-6..6)】
相机模型 离直线最大偏差 判定
针孔 0.000 px 直线 ✓
鱼眼 62.983 px 弯曲 ✗
【穿过光轴的直线 (x=0, z=3, y:-4..4)】 ← 对照组
针孔 0.000 px 直线 ✓
鱼眼 0.000 px 直线 ✓
鱼眼把一条直线弯了 63 像素。而图像才 1920px 宽,63px 是肉眼一眼可见的弯曲。 (鱼眼照片里常见的"门框/地平线朝外弯",就是这件事。)
针孔: [u v w]¹ = P · [X Y Z 1]¹ // 全是乘加,【线性】
鱼眼: r = √(a²+b²)
θ = atan(r) // 超越函数
θ_d = θ(1 + k1θ² + k2θ⁴ + k3θ⁶ + k4θ⁸) // θ 的 4 次、8 次项
线性变换可以用矩阵表示;非线性变换不能。 这不是"还没找到合适的矩阵",是数学上的不可能。
而且穿过光轴的直线不会弯,因为沿这条线 θ 只是一维单调函数,不产生弯曲。 这顺便解释了一个现象:鱼眼的畸变看起来总是"径向"的, 所有的弯曲都发生在离轴的方向上。
P 的用武之地:校正之后| 你手上是 | 用哪些参数 |
|---|---|
| 鱼眼原图(带畸变) | K + D + distortion_model,没有 P |
| 已校正成针孔图 | P(3×4),因为那已经是一台虚拟针孔相机 |
鱼眼原图 ──[校正:为虚拟针孔相机逐像素采样]──► 针孔图
↓
这时的投影是线性的
→ 可以有 3×4 的 P
官方对 R 字段的说明正好印证:"一个旋转矩阵,把相机坐标系对齐到理想立体像平面,
使两台相机的极线平行。"
| 代价 | 说明 |
|---|---|
| 丢视场 | 针孔表示不了 180°。必须选一个更小的 FOV 来"装"进针孔模型 |
| 重采样损失 | 逐像素插值,图像质量下降一次 |
| 超出 180° 更麻烦 | Foxglove 明确写了:kannala_brandt 只支持到 180°,超过 180° 要用 fisheye62 |
最后一条是硬约束:如果你的鱼眼 FOV > 180°,kannala_brandt
连校正都做不了。这不是精度问题,是模型本身在 θ→π 时退化。
下面五张图显示同一个世界网格(3D 空间里的直线方格)通过五种投影模型的样子。 所有模型都归一化到「θ_max 落在同一半径」,所以差别纯粹来自映射方式本身。
原因有四层,一层比一层根本。
先看一个具体画面:墨卡托世界地图上,格陵兰看起来和非洲差不多大;换成等积投影, 面积终于对了,但大陆的形状又被拉歪。每一张世界地图都在变形,只是变形的方式不同。
相机在做的事情是:
把「方向的球面」 ──映射──► 「平面的像面」
↑ ↑
高斯曲率 = 1/R² ≠ 0 高斯曲率 = 0
高斯绝妙定理 (Theorema Egregium):高斯曲率是内蕴量, 等距映射(保距映射)必须保持它。
球面的高斯曲率 ≠ 0,平面 = 0 → 球面与平面之间不存在等距映射。
这不是"模型还不够好",这是微分几何里的一个定理。
而"投影"这个词的本义就在这里:地球是球面,地图是平面,必然变形 墨卡托保角度但面积失真,等积投影保面积但形状失真, 不存在同时保角又保面积的地图投影。
方向球面 → 图像平面,是同一件事。
最常见的 plumb_bob(Brown-Conrady)是这样的:
r_d(r) = r (1 + k1 r² + k2 r⁴ + k3 r⁶) 其中 r = tan(θ)
物理约束:离轴角越大 → 像素半径必须越大(单调递增)。 一旦多项式开始下降,就意味着"更大的角度 → 更小的半径",几何上不可能。
判据是解 dr_d/dr = 1 + 3k1·r² + 5k2·r⁴ = 0,
取首次失去单调的那个根:
系数组 k1 k2 首次失去单调 r 对应 FOV 判定
--------------------------------------------------------------------------
普通镜头 (35mm eq.) -0.10 0.01 — > 152° 单调 ✓
广角 (24mm eq.) -0.25 0.04 — > 152° 单调 ✓
超广角 (16mm eq.) -0.40 0.05 1.0360 92.03° 崩溃 ✗
更激进的超广角 -0.50 0.10 1.0000 90.00° 崩溃 ✗
--------------------------------------------------------------------------
描述"模型从哪一点开始失效",正确的量是导数第一次穿过零处的 r: 在它之前投影半径随离轴角单调递增(物理上成立),在它之后出现折叠(几何上不可能)。
对比另一个也"看起来合理"的量 argmin(dr_d/dr)(折叠最厉害的位置):
那个点落在折叠区间内部,模型早已失效,用它当判据会把崩溃点报得太晚。
多项式在这里还有一个病态行为(以 k1=-0.40, k2=0.05 为例):
第 1 个零点: r=1.0360 θ=46.01° FOV=92.0° ← 模型从这里开始失效
第 2 个零点: r=1.9305 θ=62.62° FOV=125.2° ← 又变回单调了
→ 在 r ∈ (1.0360, 1.9305) 区间内导数 < 0(非单调,"折叠")
→ 之后又恢复单调
一个"折叠后又展开"的投影模型显然不是物理的 这正是多项式拟合的典型病态:在拟合区间内表现很好,往外推就完全失控。
k1 越负(桶形畸变越强),单调性崩溃来得越早。
这就是为什么 plumb_bob 只适合中窄视场。
换成 θ 参数化就好很多:θ_d(θ) = θ(1 + k1θ² + …),关键优势是
θ 是有界的(θ ∈ [0, π]),而 tan(θ) 在 90° 就发散。
theta = 170° -> 像素半径 5293.5 px
theta = 179° -> 像素半径 7765.1 px
theta = 180° -> 像素半径 8109.3 px
theta = 181° -> 像素半径 8469.6 px <-- 超过 180°,方向开始折返
原因在于:θ → 180° 时光线几乎从正后方来,投影映射不再是单射: 图像上的一个点可能对应两个方向(前面一个、后面一个)。这不是拟合不好,是拓扑退化。
| 因素 | 后果 |
|---|---|
| 每台相机的制造公差、装配偏心不同 | 出厂标称参数只能参考,必须逐台标定 |
| 变焦镜头:畸变随焦距变化 | 24mm 下标的参数,70mm 下不适用 |
| 对焦距离影响畸变(尤其近距) | 拍 0.3m 和拍 10m,畸变场不同 |
| 温度 → 镜筒和元件热胀冷缩 | 车载场景冬夏差异可达可测级别 |
| 传感器封装倾斜 | 引入不在模型里的非径向误差 |
| 做法 | 说明 |
|---|---|
| 稠密畸变查找表 (dense LUT) | 存每个像素的位移量。640×480×2 floats ≈ 2.4 MB。相机 ISP 里真实就是这么干的 |
| 密网格 + 样条插值 | 薄板样条 (TPS) 可以任意逼近 |
| 光学光线追迹 | 直接用镜头的设计模型计算,不拟合 |
这是"完美"的(在采样精度内),因为它不假设任何函数形式。代价是: 存储大(相比 8 个浮点数)、不可解释(出问题不知道是哪个参数错了)、 不能外推(表里没有的方向就是没有)、每台相机/每个焦距/每个对焦距离都要一张表。
所以工程上永远是"参数化模型的近似" vs "非参数表的精确",前者赢在可用性。
它们分成两类,这个区分很重要:
| 模型 | 公式 | 保住什么 | FOV |
|---|---|---|---|
| 直线 (rectilinear) | r = f·tan θ | 直线 | < 180°(实用 ~120°) |
| 等距 (equidistant) | r = f·θ | 角度→半径线性 | ~180° |
| 等立体角 (equisolid) | r = 2f·sin(θ/2) | 面积 | > 180° |
| 正交 (orthographic) | r = f·sin θ | — | 180° |
| 立体 (stereographic) | r = 2f·tan(θ/2) | 角度(共形) | < 360° |
下面按「多项式(或映射)建在哪个变量上」分组 这是决定 FOV 天花板的那一件事,比「有几个参数」重要得多。 最后一列标出你会在哪儿遇到它。
| 模型 | 参数 | 建在 | 实用 FOV | 常见实现 / 说明 |
|---|---|---|---|---|
① tan θ 基:针孔 + 修正 → 天花板 < 180° | ||||
plumb_bob= radtan / Brown-Conrady |
k1 k2 p1 p2 k3 |
tan θ | ≈ 120° | ROS camera_calibration 默认、OpenCV CALIB_STANDARD、Kalibr pinhole-radtan、Colmap OPENCV、VINS / OpenVINS radtan |
rational_polynomial |
k1…k6 p1 p2 |
tan θ 有理式 |
≈ 165° | ROS rational_polynomial、OpenCV CALIB_RATIONAL。分母可能出奇点 |
thin_prism |
k1…k6 p1 p2 s1…s4 |
tan θ | ≈ 170° | OpenCV CALIB_THIN_PRISM、Colmap THIN_PRISM_FISHEYE。多 4 个薄棱镜项,治装配偏心 |
tilted |
基础模型参数 + tauX tauY |
tan θ | 同基础模型 | OpenCV 4.5+ CALIB_TILTED。显式建模传感器不垂直于光轴 |
② θ 基:鱼眼多项式 → 天花板 180°,再往外要加项 | ||||
equidistant= Aria「Spherical」 |
fx fy cx cy |
θ | 180° | Kalibr pinhole-equi、OpenVINS / VINS equidistant、Colmap SIMPLE_RADIAL_FISHEYE 的基。只有一个解析式 r = fθ,没有多余参数 |
fovDevernay–Faugeras |
fx fy cx cy + ω |
θ 解析 |
≈ 180° | Kalibr pinhole-fov、Colmap FOV。单个 ω 描述整个畸变场,极简 |
kannala_brandt= KB4 / OpenCV fisheye |
k1 k2 k3 k4 |
θ | ≤ 180° | OpenCV cv::fisheye、ROS fisheye、Kalibr kb4、Basalt KB4、Colmap OPENCV_FISHEYE。Foxglove 官方:支持到 180° |
| KB3 KannalaBrandtK3 |
k0 k1 k2 k3(9 阶) |
θ | ≤ 180° | Project Aria 的眼动相机用它。形式:r(θ) = θ + k0θ³ + k1θ⁵ + k2θ⁷ + k3θ⁹ |
fisheye62 |
k0 k1 k2 k3 p0 p1+ crit_theta(可选) |
θ + 切向 | > 180° | 就是 Project Aria 的 FisheyeRadTanThinPrism。Foxglove 明确写:超过 180° 就用它 |
fisheye624 |
6 径向 + 2 切向 + 4 薄棱镜 | θ + 更多项 | > 180° | Aria 的完整版;nerfstudio 直接叫它 FISHEYE624,Blender Cycles 也加了它 |
| ③ 球面 / 统一模型(Unified):把像面当成球的一部分 → > 180° | ||||
ucm= omni (Mei) |
ξ(+ 可选 k1 k2 p1 p2) |
球面 | ≈ 185° | Kalibr omni-radtan。只多 1 个参数就跨过 180°,性价比最高的一个 |
eucm |
α β |
球面 | > 180° | Kalibr eucm、Basalt EUCM。闭式反投影,工程上很好用 |
double_sphere= DS / DS+ |
ξ α |
双球面 | > 180° | Kalibr ds、Basalt DS。无三角函数,速度快 |
| OCamCalib | r(θ) 反函数多项式 + 仿射 c d e |
θ 的反函数 | > 200° | Scaramuzza。超大 FOV 的经典选择,但需要特制靶标 |
| ④ 非参数 / 全景 | ||||
| 稠密 LUT | 无参数 | — | 任意 | OpenCV initUndistortRectifyMap + remap。不需要模型,但存储大、不可解释、不能外推 |
| ERP / 等距柱状 | — | 球面 | 360° | 全景相机。已经不是「相机模型」而是「球面展开」 |
上面这张表最反直觉的一点是:一个模型能覆盖多广,主要不取决于它有「几个」参数, 而取决于它把多项式建在哪个变量上。
| 建在 | 这个变量最大能到多少 | 于是 FOV 天花板 |
|---|---|---|
tan θ | θ → 90° 时 趋向无穷 | 永远到不了 180° |
θ | 最大 π(= 180°),有界 | 正好 180° |
| 球面映射 / 反函数 | 有界且平滑 | 超过 180° |
所以「换个模型」的本质,不是「多加几个参数」,而是换一个在更大的 θ 范围里不发散的映射。
表里最漂亮的反例就是 ucm:只多 一个 参数 ξ,就把天花板从 180° 抬到了 185° 以上。
① 这里的 FOV 是「全视场角」(对角),不是半角。半角 = FOV ÷ 2。
180° 的全视场对应 90° 半角,正好是 tan 发散的地方。这就是那条黄色虚线的位置。
② 上面的「实用 FOV」是量级参考,不是硬规格。真正能标到多少还取决于镜头本身、靶标覆盖、标定质量。 「≈120°」的意思是再大就开始不划算了,不是「数学上不可能」。
③ 参数个数不决定覆盖面。ucm 只多 1 个参数就跨过了 180°;
而 rational_polynomial 有 8 个,仍然过不去。
决定因素是「建在哪个变量上」,不是「有几项」。
A 类是"设计一个这样的镜头",B 类是"拟合一个已有的镜头"。 而 B 类不可能完美,原因就是第一层:用一个有限维函数族去拟合一个任意光滑的畸变场, 只有当真实畸变恰好落在这个函数族里才能精确,而它不会。
先做一个手边就能做的实验:把一张纸卷成圆柱面,纸能平顺贴合,一个褶皱都没有;
把同一张纸拿去包一个球,无论怎么剪、怎么压,总会起皱。
而圆柱的周长公式 2πr 里,明晃晃地就带着 π。
把纸卷成圆柱 → 平滑贴合,一个褶皱都没有
把纸卷成圆锥 → 平滑贴合,一个褶皱都没有
把纸包住一个球 → 【必然起褶皱】,怎么剪都不行
每个试过用包装纸包球的人都知道这件事。而且注意: 这个现象跟用什么单位、算到多少位小数,完全无关,它就是不行。
所以本节的结论可以直接看出来:π 在不在场,和"能不能零失真展平"是两件无关的事。 下面把这个手感换成能逐项核对的数字。
圆柱侧面:
周长 = 2πr ← 含 π
高斯曲率 K = 0 ← 可展曲面
沿一条母线剪开 → 展平成矩形 ← 零失真,精确
圆锥侧面:
底圆周长 = 2πr ← 含 π
高斯曲率 K = 0 ← 可展曲面
沿一条母线剪开 → 展平成扇形 ← 零失真,精确
π 就在里面,但它们展平完全没有失真。
可展与不可展的分界在曲率(K=0 vs K≠0),不在 π 在不在场。
| π 的无理性 | 球面无法展平 | |
|---|---|---|
| 是什么问题 | 精度 / 可构造性 | 存在性 |
| 说的是 | "精确数值写不出来" | "那个映射根本不存在" |
| 提高精度有用吗 | ✅ 有用 | ❌ 完全没用 |
∫∫ K dA = 2π χ
↑ ↑
高斯曲率积分 欧拉示性数(拓扑量)
球面: K=1/R², χ=2 → 左边积出 4π,右边 2π·2 = 4π
平面: K=0, χ=0 → 左边 0,右边 0
π 在等式两边都出现,它是个公共因子。真正区分球面和平面的,
是 K≠0 vs K=0 和 χ=2 vs χ=0
一个是曲率(几何),一个是拓扑。
边长 s 内角和 平面值 180° 超出量
------------------------------------------------------------------
1° 180.0076° 180° 0.0076°
5° 180.1891° 180° 0.1891°
10° 180.7586° 180° 0.7586°
30° 187.0437° 180° 7.0437°
60° 211.5863° 180° 31.5863°
90° 270.0000° 180° 90.0000°
------------------------------------------------------------------
平面三角形的内角和 = 恰好 π
球面三角形的内角和 = π + 超出量
π 在等号两边都有,它是基准线,不是差异。差异是那个超出量 (spherical excess),它等于三角形的面积。
而且这个实验顺便解释了"为什么小角度时鱼眼 ≈ 针孔": 边长 1° 时超出量只有 0.0076°,球面局部看起来就是平的。 差异不是来自"π 算不精确",是来自"曲率累积起来了"。
| 例子 | 为什么不可能 |
|---|---|
| 化圆为方 | π 是超越数(Lindemann 1882)→ 尺规作不出来 |
| 无理旋转 | 转 π 的无理数倍 → 格点不再映射到格点(离散对称被破坏) |
| 精确等分任意角 | 涉及不可构造数 |
| 精确的十进制表示 | π 的小数位永不终止 |
但这些全是「构造问题」或「离散对称问题」,不是「映射存在性问题」。
化圆为方: 『造不出那个正方形』 ← 存在,但造不出来(可构造性)
展平球面: 『那个映射根本不存在』 ← 不存在(存在性)
一个简单的判别标准:提高精度 / 用更强的计算能力,能不能解决它?
能 → 精度问题(π 的无理性在这一类)
不能 → 存在性问题(球面展平在这一类)
球面展平,哪怕给无限精度、无限算力、上帝的计算器,也做不到。
下面这张表可以直接对位读:左边是镜头投影模型,右边是制图学里的方位投影。
| 镜头投影模型 | 对应的地图方位投影 | 说明 |
|---|---|---|
| gnomonic | 日晷投影 / 球心投影 | 针孔相机就是这个 |
| stereographic | 球极投影(保角) | 鱼眼镜头的一种 |
| equidistant | 等距方位投影 | 最常见的鱼眼 |
| equisolid | Lambert 等积方位投影 | 与前者只差系数 2 |
| orthographic | 正射投影 | 180° 鱼眼的极限形态 |
光学和制图学各自独立发现了同一批数学。这不是巧合 因为两者在解决同一个问题:把球面摊平到平面。
面积放大率 = r(θ)·r'(θ) / sin(θ),等于 1 表示"面积不失真":
θ gnomonic stereographic equidistant equisolid orthographic
------------------------------------------------------------------------------
10° 1.047 1.015 1.005 1.000 0.985
20° 1.205 1.063 1.021 1.000 0.940
30° 1.540 1.149 1.047 1.000 0.866
40° 2.225 1.282 1.086 1.000 0.766
50° 3.765 1.482 1.139 1.000 0.643
60° 8.000 1.778 1.209 1.000 0.500
70° 24.995 2.221 1.300 1.000 0.342
80° 190.981 2.904 1.418 1.000 0.174
------------------------------------------------------------------------------
| 模型 | 保住什么 | 牺牲什么 | FOV |
|---|---|---|---|
| gnomonic / 针孔 | 保直线 | 面积 80° 放大 191 倍 | 实用 < ~120° |
| stereographic | 保角度/形状 | 面积 80° 放大 2.9 倍 | 可到 ~360° |
| equidistant | 角度→半径线性 | 面积 80° 放大 1.42 倍 | ~180° |
| equisolid | 保面积 | 严格 = 1 | > 180° |
| orthographic | — | 边缘压缩到 0.17 | 恰 180° |
191 倍这个数字解释了为什么针孔做不了广角
画面角落的一个像素代表的世界面积,是画面中心的 191 倍。这就是"针孔广角的边缘拉伸"。
假设一个投影【同时】保角和保面积:
保角 → 局部线性缩放因子 λ(θ) 各方向相同
保角 → 面积缩放因子 = λ² // 因为两个方向都缩放 λ
保面积 → 面积缩放因子 = 1
=> λ² = 1 => λ = 1 处处成立
=> 这就是【等距映射】
=> 但球面到平面不存在等距映射(高斯绝妙定理)
=> 矛盾。□
所以:保角、保面积、保距 三者【至多取一】。
想判断"信息有没有丢",最直接的办法是跑一遍往返:鱼眼把方向投成像素、再反解回方向; 地图把经纬度投到平面、再反查回经纬度。两组实验的结果如下。
实验 A:鱼眼 —— 方向 → 像素 → 方向
2000 个方向往返
最大角误差 = 1.479e-06 度 ← 数值精度级别
>>> 方向【完全可以精确还原】,鱼眼画面没丢任何方向信息
实验 B:地图 —— 经纬度 → 平面 → 经纬度
墨卡托往返
最大纬误差 = 3.817e-14 度 ← 解析逆函数,精确
>>> 地图同样【完全可以反查经纬度】
两组数字都回到数值精度级别:两个方向都是精确可逆的。
墨卡托的逆函数是写得出公式的:φ = 2·atan(e^y) - π/2。
镜头也一样,像素反解方向就是数值反解 θ_d(θ),而它是单调的,所以逆唯一。
所以「方向反了」这个说法并不成立,真正的差别不在方向,而在别的维度上。 把两类不同的"还原不了"放在一起对照,混乱就消失了。
可逆性(能不能反查)
→ 地图 ✅ 镜头 ✅
→ 两者都没问题
度量(能不能保住 距离/面积/角度)
→ 地图 ❌ 镜头 ❌
→ 高斯绝妙定理:三者至多保一个
✓ 但这两件事【毫无关系】
一个映射可以完全可逆,同时严重失真。墨卡托就是这样:能精确查回经纬度, 但格陵兰在地图上跟非洲一样大。
| ① 度量失真 | ② 深度丢失 | |
|---|---|---|
| 谁有 | 地图和镜头共有 | 只有相机有,和地图完全无关 |
| 内容 | 不能同时保住 保角 / 保面积 / 保距 | 一个像素 = 一条射线 = 无穷多个 3D 点 |
| 丢信息吗 | 不丢,正反向都可逆 | 丢了深度 |
| 和投影模型的关系 | 正是投影模型在描述的 | 毫无关系,哪怕数学上完美的模型也还原不出深度 |
"还原不了"指的是什么?
→ 还原出【方向】(射线) ✅ 能,精确可还原
→ 还原出【3D 点】(位置) ❌ 不能,但原因是【缺深度】,不是投影失真
地图没有对应的第二类问题 —— 地图不装深度,它只有"度量失真"这一件事。
FOV ≤ 180°(普通相机 / 鱼眼):
半球 → 圆盘
双射,【不需要切缝】
✅ 比地图还【好处理】—— 因为只覆盖半个球
FOV = 360°(全向相机 / 等距柱状全景图):
整球 → 矩形
【必须切缝】—— 等距柱状图的左右两条边缘,就是"日期变更线"
⚠️ 这时和地图问题【完全同构】
半球不需要切缝,原因在曲面的开闭:
球面是【闭曲面】(紧致无边)
→ 连续映射到有界的平面区域,必然"粘"在一起
→ 必须切一刀才能摊开(否则所有经线在极点挤成一团)
半球是【开曲面】(有边界)
→ 边界正好对应图像的外圈
→ 天然可以摊开,不需要切
不是"方向反了",而是"覆盖范围不同"。
覆盖半球时,相机反而比地图简单(不用切缝);覆盖整球时,两者完全一样。
这也顺便解释了 360° 全景图为什么上下两端被拉得那么可怕 那正是世界地图在两极的畸变,是同一个现象。
| 地图制作者 | 相机使用者 | |
|---|---|---|
| 有 | 球面(地球) | 平面(图像) |
| 想要 | 平面(地图) | 球面(方向) |
| 能正向映射 | ✅ | ✅ |
| 能反查 | ✅ φ = 2atan(e^y) - π/2 | ✅ 数值反解 θ_d(θ) |
| 丢信息吗 | ❌ 不丢 | ❌ 不丢(方向层面) |
| 不可能在哪 | 保角/保面积/保距至多一个 | 同一条定理 |
| 额外难题 | 整球必须切缝 | ① FOV≤180° 无需切缝(比地图简单) ② 丢深度(与投影无关) |
① 按 FOV 选模型:
< 90° → plumb_bob(最通用,工具链支持最全)
< 180° → kannala_brandt / rational_polynomial
> 180° → fisheye62 / double_sphere / OCamCalib
需要研究级精度 → OCamCalib 或稠密 LUT
② 按最在乎的性质选:
保直线 → 直线透镜(但 FOV 受限)
保面积 → 等立体角
保形状(共角) → 立体投影
纯测量精度 → 等距
③ 按验收标准选:
看【重投影误差在画面边缘有多大】
边缘误差明显大于中心 → 模型阶数不够 or 模型类型不对
"中心对齐、边缘错位"的成因里有一条是模型族选错了。
而且这一条不能靠调参数解决,参数再优化也跳不出函数族。必须换模型。
投影参数属于谁:
只有「输出降维且不可逆」的传感器 —— 也就是【测不到深度】的
相机 ✅ LiDAR ❌ IMU/编码器 ❌
标准形态 P = K[R|t](3×4),只对针孔类模型存在
鱼眼:
内参 = fx fy cx cy + k1..k4 + 模型名(8 个数 + 1 个字符串)
外参 = [R|t],和普通相机一样
投影参数 = 没有矩阵 —— 投影是非线性的,直线会弯 63px
只有【校正成针孔】之后才有 P,代价是丢 FOV + 重采样
为什么没有完美模型(四层):
① 数学:球面无法无失真展平(高斯绝妙定理)—— 定理,不是工程问题
② 数值:多项式会单调性崩溃(超广角在 92.03° 附近就崩)
③ 拓扑:θ→180° 时映射非单射
④ 工程:每台相机不同、随焦距/对焦/温度变化
不是 π 的锅:
圆柱/圆锥满是 π,却零失真展平 → π 在场 ≠ 不能展平
π 在高斯-博内等式两边都有,约掉了
球面三角形内角和 = π + 超出量 → π 是基准,超出量才是差异
拿纸包球必然起皱,跟算多少位无关
区分:精度问题(π) vs 存在性问题(展平)
和地图投影:
同一批公式。针孔相机 = 日晷投影(gnomonic)
不可能三角:保角/保面积/保距 至多取一
两者【都可逆】,不可能的不是可逆性,是度量
真正的不对称是【切缝】:FOV≤180° 半球不需要切(比地图简单)
FOV=360° 整球必须切(和地图完全同构)