Initial snapshot: hand motion pipeline (Dyn-HaMR + dex-retargeting + SPIDER)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 14:13:49 +08:00
commit 7e4ef6f98b
7002 changed files with 1452249 additions and 0 deletions
@@ -0,0 +1,28 @@
# MMPose Webcam API
MMPose Webcam API is a handy tool to develop interactive webcam applications with MMPose functions.
<div align=center>
<img src="https://user-images.githubusercontent.com/15977946/153800450-2522efe8-bc11-457d-9037-d8aee4fc4f36.png">
<figcaption>MMPose Webcam API Overview</figcaption>
</div>
## Requirements
* Python >= 3.7.0
* MMPose >= 0.23.0
* MMDetection >= 2.21.0
## Tutorials
* [Get started with MMPose Webcam API (Chinese)](/tools/webcam/docs/get_started_cn.md)
* [Build a Webcam App: A Step-by-step Instruction (Chinese)](/tools/webcam/docs/example_cn.md)
## Examples
* [Pose Estimation](/tools/webcam/configs/examples/): A simple example to estimate and visualize human/animal pose.
* [Eye Effects](/tools/webcam/configs/eyes/): Apply sunglasses and bug-eye effects.
* [Face Swap](/tools/webcam/configs/face_swap/): Everybody gets someone else's face.
* [Meow Dwen Dwen](/tools/webcam/configs/meow_dwen_dwen/): Dress up your cat in Bing Dwen Dwen costume.
* [Super Saiyan](/tools/webcam/configs/supersaiyan/): Super Saiyan transformation!
* [New Year](/tools/webcam/configs/newyear/): Set off some firecrackers to celebrate Chinese New Year.
@@ -0,0 +1,73 @@
# Matting Effects
We can apply background matting to the videos.
## Instruction
### Get started
Launch the demo from the mmpose root directory:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/background/background.py
```
### Hotkeys
| Hotkey | Function |
| -- | -- |
| b | Toggle the background matting effect on/off. |
| h | Show help information. |
| m | Show the monitoring information. |
| q | Exit. |
Note that the demo will automatically save the output video into a file `record.mp4`.
### Configuration
- **Choose a detection model**
Users can choose detection models from the [MMDetection Model Zoo](https://mmdetection.readthedocs.io/en/v2.20.0/model_zoo.html). Just set the `model_config` and `model_checkpoint` in the detector node accordingly, and the model will be automatically downloaded and loaded.
Note that in order to perform background matting, the model should be able to produce segmentation masks.
```python
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/mask_rcnn_r50_fpn_2x_coco.py',
model_checkpoint='https://download.openmmlab.com/'
'mmdetection/v2.0/mask_rcnn/mask_rcnn_r50_fpn_2x_coco/'
'mask_rcnn_r50_fpn_2x_coco_bbox_mAP-0.392'
'__segm_mAP-0.354_20200505_003907-3e542a40.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
```
- **Run the demo without GPU**
If you don't have GPU and CUDA in your device, the demo can run with only CPU by setting `device='cpu'` in all model nodes. For example:
```python
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/mask_rcnn_r50_fpn_2x_coco.py',
model_checkpoint='https://download.openmmlab.com/'
'mmdetection/v2.0/mask_rcnn/mask_rcnn_r50_fpn_2x_coco/'
'mask_rcnn_r50_fpn_2x_coco_bbox_mAP-0.392'
'__segm_mAP-0.354_20200505_003907-3e542a40.pth',
device='cpu',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
```
- **Debug webcam and display**
You can launch the webcam runner with a debug config:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/examples/test_camera.py
```
@@ -0,0 +1,93 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
# Basic configurations of the runner
name='Matting Effects',
camera_id=0,
camera_fps=10,
synchronous=False,
# Define nodes.
# The configuration of a node usually includes:
# 1. 'type': Node class name
# 2. 'name': Node name
# 3. I/O buffers (e.g. 'input_buffer', 'output_buffer'): specify the
# input and output buffer names. This may depend on the node class.
# 4. 'enable_key': assign a hot-key to toggle enable/disable this node.
# This may depend on the node class.
# 5. Other class-specific arguments
nodes=[
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/mask_rcnn_r50_fpn_2x_coco.py',
model_checkpoint='https://download.openmmlab.com/'
'mmdetection/v2.0/mask_rcnn/mask_rcnn_r50_fpn_2x_coco/'
'mask_rcnn_r50_fpn_2x_coco_bbox_mAP-0.392'
'__segm_mAP-0.354_20200505_003907-3e542a40.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
# 'TopDownPoseEstimatorNode':
# This node performs keypoint detection from the frame image using an
# MMPose top-down model. Detection results is needed.
dict(
type='TopDownPoseEstimatorNode',
name='Human Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_mbv3_coco_wholebody_256x192_dark.py',
model_checkpoint='https://openmmlab-share.oss-cn-hangz'
'hou.aliyuncs.com/mmpose/top_down/vipnas/vipnas_mbv3_co'
'co_wholebody_256x192_dark-e2158108_20211205.pth',
cls_names=['person'],
input_buffer='det_result',
output_buffer='human_pose'),
# 'ModelResultBindingNode':
# This node binds the latest model inference result with the current
# frame. (This means the frame image and inference result may be
# asynchronous).
dict(
type='ModelResultBindingNode',
name='ResultBinder',
frame_buffer='_frame_', # `_frame_` is a runner-reserved buffer
result_buffer='human_pose',
output_buffer='frame'),
# 'MattingNode':
# This node draw the matting visualization result in the frame image.
# mask results is needed.
dict(
type='BackgroundNode',
name='Visualizer',
enable_key='b',
enable=True,
frame_buffer='frame',
output_buffer='vis_bg',
cls_names=['person']),
# 'NoticeBoardNode':
# This node show a notice board with given content, e.g. help
# information.
dict(
type='NoticeBoardNode',
name='Helper',
enable_key='h',
frame_buffer='vis_bg',
output_buffer='vis',
content_lines=[
'This is a demo for background changing effects. Have fun!',
'', 'Hot-keys:', '"b": Change background',
'"h": Show help information',
'"m": Show diagnostic information', '"q": Exit'
],
),
# 'MonitorNode':
# This node show diagnostic information in the frame image. It can
# be used for debugging or monitoring system resource status.
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
enable=False,
frame_buffer='vis',
output_buffer='_display_') # `_frame_` is a runner-reserved buffer
])
@@ -0,0 +1,110 @@
# Pose Estimation Demo
This demo performs human bounding box and keypoint detection, and visualizes results.
<div align="center">
<img src="https://user-images.githubusercontent.com/15977946/148911749-57b263c0-1075-4a65-af53-b51fc815da68.gif" width="600px" alt><br>
</div>
## Instruction
### Get started
Launch the demo from the mmpose root directory:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/examples/pose_estimation.py
```
### Hotkeys
| Hotkey | Function |
| -- | -- |
| v | Toggle the pose visualization on/off. |
| h | Show help information. |
| m | Show the monitoring information. |
| q | Exit. |
Note that the demo will automatically save the output video into a file `record.mp4`.
### Configuration
- **Choose a detection model**
Users can choose detection models from the [MMDetection Model Zoo](https://mmdetection.readthedocs.io/en/v2.20.0/model_zoo.html). Just set the `model_config` and `model_checkpoint` in the detector node accordingly, and the model will be automatically downloaded and loaded.
```python
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/'
'ssdlite_mobilenetv2_scratch_600e_coco.py',
model_checkpoint='https://download.openmmlab.com'
'/mmdetection/v2.0/ssd/'
'ssdlite_mobilenetv2_scratch_600e_coco/ssdlite_mobilenetv2_'
'scratch_600e_coco_20210629_110627-974d9307.pth',
input_buffer='_input_',
output_buffer='det_result')
```
- **Choose a or more pose models**
In this demo we use two [top-down](https://github.com/open-mmlab/mmpose/tree/master/configs/body/2d_kpt_sview_rgb_img/topdown_heatmap) pose estimation models for humans and animals respectively. Users can choose models from the [MMPose Model Zoo](https://mmpose.readthedocs.io/en/latest/modelzoo.html). To apply different pose models on different instance types, you can add multiple pose estimator nodes with `cls_names` set accordingly.
```python
# 'TopDownPoseEstimatorNode':
# This node performs keypoint detection from the frame image using an
# MMPose top-down model. Detection results is needed.
dict(
type='TopDownPoseEstimatorNode',
name='Human Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_mbv3_coco_wholebody_256x192_dark.py',
model_checkpoint='https://openmmlab-share.oss-cn-hangz'
'hou.aliyuncs.com/mmpose/top_down/vipnas/vipnas_mbv3_co'
'co_wholebody_256x192_dark-e2158108_20211205.pth',
cls_names=['person'],
input_buffer='det_result',
output_buffer='human_pose'),
dict(
type='TopDownPoseEstimatorNode',
name='Animal Pose Estimator',
model_config='configs/animal/2d_kpt_sview_rgb_img/topdown_heatmap'
'/animalpose/hrnet_w32_animalpose_256x256.py',
model_checkpoint='https://download.openmmlab.com/mmpose/animal/'
'hrnet/hrnet_w32_animalpose_256x256-1aa7f075_20210426.pth',
cls_names=['cat', 'dog', 'horse', 'sheep', 'cow'],
input_buffer='human_pose',
output_buffer='animal_pose')
```
- **Run the demo without GPU**
If you don't have GPU and CUDA in your device, the demo can run with only CPU by setting `device='cpu'` in all model nodes. For example:
```python
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/'
'ssdlite_mobilenetv2_scratch_600e_coco.py',
model_checkpoint='https://download.openmmlab.com'
'/mmdetection/v2.0/ssd/'
'ssdlite_mobilenetv2_scratch_600e_coco/ssdlite_mobilenetv2_'
'scratch_600e_coco_20210629_110627-974d9307.pth',
device='cpu',
input_buffer='_input_',
output_buffer='det_result')
```
- **Debug webcam and display**
You can lanch the webcam runner with a debug config:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/examples/test_camera.py
```
@@ -0,0 +1,115 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
# Basic configurations of the runner
name='Pose Estimation',
camera_id=0,
camera_fps=20,
synchronous=False,
# Define nodes.
# The configuration of a node usually includes:
# 1. 'type': Node class name
# 2. 'name': Node name
# 3. I/O buffers (e.g. 'input_buffer', 'output_buffer'): specify the
# input and output buffer names. This may depend on the node class.
# 4. 'enable_key': assign a hot-key to toggle enable/disable this node.
# This may depend on the node class.
# 5. Other class-specific arguments
nodes=[
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/'
'ssdlite_mobilenetv2_scratch_600e_coco.py',
model_checkpoint='https://download.openmmlab.com'
'/mmdetection/v2.0/ssd/'
'ssdlite_mobilenetv2_scratch_600e_coco/ssdlite_mobilenetv2_'
'scratch_600e_coco_20210629_110627-974d9307.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
# 'TopDownPoseEstimatorNode':
# This node performs keypoint detection from the frame image using an
# MMPose top-down model. Detection results is needed.
dict(
type='TopDownPoseEstimatorNode',
name='Human Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_mbv3_coco_wholebody_256x192_dark.py',
model_checkpoint='https://download.openmmlab.com/mmpose/top_down/'
'vipnas/vipnas_mbv3_coco_wholebody_256x192_dark'
'-e2158108_20211205.pth',
cls_names=['person'],
input_buffer='det_result',
output_buffer='human_pose'),
dict(
type='TopDownPoseEstimatorNode',
name='Animal Pose Estimator',
model_config='configs/animal/2d_kpt_sview_rgb_img/topdown_heatmap'
'/animalpose/hrnet_w32_animalpose_256x256.py',
model_checkpoint='https://download.openmmlab.com/mmpose/animal/'
'hrnet/hrnet_w32_animalpose_256x256-1aa7f075_20210426.pth',
cls_names=['cat', 'dog', 'horse', 'sheep', 'cow'],
input_buffer='human_pose',
output_buffer='animal_pose'),
# 'ModelResultBindingNode':
# This node binds the latest model inference result with the current
# frame. (This means the frame image and inference result may be
# asynchronous).
dict(
type='ModelResultBindingNode',
name='ResultBinder',
frame_buffer='_frame_', # `_frame_` is a runner-reserved buffer
result_buffer='animal_pose',
output_buffer='frame'),
# 'PoseVisualizerNode':
# This node draw the pose visualization result in the frame image.
# Pose results is needed.
dict(
type='PoseVisualizerNode',
name='Visualizer',
enable_key='v',
frame_buffer='frame',
output_buffer='vis'),
# 'NoticeBoardNode':
# This node show a notice board with given content, e.g. help
# information.
dict(
type='NoticeBoardNode',
name='Helper',
enable_key='h',
enable=True,
frame_buffer='vis',
output_buffer='vis_notice',
content_lines=[
'This is a demo for pose visualization and simple image '
'effects. Have fun!', '', 'Hot-keys:',
'"v": Pose estimation result visualization',
'"s": Sunglasses effect B-)', '"b": Bug-eye effect 0_0',
'"h": Show help information',
'"m": Show diagnostic information', '"q": Exit'
],
),
# 'MonitorNode':
# This node show diagnostic information in the frame image. It can
# be used for debugging or monitoring system resource status.
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
enable=False,
frame_buffer='vis_notice',
output_buffer='display'),
# 'RecorderNode':
# This node save the output video into a file.
dict(
type='RecorderNode',
name='Recorder',
out_video_file='record.mp4',
frame_buffer='display',
output_buffer='_display_'
# `_display_` is a runner-reserved buffer
)
])
@@ -0,0 +1,19 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
name='Debug CamRunner',
camera_id=0,
camera_fps=20,
nodes=[
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
frame_buffer='_frame_',
output_buffer='display'),
dict(
type='RecorderNode',
name='Recorder',
out_video_file='webcam_output.mp4',
frame_buffer='display',
output_buffer='_display_')
])
@@ -0,0 +1,31 @@
# Sunglasses and Bug-eye Effects
We can apply fun effects on videos with pose estimation results, like adding sunglasses on the face, or make the eyes look bigger.
<div align="center">
<img src="https://user-images.githubusercontent.com/15977946/124059525-ce20c580-da5d-11eb-8e4a-2d96cd31fe9f.gif" width="600px" alt><br>
</div>
## Instruction
### Get started
Launch the demo from the mmpose root directory:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/examples/pose_estimation.py
```
### Hotkeys
| Hotkey | Function |
| -- | -- |
| s | Toggle the sunglasses effect on/off. |
| b | Toggle the bug-eye effect on/off. |
| h | Show help information. |
| m | Show the monitoring information. |
| q | Exit. |
### Configuration
See the [README](/tools/webcam/configs/examples/README.md#configuration) of pose estimation demo for model configurations.
@@ -0,0 +1,114 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
# Basic configurations of the runner
name='Eye Effects',
camera_id=0,
camera_fps=20,
synchronous=False,
# Define nodes.
# The configuration of a node usually includes:
# 1. 'type': Node class name
# 2. 'name': Node name
# 3. I/O buffers (e.g. 'input_buffer', 'output_buffer'): specify the
# input and output buffer names. This may depend on the node class.
# 4. 'enable_key': assign a hot-key to toggle enable/disable this node.
# This may depend on the node class.
# 5. Other class-specific arguments
nodes=[
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/'
'ssdlite_mobilenetv2_scratch_600e_coco.py',
model_checkpoint='https://download.openmmlab.com'
'/mmdetection/v2.0/ssd/'
'ssdlite_mobilenetv2_scratch_600e_coco/ssdlite_mobilenetv2_'
'scratch_600e_coco_20210629_110627-974d9307.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
# 'TopDownPoseEstimatorNode':
# This node performs keypoint detection from the frame image using an
# MMPose top-down model. Detection results is needed.
dict(
type='TopDownPoseEstimatorNode',
name='Human Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_mbv3_coco_wholebody_256x192_dark.py',
model_checkpoint='https://openmmlab-share.oss-cn-hangz'
'hou.aliyuncs.com/mmpose/top_down/vipnas/vipnas_mbv3_co'
'co_wholebody_256x192_dark-e2158108_20211205.pth',
cls_names=['person'],
input_buffer='det_result',
output_buffer='human_pose'),
dict(
type='TopDownPoseEstimatorNode',
name='Animal Pose Estimator',
model_config='configs/animal/2d_kpt_sview_rgb_img/topdown_heatmap'
'/animalpose/hrnet_w32_animalpose_256x256.py',
model_checkpoint='https://download.openmmlab.com/mmpose/animal/'
'hrnet/hrnet_w32_animalpose_256x256-1aa7f075_20210426.pth',
cls_names=['cat', 'dog', 'horse', 'sheep', 'cow'],
input_buffer='human_pose',
output_buffer='animal_pose'),
# 'ModelResultBindingNode':
# This node binds the latest model inference result with the current
# frame. (This means the frame image and inference result may be
# asynchronous).
dict(
type='ModelResultBindingNode',
name='ResultBinder',
frame_buffer='_frame_', # `_frame_` is a runner-reserved buffer
result_buffer='animal_pose',
output_buffer='frame'),
# 'SunglassesNode':
# This node draw the sunglasses effect in the frame image.
# Pose results is needed.
dict(
type='SunglassesNode',
name='Visualizer',
enable_key='s',
enable=True,
frame_buffer='frame',
output_buffer='vis_sunglasses'),
# 'BugEyeNode':
# This node draw the bug-eye effetc in the frame image.
# Pose results is needed.
dict(
type='BugEyeNode',
name='Visualizer',
enable_key='b',
enable=False,
frame_buffer='vis_sunglasses',
output_buffer='vis_bugeye'),
# 'NoticeBoardNode':
# This node show a notice board with given content, e.g. help
# information.
dict(
type='NoticeBoardNode',
name='Helper',
enable_key='h',
frame_buffer='vis_bugeye',
output_buffer='vis',
content_lines=[
'This is a demo for pose visualization and simple image '
'effects. Have fun!', '', 'Hot-keys:',
'"s": Sunglasses effect B-)', '"b": Bug-eye effect 0_0',
'"h": Show help information',
'"m": Show diagnostic information', '"q": Exit'
],
),
# 'MonitorNode':
# This node show diagnostic information in the frame image. It can
# be used for debugging or monitoring system resource status.
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
enable=False,
frame_buffer='vis',
output_buffer='_display_') # `_frame_` is a runner-reserved buffer
])
@@ -0,0 +1,31 @@
# Sunglasses and Bug-eye Effects
Look! Where is my face?:eyes: And whose face is it?:laughing:
<div align="center">
<img src="https://user-images.githubusercontent.com/15977946/149705000-8818d55c-9264-409f-b995-77718cc6c822.gif" width="600px" alt><br>
</div>
## Instruction
### Get started
Launch the demo from the mmpose root directory:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/face_swap/face_swap.py
```
### Hotkeys
| Hotkey | Function |
| -- | -- |
| s | Switch between modes <ul><li>Shuffle: Randomly shuffle all faces</li><li>Clone: Choose one face and clone it for everyone</li><li>None: Nothing happens and everyone is safe :)</li></ul>|
| v | Toggle the pose visualization on/off. |
| h | Show help information. |
| m | Show diagnostic information. |
| q | Exit. |
### Configuration
See the [README](/tools/webcam/configs/examples/README.md#configuration) of pose estimation demo for model configurations.
@@ -0,0 +1,79 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
name='FaceSwap',
camera_id=0,
camera_fps=20,
synchronous=False,
nodes=[
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/'
'ssdlite_mobilenetv2_scratch_600e_coco.py',
model_checkpoint='https://download.openmmlab.com'
'/mmdetection/v2.0/ssd/'
'ssdlite_mobilenetv2_scratch_600e_coco/ssdlite_mobilenetv2_'
'scratch_600e_coco_20210629_110627-974d9307.pth',
device='cpu',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
dict(
type='TopDownPoseEstimatorNode',
name='TopDown Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_res50_coco_wholebody_256x192_dark.py',
model_checkpoint='https://openmmlab-share.oss-cn-hangzhou'
'.aliyuncs.com/mmpose/top_down/vipnas/'
'vipnas_res50_wholebody_256x192_dark-67c0ce35_20211112.pth',
device='cpu',
cls_names=['person'],
input_buffer='det_result',
output_buffer='pose_result'),
dict(
type='ModelResultBindingNode',
name='ResultBinder',
frame_buffer='_frame_', # `_frame_` is a runner-reserved buffer
result_buffer='pose_result',
output_buffer='frame'),
dict(
type='FaceSwapNode',
name='FaceSwapper',
mode_key='s',
frame_buffer='frame',
output_buffer='face_swap'),
dict(
type='PoseVisualizerNode',
name='Visualizer',
enable_key='v',
frame_buffer='face_swap',
output_buffer='vis_pose'),
dict(
type='NoticeBoardNode',
name='Help Information',
enable_key='h',
content_lines=[
'Swap your faces! ',
'Hot-keys:',
'"v": Toggle the pose visualization on/off.',
'"s": Switch between modes: Shuffle, Clone and None',
'"h": Show help information',
'"m": Show diagnostic information',
'"q": Exit',
],
frame_buffer='vis_pose',
output_buffer='vis_notice'),
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
enable=False,
frame_buffer='vis_notice',
output_buffer='display'),
dict(
type='RecorderNode',
name='Recorder',
out_video_file='faceswap_output.mp4',
frame_buffer='display',
output_buffer='_display_')
])
@@ -0,0 +1,44 @@
# Meow Dwen Dwen
Do you know [Bing DwenDwen (冰墩墩)](https://en.wikipedia.org/wiki/Bing_Dwen_Dwen_and_Shuey_Rhon_Rhon), the mascot of 2022 Beijing Olympic Games?
<div align="center">
<img src="https://user-images.githubusercontent.com/15977946/152742707-c0c51844-e1d0-42d0-9a12-e369002e082f.jpg" width="224px" alt><br>
</div>
Now you can dress your cat up in this costume and TA-DA! Be prepared for super cute **Meow Dwen Dwen**.
<div align="center">
<img src="https://user-images.githubusercontent.com/15977946/152942243-a17194a2-0fd1-4467-993c-634f6d7966d8.gif" width="300px" alt><br>
</div>
You are a dog fan? Hold on, here comes Woof Dwen Dwen.
<div align="center">
<img src="https://user-images.githubusercontent.com/15977946/152942747-84240692-3944-48a5-b60b-e60bd0a4339c.gif" width="300px" alt><br>
</div>
## Instruction
### Get started
Launch the demo from the mmpose root directory:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/meow_dwen_dwen/meow_dwen_dwen.py
```
### Hotkeys
| Hotkey | Function |
| -- | -- |
| s | Change the background. |
| h | Show help information. |
| m | Show diagnostic information. |
| q | Exit. |
### Configuration
- **Use video input**
As you can see in the config, we set `camera_id` as the path of the input image. You can also set it as a video file path (or url), or a webcam ID number (e.g. `camera_id=0`), to capture the dynamic face from the video input.
@@ -0,0 +1,92 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
# Basic configurations of the runner
name='Little fans of 2022 Beijing Winter Olympics',
# Cat image
camera_id='https://user-images.githubusercontent.com/'
'15977946/152932036-b5554cf8-24cf-40d6-a358-35a106013f11.jpeg',
# Dog image
# camera_id='https://user-images.githubusercontent.com/'
# '15977946/152932051-cd280b35-8066-45a0-8f52-657c8631aaba.jpg',
camera_fps=20,
nodes=[
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/'
'ssdlite_mobilenetv2_scratch_600e_coco.py',
model_checkpoint='https://download.openmmlab.com'
'/mmdetection/v2.0/ssd/'
'ssdlite_mobilenetv2_scratch_600e_coco/ssdlite_mobilenetv2_'
'scratch_600e_coco_20210629_110627-974d9307.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
dict(
type='TopDownPoseEstimatorNode',
name='Animal Pose Estimator',
model_config='configs/animal/2d_kpt_sview_rgb_img/topdown_heatmap'
'/ap10k/hrnet_w32_ap10k_256x256.py',
model_checkpoint='https://download.openmmlab.com/mmpose/animal/'
'hrnet/hrnet_w32_ap10k_256x256-18aac840_20211029.pth',
cls_names=['cat', 'dog'],
input_buffer='det_result',
output_buffer='animal_pose'),
dict(
type='TopDownPoseEstimatorNode',
name='TopDown Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_res50_coco_wholebody_256x192_dark.py',
model_checkpoint='https://openmmlab-share.oss-cn-hangzhou'
'.aliyuncs.com/mmpose/top_down/vipnas/'
'vipnas_res50_wholebody_256x192_dark-67c0ce35_20211112.pth',
device='cpu',
cls_names=['person'],
input_buffer='animal_pose',
output_buffer='human_pose'),
dict(
type='ModelResultBindingNode',
name='ResultBinder',
frame_buffer='_frame_', # `_frame_` is a runner-reserved buffer
result_buffer='human_pose',
output_buffer='frame'),
dict(
type='XDwenDwenNode',
name='XDwenDwen',
mode_key='s',
resource_file='tools/webcam/configs/meow_dwen_dwen/'
'resource-info.json',
out_shape=(480, 480),
frame_buffer='frame',
output_buffer='vis'),
dict(
type='NoticeBoardNode',
name='Helper',
enable_key='h',
enable=False,
frame_buffer='vis',
output_buffer='vis_notice',
content_lines=[
'Let your pet put on a costume of Bing-Dwen-Dwen, '
'the mascot of 2022 Beijing Winter Olympics. Have fun!', '',
'Hot-keys:', '"s": Change the background',
'"h": Show help information',
'"m": Show diagnostic information', '"q": Exit'
],
),
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
enable=False,
frame_buffer='vis_notice',
output_buffer='display'),
dict(
type='RecorderNode',
name='Recorder',
out_video_file='record.mp4',
frame_buffer='display',
output_buffer='_display_'
# `_display_` is a runner-reserved buffer
)
])
@@ -0,0 +1,26 @@
[
{
"id": 1,
"result": "{\"width\":690,\"height\":713,\"valid\":true,\"rotate\":0,\"step_1\":{\"toolName\":\"pointTool\",\"result\":[{\"x\":374.86387434554973,\"y\":262.8020942408377,\"attribute\":\"\",\"valid\":true,\"id\":\"8SK9cVyu\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":2},{\"x\":492.8261780104712,\"y\":285.2,\"attribute\":\"\",\"valid\":true,\"id\":\"qDk54WsI\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":1},{\"x\":430.11204188481673,\"y\":318.0502617801047,\"attribute\":\"\",\"valid\":true,\"id\":\"4H80L7lL\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":3}]},\"step_2\":{\"dataSourceStep\":0,\"toolName\":\"polygonTool\",\"result\":[{\"id\":\"pwUsrf9u\",\"sourceID\":\"\",\"valid\":true,\"textAttribute\":\"\",\"pointList\":[{\"x\":423.3926701570681,\"y\":191.87539267015708},{\"x\":488.3465968586388,\"y\":209.04712041884818},{\"x\":535.3821989528797,\"y\":248.6167539267016},{\"x\":549.5675392670157,\"y\":306.8513089005236},{\"x\":537.6219895287959,\"y\":349.407329842932},{\"x\":510.74450261780106,\"y\":381.51099476439794},{\"x\":480.1340314136126,\"y\":394.9497382198953},{\"x\":411.4471204188482,\"y\":390.47015706806286},{\"x\":355.45235602094243,\"y\":373.29842931937173},{\"x\":306.17696335078534,\"y\":327.00942408376966},{\"x\":294.97801047120424,\"y\":284.45340314136126},{\"x\":306.9235602094241,\"y\":245.6303664921466},{\"x\":333.8010471204189,\"y\":217.25968586387435},{\"x\":370.3842931937173,\"y\":196.35497382198955}],\"attribute\":\"\",\"order\":1}]}}",
"url": "https://user-images.githubusercontent.com/15977946/152742677-35fe8a01-bd06-4a12-a02e-949e7d71f28a.jpg",
"fileName": "bing_dwen_dwen1.jpg"
},
{
"id": 2,
"result": "{\"width\":690,\"height\":659,\"valid\":true,\"rotate\":0,\"step_1\":{\"dataSourceStep\":0,\"toolName\":\"pointTool\",\"result\":[{\"x\":293.2460732984293,\"y\":242.89842931937173,\"attribute\":\"\",\"valid\":true,\"id\":\"KgPs39bY\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":1},{\"x\":170.41675392670155,\"y\":270.50052356020944,\"attribute\":\"\",\"valid\":true,\"id\":\"XwHyoBFU\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":2},{\"x\":224.24083769633506,\"y\":308.45340314136126,\"attribute\":\"\",\"valid\":true,\"id\":\"Qfs4YfuB\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":3}]},\"step_2\":{\"dataSourceStep\":0,\"toolName\":\"polygonTool\",\"result\":[{\"id\":\"ts5jlJxb\",\"sourceID\":\"\",\"valid\":true,\"textAttribute\":\"\",\"pointList\":[{\"x\":178.69738219895285,\"y\":184.93403141361256},{\"x\":204.91937172774865,\"y\":172.5130890052356},{\"x\":252.5329842931937,\"y\":169.0628272251309},{\"x\":295.3162303664921,\"y\":175.27329842931937},{\"x\":333.95916230366487,\"y\":195.2848167539267},{\"x\":360.18115183246067,\"y\":220.1267015706806},{\"x\":376.0523560209424,\"y\":262.909947643979},{\"x\":373.98219895287957,\"y\":296.0324607329843},{\"x\":344.99999999999994,\"y\":335.365445026178},{\"x\":322.22827225130885,\"y\":355.37696335078533},{\"x\":272.544502617801,\"y\":378.1486910994764},{\"x\":221.48062827225127,\"y\":386.42931937172773},{\"x\":187.6680628272251,\"y\":385.7392670157068},{\"x\":158.68586387434553,\"y\":369.1780104712042},{\"x\":137.98429319371724,\"y\":337.43560209424083},{\"x\":127.63350785340312,\"y\":295.34240837696336},{\"x\":131.0837696335078,\"y\":242.89842931937173},{\"x\":147.64502617801045,\"y\":208.3958115183246}],\"attribute\":\"\",\"order\":1}]}}",
"url": "https://user-images.githubusercontent.com/15977946/152742707-c0c51844-e1d0-42d0-9a12-e369002e082f.jpg",
"fileName": "bing_dwen_dwen2.jpg"
},
{
"id": 3,
"result": "{\"width\":690,\"height\":811,\"valid\":true,\"rotate\":0,\"step_1\":{\"dataSourceStep\":0,\"toolName\":\"pointTool\",\"result\":[{\"x\":361.13507853403144,\"y\":300.62198952879584,\"attribute\":\"\",\"valid\":true,\"id\":\"uAtbXtf2\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":1},{\"x\":242.24502617801048,\"y\":317.60628272251313,\"attribute\":\"\",\"valid\":true,\"id\":\"iLtceHMA\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":2},{\"x\":302.5392670157068,\"y\":356.67015706806285,\"attribute\":\"\",\"valid\":true,\"id\":\"n9MTlJ6A\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":3}]},\"step_2\":{\"dataSourceStep\":0,\"toolName\":\"polygonTool\",\"result\":[{\"id\":\"5sTLU5wF\",\"sourceID\":\"\",\"valid\":true,\"textAttribute\":\"\",\"pointList\":[{\"x\":227.80837696335078,\"y\":247.12146596858642},{\"x\":248.18952879581153,\"y\":235.23246073298432},{\"x\":291.4994764397906,\"y\":225.04188481675394},{\"x\":351.7937172774869,\"y\":229.28795811518327},{\"x\":393.40523560209425,\"y\":245.42303664921468},{\"x\":424.8261780104712,\"y\":272.59790575916236},{\"x\":443.5089005235602,\"y\":298.07434554973827},{\"x\":436.7151832460733,\"y\":345.6303664921466},{\"x\":406.1434554973822,\"y\":382.9958115183247},{\"x\":355.1905759162304,\"y\":408.4722513089006},{\"x\":313.57905759162304,\"y\":419.5120418848168},{\"x\":262.6261780104712,\"y\":417.81361256544506},{\"x\":224.41151832460733,\"y\":399.9801047120419},{\"x\":201.48272251308902,\"y\":364.3130890052356},{\"x\":194.68900523560208,\"y\":315.0586387434555},{\"x\":202.33193717277487,\"y\":272.59790575916236}],\"attribute\":\"\",\"order\":1}]}}",
"url": "https://user-images.githubusercontent.com/15977946/152742728-99392ecf-8f5c-46cf-b5c4-fe7fb6b39976.jpg",
"fileName": "bing_dwen_dwen3.jpg"
},
{
"id": 4,
"result": "{\"width\":690,\"height\":690,\"valid\":true,\"rotate\":0,\"step_1\":{\"dataSourceStep\":0,\"toolName\":\"pointTool\",\"result\":[{\"x\":365.9528795811519,\"y\":464.5759162303665,\"attribute\":\"\",\"valid\":true,\"id\":\"IKprTuHS\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":1},{\"x\":470.71727748691103,\"y\":445.06806282722516,\"attribute\":\"\",\"valid\":true,\"id\":\"Z90CWkEI\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":2},{\"x\":410.74869109947645,\"y\":395.2146596858639,\"attribute\":\"\",\"valid\":true,\"id\":\"UWRstKZk\",\"sourceID\":\"\",\"textAttribute\":\"\",\"order\":3}]},\"step_2\":{\"dataSourceStep\":0,\"toolName\":\"polygonTool\",\"result\":[{\"id\":\"C30Pc9Ww\",\"sourceID\":\"\",\"valid\":true,\"textAttribute\":\"\",\"pointList\":[{\"x\":412.91623036649213,\"y\":325.85340314136124},{\"x\":468.5497382198953,\"y\":335.9685863874345},{\"x\":501.78534031413614,\"y\":369.2041884816754},{\"x\":514.0680628272252,\"y\":415.44502617801044},{\"x\":504.67539267015707,\"y\":472.5235602094241},{\"x\":484.44502617801044,\"y\":497.0890052356021},{\"x\":443.26178010471205,\"y\":512.9842931937172},{\"x\":389.7958115183246,\"y\":518.7643979057591},{\"x\":336.32984293193715,\"y\":504.31413612565444},{\"x\":302.3717277486911,\"y\":462.40837696335075},{\"x\":298.0366492146597,\"y\":416.89005235602093},{\"x\":318.26701570680626,\"y\":372.0942408376963},{\"x\":363.0628272251309,\"y\":341.0261780104712}],\"attribute\":\"\",\"order\":1}]}}",
"url": "https://user-images.githubusercontent.com/15977946/152742755-9dc75f89-4156-4103-9c6d-f35f1f409d11.jpg",
"fileName": "bing_dwen_dwen4.jpg"
}
]
@@ -0,0 +1,31 @@
# New Year Hat and Firecracker Effects
This demo provides new year effects with pose estimation results, like adding hat on the head and firecracker in the hands.
<div align="center">
<img src="https://user-images.githubusercontent.com/28900607/149774252-8bff7738-2a54-4480-a31c-0a574e7159cb.gif" width="600px" alt><br>
</div>
## Instruction
### Get started
Launch the demo from the mmpose root directory:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/newyear/new_year.py
```
### Hotkeys
| Hotkey | Function |
| -- | -- |
| t | Toggle the hat effect on/off. |
| f | Toggle the firecracker effect on/off. |
| h | Show help information. |
| m | Show the monitoring information. |
| q | Exit. |
### Configuration
See the [README](/tools/webcam/configs/examples/README.md#configuration) of pose estimation demo for model configurations.
@@ -0,0 +1,122 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
# Basic configurations of the runner
name='Pose Estimation',
camera_id=0,
camera_fps=20,
synchronous=False,
# Define nodes.
# The configuration of a node usually includes:
# 1. 'type': Node class name
# 2. 'name': Node name
# 3. I/O buffers (e.g. 'input_buffer', 'output_buffer'): specify the
# input and output buffer names. This may depend on the node class.
# 4. 'enable_key': assign a hot-key to toggle enable/disable this node.
# This may depend on the node class.
# 5. Other class-specific arguments
nodes=[
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/'
'ssdlite_mobilenetv2_scratch_600e_coco.py',
model_checkpoint='https://download.openmmlab.com'
'/mmdetection/v2.0/ssd/'
'ssdlite_mobilenetv2_scratch_600e_coco/ssdlite_mobilenetv2_'
'scratch_600e_coco_20210629_110627-974d9307.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
# 'TopDownPoseEstimatorNode':
# This node performs keypoint detection from the frame image using an
# MMPose top-down model. Detection results is needed.
dict(
type='TopDownPoseEstimatorNode',
name='Human Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_mbv3_coco_wholebody_256x192_dark.py',
model_checkpoint='https://openmmlab-share.oss-cn-hangz'
'hou.aliyuncs.com/mmpose/top_down/vipnas/vipnas_mbv3_co'
'co_wholebody_256x192_dark-e2158108_20211205.pth',
cls_names=['person'],
input_buffer='det_result',
output_buffer='human_pose'),
dict(
type='TopDownPoseEstimatorNode',
name='Animal Pose Estimator',
model_config='configs/animal/2d_kpt_sview_rgb_img/topdown_heatmap'
'/animalpose/hrnet_w32_animalpose_256x256.py',
model_checkpoint='https://download.openmmlab.com/mmpose/animal/'
'hrnet/hrnet_w32_animalpose_256x256-1aa7f075_20210426.pth',
cls_names=['cat', 'dog', 'horse', 'sheep', 'cow'],
input_buffer='human_pose',
output_buffer='animal_pose'),
# 'ModelResultBindingNode':
# This node binds the latest model inference result with the current
# frame. (This means the frame image and inference result may be
# asynchronous).
dict(
type='ModelResultBindingNode',
name='ResultBinder',
frame_buffer='_frame_', # `_frame_` is a runner-reserved buffer
result_buffer='animal_pose',
output_buffer='frame'),
# 'HatNode':
# This node draw the hat effect in the frame image.
# Pose results is needed.
dict(
type='HatNode',
name='Visualizer',
enable_key='t',
frame_buffer='frame',
output_buffer='vis_hat'),
# 'FirecrackerNode':
# This node draw the firecracker effect in the frame image.
# Pose results is needed.
dict(
type='FirecrackerNode',
name='Visualizer',
enable_key='f',
frame_buffer='vis_hat',
output_buffer='vis_firecracker'),
# 'NoticeBoardNode':
# This node show a notice board with given content, e.g. help
# information.
dict(
type='NoticeBoardNode',
name='Helper',
enable_key='h',
enable=True,
frame_buffer='vis_firecracker',
output_buffer='vis_notice',
content_lines=[
'This is a demo for pose visualization and simple image '
'effects. Have fun!', '', 'Hot-keys:', '"t": Hat effect',
'"f": Firecracker effect', '"h": Show help information',
'"m": Show diagnostic information', '"q": Exit'
],
),
# 'MonitorNode':
# This node show diagnostic information in the frame image. It can
# be used for debugging or monitoring system resource status.
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
enable=False,
frame_buffer='vis_notice',
output_buffer='display'),
# 'RecorderNode':
# This node save the output video into a file.
dict(
type='RecorderNode',
name='Recorder',
out_video_file='record.mp4',
frame_buffer='display',
output_buffer='_display_'
# `_display_` is a runner-reserved buffer
)
])
@@ -0,0 +1,96 @@
# Super Saiyan Effects
We can apply fun effects on videos with pose estimation results, like Super Saiyan transformation.
https://user-images.githubusercontent.com/11788150/150138076-2192079f-068a-4d43-bf27-2f1fd708cabc.mp4
## Instruction
### Get started
Launch the demo from the mmpose root directory:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/supersaiyan/saiyan.py
```
### Hotkeys
| Hotkey | Function |
| -- | -- |
| s | Toggle the Super Saiyan effect on/off. |
| h | Show help information. |
| m | Show the monitoring information. |
| q | Exit. |
Note that the demo will automatically save the output video into a file `record.mp4`.
### Configuration
- **Choose a detection model**
Users can choose detection models from the [MMDetection Model Zoo](https://mmdetection.readthedocs.io/en/v2.20.0/model_zoo.html). Just set the `model_config` and `model_checkpoint` in the detector node accordingly, and the model will be automatically downloaded and loaded.
```python
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/mask_rcnn_r50_fpn_2x_coco.py',
model_checkpoint='https://download.openmmlab.com/'
'mmdetection/v2.0/mask_rcnn/mask_rcnn_r50_fpn_2x_coco/'
'mask_rcnn_r50_fpn_2x_coco_bbox_mAP-0.392'
'__segm_mAP-0.354_20200505_003907-3e542a40.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
```
- **Choose a or more pose models**
In this demo we use two [top-down](https://github.com/open-mmlab/mmpose/tree/master/configs/body/2d_kpt_sview_rgb_img/topdown_heatmap) pose estimation models for humans and animals respectively. Users can choose models from the [MMPose Model Zoo](https://mmpose.readthedocs.io/en/latest/modelzoo.html). To apply different pose models on different instance types, you can add multiple pose estimator nodes with `cls_names` set accordingly.
```python
# 'TopDownPoseEstimatorNode':
# This node performs keypoint detection from the frame image using an
# MMPose top-down model. Detection results is needed.
dict(
type='TopDownPoseEstimatorNode',
name='Human Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_mbv3_coco_wholebody_256x192_dark.py',
model_checkpoint='https://openmmlab-share.oss-cn-hangz'
'hou.aliyuncs.com/mmpose/top_down/vipnas/vipnas_mbv3_co'
'co_wholebody_256x192_dark-e2158108_20211205.pth',
cls_names=['person'],
input_buffer='det_result',
output_buffer='human_pose')
```
- **Run the demo without GPU**
If you don't have GPU and CUDA in your device, the demo can run with only CPU by setting `device='cpu'` in all model nodes. For example:
```python
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/mask_rcnn_r50_fpn_2x_coco.py',
model_checkpoint='https://download.openmmlab.com/'
'mmdetection/v2.0/mask_rcnn/mask_rcnn_r50_fpn_2x_coco/'
'mask_rcnn_r50_fpn_2x_coco_bbox_mAP-0.392'
'__segm_mAP-0.354_20200505_003907-3e542a40.pth',
device='cpu',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
```
- **Debug webcam and display**
You can launch the webcam runner with a debug config:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/examples/test_camera.py
```
@@ -0,0 +1,93 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
# Basic configurations of the runner
name='Super Saiyan Effects',
camera_id=0,
camera_fps=30,
synchronous=False,
# Define nodes.
# The configuration of a node usually includes:
# 1. 'type': Node class name
# 2. 'name': Node name
# 3. I/O buffers (e.g. 'input_buffer', 'output_buffer'): specify the
# input and output buffer names. This may depend on the node class.
# 4. 'enable_key': assign a hot-key to toggle enable/disable this node.
# This may depend on the node class.
# 5. Other class-specific arguments
nodes=[
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/mask_rcnn_r50_fpn_2x_coco.py',
model_checkpoint='https://download.openmmlab.com/'
'mmdetection/v2.0/mask_rcnn/mask_rcnn_r50_fpn_2x_coco/'
'mask_rcnn_r50_fpn_2x_coco_bbox_mAP-0.392'
'__segm_mAP-0.354_20200505_003907-3e542a40.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
# 'TopDownPoseEstimatorNode':
# This node performs keypoint detection from the frame image using an
# MMPose top-down model. Detection results is needed.
dict(
type='TopDownPoseEstimatorNode',
name='Human Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_mbv3_coco_wholebody_256x192_dark.py',
model_checkpoint='https://openmmlab-share.oss-cn-hangz'
'hou.aliyuncs.com/mmpose/top_down/vipnas/vipnas_mbv3_co'
'co_wholebody_256x192_dark-e2158108_20211205.pth',
cls_names=['person'],
input_buffer='det_result',
output_buffer='human_pose'),
# 'ModelResultBindingNode':
# This node binds the latest model inference result with the current
# frame. (This means the frame image and inference result may be
# asynchronous).
dict(
type='ModelResultBindingNode',
name='ResultBinder',
frame_buffer='_frame_', # `_frame_` is a runner-reserved buffer
result_buffer='human_pose',
output_buffer='frame'),
# 'SaiyanNode':
# This node draw the Super Saiyan effect in the frame image.
# Pose results is needed.
dict(
type='SaiyanNode',
name='Visualizer',
enable_key='s',
cls_names=['person'],
enable=True,
frame_buffer='frame',
output_buffer='vis_saiyan'),
# 'NoticeBoardNode':
# This node show a notice board with given content, e.g. help
# information.
dict(
type='NoticeBoardNode',
name='Helper',
enable_key='h',
frame_buffer='vis_saiyan',
output_buffer='vis',
content_lines=[
'This is a demo for super saiyan effects. Have fun!', '',
'Hot-keys:', '"s": Saiyan effect',
'"h": Show help information',
'"m": Show diagnostic information', '"q": Exit'
],
),
# 'MonitorNode':
# This node show diagnostic information in the frame image. It can
# be used for debugging or monitoring system resource status.
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
enable=False,
frame_buffer='vis',
output_buffer='_display_') # `_frame_` is a runner-reserved buffer
])
@@ -0,0 +1,35 @@
# Valentine Magic
Do you want to show your **love** to your beloved one, especially on Valentine's Day? Express it with your pose using MMPose right away and see the Valentine Magic!
Try to pose a hand heart gesture, and see what will happen?
Prefer a blow kiss? Here comes your flying heart~
<div align="center">
<img src="https://user-images.githubusercontent.com/87690686/153837208-8975eda1-17d5-4ee7-8c83-9b535e5ee9d9.gif" width="300px" alt><br>
</div>
## Instruction
### Get started
Launch the demo from the mmpose root directory:
```shell
python tools/webcam/run_webcam.py --config tools/webcam/configs/valentinemagic/valentinemagic.py
```
### Hotkeys
| Hotkey | Function |
| -- | -- |
| l | Toggle the Valentine Magic effect on/off. |
| v | Toggle the pose visualization on/off. |
| h | Show help information. |
| m | Show diagnostic information. |
| q | Exit. |
### Configuration
See the [README](/tools/webcam/configs/examples/README.md#configuration) of pose estimation demo for model configurations.
@@ -0,0 +1,118 @@
# Copyright (c) OpenMMLab. All rights reserved.
runner = dict(
# Basic configurations of the runner
name='Human Pose and Effects',
camera_id=0,
camera_fps=30,
# Define nodes.
#
# The configuration of a node usually includes:
# 1. 'type': Node class name
# 2. 'name': Node name
# 3. I/O buffers (e.g. 'input_buffer', 'output_buffer'): specify the
# input and output buffer names. This may depend on the node class.
# 4. 'enable_key': assign a hot-key to toggle enable/disable this node.
# This may depend on the node class.
# 5. Other class-specific arguments
nodes=[
# 'DetectorNode':
# This node performs object detection from the frame image using an
# MMDetection model.
dict(
type='DetectorNode',
name='Detector',
model_config='demo/mmdetection_cfg/'
'ssdlite_mobilenetv2_scratch_600e_coco.py',
model_checkpoint='https://download.openmmlab.com'
'/mmdetection/v2.0/ssd/'
'ssdlite_mobilenetv2_scratch_600e_coco/ssdlite_mobilenetv2_'
'scratch_600e_coco_20210629_110627-974d9307.pth',
input_buffer='_input_', # `_input_` is a runner-reserved buffer
output_buffer='det_result'),
# 'TopDownPoseEstimatorNode':
# This node performs keypoint detection from the frame image using an
# MMPose top-down model. Detection results is needed.
dict(
type='TopDownPoseEstimatorNode',
name='Human Pose Estimator',
model_config='configs/wholebody/2d_kpt_sview_rgb_img/'
'topdown_heatmap/coco-wholebody/'
'vipnas_mbv3_coco_wholebody_256x192_dark.py',
model_checkpoint='https://download.openmmlab.com/mmpose/top_down/'
'vipnas/vipnas_mbv3_coco_wholebody_256x192_dark'
'-e2158108_20211205.pth',
cls_names=['person'],
input_buffer='det_result',
output_buffer='pose_result'),
# 'ModelResultBindingNode':
# This node binds the latest model inference result with the current
# frame. (This means the frame image and inference result may be
# asynchronous).
dict(
type='ModelResultBindingNode',
name='ResultBinder',
frame_buffer='_frame_', # `_frame_` is a runner-reserved buffer
result_buffer='pose_result',
output_buffer='frame'),
# 'PoseVisualizerNode':
# This node draw the pose visualization result in the frame image.
# Pose results is needed.
dict(
type='PoseVisualizerNode',
name='Visualizer',
enable_key='v',
enable=False,
frame_buffer='frame',
output_buffer='vis'),
# 'ValentineMagicNode':
# This node draw heart in the image.
# It can launch dynamically expanding heart from the middle of
# hands if the persons pose a "hand heart" gesture or blow a kiss.
# Only there are two persons in the image can trigger this effect.
# Pose results is needed.
dict(
type='ValentineMagicNode',
name='Visualizer',
enable_key='l',
frame_buffer='vis',
output_buffer='vis_heart',
),
# 'NoticeBoardNode':
# This node show a notice board with given content, e.g. help
# information.
dict(
type='NoticeBoardNode',
name='Helper',
enable_key='h',
enable=False,
frame_buffer='vis_heart',
output_buffer='vis_notice',
content_lines=[
'This is a demo for pose visualization and simple image '
'effects. Have fun!', '', 'Hot-keys:',
'"h": Show help information', '"l": LoveHeart Effect',
'"v": PoseVisualizer', '"m": Show diagnostic information',
'"q": Exit'
],
),
# 'MonitorNode':
# This node show diagnostic information in the frame image. It can
# be used for debugging or monitoring system resource status.
dict(
type='MonitorNode',
name='Monitor',
enable_key='m',
enable=False,
frame_buffer='vis_notice',
output_buffer='display'), # `_frame_` is a runner-reserved buffer
# 'RecorderNode':
# This node record the frames into a local file. It can save the
# visualiztion results. Uncommit the following lines to turn it on.
dict(
type='RecorderNode',
name='Recorder',
out_video_file='record.mp4',
frame_buffer='display',
output_buffer='_display_')
])
@@ -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_来实时绘制可视化的结果。因为各个节点是异步运行的,这样就可以保证可视化的实时和流畅。
@@ -0,0 +1,38 @@
# Copyright (c) OpenMMLab. All rights reserved.
from argparse import ArgumentParser
from mmcv import Config, DictAction
from webcam_apis import WebcamRunner
def parse_args():
parser = ArgumentParser('Lauch webcam runner')
parser.add_argument(
'--config',
type=str,
default='tools/webcam/configs/meow_dwen_dwen/meow_dwen_dwen.py')
parser.add_argument(
'--cfg-options',
nargs='+',
action=DictAction,
default={},
help='override some settings in the used config, the key-value pair '
'in xxx=yyy format will be merged into config file. For example, '
"'--cfg-options runner.camera_id=1 runner.synchronous=True'")
return parser.parse_args()
def launch():
args = parse_args()
cfg = Config.fromfile(args.config)
cfg.merge_from_dict(args.cfg_options)
runner = WebcamRunner(**cfg.runner)
runner.run()
if __name__ == '__main__':
launch()
@@ -0,0 +1,4 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .webcam_runner import WebcamRunner
__all__ = ['WebcamRunner']
@@ -0,0 +1,18 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .builder import NODES
from .faceswap_node import FaceSwapNode
from .frame_effect_node import (BackgroundNode, BugEyeNode, MoustacheNode,
NoticeBoardNode, PoseVisualizerNode,
SaiyanNode, SunglassesNode)
from .helper_node import ModelResultBindingNode, MonitorNode, RecorderNode
from .mmdet_node import DetectorNode
from .mmpose_node import TopDownPoseEstimatorNode
from .valentinemagic_node import ValentineMagicNode
from .xdwendwen_node import XDwenDwenNode
__all__ = [
'NODES', 'PoseVisualizerNode', 'DetectorNode', 'TopDownPoseEstimatorNode',
'MonitorNode', 'BugEyeNode', 'SunglassesNode', 'ModelResultBindingNode',
'NoticeBoardNode', 'RecorderNode', 'FaceSwapNode', 'MoustacheNode',
'SaiyanNode', 'BackgroundNode', 'XDwenDwenNode', 'ValentineMagicNode'
]
@@ -0,0 +1,4 @@
# Copyright (c) OpenMMLab. All rights reserved.
from mmcv.utils import Registry
NODES = Registry('node')
@@ -0,0 +1,254 @@
# Copyright (c) OpenMMLab. All rights reserved.
from enum import IntEnum
from typing import List, Union
import cv2
import numpy as np
from mmpose.datasets import DatasetInfo
from .builder import NODES
from .frame_drawing_node import FrameDrawingNode
class Mode(IntEnum):
NONE = 0,
SHUFFLE = 1,
CLONE = 2
@NODES.register_module()
class FaceSwapNode(FrameDrawingNode):
def __init__(
self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
mode_key: Union[str, int],
):
super().__init__(name, frame_buffer, output_buffer, enable=True)
self.mode_key = mode_key
self.mode_index = 0
self.register_event(
self.mode_key, is_keyboard=True, handler_func=self.switch_mode)
self.history = dict(mode=None)
self._mode = Mode.SHUFFLE
@property
def mode(self):
return self._mode
def switch_mode(self):
"""Switch modes by updating mode index."""
self._mode = Mode((self._mode + 1) % len(Mode))
def draw(self, frame_msg):
if self.mode == Mode.NONE:
self.history = {'mode': Mode.NONE}
return frame_msg.get_image()
# Init history
if self.history['mode'] != self.mode:
self.history = {'mode': self.mode, 'target_map': {}}
# Merge pose results
pose_preds = self._merge_pose_results(frame_msg.get_pose_results())
num_target = len(pose_preds)
# Show mode
img = frame_msg.get_image()
canvas = img.copy()
if self.mode == Mode.SHUFFLE:
mode_txt = 'Shuffle'
else:
mode_txt = 'Clone'
cv2.putText(canvas, mode_txt, (10, 50), cv2.FONT_HERSHEY_DUPLEX, 0.8,
(255, 126, 0), 1)
# Skip if target number is less than 2
if num_target >= 2:
# Generate new mapping if target number changes
if num_target != len(self.history['target_map']):
if self.mode == Mode.SHUFFLE:
self.history['target_map'] = self._get_swap_map(num_target)
else:
self.history['target_map'] = np.repeat(
np.random.choice(num_target), num_target)
# # Draw on canvas
for tar_idx, src_idx in enumerate(self.history['target_map']):
face_src = self._get_face_info(pose_preds[src_idx])
face_tar = self._get_face_info(pose_preds[tar_idx])
canvas = self._swap_face(img, canvas, face_src, face_tar)
return canvas
def _crop_face_by_contour(self, img, contour):
mask = np.zeros(img.shape[:2], dtype=np.uint8)
cv2.fillPoly(mask, [contour.astype(np.int32)], 1)
mask = cv2.dilate(
mask, kernel=np.ones((9, 9), dtype=np.uint8), anchor=(4, 0))
x1, y1, w, h = cv2.boundingRect(mask)
x2 = x1 + w
y2 = y1 + h
bbox = np.array([x1, y1, x2, y2], dtype=np.int64)
patch = img[y1:y2, x1:x2]
mask = mask[y1:y2, x1:x2]
return bbox, patch, mask
def _swap_face(self, img_src, img_tar, face_src, face_tar):
if face_src['dataset'] == face_tar['dataset']:
# Use full keypoints for face alignment
kpts_src = face_src['contour']
kpts_tar = face_tar['contour']
else:
# Use only common landmarks (eyes and nose) for face alignment if
# source and target have differenet data type
# (e.g. human vs animal)
kpts_src = face_src['landmarks']
kpts_tar = face_tar['landmarks']
# Get everything local
bbox_src, patch_src, mask_src = self._crop_face_by_contour(
img_src, face_src['contour'])
bbox_tar, _, mask_tar = self._crop_face_by_contour(
img_tar, face_tar['contour'])
kpts_src = kpts_src - bbox_src[:2]
kpts_tar = kpts_tar - bbox_tar[:2]
# Compute affine transformation matrix
trans_mat, _ = cv2.estimateAffine2D(
kpts_src.astype(np.float32), kpts_tar.astype(np.float32))
patch_warp = cv2.warpAffine(
patch_src,
trans_mat,
dsize=tuple(bbox_tar[2:] - bbox_tar[:2]),
borderValue=(0, 0, 0))
mask_warp = cv2.warpAffine(
mask_src,
trans_mat,
dsize=tuple(bbox_tar[2:] - bbox_tar[:2]),
borderValue=(0, 0, 0))
# Target mask
mask_tar = mask_tar & mask_warp
mask_tar_soft = cv2.GaussianBlur(mask_tar * 255, (3, 3), 3)
# Blending
center = tuple((0.5 * (bbox_tar[:2] + bbox_tar[2:])).astype(np.int64))
img_tar = cv2.seamlessClone(patch_warp, img_tar, mask_tar_soft, center,
cv2.NORMAL_CLONE)
return img_tar
@staticmethod
def _get_face_info(pose_pred):
keypoints = pose_pred['keypoints'][:, :2]
model_cfg = pose_pred['model_cfg']
dataset_info = DatasetInfo(model_cfg.data.test.dataset_info)
face_info = {
'dataset': dataset_info.dataset_name,
'landmarks': None, # For alignment
'contour': None, # For mask generation
'bbox': None # For image warping
}
# Fall back to hard coded keypoint id
if face_info['dataset'] == 'coco':
face_info['landmarks'] = np.stack([
keypoints[1], # left eye
keypoints[2], # right eye
keypoints[0], # nose
0.5 * (keypoints[5] + keypoints[6]), # neck (shoulder center)
])
elif face_info['dataset'] == 'coco_wholebody':
face_info['landmarks'] = np.stack([
keypoints[1], # left eye
keypoints[2], # right eye
keypoints[0], # nose
keypoints[32], # chin
])
contour_ids = list(range(23, 40)) + list(range(40, 50))[::-1]
face_info['contour'] = keypoints[contour_ids]
elif face_info['dataset'] == 'ap10k':
face_info['landmarks'] = np.stack([
keypoints[0], # left eye
keypoints[1], # right eye
keypoints[2], # nose
keypoints[3], # neck
])
elif face_info['dataset'] == 'animalpose':
face_info['landmarks'] = np.stack([
keypoints[0], # left eye
keypoints[1], # right eye
keypoints[4], # nose
keypoints[5], # throat
])
elif face_info['dataset'] == 'wflw':
face_info['landmarks'] = np.stack([
keypoints[97], # left eye
keypoints[96], # right eye
keypoints[54], # nose
keypoints[16], # chine
])
contour_ids = list(range(33))[::-1] + list(range(33, 38)) + list(
range(42, 47))
face_info['contour'] = keypoints[contour_ids]
else:
raise ValueError('Can not obtain face landmark information'
f'from dataset: {face_info["type"]}')
# Face region
if face_info['contour'] is None:
# Manually defined counter of face region
left_eye, right_eye, nose = face_info['landmarks'][:3]
eye_center = 0.5 * (left_eye + right_eye)
w_vec = right_eye - left_eye
eye_dist = np.linalg.norm(w_vec) + 1e-6
w_vec = w_vec / eye_dist
h_vec = np.array([w_vec[1], -w_vec[0]], dtype=w_vec.dtype)
w = max(0.5 * eye_dist, np.abs(np.dot(nose - eye_center, w_vec)))
h = np.abs(np.dot(nose - eye_center, h_vec))
left_top = eye_center + 1.5 * w * w_vec - 0.5 * h * h_vec
right_top = eye_center - 1.5 * w * w_vec - 0.5 * h * h_vec
left_bottom = eye_center + 1.5 * w * w_vec + 4 * h * h_vec
right_bottom = eye_center - 1.5 * w * w_vec + 4 * h * h_vec
face_info['contour'] = np.stack(
[left_top, right_top, right_bottom, left_bottom])
# Get tight bbox of face region
face_info['bbox'] = np.array([
face_info['contour'][:, 0].min(), face_info['contour'][:, 1].min(),
face_info['contour'][:, 0].max(), face_info['contour'][:, 1].max()
]).astype(np.int64)
return face_info
@staticmethod
def _merge_pose_results(pose_results):
preds = []
if pose_results is not None:
for prefix, pose_result in enumerate(pose_results):
model_cfg = pose_result['model_cfg']
for idx, _pred in enumerate(pose_result['preds']):
pred = _pred.copy()
pred['id'] = f'{prefix}.{_pred.get("track_id", str(idx))}'
pred['model_cfg'] = model_cfg
preds.append(pred)
return preds
@staticmethod
def _get_swap_map(num_target):
ids = np.random.choice(num_target, num_target, replace=False)
target_map = ids[(ids + 1) % num_target]
return target_map
@@ -0,0 +1,65 @@
# Copyright (c) OpenMMLab. All rights reserved.
from abc import abstractmethod
from typing import Dict, List, Optional, Union
import numpy as np
from ..utils import FrameMessage, Message
from .node import Node
class FrameDrawingNode(Node):
"""Base class for Node that draw on single frame images.
Args:
name (str, optional): The node name (also thread name).
frame_buffer (str): The name of the input buffer.
output_buffer (str | list): The name(s) of the output buffer(s).
enable_key (str | int, optional): Set a hot-key to toggle
enable/disable of the node. If an int value is given, it will be
treated as an ascii code of a key. Please note:
1. If enable_key is set, the bypass method need to be
overridden to define the node behavior when disabled
2. Some hot-key has been use for particular use. For example:
'q', 'Q' and 27 are used for quit
Default: None
enable (bool): Default enable/disable status. Default: True.
"""
def __init__(self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
enable: bool = True):
super().__init__(name=name, enable_key=enable_key)
# Register buffers
self.register_input_buffer(frame_buffer, 'frame', essential=True)
self.register_output_buffer(output_buffer)
self._enabled = enable
def process(self, input_msgs: Dict[str, Message]) -> Union[Message, None]:
frame_msg = input_msgs['frame']
img = self.draw(frame_msg)
frame_msg.set_image(img)
return frame_msg
def bypass(self, input_msgs: Dict[str, Message]) -> Union[Message, None]:
return input_msgs['frame']
@abstractmethod
def draw(self, frame_msg: FrameMessage) -> np.ndarray:
"""Draw on the frame image with information from the single frame.
Args:
frame_meg (FrameMessage): The frame to get information from and
draw on.
Returns:
array: The output image
"""
@@ -0,0 +1,917 @@
# Copyright (c) OpenMMLab. All rights reserved.
from typing import Dict, List, Optional, Tuple, Union
import cv2
import numpy as np
from mmcv import color_val
from mmpose.core import (apply_bugeye_effect, apply_sunglasses_effect,
imshow_bboxes, imshow_keypoints)
from mmpose.datasets import DatasetInfo
from ..utils import (FrameMessage, copy_and_paste, expand_and_clamp,
get_cached_file_path, get_eye_keypoint_ids,
get_face_keypoint_ids, get_wrist_keypoint_ids,
load_image_from_disk_or_url, screen_matting)
from .builder import NODES
from .frame_drawing_node import FrameDrawingNode
try:
import psutil
psutil_proc = psutil.Process()
except (ImportError, ModuleNotFoundError):
psutil_proc = None
@NODES.register_module()
class PoseVisualizerNode(FrameDrawingNode):
"""Draw the bbox and keypoint detection results.
Args:
name (str, optional): The node name (also thread name).
frame_buffer (str): The name of the input buffer.
output_buffer (str|list): The name(s) of the output buffer(s).
enable_key (str|int, optional): Set a hot-key to toggle enable/disable
of the node. If an int value is given, it will be treated as an
ascii code of a key. Please note:
1. If enable_key is set, the bypass method need to be
overridden to define the node behavior when disabled
2. Some hot-key has been use for particular use. For example:
'q', 'Q' and 27 are used for quit
Default: None
enable (bool): Default enable/disable status. Default: True.
kpt_thr (float): The threshold of keypoint score. Default: 0.3.
radius (int): The radius of keypoint. Default: 4.
thickness (int): The thickness of skeleton. Default: 2.
bbox_color (str|tuple|dict): If a single color (a str like 'green' or
a tuple like (0, 255, 0)), it will used to draw the bbox.
Optionally, a dict can be given as a map from class labels to
colors.
"""
default_bbox_color = {
'person': (148, 139, 255),
'cat': (255, 255, 0),
'dog': (255, 255, 0),
}
def __init__(self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
enable: bool = True,
kpt_thr: float = 0.3,
radius: int = 4,
thickness: int = 2,
bbox_color: Optional[Union[str, Tuple, Dict]] = None):
super().__init__(name, frame_buffer, output_buffer, enable_key, enable)
self.kpt_thr = kpt_thr
self.radius = radius
self.thickness = thickness
if bbox_color is None:
self.bbox_color = self.default_bbox_color
elif isinstance(bbox_color, dict):
self.bbox_color = {k: color_val(v) for k, v in bbox_color.items()}
else:
self.bbox_color = color_val(bbox_color)
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 frame_msg.get_pose_results():
model_cfg = pose_result['model_cfg']
dataset_info = DatasetInfo(model_cfg.dataset_info)
# Extract bboxes and poses
bbox_preds = []
bbox_labels = []
pose_preds = []
for pred in pose_result['preds']:
if 'bbox' in pred:
bbox_preds.append(pred['bbox'])
bbox_labels.append(pred.get('label', None))
pose_preds.append(pred['keypoints'])
# Get bbox colors
if isinstance(self.bbox_color, dict):
bbox_colors = [
self.bbox_color.get(label, (0, 255, 0))
for label in bbox_labels
]
else:
bbox_labels = self.bbox_color
# Draw bboxes
if bbox_preds:
bboxes = np.vstack(bbox_preds)
imshow_bboxes(
canvas,
bboxes,
labels=bbox_labels,
colors=bbox_colors,
text_color='white',
font_scale=0.5,
show=False)
# Draw poses
if pose_preds:
imshow_keypoints(
canvas,
pose_preds,
skeleton=dataset_info.skeleton,
kpt_score_thr=0.3,
pose_kpt_color=dataset_info.pose_kpt_color,
pose_link_color=dataset_info.pose_link_color,
radius=self.radius,
thickness=self.thickness)
return canvas
@NODES.register_module()
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 = '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
@NODES.register_module()
class SpriteNode(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:
# Sprites of Touhou characters :)
# Come from https://www.deviantart.com/shadowbendy/art/Touhou-rpg-maker-vx-Sprite-1-812746920 # noqa: E501
src_img_path = (
'https://user-images.githubusercontent.com/'
'26739999/151532276-33f968d9-917f-45e3-8a99-ebde60be83bb.png')
self.src_img = load_image_from_disk_or_url(
src_img_path, cv2.IMREAD_UNCHANGED)[:144, :108]
tmp = np.array(np.split(self.src_img, range(36, 144, 36), axis=0))
tmp = np.array(np.split(tmp, range(36, 108, 36), axis=2))
self.sprites = tmp
self.pos = None
self.anime_frame = 0
def apply_sprite_effect(self,
img,
pose_results,
left_hand_index,
right_hand_index,
kpt_thr=0.5):
"""Apply sprite effect.
Args:
img (np.ndarray): Image data.
pose_results (list[dict]): The pose estimation results containing:
- "keypoints" ([K,3]): detection result in [x, y, score]
left_hand_index (int): Keypoint index of left hand
right_hand_index (int): Keypoint index of right hand
kpt_thr (float): The score threshold of required keypoints.
"""
hm, wm = self.sprites.shape[2:4]
# anchor points in the sunglasses mask
if self.pos is None:
self.pos = [img.shape[0] // 2, img.shape[1] // 2]
if len(pose_results) == 0:
return img
kpts = pose_results[0]['keypoints']
if kpts[left_hand_index, 2] < kpt_thr and kpts[right_hand_index,
2] < kpt_thr:
aim = self.pos
else:
kpt_lhand = kpts[left_hand_index, :2][::-1]
kpt_rhand = kpts[right_hand_index, :2][::-1]
def distance(a, b):
return (a[0] - b[0])**2 + (a[1] - b[1])**2
# Go to the nearest hand
if distance(kpt_lhand, self.pos) < distance(kpt_rhand, self.pos):
aim = kpt_lhand
else:
aim = kpt_rhand
pos_thr = 15
if aim[0] < self.pos[0] - pos_thr:
# Go down
sprite = self.sprites[self.anime_frame][3]
self.pos[0] -= 1
elif aim[0] > self.pos[0] + pos_thr:
# Go up
sprite = self.sprites[self.anime_frame][0]
self.pos[0] += 1
elif aim[1] < self.pos[1] - pos_thr:
# Go right
sprite = self.sprites[self.anime_frame][1]
self.pos[1] -= 1
elif aim[1] > self.pos[1] + pos_thr:
# Go left
sprite = self.sprites[self.anime_frame][2]
self.pos[1] += 1
else:
# Stay
self.anime_frame = 0
sprite = self.sprites[self.anime_frame][0]
if self.anime_frame < 2:
self.anime_frame += 1
else:
self.anime_frame = 0
x = self.pos[0] - hm // 2
y = self.pos[1] - wm // 2
x = max(0, min(x, img.shape[0] - hm))
y = max(0, min(y, img.shape[0] - wm))
# Overlay image with transparent
img[x:x + hm, y:y +
wm] = (img[x:x + hm, y:y + wm] * (1 - sprite[:, :, 3:] / 255) +
sprite[:, :, :3] * (sprite[:, :, 3:] / 255)).astype('uint8')
return img
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_hand_idx, right_hand_idx = get_wrist_keypoint_ids(model_cfg) # noqa: E501
left_hand_idx, right_hand_idx = get_eye_keypoint_ids(model_cfg)
canvas = self.apply_sprite_effect(canvas, preds, left_hand_idx,
right_hand_idx)
return canvas
@NODES.register_module()
class BackgroundNode(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,
cls_ids: Optional[List] = None,
cls_names: Optional[List] = None):
super().__init__(name, frame_buffer, output_buffer, enable_key, enable)
self.cls_ids = cls_ids
self.cls_names = cls_names
if src_img_path is None:
src_img_path = 'https://user-images.githubusercontent.com/'\
'11788150/149731957-abd5c908-9c7f-45b2-b7bf-'\
'821ab30c6a3e.jpg'
self.src_img = load_image_from_disk_or_url(src_img_path)
def apply_background_effect(self,
img,
det_results,
background_img,
effect_region=(0.2, 0.2, 0.8, 0.8)):
"""Change background.
Args:
img (np.ndarray): Image data.
det_results (list[dict]): The detection results containing:
- "cls_id" (int): Class index.
- "label" (str): Class label (e.g. 'person').
- "bbox" (ndarray:(5, )): bounding box result
[x, y, w, h, score].
- "mask" (ndarray:(w, h)): instance segmentation result.
background_img (np.ndarray): Background image.
effect_region (tuple(4, )): The region to apply mask,
the coordinates are normalized (x1, y1, x2, y2).
"""
if len(det_results) > 0:
# Choose the one with the highest score.
det_result = det_results[0]
bbox = det_result['bbox']
mask = det_result['mask'].astype(np.uint8)
img = copy_and_paste(img, background_img, mask, bbox,
effect_region)
return img
else:
return background_img
def draw(self, frame_msg):
canvas = frame_msg.get_image()
if canvas.shape != self.src_img.shape:
self.src_img = cv2.resize(self.src_img, canvas.shape[:2])
det_results = frame_msg.get_detection_results()
if not det_results:
return canvas
full_preds = []
for det_result in det_results:
preds = det_result['preds']
if self.cls_ids:
# Filter results by class ID
filtered_preds = [
p for p in preds if p['cls_id'] in self.cls_ids
]
elif self.cls_names:
# Filter results by class name
filtered_preds = [
p for p in preds if p['label'] in self.cls_names
]
else:
filtered_preds = preds
full_preds.extend(filtered_preds)
canvas = self.apply_background_effect(canvas, full_preds, self.src_img)
return canvas
@NODES.register_module()
class SaiyanNode(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,
hair_img_path: Optional[str] = None,
light_video_path: Optional[str] = None,
cls_ids: Optional[List] = None,
cls_names: Optional[List] = None):
super().__init__(name, frame_buffer, output_buffer, enable_key, enable)
self.cls_ids = cls_ids
self.cls_names = cls_names
if hair_img_path is None:
hair_img_path = 'https://user-images.githubusercontent.com/'\
'11788150/149732117-fcd2d804-dc2c-426c-bee7-'\
'94be6146e05c.png'
self.hair_img = load_image_from_disk_or_url(hair_img_path)
if light_video_path is None:
light_video_path = get_cached_file_path(
'https://'
'user-images.githubusercontent.com/11788150/149732080'
'-ea6cfeda-0dc5-4bbb-892a-3831e5580520.mp4')
self.light_video_path = light_video_path
self.light_video = cv2.VideoCapture(self.light_video_path)
def apply_saiyan_effect(self,
img,
pose_results,
saiyan_img,
light_frame,
face_indices,
bbox_thr=0.3,
kpt_thr=0.5):
"""Apply saiyan hair effect.
Args:
img (np.ndarray): Image data.
pose_results (list[dict]): The pose estimation results containing:
- "keypoints" ([K,3]): keypoint detection result
in [x, y, score]
saiyan_img (np.ndarray): Saiyan image with transparent background.
light_frame (np.ndarray): Light image with green screen.
face_indices (int): Keypoint index of the face
kpt_thr (float): The score threshold of required keypoints.
"""
img = img.copy()
im_shape = img.shape
# Apply lightning effects.
light_mask = screen_matting(light_frame, color='green')
# anchor points in the mask
pts_src = np.array(
[
[84, 398], # face kpt 0
[331, 393], # face kpt 16
[84, 145],
[331, 140]
],
dtype=np.float32)
for pose in pose_results:
bbox = pose['bbox']
if bbox[-1] < bbox_thr:
continue
mask_inst = pose['mask']
# cache
fg = img[np.where(mask_inst)]
bbox = expand_and_clamp(bbox[:4], im_shape, s=3.0)
# Apply light effects between fg and bg
img = copy_and_paste(
light_frame,
img,
light_mask,
effect_region=(bbox[0] / im_shape[1], bbox[1] / im_shape[0],
bbox[2] / im_shape[1], bbox[3] / im_shape[0]))
# pop
img[np.where(mask_inst)] = fg
# Apply Saiyan hair effects
kpts = pose['keypoints']
if kpts[face_indices[0], 2] < kpt_thr or kpts[face_indices[16],
2] < kpt_thr:
continue
kpt_0 = kpts[face_indices[0], :2]
kpt_16 = kpts[face_indices[16], :2]
# orthogonal vector
vo = (kpt_0 - kpt_16)[::-1] * [-1, 1]
# anchor points in the image by eye positions
pts_tar = np.vstack([kpt_0, kpt_16, kpt_0 + vo, kpt_16 + vo])
h_mat, _ = cv2.findHomography(pts_src, pts_tar)
patch = cv2.warpPerspective(
saiyan_img,
h_mat,
dsize=(img.shape[1], img.shape[0]),
borderValue=(0, 0, 0))
mask_patch = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
mask_patch = (mask_patch > 1).astype(np.uint8)
img = cv2.copyTo(patch, mask_patch, img)
return img
def draw(self, frame_msg):
canvas = frame_msg.get_image()
det_results = frame_msg.get_detection_results()
if not det_results:
return canvas
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']
face_indices = get_face_keypoint_ids(model_cfg)
ret, frame = self.light_video.read()
if not ret:
self.light_video = cv2.VideoCapture(self.light_video_path)
ret, frame = self.light_video.read()
canvas = self.apply_saiyan_effect(canvas, preds, self.hair_img,
frame, face_indices)
return canvas
@NODES.register_module()
class MoustacheNode(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:
src_img_path = 'https://user-images.githubusercontent.com/'\
'11788150/149732141-3afbab55-252a-428c-b6d8'\
'-0e352f432651.jpeg'
self.src_img = load_image_from_disk_or_url(src_img_path)
def apply_moustache_effect(self,
img,
pose_results,
moustache_img,
face_indices,
kpt_thr=0.5):
"""Apply moustache effect.
Args:
img (np.ndarray): Image data.
pose_results (list[dict]): The pose estimation results containing:
- "keypoints" ([K,3]): keypoint detection result
in [x, y, score]
moustache_img (np.ndarray): Moustache image with white background.
left_eye_index (int): Keypoint index of left eye
right_eye_index (int): Keypoint index of right eye
kpt_thr (float): The score threshold of required keypoints.
"""
hm, wm = moustache_img.shape[:2]
# anchor points in the moustache mask
pts_src = np.array([[1164, 741], [1729, 741], [1164, 1244],
[1729, 1244]],
dtype=np.float32)
for pose in pose_results:
kpts = pose['keypoints']
if kpts[face_indices[32], 2] < kpt_thr \
or kpts[face_indices[34], 2] < kpt_thr \
or kpts[face_indices[61], 2] < kpt_thr \
or kpts[face_indices[63], 2] < kpt_thr:
continue
kpt_32 = kpts[face_indices[32], :2]
kpt_34 = kpts[face_indices[34], :2]
kpt_61 = kpts[face_indices[61], :2]
kpt_63 = kpts[face_indices[63], :2]
# anchor points in the image by eye positions
pts_tar = np.vstack([kpt_32, kpt_34, kpt_61, kpt_63])
h_mat, _ = cv2.findHomography(pts_src, pts_tar)
patch = cv2.warpPerspective(
moustache_img,
h_mat,
dsize=(img.shape[1], img.shape[0]),
borderValue=(255, 255, 255))
# mask the white background area in the patch with a threshold 200
mask = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
mask = (mask < 200).astype(np.uint8)
img = cv2.copyTo(patch, mask, img)
return img
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']
face_indices = get_face_keypoint_ids(model_cfg)
canvas = self.apply_moustache_effect(canvas, preds, self.src_img,
face_indices)
return canvas
@NODES.register_module()
class BugEyeNode(FrameDrawingNode):
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_bugeye_effect(canvas, preds, left_eye_idx,
right_eye_idx)
return canvas
@NODES.register_module()
class NoticeBoardNode(FrameDrawingNode):
default_content_lines = ['This is a notice board!']
def __init__(
self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
enable: bool = True,
content_lines: Optional[List[str]] = None,
x_offset: int = 20,
y_offset: int = 20,
y_delta: int = 15,
text_color: Union[str, Tuple[int, int, int]] = 'black',
background_color: Union[str, Tuple[int, int, int]] = (255, 183, 0),
text_scale: float = 0.4,
):
super().__init__(name, frame_buffer, output_buffer, enable_key, enable)
self.x_offset = x_offset
self.y_offset = y_offset
self.y_delta = y_delta
self.text_color = color_val(text_color)
self.background_color = color_val(background_color)
self.text_scale = text_scale
if content_lines:
self.content_lines = content_lines
else:
self.content_lines = self.default_content_lines
def draw(self, frame_msg: FrameMessage) -> np.ndarray:
img = frame_msg.get_image()
canvas = np.full(img.shape, self.background_color, dtype=img.dtype)
x = self.x_offset
y = self.y_offset
max_len = max([len(line) for line in self.content_lines])
def _put_line(line=''):
nonlocal y
cv2.putText(canvas, line, (x, y), cv2.FONT_HERSHEY_DUPLEX,
self.text_scale, self.text_color, 1)
y += self.y_delta
for line in self.content_lines:
_put_line(line)
x1 = max(0, self.x_offset)
x2 = min(img.shape[1], int(x + max_len * self.text_scale * 20))
y1 = max(0, self.y_offset - self.y_delta)
y2 = min(img.shape[0], y)
src1 = canvas[y1:y2, x1:x2]
src2 = img[y1:y2, x1:x2]
img[y1:y2, x1:x2] = cv2.addWeighted(src1, 0.5, src2, 0.5, 0)
return img
@NODES.register_module()
class HatNode(FrameDrawingNode):
def __init__(self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
src_img_path: Optional[str] = None):
super().__init__(name, frame_buffer, output_buffer, enable_key)
if src_img_path is None:
# The image attributes to:
# http://616pic.com/sucai/1m9i70p52.html
src_img_path = 'https://user-images.githubusercontent.' \
'com/28900607/149766271-2f591c19-9b67-4' \
'd92-8f94-c272396ca141.png'
self.src_img = load_image_from_disk_or_url(src_img_path,
cv2.IMREAD_UNCHANGED)
@staticmethod
def apply_hat_effect(img,
pose_results,
hat_img,
left_eye_index,
right_eye_index,
kpt_thr=0.5):
"""Apply hat effect.
Args:
img (np.ndarray): Image data.
pose_results (list[dict]): The pose estimation results containing:
- "keypoints" ([K,3]): keypoint detection result in
[x, y, score]
hat_img (np.ndarray): Hat image with white alpha channel.
left_eye_index (int): Keypoint index of left eye
right_eye_index (int): Keypoint index of right eye
kpt_thr (float): The score threshold of required keypoints.
"""
img_orig = img.copy()
img = img_orig.copy()
hm, wm = hat_img.shape[:2]
# anchor points in the sunglasses mask
a = 0.3
b = 0.7
pts_src = np.array([[a * wm, a * hm], [a * wm, b * hm],
[b * wm, a * hm], [b * wm, b * hm]],
dtype=np.float32)
for pose in pose_results:
kpts = pose['keypoints']
if kpts[left_eye_index, 2] < kpt_thr or \
kpts[right_eye_index, 2] < kpt_thr:
continue
kpt_leye = kpts[left_eye_index, :2]
kpt_reye = kpts[right_eye_index, :2]
# orthogonal vector to the left-to-right eyes
vo = 0.5 * (kpt_reye - kpt_leye)[::-1] * [-1, 1]
veye = 0.5 * (kpt_reye - kpt_leye)
# anchor points in the image by eye positions
pts_tar = np.vstack([
kpt_reye + 1 * veye + 5 * vo, kpt_reye + 1 * veye + 1 * vo,
kpt_leye - 1 * veye + 5 * vo, kpt_leye - 1 * veye + 1 * vo
])
h_mat, _ = cv2.findHomography(pts_src, pts_tar)
patch = cv2.warpPerspective(
hat_img,
h_mat,
dsize=(img.shape[1], img.shape[0]),
borderValue=(255, 255, 255))
# mask the white background area in the patch with a threshold 200
mask = (patch[:, :, -1] > 128)
patch = patch[:, :, :-1]
mask = mask * (cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY) > 30)
mask = mask.astype(np.uint8)
img = cv2.copyTo(patch, mask, img)
return img
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 = self.apply_hat_effect(canvas, preds, self.src_img,
left_eye_idx, right_eye_idx)
return canvas
@NODES.register_module()
class FirecrackerNode(FrameDrawingNode):
def __init__(self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
src_img_path: Optional[str] = None):
super().__init__(name, frame_buffer, output_buffer, enable_key)
if src_img_path is None:
self.src_img_path = 'https://user-images.githubusercontent' \
'.com/28900607/149766281-6376055c-ed8b' \
'-472b-991f-60e6ae6ee1da.gif'
src_img = cv2.VideoCapture(self.src_img_path)
self.frame_list = []
ret, frame = src_img.read()
while frame is not None:
self.frame_list.append(frame)
ret, frame = src_img.read()
self.num_frames = len(self.frame_list)
self.frame_idx = 0
self.frame_period = 4 # each frame in gif lasts for 4 frames in video
@staticmethod
def apply_firecracker_effect(img,
pose_results,
firecracker_img,
left_wrist_idx,
right_wrist_idx,
kpt_thr=0.5):
"""Apply firecracker effect.
Args:
img (np.ndarray): Image data.
pose_results (list[dict]): The pose estimation results containing:
- "keypoints" ([K,3]): keypoint detection result in
[x, y, score]
firecracker_img (np.ndarray): Firecracker image with white
background.
left_wrist_idx (int): Keypoint index of left wrist
right_wrist_idx (int): Keypoint index of right wrist
kpt_thr (float): The score threshold of required keypoints.
"""
hm, wm = firecracker_img.shape[:2]
# anchor points in the firecracker mask
pts_src = np.array([[0. * wm, 0. * hm], [0. * wm, 1. * hm],
[1. * wm, 0. * hm], [1. * wm, 1. * hm]],
dtype=np.float32)
h, w = img.shape[:2]
h_tar = h / 3
w_tar = h_tar / hm * wm
for pose in pose_results:
kpts = pose['keypoints']
if kpts[left_wrist_idx, 2] > kpt_thr:
kpt_lwrist = kpts[left_wrist_idx, :2]
# anchor points in the image by eye positions
pts_tar = np.vstack([
kpt_lwrist - [w_tar / 2, 0],
kpt_lwrist - [w_tar / 2, -h_tar],
kpt_lwrist + [w_tar / 2, 0],
kpt_lwrist + [w_tar / 2, h_tar]
])
h_mat, _ = cv2.findHomography(pts_src, pts_tar)
patch = cv2.warpPerspective(
firecracker_img,
h_mat,
dsize=(img.shape[1], img.shape[0]),
borderValue=(255, 255, 255))
# mask the white background area in the patch with
# a threshold 200
mask = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
mask = (mask < 240).astype(np.uint8)
img = cv2.copyTo(patch, mask, img)
if kpts[right_wrist_idx, 2] > kpt_thr:
kpt_rwrist = kpts[right_wrist_idx, :2]
# anchor points in the image by eye positions
pts_tar = np.vstack([
kpt_rwrist - [w_tar / 2, 0],
kpt_rwrist - [w_tar / 2, -h_tar],
kpt_rwrist + [w_tar / 2, 0],
kpt_rwrist + [w_tar / 2, h_tar]
])
h_mat, _ = cv2.findHomography(pts_src, pts_tar)
patch = cv2.warpPerspective(
firecracker_img,
h_mat,
dsize=(img.shape[1], img.shape[0]),
borderValue=(255, 255, 255))
# mask the white background area in the patch with
# a threshold 200
mask = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
mask = (mask < 240).astype(np.uint8)
img = cv2.copyTo(patch, mask, img)
return img
def draw(self, frame_msg):
canvas = frame_msg.get_image()
pose_results = frame_msg.get_pose_results()
if not pose_results:
return canvas
frame = self.frame_list[self.frame_idx // self.frame_period]
for pose_result in pose_results:
model_cfg = pose_result['model_cfg']
preds = pose_result['preds']
left_wrist_idx, right_wrist_idx = get_wrist_keypoint_ids(model_cfg)
canvas = self.apply_firecracker_effect(canvas, preds, frame,
left_wrist_idx,
right_wrist_idx)
self.frame_idx = (self.frame_idx + 1) % (
self.num_frames * self.frame_period)
return canvas
@@ -0,0 +1,296 @@
# Copyright (c) OpenMMLab. All rights reserved.
import logging
import time
from queue import Full, Queue
from threading import Thread
from typing import List, Optional, Union
import cv2
import numpy as np
from mmcv import color_val
from mmpose.utils.timer import RunningAverage
from .builder import NODES
from .node import Node
try:
import psutil
psutil_proc = psutil.Process()
except (ImportError, ModuleNotFoundError):
psutil_proc = None
@NODES.register_module()
class ModelResultBindingNode(Node):
def __init__(self, name: str, frame_buffer: str, result_buffer: str,
output_buffer: Union[str, List[str]]):
super().__init__(name=name, enable=True)
self.synchronous = None
# Cache the latest model result
self.last_result_msg = None
self.last_output_msg = None
# Inference speed analysis
self.frame_fps = RunningAverage(window=10)
self.frame_lag = RunningAverage(window=10)
self.result_fps = RunningAverage(window=10)
self.result_lag = RunningAverage(window=10)
# Register buffers
# Note that essential buffers will be set in set_runner() because
# it depends on the runner.synchronous attribute.
self.register_input_buffer(result_buffer, 'result', essential=False)
self.register_input_buffer(frame_buffer, 'frame', essential=False)
self.register_output_buffer(output_buffer)
def set_runner(self, runner):
super().set_runner(runner)
# Set synchronous according to the runner
if runner.synchronous:
self.synchronous = True
essential_input = 'result'
else:
self.synchronous = False
essential_input = 'frame'
# Set essential input buffer according to the synchronous setting
for buffer_info in self._input_buffers:
if buffer_info.input_name == essential_input:
buffer_info.essential = True
def process(self, input_msgs):
result_msg = input_msgs['result']
# Update last result
if result_msg is not None:
# Update result FPS
if self.last_result_msg is not None:
self.result_fps.update(
1.0 /
(result_msg.timestamp - self.last_result_msg.timestamp))
# Update inference latency
self.result_lag.update(time.time() - result_msg.timestamp)
# Update last inference result
self.last_result_msg = result_msg
if not self.synchronous:
# Asynchronous mode: Bind the latest result with the current frame.
frame_msg = input_msgs['frame']
self.frame_lag.update(time.time() - frame_msg.timestamp)
# Bind result to frame
if self.last_result_msg is not None:
frame_msg.set_full_results(
self.last_result_msg.get_full_results())
frame_msg.merge_route_info(
self.last_result_msg.get_route_info())
output_msg = frame_msg
else:
# Synchronous mode: Directly output the frame that the model result
# was obtained from.
self.frame_lag.update(time.time() - result_msg.timestamp)
output_msg = result_msg
# Update frame fps and lag
if self.last_output_msg is not None:
self.frame_lag.update(time.time() - output_msg.timestamp)
self.frame_fps.update(
1.0 / (output_msg.timestamp - self.last_output_msg.timestamp))
self.last_output_msg = output_msg
return output_msg
def _get_node_info(self):
info = super()._get_node_info()
info['result_fps'] = self.result_fps.average()
info['result_lag (ms)'] = self.result_lag.average() * 1000
info['frame_fps'] = self.frame_fps.average()
info['frame_lag (ms)'] = self.frame_lag.average() * 1000
return info
@NODES.register_module()
class MonitorNode(Node):
_default_ignore_items = ['timestamp']
def __init__(self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
enable: bool = False,
x_offset=20,
y_offset=20,
y_delta=15,
text_color='black',
background_color=(255, 183, 0),
text_scale=0.4,
ignore_items: Optional[List[str]] = None):
super().__init__(name=name, enable_key=enable_key, enable=enable)
self.x_offset = x_offset
self.y_offset = y_offset
self.y_delta = y_delta
self.text_color = color_val(text_color)
self.background_color = color_val(background_color)
self.text_scale = text_scale
if ignore_items is None:
self.ignore_items = self._default_ignore_items
else:
self.ignore_items = ignore_items
self.register_input_buffer(frame_buffer, 'frame', essential=True)
self.register_output_buffer(output_buffer)
def process(self, input_msgs):
frame_msg = input_msgs['frame']
frame_msg.update_route_info(
node_name='System Info',
node_type='dummy',
info=self._get_system_info())
img = frame_msg.get_image()
route_info = frame_msg.get_route_info()
img = self._show_route_info(img, route_info)
frame_msg.set_image(img)
return frame_msg
def _get_system_info(self):
sys_info = {}
if psutil_proc is not None:
sys_info['CPU(%)'] = psutil_proc.cpu_percent()
sys_info['Memory(%)'] = psutil_proc.memory_percent()
return sys_info
def _show_route_info(self, img, route_info):
canvas = np.full(img.shape, self.background_color, dtype=img.dtype)
x = self.x_offset
y = self.y_offset
max_len = 0
def _put_line(line=''):
nonlocal y, max_len
cv2.putText(canvas, line, (x, y), cv2.FONT_HERSHEY_DUPLEX,
self.text_scale, self.text_color, 1)
y += self.y_delta
max_len = max(max_len, len(line))
for node_info in route_info:
title = f'{node_info["node"]}({node_info["node_type"]})'
_put_line(title)
for k, v in node_info['info'].items():
if k in self.ignore_items:
continue
if isinstance(v, float):
v = f'{v:.1f}'
_put_line(f' {k}: {v}')
x1 = max(0, self.x_offset)
x2 = min(img.shape[1], int(x + max_len * self.text_scale * 20))
y1 = max(0, self.y_offset - self.y_delta)
y2 = min(img.shape[0], y)
src1 = canvas[y1:y2, x1:x2]
src2 = img[y1:y2, x1:x2]
img[y1:y2, x1:x2] = cv2.addWeighted(src1, 0.5, src2, 0.5, 0)
return img
def bypass(self, input_msgs):
return input_msgs['frame']
@NODES.register_module()
class RecorderNode(Node):
"""Record the frames into a local file."""
def __init__(
self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
out_video_file: str,
out_video_fps: int = 30,
out_video_codec: str = 'mp4v',
buffer_size: int = 30,
):
super().__init__(name=name, enable_key=None, enable=True)
self.queue = Queue(maxsize=buffer_size)
self.out_video_file = out_video_file
self.out_video_fps = out_video_fps
self.out_video_codec = out_video_codec
self.vwriter = None
# Register buffers
self.register_input_buffer(frame_buffer, 'frame', essential=True)
self.register_output_buffer(output_buffer)
# Start a new thread to write frame
self.t_record = Thread(target=self._record, args=(), daemon=True)
self.t_record.start()
def process(self, input_msgs):
frame_msg = input_msgs['frame']
img = frame_msg.get_image() if frame_msg is not None else None
img_queued = False
while not img_queued:
try:
self.queue.put(img, timeout=1)
img_queued = True
logging.info(f'{self.name}: recorder received one frame!')
except Full:
logging.info(f'{self.name}: recorder jamed!')
return frame_msg
def _record(self):
while True:
img = self.queue.get()
if img is None:
break
if self.vwriter is None:
fourcc = cv2.VideoWriter_fourcc(*self.out_video_codec)
fps = self.out_video_fps
frame_size = (img.shape[1], img.shape[0])
self.vwriter = cv2.VideoWriter(self.out_video_file, fourcc,
fps, frame_size)
assert self.vwriter.isOpened()
self.vwriter.write(img)
logging.info('Video recorder released!')
if self.vwriter is not None:
self.vwriter.release()
def on_exit(self):
try:
# Try putting a None into the output queue so the self.vwriter will
# be released after all queue frames have been written to file.
self.queue.put(None, timeout=1)
self.t_record.join(timeout=1)
except Full:
pass
if self.t_record.is_alive():
# Force to release self.vwriter
logging.info('Video recorder forced release!')
if self.vwriter is not None:
self.vwriter.release()
@@ -0,0 +1,84 @@
# Copyright (c) OpenMMLab. All rights reserved.
from typing import List, Optional, Union
from .builder import NODES
from .node import Node
try:
from mmdet.apis import inference_detector, init_detector
has_mmdet = True
except (ImportError, ModuleNotFoundError):
has_mmdet = False
@NODES.register_module()
class DetectorNode(Node):
def __init__(self,
name: str,
model_config: str,
model_checkpoint: str,
input_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
device: str = 'cuda:0'):
# Check mmdetection is installed
assert has_mmdet, 'Please install mmdet to run the demo.'
super().__init__(name=name, enable_key=enable_key, enable=True)
self.model_config = model_config
self.model_checkpoint = model_checkpoint
self.device = device.lower()
# Init model
self.model = init_detector(
self.model_config,
self.model_checkpoint,
device=self.device.lower())
# Register buffers
self.register_input_buffer(input_buffer, 'input', essential=True)
self.register_output_buffer(output_buffer)
def bypass(self, input_msgs):
return input_msgs['input']
def process(self, input_msgs):
input_msg = input_msgs['input']
img = input_msg.get_image()
preds = inference_detector(self.model, img)
det_result = self._post_process(preds)
input_msg.add_detection_result(det_result, tag=self.name)
return input_msg
def _post_process(self, preds):
if isinstance(preds, tuple):
dets = preds[0]
segms = preds[1]
else:
dets = preds
segms = [None] * len(dets)
assert len(dets) == len(self.model.CLASSES)
assert len(segms) == len(self.model.CLASSES)
result = {'preds': [], 'model_cfg': self.model.cfg.copy()}
for i, (cls_name, bboxes,
masks) in enumerate(zip(self.model.CLASSES, dets, segms)):
if masks is None:
masks = [None] * len(bboxes)
else:
assert len(masks) == len(bboxes)
preds_i = [{
'cls_id': i,
'label': cls_name,
'bbox': bbox,
'mask': mask,
} for (bbox, mask) in zip(bboxes, masks)]
result['preds'].extend(preds_i)
return result
@@ -0,0 +1,122 @@
# Copyright (c) OpenMMLab. All rights reserved.
import time
from typing import Dict, List, Optional, Union
from mmpose.apis import (get_track_id, inference_top_down_pose_model,
init_pose_model)
from ..utils import Message
from .builder import NODES
from .node import Node
@NODES.register_module()
class TopDownPoseEstimatorNode(Node):
def __init__(self,
name: str,
model_config: str,
model_checkpoint: str,
input_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
enable: bool = True,
device: str = 'cuda:0',
cls_ids: Optional[List] = None,
cls_names: Optional[List] = None,
bbox_thr: float = 0.5):
super().__init__(name=name, enable_key=enable_key, enable=enable)
# Init model
self.model_config = model_config
self.model_checkpoint = model_checkpoint
self.device = device.lower()
self.cls_ids = cls_ids
self.cls_names = cls_names
self.bbox_thr = bbox_thr
# Init model
self.model = init_pose_model(
self.model_config,
self.model_checkpoint,
device=self.device.lower())
# Store history for pose tracking
self.track_info = {
'next_id': 0,
'last_pose_preds': [],
'last_time': None
}
# Register buffers
self.register_input_buffer(input_buffer, 'input', essential=True)
self.register_output_buffer(output_buffer)
def bypass(self, input_msgs):
return input_msgs['input']
def process(self, input_msgs: Dict[str, Message]) -> Message:
input_msg = input_msgs['input']
img = input_msg.get_image()
det_results = input_msg.get_detection_results()
if det_results is None:
raise ValueError(
'No detection results are found in the frame message.'
f'{self.__class__.__name__} should be used after a '
'detector node.')
full_det_preds = []
for det_result in det_results:
det_preds = det_result['preds']
if self.cls_ids:
# Filter detection results by class ID
det_preds = [
p for p in det_preds if p['cls_id'] in self.cls_ids
]
elif self.cls_names:
# Filter detection results by class name
det_preds = [
p for p in det_preds if p['label'] in self.cls_names
]
full_det_preds.extend(det_preds)
# Inference pose
pose_preds, _ = inference_top_down_pose_model(
self.model,
img,
full_det_preds,
bbox_thr=self.bbox_thr,
format='xyxy')
# Pose tracking
current_time = time.time()
if self.track_info['last_time'] is None:
fps = None
elif self.track_info['last_time'] >= current_time:
fps = None
else:
fps = 1.0 / (current_time - self.track_info['last_time'])
pose_preds, next_id = get_track_id(
pose_preds,
self.track_info['last_pose_preds'],
self.track_info['next_id'],
use_oks=False,
tracking_thr=0.3,
use_one_euro=True,
fps=fps)
self.track_info['next_id'] = next_id
self.track_info['last_pose_preds'] = pose_preds.copy()
self.track_info['last_time'] = current_time
pose_result = {
'preds': pose_preds,
'model_cfg': self.model.cfg.copy(),
}
input_msg.add_pose_result(pose_result, tag=self.name)
return input_msg
@@ -0,0 +1,372 @@
# Copyright (c) OpenMMLab. All rights reserved.
import logging
import time
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
from queue import Empty
from threading import Thread
from typing import Callable, Dict, List, Optional, Tuple, Union
from mmcv.utils.misc import is_method_overridden
from mmpose.utils import StopWatch
from ..utils import Message, VideoEndingMessage, limit_max_fps
@dataclass
class BufferInfo():
"""Dataclass for buffer information."""
buffer_name: str
input_name: Optional[str] = None
essential: bool = False
@dataclass
class EventInfo():
"""Dataclass for event handler information."""
event_name: str
is_keyboard: bool = False
handler_func: Optional[Callable] = None
class Node(Thread, metaclass=ABCMeta):
"""Base interface of functional module.
Parameters:
name (str, optional): The node name (also thread name).
enable_key (str|int, optional): Set a hot-key to toggle enable/disable
of the node. If an int value is given, it will be treated as an
ascii code of a key. Please note:
1. If enable_key is set, the bypass method need to be
overridden to define the node behavior when disabled
2. Some hot-key has been use for particular use. For example:
'q', 'Q' and 27 are used for quit
Default: None
max_fps (int): Maximum FPS of the node. This is to avoid the node
running unrestrictedly and causing large resource consuming.
Default: 30
input_check_interval (float): Minimum interval (in millisecond) between
checking if input is ready. Default: 0.001
enable (bool): Default enable/disable status. Default: True.
daemon (bool): Whether node is a daemon. Default: True.
"""
def __init__(self,
name: Optional[str] = None,
enable_key: Optional[Union[str, int]] = None,
max_fps: int = 30,
input_check_interval: float = 0.01,
enable: bool = True,
daemon=False):
super().__init__(name=name, daemon=daemon)
self._runner = None
self._enabled = enable
self.enable_key = enable_key
self.max_fps = max_fps
self.input_check_interval = input_check_interval
# A partitioned buffer manager the runner's buffer manager that
# only accesses the buffers related to the node
self._buffer_manager = None
# Input/output buffers are a list of registered buffers' information
self._input_buffers = []
self._output_buffers = []
# Event manager is a copy of assigned runner's event manager
self._event_manager = None
# A list of registered event information
# See register_event() for more information
# Note that we recommend to handle events in nodes by registering
# handlers, but one can still access the raw event by _event_manager
self._registered_events = []
# A list of (listener_threads, event_info)
# See set_runner() for more information
self._event_listener_threads = []
# A timer to calculate node FPS
self._timer = StopWatch(window=10)
# Register enable toggle key
if self.enable_key:
# If the node allows toggling enable, it should override the
# `bypass` method to define the node behavior when disabled.
if not is_method_overridden('bypass', Node, self.__class__):
raise NotImplementedError(
f'The node {self.__class__} does not support toggling'
'enable but got argument `enable_key`. To support toggling'
'enable, please override the `bypass` method of the node.')
self.register_event(
event_name=self.enable_key,
is_keyboard=True,
handler_func=self._toggle_enable,
)
@property
def registered_buffers(self):
return self._input_buffers + self._output_buffers
@property
def registered_events(self):
return self._registered_events.copy()
def _toggle_enable(self):
self._enabled = not self._enabled
def register_input_buffer(self,
buffer_name: str,
input_name: str,
essential: bool = False):
"""Register an input buffer, so that Node can automatically check if
data is ready, fetch data from the buffers and format the inputs to
feed into `process` method.
This method can be invoked multiple times to register multiple input
buffers.
The subclass of Node should invoke `register_input_buffer` in its
`__init__` method.
Args:
buffer_name (str): The name of the buffer
input_name (str): The name of the fetched message from the
corresponding buffer
essential (bool): An essential input means the node will wait
until the input is ready before processing. Otherwise, an
inessential input will not block the processing, instead
a None will be fetched if the buffer is not ready.
"""
buffer_info = BufferInfo(buffer_name, input_name, essential)
self._input_buffers.append(buffer_info)
def register_output_buffer(self, buffer_name: Union[str, List[str]]):
"""Register one or multiple output buffers, so that the Node can
automatically send the output of the `process` method to these buffers.
The subclass of Node should invoke `register_output_buffer` in its
`__init__` method.
Args:
buffer_name (str|list): The name(s) of the output buffer(s).
"""
if not isinstance(buffer_name, list):
buffer_name = [buffer_name]
for name in buffer_name:
buffer_info = BufferInfo(name)
self._output_buffers.append(buffer_info)
def register_event(self,
event_name: str,
is_keyboard: bool = False,
handler_func: Optional[Callable] = None):
"""Register an event. All events used in the node need to be registered
in __init__(). If a callable handler is given, a thread will be create
to listen and handle the event when the node starts.
Args:
Args:
event_name (str|int): The event name. If is_keyboard==True,
event_name should be a str (as char) or an int (as ascii)
is_keyboard (bool): Indicate whether it is an keyboard
event. If True, the argument event_name will be regarded as a
key indicator.
handler_func (callable, optional): The event handler function,
which should be a collable object with no arguments or
return values. Default: None.
"""
event_info = EventInfo(event_name, is_keyboard, handler_func)
self._registered_events.append(event_info)
def set_runner(self, runner):
# Get partitioned buffer manager
buffer_names = [
buffer.buffer_name
for buffer in self._input_buffers + self._output_buffers
]
self._buffer_manager = runner.buffer_manager.get_sub_manager(
buffer_names)
# Get event manager
self._event_manager = runner.event_manager
def _get_input_from_buffer(self) -> Tuple[bool, Optional[Dict]]:
"""Get and pack input data if it's ready. The function returns a tuple
of a status flag and a packed data dictionary. If input_buffer is
ready, the status flag will be True, and the packed data is a dict
whose items are buffer names and corresponding messages (unready
additional buffers will give a `None`). Otherwise, the status flag is
False and the packed data is None.
Returns:
bool: status flag
dict[str, Message]: the packed inputs where the key is the buffer
name and the value is the Message got from the corresponding
buffer.
"""
buffer_manager = self._buffer_manager
if buffer_manager is None:
raise ValueError(f'{self.name}: Runner not set!')
# Check that essential buffers are ready
for buffer_info in self._input_buffers:
if buffer_info.essential and buffer_manager.is_empty(
buffer_info.buffer_name):
return False, None
# Default input
result = {
buffer_info.input_name: None
for buffer_info in self._input_buffers
}
for buffer_info in self._input_buffers:
try:
result[buffer_info.input_name] = buffer_manager.get(
buffer_info.buffer_name, block=False)
except Empty:
if buffer_info.essential:
# Return unsuccessful flag if any
# essential input is unready
return False, None
return True, result
def _send_output_to_buffers(self, output_msg):
"""Send output of the process method to registered output buffers.
Args:
output_msg (Message): output message
force (bool, optional): If True, block until the output message
has been put into all output buffers. Default: False
"""
for buffer_info in self._output_buffers:
buffer_name = buffer_info.buffer_name
self._buffer_manager.put_force(buffer_name, output_msg)
@abstractmethod
def process(self, input_msgs: Dict[str, Message]) -> Union[Message, None]:
"""The core method that implement the function of the node. This method
will be invoked when the node is enabled and the input data is ready.
All subclasses of Node should override this method.
Args:
input_msgs (dict): The input data collected from the buffers. For
each item, the key is the `input_name` of the registered input
buffer, while the value is a Message instance fetched from the
buffer (or None if the buffer is unessential and not ready).
Returns:
Message: The output message of the node. It will be send to all
registered output buffers.
"""
def bypass(self, input_msgs: Dict[str, Message]) -> Union[Message, None]:
"""The method that defines the node behavior when disabled. Note that
if the node has an `enable_key`, this method should be override.
The method input/output is same as it of `process` method.
Args:
input_msgs (dict): The input data collected from the buffers. For
each item, the key is the `input_name` of the registered input
buffer, while the value is a Message instance fetched from the
buffer (or None if the buffer is unessential and not ready).
Returns:
Message: The output message of the node. It will be send to all
registered output buffers.
"""
raise NotImplementedError
def _get_node_info(self):
"""Get route information of the node."""
info = {'fps': self._timer.report('_FPS_'), 'timestamp': time.time()}
return info
def on_exit(self):
"""This method will be invoked on event `_exit_`.
Subclasses should override this method to specifying the exiting
behavior.
"""
def run(self):
"""Method representing the Node's activity.
This method override the standard run() method of Thread. Users should
not override this method in subclasses.
"""
logging.info(f'Node {self.name} starts')
# Create event listener threads
for event_info in self._registered_events:
if event_info.handler_func is None:
continue
def event_listener():
while True:
with self._event_manager.wait_and_handle(
event_info.event_name, event_info.is_keyboard):
event_info.handler_func()
t_listener = Thread(target=event_listener, args=(), daemon=True)
t_listener.start()
self._event_listener_threads.append(t_listener)
# Loop
while True:
# Exit
if self._event_manager.is_set('_exit_'):
self.on_exit()
break
# Check if input is ready
input_status, input_msgs = self._get_input_from_buffer()
# Input is not ready
if not input_status:
time.sleep(self.input_check_interval)
continue
# If a VideoEndingMessage is received, broadcast the signal
# without invoking process() or bypass()
video_ending = False
for _, msg in input_msgs.items():
if isinstance(msg, VideoEndingMessage):
self._send_output_to_buffers(msg)
video_ending = True
break
if video_ending:
self.on_exit()
break
# Check if enabled
if not self._enabled:
# Override bypass method to define node behavior when disabled
output_msg = self.bypass(input_msgs)
else:
with self._timer.timeit():
with limit_max_fps(self.max_fps):
# Process
output_msg = self.process(input_msgs)
if output_msg:
# Update route information
node_info = self._get_node_info()
output_msg.update_route_info(node=self, info=node_info)
# Send output message
if output_msg is not None:
self._send_output_to_buffers(output_msg)
logging.info(f'{self.name}: process ending.')
@@ -0,0 +1,340 @@
# Copyright (c) OpenMMLab. All rights reserved.
import time
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple, Union
import cv2
import numpy as np
from ..utils import (FrameMessage, get_eye_keypoint_ids, get_hand_keypoint_ids,
get_mouth_keypoint_ids, load_image_from_disk_or_url)
from .builder import NODES
from .frame_drawing_node import FrameDrawingNode
@dataclass
class HeartInfo():
"""Dataclass for heart information."""
heart_type: int
start_time: float
start_pos: Tuple[int, int]
end_pos: Tuple[int, int]
@NODES.register_module()
class ValentineMagicNode(FrameDrawingNode):
def __init__(self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
enable_key: Optional[Union[str, int]] = None,
kpt_vis_thr: float = 0.3,
hand_heart_angle_thr: float = 90.0,
longest_duration: float = 2.0,
largest_ratio: float = 0.25,
hand_heart_img_path: Optional[str] = None,
flying_heart_img_path: Optional[str] = None,
hand_heart_dis_ratio_thr: float = 1.0,
flying_heart_dis_ratio_thr: float = 3.5,
num_persons: int = 2):
super().__init__(
name, frame_buffer, output_buffer, enable_key=enable_key)
if hand_heart_img_path is None:
hand_heart_img_path = 'https://user-images.githubusercontent.com/'\
'87690686/149731850-ea946766-a4e8-4efa-82f5'\
'-e2f0515db8ae.png'
if flying_heart_img_path is None:
flying_heart_img_path = 'https://user-images.githubusercontent.'\
'com/87690686/153554948-937ce496-33dd-4'\
'9ab-9829-0433fd7c13c4.png'
self.hand_heart = load_image_from_disk_or_url(hand_heart_img_path)
self.flying_heart = load_image_from_disk_or_url(flying_heart_img_path)
self.kpt_vis_thr = kpt_vis_thr
self.hand_heart_angle_thr = hand_heart_angle_thr
self.hand_heart_dis_ratio_thr = hand_heart_dis_ratio_thr
self.flying_heart_dis_ratio_thr = flying_heart_dis_ratio_thr
self.longest_duration = longest_duration
self.largest_ratio = largest_ratio
self.num_persons = num_persons
# record the heart infos for each person
self.heart_infos = {}
def _cal_distance(self, p1: np.ndarray, p2: np.ndarray) -> np.float64:
"""calculate the distance of points p1 and p2."""
return np.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)
def _cal_angle(self, p1: np.ndarray, p2: np.ndarray, p3: np.ndarray,
p4: np.ndarray) -> np.float64:
"""calculate the angle of vectors v1(constructed by points p2 and p1)
and v2(constructed by points p4 and p3)"""
v1 = p2 - p1
v2 = p4 - p3
vector_prod = v1[0] * v2[0] + v1[1] * v2[1]
length_prod = np.sqrt(pow(v1[0], 2) + pow(v1[1], 2)) * np.sqrt(
pow(v2[0], 2) + pow(v2[1], 2))
cos = vector_prod * 1.0 / (length_prod * 1.0 + 1e-6)
return (np.arccos(cos) / np.pi) * 180
def _check_heart(self, pred: Dict[str,
np.ndarray], hand_indices: List[int],
mouth_index: int, eye_indices: List[int]) -> int:
"""Check the type of Valentine Magic based on the pose results and
keypoint indices of hand, mouth. and eye.
Args:
pred(dict): The pose estimation results containing:
- "keypoints" (np.ndarray[K,3]): keypoint detection result
in [x, y, score]
hand_indices(list[int]): keypoint indices of hand
mouth_index(int): keypoint index of mouth
eye_indices(list[int]): keypoint indices of eyes
Returns:
int: a number representing the type of heart pose,
0: None, 1: hand heart, 2: left hand blow kiss,
3: right hand blow kiss
"""
kpts = pred['keypoints']
left_eye_idx, right_eye_idx = eye_indices
left_eye_pos = kpts[left_eye_idx][:2]
right_eye_pos = kpts[right_eye_idx][:2]
eye_dis = self._cal_distance(left_eye_pos, right_eye_pos)
# these indices are corresoponding to the following keypoints:
# left_hand_root, left_pinky_finger1,
# left_pinky_finger3, left_pinky_finger4,
# right_hand_root, right_pinky_finger1
# right_pinky_finger3, right_pinky_finger4
both_hands_vis = True
for i in [0, 17, 19, 20, 21, 38, 40, 41]:
if kpts[hand_indices[i]][2] < self.kpt_vis_thr:
both_hands_vis = False
if both_hands_vis:
p1 = kpts[hand_indices[20]][:2]
p2 = kpts[hand_indices[19]][:2]
p3 = kpts[hand_indices[17]][:2]
p4 = kpts[hand_indices[0]][:2]
left_angle = self._cal_angle(p1, p2, p3, p4)
p1 = kpts[hand_indices[41]][:2]
p2 = kpts[hand_indices[40]][:2]
p3 = kpts[hand_indices[38]][:2]
p4 = kpts[hand_indices[21]][:2]
right_angle = self._cal_angle(p1, p2, p3, p4)
hand_dis = self._cal_distance(kpts[hand_indices[20]][:2],
kpts[hand_indices[41]][:2])
if (left_angle < self.hand_heart_angle_thr
and right_angle < self.hand_heart_angle_thr
and hand_dis / eye_dis < self.hand_heart_dis_ratio_thr):
return 1
# these indices are corresoponding to the following keypoints:
# left_middle_finger1, left_middle_finger4,
left_hand_vis = True
for i in [9, 12]:
if kpts[hand_indices[i]][2] < self.kpt_vis_thr:
left_hand_vis = False
break
# right_middle_finger1, right_middle_finger4
right_hand_vis = True
for i in [30, 33]:
if kpts[hand_indices[i]][2] < self.kpt_vis_thr:
right_hand_vis = False
break
mouth_vis = True
if kpts[mouth_index][2] < self.kpt_vis_thr:
mouth_vis = False
if (not left_hand_vis and not right_hand_vis) or not mouth_vis:
return 0
mouth_pos = kpts[mouth_index]
left_mid_hand_pos = (kpts[hand_indices[9]][:2] +
kpts[hand_indices[12]][:2]) / 2
lefthand_mouth_dis = self._cal_distance(left_mid_hand_pos, mouth_pos)
if lefthand_mouth_dis / eye_dis < self.flying_heart_dis_ratio_thr:
return 2
right_mid_hand_pos = (kpts[hand_indices[30]][:2] +
kpts[hand_indices[33]][:2]) / 2
righthand_mouth_dis = self._cal_distance(right_mid_hand_pos, mouth_pos)
if righthand_mouth_dis / eye_dis < self.flying_heart_dis_ratio_thr:
return 3
return 0
def _get_heart_route(self, heart_type: int, cur_pred: Dict[str,
np.ndarray],
tar_pred: Dict[str,
np.ndarray], hand_indices: List[int],
mouth_index: int) -> Tuple[int, int]:
"""get the start and end position of the heart, based on two keypoint
results and keypoint indices of hand and mouth.
Args:
cur_pred(dict): The pose estimation results of current person,
containing: the following keys:
- "keypoints" (np.ndarray[K,3]): keypoint detection result
in [x, y, score]
tar_pred(dict): The pose estimation results of target person,
containing: the following keys:
- "keypoints" (np.ndarray[K,3]): keypoint detection result
in [x, y, score]
hand_indices(list[int]): keypoint indices of hand
mouth_index(int): keypoint index of mouth
Returns:
tuple(int): the start position of heart
tuple(int): the end position of heart
"""
cur_kpts = cur_pred['keypoints']
assert heart_type in [1, 2,
3], 'Can not determine the type of heart effect'
if heart_type == 1:
p1 = cur_kpts[hand_indices[20]][:2]
p2 = cur_kpts[hand_indices[41]][:2]
elif heart_type == 2:
p1 = cur_kpts[hand_indices[9]][:2]
p2 = cur_kpts[hand_indices[12]][:2]
elif heart_type == 3:
p1 = cur_kpts[hand_indices[30]][:2]
p2 = cur_kpts[hand_indices[33]][:2]
cur_x, cur_y = (p1 + p2) / 2
# the mid point of two fingers
start_pos = (int(cur_x), int(cur_y))
tar_kpts = tar_pred['keypoints']
end_pos = tar_kpts[mouth_index][:2]
return start_pos, end_pos
def _draw_heart(self, canvas: np.ndarray, heart_info: HeartInfo,
t_pass: float) -> np.ndarray:
"""draw the heart according to heart info and time."""
start_x, start_y = heart_info.start_pos
end_x, end_y = heart_info.end_pos
scale = t_pass / self.longest_duration
max_h, max_w = canvas.shape[:2]
hm, wm = self.largest_ratio * max_h, self.largest_ratio * max_h
new_h, new_w = int(hm * scale), int(wm * scale)
x = int(start_x + scale * (end_x - start_x))
y = int(start_y + scale * (end_y - start_y))
y1 = max(0, y - int(new_h / 2))
y2 = min(max_h - 1, y + int(new_h / 2))
x1 = max(0, x - int(new_w / 2))
x2 = min(max_w - 1, x + int(new_w / 2))
target = canvas[y1:y2 + 1, x1:x2 + 1].copy()
new_h, new_w = target.shape[:2]
if new_h == 0 or new_w == 0:
return canvas
assert heart_info.heart_type in [
1, 2, 3
], 'Can not determine the type of heart effect'
if heart_info.heart_type == 1: # hand heart
patch = self.hand_heart.copy()
elif heart_info.heart_type >= 2: # hand blow kiss
patch = self.flying_heart.copy()
if heart_info.start_pos[0] > heart_info.end_pos[0]:
patch = patch[:, ::-1]
patch = cv2.resize(patch, (new_w, new_h))
mask = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
mask = (mask < 100)[..., None].astype(np.float32) * 0.8
canvas[y1:y2 + 1, x1:x2 + 1] = patch * mask + target * (1 - mask)
return canvas
def draw(self, frame_msg: FrameMessage) -> np.ndarray:
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 = [pred.copy() for pred in pose_result['preds']]
# if number of persons in the image is less than 2,
# no heart effect will be triggered
if len(preds) < self.num_persons:
continue
# if number of persons in the image is more than 2,
# only use the first two pose results
preds = preds[:self.num_persons]
ids = [preds[i]['track_id'] for i in range(self.num_persons)]
for id in self.heart_infos.copy():
if id not in ids:
# if the id of a person not in previous heart_infos,
# delete the corresponding field
del self.heart_infos[id]
for i in range(self.num_persons):
id = preds[i]['track_id']
# if the predicted person in previous heart_infos,
# draw the heart
if id in self.heart_infos.copy():
t_pass = time.time() - self.heart_infos[id].start_time
# the time passed since last heart pose less than
# longest_duration, continue to draw the heart
if t_pass < self.longest_duration:
canvas = self._draw_heart(canvas, self.heart_infos[id],
t_pass)
# reset corresponding heart info
else:
del self.heart_infos[id]
else:
hand_indices = get_hand_keypoint_ids(model_cfg)
mouth_index = get_mouth_keypoint_ids(model_cfg)
eye_indices = get_eye_keypoint_ids(model_cfg)
# check the type of Valentine Magic based on pose results
# and keypoint indices of hand and mouth
heart_type = self._check_heart(preds[i], hand_indices,
mouth_index, eye_indices)
# trigger a Valentine Magic effect
if heart_type:
# get the route of heart
start_pos, end_pos = self._get_heart_route(
heart_type, preds[i],
preds[self.num_persons - 1 - i], hand_indices,
mouth_index)
start_time = time.time()
self.heart_infos[id] = HeartInfo(
heart_type, start_time, start_pos, end_pos)
return canvas
@@ -0,0 +1,240 @@
# Copyright (c) OpenMMLab. All rights reserved.
import json
from dataclasses import dataclass
from typing import List, Tuple, Union
import cv2
import numpy as np
from mmpose.datasets.dataset_info import DatasetInfo
from ..utils import load_image_from_disk_or_url
from .builder import NODES
from .frame_drawing_node import FrameDrawingNode
@dataclass
class DynamicInfo:
pos_curr: Tuple[int, int] = (0, 0)
pos_step: Tuple[int, int] = (0, 0)
step_curr: int = 0
@NODES.register_module()
class XDwenDwenNode(FrameDrawingNode):
"""An effect drawing node that captures the face of a cat or dog and blend
it into a Bing-Dwen-Dwen (the mascot of 2022 Beijing Winter Olympics).
Parameters:
name (str, optional): The node name (also thread name).
frame_buffer (str): The name of the input buffer.
output_buffer (str | list): The name(s) of the output buffer(s).
mode_key (str | int): A hot key to switch the background image.
resource_file (str): The annotation file of resource images, which
should be in Labelbee format and contain both facial keypoint and
region annotations.
out_shape (tuple): The shape of output frame in (width, height).
"""
dynamic_scale = 0.15
dynamic_max_step = 15
def __init__(
self,
name: str,
frame_buffer: str,
output_buffer: Union[str, List[str]],
mode_key: Union[str, int],
resource_file: str,
out_shape: Tuple[int, int] = (480, 480),
rigid_transform: bool = True,
):
super().__init__(name, frame_buffer, output_buffer, enable=True)
self.mode_key = mode_key
self.mode_index = 0
self.out_shape = out_shape
self.rigid = rigid_transform
self.latest_pred = None
self.dynamic_info = DynamicInfo()
self.register_event(
self.mode_key, is_keyboard=True, handler_func=self.switch_mode)
self._init_resource(resource_file)
def _init_resource(self, resource_file):
# The resource_file is a JSON file that contains the facial
# keypoint and mask annotation information of the resource files.
# The annotations should follow the label-bee standard format.
# See https://github.com/open-mmlab/labelbee-client for details.
with open(resource_file) as f:
anns = json.load(f)
resource_infos = []
for ann in anns:
# Load image
img = load_image_from_disk_or_url(ann['url'])
# Load result
rst = json.loads(ann['result'])
# Check facial keypoint information
assert rst['step_1']['toolName'] == 'pointTool'
assert len(rst['step_1']['result']) == 3
keypoints = sorted(
rst['step_1']['result'], key=lambda x: x['order'])
keypoints = np.array([[pt['x'], pt['y']] for pt in keypoints])
# Check facial mask
assert rst['step_2']['toolName'] == 'polygonTool'
assert len(rst['step_2']['result']) == 1
assert len(rst['step_2']['result'][0]['pointList']) > 2
mask_pts = np.array(
[[pt['x'], pt['y']]
for pt in rst['step_2']['result'][0]['pointList']])
mul = 1.0 + self.dynamic_scale
w_scale = self.out_shape[0] / img.shape[1] * mul
h_scale = self.out_shape[1] / img.shape[0] * mul
img = cv2.resize(
img,
dsize=None,
fx=w_scale,
fy=h_scale,
interpolation=cv2.INTER_CUBIC)
keypoints *= [w_scale, h_scale]
mask_pts *= [w_scale, h_scale]
mask = cv2.fillPoly(
np.zeros(img.shape[:2], dtype=np.uint8),
[mask_pts.astype(np.int32)],
color=1)
res = {
'img': img,
'keypoints': keypoints,
'mask': mask,
}
resource_infos.append(res)
self.resource_infos = resource_infos
self._reset_dynamic()
def switch_mode(self):
self.mode_index = (self.mode_index + 1) % len(self.resource_infos)
def _reset_dynamic(self):
x_tar = np.random.randint(int(self.out_shape[0] * self.dynamic_scale))
y_tar = np.random.randint(int(self.out_shape[1] * self.dynamic_scale))
x_step = (x_tar -
self.dynamic_info.pos_curr[0]) / self.dynamic_max_step
y_step = (y_tar -
self.dynamic_info.pos_curr[1]) / self.dynamic_max_step
self.dynamic_info.pos_step = (x_step, y_step)
self.dynamic_info.step_curr = 0
def draw(self, frame_msg):
full_pose_results = frame_msg.get_pose_results()
pred = None
if full_pose_results:
for pose_results in full_pose_results:
if not pose_results['preds']:
continue
pred = pose_results['preds'][0].copy()
pred['dataset'] = DatasetInfo(pose_results['model_cfg'].data.
test.dataset_info).dataset_name
self.latest_pred = pred
break
# Use the latest pose result if there is none available in
# the current frame.
if pred is None:
pred = self.latest_pred
# Get the background image and facial annotations
res = self.resource_infos[self.mode_index]
img = frame_msg.get_image()
canvas = res['img'].copy()
mask = res['mask']
kpts_tar = res['keypoints']
if pred is not None:
if pred['dataset'] == 'ap10k':
# left eye: 0, right eye: 1, nose: 2
kpts_src = pred['keypoints'][[0, 1, 2], :2]
elif pred['dataset'] == 'coco_wholebody':
# left eye: 1, right eye 2, nose: 0
kpts_src = pred['keypoints'][[1, 2, 0], :2]
else:
raise ValueError('Can not obtain face landmark information'
f'from dataset: {pred["type"]}')
trans_mat = self._get_transform(kpts_src, kpts_tar)
warp = cv2.warpAffine(img, trans_mat, dsize=canvas.shape[:2])
cv2.copyTo(warp, mask, canvas)
# Add random movement to the background
xc, yc = self.dynamic_info.pos_curr
xs, ys = self.dynamic_info.pos_step
w, h = self.out_shape
x = min(max(int(xc), 0), canvas.shape[1] - w + 1)
y = min(max(int(yc), 0), canvas.shape[0] - h + 1)
canvas = canvas[y:y + h, x:x + w]
self.dynamic_info.pos_curr = (xc + xs, yc + ys)
self.dynamic_info.step_curr += 1
if self.dynamic_info.step_curr == self.dynamic_max_step:
self._reset_dynamic()
return canvas
def _get_transform(self, kpts_src, kpts_tar):
if self.rigid:
# rigid transform
n = kpts_src.shape[0]
X = np.zeros((n * 2, 4), dtype=np.float32)
U = np.zeros((n * 2, 1), dtype=np.float32)
X[:n, :2] = kpts_src
X[:n, 2] = 1
X[n:, 0] = kpts_src[:, 1]
X[n:, 1] = -kpts_src[:, 0]
X[n:, 3] = 1
U[:n, 0] = kpts_tar[:, 0]
U[n:, 0] = kpts_tar[:, 1]
M = np.linalg.pinv(X).dot(U).flatten()
trans_mat = np.array([[M[0], M[1], M[2]], [-M[1], M[0], M[3]]],
dtype=np.float32)
else:
# normal affine transform
# adaptive horizontal flipping
if (np.linalg.norm(kpts_tar[0] - kpts_tar[2]) -
np.linalg.norm(kpts_tar[1] - kpts_tar[2])) * (
np.linalg.norm(kpts_src[0] - kpts_src[2]) -
np.linalg.norm(kpts_src[1] - kpts_src[2])) < 0:
kpts_src = kpts_src[[1, 0, 2], :]
trans_mat, _ = cv2.estimateAffine2D(
kpts_src.astype(np.float32), kpts_tar.astype(np.float32))
return trans_mat
@@ -0,0 +1,31 @@
# Copyright (c) OpenMMLab. All rights reserved.
from .buffer import BufferManager
from .event import EventManager
from .message import FrameMessage, Message, VideoEndingMessage
from .misc import (ImageCapture, copy_and_paste, expand_and_clamp,
get_cached_file_path, is_image_file, limit_max_fps,
load_image_from_disk_or_url, screen_matting)
from .pose import (get_eye_keypoint_ids, get_face_keypoint_ids,
get_hand_keypoint_ids, get_mouth_keypoint_ids,
get_wrist_keypoint_ids)
__all__ = [
'BufferManager',
'EventManager',
'FrameMessage',
'Message',
'limit_max_fps',
'VideoEndingMessage',
'load_image_from_disk_or_url',
'get_cached_file_path',
'screen_matting',
'expand_and_clamp',
'copy_and_paste',
'is_image_file',
'ImageCapture',
'get_eye_keypoint_ids',
'get_face_keypoint_ids',
'get_wrist_keypoint_ids',
'get_mouth_keypoint_ids',
'get_hand_keypoint_ids',
]
@@ -0,0 +1,106 @@
# Copyright (c) OpenMMLab. All rights reserved.
from functools import wraps
from queue import Queue
from typing import Dict, List, Optional
from mmcv import is_seq_of
__all__ = ['BufferManager']
def check_buffer_registered(exist=True):
def wrapper(func):
@wraps(func)
def wrapped(manager, name, *args, **kwargs):
if exist:
# Assert buffer exist
if name not in manager:
raise ValueError(f'Fail to call {func.__name__}: '
f'buffer "{name}" is not registered.')
else:
# Assert buffer not exist
if name in manager:
raise ValueError(f'Fail to call {func.__name__}: '
f'buffer "{name}" is already registered.')
return func(manager, name, *args, **kwargs)
return wrapped
return wrapper
class Buffer(Queue):
def put_force(self, item):
"""Force to put an item into the buffer.
If the buffer is already full, the earliest item in the buffer will be
remove to make room for the incoming item.
"""
with self.mutex:
if self.maxsize > 0:
while self._qsize() >= self.maxsize:
_ = self._get()
self.unfinished_tasks -= 1
self._put(item)
self.unfinished_tasks += 1
self.not_empty.notify()
class BufferManager():
def __init__(self,
buffer_type: type = Buffer,
buffers: Optional[Dict] = None):
self.buffer_type = buffer_type
if buffers is None:
self._buffers = {}
else:
if is_seq_of(list(buffers.values()), buffer_type):
self._buffers = buffers.copy()
else:
raise ValueError('The values of buffers should be instance '
f'of {buffer_type}')
def __contains__(self, name):
return name in self._buffers
@check_buffer_registered(False)
def register_buffer(self, name, maxsize=0):
self._buffers[name] = self.buffer_type(maxsize)
@check_buffer_registered()
def put(self, name, item, block=True, timeout=None):
self._buffers[name].put(item, block, timeout)
@check_buffer_registered()
def put_force(self, name, item):
self._buffers[name].put_force(item)
@check_buffer_registered()
def get(self, name, block=True, timeout=None):
return self._buffers[name].get(block, timeout)
@check_buffer_registered()
def is_empty(self, name):
return self._buffers[name].empty()
@check_buffer_registered()
def is_full(self, name):
return self._buffers[name].full()
def get_sub_manager(self, buffer_names: List[str]):
buffers = {name: self._buffers[name] for name in buffer_names}
return BufferManager(self.buffer_type, buffers)
def get_info(self):
buffer_info = {}
for name, buffer in self._buffers.items():
buffer_info[name] = {
'size': buffer.size,
'maxsize': buffer.maxsize
}
return buffer_info
@@ -0,0 +1,59 @@
# Copyright (c) OpenMMLab. All rights reserved.
from collections import defaultdict
from contextlib import contextmanager
from threading import Event
from typing import Optional
class EventManager():
def __init__(self):
self._events = defaultdict(Event)
def register_event(self,
event_name: str = None,
is_keyboard: bool = False):
if is_keyboard:
event_name = self._get_keyboard_event_name(event_name)
self._events[event_name] = Event()
def set(self, event_name: str = None, is_keyboard: bool = False):
if is_keyboard:
event_name = self._get_keyboard_event_name(event_name)
return self._events[event_name].set()
def wait(self,
event_name: str = None,
is_keyboard: Optional[bool] = False,
timeout: Optional[float] = None):
if is_keyboard:
event_name = self._get_keyboard_event_name(event_name)
return self._events[event_name].wait(timeout)
def is_set(self,
event_name: str = None,
is_keyboard: Optional[bool] = False):
if is_keyboard:
event_name = self._get_keyboard_event_name(event_name)
return self._events[event_name].is_set()
def clear(self,
event_name: str = None,
is_keyboard: Optional[bool] = False):
if is_keyboard:
event_name = self._get_keyboard_event_name(event_name)
return self._events[event_name].clear()
@staticmethod
def _get_keyboard_event_name(key):
return f'_keyboard_{chr(key) if isinstance(key,int) else key}'
@contextmanager
def wait_and_handle(self,
event_name: str = None,
is_keyboard: Optional[bool] = False):
self.wait(event_name, is_keyboard)
try:
yield
finally:
self.clear(event_name, is_keyboard)
@@ -0,0 +1,204 @@
# Copyright (c) OpenMMLab. All rights reserved.
import time
import uuid
import warnings
from typing import Dict, List, Optional
import numpy as np
class Message():
"""Message base class.
All message class should inherit this class. The basic use of a Message
instance is to carray a piece of text message (self.msg) and a dict that
stores structured data (self.data), e.g. frame image, model prediction,
et al.
A message may also hold route information, which is composed of
information of all nodes the message has passed through.
Parameters:
msg (str): The text message.
data (dict, optional): The structured data.
"""
def __init__(self, msg: str = '', data: Optional[Dict] = None):
self.msg = msg
self.data = data if data else {}
self.route_info = []
self.timestamp = time.time()
self.id = uuid.uuid4()
def update_route_info(self,
node=None,
node_name: Optional[str] = None,
node_type: Optional[str] = None,
info: Optional[Dict] = None):
"""Append new node information to the route information.
Args:
node (Node, optional): An instance of Node that provides basic
information like the node name and type. Default: None.
node_name (str, optional): The node name. If node is given,
node_name will be ignored. Default: None.
node_type (str, optional): The class name of the node. If node
is given, node_type will be ignored. Default: None.
info (dict, optional): The node information, which is usually
given by node.get_node_info(). Default: None.
"""
if node is not None:
if node_name is not None or node_type is not None:
warnings.warn(
'`node_name` and `node_type` will be overridden if node'
'is provided.')
node_name = node.name
node_type = node.__class__.__name__
node_info = {'node': node_name, 'node_type': node_type, 'info': info}
self.route_info.append(node_info)
def set_route_info(self, route_info: List):
"""Directly set the entire route information.
Args:
route_info (list): route information to set to the message.
"""
self.route_info = route_info
def merge_route_info(self, route_info: List):
"""Merge the given route information into the original one of the
message. This is used for combining route information from multiple
messages. The node information in the route will be reordered according
to their timestamps.
Args:
route_info (list): route information to merge.
"""
self.route_info += route_info
self.route_info.sort(key=lambda x: x.get('timestamp', np.inf))
def get_route_info(self) -> List:
return self.route_info.copy()
class VideoEndingMessage(Message):
"""A special message to indicate the input video is ending."""
class FrameMessage(Message):
"""The message to store information of a video frame.
A FrameMessage instance usually holds following data in self.data:
- image (array): The frame image
- detection_results (list): A list to hold detection results of
multiple detectors. Each element is a tuple (tag, result)
- pose_results (list): A list to hold pose estimation results of
multiple pose estimator. Each element is a tuple (tag, result)
"""
def __init__(self, img):
super().__init__(data=dict(image=img))
def get_image(self):
"""Get the frame image.
Returns:
array: The frame image.
"""
return self.data.get('image', None)
def set_image(self, img):
"""Set the frame image to the message."""
self.data['image'] = img
def add_detection_result(self, result, tag: str = None):
"""Add the detection result from one model into the message's
detection_results.
Args:
tag (str, optional): Give a tag to the result, which can be used
to retrieve specific results.
"""
if 'detection_results' not in self.data:
self.data['detection_results'] = []
self.data['detection_results'].append((tag, result))
def get_detection_results(self, tag: str = None):
"""Get detection results of the message.
Args:
tag (str, optional): If given, only the results with the tag
will be retrieved. Otherwise all results will be retrieved.
Default: None.
Returns:
list[dict]: The retrieved detection results
"""
if 'detection_results' not in self.data:
return None
if tag is None:
results = [res for _, res in self.data['detection_results']]
else:
results = [
res for _tag, res in self.data['detection_results']
if _tag == tag
]
return results
def add_pose_result(self, result, tag=None):
"""Add the pose estimation result from one model into the message's
pose_results.
Args:
tag (str, optional): Give a tag to the result, which can be used
to retrieve specific results.
"""
if 'pose_results' not in self.data:
self.data['pose_results'] = []
self.data['pose_results'].append((tag, result))
def get_pose_results(self, tag=None):
"""Get pose estimation results of the message.
Args:
tag (str, optional): If given, only the results with the tag
will be retrieved. Otherwise all results will be retrieved.
Default: None.
Returns:
list[dict]: The retrieved pose results
"""
if 'pose_results' not in self.data:
return None
if tag is None:
results = [res for _, res in self.data['pose_results']]
else:
results = [
res for _tag, res in self.data['pose_results'] if _tag == tag
]
return results
def get_full_results(self):
"""Get all model predictions of the message.
See set_full_results() for inference.
Returns:
dict: All model predictions, including:
- detection_results
- pose_results
"""
result_keys = ['detection_results', 'pose_results']
results = {k: self.data[k] for k in result_keys}
return results
def set_full_results(self, results):
"""Set full model results directly.
Args:
results (dict): All model predictions including:
- detection_results (list): see also add_detection_results()
- pose_results (list): see also add_pose_results()
"""
self.data.update(results)
@@ -0,0 +1,343 @@
# Copyright (c) OpenMMLab. All rights reserved.
import os
import os.path as osp
import sys
import time
from contextlib import contextmanager
from typing import Optional
from urllib.parse import urlparse
from urllib.request import urlopen
import cv2
import numpy as np
from torch.hub import HASH_REGEX, download_url_to_file
@contextmanager
def limit_max_fps(fps: Optional[float]):
t_start = time.time()
try:
yield
finally:
t_end = time.time()
if fps is not None:
t_sleep = 1.0 / fps - t_end + t_start
if t_sleep > 0:
time.sleep(t_sleep)
def _is_url(filename):
"""Check if the file is a url link.
Args:
filename (str): the file name or url link.
Returns:
bool: is url or not.
"""
prefixes = ['http://', 'https://']
for p in prefixes:
if filename.startswith(p):
return True
return False
def load_image_from_disk_or_url(filename, readFlag=cv2.IMREAD_COLOR):
"""Load an image file, from disk or url.
Args:
filename (str): file name on the disk or url link.
readFlag (int): readFlag for imdecode.
Returns:
np.ndarray: A loaded image
"""
if _is_url(filename):
# download the image, convert it to a NumPy array, and then read
# it into OpenCV format
resp = urlopen(filename)
image = np.asarray(bytearray(resp.read()), dtype='uint8')
image = cv2.imdecode(image, readFlag)
return image
else:
image = cv2.imread(filename, readFlag)
return image
def mkdir_or_exist(dir_name, mode=0o777):
if dir_name == '':
return
dir_name = osp.expanduser(dir_name)
os.makedirs(dir_name, mode=mode, exist_ok=True)
def get_cached_file_path(url,
save_dir=None,
progress=True,
check_hash=False,
file_name=None):
r"""Loads the Torch serialized object at the given URL.
If downloaded file is a zip file, it will be automatically decompressed
If the object is already present in `model_dir`, it's deserialized and
returned.
The default value of ``model_dir`` is ``<hub_dir>/checkpoints`` where
``hub_dir`` is the directory returned by :func:`~torch.hub.get_dir`.
Args:
url (str): URL of the object to download
save_dir (str, optional): directory in which to save the object
progress (bool, optional): whether or not to display a progress bar
to stderr. Default: True
check_hash(bool, optional): If True, the filename part of the URL
should follow the naming convention ``filename-<sha256>.ext``
where ``<sha256>`` is the first eight or more digits of the
SHA256 hash of the contents of the file. The hash is used to
ensure unique names and to verify the contents of the file.
Default: False
file_name (str, optional): name for the downloaded file. Filename
from ``url`` will be used if not set. Default: None.
"""
if save_dir is None:
save_dir = os.path.join('webcam_resources')
mkdir_or_exist(save_dir)
parts = urlparse(url)
filename = os.path.basename(parts.path)
if file_name is not None:
filename = file_name
cached_file = os.path.join(save_dir, filename)
if not os.path.exists(cached_file):
sys.stderr.write('Downloading: "{}" to {}\n'.format(url, cached_file))
hash_prefix = None
if check_hash:
r = HASH_REGEX.search(filename) # r is Optional[Match[str]]
hash_prefix = r.group(1) if r else None
download_url_to_file(url, cached_file, hash_prefix, progress=progress)
return cached_file
def screen_matting(img, color_low=None, color_high=None, color=None):
"""Screen Matting.
Args:
img (np.ndarray): Image data.
color_low (tuple): Lower limit (b, g, r).
color_high (tuple): Higher limit (b, g, r).
color (str): Support colors include:
- 'green' or 'g'
- 'blue' or 'b'
- 'black' or 'k'
- 'white' or 'w'
"""
if color_high is None or color_low is None:
if color is not None:
if color.lower() == 'g' or color.lower() == 'green':
color_low = (0, 200, 0)
color_high = (60, 255, 60)
elif color.lower() == 'b' or color.lower() == 'blue':
color_low = (230, 0, 0)
color_high = (255, 40, 40)
elif color.lower() == 'k' or color.lower() == 'black':
color_low = (0, 0, 0)
color_high = (40, 40, 40)
elif color.lower() == 'w' or color.lower() == 'white':
color_low = (230, 230, 230)
color_high = (255, 255, 255)
else:
NotImplementedError(f'Not supported color: {color}.')
else:
ValueError('color or color_high | color_low should be given.')
mask = cv2.inRange(img, np.array(color_low), np.array(color_high)) == 0
return mask.astype(np.uint8)
def expand_and_clamp(box, im_shape, s=1.25):
"""Expand the bbox and clip it to fit the image shape.
Args:
box (list): x1, y1, x2, y2
im_shape (ndarray): image shape (h, w, c)
s (float): expand ratio
Returns:
list: x1, y1, x2, y2
"""
x1, y1, x2, y2 = box[:4]
w = x2 - x1
h = y2 - y1
deta_w = w * (s - 1) / 2
deta_h = h * (s - 1) / 2
x1, y1, x2, y2 = x1 - deta_w, y1 - deta_h, x2 + deta_w, y2 + deta_h
img_h, img_w = im_shape[:2]
x1 = min(max(0, int(x1)), img_w - 1)
y1 = min(max(0, int(y1)), img_h - 1)
x2 = min(max(0, int(x2)), img_w - 1)
y2 = min(max(0, int(y2)), img_h - 1)
return [x1, y1, x2, y2]
def _find_connected_components(mask):
"""Find connected components and sort with areas.
Args:
mask (ndarray): instance segmentation result.
Returns:
ndarray (N, 5): Each item contains (x, y, w, h, area).
"""
num, labels, stats, centroids = cv2.connectedComponentsWithStats(mask)
stats = stats[stats[:, 4].argsort()]
return stats
def _find_bbox(mask):
"""Find the bounding box for the mask.
Args:
mask (ndarray): Mask.
Returns:
list(4, ): Returned box (x1, y1, x2, y2).
"""
mask_shape = mask.shape
if len(mask_shape) == 3:
assert mask_shape[-1] == 1, 'the channel of the mask should be 1.'
elif len(mask_shape) == 2:
pass
else:
NotImplementedError()
h, w = mask_shape[:2]
mask_w = mask.sum(0)
mask_h = mask.sum(1)
left = 0
right = w - 1
up = 0
down = h - 1
for i in range(w):
if mask_w[i] > 0:
break
left += 1
for i in range(w - 1, left, -1):
if mask_w[i] > 0:
break
right -= 1
for i in range(h):
if mask_h[i] > 0:
break
up += 1
for i in range(h - 1, up, -1):
if mask_h[i] > 0:
break
down -= 1
return [left, up, right, down]
def copy_and_paste(img,
background_img,
mask,
bbox=None,
effect_region=(0.2, 0.2, 0.8, 0.8),
min_size=(20, 20)):
"""Copy the image region and paste to the background.
Args:
img (np.ndarray): Image data.
background_img (np.ndarray): Background image data.
mask (ndarray): instance segmentation result.
bbox (ndarray): instance bbox, (x1, y1, x2, y2).
effect_region (tuple(4, )): The region to apply mask, the coordinates
are normalized (x1, y1, x2, y2).
"""
background_img = background_img.copy()
background_h, background_w = background_img.shape[:2]
region_h = (effect_region[3] - effect_region[1]) * background_h
region_w = (effect_region[2] - effect_region[0]) * background_w
region_aspect_ratio = region_w / region_h
if bbox is None:
bbox = _find_bbox(mask)
instance_w = bbox[2] - bbox[0]
instance_h = bbox[3] - bbox[1]
if instance_w > min_size[0] and instance_h > min_size[1]:
aspect_ratio = instance_w / instance_h
if region_aspect_ratio > aspect_ratio:
resize_rate = region_h / instance_h
else:
resize_rate = region_w / instance_w
mask_inst = mask[int(bbox[1]):int(bbox[3]), int(bbox[0]):int(bbox[2])]
img_inst = img[int(bbox[1]):int(bbox[3]), int(bbox[0]):int(bbox[2])]
img_inst = cv2.resize(img_inst, (int(
resize_rate * instance_w), int(resize_rate * instance_h)))
mask_inst = cv2.resize(
mask_inst,
(int(resize_rate * instance_w), int(resize_rate * instance_h)),
interpolation=cv2.INTER_NEAREST)
mask_ids = list(np.where(mask_inst == 1))
mask_ids[1] += int(effect_region[0] * background_w)
mask_ids[0] += int(effect_region[1] * background_h)
background_img[tuple(mask_ids)] = img_inst[np.where(mask_inst == 1)]
return background_img
def is_image_file(path):
if isinstance(path, str):
if path.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp')):
return True
return False
class ImageCapture:
"""A mock-up version of cv2.VideoCapture that always return a const image.
Args:
image (str | ndarray): The image or image path
"""
def __init__(self, image):
if isinstance(image, str):
self.image = load_image_from_disk_or_url(image)
else:
self.image = image
def isOpened(self):
return (self.image is not None)
def read(self):
return True, self.image.copy()
def release(self):
pass
def get(self, propId):
if propId == cv2.CAP_PROP_FRAME_WIDTH:
return self.image.shape[1]
elif propId == cv2.CAP_PROP_FRAME_HEIGHT:
return self.image.shape[0]
elif propId == cv2.CAP_PROP_FPS:
return np.nan
else:
raise NotImplementedError()
@@ -0,0 +1,226 @@
# Copyright (c) OpenMMLab. All rights reserved.
from typing import List, Tuple
from mmcv import Config
from mmpose.datasets.dataset_info import DatasetInfo
def get_eye_keypoint_ids(model_cfg: Config) -> Tuple[int, int]:
"""A helpfer function to get the keypoint indices of left and right eyes
from the model config.
Args:
model_cfg (Config): pose model config.
Returns:
int: left eye keypoint index.
int: right eye keypoint index.
"""
left_eye_idx = None
right_eye_idx = None
# try obtaining eye point ids from dataset_info
try:
dataset_info = DatasetInfo(model_cfg.data.test.dataset_info)
left_eye_idx = dataset_info.keypoint_name2id.get('left_eye', None)
right_eye_idx = dataset_info.keypoint_name2id.get('right_eye', None)
except AttributeError:
left_eye_idx = None
right_eye_idx = None
if left_eye_idx is None or right_eye_idx is None:
# Fall back to hard coded keypoint id
dataset_name = model_cfg.data.test.type
if dataset_name in {
'TopDownCocoDataset', 'TopDownCocoWholeBodyDataset'
}:
left_eye_idx = 1
right_eye_idx = 2
elif dataset_name in {'AnimalPoseDataset', 'AnimalAP10KDataset'}:
left_eye_idx = 0
right_eye_idx = 1
else:
raise ValueError('Can not determine the eye keypoint id of '
f'{dataset_name}')
return left_eye_idx, right_eye_idx
def get_face_keypoint_ids(model_cfg: Config) -> Tuple[int, int]:
"""A helpfer function to get the keypoint indices of the face from the
model config.
Args:
model_cfg (Config): pose model config.
Returns:
list[int]: face keypoint index.
"""
face_indices = None
# try obtaining nose point ids from dataset_info
try:
dataset_info = DatasetInfo(model_cfg.data.test.dataset_info)
for id in range(68):
face_indices.append(
dataset_info.keypoint_name2id.get(f'face_{id}', None))
except AttributeError:
face_indices = None
if face_indices is None:
# Fall back to hard coded keypoint id
dataset_name = model_cfg.data.test.type
if dataset_name in {'TopDownCocoWholeBodyDataset'}:
face_indices = list(range(23, 91))
else:
raise ValueError('Can not determine the face id of '
f'{dataset_name}')
return face_indices
def get_wrist_keypoint_ids(model_cfg: Config) -> Tuple[int, int]:
"""A helpfer function to get the keypoint indices of left and right wrist
from the model config.
Args:
model_cfg (Config): pose model config.
Returns:
int: left wrist keypoint index.
int: right wrist keypoint index.
"""
# try obtaining eye point ids from dataset_info
try:
dataset_info = DatasetInfo(model_cfg.data.test.dataset_info)
left_wrist_idx = dataset_info.keypoint_name2id.get('left_wrist', None)
right_wrist_idx = dataset_info.keypoint_name2id.get(
'right_wrist', None)
except AttributeError:
left_wrist_idx = None
right_wrist_idx = None
if left_wrist_idx is None or right_wrist_idx is None:
# Fall back to hard coded keypoint id
dataset_name = model_cfg.data.test.type
if dataset_name in {
'TopDownCocoDataset', 'TopDownCocoWholeBodyDataset'
}:
left_wrist_idx = 9
right_wrist_idx = 10
elif dataset_name == 'AnimalPoseDataset':
left_wrist_idx = 16
right_wrist_idx = 17
elif dataset_name == 'AnimalAP10KDataset':
left_wrist_idx = 7
right_wrist_idx = 10
else:
raise ValueError('Can not determine the eye keypoint id of '
f'{dataset_name}')
return left_wrist_idx, right_wrist_idx
def get_mouth_keypoint_ids(model_cfg: Config) -> Tuple[int, int]:
"""A helpfer function to get the keypoint indices of the left and right
part of mouth from the model config.
Args:
model_cfg (Config): pose model config.
Returns:
int: left-part mouth keypoint index.
int: right-part mouth keypoint index.
"""
# try obtaining mouth point ids from dataset_info
try:
dataset_info = DatasetInfo(model_cfg.data.test.dataset_info)
mouth_index = dataset_info.keypoint_name2id.get('face-62', None)
except AttributeError:
mouth_index = None
if mouth_index is None:
# Fall back to hard coded keypoint id
dataset_name = model_cfg.data.test.type
if dataset_name == 'TopDownCocoWholeBodyDataset':
mouth_index = 85
else:
raise ValueError('Can not determine the eye keypoint id of '
f'{dataset_name}')
return mouth_index
def get_hand_keypoint_ids(model_cfg: Config) -> List[int]:
"""A helpfer function to get the keypoint indices of left and right hand
from the model config.
Args:
model_cfg (Config): pose model config.
Returns:
list[int]: hand keypoint indices.
"""
# try obtaining hand keypoint ids from dataset_info
try:
hand_indices = []
dataset_info = DatasetInfo(model_cfg.data.test.dataset_info)
hand_indices.append(
dataset_info.keypoint_name2id.get('left_hand_root', None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'left_thumb{id}', None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'left_forefinger{id}',
None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'left_middle_finger{id}',
None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'left_ring_finger{id}',
None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'left_pinky_finger{id}',
None))
hand_indices.append(
dataset_info.keypoint_name2id.get('right_hand_root', None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'right_thumb{id}', None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'right_forefinger{id}',
None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'right_middle_finger{id}',
None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'right_ring_finger{id}',
None))
for id in range(1, 5):
hand_indices.append(
dataset_info.keypoint_name2id.get(f'right_pinky_finger{id}',
None))
except AttributeError:
hand_indices = None
if hand_indices is None:
# Fall back to hard coded keypoint id
dataset_name = model_cfg.data.test.type
if dataset_name in {'TopDownCocoWholeBodyDataset'}:
hand_indices = list(range(91, 133))
else:
raise ValueError('Can not determine the hand id of '
f'{dataset_name}')
return hand_indices
@@ -0,0 +1,272 @@
# Copyright (c) OpenMMLab. All rights reserved.
import logging
import sys
import time
import warnings
from contextlib import nullcontext
from threading import Thread
from typing import Dict, List, Optional, Tuple, Union
import cv2
from .nodes import NODES
from .utils import (BufferManager, EventManager, FrameMessage, ImageCapture,
VideoEndingMessage, is_image_file, limit_max_fps)
DEFAULT_FRAME_BUFFER_SIZE = 1
DEFAULT_INPUT_BUFFER_SIZE = 1
DEFAULT_DISPLAY_BUFFER_SIZE = 0
DEFAULT_USER_BUFFER_SIZE = 1
class WebcamRunner():
"""An interface for building webcam application from config.
Parameters:
name (str): Runner name.
camera_id (int | str): The camera ID (usually the ID of the default
camera is 0). Alternatively a file path or a URL can be given
to load from a video or image file.
camera_frame_shape (tuple, optional): Set the frame shape of the
camera in (width, height). If not given, the default frame shape
will be used. This argument is only valid when using a camera
as the input source. Default: None
camera_fps (int): Video reading maximum FPS. Default: 30
buffer_sizes (dict, optional): A dict to specify buffer sizes. The
key is the buffer name and the value is the buffer size.
Default: None
nodes (list): Node configs.
"""
def __init__(self,
name: str = 'Default Webcam Runner',
camera_id: Union[int, str] = 0,
camera_fps: int = 30,
camera_frame_shape: Optional[Tuple[int, int]] = None,
synchronous: bool = False,
buffer_sizes: Optional[Dict[str, int]] = None,
nodes: Optional[List[Dict]] = None):
# Basic parameters
self.name = name
self.camera_id = camera_id
self.camera_fps = camera_fps
self.camera_frame_shape = camera_frame_shape
self.synchronous = synchronous
# self.buffer_manager manages data flow between runner and nodes
self.buffer_manager = BufferManager()
# self.event_manager manages event-based asynchronous communication
self.event_manager = EventManager()
# self.node_list holds all node instance
self.node_list = []
# self.vcap is used to read camera frames. It will be built when the
# runner starts running
self.vcap = None
# Register runner events
self.event_manager.register_event('_exit_', is_keyboard=False)
if self.synchronous:
self.event_manager.register_event('_idle_', is_keyboard=False)
# Register nodes
if not nodes:
raise ValueError('No node is registered to the runner.')
# Register default buffers
if buffer_sizes is None:
buffer_sizes = {}
# _frame_ buffer
frame_buffer_size = buffer_sizes.get('_frame_',
DEFAULT_FRAME_BUFFER_SIZE)
self.buffer_manager.register_buffer('_frame_', frame_buffer_size)
# _input_ buffer
input_buffer_size = buffer_sizes.get('_input_',
DEFAULT_INPUT_BUFFER_SIZE)
self.buffer_manager.register_buffer('_input_', input_buffer_size)
# _display_ buffer
display_buffer_size = buffer_sizes.get('_display_',
DEFAULT_DISPLAY_BUFFER_SIZE)
self.buffer_manager.register_buffer('_display_', display_buffer_size)
# Build all nodes:
for node_cfg in nodes:
logging.info(f'Create node: {node_cfg.name}({node_cfg.type})')
node = NODES.build(node_cfg)
# Register node
self.node_list.append(node)
# Register buffers
for buffer_info in node.registered_buffers:
buffer_name = buffer_info.buffer_name
if buffer_name in self.buffer_manager:
continue
buffer_size = buffer_sizes.get(buffer_name,
DEFAULT_USER_BUFFER_SIZE)
self.buffer_manager.register_buffer(buffer_name, buffer_size)
logging.info(
f'Register user buffer: {buffer_name}({buffer_size})')
# Register events
for event_info in node.registered_events:
self.event_manager.register_event(
event_name=event_info.event_name,
is_keyboard=event_info.is_keyboard)
logging.info(f'Register event: {event_info.event_name}')
# Set runner for nodes
# This step is performed after node building when the runner has
# create full buffer/event managers and can
for node in self.node_list:
logging.info(f'Set runner for node: {node.name})')
node.set_runner(self)
def _read_camera(self):
"""Continually read video frames and put them into buffers."""
camera_id = self.camera_id
fps = self.camera_fps
# Build video capture
if is_image_file(camera_id):
self.vcap = ImageCapture(camera_id)
else:
self.vcap = cv2.VideoCapture(camera_id)
if self.camera_frame_shape is not None:
width, height = self.camera_frame_shape
self.vcap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.vcap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
if not self.vcap.isOpened():
warnings.warn(f'Cannot open camera (ID={camera_id})')
sys.exit()
# Read video frames in a loop
first_frame = True
while not self.event_manager.is_set('_exit_'):
if self.synchronous:
if first_frame:
cm = nullcontext()
else:
# Read a new frame until the last frame has been processed
cm = self.event_manager.wait_and_handle('_idle_')
else:
# Read frames with a maximum FPS
cm = limit_max_fps(fps)
first_frame = False
with cm:
# Read a frame
ret_val, frame = self.vcap.read()
if ret_val:
# Put frame message (for display) into buffer `_frame_`
frame_msg = FrameMessage(frame)
self.buffer_manager.put('_frame_', frame_msg)
# Put input message (for model inference or other use)
# into buffer `_input_`
input_msg = FrameMessage(frame.copy())
input_msg.update_route_info(
node_name='Camera Info',
node_type='dummy',
info=self._get_camera_info())
self.buffer_manager.put_force('_input_', input_msg)
else:
# Put a video ending signal
self.buffer_manager.put('_frame_', VideoEndingMessage())
self.vcap.release()
def _display(self):
"""Continually obtain and display output frames."""
output_msg = None
while not self.event_manager.is_set('_exit_'):
while self.buffer_manager.is_empty('_display_'):
time.sleep(0.001)
# Set _idle_ to allow reading next frame
if self.synchronous:
self.event_manager.set('_idle_')
# acquire output from buffer
output_msg = self.buffer_manager.get('_display_')
# None indicates input stream ends
if isinstance(output_msg, VideoEndingMessage):
self.event_manager.set('_exit_')
break
img = output_msg.get_image()
# show in a window
cv2.imshow(self.name, img)
# handle keyboard input
key = cv2.waitKey(1)
if key != -1:
self._on_keyboard_input(key)
cv2.destroyAllWindows()
def _on_keyboard_input(self, key):
"""Handle the keyboard input."""
if key in (27, ord('q'), ord('Q')):
logging.info(f'Exit event captured: {key}')
self.event_manager.set('_exit_')
else:
logging.info(f'Keyboard event captured: {key}')
self.event_manager.set(key, is_keyboard=True)
def _get_camera_info(self):
"""Return the camera information in a dict."""
frame_width = self.vcap.get(cv2.CAP_PROP_FRAME_WIDTH)
frame_height = self.vcap.get(cv2.CAP_PROP_FRAME_HEIGHT)
frame_rate = self.vcap.get(cv2.CAP_PROP_FPS)
cam_info = {
'Camera ID': self.camera_id,
'Source resolution': f'{frame_width}x{frame_height}',
'Source FPS': frame_rate,
}
return cam_info
def run(self):
"""Program entry.
This method starts all nodes as well as video I/O in separate threads.
"""
try:
# Start node threads
non_daemon_nodes = []
for node in self.node_list:
node.start()
if not node.daemon:
non_daemon_nodes.append(node)
# Create a thread to read video frames
t_read = Thread(target=self._read_camera, args=())
t_read.start()
# Run display in the main thread
self._display()
logging.info('Display shut down')
# joint non-daemon nodes and runner threads
logging.info('Camera reading about to join')
t_read.join()
for node in non_daemon_nodes:
logging.info(f'Node {node.name} about to join')
node.join()
except KeyboardInterrupt:
pass