Управляем веб-камерой с помощью джойстика

от автора

Введение

Лирика

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

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

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

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

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

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

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

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

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

Фото

image

Сборка завершена, сервоприводы и ультразвуковой дальномер подключены к 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 <Servo.h> #include <String.h> /* Тут реализован алгоритм приема строки строка должна быть вида 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<n; i++)     _value += distance();     return _value/n; }  long distance() //считываем показания ультразвукового дальномера {   long duration, cm;      pinMode(distancePin, OUTPUT);   digitalWrite(distancePin, LOW);   delayMicroseconds(2);   digitalWrite(distancePin, HIGH);   delayMicroseconds(10);   digitalWrite(distancePin, LOW);    pinMode(distancePin, INPUT);   duration = pulseIn(distancePin, HIGH);    cm = microsecondsToCentimeters(duration);  return cm; }  long microsecondsToCentimeters(long microseconds) //переводим микросекунды в сантиметры {   return microseconds / 29 / 2; } 

Проблема с неправильным разбором координат исчезла на совсем, 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<Bgr, Byte> 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, Byte> gray = image.Convert<Gray, Byte>().Canny(100, 60);                 // CamImageBoxGray.Image = gray;             }             catch { }               /*детектор лиц */             if (FaceCheck.Checked)             {                 List<System.Drawing.Rectangle> faces = new List<System.Drawing.Rectangle>();                  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[0] == '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<Bgr, Byte> image, String faceFileName, String eyeFileName, List<Rectangle> faces)         {             CascadeClassifier face = new CascadeClassifier(faceFileName);            // CascadeClassifier eye = new CascadeClassifier(eyeFileName);              Image<Gray, Byte> gray = image.Convert<Gray, Byte>();              gray._EqualizeHist();              Rectangle[] facesDetected = face.DetectMultiScale(                gray,                1.1,                5,                new Size(70, 70),                Size.Empty);             faces.AddRange(facesDetected);         }     } 

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

Видео

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



Заключение

Итоги

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

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

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

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/198102/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *