计算机视觉自动驾驶
计算机视觉自动驾驶与深度学习自动驾驶类似,都是通过解读摄像头图像来确定转向和油门值。然而,计算机视觉自动驾驶不使用深度学习模型,而是利用传统的计算机视觉算法,如Canny边缘检测,来解读赛道的图像。计算机视觉自动驾驶专门设计为方便编写自己的算法,并将其用于替代内置算法的位置。
内置算法是一种线路跟踪算法;它期望赛道有一条中心线,最好是实线,以便进行检测。可以通过配置来调整线路的预期颜色;默认情况下,它期望是黄色线。该算法计算线路距离图像中心的距离,然后使用PID控制器根据该值计算转向值。如果车辆在线路左侧,则向右转。如果车辆在线路右侧,则向左转。所选的转向角度与距离成正比。所选的油门与转向角度成反比,以便车辆在直线上加速并在转弯时减速。下面将详细讨论算法和配置参数。
但是,如果您的赛道没有中心线呢?如果只有左右车道边界线呢?如果是人行道呢?如果您只是想实现自己的算法呢?计算机视觉模板旨在使这一切变得非常容易。您可以编写自己的Python部分作为自动驾驶,然后在myconfig.py中更改配置以指向它。您的部分可以利用cv.py中的计算机视觉部分,或者直接调用OpenCV的Python API。下面是一个简化的示例。
重要提示:计算机视觉模板需要安装OpenCV。Jetson Nano上预装了OpenCV,但在树莓派上必须明确安装。请参阅树莓派安装第9步和第11步。
创建计算机视觉应用程序
您可以像创建深度学习应用程序一样创建计算机视觉应用程序;我们只需告诉它使用cv_control模板而不是默认模板。首先,确保您已激活donkeycar的Python环境,然后使用createcar命令创建您的应用程序文件夹。
donkey createcar --template=cv_control --path=~/mycar
在更新到donkeycar的新版本时,您需要刷新应用程序文件夹。您可以使用相同的命令,但添加--overwrite
选项,以免删除您的myconfig.py文件。
donkey createcar --template=cv_control --path=~/mycar --overwrite
路线跟随器
内置算法可以使用摄像头跟踪线路。默认情况下,它被调整为追踪黄色线路,但可以配置要追踪的颜色。算法的许多其他方面也可以进行调整。以下是算法的描述以及如何使用配置值。随后列出并描述了这些值。
- 如果
TARGET_PIXEL
为None,则使用步骤1到5来估计线路的目标(预期)位置。 - 复制图像在高度为
SCAN_Y
和SCAN_HEIGHT
像素的行。结果是一个宽度与图像相同且高度为SCAN_HEIGHT
的像素块。 - 将像素从RGB(红绿蓝)颜色空间转换为HSV(色调饱和度值)颜色空间。
- 然后,算法识别块中所有HSV颜色在
COLOR_THRESHOLD_LOW
和COLOR_THRESHOLD_HIGH
之间的像素。 - 一旦隔离出目标颜色的像素,就会创建一个直方图,用于每个宽度为1像素、高度为
SCAN_HEIGHT
的切片从左到右计算黄色像素的计数。 - 选择具有最多黄色像素的切片的x值(水平偏移)。这是算法认为黄色线路的位置。
- 使用此x值与
TARGET_PIXEL
值之间的差作为PID算法计算新转向的值。如果该值比TARGET_PIXEL
向左偏移超过TARGET_THRESHOLD
像素,则车辆向右转;如果该值比TARGET_PIXEL
向右偏移超过TARGET_THRESHOLD
像素,则车辆向左转。如果该值与TARGET_PIXEL
在TARGET_THRESHOLD
像素范围内,则不改变转向。 - 转向值用于决定车辆是否加速或减速。如果转向不改变,则油门增加
THROTTLE_STEP
,但不超过THROTTLE_MAX
。如果转向改变,则油门减小THROTTLE_STEP
,但不低于THROTTLE_MIN
。
以下是算法中使用的配置值:
TARGET_PIXEL
:目标像素位置,用于计算转向。如果为None,则通过步骤1到5估计目标位置。SCAN_Y
:要扫描的图像行的起始位置。SCAN_HEIGHT
:要扫描的图像行的高度。COLOR_THRESHOLD_LOW
:HSV颜色范围的下限,用于识别目标颜色。COLOR_THRESHOLD_HIGH
:HSV颜色范围的上限,用于识别目标颜色。TARGET_THRESHOLD
:目标像素位置与实际检测到的位置之间的允许偏差。THROTTLE_STEP
:转向未改变时油门的增加量。THROTTLE_MAX
:油门的最大值。THROTTLE_MIN
:油门的最小值。
您可以根据自己的需求调整这些配置值,以便适应不同的线路和环境。
这篇pyimagesearch文章和附带的视频介绍了OpenCV中可用的各种颜色空间及其特点。
完整的源代码在本页面末尾的LineFollower类部分提供并进行了讨论。
摄像头设置
页面顶部的图像显示了使用标准Donkeycar架构时的摄像头设置大致情况。它被倾斜以看到地平线,这样可以远距离观察到转弯。这对于高速行驶非常有用,因为可以提前看到很远的地方。然而,如果检测到的线路非常细,则可能会出现伪阳性(导致车辆偏离线路)的伪影(噪声)。如果您不需要高速行驶,并且希望尽可能准确,那么将摄像头对准线路是一个好主意。因此,如果您的摄像头可以调整,您可以在准确性(将其指向下方)和速度(将其指向地平线)之间进行权衡。
选择LineFollower的参数
计算机视觉模板与深度学习和路径跟随模板略有不同;它没有数据记录功能。在设置配置参数后,您只需将汽车放在具有您想要跟随的线路的赛道上,然后从用户模式切换到全自动模式或自动转向模式。完整的配置参数集可以在下面的LineFollower配置部分找到;我们将在本节中更详细地讨论最重要的配置参数。
SCAN_Y和SCAN_HEIGHT
将用于扫描线路的矩形区域称为检测区域,其大小由SCAN_Y
和SCAN_HEIGHT
确定。
在自动驾驶模式下,LineFollower
会将检测区域显示为一个水平的黑色条。落在颜色阈值范围内的像素(请参见下一节)将被绘制为白色像素。理想情况下,只有检测条中线路上的像素会显示为白色;任何不属于您要跟随的线路的白色像素都被视为伪阳性。如果伪阳性相对分散,它们不应干扰线路的检测。然而,如果有大面积的白色伪阳性,它们可能会欺骗算法。请参阅下一节,了解如何调整颜色阈值范围以最小化伪阳性。
下图显示了检测区域和检测到的线路。
COLOR_THRESHOLD_LOW, COLOR_THRESHOLD_HIGH
颜色阈值代表用于检测线条的颜色范围;它们应该被选择为包括线条在检测栏通过的区域中的颜色,并且最好不包括其他任何颜色。颜色阈值使用HSV颜色空间(色调、饱和度、亮度)格式,而不是RGB格式。RGB颜色空间是计算机显示颜色的方式,而HSV颜色空间更接近人类对颜色的感知。对于我们的目的,"色调"部分是指纯粹的颜色,不考虑阴影或光照。这使得更容易找到一种颜色,因为它只是一个数字,而不是三个数字的组合。
有许多在线的RGB和HSV之间的转换工具。在创建本文档时使用了一个名为peko-step的工具。我喜欢这个工具,因为它允许将饱和度和亮度输出为0到255的范围,这正是我们需要的。重要提示:在线工具使用标准的HSV表示方式,即色调值为0到359度,饱和度为0到100%,亮度为0到100%。而我们的代码基于OpenCV,它使用的色调值范围是0到179,饱和度和亮度的范围是0到255。因此,在更改这些配置时,请注意可能需要将工具的值转换为OpenCV的值。
在选择阈值颜色时,重要的是要考虑到摄像头所能看到的内容,包括光照条件。Donkeycar包含一个脚本,可以方便地进行这个操作。hsv_picker.sh
脚本允许您查看实时摄像头图像,或选择一个静态图像进行查看。因此,如果您在车上运行的是桌面图像(而不是服务器图像或无头图像),那么您可以运行该脚本并查看摄像头图像。如果您的车上没有桌面环境,那么您可以运行车辆,并在主机笔记本电脑上的浏览器中打开Web视图,然后截屏保存图像,然后将该静态图像与您的笔记本电脑上的hsv_picker.sh
脚本一起使用。无论哪种情况,都要将车辆放置在赛道上,以便它能够像在自动驾驶时那样看到线条,以获取真实的视图。
您可以运行hsv_picker.sh
脚本来查看截图图像;在激活了Donkey Python环境的情况下,从您的Donkeycar存储库文件夹的根目录运行该脚本。
python scripts/hsv_picker.sh --file=<path-to-image>
要查看摄像头的视频流,请再次激活Donkey Python环境,并从您的Donkeycar存储库文件夹的根目录运行该脚本:
python scripts/hsv_picker.sh
如果您有多个摄像头并且没有显示正确的摄像头,您可以选择摄像头索引并/或设置图像大小:
python scripts/hsv_picker.sh --camera=2 --width=320 --height=240
上面的图像显示了加载了Web界面截图的hsv_script.sh
。图像中央的蓝线是我们要跟随的线路。摄像头图像中的水平黑色条形是检测条;它由SCAN_Y
和SCAN_HEIGHT
定义,并且是应用掩码的区域,以尝试隔离线路中的像素。当检测到像素时,它们将在检测区域中以白色绘制。
屏幕底部有6个滑动条,用于选择低HSV值的3个部分和高HSV值的3个部分,这些值用于创建一个掩膜,以提取线条中的像素。您可以手动移动这些滑动条,尝试找到最佳的检测范围。当您改变滑动条时,结果掩膜将应用于图像,并且您将看到像素开始恢复。色调值通常是最重要的值。您可以通过选择键盘上的Escape键随时重置滑动条并清除掩膜。
使用滚动条确实可以工作,但还有一种更简单的方法。您还可以通过点击-拖动-释放来选择一个矩形区域;该区域内的像素将被搜索以找到低值和高值,并将这些低值和高值更新到滚动条上。因此,找到线条的掩码最简单的方法是在线条本身上选择一个矩形区域。您可以使用滚动条对所选的掩码进行微调。
下面的图像显示了通过在蓝线内选择一个矩形区域创建的掩码。
hsv_picker.sh
脚本的特点:
- 使用屏幕底部的滑动条更改低值和高值的掩码。
- 通过在图像上使用点击-拖动-释放来选择一个矩形区域来设置低值和高值的掩码。
- 选择键盘上的Escape键以清除掩码。
- 选择键盘上的'p'键将当前掩码值打印到控制台。
- 选择键盘上的'q'键将打印最终掩码值并退出。
TARGET_PIXEL
TARGET_PIXEL
值是图像中要跟随的线的预期水平位置。线路跟随算法将调整转向以尽量保持线在图像中的该位置。更具体地说,线路跟随算法检测到的实际线在图像中的位置与TARGET_PIXEL
值之间的差异被PID控制器用于调整转向(参见下面的"PID控制器")。
如果您是赛道上唯一的汽车,那么您可能希望汽车直接沿着线行驶。在这种情况下,将TARGET_PIXEL
设置为图像的水平中心位置(IMAGE_W / 2)
意味着自动驾驶系统假设要跟随的线应该直接位于图像中间,因此汽车将尽力保持在中间位置。因此,如果您的汽车实际上起始位置在该线的左侧或右侧,它将迅速移动到该线上并保持在上面。
然而,如果您在一个同时有两辆车行驶的赛道上(有一条线将两个车道分开),那么您可能希望您的车辆保持在自己的车道上。在这种情况下,您将TARGET_PIXEL
设置为None,这将导致车辆在启动时检测线路的位置。然后,自动驾驶将假设线路应该保持在图像中的该位置,因此它将尝试保持车辆在自己的车道上以实现该目标。
如果您非常有动力,那么您可以尝试实现一个车道变换算法,该算法可以动态改变目标像素值,以便从一条车道切换到另一条车道。
LineFollower配置
完整的配置值及其默认值可以在donkeycar/templates/cfg_cv_control.py中找到,为方便起见,这里复制一份。
# 配置使用作为自动驾驶的部分 - 更改以使用您自己的自动驾驶
CV_CONTROLLER_MODULE = "donkeycar.parts.line_follower"
CV_CONTROLLER_CLASS = "LineFollower"
CV_CONTROLLER_INPUTS = ['cam/image_array']
CV_CONTROLLER_OUTPUTS = ['pilot/steering', 'pilot/throttle', 'cv/image_array']
CV_CONTROLLER_CONDITION = "run_pilot"
LineFollower - 线路颜色和检测区域
SCAN_Y = 120 # 从顶部开始水平扫描的像素数
SCAN_HEIGHT = 20 # 从水平扫描中获取的像素数
COLOR_THRESHOLD_LOW = (0, 50, 50) # HSV深黄色(OpenCV中HSV色调值为0..179,饱和度和亮度都为0..255)
COLOR_THRESHOLD_HIGH = (50, 255, 255) # HSV浅黄色(OpenCV中HSV色调值为0..179,饱和度和亮度都为0..255)
根据提供的配置参数,以下是LineFollower的一些关键设置和说明:
- 目标线位置和检测阈值:
TARGET_PIXEL
:期望的黄线水平位置(以像素为单位)。如果设置为None
,则在启动时检测黄线的位置。这假设您在启动之前已经将车辆定位好。-
TARGET_THRESHOLD
:车辆必须指向TARGET_PIXEL
附近的像素数,才会进行转向调整。这可以防止算法在或接近线上时过于敏感。 -
检测阈值:
-
CONFIDENCE_THRESHOLD
:采样片段中必须为黄色的像素占总采样像素的比例。采样片段的高度为SCAN_HEIGHT
,总采样像素数为IMAGE_W x SCAN_HEIGHT
。如果希望确保采样片段中的所有像素都是黄色,则置信度阈值应为SCAN_HEIGHT / (IMAGE_W x SCAN_HEIGHT)
或(1 / IMAGE_W)
。如果在控制台中不断出现No line detected
的日志,则可能需要降低阈值。 -
油门控制器:
THROTTLE_MAX
:控制器产生的最大油门值。THROTTLE_MIN
:控制器产生的最小油门值。THROTTLE_INITIAL
:初始油门值。-
THROTTLE_STEP
:当车辆偏离线路时,每次改变油门的量。 -
PID控制器:
PID_P
:比例系数,用于PID路径跟踪。PID_I
:积分系数,用于PID路径跟踪。-
PID_D
:微分系数,用于PID路径跟踪。 -
其他设置:
OVERLAY_IMAGE
:在Web界面的相机图像上绘制计算机视觉叠加层。
PID控制器是一种常用的控制器,用于控制轮式机器人的油门和/或转向。在Line Follower算法中,使用PID控制器类似地进行控制。Line Follower算法输出一个与车辆离中线的距离成比例的值,并根据符号指示车辆在中线的哪一侧。PID控制器利用与中线距离的大小和符号来计算一个转向值,将车辆移向中线。
关于如何调整PID控制器的参数,可以参考路径跟随(Path Follow)自动驾驶的文档中的确定PID系数部分的描述。
编写计算机视觉自动驾驶部件
您可以使用CV_CONTROLLER_*
配置值指向一个Python文件和类,该类实现了您自己的计算机视觉自动驾驶部分。您的自动驾驶类必须符合donkeycar部件的标准。您还可以确定输入值、输出值和运行条件的名称。默认的配置值指向了包含的LineFollower部件。至少,计算机视觉自动驾驶部分需要将摄像头图像作为输入,并输出自动驾驶的油门和转向值。
让我们创建一个简单的自定义计算机视觉部分。它不会成为一个真正的自动驾驶器,因为它只会输出一个恒定的油门和转向值,以及一个计数帧的图像。
计算机视觉部分是一个donkeycar部件,所以至少它必须是一个具有run(self)
方法的Python类。自动驾驶器需要更多的功能,我们将在下面看到,但这是一个最简单的结构:
import cv2
import numpy as np
from simple_pid import PID
import logging
logger = logging.getLogger(__name__)
class MockCvPilot:
def __init__(self, pid, cfg):
# 初始化实例属性
pass
def run(self, img):
# 使用图像确定转向和油门值
return 0, 0, None # 转向角度、油门、图像
构造函数__init__(self, pid, cfg)
接受一个PID控制器实例和车辆配置属性。在自动驾驶中使用PID控制器非常常见,因此框架提供了一个。您可能有一些要调整以优化算法的值,您可以将这些值放在myconfig.py
配置文件中,然后在构造函数中检索它们。在我们的MockCvPilot
中,我们想知道用户是否希望看到遥测图像还是仅相机图像。我们在内置的LineFollower
自动驾驶部分中也做同样的事情,因此我们可以在我们的自动驾驶中重用该配置值OVERLAY_IMAGE
。我们可以在构造函数中添加它:
def __init__(self, pid, cfg):
self.pid_st = pid
self.overlay_image = cfg.OVERLAY_IMAGE
self.counter = 0
run(self, img)
方法在每次循环中被调用。在这里,您将解释传递的图像并确定车辆应该使用的转向和油门值。计算机视觉模板还允许在自动驾驶模式下在Web界面中显示不同的图像;通常,您会向传递给run
的相机图像添加遥测信息,例如新的转向和油门值,以及对图像的其他修改,以便用户可以更好地理解算法的工作原理。例如,如果您的算法使用Canny算法进行边缘检测,那么您可能希望显示带有边缘的处理后图像。因此,最简化的自动驾驶部分返回一个元组(转向角度、油门、图像)。
为了保持简单,MockCvPilot实际上不会预测转向和油门,它只会为每个返回零。但是,它将维护一个计数器,并在遥测图像中显示该计数器。我们可以在run()
方法中看到这一点。
def run(self, cam_img):
if cam_img is None:
return 0, 0, None
self.counter += 1
# 显示一些诊断信息
if self.overlay_image:
# 在图像的副本上绘制,以免改变原始图像
cam_img = self.overlay_display(np.copy(cam_img))
return self.steering, self.throttle, cam_img
希望这可以帮助到您!如果您有任何其他问题,请随时提问。
有几点需要注意:
run()
方法对空相机图像进行了保护处理 - 这可能会发生,特别是在启动过程中。因此,在这种情况下,我们停止车辆。- 只有当原始配置值
OVERLAY_IMAGE
(我们复制到self.overlay_image
的值)为True
时,我们才生成遥测图像。如果它不是True
,那么我们只是传递原始相机图像。 - 请注意,我们对原始相机图像进行了拷贝,以便不修改原始图像。这被称为“防御性拷贝”;我们不知道车辆中的其他部分可能需要对原始图像进行的操作,因此我们不希望修改它。
我们将绘制遥测图像的逻辑放入了一个单独的方法中,以保持该方法和 run()
方法的清晰性和内聚性。此外,由于 run()
方法已经对原始图像进行了防御性拷贝,因此该方法可以对图像进行任何操作,甚至完全覆盖它。在我们的情况下,我们只是在图像上绘制一些文本,以显示转向角度、油门和计数器的值。我们知道在我们的模拟自动驾驶中,转向角度和油门值将为零,但是展示如何显示它们是有益的。在这种情况下,我们将它们显示为文本,但是您也可以选择将它们显示为条形图,就像我们在 Web UI 中所做的那样,或者使用其他可视化方式。这是我们在模拟自动驾驶中使用的显示方法:
def overlay_display(self, img):
display_str = []
display_str.append(f"STEERING:{self.steering:.1f}")
display_str.append(f"THROTTLE:{self.throttle:.2f}")
display_str.append(f"COUNTER:{self.counter}")
lineheight = 25
y = lineheight
x = lineheight
for s in display_str:
cv2.putText(img, s, color=(0, 0, 0), org=(x ,y), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, thickness=3)
cv2.putText(img, s, color=(0, 255, 0), org=(x ,y), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, thickness=1)
y += lineheight
return img
有几点需要注意:
- 我们将文本组织为字符串数组;这样在绘制文本时很容易处理每一行。您甚至可以编写一个单独的方法来创建此文本字符串列表,并在需要时将其传递给 display()
方法,如果这样做可以简化显示方法或使其更加灵活(可以同时处理多种情况)。
- 我们两次绘制文本,首先使用粗黑色描边,然后再使用较细的绿色描边。这样可以创建具有黑色轮廓的绿色文本,这样在不可预测的背景上更容易阅读。
以下是完整的自定义计算机视觉自动驾驶部分的代码:
import cv2
import numpy as np
from simple_pid import PID
import logging
logger = logging.getLogger(__name__)
class MockCvPilot:
'''
基于OpenCV的MOCK控制器;只绘制计数器并返回0作为油门和转向角度。
:param pid: 可用于估计转向角度和/或油门的PID控制器
:param cfg: 车辆配置属性
'''
def __init__(self, pid, cfg):
self.pid_st = pid
self.overlay_image = cfg.OVERLAY_IMAGE
self.steering = 0
self.throttle = 0
self.counter = 0
def run(self, cam_img):
'''
CV控制器的主要运行循环。
:param cam_img: 相机图像,一个RGB的numpy数组
:return: 转向角度、油门和遥测图像的元组。
如果 overlay_image 为 True,则输出图像包括一个显示算法工作方式的叠加层;否则,图像将直接通过不做任何修改。
'''
if cam_img is None:
return 0, 0, None
self.counter += 1
# 显示一些诊断信息
if self.overlay_image:
# 在图像的拷贝上绘制,以免修改原始图像
cam_img = self.overlay_display(np.copy(cam_img))
return self.steering, self.throttle, cam_img
def overlay_display(self, img):
'''
在给定图像上绘制叠加层。
显示一些用于控制的值。
:param img: 要绘制的图像,作为numpy数组
:return: 绘制了叠加层的图像
'''
# 一些显示在叠加层上的文本
display_str = []
display_str.append(f"STEERING:{self.steering:.1f}")
display_str.append(f"THROTTLE:{self.throttle:.2f}")
display_str.append(f"COUNTER:{self.counter}")
lineheight = 25
y = lineheight
x = lineheight
for s in display_str:
# 绿色文本,黑色轮廓,以便在任何背景上都能显示出来
cv2.putText(img, s, color=(0, 0, 0), org=(x ,y), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, thickness=3)
cv2.putText(img, s, color=(0, 255, 0), org=(x ,y), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=1, thickness=1)
y += lineheight
return img
要使用自定义部分,我们必须修改 mycar 文件夹中的 myconfig.py 文件,以定位 Python 文件及其中的类,并指定在将该部分添加到车辆循环时应使用的输入、输出和运行条件:
# 配置用作自动驾驶的部分 - 更改为使用您自己的自动驾驶部分
CV_CONTROLLER_MODULE = "my_cv_pilot"
CV_CONTROLLER_CLASS = "MockCvPilot"
CV_CONTROLLER_INPUTS = ['cam/image_array']
CV_CONTROLLER_OUTPUTS = ['pilot/steering', 'pilot/throttle', 'cv/image_array']
CV_CONTROLLER_CONDITION = "run_pilot"
CV_CONTROLLER_MODULE
是指向 my_cv_autopilot.py
文件的包路径。通常将其放在 mycar 文件夹中会很方便,这就是我们在这里所做的。但是,如果您在自己的存储库中开发此部分,那么如果您使用的是 Mac 或 Linux 计算机,可以创建一个符号链接,链接到文件或文件所在的文件夹。
CV_CONTROLLER_CLASS
是指向 Python 文件中该部分类的名称,该文件由 CV_CONTROLLER_MODULE
指向。在我们的示例中,这是 MockCvPilot
。
CV_CONTROLLER_INPUTS
是传递给部分的命名输入的数组,这些输入在将部分添加到车辆循环时传递。对于计算机视觉自动驾驶,图像是必需的最小要求。但是,您可以传递车辆内存中的任何命名值。它们与自动驾驶的 run()
方法的参数(忽略 self 参数)一一对应。因此,我们的模拟示例仅期望一个图像 run(self, cam_img)
,并且我们只在输入中声明了一个图像,['cam/image_array']
。
CV_CONTROLLER_OUTPUTS
是传递给部分的命名输出的数组,这些输出在将部分添加到车辆循环时传递。它们对应于自动驾驶的 run()
的返回值。这是一个自动驾驶部分,因此我们返回转向值和油门值。我们还生成一个带有遥测信息的新图像。因此,我们的模拟自动驾驶返回 return self.steering, self.throttle, cam_img
,这对应于声明的输出值,['pilot/steering', 'pilot/throttle', 'cv/image_array']
。
CV_CONTROLLER_CONDITION
是决定自动驾驶部分是否运行的命名值。如果您希望它始终运行,则传递 None
,否则它应该是一个布尔值的名称;当它为 True
时,将调用部分的 run()
方法;当它为 False
时,不调用 run()
方法。模板中维护了一个名为 "run_pilot"
的布尔值,所以我们使用它。
现在您已经了解了自动驾驶部分的结构,值得回顾上面的Line Follower部分中的伪代码,并将其与实际实现进行比较。Python 文件位于 https://github.com/autorope/donkeycar/blob/main/donkeycar/parts/line_follower.py,并在下面复制了一份。特别是:
get_i_color()
方法使用SCAN_Y
和SCAN_HEIGHT
复制相机图像的一部分,将其转换为 HSV 并应用由低和高 HSV 掩码值创建的掩码。然后,它找到该区域中具有最多正像素的 x(水平)索引;这是自动驾驶认为线路所在的位置。
def get_i_color(self, cam_img):
# 取图像的水平切片
iSlice = self.scan_y
scan_line = cam_img[iSlice : iSlice + self.scan_height, :, :]
# 转换为 HSV 颜色空间
img_hsv = cv2.cvtColor(scan_line, cv2.COLOR_RGB2HSV)
# 创建我们要查找的颜色范围的掩码
mask = cv2.inRange(img_hsv, self.color_thr_low, self.color_thr_hi)
# 哪个索引的范围中的黄色最多?
hist = np.sum(mask, axis=0)
max_yellow = np.argmax(hist)
return max_yellow, hist[max_yellow], mask
- 注意,如果配置中的图像尚未初始化,则
get_i_color()
方法通过读取图像来初始化目标像素值:
max_yellow, confidence, mask = self.get_i_color(cam_img)
if self.target_pixel is None:
self.target_pixel = max_yellow
- 注意,在
run()
方法中使用TARGET_PIXEL
值初始化 PID 控制器的设定点(目标值):
if self.pid_st.setpoint != self.target_pixel:
# 这是我们转向 PID 控制器的目标值
self.pid_st.setpoint = self.target_pixel
- 如果我们从图像中获得了良好的读数,则根据检测到的线路与
target_pixel
的水平距离预测一个新的转向值:
if confidence >= self.confidence_threshold:
# 使用当前黄线位置调用控制器
# 获取追踪理想目标值时的新转向值
self.steering = self.pid_st(max_yellow)
- 如果我们正在转弯,则减速:
if abs(max_yellow - self.target_pixel) > self.target_threshold:
# 我们将转弯,所以减速
if self.throttle > self.throttle_min:
self.throttle -= self.delta_th
- 如果我们直行,则加速:
else:
# 我们直行,所以加速
if self.throttle < self.throttle_max:
self.throttle += self.delta_th
Here is the complete source to the LineFollower
part.
import cv2
import numpy as np
from simple_pid import PID
import logging
logger = logging.getLogger(__name__)
class LineFollower:
'''
OpenCV based controller
This controller takes a horizontal slice of the image at a set Y coordinate.
Then it converts to HSV and does a color thresh hold to find the yellow pixels.
It does a histogram to find the pixel of maximum yellow. Then is uses that iPxel
to guid a PID controller which seeks to maintain the max yellow at the same point
in the image.
'''
def __init__(self, pid, cfg):
self.overlay_image = cfg.OVERLAY_IMAGE
self.scan_y = cfg.SCAN_Y # num pixels from the top to start horiz scan
self.scan_height = cfg.SCAN_HEIGHT # num pixels high to grab from horiz scan
self.color_thr_low = np.asarray(cfg.COLOR_THRESHOLD_LOW) # hsv dark yellow
self.color_thr_hi = np.asarray(cfg.COLOR_THRESHOLD_HIGH) # hsv light yellow
self.target_pixel = cfg.TARGET_PIXEL # of the N slots above, which is the ideal relationship target
self.target_threshold = cfg.TARGET_THRESHOLD # minimum distance from target_pixel before a steering change is made.
self.confidence_threshold = cfg.CONFIDENCE_THRESHOLD # percentage of yellow pixels that must be in target_pixel slice
self.steering = 0.0 # from -1 to 1
self.throttle = cfg.THROTTLE_INITIAL # from -1 to 1
self.delta_th = cfg.THROTTLE_STEP # how much to change throttle when off
self.throttle_max = cfg.THROTTLE_MAX
self.throttle_min = cfg.THROTTLE_MIN
self.pid_st = pid
def get_i_color(self, cam_img):
'''
get the horizontal index of the color at the given slice of the image
input: cam_image, an RGB numpy array
output: index of max color, value of cumulative color at that index, and mask of pixels in range
'''
# take a horizontal slice of the image
iSlice = self.scan_y
scan_line = cam_img[iSlice : iSlice + self.scan_height, :, :]
# convert to HSV color space
img_hsv = cv2.cvtColor(scan_line, cv2.COLOR_RGB2HSV)
# make a mask of the colors in our range we are looking for
mask = cv2.inRange(img_hsv, self.color_thr_low, self.color_thr_hi)
# which index of the range has the highest amount of yellow?
hist = np.sum(mask, axis=0)
max_yellow = np.argmax(hist)
return max_yellow, hist[max_yellow], mask
def run(self, cam_img):
'''
main runloop of the CV controller
input: cam_image, an RGB numpy array
output: steering, throttle, and the image.
If overlay_image is True, then the output image
includes and overlay that shows how the
algorithm is working; otherwise the image
is just passed-through untouched.
'''
if cam_img is None:
return 0, 0, False, None
max_yellow, confidence, mask = self.get_i_color(cam_img)
conf_thresh = 0.001
if self.target_pixel is None:
# Use the first run of get_i_color to set our relationship with the yellow line.
# You could optionally init the target_pixel with the desired value.
self.target_pixel = max_yellow
logger.info(f"Automatically chosen line position = {self.target_pixel}")
if self.pid_st.setpoint != self.target_pixel:
# this is the target of our steering PID controller
self.pid_st.setpoint = self.target_pixel
if confidence >= self.confidence_threshold:
# invoke the controller with the current yellow line position
# get the new steering value as it chases the ideal target_value
self.steering = self.pid_st(max_yellow)
# slow down linearly when away from ideal, and speed up when close
if abs(max_yellow - self.target_pixel) > self.target_threshold:
# we will be turning, so slow down
if self.throttle > self.throttle_min:
self.throttle -= self.delta_th
if self.throttle < self.throttle_min:
self.throttle = self.throttle_min
else:
# we are going straight, so speed up
if self.throttle < self.throttle_max:
self.throttle += self.delta_th
if self.throttle > self.throttle_max:
self.throttle = self.throttle_max
else:
logger.info(f"No line detected: confidence {confidence} < {self.confidence_threshold}")
# show some diagnostics
if self.overlay_image:
cam_img = self.overlay_display(cam_img, mask, max_yellow, confidence)
return self.steering, self.throttle, cam_img
def overlay_display(self, cam_img, mask, max_yellow, confidense):
'''
composite mask on top the original image.
show some values we are using for control
'''
mask_exp = np.stack((mask, ) * 3, axis=-1)
iSlice = self.scan_y
img = np.copy(cam_img)
img[iSlice : iSlice + self.scan_height, :, :] = mask_exp
# img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
display_str = []
display_str.append("STEERING:{:.1f}".format(self.steering))
display_str.append("THROTTLE:{:.2f}".format(self.throttle))
display_str.append("I YELLOW:{:d}".format(max_yellow))
display_str.append("CONF:{:.2f}".format(confidense))
y = 10
x = 10
for s in display_str:
cv2.putText(img, s, color=(0, 0, 0), org=(x ,y), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.4)
y += 10
return img