Lane Detect

basic image process

使用数字图像的基础方法进行车道线检测(概率霍夫+聚类),不调用opencv相关算法,在TuSimple Lane Dataset评测。

数字图像处理课程的大作业,要求c++实现,不能使用神经网络,除输入输出外不调用opencv库。

程序思路

  • 转换到HSL空间, 提取黄色车道线

  • 转换为灰度图并进行对比度拉伸(Gamma), 设阈值得到白色车道线

  • 相加两张图片,并遮盖天空(固定区域)

  • 通过概率霍夫检测直线

  • 对直线进行聚类

1.提取车道线

  • 黄色:

    转换到HSL色彩空间,取H:0~40,L大于100左右来剔除周围黄土

  • 白色:

    对灰度图进行对比度拉伸,然后根据平均亮度取一个阈值(写完仔细一想直方图均衡化就行了,但这样效果感觉够用,时间也比较紧张就没改)

	// contrast stretching
    Mat image_Gray;
    cvtColor(src, image_Gray, cv::COLOR_BGR2GRAY);
    uchar* ptr = image_Gray.ptr();
    for (int i = 0; i < image_Gray.cols * image_Gray.rows; i++, ptr++) {
        int val = *ptr;
        *ptr = 255 * pow(val / 255.0, 1.2);//gamma
    }

根据图片亮度选择阈值,只计算下半张图片以除去天空影响,得到平均亮度,乘以一个系数作为亮度阈值(1.4左右)

    // determine lightness threshold = average lightness * coefficient
    long threshold_image = 0;
    for (int i = 360; i < src.rows; i++) {
        for (int j = 0; j < src.cols; j++) {
            threshold_image += image_HSL.at<Vec3b>(i, j)[2];
        }
    }
    threshold_image /= src.cols * (src.rows - 360);
    threshold_image *= 1.2; //1.4

原图:

灰度图:

计算阈值并遮盖天空后:

2.概率霍夫直线检测

一开始写了标准霍夫,发现效果很不好,尤其是因为需要检测出图中所有车道线,所以对照opencv源码写了概率霍夫。概率霍夫不进行边缘检测得到的效果更好,故没有进行边缘检测。

记录图中所有点:

// collect points
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            if (image.at<uchar>(i, j) > 0) {
                pts.push_back(Point(i, j));
                msk[i * width + j] = (uchar)1;
            }
            else {
                msk[i * width + j] = 0;
            }
        }
    }

随机选择点:

// select point
    std::random_device rand_dev;
    std::mt19937 generator(rand_dev());
    std::shuffle(pts.begin(), pts.end(), generator); // shuffle points randomly

对每个点各角度计算vote(为方便直接固定步长1度):

for (int n = 0; n < 180; n++) {
            int r = round(j * ttab[2 * n] + i * ttab[2 * n + 1]);
            r += (numrho - 1)/2;
            int vote = ++acc[n*180+r];
            if (max_vote < vote) {
                max_vote = vote;
                max_angle = n;
                rec_r = r- (numrho - 1) / 2;
            }
        }

如果当前[rho,theta]的vote未达到阈值,计算下一个点

if (max_vote < threshold )
            continue;

如果达到vote阈值,沿此方向找直线,大于允许的最大间隔长度则停止,期间更新直线的起点和终点。 计算di时由于取dj=1, di很小, 需要扩大移位避免精度问题。

// go left or right
for (int k = 0; k < 2; k++) {
    int gap = 0;

    int i_mv = i;
    int j_mv = j;

    int dj = 1;
    int di = cos_dir < 0 ? cot_dir : -cot_dir;

    i_mv = i_mv << shift;

    if (k == 1) {
        di = -di;
        dj = -dj;
    }
    for (; i_mv>>shift < height && i_mv>>shift > 0 && j_mv < width && j_mv > 0; j_mv += dj, i_mv += di) {
        if (msk[(i_mv >> shift) * width + j_mv]) {
            gap = 0;
            if (k == 0) {
                line_st.y = i_mv >> shift;
                line_st.x = j_mv;
            }
            else {
                line_end.y = i_mv >> shift;
                line_end.x = j_mv;
            }
        }
        else if (++gap > lineGap)break;
    }
}

确定直线后,再对直线上每个点各角度的vote–,消除本条直线的影响。

3.直线聚类

对直线进行聚类,因为前一步检测效果还行,聚类的要求不高,随便写了一个简单的分组,由于较长直线一般是最符合实际方向的,将每组中最长的线作为代表直线。

Cluster记录[rho, theta, cnt, length]

vector<Vec4i> Hough::ClusterLines(std::vector<Vec4i>& lines, vector<Vec2d>& pos, int threshold_rho, int threshold_theta) {
    int siz = lines.size();
    int* vis = new int[siz];
    vector<Vec4i> cluster;
    for (int k = 0; k < siz; k++)vis[k] = 0;

    for (int i = 0; i < siz; i++) {
        if (vis[i])continue;
        int m_theta = pos[i][1];
        int m_rho = pos[i][0];
        int m_len = sqrt(pow(lines[i][0] - lines[i][2], 2) + pow(lines[i][1] - lines[i][3], 2));
        int cnt = 1;
        vis[i] = 1;
        for (int j = 0; j < siz; j++) {
            if (vis[j])continue;
            // find nearby lines and group them
            if (abs(pos[i][0] - pos[j][0]) < threshold_rho && abs(pos[i][1] - pos[j][1]) < threshold_theta) {
                int cl_len = sqrt(pow(lines[j][0] - lines[j][2], 2) + pow(lines[j][1] - lines[j][3], 2));
                vis[j] = 1;
                // take longest line as represent
                if (m_len > cl_len) {
                    m_len = cl_len;
                    m_rho = pos[j][0];
                    m_theta = pos[j][1];
                }
                cnt++;
                
            }
        }
        MergeCluster(cluster, m_rho, m_theta, m_len, cnt, threshold_rho, threshold_theta);
    }

    return cluster;
}

这样分组后,可能有实际很近的组因为选到了边缘直线最为初始分组依据被分开,所以递归合并各组MergeCluster,对于长度更长的组,合并时给予更大权重。

cluster[no][rho, theta, cnt, length]

for (int p = 0; p < cluster.size(); p++) {
    if (abs(cluster[p][0] - rho_mean) < threshold_rho && abs(cluster[p][1] - theta_mean) < threshold_theta) {
        // weighted merge
        int new_rho = (cluster[p][3]*cluster[p][0] + len_mean*rho_mean) / (len_mean+cluster[p][3]);
        int new_theta = (cluster[p][3] * cluster[p][1] + len_mean * theta_mean) / (len_mean + cluster[p][3]);
        int new_cnt = cluster[p][2] + cnt;

        cluster.erase(cluster.begin() + p);
        MergeCluster(cluster, new_rho, new_theta, new_cnt, new_len, threshold_rho, threshold_theta);
    }
}

如果合并后数量仍多于4条,不是噪声影响就是有五条车道,噪声的情况不谈,5条车道的话因为要求最多输出4条,选择4条最可能的直线(计算与人为设定的4条标准直线的距离)

结果:

红色为霍夫直线,绿色为聚类后直线

输出时截掉上半部分直线。

4.结果及分析

使用TuSimple提供的测试脚本,绿色为ground truth, 蓝色为预测结果,输出(accuracy, fp, fn)

对于光照条件正常,车道线清晰的情况比较准确:

如果地面没有白色线或间距大,没办法检测出(右侧第二根):

此方法也没有考虑弯道:


See also