Управление дронами с помощью приложений для распознавания речи на основе Intel RealSense SDK
В новостях рассказывают о дронах — беспилотных летательных аппаратах — буквально каждый день. Области применения у них самые разные: разведка и боевые операции, фото- и видеосъемка, да и просто развлечения. Технология дронов достаточно новая и заслуживает интереса.
Разработчики могут создавать приложения для управления дронами. Дрон в конечном итоге является обычным программируемым устройством, поэтому к нему можно подключаться и отдавать команды для выполнения нужных действий с помощью обычных приложений для ПК и смартфонов. Для этой статьи я выбрал один из дронов с самыми мощными возможностями программирования — AR.Drone 2.0 компании Parrot.
Мы узнаем, как взаимодействовать с таким дроном и управлять им с помощью библиотеки, написанной на C#. Опираясь на эту основу, мы добавим речевые команды для управления дроном с помощью Intel RealSense SDK.
Модель AR.Drone 2.0 компании Parrot — один из наиболее интересных дронов, предлагаемых на рынке для энтузиастов. Этот дрон обладает множеством функций и включает встроенную систему помощи с интерфейсами стабилизации и калибровки. Дрон оснащен защитным каркасом из прочного пенополистирола, предохраняющим лопасти винтов и движущиеся части в случае падения или столкновения с неподвижными препятствиями.
AR.Drone* 2.0 компании Parrot
Оборудование дрона обеспечивает его подключение по собственной сети Wi-Fi* к внешним устройствам (смартфонам, планшетам, ПК). Протокол связи основан на АТ-подобных сообщениях (подобные команды несколько лет назад использовались для программирования модемов для связи по телефонной сети).
С помощью этого простого протокола можно отправлять дрону все команды, необходимые для взлета, подъема или спуска, полета в разных направлениях. Также можно считывать поток изображений, снятых камерами (в формате высокой четкости), установленными на дроне (одна камера направлена вперед, другая — вниз), чтобы сохранять отснятые в полете фотографии или записывать видео.
Компания-производитель предоставляет несколько приложений для пилотирования дрона вручную, но намного интереснее узнать, как добиться автономного управления полетом. Для этого я решил (при содействии моего коллеги Марко Минерва) создать интерфейс, который позволил бы управлять дроном с разных устройств.
Программное управление дроном
У дрона есть собственная сеть Wi-Fi, поэтому подключимся к ней для передачи команд управления. Всю нужную информацию мы нашли в руководстве для разработчиков AR.Drone 2.0. Например, в руководстве сказано, что нужно отправлять команды по протоколу UDP на IP-адрес 192.168.1.1, порт 5556. Это простые строки в формате AT:
- AT * REF — управление взлетом и посадкой;
- AT * PCMD — движение дрона (направление, скорость, высота).
После подключения к дрону мы создадим своего рода «игру», в которой будем отправлять команды дрону на основе входных данных приложения. Попробуем создать библиотеку классов.
Сначала нужно подключиться к устройству.
public static async Task ConnectAsync(string hostName = HOST_NAME, string port = REMOTE_PORT)
{
// Set up the UDP connection.
var droneIP = new HostName(hostName);
udpSocket = new DatagramSocket();
await udpSocket.BindServiceNameAsync(port);
await udpSocket.ConnectAsync(droneIP, port);
udpWriter = new DataWriter(udpSocket.OutputStream);
udpWriter.WriteByte(1);
await udpWriter.StoreAsync();
var loop = Task.Run(() => DroneLoop());
}
Как уже было сказано ранее, нужно использовать протокол UDP, следовательно, нужен объект DatagramSocket. После подключения с помощью метода ConnectAsync мы создаем DataWriter в выходном потоке для отправки команд. И наконец, мы отправляем первый байт по Wi-Fi. Он служит только для инициализации системы и будет отброшен дроном.
Проверим команду, отправленную дрону.
private static async Task DroneLoop()
{
while (true)
{
var commandToSend = DroneState.GetNextCommand(sequenceNumber);
await SendCommandAsync(commandToSend);
sequenceNumber++;
await Task.Delay(30);
}
}
Тег DroneState.GetNextCommand форматирует строковую АТ-команду, которую нужно отправить устройству. Для этого нужен порядковый номер: дрон ожидает, что каждая команда сопровождается порядковым номером, и игнорирует все команды, номера которых меньше или равны номерам уже полученных команд.
После этого мы используем WriteString для отправки в поток команд через StreamSocket, при этом StoreAsync записывает команды в буфер и отправляет их. И наконец, мы увеличиваем порядковый номер и используем параметр Task Delay, чтобы ввести задержку в 30 миллисекунд перед следующей итерацией.
Класс DroneState определяет, какую команду отправить.
public static class DroneState
{
public static double StrafeX { get; set; }
public static double StrafeY { get; set; }
public static double AscendY { get; set; }
public static double RollX { get; set; }
public static bool Flying { get; set; }
public static bool isFlying { get; set; }
internal static string GetNextCommand(uint sequenceNumber)
{
// Determine if the drone needs to take off or land
if (Flying && !isFlying)
{
isFlying = true;
return DroneMovement.GetDroneTakeoff(sequenceNumber);
}
else if (!Flying && isFlying)
{
isFlying = false;
return DroneMovement.GetDroneLand(sequenceNumber);
}
// If the drone is flying, sends movement commands to it.
if (isFlying && (StrafeX != 0 || StrafeY != 0 || AscendY != 0 || RollX != 0))
return DroneMovement.GetDroneMove(sequenceNumber, StrafeX, StrafeY, AscendY, RollX);
return DroneMovement.GetHoveringCommand(sequenceNumber);
}
}
Свойства StrafeX, StrafeY, AscendY и RollX определяют соответственно скорость движения влево и вправо, вперед и назад, высоту и угол вращения дрона. Эти свойства имеют тип данных Double, допустимые значения — от 1 до -1. Например, если задать для свойства StrafeX значение -0,5, то дрон будет перемещаться влево с половиной максимальной скорости; если задать 1, то дрон полетит вправо с максимальной скоростью.
Переменная Flying определяет взлет и посадку. В методе GetNextCommand мы проверяем значения этих полей, чтобы определить, какую команду отправить дрону. Эти команды, в свою очередь, находятся под управлением класса DroneMovement.
Обратите внимание, что, если команды не заданы, последняя инструкция создают так называемую команду Hovering. Это пустая команда, поддерживающая открытый канал связи между дроном и устройством. Дрон должен постоянно получать сообщения от управляющего им приложения, даже если не нужно выполнять никаких действий и ничего не изменилось.
Самый интересный метод класса DroneMovement — метод GetDroneMove, который фактически и занимается составлением и отправкой команд дрону. Другие методы, связанные с движением, см. в этом примере.
public static string GetDroneMove(uint sequenceNumber, double velocityX, double velocityY, double velocityAscend, double velocityRoll)
{
var valueX = FloatConversion(velocityX);
var valueY = FloatConversion(velocityY);
var valueAscend = FloatConversion(velocityAscend);
var valueRoll = FloatConversion(velocityRoll);
var command = string.Format("{0},{1},{2},{3}", valueX, valueY, valueAscend, valueRoll);
return CreateATPCMDCommand(sequenceNumber, command);
}
private static string CreateATPCMDCommand(uint sequenceNumber, string command, int mode = 1)
{
return string.Format("AT*PCMD={0},{1},{2}{3}", sequenceNumber, mode, command, Environment.NewLine);
}
Метод FloatConversion не указан здесь, но он преобразует значение типа Double диапазона от -1 до 1 в целочисленное значение со знаком, которое может быть использовано АТ-командами, например строкой PCMD для управления движением.
Показанный здесь код доступен в виде бесплатной библиотеки на сайте NuGet (AR.Drone 2.0 Interaction Library). Эта библиотека предоставляет все необходимое для управления — от взлета до посадки.
Пользовательский интерфейс AR.Drone UI на сайте NuGet
Благодаря этому образцу приложения можно забыть о тонкостях реализации и сосредоточиться на создании приложений, которые дают нам возможность пилотировать дрон, используя разные способы взаимодействия.
Intel RealSense SDK
Теперь посмотрим на одну из самых интересных и удобных в использовании (для меня) возможностей Intel RealSense SDK — распознавание речи.
В SDK поддерживается два подхода к распознаванию речи.
- Распознавание команд (по заданному словарю).
- Распознавание свободного текста (диктовка).
Первый подход представляет собой своего рода список команд, заданный приложением, на указанном языке, который обрабатывается «распознавателем». Все слова, которых нет в списке, игнорируются.
Второй подход — что-то типа диктофона, «понимающего» любой текст в свободной форме. Этот подход идеален для стенографирования, автоматического создания субтитров и т. п.
В этом проекте мы используем первый вариант, поскольку требуется поддерживать конечное количество команд, отправляемых дрону.
Сначала нужно определить некоторые переменные.
private PXCMSession Session;
private PXCMSpeechRecognition SpeechRecognition;
private PXCMAudioSource AudioSource;
private PXCMSpeechRecognition.Handler RecognitionHandler;
Session — тег, необходимый для доступа к вводу-выводу и к алгоритмам SDK, поскольку все последующие действия унаследованы от этого экземпляра.
SpeechRecognition — экземпляр модуля распознавания, созданного функцией CreateImpl в среде Session.
AudioSource — интерфейс устройства, позволяющий установить и выбрать входное аудиоустройство (в нашем примере кода мы для простоты выбираем первое доступное аудиоустройство).
RecognitionHandler — фактический обработчик, назначающий обработчик событий для события OnRecognition.
Теперь инициализируем сеанс, AudioSource и экземпляр SpeechRecognition.
Session = PXCMSession.CreateInstance();
if (Session != null)
{
// session is a PXCMSession instance.
AudioSource = Session.CreateAudioSource();
// Scan and Enumerate audio devices
AudioSource.ScanDevices();
PXCMAudioSource.DeviceInfo dinfo = null;
for (int d = AudioSource.QueryDeviceNum() - 1; d >= 0; d--)
{
AudioSource.QueryDeviceInfo(d, out dinfo);
}
AudioSource.SetDevice(dinfo);
Session.CreateImpl(out SpeechRecognition);
Как было отмечено ранее, для простоты кода мы выбираем первое доступное аудиоустройство.
PXCMSpeechRecognition.ProfileInfo pinfo;
SpeechRecognition.QueryProfile(0, out pinfo);
SpeechRecognition.SetProfile(pinfo);
Затем нужно опросить систему, узнать фактический параметр конфигурации и назначить его переменной (pinfo).
Также нужно настроить ряд параметров в профиле, чтобы изменить язык распознавания. Задайте уровень достоверности распознавания (при более высоком значении требуется более уверенное распознавание), интервал окончания распознавания и т. д.
В нашем случае параметр по умолчанию устанавливается как в профиле 0 (полученном из Queryprofile).
String[] cmds = new String[] { "Takeoff", "Land", "Rotate Left", "Rotate Right", "Advance",
"Back", "Up", "Down", "Left", "Right", "Stop" , "Dance"};
int[] labels = new int[] { 1, 2, 4, 5, 8, 16, 32, 64, 128, 256, 512, 1024 };
// Build the grammar.
SpeechRecognition.BuildGrammarFromStringList(1, cmds, labels);
// Set the active grammar.
SpeechRecognition.SetGrammar(1);
Затем задаем грамматический словарь для обучения системы распознавания. С помощью BuildGrammarFromStringList мы создаем простой список глаголов и соответствующих возвращаемых значений, определяя грамматику номер 1.
Можно задать несколько грамматик для использования в приложении и включать одну из них при необходимости, поэтому можно создать разные словари команд для всех поддерживаемых языков и предоставить пользователю возможность переключаться между языками, распознаваемыми в SDK. В этом случае нужно установить соответствующие DLL-файлы поддержки языка, поскольку при установке SDK по умолчанию устанавливается поддержка только для языка «Английский (США)». В этом примере мы используем только грамматику, установленную по умолчанию вместе с языком «Английский (США)».
Затем выбираем, какую грамматику следует назначить активной в экземпляре SpeechRecognition.
RecognitionHandler = new PXCMSpeechRecognition.Handler();
RecognitionHandler.onRecognition = OnRecognition;
Эти инструкции определяют новый обработчик событий для события OnRecognition и назначают его методу, описанному ниже.
public void OnRecognition(PXCMSpeechRecognition.RecognitionData data)
{
var RecognizedValue = data.scores[0].label;
double movement = 0.3;
TimeSpan duration = new TimeSpan(0, 0, 0, 500);
switch (RecognizedValue)
{
case 1:
DroneState.TakeOff();
WriteInList("Takeoff");
break;
case 2:
DroneState.Land();
WriteInList("Land");
break;
case 4:
DroneState.RotateLeftForAsync(movement, duration);
WriteInList("Rotate Left");
break;
case 5:
DroneState.RotateRightForAsync(movement, duration);
WriteInList("Rotate Right");
break;
case 8:
DroneState.GoForward(movement);
Thread.Sleep(500);
DroneState.Stop();
WriteInList("Advance");
break;
case 16:
DroneState.GoBackward(movement);
Thread.Sleep(500);
DroneState.Stop();
WriteInList("Back");
break;
case 32:
DroneState.GoUp(movement);
Thread.Sleep(500);
DroneState.Stop();
WriteInList("Up");
break;
case 64:
DroneState.GoDown(movement);
Thread.Sleep(500);
DroneState.Stop();
WriteInList("Down");
break;
case 128:
DroneState.StrafeX = .5;
Thread.Sleep(500);
DroneState.StrafeX = 0;
WriteInList("Left");
break;
case 256:
DroneState.StrafeX = -.5;
Thread.Sleep(500);
DroneState.StrafeX = 0;
WriteInList("Right");
break;
case 512:
DroneState.Stop();
WriteInList("Stop");
break;
case 1024:
WriteInList("Dance");
DroneState.RotateLeft(movement);
Thread.Sleep(500);
DroneState.RotateRight(movement);
Thread.Sleep(500);
DroneState.RotateRight(movement);
Thread.Sleep(500);
DroneState.RotateLeft(movement);
Thread.Sleep(500);
DroneState.GoForward(movement);
Thread.Sleep(500);
DroneState.GoBackward(movement);
Thread.Sleep(500);
DroneState.Stop();
break;
default:
break;
}
Debug.WriteLine(data.grammar.ToString());
Debug.WriteLine(data.scores[0].label.ToString());
Debug.WriteLine(data.scores[0].sentence);
// Process Recognition Data
}
Это метод получения значения, возвращенного из данных распознавания, и выполнения соответствующей команды (в нашем случае — соответствующей команды управления полетом дрона).
Каждая команда дрона относится к вызову DroneState с определенным методом (TakeOff, GoUp, DoDown и т. д.) и с определенным параметром движения или длительности, который в каждом случае касается определенного количества или длительности движения.
Некоторым командам требуется явный вызов метода Stop для остановки текущего действия, иначе дрон продолжит двигаться согласно полученной команде (команды см. в предыдущем фрагменте кода).
В некоторых случаях нужно вставить Thread.Sleep между двумя разными командами, чтобы дождаться завершения предыдущего действия перед отправкой новой команды.
Для проверки распознавания, даже если нет доступного дрона, я вставил переменную (она управляется флажком в главном окне), которая включает функциональный режим Drone Stub (в этом режиме команды создаются, но не отправляются).
Чтобы закрыть приложение, вызовите метод OnClosing для закрытия и уничтожения всех экземпляров и обработчиков и для общей очистки системы.
В коде содержатся некоторые команды отладки, выводящие полезную информацию в окнах отладки Visual Studio* при тестировании системы.
Заключение
В этой статье мы увидели, как взаимодействовать с устройством (таким сложным, как дрон) с помощью интерфейса взаимодействия на естественном языке. Мы увидели, как можно создать простой словарь команд, научить систему понимать его и соответственным образом управлять сложным устройством — дроном в полете. Показанное в этой статье — лишь малая доля доступных возможностей по управлению дроном. Возможности поистине безграничны.