1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry Pi Zero 2WとDiscordBotを使用してDiscordにカメラの画像または映像を送信してみる

Posted at

はじめに

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を選択
スクリーンショット 2024-09-02 100911.png
2.OSの選択では、画像赤枠のRaspberry Pi OS(other)を選択し、一番下まで移動し、Raspberry Pi OS(Legacy,64-bit) Liteを選択します。
ここで、Debian Bookworm版のRaspberry Pi OSを使用するとこの後詰みます。必ず、A port of Debian Bullseyeと書かれたOSを選択してください。
スクリーンショット 2024-09-02 100934.png
スクリーンショット 2024-09-02 101029.png
3.ストレージの選択で、各自のMicro SDカードを選択し、右下の「次へ」を選択します。
スクリーンショット 2024-09-02 101222.png
4.「設定を編集する」を選択し、ユーザー名やWiFiパスワードなどを各自で入力してください。「サービス」という項目ではSSHを有効化しておきます。それぞれの設定が終わったら「保存」をクリックし、「はい」をクリックすると、「データを完全に削除しますか。」を聞かれますが、「はい」を選択することで書き込みが開始されます。
スクリーンショット 2024-09-02 101230.png
スクリーンショット 2024-09-02 101306.png
スクリーンショット 2024-09-02 101319.png
スクリーンショット 2024-09-02 101325.png
スクリーンショット 2024-09-02 102931.png

OS、Raspberry Piのセットアップ

今回使用したケースでは、画像のように少し削らないとリボンケーブルが通りませんでした。
PXL_20240902_013434671.MP.jpg
電源ケーブル、キーボード、MiniHDMI、カメラなどを接続します。
起動したら、まず各自で設定したユーザー名、パスワードを入力しログインします。
Screenshot 2024-09-02 10-44-44.png
ログイン後、以下のコマンドを入力し、インターネットの接続ができていることを確認します。(コマンドを止めるにはCTRL+C)

ping 8.8.8.8

image.png
以下のコマンドを入力し、アップデート、アップグレードをします。途中で何か聞かれた場合、yを入力すればOKです。

sudo apt update && sudo apt upgrade -y

以下からはネットワーク接続が確認できなかった人向けの説明。

まず、以下のコマンドを入力します。パスワードを求められるので各自で決めたパスワードを入力してください。

sudo raspi-config

画像のように、System Options > Wireless LANと進みます。(エンターキーで進める)
Screenshot 2024-09-02 10-54-29.png
Screenshot 2024-09-02 10-54-35.png
ここでネットワーク名を求められるので各自のWiFiの名前を入力してください。
Screenshot 2024-09-02 10-55-15.png
次にパスワードを求められるのでWiFiのパスワードを入力します。入力が終わったら、エンターを押すと最初の画面に戻ります。
Screenshot 2024-09-02 10-54-47.png
TABキーを2回ほど押すと、右下のFinishが赤くなるのでエンターを押します。
Screenshot 2024-09-02 11-00-19.png
以下のコマンドを入力し、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

以下の画像の部分をメモなどして覚えてください。
Screenshot 2024-09-02 11-51-00.png
次にIPアドレスを固定します。
以下のコマンドを入力してください。

sudo nano /etc/dhcpcd.conf

以下の画像のような画面が表示されます。
Screenshot 2024-09-02 12-46-25.png
矢印キーで一番下までいき、画像のように入力します。
Screenshot 2024-09-02 12-46-41.png

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アドレスに変更されていれば完了です。
Screenshot 2024-09-02 12-54-31.png

TeraTermでSSHログイン

