ジョイスティックを使用してウェブカメラを制御します

はじめに



歌詞


こんにちは 自家製ロボットに関するHabréの多数の投稿に動機付けられて、彼は多かれ少なかれ価値のある興味深い何かをすることにしました。



一般的に、私は長い間ロボットが好きでしたが、私の手は通常のプロジェクトに届かず、基本的には演奏されていました。 少し考えた後、彼は自分のプロジェクトを思いつき、詳細を探し、スケッチを描き、ロボットの将来の能力について空想しました。 私は有名なウェブサイトで詳細を注文し、詳細が天国からの道を克服している間、私は手元にある未来のロボットのモジュールの1つを実装することにしました。 むしろ、モジュール自体を実装するのではなく、プロトタイプを組み立ててソフトウェアを作成し、プログラムを作成することで気が散らないようにし、さらに詳細が続く限り多くの自由時間があり、何かをしたいという欲求は休息を与えません。



私の指先には、Arduino Diecimilaショール、複数のサーボ、ウェブカメラ、ジョイスティック、超音波距離計がありました。 したがって、ウェブカメラに基づいて「自律的な操作」と「手動制御(ジョイスティック)」の両方の可能性を備えた「コンピュータービジョン」を作成するという要望がすぐに生じました。



この記事を書いたきっかけは何ですか?


インターネットを介して大騒ぎして、基本的にフォーラムであらゆる種類のごみの不明瞭な質問、ニーズから少し離れた記事からの抜粋を見つけました。 一般に、最初から最後まで、動くウェブカメラの作成をコード例とともに、さらには距離計とジョイスティックと組み合わせて説明した、優れた本格的な記事は見つかりませんでした。

特に、ほとんどの記事は古くなっていたため、記事を処理してすべての情報を一緒に収集する時間は、最初からすべてを自分で行った場合よりも多くなり始めたため、他のものを探すことはありませんでした。



タスクは簡単で、ジョイスティックからArduinoに情報を送信します。Arduinoは、Webカメラが接続された状態で2つのサーボを特定の角度に回転させ、必要に応じてレンジファインダーから情報を読み取り、SerialPortに送信します。

もう一度考え直した後、私はこのプロトタイプを自分で作成し始めることにしました。 行こう!



本体



プロトタイプアセンブリ


プロトタイプは5分以内に作成されました。 プロトタイプの外観にはまったく関心がありません。その主な目的は、ロボットの部品が到着する前にソフトウェア部品を開発することです。

そして、私はいくつかのビタミン、2つのサーボ、ウェブカメラ、ペーパークリップ、電気テープ、グルーガンの最初の瓶からそれを作りました。 次のことが判明しました。



写真
画像








組み立てが完了し、サーボと超音波距離計がArduinoに接続され、ArduinoがPCに接続されたら、Arduinoのプログラミングに進みます。



Arduinoのプログラミング


ジョイスティックがPCに接続されており、メインのビデオ処理もPC上で行われるため、ArduinoはPCからの情報を受信して​​処理し、サーボを制御するだけなので、すべて非常に単純に見えました。 したがって、シリアルポートを読み取り、着信情報を何らかの方法で処理し、何らかの方法でそれに応答するだけで済みます。



少し先を見据えて、C#でプログラムを作成した後、エラーに戻らなければならなかったとすぐに言います。 間違いはこれでした-素朴で熱意に満ちた私は、シリアルポートに入ってくる「90:90」のような行をそれぞれ2つの部分に解析するプログラムを書きました。最初の部分はX座標に沿った度数、2番目の部分はYですすべてがポートでテストされ、正常に動作しましたが、ジョイスティック制御プログラムが作成されたとき、ポートが値が変化する文字列で攻撃したとき、Arduinoはすべてを順番に読み取る時間を持っていなかったため、行が「0:909」、「:9090」になりましたそしてそれ 承認されました。

それに応じて、サーボは狂ってしまい、必要なもの以外のすべての位置を占めました。



したがって、ためらうことなく、改行文字と改行文字が必要であるという結論に達しました。 繰り返しになりますが、ラテンアルファベットの最初の文字「a」が行の先頭文字、最後の「z」が文字列の末尾、「x」および「y」軸の値の先頭の文字としてそれぞれ選択されました。 合計入力行は、「ax90y90z」の形式を取りました。



レンジファインダー用でなければ、すべてがうまくいきます。 距離計は超音波であり、バタンと距離を決定しますが、いくつかのニュアンスがあります。 まず、距離計と壁の間の角度が45度(プラスまたはマイナス)より鋭い場合、音は壁から接線方向に反射され、値は現実に対応しません。 第二に、センサーが角度を持っている物体からの信号が反対方向に反射され、直線でほぼ実際の距離が得られるため、信号放出のかなり大きな角度、約30度(マニュアルによる)、および最も近い物体までの距離が測定されます、しかし干渉はまだ起こり、そしてかなり頻繁に。 そのため、n個の距離測定を行う関数をもう1つ追加し、それらを合計して数値で除算し、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#)


最初はQtでC ++ですべてを書きたかったのですが、後でC#で書く必要がありましたが、まあまあです。



私が手に入れたかったもの:

1.人々の顔認識。

2.人の顔を追跡します。

3.ジョイスティックによる手動制御。

4.オブジェクトまでの距離を決定します。



顔を認識し、ウェブカメラからの画像を表示するために、質問なしで、OpenCVライブラリ、またはC#-Emgu CVのシェルが選択されました。



ジョイスティックの位置を読み取るために、最初はMicrosoft.DirectX.DirectInputライブラリを使用しましたが、これは私は本当に好きではありませんでした。さらに、SharpDXライブラリを使用しました。



プログラムに必要なもの:

1. Webカメラから画像をキャプチャし、画面に表示します。

2.画像​​内の顔を認識し、それらを円で囲み、画像内の顔の座標を取得します。

3.「ax90y90z」という形式の文字列を作成し、シリアルポートに送信してサーボを制御します。

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)の構成、センサーの取り付け(温度、圧力など)、音声合成です。 ウィッシュリストには、機械式アームの計画もあります。



この記事とその継続に興味があれば、間違いなくそれに続くと思います! 訂正と批判は大歓迎です!



ご清聴ありがとうございました!



All Articles