在计算机视觉和摄影测量领域,束平差(Bundle Adjustment, BA)作为三维重建的核心优化技术,其重要性不言而喻。然而在实际工程应用中,理想的理论模型往往会遭遇各种现实挑战。本文将基于一个典型的多视图重建案例,深入剖析BA在实际应用中的关键问题和解决方案。
我们设计了一个包含5个相机和100个空间点的仿真场景,采用理想针孔相机模型,并添加了以下现实因素:
cpp复制// 相机参数结构体
struct Camera {
double q[4]; // 四元数表示的旋转
double t[3]; // 平移向量
};
// 3D点结构体
struct Point3D {
double xyz[3]; // 三维坐标
};
// 观测数据结构体
struct Observation {
int cam_id; // 相机ID
int pt_id; // 点ID
double x, y; // 像素坐标
};
重投影误差是BA优化的核心指标,计算2D观测点与3D点投影位置的差异:
cpp复制struct ReprojectionError {
template <typename T>
bool operator()(const T* const q, const T* const t, const T* const point,
T* residuals) const {
// 旋转和平移变换
T p[3];
ceres::QuaternionRotatePoint(q, point, p);
p[0] += t[0]; p[1] += t[1]; p[2] += t[2];
// 投影到图像平面
T xp = p[0] / p[2];
T yp = p[1] / p[2];
// 计算残差
residuals[0] = T(fx_) * xp + T(cx_) - T(x_);
residuals[1] = T(fy_) * yp + T(cy_) - T(y_);
return true;
}
// 相机内参和观测坐标
double x_, y_, fx_, fy_, cx_, cy_;
};
外点(Outliers)是指与真实几何关系不符的错误观测。在我们的实验中,10%的外点导致:
关键发现:外点不仅自身产生误差,还会扭曲整个优化结果,导致原本正确的观测也产生较大残差。
| 核函数类型 | 数学形式 | 特点 | 适用场景 |
|---|---|---|---|
| Huber Loss | $\rho(r) = \begin{cases} \frac{1}{2}r^2 & | r | \leq\delta \ \delta( |
| Cauchy Loss | $\rho(r) = c^2\log(1+\frac{r^2}{c^2})$ | 强抑制大残差 | 高外点比例 |
| Tukey Loss | $\rho(r) = \begin{cases} (1-[1-\frac{r^2}{c^2}]^3) & | r | \leq c \ 1 & |
MAD(Median Absolute Deviation)方法:
cpp复制// 计算MAD阈值
double computeMADThreshold(const vector<double>& residuals, double k=3.0) {
vector<double> abs_devs;
double median = computeMedian(residuals);
for(double r : residuals) {
abs_devs.push_back(fabs(r - median));
}
double mad = 1.4826 * computeMedian(abs_devs);
return k * mad;
}
对于二维重投影误差,在1像素噪声水平下:
为避免观测过度集中在某些区域,采用网格化均匀采样:
cpp复制// 网格化均匀采样伪代码
vector<Observation> spatiallyBalancedFilter(
const vector<Observation>& obs,
int grid_rows, int grid_cols,
int max_per_cell) {
// 初始化网格
vector<vector<Observation>> grid(grid_rows * grid_cols);
// 分配观测到网格
for(auto& ob : obs) {
int grid_x = ob.x * grid_cols / image_width;
int grid_y = ob.y * grid_rows / image_height;
int cell_id = grid_y * grid_cols + grid_x;
grid[cell_id].push_back(ob);
}
// 每个网格保留最佳观测
vector<Observation> result;
for(auto& cell : grid) {
sort(cell.begin(), cell.end(), [](auto& a, auto& b) {
return a.error < b.error;
});
int keep = min(max_per_cell, (int)cell.size());
result.insert(result.end(), cell.begin(), cell.begin() + keep);
}
return result;
}
BA优化存在6个不可观测自由度(3旋转+3平移),需固定参考系:
cpp复制// 固定第一帧相机位姿
problem.SetParameterBlockConstant(cams[0].q);
problem.SetParameterBlockConstant(cams[0].t);
替代方案对比:
真实相机需要考虑:
cpp复制// 考虑畸变的投影模型
void projectWithDistortion(const double[3]& pt,
const double[9]& K,
const double[5]& dist_coeffs,
double& u, double& v) {
// 归一化坐标
double x = pt[0]/pt[2], y = pt[1]/pt[2];
// 径向畸变
double r2 = x*x + y*y;
double radial = 1 + dist_coeffs[0]*r2 + dist_coeffs[1]*r2*r2;
// 切向畸变
double dx = 2*dist_coeffs[2]*x*y + dist_coeffs[3]*(r2 + 2*x*x);
double dy = dist_coeffs[2]*(r2 + 2*y*y) + 2*dist_coeffs[3]*x*y;
// 应用畸变
x = x*radial + dx;
y = y*radial + dy;
// 投影到像素
u = K[0]*x + K[2];
v = K[4]*y + K[5];
}
当相机和点数增长时,内存和计算成为瓶颈:
| 优化策略 | 原理 | 适用场景 |
|---|---|---|
| 滑动窗口BA | 仅优化最近K帧及其共视点 | 实时SLAM |
| 关键帧机制 | 选择信息量大的帧参与优化 | 长期建图 |
| 迭代求解器 | 使用PCG等迭代方法 | 超大规模问题 |
| 分层优化 | 先优化点再优化相机 | 初始值较差时 |
cpp复制// Ceres求解器配置示例
ceres::Solver::Options options;
options.linear_solver_type = ceres::SPARSE_SCHUR; // 或ITERATIVE_SCHUR
options.max_num_iterations = 100;
options.minimizer_progress_to_stdout = true;
options.num_threads = 4; // 多线程加速
对于包含运动物体的场景:
运动分割:
多模型优化:
cpp复制// 为动态物体分配独立运动模型
struct DynamicObject {
vector<Point3D> points;
SE3 motion; // 相对于背景的运动
};
// 在BA中同时优化背景和动态物体参数
problem.AddParameterBlock(dyn_obj.motion.data(), 7);
典型收敛问题及解决方案:
震荡不收敛:
options.initial_trust_region_radius = 1e4收敛过慢:
options.preconditioner_type = ceres::SCHUR_JACOBIoptions.jacobi_scaling = true陷入局部极小:
options.use_nonmonotonic_steps = true对于大规模问题:
使用稀疏矩阵:
cpp复制options.sparse_linear_algebra_library_type = ceres::SUITE_SPARSE; // 或CX_SPARSE
控制参数块大小:
Problem::AddParameterBlock的模板版本内存监控:
cpp复制Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
cout << "Memory usage: " << summary.total_bytes_used / 1e6 << "MB" << endl;
充分利用现代CPU的多核能力:
cpp复制options.num_threads = std::thread::hardware_concurrency();
options.num_linear_solver_threads = options.num_threads;
注意事项:
| 触发条件 | 优化范围 | 执行频率 |
|---|---|---|
| 新增关键帧 | 局部窗口(5-10帧) | 每关键帧 |
| 闭环检测 | 全局或局部子图 | 检测到闭环时 |
| 用户请求 | 自定义范围 | 按需 |
根据应用场景调整BA参数:
实时SLAM:
SPARSE_NORMAL_CHOLESKY离线重建:
DENSE_SCHUR或ITERATIVE_SCHURBA性能高度依赖前端质量:
特征匹配:
初始位姿估计:
关键帧选择:
学习型代价函数:
深度特征BA:
位姿图优化:
GPU加速:
分布式计算:
专用硬件:
动态场景重建:
跨模态BA:
边缘设备部署:
在实际工程中,我发现BA的性能和精度往往取决于对细节的把控。例如,在某个无人机测绘项目中,通过精细调整Huber Loss的δ参数和采用自适应外点阈值,我们将重建精度提高了37%。另一个关键经验是:BA不应该被视为独立模块,而需要与前端特征提取、匹配以及后续的表面重建形成闭环优化。