使用 Supabase 和 WebGazer.js 构建实时眼动追踪体验
TL;博士:
- 使用 Supabase、React、WebGazer.js、Motion One、anime.js、稳定音频构建
- 利用 Supabase 实时呈现和广播(根本不使用数据库表!)
- GitHub 存储库
- 网站
- 演示视频
又一场 Supabase 启动周黑客马拉松和另一个实验项目,名为 凝视深渊。 这最终成为了最简单又最复杂的项目之一。幸运的是,我最近很喜欢 Cursor,所以我得到了一些帮助来完成它!我还想验证我心中的一个问题:是否可以使用仅 Supabase 的实时功能而无需任何数据库表? (也许有些明显)答案是:是的,是的(爱你,实时团队♥️)。因此,让我们更深入地了解实现。
这个想法
有一天,我随机想到了尼采关于深渊的名言,如果能够以某种方式实际想象它会很好(而且很酷):你凝视着黑暗的屏幕,有东西在凝视着你。没有更多了!
构建项目
最初我的想法是使用 Three.js 来制作这个项目,但我意识到这意味着我需要为 3D 眼睛创建或找到一些免费资源。我认为这有点太多了,特别是因为我没有太多时间来处理项目本身,因此决定使用 SVG 进行 2D 制作。
我也不希望它只是视觉效果:如果有一些音频也会有更好的体验。所以我有一个想法,如果参与者可以对着麦克风说话,而其他人可以听到不合格的低语或风过时的声音,那就太棒了。然而,这非常具有挑战性,因此我决定完全放弃它,因为我无法将 WebAudio 和 WebRTC 很好地连接在一起。我的代码库中确实有一个剩余组件,如果您想看一下,它会监听本地麦克风并为当前用户触发“风声”。也许将来会添加一些东西?
实时房间
在处理任何视觉内容之前,我想测试一下我想要的实时设置。由于实时功能存在一些限制,我希望它能够工作,以便:
- 最多有。一个频道一次有 10 名参与者
- 意味着如果一个新频道已满,您需要加入一个新频道
- 你应该只看到其他参与者的眼睛
为此,我想出了一个 useEffect 设置,它递归地加入到实时通道,如下所示:
这个 joinRoom 位于 useEffect 钩子内,并在安装房间组件时被调用。我在开发此功能时发现的一个警告是,currentPresences 参数在连接事件中不包含任何值,即使它可用。我不确定这是否是实施中的错误或按预期工作。因此,每当用户加入时,需要手动获取 room.presenceState 来获取房间中的参与者数量。
我们检查参与者数量,然后取消订阅当前房间并尝试加入另一个房间,或者然后继续当前房间。我们在加入事件中执行此操作,因为同步太晚了(它在加入或离开事件后触发)。
我通过在浏览器中打开大量选项卡来测试此实现,一切看起来都很棒!
之后,我想通过鼠标位置更新来调试解决方案,但很快遇到了一些在频道中发送过多消息的问题!解决方案:限制调用。
/** * 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) } } }
光标想出了这个小油门函数创建器,我将它与眼动追踪广播一起使用,如下所示:
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 })
这很有帮助!另外,在最初的版本中,我在存在状态下发送了眼动追踪消息,但是广播每秒允许更多消息,所以我将实现切换到了这一点。这在眼动追踪中尤其重要,因为相机会一直记录一切。
眼球追踪
当我第一次有了这个项目的想法时,我就遇到了 WebGazer.js。这是一个非常有趣的项目,而且效果出奇的好!
整个眼球追踪功能是在 useEffect 挂钩中的一个函数中完成的:
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) } })
获取用户正在查看的信息很简单,就像获取屏幕上的鼠标位置一样。然而,我还想添加眨眼检测作为(一项很酷的)功能,这需要跳过一些环节。
当您在 google 上搜索有关 WebGazer 和眨眼检测的信息时,您可以看到初始实现的一些剩余内容。就像源代码中甚至有注释掉的代码一样。不幸的是,库中不存在此类功能。您需要手动完成。
经过大量的试验和错误,Cursor 和我想出了一个解决方案,可以根据眼罩数据计算像素和亮度级别,以确定用户何时眨眼。它还具有一些动态照明调整功能,因为我注意到(至少对我来说)网络摄像头并不总是能根据您的照明来识别您何时眨眼。对我来说,我的照片/房间越亮,效果越差,而在较暗的灯光下效果更好(见图)。
在调试眼动追踪功能时(WebGazer 有一个非常好的 .setPredictionPoints 调用,它在屏幕上显示一个红点以可视化您正在看的位置),我注意到跟踪不是很准确 除非您进行校准 这是项目要求您在加入任何房间之前要做的事情。
/** * 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) } } }
看到它的实际应用是一次非常酷的体验!我对周围的线条应用了相同的方法,并指示光标将它们向中心“折叠”:它几乎一气呵成!
然后,眼睛将在一个简单的 CSS 网格内渲染,单元格对齐,这样整个房间看起来就像一只大眼睛。
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 })
最后的润色
然后播放一些不错的介绍屏幕和背景音乐,项目就可以开始了!
当您处理此类事情时,音频总是可以改善体验,因此我使用稳定音频在用户“进入深渊”时生成背景音乐。我用于音乐的提示如下:
环境、令人毛骨悚然、背景音乐、低语声、风、慢节奏、怪异、深渊
我还认为纯黑屏幕有点无聊,所以我在背景上添加了一些动画 SVG 滤镜。此外,我在屏幕中央添加了一个黑暗的、模糊的圆圈,以产生一些漂亮的淡入淡出效果。我可能可以使用 SVG 滤镜来完成此操作,但我不想在这方面花费太多时间。然后为了有更多的运动,我让背景绕其轴旋转。有时使用 SVG 滤镜制作动画有点奇怪,所以我决定采用这种方式。
<div> <h2> 结论 </h2> <p>现在您已经了解了:相当直接地了解如何使用 Supabase 的实时功能实现程式化的眼球追踪。就我个人而言,我发现这是一个非常有趣的实验,并且在进行过程中没有遇到太多问题。令人惊讶的是,在提交项目之前我不需要在最后一晚熬夜!</p> <p>请随意查看该项目或演示视频的结果。如果一群人同时使用它,可能会出现一些问题(很难测试,因为它需要多个设备和网络摄像头才能正确完成),但我想这是黑客马拉松项目的时尚?如果您确实进行了测试,请记住,如果您看到一只眼睛,那就是其他人通过互联网在某个地方看着您!</p> </div>
以上是使用 Supabase 和 WebGazer.js 构建实时眼动追踪体验的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

Python和JavaScript开发者的薪资没有绝对的高低,具体取决于技能和行业需求。1.Python在数据科学和机器学习领域可能薪资更高。2.JavaScript在前端和全栈开发中需求大,薪资也可观。3.影响因素包括经验、地理位置、公司规模和特定技能。

学习JavaScript不难,但有挑战。1)理解基础概念如变量、数据类型、函数等。2)掌握异步编程,通过事件循环实现。3)使用DOM操作和Promise处理异步请求。4)避免常见错误,使用调试技巧。5)优化性能,遵循最佳实践。

实现视差滚动和元素动画效果的探讨本文将探讨如何实现类似资生堂官网(https://www.shiseido.co.jp/sb/wonderland/)中�...

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

如何在JavaScript中将具有相同ID的数组元素合并到一个对象中?在处理数据时,我们常常会遇到需要将具有相同ID�...

深入探讨console.log输出差异的根源本文将分析一段代码中console.log函数输出结果的差异,并解释其背后的原因。�...
