RaspberryPiで音声つき監視カメラの構築 動体検知

  • Post Author:

概要

以前から玄関に設置して訪問者とのトラブル防止や抑止のため、屋内で監視カメラを使いたいと考えていました。動画の録画をしないのであればAmazonのRingなど安価で便利な選択肢がありますが、録画をしたい場合大抵サブスクリプションの契約が必要になります。そこでサブスクリプション無しで監視カメラシステムを構築するため家に余っていたRaspberryPiとUSBカメラを利用できないかと考えました。

RaspberryPiで監視カメラシステムを構築する記事は多くあり大抵の場合それらで事足りると思いますし、今回記事にする手法より簡単に構築出来る選択肢も多くあると思います。しかし、音声込みの動画を撮る記事は見当たりませんでした。

今回はRaspberryPiで動体検知した時に音声つき動画を録画するシステムの構築をご紹介いたします。

要約

  • サブスクリプションなしの音声つき監視カメラシステム構築
  • RaspberryPiとUSBカメラ
  • 動体検知した時音声つき動画の録画

主な機能

  • 動体検知: OpenCVを使ってリアルタイムに動きを検出し、動きがあったときだけ録画を開始
  • タイムスタンプ表示 FFmpegの`drawtext`フィルターで録画映像に日時を重ねて表示
  • 音声同時録音: 映像だけでなくマイクからの音声も同時に記録
  • ストレージ管理: 保存ファイル数が上限に達すると古いファイルから自動削除
  • カメラ安定化: 起動直後の自動露出調整による誤検知を防止するウォームアップ機能

必要な物

  • Raspberry Pi(今回は3B)
  • USBカメラ(UVC対応)
  • USBマイク(またはマイク付きカメラ)
  • microSDカード(16GB以上推奨)
  • 有線のネット回線

目次

  • RaspberryPiの設定
    • OSの書き込み
    • IPの固定
    • SSH接続
  • 必要なパッケージのインストール
  • 監視カメラシステムの構築
    • USBカメラの確認
    • 音声情報の確認
    • ffmepgのテスト実行
    • プログラムの修正
    • プログラムの実行
      • 負荷の確認
  • オプション項目
    • 外部ストレージ(SSD)への書込み
    • RaspberryPi実行時にプログラム起動
    • 動画のダウンロード
    • 動体検知なし版

RaspberryPiの設定

OSの書き込み

RaspberryPi公式のRaspberryPi Imagerを使用してOSをMicroSDカードに書き込みます。

https://www.raspberrypi.com/software

今回はリソース節約のためGUIを使用しないでCUIバージョンでインストールします。

OS > RaspberryPi OS (other) > RaspberryPi OS Lite(64-bit)

IPの固定

SSHでの接続などを考えローカルのIPを固定化します。

nmcli c で有線回線のUUIDを確認します。

「確認したUUID」の部分を適宜変更して実行してください。

sudo nmcli c
sudo nmcli connection modify 確認したUUID ipv4.addresses 192.168.0.252/24 ipv4.gateway 192.168.0.254 ipv4.dns "192.168.0.254" ipv4.method manual

SSH接続

IP固定が完了したら別のパソコンからSSH接続で接続していきます。

WindowsではTeraTermを使うと便利です。Mac OSではターミナルからSSHコマンドを使うことができます。

ssh username@192.168.0.252

必要なパッケージのインストール

動体検知を担当するOpenCVと録画部分を担当するffmpegをインストールします。

音声つき動画にするにはffmpegを利用するのが良いようです。

sudo apt update
sudo apt install python3-opencv ffmpeg
pip3 install opencv-python

監視カメラシステムの構築

USBカメラの確認

カメラが認識されているか確認します。

ls -l /dev/video*

USBマイクの確認

オーディオデバイスの確認。

arecord -l

実行結果例

card 2: C615 [HD Webcam C615], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

card 2 device 0 となっていますが、この「2」「0」が必要になります。

ffmepgのテスト実行

