Автоматизация системы управления квадрокоптера

Рассмотрение и характеристика особенностей беспилотных мультироторных летательных аппаратов. Исследование технологии компьютерного зрения. Анализ процесса передачи данных на бортовой контроллер. Ознакомление с базовыми принципами полета квадрокоптера.

Рубрика Коммуникации, связь, цифровые приборы и радиоэлектроника
Вид дипломная работа
Язык русский
Дата добавления 25.06.2017
Размер файла 1,2 M

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

Начнем с преобразования рыскания. Как оговаривалось ранее, его значение центрируется, то есть задается определенный угол yawCenter, который считается нулевым. Значит для получения конечного результата следует пользоваться не углом yaw, а углом yaw - yawCenter, он равен нулю если датчик находится в положении, при котором была нажата кнопка пуска и отклоняется от нуля при изменении этого положения. Нас интересует изменение положения максимум на 40 градусов в каждую сторону, на больший угол повернуть руку трудно физически. Экспериментально выявлено, что значение угла является положительным при повороте влево и отрицательным при повороте вправо. Значит нужно составить такую функцию sendYaw (yaw - yawCenter), чтобы

· При yaw - yawCenter = 0, sendYaw = 0;

· При yaw - yawCenter = 40, sendYaw = -127;

· При yaw - yawCenter = -40, sendYaw = 127;

Решив простую систему уравнений, выясняем, что такой функцией является sendYaw = -3,175 (yaw - yawCenter).

Теперь данные изменяются в нужном виде, но значения могут быть больше 127 и меньше -127. Можно исправить это простыми условиями: если sendYaw> 127, значит sendYaw нужно принудительно присвоить значение 127. Если же sendYaw< -127, значит sendYaw нужно принудительно присвоить значение -127.

C тангажом и креном необходимо произвести аналогичные преобразования, учитывая характер показаний с датчиков.

Для тангажа:

· При pitch = 180 или -180, sendPitch = 0;

· При pitch = 140, sendPitch = -127;

· При pitch = -140, sendPitch = 127;

Получившееся уравнение:

sendPitch = 3,175 * pitch - 571,5, если -180 <= pitch < -140;

sendPitch = 3,175 * pitch + 571,5, если 140 < pitch <= 180;

sendPitch = 127, если 0 > pitch >= -140;

sendPitch = -127, если 0 <= pitch <= 140;

Для крена:

· При roll = 180 или -180, sendRoll = 0;

· При roll = -140, sendRoll = -127;

· При roll = 140, sendRoll = 127;

Получившееся уравнение:

sendRoll= -3,175 * roll - 571,5, если -180 <= roll < -140;

sendRoll = -3,175 * roll + 571,5, если 140 < roll <= 180;

sendRoll = 127, если 0 < roll =< 140;

sendRoll = -127, если 0 >= roll >= -140;

Для газа же нужно произвольно выбрать значение исходной величины, которое будет соответствовать максимальному. Также нужно учесть, что датчик не выдает значения, равные нулю, поэтому точку нуля стоит выбрать другую. Выберем 2000 для максимальной точки и 400 для минимальной и тогда условия выглядят так:

· При throttle = 400, sendThrottle = 0;

· При throttle = 2000, sendThrottle = 255;

Тогда,

sendThrottle = 0.159375 * throttle - 63,75, если 400 < throttle < 2000;

sendThrottle = 0, если throttle <= 400;

sendThrottle = 255, еслиthrottle>= 2000;

Теперь данные преобразованы и их можно отправлять на бортовой контроллер. Но перед этим следует написать программу для управления пятым и шестым каналами - режимами полетов.

5. Разработка ПО для распознавания жестов руки с видеосигнала методами библиотеки OpenCV

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

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

Программа должна каким-то образом распознавать жест, показанный пользователем, возвращать результат (определенный код для каждого предусмотренного жеста), затем этот результат передавать в программу, анализирующую показания датчиков и формирующую сигнал для передачи на квадрокоптер. Эта задача является достаточно сложной в плане производительности - жест руки снимается на видео и затем видеосигнал анализируется сложными алгоритмами, требующими значительных вычислительных мощностей. Микроконтроллер AVR для этих целей не подойдет, а персональный компьютер обладает слишком низкой мобильностью для практического применения устройства. Поэтому идеальным вариантом бы стал мобильный телефон на системе Android: мы можем создать приложение, захватывающее видеосигнал с камеры устройства, обрабатывающее его и передающее результат по протоколу Bluetooth на основной модуль устройства. Поэтому для разработки был выбран язык программирования, используемый в разработке приложений для Android - Java. Первый вариант программы, для упрощения отладки и тестирования, создан как приложение с графическим интерфейсом на языке JavaFX для платформы IBM PC, но в будущем перенос приложения на Android не займет много сил и времени, так как Java является мультиплафтормерным языком программирования. Функционал первой версии программы пока будет ограничен подсчетом загнутых пальцев и проверкой состояния руки (соединены ли пальцы вместе или раздвинуты).

5.1 Постановка задачи и проектирование программного обеспечения

Рис. 15. Принципиальная схема программы

Разработку программы имеет смысл разделить на несколько шагов. Сначала программа должна получить видеосигнал с камеры и выделить из него одно изображение. Все остальные действия ведутся над анализом этого изображения, и, после завершения всех процедур, программа возвращает результат и захватывает новое изображение. Следующий после захвата изображения этап - его редактирование. Программа должна максимально упростить себе задачу анализа: изображение должно быть максимально сжато по разрешению и не должно содержать лишних цветов. В данном случае нужно произвести пороговое преобразование в бинарный вид, так как будут использоваться именно бинарные методы. Далее программа должна выделить необходимые контуры изображения с помощью алгоритмов компьютерного зрения и найти их дефекты. Подробнее об этом пойдет речь в соответствующем подразделе. Далее нужно найти и отфильтровать так называемые экстремальные точки - они содержат большинство информации об объекте. Затем исходя из положения этих точек нужно произвести необходимые вычисления и сделать вывод о том, какой именно жест демонстрируется. Так же вся графическая информация должна быть показана на экране. После всех этих действий цикл следует повторить.

Для программы был создан проект на платформе JavaFX в среде разработки IntelliJ IDEA. JavaFX по своей сути есть платформа для создания приложений на языке Java с графическим интерфейсом. Для создания интерфейсов существует файл на языке разметки FXML, который «общается» с программой на Java с помощью класса controller.java. FXML-файл описывает положение, размеры, графическое оформление всех элементов программы, а controller.java связывает элементы в программный код.

5.2 Захват видеосигнала

Первой задачей стал вывод на экран компьютера видеопотока с камеры. Сделать это можно и без использования библиотеки OpenCV, но мы сразу будем использовать её возможности. В классе Main создадим метод start, который будет производить действия для предварительной подготовки программы. В этом методе мы создаем объект класса FXMLLoader, указываем ему путь на FXML-файл с разметкой и назначаем его «родителем» (Parent). Отныне каждый объект, находящийся в программе будет являться классом-«ребенком» (Child) и подчиняться FXML-файлу и его контроллеру. Затем создается объект класса Stage (а так же дочерний объект класса Scene, который является окном программы в системе Microsoft Windows) и задаются его параметры - заголовок, размеры, возможность изменять размеры окна. Затем методом Stage.show окно вызывается для взаимодействия с пользователем.

Выполнив эти действия, метод передает управление классу controller.java, в котором содержится основной код программы. В этом месте нужно создать объекты для всех элементов программы. Это объекты классов Button (графическая кнопка включения и выключения камеры), ImageView (класс, предназначенный для вывода изображений на экран), ScheduledExecutorService (таймер для контроля видеопотока) и VideoCapture (класс OpenCV, захватывающий видео с камеры).

Запрограммируем метод private Mat grabFrame, захватывающий видеопоток. Как мы знаем, слово перед названием метода означает тип данных переменной, которую возвращает этот метод. В данном случае это не стандартная переменная, а объект класса Mat, содержащегося в библиотеке OpenCV. В нашем случае это будет объект frame - одно изображение, взятое из видеопотока. Итак, в методе private Mat grabFrame создаем объект frame класса Mat. С помощью метода this.capture.isOpened программа проверяет, поступает ли с порта камеры изображение, и, если да, то применяет метод this.capture.read (frame), который записывает кадр в объект, а если нет, то выдает ошибку. Далее идет проверка, содержит ли кадр какую-либо информацию, и, если содержит, то над ним будут проводиться необходимые преобразования, которые мы пропишем позже. Пустой же кадр метод возвращает без изменений.

