文章目录[隐藏]
本文用来整理回顾所学知识,也能使视觉领域初学者的同伴们少走些弯路。
参考链接:无人驾驶汽车系统入门(三十)——基于深度神经网络LaneNet的车道线检测及ROS实现_AdamShan的博客-CSDN博客
论文原文地址:Towards End-to-End Lane Detection: an Instance Segmentation Approach
Tensorflow代码地址:lanenet-lane-detection
现在作者维护的版本更新到了2021.4.29。我上传到了百度网盘里:
链接:https://pan.baidu.com/s/1XydBVV-niTo9GybRDhquww
提取码:yos4
文件夹结构为:
```
lanenet-lane-detection-master
├─ .idea
│ └─ vcs.xml
├─ config
│ └─ tusimple_lanenet.yaml
├─ data
│ ├─ source_image
│ │ ├─ accuracy.png
│ │ ├─ binary_seg_loss.png
│ │ ├─ instance_seg_loss.png
│ │ ├─ lanenet_batch_test.gif
│ │ ├─ lanenet_binary_seg.png
│ │ ├─ lanenet_embedding.png
│ │ ├─ lanenet_instance_seg.png
│ │ ├─ lanenet_mask_result.png
│ │ ├─ network_architecture.png
│ │ ├─ qr.jpg
│ │ └─ total_loss.png
│ ├─ training_data_example
│ │ ├─ gt_binary_image
│ │ │ ├─ 0000.png
│ │ │ ├─ 0001.png
│ │ │ ├─ 0002.png
│ │ │ ├─ 0003.png
│ │ │ ├─ 0004.png
│ │ │ └─ 0005.png
│ │ ├─ gt_instance_image
│ │ │ ├─ 0000.png
│ │ │ ├─ 0001.png
│ │ │ ├─ 0002.png
│ │ │ ├─ 0003.png
│ │ │ ├─ 0004.png
│ │ │ └─ 0005.png
│ │ ├─ image
│ │ │ ├─ 0000.png
│ │ │ ├─ 0001.png
│ │ │ ├─ 0002.png
│ │ │ ├─ 0003.png
│ │ │ ├─ 0004.png
│ │ │ └─ 0005.png
│ │ ├─ train.txt
│ │ └─ val.txt
│ ├─ tusimple_ipm_remap.yml
│ └─ tusimple_test_image
│ ├─ 0.jpg
│ ├─ 1.jpg
│ ├─ 2.jpg
│ └─ 3.jpg
├─ data_provider
│ ├─ lanenet_data_feed_pipline.py
│ └─ tf_io_pipline_tools.py
├─ lanenet_model
│ ├─ lanenet.py
│ ├─ lanenet_back_end.py
│ ├─ lanenet_discriminative_loss.py
│ ├─ lanenet_front_end.py
│ ├─ lanenet_postprocess.py
│ └─ __init__.py
├─ LICENSE
├─ local_utils
│ ├─ config_utils
│ │ ├─ parse_config_utils.py
│ │ └─ __init__.py
│ └─ log_util
│ ├─ init_logger.py
│ └─ __init__.py
├─ mnn_project
│ ├─ config.ini
│ ├─ config_parser.cpp
│ ├─ config_parser.h
│ ├─ convert_lanenet_model_into_mnn_model.sh
│ ├─ dbscan.hpp
│ ├─ freeze_lanenet_model.py
│ ├─ kdtree.cpp
│ ├─ kdtree.h
│ ├─ lanenet_model.cpp
│ ├─ lanenet_model.h
│ └─ __init__.py
├─ README.md
├─ requirements.txt
├─ semantic_segmentation_zoo
│ ├─ bisenet_v2.py
│ ├─ cnn_basenet.py
│ ├─ vgg16_based_fcn.py
│ └─ __init__.py
├─ tools
│ ├─ evaluate_lanenet_on_tusimple.py
│ ├─ evaluate_model_utils.py
│ ├─ generate_tusimple_dataset.py
│ ├─ make_tusimple_tfrecords.py
│ ├─ test_lanenet.py
│ └─ train_lanenet_tusimple.py
├─ trainner
│ ├─ tusimple_lanenet_multi_gpu_trainner.py
│ ├─ tusimple_lanenet_single_gpu_trainner.py
│ └─ __init__.py
└─ _config.yml
```
一、使用TensorFlow2.4.0实现及基于tuSimple数据集的模型训练
1. tuSimple数据集准备
下载地址:tusimple数据集
我也将它上传到了百度网盘:
链接:https://pan.baidu.com/s/1ZyH4_tV7Nxphrb4MBt7Lcw
提取码:9ioq
下载完成后解压缩到一个目录下,目录内容如下:
tuSimple/
├── clips
│ ├── 0313-1
│ ├── 0313-2
│ ├── 0530
│ ├── 0531
│ └── 0601
├── label_data_0313.json
├── label_data_0531.json
├── label_data_0601.json
├── readme.md
└── test_tasks_0627.json
将解压好的tuSimple文件夹放置于data目录下。
2. 如何将代码在 TensorFlow 2.4.0上使用
只需在每个需要导入 TensorFlow 模块的.py文档中加入下述语句:
import tensorflow
tensorflow.compat.v1.disable_eager_execution()
tf = tensorflow.compat.v1
这样就可以在TensorFlow 2.4.0运行 TensorFlow 1.0 版本的代码。
使用以下命令安装一下运行代码所需的包。注意这里的requirements.txt文件中的包可能版本比较老,如果和TensorFlow 2.4.0不兼容,升级一下即可。
pip install -r requirements.txt
3. 生成用于训练的数据
使用项目lanenet-lane-detection中的脚本generate_tusimple_dataset.py产生用于训练的binary mask和instance mask;
3.1 生成文件夹training和testing
根据.json文件转换训练集,生成图片文件夹gt_image、gt_binary_image、gt_instance_image 以及文本文件 train.txt。
在终端输入以下命令:
cd ./tools
python generate_tusimple_dataset.py --src_dir=D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\data\tuSimple
此处需要等待几分钟:
在tuSimple目录下自动生成了training
和testing
两个目录,如下所示:
tuSimple/
|
├──training/
| ├── gt_binary_image
| ├── gt_image
| ├── gt_instance_image
| ├── label_data_0313.json
| ├── label_data_0531.json
| ├── label_data_0601.json
| └── train.txt
└──testing/
└── test_tasks_0627.json
可见该脚本仅生成了train.txt,我们可以手动分割一下train set和val set,也就是剪切train.txt中的一部分到一个新建的val.txt和test.txt文件中。
打开train.txt,可以看到共有3626行数据,此处我粘贴了400行到val.txt中,200行粘贴到test.txt中。这里可以在合理范围内自己设置。
之后运行代码可能遇到的问题:
ModuleNotFoundError: No module named 'data_provider'
只需在代码前加上以下语句:
import sys
import os
sys.path.append(os.getcwd())
3.2 将标注格式转换成TFRecord
3.2.1 更改config目录下的tusimple_lanenet.yaml文件。
更改数据集的路径,如下所示。
DATASET:
DATA_DIR: 'D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\data\tuSimple\training'
IMAGE_TYPE: 'rgb' # choice rgb or rgba
NUM_CLASSES: 2
TEST_FILE_LIST: 'D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\data\tuSimple\training\test.txt'
TRAIN_FILE_LIST: 'D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\data\tuSimple\training\train.txt'
VAL_FILE_LIST: 'D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\data\tuSimple\training\val.txt'
IGNORE_INDEX: 255
PADDING_VALUE: [127.5, 127.5, 127.5]
MEAN_VALUE: [0.5, 0.5, 0.5]
STD_VALUE: [0.5, 0.5, 0.5]
CPU_MULTI_PROCESS_NUMS: 8
3.2.2 使用脚本生成tfrecord文件
命令如下:
cd ..
python tools/make_tusimple_tfrecords.py --dataset_dir D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\data\tuSimple\training --tfrecords_dir D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\data\tuSimple\training\tfrecords
等待几分钟,脚本会在项目的data/training/tfrecords
目录下生成相应的tfrecord文件,如下所示:
4. 训练自己的数据集
4.1更改代码:
更改data_provider\lanenet_data_feed_pipline.py第799行
def get_next(self,name=None):
return self._next_internal()
# def get_next(self):
# return self._next_internal()
4.2 开始训练
运行 tools/train_lanenet_tusimple.py,开始训练:
也可以在终端中输入:
python tools/train_lanenet.py --dataset_dir D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\data\tuSimple\training --multi_gpus False
使用tensorboard查看训练过程:
cd tboard/tusimple/bisenetv2_lanenet
tensorboard --logdir=.
在训练过程中,可以通过tensorboard查看模型在验证集上的总损失(val_cost)、分割损失(val_binary_seg_loss)、嵌入损失(val_instance_seg_loss)以及分割精度(val_accuracy)变化曲线,如下所示:
还可以查看模型在训练过程中的分割分支和嵌入分支输出到预测图,如下图所示:
若出现以下问题:
2021-11-15 21:43:06.595835: I tensorflow/stream_executor/cuda/cuda_driver.cc:789] failed to allocate 2.20G (2363280384 bytes) from device: CUDA_ERROR_OUT_OF_MEMORY: out of memory
更改config目录下的tusimple_lanenet.yaml文件中的 batch_num (从32改为8)
TRAIN:
MODEL_SAVE_DIR: 'model/tusimple/'
TBOARD_SAVE_DIR: 'tboard/tusimple/'
MODEL_PARAMS_CONFIG_FILE_NAME: "model_train_config.json"
RESTORE_FROM_SNAPSHOT:
ENABLE: False
SNAPSHOT_PATH: ''
SNAPSHOT_EPOCH: 8
BATCH_SIZE: 8
VAL_BATCH_SIZE: 4
EPOCH_NUMS: 905
WARM_UP:
ENABLE: True
EPOCH_NUMS: 8
FREEZE_BN:
ENABLE: False
COMPUTE_MIOU:
ENABLE: True
EPOCH: 1
MULTI_GPU:
ENABLE: True
GPU_DEVICES: ['0', '1']
CHIEF_DEVICE_INDEX: 0
SOLVER:
LR: 0.001
LR_POLICY: 'poly'
LR_POLYNOMIAL_POWER: 0.9
OPTIMIZER: 'sgd'
MOMENTUM: 0.9
WEIGHT_DECAY: 0.0005
MOVING_AVE_DECAY: 0.9995
LOSS_TYPE: 'cross_entropy'
OHEM:
ENABLE: False
SCORE_THRESH: 0.65
MIN_SAMPLE_NUMS: 65536
GPU:
GPU_MEMORY_FRACTION: 0.9
TF_ALLOW_GROWTH: True
POSTPROCESS:
MIN_AREA_THRESHOLD: 100
DBSCAN_EPS: 0.35
DBSCAN_MIN_SAMPLES: 1000
LOG:
SAVE_DIR: './log'
LEVEL: INFO
这里我们仍然是训练的tuSimple数据集,也可以制作自己的数据集。
训练好的权重 百度网盘链接:
链接:https://pan.baidu.com/s/1iNvf6IfU9Fg718xIhze4rg
提取码:9b0a
复制这段内容后打开百度网盘手机App,操作更方便哦
将权重文件放在model目录下,然后就可以测试结果了。
5. 测试结果
终端输入:
python tools/test_lanenet.py --weights_path D:\DOCS\deepl_lane_detection\lanenet-lane-detection-master\model\tusimple_lanenet.ckpt --image_path ./data/tusimple_test_image/0.jpg
结果:
二、代码及原理知识梳理(未完待续)
上图为LaneNet车道线检测的网络结构。这是一种端到端的车道线检测算法,包括LaneNet和H-Net两个网络模型。
其中,LaneNet是一种将语义分割和对像素进行向量表示结合起来的多任务模型,目的是将不同车道线实例化(通过用车道 id 标记每个车道像素来输出车道实例图);
H-Net是由卷积层和全连接层组成的网络模型,负责预测转换矩阵H,目的是对属于同一车道线的像素点进行回归(该矩阵学习以输入图像为条件的透视变换)。
对于每条车道,拟合三阶多项式并将车道重新投影到图像上。下文将具体介绍每一部分实现的功能。
1. LaneNet网络架构
如图,有两个解码分支:
1)Segmentation branch 负责对输入图像进行语义分割(对像素进行二分类,判断像素属于车道线还是背景),得到Binary image;
为了训练一个这样的分割网络,原论文使用了tuSimple数据集,在数据集中,车道线可能被其他车辆阻挡,在这种情况下,将车道线的标注(Ground truth)贯穿障碍物,如下图所示,从而使得分割网络在车道线被其他障碍物阻挡的情况下,依然可以完整检测出完整的车道线像素。
分割网络使用标准的交叉熵损失函数进行训练,对于这个逐像素分类任务而言(车道线像素/非车道线像素分类),由于两个类别的像素极度不均衡,为了处理此问题,作者使用了bounded inverse class weighting方法。
实例分割任务由两部分组成,分割和聚类部分,在下面的部分中更详细地解释。 为了在速度和准确性方面提高性能 [27],这两个部分在多任务网络中联合训练
2) Embedding branch 对像素进行嵌入式表示,论文将嵌入式向量的维度设置为4,训练得到的 embedding 向量用于聚类。最后将两个分支的结果进行结合利用 Mean-Shift 算法进行聚类,得到实例分割的结果。
不受可以检测的车道数量的限制,并且能够应对车道变化。
通过叠加嵌入分支和分割分支,在使用神经网络提取出车道线像素的同时,还能够对每个车道线实现聚类(即像素属于哪一根车道线)。为了训练这样的聚类嵌入网络,聚类损失函数(嵌入网络)包含两部分,方差项 L v a r L_{var}Lvar 和距离项 L d i s t L_{dist}Ldist ,其中 L v a r L_{var}Lvar 将每个嵌入的向量往某条车道线聚类中心(均值)方向拉,这种“拉力”激活的前提是嵌入向量到平均嵌入向量的距离过远,大于阈值 δ v \delta_vδv ; L d i s t L_{dist}Ldist 使两个类别的车道线越远越好,激活这个“推力”的前提是两条车道线聚类中心的距离过近,近于阈值 δ d \delta_dδd 。最后总的损失函数L的公式如下:
其中 C CC 表示聚类的数量(也就是车道线的数量),N c N_cNc 表示聚类 c cc 中的元素数量,x i x_ixi 表示一个像素嵌入向量, μ c \mu_{c}μc 表示聚类 c cc 的均值向量,[ x ] + = m a x ( 0 , x ) [x]_{+} = max(0, x)[x]+=max(0,x),最后的损失函数为 L = L v a r + L d i s t L = L_{var} + L_{dist}L=Lvar+Ldist 。该损失函数在实际实现(TensorFlow)中代码如下:
2. H-Net网络架构
1. generate_tusimple_dataset.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Time : 18-5-18 下午7:31
# @Author : MaybeShewill-CV
# @Site : https://github.com/MaybeShewill-CV/lanenet-lane-detection
# @File : generate_tusimple_dataset.py
# @IDE: PyCharm Community Edition
"""
generate tusimple training dataset
"""
import argparse
import glob
import json
import os
import os.path as ops
import shutil
import sys
sys.path.append(os.getcwd())
import cv2
import numpy as np
def init_args():
"""
:return:
"""
parser = argparse.ArgumentParser()
parser.add_argument('--src_dir', type=str, help='The origin path of unzipped tusimple dataset')
return parser.parse_args()
def process_json_file(json_file_path, src_dir, ori_dst_dir, binary_dst_dir, instance_dst_dir):
"""
:param json_file_path:
:param src_dir: origin clip file path
:param ori_dst_dir:
:param binary_dst_dir:
:param instance_dst_dir:
:return:
"""
assert ops.exists(json_file_path), '{:s} not exist'.format(json_file_path)
image_nums = len(os.listdir(ori_dst_dir))
with open(json_file_path, 'r') as file:
for line_index, line in enumerate(file):
info_dict = json.loads(line)
image_dir = ops.split(info_dict['raw_file'])[0]
image_dir_split = image_dir.split('/')[1:]
image_dir_split.append(ops.split(info_dict['raw_file'])[1])
image_name = '_'.join(image_dir_split)
image_path = ops.join(src_dir, info_dict['raw_file'])
assert ops.exists(image_path), '{:s} not exist'.format(image_path)
h_samples = info_dict['h_samples']
lanes = info_dict['lanes']
image_name_new = '{:s}.png'.format('{:d}'.format(line_index + image_nums).zfill(4))
src_image = cv2.imread(image_path, cv2.IMREAD_COLOR)
dst_binary_image = np.zeros([src_image.shape[0], src_image.shape[1]], np.uint8)
dst_instance_image = np.zeros([src_image.shape[0], src_image.shape[1]], np.uint8)
for lane_index, lane in enumerate(lanes):
assert len(h_samples) == len(lane)
lane_x = []
lane_y = []
for index in range(len(lane)):
if lane[index] == -2:
continue
else:
ptx = lane[index]
pty = h_samples[index]
lane_x.append(ptx)
lane_y.append(pty)
if not lane_x:
continue
lane_pts = np.vstack((lane_x, lane_y)).transpose()
lane_pts = np.array([lane_pts], np.int64)
cv2.polylines(dst_binary_image, lane_pts, isClosed=False,
color=255, thickness=5)
cv2.polylines(dst_instance_image, lane_pts, isClosed=False,
color=lane_index * 50 + 20, thickness=5)
dst_binary_image_path = ops.join(binary_dst_dir, image_name_new)
dst_instance_image_path = ops.join(instance_dst_dir, image_name_new)
dst_rgb_image_path = ops.join(ori_dst_dir, image_name_new)
cv2.imwrite(dst_binary_image_path, dst_binary_image)
cv2.imwrite(dst_instance_image_path, dst_instance_image)
cv2.imwrite(dst_rgb_image_path, src_image)
print('Process {:s} success'.format(image_name))
def gen_train_sample(src_dir, b_gt_image_dir, i_gt_image_dir, image_dir):
"""
generate sample index file
:param src_dir:
:param b_gt_image_dir:
:param i_gt_image_dir:
:param image_dir:
:return:
"""
with open('{:s}/training/train.txt'.format(src_dir), 'w') as file:
for image_name in os.listdir(b_gt_image_dir):
if not image_name.endswith('.png'):
continue
binary_gt_image_path = ops.join(b_gt_image_dir, image_name)
instance_gt_image_path = ops.join(i_gt_image_dir, image_name)
image_path = ops.join(image_dir, image_name)
assert ops.exists(image_path), '{:s} not exist'.format(image_path)
assert ops.exists(instance_gt_image_path), '{:s} not exist'.format(instance_gt_image_path)
b_gt_image = cv2.imread(binary_gt_image_path, cv2.IMREAD_COLOR)
i_gt_image = cv2.imread(instance_gt_image_path, cv2.IMREAD_COLOR)
image = cv2.imread(image_path, cv2.IMREAD_COLOR)
if b_gt_image is None or image is None or i_gt_image is None:
print('图像对: {:s}损坏'.format(image_name))
continue
else:
info = '{:s} {:s} {:s}'.format(image_path, binary_gt_image_path, instance_gt_image_path)
file.write(info + '\n')
return
def process_tusimple_dataset(src_dir):
"""
:param src_dir:
:return:
"""
traing_folder_path = ops.join(src_dir, 'training')
testing_folder_path = ops.join(src_dir, 'testing')
os.makedirs(traing_folder_path, exist_ok=True)
os.makedirs(testing_folder_path, exist_ok=True)
for json_label_path in glob.glob('{:s}/label*.json'.format(src_dir)):
json_label_name = ops.split(json_label_path)[1]
shutil.copyfile(json_label_path, ops.join(traing_folder_path, json_label_name))
for json_label_path in glob.glob('{:s}/test*.json'.format(src_dir)):
json_label_name = ops.split(json_label_path)[1]
shutil.copyfile(json_label_path, ops.join(testing_folder_path, json_label_name))
gt_image_dir = ops.join(traing_folder_path, 'gt_image')
gt_binary_dir = ops.join(traing_folder_path, 'gt_binary_image')
gt_instance_dir = ops.join(traing_folder_path, 'gt_instance_image')
os.makedirs(gt_image_dir, exist_ok=True)
os.makedirs(gt_binary_dir, exist_ok=True)
os.makedirs(gt_instance_dir, exist_ok=True)
for json_label_path in glob.glob('{:s}/*.json'.format(traing_folder_path)):
process_json_file(json_label_path, src_dir, gt_image_dir, gt_binary_dir, gt_instance_dir)
gen_train_sample(src_dir, gt_binary_dir, gt_instance_dir, gt_image_dir)
return
if __name__ == '__main__':
args = init_args()
process_tusimple_dataset(args.src_dir)
2.
三、一些参考文献
1、图森TuSimple车道线检测数据集介绍_十栋前的猫的博客-CSDN博客
tusimple-benchmark/doc/lane_detection at master · TuSimple/tusimple-benchmark · GitHub
简略总结:
tuSimple数据集大约包括7000个1秒钟长的20帧的视频剪辑(clips)。只有每个剪辑的最后一帧的车道带标签。
数据集的规模:
- 训练:3626个视频剪辑,3626个带标注的帧(每个剪辑clips有20帧,最后一帧带有标注)
- 测试:2782个视频剪辑
数据集采集时复杂条件:1. 复杂的天气条件(良好和中等) 2. 白天傍晚 3. 车道线数量(2/3/4/甚至更多) 4. 复杂的路况
注释类型: 折现标记
2、[深度学习] 车道线检测调研(lane detection)_Holeung blog-CSDN博客
深度学习车道线检测论文集
版权声明:本文为CSDN博主「tiger&sheep」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_49332456/article/details/121276739
暂无评论