Heim >Web-Frontend >js-Tutorial >Erstellen Sie mit Supabase und WebGazer.js ein Echtzeit-Eye-Tracking-Erlebnis
TL;DR:
- Erstellt mit Supabase, React, WebGazer.js, Motion One, anime.js, Stable Audio
- Nutzt Supabase Realtime Presence & Broadcast (es werden überhaupt keine Datenbanktabellen verwendet!)
- GitHub-Repo
- Website
- Demovideo
Noch ein weiterer Supabase Launch Week Hackathon und ein weiteres experimentelles Projekt namens Gaze into the Abyss. Dies war letztendlich eines der einfachsten und komplexesten Projekte zugleich. Zum Glück hat mir Cursor in letzter Zeit ziemlich viel Spaß gemacht, sodass ich einige helfende Hände dabei hatte, es durchzustehen! Ich wollte auch eine Frage in meinem Kopf validieren: Ist es möglich, nur die Echtzeitfunktionen von Supabase ohne Datenbanktabellen zu verwenden? Die (vielleicht etwas offensichtliche) Antwort lautet: Ja, ja (liebe Grüße, Realtime-Team ♥️). Tauchen wir also etwas tiefer in die Umsetzung ein.
Eines Tages dachte ich zufällig an Nietzsches Zitat über den Abgrund und dass es schön (und cool) wäre, es sich tatsächlich irgendwie vorzustellen: Man starrt auf einen dunklen Bildschirm und etwas starrt zurück. Mehr ist da nicht drin!
Anfangs hatte ich die Idee, Three.js zu verwenden, um dieses Projekt zu erstellen, mir wurde jedoch klar, dass dies bedeuten würde, dass ich einige kostenlose Assets für die 3D-Augen erstellen oder finden müsste. Ich entschied, dass es etwas zu viel ist, vor allem, weil ich nicht viel Zeit hatte, um am Projekt selbst zu arbeiten, und habe mich stattdessen dafür entschieden, es in 2D mit SVGs zu machen.
Ich wollte auch nicht, dass es nur visuell ist: Mit etwas Audio wäre es auch ein besseres Erlebnis. Deshalb kam mir die Idee, dass es großartig wäre, wenn die Teilnehmer über ein Mikrofon sprechen könnten und andere es als unberechtigtes Flüstern oder vorbeiziehenden Wind hören könnten. Dies erwies sich jedoch als sehr herausfordernd und ich beschloss, es ganz aufzugeben, da ich WebAudio und WebRTC nicht gut miteinander verbinden konnte. Ich habe eine übrig gebliebene Komponente in der Codebasis, die auf das lokale Mikrofon hört und „Windgeräusche“ für den aktuellen Benutzer auslöst, falls Sie einen Blick darauf werfen möchten. Vielleicht gibt es in Zukunft noch etwas hinzuzufügen?
Bevor ich an visuellen Dingen arbeitete, wollte ich das Echtzeit-Setup testen, das ich mir vorgestellt hatte. Da es bei der Echtzeitfunktion einige Einschränkungen gibt, wollte ich, dass sie so funktioniert:
Dazu habe ich mir ein useEffect-Setup ausgedacht, bei dem es rekursiv mit einem Echtzeitkanal verknüpft wird, etwa so:
Dieser joinRoom befindet sich im useEffect-Hook und wird aufgerufen, wenn die Raumkomponente gemountet wird. Eine Einschränkung, die ich bei der Arbeit an dieser Funktion festgestellt habe, war, dass der Parameter „currentPresences“ keine Werte im Join-Ereignis enthält, obwohl er verfügbar ist. Ich bin mir nicht sicher, ob es sich um einen Fehler in der Implementierung handelt oder ob es wie vorgesehen funktioniert. Daher muss ein manueller Abruf von „room.presenceState“ durchgeführt werden, um die Anzahl der Teilnehmer im Raum zu ermitteln, wann immer der Benutzer beitritt.
Wir überprüfen die Teilnehmerzahl und melden uns entweder vom aktuellen Raum ab und versuchen, einem anderen Raum beizutreten, oder fahren dann mit dem aktuellen Raum fort. Wir tun dies im Beitrittsereignis, da die Synchronisierung zu spät wäre (sie wird nach Beitritts- oder Austrittsereignissen ausgelöst).
Ich habe diese Implementierung getestet, indem ich eine ganze Reihe von Tabs in meinem Browser geöffnet habe, und alles schien gut zu sein!
Danach wollte ich die Lösung mit Mauspositionsaktualisierungen debuggen und stieß schnell auf einige Probleme beim Senden zu vieler Nachrichten im Kanal! Die Lösung: Anrufe drosseln.
/** * Creates a throttled version of a function that can only be called at most once * in the specified time period. */ function createThrottledFunction<T extends (...args: unknown[]) => unknown>( functionToThrottle: T, waitTimeMs: number ): (...args: Parameters<T>) => void { let isWaitingToExecute = false return function throttledFunction(...args: Parameters<T>) { if (!isWaitingToExecute) { functionToThrottle.apply(this, args) isWaitingToExecute = true setTimeout(() => { isWaitingToExecute = false }, waitTimeMs) } } }
Cursor hat sich diesen kleinen Drosselfunktions-Ersteller ausgedacht und ich habe ihn für die Eye-Tracking-Übertragungen wie folgt verwendet:
const throttledBroadcast = createThrottledFunction((data: EyeTrackingData) => { if (currentChannel) { currentChannel.send({ type: 'broadcast', event: 'eye_tracking', payload: data }) } }, THROTTLE_MS) throttledBroadcast({ userId: userId.current, isBlinking: isCurrentlyBlinking, gazeX, gazeY })
Das hat sehr geholfen! Außerdem habe ich in den ersten Versionen die Eye-Tracking-Nachrichten mit Präsenz gesendet, Broadcast erlaubt jedoch mehr Nachrichten pro Sekunde, also habe ich die Implementierung stattdessen darauf umgestellt. Dies ist besonders wichtig beim Eye-Tracking, da die Kamera ständig alles aufzeichnet.
Ich bin vor einiger Zeit auf WebGazer.js gestoßen, als ich zum ersten Mal die Idee für dieses Projekt hatte. Es ist ein sehr interessantes Projekt und funktioniert überraschend gut!
Die gesamten Eye-Tracking-Funktionen werden in einer Funktion in einem useEffect-Hook ausgeführt:
window.webgazer .setGazeListener(async (data: any) => { if (data == null || !currentChannel || !ctxRef.current) return try { // Get normalized gaze coordinates const gazeX = data.x / windowSize.width const gazeY = data.y / windowSize.height // Get video element const videoElement = document.getElementById('webgazerVideoFeed') as HTMLVideoElement if (!videoElement) { console.error('WebGazer video element not found') return } // Set canvas size to match video imageCanvasRef.current.width = videoElement.videoWidth imageCanvasRef.current.height = videoElement.videoHeight // Draw current frame to canvas ctxRef.current?.drawImage(videoElement, 0, 0) // Get eye patches const tracker = window.webgazer.getTracker() const patches = await tracker.getEyePatches( videoElement, imageCanvasRef.current, videoElement.videoWidth, videoElement.videoHeight ) if (!patches?.right?.patch?.data || !patches?.left?.patch?.data) { console.error('No eye patches detected') return } // Calculate brightness for each eye const calculateBrightness = (imageData: ImageData) => { let total = 0 for (let i = 0; i < imageData.data.length; i += 16) { // Convert RGB to grayscale const r = imageData.data[i] const g = imageData.data[i + 1] const b = imageData.data[i + 2] total += (r + g + b) / 3 } return total / (imageData.width * imageData.height / 4) } const rightEyeBrightness = calculateBrightness(patches.right.patch) const leftEyeBrightness = calculateBrightness(patches.left.patch) const avgBrightness = (rightEyeBrightness + leftEyeBrightness) / 2 // Update rolling average if (brightnessSamples.current.length >= SAMPLES_SIZE) { brightnessSamples.current.shift() // Remove oldest sample } brightnessSamples.current.push(avgBrightness) // Calculate dynamic threshold from rolling average const rollingAverage = brightnessSamples.current.reduce((a, b) => a + b, 0) / brightnessSamples.current.length const dynamicThreshold = rollingAverage * THRESHOLD_MULTIPLIER // Detect blink using dynamic threshold const blinkDetected = avgBrightness > dynamicThreshold // Debounce blink detection to avoid rapid changes if (blinkDetected !== isCurrentlyBlinking) { const now = Date.now() if (now - lastBlinkTime > 100) { // Minimum time between blink state changes isCurrentlyBlinking = blinkDetected lastBlinkTime = now } } // Use throttled broadcast instead of direct send throttledBroadcast({ userId: userId.current, isBlinking: isCurrentlyBlinking, gazeX, gazeY }) } catch (error) { console.error('Error processing gaze data:', error) } })
Das Ermitteln der Informationen dort, wo der Benutzer gerade hinschaut, ist einfach und funktioniert wie das Ermitteln der Mauspositionen auf dem Bildschirm. Allerdings wollte ich auch die Blinzelerkennung als (coole) Funktion hinzufügen, was einige Hürden erforderte.
Wenn Sie Informationen zu WebGazer und Blinzelerkennung googeln, können Sie einige Überreste einer ersten Implementierung sehen. Es gibt sogar auskommentierten Code in der Quelle. Leider sind diese Funktionen in der Bibliothek nicht verfügbar. Sie müssen dies manuell tun.
Nach vielen Versuchen konnten Cursor und ich eine Lösung entwickeln, die Pixel und Helligkeitsstufen aus den Augenklappendaten berechnet, um festzustellen, wann der Benutzer blinzelt. Es gibt auch einige dynamische Beleuchtungsanpassungen, da mir aufgefallen ist, dass die Webcam (zumindest für mich) je nach Beleuchtung nicht immer erkennt, wann Sie blinzeln. Bei mir funktionierte es schlechter, je heller mein Bild/Raum war, und besser bei dunklerer Beleuchtung (siehe Abbildung).
Beim Debuggen der Eye-Tracking-Funktionen (WebGazer verfügt über einen sehr schönen .setPredictionPoints-Aufruf, der einen roten Punkt auf dem Bildschirm anzeigt, um zu visualisieren, wohin Sie schauen) ist mir aufgefallen, dass das Tracking nicht sehr genau ist es sei denn, Sie kalibrieren es. Dazu werden Sie im Projekt aufgefordert, bevor Sie einem Raum beitreten.
/** * Creates a throttled version of a function that can only be called at most once * in the specified time period. */ function createThrottledFunction<T extends (...args: unknown[]) => unknown>( functionToThrottle: T, waitTimeMs: number ): (...args: Parameters<T>) => void { let isWaitingToExecute = false return function throttledFunction(...args: Parameters<T>) { if (!isWaitingToExecute) { functionToThrottle.apply(this, args) isWaitingToExecute = true setTimeout(() => { isWaitingToExecute = false }, waitTimeMs) } } }
Es war eine sehr coole Erfahrung, dies in Aktion zu sehen! Ich habe den gleichen Ansatz auf die umgebenden Linien angewendet und den Cursor angewiesen, sie zur Mitte hin zu „kollabieren“: was praktisch auf einmal geschah!
Die Augen würden dann in einem einfachen CSS-Raster gerendert, wobei die Zellen so ausgerichtet sind, dass ein vollständiger Raum wie ein großes Auge aussieht.
const throttledBroadcast = createThrottledFunction((data: EyeTrackingData) => { if (currentChannel) { currentChannel.send({ type: 'broadcast', event: 'eye_tracking', payload: data }) } }, THROTTLE_MS) throttledBroadcast({ userId: userId.current, isBlinking: isCurrentlyBlinking, gazeX, gazeY })
Dann legen Sie noch einen schönen Einführungsbildschirm und Hintergrundmusik ein und das Projekt kann losgehen!
Audio verbessert immer das Erlebnis, wenn man an solchen Dingen arbeitet, deshalb habe ich Stable Audio verwendet, um eine Hintergrundmusik zu erzeugen, wenn der Benutzer „den Abgrund betritt“. Die Eingabeaufforderung, die ich für die Musik verwendet habe, war folgende:
Ambiente, gruselig, Hintergrundmusik, flüsternde Geräusche, Winde, langsames Tempo, unheimlich, Abgrund
Ich fand auch, dass ein einfacher schwarzer Bildschirm etwas langweilig ist, also habe ich ein paar animierte SVG-Filtersachen im Hintergrund hinzugefügt. Zusätzlich habe ich einen dunklen, unscharfen Kreis in der Mitte des Bildschirms hinzugefügt, um einen schönen Fade-Effekt zu erzielen. Ich hätte das wahrscheinlich mit SVG-Filtern machen können, aber ich wollte nicht zu viel Zeit damit verbringen. Um noch mehr Bewegung zu haben, habe ich den Hintergrund um seine Achse drehen lassen. Manchmal ist das Erstellen von Animationen mit den SVG-Filtern etwas kompliziert, deshalb habe ich mich stattdessen für diese Methode entschieden.
<div> <h2> Abschluss </h2> <p>Da haben Sie es also: Ein ziemlich einfacher Einblick in die Implementierung eines stilisierten Eye-Trackings mit den Echtzeitfunktionen von Supabase. Ich persönlich fand das ein sehr interessantes Experiment und hatte bei der Arbeit nicht allzu viele Probleme. Und überraschenderweise musste ich die letzte Nacht nicht durcharbeiten, bevor ich das Projekt einreichte!</p> <p>Schauen Sie sich gerne das Projekt oder das Demo-Video an, wie es entstanden ist. Es kann zu Problemen kommen, wenn viele Leute es gleichzeitig verwenden (sehr schwer zu testen, da mehrere Geräte und Webcams erforderlich sind, um es richtig zu machen), aber ich denke, das ist bei Hackathon-Projekten üblich? Und wenn Sie es ausprobieren, denken Sie daran: Wenn Sie ein Auge sehen, ist es jemand anderes, der Sie irgendwo im Internet beobachtet!</p>
Das obige ist der detaillierte Inhalt vonErstellen Sie mit Supabase und WebGazer.js ein Echtzeit-Eye-Tracking-Erlebnis. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!