Vision Proに13個目のカメラを増設してみた
リアルタイムにビデオを取得したい
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円ぐらいです。
カメラのリアルタイム映像をWiFiで送信
UnitCAMS3は出荷時にデモ用のスケッチがインストールされていますが、毎回手動でビデオ送信を開始する必要があります。やっぱり自動起動させたいし、あとあと小回りを効かせたいので、自前でスケッチをプログラムしておくことにします。
MacにVSCodeとPlatformIOをインストールして、下記のスケッチをビルド。
// 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の中身は以下の通りです。
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オリジナル映像に重ねてみます。
映像の拡縮や位置合わせは手動です(汗)。このキャリブレーションの自動化が課題ですが、カメラをマウントして一度設定してしまえば大きなズレは起こらないのではと楽観視しています。次のステップ
・Cam13用カメラマウントを3Dプリンタで作って固定する
・Vision Proの映像とCam13の映像を重ねるキャリブレーションを自動化する
・リアルタイムにAI画像解析をする(これの追加機能)
・解析結果情報をVision Proの映像の上にHUD表示する
・すべてをVision Proオンデバイスで行う(プライバシーを考慮)
** シンギュラリティ・ソサエティのAI Innovators Hubに参加しています。**
Discussion