下記のコードの「/dev/video0」と「hw:2,0」の部分をそれぞれビデオデバイス、オーディオデバイスをお使いの値に書き換えて実行してください。

ffmpeg -y -f v4l2 -framerate 10 -video_size 640x480 -i /dev/video0 -f alsa -channels 1 -i hw:2,0 -t 5 -c:v libx264 -preset ultrafast -crf 28 -c:a aac test_output.mp4

エラー無く実行できればプログラム上でも動くと思われます。

プログラムの修正

プログラムは以下にもあります。

https://github.com/TKano-Null/PiRecorder

import os
import subprocess
import time
import cv2
from datetime import datetime

# 設定
SAVE_DIR = "/home/username/monitor_videos"  # 保存先ディレクトリ
MAX_FILES = 5                        # 最大保持ファイル数(1日分)
DURATION = 10                        # 動体検知後の録画秒数
VIDEO_DEVICE = 0                      # ビデオデバイス(OpenCV用インデックス)
VIDEO_DEVICE_PATH = '/dev/video0'     # ビデオデバイス(ffmpeg用パス)
AUDIO_DEVICE = 'hw:2,0'              # オーディオデバイス
MOTION_THRESHOLD = 5000               # 動体検知の閾値(小さいほど敏感)
MIN_CONTOUR_AREA = 500                # 検知する動体の最小面積
WARMUP_FRAMES = 30                    # カメラ安定化のための読み捨てフレーム数

if not os.path.exists(SAVE_DIR):
    os.makedirs(SAVE_DIR)

def get_video_files():
    files = [os.path.join(SAVE_DIR, f) for f in os.listdir(SAVE_DIR) if f.endswith('.mp4')]
    files.sort(key=os.path.getmtime)
    return files

def manage_storage():
    files = get_video_files()
    while len(files) >= MAX_FILES:
        oldest_file = files.pop(0)
        os.remove(oldest_file)
        print(f"Deleted: {oldest_file}")

def record_video(filename):
    manage_storage()
    cmd = [
        'ffmpeg',
        '-y',
        '-f', 'v4l2',
        '-framerate', '10',
        '-video_size', '640x480',
        '-i', VIDEO_DEVICE_PATH,
        '-f', 'alsa',
        '-channels', '1',
        '-i', AUDIO_DEVICE,
        '-t', str(DURATION),
        '-vf', "drawtext=text='%{localtime}':fontsize=20:fontcolor=white:borderw=2:bordercolor=black:x=10:y=10",
        '-c:v', 'libx264',
        '-preset', 'ultrafast',
        '-crf', '32',
        '-c:a', 'aac',
        filename
    ]

    print(f"Recording: {filename}")
    result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    if result.returncode == 0:
        print(f"Completed: {filename}")
    else:
        print(f"Error: Recording failed (return code: {result.returncode})")
        print("FFmpeg command failed. Please check your camera and microphone settings.")
        exit(1)

def detect_motion():
    cap = cv2.VideoCapture(VIDEO_DEVICE)
    if not cap.isOpened():
        print("Error: Cannot open camera.")
        exit(1)

    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

    print("Waiting for camera to stabilize...")
    for _ in range(WARMUP_FRAMES):
        cap.read()

    print("Monitoring for motion...")
    prev_frame = None

    while True:
        ret, frame = cap.read()
        if not ret:
            print("Error: Cannot read frame.")
            exit(1)

        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        gray = cv2.GaussianBlur(gray, (21, 21), 0)

        if prev_frame is None:
            prev_frame = gray
            continue

        delta = cv2.absdiff(prev_frame, gray)
        thresh = cv2.threshold(delta, 25, 255, cv2.THRESH_BINARY)[1]
        thresh = cv2.dilate(thresh, None, iterations=2)

        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        motion_detected = any(cv2.contourArea(c) > MIN_CONTOUR_AREA for c in contours)

        if motion_detected:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = os.path.join(SAVE_DIR, f"video_{timestamp}.mp4")
            print(f"Motion detected! ({timestamp})")

            cap.release()
            record_video(filename)

            cap = cv2.VideoCapture(VIDEO_DEVICE)
            if not cap.isOpened():
                print("Error: Cannot reopen camera.")
                exit(1)
            cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
            cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
            print("Waiting for camera to stabilize...")
            for _ in range(WARMUP_FRAMES):
                cap.read()
            prev_frame = None
            print("Monitoring for motion...")
        else:
            prev_frame = gray

        time.sleep(0.2)

    cap.release()

