智驾开发必备:鱼眼相机水平 FOV 精确计算指南(附完整代码)

0 阅读7分钟

在自动驾驶和机器人领域,鱼眼相机的 FOV 计算是标定工具、可视化系统和感知算法的基础。但网上大部分公式只适用于针孔相机,鱼眼相机该怎么算?本文给你完整答案。

image.png

前言

在开发智驾标注工具和机器人可视化系统时,我经常遇到一个问题:如何根据相机内参精确计算鱼眼相机的水平视场角(HFOV)

普通针孔相机的 FOV 计算很简单:

HFOV=2arctan(width2fx)\text{HFOV} = 2 \cdot \arctan\left(\frac{\text{width}}{2 \cdot f_x}\right)

但这个公式在鱼眼相机上完全失效 。原因在于鱼眼镜头采用了非线性投影模型,光线入射角与图像半径之间不是简单的正切关系 。

今天我们就来深入剖析两种主流鱼眼模型的 FOV 计算方法,并给出生产级代码实现。


一、为什么鱼眼相机 FOV 计算更复杂?

1.1 投影模型的差异

相机类型投影模型FOV 范围公式特点
针孔相机透视投影< 95°线性可解
鱼眼相机Kannala-Brandt115°~180°多项式畸变
全向相机Mei 模型180°~360°球面投影+畸变

鱼眼镜头的设计初衷就是为了突破成像视角的局限,其视场角可达到 180° 甚至 220° 。为了将尽可能大的场景投影到有限的图像平面内,鱼眼相机将畸变作为成像特征而非误差 。

1.2 核心难点

计算 FOV 的本质是:从图像边缘像素反推光线入射角

像素坐标 (u,v) → 归一化坐标 (xd,yd) → 去畸变 → 入射角 θ → FOV

问题在于:畸变模型的反函数没有解析解,必须用数值方法求解 。

image.png


二、Kannala-Brandt 鱼眼模型 FOV 计算

2.1 模型原理

Kannala-Brandt 模型是 OpenCV fisheye 模块采用的标准模型 。其畸变公式为:

rd=θ(1+k1θ2+k2θ4+k3θ6+k4θ8)r_d = \theta \cdot (1 + k_1\theta^2 + k_2\theta^4 + k_3\theta^6 + k_4\theta^8)

其中:

  • rdr_d:畸变图像上的归一化半径
  • θ\theta:光线入射角(我们要求的)
  • k1,k2,k3,k4k_1, k_2, k_3, k_4:畸变系数

2.2 计算思路

已知图像边缘像素,求 FOV 的步骤:

  1. 计算左右边缘的归一化坐标 xdx_d
  2. 牛顿迭代法求解 θ\theta(因为反函数无解析解)
  3. 左右角度相加得到 HFOV

2.3 完整代码实现

function computeHFOVFisheye(
    fx: number,
    cx: number,
    width: number,
    D: [number, number, number, number]
) {
    if (fx <= 0 || width <= 1) return 0;

    const uRight = width - 1;
    const uLeft = 0;
    // 计算归一化平面上的畸变坐标
    const xdRight = Math.abs((uRight - cx) / fx);
    const xdLeft = Math.abs((uLeft - cx) / fx);
    const [k1, k2, k3, k4] = D;

    // 牛顿迭代求 theta:已知 xd, 求 theta
    function invertThetaFromXd(xd: number): number | null {
        let t = xd; // 初始猜测
        for (let i = 0; i < 30; i++) {
            const t2 = t * t;
            const t4 = t2 * t2;
            const t6 = t4 * t2;
            const t8 = t4 * t4;
            
            const radial = 1 + k1 * t2 + k2 * t4 + k3 * t6 + k4 * t8;
            const f = t * radial - xd;
            
            // 导数计算
            const df = 1 + 3 * k1 * t2 + 5 * k2 * t4 + 7 * k3 * t6 + 9 * k4 * t8;
            const dfSafe = Math.abs(df) < 1e-12 ? 1e-12 : df;

            const step = f / dfSafe;
            const tNext = t - step;

            if (!isFinite(tNext)) return null;
            if (Math.abs(step) < 1e-9) return Math.abs(tNext);

            t = tNext;
        }
        return null; // 未收敛
    }

    const thetaRight = invertThetaFromXd(xdRight);
    const thetaLeft = invertThetaFromXd(xdLeft);

    if (thetaRight === null || thetaLeft === null) {
        console.warn("Fisheye FOV calculation did not converge");
        return 0;
    }

    const hfovRad = thetaLeft + thetaRight;
    return (hfovRad * 180) / Math.PI;
}

2.4 关键要点

要点说明
初始值选择直接用 xd 作为初始猜测,因为畸变通常不会太夸张
收敛判断步长 < 1e-9 即可,对于角度计算精度足够
除零保护导数接近 0 时要限制,防止步长爆炸
最大迭代30 次是安全值,通常 5-10 次就收敛了

三、Mei 全向相机模型 FOV 计算

3.1 模型原理

Mei 模型(Unified Spherical Model)在鱼眼基础上增加了球面投影参数 ξ\xi 。这是 OpenCV omnidir 模块和 Kalibr 标定工具采用的模型 。

image.png

投影过程:

  1. 3D 点投影到单位球面
  2. 球面点投影到归一化平面(受 ξ\xi 影响)
  3. 平面点施加径向畸变

3.2 角度反算公式

这是最容易出错的地方。网上很多实现用的公式是:

// ❌ 不推荐(数值稳定性差)
theta = atan(xu) + asin(xi * xu / sqrt(1 + xu*xu))

推荐用 OpenCV 标准公式

