计算机视觉自动驾驶

计算机视觉自动驾驶与深度学习自动驾驶类似,都是通过解读摄像头图像来确定转向和油门值。然而,计算机视觉自动驾驶不使用深度学习模型,而是利用传统的计算机视觉算法,如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

路线跟随器

内置算法可以使用摄像头跟踪线路。默认情况下,它被调整为追踪黄色线路,但可以配置要追踪的颜色。算法的许多其他方面也可以进行调整。以下是算法的描述以及如何使用配置值。随后列出并描述了这些值。

  1. 如果TARGET_PIXEL为None,则使用步骤1到5来估计线路的目标(预期)位置。
  2. 复制图像在高度为SCAN_YSCAN_HEIGHT像素的行。结果是一个宽度与图像相同且高度为SCAN_HEIGHT的像素块。
  3. 将像素从RGB(红绿蓝)颜色空间转换为HSV(色调饱和度值)颜色空间。
  4. 然后,算法识别块中所有HSV颜色在COLOR_THRESHOLD_LOWCOLOR_THRESHOLD_HIGH之间的像素。
  5. 一旦隔离出目标颜色的像素,就会创建一个直方图,用于每个宽度为1像素、高度为SCAN_HEIGHT的切片从左到右计算黄色像素的计数。
  6. 选择具有最多黄色像素的切片的x值(水平偏移)。这是算法认为黄色线路的位置。
  7. 使用此x值与TARGET_PIXEL值之间的差作为PID算法计算新转向的值。如果该值比TARGET_PIXEL向左偏移超过TARGET_THRESHOLD像素,则车辆向右转;如果该值比TARGET_PIXEL向右偏移超过TARGET_THRESHOLD像素,则车辆向左转。如果该值与TARGET_PIXELTARGET_THRESHOLD像素范围内,则不改变转向。
  8. 转向值用于决定车辆是否加速或减速。如果转向不改变,则油门增加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_YSCAN_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_YSCAN_HEIGHT定义,并且是应用掩码的区域,以尝试隔离线路中的像素。当检测到像素时,它们将在检测区域中以白色绘制。

屏幕底部有6个滑动条,用于选择低HSV值的3个部分和高HSV值的3个部分,这些值用于创建一个掩膜,以提取线条中的像素。您可以手动移动这些滑动条,尝试找到最佳的检测范围。当您改变滑动条时,结果掩膜将应用于图像,并且您将看到像素开始恢复。色调值通常是最重要的值。您可以通过选择键盘上的Escape键随时重置滑动条并清除掩膜。

使用滚动条确实可以工作,但还有一种更简单的方法。您还可以通过点击-拖动-释放来选择一个矩形区域;该区域内的像素将被搜索以找到低值和高值,并将这些低值和高值更新到滚动条上。因此,找到线条的掩码最简单的方法是在线条本身上选择一个矩形区域。您可以使用滚动条对所选的掩码进行微调。

下面的图像显示了通过在蓝线内选择一个矩形区域创建的掩码。

A masked screenshot in the hsv_picker.sh script

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的一些关键设置和说明:

  1. 目标线位置和检测阈值:
  2. TARGET_PIXEL:期望的黄线水平位置(以像素为单位)。如果设置为None,则在启动时检测黄线的位置。这假设您在启动之前已经将车辆定位好。
  3. TARGET_THRESHOLD:车辆必须指向TARGET_PIXEL附近的像素数,才会进行转向调整。这可以防止算法在或接近线上时过于敏感。

  4. 检测阈值:

  5. CONFIDENCE_THRESHOLD:采样片段中必须为黄色的像素占总采样像素的比例。采样片段的高度为SCAN_HEIGHT,总采样像素数为IMAGE_W x SCAN_HEIGHT。如果希望确保采样片段中的所有像素都是黄色,则置信度阈值应为SCAN_HEIGHT / (IMAGE_W x SCAN_HEIGHT)(1 / IMAGE_W)。如果在控制台中不断出现No line detected的日志,则可能需要降低阈值。

  6. 油门控制器:

  7. THROTTLE_MAX:控制器产生的最大油门值。
  8. THROTTLE_MIN:控制器产生的最小油门值。
  9. THROTTLE_INITIAL:初始油门值。
  10. THROTTLE_STEP:当车辆偏离线路时,每次改变油门的量。

  11. PID控制器:

  12. PID_P:比例系数,用于PID路径跟踪。
  13. PID_I:积分系数,用于PID路径跟踪。
  14. PID_D:微分系数,用于PID路径跟踪。

  15. 其他设置:

  16. 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_YSCAN_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