9.自主驾驶

在接下来的环节中,我们要实现漫游者号的自动驾驶功能。

完成这个功能我们需要四个程序,第一个为感知程序,其对摄像头输入的图片进行变换处理和坐标变换使用。第二个程序为决策程序,功能是帮助漫游者号根据当前条件和状态进行相应的决策,以实现漫游者号前进,后退,转向等功能。第三个是支持程序,来定义一些关于漫游者号状态的类等。最后为主程序,来调用三个函数对漫游者号进行控制的。

Udacity提供的初始程序在Udacity机器人软件工程师课程笔记(三)中有所提供。

经过前面的学习,我们已经清楚了感知程序和决策程序,因为之前我都把它写成了一个程序,这样导致程序显得非常复杂,可读性不是太高。现在针对各个功能程序进行程序的划分,来方便最后的主程序的调用。以下的初始程序都放在RoboND-Rover-Project文件夹下的code文件夹中。

(1) 感知部分perception.py
首先是感知部分。感知部分perception.py应当具有三个功能,

图像阈值处理
透视变换
坐标变换

这些功能在之前的程序中已经多次使用了,但是我们需要调用这些功能去编写一个综合感知函数 perception_step() 来对图像进行处理,它需要完成以下功能:

1)定义透视变换的源点和目标点
2)应用透视变换
3)应用颜色阈值来识别可导航的地形/障碍物/岩石样本
4)更新Rover.vision_image(这个图像显示在屏幕的左侧)
5)将地图图像像素值转换为以漫游者号为中心的坐标
6)将以漫游者号为中心的像素值坐标转换为世界坐标
7)更新漫游者号的世界地图(这个图像显示在屏幕右侧)
8)将以漫游者号为中心的像素位置转换为极坐标
在这里插入图片描述

感知部分的程序如下

import numpy as np
import cv2
import matplotlib.pyplot as plt


##图像处理
# 定义二值化图像函数
def color_thresh(img, rgb_thresh=(160, 160, 160)):
    img_thresh = np.zeros_like(img[:, :, 0])
    above_thresh = (img[:, :, 0] > rgb_thresh[0]) \
                   & (img[:, :, 1] > rgb_thresh[1]) \
                   & (img[:, :, 2] > rgb_thresh[2])
    img_thresh[above_thresh] = 255

    return img_thresh

##透视变换
# 定义图像映射函数,将摄像头的图像映射到平面坐标中去
def perspect_transform(img, src, dst):
    M = cv2.getPerspectiveTransform(src, dst)  # 定义变换矩阵
    img_perspect = cv2.warpPerspective(img, M, (img.shape[1], img.shape[0]))
    return img_perspect

##坐标变换
# 定义从图像坐标转换成漫游者号坐标函数
def rover_coords(binary_img):
    ypos, xpos = binary_img.nonzero()
    x_pixel = -(ypos - binary_img.shape[0]).astype(np.float)
    y_pixel = -(xpos - binary_img.shape[1]/2 ).astype(np.float)
    return x_pixel, y_pixel

# 定义旋转操作函数
def rotate_pix(xpix, ypix, yaw):
    yaw_rad = yaw * np.pi / 180
    xpix_rotated = (xpix * np.cos(yaw_rad)) - (ypix * np.sin(yaw_rad))
    ypix_rotated = (xpix * np.sin(yaw_rad)) + (ypix * np.cos(yaw_rad))
    return xpix_rotated, ypix_rotated


# 定义平移操作函数
def translate_pix(xpix_rot, ypix_rot, xpos, ypos, scale):
    xpix_translated = (xpix_rot / scale) + xpos
    ypix_translated = (ypix_rot / scale) + ypos
    return xpix_translated, ypix_translated


# 定义综合函数,将旋转和平移函数进行结合,并限制了图像范围
def pix_to_world(xpix, ypix, xpos, ypos, yaw, world_size, scale):
    xpix_rot, ypix_rot = rotate_pix(xpix, ypix, yaw)
    xpix_tran, ypix_tran = translate_pix(xpix_rot, ypix_rot, xpos, ypos, scale)
    x_pix_world = np.clip(np.int_(xpix_tran), 0, world_size - 1)
    y_pix_world = np.clip(np.int_(ypix_tran), 0, world_size - 1)
    return x_pix_world, y_pix_world


# 定义转换为极坐标函数
def to_polar_coords(xpix, ypix):
    dist = np.sqrt(xpix**2 + ypix ** 2)
    angles = np.arctan2(ypix, xpix)
    return dist, angles


