YOLOを使ってみる

ラズパイ5でカメラから取り込んだ画像の処理は、今ならYOLOがそこそこの精度で使えそうだからやってみた

全体の流れと動作確認できるPythonスクリプト

https://techblog-ai.com/【raspberry-pi-5xカメラ】yolov8-リアルタイム推論-usbカメラ-picameraモ/

Yoloのインスト

https://techblog-ai.com/【raspberry-pi-5】yolov8のインストール方法/

但しpipでpicameraインストしてはあかんらしい(見つからないと言われたよ)から、aptでインストした

$ sudo apt install -y python3-picamera2

前の仮想環境(venv)は汚染されたぽいから、新たな仮想環境(venv2)を作る、picameraも別環境だから再度インスト必要

静止画像からの検出スクリプトを動かしてみると

$ cat yolo.py
from ultralytics import YOLO
model = YOLO("yolov8x.pt")
results = model('test.png', save=True)

学習済みモデルの最高精度版(yolov8x.pt)をダウンロードしてきて検出してます、これは壁紙のスクショ(test.png)をターゲットに実行、結果の画像は省略

次にラズパイカメラの動画からの検出は、

こんな感じで、モバイル用の辞書(yolov8n.pt)でも精度は高そうですね

またフレーム周波数は数フレームぐらいでは動いてくれているようです

Yoloは負荷重いから、CPU温度は60℃ぐらいまでアップしてファンは常時回転してますが

 

admin

ラズパイ5のPythonでの音声入力デバイス指定

USBマイクの接続先をUSB2.0のコネクタからUSB3.0にしたら同じコードで入力がされない状態に、コメントアウトしている# device=2,を指定しないとダメ

import vosk
import pyaudio
import json
import numpy as np
import sounddevice as sd
import queue
import threading
import time

class VoskSpeechRecognizer:
    def __init__(self, model_path='./vosk-model-ja-0.22'):
        # モデルの初期化
        vosk.SetLogLevel(-1)
        self.model = vosk.Model(model_path)
        self.recognizer = vosk.KaldiRecognizer(self.model, 16000)
        
        # キュー設定
        self.audio_queue = queue.Queue()
        self.stop_event = threading.Event()
        
        # マイク設定
        self.sample_rate = 16000
        self.channels = 1
        
        # スレッド準備
        self.recording_thread = threading.Thread(target=self._record_audio)
        self.recognition_thread = threading.Thread(target=self._recognize_audio)
        
    def _record_audio(self):
        """
        連続的な音声録音スレッド
        """
        with sd.InputStream(
            samplerate=self.sample_rate, 
            channels=self.channels,
            dtype='int16',
            # device=2,
            callback=self._audio_callback
        ):
            while not self.stop_event.is_set():
                sd.sleep(100)
    
    def _audio_callback(self, indata, frames, time, status):
        """
        音声入力のコールバック関数
        """
        if status:
            print(status)
        self.audio_queue.put(indata.copy())
    
    def _recognize_audio(self):
        """
        連続的な音声認識スレッド
        """
        while not self.stop_event.is_set():
            try:
                audio_chunk = self.audio_queue.get(timeout=0.5)
                if self.recognizer.AcceptWaveform(audio_chunk.tobytes()):
                    result = json.loads(self.recognizer.Result())
                    text = result.get('text', '').strip()
                    if text:
                        print(f"{text}")
            except queue.Empty:
                continue
    
    def start_recognition(self):
        """
        音声認識の開始
        """
        self.stop_event.clear()
        self.recording_thread.start()
        self.recognition_thread.start()
    
    def stop_recognition(self):
        """
        音声認識の停止
        """
        self.stop_event.set()
        self.recording_thread.join()
        self.recognition_thread.join()

def main():
    recognizer = VoskSpeechRecognizer()
    
    try:
        print("音声認識を開始します。Ctrl+Cで終了できます。")
        recognizer.start_recognition()
        
        # 無限ループを防ぐ
        while True:
            time.sleep(1)
    
    except KeyboardInterrupt:
        print("\n音声認識を終了します...")
    finally:
        recognizer.stop_recognition()

if __name__ == "__main__":
    main()

原因はいまいち不明ですが、もとのソースにマイクのチャネル指定はなかった訳だけど、USB3.0側にすると明示しないとダメそうです

