🤿

Vision Proに13個目のカメラを増設してみた

2024/04/20に公開

リアルタイムにビデオを取得したい

Vision Proで見ている映像をAIでリアルタイムに解析しようとしています。
しかしiPhoneやiPadでは簡単に取得できていたカメラからのビデオや写真をVision Proではプログラム的に取得することができなくなっており、ここで詰みそうになりました。Vision Proには12個もカメラが搭載されているのに、利用者がリアルタイムに何を見ているかはプライバシーに抵触するので利用が禁止されているのです。

しかしそれでは面白くないので、13個目のカメラ(Cam13)を自前で追加してみることにしました。

Cam13の仕組み

外付けでカメラを追加して、その映像をWiFi経由でWKWebViewで受け取り、Vision Proの映像に重ねます。外付けカメラで取得した映像をAI処理することにより、Vision Proの映像を解析するような効果を得ようというアイデアです。
※今年のWWDC24でカメラ映像が解禁されたら、Cam13を捨てて公式APIを利用します(笑)

M5Stackのカメラユニットを繋げる

市販のウェブカメラでも良いのですがVision Proにマウントする自由度を上げたいので、安価で軽量でプログラムを焼くことができて、電源も取り回しやすいユニットを探しました。
選定したのはESP32S3搭載のM5Stackシリーズ UnitCAMS3。送料込みで2,500円ぐらいです。
https://www.switch-science.com/products/9428

カメラのリアルタイム映像をWiFiで送信

UnitCAMS3は出荷時にデモ用のスケッチがインストールされていますが、毎回手動でビデオ送信を開始する必要があります。やっぱり自動起動させたいし、あとあと小回りを効かせたいので、自前でスケッチをプログラムしておくことにします。
MacにVSCodeとPlatformIOをインストールして、下記のスケッチをビルド。

main.cpp
// Access
//   http://192.168.4.1/stream
//   http://192.168.4.1/still

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <esp32cam.h>
#include <esp32cam-asyncweb.h>
#include "apis/camera/api_cam.h"

// WiFi
#define WIFI_SSID "--YourSSID--"
#define WIFI_PASSWORD "--password--"
bool isAccessPointMode = false;

// Camera
esp32cam::Resolution initialResolution;
constexpr esp32cam::Pins UnitCamS3{
  D0: 6,  D1: 15,  D2: 16,  D3: 7,
  D4: 5,  D5: 10,  D6:  4,  D7: 13,
  XCLK: 11,  PCLK: 12,  VSYNC: 42,
  HREF: 18,  SDA: 17,  SCL: 41,
  RESET: 21,  PWDN: -1,
};

// Web server
static void serveStill(AsyncWebServerRequest *request);
AsyncWebServer server(80);

void setup() {
  Serial.begin(115200);

  IPAddress ipAp(192, 168, 4, 1);
  IPAddress ip(192, 168, 1, 123);
  IPAddress gateway(192, 168, 1, 1);
  IPAddress subnet(255, 255, 255, 0);

  if(isAccessPointMode) {
    WiFi.mode(WIFI_AP);
    WiFi.softAP("VisionProCam13-WiFi");
    delay(100);
    WiFi.softAPConfig(ipAp, ip, subnet);
  }
  else {
    if (!WiFi.config(ip,gateway,subnet)){
        Serial.println("Failed to configure!");
    }
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
    }
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("Access 'http://");
    Serial.print(WiFi.localIP());
    Serial.println("/stream' to connect webcam");
  }

  {
    using namespace esp32cam;

    initialResolution = Resolution::find(800, 600);
    Config cfg;
    cfg.setPins(UnitCamS3);
    cfg.setResolution(initialResolution);
    cfg.setJpeg(80);

    bool ok = Camera.begin(cfg);
    if (!ok) {
      Serial.println("camera initialize failure");
      delay(5000);
      ESP.restart();
    }
    Serial.println("camera initialize success");
  }

  server.on("/still", HTTP_GET, serveStill);
  server.on("/stream", HTTP_GET, streamJpg);
  server.begin();
}

void loop() {
  delay(1);
}

// Photo
static void serveStill(AsyncWebServerRequest *request) {
  auto frame = esp32cam::capture();
  if (frame == nullptr) {
    Serial.println("capture() failure");
    request->send(500, "text/plain", "still capture error\n");
    return;
  }
  AsyncWebServerResponse *response = request->beginResponse_P(200, "image/jpeg", frame->data(), frame->size());
  request->send(response);
}

PlatformIOでBuild→Runさせるとサーバーモードで待ち受けます。isAccessPointModeをtrueに設定すると、192.168.4.1で待ち受けるアクセスポイントモード、falseに設定すると192.168.1.123で待ち受けるステーションモードになります。
アクセスポイントモードだと30fps、ステーションモードだと10fpsほどです。Vision ProはWiFiでしかネットに繋がらないので、インターネットとカメラに同時にアクセスするためにはステーションモードで動作させる事になります。

visionOSアプリで映像を受信する

visionOSアプリはSwiftUIで作りました。WKWebViewで"http://192.168.1.123/stream"からのストリーミングを受信するだけのウィンドウです。
Xcodeで新規プロジェクトを作成し、visionOSアプリを選択。Initial SceneはWindowを選択して進めて下さい。ContentView.swiftの中身は以下の通りです。

ContentView.swift
import SwiftUI
import RealityKit
import RealityKitContent
import WebKit

#if os(macOS)
struct WebView: NSViewRepresentable {
	let loadUrl: URL
	func makeNSView(context: Context) -> WKWebView {
		return WKWebView()
	}
	func updateNSView(_ uiView: WKWebView, context: Context) {
		let request = URLRequest(url: loadUrl)
		uiView.load(request)
	}
}
#else
struct WebView: UIViewRepresentable {
	let loadUrl: URL
	func makeUIView(context: Context) -> WKWebView {
		return WKWebView()
	}
	func updateUIView(_ uiView: WKWebView, context: Context) {
		let request = URLRequest(url: loadUrl)
		uiView.load(request)
	}
}
#endif

struct ContentView: View {
	var body: some View {
		WebView(loadUrl: URL(string: "http://192.168.1.123/stream")!)
	}
}

Info.plistに"Privacy - Local Network Usage Description"を追加して下さい。この設定が無いとLAN経由の通信ができずにWebViewが真っ白になります。
スチル映像を取得するには"http://192.168.1.123/still"にアクセスします。

Vision Proの映像に重ねてみる

それではCam13映像をVision Proオリジナル映像に重ねてみます。
https://youtu.be/HjN32xCPnnA
映像の拡縮や位置合わせは手動です(汗)。このキャリブレーションの自動化が課題ですが、カメラをマウントして一度設定してしまえば大きなズレは起こらないのではと楽観視しています。

次のステップ

・Cam13用カメラマウントを3Dプリンタで作って固定する
・Vision Proの映像とCam13の映像を重ねるキャリブレーションを自動化する
・リアルタイムにAI画像解析をする(これの追加機能)
・解析結果情報をVision Proの映像の上にHUD表示する
・すべてをVision Proオンデバイスで行う(プライバシーを考慮)

https://github.com/AlohaYos/VisionProCam13/tree/main

** シンギュラリティ・ソサエティのAI Innovators Hubに参加しています。**

Discussion