深圳幻海软件技术有限公司 欢迎您!

【OpenAI】基于 Gym-CarRacing 的自动驾驶项目 | 前置知识介绍 | 项目环境准备

2023-02-28

 猛戳!跟哥们一起玩蛇啊 👉 《一起玩蛇》🐍 =💭写在前面: 本篇是关于多伦多大学自动驾驶专业项目Gym-CarRacing的博客。GYM-Box2DCarRacing是一种在OpenAIGym平台上开发和比较强化学习算法的模拟环境。它是流行的

 猛戳!跟哥们一起玩蛇啊 👉 《一起玩蛇》🐍 =

💭 写在前面: 本篇是关于多伦多大学自动驾驶专业项目 Gym-CarRacing 的博客。GYM-Box2D CarRacing 是一种在 OpenAI Gym 平台上开发和比较强化学习算法的模拟环境。它是流行的 Box2D 物理引擎的一个版本,经过修改以支持模拟汽车在赛道上行驶的物理过程。由于内容比较多所以分多次更新,本篇是关于前置知识介绍,以及项目环境准备的。具体如下:

  • 自动驾驶的背景知识介绍。
  • 然后会讲解本项目可能所需的知识点,需要用到图像处理算法和基础车道线检测算法,这里的讲解并不会太细,读者如果对不熟悉可以在单独搜索,C站上也有不少介绍这些算法的博客。
  • 项目所需的环境安装教程(这个部分是我在博客审阅阶段临时增添的),因为该项目需要 Conda 和 gym 环境和一堆必不可少的软件包(少一个都跑不了)。我看了网上有不少朋友在做该项目时,在环境安装上踩了不少坑。所以为了方便需要做该项目的同学、对该项目感兴趣的朋友,我这里准备了环境安装的教程。

🔗 多伦多大学自动驾驶专项课程:Motion Planning for Self-Driving Cars | Coursera

🔗 Gym Car Racing 文档:Car Racing - Gym Documentation

   本篇博客全站热榜排名:4


 Ⅰ. 背景知识介绍

0x00 引入: 什么是自动驾驶?

" Autonomous(自动)+  driving(驾驶) "

​​  自动驾驶,即汽车自主认知周边环境并安全行驶的技术。

Self-driving car, autonomous vehicle, driver-less car, or robotic car……

0x01 自动驾驶的基本组件

  • 汽车(Car):实际移动的车辆,代理人应该控制它
  • 传感器(Sensors):探测周围环境的设备
  • 代理人(Agent):一个在给定的周围环境中安全地驱动 var 的物体

​​

传感器(Sensors on self-driving cars):

  • 460 个激光雷达摄像机、RGB 摄像机

​​

自动驾驶的目标(Goal of Autonomous Driving):

  • 在给定的情况下的安全驾驶
  • 根据给定的情况 (state) 安全驾驶汽车

​​

映射函数:Sensor Input → Action

  • 模块化管线(modular Pipelines)
  • 端到端训练(End-to-End Learning)
  • 直接感知(Direct Perception)

0x02 模块化组件(Modular Pipeline)

​​

每个模块连接下一个模块的输入:低级感知、场景解析、路径训练、车辆控制。

​​

❓ 需要思考的问题:为遵循选定的路径,我们应将手柄转多少度?我们应该以什么速度前进?

​​

Ⅱ. 前置知识

0x00 车道标记与车道检测(Lane marking & Lane detection)

  • 使用梯度图或边缘过滤的图像,我们可以通过阈值处理来检测车道标记。
  • 考虑在附近存在相反梯度的点。

​​

0x01 边缘检测(Edge Detection)

  • 通过用边缘过滤器对图像进行卷积,得到两个方向的梯度图。
  • 这里也可以使用其他的边缘核进行边缘检测。  ​​ 

​​

边缘检测是图像处理的一种常见技术,用于检测图像中的边缘和边界。这对于自动驾驶系统来说是非常重要的,因为边缘检测可以帮助系统识别道路、车辆、行人等重要物体。

通常边缘检测是通过使用边缘过滤器对图像进行卷积来实现的,边缘过滤器是一种特殊的卷积核,其中包含了两个方向的梯度图,可以检测出图像中的垂直和水平边缘。比如图中显示的 Sobel 过滤器,就可以得到一张图像的  方向和  方向的梯度图,其中  方向的梯度图可以检测出图像中的垂直边缘, 方向的梯度图可以检测出图像中的水平边缘。

