Поиск вертолетных площадок

Описание проекта

Данный проект предназначен для ознакомления с бесплатным программным обеспечением LobeAI, позволяющим с помощью простого графического интерфейса натренировать собственную нейронную сеть. Нейронная сеть работает в режиме классификатора, это значит, что она может лишь определять, есть ли на изображении объект того или иного класса, или его нет, положение на изображении не определяется.

image0

Проект доступен на GitHub

Примечание

При клонировании проекта и создании знака, непохожего на показанный в видео, у вас может нестабильно работать определение вертолетных площадок. Это связано с тем, что нейросеть натренирована на представленном выше месте, поэтому другие знаки она может определять хуже. Если вы столкнулись с некорректной работой нейросети, то проведите процесс дополнительного обучения при тестировании, о коотором пойдет речь далее.

Установка компонентов

Важно

Не рекомендуется использовать последнюю прошивку автопилота (1.6.7747) так как она замедляет скорость выполнения команд квадрокоптером. Для нормальнйо работы программы установите прошивку версии 1.6.7482.

Для работы проекта нам понадобится установить некоторые библиотеки и программы.

Для установки всех необходимых библиотек в папке проекта есть файл requirements.txt, в котором есть список названий. Запустите команду из командной строки, находясь в корневой папке проекта:

pip install -r requirements.txt

Примечание

Чтобы быстро открыть командную строку, перейдите в проводнике в нужную папку и впишите «cmd» (без кавычек) в строку с текущим путём.

Для установки программы Lobe переходим на их официальный сайт и нажимаем кнопку Download, после чего устанавливаем как самую обычную программу.

image15

Этапы разработки

Начало

Для того, чтобы натренировать нейронную сеть, необходимо сначала получить тренировочные двнные (датасет). Сделать это можно двумя способами:

  • скачать готовый датасет,

  • скачать картинки из интернета и вручную отсортировать по классам,

  • сделать свои фотографии.

Поиск датасетов в интернете

На данный момент сообщество, работающее с нейросетями, развивается очень стремительно, и отчасти это связано с тем, что все больше наборов данных появляются в сети в свободном доступе.

Для поиска датасетов существует сайт Kaggle, на котором помимо форума, примеров программ, соревнований, есть как раз множество открытых датасетов.

Самостоятельное создание датасета

Датасета с фотографиями вертолетных площадок на просторах интернета найти не удалось, поэтому попробуем его создать собственноручно. Для этого напишем небольшую прогруммку, которую в дальнейшем можно будет использовать и для тестирования проекта.

Для проверки корректности установки всех библиотек, связанных с работой с дроном, можно запустить файл pioneer_sdk/examples/camera_stream.py

При подключении к коптеру и запуске программы у вас появится окно с изображением с камеры Пионера:

image

Теперь можно усовершенствовать нашу программу таким образом, чтобы она сохраняла в определенный каталог фотографии по нажатии на кнопки (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

image2

Для обучения в программе Lobe требуется минимум 5 фотографий на каждый класс, при этом классов может быть минимум 2. Однако на таком небольшом объеме данных нейросеть не может обучиться хорошо, поэтому следует на каждый класс делать не менее 20 фотографий в разных условиях.

Примечание

Например, в нашем случае фотографии места посадки стоит делать на разной высоте, с разным углом поворота и углом обзора.

Загрузка датасета в Lobe

Для загрузки наших созданных папок с фотографиями в Lobe необходимо провести следующие операции:

  1. Открываем Lobe и нажимаем Import в правом верхнем углу:

    image3

  2. Появляется 3 режима импорта, выбираем Dataset:

    image4

  3. Заходим в рабочую директорию проекта и выбираем папку с изображениями первого класса:

    image5

  4. При импорте вы можете изменить название класса, или оставить его таким же, как название папки. Оставляем без изменений:

    image6

  5. Класс изображений импортировался. Проводим аналогичную операцию с оставшимися классами; нажимаем Import:

    image7

  6. Выбираем Dataset:

    image8

  7. Как и в прошлый раз выбираем папку с фотографиями нужного класса

  8. Слева в разделе Training появится круговая диаграмма прогресса обучения модели. Немного подождите, пока нейросеть обучится, после чего можете перейти на вкладку Camera для тестирования:

    image9

    Примечание

    Для работы данной функции ван нужна вебкамера.

    Перед вами откроется окно с изображением с вашей камеры, по которому нейросеть будет пытаться предсказывать класс объекта. Слева у вас показывается предсказанный класс и степень «уверенности» нейросети в предсказании (заполненность полоски). Справа расположены кнопки, которые позволяют сказать нейросети, правильно ли она сделала предсказание. Такое решение позволяет прямо во время тестирования улучшать работу нейросети.

  9. После обучения модели перейдите в раздел Use чтобы провести валидацию модели - загрузить дополнительные снимки с коптера и проверить работу обученной модели

    Важно

    Валидационные изображения не должны быть взяты из набора, на котором модель обучалась

  10. После получения удовлетворительных результатов нам нужно экспортировать обученную нейросеть для использования в своих программах. Для этого в левом меню нажимаем на Export:

    image10

  11. Выбираем TensorFlow (красная кнопка):

    image11

  12. Выбираем место, куда экспортируется модель:

    image12

  13. Соглашаемся на оптимизацию:

    image13

  14. Через некоторое время получаем папку с натренированной моделью, готовой к использованию:

    image14

Исходный код

Интеграция с программой управления квадрокоптером

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)