NXP

基于S32V的车道线检测

2019-07-12 12:07发布

1.读取图像

/* SDI frame processing */ sdi_grabber lGrabber; // ... SDI init // Grabbing loop while(1) { SDI_Frame lFrame = lGrabber.FramePop(); // UMat is in lFrame.UMat if (!lFrame.mUMat.empty() { // UMat processing } // We need to push the buffer back lGrabber.FramePush(lFrame); } 相机将拍到的内容在SDI中以帧队列的形式存放,lGrabber调用FramePop()方法获取一帧图像数据,在SDI_Frame中的mUMat属性中就是我们将要操作的图片数据。 调用setInputImage()方法将待操作的UMat图像传入我们的车道线检测类中进行处理。

2.灰度化

我们从摄像头中获取到的数据都是VSDK_8UC3这中RGB彩 {MOD}图片,但在APEX中的处理对象几乎都是VSDK_8UC1这种灰度图,所以首先要将彩 {MOD}图转换为灰度图。 apexcv::ColorConverter colorcv2grey_op.Initialize(vsdkWarpOriImage, colorcv2rgb_op.eBGR888_TO_GREY, vsdkWrapImageGray); colorcv2grey_op.ReconnectIO(vsdkOriImage, vsdkWrapImageGray); colorcv2grey_op.Process(); 转换后的图像:

3.透视变换

