1. 简介

关于车辆的全景环视系统网上已经有很多的资料,然而几乎没有可供参考的代码,这一点对入门的新人来说非常不友好。

全景环视系统,又称AVM。在自动驾驶领域,AVM属于自动泊车系统的一部分,是一种实用性极高、可大幅提升用户体验和驾驶安全性的功能。

AVM汽车环视影像系统如图所示,由安装在前保险杠、后备箱、后视镜上的四个外置鱼眼相机构成。

该系统包含的算子按照先后顺序:去畸变、四路鱼眼相机联合标定、投影变换、鸟瞰图微调、拼接融合、3D模型纹理映射等。

下面我们将围绕着算子的先后顺序来对AVM进行介绍。

2. AVM算法分类

先来粗略浏览下AVM算法Pipeline包含那些算子:

2D AVM

3D AVM

3. 镜头去畸变标定

首先我们需要获取每个相机的内参矩阵与畸变系数。以下是视频中四个相机分别拍摄的原始画面,顺序依次为前、后、左、右,并命名为 front.png、back.png、left.png、right.png 。

你可以看到图中地面上铺了一张标定布,这个布的尺寸是 6mx10m,每个黑白方格的尺寸为 40cmx40cm,每个圆形图案所在的方格是 80cmx80cm。我们将利用这个标定物来手动选择对应点获得投影矩阵。

相机去畸变通常使用张正友老师的棋盘格标定方法,首先通过矩阵推导得到一个比较好的初始解,然后通过非线性优化得到最优解,包括相机的内参、外参、畸变系数,然后对鱼眼图像做去畸变处理。内参即:

4. 四路鱼眼联合标定

接下来我们需要获取每个相机到地面的投影矩阵,这个投影矩阵会把相机校正后的画面转换为对地面上某个矩形区域的鸟瞰图。这四个相机的投影矩阵不是独立的,它们必须保证投影后的区域能够正好拼起来。

这一步是通过联合标定实现的,即在车的四周地面上摆放标定物,拍摄图像,手动选取对应点,然后获取投影矩阵。

请看下图:

每个标定板应当恰好位于相邻的两个相机视野的重合区域中。

在上面拍摄的相机画面中车的四周铺了一张标定布,这个具体是标定板还是标定布不重要,只要能清楚的看到特征点就可以了。

然后我们需要设置几个参数:(以下所有参数均以厘米为单位)

  • innerShiftWidth, innerShiftHeight:标定板内侧边缘与车辆左右两侧的距离,标定板内侧边缘与车辆前后方的距离。

  • shiftWidth, shiftHeight:这两个参数决定了在鸟瞰图中向标定板的外侧看多远。这两个值越大,鸟瞰图看的范围就越大,相应地远处的物体被投影后的形变也越严重,所以应酌情选择。

  • totalWidth, totalHeight:这两个参数代表鸟瞰图的总宽高,在这个里我们设置标定布宽 6m 高 10m,于是鸟瞰图中地面的范围为 (600 + 2 * shiftWidth, 1000 + 2 * shiftHeight)。为方便计我们让每个像素对应 1 厘米,于是鸟瞰图的总宽高为:

    totalWidth = 600 + 2 * shiftWidth

    totalHeight = 1000 + 2 * shiftHeight

  • 车辆所在矩形区域的四角 (图中标注的红色圆点),这四个角点的坐标分别为 (xl, yt), (xr, yt), (xl, yb), (xr, yb) (l 表示 left, r 表示 right,t 表示 top,b 表示 bottom)。这个矩形区域相机是看不到的,我们会用一张车辆的图标来覆盖此处。

设置好参数以后,每个相机的投影区域也就确定了,比如前方相机对应的投影区域如下:

5. 投影变换

投影变换的通俗理解就是:假设同一个相机分别在A、B两个不同位置,以不同的位姿拍摄同一个平面(重点是拍摄平面,例如桌面、墙面、地平面),生成了两张图象,这两张图象之间的关系就叫做投影变换。

张正友老师的相机标定法使用的就是从标定板平面到图像平面之间的投影模型。

图中相机从两个不同的角度拍摄同一个X平面,两个相机拍摄到的图像之间的投影变换矩阵H(单应矩阵)为:

