Краткая справка: Motion — Программа Motion способна контролировать сигнал, полученный с одной или нескольких видеокамер, и обнаруживать наличие изменений на картинке. На выходе получаем фотки в форматах jpeg, ppm или mpeg видеопоток, который может транслироваться в сеть или записываться в файл. При необходимости на указанный почтовый адрес может быть отослано сообщение с информацией о событии. Возможно выполнение любой предусмотренной пользователем команды или скрипта, поэтому реакция системы зависит только от твоей фантазии. Motion написан на языке Си, изначально разрабатывался для Linux, но может работать и в FreeBSD и Mac OS X. Поддерживаются все типы популярных сегодня видеокамер, подключаемых к компьютеру через USB порт, Video4Linux устройства и сетевые камеры. Драйверов для видеокамер проект Motion не предоставляет, поэтому прежде чем начинать настройку, убедись, что твоя камера видна операционке.

И так начнём. Я использую систему на базе amd64, всё что представлено ниже действия которыми у меня получилось запустить камеры в motion.

Будем ставить систему с помощью которой можно отслеживать изменения попадающие в поле деятельности нашей камеры.

1) Для начала убедимся что в нашей системе появилось видеоустройство.
Убедимся что система видит нашу web-камеру
ls -l /dev/
crw-rw—- 1 root video 81, 0 Авг 22 20:08 video0

2) Убедимся что присутствует модуль v4l
keiz@ekzorchik:~$ lsmod | grep v4l
v4l1_compat 17284 2 uvcvideo,videodev
keiz@ekzorchik:~$

3) Проверим информация о модуле v4l1_compat (так для общих сведений)
keiz@ekzorchik:~$ sudo modinfo v4l1_compat
filename: /lib/modules/2.6.26-2-amd64/kernel/drivers/media/video/v4l1-compat.ko
license: GPL
description: v4l(1) compatibility layer for v4l2 drivers.
author: Bill Dirks
depends:
vermagic: 2.6.26-2-amd64 SMP mod_unload modversions
parm: debug:enable debug messages (int)

4) Задействуем установку и пакетов
sudo apt-get install motion
keiz@ekzorchik:~$ whereis motion
motion: /usr/bin/motion /etc/motion /usr/share/man/man1/motion.1.gz

5) Открываем наш конфигурационный файл
sudo nano /etc/motion/motion.conf

И начинаем:

  • определяем видеоустройство для захвата
    videodevice /dev/video0
  • Используем вход для видео, может иметь два значения:
    8 = (для USB камер), для v4l — ставим 1
    input = 8 (у меня Logitech C250)
  • Устанавливаем количество захватываемых кадров в секунду
    framate 25
  • Указываем каталог куда будут складываться захваченные с видеокамеры файлы
    target_dir /home/keiz/test_motion
  • Имя файла для снимков и видео, в примере оставляем значение по умолчанию# %Y = год, %m = месяц, %d = день, %H = час, %M = минута, %S = секундаsnapshot_filename %v-%Y%m%d%H%M%S-snapshotjpeg_filename %v-%Y%m%d%H%M%S-%q

    movie_filename %v-%Y%m%d%H%M%S

    timelapse_filename %Y%m%d-timelapse

  • Если используется карта видеозахвата или TV тюнер, при помощи параметра norm указываем стандарт. По умолчанию используется 0, то есть PAL. Возможны значения 1 — NTSC, 2 – SECAM и 3 PAL NC. Для TV тюнера также указываем частоту. По умолчанию frequency = 0.
  • Чтобы Motion не переходил в режим демона и выводил отладочную информацию в консоль, используем флаг ‘–n’.$ motion –n
  • В тестовом прогоне можно использовать sudo, а в повседневной жизни лучше разрешить запись в этот каталог членам группы (например, video) и себя, естественно, в нее включить.
  • В состав Motion включен мини http-сервер, который позволяет просматривать в реальном времени картинку с камеры в окне браузера
    webcam_port 0 -> т.е. htt-сервер отключен, чтобы он заработал нужно указать любой свободный порт(допустим 8081)

    webcam_localhost on -> определяет откуда на нашему http-серверу можно будет подключиться (только с локального компьютера)

    webcam_localhost off -> если планируется заходить по сети

  • webcam_quality 50 -> качество выводимых сервером jpeg изображений
    webcam_motion on -> укажем. чтобы картинка менялась постоянно с частотой 1 кадр, а при движении — с частотой, указанной в параметре webcam_maxrate
    webcam_limit 0 -> указывает максимальное количество изображений за соединение (по умолчанию 0, т.е. без ограничений)
  • control_authenticaton username:password -> ограничиваем доступ на сервер, указываем логин и пароль для аутентификации.Теперь, набрав в консоли «motion -n», среди строк вывода ты должен увидеть «motion-httpd: waiting for data on port TCP 8080». Набираем в браузере адрес и получаем возможность указывать настройки для каждой камеры (thread), выбирая их и вводя нужные значения. При работе с несколькими камерами такой способ тебе, вероятно, покажется более удобным.
  • rotate 0 -> снимать изображение в нормальном виде, без вращения (с помощью других параметров 90,180,270 можно вращать изображение) (Понадобиться если камеру расположили в перевернутом виде)
  • По умолчанию при обнаружении движения образуется не только видеофайл, но и последовательность изображений. Параметром output_normal можно изменить такое поведение. Так при установке в first будет сохранено только первое изображение, best – лучшее, а отключить эту функцию можно, использовав off. Активация output_motion разрешит сохранять в снимке только пиксели, показывающие движущийся объект.
  • в конфигурационном файле motion.conf функция записи захваченного видео отключена. Поэтому при необходимости измени значение ffmpeg_cap_new на on. Аналогично с output_motion, есть такой же параметр и для видео — ffmpeg_cap_motion, при активации которого в результирующий видеофайл будут сохранены пиксели, показывающие движущийся объект.
  • За качество результирующего видео отвечают два параметра: ffmpeg_bps или ffmpeg_variable_bitrate. При настройке следует использовать лишь один из них. Качество лучше подбирать экспериментальным путем, исходя из мощности системы и возможностей камеры. Кодек задается при помощи ffmpeg_video_codec, по умолчанию используется mpeg4, но при необходимости можно использовать: mpeg1, msmpeg4, swf, flv или ffv1.
  • (УДОБНАЯ ВЕЩЬ) В некоторых случаях полезной будет возможность периодической записи. Параметр ffmpeg_timelapse отвечает за период, в течение которого ведется запись информации в один видеофайл, затем будет создан новый. Возможные значения: daily (за день, по умолчанию), hourly, weekly-sunday, weekly-monday, monthly и manual. Например, чтобы запись на видео велась каждую секунду, устанавливаем «ffmpeg_timelapse 1». Если во время захвата с аналоговой камеры при перемещении объектов появляются искажения, установи ffmpeg_deinterlace в on.
  • В файл, кроме собственно объекта, за которым следит камера, заносится и дополнительная информация, позволяющая определить время съемки. Эти данные настраиваются в секции Snapshots. Например, установка цифры в snapshot_interval позволит делать снимки с указанным периодом вне зависимости от обнаружения движения. Активация locate выделит на снимке движущийся объект. Текст, выводимый в левом и правом углах снимка, указывается соответственно в text_left и text_right. В настройках по умолчанию выводится дата и время, когда сделан снимок (формат strftime(3)). Если камер несколько, для удобства можно активировать text_left, где прописать что-то вроде «Camera 1».
  • threshold позволяет указать количество пикселей, которые должны измениться для срабатывания детектора, а minimum_motion_frames — количество кадров, в котором они зафиксированы. Подобрав эти значения, можно сделать так, что Motion не будет замечать пролетающую птицу, но без проблем реагировать на человека. Фильтры для сглаживания шума подключаются при помощи despeckle. По умолчанию используется оптимальное значение EedDl. При появлении проблем следует поэкспериментировать, убирая буквы в сочетании EedDl и пробуя их в разных комбинациях.
  • Параметры noise_level, noise_tune, night_compensate и lightswitch отвечают за уровень порога шума и компенсацию темных и светлых участков.
  • Комбинация параметров pre_capture, post_capture и gap позволяет записать законченную сцену, где будет снят контролируемый объект до и после того, как было обнаружено движение. Значение gap по умолчанию установлено в оптимальные 60 (секунд), если движение не будет обнаружено, то создается новый видеофайл, а старый удаляется. Чтобы захваченный файл не был большим, его продолжительность можно ограничить параметром max_mpeg_time, указав в качестве значения время в секундах.