Следующий необходимый метод обрабатывает событие, возникающее при нажатии кнопки Start Camera. Назовем его void startCamera (ActionEvent event). Как мы можем увидеть, этот метод принимает как аргумент событие нажатия кнопки и не возвращает никаких значений.

Как только кнопка оказывается нажатой, метод активируется и сначала применяет метод this.capture.open (cameraId), включающий захват с камеры с номером cameraId. Если захват включён, то функция frameGrabber с помощью упомянутого ранее таймера захватывает изображение с потока раз в 33мс (30 кадров в секунду). Тут же производится и перенос изображения из объекта класса Mat в объект класса Image. Так же в методе предусмотрен вывод сообщений об ошибке, процедура закрытия программы, изменений надписи на кнопке, в зависимости от состояния камеры (включена или выключена).

Теперь в FXML-файле нужно создать разметку: в текстовом виде или с помощью утилиты SceneBuilder. В объекте класса Scene, который является окном программы в операционной системе Microsoft Windows, создадим связанные элементы GridPane и BorderPane. Они служат для удобной расстановки элементов управления по рабочему пространству программы. Так же необходимо создать объект класса ImageView для показа захватываемого изображения на экран и объект класса Button для включения и выключения камеры.

Рис. 16. Интерфейс программы в утилитеSceneBuilder

Представленная на Рис. 17 программа - результат на данный момент. Пока она умеет лишь захватывать видео с камеры и выводить его на экран. Так же имеется кнопка Start/Stop Camera, которая соответственно включает и выключает камеру.

Рис. 17. Программа, захватывающая видеосигнал с камеры

5.3 Обработка изображения

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

В первую очередь, информация о цвете изображения является избыточной и нет нужды тратить вычислительные мощности на ее обработку. Тем более, она может не только оказаться ненужной, но и навредить, уменьшая эффективность распознавания объекта. Так же многие методы библиотеки OpenCV в принципе могут работать только с двухцветными изображениями. Поэтому в большинстве проектов по распознаванию объектов изображение переводится в бинарный вид (черные и белые пиксели без оттенков серого). Часто этот перевод происходит с помощью семплирования цвета - в таком случае программа запоминает определенный цвет и переводит все пикселы с цветами, имеющими похожий цвет, в белый, а все остальные пикселы - в черный. Однако в данном случае этот метод является излишним, так как предполагается только распознавание темного предмета на светлом фоне. Итак, как мы знаем, результатом захвата видеосигнала является объект frame класса Mat. Создадим метод, преобразующий его в двоичный вид и назовем его imageEdit. В результате всех преобразований над изображением оно должно максимально полно передавать информацию о положении объекта, то есть в идеале полностью обозначать объект одним из цветов, а фон - другим.

Первое необходимое действие - перевод изображения в монохромный вид. Оно производится методом Imgproc.cvtColor, который принимает три аргумента: объект изображения-источника, объект, в который записывается преобразованное изображение и тип преобразования. В качестве первых двух выберем объект frame, а типом преобразования будет COLOR_BGR2GRAY. Теперь для получения бинарного изображения, нужно произвести пороговое преобразование. За это отвечает метод Imgproc.threshold, который имеет пять аргументов: два первых повторяют аналогичные для Imgproc.cvtColor, третий - величина порога выбора цвета, четвертый - максимальное значение переменной, а пятый - тип преобразования (метод поддерживает различные алгоритмы, из который мы выбираем находящийся под номером один, в котором пиксел перекрашивается в белый цвет, если значение его яркости меньше порогового значения, и в черный в другом случае). Значение порога не может быть выбрано раз и навсегда, так как оно должно зависеть от внешних условий, таких как освещение. Поэтому добавим объект класса Slider в класс controller и в файл разметки. В нем же устанавливаются максимальное, минимальное значения и значение по умолчанию для слайдера. Теперь команду slider.getValue можно вставить в качестве третьего аргумента метода порогового преобразования и управлять величиной порога непосредственно с экрана.