if __name__ == "__main__":
    print("Security System Started (Motion Detection Mode)...")
    try:
        detect_motion()
    except KeyboardInterrupt:
        print("Stopped.")

プログラムの冒頭にある設定値を環境に合わせて変更してください。

SAVE_DIR = "/home/username/monitor_videos" # 保存先ディレクトリ

MAX_FILES = 96 # 最大保持ファイル数

DURATION = 900 # 動体検知後の録画秒数

VIDEO_DEVICE = 0 # ビデオデバイス(OpenCV用インデックス)

VIDEO_DEVICE_PATH = '/dev/video0' # ビデオデバイス(ffmpeg用パス)

AUDIO_DEVICE = 'hw:2,0' # オーディオデバイス

MOTION_THRESHOLD = 5000 # 動体検知の閾値(小さいほど敏感)

MIN_CONTOUR_AREA = 500 # 検知する動体の最小面積

WARMUP_FRAMES = 30 # カメラ安定化のための読み捨てフレーム数

設定項目 説明

`SAVE_DIR` 録画ファイルの保存先ディレクトリ

`MAX_FILES` 保存する最大ファイル数。超えると古いものから削除

`DURATION` 1回の録画時間(秒)

`VIDEO_DEVICE` OpenCVで使用するカメラのデバイスインデックス

`VIDEO_DEVICE_PATH` FFmpegで使用するカメラのデバイスパス

`AUDIO_DEVICE` ALSAオーディオデバイス名

`MIN_CONTOUR_AREA` 動体として検知する最小面積。小さくすると敏感になる

`WARMUP_FRAMES` カメラ起動後に読み捨てるフレーム数。自動露出の安定化用

プログラムの実行

python3 main.py

起動すると以下のように表示されます。

Security System Started (Motion Detection Mode)...
Waiting for camera to stabilize...
Monitoring for motion...

動きを検知すると自動的に録画が始まります。

Motion detected! (20250115_143000)
Recording: /home/username/monitor_videos/video_20250115_143000.mp4
Completed: /home/username/monitor_videos/video_20250115_143000.mp4
Monitoring for motion...

仕組みの解説

カメラ安定化の工夫

USBカメラは起動直後に自動露出や絞りの調整を行うため、画面全体の明るさが急激に変化します。これを動体として誤検知してしまう問題を、起動後に一定フレーム数を読み捨てる「ウォームアップ」処理で解決しています。

ストレージ管理

SDカードの容量を圧迫しないよう、保存ファイル数が `MAX_FILES` に達すると古いファイルから自動的に削除されます。録画時間とファイル数を調整することで、必要な期間分の映像を保持できます。

オプション項目

外部ストレージ(SSD)への書込み

1. SSDの認識確認

lsblk

SSDが 「/dev/sda1」などとして表示されるはずです。

2. マウントポイントの作成

sudo mkdir -p /mnt/ssd

3. SSDをマウント

sudo mount /dev/sda1 /mnt/ssd

※パーティション名「sda1」は「lsblk」の出力に合わせてください。

4. 書き込み権限の設定

sudo chown $USER:$USER /mnt/ssd

5. 起動時に自動マウント(永続化)

SSDのUUIDを確認します。

sudo blkid /dev/sda1

出力例: 「UUID=”xxxx-xxxx”」と「TYPE=”ext4″」をfstabに追記します。

sudo nano /etc/fstab

以下の行を追加(UUID・ファイルシステムは実際の値に置き換え)

UUID=xxxx-xxxx /mnt/ssd ext4 defaults,nofail 0 2

 「nofail」を付けることで、SSDが接続されていない場合でも起動が止まりません。

6. プログラムの保存先を変更

プログラムの「SAVE_DIR」 を変更すれば完了です。

RaspberryPi実行時にプログラム起動

Raspberry Piの起動時に自動で監視を開始したい場合は、systemdサービスとして登録します。

sudo nano /etc/systemd/system/security-camera.service

以下の内容を記述します。

[Unit]

Description=Security Camera Motion Detection

After=multi-user.target

[Service]

ExecStart=/usr/bin/python3 /home/username/main.py

WorkingDirectory=/home/username

Restart=always

User=username

[Install]

WantedBy=multi-user.target

サービスを有効化して起動します。

sudo systemctl enable security-camera

sudo systemctl start security-camera

動画のダウンロード

現状ではscpコマンドを使用してRaspberryPiからホスト端末へダウンロードします。

scp -r username@192.168.0.252:/home/username/monitor_videos/ ~/Desktop/testvideo/

動体検知なし版

DURATION 秒ごとに連続録画を繰り返すシンプルなバージョンです。OpenCVへの依存もなくなっています。

import os
import subprocess
from datetime import datetime

# 設定
SAVE_DIR = "/home/username/monitor_videos"  # 保存先ディレクトリ
MAX_FILES = 5                        # 最大保持ファイル数
DURATION = 10                        # 録画秒数
VIDEO_DEVICE_PATH = '/dev/video0'     # ビデオデバイス
AUDIO_DEVICE = 'hw:2,0'              # オーディオデバイス

if not os.path.exists(SAVE_DIR):
    os.makedirs(SAVE_DIR)

def get_video_files():
    files = [os.path.join(SAVE_DIR, f) for f in os.listdir(SAVE_DIR) if f.endswith('.mp4')]
    files.sort(key=os.path.getmtime)
    return files

def manage_storage():
    files = get_video_files()
    while len(files) >= MAX_FILES:
        oldest_file = files.pop(0)
        os.remove(oldest_file)
        print(f"Deleted: {oldest_file}")

def record_video(filename):
    manage_storage()
    cmd = [
        'ffmpeg',
        '-y',
        '-f', 'v4l2',
        '-framerate', '10',
        '-video_size', '640x480',
        '-i', VIDEO_DEVICE_PATH,
        '-f', 'alsa',
        '-channels', '1',
        '-i', AUDIO_DEVICE,
        '-t', str(DURATION),
        '-vf', "drawtext=text='%{localtime}':fontsize=20:fontcolor=white:borderw=2:bordercolor=black:x=10:y=10",
        '-c:v', 'libx264',
        '-preset', 'ultrafast',
        '-crf', '32',
        '-c:a', 'aac',
        filename
    ]

    print(f"Recording: {filename}")
    result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    if result.returncode == 0:
        print(f"Completed: {filename}")
    else:
        print(f"Error: Recording failed (return code: {result.returncode})")
        print("FFmpeg command failed. Please check your camera and microphone settings.")
        exit(1)

if __name__ == "__main__":
    print("Security System Started (Continuous Recording Mode)...")
    try:
        while True:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = os.path.join(SAVE_DIR, f"video_{timestamp}.mp4")
            record_video(filename)
    except KeyboardInterrupt:
        print("Stopped.")

まとめ

RaspberryPiを使用してある程度実用可能な監視カメラシステムを構築することができました。

RaspberryPiやUSBカメラは安価なため、トータルでも1万円以下で揃えられるのではないでしょうか。

OSを格納しているSDカードは大量の書き込みをしてしまうとSDカードを消耗させてしまうため外部ストレージを用意することをお勧めします。

we are hiring

優秀な技術者と一緒に、好きな場所で働きませんか

株式会社もばらぶでは、優秀で意欲に溢れる方を常に求めています。働く場所は自由、働く時間も柔軟に選択可能です。

現在、以下の職種を募集中です。ご興味のある方は、リンク先をご参照下さい。

コメントを残す