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);
最后得到的效果图如下: