Initial snapshot: hand motion pipeline (Dyn-HaMR + dex-retargeting + SPIDER)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# 开发示例:给猫咪戴上太阳镜
|
||||
|
||||
## 设计思路
|
||||
|
||||
在动手之前,我们先考虑如何实现这个功能:
|
||||
|
||||
- 首先,要做目标检测,找到图像中的猫咪
|
||||
- 接着,要估计猫咪的关键点位置,比如左右眼的位置
|
||||
- 最后,把太阳镜素材图片贴在合适的位置,TA-DA!
|
||||
|
||||
按照这个思路,下面我们来看如何一步一步实现它。
|
||||
|
||||
## Step 1:从一个现成的 Config 开始
|
||||
|
||||
在 WebcamAPI 中,已经添加了一些实现常用功能的 Node,并提供了对应的 config 示例。利用这些可以减少用户的开发量。例如,我们可以以上面的姿态估计 demo 为基础。它的 config 位于 `tools/webcam/configs/example/pose_estimation.py`。为了更直观,我们把这个 config 中的功能节点表示成以下流程图:
|
||||
|
||||
<div align=center>
|
||||
<img src="https://user-images.githubusercontent.com/15977946/153801397-640f2b45-64e7-41b3-8b00-670c16c57df5.png">
|
||||
<figcaption>Pose Estimation Config 示意</figcaption>
|
||||
</div>
|
||||
|
||||
可以看到,这个 config 已经实现了我们设计思路中“1-目标检测”和“2-关键点检测”的功能。我们还需要实现“3-贴素材图”功能,这就需要定义一个新的 Node了。
|
||||
|
||||
## Step 2:实现一个新 Node
|
||||
|
||||
在 WebcamAPI 我们定义了以下 2 个 Node 基类:
|
||||
|
||||
1. Node:所有 node 的基类,实现了初始化,绑定 runner,启动运行,数据输入输出等基本功能。子类通过重写抽象方法`process()`方法定义具体的 node 功能。
|
||||
2. FrameDrawingNode:用来绘制图像的 node 基类。FrameDrawingNode继承自 Node 并进一步封装了`process()`方法,提供了抽象方法`draw()`供子类实现具体的图像绘制功能。
|
||||
|
||||
显然,“贴素材图”这个功能属于图像绘制,因此我们只需要继承 BaseFrameEffectNode 类即可。具体实现如下:
|
||||
|
||||
```python
|
||||
# 假设该文件路径为
|
||||
# <MMPose Root>/tools/webcam/webcam_apis/nodes/sunglasses_node.py
|
||||
from mmpose.core import apply_sunglasses_effect
|
||||
from ..utils import (load_image_from_disk_or_url,
|
||||
get_eye_keypoint_ids)
|
||||
from .frame_drawing_node import FrameDrawingNode
|
||||
from .builder import NODES
|
||||
|
||||
@NODES.register_module() # 将 SunglassesNode 注册到 NODES(Registry)
|
||||
class SunglassesNode(FrameDrawingNode):
|
||||
|
||||
def __init__(self,
|
||||
name: str,
|
||||
frame_buffer: str,
|
||||
output_buffer: Union[str, List[str]],
|
||||
enable_key: Optional[Union[str, int]] = None,
|
||||
enable: bool = True,
|
||||
src_img_path: Optional[str] = None):
|
||||
|
||||
super().__init__(name, frame_buffer, output_buffer, enable_key, enable)
|
||||
|
||||
# 加载素材图片
|
||||
if src_img_path is None:
|
||||
# The image attributes to:
|
||||
# https://www.vecteezy.com/free-vector/glass
|
||||
# Glass Vectors by Vecteezy
|
||||
src_img_path = ('https://raw.githubusercontent.com/open-mmlab/'
|
||||
'mmpose/master/demo/resources/sunglasses.jpg')
|
||||
self.src_img = load_image_from_disk_or_url(src_img_path)
|
||||
|
||||
def draw(self, frame_msg):
|
||||
# 获取当前帧图像
|
||||
canvas = frame_msg.get_image()
|
||||
# 获取姿态估计结果
|
||||
pose_results = frame_msg.get_pose_results()
|
||||
if not pose_results:
|
||||
return canvas
|
||||
|
||||
# 给每个目标添加太阳镜效果
|
||||
for pose_result in pose_results:
|
||||
model_cfg = pose_result['model_cfg']
|
||||
preds = pose_result['preds']
|
||||
# 获取目标左、右眼关键点位置
|
||||
left_eye_idx, right_eye_idx = get_eye_keypoint_ids(model_cfg)
|
||||
# 根据双眼位置,绘制太阳镜
|
||||
canvas = apply_sunglasses_effect(canvas, preds, self.src_img,
|
||||
left_eye_idx, right_eye_idx)
|
||||
return canvas
|
||||
```
|
||||
|
||||
这里对代码实现中用到的一些函数和类稍作说明:
|
||||
|
||||
1. `NODES`:是一个 mmcv.Registry 实例。相信用过 OpenMMLab 系列的同学都对 Registry 不陌生。这里用 NODES来注册和管理所有的 node 类,从而让用户可以在 config 中通过类的名称(如 "DetectorNode","SunglassesNode" 等)来指定使用对应的 node。
|
||||
2. `load_image_from_disk_or_url`:用来从本地路径或 url 读取图片
|
||||
3. `get_eye_keypoint_ids`:根据模型配置文件(model_cfg)中记录的数据集信息,返回双眼关键点的索引。如 COCO 格式对应的左右眼索引为 $(1,2)$
|
||||
4. `apply_sunglasses_effect`:将太阳镜绘制到原图中的合适位置,具体步骤为:
|
||||
- 在素材图片上定义一组源锚点 $(s_1, s_2, s_3, s_4)$
|
||||
- 根据目标左右眼关键点位置 $(k_1, k_2)$,计算目标锚点 $(t_1, t_2, t_3, t_4)$
|
||||
- 通过源锚点和目标锚点,计算几何变换矩阵(平移,缩放,旋转),将素材图片做变换后贴入原图片。即可将太阳镜绘制在合适的位置。
|
||||
|
||||
<div align=center>
|
||||
<img src="https://user-images.githubusercontent.com/15977946/153773612-bcf86b91-31a3-47b5-886d-e33577016f85.png">
|
||||
<figcaption>太阳镜特效原理示意</figcaption>
|
||||
</div>
|
||||
|
||||
### Get Advanced:关于 Node 和 FrameEffectNode
|
||||
|
||||
[Node 类](/tools/webcam/webcam_apis/nodes/node.py) :继承自 Thread 类。正如我们在前面 数据流 部分提到的,所有节点都在各自的线程中彼此异步运行。在`Node.run()` 方法中定义了节点的基本运行逻辑:
|
||||
|
||||
1. 当 buffer 中有数据时,会触发一次运行
|
||||
2. 调用`process()`来执行具体的功能。`process()`是一个抽象接口,由子类具体实现
|
||||
- 特别地,如果节点需要实现“开/关”功能,则还需要实现`bypass()`方法,以定义节点“关”时的行为。`bypass()`与`process()`的输入输出接口完全相同。在run()中会根据`Node.enable`的状态,调用`process()`或`bypass()`
|
||||
3. 将运行结果发送到输出 buffer
|
||||
|
||||
在继承 Node 类实现具体的节点类时,通常需要完成以下工作:
|
||||
|
||||
1. 在__init__()中注册输入、输出 buffer,并调用基类的__init__()方法
|
||||
2. 实现process()和bypass()(如需要)方法
|
||||
|
||||
[FrameDrawingNode 类](tools/webcam/webcam_apis/nodes/frame_drawing_node.py) :继承自 Node 类,对`process()`和`bypass()`方法做了进一步封装:
|
||||
|
||||
- process():从接到输入中提取帧图像,传入draw()方法中绘图。draw()是一个抽象接口,有子类实现
|
||||
- bypass():直接将节点输入返回
|
||||
|
||||
### Get Advanced: 关于节点的输入、输出格式
|
||||
|
||||
我们定义了[FrameMessage 类](tools/webcam/webcam_apis/utils/message.py)作为节点间通信的数据结构。也就是说,通常情况下节点的输入、输出和 buffer 中存储的元素,都是 FrameMessage 类的实例。FrameMessage 通常用来存储视频中1帧的信息,它提供了简单的接口,用来提取和存入数据:
|
||||
|
||||
- `get_image()`:返回图像
|
||||
- `set_image()`:设置图像
|
||||
- `add_detection_result()`:添加一个目标检测模型的结果
|
||||
- `get_detection_results()`:返回所有目标检测结果
|
||||
- `add_pose_result()`:添加一个姿态估计模型的结果
|
||||
- `get_pose_results()`:返回所有姿态估计结果
|
||||
|
||||
## Step 3:调整 Config
|
||||
|
||||
有了 Step 2 中实现的 SunglassesNode,我们只要把它加入 config 里就可以使用了。比如,我们可以把它放在“Visualizer” node 之后:
|
||||
|
||||
<div align=center>
|
||||
<img src="https://user-images.githubusercontent.com/15977946/153801499-590a7810-b231-4a38-8053-c7d33af1535a.png">
|
||||
<figcaption>修改后的 Config,添加了 SunglassesNode 节点</figcaption>
|
||||
</div>
|
||||
|
||||
具体的写法如下:
|
||||
|
||||
```python
|
||||
runner = dict(
|
||||
# runner的基本参数
|
||||
name='Everybody Wears Sunglasses',
|
||||
camera_id=0,
|
||||
camera_fps=20,
|
||||
# 定义了若干节点(node)
|
||||
nodes=[
|
||||
...,
|
||||
dict(
|
||||
type='SunglassesNode', # 节点类名称
|
||||
name='Sunglasses', # 节点名,由用户自己定义
|
||||
frame_buffer='vis', # 输入
|
||||
output_buffer='sunglasses', # 输出
|
||||
enable_key='s', # 定义开关快捷键
|
||||
enable=True,) # 启动时默认的开关状态
|
||||
...] # 更多节点
|
||||
)
|
||||
```
|
||||
|
||||
此外,用户还可以根据需求调整 config 中的参数。一些常用的设置包括:
|
||||
|
||||
1. 选择摄像头:可以通过设置camera_id参数指定使用的摄像头。通常电脑上的默认摄像头 id 为 0,如果有多个则 id 数字依次增大。此外,也可以给camera_id设置一个本地视频文件的路径,从而使用该视频文件作为应用程序的输入
|
||||
2. 选择模型:可以通过模型推理节点(如 DetectorNode,TopDownPoseEstimationNode)的model_config和model_checkpoint参数来配置。用户可以根据自己的需求(如目标物体类别,关键点类别等)和硬件情况选用合适的模型
|
||||
3. 设置快捷键:一些 node 支持使用快捷键开关,用户可以设置对应的enable_key(快捷键)和enable(默认开关状态)参数
|
||||
4. 提示信息:通过设置 NoticeBoardNode 的 content_lines参数,可以在程序运行时在画面上显示提示信息,帮助使用者快速了解这个应用程序的功能和操作方法
|
||||
|
||||
最后,将修改过的 config 存到文件`tools/webcam/configs/sunglasses.py`中,就可以运行了:
|
||||
|
||||
```shell
|
||||
python tools/webcam/run_webcam.py --config tools/webcam/configs/sunglasses.py
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
# MMPose Webcam API 快速上手
|
||||
|
||||
## 什么是 MMPose Webcam API
|
||||
|
||||
MMPose WebcamAPI 是一套简单的应用开发接口,可以帮助用户方便的调用 MMPose 以及其他 OpenMMLab 算法库中的算法,实现基于摄像头输入视频的交互式应用。
|
||||
|
||||
<div align=center>
|
||||
<img src="https://user-images.githubusercontent.com/15977946/153800450-2522efe8-bc11-457d-9037-d8aee4fc4f36.png">
|
||||
<figcaption>MMPose Webcam API 框架概览</figcaption>
|
||||
</div>
|
||||
|
||||
## 运行一个 Demo
|
||||
|
||||
我们将从一个简单的 Demo 开始,向您介绍 MMPose WebcamAPI 的功能和特性,并详细展示如何基于这个 API 搭建自己的应用。为了使用 MMPose WebcamAPI,您只需要做简单的准备:
|
||||
|
||||
1. 一台计算机(最好有 GPU 和 CUDA 环境,但这并不是必须的)
|
||||
1. 一个摄像头。计算机自带摄像头或者外接 USB 摄像头均可
|
||||
1. 安装 MMPose
|
||||
- 在 OpenMMLab [官方仓库](https://github.com/open-mmlab/mmpose) fork MMPose 到自己的 github,并 clone 到本地
|
||||
- 安装 MMPose,只需要按照我们的 [安装文档](https://mmpose.readthedocs.io/zh_CN/latest/install.html) 中的步骤操作即可
|
||||
|
||||
完成准备工作后,请在命令行进入 MMPose 根目录,执行以下指令,即可运行 demo:
|
||||
|
||||
```shell
|
||||
python tools/webcam/run_webcam.py --config tools/webcam/configs/examples/pose_estimation.py
|
||||
```
|
||||
|
||||
这个 demo 实现了目标检测,姿态估计和可视化功能,效果如下:
|
||||
|
||||
<div align=center>
|
||||
<img src="https://user-images.githubusercontent.com/15977946/153772158-a5702193-3d3f-40c8-bd6b-ab186979d1b4.png">
|
||||
<figcaption>Pose Estimation Demo 效果</figcaption>
|
||||
</div>
|
||||
|
||||
## Demo 里面有什么?
|
||||
|
||||
### 从 Config 说起
|
||||
|
||||
成功运行 demo 后,我们来看一下它是怎样工作的。在启动脚本 `tools/webcam/run_webcam.py` 中可以看到,这里的操作很简单:首先读取了一个 config 文件,接着使用 config 构建了一个 runner ,最后调用了 runner 的 `run()` 方法,这样 demo 就开始运行了。
|
||||
|
||||
```python
|
||||
# tools/webcam/run_webcam.py
|
||||
|
||||
def launch():
|
||||
# 读取 config 文件
|
||||
args = parse_args()
|
||||
cfg = mmcv.Config.fromfile(args.config)
|
||||
# 构建 runner(WebcamRunner类的实例)
|
||||
runner = WebcamRunner(**cfg.runner)
|
||||
# 调用 run()方法,启动程序
|
||||
runner.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
launch()
|
||||
```
|
||||
|
||||
我们先不深究 runner 为何物,而是接着看一下这个 config 文件的内容。省略掉细节和注释,可以发现 config 的结构大致包含两部分(如下图所示):
|
||||
|
||||
1. Runner 的基本参数,如 camera_id,camera_fps 等。这部分比較好理解,是一些在读取视频时的必要设置
|
||||
2. 一系列"节点"(Node),每个节点属于特定的类型(type),并有对应的一些参数
|
||||
|
||||
```python
|
||||
runner = dict(
|
||||
# runner的基本参数
|
||||
name='Pose Estimation',
|
||||
camera_id=0,
|
||||
camera_fps=20,
|
||||
# 定义了若干节点(Node)
|
||||
Nodes=[
|
||||
dict(
|
||||
type='DetectorNode', # 节点1类型
|
||||
name='Detector', # 节点1名字
|
||||
input_buffer='_input_', # 节点1数据输入
|
||||
output_buffer='det_result', # 节点1数据输出
|
||||
...), # 节点1其他参数
|
||||
dict(
|
||||
type='TopDownPoseEstimatorNode', # 节点2类型
|
||||
name='Human Pose Estimator', # 节点2名字
|
||||
input_buffer='det_result', # 节点2数据输入
|
||||
output_buffer='pose_result', # 节点2数据输出
|
||||
...), # 节点2参数
|
||||
...] # 更多节点
|
||||
)
|
||||
```
|
||||
|
||||
### 核心概念:Runner 和 Node
|
||||
|
||||
到这里,我们已经引出了 MMPose WebcamAPI 的2个最重要的概念:runner 和 Node,下面做正式介绍:
|
||||
|
||||
- Runner:Runner 类是程序的主体,提供了程序启动的入口runner.run()方法,并负责视频读入,输出显示等功能。此外,runner 中会包含若干个 Node,分别负责在视频帧的处理中执行不同的功能。
|
||||
- Node:Node 类用来定义功能模块,例如模型推理,可视化,特效绘制等都可以通过定义一个对应的 Node 来实现。如上面的 config 例子中,2 个节点的功能分别是做目标检测(Detector)和姿态估计(TopDownPoseEstimator)
|
||||
|
||||
Runner 和 Node 的关系简单来说如下图所示:
|
||||
|
||||
<div align=center>
|
||||
<img src="https://user-images.githubusercontent.com/15977946/153772839-104430bd-de0e-4ee5-bd67-dff4e52d784c.png">
|
||||
<figcaption>Runner 和 Node 逻辑关系示意</figcaption>
|
||||
</div>
|
||||
|
||||
### 数据流
|
||||
|
||||
一个重要的问题是:当一帧视频数据被 runner 读取后,会按照怎样的顺序通过所有的 Node 并最终被输出(显示)呢?
|
||||
答案就是 config 中每个 Node 的输入输出配置。如示例 config 中,可以看到每个 Node 都有`input_buffer`,`output_buffer`等参数,用来定义该节点的输入输出。通过这种连接关系,所有的 Node 构成了一个有向无环图结构,如下图所示:
|
||||
|
||||
<div align=center>
|
||||
<img src="https://user-images.githubusercontent.com/15977946/153772900-9619ae80-3e64-4b40-bc1e-b184405e3d5b.png">
|
||||
<figcaption>数据流示意</figcaption>
|
||||
</div>
|
||||
|
||||
图中的每个 Data Buffer 就是一个用来存放数据的容器。用户不需要关注 buffer 的具体细节,只需要将其简单理解成 Node 输入输出的名字即可。用户在 config 中可以任意定义这些名字,不过要注意有以下几个特殊的名字:
|
||||
|
||||
- _input_:存放 runner 读入的视频帧,用于模型推理
|
||||
- _frame_ :存放 runner 读入的视频帧,用于可视化
|
||||
- _display_:存放经过所以 Node 处理后的结果,用于在屏幕上显示
|
||||
|
||||
当一帧视频数据被 runner 读入后,会被放进 _input_ 和 _frame_ 两个 buffer 中,然后按照 config 中定义的 Node 连接关系依次通过各个 Node ,最终到达 _display_ ,并被 runner 读出显示在屏幕上。
|
||||
|
||||
#### Get Advanced: 关于 buffer
|
||||
|
||||
- Buffer 本质是一个有限长度的队列,在 runner 中会包含一个 BufferManager 实例(见`mmpose/tools/webcam/webcam_apis/buffer.py')来生成和管理所有 buffer。Node 会按照 config 从对应的 buffer 中读出或写入数据。
|
||||
- 当一个 buffer 已满(达到最大长度)时,写入数据的操作通常不会被 block,而是会将 buffer 中已有的最早一条数据“挤出去”。
|
||||
- 为什么有_input_和_frame_两个输入呢?因为有些 Node 的操作较为耗时(如目标检测,姿态估计等需要模型推理的 Node)。为了保证显示的流畅,我们通常用_input_来作为这类耗时较大的操作的输入,而用_frame_来实时绘制可视化的结果。因为各个节点是异步运行的,这样就可以保证可视化的实时和流畅。
|
||||
Reference in New Issue
Block a user