Теперь изображение имеет бинарный вид, но необходимо произвести еще одно преобразование - размытие, защищающее от помех. Оно производится методом Imgproc.medianBlur. Аналогично с пороговым преобразованием, создадим слайдер, управляющий глубиной размытия и эмпирически выберем оптимальный тип алгоритма для данной задачи.

Рис. 18. Внешний вид программы после редактирования изображения

5.4 Выделение контуров

Добившись результата, в котором программа выдает качественное бинарное изображение руки, можно приступать к анализу этого изображения. Сейчас для компьютера оно представляет собой двоичный массив, в котором черные пиксели можно считать за состояние true, а белые - за состояние false. Наша задача - анализ информации, содержащейся в этом массиве и вынесение решения согласно этой информации. Алгоритм должен состоять из двух частей: первая каким-то образом структурирует информацию, содержащуюся в изображении, представляет всю полезную часть информации в виде определенного блока данных. Вторая часть сравнивает эти данные с определенными константами, заданными программистом. Если обработанные данные соответствуют какой-то из констант, то программа возвращает определенный результат, иначе - сообщение об ошибке или отсутствии необходимого типа изображения.

Самый примитивный вариант реализации - полностью игнорировать первый шаг, оставив данные в массиве двоичных переменных и сравнивать этот массив с заранее записанным в программу изображением какого-либо жеста. В таком случае используемый метод фактически представляет собой корреляционный прием: значение каждого пиксела полученного изображения перемножается со значением соответствующего пиксела заданного изображения. Затем эти значения суммируются и по этой сумме можно делать вывод о степени «похожести» двух изображений. Этот метод прост для программирования, но чрезвычайно неэффективен: из-за различных вариаций захваченного изображения (масштаб, положение руки, индивидуальные особенности каждого пользователя) метод будет давать существенные ошибки [11]. Поэтому в данном проекте мы перейдем от анализа пикселей к анализу контуров.

Стоит обговорить, какую информацию следует выделить из изображения для дальнейшей обработки. Начать следует с самых простых жестов - когда плоскость ладони перпендикулярна камере и в качестве переменных для вычислений используется только длины пальцев (прижат тот или иной палец или нет) и углы между пальцами. Для получения всей этой информации достаточно найти на изображении несколько точек, называемых экстремальными: кончик каждого пальца и каждую перепонку между пальцами. В таком случае необходимо создать два контура: один точно очерчивает руку, а другой соединяет крайние точки в многоугольник. В таком случае точки на кончиках пальцев - это и есть точки, по которым следует построить обводящий контур, а точки между пальцами можно найти как максимальные отклонения одного контура от другого. Значит сначала нужно найти и нарисовать оба контура, а затем производить операции над ними. Далее контур, повторяющий форму руки, будем называть внутренним (в программе он значится как contour), а контур, соединяющий экстремальные точки объекта в многоугольник, будем называть обводящим (или convexHull). Естественно, на изображении может быть целое множество контуров, и все контуры того или иного вида объединяются в массив, каждый элемент которого является, в свою очередь, массивом точек или иных типов переменных. Таким образом, и contour, и convexHull - это двухмерные массивы, и каждый отдельный контур можно выделить с помощью метода get(i), где i - номер контура [9].

Дополним метод обработки изображения командами, которые будут находить контуры изображения и графически выделять их на рабочем поле. Для начала пропишем команду LinkedList<MatOfPoint> contours = new LinkedList<>. Она создает массив объектов класса MatOfPoint - это тип данных, хранящий в себе положение множества точек на двухмерном пространстве изображения. Значит каждый контур будет обрабатываться программой, как массив точек, находящихся на определенной кривой. Затем с помощью команды contours.clear убедимся, что заданный массив пуст. Далее применяется метод findContours из класса Imgproc. Он, используя определенные алгоритмы, выделит контуры на порогах белого и черного цветов и запишет их в массив contours. Настройки алгоритма задаются двумя атрибутами: мы выбираем Imgproc.RETR_EXTERNAL для того, чтобы сообщить программе, что следует игнорировать внутренние контуры и выделять следует только наружные. Второй атрибут характеризует сам тип используемого алгоритма и эмпирическим путем было выявлено, что оптимально подходит Imgproc.CHAIN_APPROX_TC89_KCOS.

