开发车载环视(AVM)系统时,摄像头画面直接拿过来是没法用的。雨天的噪点、夜间的椒盐干扰、广角镜头带来的桶形畸变——这些不先处理好,后面的拼接和检测都是空谈。这篇笔记整理一下滤波与畸变矫正的工程实践,附带一些 C++ 里值得注意的细节。
图像滤波与降噪
车载场景下最常见的噪声有两种:高斯噪声(画面发灰,像蒙了层雾)和椒盐噪声(随机黑白点,夜间灯光干扰时尤其明显)。滤波的核心要求是:去噪的同时别把车道线、障碍物边缘给模糊了。
高斯滤波是最基础的选择。计算量小,平滑效果自然,晴雨天都能用。OpenCV 里直接调 GaussianBlur,一般用 3×3 核,σ 取 1.5 左右就好。车速快、帧率高的时候,高斯是实时的保证。
中值滤波专治椒盐噪声。它把像素邻域排序后取中值,能有效抹掉孤立噪点。但中值滤波的耗时比高斯高不少,所以通常只在夜间或检测到大量椒盐时才切换过去。
双边滤波既能降噪又能保边缘,听起来很完美,但计算量实在太大。只在一些高端车、算力充裕的项目里用,或者只对关键区域(比如车位线周围)局部使一下。
在代码里,滤波函数的参数用 Mat& 引用传递,这是车载实时性的硬要求。OpenCV 的 Mat 对象内部存着几 MB 的图像数据,值传递会触发深拷贝,帧率直接崩掉。引用传递只传指针,没有拷贝开销。另外,为了方便调用,我在实现文件里用了 using namespace cv;,省得到处写 cv:: 前缀。头文件里别这样干,污染命名空间。
实际工程中,路况多变,需要根据光照和环境动态切换滤波算法。策略模式很适合这个场景:定义一个 FilterStrategy 接口,高斯、中值各自实现,再用一个上下文类按条件调度。新增算法(比如导向滤波)时完全不用改老代码。下面是一个精简过的实现:
#include <opencv2/opencv.hpp>
using namespace cv;
class FilterStrategy {
public:
virtual ~FilterStrategy() {}
virtual Mat filter(Mat& src) = 0;
};
class GaussianFilter : public FilterStrategy {
public:
Mat filter(Mat& src) override {
Mat dst;
GaussianBlur(src, dst, Size(3, 3), 1.5);
return dst;
}
};
class MedianFilter : public FilterStrategy {
public:
Mat filter(Mat& src) override {
Mat dst;
medianBlur(src, dst, 3);
return dst;
}
};
class FilterContext {
private:
FilterStrategy* strategy;
public:
explicit FilterContext(FilterStrategy* s) : strategy(s) {}
~FilterContext() { delete strategy; }
Mat doFilter(Mat& src) { return strategy->filter(src); }
};
// 根据路况切换
Mat avm_filter(Mat& src, int road_condition) {
FilterStrategy* strategy = nullptr;
if (road_condition == 0) {
strategy = new GaussianFilter();
} else if (road_condition == 1) {
strategy = new MedianFilter();
} else {
strategy = new GaussianFilter(); // 兜底
}
FilterContext ctx(strategy);
return ctx.doFilter(src);
}
畸变矫正:把鱼眼画面拉直
AVM 一般用 180° 以上鱼眼镜头获取大视野,代价是严重的径向畸变:直线变圆弧,物体靠近边缘会明显扭曲。矫正的核心是摄像头的标定数据——内参矩阵 K 和畸变系数 distCoeff。这套参数出厂时标定一次,后面不再改动。
普通广角就用 initUndistortRectifyMap + remap 做标准矫正;鱼眼镜头用 fisheye::initUndistortRectifyMap 那套 API。为了统一接口,我会在矫正函数里加一个默认参数 bool is_fisheye = false,不用为两种类型写两个重载。K 和 distCoeff 传参时改成 const Mat&,避免内部意外修改标定数据,也保持引用传递的高效。
标定参数是全局唯一的,系统中多个模块都要读。这里很适合用单例模式管理:
class CalibManager {
private:
static CalibManager* instance;
CalibManager() {}
public:
static CalibManager* getInstance() {
if (!instance) instance = new CalibManager();
return instance;
}
};
CalibManager* CalibManager::instance = nullptr;
这个单例负责加载和提供所有摄像头的标定参数,保证了数据一致性。
一点补充
上面的滤波和畸变矫正是预处理阶段最基础的两个模块。实际量产时,还得搭上功能安全(比如 ISO 26262 要求的异常监测)和大量极端场景测试。但把核心流程和 C++ 坑点吃透,后面才不会在性能瓶颈上反复折腾。