sounddeviceで検索すると、0指定にするのが正しそうですが、0にするとエラーが出ます

2はシステムのデフォルト指定なので、それはそれで正しそう、但し2が常にデフォルトではあると限らないから、

device= “sysdefault”,

とデフォルト指定と明示した方が、音声入力手段が複数あれば別ですが、一個しか無いんだったらこれが汎用で使えそうです

$ python
Python 3.11.2 (main, Nov 30 2024, 21:22:50) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sounddevice as sd
>>> print(sd.query_devices())  # すべてのデバイス一覧を表示
   0 SF-558: USB Audio (hw:0,0), ALSA (1 in, 0 out)
   1 UACDemoV1.0: USB Audio (hw:2,0), ALSA (0 in, 2 out)
   2 sysdefault, ALSA (128 in, 0 out)
   3 spdif, ALSA (1 in, 0 out)
   4 lavrate, ALSA (128 in, 0 out)
   5 samplerate, ALSA (128 in, 0 out)
   6 speexrate, ALSA (128 in, 0 out)
   7 pulse, ALSA (32 in, 32 out)
   8 speex, ALSA (1 in, 0 out)
   9 upmix, ALSA (8 in, 0 out)
  10 vdownmix, ALSA (6 in, 0 out)
* 11 default, ALSA (32 in, 32 out)

 

admin

ラズパイ5のドライブをSSDにする

そこそこSDカードの中身も落ち着いてきて、サイズも20GB/64GBぐらいになってきたので、運用寿命と速度を勘案してSSDにしました

仕様は写真から読み取れますが、アダプタボードとNVMe仕様のSSD、サイズは256GBでおそらく一生持ちそう

ディスクの作成は、SD Card Copierを使って行いました、

New Partition UUIDsにチェックは重要、チェックしないと完全クローンになるので、SDカードとSSD同時実装時に区別つきません

最初、チェックしないでコピーしたのでリカバリに苦労、以下LLMに聞きながら実行した手順、要は「MBR/GPTヘッダ完全削除
」をしないとダメのようです

同じUUIDでコピーした時のリカバリ手段、macで単純ディスク消去だと
MBR/GPTヘッダが残ってるらしく不完全な初期化です

$ diskutil list # SSDのドライブ番号を検索する(/dev/disk4 だった)
$ diskutil unmountDisk /dev/disk4	# 外付けSSD番号を指定
# MBR/GPTヘッダ完全削除
(初期化するからディスク名や形式は重要ではないはず)
$ diskutil eraseDisk JHFS+ NewDiskName /dev/disk4
$ sudo dd if=/dev/zero of=/dev/disk4 bs=1M count=100

インストールして、bootデバイスを探す時間は無駄なのでSSDを一番最初にアクセスするような設定に変更します

$ sudo rpi-eeprom-config --edit
 ファイルの461 -> 416に変更(右から順番に探して、6はSSDです)

$dfの結果はこんな感じ、

電源オン起動時間は体感でも明らかに早いし、アプリ(例えばブラウザ)の起動も早い、VNCでダミーHDMI入れてないから、相変わらずフレームレートは出ないけども

P.S. デスクアクセス性能

以前SDカードの値があったので比較してみる

$ sudo curl https://raw.githubusercontent.com/TheRemote/PiBenchmarks/master/Storage.sh | sudo bash


     Category                  Test                      Result     
HDParm                    Disk Read                 90.82 MB/sec             
HDParm                    Cached Disk Read          90.94 MB/sec             
DD                        Disk Write                32.5 MB/s                
FIO                       4k random read            6317 IOPS (25268 KB/s)   
FIO                       4k random write           845 IOPS (3382 KB/s)     
IOZone                    4k read                   29989 KB/s               
IOZone                    4k write                  3288 KB/s                
IOZone                    4k random read            30032 KB/s               
IOZone                    4k random write           3268 KB/s                

                          Score: 2530                             


     Category                  Test                      Result     
HDParm                    Disk Read                 441.07 MB/sec            
HDParm                    Cached Disk Read          432.23 MB/sec            
DD                        Disk Write                345 MB/s                 
FIO                       4k random read            87521 IOPS (350085 KB/s) 
FIO                       4k random write           85333 IOPS (341333 KB/s) 
IOZone                    4k read                   132985 KB/s              
IOZone                    4k write                  169992 KB/s              
IOZone                    4k random read            63871 KB/s               
IOZone                    4k random write           181370 KB/s              

                          Score: 39288                                       

