はじめに
Ultralytics製の物体検出モデルであるYOLOv8を用いて、Raspberry Pi 5へ接続したカメラに何の物体が映ったか推論し、標準出力へ表示させてみます。
環境構築について長々と説明しています。
プログラムだけほしい方は#カメラの映像を推論し続けるにジャンプしてください。
ハードウェアを準備
まずは必要な機械たちを用意します。
Raspberry Piは4系でも動作はしますが、5だとそれなりの速さで推論できます。
microSDカードは16GB-32GB程度が適切です。
カメラは何でもいいですが、USBで接続できるものを用意してください。
今回使用したものはこんな感じです:
- Raspberry Pi 5 (メモリ8GBモデル)
- ACアダプター (スイッチサイエンス; SSCI-093453)
- 家に転がっていたmicroSDカード(KLEVV NEO; 64GB)
- OTGカードリーダー(詳細不明)
- 家に放置されていたウェブカメラ(詳細不明)
Raspberry Piへの接続やコードの開発に使用するPCは、Linux/MacなどUnixコマンドが使用できる環境であると仮定します。Windowsをお使いの場合は、ターミナル操作にWSLを使用してください。
イメージを作成
Raspberry Pi Imagerを用いて、SDカードにイメージを書き込みます。
ユーザー名やパスワード、Wi-Fi、SSHなども同時に設定します。
イメージ作成手順
OS設定の変更も同時に行います。
設定を編集する
をクリックします。
ホスト名は自分が区別しやすい適当なものに設定します。
ユーザー名とパスワードをデフォルトのまま放っておくと危ない上、Pi側にも怒られますので、適当なものに変えておきます。
有線接続できるネットワークがなければWi-Fiも設定しておきましょう。
タイムゾーンは正しく設定しておかないとログの日時が大変なことになったりします。
いちいちディスプレイやキーボードを接続するのが面倒なので、SSH接続は有効化しておきます。
今回はより簡単に接続できるパスワード認証で作業を進めますが、安全性を高めたい場合は公開鍵認証のみを許可する
を選択してください。
設定が完了したら、保存
をクリックし、元のウィンドウに戻ったらはい
で設定を適用させます。
最終確認です。書き込みするデバイスが間違いないことを確認し、はい
をクリックします。
SSH接続を確立
お手元の端末からRaspberry Piに接続します。
ディスプレイとキーボードがあれば、直接ターミナルからip a
等を実行してIPアドレスを確認できますが、今回は面倒なので手元からnmapを叩きます。
外のデバイスにスキャンする行為は一般的に攻撃とみなされます。
スキャンは自分のネットワーク上で自分のデバイスにのみ行ってください。
$ sudo nmap -sn 192.168.68.0/24 | grep -B2 -- "Raspberry Pi"
Nmap scan report for 192.168.68.138
Host is up (0.038s latency).
MAC Address: 2C:CF:67:3A:60:FE (Raspberry Pi (Trading))
今回セットアップしているRaspberry Piには192.168.68.138
が割り振られているとわかりました。
では接続しましょう。
$ ssh s23201@192.168.68.138
The authenticity of host '192.168.68.138 (192.168.68.138)' can't be established.
ED25519 key fingerprint is SHA256:****.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.68.138' (ED25519) to the list of known hosts.
s23201@192.168.68.138's password:
Linux yolopi 6.6.51+rpt-rpi-2712 #1 SMP PREEMPT Debian 1:6.6.51-1+rpt3 (2024-10-08) aarch64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Nov 19 22:44:00 2024
s23201@yolopi:~ $
設定変更やパッケージ更新・追加
YOLOを触る前に、設定の変更やパッケージの更新・追加を行っておきます。
raspi-config
対話型CLI上で簡単に設定を変更できます。
$ sudo raspi-config
┌───────────────┤ Raspberry Pi Software Configuration Tool (raspi-config) ├───────────────┐
│ │
│ 1 System Options Configure system settings │
│ 2 Display Options Configure display settings │
│ 3 Interface Options Configure connections to peripherals │
│ 4 Performance Options Configure performance settings │
│ 5 Localisation Options Configure language and regional settings │
│ 6 Advanced Options Configure advanced settings │
│ 8 Update Update this tool to the latest version │
│ 9 About raspi-config Information about this configuration tool │
│ │
│ │
│ │
│ │
│ │
│ <Select> <Finish> │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────┘
1 System Options
を矢印キーで選択し、Enter
で開きます。
GUIブートを無効化したいので、S5 Boot / Auto Login
を選択し、B1 Console
を選びます。Piへ物理的にアクセスできるのが自分のみだとわかっている場合は、B2 Console Autologin
でもOKです。
スプラッシュスクリーンもいらないので切りましょう。S6 Splash Screen
を選択し、No
を選びます。
必要な変更を行い終わったら、Finish
を選び、選択肢に従ってリブートします。
しばらく待った後、再接続しておいてください。
パッケージの更新
今のうちに各パッケージを更新しておきます。
$ sudo apt update && sudo apt upgrade
Hit:1 http://deb.debian.org/debian bookworm InRelease
Get:2 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
...
update-initramfs: Generating /boot/initrd.img-6.6.62+rpt-rpi-2712
'/boot/initrd.img-6.6.62+rpt-rpi-2712' -> '/boot/firmware/initramfs_2712'
パッケージのインストール
piにはデフォルトでviとnanoが入っていますが、vimはありません。
このタイミングでインストールしておきます。
$ sudo apt install vim
...
The following additional packages will be installed:
vim-runtime
Suggested packages:
ctags vim-doc vim-scripts
The following NEW packages will be installed:
vim vim-runtime
0 upgraded, 2 newly installed, 0 to remove and 1 not upgraded.
Need to get 8,449 kB of archives.
After this operation, 41.2 MB of additional disk space will be used.
Do you want to continue? [Y/n]
...
$ sudo update-alternatives --config editor
There are 4 choices for the alternative editor (providing /usr/bin/editor).
Selection Path Priority Status
------------------------------------------------------------
* 0 /bin/nano 40 auto mode
1 /bin/ed -100 manual mode
2 /bin/nano 40 manual mode
3 /usr/bin/vim.basic 30 manual mode
4 /usr/bin/vim.tiny 15 manual mode
Press <enter> to keep the current choice[*], or type selection number: 3
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in manual mode
Pythonのパッケージ管理では一般的にpipを使用しますが、ここではより高速なuvをインストールします。
$ curl -LsSf https://astral.sh/uv/install.sh | sh
downloading uv 0.5.7 aarch64-unknown-linux-gnu
no checksums to verify
installing to /home/s23201/.local/bin
uv
uvx
everything's installed!
To add $HOME/.local/bin to your PATH, either restart your shell or run:
source $HOME/.local/bin/env (sh, bash, zsh)
source $HOME/.local/bin/env.fish (fish)
$ source $HOME/.local/bin/env
パッケージの更新やインストールが終わったら、もう一度リブートしておきます。
rootパスワードを設定
いざという時に使うので忘れず設定しておきましょう。(1敗)
$ sudo passwd
New password:
Retype new password:
passwd: password updated successfully
sudo時にパスワードを要求
なんとRaspberry Piのデフォルトではsudo使用時にパスワードが要求されません。
事故を避けるため、visudo
を使って/etc/sudoers.d/010_pi-nopasswd
の内容をコメントアウトします。
$ sudo visudo /etc/sudoers.d/010_pi-nopasswd
- s23201 ALL=(ALL) NOPASSWD: ALL
+ #s23201 ALL=(ALL) NOPASSWD: ALL
YOLOの環境を用意する
ここからが本題です。先程インストールしたuv
でプロジェクトを作成し、パッケージultralytics[export]
をインストールします。この際、自動的にvenv環境が作成されるので、システムが汚れることはありません。
$ uv init ~/yolov8 && cd $_
Initialized project `yolov8` at `/home/s23201/yolov8`
$ uv add 'ultralytics[export]'
Using CPython 3.11.2 interpreter at: /usr/bin/python3.11
Creating virtual environment at: .venv
Resolved 113 packages in 25.73s
Built coremltools==8.1
Prepared 79 packages in 2m 42s
Installed 79 packages in 1.24s
+ absl-py==2.1.0
+ astunparse==1.6.3
...
+ wheel==0.45.1
+ wrapt==1.17.0
pipを使用するのは避けてください。
どうしてもuvを使用できない場合は、Dockerイメージultralytics/ultralytics:latest-arm64
からpip freeze | tee requirements.txt
でパッケージ一覧を出力し、Piでpip install -r requirements.txt
を実行してください。
pip install 'ultralytics[export]'
は数十時間かかる上、途中でエラー落ちするリスクがあります。
開発環境を用意する
このままSSH越しでvimを使ってコードを書くという選択肢もありますが、これではあまりにしんどいので開発環境を用意しましょう。
ローカルにもPython環境を建てる場合
yoloの環境を用意すると同じ作業をローカルで繰り返してください。
このディレクトリを、好きなエディタで開けば完成です。
注意点として、開発環境で作成したファイルは手動で同期する必要があります。
scp等で転送するか、コードをgit管理下に置いて、開発環境からpushし、Pi側からpullすると良いでしょう。
以下はPi側にbareリポジトリを作成し、push/pullを行えるような環境を構築する例です。
$ git init --bare -b main ~/yolov8.git
Initialized empty Git repository in /home/s23201/yolov8.git
$ git branch -m 'main'
$ rm hello.py
$ cat << EOF >> .gitignore
bus.jpg
yolo*.pt
runs/
EOF
$ git config --global user.name 's23201'
$ git config --global user.email 's23201@yolopi.local'
$ git add . && git commit -m 'Initialized repository'
[main (root-commit) e8dc213] Initialized repository
5 files changed, 2274 insertions(+)
create mode 100644 .gitignore
create mode 100644 .python-version
create mode 100644 README.md
create mode 100644 pyproject.toml
create mode 100644 uv.lock
$ git remote add origin ~/yolov8.git
$ git push --set-upstream origin main
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 4 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 79.29 KiB | 5.66 MiB/s, done.
Total 7 (delta 0), reused 0 (delta 0), pack-reused 0
To /home/s23201/yolov8.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
$ curl -LsSf https://astral.sh/uv/install.sh | sh
downloading uv 0.5.7 x86_64-unknown-linux-gnu
no checksums to verify
installing to /home/s23201/.local/bin
uv
uvx
everything's installed!
To add $HOME/.local/bin to your PATH, either restart your shell or run:
source $HOME/.local/bin/env (sh, bash, zsh)
source $HOME/.local/bin/env.fish (fish)
$ source $HOME/.local/bin/env
$ git clone s23201@192.168.68.138:/home/s23201/yolov8.git
Cloning into 'yolov8'...
s23201@192.168.68.138's password:
remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 7 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (7/7), 79.29 KiB | 3.60 MiB/s, done.
$ cd yolov8
$ uv sync --frozen
Using CPython 3.11.9 interpreter at: /usr/bin/python3.11
Creating virtual environment at: .venv
Prepared 91 packages in 48m 07s
Installed 91 packages in 264ms
+ absl-py==2.1.0
+ astunparse==1.6.3
...
+ wheel==0.45.1
+ wrapt==1.17.0
VSCodeでPiへ接続する場合
Visual Studio Codeを用いてPiへ接続し、Pi上で直接開発を行います。
F1
でコマンドパレットを開き、「ssh」と入力したら、Remote-SSH: ホストに接続する...
を選択します。
新しいウィンドウが開きます。ここでパスワードが要求されるので入力します。
自動的にVS Code Serverがインストールされます。
フォルダーを開く
から、ultralyticsパッケージをインストールしたディレクトリを選択します。
完了です。ここで書いたコードは、すぐにPiへ反映されます。
YOLOv8の動作確認を行う
まずは推論が行えることを確認します。
今回はYOLOv8系モデルのうち、軽量なyolov8n.pt
を使用します。
from ultralytics import YOLO
from ultralytics.engine.results import Results
model = YOLO('yolov8n.pt')
results: list[Results] = model(
'https://ultralytics.com/images/bus.jpg',
save=True
)
$ uv run yolov8-test.py
Creating new Ultralytics Settings v0.0.6 file ✅
View Ultralytics Settings with 'yolo settings' or at '/home/s23201/.config/Ultralytics/settings.json'
Update Settings with 'yolo settings key=value', i.e. 'yolo settings runs_dir=path/to/dir'. For help see https://docs.ultralytics.com/quickstart/#ultralytics-settings.
Downloading https://github.com/ultralytics/assets/releases/download/v8.3.0/yolov8n.pt to 'yolov8n.pt'...
100%|██████████████████████████████████████████████████| 6.25M/6.25M [00:06<00:00, 946kB/s]
Downloading https://ultralytics.com/images/bus.jpg to 'bus.jpg'...
100%|███████████████████████████████████████████████████| 134k/134k [00:00<00:00, 3.45MB/s]
image 1/1 /home/s23201/yolov8/bus.jpg: 640x480 4 persons, 1 bus, 1 stop sign, 602.9ms
Speed: 7.9ms preprocess, 602.9ms inference, 2.3ms postprocess per image at shape (1, 3, 640, 480)
Results saved to /home/s23201/yolov8/runs/detect/predict
結果をPi上で開けない場合は、代わりにscpで転送して閲覧します。
$ scp s23201@192.168.68.138:/home/s23201/yolov8/runs/detect/predict/bus.jpg ./
s23201@192.168.68.138's password:
bus.jpg 100% 357KB 3.0MB/s 00:00
または、waypipeを使用して、Piで描画したウィンドウをローカルに転送することもできます。
waypipeを使用する例
$ sudo apt install waypipe
[sudo] password for s23201:
...
The following NEW packages will be installed:
waypipe
0 upgraded, 1 newly installed, 0 to remove and 1 not upgraded.
Need to get 78.2 kB of archives.
After this operation, 285 kB of additional disk space will be used.
...
$ waypipe ssh s23201@192.168.68.138 eom /home/s23201/yolov8/runs/detect/predict/bus.jpg
s23201@192.168.68.138's password:
(eom:21645): dbind-WARNING **: 21:58:20.466: AT-SPI: Error retrieving accessibility bus address: org.freedesktop.DBus.Error.ServiceUnknown: The name org.a11y.Bus was not provided by any .service files
(eom:21645): EOM-WARNING **: 21:58:20.504: Error loading Peas typelib: Typelib file for namespace 'Peas', version '1.0' not found
(eom:21645): EOM-WARNING **: 21:58:20.504: Error loading PeasGtk typelib: Typelib file for namespace 'PeasGtk', version '1.0' not found
カメラの映像を推論し続ける
カメラの映像を取得し推論させつづけることで、カメラに何が映っているかコンソールから確認できるようにします。
まずはカメラが接続されていることを確認します。
$ v4l2-ctl --list-devices
...
USB2.0 Camera: PC Camera (usb-xhci-hcd.0-2):
/dev/video0
/dev/video1
/dev/media3
/dev/video0
で利用可能だとわかりました。
このインデックス0
をPython側でも使用します。
カメラはcv2
のVideoCapture()
にインデックスを渡すことで取得できます。
カメラの映像はread()
から得られます。
>>> import cv2
>>> camera = cv2.VideoCapture(0)
>>> success, frame = camera.read()
>>> success
True
>>> cv2.imwrite('image.jpg', frame)
True
$ file image.jpg
image.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 640x480, components 3
では本体となるコードを準備します。
煩雑なコードは関数化してutil.py
に分割することにします。
import cv2
class NoCameraFoundException(Exception):
pass
class CameraCaptureException(Exception):
pass
def get_cam() -> cv2.VideoCapture:
max_try = 20
for i in range(max_try):
cam = cv2.VideoCapture(i)
if cam.isOpened():
return cam
cam.release()
raise NoCameraFoundException(f'Cannot detect available camera; tried {max_try} cameras')
def shot(cam: cv2.VideoCapture) -> cv2.typing.MatLike:
success, frame = cam.read()
if not success:
raise CameraCaptureException('Failed to read frame from camera')
return frame
def simplify_summary(summaries: list) -> str:
sorted_summaries = sorted(
summaries,
key=lambda x: x['confidence']
)
simplified_summaries = [
f'{summary["name"]}[{round(summary["confidence"] * 100, 1)}%]'
# f'{summary["name"]}@{summary["class"]}[{round(summary["confidence"] * 100, 1)}%]'
for summary in sorted_summaries
]
return ', '.join(simplified_summaries)
import logging
from logging import getLogger
from ultralytics import YOLO
from ultralytics.engine.results import Results
import util
logger = getLogger(__name__)
logging.basicConfig(level='INFO')
model = YOLO('yolov8n.pt')
camera = util.get_cam()
try:
while True:
frame = util.shot(camera)
results: list[Results] = model(frame, verbose=False)
summary = util.simplify_summary(results[0].summary())
summary and logger.info(summary)
except KeyboardInterrupt:
logger.warning('Exiting...')
finally:
camera.release()
logger.warning('Camera released')
カメラを自分に向けて、person
と表示されれば成功です。
$ uv run main.py
INFO:__main__:person[71.4%]
INFO:__main__:person[70.4%]
INFO:__main__:person[76.0%]
INFO:__main__:person[77.9%]
INFO:__main__:person[68.6%]
INFO:__main__:person[72.8%]
INFO:__main__:keyboard[26.7%]
INFO:__main__:remote[42.3%]
INFO:__main__:keyboard[40.7%]
INFO:__main__:keyboard[27.5%]
INFO:__main__:remote[50.4%]
INFO:__main__:remote[27.7%]
INFO:__main__:cat[25.0%], laptop[26.6%]
INFO:__main__:person[50.8%]
INFO:__main__:keyboard[41.6%]
INFO:__main__:mouse[36.5%], keyboard[56.9%]
INFO:__main__:cell phone[26.4%], mouse[27.2%], keyboard[52.5%], laptop[63.5%]
INFO:__main__:laptop[27.9%], toothbrush[36.7%], keyboard[57.3%]
INFO:__main__:cell phone[25.7%]
INFO:__main__:cell phone[28.4%], laptop[38.2%], keyboard[52.3%]
INFO:__main__:cell phone[36.1%], cell phone[39.5%], keyboard[48.5%], laptop[56.0%]
INFO:__main__:keyboard[40.8%], laptop[47.8%]
INFO:__main__:laptop[52.9%], keyboard[56.7%]
INFO:__main__:laptop[57.6%], keyboard[58.6%]
INFO:__main__:laptop[31.6%], keyboard[50.3%]
INFO:__main__:book[25.5%], toothbrush[67.8%]
INFO:__main__:toothbrush[56.1%]
INFO:__main__:toothbrush[51.3%]
INFO:__main__:toothbrush[28.9%]
INFO:__main__:tie[36.5%], person[81.9%]
INFO:__main__:person[86.3%]
INFO:__main__:person[82.2%]
...
^CWARNING:__main__:Exiting...
WARNING:__main__:Camera released
システムを改善する
より実用的なシステムを組み上げる際、必要になるかもしれない事項をまとめておきます。
ロギングを整理する
dictConfig
やカスタムハンドラー、カスタムフォーマッターを用いて、ロギングを整えられます。
標準出力やファイルへの書き出しだけでなく、調整次第ではWebhookでのログ送信にも対応できます。
動画ファイルを書き出す
カメラから映像を取得し、推論しながら書き出すことで、動画ファイルという形で記録を残せます。
録画を推論と同スレッドで実行すると動画のフレームレートが著しく下がるため、multiprocessing
等で並列処理を行うのが理想です。
cv2で書き出す際、mp4形式などを使用する場合は、release()
を必ず行ってください。
終了処理が正しく行われないまま、例えばエラーなどでプログラムが終了すると、書き込み途中の動画ファイルは再生できなくなります。
設定を変更できるようにする
python-dotenv
やpyyaml
等で、設定ファイルを読み込めるようにすると、システムがより柔軟になります。
使用するモデルや各種しきい値、ログの書き出し先などは重要です。
安定性を高める
外部ストレージを接続し、Pi本体にOverlayFSを設定することで、SDカードへの書き込みを避けられます。これにより、長期的に運用する場合でもSDカードが故障しづらくなります。