166

Всем доброго времени суток.

Как всегда начнем с предыстории. Необходимо было реализовать видеонаблюдение за помещением, естественно с нулевым бюджетом, а как иначе. В наличие имелся сервер с Linux на борту и домашняя usb web камера. За основу был взят программный пакет Motion. В принципе ничего сложного в настройке нет, правим пару строк в конфиге и вуаля. Писать было ни о чем.

На днях обновил Ubuntu до версии 16.04.1 LTS и заодно, решено было обновить часть пакетов, в том числе Motion, и переделать систему видеонаблюдения. От usb камеры давно было решено отказаться, и ей на смену пришла дешевая IP камера (поддерживающая MJPEG поток) купленная на распродаже.


Отразим все на "бумаге"

Имеется :

  • Сервер Linux Ubuntu 16.04.1 LTS
  • Пакет Motion 3.2.12
  • IP камера с поддержкой MJPEG

Требуется :

  • Писать только видео (никаких фото и таймлапсов)
  • Писать только по движению
  • Оповещать об обнаружении движения
  • Транслировать поток в реальном времени с сервера

В Ubuntu установка пакетов проста до безобразия

apt - get update && apt - get install motion

По завершении установки переходим в каталог с файлом конфигурации

cd / etc / motion nano motion . conf

Сразу разберемся с порядком действий т.к понимая, что сейчас будем делать, мы сможем настроить эту программу вне зависимости от её версии и версии операционной системы на которой она установлена.

Сам Motion может работать одновременно с несколькими потоками (камерами для обывателя), неважно будь то IP камеры, USB камеры или иные устройства захвата видео. Да хоть все вперемешку. Самое главное, что если мы используем IP камеры, то они должны поддерживать MJPEG поток. Никакого RTSP (Real Time Streaming Protocol) Motion не поддерживает.

Файл конфигурации motion.conf представляет из себя набор основных настроек программы и настройки для первого потока. Все последующие потоки можно конфигурировать в файлах thread1.conf, thread2.conf ... thread9.conf (по умолчанию они отсутствуют) и подключать в конце файла motion.conf

Лично я предпочитаю основные настройки и настройки по умолчанию хранить в основном файле конфигурации, а настройки самих камер вынести во внешние файлы thread, по одному файлу на каждую камеру. Таким образом, не будет никакой путаницы если например камера №2 выйдет из строя и её понадобится заменить на не типичную камеру.

Основные настройки представлены в двух секциях

############################################################ # Daemon ############################################################ # Запуск в фоновом режиме (default: off) daemon on # Файл для хранения идентификатора процесса (default: not defined) process_id_file /var/run/motion/motion.pid ############################################################ # Basic Setup Mode ############################################################ # Запускать в Setup-Mode, отключает режим демона. (default: off) setup_mode off # Путь до файла с логами. (default: not defined) logfile /var/log/motion/motion.log # Уровель лог сообщений (EMR, ALR, CRT, ERR, WRN, NTC, INF, DBG, ALL). (default: 6 / NTC) log_level 6 # Фильтр лог сообщений по типу (COR, STR, ENC, NET, DBL, EVT, TRK, VID, ALL). (default: ALL) log_type all

Тут нас интересует только запуск программы в фоновом режиме, остальные настройки можно оставить без изменения.

########################################################### # Capture device options ############################################################ # Videodevice to be used for capturing (default /dev/video0) # for FreeBSD default is /dev/bktr0 ;videodevice /dev/video0 ...

Закомментируем все настройки в этой секции, в этом файле они нам не нужны.

Отключаем создание снимков при обнаружении движения. Можно указать один из режимов (first, best, center) и тогда мы будем получать по одному снимку на одно видео. Это может пригодиться в том случае, если вы собираетесь организовывать WEB доступ к архиву видеозаписей.

############################################################ # Image File Output ############################################################ # Output "normal" pictures when motion is detected (default: on) # Valid values: on, off, first, best, center # When set to "first", only the first picture of an event is saved. # Picture with most motion of an event is saved when set to "best". # Picture with motion nearest center of picture is saved when set to "center". # Can be used as preview shot for the corresponding movie. output_pictures off

Отключаем стриминг по умолчанию

############################################################ # Live Stream Server ############################################################ # The mini-http server listens to this port for requests (default: 0 = disabled) stream_port 0

Отключаем встроенный web сервер

############################################################ # HTTP Based Control ############################################################ # TCP/IP port for the http server to listen on (default: 0 = disabled) webcontrol_port 0

В самом конце основного файла конфигурации motion.conf имеется секция для подключения дополнительных потоков. По умолчанию файл motion.conf соответствовал файлу thread0.conf, но мы это доблестно исправили выше. Теперь нам необходимо включить в файл конфигурации файлы с настройками для работы с нашими камерами. Мы рассмотрим на примере конфигурации одной камеры, раскомментируем thread1.conf

############################################################## # Thread config files - One for each camera. # Except if only one camera - You only need this config file. # If you have more than one camera you MUST define one thread # config file for each camera in addition to this config file. ############################################################## # Remember: If you have more than one camera you must have one # thread file for each camera. E.g. 2 cameras requires 3 files: # This motion.conf file AND thread1.conf and thread2.conf. # Only put the options that are unique to each camera in the # thread config files. thread /etc/motion/thread1.conf ; thread /etc/motion/thread2.conf ; thread /etc/motion/thread3.conf ; thread /etc/motion/thread4.conf

Создадим файл thread1.conf

Cd /etc/motion && touch thread1.conf && nano thread1.conf

Первым делом вам необходимо узнать:

  • адрес MJPEG потока вашей камеры
  • разрешение камеры (обычно для дешевых камер это 640х480)
  • частоту кадров (у дешевых камер обычно не более 20)
# Разрешение картинки width 640 height 480 # Частота кадров framerate 20

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

# Сетевой адрес для захвата изображения с камеры (mjpeg поток) netcam_url http://10.10.1.101/mjpeg.cgi # Логин и пароль к видео потоку (only if required). Default: not defined ; netcam_userpass admin:password

Укажем подпись для камеры, это поможет быстро идентификации её местоположение (можно использовать символ перевода каретки)

# Подпись к камере (название камеры) text_left CAMERA 1\nSH-21 STORAGE

Укажем путь до места хранения видео фрагментов (лучше создать каталог заранее)

# Путь до места сохранения видео\фото target_dir /media/share/motion/cam1

Включаем детектор движения

# Количество изменившихся пикселей для триггера обнаружения движения threshold 2000

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

# Не реагировать на резкое изменение яркости (0 - отключено, 0-100% от общего числа пикселей) lightswitch 60 # Минимальное число кадров, в которых фиксируется движение для взведения триггера тревоги minimum_motion_frames 5

Добавляем мертвую зону. Она позволит не создавать кучу мусора в папке с видео

# Количество секунд (мертвая зона) после окончания движения, в течении которых отключен детектор event_gap 10

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

# Рисуем рамку вокруг движущегося объекта locate_motion_mode on # Стиль рамки вокруг движущегося объекта locate_motion_style redbox

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

# Показывать количество изменившихся пикселей (используем для настройки threshold) text_changes on

Включаем возможность стримить с сервера все происходящее на данной камере

# Настройки потока вещания (порт\качество\частота кадров\доступ только с 172.0.0.1\ограничение потока) stream_port 8081 stream_quality 70 stream_maxrate 20 stream_localhost off stream_limit 0

Можно (и нужно, если установлен stream_localhost off) требовать авторизацию при доступе к потоку с этой камеры из вне

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

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

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

Скрытый текст

%Y = year %m = month %d = date %H = hour %M = minute %S = second, %v = event %q = frame number %t = thread (camera) number %D = changed pixels %N = noise level %i and %J = width and height of motion area %K and %L = X and Y coordinates of motion center %C = value defined by text_event %f = filename with full path %n = number indicating filetype # Both %f and %n are only defined for on_picture_save, # on_movie_start and on_movie_end # Quotation marks round string are allowed.

Добавим строку вызова внешнего исполняемого файла с передачей ему параметрами - номер камеры и координаты центра начала движения

# При обнаружении движения запускать внешний скрипт on_event_start /etc/motion/zone_alarm.sh %t %K %L

Содержимое zone_alarm.sh

#!/bin/bash CAM = "$1" X = "$2" Y = "$3" if (("$CAM" == "1" )) then if (("$X" < "320" && "$Y" < "240" )) then beep - f 4000 - l 200 - n - f 4000 - l 200 - n - f 4000 - l 200 - n - f 4000 - l 200 - n - f 4000 - l 200 fi fi

В данном случае происходит звуковое оповещение на самом сервере с помощью утилиты beep, по умолчанию её может и не быть

Apt-get install beep

Также можно сигнализировать об обнаружении движения на удаленную станцию или на мобильное устройство. Очень актуально если камера наблюдает за помещением, в котором не бывает и не должно бывать людей. Например, склад.

Необходимо разрешить Motion работать в режиме демона, для этого правим файл /etc/default/motion

# set to "yes" to enable the motion daemon start_motion_daemon = yes

Проверяем работу

/ etc / init . d / motion start

Что мы имеем на выходе


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

Crontab -e

И добавим запись, которая будет выполняться ежедневно в 23:30 и искать файлы формата.avi в указанном нами каталоге и если найденный файл существует более 14 суток, то он удаляется. Естественно если камера стоит в проходном месте, то стоит уменьшить время жизни видео.

30 23 * * * find /media/share/motion/ -name "*.avi" -mtime +14 -exec rm {} \;

На этом настройка завершена.

PS : в файле motion.conf можно найти еще очень много интересных параметров!

166

Для того, чтобы motion стартовал автоматически при запуске системы, попробуйте добавить разрешение на запуск юнита в systemd

systemctl enable motion / lib / systemd / systemd - sysv - install enable motion

перезапускаем систему и смотрим вывод

service motion status

Скрытый текст

Service motion status ● motion . service - LSB : Start Motion detection Loaded : loaded (/ etc / init . d / motion ; bad ; vendor preset : enabled ) Active : active (running ) since Пн 2016 - 10 - 17 15 : 26 : 47 MSK ; 2min 24s ago Docs : man : systemd - sysv - generator (8 ) Process : 568 ExecStart =/ etc / init . d / motion start (code = exited , status = 0 / SUCCESS ) CGroup : / system . slice / motion . service └─ 1324 / usr / bin / motion окт 17 15 : 26 : 46 hostname systemd [ 1 ]: Starting LSB : Start Motion detection ... окт 17 15 : 26 : 47 hostname motion [ 568 ]: * Starting motion detection daemon motion окт 17 15 : 26 : 47 hostname motion [ 568 ]: ... done . окт 17 15 : 26 : 47 hostname systemd [ 1 ]: Started LSB : Start Motion detection . окт 17 15 : 26 : 53 hostname motion [ 796 ]: [ 25567616 ] [ NTC ] [ ALL ] conf_load : Processing thread 0 - config file / etc / motion / motion . conf окт 17 15 : 26 : 53 hostname motion [ 796 ]: [ 25567616 ] [ NTC ] [ ALL ] config_thread : Processing config file / etc / motion / thread1 . conf окт 17 15 : 26 : 53 hostname motion [ 796 ]: [ 25567616 ] [ NTC ] [ ALL ] motion_startup : Motion 3.2 . 12 + git20140228 Started with SDL support окт 17 15 : 26 : 53 hostname motion [ 796 ]: [ 25567616 ] [ NTC ] [ ALL ] motion_startup : Logging to file (/ var / log / motion / motion . log )

Лирика
Добрый день. Мотивированный многочисленными постами на Хабре о самодельных роботах решил сделать и что-нибудь свое более менее стоящее и интересное.

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

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

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

Задача ведь тривиальная, посылать информацию с джойстика на Arduino, которая на определенный угол будет поворачивать 2 сервопривода с прикрепленной веб-камерой, и по необходимости считывать информацию с дальномера, отсылая ее в SerialPort.
Обдумав все еще раз, решил приступить к созданию данного прототипа самостоятельно. Поехали!

Основная часть

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

Фото

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

Программируем Arduino
Тут все казалось очень просто, так как джойстик подключается к ПК, основная обработка видео тоже будет на ПК, то Arduino займется лишь приемом и обработкой информации с ПК и управлением сервоприводами. Поэтому нам надо лишь читать Serial Port, обрабатывать каким-то образом поступающую информацию и как-то на нее реагировать.

Забегая немного вперед сразу скажу, тут и произошла ошибка, к которой мне пришлось вернуться уже после написания программы на C#. Ошибка была вот в чем — я, наивный и полный энтузиазма, написал программку которая разбирает поступающую в Serial Port строку примерно следующего вида «90:90» на две части, соответственно первая часть это градусы по координате X, вторая часть Y. При помощи монитора порта все было оттестировано и работало прекрасно, но когда была написана программа для управления с джойстика, при усиленной атаке порта строками с изменяющимися значениями, Arduino просто не успевала считывать все последовательно, поэтому зачастую строки превращались в «0:909», ":9090" и тому подобное.
Соответственно сервоприводы сходили с ума и принимали все положения, кроме тех, что нужны нам.

Поэтому, не долго думая, я пришел к выводу что нам нужен символ начала строки и символ конца строки. Опять же, не долго думая, символом начала строки был выбран первый символ латинского алфавита — «a», концом строки последний — «z», а символы начала значений осей «x» и «y» соответственно. Итого входная строка принимала следующий вид: «ax90y90z».

Все бы хорошо, если бы не дальномер. Дальномер ультразвуковой, расстояние он определяет на ура, но есть несколько нюансов. Во-первых, если угол между дальномером и стеной острее 45 градусов (плюс-минус), то звук отражается от стены по касательной, и значение, не соответствует действительности. Во-вторых довольно большой угол испускания сигнала, около 30 градусов(по мануалу), а замеряется расстояние до ближайшего объекта, благо что сигнал от объектов к которым датчик находится под углом, отражается в другую сторону, и мы получаем более менее реальное расстояние по прямой, но помехи все же бывают, и довольно часто. Поэтому я дописал еще одну функцию, которая берет n замеров расстояния, складывает их и делит на кол-во, выставил n=10, так помехи стали более сглажены и менее заметны.

Код на Arduino был тут же переписан и принял следующий вид:

Код Arduino

#include #include /* Тут реализован алгоритм приема строки строка должна быть вида ax180y180z Где a - символ начала строки x - символ начала координат x y - символ начала координат y z - символ конца строки */ String str_X=""; String str_Y=""; int XY_Flag=0; // 1 = X, 2 = Y Servo X_Servo; Servo Y_Servo; const int distancePin = 12; const int distancePin2 = 11; void setup() { Serial.begin(115200); X_Servo.attach(7); Y_Servo.attach(8); } void loop() { delay(50); if(Serial.available()>0) //считываем значения из порта { int inChar=Serial.read(); //считываем байт if(inChar == 97) { // Если это начало строки while(Serial.available()>0) { inChar=Serial.read(); //считываем байт if(inChar==120){ // x XY_Flag=1; continue; } if(inChar==121){ // y XY_Flag=2; continue; } if(inChar==122){ // z (конец строки) XY_Flag=0; } if(XY_Flag==0) break; // Если конец строки, то досрочный выход из цикла if(XY_Flag==1) str_X +=(char)inChar; //если X, то пишем в X if(XY_Flag==2) str_Y +=(char)inChar; //Если Y, то пишем в Y } if(XY_Flag==0) // Если был конец строки, то выполняем... { servo(str_X.toInt(), str_Y.toInt()); str_X=""; str_Y=""; //очищаем переменные Serial.println("d" + String(trueDistance()) + "z"); } } } } void servo(int x, int y){ //говорим сервоприводам сколько градусов им нужно взять:) X_Servo.write(x); Y_Servo.write(y); } long trueDistance() //считываем датчик n раз и возвращаем среднее значение { int n=10; long _value=0; for(int i =0; i

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