def perception_step(Rover):
    # (1)定义透视变换的原点和目标点
    # 参考图像
    filename = 'E:/2019/RoboND-Rover-Project-master/calibration_images/example_grid1.jpg'
    image = cv2.imread(filename)

    dst_size = 5
    bottom_offset = 0

    src = np.float32([[14, 140], [301, 140], [200, 96], [118, 96]])
    dst = np.float32([[image.shape[1] / 2 - dst_size, image.shape[0] - bottom_offset],
                      [image.shape[1] / 2 + dst_size, image.shape[0] - bottom_offset],
                      [image.shape[1] / 2 + dst_size, image.shape[0] - 2 * dst_size - bottom_offset],
                      [image.shape[1] / 2 - dst_size, image.shape[0] - 2 * dst_size - bottom_offset],
                      ])

    # (2)应用透视变换
    img_perspect = perspect_transform(Rover.img, src, dst)

    # (3)应用颜色阈值来识别可导航的地形/障碍物/岩石样本
    img_thresh_ter = color_thresh(img_perspect, rgb_thresh=(160, 160, 160))
    img_thresh_obs = color_thresh(img_perspect, rgb_thresh=(160, 160, 160))
    img_thresh_roc = color_thresh(img_perspect, rgb_thresh=(160, 130, 0))

    # (4)更新ROver.vision_image
    Rover.vision_image[:, :, 0] = img_thresh_roc
    Rover.vision_image[:, :, 1] = img_thresh_obs
    Rover.vision_image[:, :, 2] = img_thresh_ter

    # (5)将地图像素值转换为以漫游者号为中心的坐标
    xpix, ypix = rover_coords(img_thresh_ter)

    # (6)将以漫游者号为中心的像素值坐标转化为世界地图坐标
    x_pix_world, y_pix_world = pix_to_world(xpix, ypix, xpos=Rover.pos[0], ypos=Rover.pos[1], yaw=Rover.yaw, world_size=200, scale=10)

    # (7)更新漫游者号的世界地图
    Rover.worldmap[x_pix_world, y_pix_world] += 1

    # (8)将以漫游者号为中心的像素位置转换为极坐标
    rover_distances, rover_angles = to_polar_coords(xpix, ypix)

    # 可导航地形像素的角度的数组
    Rover.nav_angles = rover_angles

    # 获取转向角,使用极坐标系角度的平均值,这样可以运行,但是会有意外情况
    avg_angle_degrees = np.mean((rover_angles ) * 180 / np.pi)
    Rover.steer= np.clip(avg_angle_degrees, -15, 15)

    # 获取油门速度,使用平均极坐标系的平均值
    Rover.throttle = np.mean(rover_distances)
    print('steer:', Rover.steer, 'Rover.throttle:', Rover.throttle)

    return Rover

(2) 决策部分decision.py

使用决策树逻辑编写的简单判别系统,控制漫游者号对目前的状态和环境做出相应的判断,并执行相应的加速,转向,停止等操作。

import numpy as np

# 命令基于perception_step()函数的输出
def decision_step(Rover):

    if Rover.nav_angles is not None:
        # 检查 Rover.mode 状态
        if Rover.mode == 'forward':
            # 检查可导航地形的范围
            if len(Rover.nav_angles) >= Rover.stop_forward:  
                # 如果为forward模式,可导航地图是好的,速度如果低于最大值,则加速
                if Rover.vel < Rover.max_vel:
                    # 设置油门值
                    Rover.throttle = Rover.throttle_set
                else:
                    Rover.throttle = 0
                Rover.brake = 0
                # 将转向设置为平均夹角,范围为 +/- 15
                Rover.steer = np.clip(np.mean(Rover.nav_angles * 180/np.pi), -15, 15)
            # 如果缺少地形图,则设置为停止模式
            elif len(Rover.nav_angles) < Rover.stop_forward:
                    # 设置到停止模式并刹车
                    Rover.throttle = 0
                    # 设置制动值
                    Rover.brake = Rover.brake_set
                    Rover.steer = 0
                    Rover.mode = 'stop'

        # 如果我们已经处于“停止”模式,那么做出不同的决定
        elif Rover.mode == 'stop':
            # 如果我们处于停止模式但仍然继续制动
            if Rover.vel > 0.2:
                Rover.throttle = 0
                Rover.brake = Rover.brake_set
                Rover.steer = 0
            # 如果我们没有移动(vel <0.2),那就做点别的
            elif Rover.vel <= 0.2:
                # 现在为停止状态,依靠视觉数据,判断是否有前进的道路
                if len(Rover.nav_angles) < Rover.go_forward:
                    Rover.throttle = 0
                    # 松开制动器以便转动
                    Rover.brake = 0
                    # 转弯范围为+/- 15度,当停止时,下一条线将引起4轮转向
                    Rover.steer = -15
                #如果停下来但看到前面有足够的可导航地形,那就启动
                if len(Rover.nav_angles) >= Rover.go_forward:
                    # 将油门设置回存储值
                    Rover.throttle = Rover.throttle_set
                    Rover.brake = 0
                    # 将转向设置为平均角度
                    Rover.steer = np.clip(np.mean(Rover.nav_angles * 180/np.pi), -15, 15)
                    Rover.mode = 'forward'
    else:
        Rover.throttle = Rover.throttle_set
        Rover.steer = 0
        Rover.brake = 0
        
    # 在可拾取岩石的状态下发送拾取命
    if Rover.near_sample and Rover.vel == 0 and not Rover.picking_up:
        Rover.send_pickup = True
    
    return Rover

(3)项目支持函数

提供一些支持项目运行的函数,比如创建输出图像等等,具体的功能可以看程序。
程序如下:

import numpy as np
import cv2
from PIL import Image
from io import BytesIO, StringIO
import base64
import time


# 定义一个函数,将遥测字符串转换为浮点数,与十进制约定无关
def convert_to_float(string_to_convert):
    if ',' in string_to_convert:
        float_value = np.float(string_to_convert.replace(',', '.'))
    else:
        float_value = np.float(string_to_convert)
    return float_value


def update_rover(Rover, data):
    # 初始化开始时间和样本位置
    if Rover.start_time is None:
        Rover.start_time = time.time()
        Rover.total_time = 0
        samples_xpos = np.int_([convert_to_float(pos.strip()) for pos in data["samples_x"].split(';')])
        samples_ypos = np.int_([convert_to_float(pos.strip()) for pos in data["samples_y"].split(';')])
        Rover.samples_pos = (samples_xpos, samples_ypos)
        Rover.samples_to_find = np.int(data["sample_count"])
    # 或者只是更新已用时间e
    else:
        tot_time = time.time() - Rover.start_time
        if np.isfinite(tot_time):
            Rover.total_time = tot_time
    # 打印遥测数据字典中的字段
    print(data.keys())
    # 漫游者号的当前速度,单位为m / s
    Rover.vel = convert_to_float(data["speed"])
    # 漫游者号的当前位置
    Rover.pos = [convert_to_float(pos.strip()) for pos in data["position"].split(';')]
    # 当前的漫游者号偏航角
    Rover.yaw = convert_to_float(data["yaw"])
    # 当前的漫游者号的倾斜度
    Rover.pitch = convert_to_float(data["pitch"])
    # 当前的漫游者号转动角度
    Rover.roll = convert_to_float(data["roll"])
    # 当前油门设置
    Rover.throttle = convert_to_float(data["throttle"])
    # 当前转向角
    Rover.steer = convert_to_float(data["steering_angle"])
    # Near sample 标志
    Rover.near_sample = np.int(data["near_sample"])
    # Picking up 标志
    Rover.picking_up = np.int(data["picking_up"])
    # 更新收集的岩石数量
    Rover.samples_collected = Rover.samples_to_find - np.int(data["sample_count"])

    print('speed =', Rover.vel, 'position =', Rover.pos, 'throttle =',
          Rover.throttle, 'steer_angle =', Rover.steer, 'near_sample:', Rover.near_sample,
          'picking_up:', data["picking_up"], 'sending pickup:', Rover.send_pickup,
          'total time:', Rover.total_time, 'samples remaining:', data["sample_count"],
          'samples collected:', Rover.samples_collected)
    # 从漫游者号的中央摄像头获取当前图像
    imgString = data["image"]
    image = Image.open(BytesIO(base64.b64decode(imgString)))
    Rover.img = np.asarray(image)

    # 返回更新的漫游者号和单独的图像以进行可选保存
    return Rover, image


