PythonとStreamlitでRaspiのタイムラプスカメラを作った話

理科の先生からタイムラプスで植物の成長を定点観測したいと要望があり作成しました。
iPadでタイムラプスしようとしてたらしいですが、定点観測なら断然Raspiの出番でしょう。
RaspberryPi4B（2GB版）
Bookworm64bit版

サクッと完成させようと思っていたのに意外にも躓いてしまったので忘備録として残しておきます。
躓き原因はOpenCVがライブラリが足りないとかで上手くインストールが出来ず、強制的に入れても今度はStreamlitでエラーが出てしまうと言う無限ループに陥りました。
何とか解消したいと藻掻きましたがPythonのバージョンとの兼ね合いで無理な事が分かり結局はvenvで対策をしました。

```
sudo apt update
sudo apt upgrade -y
sudo apt install -y python3-pip python3-venv libatlas-base-dev libhdf5-dev libhdf5-serial-dev libhdf5-103 libjasper-dev libqtgui4 libqt4-test
```

```bash
# プロジェクトディレクトリに移動
cd my_project
# 仮想環境の作成
python3 -m venv venv

# 仮想環境の有効化（LinuxおよびmacOSの場合）
source venv/bin/activate
```

仮想環境をアクティブにして、以下をインストール
```bash
pip install numpy==1.23.5 opencv-python-headless==4.5.3.56 streamlit
```

Streamlitで定点カメラを駆動させるスクリプト
```timelapse.py
import streamlit as st
import cv2
import time
import os
from datetime import datetime

# ディレクトリの作成
output_dir = "timelapse_images"
if not os.path.exists(output_dir):
    os.makedirs(output_dir)

# 解像度の選択肢
resolutions = {
    "320x240": (320, 240),
    "640x480": (640, 480),
    "800x600": (800, 600),
    "1024x768": (1024, 768),
    "1280x720": (1280, 720),
    "1920x1080": (1920, 1080)
}

# Streamlitアプリの設定
st.title("理科実験タイムラプスカメラ")
st.write("Webカメラの画像を一定間隔で撮影し、タイムラプス動画を生成します。")

# キャプチャ間隔の設定（秒）
capture_interval = st.number_input("キャプチャ間隔（秒）", min_value=1, value=60, step=1)

# 解像度の選択
resolution_label = st.selectbox("解像度を選択してください", list(resolutions.keys()))
selected_resolution = resolutions[resolution_label]

# 画像表示のプレースホルダーを作成
image_placeholder = st.empty()

# プレビュー表示のプレースホルダーを作成
preview_placeholder = st.empty()

# 画像キャプチャと保存
def capture_image():
    cap = st.session_state.cap
    if cap is not None:
        ret, frame = cap.read()
        if ret:
            # 画像の解像度を変更
            frame = cv2.resize(frame, selected_resolution)
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = os.path.join(output_dir, f"{timestamp}.jpg")
            cv2.imwrite(filename, frame)
            # プレースホルダーに画像を表示
            image_placeholder.image(frame, caption=f"Captured: {timestamp}")

# プレビューの表示
def preview():
    cap = st.session_state.cap
    if cap is not None:
        ret, frame = cap.read()
        if ret:
            # 画像の解像度を変更
            frame = cv2.resize(frame, selected_resolution)
            # プレースホルダーにプレビュー画像を表示
            preview_placeholder.image(frame, caption="Preview")

# セッション状態の初期化
if "capturing" not in st.session_state:
    st.session_state.capturing = False
if "previewing" not in st.session_state:
    st.session_state.previewing = False
if "cap" not in st.session_state:
    st.session_state.cap = None

# キャプチャの制御
def start_capturing():
    # プレビューが動作している場合は終了する
    if st.session_state.previewing:
        stop_preview()
    st.session_state.capturing = True
    st.session_state.cap = cv2.VideoCapture(0)
    if not st.session_state.cap.isOpened():
        st.error("Webカメラを開けませんでした。")
        st.session_state.capturing = False

def stop_capturing():
    st.session_state.capturing = False
    if st.session_state.cap is not None:
        st.session_state.cap.release()
        st.session_state.cap = None

def start_preview():
    st.session_state.previewing = True
    st.session_state.cap = cv2.VideoCapture(0)
    if not st.session_state.cap.isOpened():
        st.error("Webカメラを開けませんでした。")
        st.session_state.previewing = False

def stop_preview():
    st.session_state.previewing = False
    if st.session_state.cap is not None:
        st.session_state.cap.release()
        st.session_state.cap = None

# プレビューボタンとキャプチャボタンの配置
col1, col2, col3, col4 = st.columns(4)
with col1:
    if st.button("プレビュー開始"):
        start_preview()
with col2:
    if st.button("プレビュー終了"):
        stop_preview()
with col3:
    if st.button("キャプチャ開始"):
        start_capturing()
with col4:
    if st.button("キャプチャ終了"):
        stop_capturing()

# プレビューの表示
while st.session_state.previewing:
    preview()
    time.sleep(1)

# タイムラプス画像のキャプチャ
while st.session_state.capturing:
    capture_image()
    time.sleep(capture_interval)
    if not st.session_state.capturing:
        break

# Streamlitアプリの停止時にWebカメラを解放
if "cap" in st.session_state and st.session_state.cap is not None:
    st.session_state.cap.release()
    st.session_state.cap = None
```
コードはの簡単な説明ですが、StreamlitでRaspiのWebカメラからのプレビューとキャプチャを制御します。解像度とキャプチャ間隔を設定できるようにしました。プレビューとキャプチャの開始・停止がボタンで行えるようにしています。

実行は
```bash
streamlit run /timelapse.py
```

ターミナルに表示されているipとポート番号を叩いてやるとブラウザから使用出来るようになります。

![time.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2841127/2af6f751-32d8-988e-edc7-4ff915a0f86e.png)




正直自分が使う場合はStreamlitを使わずコマンドだけでやるのですが、今回はRaspiを触った事のない理科の先生が使うので自分のPCやiPadから実行出来る方が使いやすいと判断しました。
Streamlitメチャクチャ便利ですね。

応用出来る事多々ありそうなのでもっと勉強していこうと思います。