由于公众号文章的推送规则已经改成了不按照发布时间排序,所以为了大家能够准时收到我们的文章推送,请记得猛戳右下角的
「在看」
,
并点击公号主页右上角的“
...
”将
程序员极客实验室
设为星标~这样就不会错过每一篇精彩的推送啦~
为了能安心看几集 Netflix 剧,技术宅奶爸都做了些什么……
长期以来,「奶爸」+「萌娃」一直是一个不被看好的组合,甚至有人说,「父爱如山体滑坡」。不信的话,以下都是证据:
众所周知,人类幼崽似乎是台永动机,在一天 24 小时任何时间段里都有可能向你发难。你能让自己睡个安稳觉的方法看来是在白天消耗他们的精力,因此人们想出了各种各样的方法。
当然,并不是所有的奶爸都这么不靠谱,也有人带起娃来挺正常的,Agustinus Nalwan 就是其中之一。
Agustinus Nalwan 是 Medium 上的一位博主,曾经从事计算机视觉、3D / 动画、游戏开发等方面的工作,目前供职于澳洲最大的汽车交易平台 carsale.com.au。
他有一个两岁半的儿子,名叫 Dexie。Dexie 非常活泼,喜欢动物,尤其是老鹰,经常学老鹰在家里飞来飞去。
孩子的这种举动一般也就引起家长的「哈哈」一笑(有的会拍成短视频发网上),但 Nalwan 可不是一般的家长,他一直在尝试用技术增添带娃的乐趣。去年三月份,Nalwan 就开发过一款具有玩具识别能力的系统,可以和儿子互动并根据他手里的玩具播放相关视频。
这个项目帮他拿到了英伟达「Jetson Project of the Month: Qrio – an interactive AI bot」活动的大奖,奖品是一台英伟达 Jetson AGX Xavier。
这是一种算力不小的开发者套件,曾被京东、美团、菜鸟的无人快递车用作计算核心。鉴于 Jetson AGX Xavier 配置还不错,Nalwan 决定用它来帮儿子做一个新玩具,实现他「展翅高飞」的梦想。
新玩具名叫 Griffin(神话中的狮鹫),最终实现效果是这样的:
这么好的带娃经验当然要分享出来。在最近的一篇博客中,Nalwan 完整地介绍了他打造 Griffin 的完整过程,手头有娃的可以参考一下。
传说中狮身鹰首的 griffin。《哈利波特》中的 Gryffindor 学院意为金色的 griffin。
以下是 Nalwan「从零开始」构建整套姿态识别游戏的历程。
要实现上图中的效果,Griffin 需要具备以下模块:
-
3D 游戏引擎:借助一个用 OpenGL 写成的飞行模拟器生成带有山脉、天空和 Griffin 的 3D 魔幻世界。
-
人体姿态估计:使用 OpenPose 姿态估计模型和 SSD 目标检测模型来持续检测玩家的身体姿态,作为系统的输入,以控制 Griffin。
-
动作映射和手势识别:将身体姿态转化为有意义的动作和手势,如抬起左 / 右翅膀、左右翻滚身体、起飞等。
-
通信系统:使用 socket 将姿态输入送进 3D 游戏引擎。
-
NVIDIA Jetson AGX Xavier:这是一个 GPU 驱动的小型嵌入式设备,用来运行以上所有模块。它可以通过一个简单的 HDMI 接口支持音视频输出。此外,他还有一个以太网接口,方便联网。你甚至可以插入鼠标和键盘在该设备上进行开发和调试,因为它有一个功能齐全的 Ubuntu 18.04 OS。
-
TV(带有 HDMI 输入和内置扬声器):作为游戏引擎的显示器。
-
摄像头:我用的是 Sony IMX327。其实这里只需要 224x224 的图像分辨率,因此也可以选低端一点的摄像头。
-
Blu-Tack:把所有硬件拼接在一起。
Jetson AGX Xavier、 IMX327 摄像头和 Blu Tack。
为了更好地模拟飞行体验,Griffin 系统将以第三人称视角渲染 3D 世界。想象一下在 Griffin 正后方有一个摄像头看着他所看的地方。为什么不用飞行模拟器那样的第一人称视角呢?因为看到鹰的翅膀并同步移动自己的手臂,可以帮助 Dexie 快速学习如何控制这个游戏,并拥有一个更沉浸式的体验。
自行构建 3D 游戏引擎并非易事,可能需要好几周的时间。现在大多数开发者只使用专门的游戏引擎,如 Unity 或 Unreal。但是很遗憾,我找不到可以在 Ubuntu OS/ARM 芯片组上运行的游戏引擎。一种替代方法是找到在 OpenGL 上运行的开源飞行模拟器。这可以保证游戏引擎能在 AGX 上运行,因为它支持 OpenGL ES(OpenGL 的轻量级版本)并且得到硬件加速。如果你不想游戏引擎以龟速运行的话,则这是必要的条件。
幸运的是,我找到了一个满足标准的 C++ 开源飞行模拟器,并做了以下修改:
-
我用基于目标的系统替换了基于按键的飞行控制系统。这样我就可以不时地设置 Griffin 身体的目标旋转角度,之后这一旋转目标将通过手势识别模块自行设置,该模块可以映射 Dexie 胳膊的方向。
-
我增强了静态 3D 模型管理,以支持层级结构。原始的飞机模型是作为一个刚体移动的,它没有移动的身体部位。但是 Griffin 有两个翅膀,需要独立于身体单独运动。为此,我添加了两个翅膀,使之作为身体之上的单独 3D 模型。我可以单独旋转每个翅膀,也可以移动 Griffin 的身体,间接移动两个翅膀。实现该目标的一种恰当方式是构建骨骼动画系统,将身体部位组织为树结构的形式。但是,由于我要处理的身体部位只有三个(身体和两个翅膀),因此我可以选择一种简便的方式。为了编辑鹰和树 3D 模型,我使用了一个免费易用的 3D 编辑工具 Blender。
该模块旨在检测来自摄像头输入的人体姿态。具体而言,我们需要知道左 / 右手肘、左 / 右肩膀、脖子和鼻子的位置,才能驾驭 Griffin 的翅膀和身体,并触发特定的姿势。OpenPose 是一个流行的开源库,并具备大量估计人体姿态、手部姿势和面部特征的 AI 模型。我使用的是人体姿态估计 COCO 模型,以 resnet18 作为骨干特征提取器。该模型可以实时检测 18 个关节点,包括上述我们所需的 6 个点。
这里存在一个大问题:OpenPose 基于 PyTorch 框架构建,在 NVIDIA AGX Xavier 中运行速度很慢(4FPS),因为它无法利用重度优化的 TensorRT 框架。幸运的是,还有一个厉害的工具 torch2trt,它可以自动将 PyTorch 模型移植到 TensorRT 框架中!具体步骤是:安装 OpenPose,将 PyTorch 转换为 TensorRT,下载预训练 resnet18 骨干模型。
为了获取来自摄像头的视频内容,我使用另一个库 Jetcam。只需要四行代码,就可以运行。
这样就得到了可以 100FPS 速度运行的人体姿态估计模块!
经过一些测试后,我发现有时候这个模型会将随机对象错误地识别为关节点(假正例,如下图所示),这会给 Griffin 的动作控制带来麻烦。
使用 Amazon SageMaker JumpStart 构建目标检测模型
解决该问题的一种方式是添加一个辅助 AI 模型,用目标检测模块来提供人体边界框,这样就可以排除掉在边界框以外检测到的人体关节点了。此外,这些边界框还可以帮助在一堆人中识别主要玩家,距离摄像头最近的人应该是主要玩家。
在之前的项目中,我手动训练过 SSDMobileNetV2 目标检测模型。这次我选择使用 Amazon SageMaker JumpStart,只需一键操作就可以从 TensorFlowHub 和 PyTorchHub 部署 AI 模型。这里有 150 多个可选的模型,其中就有经过完全预训练的 SSDMobileNetV2。
从 Amazon SageMaker Studio 中启动 JumpStart。
在 Amazon JumpStart 中选择 SSDMobileNetV2 后,只需一键操作就可以部署模型。有了目标检测模型后,我可以为边界框以外的关节点添加 exclusion logic,这样假正例就会少很多!
该模块对于将人体姿态估计模块检测到的 6 个关节点动作转换为更具意义的输入至关重要。这包括三种直接的动作映射:
-
飞行时的身体转动:用于控制 Griffin 飞行时的方向。身体转动可以通过横轴和左右手肘向量之间的夹角进行计算(下图上)。在飞行时,两只翅膀基于这一转动角度同步移动。选择手肘而不是手腕是为了最大化可见度,因为手腕经常会掉出摄像头视角或被其他身体部位遮挡住。
-
站立时的翅膀旋转:这纯粹是出于美观,为了让游戏更具趣味性,给人一种站立的时候可以单独控制每个翅膀的印象。这通过横轴与肩膀 - 手肘向量之间的夹角进行计算(下图下)。最终的翅膀旋转角度会添加 15 度,以加大翅膀的动作,毕竟人长时间举高胳膊会很累。
现在,我们完成了三个主要组件,只需要将它们粘合在一起就行了。我们需要将姿态估计模块检测到的人体关节点发送至手势识别模块,这个任务比较简单。但是,将动作和姿势映射结果发送至 3D 游戏引擎就不那么简单了,因为游戏引擎是用 C++ 写的。你可能会疑惑为什么不用 Python 构建 3D 游戏引擎,原因在于没有靠谱的方式来使用 Python 访问 OpenGL。此外,即使可能,我也不想花费好几周时间将 C++ 转换为 Python 代码。
此时我需要以最小花销高效地在这二者之间传递信息。对于游戏引擎而言,最小花销是非常重要的因素,输入控制器和动作发生之间出现 100ms 的延迟都会导致玩家失去沉浸式体验。因此,两个单独应用之间的最好通信媒介是 socket。由于这两个应用在同一台计算机内,因此延迟会在 5ms 以内。
在 C++ 中,我们简单地使用 sys/socket 库,而在 Python 中,我们可以使用 socket 框架。从现在开始,我把手势识别和姿态估计模块称作 Python app,该客户端发送五种信息:roll_target、lwing_target、rwing_target、body_height 和 game_state。把 3D 游戏引擎称为 C++ app,充当监听并不断接收上述信息的服务器。
为了将这五种信息 / 变量正确地从 Python 映射到 C++ 上,在发送之前我们需要将其放置在 Python C-like 结构中。
class Payload(Structure):
_fields_ = [(“roll_target”, c_int32),
(“lwing_target”, c_int32),
("rwing_target", c_int32),
("body_height", c_int32),
("game_state", c_int32)]
在 C++ app 中,它们以本机 C 结构形式接收。
typedef struct payload_t
{
int32_t roll_target;
int32_t lwing_target;
int32_t rwing_target;
int32_t body_height;