ゆるエンジニアはいろいろ遊びたい

FAエンジニアが週末にいろいろ遊ぶブログです

ShelPiがジェスチャーを理解できるようになりました

ShelPiはYOLOによる物体検出ができるため、特定のオブジェクトに対しアクションを行うといった制御が可能となっています。
よりグレードアップさせるために、手の動きを認識させてジェスチャー操作ができるか試してみました。
ChatGPTによると、MediaPipeというGoogleが提供している機械学習を用いたオープンソース・フレームワークというものがいいらしい。よくわかりませんが。
なにやらこれでハンドトラッキングができるようなので、早速試してみました。
MediaPipeは旧バージョンを使った方が簡単らしいので、仮想環境に以下のバージョンをインストールします。

pip install mediapipe==0.10.13
import cv2
import mediapipe as mp
import requests
import numpy as np

mp_hands = mp.solutions.hands
hands = mp_hands.Hands()
mp_draw = mp.solutions.drawing_utils

#カメラ映像取得
SHELPI_IP = "ShelpiのIPアドレス"
SNAPSHOT_URL = f"http://{SHELPI_IP}:8080/?action=snapshot"

def get_frame():
    r = requests.get(SNAPSHOT_URL)
    img_arr = np.frombuffer(r.content, np.uint8)
    frame = cv2.imdecode(img_arr, cv2.IMREAD_COLOR)
    return frame

while True:
    frame = get_frame()
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

    results = hands.process(frame_rgb)

    if results.multi_hand_landmarks:
        if results.multi_hand_landmarks:
            for hand_idx, hand_landmarks in enumerate(results.multi_hand_landmarks):
                print(f"\n--- Hand {hand_idx} ---")

        for i, lm in enumerate(hand_landmarks.landmark):
            print(f"{i}: x={lm.x:.3f}, y={lm.y:.3f}, z={lm.z:.3f}")

        for handLms in results.multi_hand_landmarks:
            mp_draw.draw_landmarks(frame, handLms, mp_hands.HAND_CONNECTIONS)

    cv2.imshow("hand", frame)

    if cv2.waitKey(1) & 0xFF == 27:
        break

ShelPiはmjpg-streamerで映像をストリーミングしているので、それを取得して解析します。
実行すると、プレビューで手の骨格が表示され、ターミナルには各ポイントの座標が表示されます。


かなり正確に骨格をトラッキングしているのが確認できました。
各ポイントのIDですが、

このようになっていて、各ポイントの座標の位置関係を比較してジェスチャーをプログラムしました。

グッドポーズ : じたばたして喜ぶ
手を振る : バイバイする
手の平を下にする : お手をする

カメラの画角が狭くて、ロボットが動いているとなかなか認識させるのが大変でした。
広角カメラだともっといいんでしょうね。
でも、本格的なロボットになってきて満足です。

YOLOをGPUで動かしてみた

ShelPiで使っている物体検出モデルのYOLOですが、現在CPUで動作しています。
これでも全然高速なのですが、今回はGPUで動かしてみたいと思います。
GPUで動作させるためには、pytorchというものを入れる必要があるようです。

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install ultralytics

インストールが完了したら、GPUを認識しているかチェックします。
pythonで

import torch
print(torch.cuda.is_available())
print(torch.cuda.get_device_name(0))

結果

  • True
  • NVIDIA GeForce RTX3060(自分のGPU)

となれば成功
GPUで動かすには、modelにcudaを指定するといいようです。

model = YOLO("yolov8n.pt")
model.to("cuda")
#または
results = model(frame, divice="cuda")

GPUを使っているかの確認は

nvidia-smi

で、

====================================
|Voltage     Uncorr.ECC|
|GPU-Util Compute M.|
|                       MIG M.|
====================================
|                             N/A|
|       42%          Default|
|                             N/A|

こんな感じの画面が出てきますので、このGPU-Utilの部分がYOLO実行中に増加すればGPUを使っていると判断できます。
画像1枚ではすぐ終わってしまいわからないので、動画ファイルを与えてあげるとわかりやすいです。
YOLO自体がすごく軽いので、GPUの恩恵を全く感じませんでしたが、今後他のモデルを同時に使うときに役立ちそうです。

ShelPiに視覚を実装しました

ShelPiのカメラでストリーミングした映像をメインPCで画像認識することができたので、これをShelPiに反映させてみました。

当初計画していたシステムでは、メインPCで画像認識をローカルLLMで行い、ShelPiに行動の指示をする予定でした。
ローカルLLMは画像からとるべき行動を考え、ShelPiに直接コマンドを送って制御しようというものです。
ですが、ローカルLLMでは応答速度が遅くリニアな反応が期待できません。

ロボットを制御する場合はリアルタイムで画像を認識できないと反応が遅くストレスがたまることが分かったので、LLMではなく、YOLOによる画像認識の結果を使う事にしました。
YOLOであれば、メインPCで50ms程度の速度で処理できるため、リニアな反応をさせる事ができます。

メインPCでどこまで制御するかがわりと難しく、LLMを使ってメインPC側でShelPiを制御したいという当初の計画でプログラムを作ると、画像認識して特定のオブジェクトを検出したらShelPiの動作モードを変えるフラグをjson形式で送信するようなプログラムになりました。