其中K为相机内参矩阵,R、T为两个相机之间的外参。这个公式怎么推导的网上有很多,我们只需要知道,这个单应矩阵H内部实际是包含了两个相机之间的位姿关系即可。

这也就解释了:为什么有的AVM pipeline的方法是需要标定相机的外参,然后通过厂家提供的相机安装参数将四路鱼眼全部统一到车身坐标系下,而我们不需要这个过程,只需要用标定布来做联合标定。

其实两种方法内部都是相通的,都绕不开计算相机外参这件事情。

下面就展示了我们使用标定布的过程

然后依次点击事先确定好的四个标志点 (顺序不能错!),得到的效果如下:

这四个点是可以自由设置的,当你在校正图中点击这四个点时,OpenCV 会根据它们在校正图中的像素坐标和在鸟瞰图中的像素坐标的对应关系计算一个射影矩阵。

这里用到的原理就是四点对应确定一个射影变换 (四点对应可以给出八个方程,从而求解出射影矩阵的八个未知量。注意射影矩阵的最后一个分量总是固定为 1)。

6. 鸟瞰图的拼接与平滑

到这一步其实就是最重要的一步了,如何将我们想要的图片进行拼接,并完成图片生成。生成鸟瞰图的过程可以理解为:将鱼眼相机拍摄到的图像,投影到某个在汽车上方平行地面拍摄的相机的平面上去。

这个单应矩阵H具体是多少,由去畸变图中检测到的棋盘格角点坐标和联合标定全景图中棋盘格角点坐标来决定。

如图所示,以后置相机为例,联合标定已知图(2)中框出棋盘格的坐标,图(1)中的棋盘格坐标可通过opencv的函数进行检测,从而建立单应矩阵H的求解模型。

我来逐步介绍它是怎么做到的:

由于相邻相机之间有重叠的区域,所以这部分的融合是关键。如果直接采取两幅图像加权平均 (权重各自为 1/2) 的方式融合的话你会得到类似下面的结果:

你可以看到由于校正和投影的误差,相邻相机在重合区域的投影结果并不能完全吻合,导致拼接的结果出现乱码和重影。这里的关键在于权重系数应该是随像素变化而变化的,并且是随着像素连续变化。

以左上角区域为例,这个区域是 front, left 两个相机视野的重叠区域。我们首先将投影图中的重叠部分取出来:

灰度化并二值化,并用形态学操作去掉 噪点(不必特别精细,大致去掉即可):

至此我们就得到了重叠区域的一个完整 mask。

然后将mask加入到拼接当中,通常的做法是分别以AB、CD为边界,计算白色区域像素点与AB、CD之间的距离,然后计算一个权重,距离CD越近的位置,前俯视图权重越大;距离AB越近的位置,左俯视图权重越大。但会出现边界效应如图所示:

7. C++ 代码展示

main.cpp

#include  "birdView.hpp"int main()
{Mat v[4];for (int i = 0; i < 4; i++){char buf[10];sprintf(buf, "%d.png", i);v[i] = imread(buf);}BirdView b("config.yml");b.setCarSize(240, 380); b.setChessSize(60,60);b.setMaskHeigth(200);b.setInternalShift(27,27);//b.sourcePointClick(v);while (1){imshow("bird view", b.transformView(v));if (waitKey(20) == 27)    break;}
}

birdView.hpp

