注釈

こんにちは、SunFounder Raspberry Pi & Arduino & ESP32 Enthusiast Community on Facebookへようこそ!他の愛好家と一緒に、Raspberry Pi、Arduino、ESP32の世界により深く入り込みましょう。

参加する理由

  • 専門家サポート: 購入後の問題や技術的な課題を、コミュニティと私たちのチームの助けを借りて解決します。

  • 学習と共有: ヒントやチュートリアルを交換して、スキルを向上させましょう。

  • 限定プレビュー: 新製品の発表や先行プレビューに早期アクセスできます。

  • 特別割引: 最新製品を特別割引でお楽しみいただけます。

  • 季節限定キャンペーンとプレゼント: プレゼント企画やホリデーキャンペーンに参加しましょう。

👉 一緒に発見し、創造する準備はできましたか? [こちら] をクリックして、今すぐ参加しましょう!

4.16 パン・チルトカメラ制御システム

はじめに

このプロジェクトでは、ジョイスティックを使用してカメラをパン(水平移動)およびチルト(垂直移動)できるカメラ制御システムを作成します。サーボモーターに取り付けられたカメラの向きを遠隔操作し、リアルタイムで映像をプレビューしながら、ジョイスティックのボタンを押すことで写真を撮影することができます。このプロジェクトは、監視用途、写真撮影プロジェクト、またはサーボモーター制御やカメラ統合の学習に最適です。


必要なもの

このプロジェクトには、以下のコンポーネントが必要です。

COMPONENT INTRODUCTION

PURCHASE LINK

ブレッドボード

購入

ジャンパーワイヤー

購入

サーボ

購入

ジョイスティックモジュール

-

カメラモジュール

購入

Fusion HAT+

-

Raspberry Pi

-


回路図

../_images/2.1.9_sch.png

配線図

  1. カメラモジュールをより便利に使用するために、パン・チルトの組み立て(カメラ用) の組み立てを推奨します。

    注釈

    パン・チルト機構を組み立てると、一部のピンが隠れてしまう場合があります。そのため、カメラを使用する場合のみ組み立てるか、組み立て後に外側に配置することを推奨します。

    ../_images/gimbal_assemble.png
  2. 次の配線図に従って回路を接続してください:

    ../_images/4.16_joystick_camera_bb.png

サンプルの実行

  1. Raspberry Pi デスクトップにアクセスします:

    • リモートデスクトップ : VNC を使用してフルデスクトップ環境にアクセスします。

    • Raspberry Pi Connect : Raspberry Pi Connect を使用して、任意のブラウザから安全に Raspberry Pi にアクセスできます。

  2. ターミナルを開き、コードフォルダへ移動します:

    cd ~/ai-lab-kit/python
    
  3. スクリプトを実行してカメラを起動します:

    sudo python3 pan_tilt_camera.py
    
  4. スクリプトを実行すると、パン・チルトカメラシステムが起動し、カメラとサーボが初期化されます。

    • ディスプレイが利用可能な場合は、リアルタイムのカメラプレビューが表示されます。ディスプレイがない場合でも、プログラムはヘッドレスモードで正常に動作します。

    • ジョイスティックを左右に動かすとカメラが水平(パン)方向に回転し、上下に動かすと垂直(チルト)方向に動きます。

    • ジョイスティックのボタンを押すとカメラが写真を撮影し、 Pictures/camera_pan_tilt ディレクトリに photo_001.jpg のような連番ファイル名で保存されます。

    • プログラムは Ctrl + C を押して停止するまで、ユーザー入力に応答しながら動作を続けます。


コード

以下は、このプロジェクトで使用する Python スクリプトです:

#!/usr/bin/env python3
import os, time
from picamera2 import Picamera2, Preview
from fusion_hat.adc import ADC
from fusion_hat.pin import Pin, Mode, Pull
from fusion_hat.servo import Servo

# Servo channels for pan (horizontal) and tilt (vertical)
PAN_CHANNEL, TILT_CHANNEL = 2, 3

# Joystick ADC pins (X/Y axis) and button pin
X_PIN, Y_PIN = "A1", "A0"
BTN_PIN = 17

# Angle limits to protect servos
PAN_MIN, PAN_MAX = -90, 90
TILT_MIN, TILT_MAX = -45, 45

# Deadzone ignores small joystick movement
DEADZONE = 15
MOVE_SPEED = 3
LOOP_DELAY = 0.05

# Photo save directory (works with sudo)
REAL_USER = os.getenv("SUDO_USER") or os.getlogin()
PHOTO_DIR = os.path.join(f"/home/{REAL_USER}", "Pictures", "camera_pan_tilt")
os.makedirs(PHOTO_DIR, exist_ok=True)

# Initialize servos
pan_servo = Servo(PAN_CHANNEL)
tilt_servo = Servo(TILT_CHANNEL)

# Initialize joystick and button (active-low)
x_adc = ADC(X_PIN)
y_adc = ADC(Y_PIN)
joystick_button = Pin(BTN_PIN, mode=Mode.IN, pull=Pull.UP)  # pressed -> 0

# Initialize camera
camera = Picamera2()
camera.configure(camera.create_preview_configuration(main={"size": (1280, 720)}))

preview_started = False
photo_count = 1
current_pan = 0
current_tilt = 0
last_button_state = 1  # Used for edge detection

def clamp(v, vmin, vmax):
    # Limit value to a safe range
    return max(vmin, min(vmax, v))

def map_value(value, in_min, in_max, out_min, out_max):
    # Map ADC value to a new range
    return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min

def apply_deadzone(v, dz):
    # Ignore small joystick movement
    return 0 if (-dz < v < dz) else v

def read_joystick():
    # Read joystick X/Y position
    x = map_value(x_adc.read(), 0, 4095, -100, 100)
    y = map_value(y_adc.read(), 0, 4095, -100, 100)
    return x, y

def check_button_press():
    # Detect button press (HIGH -> LOW)
    global last_button_state
    current_state = joystick_button.value()
    if last_button_state == 1 and current_state == 0:
        last_button_state = current_state
        return True
    last_button_state = current_state
    return False

def take_photo():
    # Capture and save one photo
    global photo_count
    filename = f"photo_{photo_count:03d}.jpg"
    filepath = os.path.join(PHOTO_DIR, filename)
    camera.capture_file(filepath)
    print("Saved:", filepath)
    photo_count += 1

def start_preview_if_available():
    # Start camera preview only if a display is available
    global preview_started
    preview_started = False
    if os.getenv("DISPLAY"):
        try:
            camera.start_preview(Preview.QT)
            preview_started = True
        except Exception:
            preview_started = False

def cleanup():
    # Safely stop camera and release resources
    try:
        camera.stop()
    except Exception:
        pass
    if preview_started:
        try:
            camera.stop_preview()
        except Exception:
            pass
    try:
        camera.close()
    except Exception:
        pass

def main():
    global current_pan, current_tilt

    start_preview_if_available()
    camera.start()

    # Center camera at startup
    pan_servo.angle(0)
    tilt_servo.angle(0)

    try:
        while True:
            # Read joystick and move camera
            x, y = read_joystick()
            x = apply_deadzone(x, DEADZONE)
            y = apply_deadzone(y, DEADZONE)

            new_pan = current_pan + (MOVE_SPEED if x > DEADZONE else -MOVE_SPEED if x < -DEADZONE else 0)
            new_tilt = current_tilt + (MOVE_SPEED if y > DEADZONE else -MOVE_SPEED if y < -DEADZONE else 0)

            new_pan = clamp(new_pan, PAN_MIN, PAN_MAX)
            new_tilt = clamp(new_tilt, TILT_MIN, TILT_MAX)

            if new_pan != current_pan:
                current_pan = new_pan
                pan_servo.angle(current_pan)

            if new_tilt != current_tilt:
                current_tilt = new_tilt
                tilt_servo.angle(current_tilt)

            # Take photo when button is pressed
            if check_button_press():
                take_photo()

            time.sleep(LOOP_DELAY)

    except KeyboardInterrupt:
        pass
    finally:
        cleanup()

if __name__ == "__main__":
    main()

コードの解説

  1. ハードウェアの初期化

    • 2 つのサーボモーターを初期化し、カメラのパン(水平)およびチルト(垂直)の動きを制御します

    • ジョイスティックは ADC チャンネルを使用して X 軸と Y 軸のアナログ値を読み取り、GPIO ピンでボタン入力を検出します

    • カメラモジュールはプレビューモード用に初期化され、ディスプレイあり/なしの両方の環境に対応します

  2. ジョイスティック入力の読み取りと処理

    • read_joystick() はジョイスティックの X 軸と Y 軸のアナログ値を取得します

    • map_value() は ADC 値(0–4095)を −100〜100 の範囲に変換します

    • apply_deadzone() は小さな入力を無視し、意図しないカメラの微動を防ぎます

  3. カメラの動作制御

    • ジョイスティック入力をパン角度とチルト角度の増分に変換します

    • clamp() により角度を安全範囲内に制限し、サーボモーターを保護します

    • 角度が変化した場合のみサーボを更新することで、滑らかで安定した動作を実現します

  4. ボタン押下の検出

    • ジョイスティックのボタンはプルアップ抵抗を使用したアクティブロー入力として設定されています

    • check_button_press() はエッジ検出(HIGH → LOW)によってボタン押下を検出します

    • これにより、ボタンを押し続けても 1 回の押下につき 1 枚の写真だけが撮影されます

  5. 写真の撮影と保存

    • take_photo() はカメラモジュールを使用して画像を撮影します

    • 写真は photo_001.jpg のような連番ファイル名で保存されます

    • すべての画像はユーザーの Pictures/camera_pan_tilt ディレクトリに保存されます

  6. カメラプレビューの処理

    • グラフィカルディスプレイが利用可能な場合のみ、ライブカメラプレビューが開始されます

    • ディスプレイがない環境でもスクリプトは正常に動作します

  7. メインループと終了処理

    • メインループではジョイスティック入力を継続的に読み取り、リアルタイムでカメラを制御します

    • Ctrl + C でプログラムを終了すると、カメラが安全に停止します

    • すべてのハードウェアリソースが適切に解放され、正常に終了します


トラブルシューティング

  1. サーボが動作しない

    • 原因:サーボ接続の誤り、または電源の問題

    • 解決方法

      • サーボが正しいチャンネル(2 と 3)に接続されていることを確認する

      • Fusion HAT が正しく給電されていることを確認する

      • サーボ配線に緩みがないか確認する

  2. カメラプレビューが表示されない

    • 原因:カメラモジュールが検出されていない、または設定が正しくない

    • 解決方法

      • カメラケーブルが CSI ポートにしっかり接続されていることを確認する

      • Raspberry Pi の設定でカメラが有効になっているか確認する

      • カメラモジュールの互換性を確認する

  3. ジョイスティックが反応しない

    • 原因:ピン設定の誤り、または ADC の問題

    • 解決方法

      • ジョイスティックが A0、A1、および GPIO 17 に正しく接続されているか確認する

      • 簡単な print 文で ADC の値を確認する

      • Fusion HAT の ADC が正常に動作しているか確認する

  4. 写真が保存されない

    • 原因:権限の問題、またはディレクトリの問題

    • 解決方法

      • ユーザーのホームディレクトリに Pictures フォルダが存在するか確認する

      • 写真保存フォルダの書き込み権限を確認する

      • 権限問題が続く場合は sudo で実行してみる

  5. サーボが不安定に動く

    • 原因:電源の不安定、またはソフトウェアのタイミング問題

    • 解決方法

      • Fusion HAT への電源供給が安定しているか確認する

      • MOVE_SPEED や遅延値を調整する

      • 必要に応じてサーボ電源ラインにコンデンサを追加する


発展アイデア

  1. 動画録画機能:録画の開始/停止を制御できる動画録画機能を追加します:

    def start_recording():
        timestamp = time.strftime("%Y%m%d_%H%M%S")
        video_path = os.path.join(VIDEO_DIR, f"video_{timestamp}.mp4")
        camera.start_recording(video_path)
        print(f"Recording started: {video_path}")
    
    def stop_recording():
        camera.stop_recording()
        print("Recording stopped")
    
  2. プリセット位置:カメラを素早く移動できるプリセット位置を設定します:

    PRESETS = {
        'center': (0, 0),
        'left': (-45, 0),
        'right': (45, 0),
        'up': (0, 30),
        'down': (0, -30)
    }
    
    def goto_preset(preset_name):
        if preset_name in PRESETS:
            pan, tilt = PRESETS[preset_name]
            pan_servo.angle(pan)
            tilt_servo.angle(tilt)
    

まとめ

このプロジェクトでは、Raspberry Pi、サーボモーター、カメラモジュールを組み合わせて高度なパン・チルトカメラ制御システムを構築する方法を紹介しました。ハードウェア制御、リアルタイム映像処理、ユーザー入力処理を統合した実用的なシステムです。このプロジェクトは、監視システム、撮影ロボット、インタラクティブアート装置など、より高度な応用へ発展させるための基礎となります。