【车道线检测项目-课程学习笔记】

一、车道线检测概述及传统视觉检测方法实战

1、学习准备

  • 第三方库:numpy(矩阵计算)
  • 基础图像处理相关知识:opencv
  • 深度学习框架:pytorch、tensorflow

 

2、算法介绍

传统的语义分割要判断每一个像素点,该算法把图像分割成一个个的小网格;

例如原图是1280*720像素的图像,通过分割成一个个网格,把判断像素->判断小网格,把识别到的小网格连接起来,从而大大减小计算量,易于部署到移动端,且准确度高;

从而把语义分割问题->检测问题;

 

 

3、传统车道线检测方法

openCV进行编码完成

4、数据增强方法

在有限的数据集中,通过数据增强,达到提升识别准确度和解决图像损坏、位置被遮挡的问题。

一般常用前两种方法:空间转换和颜色扭曲

 

进行图像擦除,从而提高图像的整体识别能力和抗干扰能力

 进行图像融合,达到图像雾化模糊的效果,从而提高图像的整体识别能力和抗干扰能力

同理还有图像拼接

5、作业

(1)准备训练数据

 

(2)完成camera calibration文件代码,该代码可以将有镜头畸变点图像进行校正,类似鱼眼摄像头的图像转换为平整的图像;大体实验流程是根据棋盘格校正法求出相机的内参和外参,然后根据求得的内参,外参进行图像的校正(undistort)

  • cv2.getPerspectiveTransform()
复制代码
import cv2
import numpy as np

# 读取图像
img = cv2.imread('source_image.jpg')

# 定义源点和目标点
pts1 = np.float32([[94,302], [205,243], [152,369], [265,300]])
pts2 = np.float32([[0,0], [300,0], [0,200], [300,200]])

# 计算透视变换矩阵
matrix = cv2.getPerspectiveTransform(pts1, pts2)

# 应用透视变换
img_output = cv2.warpPerspective(img, matrix, (300, 200))

# 展示图像
cv2.imshow('Original Image', img)
cv2.imshow('Transformed Image', img_output)
cv2.waitKey(0)
cv2.destroyAllWindows()
复制代码

 

  • cv2.warpPerspective()
复制代码
import cv2
import numpy as np

if __name__ == '__main__':
    # 生成输入图像
    h, w = 200, 300  # 图像大小
    origin_image = np.zeros((h, w), dtype=np.uint8)
    pts = np.array([[50, 50], [100, 160], [250, 50], [100, 250]])  # 源图像中的四个角点坐标
    pts_new = np.array([[10, 10], [200, 150], [100, 300], [200, 250]])  # 目标图像中的四个角点坐标
    M = cv2.getPerspectiveTransform(pts, pts_new)  # 获取透视变换矩阵
    dst = cv2.warpPerspective(origin_image, M, (w, h))  # 应用透视变换
复制代码
  • cv2.undistort()
复制代码
import cv2
import numpy as np

# 假设 cameraMatrix 和 distCoeffs 已经通过标定获得
cameraMatrix = np.array(...)
distCoeffs = np.array(...)

# 读取图像
img = cv2.imread('input.jpg')

# 去畸变处理
dst = cv2.undistort(img, cameraMatrix, distCoeffs, None, cameraMatrix)

# 显示结果图像
cv2.imshow('Undistorted Image', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
复制代码

 

  •  cv2.calibrateCamera()
复制代码
 1 import numpy as np
 2 import cv2
 3 
 4 # 定义世界坐标系中的角点位置
 5 def calculate_world_corner(corner_scale, square_size):
 6     corner_height, corner_width = corner_scale
 7     obj_points = np.zeros([corner_height * corner_width, 3])
 8     obj_points[:, :2] = np.mgrid[0:corner_height, 0:corner_width].T.reshape(-1, 2) * square_size
 9     return obj_points
10 
11 # 设置相机内参矩阵和畸变系数初始值
12 img_size = (960, 960)
13 focal = 1200
14 pix_spacing = 0.309
15 camera_matrix = np.array([[focal / pix_spacing, 0, img_size / 2], [0, focal / pix_spacing, img_size‌:ml-citation{ref="1" data="citationList"} / 2], [0, 0, 1]])
16 dist_coeffs = np.zeros((1, 5))
17 obj_points = calculate_world_corner([5, 5], 25)  # 生成世界坐标系中的角点位置
18 
19 # 进行相机标定
20 ret, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points, img_size, cameraMatrix, distCoeffs)
复制代码

 

(3) 完成perspective_transform文件代码,该代码将车道线尽头汇聚到一起的图像转换成车道线近似平行的图像;大体实验步骤是根据原图中4个点坐标和变换后4个点坐标,求得投射变换矩阵,然后根据投射变换矩阵进行图像投射变换。

运行结果类似如下:

 

二、CNN经典网络和语义分割模型

1、作业讲解:仿射变换

 

 

 

 

 

2、作业讲解:坐标系转换

人为指定空间坐标的位置作为一个世界坐标系的原点,而相机的位置是固定的,它们两者之间的关系就是旋转和平移的关系,可以用矩阵进行旋转、平移等操作;

3、作业讲解:相机标定法

 

 

4、CNN(卷积神经网络)

 

  • 局部感知:用图像的一小块进行感知
  • 权值共享:在整个图像用同一个卷积核,减小计算量
  • 多卷积核:在每一层,用多个卷积核,提取不同的特征

5、卷积类型

Full卷积

