.. include:: /index.rst
:start-after: start_hello_message
:end-before: end_hello_message
.. _py_pan_tilt_camera:
4.16 パン・チルトカメラ制御システム
==========================================================
**はじめに**
このプロジェクトでは、ジョイスティックを使用してカメラをパン(水平移動)およびチルト(垂直移動)できるカメラ制御システムを作成します。サーボモーターに取り付けられたカメラの向きを遠隔操作し、リアルタイムで映像をプレビューしながら、ジョイスティックのボタンを押すことで写真を撮影することができます。このプロジェクトは、監視用途、写真撮影プロジェクト、またはサーボモーター制御やカメラ統合の学習に最適です。
----------------------------------------------
**必要なもの**
このプロジェクトには、以下のコンポーネントが必要です。
.. list-table::
:widths: 30 20
:header-rows: 1
* - COMPONENT INTRODUCTION
- PURCHASE LINK
* - :ref:`cpn_breadboard`
- |link_breadboard_buy|
* - :ref:`cpn_wires`
- |link_wires_buy|
* - :ref:`cpn_servo`
- |link_servo_buy|
* - :ref:`cpn_joystick`
- \-
* - :ref:`cpn_camera_module`
- |link_camera_buy|
* - :ref:`cpn_fusion_hat`
- \-
* - Raspberry Pi
- \-
--------------------
**回路図**
.. image:: img/fzz/2.1.9_sch.png
:width: 80%
:align: center
---------------
**配線図**
#. カメラモジュールをより便利に使用するために、:ref:`assemble_fusion_hat_pan_tilt` の組み立てを推奨します。
.. note::
パン・チルト機構を組み立てると、一部のピンが隠れてしまう場合があります。そのため、カメラを使用する場合のみ組み立てるか、組み立て後に外側に配置することを推奨します。
.. image:: ../quick_start/img/gimbal_assemble.png
#. 次の配線図に従って回路を接続してください:
.. image:: img/fzz/4.16_joystick_camera_bb.png
:width: 100%
:align: center
----------------------------------------------
**サンプルの実行**
1. Raspberry Pi デスクトップにアクセスします:
* :ref:`remote_desktop` : **VNC** を使用してフルデスクトップ環境にアクセスします。
* |link_rpi_connect| : **Raspberry Pi Connect** を使用して、任意のブラウザから安全に Raspberry Pi にアクセスできます。
2. ターミナルを開き、コードフォルダへ移動します:
.. raw:: html
.. code-block:: shell
cd ~/ai-lab-kit/python
3. スクリプトを実行してカメラを起動します:
.. raw:: html
.. code-block:: shell
sudo python3 pan_tilt_camera.py
4. スクリプトを実行すると、パン・チルトカメラシステムが起動し、カメラとサーボが初期化されます。
* ディスプレイが利用可能な場合は、リアルタイムのカメラプレビューが表示されます。ディスプレイがない場合でも、プログラムはヘッドレスモードで正常に動作します。
* ジョイスティックを左右に動かすとカメラが水平(パン)方向に回転し、上下に動かすと垂直(チルト)方向に動きます。
* ジョイスティックのボタンを押すとカメラが写真を撮影し、 ``Pictures/camera_pan_tilt`` ディレクトリに ``photo_001.jpg`` のような連番ファイル名で保存されます。
* プログラムは Ctrl + C を押して停止するまで、ユーザー入力に応答しながら動作を続けます。
----------------------------------------------
**コード**
以下は、このプロジェクトで使用する Python スクリプトです:
.. raw:: html
.. code-block:: 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()
----------------------------------------------
**コードの解説**
#. **ハードウェアの初期化**
- 2 つのサーボモーターを初期化し、カメラのパン(水平)およびチルト(垂直)の動きを制御します
- ジョイスティックは ADC チャンネルを使用して X 軸と Y 軸のアナログ値を読み取り、GPIO ピンでボタン入力を検出します
- カメラモジュールはプレビューモード用に初期化され、ディスプレイあり/なしの両方の環境に対応します
#. **ジョイスティック入力の読み取りと処理**
- ``read_joystick()`` はジョイスティックの X 軸と Y 軸のアナログ値を取得します
- ``map_value()`` は ADC 値(0–4095)を −100〜100 の範囲に変換します
- ``apply_deadzone()`` は小さな入力を無視し、意図しないカメラの微動を防ぎます
#. **カメラの動作制御**
- ジョイスティック入力をパン角度とチルト角度の増分に変換します
- ``clamp()`` により角度を安全範囲内に制限し、サーボモーターを保護します
- 角度が変化した場合のみサーボを更新することで、滑らかで安定した動作を実現します
#. **ボタン押下の検出**
- ジョイスティックのボタンはプルアップ抵抗を使用したアクティブロー入力として設定されています
- ``check_button_press()`` はエッジ検出(HIGH → LOW)によってボタン押下を検出します
- これにより、ボタンを押し続けても 1 回の押下につき 1 枚の写真だけが撮影されます
#. **写真の撮影と保存**
- ``take_photo()`` はカメラモジュールを使用して画像を撮影します
- 写真は ``photo_001.jpg`` のような連番ファイル名で保存されます
- すべての画像はユーザーの ``Pictures/camera_pan_tilt`` ディレクトリに保存されます
#. **カメラプレビューの処理**
- グラフィカルディスプレイが利用可能な場合のみ、ライブカメラプレビューが開始されます
- ディスプレイがない環境でもスクリプトは正常に動作します
#. **メインループと終了処理**
- メインループではジョイスティック入力を継続的に読み取り、リアルタイムでカメラを制御します
- ``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. **動画録画機能**:録画の開始/停止を制御できる動画録画機能を追加します:
.. code-block:: python
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. **プリセット位置**:カメラを素早く移動できるプリセット位置を設定します:
.. code-block:: python
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、サーボモーター、カメラモジュールを組み合わせて高度なパン・チルトカメラ制御システムを構築する方法を紹介しました。ハードウェア制御、リアルタイム映像処理、ユーザー入力処理を統合した実用的なシステムです。このプロジェクトは、監視システム、撮影ロボット、インタラクティブアート装置など、より高度な応用へ発展させるための基礎となります。