除了 Sobel 过滤器之外,还有许多其他的边缘核可以用于边缘检测。比如 Canny 边缘检测算法,这是一种非常流行的边缘检测算法,它可以有效地消除噪声并提供清晰的边缘检测结果。

Canny 边缘检测是由 John F. Canny 在 1986 年提出的一种边缘检测算法。

它是一种多步骤的边缘检测算法,包括以下几个步骤:

  • 图像高斯滤波:使用高斯滤波器对图像进行模糊处理,以减少噪声并使边缘更加明显。
  • 计算图像梯度:使用 Sobel 过滤器或其他方法计算图像的梯度,并使用梯度的方向和大小来表示图像中的边缘。
  • 非极大值抑制:使用非极大值抑制算法来去除图像中的假边缘。
  • 双阈值检测:使用两个阈值来区分真正的边缘和假边缘。
  • 边缘连接:将检测到的边缘连接起来,以形成完整的边缘。

Canny 边缘检测算法的优点是它能够有效地消除噪声,并提供清晰的边缘检测结果。但是,由于它是一种多步骤的算法,所以它的计算复杂度较高,不太适用于实时边缘检测场景。

0x02 IPM 逆透视变换(Inverse Perspective Mapping)

IPM(Inverse Perspective Mapping)是一种图像处理技术,它可以将一张透视变换后的图像进行逆变换,使其看起来像是从俯视角度拍摄的。这对于自动驾驶系统来说非常重要,因为它可以帮助系统更准确地识别道路、车辆、行人等物体。

  • 在通常情况下,道路是在平面上的。
  • 如果三维变换是已知的,我们就可以将道路图像投射到地面平面上。

​​

0x03 车道线检测:参数化车道标线估算(Parametric Lane Marking Estimation)

为了给汽车导航,我们需要将检测到的标记像素与一个更有语义的曲线模型与之相匹。

​​

0x04 贝塞尔曲线(Bezier Curve)

贝塞尔曲线(Bézier curve)是一种数学曲线,贝塞尔曲线常用于计算机图形学中,因为它们可以用于创建平滑的曲线和图形。贝塞尔曲线是通过控制点来描述曲线形状的,其中一个或多个控制点用于指定曲线的形状。

贝塞尔曲线可以通过控制点的位置来控制曲线的形状,并可以通过改变控制点的位置来改变曲线的形状。这使得贝塞尔曲线非常适用于创建复杂的曲线和图形。

  • 一个由控制点定义的多项式曲线

​​

0x05 线性贝塞尔曲线(Linear Bezier Curve)

线性贝塞尔曲线是一种特殊的贝塞尔曲线,它由两个控制点和一个起始点和一个终止点组成。线性贝塞尔曲线是最简单的贝塞尔曲线之一,它可以用来描述直线。线性贝塞尔曲线的方程:

其中,  是贝塞尔曲线上的点, 是参数, 和  是控制点。

  • 类似线性插值法(linear interpolation)

​​

0x06 二次贝塞尔曲线(Quadratic Bezier Curve)

由一个起始点、一个终止点和两个控制点组成。二次贝塞尔曲线是一种二次方程,可用来描述曲线和复杂的形状。二次贝塞尔曲线的方程:

其中, 是贝塞尔曲线上的点, 是参数, 是控制点。

  • 两个线性内插点的内插

​​

0x07 三次贝塞尔曲线(Cubic Bezier Curve)

由一个起始点、一个终止点和三个控制点组成。三次贝塞尔曲线是一种三次方程,它可以用来描述曲线和复杂的形状。三次贝塞尔曲线的方程:

其中, 是贝塞尔曲线上的点, 是参数, 是控制点。

  • 二次点的插值

​​

 0x08 B样条曲线(B-Spline Curve)

B样条曲线是通过一系列的控制点来描述曲线形状的,这些控制点可以用来指定曲线的形状。B样条曲线通常使用 B样条曲线方程来描述,这是一个多项式方程。B样条曲线有许多不同的类型,包括二次B样条曲线、三次B样条曲线和四次B样条曲线。

已知  个控制点  ,可定义  次 B 样条曲线的表达式为:

由控制点列表和程度定义的曲线,一条单片多项式曲线(它不同于与贝塞尔曲线)。

Ⅲ. 项目所需环境准备

0x00 前言

我默认读者已经安装了 Python,这里简单讲一下如何安装 Conda 和 gym。网上的资料很多,与之相比,我写的安装教程可能远没有那些专门安装环境的文章细致。我这里只是做简单介绍,旨在方便大家能快速准备好环境,能让程序正常跑起来的。因为我看了网上有不少人在做该项目时,在环境安装上踩了不少坑,很是让人头疼。所以这里我准备了 Conda 和 gym 环境安装的教程。

0x01 Step1:Python 开发环境设置 —— 安装 Conda

操作系统以 Window 为准进行说明(Linux 同理)

安装 Anaconda 或 Miniconda:

  • Python 开发环境平台
  • 支持各种环境设置与环境的更改
  • https://docs.conda.io/en/latest/miniconda.html
  • 有关安装方法和详细信息,请参阅百度

​​

Step1:打开安装包后会进入欢迎界面,点击 Next> 

Step2:许可协议界面,选择同意:

​​

Step3:看情况选择,我们选择 Just Me:

​​

Step4:选择安装路径,默认是在 C 盘下的,点击 Browse 按钮可呼出窗口更换路径:

​​

Step5:这里全部勾选

​​

Step6:等待即可,可能有点慢,但是绝对没有 Vivado 慢!安装完毕后点击 Next >

​​

Final Step:点击 Finish 

​​

​​

0x02 Step2:Conda 安装完毕后的环境设置

通过命令行操作,设置环境并激活即可:

🔍 官方说明:Managing environments — conda

详细查阅 Creating and activating an environment 部分。

安装命令示例:

  1. > conda env create –n autodriving
  2. > conda activate autodirving
  3. > conda install python
  4. > pip install pytorch

Step1:打开命令提示符,快捷键 Win+R 输入 cmd 后回车:

​​

Step2:检查是否安装正常,在 cmd 中输入 conda

conda

​​

没问题:

​​

Step3:打开命令行下输入:

conda create -n python=2.7

 ​​

Step4:进入环境内部

conda activate [文件名]

​​

0x03 关于 OpenAI GYM 的介绍