默认卷积后大小为5-3+1=3,使用Full卷积,进行padding前后补0,扩大卷积后矩阵大小

 

same卷积从中心点开始卷积

 valid卷积正常从边缘开始卷积

 

 进行深度的卷积,对应层进行计算,然后相加得到结果

 卷积的计算

 Depthwise卷积

正常卷积为一层的结果,而Depthwise,输入是n*n,输出的结果也是n*n

 

 两种卷积方式对比

6、激活函数

激活函数作用:非线性变换,把线性方程映射成非线性方程

关于零对称的tanh效果好于sigmoid,因为sigmoid只能波动下降,而tanh可以直线下降

 残差=实际观测值预测值

 

7、常见神经网络

1*1卷积就是普通的卷积,例如输入图像是100*100*3,使用100个1*1卷积核,输出深度是100,使用2个1*1卷积核,输出深度是2;即输出卷积核的数量决定了升维还是降维;(1*1的计算量小,可以理解为copy)

添加1*1矩阵是为了进行升维降维操作;

  • 降维:减小计算量
  • 升维:增加特征维度,提升准确度,但是计算量变大

resent是先降在升

mobilentv2是先升在降

depthwise卷积,因为计算量减小了,所以可以升维,其他卷积网络一般降维;

 

 

 组卷积,为了减小计算量,分组进行卷积,各卷积核是一样的

8、作业

(1)用传统车道线方法完成车道线检测

(2) 使用tf或pytorch进行卷积操作

在 TensorFlow 中,使用 tf.keras.layers.Conv2D 来创建卷积层;在 PyTorch 中,使用 torch.nn.Conv2d。两者都允许你指定过滤器(filters/out_channels)、核大小(kernel_size)、步长(stride)和填充(padding)等参数。

复制代码
 1 import tensorflow as tf
 2  
 3 # 创建一个简单的卷积层
 4 conv_layer = tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), activation='relu')
 5  
 6 # 假设 input_shape 是 (height, width, channels)
 7 input_shape = (28, 28, 1)  # 例如,一个单通道的 28x28 图像
 8  
 9 # 创建一个输入张量
10 inputs = tf.random.normal([1, 28, 28, 1])  # batch_size=1
11  
12 # 应用卷积层
13 output = conv_layer(inputs)
14  
15 print(output.shape)  # 查看输出张量的形状
复制代码

 

复制代码
 1 import torch
 2 import torch.nn as nn
 3  
 4 # 创建一个简单的卷积层
 5 conv_layer = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, stride=1, padding=0)
 6  
 7 # 假设 input_shape 是 (batch_size, channels, height, width)
 8 input_shape = (1, 1, 28, 28)  # 例如,一个单通道的 28x28 图像
 9  
10 # 创建一个输入张量
11 inputs = torch.randn(input_shape)
12  
13 # 应用卷积层
14 output = conv_layer(inputs)
15  
16 print(output.shape)  # 查看输出张量的形状
复制代码

 

部分关键代码说明:

(1)简化常见模型,定义一个卷积操作类。为显示图像特征,对卷积结果求绝对值。

复制代码
class test(nn.Module):

      def __init__(self):

          super(test, self).__init__()

          self.conv1 = nn.Conv2d(3, 16, 5)       


      def forward(self, x):       

          x = abs(self.conv1(x)) 

          return x
复制代码

 

 

(2)输入准备,使输入信息与模型输入匹配

img = cv2.imread("../testImage/straight_lines1.jpg")

img=np.transpose(img, (2, 0, 1))

img=torch.tensor(img)

img = torch.unsqueeze(img, dim=0).float()

 

 

(3)模型实例化,传入数据,完成卷积操作

model = test()

out_put=model(img)

 

(4)卷积后的特征图像显示

复制代码
for feature_map in out_put:

    # [N, C, H, W] -> [C, H, W]

    im = np.squeeze(feature_map.detach().numpy())

    # [C, H, W] -> [H, W, C]

    im = np.transpose(im, [1, 2, 0])

    # show top 16 feature maps

    plt.figure(figsize = (120, 80))

    for i in range(16):

        ax = plt.subplot(4, 4, i+1)

        # [H, W, C]

        plt.imshow(im[:, :, i])#这里为效果好一点,没设置为灰度显示

plt.show()
复制代码

 

特征图:

 

(3)跑通LeNet、VGG或ResNet(选做)

跑完后加载模型预测:

复制代码
classes = ('plane', 'car', 'bird', 'cat',

           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

net = LeNet()

net.load_state_dict(torch.load('Lenet.pth'))

 

im = Image.open('test.jpg')

plt.figure(figsize = (10, 8))

plt.title('预测的图片')  

plt.imshow(im )

plt.axis('off')

im = transform(im)  # [C, H, W]

im = torch.unsqueeze(im, dim=0)  # [N, C, H, W]

 

with torch.no_grad():

    outputs = net(im)

    predict = torch.max(outputs, dim=1)[1].data.numpy()

print(f'预测的结果:{classes[int(predict)]}')
复制代码

 

三、车道线分割模型应用(语义分割网络)

1、语义分割

语义分割是对图像中的每个像素进行分类

实例分割是检测出一个个个体实例

 

语义分割由于要对每一个像素进行判断,要求输出图像和输入图像大小保持一致;

因此在语义分割网络中,先下采样encoding编码压缩图像,后上采样decoding解码图像;

优点:

  • 增加感受野,融合了空间的特征
  • 减小计算量
  • 从不同的尺度进行融合,增加了尺度的信息

2、全卷积网络

全连接网络要求固定图像尺寸;

全卷积网络允许任意图像尺寸;

全连接网络->全卷积网络,可以在原图像基础上,和一个一样大小的卷积核进行计算,从而得到任意要求的尺寸;

 

 

 

3、上采样方法

 

 

 

 

 注意转置卷积仅能恢复到原来特征图的维度大小,不能保证图像数据的准确;

4、语义分割-FCN网络

每层的1只代表匹配到当前目标(如人、草等)

concatenate和add都要求特征图的尺度是一样的;

 add是维度不变,特征值进行相加;

concatenate是维度进行相加;

5、深度学习模型训练基本步骤

epoch是执行完所有的样本

epoch = iteration * batchsize

一个batchsize迭代一次就是iteration

在数据预处理中,可以对图像进行截取,例如车道线检测任务,上方的三分之一的天空可以截除掉;

数据归一化处理:减均值,除方差等

 

6、Finetune模型

 

自身数据集多,对预训练模型改动大;

反之数据集少,对预训练模型改动小;

7、度量方法

 IOU=交集 / 并集

8、作业

 (1)插值方式实现

复制代码
# 插值内核
def u(s, a):
    if (abs(s) >= 0) & (abs(s) <= 1):
        return (a + 2) * (abs(s) ** 3) - (a + 3) * (abs(s) ** 2) + 1
    elif (abs(s) > 1) & (abs(s) <= 2):
        return a * (abs(s) ** 3) - (5 * a) * (abs(s) ** 2) + (8 * a) * abs(s) - 4 * a
    return 0


# 补边,将输入图像矩阵前后各扩展两行两列,当插值点为边沿点时候,确保周围有16个点
def padding(image, H, W, C):
    """
    :param image: 输入图像
    :param H: 高
    :param W: 宽
    :param C: 通道数
    :return: 补边后的图像
    """
    padding_image = np.zeros((H + 4, W + 4, C))
    padding_image[2: H + 2, 2: W + 2, :] = image

    # 分别对图像前后各两行、左右各两列进行补边
    padding_image[2: H + 2, 0: 2, :] = image[:, 0: 1, :]  # 左侧两列
    padding_image[2: H + 2, -2:, :] = image[:, -1:, :]  # 右侧两列
    padding_image[0: 2, 2: W + 2, :] = image[0, :, :]  # 上边两行
    padding_image[-2:, 2: W + 2, :] = image[-1, :, :]  # 下边两行

    # 对四个角上的16个点进行填充
    padding_image[0: 2, 0: 2, :] = image[0, 0, :]      # 左上侧
    padding_image[-2: 0, 0: 2, :] = image[-1, 0, :]    # 左下侧
    padding_image[-2: 0, -2: 0, :] = image[-1, -1, :]  # 右下侧
    padding_image[0: 2, -2: 0, :] = image[0, -1, :]    # 右上侧

    return padding_image


def bicubic_interpolation(src_data, dst_size, a=-0.5):
    """
    :param a:
    :param src_data: 输入图像矩阵
    :param dst_size: (dst_height, dst_width)
    :return: 输出图像
    """
    src_height, src_width, channel = src_data.shape
    dst_height, dst_width = dst_size

    src_data = padding(src_data, src_height, src_width, channel)  # 补边操作

    ratio_height = float(src_height) / dst_height
    ratio_width = float(src_width) / dst_width
    dst_data = np.zeros((dst_height, dst_width, channel), np.uint8)

    for dst_y in range(dst_height):
        for dst_x in range(dst_width):
            # 目标在源上的坐标
            src_y = dst_y * ratio_height + 2  # 加2是因为上面扩大了四行四列,要回到原来图像的点再计算
            src_x = dst_x * ratio_width + 2
            # 16个点距源点的距离
            x1 = 1 + src_x - int(src_x)
            x2 = src_x - int(src_x)
            x3 = int(src_x) + 1 - src_x
            x4 = int(src_x) + 2 - src_x

            y1 = 1 + src_y - int(src_y)
            y2 = src_y - int(src_y)
            y3 = int(src_y) + 1 - src_y
            y4 = int(src_y) + 2 - src_y

            mat_l = np.array([[u(x1, a), u(x2, a), u(x3, a), u(x4, a)]])  # 四个横坐标的权重
            mat_r = np.array([[u(y1, a)], [u(y2, a)], [u(y3, a)], [u(y4, a)]])  # 四个纵坐标的权重
            for i in range(channel):
                mat_m = np.array([[src_data[int(src_y - y1), int(src_x - x1), i],
                                   src_data[int(src_y - y2), int(src_x - x1), i],
                                   src_data[int(src_y + y3), int(src_x - x1), i],
                                   src_data[int(src_y + y4), int(src_x - x1), i]],

                                   [src_data[int(src_y - y1), int(src_x - x2), i],
                                    src_data[int(src_y - y2), int(src_x - x2), i],
                                    src_data[int(src_y + y3), int(src_x - x2), i],
                                    src_data[int(src_y + y4), int(src_x - x2), i]],

                                   [src_data[int(src_y - y1), int(src_x + x3), i],
                                    src_data[int(src_y - y2), int(src_x + x3), i],
                                    src_data[int(src_y + y3), int(src_x + x3), i],
                                    src_data[int(src_y + y4), int(src_x + x3), i]],

                                   [src_data[int(src_y - y1), int(src_x + x4), i],
                                    src_data[int(src_y - y2), int(src_x + x4), i],
                                    src_data[int(src_y + y3), int(src_x + x4), i],
                                    src_data[int(src_y + y4), int(src_x + x4), i]]])

                dst_data[dst_y, dst_x] = np.clip(np.dot(np.dot(mat_l, mat_m), mat_r), 0, 255)
    return dst_data
复制代码

 

复制代码
def bilinear_interpolation(src_data, dst_size):
    """
    :param src_data: 输入图像矩阵
    :param dst_size: (dst_height, dst_width)
    :return: 输出图像
    """
    src_height, src_width, channel = src_data.shape
    dst_height, dst_width = dst_size
    ratio_height = float(src_height) / dst_height
    ratio_width = float(src_width) / dst_width
    dst_data = np.zeros((dst_height, dst_width, channel), np.uint8)
    for dst_y in range(dst_height):
        for dst_x in range(dst_width):
            # 目标在源上的坐标
            src_y = (dst_y + 0.5) * ratio_height - 0.5
            src_x = (dst_x + 0.5) * ratio_width - 0.5
            # 向下取整,计算在源图上四个近邻点的位置
            src_y_0 = int(src_y)
            src_x_0 = int(src_x)
            src_y_1 = min(src_y_0 + 1, src_height - 1)
            src_x_1 = min(src_x_0 + 1, src_width - 1)

            # 双线性插值
            #for i in range(channel):
            value_0 = (src_x_1 - src_x) * src_data[src_y_0, src_x_0] \
                      + (src_x - src_x_0) * src_data[src_y_0, src_x_1]
            value_1 = (src_x_1 - src_x) * src_data[src_y_1, src_x_0] \
                      + (src_x - src_x_0) * src_data[src_y_1, src_x_1]
            dst_data[dst_y, dst_x] = (src_y_1 - src_y) * value_0 + (src_y - src_y_0) * value_1
    return dst_data
复制代码

(2)数据处理和评价指标

复制代码
    def __getitem__(self, index):  # 根据索引index返回数据及标签
        path_image = self.image_path[index]
        path_label = self.label_path[index]

        # 图像处理
        image = Image.open(path_image)
        image = image.resize(self.img_size, Image.BILINEAR)
        image = np.array(image)

        image = cv2.cvtColor(image, cv2.COLOR_RGB2YUV)
        image[:, :, 0] = cv2.equalizeHist(image[:, :, 0])   # 直方图均衡化
        image = cv2.cvtColor(image, cv2.COLOR_YUV2RGB)
        image = transforms.ToTensor()(image)

        # 标签处理
        label = Image.open(path_label)
        label = label.resize(self.img_size, Image.NEAREST)
        label = np.array(label)
        gt_image = np.all(label == self.BACKGROUND_COLOR, axis=2)
        gt_image = gt_image.reshape(*gt_image.shape, 1)  # 转为(h,w,1)
        gt_image = np.concatenate((gt_image, np.invert(gt_image)), axis=2)
        gt_image = gt_image.transpose([2, 0, 1])
        gt_image = torch.FloatTensor(gt_image)

        return image, gt_image
复制代码
复制代码
    def Pixel_Accuracy(self):
        Acc = np.diag(self.confusion_matrix).sum() / self.confusion_matrix.sum()
        return Acc

    def Pixel_Accuracy_Class(self):
        Acc = np.diag(self.confusion_matrix) / self.confusion_matrix.sum(axis=1)
        Acc = np.nanmean(Acc)
        return Acc

    def Mean_Intersection_over_Union(self):
        MIoU = np.diag(self.confusion_matrix) / (
                    np.sum(self.confusion_matrix, axis=1) + np.sum(self.confusion_matrix, axis=0) -
                    np.diag(self.confusion_matrix))
        MIoU = np.nanmean(MIoU)
        return MIoU

    def Frequency_Weighted_Intersection_over_Union(self):
        freq = np.sum(self.confusion_matrix, axis=1) / np.sum(self.confusion_matrix)
        iu = np.diag(self.confusion_matrix) / (
                    np.sum(self.confusion_matrix, axis=1) + np.sum(self.confusion_matrix, axis=0) -
                    np.diag(self.confusion_matrix))

        FWIoU = (freq[freq > 0] * iu[freq > 0]).sum()
        return FWIoU
复制代码

(3)FCN模型

 

四、车道线分割模型实战(损失函数)

1、作业讲解

定义类

 

 

 

2、损失函数

sigmoid函数是非互斥的

sfotmax函数是互斥的

 

 Focal Loss

  • ∝是为了解决样本不平衡问题,用于平衡正负样本的数量
  • γ是为了解决困难样本和简单样本问题,即让简单样本(例如p=0.9)的损失函数变小,困难样本(例如p=0.1)的损失函数变大,使其更加有针对性的学习困难样本
  • 对于均衡样本的训练,∝可以不乘

 

 

 

L1L2损失函数

  • L2梯度大,容易出现梯度爆炸现象,且易受离群点影响
  • L1梯度小,收敛慢,但是梯度一直不变,容易出现来回波动的现象
  • Smooth L1 Loss 结合两者优点,去除两者缺点

 

前向传播

回归问题希望各个分类的概率平均一些,使得损失函数较小

而分类问题没有这个要求,仅需保证分类正确的类别概率最大即可

因此分类问题使用交叉熵而不是MSE

 

3、FCN语义分割网络

池化层是为了压缩特征图,使得特征图变小,得到较大的感受野的范围;

 

 

感受野的计算

通过三次卷积,感受野变成了15*15;

而使用单独三个3*3卷积,感受野变成7*7;

因此使用膨胀卷积,感受野会变得更大;

4、空洞卷积

使用膨胀卷积可以在保证参数不变情况下,增大感受野

而正常的下采样,会导致小的目标难以检测到;(例如通过5次的pooling,2^5=32,所以原图中小于32像素的物体就没办法重现了);

为了保证能检测到小物体,且让感受野变大,就提出了空洞卷积;

用空洞卷积代替pooling,保证特征图不变情况下,用小量计算来增大感受野(输入多大,输出也多大,但是感受野会变大);

膨胀卷积里补的0不会参与反向传播,反向传播使用的还是原来的参数;

输出的特征图的尺寸也不会减小;

 

 

 

 

目标:保证输入输出尺寸不变,但是要增大感受野,并且保证计算量不会增加,因此提出了空洞卷积;

例题:输入尺寸19*19,3*3卷积核,膨胀系数是6,保证输出图19*19,步长是1,计算一下padding?

 结果是6

 膨胀后卷积核尺寸 = 6*(3-1)+1 = 13

  • 19 = (19 +2 *pad - 6*(3-1)-1 )/1 +1
  • 19+2pad- 13= 18
  • 2pad = 12
  • pad=6

5、其他经典网络结构

SegNet

池化来进行上采样,保存原始像素位置,扩大时填0,比起其他计算需要的参数大大减小

SPP Net

何凯明提出,金字塔型,可以使用任意尺寸输入图像,三个卷积结果拼接成输出

6、搭建CNN模型

 

 

出现漏检的情况,说明前景和背景不均匀,需要调整损失函数;

出现后面图像识别到,前面图像没识别到,可能是由于感受野不够大,需要扩大感受野(感受野太大,计算量也会变大);

 

7、作业

 (1)二分类Focal Loss

 

复制代码
import torch
from torch import nn

# 二分类focal loss
class BCEFocalLoss(nn.Module):
    def __init__(self, gamma=2, alpha=0.25, reduction="elementwise_mean"):
        super().__init__()
        self.gamma = gamma
        self.alpha = alpha
        self.reduction = reduction

    def forward(self, pt, target):
        alpha = self.alpha
        loss = - alpha * (1 - pt) ** self.gamma * target * torch.log(pt) - \
          (1 - alpha) * pt ** self.gamma * (1 - target) * torch.log(1 - pt)

        if self.reduction == "elementwise_mean":
            loss = torch.mean(loss)
        elif self.reduction == "sum":
            loss = torch.sum(loss)
        return loss
复制代码

(2)resent网络

复制代码
    def get_upsampling_weight(self, in_channels, out_channels, kernel_size):
        """双线性插值"""
        factor = (kernel_size + 1) // 2
        if kernel_size % 2 == 1:
            center = factor - 1
        else:
            center = factor - 0.5
        og = np.ogrid[:kernel_size, :kernel_size]  # 第一组为纵向产生的kernel_size维数组, 第二组为横向产生的kernel_size维数组
        filt = (1 - abs(og[0] - center) / factor) * (1 - abs(og[1] - center) / factor)  # kernel_zize x kernel_size
        weight = np.zeros((in_channels, out_channels, kernel_size, kernel_size), dtype=np.float64)
        weight[range(in_channels), range(out_channels), :, :] = filt
        return torch.from_numpy(weight).float()
复制代码

(3)SegNet实现道路检测

 

五、车道线分割模型实战 Ⅱ

1、激活函数

(1)作用&意义:增加非线性,增加网络复杂度

(2)

sigmoid缺点:

  1. 梯度消失
  2. 不是以0为对称中心,梯度下降较慢
  3. 幂函数计算耗时

tanh解决了不以0为对称中心的问题,但其他两个缺点没有解决;

ReLu是最常用的激活函数,但是存在小于0时,梯度为0的现象;

Leaky ReLU(P ReLU)对梯度为0的问题进行解决;

Maxout训练两套激活函数,哪个大使用哪个,但是缺点是计算量变大;

ELU避免了ReLu的梯度为0的现象,而是小于0时趋于饱和,但是会计算量变大;

 

CRelu

原样本取反后,连接到原样本输出

例如64取反,64+64=128,输出结果为128

优点:减少了计算量,精度也有一定提升

 

2、实例分割

实例分割两种类型

  • 自上而下:先识别出人,在识别人的姿态
  • 自下而上:先识别出关键点(不区分是哪个人),在识别出是哪个人和对应的姿态

对于对边缘要求比较高的任务,推荐使用自下而上;

3、聚类

在自下而上中,检测出一个个像素点,怎么识别出具体属于哪个个体呢?

通过度量学习来达成

输入图像和哪个特性最相近,就是属于哪一类

也可以使用聚类的方法,车道线检测就是使用的聚类

 

 通过内力、外力、交叉熵进行计算,得到最终参数

车道线检测项目中使用DBSCAN进行聚类

 

kmeans的质心可能不是数据点,而是空间上的点

 kmeans++优化在于,先选取一个质心,然后计算其他点到它的距离,选取距离最远的作为第二个质心

 

 

DBSCAN

 

4、车道线检测网络(LaneNet)

车道线检测网络:语义分割encoder->特征表达embedding(推力拉力的方式)->聚类clustering

Embedding‌是指将高维度的数据(如文字、图片、视频)映射到低维度空间的过程,用这些维度来表达提取出的特征向量,即特征提取?

ground truth (中文意思是“地面真实值”或“基准真实值”,简单理解就是真实值) 是指用于训练和评估模型的准确标签或数据。

DBSAN的缺点是很慢,可以用KMeans来代替它,但需要指定车道线类别,可以指定2、3、4,哪个效果好用哪个进行训练。

 

5、多任务学习

识别种族、头发、肤色等信息时i,可以共享卷积特征;

不同分支乘以权重,整合损失函数,进行反向传播;

根据不同分支的loss,来调整权重;

 

鸟瞰图作用:利用二次、三次曲线表达出车道线的方程

通过LaneNet得到语义分割的图,

通过HNet学得H矩阵

通过H变化把它转换为鸟瞰图

H矩阵求逆,可以把鸟瞰图转换为原图

转换后的原图,和原图进行求loss,反向传播即可计算H矩阵的参数

即H计算两次,一次正向,一次反向

6、数据处理流程

(1)读数据

 (2)图像标签

 (3)生成标签mask

 (4)数据检查

最好在main函数中进行数据检查,单独执行这个文件,保证数据处理正确。

 

7、搭建网络结构

(1)加载配置文件

 (2)模型训练

 

 

 (3)确定网络结构

 (4)优化函数

8、作业

 (1)Kmeans聚类

复制代码
coordinate, type_index = make_blobs(
    # 1000个样本
    n_samples=1000,
    # 每个样本2个特征,代表x和y
    n_features=2,
    # 4个中心
    centers=4,
    # 随机数种子
    random_state=2
)
fig0, axi0 = plt.subplots(1)
# 传入x、y坐标,macker='o'代表打印一个圈,s=8代表尺寸
axi0.scatter(coordinate[:, 0], coordinate[:, 1], marker='o', s=8)
# 打印所有的点
plt.show()
fig0, axi0 = plt.subplots(1)
# 传入x、y坐标,macker='o'代表打印一个圈,s=8代表尺寸
axi0.scatter(coordinate[:, 0], coordinate[:, 1], marker='o', s=8)
# 打印所有的点
plt.show()
color = np.array(['red', 'yellow','blue','black'])
fig1, axi1=plt.subplots(1)
# 下面显示每个点真实的类别
for i in range(4):
    axi1.scatter(
        coordinate[type_index == i, 0],
        coordinate[type_index == i, 1],
        marker='o',
        s=8,
        c=color[i]
    )
plt.show()
# 下面用kmeans去聚类得到的结果
y_pred = KMeans(n_clusters=4, random_state=9).fit_predict(coordinate)
plt.scatter(coordinate[:, 0], coordinate[:, 1], c=color[y_pred], s=8)
plt.show()
复制代码

(2)DBSCAN聚类

复制代码
coordinate, type_index = make_blobs(
    # 1000个样本
    n_samples=1000,
    # 每个样本2个特征,代表x和y
    n_features=2,
    # 4个中心
    centers=4,
    # 随机数种子
    random_state=3
)
fig0, axi0 = plt.subplots(1)
# 传入x、y坐标,macker='o'代表打印一个圈,s=8代表尺寸
axi0.scatter(coordinate[:, 0], coordinate[:, 1], marker='o', s=8)
# 打印所有的点
plt.show()
color = np.array(['red', 'yellow', 'blue', 'black'])
fig1, axi1 = plt.subplots(1)
# 下面显示每个点真实的类别
for i in range(4):
    axi1.scatter(
        coordinate[type_index == i, 0],
        coordinate[type_index == i, 1],
        marker='o',
        s=8,
        c=color[i]
    )
plt.show()
# eps:半径,min_samples:使用附近的多少个样本来构建中心点
y_pred = DBSCAN(eps=1.8, min_samples=3).fit_predict(coordinate)
plt.scatter(coordinate[:, 0], coordinate[:, 1], c=y_pred, s=8)
plt.show()
复制代码

(3)搭建LaneNet网络

 

六、车道线检测模型实战

1、IOU

L2Loss对于离群点计算出的loss比较大,因此容易受离群点影响;但收敛速度较快;

L1Loss没有这个缺点,但是收敛速度较慢;

smoothLoss整合了L1L2的优点;

IOU越大越好

 

 IOU比L1L12的优势在于,在计算出loss的基础上,额外考虑到了几何意义;

会联系框的固定点,会进行几何约束,而非孤立的点,训练出来的框会更稳定,鲁棒性更强;

 

 

 

 

 

 

 

 为了解决IOU缺点,提出了GIOU

 

 

当预测框被真实框完全包含情况下,GIOU和IOU的值和效果一样,针对这种情况,提出了DIOU;

DIOU考虑到了预测框中心点和真实框中心点之间的距离;

 

 CIOU进一步改进,加了一个影响因子av,考虑到了框的长宽比问题,使得预测框和真实框更贴合;

 

 

2、NMS(用于目标检测的后处理)

非极大值抑制,把重复的识别框,合并成一个框;

合并方法:

  • 找到每个框的置信度,舍弃掉置信度小于阈值的框;
  • 在按置信度进行排序,找到置信度最高的框作为结果;
  • 遍历其余的置信度,与当前最高置信度进行比较,当重叠面积大于一定阈值,则舍弃比较的框;
  • 重复上述操作,直至找到唯一的框;

置信度(Confidence)‌是指在机器学习和统计中,模型对其做出的预测是正确的确信程度。是网络模型输出的结果。

 Soft-NMS

对于重叠的情况,当IOU>阈值时,不直接去掉,而是下降其置信度,当下降后仍然大于阈值,则把框进行保留;

3、两种数据处理方式(归一化)

(1)对图像做标准化(模板)

在训练集上统计一个bgr的值(3通道的均值),对每一个图像都减去这个固定的值

 

(2)针对每一张图像进行统计,计算各自的bgr的值,减去它的均值和方差(特殊处理)

 生成语义分割和实例分割两个标签

 也可以裁剪图像,减小输入图尺寸,降低计算量(注意截图后,坐标也需要更改)

4、LaneNet网络训练流程

  1. 准备数据集
  2. 图像预处理(归一化)
  3. 检测处理后的数据
  4. 生成输入的标签数据
  5. 配置文件
  6. 设置超参数
  7. lr选择
  8. DataLoader加载数据
  9. 确定网络结构(加载预训练模型)
  10. 选择优化函数(SGD、Adam)
  11. 分为两个分支(语义分割和实例分割),得到像素的结果
  12. 计算损失函数(给车道线像素加一个权重,避免背景和车道线的差异)(语义分割使用交叉熵,实例分割使用判别式类间和类内的计算方式)
  13. 整合两个分支损失函数的结果
  14. 进行反向传播和优化
  15. 训练epoch时会保存模型,得到模型的测试指标

5、代码解读和优化

更改超参数

 1E2D代表是否共享网络结果,本项目中使用语义分割和实例分割两个分支进行训练,属于多任务学习

 

实例分割的权重额外乘了0.01,是为了让语义分割和实例分割保持在一个数量级(例如语义分割是80,实例分割是80000)、

即根据语义分割和实例分割的数量级来确定比例;

另外也可以根据任务的重要程度(语义分割-识别车道线,实例分割-识别是哪一跟车道线)来确定参数;

 之后进行反向传播和优化,训练epoch时会保存模型,得到模型的测试指标;

 评价指标

 

 本项目使用准确率和召回率和IOU作为评价指标;

 embedding

使用低维的向量来描述样本向量,然后与分类向量进行比较,和哪个距离最近就分类到哪个类别;

 也可以尝试只用语义分割进行识别;

例如第一根车道线一定在图像前四分之一的左边,类别是1;

剩下的情况哪怕只有两根车道线,但是由于位置不在图像前四分之一的左边,所以其的类别也是2;

总结就是通过位置标签和类别标签进行分类;

 端到端的方式

拟合出车道线的方程,然后描绘在图像上

6、模型压缩

为了轻量化和部署到终端,需要压缩网络模型;

使用MobileNet

 

 

 

 SqueezeNet

 2个3*3可以代替5*5

7、作业

 (1)以ReNet为骨架,由一个编码和一个解码组成的lanet网络

复制代码
#-------------------------------------以ReNet为骨架,由一个编码和一个解码组成的lanet网络----------------------------------------------------
class LaneNet_FCN_Res_1E2D(nn.Module):
    def __init__(self):
        super().__init__()
        self.encoder=resnet18(pretrained=True)
        self.decoder_embed = decoder_fcn.Decoder_LaneNet_TConv_Embed()
        self.decoder_logit = decoder_fcn.Decoder_LaneNet_TConv_Logit()

    def forward(self,input):
        x=self.encoder.forward(input)
        input_tensor_list = [self.encoder.c1, self.encoder.c2, self.encoder.c3, self.encoder.c4, x]
        embedding = self.decoder_embed.forward(input_tensor_list)
        logit = self.decoder_logit.forward(input_tensor_list)

        return embedding, logit
复制代码

 (2)

 

七、超快车道线检测模型

1、BN(Batch Normalization)

BN即对每个batch进行归一化;

BN主要解决内部协变量偏移(每有一层参数变化,其他层参数也要跟着调整),BN就是让每一层参数的均值和方差相近,层与层参数之间有了一个解耦的过程,从而优化网络;

防止后面层的参数和前面层的参数有强依赖性;

从而可以固定前面层参数,自由训练后面层的参数;

 

一般在卷积后使用BN;

Droupout用在全连接层;

BN既可以提升模型精度,也可以加快训练速度;

而Droupout仅能提升模型精度,而且现在有时也不会使用全连接层;因此使用BN更广泛;

BN一般放在激活函数之前,以rerul举例,若BN放在rerul之后,会破坏rerul在小于0时梯度为0的效果;

即卷积->BN->rerul(激活函数)

训练时候计算每个batch的均值和方差;

测试的时候计算全部样本的均值和方差;

滑动加权是在训练过程中,后面计算的均值/方差加权前面计算的均值/方差,可以令训练出的均值/方差更接近测试时候全部样本的均值/方差;

 

 

BN的反向传播

 

 

 

 

 

 

2、快车道线检测模型的背景和优势

快车道线检测模型和其他模型对比

传统车道线检测基于边缘特征检测车道线,但是只能检测样本原有标注好的车道,且无法识别车道被阻挡或磨损等复杂场景的情况;

PS:假设卷积层输出shape为[56, 56, 64]:

  • 56×56:特征图的空间维度
  • 64:通道数,即64个特征图
  • 每个特征图关注不同的特征模式(如边缘、纹理等)

语义分割对图像的每一个像素进行分类,即判断每个像素点是车道还是背景,但可能存在不是车道的地方被识别成车道的错误;

缺点是计算量大,训练速度慢,且判断的是车道而非车道线;

LaneNet网络,使用语义分割+实例分割进行检测,可以检测出车道线,且在车道线被阻挡或磨损等复杂场景也可以识别;

缺点是设备要求高,必须要GPU才能实现,很难进行部署和移植;

 

 

 

 快车道线检测不用判断每一个像素,而是判断一个个点,然后把点连起来作为车道线进行检测;

快车道线检测的背景

快车道线模型为了解决之前语义分割+实例分割的两个问题

  • 车道线完全被遮挡下,像素中感受野中上下文都i没有车道线学习,导致无法识别车道线
  • 在端上速度慢,如何实时进行检测

训练后,网络可以根据是否存在车辆来判断被遮挡住的车道线;

 

3、快车道线检测的原理

之前语义分割是对每个像素进行判断,快车道线检测是把图像分为一个个栅格,对栅格进行聚类分类,判断栅格里是否有车道线;

即是检测的模型,而非语义分割了;

例如把1280*256的图像横向分为100个格,纵向分为64个格;栅格是紧挨着的;处理图像缩小,感受野变大,训练时间也变短;

对每一个纵格选择一个行格;

 

 

 

 

4、快车道线检测的数据处理

摄像头传入的图像可能有偏移,需要进行裁剪;

 

数据增强方式

  • 亮度
  • 旋转(真实场景不像训练图像一样车道线斜率固定,加入旋转操作,防止过拟合)
  • 水平和垂直平移(丰富数据集,即避免不同车辆摄像头位置不同,导致识别效果不佳,因此加垂直方向的平移量)(添加x方向的扰动,避免数据集太规律导致的过拟合)
  • 车道线延申(起点和终点)

 

5、快车道线检测项目搭建

(1)DataSet类

把图像进行处理和数据增强

 

 

 

 

(2)网络结构

分为主干和辅助两个分支

  • 辅助分支只在训练时使用
  • 测试时主要走主干分支

主干分支:使用ResNet网络得到训练结果(卷积、降采样、全连接),映射到原始图像上(哪根线对应哪个格),得到训练后的离散的栅格点,通过聚类或车道线拟合的方式把一个个格拟合成车道线;

辅助分支:把车道线离散出来,识别出是第几个车道,目的是为了使训练效果更好;

Resblocks可以选择任意网络,例如ResNet,FCN的计算量有点大;

 

(3)损失函数

存在的损失函数+权重*空间的损失函数+权重*辅助分割的损失函数

 

 (4)评价指标

 

6、模型压缩

在本地跑通模型后,若要部署到终端有两种方式;

  • 云端,但是在信号不好时,识别效率低
  • 离线,但是可能存在没有GPU,计算量不足的情况,这时就需要压缩模型

 

 剪枝

 

 即把权重接近0的参数置为0

 

 

知识蒸馏

使用大的网络来指导小的网络进行学习;

 

 搜索空间是指定搜索的网络结构和数据范围

 

 

7、作业

 

 

八、 模型压缩优化

1、低秩分解

低秩分解:将一个大矩阵拆解成两个(或更多)小矩阵,使它们相乘后能近似或正好还原原大矩阵。

好处:

    • 数据压缩:用更少的数字存下大部分有用信息。
    • 加速计算:在机器学习、数据分析任务中,可以更快地进行矩阵运算
    • 降噪/简化:去掉冗余或干扰,保留最核心的成分。

例如5*5,7*7矩阵都可以进行低秩分解;

2、剪枝

Droupout主要是对全连接层进行剪枝;

而对于已经训练好的网络,一般使用结构化剪枝、非结构化剪枝等方式;

剪枝完之后要指定mask,以便重新训练retrain,保证结果精度;

 

微调网络:通过mask,指定一部分神经元不进行学习,以保证在微调之后,神经元依然有一定稀疏程度,相当于那些已经剪枝掉的神经元,然后进行下一轮剪枝;

3、知识蒸馏

在已经有一个大模型的基础上,可以用大模型的结果(残差块)指导小模型,以达到用小模型代替大模型的效果;

 

 

 

4、模型量化

量化主要是对模型的参数和激活值的存储方式,进行位数上的更改(例如float->int)

量化可以全部参数都量化,也可以指定某一层进行量化;

可以使用pytorch里的包进行量化操作;

 

 

5、Deep Compression

把低于阈值的节点进行剪枝;

把jindex转换成jindex之间的差值,减少了存储所需的位数;

 进行聚类,每一个分类用其各自的质心来代替计算

可能t由于聚类和计算质心,导致rain的时间增大了,但是i最终实际应用nference的时间变短;

 哈夫曼编码:经常出现的参数用短码进行编码,不经常出现的参数用长码进行编码;

通过这些方式最终可以达到压缩49倍的效果

并且由于全连接层所需的空间太大,因此现在很多使用全卷积网络进行替代;

 

6、轻量化网络设计

 

 

7、MoblieNet

MobileNet V1

 

 

MobileNet V2 

MobileNet V2 (先升维再降维)是ResNet(先降维再升维)反向的过程

  • PW:升维
  • DW:降维

机器学习中的残差是指预测值与实际值之间的差异‌。

 

 

 

8、组卷积

分组group进行卷积,然后把卷积结果进行拼接

统计性能数据,得到结论如下

  • 存储空间主要由全连接层决定;
  • 计算速度主要由卷积层决定;
  • 降低计算量一般通过剪枝、压缩等方式完成;

 

矩阵连接方式

  • add:把两个矩阵按位进行相加的结果;
  • concat:把两个矩阵的维度进行拼接的结果;

 

9、NNI

使用命令行来控制py文件

 需要把中间测试的结果返回给nni

 也可以返回最终的测试结果

 

最终可以根据测试结果生成web页面

10、作业

 

 

posted @   从前慢y  阅读(72)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示