ファイルの移動や、コマンドの入力を簡単にするべく、TeraTermのセットアップをします。
まず、TeraTermを普段使用するPCにダウンロード、インストールします。
以下のサイトから.exeファイルをクリックし、ダウンロードします。
現時点のバージョンは5.2ですが、ダウンロードする際の最新バージョンをダウンロードすることをおすすめします。
https://github.com/TeraTermProject/teraterm/releases
スクリーンショット 2024-09-02 130607.png
ダウンロードしたexeファイルを起動し、画面に沿ってインストールしてください。
インストール後起動し、ホスト欄にRaspberry Pi Zero 2Wの固定したIPアドレス(自分の場合は192.168.11.100)を入力し、OKを押してください。
スクリーンショット 2024-09-02 130916.png
以下の画像のような警告がでますが、続行を押してください。
image.png
次にログイン画面が表示されるので、Raspberry Pi Zero 2Wのユーザー名とパスワードを入力しログインしてください。
スクリーンショット 2024-09-02 131840.png
ログイン後、無事に完了すると以下の画面になります。
スクリーンショット 2024-09-02 132037.png

カメラのセットアップ(USBカメラを使用する場合は以下の設定は不要)

以下のコマンドを入力し設定画面に移動する。

sudo raspi-config

矢印キーで3.Interface Optionsまで移動し、エンターキー
Screenshot 2024-09-02 11-09-32.png
一番上にLegacy Cameraという項目があり、そのままエンターを押していく。
ここでLegacy Camera Enable/Disableがない場合、OSの選択が間違っています。Debian BookWormではなくDebian Bullseyeをインストールしなおしてください。最初の「OSの書き込み」を見ながらやり直してください。
Screenshot 2024-09-02 11-09-39.png
2回ほど尋ねられますが、そのままエンターキーを教えてください。
Screenshot 2024-09-02 11-09-45.png
Screenshot 2024-09-02 11-09-49.png
最初の画面に戻った後、Tabキーを2回ほど押すと、Finishのところが赤くなるのでそのままエンターキーを押してください。
Screenshot 2024-09-02 11-00-19.png
以下のコマンドを入力しRaspberry Pi Zero 2Wを再起動します。

sudo reboot now

再起動後、ログインが完了したあと以下のコマンドを入力してください。

vcgencmd get_camera

以下の画像のようにsupported=1 detected=1となっていれば正しく接続されています。
もし以下のようになっていなければ、カメラのコネクタが正しく接続されているかチェックする、再起動する、上で説明したカメラのセットアップをもう一度行うなどしてください。
Screenshot 2024-09-02 11-21-11.png

Pythonの仮想環境のセットアップ

まず、以下のコマンドでフォルダを作成、移動します。

mkdir venv && cd venv

以下のコマンドで、pythonで仮想環境を構築するためのパッケージをインストールします。

sudo apt install python3-venv

次に、以下のコマンドでPythonの仮想環境を作成します。

python3 -m venv discord

作成した仮想環境をアクティベートします。

source discord/bin/activate

正しく作成できると以下のように(discord)~~@raspberrypiと表示されます。
Screenshot 2024-09-02 11-35-42.png
次にプログラムを保存しておくフォルダを作成します。

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します。
スクリーンショット 2024-09-02 132532.png
スクリーンショット 2024-09-02 132732.png
次に左側の「OAuth2」を開き、画像のようにbotにチェックをいれます。すると下に「BOT PERMISSIONS」という項目が現れるので画像のようにチェックしてください。
スクリーンショット 2024-09-02 133923.png
image.png
チェック完了後、一番下に「GENERATED URL」という欄に招待リンクが生成されるので、コピーしブラウザで開きます。
image.png
ブラウザで開くと以下の画像のようなものが表示されるので、各自の追加したいサーバを選択し、追加します。(管理者権限を保有しているサーバに追加してください。)
image.png
次に、BOTの細かい設定をしていきます。
「Installation」にある「Install Link」をNoneに変更し、一番下の「Save Changes」を選択します。
image.png
「BOT」にある「PUBLIC BOT」をOFF、「PRESENCE INTENT」,「SERVER MEMBERS INTENT」,「MESSAGE CONTENT INTENT」をONにし、一番下の「Save Changes」を選択します。
image.png
最後に、プログラムでBOTを操作するためのトークンを発行します。
「BOT」にある「RESET TOKEN」を選択します。
image.png
image.png
Discordアカウントのパスワードを聞かれるので、入力後Submitを選択。
image.png
Submit後、以下の画像のようにトークンが発行されるためコピーし、メモします。
このトークンは一度ブラウザを更新すると消えるため、必ずメモしてください!
もしメモし忘れた場合は、もう一度トークンをリセットし直す必要があります。

image.png

プログラム

 今回の仕様としては、discordのチャットで/image,/videoと入力すると、画像、動画をdiscordのチャットに送信する。また、一時間毎(任意の時間毎)にDiscordのチャットに動画(画像)を送信する。
画像、録画のファイルが溜まるとMicro SDカードがすぐに満タンになるため、 撮影から1週間が過ぎた録画、画像ファイルは自動的に削除されるようになっています。(1週間ではなく、3日前など別の期間を指定したい場合は要プログラム変更)

  • Botの起動
    まず、DiscordのBotを追加したサーバでBot専用チャットを作成します。
    スクリーンショット 2024-09-03 155805.png
    次に、venv/programに移動し、下に書いてあるdiscordbot.pyというプログラムをコピーしRaspberry Pi zero 2Wに貼り付けます。
    cd program
    
    nano discordbot.py
    
    image.png
    image.png
    貼り付け後、作成したチャンネルを右クリックし、チャンネルIDをコピーします。
    image.png
    コピー後、loop_channel_idに貼り付けます。
    image.png
    プログラムの一番下にいき、client.run()の()の中に、メモしておいたDiscordTokenを貼り付けます。貼り付け後は、CTRL+Oで保存し、CTRL+Xで元の画面に戻ります。
    image.png
    image.png
    次に、下のプログラムにあるcamera.pyをコピーし、貼り付けます。
    貼り付け後、CTRL+Oで保存し、CTRL+Xで元の画面に戻ります。
    image.png
    image.png
    ここまでの作業が完了すると以下のディレクトリ構成になると思います。
venv/
    └─discord/bin/activate
    └─program/
        discordbot.py
        camera.py
        └─video/
        └─image/

それではBOTを起動していきます。
以下の画像のように(discord)と表示されていることを確認します。なっていない場合は、以下のコマンドを実行してください。
image.png

cd && cd venv && source discord/bin/activate

次に、programフォルダに移動し、Bot(プログラム)を起動します。

cd program
python discordbot.py

起動後、以下の画像のようになれば成功です。
image.png
また、起動後すぐに動画の撮影が開始されDiscordのチャットに映像が送信されます。
image.png

  • Discordでの使い方
    Botチャンネルで、/imageと入力すると2つ引数が出てきます。
    引数を何もしてせずに、/imageだけを送信すると、1920*1080で撮影され、日付が印字されます。
    widthは撮影サイズの横の長さを指定。(16:9の比でwidthからheightを自動的に求めます。)
    dateは、画像に日付を印字するかを指定。(Trueで印字、Falseで印字しない)
    スクリーンショット 2024-09-03 16302あ6.png
    スクリーンショット 2024-09-03 163032.png

/videoの使い方。引数は4つあります。
timeは撮影時間を指定します。(初期値は10)
widthは動画の横の長さを指定。(初期値は640で、4:3の比で録画されます。)私が使用しているカメラが16:9で撮影すると、BGR2RGB変換はうまくできなかったので、4:3にしてます。画質を上げすぎると、Raspberry Pi Zero 2Wが処理しきれずプログラムが固まります。
fpsは録画のfpsを指定。(初期値は30)
dateは、画像に日付を印字するかを指定。(Trueで印字、Falseで印字しない)
image.png

/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で画像)
image.png

discordbot.py
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を入力')


camera.py
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
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?