はじめに
Raspberry pi Zero 2Wを購入したはいいものの、思ったよりも使い道がなく放置していたためなんとなく思いついたDiscordBotにカメラの画像または映像を送信するという使い道を思いついたので、それの制作方法を共有したいと思います。
中級者~(CLI操作を理解してる人向け)
必要なもの
・Raspberry pi zero 2W(2024/09/02時点ではどこのお店(ネット通販)でも品切れ中、自分はDigikeyで購入しました。)
・カメラ(今回使用したカメラモジュール https://amzn.asia/d/dP9jh0V)
・ケース(今回使用したケース https://amzn.asia/d/8OJk9JR)
・micro SDカード(64GB以上がおすすめ、今回使用したSDカード https://amzn.asia/d/hJ0b8ON)
・カードリーダ(USB3.0接続できるものがおすすめ)
・USBハブ(あったほうがいい)
OSの書き込み
Raspberry piの公式サイトからRaspberry Pi Imagerをダウンロードしインストールします。
https://www.raspberrypi.com/software/ または https://downloads.raspberrypi.org/imager/imager_latest.exe(ダウンロード直リンク)
インストールし終わったら、画像の通りに進んでいきます。
1.デバイスの選択で、Raspberry Pi Zero 2Wを選択
2.OSの選択では、画像赤枠のRaspberry Pi OS(other)を選択し、一番下まで移動し、Raspberry Pi OS(Legacy,64-bit) Liteを選択します。
ここで、Debian Bookworm版のRaspberry Pi OSを使用するとこの後詰みます。必ず、A port of Debian Bullseyeと書かれたOSを選択してください。
3.ストレージの選択で、各自のMicro SDカードを選択し、右下の「次へ」を選択します。
4.「設定を編集する」を選択し、ユーザー名やWiFiパスワードなどを各自で入力してください。「サービス」という項目ではSSHを有効化しておきます。それぞれの設定が終わったら「保存」をクリックし、「はい」をクリックすると、「データを完全に削除しますか。」を聞かれますが、「はい」を選択することで書き込みが開始されます。
OS、Raspberry Piのセットアップ
今回使用したケースでは、画像のように少し削らないとリボンケーブルが通りませんでした。
電源ケーブル、キーボード、MiniHDMI、カメラなどを接続します。
起動したら、まず各自で設定したユーザー名、パスワードを入力しログインします。
ログイン後、以下のコマンドを入力し、インターネットの接続ができていることを確認します。(コマンドを止めるにはCTRL+C)
ping 8.8.8.8
以下のコマンドを入力し、アップデート、アップグレードをします。途中で何か聞かれた場合、yを入力すればOKです。
sudo apt update && sudo apt upgrade -y
以下からはネットワーク接続が確認できなかった人向けの説明。
まず、以下のコマンドを入力します。パスワードを求められるので各自で決めたパスワードを入力してください。
sudo raspi-config
画像のように、System Options > Wireless LANと進みます。(エンターキーで進める)
ここでネットワーク名を求められるので各自のWiFiの名前を入力してください。
次にパスワードを求められるのでWiFiのパスワードを入力します。入力が終わったら、エンターを押すと最初の画面に戻ります。
TABキーを2回ほど押すと、右下のFinishが赤くなるのでエンターを押します。
以下のコマンドを入力し、Raspberry pi 2Wを再起動します。
sudo reboot now
再起動後、もう一度以下のコマンドを入力しネットワークとの接続ができているか確認します。(CTRLキー+Cで止めれる)
ping 8.8.8.8
もし、接続できていない場合はもう一度上のネットワーク設定をやり直してください。(ネットワーク名かパスワードを打ち間違えている)
以下のコマンドを入力し、アップデート、アップグレードをします。途中で何か聞かれた場合、yを入力すればOKです。
sudo apt update && sudo apt upgrade -y
IPアドレスの固定
まず初めに、以下のコマンドでルータのIPを特定します。
ip a
以下の画像の部分をメモなどして覚えてください。
次にIPアドレスを固定します。
以下のコマンドを入力してください。
sudo nano /etc/dhcpcd.conf
以下の画像のような画面が表示されます。
矢印キーで一番下までいき、画像のように入力します。
static ip_address=192.168.11.100/24 #適当なipアドレスにする(192.168.1の人は192.168.1.100などとする)
static routers=192.168.1.1 #または192.168.1.1
static domain_name_servers=8.8.8.8 8.8.4.4
static routersは、さきほどメモしたipを入力しますが、192.168.11の場合は、画像のように192.168.11.1とし、192.168.1の場合は、192.168.1.1としてください。
また、ip_addressは192.168.~.50~200の間にすると他の機器と被ることはないでしょう。
入力が終わったあとはCTRLキー+Oで保存し、CTRL+Xで閉じます。
閉じた後は再起動します。
sudo reboot now
起動後ログインし、以下のコマンドでIP_addressが変わっている(固定されている)ことを確認してください。
ip a
画像のところが、先ほどしてしたIPアドレスに変更されていれば完了です。
TeraTermでSSHログイン
ファイルの移動や、コマンドの入力を簡単にするべく、TeraTermのセットアップをします。
まず、TeraTermを普段使用するPCにダウンロード、インストールします。
以下のサイトから.exeファイルをクリックし、ダウンロードします。
現時点のバージョンは5.2ですが、ダウンロードする際の最新バージョンをダウンロードすることをおすすめします。
https://github.com/TeraTermProject/teraterm/releases
ダウンロードしたexeファイルを起動し、画面に沿ってインストールしてください。
インストール後起動し、ホスト欄にRaspberry Pi Zero 2Wの固定したIPアドレス(自分の場合は192.168.11.100)を入力し、OKを押してください。
以下の画像のような警告がでますが、続行を押してください。
次にログイン画面が表示されるので、Raspberry Pi Zero 2Wのユーザー名とパスワードを入力しログインしてください。
ログイン後、無事に完了すると以下の画面になります。
カメラのセットアップ(USBカメラを使用する場合は以下の設定は不要)
以下のコマンドを入力し設定画面に移動する。
sudo raspi-config
矢印キーで3.Interface Optionsまで移動し、エンターキー
一番上にLegacy Cameraという項目があり、そのままエンターを押していく。
ここでLegacy Camera Enable/Disableがない場合、OSの選択が間違っています。Debian BookWormではなくDebian Bullseyeをインストールしなおしてください。最初の「OSの書き込み」を見ながらやり直してください。
2回ほど尋ねられますが、そのままエンターキーを教えてください。
最初の画面に戻った後、Tabキーを2回ほど押すと、Finishのところが赤くなるのでそのままエンターキーを押してください。
以下のコマンドを入力しRaspberry Pi Zero 2Wを再起動します。
sudo reboot now
再起動後、ログインが完了したあと以下のコマンドを入力してください。
vcgencmd get_camera
以下の画像のようにsupported=1 detected=1となっていれば正しく接続されています。
もし以下のようになっていなければ、カメラのコネクタが正しく接続されているかチェックする、再起動する、上で説明したカメラのセットアップをもう一度行うなどしてください。
Pythonの仮想環境のセットアップ
まず、以下のコマンドでフォルダを作成、移動します。
mkdir venv && cd venv
以下のコマンドで、pythonで仮想環境を構築するためのパッケージをインストールします。
sudo apt install python3-venv
次に、以下のコマンドでPythonの仮想環境を作成します。
python3 -m venv discord
作成した仮想環境をアクティベートします。
source discord/bin/activate
正しく作成できると以下のように(discord)~~@raspberrypi
と表示されます。
次にプログラムを保存しておくフォルダを作成します。
mkdir program && cd program && mkdir video && mkdir image
現在のディレクトリ構成
venv/
└─discord/
└─program/
└─video/
└─image/
DiscordBotなどを動かすpythonモジュールをインストールしていきます。
以下のコマンドを順番に入力してください。
pip install opencv-python
pip install discord.py
sudo apt install ffmpeg -y
sudo apt-get install -y libgl1-mesa-glx
DiscordBotの作成
以下のサイトにアクセスします。
ログイン後、「New Application」を選択し、適当な名前を決めCreateします。
次に左側の「OAuth2」を開き、画像のようにbotにチェックをいれます。すると下に「BOT PERMISSIONS」という項目が現れるので画像のようにチェックしてください。
チェック完了後、一番下に「GENERATED URL」という欄に招待リンクが生成されるので、コピーしブラウザで開きます。
ブラウザで開くと以下の画像のようなものが表示されるので、各自の追加したいサーバを選択し、追加します。(管理者権限を保有しているサーバに追加してください。)
次に、BOTの細かい設定をしていきます。
「Installation」にある「Install Link」をNoneに変更し、一番下の「Save Changes」を選択します。
「BOT」にある「PUBLIC BOT」をOFF、「PRESENCE INTENT」,「SERVER MEMBERS INTENT」,「MESSAGE CONTENT INTENT」をONにし、一番下の「Save Changes」を選択します。
最後に、プログラムでBOTを操作するためのトークンを発行します。
「BOT」にある「RESET TOKEN」を選択します。
Discordアカウントのパスワードを聞かれるので、入力後Submitを選択。
Submit後、以下の画像のようにトークンが発行されるためコピーし、メモします。
このトークンは一度ブラウザを更新すると消えるため、必ずメモしてください!
もしメモし忘れた場合は、もう一度トークンをリセットし直す必要があります。
プログラム
今回の仕様としては、discordのチャットで/image,/videoと入力すると、画像、動画をdiscordのチャットに送信する。また、一時間毎(任意の時間毎)にDiscordのチャットに動画(画像)を送信する。
画像、録画のファイルが溜まるとMicro SDカードがすぐに満タンになるため、 撮影から1週間が過ぎた録画、画像ファイルは自動的に削除されるようになっています。(1週間ではなく、3日前など別の期間を指定したい場合は要プログラム変更)
- Botの起動
まず、DiscordのBotを追加したサーバでBot専用チャットを作成します。
次に、venv/programに移動し、下に書いてあるdiscordbot.pyというプログラムをコピーしRaspberry Pi zero 2Wに貼り付けます。cd program
nano discordbot.py
貼り付け後、作成したチャンネルを右クリックし、チャンネルIDをコピーします。
コピー後、loop_channel_idに貼り付けます。
プログラムの一番下にいき、client.run()の()の中に、メモしておいたDiscordTokenを貼り付けます。貼り付け後は、CTRL+Oで保存し、CTRL+Xで元の画面に戻ります。
次に、下のプログラムにあるcamera.pyをコピーし、貼り付けます。
貼り付け後、CTRL+Oで保存し、CTRL+Xで元の画面に戻ります。
ここまでの作業が完了すると以下のディレクトリ構成になると思います。
venv/
└─discord/bin/activate
└─program/
discordbot.py
camera.py
└─video/
└─image/
それではBOTを起動していきます。
以下の画像のように(discord)と表示されていることを確認します。なっていない場合は、以下のコマンドを実行してください。
cd && cd venv && source discord/bin/activate
次に、programフォルダに移動し、Bot(プログラム)を起動します。
cd program
python discordbot.py
起動後、以下の画像のようになれば成功です。
また、起動後すぐに動画の撮影が開始されDiscordのチャットに映像が送信されます。
- Discordでの使い方
Botチャンネルで、/imageと入力すると2つ引数が出てきます。
引数を何もしてせずに、/imageだけを送信すると、1920*1080で撮影され、日付が印字されます。
widthは撮影サイズの横の長さを指定。(16:9の比でwidthからheightを自動的に求めます。)
dateは、画像に日付を印字するかを指定。(Trueで印字、Falseで印字しない)
/videoの使い方。引数は4つあります。
timeは撮影時間を指定します。(初期値は10)
widthは動画の横の長さを指定。(初期値は640で、4:3の比で録画されます。)私が使用しているカメラが16:9で撮影すると、BGR2RGB変換はうまくできなかったので、4:3にしてます。画質を上げすぎると、Raspberry Pi Zero 2Wが処理しきれずプログラムが固まります。
fpsは録画のfpsを指定。(初期値は30)
dateは、画像に日付を印字するかを指定。(Trueで印字、Falseで印字しない)
/set_loopの使い方。引数は7つあります。(定期撮影の設定)
rec_timeは撮影時間を指定します。(初期値は10)
widthは動画の横の長さを指定。(初期値は640で、4:3の比で録画されます。)
fpsは録画のfpsを指定。(初期値は30)
time_spanは録画の周期時間を指定。単位は秒(初期値は3600)ただし、Botを起動したときに1時間のタイマーがスタートしているため、次の撮影が終わってからの反映になります。
channel_idは定期撮影を送信するチャンネルIDを変更できます。(初期値は最初に指定したIDです。)
dateは、画像に日付を印字するかを指定。(Trueで印字、Falseで印字しない)
rec_modeは定期撮影を動画か画像なのかを指定できます。(初期値はTrue,Falseで画像)
import discord
import camera
import asyncio
from discord import app_commands
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
loop_rec_time = 10
loop_width = 640
loop_fps = 30
loop_time_span = 3600
loop_channel_id = #定期撮影の画像または映像を送信したいチャンネルのIDを入力
loop_date = True
loop_rec_mode = True
@client.event
async def on_ready():
print(f'We have logged in as {client.user}')
await tree.sync()
client.loop.create_task(send_message_every_hour())
#/imageで画像を撮影、送信する
@tree.command(name="image",description="画像を取得します。")
async def image_command(interaction: discord.Interaction,width:int=1920,date:bool=True):
file_name = camera.get_frame(width,date)
if file_name == "Failed":
await interaction.response.send_message(file_name,ephemeral=True)
else:
with open(file_name, "rb") as image_file:
discord_file = discord.File(image_file, file_name)
await interaction.response.send_message(file=discord_file)
#/videoで動画を撮影、送信する
@tree.command(name="video",description="動画を取得します。")
async def cap_command(interaction: discord.Interaction,time:int=10,width:int=640,fps:int=30,date:bool=True):
try:
await interaction.response.send_message("録画中...")
file_name = camera.cap_video(time,width,fps,date)
with open(file_name, "rb") as video_file:
discord_file = discord.File(video_file, file_name)
await interaction.followup.send(file=discord_file)
except discord.errors.InteractionResponded:
print("インタラクションに既に応答されています")
except Exception as e:
print(f"エラーが発生しました: {e}")
#/set_loopで定期撮影の設定を変更
@tree.command(name="set_loop",description="定期的に録画する設定を変更できます。ex)width=640,1280,1920,date=Trueで表示、Falseで非表示,rec_mode=Trueで動画,Falseで画像")
async def test_command(interaction: discord.Interaction,rec_time:int=10,width:int=640,fps:int=30,time_span:int=3600,channel_id:int=loop_channel_id,date:bool=True,rec_mode:bool=True):
global loop_time_span,loop_rec_time,loop_channel_id,loop_fps,loop_width,loop_date,loop_rec_mode
loop_time_span = time_span
loop_rec_time = rec_time
loop_fps = fps
loop_width = width
loop_channel_id = channel_id
loop_date = date
loop_rec_mode = rec_mode
await interaction.response.send_message(f"現在の設定値,定期録画時間:{loop_time_span}秒,撮影モード:{loop_rec_time},録画時間:{loop_rec_time},録画fps:{loop_fps},録画映像のwidth:{loop_width},配信チャンネルID:{loop_channel_id},日付の書き込み:{loop_date}")
#任意の時間毎に撮影
async def send_message_every_hour():
await client.wait_until_ready()
channel = client.get_channel(loop_channel_id)
while not client.is_closed():
await channel.send("定期撮影中...")
if loop_rec_mode == True:
file_name = camera.cap_video(loop_rec_time,loop_width,loop_fps,loop_date)
else:
file_name = camera.get_frame(loop_width,loop_date)
await channel.send("定期撮影:", file=discord.File(file_name))
await asyncio.sleep(loop_time_span) # 3600秒 (1時間) 待機
client.run('各自のDiscordTokenを入力')
import cv2
from datetime import datetime,timedelta
import time
import subprocess
import os
#日付のフォント、サイズの定義
font = cv2.FONT_HERSHEY_SIMPLEX
font_image_scale = 1
font_video_scale = 0.5
font_thickness = 2
#1週間以上前に作成された画像、動画を削除する
def delete_file(folder_path):
now = datetime.now()
for filename in os.listdir(folder_path):
file_path = os.path.join(folder_path, filename)
if filename.lower().endswith(('.jpg', '.jpeg', '.png',".mp4")):
file_creation_time = datetime.fromtimestamp(os.path.getctime(file_path))
one_week_ago = now - timedelta(weeks=1)#ex) days=3
if file_creation_time < one_week_ago:
# ファイルを削除
os.remove(file_path)
print(f'Deleted: {file_path}')
def get_frame(width,date):
height = int((width * 9) / 16)
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
ret,frame = cap.read()
if ret:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
now = datetime.now()
dt = str(now.strftime('%Y_%m_%d-%H_%M_%S'))
file_name = f"image/{dt}.jpg"
if date:
date_time = now.strftime("%Y-%m-%d %H:%M:%S")
cv2.putText(frame_rgb, date_time, (10, height-10), font, font_image_scale, (255, 255, 255), font_thickness, cv2.LINE_AA)
cv2.imwrite(file_name, frame_rgb)
cap.release()
delete_file("image/")
return file_name
else:
return "Failed"
def cap_video(cap_time,width,fps,date):
cap = cv2.VideoCapture(0)
height = int((width * 3) / 4)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
now = datetime.now()
dt = str(now.strftime('%Y_%m_%d-%H_%M_%S'))
file_name_b = f"video/{dt}-b.mp4"
file_name_a = f"video/{dt}-a.mp4"
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(file_name_b, fourcc, fps, (width, height))
start_time = time.time()
while int(time.time() - start_time) < cap_time:
ret, frame = cap.read()
if ret:
if date:
date_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cv2.putText(frame, date_time, (10, height-10), font, font_video_scale, (255, 255, 255), font_thickness, cv2.LINE_AA)
out.write(frame)
cap.release()
out.release()
#Discordのチャットに8MBまでしか送信できないため、ffpmegで動画を圧縮(ファイルサイズを縮小)する
subprocess.run([
"ffmpeg", '-i', file_name_b, '-b:v', '4M' , file_name_a
], check=True)
os.remove(file_name_b)
delete_file("video/")
return file_name_a