Поиск вертолетных площадок¶
Описание проекта¶
Данный проект предназначен для ознакомления с бесплатным программным обеспечением LobeAI, позволяющим с помощью простого графического интерфейса натренировать собственную нейронную сеть. Нейронная сеть работает в режиме классификатора, это значит, что она может лишь определять, есть ли на изображении объект того или иного класса, или его нет, положение на изображении не определяется.
Проект доступен на GitHub
Примечание
При клонировании проекта и создании знака, непохожего на показанный в видео, у вас может нестабильно работать определение вертолетных площадок. Это связано с тем, что нейросеть натренирована на представленном выше месте, поэтому другие знаки она может определять хуже. Если вы столкнулись с некорректной работой нейросети, то проведите процесс дополнительного обучения при тестировании, о коотором пойдет речь далее.
Установка компонентов¶
Важно
Не рекомендуется использовать последнюю прошивку автопилота (1.6.7747) так как она замедляет скорость выполнения команд квадрокоптером. Для нормальнйо работы программы установите прошивку версии 1.6.7482.
Для работы проекта нам понадобится установить некоторые библиотеки и программы.
Для установки всех необходимых библиотек в папке проекта есть файл requirements.txt, в котором есть список названий. Запустите команду из командной строки, находясь в корневой папке проекта:
pip install -r requirements.txt
Примечание
Чтобы быстро открыть командную строку, перейдите в проводнике в нужную папку и впишите «cmd» (без кавычек) в строку с текущим путём.
Для установки программы Lobe переходим на их официальный сайт и нажимаем кнопку Download, после чего устанавливаем как самую обычную программу.
Этапы разработки¶
Начало¶
Для того, чтобы натренировать нейронную сеть, необходимо сначала получить тренировочные двнные (датасет). Сделать это можно двумя способами:
скачать готовый датасет,
скачать картинки из интернета и вручную отсортировать по классам,
сделать свои фотографии.
Поиск датасетов в интернете¶
На данный момент сообщество, работающее с нейросетями, развивается очень стремительно, и отчасти это связано с тем, что все больше наборов данных появляются в сети в свободном доступе.
Для поиска датасетов существует сайт Kaggle, на котором помимо форума, примеров программ, соревнований, есть как раз множество открытых датасетов.
Самостоятельное создание датасета¶
Датасета с фотографиями вертолетных площадок на просторах интернета найти не удалось, поэтому попробуем его создать собственноручно. Для этого напишем небольшую прогруммку, которую в дальнейшем можно будет использовать и для тестирования проекта.
Для проверки корректности установки всех библиотек, связанных с работой с дроном, можно запустить файл pioneer_sdk/examples/camera_stream.py
При подключении к коптеру и запуске программы у вас появится окно с изображением с камеры Пионера:
Теперь можно усовершенствовать нашу программу таким образом, чтобы она сохраняла в определенный каталог фотографии по нажатии на кнопки (f, b - переключение классов; a - добавить фотографию в класс):
import pioneer_sdk
import cv2
import numpy as np
import os
pioneer = pioneer_sdk.Pioneer()
classes = ('NoPlace', 'Place')
indexes = []
cur_class = 0
for cls in classes:
if f'Class_{cls}' not in os.listdir():
os.mkdir(f'Class_{cls}')
indexes.append(len(os.listdir(path=f'./Class_{cls}')))
while True:
raw = pioneer.get_raw_video_frame()
frame = cv2.imdecode(np.frombuffer(raw, dtype=np.uint8), cv2.IMREAD_COLOR)
k = cv2.waitKey(1)
if k == ord('q'):
break
if k == ord('f') and cur_class < len(classes)-1:
cur_class += 1
if k == ord('b') and cur_class > 0:
cur_class -= 1
if k == ord('a'):
indexes[cur_class] += 1
cv2.imwrite(f'./Class_{classes[cur_class]}/{classes[cur_class]}_{indexes[cur_class]}.png', frame)
print(f'Image added to class {classes[cur_class]}!!!')
cv2.putText(frame, f'Current class is {classes[cur_class]}', (20, 450), cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.5,
color=(0, 0, 255))
cv2.putText(frame, f'Images in class: {indexes[cur_class]}', (20, 470), cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.5,
color=(0, 0, 255))
cv2.imshow("Frame", frame)
cv2.destroyAllWindows()
Фотографии сохраняются в папках с названием, соответствующих названиям классов: Class_<ИмяКласса>, имена фотографий также соответсвуют названию классов + порядковый номер: <ИмяКласса>_<Номер>.png
Для обучения в программе Lobe требуется минимум 5 фотографий на каждый класс, при этом классов может быть минимум 2. Однако на таком небольшом объеме данных нейросеть не может обучиться хорошо, поэтому следует на каждый класс делать не менее 20 фотографий в разных условиях.
Примечание
Например, в нашем случае фотографии места посадки стоит делать на разной высоте, с разным углом поворота и углом обзора.
Загрузка датасета в Lobe¶
Для загрузки наших созданных папок с фотографиями в Lobe необходимо провести следующие операции:
Открываем Lobe и нажимаем Import в правом верхнем углу:
Появляется 3 режима импорта, выбираем Dataset:
Заходим в рабочую директорию проекта и выбираем папку с изображениями первого класса:
При импорте вы можете изменить название класса, или оставить его таким же, как название папки. Оставляем без изменений:
Класс изображений импортировался. Проводим аналогичную операцию с оставшимися классами; нажимаем Import:
Выбираем Dataset:
Как и в прошлый раз выбираем папку с фотографиями нужного класса
Слева в разделе Training появится круговая диаграмма прогресса обучения модели. Немного подождите, пока нейросеть обучится, после чего можете перейти на вкладку Camera для тестирования:
Примечание
Для работы данной функции ван нужна вебкамера.
Перед вами откроется окно с изображением с вашей камеры, по которому нейросеть будет пытаться предсказывать класс объекта. Слева у вас показывается предсказанный класс и степень «уверенности» нейросети в предсказании (заполненность полоски). Справа расположены кнопки, которые позволяют сказать нейросети, правильно ли она сделала предсказание. Такое решение позволяет прямо во время тестирования улучшать работу нейросети.
После обучения модели перейдите в раздел Use чтобы провести валидацию модели - загрузить дополнительные снимки с коптера и проверить работу обученной модели
Важно
Валидационные изображения не должны быть взяты из набора, на котором модель обучалась
После получения удовлетворительных результатов нам нужно экспортировать обученную нейросеть для использования в своих программах. Для этого в левом меню нажимаем на Export:
Выбираем TensorFlow (красная кнопка):
Выбираем место, куда экспортируется модель:
Соглашаемся на оптимизацию:
Через некоторое время получаем папку с натренированной моделью, готовой к использованию:
Исходный код¶
Интеграция с программой управления квадрокоптером¶
import pioneer_sdk
import cv2
import numpy as np
from lobe import ImageModel
from PIL import Image
pioneer = pioneer_sdk.Pioneer()
model = ImageModel.load('./Helicopter_Place_TensorFlow')
command_x = float(0)
command_y = float(0)
command_z = float(1)
command_yaw = np.radians(float(0))
increment_xy = float(0.2)
increment_z = float(0.1)
increment_deg = np.radians(float(10))
new_command = False
leds_sent = False
old_prediction = None
def local_to_global(dx, dy):
dx_t = dx*np.cos(command_yaw) - dy*np.sin(command_yaw)
dy_t = dx*np.sin(command_yaw) + dy*np.cos(command_yaw)
return dx_t, dy_t
while True:
raw = pioneer.get_raw_video_frame()
frame = cv2.imdecode(np.frombuffer(raw, dtype=np.uint8), cv2.IMREAD_COLOR)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
model_frame = Image.fromarray(frame_rgb)
predictions = model.predict(model_frame)
key = cv2.waitKey(1)
if key == 32:
print('space pressed')
pioneer.arm()
print('point')
pioneer.takeoff()
if key == 27: # esc
print('esc pressed')
pioneer.land()
if key == 27: # esc
print('esc pressed')
cv2.destroyAllWindows()
pioneer.land()
break
elif key == ord('w'):
print('w')
dx, dy = local_to_global(0, increment_xy)
command_x += dx
command_y += dy
new_command = True
elif key == ord('s'):
print('s')
dx, dy = local_to_global(0, -increment_xy)
command_x += dx
command_y += dy
new_command = True
elif key == ord('a'):
print('a')
dx, dy = local_to_global(-increment_xy, 0)
command_x += dx
command_y += dy
new_command = True
elif key == ord('d'):
print('d')
dx, dy = local_to_global(increment_xy, 0)
command_x += dx
command_y += dy
new_command = True
elif key == ord('q'):
print('q')
command_yaw += increment_deg
new_command = True
elif key == ord('e'):
print('e')
command_yaw -= increment_deg
new_command = True
elif key == ord('h'):
print('h')
command_z += increment_z
new_command = True
elif key == ord('l'):
print('l')
command_z -= increment_z
new_command = True
elif key == ord('k'):
break
if new_command:
pioneer.go_to_local_point(x=command_x, y=command_y, z=command_z, yaw=command_yaw)
new_command = False
cv2.putText(frame, f'Predicted class is {predictions.prediction}', (20, 450), cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.5, color=(0, 0, 255))
if predictions.prediction == 'Class_Place' and not leds_sent:
pioneer.led_control(0, 255, 0, 0)
pioneer.led_control(1, 255, 0, 0)
pioneer.led_control(2, 255, 0, 0)
pioneer.led_control(3, 255, 0, 0)
leds_sent = True
elif predictions.prediction == 'Class_NoPlace' and not leds_sent:
pioneer.led_control(0, 0, 0, 0)
pioneer.led_control(1, 0, 0, 0)
pioneer.led_control(2, 0, 0, 0)
pioneer.led_control(3, 0, 0, 0)
leds_sent = True
if predictions.prediction != old_prediction:
leds_sent = False
old_prediction = predictions.prediction
cv2.imshow("Frame", frame)
cv2.destroyAllWindows()
Данный код позволяет управлять квадрокоптером Геоскан Пионер Мини с помощью ноутбука клавишами WASD, а также с помощью обученной нейросети квадрокоптер будет автоматически включать сигнальные светодиоды при нахождении над предполагаемым местом посадки.
Разбор кода¶
Давайте разобъем код на блоки и рассмотрим их подробнее, чтобы лучше понять, как программа работает.
В строке 7 загружается модель. Проверьте, что название модели, экспортированной из Lobe, совпадает с названием в коде.
Тут задаются переменные для перемещений квадрокоптера, то есть указывается расстояние, на котороое коптер должен перемещаться при одном нажатии на кнопку:
command_x = float(0)
command_y = float(0)
command_z = float(1)
command_yaw = np.radians(float(0))
increment_xy = float(0.2)
increment_z = float(0.1)
increment_deg = np.radians(float(10))
На строках 19-22 объявляются вспомогательные переменные:
new_command - если True, коптеру необходимо лететь в новую точку;
leds_sent - если True, коптеру необходимо изменить состояние светодиодов. Переменная нужна для единоразовой отправки таковой команды, чтобы не слать значение светодиодов постоянно;
old_prediction - хранит значение предсказания нейросети с предыдущей итерации цикла. Служит в тех же целях, что и leds_sent.
new_command = False
leds_sent = False
old_prediction = None
На строках 24-27 реализованна функция учёта угла курса при подсчёте смещения по заданной команде с клавиатуры:
def local_to_global(dx, dy):
dx_t = dx*np.cos(command_yaw) - dy*np.sin(command_yaw)
dy_t = dx*np.sin(command_yaw) + dy*np.cos(command_yaw)
return dx_t, dy_t
Внутри бесконечного цикла на строках 30-36 происходит:
считывание сырого изображения с камеры квадрокоптера (30),
преобразование его в понятный OpenCV формат (31),
преобразование изображения из цветового пространства BGR (Blue Green Red), используемого в OpenCV в пространство RGB (Red Green Blue), используемого нейросетью (33)
а также преобразование RGB-зображения в формат PIL (библиотека для работы с изображениями) (34)
и наконец получение предсказания модели (36)
dx_t = dx*np.cos(command_yaw) - dy*np.sin(command_yaw)
dy_t = dx*np.sin(command_yaw) + dy*np.cos(command_yaw)
return dx_t, dy_t
while True:
raw = pioneer.get_raw_video_frame()
frame = cv2.imdecode(np.frombuffer(raw, dtype=np.uint8), cv2.IMREAD_COLOR)
На строках 33-93 происходит считывание и обработка нажатий клавиш клавиатуры, а также отправка команды квадрокоптеру на движение в новую точку:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
model_frame = Image.fromarray(frame_rgb)
predictions = model.predict(model_frame)
key = cv2.waitKey(1)
if key == 32:
print('space pressed')
pioneer.arm()
print('point')
pioneer.takeoff()
if key == 27: # esc
print('esc pressed')
pioneer.land()
if key == 27: # esc
print('esc pressed')
cv2.destroyAllWindows()
pioneer.land()
break
elif key == ord('w'):
print('w')
dx, dy = local_to_global(0, increment_xy)
command_x += dx
command_y += dy
new_command = True
elif key == ord('s'):
print('s')
dx, dy = local_to_global(0, -increment_xy)
command_x += dx
command_y += dy
new_command = True
elif key == ord('a'):
print('a')
dx, dy = local_to_global(-increment_xy, 0)
command_x += dx
command_y += dy
new_command = True
elif key == ord('d'):
print('d')
dx, dy = local_to_global(increment_xy, 0)
command_x += dx
command_y += dy
new_command = True
elif key == ord('q'):
print('q')
command_yaw += increment_deg
new_command = True
elif key == ord('e'):
print('e')
command_yaw -= increment_deg
new_command = True
elif key == ord('h'):
print('h')
command_z += increment_z
new_command = True
elif key == ord('l'):
print('l')
command_z -= increment_z
new_command = True
elif key == ord('k'):
break
На строке 99-100 располагается отрисовка текста ка показываемом на экране компьютера изображении, в котором содержится наименование предсказанного нейросетью класса.
cv2.putText(frame, f'Predicted class is {predictions.prediction}', (20, 450), cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.5, color=(0, 0, 255))
На строках 102-113 происходит установка значений светодиодов на коптере. Логика у условий уследующая: проверяется имя предсказанного класса и проверяется, было ли установлено значение на светодиоды (leds_sent будет False при смене предсказаний нейросети). Если условия выполняются, то на светодиоды устанавливаются новые значения, а переменная leds_sent принимает значение True. Далее происходит проверка, различаются ли предсказания на текущей и предыдущей итерациях. Если различаются, то это значит, что необходимо отправлять на светодиоды новые значения, то есть переменная leds_sent становится False.
if predictions.prediction == 'Class_Place' and not leds_sent:
pioneer.led_control(0, 255, 0, 0)
pioneer.led_control(1, 255, 0, 0)
pioneer.led_control(2, 255, 0, 0)
pioneer.led_control(3, 255, 0, 0)
leds_sent = True
elif predictions.prediction == 'Class_NoPlace' and not leds_sent:
pioneer.led_control(0, 0, 0, 0)
pioneer.led_control(1, 0, 0, 0)
pioneer.led_control(2, 0, 0, 0)
pioneer.led_control(3, 0, 0, 0)
leds_sent = True
Последними строками идут отображение вида с камеры коптера на экране ноутбука (120) и закрытие всех окон после выхода из цикла (122)