はじめに
Raspberry Pi 3 ModelBが余っていました。サーバにするにもスペックが低いし、何か転用できるものがないか…と考えたときに、ふとLegacyCameraがあったので、監視カメラを作ろうかと思いました。
やりたいこと
ここで実際にやりたいことを整理します。
- 最新の映像をブラウザ経由で見られるようにしたい。
- どうせなら不審者が来たら、検出してDiscordにでも通知してほしい。
環境
pi@pi:/scripts/live_streaming/mediamtx $ cat /proc/device-tree/model
Raspberry Pi 3 Model B Rev 1.2pi
pi@pi:/scripts/live_streaming/mediamtx $ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 13 (trixie)
Release: 13
Codename: trixie
※TrixieのFull版だと、メモリが枯渇するのかすぐフリーズします。そのため、CUI(Lite版)をインストールしています。
Legacy Camera:https://www.amazon.co.jp/dp/B086MK17K5?ref=ppx_yo2ov_dt_b_fed_asin_title
(OV5647センサの安いやつ)
目指すシステム構成
青がサーバ、デバイス。緑はフォーマット。
雑に描いたので、統一感なくても許してください。

前提
- 使用ポート:80・8554・2222。sudo ufw allow等でポート開放を行う。
- ブラウザでの閲覧はVPN経由で行う。
- pip install や apt installで必要なものは省きます。
rpicamでの映像取得 ~ RTSPサーバへの転送
カメラから取り込んだ映像を配信します。
システム構成で言うと、CAMERA→tcp/h264の線が以下コマンドに当たります。
rpicam-vid -t 0 --inline --listen -o tcp://127.0.0.1:2222
※この辺りは https://karakuri-musha.com/inside-technology/02-raspi-camera-common-capture01/ を参考にしました。
rpicam-vidでのストリーミングは、1対1となってしまいます。
そのため、ストリーミング映像をリアルタイムで見られるようになることと、動体検知ができるようになるという2点を満たすことが出来ません。
なので、tcp/h264をRTSPサーバ経由でマルチストリーミングします。
RTSPサーバにはMediaMTX、RTSPサーバにRaspberry Piの映像を転送する役割をffmpegで行ってもらいます。
①MediaMTXのダウンロード・サーバ起動
wget https://github.com/bluenviron/mediamtx/releases/download/v1.15.4/mediamtx_v1.15.4_linux_armv7.tar.gz
tar xzvf mediamtx_v1.15.4_linux_armv7.tar.gz
chmod 777 mediamtx
./mediamtx
②rpicam-vidで取得したデータをRTSPサーバに配信(tcp/h264→MediaMTXの線)
ffmpeg -i tcp://0.0.0.0:2222 -c copy -f rtsp rtsp://0.0.0.0:8554/stream
VLC Media Playerで rtsp://<ラズパイのアドレス>:8554/stream でネットワークストリーミングを開くと、リアルタイムで映像が更新されます。
http形式でのリアルタイム配信
nginxでサーバを建てます。デフォルトは/var/www/htmlですが、必要に応じてルートディレクトリを変えてください。
※参考: https://www.khstasaba.com/?p=953
私は /scripts/live_streaming/html配下にindex.htmlを置いています。
index.htmlの中身は以下の通り。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Raspberry Pi カメラ HLS</title>
</head>
<body>
<h1>カメラ映像 HLS</h1>
<video id="video" controls autoplay muted width="640" height="480"></video>
<!-- Hls.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
const video = document.getElementById('video');
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource('stream.m3u8'); // HLS のパス
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'hls/stream.m3u8';
video.addEventListener('loadedmetadata', function() {
video.play();
});
}
</script>
</body>
</html>
これでhtmlの準備は完了です。
html形式で配信を行うためには、HLS形式に変える必要があります。
RTSPサーバから配信された映像データをHLS形式に変えて /scripts/live_streaming/html/ 配下に配置するコマンドが以下です。
ffmpeg -i rtsp://127.0.0.1:8554/stream -c copy -f hls -hls_time 2 -hls_list_size 5 -hls_flags delete_segments /scripts/live_streaming/html/stream.m3u8
nginxを有効にすると、ちょっとラグはあるものの、カメラ映像が映るようになりました。

動体検知
動体検知して、Discordに通知するソースコードです。
動体検知を行ったら、検出した箇所を□で囲った画像を保存。その後、Discordに投稿するといったコードです。
検知しすぎても通知がうっとうしいので、検出後30分は動体検知しないようにしています。
WebhookURLの取得方法はこちらを参考ください。
https://zenn.dev/discorders/articles/discord-webhook-guide
import cv2
import time
import requests
# config
WEBHOOK_URL = '★WebhookURLを入力★'
WORKING_DIRECTORY = "/scripts/live_streaming/" ★pythonを実行するディレクトリを入力
def send_to_discord(filepath):
with open(filepath, "rb") as f:
files = {"file": f}
data = {"content": "人を検知しました!"}
requests.post(WEBHOOK_URL, data=data, files=files)
# RTSPストリームを開く
cap = cv2.VideoCapture("rtsp://192.168.50.50:8554/stream")
if not cap.isOpened():
print("RTSPストリームを開けませんでした。")
exit()
prev_gray = None
while True:
ret, frame = cap.read()
if not ret:
print("フレーム取得失敗")
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 初回のみ prev_gray をセットして継続
if prev_gray is None:
prev_gray = gray
continue
# 差分
diff = cv2.absdiff(prev_gray, gray)
_, thresh = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
# ノイズ除去:膨張して輪郭を強調
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
dilated = cv2.dilate(thresh, kernel, iterations=2)
# 輪郭を取得
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
detected = False
biggest_box = None
# 動きのある領域を探索
for cnt in contours:
area = cv2.contourArea(cnt)
if area < 5000: # ノイズ除去:小さい動きは無視
continue
# 大きい動体と判定
x, y, w, h = cv2.boundingRect(cnt)
biggest_box = (x, y, w, h)
detected = True
if detected and biggest_box:
x, y, w, h = biggest_box
# 枠を描画(赤色)
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
# 保存
filename = f"detected_{int(time.time())}.jpg"
cv2.imwrite(filename, frame)
print(f"動体検知 → 保存しました: {filename}")
work_directory = WORKING_DIRECTORY + filename
send_to_discord(work_directory)
# 必要であれば break も可能
# break
prev_gray = gray
if detected:
time.sleep(30 * 60)
cap.release()
cap = cv2.VideoCapture("rtsp://192.168.50.50:8554/stream")
if not cap.isOpened():
print("RTSPストリームを開けませんでした。")
exit()
cap.release()
・・・まぁ動作不安定です。ここは要改善。とりあえず30分に一回は監視カメラの映像が送られてくるので、
- なぜか動いていないはずの箇所も検知する
- 30分に一回必ず動体検知する
終わりに
とりあえず監視カメラっぽい何かは出来たので、良しとしましょう。
進展合ったら、また記事書きます。
映像配信は大変ということがわかりました。htmlで配信できるフォーマットを気にしないといけないなど、フォーマット関連の縛りがあるなぁと思いました