今時のベンチマークに比較して、めちゃくちゃ早いわけではないが、妥当な値と言えるだろう

 

admin

ラズパイ5で日本語入力

vimで日本語入力できないなと思ったら、そもそも今までラズパイで日本語入力使ったことがなかったのだ、

ツールとしてはfcitx-mozcを使うと日本語は入力できるようにはなる、ただし使い勝手はイマイチ感あるから、Macのエディタで作成したものを貼り付けるのが実用的だな、と思ったから非常手段かもしれない

 

admin

 

 

比較のついでにラズパイzeroの実行速度も測定してみた

ラズパイzeroは段違いに遅いことはわかっていたけれども、どれだけ遅いのかをみてみた、ソースコードは共通ですがzeroはシングルコアなのでマルチスレッドの数値はなし

コンパイルもzeroでは実質的にはできないからMac上でクロスでバイナリ作成してます

比較してみると、他に比べて絶望的に遅いことがわかります、まあアプリケーションによって使い分けるわけではありますが

 

admin

ラズパイ5とApple Siliconの速度比較

Apple Siliconとの比較でラズパイ5で実行時間も測定してみた、Apple Siliconの数値は以下のリンクから

https://isehara-3lv.sakura.ne.jp/blog/2025/03/10/m1-vs-m4の性能比較(非常に限定的な場合で)/

<ラズパイ5の実行結果>

ラズパイ5のRustでの実行速度

<シングルスレッド>

pi@rasp5:~/rust/prime_single_thread/src $ cargo run --release
    Finished `release` profile [optimized] target(s) in 0.00s
     Running `/home/pi/rust/prime_single_thread/target/release/prime_single_thread`
2から1000000までの素数:
Elapsed time: 81.208438ms
number of primes: 78498

<マルチスレッド>

pi@rasp5:~/rust/prime/src $ cargo run --release
    Finished `release` profile [optimized] target(s) in 0.01s
     Running `/home/pi/rust/prime/target/release/prime`
2から1000000までの素数:
Elapsed time: 25.209907ms
number of primes: 78498

<Apple siliconとの比較>

予想外にラズパイ5が早いというべきか、Mxが遅いというべきか、概ねM1 Macとラズパイzeroだと概ね速度差は60倍ぐらいあったのが、ラズパイ5だとその差は3倍ぐらい、ということはラズパイzeroと5で実行速度は20倍ぐらいの差があるということになるから、ちょうど2018年ごろのMacBook Air(Intel CPU)の速度ぐらいは出ているということになるだろう、体感速度というのはCPU速度だけではなくファイルアクセスも重要だからSDカードで動かしているラズパイ5はかなりハンディはあるだろう

ラズパイzeroでRustやGolangのコンパイルは実用的な速度で実行できないけど、ラズパイ5なら問題なくできるから、言い換えれば一桁以上の性能差があるということだから、体感というのは概ね当たっているようだ

P.S. 2025/3/12

ラズパイzeroの実行時間は次の記事で追加、予想のさらに下でした

 

admin

目玉モデルを作る(ラズパイ5+カメラ)

視覚相当機能はラズパイ5の標準カメラで実現しますが、ロボットなので目玉風に格納したい

目玉の中心部にカメラ、カメラを覆うような形でサーボモーターで瞼を動かしてみる

<瞼側>

<眼球側>

サーボモーターを取り付け部分はそのままだと造形できないので、仮にサポートを立てて、後でハンダゴテで溶かしてニッパーで切り落とし、最初からニッパーだとストレスで割れます

 

<カメラ取り付け>

<カメラの外観>

<可動部分>

<動作の動画>

マイムービー – SD 480p

カメラからの画像をどう扱うかはこれから、YOLOとか有力だと思うけど

 

<サーボモーター駆動のコード>

PWM機能はgpiodにないのでソフトでPWM作成

import gpiod
import time

CHIP = 'gpiochip4'  # Raspberry Pi 5では'gpiochip4'を使用
PIN = 18  

