Для полноценного описания в данном проекте достаточно точек start, end и defect. Для удобства создадим так же обычный одномерный массив типа Integer и назовем его cdList. Методом Imgproc.convexityDefects выделим массив дефектов обводящего контура. Метод имеет три атрибута: внутренний, контур, обводящий контур, и объект в который будет записываться информация, convDef. Но формат IntOfMat4 неудобен для использования и не принимается методами рисования, поэтому переведем всю информацию в обычный массив data методом convDef.get(0).toList. Также создадим массив cdList, который наполнится натуральными числами от нуля до количества дефектов умноженное на четыре. Теперь информация записана так: в нулевом элементе находится число, характеризующее start первого дефекта, в первом - end первого дефекта, во втором defect первого дефекта, в третьем - depth первого дефекта, в четвертом - start второго дефекта и так далее. Теперь создадим три массива типа Point, в одном из них будут храниться все точки start, во втором все точки end, а в третьем - все точки defect.
После этого можно сразу создать цикл, рисующий
все точки из массивов на экране, на этот раз методом Imgproc.circle и
посмотреть на соответствующий результат.
Рис. 21. Вид программы с найденными
экстремальными точками
5.6 Фильтрация точек дефектов
Прежде чем приступать к анализу найденных точек, необходимо отфильтровать результаты, так как не все из них несут полезную информацию. Нужно, во-первых, исключить все точки defect, образующие с соседними точками start и end слишком большой угол, так как даже максимальный угол отклонения между указательным и большими пальцами не может превышать π/2 и эти точки не несут в себе информации о положении пальцев. Во-вторых, нужно исключить точки start, которые находятся слишком близко друг к другу, так как алгоритм может ошибочно выделять несколько рядом стоящих точек и один палец будет считаться за два. И, в-третьих, нужно исключить точки, находящиеся в непосредственной близости к краю изображения, ведь, как показывает практика, алгоритм часто дает просчеты и в этих областях. Для работы с каждым из трех массивов точек создадим еще по два массива - x и y координат соответственно. Назовем их соответственно startX, startY,endX, endY, defectX, defectY, и присвоим им тип double. Наполнить их информацией о соответствующих координатах каждой точки можно с помощью цикла по всем индексам точек. Далее условными операторами будем производить фильтрацию. Сделаем внутри цикла оператор if с тремя условиями прохождения фильтрации. Если координаты точек defect и start с неким порядковым номером будут отвечать заданным условиям, то эти точки будут записаны в новые массивы, названные defectFiltered и startFiltered. Эти условия будут объединены как логические элементы «и», то есть присвоение случится только если все три условия одновременно будут выполнены.
Первое условие - это достаточно малая величина угла между двумя точками start и точкой defect, находящейся между ними. В java и OpenCV не существует стандартной функции для нахождения угла, описанного тремя точками, поэтому придется сделать ее самостоятельно. По теореме косинусов можно найти любой угол треугольника, если известно три его стороны. Поэтому построим воображаемый треугольник из точекstart,defectиendи вычислим длины его сторон, зная координаты точек. Затем применим теорему косинусов и запишем значение угла в переменную angle. Теперь можно создать первое условие - точка будет записываться в конечный массив только когда значение угла меньше определенного значения. Пока остановимся на значении π/2.
Второе условие - это достаточно большое расстояние между точками startи end. Если расстояние между двумя точками будет меньше определенного значения, то соответствующая точка start должна записаться в конечный массив, поэтому с помощью цикла сравним каждую точку start [i] с точкой end [i]. При сумме квадратов координат больше определенного значения, точка записывается в конечный массив.
Третье условие наиболее просто в реализации:
необходимо чтобы каждая координата находилась в определенном интервале, это
можно реализовать с помощью оператора «и» (значение должно быть больше нижнего
значения и, в то же время, меньше верхнего значения). Выберем значения отступов
по 30 пикселей с каждого края экрана.
Рис. 22. Вид программы после фильтрации точек
дефектов
5.7 Обработка результатов
Итак, теперь мы имеем достаточно много
информации о положении руки: каждый из пяти пальцев обозначен точкой из
массива, причем номер пальца, считая слева направо, соответствует номеру
элемента массива. Четыре промежутка между пальцами так же обозначены точками из
другого массива. В дальнейшем, обрабатывая эту информацию, можно получать
разнообразные результаты. Для примера усовершенствуем код, чтобы программа
выводила на экран количество не прижатых пальцев, и, если пальцы соединены,
меняла цвет внутреннего контура на красный. Информация о количестве пальцев уже
присутствует в программе - это размер массива startFiltered. Преобразуем ее в
тип данных String и выведем на экран методом Imgproc.putText. Метод для
вычисления углов тоже уже был создан в процессе произведения фильтрации,
поэтому воспользуемся им, только на этот раз суммируем все углы циклом и выведем
их среднее арифметическое. Среднее арифметическое теперь снова можно сравнить с
порогом, только теперь при его значении меньше порога объект класса Scalar,
отвечающий за цвет внутреннего контура, будет менять свои атрибуты. Конечный
результат можно наблюдать на рис. 23.
Рис. 23. Конечный вариант программы
6. Формирование сигналов и
передача полученных данных
После получения данных, встает вопрос их
передачи на полетный контроллер. Данные из программы на Javaдолжны
быть переданы на наземную установку на базе Arduinoи,
в дальнейшем, отправлены на полетный контроллер. В будущем, при реализации
проекта в качестве приложения для смартфонов, можно будет воспользоваться
беспроводной связью по протоколу Bluetooth,
но на данный момент более удобным для отладки вариантом будет отправить данные
через серийный порт.
6.1 Передача данных через
серийный порт
Для работы с серийным портом будем пользоваться библиотекой RXTX. Добавим новый класс и назовем его Serial.java. После импортирования всех необходимых элементов библиотеки в теле класса необходимо инициализировать объекты классов serialPort, input, output, а также объявить константы:TIME_OUT=2000 (время ожидания информации),DATA_RATE = 9600 (скорость передачи серийного порта, должна совпадать с заданной в коде для Arduino), PORT_NAMES (массив имен порта для различных операционных систем). Также в классе должно содержаться несколько методов.
Первый метод носит название initializeи используется для подготовки порта и инициализации переменных и объектов. Сначала в нем выбирается подходящее имя порта: в цикле по всем элементам PORT_NAMEсравнивается значение элемента массива и имени порта, полученного из системы. Если не требуется мультиплатформерность, то эта часть необязательная и для Windowsдостаточно задать имя порта как, например, «COM4».
ЗатемвэтомжеметоденужнометодомportId.openоткрытьсерийныйпорти установить параметры DATA_RATE,DATABITS_8, STOPBITS_1 и PARITY_NONE. МетодамиgetInputStreamиgetOutputStream открываем входящие и исходящие потоки данных. Входящий поток не пригодится, так как информация будет передаваться только в сторону Arduino, но можно добавить этот метод для дальнейших усовершенствований. Также включим так называемое прослушивание событий методом addEventListener - тогда программа будет реагировать на входящие сигналы.
Следующим шагом необходимо прописать метод закрытия серийного порта.Назовем его close. При условии, что серийный порт существует, прослушивание событий должно быть закрыто методом removeEventListenerи порт закрыт методом close.
Для отправки данных будет использоваться метод sendSingleByte. Он принимает один аргумент - байт, который будет передаваться, назовем его myByte. Методом output.writeзначение прописывается в серийный порт.
Метод serialEventбудет вызываться автоматически, когда в серийный порт будут поступать данные. Для этого нужно в его аргументы поставить событие прихода данных SerialPortEvent. В этом методе значение считывается методом input.read, преобразуется из типа byteв тип и хранится в переменной value. Также пропишем вывод переменной в консоль, предварительно проверив, входит ли она в интервал от 0 до 255.
Теперь для класса можно создавать объекты и вызывать их методы.Создадим объект serialкласса Serialи запустим метод его инициализации в функции start. В методе setClosed, выполняющемся при закрытии программы, пропишем serial.close.
Теперь закодируем информацию с двух переменных, получаемых с камеры, в один байт. При разжатых пальцах в этом байте будет содержатся просто количество отфильтрованных контуров. При сжатых байт будет содержать значение {1, 1, 1, 1, 1, 1, 1, 1} (127).В конце функции, анализирующей изображение следует добавить метод, осуществляющий отправку данных - serial.sendSingleByteи в аргументах ему задать полученный байт.
Следующий этап - модернизировать код для Arduinoтак, чтобы он мог принимать данные, отправляемые в серийный порт.Добавим переменную типа Byteи назовем ее getByte. В каждом повторении цикла loopтеперь можно добавить команду Serial.read, при условии, если Serial.avaliable.
Известно, что контроллер ArduPilotвоспринимает сигнал смены режима полета так же, как и сигналы газа, рыскания, тангажа и крена - в виде сигнала с широтно-импульсной модуляцией. Конкретные режимы, которые будут переключаться этим сигналом, можно настроить в программе MissionPlanner.Одному каналу можно присвоить функцию смены до 256 режимов, но чаще всего один канал на пульте меняет два или три режима. В нашем случае подобный вариант является оптимальным. Сделаем так, чтобы при сжатых пальцах первый канал передавал байт 11111111, а при разжатых - 0000000. Второй канал будет управляться количеством пальцев - пусть при сжатых пальцах значение равно 0, при трех разжатых - 86, при четырех 172, при пяти - 255.
Принятый байт нужно «разложить» обратно на два
сигнала, аналогичными условиями: если принятый байт равен 127, то переменная mode1
типа intравна 255, а
mode2 -нулю, иначе mode
1 равна нулю, а mode2
определяется по вышеописанному методу.
6.2 Передача данных на бортовой
контроллер
Теперь передадим данные на другой микроконтроллер, который будет находиться на борту квадрокоптера. Для этой цели будут использоваться два модуля nRF24L01. Это модули радиосвязи, работающие на частоте 2,4 ГГц и имеющие встроенную библиотеку для платформ Arduino и RaspberryPI. Модули могут передавать сигнал на расстоянии до 100 м наскорости до 2 Мб/с. Модуль взаимодействует с контроллером с помощью интерфейса SPI.
Кроме ножек питания, модуль имеет следующие выводы:
· SCK (Serial ClocK) - тактирование (синхронизация).
· MOSI / MI (Master Out Slave In) - входданных.
· MISO / MO (Master In Slave Out) - выходданных.
· CE/SS - Выбор ведомого на шине SPI из нескольких устройств.
· SCN - выбор режима приема/передача, фактически тот же CE.
· IRQ - выход прерывания,чаще всего не используется. Необходим для немедленной реакции микроконтроллера при приеме нового пакета данных.
В код для Arduinoкак на приемной, так и на передающей стороне, необходимо импортировать библиотеки SPI.h, nRF24L01.h и RF24.h, а также инициализировать несколько объектов и переменных и запустить определенные методы.
Сначала создадим объект radioкласса RF24, в качестве аргументов он принимает номера выводов Arduino, через которые идет передача данных. Далее следует объявить массив адресов типа byte, в этом массиве хранится информация о наименованиях так называемых «труб» - каналов по которым модуль производит передачу.
В функции setup нужно активировать модуль методом .begin, затем задать режим подтверждения приема на случай, если понадобится отправлять данные с борта на землю методом. Затем методомsetRetries устанавливается количество попыток передать пакет данных и время между ними. Метод enableAckPayloadразрешает отсылку данных в ответ на входящий сигнал.setPayloadSize устанавливает размер пакета в байтах, (в нашем случае 6 байт).
openReadingPipe включает для прослушивания «трубу» спредварительно объявленным номером Adress. setChannel используется для выбора канала в шестнадцатиричной системе счисления (можно выбрать любой, но на некоторых частотах могут присутствовать шумы) setPALevelустанавливаетуровень мощности передатчика. Аргументами могут быть RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH или RF24_PA_MAX. Метод setDataRateпозволяет выбрать максимальную скорость обмена информацией. Выбираем 250 кбит/с, так как при небольшой скорости передачи наблюдается максимальная чувствительность и дальность. Метод powerUp сигнализирует о начале работы модуля. Так же для приемника необходимо прописать метод startListening, а для передатчика - stopListening.
Код для передачи и приема так же прост, как и в случае с серийным портом: для передачи используется метод writeобъекта radio, а для приема - метод read. Далее в приемнике можно вывести каждую полученную величину на вывод, поддерживающий широтно-импульсную модуляцию и направить непосредственно на входы полетного контроллера.
В прил. 1 приведен полный код программы для
распознавания жестов, в прил. 2 - код программы для передатчика и приемника на
платформе Arduino.