Настойка Duckietown Gym и первый запуск.
Эмулятор робота duckietown gym — среда тестирования робота. Настойка Duckietown Gym — это ключевой момент перед обучением робота.
Одна из проблем мобильной робототехники — это корректное поведение устройства в дорожной среде. Робот при движении в пространстве опирается на показания датчиков и камер. Для этих целей применяют знания по компьютерному зрению и машинному обучению. Данный подход требует много времени на обучения робота и поиск оптимального алгоритма.
Для решения такой проблемы сотрудники MIT разработали платформу Duckietown. Здесь мы познакомимся с эмулятором робота Duckietown gym, который можно дословно перевести как «Уткоград».
Статья разбита на несколько шагов для удобства работы:
- настройка duckietown gym.
- знакомимся и анализируем базовый код по управлению роботом в эмуляторе.
- учимся управлять роботом.
- изучаем что такое карты и как они создаются.
- создаём собственную карту местности.
Настройка
Перейдём к настройке эмулятора робота duckietown gym
Для начала, необходимо перейти в корневой католог gym-duckietown.
Для этого нам необходимо открыть Терминал, через сочетание CTRL + ALT + T (или открыть через меню).
Затем, с помощью команды cd перейти в нужный каталог. Пример ниже.
cd ~/Documents/gym-duckietown/
Анализ базового кода
В данном разделе мы познакомимся с основной структурой кода, позволяющего работать в среде тренировочного зала уткограда и напишем свой шаблон для последующей работы с ним
Задание №1 “Первый запуск”
Используя базовое приложение с пользовательским интерфейсом познакомимся с уткоградом. Для этого запустим программу manual_control.py .
python3 ./manual_control.py --env-name Duckietown-udem1-v0
После этого откроется окно эмулятора, где вы сможете осуществить ручное управление вашим виртуальным роботом.
Виртуальное окружение
Теперь открываем код manual_control.py и начинаем его припарировать:
#!/usr/bin/env python
# manual
"""
This script allows you to manually control the simulator or Duckiebot
using the keyboard arrows.
"""
from PIL import Image
import argparse
import sys
import gym
import numpy as np
import pyglet
from pyglet.window import key
from gym_duckietown.envs import DuckietownEnv
# from experiments.utils import save_img
parser = argparse.ArgumentParser()
parser.add_argument("--env-name", default=None)
parser.add_argument("--map-name", default="udem1")
parser.add_argument("--distortion", default=False, action="store_true")
parser.add_argument("--camera_rand", default=False, action="store_true")
parser.add_argument("--draw-curve", action="store_true", help="draw the lane following curve")
parser.add_argument("--draw-bbox", action="store_true", help="draw collision detection bounding boxes")
parser.add_argument("--domain-rand", action="store_true", help="enable domain randomization")
parser.add_argument("--dynamics_rand", action="store_true", help="enable dynamics randomization")
parser.add_argument("--frame-skip", default=1, type=int, help="number of frames to skip")
parser.add_argument("--seed", default=1, type=int, help="seed")
args = parser.parse_args()
if args.env_name and args.env_name.find("Duckietown") != -1:
env = DuckietownEnv(
seed=args.seed,
map_name=args.map_name,
draw_curve=args.draw_curve,
draw_bbox=args.draw_bbox,
domain_rand=args.domain_rand,
frame_skip=args.frame_skip,
distortion=args.distortion,
camera_rand=args.camera_rand,
dynamics_rand=args.dynamics_rand,
)
else:
env = gym.make(args.env_name)
env.reset()
env.render()
Нас интересует 21 строка кода. Видим, что все начинается с парсера, который считывает аргументы, которые мы записываем после вызываемой программы. Эти аргументы передаются в объект env типа DuckietownEnv — виртуальное окружение тренировочного зала.
Избавимся от парсера, закомментировав данные строки или удалив их. Оставим только это:
#!/usr/bin/env python
# manual
"""
This script allows you to manually control the simulator or Duckiebot
using the keyboard arrows.
"""
from PIL import Image
import argparse
import sys
import gym
import numpy as np
import pyglet
from pyglet.window import key
from gym_duckietown.envs import DuckietownEnv
# from experiments.utils import save_img
env = DuckietownEnv(
seed=args.seed,
map_name=args.map_name,
draw_curve=args.draw_curve,
draw_bbox=args.draw_bbox,
domain_rand=args.domain_rand,
frame_skip=args.frame_skip,
distortion=args.distortion,
camera_rand=args.camera_rand,
dynamics_rand=args.dynamics_rand,
)
env.reset()
env.render()
Теперь запустим и проверим работоспособность кода.
python3 ./manual_control.py
Пройдемся по аргументам создаваемого пространства уткограда (DuckietownEnv ) :
Базовые:
- seed — сид, зерно (и т.п. названия) — целое число которое, задает начальное положение (псевдослучайным образом — т.е. каждое число будет давать один и тот же результат, но заранее неизвестно, если ранее не запускалось с таким числом), в случае когда не задается, начальное положение выбирается случайным образом (int)
- map_name — наименование, открываемой карты (name.yaml)
- draw_curve — происходит отрисовка линии следования (True/False)
- draw_bbox — происходит отрисовка границ, пересечение которых вызывает столкновение (True/False) // камера переходит в режим: вид сверху
domain_rand — разрешение областной рандомизации на карте (True/False)
dynamics_rand — если включен, добавляет динамическую тряску (True/False)
Положение:
- param_user_tile_start— устанавливает начальный тайл, в котором появляется дакибот ((int,int))
// только не выбирайте тайлы, в которых бота не должно быть, т.к. тогда программа уходит во внутренний спор
- accept_start_angle_deg— устанавливает угол допустимый в полосе движения для стартовой позиции (float), по умолчанию = 60
Настройка камеры: (по базовым установкам камера имеет разрешение 640 на 480)
- camera_rand — если включен, добавляется рандомная часть ошибок калибровки камеры (True/False)
- frame_rate — частота кадров (int), по умолчанию = 30 кадрам в секунду
- frame_skip — пропускаемые кадры (int)
- distortion — искривление изображения, если правде, то искривляет аппроксимацией рыбий глаз (True/False)
- camera_width — ширина камеры (int)
- camera_height — высота камеры (int)
сравнение базовой настройки 640 x 480 с 400 x 300
Со знакомством и настройкой окружения закончили.
Смотрим внутрь
Возвращаясь к основному коду, видим, что далее по тексту у нас происходит сброс (reset()) для введенного окружения (env), в котором происходит запуск с первичными настройками, после чего начинается “жизнь” в виртуальном окружении.
import gym
import numpy as np
import pyglet
from pyglet.window import key
from gym_duckietown.envs import DuckietownEnv
# from experiments.utils import save_img
env = DuckietownEnv(
seed=args.seed,
map_name=args.map_name,
draw_curve=args.draw_curve,
draw_bbox=args.draw_bbox,
domain_rand=args.domain_rand,
frame_skip=args.frame_skip,
distortion=args.distortion,
camera_rand=args.camera_rand,
dynamics_rand=args.dynamics_rand,
)
env.reset()
env.render()
Для наблюдения того, что происходит в “виртуальности” начинается рендеринг (render( mode)) возможны несколько вариантов отображения:
- “top_down” — вид на всю карту сверху вниз
“human” — отображение “для человека”, вид через камеру на дакиботе (базовое значение)
- “free_cam” — аналогично “human”, но без дополнительной информации о положении и параметрах движении
Управление дакиботом
Снова возвращаемся к тексту программы manual_control.py по тексту программы у нас идет функция, вызываемая при нажатии клавиш on_key_press
@env.unwrapped.window.event
def on_key_press(symbol, modifiers):
"""
This handler processes keyboard commands that
control the simulation
"""
if symbol == key.BACKSPACE or symbol == key.SLASH:
print("RESET")
env.reset()
env.render()
elif symbol == key.PAGEUP:
env.unwrapped.cam_angle[0] = 0
elif symbol == key.ESCAPE:
env.close()
sys.exit(0)
# Take a screenshot
# UNCOMMENT IF NEEDED - Skimage dependency
# elif symbol == key.RETURN:
# print('saving screenshot')
# img = env.render('rgb_array')
# save_img('screenshot.png', img)
# Register a keyboard handler
key_handler = key.KeyStateHandler()
env.unwrapped.window.push_handlers(key_handler)
Дает возможность сделать перезагрузку (reset()) карты, установить угол камеры, закрыть приложение и сделать скрин (если раскомментировать участок кода)
Полезно, но не интересно и зачастую не используется, можно упростить. Удаляем и идем дальше…, а дальше у нас участок, отвечающий за управление дакиботом (update):
def update(dt):
"""
This function is called at every frame to handle
movement/stepping and redrawing
"""
wheel_distance = 0.102
min_rad = 0.08
action = np.array([0.0, 0.0])
if key_handler[key.UP]:
action += np.array([0.44, 0.0])
if key_handler[key.DOWN]:
action -= np.array([0.44, 0])
if key_handler[key.LEFT]:
action += np.array([0, 1])
if key_handler[key.RIGHT]:
action -= np.array([0, 1])
if key_handler[key.SPACE]:
action = np.array([0, 0])
v1 = action[0]
v2 = action[1]
# Limit radius of curvature
if v1 == 0 or abs(v2 / v1) > (min_rad + wheel_distance / 2.0) / (min_rad - wheel_distance / 2.0):
# adjust velocities evenly such that condition is fulfilled
delta_v = (v2 - v1) / 2 - wheel_distance / (4 * min_rad) * (v1 + v2)
v1 += delta_v
v2 -= delta_v
action[0] = v1
action[1] = v2
# Speed boost
if key_handler[key.LSHIFT]:
action *= 1.5
Который вызывается каждый раз по таймеру:
pyglet.clock.schedule_interval(update, 1.0 / env.unwrapped.frame_rate)
Сами нажатия клавиш обрабатываются при помощи строк:
# Register a keyboard handler
key_handler = key.KeyStateHandler()
env.unwrapped.window.push_handlers(key_handler)
Мы реализуем вызов обновления каждого кадра через цикл типа:
While True:
update(0)
Задание №2 “Переделываем вызов обновления”
Добавим в функцию обновления (update) строчку, отвечающую за закрытие приложения:
def update(dt):
"""
This function is called at every frame to handle
movement/stepping and redrawing
"""
wheel_distance = 0.102
min_rad = 0.08
action = np.array([0.0, 0.0])
if key_handler[key.UP]:
action += np.array([0.44, 0.0])
if key_handler[key.DOWN]:
action -= np.array([0.44, 0])
if key_handler[key.LEFT]:
action += np.array([0, 1])
if key_handler[key.RIGHT]:
action -= np.array([0, 1])
if key_handler[key.SPACE]:
action = np.array([0, 0])
if key_handler[key.ESCAPE]:
env.close()
sys.exit(0)
Теперь можно закрыть окно нажатием клавиши ESCAPE. Проверяем работу программы .
Как можно видеть, внутри функции update, происходит работа с переменной action, представляющий из себя массив из одной строчки и двух столбцов, элементы меняют скорость и направление движения дакибота.
В зависимости от нажатых клавиш задаются параметры скорости для ведущих колёс дакибота:
obs, reward, done, info = env.step(action)
Далее, данные обрабатываются и отправляются на рендер кадра.
obs, reward, done, info = env.step(action)
print("step_count = %s, reward=%.3f" % (env.unwrapped.step_count, reward))
if key_handler[key.RETURN]:
im = Image.fromarray(obs)
im.save("screen.png")
if done:
print("done!")
env.reset()
env.render()
env.render()
После объявления функции, добавляем цикл:
While True:
update(0)
В итоге у нас есть функция в таком виде.
def update(dt):
"""
This function is called at every frame to handle
movement/stepping and redrawing
"""
wheel_distance = 0.102
min_rad = 0.08
action = np.array([0.0, 0.0])
if key_handler[key.UP]:
action += np.array([0.44, 0.0])
if key_handler[key.DOWN]:
action -= np.array([0.44, 0])
if key_handler[key.LEFT]:
action += np.array([0, 1])
if key_handler[key.RIGHT]:
action -= np.array([0, 1])
if key_handler[key.SPACE]:
action = np.array([0, 0])
if key_handler[key.ESCAPE]:
env.close()
sys.exit(0)
v1 = action[0]
v2 = action[1]
# Limit radius of curvature
if v1 == 0 or abs(v2 / v1) > (min_rad + wheel_distance / 2.0) / (min_rad - wheel_distance / 2.0):
# adjust velocities evenly such that condition is fulfilled
delta_v = (v2 - v1) / 2 - wheel_distance / (4 * min_rad) * (v1 + v2)
v1 += delta_v
v2 -= delta_v
action[0] = v1
action[1] = v2
obs, reward, done, info = env.step(action)
print("step_count = %s, reward=%.3f" % (env.unwrapped.step_count, reward))
env.render(mode)
while True:
update(0)
Запускаем, проверяем работоспособность нашего кода.
В итоге любая программа будет в общем виде иметь вид:
- импорты нужных библиотек
- создание среды, ее сброс и рендер
- создание функции для управления дакиботом
- цикл с функцией управления дакиботом
Карта
Для тренировки нашего робота используют разнообразные карты дорог. Существует несколько карт, встроенных в эмулятор уткограда. Для этого достаточно перейти в папку, где расположены карты:
DEBUG:gym-duckietown:loading map file "/home/art/.local/lib/python3.8/site-packages/duckietown_world/data/gbl/maps/udem1.yaml"
Ниже представлены варианты карт в данном каталоге.
Пропишим новое имя карты в конструкторе среды для её загрузки.
В папке содержаться карты:
- 4way
- 4way_bordered
- calibration_map_ext
- calibration_map_int
- ETH_intersection_map
- ETH_large_loop
- ETH_small_loop_1
- ETH_small_loop_2
- ETH_small_loop_2_bordered
- ETH_small_loop_3
- ETHZ_autolab_fast_track
- ETHZ_autolab_technical_track
- ETHZ_autolab_technical_track_bordered
- ETHZ_loop
- ETHZ_loop_bordered
- loop_dyn_duckiebots
- loop_empty
- loop_obstacles
- loop_pedestrains
- Montreal_loop
- regress_4way_adam
все имеют формат yaml — Yet Another Markup Language,
Рассмотрим, что можно задать. В первую очередь следует указать, что карта строится из квадратных тайлов,имеющих размеры:
title_size:0.585
Тайлы располагаются в таблице, с которой связана координатная сетка, чей ноль располагается в верхнем левом углу:
Все тайлы задаются после ключевого слова tiles как набор массивов, где идет перечисление типов тайлов через запятую, выделяя строку, заключая тайлы в квадратные скобки:
titles:
- [floor, floor, floor, floor, floor, floor, floor, floor]
- [floor, floor, straight/W, floor, straight/W, straight/W, left/Ncurve, floor]
Доступны следующий тайлы:
- empty
- straight
- curve_left
- curve_right
- 3way_left
- 3way_right
- 4way
- asphalt
- grass
- floor
Каждый из указанных тайлов может быть направлен относительно поля, при помощи указания какая сторона тайла смотрит в верхнюю часть карты:
На карту можно установить объекты. Например такой:
objects:
-kind: tree
pos:[2.5, 4.5]
rotate: 180
height: 0.25
Возможно добавить следующие объекты:
- barrier cone
- duckie
- duckiebot
- tree
- house
- truck
- bus building
- sign_stop
- sign_T_intersect
- sign_yield
Теперь рассмотрим самое главное, что есть в настройках карты — это возможность установки места появления дакибота, в начале симуляции:
start_pose[[0.4, 0, 0.4], 0]
Где первые три числа списка обозначают положение в соответствии с изначально приведенной координатной сеткой (в условии, что в установке среды начальный тайл установлен как (0,0)), а четвёртое число — угол поворота в радианах.
Напишем код окружения для карты.
title:
-[curve_left/W , floor, floor , floor, curve_left/N]
-[straight/S , floor , straight/N , floor , straight/N]
-[3way_left/S , floor , 4way , floor , 3way_left/N]
-[straight/S, floor , straight/S , asphalt , straight/N]
-[curve_left/S , floor , 3way_left/E , floor , curve_left/E]
start_pose[[0.4, 0, 0.4], 1]
objects:
trafficlight:
kind: trafficlight
pos: [0.18,-0.18]
rotate: 135
height: 0.4
optional: true
title_size: 0.585
Результатом станет такая карта:
Итог
Мы познакомились со средой Duckietown Gym и основными функциями эмулятора. Научились создавать карты и управлять виртуальным роботом.
Вопросы.
- опишите архитектуру программы.
- какие параметры управления в симуляции присутствуют?
- как строится карта?
П