chip = gpiod.Chip(CHIP)
line = chip.get_line(PIN)
line.request(consumer="Servo", type=gpiod.LINE_REQ_DIR_OUT)

def set_servo_angle(angle):
    duty_cycle = (angle / 18) + 2.5
    pulse_width = duty_cycle / 100 * 20000  # 20ms周期

    line.set_value(1)
    time.sleep(pulse_width / 1000000)
    line.set_value(0)
    time.sleep((20000 - pulse_width) / 1000000)

try:
    while True:
        for angle in range(80,141, 5):
            for _ in range(10):  # 各角度でn回パルスを送信
                set_servo_angle(angle)
            time.sleep(0.1)
except KeyboardInterrupt:
    pass
finally:
    line.release()

 

admin

 

ラズパイ5のWi-Fi接続遅い問題の解決(SSIDの接続優先順位指定)

タイトル通りですが、やることは二つ

一つ目はLocalを日本に設定して5Gが使えるようにすること

二つ目は複数のSSIDを使っている時に(今回はnaとn接続の場合には)acのSSIDの優先順位を高く設定する

この二つですが、ロケール設定はraspy-configで、優先順位指定は最近のラズパイOSでは設定ファイル編集ではなく、nmcli(おそらくnatwork manager by command line interfaceの略)を使うようです

以下、二つのSSIDの場合の設定例です

設定(数字が大きい方を優先する、デフォルトは0らしい)
$ sudo nmcli connection modify "preconfigured" connection.autoconnect-priority 100

確認方法
$ nmcli connection show "preconfigured" | grep connection.autoconnect-priority

connection.autoconnect-priority:        100

$ nmcli connection show "他のssid” | grep connection.autoconnect-priority

connection.autoconnect-priority:        0

この場合、デフォルト設定の方がac接続なのでそこの優先順位を上げました、これで設定を有効(リブートあるいはnmcliで指定)すれば大丈夫です

ブラウザからの速度確認で、n接続では30Mbps程度でしたが、この程度の速度になりました、ac接続としてはそれほど高速でもないですが、相対的にはかなり速くはなってます

 

admin

ラズパイ5でgpioを扱う

ラズパイ5で大きなハードウェア変更があり、結果としてgpioを扱うライブラリも変更があったようです、一番大きな変更はcやPythonでgpio扱う時にはroot権限でないと動作しないということかもしれません

プロービングしやすいgpioの21ピンを対象にして、動かしてみました

これはPythonのコード、10ms毎にステートを0,1 繰り返しているだけ

こんな当たり前の波形

次にRustでもやってみた、以下がコードですが終了しないので最後のok(())はコメントアウト

実はスレッドの起動と終了待ちには結構時間かかってそれだけで50μs近くかかるから、時間待ちに数μs以下を指定しても無味

これは1μs時間待ちのケース

そしてこれはスレッド起動をコメントアウトしたもの

およそ50nsぐらいのパルスになっています、オシロのサンプリング周波数が40MHzぐらいなので、帯域が追いついてないですが

これでもラズピコのgpio制御に比較すると遅いので、組み込み系にはラズピコの方が向いているということなんだろうと思う

 

admin

 

 

 

 

Geminiへのリクエストを音声でやりとりするようにしてみた

音声認識、LLMへのリクエストとレスポンス、text2speechを一連の流れで実行できるようにしてみた

最初はMacでやったけれども、数箇所手直しするだけでラズパイ5でもちゃんと動作、正常系だけなのでユーザエクスペリエンス的にはまだまだ改善必要ですが、

三本のコードのマージはPerplexityで実行させてます、GeminiにPythonからアクセスするためにAPIキーが必要になりますが、以下のリンクから今は無償で取得できます、APIキーはシステム環境変数に保存、他人の資産だからそれはオープンにはできない

https://aistudio.google.com/apikey

<全体のコード>

import vosk
import pyaudio
import json
import numpy as np
import sounddevice as sd
import queue
import threading
import time
import os
from dotenv import load_dotenv
import google.generativeai as genai
import subprocess

class VoskSpeechRecognizer:
    def __init__(self, model_path='./vrecog/vosk-model-ja-0.22'):
        # モデルの初期化
        vosk.SetLogLevel(-1)
        self.model = vosk.Model(model_path)
        self.recognizer = vosk.KaldiRecognizer(self.model, 16000)
        
        # キュー設定
        self.audio_queue = queue.Queue()
        self.stop_event = threading.Event()
        
        # マイク設定
        self.sample_rate = 16000
        self.channels = 1
        
        # スレッド準備
        self.recording_thread = threading.Thread(target=self._record_audio)
        self.recognition_thread = threading.Thread(target=self._recognize_audio)
        
    def _record_audio(self):
        """
        連続的な音声録音スレッド
        """
        with sd.InputStream(
            samplerate=self.sample_rate, 
            channels=self.channels,
            dtype='int16',
            callback=self._audio_callback
        ):
            while not self.stop_event.is_set():
                sd.sleep(100)
    
    def _audio_callback(self, indata, frames, time, status):
        """
        音声入力のコールバック関数
        """
        if status:
            print(status)
        self.audio_queue.put(indata.copy())
    
    def _recognize_audio(self):
        """
        連続的な音声認識スレッド
        """
        while not self.stop_event.is_set():
            try:
                audio_chunk = self.audio_queue.get(timeout=0.5)
                if self.recognizer.AcceptWaveform(audio_chunk.tobytes()):
                    result = json.loads(self.recognizer.Result())
                    text = result.get('text', '').strip()
                    if text:
                        print(f"認識結果: {text}")
                        response_text = query_gemini(text)  # Gemini APIに問い合わせる
                        jtalk(response_text)  # 結果を音声合成して再生する
            except queue.Empty:
                continue
    
    def start_recognition(self):
        """
        音声認識の開始
        """
        self.stop_event.clear()
        self.recording_thread.start()
        self.recognition_thread.start()
    
    def stop_recognition(self):
        """
        音声認識の停止
        """
        self.stop_event.set()
        self.recording_thread.join()
        self.recognition_thread.join()

def query_gemini(prompt):
    """
    Gemini APIに問い合わせて応答を取得する関数。
    """
    try:
        response = model.generate_content(prompt)
        print(f"Gemini応答: {response.text}")
        return response.text.strip()
    except Exception as e:
        print(f"Gemini APIエラー: {e}")
        return "エラーが発生しました。もう一度試してください。"

def jtalk(text):
    """
    Open JTalkでテキストを音声合成し再生する関数。
    """
    open_jtalk = ['/usr/bin/open_jtalk']
    mech = ['-x', '/var/lib/mecab/dic/open-jtalk/naist-jdic']
    htsvoice = ['-m', '/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice']
    speed = ['-r', '1.0']
    outwav = ['-ow', 'out.wav']
    cmd = open_jtalk + mech + htsvoice + speed + outwav
    
    try:
        proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
        proc.stdin.write(text.encode('utf-8'))
        proc.stdin.close()
        proc.wait()
        
        # 音声ファイルを再生する場合
        subprocess.call(['aplay', 'out.wav'])
    except Exception as e:
        print(f"音声合成エラー: {e}")

def main():
    # 環境変数からGoogle APIキーを読み込む
    load_dotenv()
    GOOGLE_API_KEY = os.getenv('GOOGLE_API_KEY')
    
    if not GOOGLE_API_KEY:
        print("Google APIキーが設定されていません。")
        return
    
    # Google Gemini APIの設定
    genai.configure(api_key=GOOGLE_API_KEY, transport="rest")
    
    global model  # グローバル変数としてモデルを定義(query_geminiで使用)
    model = genai.GenerativeModel("gemini-1.5-flash")
    
    recognizer = VoskSpeechRecognizer()
    
    try:
        print("音声認識を開始します。Ctrl+Cで終了できます。")
        recognizer.start_recognition()
        
        # 無限ループを防ぐ(Ctrl+Cで停止可能)
        while True:
            time.sleep(1)
    
    except KeyboardInterrupt:
        print("\n音声認識を終了します...")
    finally:
        recognizer.stop_recognition()

if __name__ == "__main__":
    main()

<動作例>

jtalkは返答の最初のブロックしか読み上げないようだけれども、長いレスポンスを全部読み上げられてもというところだから、最初だけで十分かもしれない

Geminiはマルチモーダルなので、音声認識や合成もクラウドでできそうですが、そこまでクラウドにアップロードは躊躇われるので、端末側で処理するのが妥当じゃないかと今は考えています

 

admin