前言
相信很多读者和我一样,都有一个巫师梦,等着自己的那只猫头鹰送来霍格沃茨的入学通知书。世界各地的麻瓜们也在乐此不疲地使用科技构筑自己的魔法世界。前段时间B站就有一个视频大火,使用自制魔杖控制家居设备,很是浪漫!1但很多巫师丢失魔杖后只能任人摆布,还是邓布利多和伏地魔的无杖施法更吸引人,本麻瓜当然更向往这种高阶法术(不是)。
无杖施法虽然摆脱了魔杖,但还是至少要挥一挥手,摆个 pose 的。那么是否有识别我的姿态和手势,来理解我的意图的方法呢。
经过调研,使用机器视觉是一个可行的方案,我发现不少人已经做了相关尝试,甚至商用摄像头也初步做过简单手势控制功能。而大多数开源项目采用 Mediapipe 方案,这是一个 Google 开源的手势识别套件,使用简单,但缺陷在于仅支持单人、近距离识别。2且多数项目仅仅在 PC 上调用 Mediapipe 库做了一些简单的特性演示,没有进一步扩展实用功能。
我曾计划在自己的服务器上实现该项目,但考虑到项目对实时性要求高,采用了边缘计算(Edge Computing)方案。对物联网而言,边缘计算意味着许多控制将通过本地设备实现而无需交由云端,大大提升处理效率,由于更加靠近用户,还可提供更快的响应。但是,边缘设备上部署需要克服额外的困难,包括模型的转换与调整,精度与实时性能的权衡。
最终,基于边缘设备(香橙派),我开发了一个手势自动化项目,「Gesture-Based-Home-Automation」。
效果展示
由于少数派附件的大小限制,难以展示长时高清的动画;本文所有演示将人与被控主体放在同一个画面,且手部尽可能正对镜头以展示手势细节;画面有裁剪、无加速。
「Gesture-Based-Home-Automation」的关键特性有:
- 支持多人场景,通过「抬手」动作触发手势识别;
- 低延迟,手势识别延迟 30ms,「抬手」检测延迟 50ms,低延迟能更好地支撑复杂手势并提高「跟手」体验;
- 长距离,能识别超过 5 米距离的手势信号,大体能覆盖单个空间;
- 空间定位,能粗略地识别人体、手部在空间中的位置;
- 复杂手势组合,支持左右手识别与跟踪,支持 5 种静态手势、4 种动态手势、4 种触发模式,通过相互组合并结合手部旋转状态、空间位置,可以定义无穷种手势信号。3
下面展示了项目的部分关键特性。第一组为手部的 5 种基础手势与手掌旋转状态识别;第二组为左右手识别与跟踪、长距离手部定位与手势识别;第三组图为「抬手」触发与基础的开关灯控制演示;第四组图,通过空间定位与手势指令,呼唤扫地机器人前往人体地点进行清扫。(这里使用的是石头 G10S。)常规的做法是「点开 app-进入设备-设置-指哪到哪-点选-确认」,而语音控制无法完成该指令。
以上先演示了部分基础指令,下面先介绍其基本原理,然后引入「高级手势控制」,将更加符合人们习惯的动态手势转译为控制信号,进行更复杂的控制,更多演示可以跳转至高级手势与智能控制章节部分查看。
硬件概况
硬件组成:一个香橙派 5 Pro (Orange Pi 5 Pro)4,一个 5V4A 电源,一个 USB 摄像头。
Orange Pi 5 Pro,尺寸 89mm*56mm*1.6mm,采用了瑞芯微 RK3588S 处理器,四核 A76 + 四核 A55,主频 2.4GHz,集成 ARM Mali-G610;内嵌的 NPU 支持 INT4/INT8/INT16 混合运算,算力 6TOPS,4GB LPDDR5,Wi-Fi 5、BLE。支持 Orange Pi OS、Ubuntu、Android 12、Debian 等操作系统。摄像头为无畸变 86° 视角,1080P/30FPS 的 USB 摄像头。
这颗 6TOPS 的 NPU 是我选择香橙派 5 Pro 的主要理由,我们需要在上面做各种神经网络推理,同时它的价格足够低,总成本约 650 元。
手势识别流程
在家居场景中,我们的关键需求是全天候运行、使用流畅。具体分析下来,手势使用频率相对不高,这决定了我们并不需要时时刻刻获取手势信息,而为了兼顾长时间运行,设计了这样一个流程:
- 常规时间中,运行多人人体关键点检测,输出人体关键点和框;该阶段的特点是资源消耗低,实时性较差(约 50ms),主打一个够用就好;
- 当任一一只手腕高于肩部时,停止人体关键点检测,同时在该人体框内进行手部检测,根据人体关键点匹配左右手,将裁剪的手部图像传入手部关键点检测;该阶段为过渡阶段,耗时约 20ms,将人体关键点检测转换为手部关键点检测,同时输出该帧的头部位置与大小;
- 启动多线程检测手部关键点(最多 2 只手),识别手势,同时预测下一帧的手部位置;只要手部检测的置信度足够高,会一直保持在该阶段,该阶段资源消耗较高,实时性较强(约 29ms),对于 30FPS 的摄像头而言,几乎可以跑满帧率;快速移动手掌或遮挡双手,或双手都丢失预测位置,自动回到人体关键点检测流程。
在常规时间中(生活中的绝大多数时间),系统保持较低的功耗运行。需要手势控制时,又能快速切换模式,并实时地检测。设计切换信号(手腕高于肩部)可以避免误触发。基本满足了全天候运行、使用流畅的需求。小目标的检测往往是具有挑战的,通过人体框的裁剪,采用 Top-Down 模式,实现最远 5 米的手势识别,基本可以覆盖屋内的环境了。
模型选择方面,个人水平有限,考虑到时间成本,贯彻了「拿来主义」,搜刮了一些优秀的模型。但在过程中,转换为 3588 NPU 的模型格式时,又出现了各种问题,包括 rknn 的各种 bug,算子不支持等。后续会根据需要更换模型。当前版本的模型选择如下:
- 多人人体关键点检测:Yolo v8 pose (nano),转换为 rknpu 模型 rknn,使用 INT8 量化,追求较高的速度而牺牲精度;
- 手部与头部检测:Gold Yolo (nano),fp16,输出的手部位置为第一帧位置,要求较高的精度;
- 手部关键点检测:RTMPose (m),fp16,同时要求较高精度和较高速度。
空间位置估计
为了获取目标在空间中的位置,需要先估算深度,即目标距相机的距离(光轴方向)。常用的深度估计方法主要包括双目视觉深度估计和单目视觉深度估计。双目视觉使用两个相机,类似于人类的双眼。通过捕捉从两个不同视角的图像,利用视差来估计物体的深度信息。单目视觉的挑战在于,单个相机只能提供二维图像,没有直接的深度信息。因此,需要借助一些额外的信息来推断深度,如通过训练深度神经网络,利用标注数据来学习从单目图像中估计深度;利用物体的已知尺寸、平行线的消失点、阴影等几何特征来推断深度;通过分析物体在多帧图像中的移动情况来估计深度。
双目相机比单目相机贵得多,我想控制住预算,同时,我们只要十分粗略的空间位置,对精度要求很低。通过实验发现,在多数生活情况下,Gold Yolo 识别出的头部检测框宽度相对稳定(即使头部发生旋转),因此我们使用头部的宽度信息来标定人体距离相机的深度。获取头部宽度像素数,进而得到人体深度值,与相机的视角、拍摄像素计算,可以得到 (x, y, z) 坐标,组合得到每个手掌的 (x, y, z) 坐标值。该值为相机局部坐标系下的坐标,通过坐标变换,转换为全局坐标系下的坐标值:只需预设相机 x 轴和 y 轴在全局坐标系中的向量、相机原点在全局坐标系中的坐标值即可,分别为「vector_x」「vector_y」和「vector_o」。
手势控制信号
获取手部的 21 个关键点坐标后,通过计算关节点的弯曲角度得到每个手指的弯曲状态(弯曲或伸直),组合每个手指的弯曲状态,定义了 5 种手势:「握拳」/「fist」,「一」/「one」,「二」/「two」,「三」/「ok」,「五」/「open」。由于拇指非常容易被遮挡,其弯曲状态容易误判,因此在这 5 种手势的判断过程中,拇指的状态不纳入考虑,即「四指张开」=「五指张开」。
使用手掌的关键点(0、5、9、13号关键点),计算得到手掌的「旋转角度」/「rotation」。从人的视角看,五指竖直向上为 0,逆时针为正,顺时针为负,单位为弧度,范围(-pi,pi)。
记录每一次检测的食指指尖坐标,获得手指移动轨迹,用线性回归拟合趋势线,根据纵横比和趋势线系数判断手势,分类为 4 种轨迹「上滑」/「UP」,「下滑」/「DOWN」,「左滑」/「LEFT」,「右滑」/「RIGHT」。拟合趋势线前会移除离群点,增加判断的准确性。轨迹判断具有冗余度,允许轨迹有一定倾角。这里只预设了最简单的 4 种轨迹,读者可以根据需要自行编写判断轨迹类别(如顺时针、逆时针)。对于更为复杂的手势,可以参考 hand-gesture-recognition-using-mediapipe 项目,训练轨迹分类模型。
总结下来,单次单手检测具有 5 种手势 4 种轨迹 N 种旋转角度类别,通过左右手的组合以及空间位置的判断,可以获得数千种手势控制信号。
高级手势与智能控制
现在我们已经能获取每一个时刻的手势信号了,即使对检测结果已经进行了初步处理,但是还是会有一些漏检误检。除此之外,我们还要将人类习惯的动态手势转译为控制信号。这里参考 GitHub - geaxgx/depthai_hand_tracker) 构建了一个 HandController 类。
HandController 基于事件循环,提前在配置中定义一个手势动作列表。手势动作将手势(例如,右手的 OPEN 姿势)与回调(识别手势时调用的函数)相关联。根据回调的运行时间和频率设置了 4 个模式:
- enter(默认):当手势开始时,触发一次事件;
- enter_leave:触发两个事件,一个是手势开始时,一个是手势结束时;
- period:只要手势保持不变,事件就会定期触发;
- continuous:每一帧都会触发事件。
同时引入以下三个参数:
- first_trigger_delay:由于手势识别中会出现误报,我们通常不希望在识别姿势的第一帧上触发事件。以秒为单位的「first_trigger_delay」指定手势在触发相应事件之前必须保持多长时间;
- next_trigger_delay:当模式为 period 时,两个连续事件之间的延迟(以秒为单位);
- max_missing_frames:由于模型并不完美,因此会出现假阴性。比如,手在 20 帧中保持着「ONE」手势,但模型在 20 帧中的几帧中没有识别出「ONE」姿势。max_missing_frames 是允许丢失的帧数的最大值,超出该值,当前手势结束。
下面举几个例子说明使用方法,更多高级手势可以由读者根据手势控制信号和 4 种控制模式自由组合而成(如单手单手势+多轨迹的组合,单手 0 多手势+时序组合判断等)。
- 挥手下一个电视节目。采用 enter_leave 模式,基础手势为「OPEN」,触发两个事件,判断手势开始时的「rotation」是否在 (-1.5, -0.3) 内,手势结束时的「rotation」是否在 (0.3, 1.5) 内,max_missing_frames 设置为较大值 10,即允许挥动过程中的较长时间的漏检误检。结束的时候握拳,即结束该手势信号,如果想中断该信号,则将手旋转至区间外结束手势即可。
- 连续调节电视音量。采用 period 模式,基础手势为左手「TWO」,右手「ONE」,手向左倾持续减小音量,向右倾则持续增加音量。
- 「Blink」控制。采用 enter 模式,基础手势为左手「OPEN」,通过累计触发次数来判断是否发出信号,累计次数为 2,最短间隔为 40 帧,超时自动重置。作为对比,我们首先慢速切换手势,无信号发出;快速「Blink」,信号发出。
家居的智能控制比较简单,可以直接在回调函数中调用相关 API 进行控制,如此例中的 Sony Bravia 电视5。也可以通过 Node-Red、n8n、Home Assistant 中的无代码组件设置触发流程。
代码示例中,提供了一个 Flask API 服务,调用可以暂停或恢复服务,以降低能耗或在特定情况下避免误触发:如人离家时,通过小米无线按钮,暂停服务;如人在家健身时,肢体变化较大,可能造成控制信号的误触发,需要暂停服务。当然,还有更多的切换方式,如通过人体存在传感器切换服务,或使用其它智能语音切换服务,或通过特定手势暂停服务,或通过无检测阈值暂停服务等等。
展望
项目源码托管在 Github。考虑到项目的独立性,同时时间有限,有很多想法没融入到该项目中,欢迎大家一起讨论。
- 如果需要更高精度的人体定位,贵的方法就是用双目相机,便宜的方案我觉得可以加一个毫米波雷达定位人体。但毫米波雷达方案和单目方案都有一个缺陷:手部深度默认与人体是一致的(即处于同一个平面),比如我需要在地面(xz平面)画一个区域让机器人清扫,则无法实现;
- 也许可以通过3D人体姿态来重定位手部位置;我们在手部检测阶段同时检测了头部,如果加一个头部的姿态估计模型获取人脸朝向,可能可以做到"指哪打哪"、"看哪打哪";6人体关键点信息也可成为控制信号的构成;
- 边缘设备的安装。新家做了无主灯,每个房间都有可供电的48V轨道,我计划在轨道上做一个20W转接口,将香橙派上墙,更加美观,俯拍可以更好地避免遮挡和逆光;
- 该项目的资源占用不算高,人体检测约NPU单核12%,手势检测约NPU3核32%,感觉还有资源可以在多个房间同时部署,视频通过rtsp串流到同一个香橙派上,当然串流会带来更大的延迟;追求极致的性能需用C++重构。
- 依赖光学图像进行识别,在低照度下,有些相机会提高曝光时间、降低帧率,可能会影响体验,同时,极低照度环境功能失效。