手势识别 AI 应用:无接触航班追踪器
开发者用 AI 实现手势控制的航班跟踪应用,展示多模态交互的实际案例。应用场景有限,但展示了 AI 在交互创新中的潜力。
开发者用 AI 实现手势控制的航班跟踪应用,展示多模态交互的实际案例。应用场景有限,但展示了 AI 在交互创新中的潜力。
我编辑过这篇文章,但有 AI 的帮助。这些都是与 Advent of AI 相关的快速文章。如果我每天都要写一篇,我没有时间花几小时打磨文章。😅
Advent of AI 系列利用 Goose,一个开源 AI Agent。如果你从未听说过,一定要去看看!
在 Goose 的 Advent of AI 第 5 天,我的挑战是构建"返乡委员会"(The Homecoming Board)。这是一个由手势控制的航班到达显示屏,穿着手套和连指手套的人们可以用手势进行导航。在严寒中无需触摸屏幕。挑战要求至少两个不同的手势用于导航、真实的航班数据,以及手势识别的音频反馈作为加分项。
TL;DR:如果你没耐心,我造了一个很酷的东西,可以在 flightboard.nickyt.co 找到。
我用 TanStack Start(React + TypeScript,支持 SSR)、MediaPipe 进行手势识别,以及 OpenSky Network API 获取实时航班数据来构建这个应用。
老实说,这是我第一次用计算机视觉构建应用,完全被这个 Advent of AI 日的挑战所吸引,尽量多地完成各项功能。能够如此便捷地利用计算机视觉之类的技术真是疯狂。这让我想起了之前和 Gant Laborde 一起做的一场精彩直播,讨论的是 AI 和 TensorFlow.js。
我选择 TanStack Start 是因为我之前在一个重要项目(Pomerium MCP 应用演示)中已经用过它。
拥有可用的 API 端点是规避第三方 API 的 CORS 问题的经典做法。在这个例子中,我可以通过自己的服务器函数来代理对 OpenSky API 的请求。
以下是我成功实现的功能:
除了挑战要求外,我还添加了在有多个摄像头时选择摄像头的功能。
在移动设备上的效果也相当不错。
我首先生成了一份产品需求文档(PRD)来规划工作。这已经成为我在这类挑战中的标准做法。他们提供了挑战的所有信息,但我可以基于这些信息按照我的想法来实现。
我最初在浏览器中运行 MediaPipe 有点吃力。当时我对 MediaPipe 不熟悉,试图跟着基本设置做,但某个地方出了问题,所以我转而尝试 TensorFlow.js,它奏效了,最后我还是把 MediaPipe 搞定了。
我特意选择了 MediaPipe 的 WASM 版本,因为我想把它部署到 Netlify。WASM 运行时在浏览器中运行,这意味着我可以在任何 PaaS 平台上托管,无需担心 Python。话说回来,我知道 Vercel 现在也支持 Python 了。
手部追踪以 30-60 FPS 的帧率运行,并能可视化手部地标。我将视频源和地标都做了镜像处理,这样你移动手时看起来就很自然。
我遇到了一个奇怪的现象:绿色的骨架叠加层(手部地标的可视化)同时出现在我的头和手上。MediaPipe 把面部特征误认成了类似手部的形状。其实不算什么问题,但我通过修改逻辑,只在至少检测到一只手时才渲染骨架,这样就解决了。
我还超出了挑战的基本要求。挑战只要求至少两个手势,但我实现了四个:握拳、张开手掌、竖起大拇指、竖起下拇指。而且我让它能独立检测两只手,所以每只手都可以同时做出不同的手势。
手势识别比我预期的要复杂得多。我最初的方法是基于手指弯曲比率用固定阈值。原理很简单:测量每个指尖到手腕的距离,除以关节到手腕的距离,就能得到一个弯曲比率。比率低于阈值就说明手指是弯曲的。
在初期开发时效果不错。但当我在不同的光照条件下测试,或者在距离摄像头不同的位置测试时,手势基本识别不了。不同的手部位置和光照条件完全破坏了我的固定阈值。
解决办法是加入一个手势训练模式。用户每个手势做几遍,系统就能计算出个性化的阈值,并带有差异感知的容差。训练数据的差异越大,阈值就越宽松,这样能处理人们做手势时的自然变化。
这其实是挑战中的加分功能之一,但事实证明它不是可选的,而是必需的。没有它,手势识别就太脆弱了,根本无法实用。
这是一个典型的过拟合案例——我对自己的训练数据过拟合了。教训很清楚:在构建 ML 系统时(即使是简单的系统),一定要考虑数据的方差。固定容差在演示中能用,但自适应容差才是生产环境的正道。
对于航班数据,我用的是 OpenSky Network 的免费 API。不需要身份验证,设置很简单。API 返回实时航班位置信息,我用边界框来过滤特定机场附近的到达航班。
TanStack Query 处理所有缓存和自动刷新的逻辑:
OpenSky Network 有严格的速率限制(最少间隔 10 秒),我在开发时曾经被限流过。这就是为什么我在开发模式下把 staleTime 设到了 5 分钟。如果你也被限流了,可以用 VPN 换个 IP,这样就能再用一段时间 API。
生产环境下 20 秒的 staleTime 和 30 秒的 refetchInterval 能保持数据足够新鲜,同时不会频繁轰炸 API。
挑战要求手势识别要有音频反馈,事实证明这是必需的。我为每个手势添加了不同的音效:握拳是"嗖"的声音,张开手掌是"叮"的声音,竖起大拇指是"叮"的声音,竖起下拇指是"嗡"的声音。
音效会预加载,只在手势改变时播放,而不是每一帧都播放:
你可以在设置里切换声音开关,这在反复测试同一个手势时特别重要。
虽然这是加分项,但我能看出它的无障碍价值。想象一下,有个屏幕阅读器在朗读航班信息,你可以竖起大拇指来说"就这个航班!"
用竖起大拇指的手势选中一个航班时,会打开一个详情模态框,展示所有航班信息:国家旗帜、呼号、位置、高度、速度、航向和最后联系时间。
对于模态框 UI,我用的是 ShadCN 组件,特别是 Dialog 和 Drawer。我之前在类似场景用过它们。Dialog 用于桌面端的模态框体验,Drawer 在移动端提供那种从底部滑起的交互效果。两者都内置了适当的无障碍支持,这总是加分项。
在移动设备上,我把模态框换成了从底部滑起的抽屉组件。通过检查窗口宽度来决定要渲染哪个组件。
我设计了浅色和深色两套冬季主题,和第 4 天很相似。
浅色模式用纯白背景、深冰蓝的主色,对比度达到 13:1,符合 WCAG AAA 标准。深色模式用深蓝紫的夜空配上明亮的发光蓝色。
主题会持久化到 localStorage,首次加载时还会尊重系统偏好。
特别让我满意的是自定义 hooks 的架构。这个项目最终有 8 个自定义 hooks,它们让组件代码清晰多了。
useMediaPipe 处理所有 MediaPipe 的初始化和清理工作。它设置 Hands 实例,配置选项,返回处理函数。这个 hook 封装了所有 WASM 加载逻辑,所以组件不需要了解 MediaPipe 的内部细节。
useWebcam 管理摄像头访问和设备选择。它处理权限请求、列出可用摄像头,并把我的摄像头选择保存到 localStorage。这很关键,因为我有多个摄像头,想在笔记本摄像头和外置摄像头之间切换。
useGestures 是手势识别逻辑的所在地。它接收来自 MediaPipe 的手部地标,返回当前的手势类型。这个 hook 还处理防抖(识别手势前需要 300ms 的稳定性)和基于训练数据的差异感知阈值计算。
useFlightData 包装了 TanStack Query 来获取航班数据。它处理 OpenSky Network API 调用、把响应解析成我们的 ProcessedFlight 格式,管理缓存和刷新间隔。所有航班数据逻辑都集中在这一个 hook 里。
useLocalStorage 是个简单但必要的 hook,能把状态和 localStorage 同步。我用它来保存摄像头选择、主题偏好和音量设置。状态改变时会自动保存。我通常在大多数 React 项目中都会加上这个 hook。
useWindowFocus 检测浏览器标签何时失焦,这样我们就能暂停摄像头。这是个省电大招。没有它的话,MediaPipe 即使在我切换标签时也会继续处理视频帧,白白耗电。不仅省电,而且当你没在用应用时也不会被手势误触发。
useGestureTraining 管理整个手势训练流程。我多遍重复每个手势,这个 hook 就收集手指弯曲数据,计算均值和标准差,生成带差异感知容差的个性化阈值。
useAudio 处理所有音效。它预加载音频文件,管理播放,仅在手势改变时播放(而不是每一帧),还会尊重我的音量设置和静音开关。
自定义 hooks 在 React 应用里很常见,我对最终创建的这些 hooks 相当满意。
如前所述,这是我第一次做计算机视觉应用,所以我学到了几点:
手势需要真实场景测试和自适应阈值。我的固定阈值在一种光照条件下能用,但其他条件就不行。即使有手势训练,我觉得还是有调优的空间。
窗口焦点检测省电。MediaPipe 即使在我切换标签时也会继续运行,耗尽 CPU。在窗口失焦时暂停摄像头解决了这个问题。这我最初没想到,但除了省电,还能防止在不用应用时被手势误触。我在用其他应用时看到它的实际效果才发现了这一点。
窗口焦点检测省电。MediaPipe 即使在我切换标签时也会继续运行,耗尽 CPU。在窗口失焦时暂停摄像头解决了这个问题。这我最初没想到,但除了省电,还能防止在不用应用时被手势误触。我在用其他应用时看到它的实际效果才发现了这一点。
应用已部署并运行在 flightboard.nickyt.co。去试试吧,玩一玩!你可以训练自己的手势,用手部动作导航航班。代码在我的 Advent of AI 2025 仓库里,想看的话可以去查看。
还有不少完善工作要做。手势识别可以更加鲁棒,UI 还能进一步优化,还有些边界情况没处理。但这就是 Advent of AI,该是转向下一个挑战的时候了。
潜在的未来改进包括多机场支持、航班轨迹可视化、双手手势以及 PartyKit 集成以实现多用户控制。但对于 48 小时的构建,我对最终成果很满意。
想保持联系的话,我所有的社交媒体账户都在 nickyt.online。