Далее произведем фильтрацию: интерес для анализа представляет только один объект, так что из всех выделенных контуров можно выбрать единственный с наибольшей площадью. Поэтому создадим новый объект MatOfPoint finalContours, он будет хранить в себе наибольший контур, который программа будет отрисовывать и над которым будут проводиться дальнейшие операции. Запишем в его первый элемент изначального массива контуров, а затем циклом будем проверять каждый последующий элемент contours и, если возвращенное методом Imgproc.contourArea значение площади будет превосходить значение площади единственного элемента finalContours, то элемент finalContours будет принимать значение элемента contours. Таким образом мы получили объект, содержащий только контур вокруг самого большого предмета на изображении.

В конце пропишем метод, рисующий все контуры на рабочем поле, Imgproc.drawContours. Его параметры не представляют особого интереса: это поле, в котором нужно рисовать, массив самих контуров, цвет и толщина линий.

Нахождение обводящего контура - менее тривиальная задача. Дело в том, что библиотека OpenCV умеет оперировать множеством типов переменных и массивов, причем разные ее методы требуют применения разных типов. Поэтому нередко приходится применять весьма нетривиальные преобразования, особенно реализации библиотеки для языка Java: многие операции, в отличие от реализаций для других языков программирования, здесь приходится производить вручную.

Как мы помним, контур, очерчивающий объект, программа воспринимает как массив класса MatOfPoint, содержащего в себе координаты каждой точки данного контура. Однако объект обводящего контура задается совершенно другим путем - метод Imgproc.convexHull находит не контур, а каждую крайнюю точку изображения как элемент массива MatOfInt, состоящего из целых чисел, тогда как для графического изображения пригоден только массив MatOfPoint. Итак, для единственного оставшегося после фильтрации внутреннего контура нужно создать элемент массива с помощью метода convexHull.add(new MatOfInt), а также применить сам метод Imgproc.convexHull, отыскивающий обводящий контур вокруг внутреннего, причем в атрибутах метода должны находиться нулевые элементы массивов contour и convexHull, которые выделяются с помощью метода get. В результате массив convexHull наполняется обводящим контуром.

Теперь преобразуем его в необходимый формат, MatOfPoint. Преобразование идет сначала из формата библиотеки OpenCV MatOfInt в обычный двухмерный массив переменных типа Point, и только потом непосредственно в формат MatOfInt. Создадим массив класса Point и назовем его hullpoints. Затем в каждом шаге цикла от нуля до количества обводящих контуров создадим новую точку в массиве и еще одним циклом присвоим новое значение с помощью метода convexHull.get. Массив же в формате MatOfPoint обозначим как hullmop и еще одним циклом по всем элементам hullpoints проведем преобразование. Происходит это тремя командами: создается объект MatOfPoint, затем метод fromArray(hullpoints.get) передает ему соответствующее счетчику значение массива hullpoints, затем метод hullmop.add записывает этот объект в соответствующий массив. [5]

Теперь контур доступен для рисования методом Imgproc.drawContours. Выберем для его отображения другой цвет и толщину линий. В результате программа имеет данный вид.

В результате программа имеет вид, показанный на рис. 19.

Рис. 19. Программа после выделения контуров

5.5 Нахождение дефектов обводящего контура

Теперь найдем так называемые дефекты обводящего контура. Создадим очередной массив-объект для хранения информации, на сей раз в формате MatOfInt4, под названием convDef. Этот массив хранит данные сгруппировано по четыре числа формата Integer. Используется этот формат потому что для описания каждого дефекта метод выделяет по четыре величины: start, end, defect, depth.

Рис. 20. Схематичное изображение возвращаемых значений метода Imgproc.convexityDefects

Для полноценного описания в данном проекте достаточно точек 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.

Заключение

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

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

Тем не менее, устройство имеет реальные перспективы развития и использования в практических целях.

Разработка устройства потребовала навыки в областях радиофизики, программирования, обработки сигналов, робототехники, электроники и аэродинамики.

Список использованных источников