ところが、この方法だとShelPiの動作はメインPCに依存するので動作にかかわるプログラムが分かれてしまいます。
また、ShelPiにはカメラの他に超音波センサーが搭載されていますが、このセンサーの情報を使いたい場合、メインPC側にデータを送信しなければなりません。

このような理由からメインPC側では動作にかかわる指示はせずに、画像認識の結果のみをShelPiに出力することにしました。

いってみれば、メインPCは目としての役割のみを担うという事で、動作はShelPiに搭載されているRaspberryPi zero2Wで制御します。
YOLOの画像認識の結果、人や犬、猫などの特定のオブジェクトを認識したら、その映像をカメラの中心にとらえるように方向を調整し、超音波センサーの測定値で適切な距離まで近づきます。
適切な距離まで近づいたらコミュニケーションをさせるという制御をさせるのです。

ShelPiがとっても賢くなり、とてもかわいいです

Ultralytics YOLOを使って画像認識してみた

4脚ロボットShelPiにはラズパイのカメラモジュールV2を搭載しています。
slowtech.hateblo.jp
slowtech.hateblo.jp
mjpg-streamerでカメラの映像をストリーミングしているので、
メインPCで画像を取得し、AIによる物体認識をしてShelPiの行動を制御することが可能です。
今回はメインPCでUltralytics YOLOを使用して画像認識ができるかテストしてみました。
使用した言語はPythonです。
環境を汚さないようにvenvで仮想環境を作成します。

python3 -m venv yolo
source yolo/bin/activate

仮想環境下で、YOLOをインストールします。

pip install ultralytics

サンプルコードは以下

from ultralytics import YOLO

model = YOLO("yolov8n.pt")  # 軽量モデル
results = model("test.jpg")
results[0].show()

pythonファイルと同じディレクトリに適当な画像をいれ、test.jpgと名前を変更したら実行します。
初回実行時は何かダウンロードが始まりますので少し待ちます。
成功すれば画像がプレビューされます。
今回使用したYOLO8nは軽量モデルで、80種類のオブジェクトを認識できるようです。

from ultralytics import YOLO

model = YOLO("yolov8n.pt")
print(model.names)

これで、検出可能なリストが出てきます。

 0: person
 1: bicycle
 2: car
 3: motorcycle
 4: airplane
 5: bus
 6: train
 7: truck
 8: boat
 9: traffic light
10: fire hydrant
11: stop sign
12: parking meter
13: bench
14: bird
15: cat
16: dog
17: horse
18: sheep
19: cow
20: elephant
21: bear
22: zebra
23: giraffe
24: backpack
25: umbrella
26: handbag
27: tie
28: suitcase
29: frisbee
30: skis
31: snowboard
32: sports ball
33: kite
34: baseball bat
35: baseball glove
36: skateboard
37: surfboard
38: tennis racket
39: bottle
40: wine glass
41: cup
42: fork
43: knife
44: spoon
45: bowl
46: banana
47: apple
48: sandwich
49: orange
50: broccoli
51: carrot
52: hot dog
53: pizza
54: donut
55: cake
56: chair
57: couch
58: potted plant
59: bed
60: dining table
61: toilet
62: tv
63: laptop
64: mouse
65: remote
66: keyboard
67: cell phone
68: microwave
69: oven
70: toaster
71: sink
72: refrigerator
73: book
74: clock
75: vase
76: scissors
77: teddy bear
78: hair drier
79: toothbrush

Raspberry Pi ZERO 2Wや、RaspberryPi5では、処理速度が遅くてあまりリアルタイムで制御することが難しそうでしたので、
メインPCでAIを動かすことにしました。ShelPiは画像を送信し、PCから結果を受け取るだけの簡単なお仕事。
これを使えば、ShelPiが物体認識できるようになりますね。

ShelPiのサーボをSG90→MG90Dに換装しました

先日完成したShelPiですが、テスト駆動を重ねているうちに、ボディ側のサーボに使用しているSG90のコピーモデルのジッターが激しくなってきました。
非励磁時にはギヤが空転するものもあり、よりトルクのあるモーターに変更する必要があったので、秋月電子さんでメタルギヤのMG90Dを購入しました。

@1,350×4個で、合計¥5,400

SG90のコピーモデルは4個で¥1,000なので5倍の値段です。
仕様を比較すると、

項目 SG90 (アナログ) MG90D (デジタル) 備考
ギア素材 プラスチック (POM) メタル (6061-T6 アルミ) MG90Dは耐久性が圧倒的に高い
動作タイプ アナログ デジタル MG90Dは保持力とレスポンスが良い
トルク (4.8V) 1.8 kg·cm 2.1 kg·cm MG90Dの方がパワフル
スピード (4.8V) 0.10 ~ 0.12 sec/60° 0.10 sec/60° MG90Dの方が安定して高速
動作電圧 4.8V (〜6.0V) 4.8V 〜 6.6V MG90DはLiFe電池(6.6V)にも対応
ベアリング なし (ブッシュ) ダブルボールベアリング MG90Dは軸ブレが少なくスムーズ
重量 約 9g 約 13g メタルギアの分、MG90Dが重い
デッドバンド 7μs ~ 10μs 1μs MG90Dの方が微細な制御が可能

といった感じです。
ストール時の電流がSG90と比較すると高いため、ダイソーのモバイルバッテリーで動くか不安でしたが、試運転の際は問題ありませんでした。
使ってみて、明らかに剛性が高く、ギヤのバックラッシュも気にならず、かなりガッシリしている印象があります。


動かした感じだとあまり変化はありませんが、励磁時に負荷を与えた際の保持力が全く違います。
寸法はほぼ同じなのですが、取り付け部分の高さや、全高などが少し異なるためそのまま交換はできません。
フレームから再設計して換装する事になりました。
設計、3Dプリント、換装と、休日が一瞬で溶けました。
先端側のサーボはまだ樹脂ギヤですが、これもMG90Dに変更してもいいかもしれません。
今のところは在庫もあるので壊れたら交換して対応しましょう。
電子工作を始めて4か月ぐらい経ちました。
最初は安いモーターで動けばいいや。ぐらいの気持ちでやっていましたが、倒立振子やラズパイカーを作り、3Dプリンタを購入し、ついにロボットまで作るようになってくると徐々にいいものが欲しくなってきます。
着々とノウハウが積み重なってきているのを実感します。

ただ、お金がかかるのが・・・

RaspberryPi zero 2wで16CHサーボドライバPCA9685を使うときの注意

ShellPiをいじっていたらラズパイの設定がおかしくなってしまい、再度OSからインストールしなおしたのですが、その際にサーボドライバのPCA9685がうまく動かずに苦戦しました。
サーボドライバのライブラリは以下の2つあるようです。

adafruit-circuitpython-pca9685
adafruit-pca9685

circuitpythonとつく方が現在のライブラリ名で、

pip install adafruit-circuitpython-pca9685

でインストールします。
サンプルコードは

import time
import board
import busio
from adafruit_pca9685 import PCA9685

# I2C 初期化
i2c = busio.I2C(board.SCL, board.SDA)

# PCA9685 初期化
pca = PCA9685(i2c)
pca.frequency = 50  # サーボは 50Hz

def set_servo_angle(channel, angle):
    # 角度 → パルス幅(ms)
    pulse_min = 1.0   # 0°
    pulse_max = 2.0   # 180°
    pulse = pulse_min + (pulse_max - pulse_min) * (angle / 180)

    # duty_cycle に変換
    duty = int((pulse / 20.0) * 65535)
    pca.channels[channel].duty_cycle = duty

# テスト
while True:
    set_servo_angle(0, 0)
    time.sleep(1)
    set_servo_angle(0, 90)
    time.sleep(1)
    set_servo_angle(0, 180)
    time.sleep(1)

circuitpythonが付かない方は旧ライブラリで、

pip install adafruit-pca9685

でインストールします。
サンプルコードはこちら

import Adafruit_PCA9685
import time

# PCA9685初期設定
pwm = Adafruit_PCA9685.PCA9685()
pwm.set_pwm_freq(60)


def main():
    for i in range(1):
        print(f"{i}+番目のサーボモーター動作")
        pwm.set_pwm(i, 0, 450)
        time.sleep(0.5)
        pwm.set_pwm(i, 0, 250)
        time.sleep(0.5)
    pwm.close()
    
if __name__ == '__main__':
    main()

初期設定も制御方法も全然違います。
AIにコーディングしてもらうと、旧式を使ってみたり、新式を使ってみたりと気まぐれなので注意が必要でした。

ShelPi(シェルピー)完成

ここ最近ずっと作っているクモ型ロボ改めカメロボなんですが、ようやく完成形になりました。
これまですこし大きくてボテッとしたデザインでしたので、ボディをコンパクトに設計変更しました。
以前の記事はこちら↓
slowtech.hateblo.jp
初号機

弐号機

参号機

初号機はセンサーがなく、脚部のモーターのみ。
弐号機で足回りを対象に設置。超音波センサーとカメラを搭載しました。
その影響で上のカバーが取り付けられなくなりました。
参号機は足回りをよりコンパクトに設計変更し、超音波センサーとカメラの取り付けを前提とした構成にしています。
カバーもよりコンパクトにして、それまで横に並んでいたサーボドライバとラズパイゼロ2Wを縦に配置。
省スペース化に貢献しています。
カバー上面はV字に穴を空け、ラズパイ内部のLEDの点灯状況を確認できるようにしています。デザイン的にも良き。

このロボ、これからはShelPi(シェルピー)と呼ぶことにします。
Shell(甲羅)のような堅牢な外観と、Raspberry Piの"Pi"を組み合わせ、愛嬌のあるしぐさで楽しませてくれる事から、シェルピー。
いい名前です。

エネルギー源はダイソーのモバイルバッテリー5000mAhで、大きな電流は流せないのですが容量が結構あるのでかなりずっと起動していられます。
これからカメラをAIで処理して自立して行動できるような制御をしてみたいです。