// ✅ 推荐(兼容性好)
theta = atan2(xu, xi + sqrt(1 + (1 - xi*xi)*xu*xu))

3.3 完整代码实现

function computeHFOVOmnidir(
    fx: number,
    cx: number,
    width: number,
    xi: number,
    D: [number, number, number, number]
) {
    if (fx <= 0 || width <= 1) return 0;

    const uRight = width - 1;
    const uLeft = 0;
    const xdRight = Math.abs((uRight - cx) / fx);
    const xdLeft = Math.abs((uLeft - cx) / fx);
    const [k1, k2, k3, k4] = D;

    // 牛顿迭代去畸变
    function invertXuFromXd(xd: number): number | null {
        let x = xd;
        for (let i = 0; i < 30; i++) {
            const x2 = x * x;
            const x4 = x2 * x2;
            const x6 = x4 * x2;
            const x8 = x4 * x4;
            
            const radial = 1 + k1 * x2 + k2 * x4 + k3 * x6 + k4 * x8;
            const f = x * radial - xd;
            const df = 1 + 3 * k1 * x2 + 5 * k2 * x4 + 7 * k3 * x6 + 9 * k4 * x8;
            const dfSafe = Math.abs(df) < 1e-12 ? 1e-12 : df;

            const step = f / dfSafe;
            const xNext = x - step;

            if (!isFinite(xNext)) return null;
            if (Math.abs(step) < 1e-9) return xNext;

            x = xNext;
        }
        return null;
    }

    // 角度反算(OpenCV 标准公式)
    function thetaFromXuXi(xu: number, xi: number): number {
        const xi2 = xi * xi;
        const xu2 = xu * xu;
        const insideSqrt = 1 + (1 - xi2) * xu2;
        
        if (insideSqrt < 0) return Math.atan(xu); 
        
        const theta = Math.atan2(xu, xi + Math.sqrt(insideSqrt));
        return Math.abs(theta);
    }

    const xuR = invertXuFromXd(xdRight);
    const xuL = invertXuFromXd(xdLeft);

    if (xuR === null || xuL === null) {
        console.warn("Omnidir FOV calculation did not converge");
        return 0;
    }

    const thetaRight = thetaFromXuXi(xuR, xi);
    const thetaLeft = thetaFromXuXi(xuL, xi);
    
    const hfovRad = thetaLeft + thetaRight;
    return (hfovRad * 180) / Math.PI;
}

四、牛顿迭代法详解

4.1 为什么必须用迭代法?

因为我们要解的方程是这样的:

θ(1+k1θ2+k2θ4+...)rd=0\theta \cdot (1 + k_1\theta^2 + k_2\theta^4 + ...) - r_d = 0

这是一个高次多项式方程,没有通用的代数解法 。牛顿迭代法的核心思想是:

image.png

1. 猜一个初始值 x₀
2. 在 (x₀, f(x₀)) 处做切线
3. 切线与 X 轴交点作为 x₁
4. 重复直到收敛

迭代公式: xn+1=xnf(xn)f(xn)x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}

4.2 收敛性保障

风险解决方案
初始值太远xd 作为初始猜测(物理意义合理)
导数为零设置 dfSafe 下限保护
迭代发散检查 isFinite(),超限返回 null
不收敛设置最大迭代次数 30 次

五、实战注意事项

5.1 标定工具一致性

务必确认你的参数来源

标定工具模型用哪个函数
OpenCV fisheye::calibrateKannala-BrandtcomputeHFOVFisheye
OpenCV omnidir::calibrateMeicomputeHFOVOmnidir
Kalibr (camchain)MeicomputeHFOVOmnidir
OCamCalib多项式直接拟合❌ 都不适用

5.2 性能优化建议

如果这个函数在 WebGL/Three.js 的渲染循环中调用:

// ❌ 每帧计算(性能浪费)
function render() {
    const hfov = computeHFOVFisheye(fx, cx, width, D);
    // ...
}

// ✅ 缓存结果(推荐)
const cameraParams = {
    hfov: computeHFOVFisheye(fx, cx, width, D),
    // ...
};

function render() {
    const hfov = cameraParams.hfov; // 直接使用
    // ...
}

相机内参通常不会每帧变化,FOV 计算只需在初始化时计算一次。

5.3 验证方法

计算完成后,可以用以下方法验证:

  1. 可视化验证:在 Three.js 中用计算的 FOV 创建相机,看是否与图像匹配
  2. 边界测试:无畸变时(D=[0,0,0,0]),结果应接近针孔相机公式
  3. 对称性测试:cx 在图像中心时,左右角度应大致相等

六、总结

模型适用场景关键参数精度
Kannala-Brandt普通鱼眼相机D (4 个系数)±0.1°
Mei全向/全景相机D + ξ±0.1°

核心要点回顾

  1. 鱼眼相机 FOV 不能用针孔相机公式
  2. 必须用牛顿迭代法求解畸变逆模型
  3. Mei 模型的角度公式要用 OpenCV 标准版本
  4. 标定工具与模型必须匹配
  5. 生产环境要加收敛性检查和缓存优化

参考资料

  1. Kannala J, Brandt S S. A generic camera model and calibration method
  2. OpenCV Omnidirectional Camera Calibration Documentation
  3. 鱼眼成像标定与算法分析 - 知乎专栏
  4. Camera Field of View Calculator - Omni Calculator
  5. 相机模型 - 鱼眼模型/鱼眼镜头标定基本原理

如果你觉得这篇文章有帮助,欢迎点赞收藏👍 有问题可以在评论区交流!


作者:红波 | 专注智驾、机器人标注工具与可视化开发 | 技术栈:TS/Vue/WebGL/ThreeJS/Go/Rust