1. Герасимов В.Г. Электротехника и электроника. Кн. 2. Электромагнитные устройства и электрические машины. / В.Г. Герасимов, Э.В. Кузнецов, О.В. Николаева. - М: Энергоатомиздат, 1997. -- 288 с.

2. Корнилов В. А. Система управления мультикоптером / В.А. Корнилов, Д.С. Молодяков, Ю.А. Синявская. // Электронный журнал «Труды МАИ». - 2015. - № 62. - С. 1-8.

3. Ang K.H. PID control system analysis, design, and technology / K.H. Ang, G. Chong, Y. Li. // IEEE Transactions on Control Systems Technology. -2005.- № 4. - C. 559-576.

4. Kalman R.E. A new approach to linear filtering and prediction problems / R.E. Kalman. // Journal of Basic Engineering. - 1960. - №82.-С. 35-45

5. ФорсайтД.А. Компьютерноезрение. Современныйподход. / Д.А. Форсайт, Ж. Понс. - М :Издательскийдом «Вильямс», 2004. - 982с.

6. Шапиро Л. Компьютерное зрение. / Л. Шапиро,000 Дж. Стокман. -- М : Бином. Лаборатория знаний, 2013. -- 752 с.

7. Желтов С.Ю. Обработка и анализ изображений в задачах машинного зрения. / С.Ю. Желтов [и др.]; ред. С.Ю. Желтов. -- М : Физматкнига, 2010. -- 672 с.

8. Лукьяница А.А. Цифровая обработка видеоизображений. / А.А. Лукьяница, А.Г. Шишкин. -- М : «Ай-Эс-Эс Пресс», 2009. -- 518 с.

9. G. Bradski. LearningOpenCV. / GaryBradskiandAdrianKaehler. - M :O'ReillyMedia, Inc, 2008. - 571 c.

10. OpenCVdocumentationindex [Электронныйресурс] :сайт. - URL: https://docs.opencv.org.

11. E. R. Davies. Machine Vision: Theory, Algorithms, Practicalities. M: -- Morgan Kaufmann, 2004. - 579 c.

12. IMU-сенсор на 10 степеней свободы [Электронныйресурс] :сайт. - URL: http://wiki.amperka.ru/продукты:troyka-imu-10-dof

Приложения

Приложение 1

Main.java

package sample;

// импортирование JavaFX-классов

import javafx.application.Application;

import javafx.fxml.FXMLLoader;

import javafx.scene.Parent;

import javafx.scene.Scene;

import javafx.scene.layout.BorderPane;

import javafx.stage.Stage;

// импортирование библиотеки OpenCV

import org.opencv.core.Core;

public class Main extends Application {

@Override

public void start(Stage primaryStage) throws Exception{

// связываем с FXML-документом

FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml")); // load the FXML resource

Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));

// заголовок окна

primaryStage.setTitle("Gesture Detection 0.1");

// создаем окно с параметрами высоты и ширины

primaryStage.setScene(new Scene(root, 517, 441));

// запрет изменения размеров окна

primaryStage.setResizable(false);

// создаем сцену и выводим на экран

primaryStage.show();

}

public static void main(String[] args) {

// загружаем OpenCV

System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

// запускаем JavaFX-код

launch(args);

}

}

Utils.java

package sample;

// импортирование средства для буферизации изображений

import java.awt.image.BufferedImage;

import java.awt.image.DataBufferByte;

// импортирование JavaFX-классов

import javafx.application.Platform;

import javafx.beans.property.ObjectProperty;

import javafx.embed.swing.SwingFXUtils;

import javafx.scene.image.Image;

// импортирование библиотеки OpenCV

import org.opencv.core.Mat;

public final class Utils

{

//Конвертирует Mat-объект из OpenCV в Image из JavaFX

public static Image mat2Image(Mat frame)

{

try

{

return SwingFXUtils.toFXImage(matToBufferedImage(frame), null);

}

catch (Exception e)

{

System.err.println("Cannot convert the Mat obejct: " + e);

return null;

}

}

// метод для импортирования элементов в "нить" JavaFX

// взят из открытых источников

public static <T> void onFXThread(final ObjectProperty<T> property, final T value)

{

Platform.runLater(() -> {

property.set(value);

});

}

// буферизация изображения

private static BufferedImage matToBufferedImage(Mat original)

{

BufferedImage image = null;

int width = original.width(), height = original.height(), channels = original.channels();

byte[] sourcePixels = new byte[width * height * channels];

original.get(0, 0, sourcePixels);

if (original.channels() > 1)

{

image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);

}

else

{

image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);

}