#ifndef BIRDVIEW_HPP
#define BIRDVIEW_HPP#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;// calculate correspondence point for every input
// 0: left up  1: right up  2:rigth down  3: left downclass BirdView
{
public:BirdView(const char* configFile = NULL){SourcePoint_OK=ParamSet_OK = false;maskHeigth = clickCount = camID = 0;targetPoint.resize(4);sourcePoint.resize(4);try{carPic = imread("../img/car.jpg",CV_8UC4);}catch (...){std::cout <<"[WARNING] Car model view pic not found!\n";}for (int i = 0;i < 4; i++){targetPoint[i].resize(4);sourcePoint[i].resize(4);}// check if config file exist.if (configFile){readConfig(configFile);}}void setInternalShift(int W, int H)
{ShiftAdjust = Size(W,H);ParamSet_OK = false;setParam();}void setShift(int W, int H)
{Shift = Size(W,H);ParamSet_OK = false;setParam();}void setCarSize(int W,int H)
{carSize = Size(W, H);ParamSet_OK = false;setParam();}void setChessSize(int W, int H)
{chessBordWidth.width = W;chessBordWidth.height = H;ParamSet_OK = false;setParam();}void setMaskHeigth(int maskHeigth_)
{maskHeigth = maskHeigth_;ParamSet_OK = false;setParam();}Mat transformView(Mat* v)
{if (!SourcePoint_OK){std::cerr<<"[ERROR] Source Points have not been pointed! please Add function sourcePointClick to get Source Points!\n";throw "[ERROR] Source Points have not been pointed! please Add function sourcePointClick to get Source Points!\n";}if (!ParamSet_OK){setParam();}Mat b[4];Mat m = Mat(mSize, CV_8UC3 );int seq[4] = { 0,2,1,3 };for (int i = 0; i < 4; i++){if(!v[seq[i]].data){continue;}warpPerspective(v[seq[i]], b[seq[i]], Birdtransform[seq[i]], mSize);switch (seq[i]){case 1:b[seq[i]](r[seq[i]]).copyTo(m(r[seq[i]]), maskF);break;case 3:b[seq[i]](r[seq[i]]).copyTo(m(r[seq[i]]), maskB);break;default:b[seq[i]](r[seq[i]]).copyTo(m(r[seq[i]]));break;}}if(carPic.data) carPicTmp.copyTo(m(carPicPos));//drawing target pointsconst Scalar color[4] = { Scalar(255,0,0),Scalar(0,255,0), Scalar(255,255,0), Scalar(0,255,255) };for (int i = 0; i < 16; i++) circle(m, targetPoint[i / 4][i % 4], 5, color[i / 4], 5);return m;}void saveConfig(const char* configFile = "config.yml")
{for (int i = 0;i < 4; i++){if (sourcePoint[i].empty()){std::cout << "[ERROR] sourcePoint has not been comfired all\n"<<std::endl;return ;}}FileStorage fs(configFile, FileStorage::WRITE);if (fs.isOpened()){for (int i = 0; i < 4; i++){for (int k = 0; k < 4; k++){char buf[20];sprintf(buf, "sourcePoint%d%d", i, k);write(fs, buf, sourcePoint[i][k]);}}fs.release();std::cout << "\n param save complete! \n\n";}}void readConfig(const char* configFile = "config.yml")
{FileStorage fs(configFile, FileStorage::READ);if (fs.isOpened()){for (int i = 0; i < 4; i++){for (int k = 0; k < 4; k++){char buf[20];sprintf(buf, "sourcePoint%d%d", i, k);fs[buf] >> sourcePoint[i][k];}}SourcePoint_OK = true;  // source point reading completedParamSet_OK = false; // setting parmasetParam();std::cout << "[WARNING] Config file read sucessfully!\n";}else  std::cout << "[WARNING] There is not a config file in folder\n";}void sourcePointClick(Mat *v)
{setParam(1);// click corner-points and record themprintf("cam: %d ,pointID: %d  ", camID, clickCount);const char *windowsName = "Source point set";namedWindow(windowsName);setMouseCallback(windowsName,on_MouseHandle, (void*)this);for(int i=0;i<4;i++){for(int j=0;j<4;j++){sourcePoint[i][j]= Point2f(0,0);}}for (camID = 0, clickCount = 0; camID<4;){for (int i = 0; i < sourcePoint[camID].size(); i++){circle(v[camID], sourcePoint[camID][i], 5, Scalar(255, 0, 0), 2);}imshow(windowsName, v[camID]);if (waitKey(20) == 'j')    break;}setMouseCallback(windowsName, NULL, NULL);destroyWindow(windowsName);saveConfig("config.yml");/*save source's points*/SourcePoint_OK = true;}void sourcePointClick(cv::VideoCapture *v)
{setParam(1);Mat ans;// click corner-points and record themprintf("cam: %d ,pointID: %d  ", camID, clickCount);const char *windowsName = "Source point set";namedWindow(windowsName);setMouseCallback(windowsName,on_MouseHandle, (void*)this);for(int i=0;i<4;i++){sourcePoint[i].clear();}for (camID = 0, clickCount = 0; camID<4;){v[camID] >> ans;for (int i = 0; i < sourcePoint[camID].size(); i++){circle(ans, sourcePoint[camID][i], 5, Scalar(255, 0, 0), 2);}imshow(windowsName, ans);if (waitKey(20) == 'j')    break;}setMouseCallback(windowsName, NULL, NULL);destroyWindow(windowsName);saveConfig("config.yml");/*save source's points*/SourcePoint_OK = true;}static void on_MouseHandle(int e, int x, int y, int flag, void* param)
{BirdView & birdView = *(BirdView*)param;int camID = birdView.camID;switch (e){case EVENT_LBUTTONUP:{birdView.sourcePoint[birdView.camID][birdView.clickCount] = Point2f(x, y)    ;printf("x:%d y:%d\n", x, y);birdView.clickCount++;if (birdView.clickCount> 3){birdView.clickCount = 0;birdView.Birdtransform[camID] = getPerspectiveTransform(birdView.sourcePoint[camID], birdView.targetPoint[camID]);birdView.camID++;}if (birdView.camID<3){printf("cam: %d ,pointID: %d  ", birdView.camID, birdView.clickCount);}else printf("\n");}default: break;}}private:Rect r[4],carPicPos;int clickCount, camID, maskHeigth;Mat Birdtransform[4],maskF, maskB, carPic,carPicTmp;vector<vector<Point2f> > targetPoint, sourcePoint;Size ShiftAdjust, Shift, chessBordWidth, mSize, carSize;bool SourcePoint_OK,ParamSet_OK;void setParam(bool tranformCheck = false)
{WARMING will show when Transform is running but not all parameters have been setif (Shift.area()== 0){if (tranformCheck)std::cout << "[WARMING] Shift has not been set! Default value will be used" << std::endl;Shift.width = Shift.height = 200;}if (chessBordWidth.area() == 0){if (tranformCheck)std::cout << "[WARMING] chessBordWidth has not been set! Default value will be used" << std::endl;chessBordWidth.width = chessBordWidth.height = 60;}if (ShiftAdjust.area() == 0){if (tranformCheck)std::cout << "[WARMING] ShiftAdjust has not been set! Default value will be used" << std::endl;ShiftAdjust.width = 36;ShiftAdjust.height = 27;}if (carSize.area() == 0){if (tranformCheck)std::cout << "[WARMING] carSize has not been set! Default value will be used" << std::endl;carSize = Size(240, 380);}if (maskHeigth >=100 || maskHeigth <=0){if (tranformCheck)std::cout << "[WARMING] maskHeigth has not been set! Default value will be used" << std::endl;maskHeigth = 200;}if (!ParamSet_OK){/*The size of the entire output image*/mSize = Size(Shift.width * 2 + carSize.width + chessBordWidth.width * 2,Shift.height * 2 + carSize.height + chessBordWidth.height * 2);/*make targetPoint, need chessBordWidth,mSize,Shift*//*left*/targetPoint[0][0] = (Point2f(Shift.width + chessBordWidth.width, Shift.height));targetPoint[0][1] = (Point2f(Shift.width + chessBordWidth.width, mSize.height - Shift.height));targetPoint[0][2] = (Point2f(Shift.width, mSize.height - Shift.height));targetPoint[0][3] = (Point2f(Shift.width, Shift.height));/*forward*/targetPoint[1][0] = (Point2f(mSize.width - Shift.width, Shift.height + chessBordWidth.height));targetPoint[1][1] = (Point2f(Shift.width, Shift.height + chessBordWidth.height));targetPoint[1][2] = (Point2f(Shift.width, Shift.height));targetPoint[1][3] = (Point2f(mSize.width - Shift.width, Shift.height));/*backward*/targetPoint[3][0] = (Point2f(Shift.width, mSize.height - Shift.height - chessBordWidth.height));targetPoint[3][1] = (Point2f(mSize.width - Shift.width, mSize.height - Shift.height - chessBordWidth.height));targetPoint[3][2] = (Point2f(mSize.width - Shift.width, mSize.height - Shift.height));targetPoint[3][3] = (Point2f(Shift.width, mSize.height - Shift.height));/*right*/targetPoint[2][0] = (Point2f(mSize.width - Shift.width - chessBordWidth.width, Shift.height));targetPoint[2][1] = (Point2f(mSize.width - Shift.width - chessBordWidth.width, mSize.height - Shift.height));targetPoint[2][2] = (Point2f(mSize.width - Shift.width, mSize.height - Shift.height));targetPoint[2][3] = (Point2f(mSize.width - Shift.width, Shift.height));//need  Shift, chessBordWidth, ShiftAdjust, mSize/*roi*/r[0] = Rect(0, 0, Shift.width + chessBordWidth.width + ShiftAdjust.width, mSize.height);r[1] = Rect(0, 0, mSize.width, Shift.height + chessBordWidth.height + ShiftAdjust.height);r[2] = Rect(mSize.width - Shift.width - chessBordWidth.width - ShiftAdjust.width, 0, Shift.width + chessBordWidth.width + ShiftAdjust.width, mSize.height);r[3] = Rect(0, mSize.height - Shift.width - chessBordWidth.width - ShiftAdjust.width, mSize.width, Shift.height + chessBordWidth.height + ShiftAdjust.height);maskF = Mat(r[1].size(), CV_8UC1, Scalar(1));maskB = Mat(r[1].size(), CV_8UC1, Scalar(1));/*make mask, need r */vector<vector<Point> > maskVec;/*forward*/maskVec.push_back(vector<Point>());maskVec[0].push_back(Point(0, r[1].height));maskVec[0].push_back(Point(0, r[1].height - maskHeigth));maskVec[0].push_back(Point(r[0].width, r[1].height));maskVec.push_back(vector<Point>());maskVec[1].push_back(Point(r[1].width, r[1].height));maskVec[1].push_back(Point(r[1].width, r[1].height - maskHeigth));maskVec[1].push_back(Point(r[1].width - r[2].width, r[1].height));/*backward*/maskVec.push_back(vector<Point>());maskVec[2].push_back(Point(0, 0));maskVec[2].push_back(Point(0, maskHeigth));maskVec[2].push_back(Point(r[0].width, 0));maskVec.push_back(vector<Point>());maskVec[3].push_back(Point(mSize.width, 0));maskVec[3].push_back(Point(mSize.width, maskHeigth));maskVec[3].push_back(Point(mSize.width - r[2].width, 0));/*draw  mask*/drawContours(maskF, maskVec, 0, Scalar(0), CV_FILLED);drawContours(maskF, maskVec, 1, Scalar(0), CV_FILLED);drawContours(maskB, maskVec, 2, Scalar(0), CV_FILLED);drawContours(maskB, maskVec, 3, Scalar(0), CV_FILLED);for (size_t i = 0; i < 4 ; i++){Birdtransform[i] = getPerspectiveTransform(sourcePoint[i], targetPoint[i]);}if(carPic.data){Size newCarSize = Size(carSize.width-2*ShiftAdjust.width,carSize.height-2*ShiftAdjust.height);resize(carPic,carPicTmp,newCarSize,CV_INTER_CUBIC);carPicPos = Rect(Shift.width+chessBordWidth.width+ShiftAdjust.width,Shift.height +chessBordWidth.height+ShiftAdjust.height,newCarSize.width,newCarSize.height);}ParamSet_OK = true;}}};
#endif //BIRDVIEW_HPP

最终结果如下图所示

AVM环视拼接方法介绍相关推荐

  1. AVM 环视拼接方法介绍

    0. 简介 关于车辆的全景环视系统网上已经有很多的资料,然而几乎没有可供参考的代码,这一点对入门的新人来说非常不友好.全景环视系统,又称AVM.在自动驾驶领域,AVM属于自动泊车系统的一部分,是一种实 ...

  2. Python里面数组拼接方法介绍

    numpy数组拼接方法介绍 转载来源:https://blog.csdn.net/zyl1042635242/article/details/43162031 数组拼接方法一 思路:首先将数组转成列表 ...

  3. Go语言中的字符串拼接方法介绍

    本文介绍Go语言中的string类型.strings包和bytes.Buffer类型,介绍几种字符串拼接方法. 目录 string类型 strings包 strings.Builder类型 strin ...

  4. python加号换行,Python字符串拼接六种方法介绍

    Python字符串拼接的6种方法: 1.加号 第一种,有编程经验的人,估计都知道很多语言里面是用加号连接两个字符串,Python里面也是如此直接用"+"来连接两个字符串: prin ...

  5. 自动泊车之AVM环视系统算法框架

    AVM(Around View Monitor),中文:全景环视系统.在自动驾驶领域,AVM属于自动泊车系统的一部分,是一种实用性极高.可大幅提升用户体验和驾驶安全性的功能.AVM已经是一种较为成熟的 ...

  6. AVM环视系统算法框架

    "智能汽车生态群"加微信Time-machine-(备注公司+姓名) AVM(Around View Monitor),中文:全景环视系统.在自动驾驶领域,AVM属于自动泊车系统的 ...

  7. 自动驾驶—自动泊车之AVM环视系统算法框架

    作者丨中投靓仔@知乎 来源丨https://zhuanlan.zhihu.com/p/534553717 编辑丨3D视觉工坊 AVM(Around View Monitor),中文:全景环视系统.在自 ...

  8. ADAS摄像头图像环视拼接算法

    ADAS摄像头图像环视拼接算法 输入输出接口 Input: (1)4个摄像头采集的图像视频分辨率 (整型int) (2)4个摄像头采集的图像视频格式 (RGB,YUV,MP4等) (3)摄像头标定参数 ...

  9. Python爬取B站弹幕方法介绍

    Python爬取B站弹幕方法介绍 文章目录 Python爬取B站弹幕方法介绍 前言 寻找弹幕数据 编写爬虫 B站弹幕数量 新技术介绍 参考文章 前言 最近同学要做东西,需要用 B 站的视频对应的弹幕数 ...

最新文章

  1. jdbc批量调用oracle存储过程,oracle学习笔记(二十三)——JDBC调用存储过程以及批量操作...
  2. 在Linux(Ubuntu)下搭建ASP.NET Core环境并运行 继续跨平台
  3. 欢迎参与Java 事务讨论
  4. Boost:宏BOOST_TEST_CSTR_EQ的使用实例
  5. C++项目參考解答:求Fibonacci数列
  6. linux下能用qt5.0,qt5.0移植
  7. 数据结构算法入门--一文了解什么是复杂度
  8. 查看oracle索引状态,oracle监控索引的使用情况
  9. 联名款Redmi K40游戏增强版今日揭晓:神秘女主粉色头发吸睛
  10. MacBook 重装 Apache 和 PHP 7.2
  11. linux自动化设备,为变电站自动化设备定制Linux系统
  12. 简单几步教会你画出透明丝袜,初学者画出透明质感
  13. jq onclick 定义_jq中的onclick绑定事件
  14. 为什么说程序员的前三年不要太看重工资水平?
  15. chloe.mysql_WPF权限控制——【3】数据库、自定义弹窗、表单验证
  16. pyqt:让qlabel的图片根据鼠标指向的位置进行放缩
  17. 02 数字图像技术——颜色空间转换与颜色空间分割实验结果与分析——python
  18. matlab 按字母排序,matlab命令大全(按字母排序) 总汇详解最新发布完整珍藏版
  19. oppo java模拟器_java动物声音模拟器
  20. 大数据——云服务常用词汇及含义

热门文章

  1. 同事推荐的GIS书籍
  2. Ubuntu 18.04上搜狗输入法简繁体切换快捷键Ctrl+shift+f和AndroidSdtuio的全局搜索冲突
  3. CMake基础教程(18)find_path查找文件路径
  4. 【pytorch】optimizer(优化器)的使用详解
  5. nohup command > out.file 2>1 命令详解
  6. 基于tensorflow、CNN网络识别花卉的种类(图像识别)
  7. WiFi认证过程需要的协议和服务
  8. 刻意学习:持续行动让你人生逆袭
  9. C# 调用C++dll(以基恩士LKG5000为例)
  10. 我工作这十年-中国在崛起