# 定义一个函数,根据世界地图结果创建显示输出
def create_output_images(Rover):
    # 创建一个缩放的地图,用于绘制和清理一下障碍物/导航像素
    if np.max(Rover.worldmap[:, :, 2]) > 0:
        nav_pix = Rover.worldmap[:, :, 2] > 0
        navigable = Rover.worldmap[:, :, 2] * (255 / np.mean(Rover.worldmap[nav_pix, 2]))
    else:
        navigable = Rover.worldmap[:, :, 2]
    if np.max(Rover.worldmap[:, :, 0]) > 0:
        obs_pix = Rover.worldmap[:, :, 0] > 0
        obstacle = Rover.worldmap[:, :, 0] * (255 / np.mean(Rover.worldmap[obs_pix, 0]))
    else:
        obstacle = Rover.worldmap[:, :, 0]

    likely_nav = navigable >= obstacle
    obstacle[likely_nav] = 0
    plotmap = np.zeros_like(Rover.worldmap)
    plotmap[:, :, 0] = obstacle
    plotmap[:, :, 2] = navigable
    plotmap = plotmap.clip(0, 255)
    # 覆盖障碍物和可导航的地形图与地面实况图
    map_add = cv2.addWeighted(plotmap, 1, Rover.ground_truth, 0.5, 0)

    # 检查worldmap中是否存在任何岩石
    rock_world_pos = Rover.worldmap[:, :, 1].nonzero()
    # 如果有,我们将逐步完成已知的样品位置
    # 确认检测是否真实
    samples_located = 0
    if rock_world_pos[0].any():

        rock_size = 2
        for idx in range(len(Rover.samples_pos[0])):
            test_rock_x = Rover.samples_pos[0][idx]
            test_rock_y = Rover.samples_pos[1][idx]
            rock_sample_dists = np.sqrt((test_rock_x - rock_world_pos[1]) ** 2 + \
                                        (test_rock_y - rock_world_pos[0]) ** 2)
            # 如果在已知样品位置3米范围内检测到岩石,
            # 则认为它成功并在地图上绘制已知样品的位置
            if np.min(rock_sample_dists) < 3:
                samples_located += 1
                map_add[test_rock_y - rock_size:test_rock_y + rock_size,
                test_rock_x - rock_size:test_rock_x + rock_size, :] = 255

    # 计算地图结果的一些统计数据
    # 首先获取可导航地形图中的总像素数
    tot_nav_pix = np.float(len((plotmap[:, :, 2].nonzero()[0])))
    # 接下来计算出其中有多少对应于地面实况像素
    good_nav_pix = np.float(len(((plotmap[:, :, 2] > 0) & (Rover.ground_truth[:, :, 1] > 0)).nonzero()[0]))
    # 接下来找出有多少不对应地面实况像素
    bad_nav_pix = np.float(len(((plotmap[:, :, 2] > 0) & (Rover.ground_truth[:, :, 1] == 0)).nonzero()[0]))
    # 抓取地图像素的总数
    tot_map_pix = np.float(len((Rover.ground_truth[:, :, 1].nonzero()[0])))
    # 计算已成功找到的地面实况图的百分比
    perc_mapped = round(100 * good_nav_pix / tot_map_pix, 1)
    # 计算好的地图像素检测数除以总像素数
    # 发现是可导航的地形
    if tot_nav_pix > 0:
        fidelity = round(100 * good_nav_pix / (tot_nav_pix), 1)
    else:
        fidelity = 0
    # 翻转地图进行绘图,使y轴在显示屏中指向上方
    map_add = np.flipud(map_add).astype(np.float32)
    # 添加一些关于地图和岩石样本检测结果的文字
    cv2.putText(map_add, "Time: " + str(np.round(Rover.total_time, 1)) + ' s', (0, 10),
                cv2.FONT_HERSHEY_COMPLEX, 0.4, (255, 255, 255), 1)
    cv2.putText(map_add, "Mapped: " + str(perc_mapped) + '%', (0, 25),
                cv2.FONT_HERSHEY_COMPLEX, 0.4, (255, 255, 255), 1)
    cv2.putText(map_add, "Fidelity: " + str(fidelity) + '%', (0, 40),
                cv2.FONT_HERSHEY_COMPLEX, 0.4, (255, 255, 255), 1)
    cv2.putText(map_add, "Rocks", (0, 55),
                cv2.FONT_HERSHEY_COMPLEX, 0.4, (255, 255, 255), 1)
    cv2.putText(map_add, "  Located: " + str(samples_located), (0, 70),
                cv2.FONT_HERSHEY_COMPLEX, 0.4, (255, 255, 255), 1)
    cv2.putText(map_add, "  Collected: " + str(Rover.samples_collected), (0, 85),
                cv2.FONT_HERSHEY_COMPLEX, 0.4, (255, 255, 255), 1)
    # 将地图和视觉图像转换为base64字符串以便发送到服务器
    pil_img = Image.fromarray(map_add.astype(np.uint8))
    buff = BytesIO()
    pil_img.save(buff, format="JPEG")
    encoded_string1 = base64.b64encode(buff.getvalue()).decode("utf-8")

    pil_img = Image.fromarray(Rover.vision_image.astype(np.uint8))
    buff = BytesIO()
    pil_img.save(buff, format="JPEG")
    encoded_string2 = base64.b64encode(buff.getvalue()).decode("utf-8")

    return encoded_string1, encoded_string2

