M5Stack Unit Cam OV2640 と ラズパイ4を python でシリアル接続した

## やったこと
SWITCH SCIENCE が安売りしている[OV2640搭載 Unit Cam Wi-Fi Camera](https://www.switch-science.com/products/7231?variant=42382149091526)を購入した。
モジュールの発売日は 2021-06-24 で少し古い。
この Unit Cam を ラズパイ4 のシリアル接続のカメラにできたのでメモする。

まず、最初に入っているファームウェアの動作を確認するため先人の知恵を調べた。

Lang-shipさんの[M5Stack Unit CAM(U109)](https://lang-ship.com/blog/work/m5stack-unit-cam-u109/) に怖いことが書かれている。(私もファームウェアを見つけられなかった。)
> 最初に入っているファームウエアは垂れ流し系みたいですね。
> さて、書き換える前にどんなファームウエアかなと思ったら、、、ファームウエア未公開です。
> バイナリでも公開していないので、書き換えるともとの状態に戻せません！！！

Qiitaの@taiyyytaiさんの[M5stackCore2+Unit Camで画像を取得する](https://qiita.com/taiyyytai/items/544874fe330d1fa9782f) を読むと M5Stack Core2 で表示可能らしい。

私は M5Stack Core* を持たないので  python で Linux(ラズパイ4) と Unit Cam をシリアル接続した。
その結果、最初に入っているファームウェアで640x480の動画が乱れることなく撮影できた。

## 使ったもの
1. Unit Cam
2. Raspberry Pi 4 4GB
3. GROVEケーブル
4. M5Stack ダウンローダー

## Unit Cam と ラズパイ4の接続
ラズパイ4のUSBケーブルを M5Stack ダウンローダに接続、さらに GROVEケーブルで Unit Cam に接続した。
M5Stack ダウンローダーと GROVE ケーブルの接続は下記である。
| ダウンローダー|ケーブル色|
|:-------:|:------------:|
| GND     | 黒       |
| G0      | --       |
| EN      | --       |
| TX0     | 白       |
| RX0     | 黄       |
| 3.3V    | 赤       |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042311/8253a295-b5b3-4c09-b88d-9915bec63cc1.png)

## Raspberry Pi 4 の設定
Raspberry Pi 4 は Raspberry Pi OS 64 である。
OpenCV を使うので X Window を起動する。
pip で opencv-python と opencv-contrib-python をインストールする。

## スクリプト Cam2RPi.py の作成
SWITCH SCIENCE の販売ページの [Arduinoサンプルコード](https://github.com/m5stack/M5-ProductExampleCodes/blob/master/Unit/UNIT_CAM/CAM2CORE/CAM2CORE.ino)を python スクリプトに変換した。
変更点は２点
1. JPEGフレームの処理を加え
2. 割り込みでなくループ内で表示した

```
import serial
import threading
import queue
from PIL import Image
import io
import cv2　# OpenCV
import numpy as np
import time

# UART の設定（適切なポートに変更してください）
ser = serial.Serial('/dev/ttyACM0', baudrate=1500000, timeout=1)

# フレーム用のキュー（最大 2 フレーム）
frame_queue = queue.Queue(maxsize=2)

def uart_read_thread():
    """
    シリアルからデータを読み込み、JPEGフレームの開始・終了を検出してキューに格納するスレッド関数
    JPEGは 0xFFD8 で開始し、0xFFD9 で終了することを利用する。
    """
    buffer = bytearray()
    while True:
        data = ser.read(1024)
        if data:
            buffer.extend(data)
            while True:
                # JPEGの開始マーカー(0xFFD8)を検出
                start_index = buffer.find(b'\xff\xd8')
                if start_index == -1:
                    # 開始マーカーがない場合は、後続データのためにバッファを保持
                    break
                # JPEGの終了マーカー(0xFFD9)を開始マーカー以降から検出
                end_index = buffer.find(b'\xff\xd9', start_index + 2)
                if end_index == -1:
                    # 終了マーカーがまだ見つからない場合、フレームは未完成
                    break
                # 終了マーカーを含めるためにインデックスを調整
                end_index += 2
                # 完全なJPEGフレームを切り出し
                frame_data = buffer[start_index:end_index]
                # バッファから処理済み部分を削除
                buffer = buffer[end_index:]
                # キューにフレームを送信（キューが満杯なら破棄）
                try:
                    frame_queue.put(frame_data, block=False)
                except queue.Full:
                    pass

def display_frame_thread():
    """
    キューからJPEGフレームを取得し、デコードして表示するスレッド関数
    """
    while True:
        frame_data = frame_queue.get()  # ブロッキングで取得
        try:
            # JPEGデータをPillowで読み込み
            image = Image.open(io.BytesIO(frame_data))
            image.load()  # 画像の完全な読み込みを強制
        except Exception as e:
            print("画像デコードエラー:", e)
            continue

        # Pillow画像をOpenCV形式（BGR）に変換して表示
        image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
        cv2.imshow("UART JPEG Frame", image_cv)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

# スレッドの開始
uart_thread = threading.Thread(target=uart_read_thread, daemon=True)
# display_thread = threading.Thread(target=display_frame_thread, daemon=True)
uart_thread.start()
# display_thread.start()

# メインループを維持
try:
    while True:
        display_frame_thread()
        time.sleep(0.03)
        pass
except KeyboardInterrupt:
    print("終了します")
    cv2.destroyAllWindows()
```
## 実行
ラズパイ4の X Window のターミナルで ```python Cam2RPi.py```を実行する。
640x480の動画が乱れることなく撮影できた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042311/e4d09671-2209-4ff5-9c7f-64d7fa5e5671.png)

## 最後に
今回はじめてシリアルを python で受信、OpenCV で jpeg を表示した。
Lang-shipさん と Qiitaの@taiyyytaiさん のように誰かのヒントになれば幸いです。