Brilliant Labs の Frame で写真を撮ってみる

初めに

以下の前回の記事で Brilliant LabsFrame を Mac から動かして OLED に Hello World を表示するところまで動かしました。

ayousanz.hatenadiary.jp

今回は Frame のカメラで写真を撮って、Mac 側にファイルとして保存してみます。

開発環境

前回と同じです。

  • macOS
  • Python 3.12(uv 管理)
  • frame-ble 1.1.1 / frame-msg 5.2.1

frame-msg を使う

カメラやマイクのように Lua 文字列を送るだけでは完結しない機能を扱うときは、低レベルの frame-ble ではなく高レベルの frame-msg を使います。

frame-msg はホスト側 Python とデバイス側 Lua のセットで動かす作りになっています。典型的な流れは以下の通りです。

  1. ホストから Frame に標準 Lua ライブラリ(data, camera など)を転送する
  2. ホストから「アプリ本体の Lua」を転送する
  3. ホストから start_frame_app() でデバイス側のアプリを起動する
  4. ホスト ↔ Frame で構造化メッセージをやり取りする

カメラ用のアプリ本体である camera_frame_app.lua は、公式の frame_examples_python(BSD-3-Clause)から流用します。リポジトリの frame_msg/lua/camera_frame_app.lua をそのままコピーして、自分のプロジェクトの samples/lua/ 配下に置きました。

ホスト側のコード

ホスト側の Python サンプルです。samples/take_photo.py として置いています。

"""Frameのカメラで写真を撮ってホスト側に保存する最小サンプル."""

from __future__ import annotations

import asyncio
from datetime import datetime
from pathlib import Path

from frame_msg import FrameMsg, RxPhoto, TxCaptureSettings

LUA_APP = Path(__file__).parent / "lua" / "camera_frame_app.lua"
CAPTURES_DIR = Path(__file__).resolve().parent.parent / "captures"
CAPTURE_SETTINGS_MSG = 0x0d


async def main() -> None:
    CAPTURES_DIR.mkdir(parents=True, exist_ok=True)

    frame = FrameMsg()
    try:
        await frame.connect()

        await frame.print_short_text("Loading...")

        await frame.upload_stdlua_libs(lib_names=["data", "camera"])
        await frame.upload_frame_app(local_filename=str(LUA_APP))

        frame.attach_print_response_handler()

        await frame.start_frame_app()

        rx_photo = RxPhoto()
        photo_queue = await rx_photo.attach(frame)

        print("Letting autoexposure loop run for 5 seconds to settle")
        await asyncio.sleep(5.0)
        print("Capturing a photo")

        await frame.send_message(
            CAPTURE_SETTINGS_MSG,
            TxCaptureSettings(resolution=720).pack(),
        )

        jpeg_bytes = await asyncio.wait_for(photo_queue.get(), timeout=10.0)

        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        out_path = CAPTURES_DIR / f"photo_{timestamp}.jpg"
        out_path.write_bytes(jpeg_bytes)
        print(f"Saved {len(jpeg_bytes)} bytes to {out_path}")

        rx_photo.detach(frame)
        frame.detach_print_response_handler()

        await frame.stop_frame_app()

    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        await frame.disconnect()


if __name__ == "__main__":
    asyncio.run(main())

ポイントは以下の通りです。

  • upload_stdlua_libs(['data', 'camera']) で標準 Lua ライブラリを Frame に送る
  • upload_frame_app(...) でカメラアプリ本体を Frame に送る
  • 起動直後はオートエクスポージャが安定していないので 5 秒待つ
  • TxCaptureSettings(resolution=720) で撮影リクエストを送る(解像度 720×720 の最大値)
  • RxPhoto のキューから JPEG バイトを受け取り、captures/ 配下に保存

TxCaptureSettings には他にも quality_index(0〜4 の段階指定で、0 が VERY_LOW、4 が VERY_HIGH)や panraw などのパラメータがありますが、今回はデフォルトのままで撮ります。

実行

実機を装着して、以下を実行します。

uv run python samples/take_photo.py

Frame の OLED に一瞬白いフラッシュが出て、約 5 秒のオートエクスポージャ待機の後に撮影、JPEG が BLE 経由で Mac に転送されてきます。

Frame app is running
Letting autoexposure loop run for 5 seconds to settle
Capturing a photo
Saved 36279 bytes to /.../captures/photo_20260510_181153.jpg

縦縞のノイズが目立ちますが、被写体は判別できる程度には写っています。Frame の Bluetooth 帯域は 40kBps 程度しか出ないため、720×720 の JPEG 1 枚を受け取るだけでも数秒かかります。