(4)主函数drive_rover.py

控制漫游者号运行的主程序,调用前面三个子程序完成漫游者号的自动驾驶功能。

其中有一些关于socketio服务器和Flask应用程序的语句,这里不详细说明了,可能的话我以后会单独写一篇文章来单独说明。如果有想要详细的,可以看这里。

其程序如下:

import argparse
import shutil
import base64
from datetime import datetime
import os
import cv2
import numpy as np
import socketio
import eventlet
import eventlet.wsgi
from PIL import Image
from flask import Flask
from io import BytesIO, StringIO
import json
import pickle
import matplotlib.image as mpimg
import time

# Import functions for perception and decision making
from perception import perception_step
from decision import decision_step
from supporting_functions import update_rover, create_output_images
# 初始化socketio服务器和Flask应用程序
# (learn more at: https://python-socketio.readthedocs.io/en/latest/)
sio = socketio.Server()
app = Flask(__name__)

# 读取地面实况图并创建绿色通道的图像
# 注意:图像默认以左上角为原点(0,0)y轴向下读取图片,要求图片为单层
ground_truth = mpimg.imread('E:/2019/RoboND-Rover-Project-master/calibration_images/map_bw.png')
ground_truth = ground_truth[:,:,0]  # 我的图片维度出了一些问题
# 下一行在红色和蓝色通道中创建零数组,并将地图放入绿色通道。
# 这就是地图输出在显示图像中显示为绿色的原因
ground_truth_3d = np.dstack((ground_truth*0, ground_truth*255, ground_truth*0)).astype(np.float)


# 定义RoverState()类
class RoverState():
    def __init__(self):
        self.start_time = None  # 记录导航开始的时间
        self.total_time = None  # 记录导航的总持续时间
        self.img = None  # 当前的相机图像
        self.pos = None  # 当前的位置 (x, y)
        self.yaw = None  # 当前的偏航角
        self.pitch = None  # 当前的俯仰角
        self.roll = None  # 当前的旋转角
        self.vel = None  # 当前的速度
        self.steer = 0  # 当前的转向角
        self.throttle = 0  # 当前的油门值
        self.brake = 0  # 当前的制动值
        self.nav_angles = None  # 可导航地形像素的角度
        self.nav_dists = None  # 可导航地形像素的距离
        self.ground_truth = ground_truth_3d  # 真实的世界地图
        self.mode = 'forward'  # 当前模式 (可以是前进或者停止)
        self.throttle_set = 0.2  # 加速时的节流设定
        self.brake_set = 10  # 刹车时的刹车设定
        # 下面的stop_forward和go_forward字段表示可导航地形像素的总数。
        # 这是一种非常粗糙的形式来表示漫游者号当何时可以继续前进或者何时应该停止。
        # 可以随意添加新字段或修改这些字段。
        self.stop_forward = 50  # T启动停止时的阈值
        self.go_forward = 500  # 再次前进的阈值
        self.max_vel = 2  # 最大速度 (m/s)
        # 感知步骤的图像输入
        # 更新此图像以显示中间分析步骤
        # 在自主模式下的屏幕上
        self.vision_image = np.zeros((160, 320, 3), dtype=np.float)
        # 世界地图
        # 使用可导航的位置更新此图像
        # 障碍物和岩石样本
        self.worldmap = np.zeros((200, 200, 3), dtype=np.float)
        self.samples_pos = None  # 存储实际样本的位置
        self.samples_to_find = 0  # 储存样本的初始计数
        self.samples_located = 0  # 储存位于地图上的样本数
        self.samples_collected = 0  # 储存收集的样本数
        self.near_sample = 0  # 设置附近样本数为0
        self.picking_up = 0  # 设置["picking_up"]值
        self.send_pickup = False

# 实例化类
Rover = RoverState()

# 用于跟踪每秒帧数的变量(FPS)
# 初始化帧计数器
frame_counter = 0
# 设置帧计数器
second_counter = time.time()
fps = None