强化学习框架(https://github.com/openai/gym)

  • 开源提供多种游戏环境。
  • 本项目将使用 box2d-carracing
  • 参考资料:https://www.gymlibrary.dev/

​​

0x04 安装练习所需的软件包

  • 下载解压文件后自行解压。
  • 显示命令窗口后设置 Conda 环境。
  • 找到到解压缩文件夹后执行以下命令:
  • cd 到 /envs/box2d,python car_racing 执行 py 命令,确保 Acttion 没问题(方向键可以移动汽车就行)。

CarRacing 环境基本代码示例:

  1. gym.make() 环境配置。
  2. env.reset() 设置初始变量。
  3. env.step(action) 执行动作并返回以下观察值、补偿、是否结束。

​​

0x05 设置 GYM 环境

Step1:打开命令窗口设置准备 Conda

创建项目文件夹: conda create -n gym python=3.8

这里我去名为 3.8

​​

稍等片刻,会问你 yes 还是 no,我们输入 y 即可:​​

之后会开始提取安装包,耐心等待……

​​

之后输入 conda activate gym 进行激活:

​​

Step2:cd 至解压的文件夹位置

cd {{installed gym path}}

我们是把 gym 解压到桌面的,我们 cd 过去即可。

Step3:输入下列指令

  1. pip install -e .[box2d]
  2. pip install matplotlib
  3. pip install scipy
  4. pip install pyglet
  5. pip install pygame

 挨个安装即可:

​​

如果显示安装失败,可能是因为 python无法识别安装的版本,导致 pip install Box2D 显示无法安装,可以尝试输入以下指令安装:

python -m pip install Box2D

全部安装完毕后,跳转到 gym/envs/box2d,运行 python car_racing.py 命令测试环境:

cd gym/envs/box2d

(注意,gym 文件夹还有一个 gym 文件夹)

然后输入 python car_racing.py 运行,如果正常运行,就说明环境装好了。

🚩 运行效果如下:

​​

至此,环境已全部准备完毕!

有些人开了就不会关了,很尴尬……  建议直接强制关机 23333(滑稽)

拉闸!简单粗暴,一步到位!!!优雅,永不过时。

​​

Ⅳ. 项目准备

0x00 实验说明:Box2D CarRacing 的 lane_dection

🚩 实践目标:实现一个模块化组件框架,落实简化版的模块化流水线。了解基本概念,并积累开发一个简单的自驱应用程序的经验。

🔨 环境选用:OpenAI GYM

  • https://www.gymlibrary.ml/
  • 我们将基于 Box2D CarRacing 实现,Box2D CarRacing 基本信息如下:
    • Action:转向、加速、刹车
    • Sensor input:​​  屏幕(显示汽车的状态和路径信息)

​​

📜 尝试:

  • 为汽车上方的部分找到一个好的裁剪,一个好的方法来分配车道边界的边缘,一个好的梯度阈值和样条平滑度的参数选择。
  • 尝试找到失败的案例。

​​

* 提供基础框架,只需要在 TODO 位置填写代码即可!

我就不提供资源下载链接了,直接手动吧:首先在桌面上创建一个文件夹,我们取名  skeleton ,然后创建出如下名称的 py 文件,将代码 CV 进去。

  • lane detection.py    
  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from scipy.signal import find_peaks
  4. from scipy.interpolate import splprep, splev
  5. from scipy.optimize import minimize
  6. import time
  7. class LaneDetection:
  8. '''
  9. Lane detection module using edge detection and b-spline fitting
  10. args:
  11. cut_size (cut_size=65) cut the image at the front of the car
  12. spline_smoothness (default=10)
  13. gradient_threshold (default=14)
  14. distance_maxima_gradient (default=3)
  15. '''
  16. def __init__(self, cut_size=65, spline_smoothness=10, gradient_threshold=14, distance_maxima_gradient=3):
  17. self.car_position = np.array([48,0])
  18. self.spline_smoothness = spline_smoothness
  19. self.cut_size = cut_size
  20. self.gradient_threshold = gradient_threshold
  21. self.distance_maxima_gradient = distance_maxima_gradient
  22. self.lane_boundary1_old = 0
  23. self.lane_boundary2_old = 0
  24. def cut_gray(self, state_image_full):
  25. '''
  26. ##### TODO #####
  27. This function should cut the image at the front end of the car (e.g. pixel row 65)
  28. and translate to gray scale
  29. input:
  30. state_image_full 96x96x3
  31. output:
  32. gray_state_image 65x96x1
  33. '''
  34. return gray_state_image[::-1]
  35. def edge_detection(self, gray_image):
  36. '''
  37. ##### TODO #####
  38. In order to find edges in the gray state image,
  39. this function should derive the absolute gradients of the gray state image.
  40. Derive the absolute gradients using numpy for each pixel.
  41. To ignore small gradients, set all gradients below a threshold (self.gradient_threshold) to zero.
  42. input:
  43. gray_state_image 65x96x1
  44. output:
  45. gradient_sum 65x96x1
  46. '''
  47. return gradient_sum
  48. def find_maxima_gradient_rowwise(self, gradient_sum):
  49. '''
  50. ##### TODO #####
  51. This function should output arguments of local maxima for each row of the gradient image.
  52. You can use scipy.signal.find_peaks to detect maxima.
  53. Hint: Use distance argument for a better robustness.
  54. input:
  55. gradient_sum 65x96x1
  56. output:
  57. maxima (np.array) shape : (Number_maxima, 2)
  58. '''
  59. return argmaxima
  60. def find_first_lane_point(self, gradient_sum):
  61. '''
  62. Find the first lane_boundaries points above the car.
  63. Special cases like just detecting one lane_boundary or more than two are considered.
  64. Even though there is space for improvement ;)
  65. input:
  66. gradient_sum 65x96x1
  67. output:
  68. lane_boundary1_startpoint
  69. lane_boundary2_startpoint
  70. lanes_found true if lane_boundaries were found
  71. '''
  72. # Variable if lanes were found or not
  73. lanes_found = False
  74. row = 0
  75. # loop through the rows
  76. while not lanes_found:
  77. # Find peaks with min distance of at least 3 pixel
  78. argmaxima = find_peaks(gradient_sum[row],distance=3)[0]
  79. # if one lane_boundary is found
  80. if argmaxima.shape[0] == 1:
  81. lane_boundary1_startpoint = np.array([[argmaxima[0], row]])
  82. if argmaxima[0] < 48:
  83. lane_boundary2_startpoint = np.array([[0, row]])
  84. else:
  85. lane_boundary2_startpoint = np.array([[96, row]])
  86. lanes_found = True
  87. # if 2 lane_boundaries are found
  88. elif argmaxima.shape[0] == 2:
  89. lane_boundary1_startpoint = np.array([[argmaxima[0], row]])
  90. lane_boundary2_startpoint = np.array([[argmaxima[1], row]])
  91. lanes_found = True
  92. # if more than 2 lane_boundaries are found
  93. elif argmaxima.shape[0] > 2:
  94. # if more than two maxima then take the two lanes next to the car, regarding least square
  95. A = np.argsort((argmaxima - self.car_position[0])**2)
  96. lane_boundary1_startpoint = np.array([[argmaxima[A[0]], 0]])
  97. lane_boundary2_startpoint = np.array([[argmaxima[A[1]], 0]])
  98. lanes_found = True
  99. row += 1
  100. # if no lane_boundaries are found
  101. if row == self.cut_size:
  102. lane_boundary1_startpoint = np.array([[0, 0]])
  103. lane_boundary2_startpoint = np.array([[0, 0]])
  104. break
  105. return lane_boundary1_startpoint, lane_boundary2_startpoint, lanes_found
  106. def lane_detection(self, state_image_full):
  107. '''
  108. ##### TODO #####
  109. This function should perform the road detection
  110. args:
  111. state_image_full [96, 96, 3]
  112. out:
  113. lane_boundary1 spline
  114. lane_boundary2 spline
  115. '''
  116. # to gray
  117. gray_state = self.cut_gray(state_image_full)
  118. # edge detection via gradient sum and thresholding
  119. gradient_sum = self.edge_detection(gray_state)
  120. maxima = self.find_maxima_gradient_rowwise(gradient_sum)
  121. # first lane_boundary points
  122. lane_boundary1_points, lane_boundary2_points, lane_found = self.find_first_lane_point(gradient_sum)
  123. # if no lane was found,use lane_boundaries of the preceding step
  124. if lane_found:
  125. ##### TODO #####
  126. # in every iteration:
  127. # 1- find maximum/edge with the lowest distance to the last lane boundary point
  128. # 2- append maximum to lane_boundary1_points or lane_boundary2_points
  129. # 3- delete maximum from maxima
  130. # 4- stop loop if there is no maximum left
  131. # or if the distance to the next one is too big (>=100)
  132. # lane_boundary 1
  133. # lane_boundary 2
  134. ################
  135. ##### TODO #####
  136. # spline fitting using scipy.interpolate.splprep
  137. # and the arguments self.spline_smoothness
  138. #
  139. # if there are more lane_boundary points points than spline parameters
  140. # else use perceding spline
  141. if lane_boundary1_points.shape[0] > 4 and lane_boundary2_points.shape[0] > 4:
  142. # Pay attention: the first lane_boundary point might occur twice
  143. # lane_boundary 1
  144. # lane_boundary 2
  145. else:
  146. lane_boundary1 = self.lane_boundary1_old
  147. lane_boundary2 = self.lane_boundary2_old
  148. ################
  149. else:
  150. lane_boundary1 = self.lane_boundary1_old
  151. lane_boundary2 = self.lane_boundary2_old
  152. self.lane_boundary1_old = lane_boundary1
  153. self.lane_boundary2_old = lane_boundary2
  154. # output the spline
  155. return lane_boundary1, lane_boundary2
  156. def plot_state_lane(self, state_image_full, steps, fig, waypoints=[]):
  157. '''
  158. Plot lanes and way points
  159. '''
  160. # evaluate spline for 6 different spline parameters.
  161. t = np.linspace(0, 1, 6)
  162. lane_boundary1_points_points = np.array(splev(t, self.lane_boundary1_old))
  163. lane_boundary2_points_points = np.array(splev(t, self.lane_boundary2_old))
  164. plt.gcf().clear()
  165. plt.imshow(state_image_full[::-1])
  166. plt.plot(lane_boundary1_points_points[0], lane_boundary1_points_points[1]+96-self.cut_size, linewidth=5, color='orange')
  167. plt.plot(lane_boundary2_points_points[0], lane_boundary2_points_points[1]+96-self.cut_size, linewidth=5, color='orange')
  168. if len(waypoints):
  169. plt.scatter(waypoints[0], waypoints[1]+96-self.cut_size, color='white')
  170. plt.axis('off')
  171. plt.xlim((-0.5,95.5))
  172. plt.ylim((-0.5,95.5))
  173. plt.gca().axes.get_xaxis().set_visible(False)
  174. plt.gca().axes.get_yaxis().set_visible(False)
  175. fig.canvas.flush_events()
  • detection.py (用于测试,无需修改)
  1. import gym
  2. from gym.envs.box2d.car_racing import CarRacing
  3. import pygame
  4. from lane_detection import LaneDetection
  5. import matplotlib.pyplot as plt
  6. import numpy as np
  7. import pyglet
  8. from pyglet import gl
  9. from pyglet.window import key
  10. # action variables
  11. action = np.array([0.0, 0.0, 0.0])
  12. def register_input():
  13. for event in pygame.event.get():
  14. if event.type == pygame.KEYDOWN:
  15. if event.key == pygame.K_LEFT:
  16. action[0] = -1.0
  17. if event.key == pygame.K_RIGHT:
  18. action[0] = +1.0
  19. if event.key == pygame.K_UP:
  20. action[1] = +0.5
  21. if event.key == pygame.K_DOWN:
  22. action[2] = +0.8 # set 1.0 for wheels to block to zero rotation
  23. if event.key == pygame.K_r:
  24. global retry
  25. retry = True
  26. if event.key == pygame.K_s:
  27. global record
  28. record = True
  29. if event.key == pygame.K_q:
  30. global quit
  31. quit = True
  32. if event.type == pygame.KEYUP:
  33. if event.key == pygame.K_LEFT and action[0] < 0.0:
  34. action[0] = 0
  35. if event.key == pygame.K_RIGHT and action[0] > 0.0:
  36. action[0] = 0
  37. if event.key == pygame.K_UP:
  38. action[1] = 0
  39. if event.key == pygame.K_DOWN:
  40. action[2] = 0
  41. # init environement
  42. env = CarRacing()
  43. env.render()
  44. env.reset()
  45. # define variables
  46. total_reward = 0.0
  47. steps = 0
  48. retry = False
  49. quit = False
  50. # init modules of the pipeline
  51. LD_module = LaneDetection()
  52. # init extra plot
  53. fig = plt.figure()
  54. plt.ion()
  55. plt.show()
  56. while not quit:
  57. env.reset()
  58. retry = False
  59. while True:
  60. # perform step
  61. register_input()
  62. s, r, done, speed= env.step(action)
  63. # lane detection
  64. splines = LD_module.lane_detection(s)
  65. # reward
  66. total_reward += r
  67. # outputs during training
  68. if steps % 2 == 0 or done:
  69. print("\naction " + str(["{:+0.2f}".format(x) for x in action]))
  70. print("step {} total_reward {:+0.2f}".format(steps, total_reward))
  71. LD_module.plot_state_lane(s, steps, fig)
  72. steps += 1
  73. env.render()
  74. if done or retry or quit: break
  75. env.close()

我们会在下一章节讲解原理和有关代码的实现。

 ​​

  1. 📌 [ 笔者 ]   王亦优
  2. 📃 [ 更新 ]   2022.12.29
  3. ❌ [ 勘误 ]   /* 暂无 */
  4. 📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
  5. 本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

[6] Montemerlo M, Becker J, Bhat S, et alJunior: The Stanford entry in the Urban Challenge

Slide Credit: Steven Waslander

LaValle: Rapidly-exploring random trees: A new tool for path planning. Techical Report, 1998

Dolgov et al.: Practical Search Techniques in Path Planning for Autonomous Driving. STAIR, 2008.

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

. [EB/OL]. []. https://blog.waymo.com/2021/10/the-waymo-driver-handbook-perception.html.

文章知识点与官方知识档案匹配,可进一步学习相关知识
OpenCV技能树首页概览13291 人正在系统学习中
Hello,World!
微信名片