目前一种比较通用一种车道检测方法是将要检测的车道位置以ROI(region of interest)的方式画出来,将ROI区域变为鸟瞰区域,这样首先过滤掉了大量无关数据,同时因距离原因会造成的直线车道线变成梯形车道线,通过透视变换的方法做成的鸟瞰图,可以将这种梯形车道线还原成正常的直线。 透视变换的本质是将图像投影到一个新的视平面,其通用变换公式为: (u,v)为原始图像像素坐标,(x=x’/w’,y=y’/w’)为变换之后的图像像素坐标。透视变换矩阵图解如下: 由于CV自带的鸟瞰方法cv::warpPerspective在S32V上是用ARM进行执行,并没有用到APEX核加速,所以效率极其的慢,单执行这一个函数延迟就达到了400ms左右,所以完全不能满足至少每秒20帧的要求,所以必须使用能调用APEX核的apex cv类来进行处理。 目前在apex cv中并没有直接的可以使用的鸟瞰函数可以使用,所以可以查看官方的几个dome来寻找CV鸟瞰的替代品。在其中一个dome中找到一个Warp类实现了鸟瞰功能,通过修改它提供的配置文件即可确定ROI: /** * Class consists of function which warps an image into bird eye view */ class Warp { public: /** * Constructor */ Warp(); /** * Constructor apex init */ Warp(int apuldx); /** * Function warps the input image into the output using pre-computed * transformation. * Uses APEX subsystem. * @param in Input image * @param out Output bird eye image * @param transform Transformation LUTs to be used */ void Warp_BirdEye(vsdk::UMat in, vsdk::UMat out, Transform_LUT &transform); /* * Warp reinint */ void set_warp_init(bool value); private: /** * APU index */ int m_apuldx; // /** // * Source image // */ // vsdk::UMat src; // // /** // * Destination image // */ // vsdk::UMat dst; /** * Blocks indices */ vsdk::UMat blocks; /** * Offsets indices */ vsdk::UMat offsets; /** * Deltas values */ vsdk::UMat deltas; /** * APEX remap process */ LDWS_REMAP_SOBEL_MEDIAN apex_remap; /** * Warp initialised */ bool warp_init; }; 最后效果: 这种方法可以明显看出虽然确实发生了鸟瞰,但其效果并不是十分理想,痕迹较淡,所以需要考虑其他方法。 从原理来看,鸟瞰的过程其实是通过梯形ROI确定的四个点和变换后矩形四个点生成的转换矩阵,用原图像矩阵乘以转换矩阵生成鸟瞰图像,所以我们可以先生成转换矩阵,用这个转换矩阵生成一个原图到鸟瞰图的映射关系,将这个映射关系通过apex cv 的Remap类来实现两个图片间的转换。 apexcv::Remap warpRemap; int lRetVal = 0; lRetVal |= warpRemap.Initialize(lTransform, clSrcWidth, clSrcHeight, clSrcWidth, apexcv::INTER_LINEAR, apexcv::BORDER_CONSTANT, 0); if (lRetVal) { printf("Error on Initialize: %d ", lRetVal); } warpRemap.Process(in, out); 效果图: 从效果来看这种remap方式与cv::warpPerspective基本一致。

4.Canny边缘检测

做出鸟瞰图后,需要对车道线进行边缘检测,提取到两条车道线的特征。 int apexcv::Canny::Initialize ( vsdk::UMat & src, vsdk::UMat & dst, int width, int height, uint16_t low, uint16_t high, int iters = 1 ) Parameters src 8-bit grayscale source image dst 8-bit destination image width Width of the input image height Height of the input image low 16-bit low threshold for edge hysteresis high 16-bit high threshold for edge hysteresis iters Number of default extra times to run the edge promotion process. Has no effect on process- Combined() apexcv::Canny canny_op; canny_op.Initialize(in, out, 1280, 720, 20, 80, 3); canny_op.ReconnectIO(in, out, 1280, 720); canny_op.Process(); 效果图:

5.直方图检测

当提取到车道的相关特征后,我们需要确定一个搜索车道位置的起始点,由于车道在鸟瞰的情况下,可以看成是一条直线,所以如果我们将二维的图像向下压缩,降维到一维,这样会得到两个具有峰值到抛物线,峰值的坐标即为要寻找的车道线的起始点。这个向下的压缩过程实际上是一个将像素值纵向累加的一个过程,这里为了防止累加过程超过最大像素值(255),所以需要对图像的像素值进行二值化,对每个像素值除以255,这样白 {MOD}车道部分的像素值变为1,其他部分变为0。由于车道线可能会出现弯道情况,所以要对原图截取ROI,这样能尽可能保证处理车道线为直线,这里取原图的55%。 压缩过的图像为一个一维数组形式,以这个一维数组中心为起点,分别向两边进行搜索,找到的左右两边的峰值即为查找车道线的左右两个起始点。

6.滑动窗口搜索

通过直方图找到的起始点只是一个车道线搜索的起始参考点,并不能确定车道线的具体形状,所以我们要建立一个矩形窗口,以直方图的峰值为下边线的中心,统计出在这个窗口中车道线像素的分布,又此确定出当前窗口中车道的中心,以这个中心为下一个窗口的下边线中心,不断迭代这个过程,滑动窗口将会不断向上移动,最终将确定完整个车道线的点。

7.拟合车道线

通过滑动窗口可以获取到一系列车道线的点,根据这些点可以拟合出一个函数去表示这个车道,这里我们选取二次函数进行拟合。 做曲线的拟合对于辅助驾驶是十分重要的,通过这个函数可以画出车道线、计算车道线曲率,计算车辆与车道的夹角,这里使用Eigen库进行拟合。 curveCoefL = (leftMatrix.transpose() * leftMatrix).ldlt().solve( leftMatrix.transpose() * xValueL); curveCoefR = (rightMatrix.transpose() * rightMatrix).ldlt().solve( rightMatrix.transpose() * xValueR); curveCoefL和curveCoefR中保存的即为左右两个车道的拟合曲线的系数,车道线函数为x = a*y^2+b*y+c.

8.车道线绘制

我们在做滑动窗口时,会建立一个缓存帧机制,以5帧为一个缓存帧队列,当检测到车道线时将旧缓存帧出队,将当前新的内容加入到队列中,所以在绘制车道线时我们以缓存帧中的5个车道线函数系数做平均: curveCoefL = (curveCoefRecordL[0] + curveCoefRecordL[1] + curveCoefRecordL[2] + curveCoefRecordL[3] + curveCoefRecordL[4]) / 5; curveCoefR = (curveCoefRecordR[0] + curveCoefRecordR[1] + curveCoefRecordR[2] + curveCoefRecordR[3] + curveCoefRecordR[4]) / 5; 用平均过的系数算出从y=0到imageHeight的每一个点,由于这里的点都是基于鸟瞰图获取到的,所以我们需要将这些鸟瞰图上的点转换到正常图像上。 我们在将原图转换为鸟瞰图时,会生成一个转换矩阵 cv::Mat perspectiveMatrix = getPerspectiveTransform(perspectiveSrc, perspectiveDst); 由于要还原透视变换,所以我们对转换矩阵求逆,通过perspectiveTransform函数将要变换的点处理到原图中 cv::perspectiveTransform(curvePointsR, rPoint, inv_perspective); 最后得到的效果图如下: