Streamlitで作ったタイムラプスカメラに保存機能を追加した話

https://qiita.com/yu_uk/items/ab010cb6984747f56e63

この話の続編になります！

前回無事完成したのですが、実際使ってもらった時にこんな事言われました。
「これ撮影したデータはどうやって取り出すん？」

前回のコードを見てもらえば分かりますがPythonのスクリプトを同じディレクトリにtimelapse_imagesと言うディレクトリが無いか調べてあればそこに保存されます。（なければ作成するようにしています）

私はVNCで接続してリモートで保存データを取り出して動作のチェックをしていましたが、実際に使用する先生はブラウザからStreamlitにアクセスする訳です。
なのでこのシステムから一括でDL出来るように変更しました。

```timelapse.py
import streamlit as st
import cv2
import time
import os
from datetime import datetime
from zipfile import ZipFile

# タイトルとタブ名の設定
st.set_page_config(page_title="理科タイムラプスカメラ")
st.title("理科実験タイムラプスカメラ")
st.write("Webカメラの画像を一定間隔で撮影します。")

# ディレクトリの作成
output_dir = "timelapse_images"
os.makedirs(output_dir, exist_ok=True)

# 解像度の選択肢
resolutions = {
    "640x480": (640, 480),
    "1280x720": (1280, 720),
    "1920x1080": (1920, 1080)
}

# ユーザー入力
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 init_session_state():
    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

init_session_state()

# Webカメラの初期化
def init_camera():
    st.session_state.cap = cv2.VideoCapture(0)
    if not st.session_state.cap.isOpened():
        st.error("Webカメラを開けませんでした。")
        return False
    return True

# Webカメラの解放
def release_camera():
    if st.session_state.cap is not None:
        st.session_state.cap.release()
        st.session_state.cap = None

# 画像キャプチャと保存
def capture_image():
    if st.session_state.cap is not None:
        ret, frame = st.session_state.cap.read()
        if ret:
            frame = cv2.resize(frame, selected_resolution)
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)  # BGRからRGBに変換
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = os.path.join(output_dir, f"{timestamp}.jpg")
            cv2.imwrite(filename, cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))  # 保存時には再びBGRに変換
            image_placeholder.image(frame, caption=f"Captured: {timestamp}")

# プレビューの表示
def preview():
    if st.session_state.cap is not None:
        ret, frame = st.session_state.cap.read()
        if ret:
            frame = cv2.resize(frame, selected_resolution)
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)  # BGRからRGBに変換
            preview_placeholder.image(frame, caption="Preview", use_column_width=True)

# キャプチャの制御
def start_capturing():
    if st.session_state.previewing:
        stop_preview()
    if init_camera():
        st.session_state.capturing = True

def stop_capturing():
    st.session_state.capturing = False
    release_camera()

def start_preview():
    if init_camera():
        st.session_state.previewing = True

def stop_preview():
    st.session_state.previewing = False
    release_camera()

# ボタンの配置
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(0.03)  # 30fpsを目指すために0.03秒待つ

while st.session_state.capturing:
    capture_image()
    time.sleep(capture_interval)
    if not st.session_state.capturing:
        break

# Streamlitアプリの停止時にWebカメラを解放
release_camera()

# 画像ファイルのリスト表示と一括ダウンロード
def zip_images():
    zip_filename = os.path.join(output_dir, "images.zip")
    with ZipFile(zip_filename, 'w') as zipf:
        for image_file in os.listdir(output_dir):
            if image_file.endswith(".jpg"):
                zipf.write(os.path.join(output_dir, image_file), image_file)
    return zip_filename

# ダウンロード処理と画像ファイル名のリスト表示
if st.button("すべての画像"):
    zip_filename = zip_images()
    with open(zip_filename, "rb") as file:
        st.download_button(
            label="ここをクリックしてダウンロード",
            data=file,
            file_name="images.zip",
            mime="application/zip"
        )

    # 画像ファイル名のリスト表示
    st.write("## キャプチャされた画像一覧")
    image_files = sorted(os.listdir(output_dir))
    for image_file in image_files:
        if image_file.endswith(".jpg"):
            st.write(image_file)

# 画像ファイルの一括削除
if st.button("すべての画像を削除"):
    for image_file in os.listdir(output_dir):
        if image_file.endswith(".jpg"):
            os.remove(os.path.join(output_dir, image_file))
    st.success("すべての画像が削除されました。")
```

## 画像ファイルをZIPファイルに圧縮する関数

```python
def zip_images():
    zip_filename = os.path.join(output_dir, "images.zip")
    with ZipFile(zip_filename, 'w') as zipf:
        for image_file in os.listdir(output_dir):
            if image_file.endswith(".jpg"):
                zipf.write(os.path.join(output_dir, image_file), image_file)
    return zip_filename
```
zip_images 関数は、指定されたディレクトリ (output_dir) 内のすべての .jpg ファイルをZIPファイルに圧縮します。
圧縮されたZIPファイルのパスを返します。

## 画像ファイルのダウンロードとリスト表示

```python
if st.button("すべての画像"):
    zip_filename = zip_images()
    with open(zip_filename, "rb") as file:
        st.download_button(
            label="ここをクリックしてダウンロード",
            data=file,
            file_name="images.zip",
            mime="application/zip"
        )

    # 画像ファイル名のリスト表示
    st.write("## キャプチャされた画像一覧")
    image_files = sorted(os.listdir(output_dir))
    for image_file in image_files:
        if image_file.endswith(".jpg"):
            st.write(image_file)
```
"すべての画像" ボタンがクリックされたときに実行されます。
zip_images 関数を呼び出して、画像ファイルをZIPファイルに圧縮します。
圧縮されたZIPファイルをダウンロードボタンとして表示します。
画像ファイル名のリストを表示します。

## 画像ファイルの一括削除
``` python
if st.button("すべての画像を削除"):
    for image_file in os.listdir(output_dir):
        if image_file.endswith(".jpg"):
            os.remove(os.path.join(output_dir, image_file))
    st.success("すべての画像が削除されました。")
```

"すべての画像を削除" ボタンがクリックされたときに実行されます。
指定されたディレクトリ (output_dir) 内のすべての .jpg ファイルを削除します。
削除完了のメッセージを表示します。


## まとめ
やはり実際使ってもらう事で要望や改善点が見えてきますね。
情報技術が他の分野で活用出来ることは素晴らしいことだと思います。
是非参考にして貰えたら幸いです。