Основная управляющая программа (C#)
По началу хотел писать все на C++ под Qt, но в последствии все же пришлось писать на C#, ну да ладно.

Что хотелось получить:
1. Распознавание лиц людей.
2. Слежение за лицом человека.
3. Ручное управление с помощью джойстика.
4. Определение расстояния до объекта.

Для распознавания лиц и вывода изображения с веб-камеры, без всяких вопросов, была выбрана библиотека OpenCV, а вернее ее оболочка для C# — Emgu CV.

Для считывания положения джойстика по началу использовалась библиотека Microsoft.DirectX.DirectInput, которая мне жутко не понравилась, и я применил библиотеку SharpDX, притом довольно успешно.

Что требовалось от программы:
1. Захватывать изображение с веб-камеры и выводить его на экран.
2. Распознавать лица на изображении, обводить их и получать координаты лица на изображении.
3. Формировать строку вида «ax90y90z» и отправлять ее в Serial Port для управления сервоприводами.
4. Считывать значения положения джойстика.
5. Считывать показания с дальномера.

Сформулировав задачи, приступаем к программированию.

Библиотечка SharpDX позволяет нам находить подключенный джойстик и получать с него значения осей (от 0 до 65535), нажатие и отпускание клавиш джойстика. Сервоприводы могут поворачиваться от 0 до 180 градусов, соответственно нужно преобразовывать значения осей джойстика от 0 до 180. Я просто поделил возвращаемое значение на 363, и получил на выходе значения от 0 до 180. Далее написал функцию которая формирует строку положения сервоприводов и отправляет ее в порт.

Вывод изображения и распознавание лиц написаны с использованием OpenCV и ничего сложного не представляют (для нас).

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

Код (на всякий случай с подробными комментариями, по возможности):

Класс формы

Capture myCapture; private bool captureInProgress = false; string _distance = "0"; string coords; int X_joy = 90; int Y_joy = 90; SerialPort _serialPort = new SerialPort(); Image image; DirectInput directInput; Guid joystickGuid; Joystick joystick; Thread th; private int GRAD_TURN_X = 2; private int GRAD_TURN_Y = 2; private void GetVideo(object sender, EventArgs e) { myCapture.FlipHorizontal = true; image = myCapture.QueryFrame(); try { // Image gray = image.Convert().Canny(100, 60); // CamImageBoxGray.Image = gray; } catch { } /*детектор лиц */ if (FaceCheck.Checked) { List faces = new List(); DetectFace.Detect(image, "haarcascade_frontalface_default.xml", "haarcascade_eye.xml", faces); foreach (System.Drawing.Rectangle face in faces) { image.Draw(face, new Bgr(System.Drawing.Color.Red), 2); int faceX = face.X + face.Width / 2; int faceY = face.Y + face.Height / 2; if ((faceX - 320 > 120) || (faceX - 320 < -120)) //Чем дальше от центра изображения лицо, тем быстрее двигаем камеру GRAD_TURN_X = 4; else if ((faceX - 320 > 80) || (faceX - 320 < -80)) GRAD_TURN_X = 3; else GRAD_TURN_X = 2; if ((faceY - 240 > 120) || (faceY - 240 < -120)) GRAD_TURN_Y = 4; else if ((faceY - 240 > 80) || (faceY - 240 < -80)) GRAD_TURN_Y = 3; else GRAD_TURN_Y = 2; label7.Text = faceX.ToString(); label8.Text = faceY.ToString(); if (!JoyCheck.Checked) { if (faceX > 370) X_joy += GRAD_TURN_X; else if (faceX < 290) X_joy -= GRAD_TURN_X; if (faceY > 270) Y_joy -= GRAD_TURN_Y; else if (faceY < 210) Y_joy += GRAD_TURN_Y; serialPortWrite(X_joy, Y_joy); } } } /*=============*/ System.Drawing.Rectangle rect1 = new System.Drawing.Rectangle(305, 240, 30, 1); System.Drawing.Rectangle rect2 = new System.Drawing.Rectangle(320, 225, 1, 30); System.Drawing.Rectangle rect3 = new System.Drawing.Rectangle(0, 0, 640, 22); image.Draw(rect1, new Bgr(System.Drawing.Color.Yellow), 1); image.Draw(rect2, new Bgr(System.Drawing.Color.Yellow), 1); image.Draw(rect3, new Bgr(System.Drawing.Color.Black), 22); MCvFont f = new MCvFont(FONT.CV_FONT_HERSHEY_TRIPLEX, 0.9, 0.9); image.Draw("Distance: " + _distance + " cm", ref f, new System.Drawing.Point(0, 30), new Bgr(0, 255, 255)); CamImageBox.Image = image; if (JoyCheck.Checked) { th = new Thread(joy); // ручное управление, запускаем в потоке th.Start(); } label1.Text = X_joy.ToString(); label2.Text = Y_joy.ToString(); label3.Text = coords; } private void ReleaseData() { if (myCapture != null) myCapture.Dispose(); } public Form1() { InitializeComponent(); } private void serialPortWrite(int X, int Y) //отсылаем ардуине координаты и читаем из порта дистанцию { try { coords = "ax" + X + "y" + Y + "z"; _serialPort.Write(coords); _distance = _serialPort.ReadLine(); if (_distance == "d") if (_distance[_distance.Length - 2] == "z") { _distance = _distance.Remove(_distance.LastIndexOf("z")).Replace("d", " "); } else _distance = "0"; else _distance = "0"; } catch { } } private void joy() //ручное управление джойстиком { joystick.Poll(); var datas = joystick.GetBufferedData(); foreach (var state in datas) { if (state.Offset.ToString() == "X") X_joy = 180 - (state.Value / 363); else if (state.Offset.ToString() == "Y") Y_joy = state.Value / 363; } serialPortWrite(X_joy, Y_joy); } private void Form1_Load(object sender, EventArgs e) { if (myCapture == null) { try { myCapture = new Capture(); } catch (NullReferenceException excpt) { MessageBox.Show(excpt.Message); } } if (myCapture != null) { if (captureInProgress) { Application.Idle -= GetVideo; } else { Application.Idle += GetVideo; } captureInProgress = !captureInProgress; } _serialPort.PortName = "COM3"; _serialPort.BaudRate = 115200; if (_serialPort.IsOpen) _serialPort.Close(); if (!_serialPort.IsOpen) _serialPort.Open(); directInput = new DirectInput(); joystickGuid = Guid.Empty; foreach (var deviceInstance in directInput.GetDevices(DeviceType.Gamepad, DeviceEnumerationFlags.AllDevices)) joystickGuid = deviceInstance.InstanceGuid; if (joystickGuid == Guid.Empty) foreach (var deviceInstance in directInput.GetDevices(DeviceType.Joystick, DeviceEnumerationFlags.AllDevices)) joystickGuid = deviceInstance.InstanceGuid; joystick = new Joystick(directInput, joystickGuid); joystick.Properties.BufferSize = 128; joystick.Acquire(); } private void JoyCheck_CheckedChanged(object sender, EventArgs e) { if (FaceCheck.Checked) FaceCheck.Checked = !JoyCheck.Checked; } private void FaceCheck_CheckedChanged(object sender, EventArgs e) { if (JoyCheck.Checked) JoyCheck.Checked = !FaceCheck.Checked; } private void RadarPaint() { Bitmap map = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); Graphics g = Graphics.FromImage(map); var p = new Pen(System.Drawing.Color.Black, 2); System.Drawing.Point p1 = new System.Drawing.Point(); System.Drawing.Point p2 = new System.Drawing.Point(); System.Drawing.Point p3 = new System.Drawing.Point(); System.Drawing.Point p4 = new System.Drawing.Point(); p1.X = pictureBox1.Size.Width/2 ; //начало координат переводим в удобное нам место p1.Y = pictureBox1.Size.Height; //посередине pictureBox"a внизу for (int i = 0; i < 181; i++) { serialPortWrite(i, 90); p2.X = Convert.ToInt32(Math.Ceiling(320 + int.Parse(_distance) * Math.Cos(i * Math.PI / 180))); //считаем координаты точки p2.Y = Convert.ToInt32(Math.Ceiling(480 - int.Parse(_distance) * Math.Sin(i * Math.PI / 180))); if (i > 0) g.DrawLine(p, p2, p3); if (i % 18 == 0) { p4 = p2; p4.Y -= 50; g.DrawString(_distance, new Font("Arial", 18), new SolidBrush(System.Drawing.Color.Red), p4); } p3.X = p2.X; p3.Y = p2.Y; g.DrawLine(p, p1, p2); try { pictureBox1.Image = map; } catch (Exception e) { MessageBox.Show(e.Message); } } } private void button1_Click(object sender, EventArgs e) { if (FaceCheck.Checked || JoyCheck.Checked) { FaceCheck.Checked = false; JoyCheck.Checked = false; } Thread t = new Thread(RadarPaint); t.Start(); }


Класс DetectFace

class DetectFace { public static void Detect(Image image, String faceFileName, String eyeFileName, List faces) { CascadeClassifier face = new CascadeClassifier(faceFileName); // CascadeClassifier eye = new CascadeClassifier(eyeFileName); Image gray = image.Convert(); gray._EqualizeHist(); Rectangle facesDetected = face.DetectMultiScale(gray, 1.1, 5, new Size(70, 70), Size.Empty); faces.AddRange(facesDetected); } }

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

Видео
Вот, что получилось по завершении.



Заключение

Итоги
Оказалось все довольно просто. Цель достигнута, прототип готов. Есть над чем работать и заняться в свободное время, ожидая посылку с компонентами для робота.
Планы на будущее
Следующим шагом будет построение колесной платформы для робота, настройка удаленного управления (WiFi, 3G)., навешивание датчиков (температура, давление и прочее), синтез речи. В хотелках так же имеются планы по поводу механической руки.

Думаю, если будет интерес к данной статье и ее продолжению, то оно обязательно последует! Исправления и критика приветствуются!

Спасибо за внимание!

Лирика

Добрый день. Мотивированный многочисленными постами на Хабре о самодельных роботах решил сделать и что-нибудь свое более менее стоящее и интересное.

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

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

Что меня сподвигло написать эту статью?

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

Задача ведь тривиальная, посылать информацию с джойстика на Arduino, которая на определенный угол будет поворачивать 2 сервопривода с прикрепленной веб-камерой, и по необходимости считывать информацию с дальномера, отсылая ее в SerialPort.
Обдумав все еще раз, решил приступить к созданию данного прототипа самостоятельно. Поехали!

Основная часть

Сборка прототипа

Прототип был создан в течение 5 минут. Внешний вид прототипа не интересует вообще, основная его цель - отработка программной части до приезда деталей для робота.
А сделал я его из первой попавшейся баночки из под каких-то витаминов, двух сервоприводов, веб-камеры, скрепки, изоленты и клеевого пистолета. Получилось следующее:

Фото

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

Программируем Arduino

Тут все казалось очень просто, так как джойстик подключается к ПК, основная обработка видео тоже будет на ПК, то Arduino займется лишь приемом и обработкой информации с ПК и управлением сервоприводами. Поэтому нам надо лишь читать Serial Port, обрабатывать каким-то образом поступающую информацию и как-то на нее реагировать.

Забегая немного вперед сразу скажу, тут и произошла ошибка, к которой мне пришлось вернуться уже после написания программы на C#. Ошибка была вот в чем - я, наивный и полный энтузиазма, написал программку которая разбирает поступающую в Serial Port строку примерно следующего вида «90:90» на две части, соответственно первая часть это градусы по координате X, вторая часть Y. При помощи монитора порта все было оттестировано и работало прекрасно, но когда была написана программа для управления с джойстика, при усиленной атаке порта строками с изменяющимися значениями, Arduino просто не успевала считывать все последовательно, поэтому зачастую строки превращались в «0:909», ":9090" и тому подобное.
Соответственно сервоприводы сходили с ума и принимали все положения, кроме тех, что нужны нам.

Поэтому, не долго думая, я пришел к выводу что нам нужен символ начала строки и символ конца строки. Опять же, не долго думая, символом начала строки был выбран первый символ латинского алфавита - «a», концом строки последний - «z», а символы начала значений осей «x» и «y» соответственно. Итого входная строка принимала следующий вид: «ax90y90z».

Все бы хорошо, если бы не дальномер. Дальномер ультразвуковой, расстояние он определяет на ура, но есть несколько нюансов. Во-первых, если угол между дальномером и стеной острее 45 градусов (плюс-минус), то звук отражается от стены по касательной, и значение, не соответствует действительности. Во-вторых довольно большой угол испускания сигнала, около 30 градусов(по мануалу), а замеряется расстояние до ближайшего объекта, благо что сигнал от объектов к которым датчик находится под углом, отражается в другую сторону, и мы получаем более менее реальное расстояние по прямой, но помехи все же бывают, и довольно часто. Поэтому я дописал еще одну функцию, которая берет n замеров расстояния, складывает их и делит на кол-во, выставил n=10, так помехи стали более сглажены и менее заметны.

Код на Arduino был тут же переписан и принял следующий вид:

Код Arduino

#include #include /* Тут реализован алгоритм приема строки строка должна быть вида ax180y180z Где a - символ начала строки x - символ начала координат x y - символ начала координат y z - символ конца строки */ String str_X=""; String str_Y=""; int XY_Flag=0; // 1 = X, 2 = Y Servo X_Servo; Servo Y_Servo; const int distancePin = 12; const int distancePin2 = 11; void setup() { Serial.begin(115200); X_Servo.attach(7); Y_Servo.attach(8); } void loop() { delay(50); if(Serial.available()>0) //считываем значения из порта { int inChar=Serial.read(); //считываем байт if(inChar == 97) { // Если это начало строки while(Serial.available()>0) { inChar=Serial.read(); //считываем байт if(inChar==120){ // x XY_Flag=1; continue; } if(inChar==121){ // y XY_Flag=2; continue; } if(inChar==122){ // z (конец строки) XY_Flag=0; } if(XY_Flag==0) break; // Если конец строки, то досрочный выход из цикла if(XY_Flag==1) str_X +=(char)inChar; //если X, то пишем в X if(XY_Flag==2) str_Y +=(char)inChar; //Если Y, то пишем в Y } if(XY_Flag==0) // Если был конец строки, то выполняем... { servo(str_X.toInt(), str_Y.toInt()); str_X=""; str_Y=""; //очищаем переменные Serial.println("d" + String(trueDistance()) + "z"); } } } } void servo(int x, int y){ //говорим сервоприводам сколько градусов им нужно взять:) X_Servo.write(x); Y_Servo.write(y); } long trueDistance() //считываем датчик n раз и возвращаем среднее значение { int n=10; long _value=0; for(int i =0; i

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

Основная управляющая программа (C#)

По началу хотел писать все на C++ под Qt, но в последствии все же пришлось писать на C#, ну да ладно.

Что хотелось получить:
1. Распознавание лиц людей.
2. Слежение за лицом человека.
3. Ручное управление с помощью джойстика.
4. Определение расстояния до объекта.

Для распознавания лиц и вывода изображения с веб-камеры, без всяких вопросов, была выбрана библиотека OpenCV, а вернее ее оболочка для C# - Emgu CV.

Для считывания положения джойстика по началу использовалась библиотека Microsoft.DirectX.DirectInput, которая мне жутко не понравилась, и я применил библиотеку SharpDX, притом довольно успешно.

Что требовалось от программы:
1. Захватывать изображение с веб-камеры и выводить его на экран.
2. Распознавать лица на изображении, обводить их и получать координаты лица на изображении.
3. Формировать строку вида «ax90y90z» и отправлять ее в Serial Port для управления сервоприводами.
4. Считывать значения положения джойстика.
5. Считывать показания с дальномера.

Сформулировав задачи, приступаем к программированию.

Библиотечка SharpDX позволяет нам находить подключенный джойстик и получать с него значения осей (от 0 до 65535), нажатие и отпускание клавиш джойстика. Сервоприводы могут поворачиваться от 0 до 180 градусов, соответственно нужно преобразовывать значения осей джойстика от 0 до 180. Я просто поделил возвращаемое значение на 363, и получил на выходе значения от 0 до 180. Далее написал функцию которая формирует строку положения сервоприводов и отправляет ее в порт.

Вывод изображения и распознавание лиц написаны с использованием OpenCV и ничего сложного не представляют (для нас).

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

Код (на всякий случай с подробными комментариями, по возможности):

Класс формы

Capture myCapture; private bool captureInProgress = false; string _distance = "0"; string coords; int X_joy = 90; int Y_joy = 90; SerialPort _serialPort = new SerialPort(); Image image; DirectInput directInput; Guid joystickGuid; Joystick joystick; Thread th; private int GRAD_TURN_X = 2; private int GRAD_TURN_Y = 2; private void GetVideo(object sender, EventArgs e) { myCapture.FlipHorizontal = true; image = myCapture.QueryFrame(); try { // Image gray = image.Convert().Canny(100, 60); // CamImageBoxGray.Image = gray; } catch { } /*детектор лиц */ if (FaceCheck.Checked) { List faces = new List(); DetectFace.Detect(image, "haarcascade_frontalface_default.xml", "haarcascade_eye.xml", faces); foreach (System.Drawing.Rectangle face in faces) { image.Draw(face, new Bgr(System.Drawing.Color.Red), 2); int faceX = face.X + face.Width / 2; int faceY = face.Y + face.Height / 2; if ((faceX - 320 > 120) || (faceX - 320 < -120)) //Чем дальше от центра изображения лицо, тем быстрее двигаем камеру GRAD_TURN_X = 4; else if ((faceX - 320 > 80) || (faceX - 320 < -80)) GRAD_TURN_X = 3; else GRAD_TURN_X = 2; if ((faceY - 240 > 120) || (faceY - 240 < -120)) GRAD_TURN_Y = 4; else if ((faceY - 240 > 80) || (faceY - 240 < -80)) GRAD_TURN_Y = 3; else GRAD_TURN_Y = 2; label7.Text = faceX.ToString(); label8.Text = faceY.ToString(); if (!JoyCheck.Checked) { if (faceX > 370) X_joy += GRAD_TURN_X; else if (faceX < 290) X_joy -= GRAD_TURN_X; if (faceY > 270) Y_joy -= GRAD_TURN_Y; else if (faceY < 210) Y_joy += GRAD_TURN_Y; serialPortWrite(X_joy, Y_joy); } } } /*=============*/ System.Drawing.Rectangle rect1 = new System.Drawing.Rectangle(305, 240, 30, 1); System.Drawing.Rectangle rect2 = new System.Drawing.Rectangle(320, 225, 1, 30); System.Drawing.Rectangle rect3 = new System.Drawing.Rectangle(0, 0, 640, 22); image.Draw(rect1, new Bgr(System.Drawing.Color.Yellow), 1); image.Draw(rect2, new Bgr(System.Drawing.Color.Yellow), 1); image.Draw(rect3, new Bgr(System.Drawing.Color.Black), 22); MCvFont f = new MCvFont(FONT.CV_FONT_HERSHEY_TRIPLEX, 0.9, 0.9); image.Draw("Distance: " + _distance + " cm", ref f, new System.Drawing.Point(0, 30), new Bgr(0, 255, 255)); CamImageBox.Image = image; if (JoyCheck.Checked) { th = new Thread(joy); // ручное управление, запускаем в потоке th.Start(); } label1.Text = X_joy.ToString(); label2.Text = Y_joy.ToString(); label3.Text = coords; } private void ReleaseData() { if (myCapture != null) myCapture.Dispose(); } public Form1() { InitializeComponent(); } private void serialPortWrite(int X, int Y) //отсылаем ардуине координаты и читаем из порта дистанцию { try { coords = "ax" + X + "y" + Y + "z"; _serialPort.Write(coords); _distance = _serialPort.ReadLine(); if (_distance == "d") if (_distance[_distance.Length - 2] == "z") { _distance = _distance.Remove(_distance.LastIndexOf("z")).Replace("d", " "); } else _distance = "0"; else _distance = "0"; } catch { } } private void joy() //ручное управление джойстиком { joystick.Poll(); var datas = joystick.GetBufferedData(); foreach (var state in datas) { if (state.Offset.ToString() == "X") X_joy = 180 - (state.Value / 363); else if (state.Offset.ToString() == "Y") Y_joy = state.Value / 363; } serialPortWrite(X_joy, Y_joy); } private void Form1_Load(object sender, EventArgs e) { if (myCapture == null) { try { myCapture = new Capture(); } catch (NullReferenceException excpt) { MessageBox.Show(excpt.Message); } } if (myCapture != null) { if (captureInProgress) { Application.Idle -= GetVideo; } else { Application.Idle += GetVideo; } captureInProgress = !captureInProgress; } _serialPort.PortName = "COM3"; _serialPort.BaudRate = 115200; if (_serialPort.IsOpen) _serialPort.Close(); if (!_serialPort.IsOpen) _serialPort.Open(); directInput = new DirectInput(); joystickGuid = Guid.Empty; foreach (var deviceInstance in directInput.GetDevices(DeviceType.Gamepad, DeviceEnumerationFlags.AllDevices)) joystickGuid = deviceInstance.InstanceGuid; if (joystickGuid == Guid.Empty) foreach (var deviceInstance in directInput.GetDevices(DeviceType.Joystick, DeviceEnumerationFlags.AllDevices)) joystickGuid = deviceInstance.InstanceGuid; joystick = new Joystick(directInput, joystickGuid); joystick.Properties.BufferSize = 128; joystick.Acquire(); } private void JoyCheck_CheckedChanged(object sender, EventArgs e) { if (FaceCheck.Checked) FaceCheck.Checked = !JoyCheck.Checked; } private void FaceCheck_CheckedChanged(object sender, EventArgs e) { if (JoyCheck.Checked) JoyCheck.Checked = !FaceCheck.Checked; } private void RadarPaint() { Bitmap map = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); Graphics g = Graphics.FromImage(map); var p = new Pen(System.Drawing.Color.Black, 2); System.Drawing.Point p1 = new System.Drawing.Point(); System.Drawing.Point p2 = new System.Drawing.Point(); System.Drawing.Point p3 = new System.Drawing.Point(); System.Drawing.Point p4 = new System.Drawing.Point(); p1.X = pictureBox1.Size.Width/2 ; //начало координат переводим в удобное нам место p1.Y = pictureBox1.Size.Height; //посередине pictureBox"a внизу for (int i = 0; i < 181; i++) { serialPortWrite(i, 90); p2.X = Convert.ToInt32(Math.Ceiling(320 + int.Parse(_distance) * Math.Cos(i * Math.PI / 180))); //считаем координаты точки p2.Y = Convert.ToInt32(Math.Ceiling(480 - int.Parse(_distance) * Math.Sin(i * Math.PI / 180))); if (i > 0) g.DrawLine(p, p2, p3); if (i % 18 == 0) { p4 = p2; p4.Y -= 50; g.DrawString(_distance, new Font("Arial", 18), new SolidBrush(System.Drawing.Color.Red), p4); } p3.X = p2.X; p3.Y = p2.Y; g.DrawLine(p, p1, p2); try { pictureBox1.Image = map; } catch (Exception e) { MessageBox.Show(e.Message); } } } private void button1_Click(object sender, EventArgs e) { if (FaceCheck.Checked || JoyCheck.Checked) { FaceCheck.Checked = false; JoyCheck.Checked = false; } Thread t = new Thread(RadarPaint); t.Start(); }

Класс DetectFace

Class DetectFace { public static void Detect(Image image, String faceFileName, String eyeFileName, List faces) { CascadeClassifier face = new CascadeClassifier(faceFileName); // CascadeClassifier eye = new CascadeClassifier(eyeFileName); Image gray = image.Convert(); gray._EqualizeHist(); Rectangle facesDetected = face.DetectMultiScale(gray, 1.1, 5, new Size(70, 70), Size.Empty); faces.AddRange(facesDetected); } }

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

Видео

Вот, что получилось по завершении.



Заключение

Итоги

Оказалось все довольно просто. Цель достигнута, прототип готов. Есть над чем работать и заняться в свободное время, ожидая посылку с компонентами для робота.

Планы на будущее

Следующим шагом будет построение колесной платформы для робота, настройка удаленного управления (WiFi, 3G)., навешивание датчиков (температура, давление и прочее), синтез речи. В хотелках так же имеются планы по поводу механической руки.

Думаю, если будет интерес к данной статье и ее продолжению, то оно обязательно последует! Исправления и критика приветствуются!

Спасибо за внимание!

Лирика
Добрый день. Мотивированный многочисленными постами на Хабре о самодельных роботах решил сделать и что-нибудь свое более менее стоящее и интересное.

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

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

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

Задача ведь тривиальная, посылать информацию с джойстика на Arduino, которая на определенный угол будет поворачивать 2 сервопривода с прикрепленной веб-камерой, и по необходимости считывать информацию с дальномера, отсылая ее в SerialPort.
Обдумав все еще раз, решил приступить к созданию данного прототипа самостоятельно. Поехали!

Основная часть

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

Фото

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

Программируем Arduino
Тут все казалось очень просто, так как джойстик подключается к ПК, основная обработка видео тоже будет на ПК, то Arduino займется лишь приемом и обработкой информации с ПК и управлением сервоприводами. Поэтому нам надо лишь читать Serial Port, обрабатывать каким-то образом поступающую информацию и как-то на нее реагировать.

Забегая немного вперед сразу скажу, тут и произошла ошибка, к которой мне пришлось вернуться уже после написания программы на C#. Ошибка была вот в чем - я, наивный и полный энтузиазма, написал программку которая разбирает поступающую в Serial Port строку примерно следующего вида «90:90» на две части, соответственно первая часть это градусы по координате X, вторая часть Y. При помощи монитора порта все было оттестировано и работало прекрасно, но когда была написана программа для управления с джойстика, при усиленной атаке порта строками с изменяющимися значениями, Arduino просто не успевала считывать все последовательно, поэтому зачастую строки превращались в «0:909», ":9090" и тому подобное.
Соответственно сервоприводы сходили с ума и принимали все положения, кроме тех, что нужны нам.

Поэтому, не долго думая, я пришел к выводу что нам нужен символ начала строки и символ конца строки. Опять же, не долго думая, символом начала строки был выбран первый символ латинского алфавита - «a», концом строки последний - «z», а символы начала значений осей «x» и «y» соответственно. Итого входная строка принимала следующий вид: «ax90y90z».

Все бы хорошо, если бы не дальномер. Дальномер ультразвуковой, расстояние он определяет на ура, но есть несколько нюансов. Во-первых, если угол между дальномером и стеной острее 45 градусов (плюс-минус), то звук отражается от стены по касательной, и значение, не соответствует действительности. Во-вторых довольно большой угол испускания сигнала, около 30 градусов(по мануалу), а замеряется расстояние до ближайшего объекта, благо что сигнал от объектов к которым датчик находится под углом, отражается в другую сторону, и мы получаем более менее реальное расстояние по прямой, но помехи все же бывают, и довольно часто. Поэтому я дописал еще одну функцию, которая берет n замеров расстояния, складывает их и делит на кол-во, выставил n=10, так помехи стали более сглажены и менее заметны.

Код на Arduino был тут же переписан и принял следующий вид:

Код Arduino

#include #include /* Тут реализован алгоритм приема строки строка должна быть вида ax180y180z Где a - символ начала строки x - символ начала координат x y - символ начала координат y z - символ конца строки */ String str_X=""; String str_Y=""; int XY_Flag=0; // 1 = X, 2 = Y Servo X_Servo; Servo Y_Servo; const int distancePin = 12; const int distancePin2 = 11; void setup() { Serial.begin(115200); X_Servo.attach(7); Y_Servo.attach(8); } void loop() { delay(50); if(Serial.available()>0) //считываем значения из порта { int inChar=Serial.read(); //считываем байт if(inChar == 97) { // Если это начало строки while(Serial.available()>0) { inChar=Serial.read(); //считываем байт if(inChar==120){ // x XY_Flag=1; continue; } if(inChar==121){ // y XY_Flag=2; continue; } if(inChar==122){ // z (конец строки) XY_Flag=0; } if(XY_Flag==0) break; // Если конец строки, то досрочный выход из цикла if(XY_Flag==1) str_X +=(char)inChar; //если X, то пишем в X if(XY_Flag==2) str_Y +=(char)inChar; //Если Y, то пишем в Y } if(XY_Flag==0) // Если был конец строки, то выполняем... { servo(str_X.toInt(), str_Y.toInt()); str_X=""; str_Y=""; //очищаем переменные Serial.println("d" + String(trueDistance()) + "z"); } } } } void servo(int x, int y){ //говорим сервоприводам сколько градусов им нужно взять:) X_Servo.write(x); Y_Servo.write(y); } long trueDistance() //считываем датчик n раз и возвращаем среднее значение { int n=10; long _value=0; for(int i =0; i

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

Основная управляющая программа (C#)
По началу хотел писать все на C++ под Qt, но в последствии все же пришлось писать на C#, ну да ладно.

Что хотелось получить:
1. Распознавание лиц людей.
2. Слежение за лицом человека.
3. Ручное управление с помощью джойстика.
4. Определение расстояния до объекта.

Для распознавания лиц и вывода изображения с веб-камеры, без всяких вопросов, была выбрана библиотека OpenCV, а вернее ее оболочка для C# - Emgu CV.

Для считывания положения джойстика по началу использовалась библиотека Microsoft.DirectX.DirectInput, которая мне жутко не понравилась, и я применил библиотеку SharpDX, притом довольно успешно.

Что требовалось от программы:
1. Захватывать изображение с веб-камеры и выводить его на экран.
2. Распознавать лица на изображении, обводить их и получать координаты лица на изображении.
3. Формировать строку вида «ax90y90z» и отправлять ее в Serial Port для управления сервоприводами.
4. Считывать значения положения джойстика.
5. Считывать показания с дальномера.

Сформулировав задачи, приступаем к программированию.

Библиотечка SharpDX позволяет нам находить подключенный джойстик и получать с него значения осей (от 0 до 65535), нажатие и отпускание клавиш джойстика. Сервоприводы могут поворачиваться от 0 до 180 градусов, соответственно нужно преобразовывать значения осей джойстика от 0 до 180. Я просто поделил возвращаемое значение на 363, и получил на выходе значения от 0 до 180. Далее написал функцию которая формирует строку положения сервоприводов и отправляет ее в порт.

Вывод изображения и распознавание лиц написаны с использованием OpenCV и ничего сложного не представляют (для нас).

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

Код (на всякий случай с подробными комментариями, по возможности):

Класс формы

Capture myCapture; private bool captureInProgress = false; string _distance = "0"; string coords; int X_joy = 90; int Y_joy = 90; SerialPort _serialPort = new SerialPort(); Image image; DirectInput directInput; Guid joystickGuid; Joystick joystick; Thread th; private int GRAD_TURN_X = 2; private int GRAD_TURN_Y = 2; private void GetVideo(object sender, EventArgs e) { myCapture.FlipHorizontal = true; image = myCapture.QueryFrame(); try { // Image gray = image.Convert().Canny(100, 60); // CamImageBoxGray.Image = gray; } catch { } /*детектор лиц */ if (FaceCheck.Checked) { List faces = new List(); DetectFace.Detect(image, "haarcascade_frontalface_default.xml", "haarcascade_eye.xml", faces); foreach (System.Drawing.Rectangle face in faces) { image.Draw(face, new Bgr(System.Drawing.Color.Red), 2); int faceX = face.X + face.Width / 2; int faceY = face.Y + face.Height / 2; if ((faceX - 320 > 120) || (faceX - 320 < -120)) //Чем дальше от центра изображения лицо, тем быстрее двигаем камеру GRAD_TURN_X = 4; else if ((faceX - 320 > 80) || (faceX - 320 < -80)) GRAD_TURN_X = 3; else GRAD_TURN_X = 2; if ((faceY - 240 > 120) || (faceY - 240 < -120)) GRAD_TURN_Y = 4; else if ((faceY - 240 > 80) || (faceY - 240 < -80)) GRAD_TURN_Y = 3; else GRAD_TURN_Y = 2; label7.Text = faceX.ToString(); label8.Text = faceY.ToString(); if (!JoyCheck.Checked) { if (faceX > 370) X_joy += GRAD_TURN_X; else if (faceX < 290) X_joy -= GRAD_TURN_X; if (faceY > 270) Y_joy -= GRAD_TURN_Y; else if (faceY < 210) Y_joy += GRAD_TURN_Y; serialPortWrite(X_joy, Y_joy); } } } /*=============*/ System.Drawing.Rectangle rect1 = new System.Drawing.Rectangle(305, 240, 30, 1); System.Drawing.Rectangle rect2 = new System.Drawing.Rectangle(320, 225, 1, 30); System.Drawing.Rectangle rect3 = new System.Drawing.Rectangle(0, 0, 640, 22); image.Draw(rect1, new Bgr(System.Drawing.Color.Yellow), 1); image.Draw(rect2, new Bgr(System.Drawing.Color.Yellow), 1); image.Draw(rect3, new Bgr(System.Drawing.Color.Black), 22); MCvFont f = new MCvFont(FONT.CV_FONT_HERSHEY_TRIPLEX, 0.9, 0.9); image.Draw("Distance: " + _distance + " cm", ref f, new System.Drawing.Point(0, 30), new Bgr(0, 255, 255)); CamImageBox.Image = image; if (JoyCheck.Checked) { th = new Thread(joy); // ручное управление, запускаем в потоке th.Start(); } label1.Text = X_joy.ToString(); label2.Text = Y_joy.ToString(); label3.Text = coords; } private void ReleaseData() { if (myCapture != null) myCapture.Dispose(); } public Form1() { InitializeComponent(); } private void serialPortWrite(int X, int Y) //отсылаем ардуине координаты и читаем из порта дистанцию { try { coords = "ax" + X + "y" + Y + "z"; _serialPort.Write(coords); _distance = _serialPort.ReadLine(); if (_distance == "d") if (_distance[_distance.Length - 2] == "z") { _distance = _distance.Remove(_distance.LastIndexOf("z")).Replace("d", " "); } else _distance = "0"; else _distance = "0"; } catch { } } private void joy() //ручное управление джойстиком { joystick.Poll(); var datas = joystick.GetBufferedData(); foreach (var state in datas) { if (state.Offset.ToString() == "X") X_joy = 180 - (state.Value / 363); else if (state.Offset.ToString() == "Y") Y_joy = state.Value / 363; } serialPortWrite(X_joy, Y_joy); } private void Form1_Load(object sender, EventArgs e) { if (myCapture == null) { try { myCapture = new Capture(); } catch (NullReferenceException excpt) { MessageBox.Show(excpt.Message); } } if (myCapture != null) { if (captureInProgress) { Application.Idle -= GetVideo; } else { Application.Idle += GetVideo; } captureInProgress = !captureInProgress; } _serialPort.PortName = "COM3"; _serialPort.BaudRate = 115200; if (_serialPort.IsOpen) _serialPort.Close(); if (!_serialPort.IsOpen) _serialPort.Open(); directInput = new DirectInput(); joystickGuid = Guid.Empty; foreach (var deviceInstance in directInput.GetDevices(DeviceType.Gamepad, DeviceEnumerationFlags.AllDevices)) joystickGuid = deviceInstance.InstanceGuid; if (joystickGuid == Guid.Empty) foreach (var deviceInstance in directInput.GetDevices(DeviceType.Joystick, DeviceEnumerationFlags.AllDevices)) joystickGuid = deviceInstance.InstanceGuid; joystick = new Joystick(directInput, joystickGuid); joystick.Properties.BufferSize = 128; joystick.Acquire(); } private void JoyCheck_CheckedChanged(object sender, EventArgs e) { if (FaceCheck.Checked) FaceCheck.Checked = !JoyCheck.Checked; } private void FaceCheck_CheckedChanged(object sender, EventArgs e) { if (JoyCheck.Checked) JoyCheck.Checked = !FaceCheck.Checked; } private void RadarPaint() { Bitmap map = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); Graphics g = Graphics.FromImage(map); var p = new Pen(System.Drawing.Color.Black, 2); System.Drawing.Point p1 = new System.Drawing.Point(); System.Drawing.Point p2 = new System.Drawing.Point(); System.Drawing.Point p3 = new System.Drawing.Point(); System.Drawing.Point p4 = new System.Drawing.Point(); p1.X = pictureBox1.Size.Width/2 ; //начало координат переводим в удобное нам место p1.Y = pictureBox1.Size.Height; //посередине pictureBox"a внизу for (int i = 0; i < 181; i++) { serialPortWrite(i, 90); p2.X = Convert.ToInt32(Math.Ceiling(320 + int.Parse(_distance) * Math.Cos(i * Math.PI / 180))); //считаем координаты точки p2.Y = Convert.ToInt32(Math.Ceiling(480 - int.Parse(_distance) * Math.Sin(i * Math.PI / 180))); if (i > 0) g.DrawLine(p, p2, p3); if (i % 18 == 0) { p4 = p2; p4.Y -= 50; g.DrawString(_distance, new Font("Arial", 18), new SolidBrush(System.Drawing.Color.Red), p4); } p3.X = p2.X; p3.Y = p2.Y; g.DrawLine(p, p1, p2); try { pictureBox1.Image = map; } catch (Exception e) { MessageBox.Show(e.Message); } } } private void button1_Click(object sender, EventArgs e) { if (FaceCheck.Checked || JoyCheck.Checked) { FaceCheck.Checked = false; JoyCheck.Checked = false; } Thread t = new Thread(RadarPaint); t.Start(); }


Класс DetectFace

class DetectFace { public static void Detect(Image image, String faceFileName, String eyeFileName, List faces) { CascadeClassifier face = new CascadeClassifier(faceFileName); // CascadeClassifier eye = new CascadeClassifier(eyeFileName); Image gray = image.Convert(); gray._EqualizeHist(); Rectangle facesDetected = face.DetectMultiScale(gray, 1.1, 5, new Size(70, 70), Size.Empty); faces.AddRange(facesDetected); } }

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

Видео
Вот, что получилось по завершении.



Заключение

Итоги
Оказалось все довольно просто. Цель достигнута, прототип готов. Есть над чем работать и заняться в свободное время, ожидая посылку с компонентами для робота.
Планы на будущее
Следующим шагом будет построение колесной платформы для робота, настройка удаленного управления (WiFi, 3G)., навешивание датчиков (температура, давление и прочее), синтез речи. В хотелках так же имеются планы по поводу механической руки.

Думаю, если будет интерес к данной статье и ее продолжению, то оно обязательно последует! Исправления и критика приветствуются!

Спасибо за внимание!

Теги:

  • Arduino
  • веб-камера
  • джойстик
  • робот
Добавить метки