final byte[] targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();

System.arraycopy(sourcePixels, 0, targetPixels, 0, sourcePixels.length);

return image;

}

}

Sample.FXML

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.text.*?>

<?import java.lang.*?>

<?import javafx.geometry.*?>

<?import javafx.scene.control.*?>

<?import javafx.scene.image.*?>

<?import javafx.scene.layout.*?>

<?import javafx.geometry.Insets?>

<?import javafx.scene.control.Button?>

<?import javafx.scene.control.Slider?>

<?import javafx.scene.image.ImageView?>

<?import javafx.scene.layout.BorderPane?>

<?import javafx.scene.layout.ColumnConstraints?>

<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.layout.RowConstraints?>

<GridPane alignment="center" hgap="10" maxHeight="600.0" maxWidth="726.0" minHeight="426.0" minWidth="517.0" prefHeight="441.0" prefWidth="517.0" vgap="10" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">

<columnConstraints>

<ColumnConstraints />

</columnConstraints>

<rowConstraints>

<RowConstraints />

</rowConstraints>

<children>

<BorderPane maxHeight="600.0" maxWidth="700.0" minHeight="438.0" minWidth="530.0" prefHeight="438.0" prefWidth="530.0">

<bottom>

<Button fx:id="button" mnemonicParsing="false" onAction="#startCamera" text="Start Camera" BorderPane.alignment="CENTER">

<BorderPane.margin>

<Insets bottom="15.0" />

</BorderPane.margin>

</Button>

</bottom>

<center>

<ImageView fx:id="currentFrame" fitHeight="325.0" fitWidth="431.0" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER">

<BorderPane.margin>

<Insets right="-300.0" />

</BorderPane.margin></ImageView>

</center>

<top>

<Slider fx:id="slider" max="300.0" maxWidth="400.0" minWidth="100.0" prefHeight="14.0" prefWidth="100.0" value="150.0" BorderPane.alignment="TOP_RIGHT">

<BorderPane.margin>

<Insets right="5.0" top="10.0" />

</BorderPane.margin></Slider>

</top>

<left>

<Label alignment="TOP_LEFT" text="Threshold" BorderPane.alignment="TOP_LEFT">

<font>

<Font size="17.0" />

</font>

<padding>

<Insets left="30.0" top="-20.0" />

</padding>

</Label>

</left>

<right>

<Slider fx:id="sliderBlur" max="20.0" min="2.0" prefHeight="14.0" prefWidth="402.0" value="5.0" BorderPane.alignment="CENTER">

<padding>

<Insets right="5.0" top="-350.0" />

</padding>

</Slider>

</right>

</BorderPane>

<Label text="Blur">

<GridPane.margin>

<Insets left="50.0" top="-365.0" />

</GridPane.margin>

<font>

<Font size="17.0" />

</font>

</Label>

</children>

</GridPane>

Controller.java

package sample;

// импортирование JavaFX-классов

import javafx.event.ActionEvent;

import javafx.fxml.FXML;

import javafx.scene.control.Button;

import javafx.scene.control.Slider;

import javafx.scene.image.Image;

import javafx.scene.image.ImageView;

// импортирование библиотеки OpenCV

import org.opencv.core.*;

import org.opencv.imgproc.Imgproc;

import org.opencv.videoio.VideoCapture;

// импортирование утилит Java

import java.util.ArrayList;

import java.util.LinkedList;

import java.util.List;

import java.util.concurrent.Executors;

import java.util.concurrent.ScheduledExecutorService;

import java.util.concurrent.TimeUnit;

