首頁 >web前端 >js教程 >使用 Supabase 和 WebGazer.js 建立即時眼動追蹤體驗

使用 Supabase 和 WebGazer.js 建立即時眼動追蹤體驗

Mary-Kate Olsen
Mary-Kate Olsen原創
2024-12-28 11:06:34551瀏覽

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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn