YOLOで検出した物体の座標をシリアル通信でArduinoに送る

## 1. YOLOを使えるようにする
以下のコマンドを実行してライブラリをインストールする。
~~~
python -m pip install ultralytics
~~~
yoloを実行するためのコードは次のようになる。
~~~YOLO_Test.py
from ultralytics import YOLO

model = YOLO('yolov8n.pt')
result = model.predict(source=0 , show=True)
~~~
このファイルと同じ階層にultralyticsを置くだけで物体検出が行える。
また今回はモデルをyolov8n.ptにしているがnだけでなくモデルの小さい順にn,s,m,l,xがある。
ちなみに私のパソコンのスペックだとl,xを使ったときに大幅な遅延があった。

## 2. 座標を取り出してArduinoに送信する
~~~YOLO_Arduino.py
from ultralytics import YOLO
import cv2
from ultralytics.utils.plotting import Annotator
import serial
import time
~~~
まずは必要なライブラリインポートする。持っていないものは
~~~
python -m pip install モジュール名
~~~
これでインストールできる。
~~~YOLO_Arduino.py
model = YOLO('yolov8n.pt')
cap = cv2.VideoCapture(0)
cap.set(3, 640)
cap.set(4, 480)
ser = serial.Serial("COM6", 9600, timeout=0.1)
time.sleep(2)
~~~
各種設定を行う
一番上は先ほども出たmodelの選択である。
その下はカメラの設定、シリアル通信の設定を行い2秒間プログラムを停止(PCとArduinoが準備する時間を確保するため)させている。
ポート番号はArduinoIDEから確認できる
~~~YOLO_Arduino.py
while True:
    _, img = cap.read()
    results = model.predict(img)
~~~
カメラからの画像を取り込んで物体検出を実行しその結果をresultに格納している。
~~~YOLO_Arduino.py
    for r in results:
        annotator = Annotator(img)
        boxes = r.boxes
        for box in boxes:
            b = box.xyxy[0]  # get box coordinates in (left, top, right, bottom) format
            c = box.cls
            if int(c) == 0:
                annotator.box_label(b, model.names[int(c)])
                data = f"{(b[0]+b[2])/2},{(b[1]+b[3])/2}\n"
                ser.write(data.encode())
~~~
検出した物体の座標とクラス(何の物体なのか)がboxesに格納されているためそれぞれをb,cに代入しておく。
このとき座標データは矩形の左下と右上のx,y座標が分かるので
~~~math
(x[左]+x[右])/2　,　(y[下]+y[上])/2
~~~
それぞれこのような式で中心の座標に変換する。
dataではこの計算をしつつ二つのデータを','で区切っている。
あとはser.writeでデータを送信するだけである。
\
コードの全体は以下のようになっている。
~~~YOLO_Arduino.py
from ultralytics import YOLO
import cv2
from ultralytics.utils.plotting import Annotator
import serial
import time

model = YOLO('yolov8n.pt')
cap = cv2.VideoCapture(0)
cap.set(3, 640)
cap.set(4, 480)
ser = serial.Serial("COM6", 9600, timeout=0.1)
time.sleep(2)

while True:
    _, img = cap.read()
    results = model.predict(img)
    for r in results:
        annotator = Annotator(img)
        boxes = r.boxes
        for box in boxes:
            b = box.xyxy[0]  # get box coordinates in (left, top, right, bottom) format
            c = box.cls
            if int(c) == 0:
                annotator.box_label(b, model.names[int(c)])
                data = f"{(b[0]+b[2])/2},{(b[1]+b[3])/2}\n"
                ser.write(data.encode())
            print(f"Object {model.names[int(c)]}: X={(b[0]+b[2])/2}, Y={(b[1]+b[3])/2}")
        img = annotator.result()
    cv2.imshow('YOLO V8 Detection', img)
    if cv2.waitKey(1) & 0xFF == 27:
        break

cap.release()
cv2.destroyAllWindows()
ser.close()
~~~
\
今回は受信したx座標をもとにしてサーボモータを回している。
受信したデータを','と'改行'で区切ってx,y座標を取り出してint型に変換している。
このx座標を範囲変換で0~180までの角度としている。あとはモータを回すだけである。
~~~YOLO_servo.ino
#include <Servo.h>

String data;
long value1;
long value2;
Servo myservo;
int angle = 90;

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  myservo.attach(12);
  myservo.write(90);
}

void loop() {
  if(Serial.available()){
    data = Serial.readStringUntil(',');
    value1 = data.toInt();

    data = Serial.readStringUntil('\n');
    value2 = data.toInt();
  }

  angle = map(value1, 0, 640, 0, 180);

  myservo.write(angle);
  delay(100);
}
~~~