public class Controller {

@FXML

private Button button; // кнопка

@FXML

private Slider slider; // слайдер выбора порога

@FXML

private Slider sliderBlur; //слайдер размытия

@FXML

private ImageView currentFrame; //объект просмотра изображения

private ScheduledExecutorService timer; // таймер для контроля видеострима

private VideoCapture capture = new VideoCapture(); // объект OpenCV, захватывающий видео

private boolean cameraActive = false; // флаг, меняющий поведение кнопки

private static int cameraId = 0; // id используемой камеры

Serial serial = new Serial(); // объект для работы с серийным портом

// событие нажатия кнопки

@FXML

protected void startCamera(ActionEvent event)

{

if (!this.cameraActive) //если камера в данный момент выключена

{

// начинаем захват видео

this.capture.open(cameraId);

// видео доступно?

if (this.capture.isOpened())

{

this.cameraActive = true; //флажок поднимается

// захватываем 30 кадров в секунду (33 мс)

Runnable frameGrabber = new Runnable()

{

@Override

public void run()

{

// захватываем отдельный кадр

Mat frame = grabFrame();

// теперь в изображение его

Image imageToShow = Utils.mat2Image(frame);

updateImageView(currentFrame, imageToShow);

}

};

// настройка таймера

this.timer = Executors.newSingleThreadScheduledExecutor();

this.timer.scheduleAtFixedRate(frameGrabber, 0, 33, TimeUnit.MILLISECONDS);

// надпись на кнопке меняется

this.button.setText("Stop Camera");

serial.initialize();

}

else

{

// ошибка

System.err.println("Impossible to open the camera connection...");

}

}

else

{

// выключаем камеру

this.cameraActive = false;

// меняем надпись на кнопке

this.button.setText("Start Camera");

// останавливаем таймер

this.stopAcquisition();

}

}

//функция, захватывающая кадр из видео, возвращает переменную frame

private Mat grabFrame()

{

// создаем объект фрейма

Mat frame = new Mat();

// объявляем переменную, которая будет отправляться по серийному порту

Byte sendByte = 0;

// если захват работает

if (this.capture.isOpened())

{

// прочитать текущий кадр

this.capture.read(frame);

// если кадр не пустой, обрабатываем его

if (!frame.empty())

{

// переводим в оттенки серого

Imgproc.cvtColor(frame, frame, Imgproc.COLOR_BGR2GRAY);

// переводим в черное и белое

Imgproc.threshold(frame, frame, slider.getValue(), 5000, 1);

// сглаживаем (избавляемся от шумов)

Imgproc.medianBlur(frame, frame, ((int) sliderBlur.getValue() * 2) - 1);

// объявляем массив контуров и очищаем

LinkedList<MatOfPoint> contours = new LinkedList<>();

contours.clear();

// вспомогательный массив

Mat hierarchy = new Mat();

// ищем контуры

Imgproc.findContours(frame, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_TC89_KCOS);

// Выделяем самый большой контур

LinkedList<MatOfPoint> finalContours = new LinkedList<>();

finalContours.addFirst(contours.get(0));

for (int j = 0; j < contours.size(); j++)

{

if (Imgproc.contourArea(contours.get(j)) > Imgproc.contourArea(finalContours.getFirst()))

{

finalContours.removeFirst();

finalContours.addFirst(contours.get(j));

}

}

// Находим обводящие контуры

LinkedList<MatOfInt> convexHull = new LinkedList<>();

convexHull.add(new MatOfInt());

Imgproc.convexHull(finalContours.get(0), convexHull.get(0));

// конвертируем MatOfInt в Point для рисования обводок

// цикл по всем контурам

List<Point[]> hullpoints = new ArrayList<>();

for (int j = 0; j < convexHull.size(); j++)

{

Point[] points = new Point[convexHull.get(j).rows()];

// цикл по всем выпуклостям

for (int k = 0; k < convexHull.get(j).rows(); k++)

{

int index2 = (int) convexHull.get(j).get(k, 0)[0];

points[k] = new Point(finalContours.get(j).get(index2, 0)[0], finalContours.get(j).get(index2, 0)[1]);

}

hullpoints.add(points);

}

// конвертируем массив Point в MatOfPoint

List<MatOfPoint> hullmop = new ArrayList<>();

for (int j = 0; j < hullpoints.size(); j++)

{

MatOfPoint m = new MatOfPoint();

m.fromArray(hullpoints.get(j));

hullmop.add(m);

}

// ищем дефекты контуров

List<Integer> cdList;

LinkedList<MatOfInt4> convDef = new LinkedList<>();


Подобные документы

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.