# 定义遥测功能
@sio.on('telemetry')
def telemetry(sid, data):

    global frame_counter, second_counter, fps
    frame_counter += 1
    # 粗略计算每秒帧数(FPS)
    if (time.time() - second_counter) > 1:
        fps = frame_counter
        frame_counter = 0
        second_counter = time.time()
    print("Current FPS: {}".format(fps))

    if data:
        global Rover
        # 使用当前遥测技术初始化/更新漫游者号
        Rover, image = update_rover(Rover, data)

        if np.isfinite(Rover.vel):

            # 执行感知和决策步骤以更新漫游者号的状态
            Rover = perception_step(Rover)
            Rover = decision_step(Rover)

            # 创建输出图像以发送到服务器
            out_image_string1, out_image_string2 = create_output_images(Rover)

            # 行动步骤!,发送命令到漫游者号
 
            # 不要同时发送这两个,它们都会触发模拟器发送回新的遥测数据,
            # 因此我们只能将其中一个发送回当前的遥测数据。

            # 如果处于可拾取岩石的状态发送拾取命令
            if Rover.send_pickup and not Rover.picking_up:
                send_pickup()
                Rover.send_pickup = False
            else:
                # 发送命令到漫游者号
                commands = (Rover.throttle, Rover.brake, Rover.steer)
                send_control(commands, out_image_string1, out_image_string2)

        # 如果遥测无效,请发送空命令
        else:

            # 发送零点用于油门,制动和转向以及清空图像
            send_control((0, 0, 0), '', '')

        # 如果要从自动驾驶中保存摄像机图像,请指定路径
        # Example: $ python drive_rover.py image_folder_path
        # 如果指定了文件夹,则可以保存图像帧
        if args.image_folder != '':
            timestamp = datetime.utcnow().strftime('%Y_%m_%d_%H_%M_%S_%f')[:-3]
            image_filename = os.path.join(args.image_folder, timestamp)
            image.save('{}.jpg'.format(image_filename))

    else:
        sio.emit('manual', data={}, skip_sid=True)


@sio.on('connect')
def connect(sid, environ):
    print("connect ", sid)
    send_control((0, 0, 0), '', '')
    sample_data = {}
    sio.emit(
        "get_samples",
        sample_data,
        skip_sid=True)


def send_control(commands, image_string1, image_string2):
    # 定义要发送到漫游者号的命令
    data = {
        'throttle': commands[0].__str__(),
        'brake': commands[1].__str__(),
        'steering_angle': commands[2].__str__(),
        'inset_image1': image_string1,
        'inset_image2': image_string2,
        }
    # 通过socketIO服务器发送命令
    sio.emit(
        "data",
        data,
        skip_sid=True)
    eventlet.sleep(0)


# 定义一个发送“拾取”命令的功能
def send_pickup():
    print("Picking up")
    pickup = {}
    sio.emit(
        "pickup",
        pickup,
        skip_sid=True)
    eventlet.sleep(0)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Remote Driving')
    parser.add_argument(
        'image_folder',
        type=str,
        nargs='?',
        default='',
        help='Path to image folder. This is where the images from the run will be saved.'
    )
    args = parser.parse_args()
    
    # os.system('rm -rf IMG_stream/*')
    if args.image_folder != '':
        print("Creating image folder at {}".format(args.image_folder))
        if not os.path.exists(args.image_folder):
            os.makedirs(args.image_folder)
        else:
            shutil.rmtree(args.image_folder)
            os.makedirs(args.image_folder)
        print("Recording this run ...")
    else:
        print("NOT recording this run ...")
    
    # 用socketio的中间件包装Flask应用程序
    app = socketio.Middleware(sio, app)

    # 部署为eventlet WSGI服务器
    eventlet.wsgi.server(eventlet.listen(('', 4567)), app)

其输出如下:

在这里插入图片描述

gif演示:
在这里插入图片描述

因为图片大小限制的原因,gif的质量比较差。

但是我们可以看出,程序已经可以基本正常运行了。但是我们的自主驾驶还有很多问题,比如当我们正对着岩石障碍时,漫游者号会选择直冲向岩石。这是由我们取平均值的算法决定的,但是这完全是不可行的。

而且漫游者号现在还处于一个随机驾驶状态,它只能在地图中进行随机的驾驶,无法遍历整个地图。这也是我们的程序需要升级的地方。

在下个笔记中,要实现漫游者号对地图进行稳定的遍历,而且要对所有岩石样本进行采集。然后我们才算完成了这个漫游者号火星车项目的所有部分。