fffiloni commited on
Commit
09462dc
·
verified ·
1 Parent(s): 349dbfe

Migrated files batch 1

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +27 -0
  2. .gitmodules +3 -0
  3. LICENSE +201 -0
  4. ORIGINAL_README.md +434 -0
  5. SCAIL-Pose/.github/ISSUE_TEMPLATE/bug_report.yaml +72 -0
  6. SCAIL-Pose/.github/ISSUE_TEMPLATE/feature-request.yaml +34 -0
  7. SCAIL-Pose/DWPoseProcess/AAUtils.py +305 -0
  8. SCAIL-Pose/DWPoseProcess/checkUtils.py +263 -0
  9. SCAIL-Pose/DWPoseProcess/dwpose/__init__.py +129 -0
  10. SCAIL-Pose/DWPoseProcess/dwpose/onnxdet.py +134 -0
  11. SCAIL-Pose/DWPoseProcess/dwpose/onnxpose.py +487 -0
  12. SCAIL-Pose/DWPoseProcess/dwpose/util.py +378 -0
  13. SCAIL-Pose/DWPoseProcess/dwpose/wholebody.py +58 -0
  14. SCAIL-Pose/DWPoseProcess/extractUtils.py +75 -0
  15. SCAIL-Pose/LICENSE +201 -0
  16. SCAIL-Pose/NLFPoseExtract/align3d.py +138 -0
  17. SCAIL-Pose/NLFPoseExtract/debug_nlf.py +10 -0
  18. SCAIL-Pose/NLFPoseExtract/extract_nlfpose_batch.py +365 -0
  19. SCAIL-Pose/NLFPoseExtract/nlf_draw.py +135 -0
  20. SCAIL-Pose/NLFPoseExtract/nlf_render.py +658 -0
  21. SCAIL-Pose/NLFPoseExtract/process_animation_aio.py +236 -0
  22. SCAIL-Pose/NLFPoseExtract/process_replacement.py +235 -0
  23. SCAIL-Pose/NLFPoseExtract/reshape_utils_3d.py +304 -0
  24. SCAIL-Pose/NLFPoseExtract/v1_process_pose.py +313 -0
  25. SCAIL-Pose/NLFPoseExtract/v1_process_pose_multi.py +243 -0
  26. SCAIL-Pose/NLFPoseExtract/v2_helper.py +233 -0
  27. SCAIL-Pose/README.md +197 -0
  28. SCAIL-Pose/TrackSam3/track.py +355 -0
  29. SCAIL-Pose/pose_draw/draw_3d_utils.py +220 -0
  30. SCAIL-Pose/pose_draw/draw_pose_utils.py +169 -0
  31. SCAIL-Pose/pose_draw/draw_utils.py +658 -0
  32. SCAIL-Pose/pose_draw/reshape_utils.py +444 -0
  33. SCAIL-Pose/render_3d/render_cylinder.py +129 -0
  34. SCAIL-Pose/render_3d/taichi_cylinder.py +217 -0
  35. SCAIL-Pose/requirements.txt +23 -0
  36. SCAIL-Pose/resources/animation.png +3 -0
  37. SCAIL-Pose/resources/data.png +3 -0
  38. SCAIL-Pose/resources/pose_comp.png +3 -0
  39. SCAIL-Pose/resources/pose_result.png +3 -0
  40. SCAIL-Pose/resources/pose_teaser.png +3 -0
  41. SCAIL-Pose/resources/preteaser.png +3 -0
  42. SCAIL-Pose/resources/teaser.png +3 -0
  43. configs/config-1.3b.json +14 -0
  44. configs/config-14b.json +15 -0
  45. convert.py +199 -0
  46. examples/animation_001/combined.gif +3 -0
  47. examples/animation_001/driving.mp4 +3 -0
  48. examples/animation_001/ref.jpg +3 -0
  49. examples/animation_001/ref_mask.jpg +0 -0
  50. examples/animation_001/rendered_mask_v2.mp4 +3 -0
.gitattributes CHANGED
@@ -33,3 +33,30 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ SCAIL-Pose/resources/animation.png filter=lfs diff=lfs merge=lfs -text
37
+ SCAIL-Pose/resources/data.png filter=lfs diff=lfs merge=lfs -text
38
+ SCAIL-Pose/resources/pose_comp.png filter=lfs diff=lfs merge=lfs -text
39
+ SCAIL-Pose/resources/pose_result.png filter=lfs diff=lfs merge=lfs -text
40
+ SCAIL-Pose/resources/pose_teaser.png filter=lfs diff=lfs merge=lfs -text
41
+ SCAIL-Pose/resources/preteaser.png filter=lfs diff=lfs merge=lfs -text
42
+ SCAIL-Pose/resources/teaser.png filter=lfs diff=lfs merge=lfs -text
43
+ examples/animation_001/combined.gif filter=lfs diff=lfs merge=lfs -text
44
+ examples/animation_001/driving.mp4 filter=lfs diff=lfs merge=lfs -text
45
+ examples/animation_001/ref.jpg filter=lfs diff=lfs merge=lfs -text
46
+ examples/animation_001/rendered_mask_v2.mp4 filter=lfs diff=lfs merge=lfs -text
47
+ examples/animation_001/rendered_v2.mp4 filter=lfs diff=lfs merge=lfs -text
48
+ examples/animation_001_posedriven/combined.gif filter=lfs diff=lfs merge=lfs -text
49
+ examples/animation_001_posedriven/driving.mp4 filter=lfs diff=lfs merge=lfs -text
50
+ examples/animation_001_posedriven/ref.jpg filter=lfs diff=lfs merge=lfs -text
51
+ examples/animation_001_posedriven/rendered_mask_v2.mp4 filter=lfs diff=lfs merge=lfs -text
52
+ examples/animation_001_posedriven/rendered_v2.mp4 filter=lfs diff=lfs merge=lfs -text
53
+ examples/animation_002/driving.mp4 filter=lfs diff=lfs merge=lfs -text
54
+ examples/animation_002/ref.jpg filter=lfs diff=lfs merge=lfs -text
55
+ examples/animation_002/rendered_mask_v2.mp4 filter=lfs diff=lfs merge=lfs -text
56
+ examples/animation_002/rendered_v2.mp4 filter=lfs diff=lfs merge=lfs -text
57
+ examples/animation_003_multi_ref/background.png filter=lfs diff=lfs merge=lfs -text
58
+ examples/animation_003_multi_ref/character_0.png filter=lfs diff=lfs merge=lfs -text
59
+ examples/animation_003_multi_ref/character_1.png filter=lfs diff=lfs merge=lfs -text
60
+ examples/animation_003_multi_ref/driving.mp4 filter=lfs diff=lfs merge=lfs -text
61
+ examples/animation_003_multi_ref/ref.png filter=lfs diff=lfs merge=lfs -text
62
+ examples/animation_003_multi_ref/ref_mask.jpg filter=lfs diff=lfs merge=lfs -text
.gitmodules ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [submodule "SCAIL-Pose"]
2
+ path = SCAIL-Pose
3
+ url = https://github.com/zai-org/SCAIL-Pose
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Zhipu AI
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
ORIGINAL_README.md ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <h1>SCAIL-2: Unifying Controlled Character Animation with End-to-end In-Context Conditioning</h1>
2
+
3
+
4
+ <div align="center">
5
+ <a href="https://arxiv.org/abs/2606.10804">
6
+ <img src="https://img.shields.io/badge/arXiv-2606.10804-red" alt="arXiv">
7
+ </a>
8
+ <a href="https://huggingface.co/zai-org/SCAIL-2">
9
+ <img src="https://img.shields.io/badge/%F0%9F%A4%97%20HuggingFace-SCAIL--2-yellow" alt="HuggingFace">
10
+ </a>
11
+ <a href="https://teal024.github.io/SCAIL-2/">
12
+ <img src="https://img.shields.io/badge/Project%20Page-SCAIL--2-green" alt="Project Page">
13
+ </a>
14
+ <a href="#datasets">
15
+ <img src="https://img.shields.io/badge/Datasets-MotionPair-blue" alt="Datasets">
16
+ </a>
17
+ </div>
18
+
19
+
20
+
21
+ This repository contains the official implementation code of SCAIL-2: Unifying Controlled Character Animation with End-to-end In-Context Conditioning. The code is for the inference of SCAIL-2 Model, an open-source model to support **End-to-End** Character Animation.
22
+
23
+
24
+ <p align="center">
25
+ <img src='resources/teaser.png' alt='Teaser' width='90%'>
26
+ </p>
27
+
28
+ ## 🔎 Introduction
29
+ SCAIL-1 identifies the key bottlenecks that hinder character animation towards production level: how to represent the pose and how to inject the pose. However, the reliance on intermediate pose representation still hinders the model towards complex motion and generalizable identity. We define the issue as over reliance on intermediates.
30
+
31
+ As intermediates, skeleton maps suffer from inherent ambiguity under complex scenarios. Further, it restricts the driving source to be exocentric human movements and thus cannot handle driving sources like animals. Character replacement and multi-character animation suffers from similar issues, where state-of-the-art methods use inpainting masks, but such masks are still a form of intermediates and limits the application and bounds the performance.
32
+
33
+
34
+ <p align="center">
35
+ <img src='resources/preteaser.png' alt='preteaser' width='50%'>
36
+ </p>
37
+
38
+
39
+
40
+ To bypass intermediate pose representation, we utilize several off-the-shelf models, including [SCAIL-Preview](https://github.com/zai-org/SCAIL), [Wan-Animate](https://github.com/Wan-Video/Wan2.2), [MoCha](https://github.com/Orange-3DV-Team/MoCha) to synthesize 60K motion pairs. By designing a Unified Motion Transfer Interface containing 2 type of masking channels and a dedicated RoPE design, we support training with all those data. We utilize **reserve driving**, so that the model can learn capabilities beyond those models. From the data composition and the training recipe, the final model yield emergent capabilities. For example, it supports cross-identity replacement, animal-driving scenarios, and support more advanced control intermediate like [SAM3D-Body](https://github.com/facebookresearch/sam-3d-body)'s mesh rendering in zero-shot manner.
41
+
42
+ <p align="center">
43
+ <img src='resources/pipeline.png' alt='pipeline' width='90%'>
44
+ </p>
45
+
46
+ <p align="center">
47
+ <img src='resources/network.png' alt='Teaser' width='90%'>
48
+ </p>
49
+
50
+ We model the bias of pose-driven generators as preference and introduce Bias-Aware DPO, a novel mechanisim to further improve details. The DPO LoRA is also released at HuggingFace and can be enabled in this repo as well as ComfyUI implementations.
51
+
52
+
53
+ ## 🎨 Community Works
54
+
55
+ ❤️ We thank the community for sharing their amazing creations! Special thanks to Ablejones (Discord), 机智波, 肥猴, 绘篇AI手艺 (Bilibili), Fuzzy-Mastodon (Reddit). Audio comes from reference videos.
56
+
57
+ <table align="center" width="100%">
58
+ <tr>
59
+ <td width="27%"><video src="https://github.com/user-attachments/assets/76380c62-abdf-4149-82d7-feccaa3a380d" controls width="100%"></video></td>
60
+ <td width="27%"><video src="https://github.com/user-attachments/assets/80d37479-63ed-45f5-8c93-7d3598b6275d" controls width="100%"></video></td>
61
+ <td width="45%"><video src="https://github.com/user-attachments/assets/39d07740-5089-4d54-843e-e6311c1639f3" controls width="100%"></video></td>
62
+ </tr>
63
+ <tr>
64
+ <td width="50%" colspan="2"><video src="https://github.com/user-attachments/assets/9f8083c7-60af-4735-98d2-7dcfcccbab76" controls width="100%"></video></td>
65
+ <td width="40%" colspan="1"><video src="https://github.com/user-attachments/assets/9fb9d894-1030-448f-a3f7-71022f359ccb" controls width="100%"></video></td>
66
+ </tr>
67
+ </table>
68
+
69
+
70
+ ## 🚀 Getting Started
71
+
72
+
73
+ ### Mask Semantics
74
+ The mask is a critical input to SCAIL-2 even in Animation Mode. To visualize the channels that the mask is actually for, we encode them with colors:
75
+
76
+ - **Black** — tells the model the background at this location should *not* be visible.
77
+ - **White** — tells the model the background at this location *should* be visible.
78
+ - **Color** — encodes the correspondence between character regions and the driving motion.
79
+
80
+ Animation mode (end-to-end) example (left: reference mask, right: driving mask):
81
+
82
+ <p align="center">
83
+ <img src='examples/animation_001/combined.gif' alt='animation mask example' width='60%'>
84
+ </p>
85
+
86
+ <p align="center">
87
+ <img src='resources/multi_combination.gif' alt='multi animation mask example' width='60%'>
88
+ </p>
89
+
90
+
91
+ Animation mode (pose-driven) example (left: reference mask, right: driving mask):
92
+
93
+ <p align="center">
94
+ <img src='examples/animation_001_posedriven/combined.gif' alt='pose-driven animation mask example' width='60%'>
95
+ </p>
96
+
97
+ Replacement mode example (left: reference mask, right: driving mask):
98
+
99
+ <p align="center">
100
+ <img src='examples/replace_001/combined.gif' alt='replacement mask example' width='60%'>
101
+ </p>
102
+
103
+ Without a correct mask Animation mode collapse into Replacement-Mode behavior in certain inputs.
104
+
105
+ The masks also enable zero-shot multi-reference generation, where additional visual inputs provide information that single reference may not cover, such as back view, close-up view and occluded background. According to the color assignment logic, in multi-reference the following inputs get the corresponding masks as shown below:
106
+
107
+ <table>
108
+ <tr>
109
+ <td align="center"><img src="examples/animation_003_multi_ref/character_0.png" width="150"/></td>
110
+ <td align="center"><img src="examples/animation_003_multi_ref/character_1.png" width="150"/></td>
111
+ <td align="center"><img src="examples/animation_003_multi_ref/background.png" width="150"/></td>
112
+ <td align="center"><img src="examples/animation_003_multi_ref/ref.png" width="150"/></td>
113
+ </tr>
114
+ <tr>
115
+ <td align="center"><img src="examples/animation_003_multi_ref/character_0_mask.png" width="150"/></td>
116
+ <td align="center"><img src="examples/animation_003_multi_ref/character_1_mask.png" width="150"/></td>
117
+ <td align="center"><img src="examples/animation_003_multi_ref/background_mask.png" width="150"/></td>
118
+ <td align="center"><img src="examples/animation_003_multi_ref/ref_mask.jpg" width="150"/></td>
119
+ </tr>
120
+ </table>
121
+
122
+
123
+ ### Checkpoints Download
124
+
125
+ | ckpts | Download Link | Notes |
126
+ |--------------|------------------------------------------------------------------------------------------------------------------------------|-------------------------------|
127
+ | SCAIL-2 | [🤗 Hugging Face](https://huggingface.co/zai-org/SCAIL-2) <br> [🤖 ModelScope](https://modelscope.cn/models/ZhipuAI/SCAIL-2) | Trained with mixed resolutions and fps. <br> End-to-end driven supports both 512p and 704p. <br> Pose-driven performs better under 704p. <br> H and W should be both divisible by 32<br> (e.g. 704*1280) if using other resolutions. |
128
+
129
+ Use the following commands to download the model weights
130
+ (We have integrated both Wan VAE and T5 modules into this checkpoint for convenience).
131
+
132
+ ```bash
133
+ hf download zai-org/SCAIL-2
134
+ ```
135
+ The files should be organized like:
136
+ ```
137
+ SCAIL-2/
138
+ ├── Wan2.1_VAE.pth
139
+ ├── model
140
+ │ ├── 1
141
+ │ │ └── fsdp2_rank_0000_checkpoint.pt
142
+ │ └── latest
143
+ └── umt5-xxl
144
+ ├── ...
145
+ ```
146
+
147
+ The model weights are intended for `sat` branch, for usage in `wan` branch, convert to `safetensors` format:
148
+ ```bash
149
+ python convert.py --scail-dir /path/to/SCAIL-2 --save-path /path/to/SCAIL-2.safetensors
150
+ ```
151
+
152
+ ### Environment Setup
153
+ Please make sure your Python version is between 3.10 and 3.12, inclusive of both 3.10 and 3.12.
154
+ ```
155
+ pip install -r requirements.txt
156
+ ```
157
+
158
+
159
+
160
+
161
+ ### Input Preparation
162
+
163
+ `SCAIL-Pose` contains the preprocessing code used to prepare SCAIL-2 inputs, including pose extraction, pose rendering, reference masks, and driving-video masks. It can prepare both animation inputs and character replacement inputs. The submodule should live under the project root:
164
+
165
+ ```
166
+ SCAIL-2/
167
+ ├── generate.py
168
+ ├── examples/
169
+ ├── SCAIL-Pose/
170
+ └── ...
171
+ ```
172
+
173
+ After cloning this repository, initialize the submodule:
174
+
175
+ ```bash
176
+ git submodule update --init --recursive
177
+ ```
178
+
179
+ Enter the submodule and follow its environment setup. `SCAIL-Pose` recommends an OpenMMLab/MMPose environment, then installing its own requirements:
180
+
181
+ ```bash
182
+ cd SCAIL-Pose
183
+ pip install -r requirements.txt
184
+ ```
185
+
186
+ Download the pose-preprocessing weights inside `SCAIL-Pose/pretrained_weights`. The required layout is:
187
+
188
+ ```
189
+ pretrained_weights/
190
+ ├── nlf_l_multi_0.3.2.torchscript
191
+ └── DWPose/
192
+ ├── dw-ll_ucoco_384.onnx
193
+ └── yolox_l.onnx
194
+ ```
195
+
196
+ For SCAIL-2 animation, `SCAIL-Pose` provides an all-in-one preprocessing entrypoint:
197
+
198
+ ```bash
199
+ # Recommended end-to-end mode: rendered_v2.mp4 is the driving video copy,
200
+ # and the mask video is generated from SAM3 masks.
201
+ python NLFPoseExtract/process_animation_aio.py --subdir /path/to/input --e2e_mode
202
+
203
+ # Pose-driven mode: runs NLF + DWPose and writes a skeleton render.
204
+ python NLFPoseExtract/process_animation_aio.py --subdir /path/to/input
205
+ ```
206
+
207
+ For character replacement, use:
208
+
209
+ ```bash
210
+ python NLFPoseExtract/process_replacement.py --subdir /path/to/input
211
+
212
+ # If the driving video has multiple people and only one should be replaced:
213
+ python NLFPoseExtract/process_replacement.py --subdir /path/to/input --matchnearest
214
+ ```
215
+
216
+ The preprocessing outputs are written back to the example folder and can be passed to `generate.py` as `--image`, `--mask_image`, `--pose`, and `--mask_video`.
217
+
218
+
219
+ ## 🦾 Usage
220
+ ### Generate Input Conditions
221
+
222
+ `generate.py` runs one SCAIL-2 inference job from four local input files:
223
+
224
+ ```
225
+ examples/001/
226
+ ├── ref.jpg # reference character image
227
+ ├── ref_mask.jpg # foreground mask of the reference image
228
+ ├── rendered_v2.mp4 # driving / pose video consumed by --pose
229
+ └── rendered_mask_v2.mp4 # per-frame driving mask consumed by --mask_video
230
+ ```
231
+
232
+ The paths passed to `--image`, `--mask_image`, `--pose`, and `--mask_video` must exist. The script checks them before loading the image/video data.
233
+
234
+ For animation mode, `--pose` can be an end-to-end driving video or a pose-rendered video, depending on how the sample was prepared. `--mask_video` should be the corresponding per-frame foreground/control mask. For replacement mode, pass `--replace_flag` and provide the replacement-region mask through `--mask_video`.
235
+
236
+ ### Prompt Semantics
237
+
238
+ For both animation and character replacement, `--prompt` should describe the generated video itself. It should not be an instruction to the model.
239
+
240
+ For replacement tasks, the prompt should describe the video after replacement has already happened. For better results, describe the replacement character's visible clothing and appearance, and include objects the character interacts with or stays close to in the video, such as tools, instruments, chairs, tables, vehicles, doors, or handheld items.
241
+
242
+ ### Character Replacement Prompt Enhancer
243
+
244
+ We provide an optional Gemini-based helper, `prompt_enhancer.py`, to turn a short replacement instruction into a positive prompt for `generate.py`. The helper samples frames from the source video, reads the replacement reference image, uses few-shot examples from `prompt_examples.txt`, and outputs a long English description of the replaced video.
245
+
246
+ `google-genai` is not installed by default in `requirements.txt`. Install it before using the enhancer:
247
+
248
+ ```bash
249
+ pip install google-genai
250
+ ```
251
+
252
+ Set a Gemini API key before running.
253
+
254
+ ```bash
255
+ export GEMINI_API_KEY=your_api_key
256
+ ```
257
+
258
+ Example:
259
+
260
+ ```bash
261
+ python prompt_enhancer.py \
262
+ --video /path/to/driving.mp4 \
263
+ --image /path/to/ref.png \
264
+ --instruction "replace the man in the blue jacket in the video with the person in the image" \
265
+ --examples prompt_examples.txt \
266
+ --num_frames 8 \
267
+ --output enhanced_prompt.txt \
268
+ --caption_out source_caption.txt
269
+ ```
270
+
271
+ The `--instruction` argument is only for Gemini, so it can say who should be replaced by whom. The file written to `--output` is the positive generated-video description that should be passed to `generate.py --prompt`; the enhancer is instructed to include useful SCAIL-2 prompt details such as the replacement character's clothing and objects the character interacts with.
272
+
273
+ Use the enhanced prompt for replacement inference:
274
+
275
+ ```bash
276
+ python generate.py \
277
+ --model SCAIL-14B \
278
+ --ckpt_dir /path/to/SCAIL-2 \
279
+ --scail_path /path/to/SCAIL-2.safetensors \
280
+ --replace_flag \
281
+ --target_w 896 --target_h 512 \
282
+ --image /path/to/ref.png \
283
+ --mask_image /path/to/ref_mask.png \
284
+ --pose /path/to/driving.mp4 \
285
+ --mask_video /path/to/replace_mask.mp4 \
286
+ --prompt "$(cat enhanced_prompt.txt)" \
287
+ --save_file replacement_output.mp4
288
+ ```
289
+
290
+ `prompt_examples.txt` is used as few-shot style guidance. Add more examples there if you want the enhanced prompts to follow a different level of detail or wording.
291
+
292
+ ### Single-GPU Inference
293
+
294
+ Run inference directly with `generate.py`:
295
+
296
+ Example for animation:
297
+
298
+ ```bash
299
+ python generate.py \
300
+ --model SCAIL-14B \
301
+ --ckpt_dir /path/to/SCAIL-2 \
302
+ --scail_path /path/to/SCAIL-2.safetensors \
303
+ --target_w 896 --target_h 512 \
304
+ --image examples/001/ref.jpg \
305
+ --mask_image examples/001/ref_mask.jpg \
306
+ --pose examples/001/rendered_v2.mp4 \
307
+ --mask_video examples/001/rendered_mask_v2.mp4 \
308
+ --prompt "The girl is dancing" \
309
+ --save_file output.mp4
310
+ ```
311
+
312
+ Example for replacement:
313
+
314
+ ```bash
315
+ python generate.py \
316
+ --model SCAIL-14B \
317
+ --ckpt_dir /path/to/SCAIL-2 \
318
+ --scail_path /path/to/SCAIL-2.safetensors \
319
+ --target_w 896 --target_h 512 \
320
+ --image examples/replace_001/ref.png \
321
+ --mask_image examples/replace_001/ref_mask.png \
322
+ --pose examples/replace_001/rendered_v2.mp4 \
323
+ --mask_video examples/replace_001/replace_mask.mp4 \
324
+ --prompt "A blond white male wearing a black suit, trousers, and leather shoes is playing the violin on the street while pedestrians walk past him." \
325
+ --save_file output.mp4 \
326
+ --replace_flag
327
+ ```
328
+
329
+ Useful sampling options:
330
+
331
+ - `--sample_steps`: number of denoising steps. Defaults to `40`.
332
+ - `--sample_shift`: flow-matching scheduler shift. Defaults to `3.0` if not specified.
333
+ - `--sample_guide_scale`: classifier-free guidance scale. Defaults to `5.0`.
334
+ - `--sample_solver`: `unipc` or `dpm++`. Defaults to `unipc`.
335
+ - `--offload_model`: whether to offload model components between stages. For single-process inference, the default is `True`.
336
+
337
+ Note that SCAIL-2 is trained with long, detailed prompts. Short prompts or an empty prompt can run, but detailed descriptions of the reference subject and motion usually produce better results.
338
+
339
+
340
+ ### LoRA Integrations
341
+
342
+ If you use a Lightx2v LoRA checkpoint, pass it with `--lora_path` and set its strength with `--lora_alpha`:
343
+
344
+ ```bash
345
+ python generate.py \
346
+ --model SCAIL-14B \
347
+ --ckpt_dir /path/to/SCAIL-2 \
348
+ --scail_path /path/to/SCAIL-2.safetensors \
349
+ --lora_path Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank128_bf16.safetensors \
350
+ --lora_alpha 1.0 \
351
+ --sample_steps 8 \
352
+ --sample_shift 1 \
353
+ --sample_guide_scale 1.0 \
354
+ --target_w 896 --target_h 512 \
355
+ --image examples/001/ref.jpg \
356
+ --mask_image examples/001/ref_mask.jpg \
357
+ --pose examples/001/rendered_v2.mp4 \
358
+ --mask_video examples/001/rendered_mask_v2.mp4 \
359
+ --prompt "The girl is dancing" \
360
+ --save_file output.mp4
361
+ ```
362
+
363
+ For the DPO LoRA, you can checkout the [`sat-scail2`](https://github.com/zai-org/SCAIL-2/tree/sat-scail2) branch to fully reproduce original results, or convert it into this branch after format matching. The DPO LoRA does not only alleviate hands distortion, but also improved the synchronization of the lips and eyes. If you use ComfyUI, here is a brief comparison showing the effect of the DPO LoRA in Kijai's ComfyUI Workflow:
364
+
365
+
366
+ <p align="center"><video src="https://github.com/user-attachments/assets/bfaf6ea8-6190-4b92-a002-2bfc2b523544" controls width="90%"></video></p>
367
+
368
+
369
+
370
+
371
+
372
+ ### Experimental Functions: Multi-Reference
373
+
374
+ SCAIL-2 supports zero-shot multi-reference inference though not optimized for it. Extra references are optional images that provide additional visual evidence, such as another view of the character, a close-up of clothing details, or a clean background reference. Pass them with `--additional_ref_image` and pass one mask for each image with `--additional_ref_mask_image`. The two lists must have the same length and are paired position by position.
375
+
376
+ Choose each extra-reference mask according to the mask semantics described above:
377
+ - For a clean background reference whose visible background should be preserved, use a **white** mask over the valid background area. If the background is not occluded by the character, a full-white mask is usually appropriate.
378
+ - For extra character references where the background is different from the target scene, keep the character/control region in the semantic mask color and make the unrelated background **black**, so the model does not treat that background as visible target content.
379
+ - Use consistent mask colors for the same character or region across the main reference, extra references, and driving mask when you want them to refer to the same subject.
380
+ The following code provides a simple example of multi-reference inference:
381
+
382
+
383
+ ```bash
384
+ python generate.py \
385
+ --model SCAIL-14B \
386
+ --ckpt_dir /path/to/SCAIL-2 \
387
+ --scail_path /path/to/SCAIL-2.safetensors \
388
+ --target_w 896 --target_h 512 \
389
+ --image examples/animation_003_multi_ref/ref.png \
390
+ --mask_image examples/animation_003_multi_ref/ref_mask.jpg \
391
+ --pose examples/animation_003_multi_ref/rendered_v2.mp4 \
392
+ --mask_video examples/animation_003_multi_ref/rendered_mask_v2.mp4 \
393
+ --additional_ref_image \
394
+ examples/animation_003_multi_ref/background.png \
395
+ examples/animation_003_multi_ref/character_1.png \
396
+ examples/animation_003_multi_ref/character_0.png \
397
+ --additional_ref_mask_image \
398
+ examples/animation_003_multi_ref/background_mask.png \
399
+ examples/animation_003_multi_ref/character_1_mask.png \
400
+ examples/animation_003_multi_ref/character_0_mask.png \
401
+ --prompt "An anime style character with yellow hair, wearing a white and green sailor uniform and a green skirt, is dancing in a warm anime-style classroom." \
402
+ --save_file output_multi_ref.mp4
403
+ ```
404
+
405
+ However, as the model is not optimized for such inputs, video qualities may degrade even though additional information do get referenced. To address this, mocking those reference images as videos reduce degradation and artifacts. We specially thanks [wuwukasi](https://github.com/wuwukaka) and [iceage](https://github.com/user2318) for the collaboration to provide empircal results and implementations to support the findings. Check their refined implementations here: [WanAnimatePlus](https://github.com/wuwukaka/ComfyUI-WanAnimatePlus) and [CustomNodeKit](https://github.com/user2318/ComfyUI-CustomNodeKit/), where they will provide their workflows for SCAIL-2's multi-ref mode.
406
+
407
+
408
+ <a id="datasets"></a>
409
+
410
+ ## 🗃️ Datasets
411
+ We provide a large subset of the **MotionPair** dataset used to train SCAIL-2. To request access, please [fill out this form](https://docs.google.com/forms/d/e/1FAIpQLSfZjC0fZmiYFYHg90_79Yl45ipQLfR8ZhOAahOs19nO8nMvxA/viewform?usp=sharing&ouid=108574921907991336711) and agree to the terms of use. If you have not received a reply within a week after submitting the form, feel free to follow up at teal024@foxmail.com.
412
+
413
+
414
+ ## ✨ Acknowledgements
415
+ Our implementation is built upon the foundation of [Wan 2.1](https://github.com/Wan-Video/Wan2.1) and the overall project architecture is inherited from [SCAIL](https://github.com/zai-org/SCAIL). We specially thank [Wan-Animate](https://github.com/Wan-Video/Wan2.2), [MoCha](https://github.com/Orange-3DV-Team/MoCha) as supplement data generators besides SCAIL to make MotionPair-60K. We also thank [HuMo Dataset](https://github.com/Phantom-video/HuMo) as the high-quality source video provider.
416
+
417
+ ## 📄 Citation
418
+
419
+ If you find this work useful in your research, please cite:
420
+
421
+ ```bibtex
422
+ @misc{yan2026scail2,
423
+ title={SCAIL-2: Unifying Controlled Character Animation with End-to-end In-Context Conditioning},
424
+ author={Wenhao Yan and Fengjia Guo and Zhuoyi Yang and Jie Tang},
425
+ year={2026},
426
+ eprint={2606.10804},
427
+ archivePrefix={arXiv},
428
+ primaryClass={cs.CV},
429
+ url={https://arxiv.org/abs/2606.10804},
430
+ }
431
+ ```
432
+
433
+ ## 🗝️ License
434
+ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
SCAIL-Pose/.github/ISSUE_TEMPLATE/bug_report.yaml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "\U0001F41B Bug Report"
2
+ description: Submit a bug report to help us improve SCAIL-Pose / 提交一个 Bug 问题报告来帮助我们改进 SCAIL-Pose
3
+ body:
4
+ - type: textarea
5
+ id: system-info
6
+ attributes:
7
+ label: System Info / 系統信息
8
+ description: Your operating environment / 您的运行环境信息
9
+ placeholder: Includes Cuda version, Transformers version, Python version, operating system, hardware information (if you suspect a hardware problem)... / 包括Cuda版本,Transformers版本,Python版本,操作系统,硬件信息(如果您怀疑是硬件方面的问题)...
10
+ validations:
11
+ required: true
12
+
13
+ - type: textarea
14
+ id: who-can-help
15
+ attributes:
16
+ label: Who can help? / 谁可以帮助到您?
17
+ description: |
18
+ Your issue will be replied to more quickly if you can figure out the right person to tag with @
19
+ All issues are read by one of the maintainers, so if you don't know who to tag, just leave this blank and our maintainer will ping the right person.
20
+
21
+ Please tag fewer than 3 people.
22
+
23
+ 如果您能找到合适的标签 @,您的问题会更快得到回复。
24
+ 所有问题都会由我们的维护者阅读,如果您不知道该标记谁,只需留空,我们的维护人员会找到合适的开发组成员来解决问题。
25
+
26
+ 标记的人数应该不超过 3 个人。
27
+
28
+ If it's not a bug in these three subsections, you may not specify the helper. Our maintainer will find the right person in the development group to solve the problem.
29
+
30
+ 如果不是这三个子版块的bug,您可以不指明帮助者,我们的维护人员会找到合适的开发组成员来解决问题。
31
+
32
+ placeholder: "@Username ..."
33
+
34
+ - type: checkboxes
35
+ id: information-scripts-examples
36
+ attributes:
37
+ label: Information / 问题信息
38
+ description: 'The problem arises when using: / 问题出现在'
39
+ options:
40
+ - label: "The official example scripts / 官方的示例脚本"
41
+ - label: "My own modified scripts / 我自己修改的脚本和任务"
42
+
43
+ - type: textarea
44
+ id: reproduction
45
+ validations:
46
+ required: true
47
+ attributes:
48
+ label: Reproduction / 复现过程
49
+ description: |
50
+ Please provide a code example that reproduces the problem you encountered, preferably with a minimal reproduction unit.
51
+ If you have code snippets, error messages, stack traces, please provide them here as well.
52
+ Please format your code correctly using code tags. See https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting
53
+ Do not use screenshots, as they are difficult to read and (more importantly) do not allow others to copy and paste your code.
54
+
55
+ 请提供能重现您遇到的问题的代码示例,最好是最小复现单元。
56
+ 如果您有代码片段、错误信息、堆栈跟踪,也请在此提供。
57
+ 请使用代码标签正确格式化您的代码。请参见 https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting
58
+ 请勿使用截图,因为截图难以阅读,而且(更重要的是)不允许他人复制粘贴您的代码。
59
+ placeholder: |
60
+ Steps to reproduce the behavior/复现Bug的步骤:
61
+
62
+ 1.
63
+ 2.
64
+ 3.
65
+
66
+ - type: textarea
67
+ id: expected-behavior
68
+ validations:
69
+ required: true
70
+ attributes:
71
+ label: Expected behavior / 期待表现
72
+ description: "A clear and concise description of what you would expect to happen. /简单描述您期望发生的事情。"
SCAIL-Pose/.github/ISSUE_TEMPLATE/feature-request.yaml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "\U0001F680 Feature request"
2
+ description: Submit a request for a new SCAIL-Pose feature / 提交一个新的 SCAIL-Pose 的功能建议
3
+ labels: [ "feature" ]
4
+ body:
5
+ - type: textarea
6
+ id: feature-request
7
+ validations:
8
+ required: true
9
+ attributes:
10
+ label: Feature request / 功能建议
11
+ description: |
12
+ A brief description of the functional proposal. Links to corresponding papers and code are desirable.
13
+ 对功能建议的简述。最好提供对应的论文和代码链接
14
+
15
+ - type: textarea
16
+ id: motivation
17
+ validations:
18
+ required: true
19
+ attributes:
20
+ label: Motivation / 动机
21
+ description: |
22
+ Your motivation for making the suggestion. If that motivation is related to another GitHub issue, link to it here.
23
+ 您提出建议的动机。如果该动机与另一个 GitHub 问题有关,请在此处提供对应的链接。
24
+
25
+ - type: textarea
26
+ id: contribution
27
+ validations:
28
+ required: true
29
+ attributes:
30
+ label: Your contribution / 您的贡献
31
+ description: |
32
+
33
+ Your PR link or any other link you can help with.
34
+ 您的PR链接或者其他您能提供帮助的链接。
SCAIL-Pose/DWPoseProcess/AAUtils.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MooreAA 同样API
2
+ import importlib
3
+ import os
4
+ import os.path as osp
5
+ import shutil
6
+ import sys
7
+ from pathlib import Path
8
+ import decord
9
+ from decord import VideoReader, cpu, gpu
10
+ import av
11
+ import numpy as np
12
+ import torch
13
+ import torchvision
14
+ from einops import rearrange
15
+ from PIL import Image
16
+ import time
17
+ from fractions import Fraction
18
+ import cv2
19
+ import jsonlines
20
+ import random
21
+ import io
22
+
23
+
24
+
25
+ def seed_everything(seed):
26
+ import random
27
+
28
+ import numpy as np
29
+
30
+ torch.manual_seed(seed)
31
+ torch.cuda.manual_seed_all(seed)
32
+ np.random.seed(seed % (2**32))
33
+ random.seed(seed)
34
+
35
+
36
+ def import_filename(filename):
37
+ spec = importlib.util.spec_from_file_location("mymodule", filename)
38
+ module = importlib.util.module_from_spec(spec)
39
+ sys.modules[spec.name] = module
40
+ spec.loader.exec_module(module)
41
+ return module
42
+
43
+
44
+ def delete_additional_ckpt(base_path, num_keep):
45
+ dirs = []
46
+ for d in os.listdir(base_path):
47
+ if d.startswith("checkpoint-"):
48
+ dirs.append(d)
49
+ num_tot = len(dirs)
50
+ if num_tot <= num_keep:
51
+ return
52
+ # ensure ckpt is sorted and delete the ealier!
53
+ del_dirs = sorted(dirs, key=lambda x: int(x.split("-")[-1]))[: num_tot - num_keep]
54
+ for d in del_dirs:
55
+ path_to_dir = osp.join(base_path, d)
56
+ if osp.exists(path_to_dir):
57
+ shutil.rmtree(path_to_dir)
58
+
59
+
60
+ # def save_videos_from_pil(pil_images, path, fps=8):
61
+ # if fps is None or fps <= 0 or fps > 240:
62
+ # print(f"Warning: Invalid FPS {fps}")
63
+ # return
64
+
65
+ # save_fmt = Path(path).suffix
66
+ # os.makedirs(os.path.dirname(path), exist_ok=True)
67
+ # width, height = pil_images[0].size
68
+
69
+ # if save_fmt == ".mp4":
70
+ # try:
71
+ # codec = "libx264"
72
+ # container = av.open(path, "w")
73
+ # stream = container.add_stream(codec, rate=fps)
74
+
75
+ # stream.width = width
76
+ # stream.height = height
77
+
78
+ # for pil_image in pil_images:
79
+ # # pil_image = Image.fromarray(image_arr).convert("RGB")
80
+ # av_frame = av.VideoFrame.from_image(pil_image)
81
+ # container.mux(stream.encode(av_frame))
82
+ # container.mux(stream.encode())
83
+ # container.close()
84
+ # except Exception as e:
85
+ # print(f"Unexpected error while saving video {path}: {e}")
86
+ # if os.path.exists(path):
87
+ # try:
88
+ # os.remove(path)
89
+ # print(f"Corrupted file {path} removed successfully.")
90
+ # except Exception as rm_e:
91
+ # print(f"Failed to remove corrupted file {path}: {rm_e}")
92
+
93
+ # elif save_fmt == ".gif":
94
+ # pil_images[0].save(
95
+ # fp=path,
96
+ # format="GIF",
97
+ # append_images=pil_images[1:],
98
+ # save_all=True,
99
+ # duration=(1 / fps * 1000),
100
+ # loop=0,
101
+ # )
102
+ # else:
103
+ # raise ValueError("Unsupported file type. Use .mp4 or .gif.")
104
+
105
+
106
+ def save_videos_grid(videos: torch.Tensor, path: str, rescale=False, n_rows=6, fps=8):
107
+ videos = rearrange(videos, "b c t h w -> t b c h w")
108
+ height, width = videos.shape[-2:]
109
+ outputs = []
110
+
111
+ for x in videos: # x: b c h w
112
+ if x.shape[0] != 1:
113
+ x = torchvision.utils.make_grid(x, nrow=n_rows) # (c h w)
114
+ else:
115
+ x = x.squeeze(0)
116
+ x = x.transpose(0, 1).transpose(1, 2).squeeze(-1) # (h w c)
117
+ if rescale:
118
+ x = (x + 1.0) / 2.0 # -1,1 -> 0,1
119
+ x = (x * 255).numpy().astype(np.uint8)
120
+ x = Image.fromarray(x)
121
+
122
+ outputs.append(x)
123
+
124
+ os.makedirs(os.path.dirname(path), exist_ok=True)
125
+
126
+ save_videos_from_pil(outputs, path, fps)
127
+
128
+
129
+ def read_frames(video_path):
130
+ try:
131
+ # 使用 decord 打开视频
132
+ vr = VideoReader(video_path)
133
+ frames = []
134
+
135
+ # 逐帧解码
136
+ for i in range(len(vr)):
137
+ frame = vr[i] # 获取帧,返回的是 mx.ndarray
138
+ image = Image.fromarray(frame.asnumpy()) # 转换为 PIL 格式
139
+ frames.append(image)
140
+
141
+ return frames
142
+
143
+ except Exception as e:
144
+ print(f"Error reading frames from {video_path}: {e}")
145
+ return None # 返回 None 避免代码崩溃
146
+
147
+ def get_fps(video_path):
148
+
149
+ # container = av.open(video_path)
150
+ # video_stream = next(s for s in container.streams if s.type == "video")
151
+ # fps = video_stream.average_rate
152
+ # container.close()
153
+ # print("pyav_fps")
154
+ # print(fps)
155
+ try:
156
+ vr = decord.VideoReader(video_path)
157
+ fps = vr.get_avg_fps()
158
+ return Fraction(fps).limit_denominator(1001)
159
+ except Exception as e:
160
+ print(f"Error reading FPS from {video_path}: {e}")
161
+ return None # 返回 None 避免代码崩溃
162
+
163
+
164
+ def read_frames_and_fps(video_path):
165
+ try:
166
+ # 使用 decord 打开视频
167
+ vr = VideoReader(video_path)
168
+ fps = vr.get_avg_fps()
169
+ frames = []
170
+
171
+ # 逐帧解码
172
+ for i in range(len(vr)):
173
+ frame = vr[i] # 获取帧,返回的是 mx.ndarray
174
+ image = Image.fromarray(frame.asnumpy()) # 转换为 PIL 格式
175
+ frames.append(image)
176
+
177
+ processed_fps = Fraction(fps).limit_denominator(1001)
178
+ return frames, processed_fps
179
+
180
+ except Exception as e:
181
+ print(f"Error reading frames from {video_path}: {e}")
182
+ return None, None # 返回 None 避免代码崩溃
183
+
184
+ def read_frames_and_fps_as_np(video_path):
185
+ try:
186
+ # 使用 decord 打开视频
187
+ vr = VideoReader(video_path)
188
+ fps = vr.get_avg_fps()
189
+ frames = []
190
+
191
+ # 逐帧解码
192
+ for i in range(len(vr)):
193
+ frame = vr[i] # 获取帧,返回的是 mx.ndarray
194
+ image = frame.asnumpy() # 转换为 PIL 格式
195
+ frames.append(image)
196
+ processed_fps = Fraction(fps).limit_denominator(1001)
197
+ return frames, processed_fps
198
+
199
+ except Exception as e:
200
+ print(f"Error reading frames from {video_path}: {e}")
201
+ return None, None # 返回 None 避免代码崩溃
202
+
203
+
204
+
205
+ def save_videos_from_pil(pil_images, path, fps=8):
206
+ if fps is None or fps <= 0 or fps > 240:
207
+ print(f"Warning: Invalid FPS {fps}")
208
+ return
209
+
210
+ save_fmt = Path(path).suffix
211
+ os.makedirs(os.path.dirname(path), exist_ok=True)
212
+ width, height = pil_images[0].size
213
+
214
+ if save_fmt == ".mp4":
215
+ try:
216
+ codec = "libx264"
217
+ container = av.open(path, "w")
218
+ stream = container.add_stream(codec, rate=fps)
219
+
220
+ stream.width = width
221
+ stream.height = height
222
+
223
+ for pil_image in pil_images:
224
+ # pil_image = Image.fromarray(image_arr).convert("RGB")
225
+ av_frame = av.VideoFrame.from_image(pil_image)
226
+ container.mux(stream.encode(av_frame))
227
+ container.mux(stream.encode())
228
+ container.close()
229
+ except Exception as e:
230
+ print(f"Unexpected error while saving video {path}: {e}")
231
+ if os.path.exists(path):
232
+ try:
233
+ os.remove(path)
234
+ print(f"Corrupted file {path} removed successfully.")
235
+ except Exception as rm_e:
236
+ print(f"Failed to remove corrupted file {path}: {rm_e}")
237
+
238
+ elif save_fmt == ".gif":
239
+ pil_images[0].save(
240
+ fp=path,
241
+ format="GIF",
242
+ append_images=pil_images[1:],
243
+ save_all=True,
244
+ duration=(1 / fps * 1000),
245
+ loop=0,
246
+ )
247
+ else:
248
+ raise ValueError("Unsupported file type. Use .mp4 or .gif.")
249
+
250
+
251
+ def load_video_with_pose_from_first_frame(video_data, pose_data, sampling="uniform", duration=None, num_frames=99, wanted_fps=None, actual_fps=None,
252
+ skip_frms_num=4., nb_read_frames=None):
253
+ decord.bridge.set_bridge("torch")
254
+ vr = VideoReader(uri=video_data, height=-1, width=-1)
255
+ vr_pose = VideoReader(uri=pose_data, height=-1, width=-1)
256
+
257
+ start = 0
258
+ end = int(start + num_frames / wanted_fps * actual_fps)
259
+ n_frms = num_frames + 1 # 要取到num_frames帧, +1把第一帧需要额外拿出来处理,让第一帧和后续帧不同
260
+
261
+ if sampling == "uniform":
262
+ indices = np.arange(start, end, (end - start) / n_frms).astype(int)
263
+ else:
264
+ raise NotImplementedError
265
+
266
+ # get_batch -> T, H, W, C
267
+
268
+ temp_frms = vr.get_batch(np.arange(start, end))
269
+ temp_frms_pose = vr_pose.get_batch(np.arange(start, end))
270
+
271
+ assert temp_frms is not None
272
+ assert temp_frms_pose is not None
273
+
274
+ tensor_frms = torch.from_numpy(temp_frms) if type(temp_frms) is not torch.Tensor else temp_frms
275
+ tensor_frms = tensor_frms[torch.tensor((indices - start).tolist())]
276
+
277
+ tensor_frms_pose = torch.from_numpy(temp_frms_pose) if type(temp_frms_pose) is not torch.Tensor else temp_frms_pose
278
+ tensor_frms_pose = temp_frms_pose[torch.tensor((indices - start).tolist())]
279
+
280
+ # print(f"n_frms: {n_frms}; tensor_frms.shape: {tensor_frms.shape} tensor_frms_pose.shape: {tensor_frms_pose.shape}")
281
+ return pad_last_frame(tensor_frms, n_frms), pad_last_frame(tensor_frms_pose, n_frms)
282
+
283
+
284
+ def pad_last_frame(tensor, sampling_frms_num):
285
+ # T, H, W, C
286
+ if tensor.shape[0] < sampling_frms_num:
287
+ # 复制最后一帧
288
+ last_frame = tensor[-int(sampling_frms_num-tensor.shape[0]):]
289
+ # 将最后一帧添加到第二个维度
290
+ padded_tensor = torch.cat([tensor, last_frame], dim=0)
291
+ return padded_tensor
292
+ else:
293
+ return tensor[:sampling_frms_num]
294
+
295
+
296
+ def load_video_sampling(video_data, pose_data, num_frames, wanted_fps):
297
+ decord.bridge.set_bridge("torch")
298
+ # 以video_data的为准
299
+ vr = VideoReader(uri=video_data, height=-1, width=-1)
300
+ actual_fps = vr.get_avg_fps()
301
+ if video_data:
302
+ video, pose = load_video_with_pose_from_first_frame(video_data, pose_data, sampling="uniform", duration=100000, num_frames=num_frames, wanted_fps=wanted_fps, actual_fps=actual_fps, skip_frms_num=0, nb_read_frames=None)
303
+ return video, pose
304
+ else:
305
+ raise ValueError("mooreAA should have video data")
SCAIL-Pose/DWPoseProcess/checkUtils.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from collections import deque
3
+ import numpy as np
4
+ from collections import deque
5
+ import math
6
+
7
+ def get_bbox_area(bbox):
8
+ x1, y1, x2, y2 = bbox
9
+ return (x2 - x1) * (y2 - y1)
10
+
11
+
12
+ def check_consistant(boxA, boxB, scoreA_lst, scoreB_lst, beta, all_threshold):
13
+ """
14
+ 计算两个锚框之间的 IoU(交并比)以及分数的变化比例,来判断连续性
15
+ """
16
+ # 计算交集框的坐标
17
+ scoreA = scoreA_lst[0]
18
+ scoreB = scoreB_lst[0]
19
+ iou = get_IoU(boxA, boxB)
20
+
21
+ reduction_ratio = (scoreA - scoreB) / (scoreA + scoreB) if scoreA > scoreB else 0 # 如果分数减少的很多,就更不连续
22
+
23
+ return iou - reduction_ratio * beta > all_threshold
24
+
25
+
26
+ def get_IoU(boxA, boxB):
27
+ """
28
+ 计算两个锚框之间的 IoU(交并比)以及分数的变化比例,来判断连续性
29
+ """
30
+ # 计算交集框的坐标
31
+ x1_int = max(boxA[0], boxB[0])
32
+ y1_int = max(boxA[1], boxB[1])
33
+ x2_int = min(boxA[2], boxB[2])
34
+ y2_int = min(boxA[3], boxB[3])
35
+
36
+ # 计算交集的面积
37
+ inter_width = max(0, x2_int - x1_int)
38
+ inter_height = max(0, y2_int - y1_int)
39
+ inter_area = inter_width * inter_height
40
+
41
+ # 计算两个锚框的面积
42
+ areaA = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
43
+ areaB = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
44
+
45
+ # 计算并集的面积
46
+ union_area = areaA + areaB - inter_area
47
+
48
+ # 计算 IoU
49
+ iou = inter_area / union_area if union_area > 0 else 0.0
50
+
51
+ return iou
52
+
53
+ def check_bbox_single_for_video(bbox, reference_width, reference_height, min_bbox_width=1/6, min_bbox_area=1/30):
54
+ """
55
+ 每一帧都要检查,判断 bbox 是否符合视频格式下的要求
56
+ """
57
+ x1, y1, x2, y2 = bbox
58
+ bbox_width = x2 - x1
59
+ bbox_height = y2 - y1
60
+
61
+ # 防止非法 bbox
62
+ if bbox_width <= 0 or bbox_height <= 0:
63
+ return False
64
+
65
+ bbox_area = bbox_width * bbox_height
66
+
67
+ if bbox_area < min_bbox_area:
68
+ # print("filtered: bbox too small or too large")
69
+ return False
70
+
71
+ # 横屏额外筛
72
+ if reference_width > reference_height:
73
+ if bbox_width < min_bbox_width:
74
+ # print("filtered: bbox too wide")
75
+ return False
76
+ return True
77
+
78
+
79
+
80
+ ##############################判断点是否满足################################
81
+
82
+ def part5_valid(valid_joints):
83
+ """
84
+ 判断身体五块是不是都有点
85
+
86
+ 参数:
87
+ valid_joints:布尔数组
88
+
89
+ 返回:
90
+ bool: 如果满足要求返回True,否则返回False。
91
+ """
92
+
93
+ ### 认为以下的视频是满足我们采样需要的:
94
+ ### A只有上半身手部动作的:手部有可能在挥舞过程中移出屏幕,但是上半身应该一直在屏幕内,此时1-0, 1-2, 1-5这三条骨骼应该都存在;14-17应该至少有一个点存在
95
+ ### B全身动作:1-0, 1-2, 1-5, 1-8, 1-11这五条骨骼应该都存在
96
+
97
+ top_core_joints = valid_joints[[0,1,2,5]]
98
+ top_nece_joints = valid_joints[[14,15,16,17]]
99
+ if all(top_core_joints) and any(top_nece_joints):
100
+ return True
101
+
102
+ wholebody_core_joints = valid_joints[[0,1,2,5,8,11]]
103
+ if all(wholebody_core_joints):
104
+ return True
105
+ return False
106
+
107
+
108
+ def check_valid_sequence(valid_keypoints, threshold=0.3):
109
+ valid_joints = np.zeros(24)
110
+ for valid_keypoint in valid_keypoints:
111
+ valid_joints += valid_keypoint
112
+ return part5_valid(valid_joints)
113
+
114
+
115
+ def get_valid_indice_from_keypoints(ref_part_poses, ref_part_indices):
116
+ # ref_part_poses: poses序列,ref_part_indices poses序列里面每个值对应的在整个序列里的index
117
+ # return: 每个pose序列里面,满足要求的pose的index
118
+ valid_indice = []
119
+ for i, (keypoint_all, indice) in enumerate(zip(ref_part_poses, ref_part_indices)):
120
+ body_subset = keypoint_all["bodies"]["subset"][0]
121
+ valid_joints = body_subset > -1 # 得到一个布尔索引
122
+
123
+ if not part5_valid(valid_joints):
124
+ continue
125
+
126
+ faces = keypoint_all["faces"][0]
127
+
128
+ left_eye = faces[36:42] # 左眼关键点 5个关键点有4个认为有左眼
129
+ right_eye = faces[42:48] # 右眼关键点 5个关键点有4个认为有右眼
130
+ nose = faces[27:36] # 鼻子关键点 8个关键点有5个认为有鼻子
131
+ mouth = faces[48:68] # 嘴巴关键点 21个关键点有15个认为有嘴巴
132
+
133
+ # 计算每个部位有效的关键点数
134
+ left_eye_valid = sum(1 for point in left_eye if point[0] > 0 and point[1] > 0)
135
+ right_eye_valid = sum(1 for point in right_eye if point[0] > 0 and point[1] > 0)
136
+ nose_valid = sum(1 for point in nose if point[0] > 0 and point[1] > 0)
137
+ mouth_valid = sum(1 for point in mouth if point[0] > 0 and point[1] > 0)
138
+
139
+ # 如果有两个或以上部位有效,则认为是正脸
140
+ valid_face_parts = 0
141
+ if left_eye_valid >= 4:
142
+ valid_face_parts += 1
143
+ if right_eye_valid >= 4:
144
+ valid_face_parts += 1
145
+ if nose_valid >= 5:
146
+ valid_face_parts += 1
147
+ if mouth_valid >= 15:
148
+ valid_face_parts += 1
149
+
150
+ if valid_face_parts >= 2:
151
+ valid_indice.append(int(indice))
152
+
153
+ return valid_indice
154
+
155
+ def check_from_keypoints_core_keypoints(keypoints, bboxs):
156
+ # 用于keypoints版本,根据每一帧的18个keypoints和bbox iou来判断是否满足要求
157
+ valid_sequence = deque(maxlen=4)
158
+ for i, (keypoint_all, bbox_all) in enumerate(zip(keypoints, bboxs)):
159
+ body_subset = keypoint_all["bodies"]["subset"][0]
160
+ valid_joints = body_subset > -1 # 得到一个布尔索引
161
+ if len(valid_sequence) == 4:
162
+ if not check_valid_sequence(valid_sequence):
163
+ # print("filtered: 骨骼不满足要求")
164
+ return False # 关键点异常
165
+ valid_sequence.append(valid_joints)
166
+ return True
167
+
168
+ def select_ref_from_keypoints_bbox_multi(ref_part_indices, ref_part_bboxes, bboxs):
169
+ for ref_index, ref_bbox in zip(ref_part_indices, ref_part_bboxes):
170
+ bbox_areas_ref = [get_bbox_area(bbox) for bbox in ref_bbox]
171
+ max_bbox_area_ref = max(bbox_areas_ref)
172
+ num_human_ref = sum(1 for bbox in ref_bbox if get_bbox_area(bbox) > max_bbox_area_ref * 0.5)
173
+ if num_human_ref < 2 or num_human_ref > 5:
174
+ continue
175
+ driving_bbox_ok = True
176
+ for i, bbox_all in enumerate(bboxs):
177
+ bbox_areas = [get_bbox_area(bbox) for bbox in bbox_all]
178
+ max_bbox_area = max(bbox_areas)
179
+ num_human = sum(1 for bbox in bbox_all if get_bbox_area(bbox) > max_bbox_area * 0.5)
180
+ if num_human != num_human_ref:
181
+ driving_bbox_ok = False
182
+ break
183
+ if driving_bbox_ok:
184
+ return int(ref_index)
185
+ else:
186
+ continue
187
+ return None
188
+
189
+ def check_from_keypoints_bbox(keypoints, bboxs, IoU_thresthold, reference_width, reference_height, multi_person=False):
190
+ # 用于keypoints版本,根据每一帧的18个keypoints和bbox iou来判断是否满足要求
191
+ last_bbox = None
192
+ for i, (keypoint_all, bbox_all) in enumerate(zip(keypoints, bboxs)):
193
+ if not len(bbox_all):
194
+ return False
195
+ else:
196
+ if multi_person:
197
+ for bbox in bbox_all:
198
+ if not check_bbox_single_for_video(bbox, reference_width, reference_height, min_bbox_width=1/6):
199
+ return False
200
+ else:
201
+ bbox = bbox_all[0]
202
+ if not check_bbox_single_for_video(bbox, reference_width, reference_height, min_bbox_width=1/7):
203
+ return False # bbox大小异常
204
+ if last_bbox is not None:
205
+ if not get_IoU(bbox, last_bbox) > IoU_thresthold:
206
+ return False # IoU异常
207
+ last_bbox = bbox
208
+ return True
209
+
210
+
211
+
212
+ def check_from_keypoints_stick_movement(keypoints, angle_threshold):
213
+ # 骨骼选择:列表中每个元组表示由两个关节确定一条骨骼:格式 (joint_a, joint_b)
214
+ # bones = [(1, 0), (1, 2), (1, 5), (1, 8), (1, 11)]
215
+ bones = [(1, 0), (1, 2), (1, 5), (1, 8), (1, 11), (2, 3), (5, 6), (8, 9), (11, 12)]
216
+ max_delta_list = []
217
+ # 遍历从第二帧开始,对比前一帧和当前帧
218
+ human_num_list = [len(keypoints[idx]["bodies"]["candidate"]) for idx in range(0, len(keypoints))]
219
+ min_human_num = min(human_num_list)
220
+ for human_idx in range(min_human_num):
221
+ for i in range(1, len(keypoints)):
222
+ # 获取上一帧和当前帧的关键点数据(格式为 (18,3) 数组)
223
+ prev_frame_subset = keypoints[i-1]["bodies"]["subset"][human_idx]
224
+ curr_frame_subset = keypoints[i]["bodies"]["subset"][human_idx]
225
+ prev_frame_keypoints = keypoints[i-1]["bodies"]["candidate"][human_idx]
226
+ curr_frame_keypoints = keypoints[i]["bodies"]["candidate"][human_idx]
227
+
228
+
229
+ max_delta = 0
230
+ for (j1, j2) in bones:
231
+ # 检查上一帧中两个关节是否有效(假设 x, y 坐标需大于 0 才认为有效)
232
+ if prev_frame_subset[j1] < 0 or prev_frame_subset[j2] < 0:
233
+ continue
234
+ if curr_frame_subset[j1] < 0 or curr_frame_subset[j2] < 0:
235
+ continue
236
+
237
+ # 计算上一帧和当前帧中对应骨骼的向量(方向一致,均从 j1 指向 j2)
238
+ vec_prev = np.array([prev_frame_keypoints[j2][0] - prev_frame_keypoints[j1][0],
239
+ prev_frame_keypoints[j2][1] - prev_frame_keypoints[j1][1]])
240
+ vec_curr = np.array([curr_frame_keypoints[j2][0] - curr_frame_keypoints[j1][0],
241
+ curr_frame_keypoints[j2][1] - curr_frame_keypoints[j1][1]])
242
+
243
+ # 如果向量模长为0,则无法计算角度,跳过
244
+ if np.linalg.norm(vec_prev) == 0 or np.linalg.norm(vec_curr) == 0:
245
+ continue
246
+ # 计算向量对应的角度(弧度制)
247
+ angle_prev = math.atan2(vec_prev[1], vec_prev[0])
248
+ angle_curr = math.atan2(vec_curr[1], vec_curr[0])
249
+
250
+ # 计算角度差,并规范到 [0, pi] 范围
251
+ delta = abs(angle_curr - angle_prev)
252
+ if delta > math.pi:
253
+ delta = 2 * math.pi - delta
254
+
255
+ max_delta = max(delta, max_delta)
256
+ max_delta_list.append(max_delta)
257
+ max_delta_list = sorted(max_delta_list)
258
+ max_delta_list = max_delta_list[len(max_delta_list)//8:-len(max_delta_list)//8] # 去掉两端8分之一的值
259
+ avg_movement = sum(max_delta_list) / len(max_delta_list)
260
+ if avg_movement < angle_threshold: # 筛去过小的动作
261
+ return False
262
+ return True
263
+
SCAIL-Pose/DWPoseProcess/dwpose/__init__.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://github.com/IDEA-Research/DWPose
2
+ # Openpose
3
+ # Original from CMU https://github.com/CMU-Perceptual-Computing-Lab/openpose
4
+ # 2nd Edited by https://github.com/Hzzone/pytorch-openpose
5
+ # 3rd Edited by ControlNet
6
+ # 4th Edited by ControlNet (added face and correct hands)
7
+
8
+ import copy
9
+ import os
10
+
11
+ os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
12
+ import cv2
13
+ import numpy as np
14
+ import torch
15
+ from controlnet_aux.util import HWC3, resize_image
16
+ from PIL import Image
17
+
18
+ from . import util
19
+ from .wholebody import Wholebody
20
+
21
+
22
+ class DWposeDetector:
23
+ def __init__(self, use_batch=False):
24
+ self.use_batch = use_batch
25
+ pass
26
+
27
+ def to(self, device):
28
+ self.pose_estimation = Wholebody(device, self.use_batch)
29
+ return self
30
+
31
+ def _get_multi_result_from_est(self, candidate, score_result, det_result, H, W):
32
+ nums, keys, locs = candidate.shape # n 所有身体关键点数量,坐标
33
+ candidate[..., 0] /= float(W)
34
+ candidate[..., 1] /= float(H)
35
+ subset_score = score_result[:, :24] # 按照24个骨骼关键点来区分可见位置
36
+ face_score = score_result[:, 24:92]
37
+ hand_score = score_result[:, 92:113]
38
+ hand_score = np.vstack([hand_score, score_result[:, 113:]])
39
+
40
+ body_candidate = candidate[:, :24].copy() # body(n, 24, 2)
41
+ for i in range(len(subset_score)): # n 个
42
+ for j in range(len(subset_score[i])):
43
+ if subset_score[i][j] > 0.3:
44
+ subset_score[i][j] = j # 标注序号,这样后续用的时候可以快速查出可用点
45
+ else:
46
+ subset_score[i][j] = -1 # 躯干中去除掉不可见的骨骼
47
+
48
+ un_visible = score_result < 0.3
49
+ candidate[un_visible] = -1 # 全部关键点中去掉不可见骨骼
50
+
51
+ faces = candidate[:, 24:92]
52
+ hands = candidate[:, 92:113] # hands(2*n, 21, 2)
53
+ hands = np.vstack([hands, candidate[:, 113:]])
54
+
55
+ bodies = dict(candidate=body_candidate, subset=subset_score)
56
+ pose = dict(bodies=bodies, hands=hands, faces=faces)
57
+ score = dict(body_score=subset_score, hand_score=hand_score, face_score=face_score)
58
+
59
+ new_det_result = []
60
+ for bbox in det_result:
61
+ x1, y1, x2, y2 = bbox
62
+ new_x1 = x1 / W
63
+ new_y1 = y1 / H
64
+ new_x2 = x2 / W
65
+ new_y2 = y2 / H
66
+ new_bbox = [new_x1, new_y1, new_x2, new_y2]
67
+ new_det_result.append(new_bbox)
68
+
69
+ return pose, score, new_det_result # body_score是原始的躯干骨骼分数
70
+
71
+ # def _get_result_from_est(self, input_image, candidate, subset, det_result, image_resolution, output_type, H, W):
72
+ # nums, keys, locs = candidate.shape
73
+ # candidate[..., 0] /= float(W)
74
+ # candidate[..., 1] /= float(H)
75
+ # score = subset[:, :18] # 前18个是躯干骨骼 score(n, 18)
76
+ # max_ind = np.mean(score, axis=-1).argmax(axis=0) # 返回分数最高的锚框对应的骨骼
77
+ # score = score[[max_ind]]
78
+ # body = candidate[:, :18].copy()
79
+ # body = body[[max_ind]]
80
+ # nums = 1
81
+ # body = body.reshape(nums * 18, locs) # Moore-AA只有一个人体, 0-18表示body
82
+ # body_score = copy.deepcopy(score) # 已经去过max_ind
83
+ # for i in range(len(score)):
84
+ # for j in range(len(score[i])):
85
+ # if score[i][j] > 0.3:
86
+ # score[i][j] = int(18 * i + j)
87
+ # else:
88
+ # score[i][j] = -1 # 躯干中去除掉不可见的骨骼
89
+
90
+ # un_visible = subset < 0.3
91
+ # candidate[un_visible] = -1 # 全部关键点中去掉不可见骨骼
92
+
93
+ # foot = candidate[:, 18:24]
94
+
95
+ # faces = candidate[[max_ind], 24:92]
96
+
97
+ # hands = candidate[[max_ind], 92:113]
98
+ # hands = np.vstack([hands, candidate[[max_ind], 113:]])
99
+
100
+ # bodies = dict(candidate=body, subset=score)
101
+ # pose = dict(bodies=bodies, hands=hands, faces=faces)
102
+
103
+ # return pose, body_score, det_result # body_score是原始的躯干骨骼分数
104
+
105
+ def __call__(
106
+ self,
107
+ input,
108
+ **kwargs,
109
+ ):
110
+ if not self.use_batch:
111
+ # PIL要不要颜色反转?
112
+ input = cv2.cvtColor(
113
+ np.array(input, dtype=np.uint8), cv2.COLOR_RGB2BGR
114
+ )
115
+ input = HWC3(input)
116
+ H, W, C = input.shape
117
+
118
+ with torch.no_grad():
119
+ candidate, subset, det_result = self.pose_estimation(input) # candidate (n, 134, 2) 候选点 / subset (n, 134) 得分
120
+ return self._get_multi_result_from_est(candidate, subset, det_result, H, W)
121
+ else:
122
+ raise NotImplementedError("DWposeDetector does not support batch mode")
123
+
124
+
125
+
126
+
127
+
128
+
129
+
SCAIL-Pose/DWPoseProcess/dwpose/onnxdet.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://github.com/IDEA-Research/DWPose
2
+ import cv2
3
+ import numpy as np
4
+ import onnxruntime
5
+
6
+
7
+ def nms(boxes, scores, nms_thr):
8
+ """Single class NMS implemented in Numpy."""
9
+ x1 = boxes[:, 0]
10
+ y1 = boxes[:, 1]
11
+ x2 = boxes[:, 2]
12
+ y2 = boxes[:, 3]
13
+
14
+ areas = (x2 - x1 + 1) * (y2 - y1 + 1)
15
+ order = scores.argsort()[::-1]
16
+
17
+ keep = []
18
+ while order.size > 0:
19
+ i = order[0]
20
+ keep.append(i)
21
+ xx1 = np.maximum(x1[i], x1[order[1:]])
22
+ yy1 = np.maximum(y1[i], y1[order[1:]])
23
+ xx2 = np.minimum(x2[i], x2[order[1:]])
24
+ yy2 = np.minimum(y2[i], y2[order[1:]])
25
+
26
+ w = np.maximum(0.0, xx2 - xx1 + 1)
27
+ h = np.maximum(0.0, yy2 - yy1 + 1)
28
+ inter = w * h
29
+ ovr = inter / (areas[i] + areas[order[1:]] - inter)
30
+
31
+ inds = np.where(ovr <= nms_thr)[0]
32
+ order = order[inds + 1]
33
+
34
+ return keep
35
+
36
+
37
+ def multiclass_nms(boxes, scores, nms_thr, score_thr):
38
+ """Multiclass NMS implemented in Numpy. Class-aware version."""
39
+ final_dets = []
40
+ num_classes = scores.shape[1]
41
+ for cls_ind in range(num_classes):
42
+ cls_scores = scores[:, cls_ind]
43
+ valid_score_mask = cls_scores > score_thr
44
+ if valid_score_mask.sum() == 0:
45
+ continue
46
+ else:
47
+ valid_scores = cls_scores[valid_score_mask]
48
+ valid_boxes = boxes[valid_score_mask]
49
+ keep = nms(valid_boxes, valid_scores, nms_thr)
50
+ if len(keep) > 0:
51
+ cls_inds = np.ones((len(keep), 1)) * cls_ind
52
+ dets = np.concatenate(
53
+ [valid_boxes[keep], valid_scores[keep, None], cls_inds], 1
54
+ )
55
+ final_dets.append(dets)
56
+ if len(final_dets) == 0:
57
+ return None
58
+ return np.concatenate(final_dets, 0)
59
+
60
+
61
+ def demo_postprocess(outputs, img_size, p6=False):
62
+ grids = []
63
+ expanded_strides = []
64
+ strides = [8, 16, 32] if not p6 else [8, 16, 32, 64]
65
+
66
+ hsizes = [img_size[0] // stride for stride in strides]
67
+ wsizes = [img_size[1] // stride for stride in strides]
68
+
69
+ for hsize, wsize, stride in zip(hsizes, wsizes, strides):
70
+ xv, yv = np.meshgrid(np.arange(wsize), np.arange(hsize))
71
+ grid = np.stack((xv, yv), 2).reshape(1, -1, 2)
72
+ grids.append(grid)
73
+ shape = grid.shape[:2]
74
+ expanded_strides.append(np.full((*shape, 1), stride))
75
+
76
+ grids = np.concatenate(grids, 1)
77
+ expanded_strides = np.concatenate(expanded_strides, 1)
78
+ outputs[..., :2] = (outputs[..., :2] + grids) * expanded_strides
79
+ outputs[..., 2:4] = np.exp(outputs[..., 2:4]) * expanded_strides
80
+
81
+ return outputs
82
+
83
+
84
+ def preprocess(img, input_size, swap=(2, 0, 1)):
85
+ if len(img.shape) == 3:
86
+ padded_img = np.ones((input_size[0], input_size[1], 3), dtype=np.uint8) * 114
87
+ else:
88
+ padded_img = np.ones(input_size, dtype=np.uint8) * 114
89
+
90
+ r = min(input_size[0] / img.shape[0], input_size[1] / img.shape[1])
91
+ resized_img = cv2.resize(
92
+ img,
93
+ (int(img.shape[1] * r), int(img.shape[0] * r)),
94
+ interpolation=cv2.INTER_LINEAR,
95
+ ).astype(np.uint8)
96
+ padded_img[: int(img.shape[0] * r), : int(img.shape[1] * r)] = resized_img
97
+
98
+ padded_img = padded_img.transpose(swap)
99
+ padded_img = np.ascontiguousarray(padded_img, dtype=np.float32)
100
+ return padded_img, r
101
+
102
+
103
+ def inference_detector(session, oriImg):
104
+ input_shape = (640, 640)
105
+ img, ratio = preprocess(oriImg, input_shape)
106
+
107
+ ort_inputs = {session.get_inputs()[0].name: img[None, :, :, :]}
108
+ output = session.run(None, ort_inputs)
109
+ predictions = demo_postprocess(output[0], input_shape)[0]
110
+
111
+ boxes = predictions[:, :4]
112
+ scores = predictions[:, 4:5] * predictions[:, 5:]
113
+
114
+ boxes_xyxy = np.ones_like(boxes)
115
+ boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2.0
116
+ boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2.0
117
+ boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2.0
118
+ boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2.0
119
+ boxes_xyxy /= ratio
120
+ dets = multiclass_nms(boxes_xyxy, scores, nms_thr=0.45, score_thr=0.1)
121
+ if dets is not None:
122
+ final_boxes, final_scores, final_cls_inds = dets[:, :4], dets[:, 4], dets[:, 5]
123
+ isscore = final_scores > 0.3
124
+ iscat = final_cls_inds == 0
125
+ isbbox = [i and j for (i, j) in zip(isscore, iscat)]
126
+ final_boxes = final_boxes[isbbox]
127
+ else:
128
+ # print("no boxes detected")
129
+ return []
130
+
131
+ return final_boxes
132
+
133
+
134
+
SCAIL-Pose/DWPoseProcess/dwpose/onnxpose.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://github.com/IDEA-Research/DWPose
2
+ from typing import List, Tuple
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import onnxruntime as ort
7
+
8
+
9
+ def preprocess(
10
+ img: np.ndarray, out_bbox, input_size: Tuple[int, int] = (192, 256)
11
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
12
+ """Do preprocessing for RTMPose model inference.
13
+
14
+ Args:
15
+ img (np.ndarray): Input image in shape.
16
+ input_size (tuple): Input image size in shape (w, h).
17
+
18
+ Returns:
19
+ tuple:
20
+ - resized_img (np.ndarray): Preprocessed image.
21
+ - center (np.ndarray): Center of image.
22
+ - scale (np.ndarray): Scale of image.
23
+ """
24
+ # get shape of image
25
+ img_shape = img.shape[:2]
26
+ out_img, out_center, out_scale = [], [], []
27
+ if len(out_bbox) == 0:
28
+ out_bbox = [[0, 0, img_shape[1], img_shape[0]]]
29
+ for i in range(len(out_bbox)):
30
+ x0 = out_bbox[i][0]
31
+ y0 = out_bbox[i][1]
32
+ x1 = out_bbox[i][2]
33
+ y1 = out_bbox[i][3]
34
+ bbox = np.array([x0, y0, x1, y1])
35
+
36
+ # get center and scale
37
+ center, scale = bbox_xyxy2cs(bbox, padding=1.25)
38
+
39
+ # do affine transformation
40
+ resized_img, scale = top_down_affine(input_size, scale, center, img)
41
+
42
+ # normalize image
43
+ mean = np.array([123.675, 116.28, 103.53])
44
+ std = np.array([58.395, 57.12, 57.375])
45
+ resized_img = (resized_img - mean) / std
46
+
47
+ out_img.append(resized_img)
48
+ out_center.append(center)
49
+ out_scale.append(scale)
50
+
51
+ return out_img, out_center, out_scale
52
+
53
+
54
+ def inference(sess: ort.InferenceSession, img: np.ndarray) -> np.ndarray:
55
+ """Inference RTMPose model.
56
+
57
+ Args:
58
+ sess (ort.InferenceSession): ONNXRuntime session.
59
+ img (np.ndarray): Input image in shape.
60
+
61
+ Returns:
62
+ outputs (np.ndarray): Output of RTMPose model.
63
+ """
64
+ all_out = []
65
+ # build input
66
+ for i in range(len(img)):
67
+ input = [img[i].transpose(2, 0, 1)]
68
+
69
+ # build output
70
+ sess_input = {sess.get_inputs()[0].name: input}
71
+ sess_output = []
72
+ for out in sess.get_outputs():
73
+ sess_output.append(out.name)
74
+
75
+ outputs = sess.run(sess_output, sess_input)
76
+ # outputs 也是len为2的列表,outputs[0] outputs[1]第一维为1
77
+ all_out.append(outputs)
78
+
79
+ # breakpoint()
80
+
81
+ return all_out
82
+
83
+
84
+ def postprocess(
85
+ outputs: List[np.ndarray],
86
+ model_input_size: Tuple[int, int],
87
+ center: Tuple[int, int], # 实际是List of tuple
88
+ scale: Tuple[int, int],
89
+ simcc_split_ratio: float = 2.0,
90
+ ) -> Tuple[np.ndarray, np.ndarray]:
91
+ """Postprocess for RTMPose model output.
92
+
93
+ Args:
94
+ outputs (np.ndarray): Output of RTMPose model.
95
+ model_input_size (tuple): RTMPose model Input image size.
96
+ center (tuple): List of Center of bbox in shape (x, y).
97
+ scale (tuple): List of Scale of bbox in shape (w, h).
98
+ simcc_split_ratio (float): Split ratio of simcc.
99
+
100
+ Returns:
101
+ tuple:
102
+ - keypoints (np.ndarray): Rescaled keypoints.
103
+ - scores (np.ndarray): Model predict scores.
104
+ """
105
+ all_key = []
106
+ all_score = []
107
+ for i in range(len(outputs)):
108
+ # use simcc to decode
109
+ simcc_x, simcc_y = outputs[i]
110
+ keypoints, scores = decode(simcc_x, simcc_y, simcc_split_ratio)
111
+
112
+ # rescale keypoints
113
+ keypoints = keypoints / model_input_size * scale[i] + center[i] - scale[i] / 2
114
+ all_key.append(keypoints[0])
115
+ all_score.append(scores[0])
116
+
117
+ return np.array(all_key), np.array(all_score)
118
+
119
+
120
+ def bbox_xyxy2cs(
121
+ bbox: np.ndarray, padding: float = 1.0
122
+ ) -> Tuple[np.ndarray, np.ndarray]:
123
+ """Transform the bbox format from (x,y,w,h) into (center, scale)
124
+
125
+ Args:
126
+ bbox (ndarray): Bounding box(es) in shape (4,) or (n, 4), formatted
127
+ as (left, top, right, bottom)
128
+ padding (float): BBox padding factor that will be multilied to scale.
129
+ Default: 1.0
130
+
131
+ Returns:
132
+ tuple: A tuple containing center and scale.
133
+ - np.ndarray[float32]: Center (x, y) of the bbox in shape (2,) or
134
+ (n, 2)
135
+ - np.ndarray[float32]: Scale (w, h) of the bbox in shape (2,) or
136
+ (n, 2)
137
+ """
138
+ # convert single bbox from (4, ) to (1, 4)
139
+ dim = bbox.ndim
140
+ if dim == 1:
141
+ bbox = bbox[None, :]
142
+
143
+ # get bbox center and scale
144
+ x1, y1, x2, y2 = np.hsplit(bbox, [1, 2, 3])
145
+ center = np.hstack([x1 + x2, y1 + y2]) * 0.5
146
+ scale = np.hstack([x2 - x1, y2 - y1]) * padding
147
+
148
+ if dim == 1:
149
+ center = center[0]
150
+ scale = scale[0]
151
+
152
+ return center, scale
153
+
154
+
155
+ def _fix_aspect_ratio(bbox_scale: np.ndarray, aspect_ratio: float) -> np.ndarray:
156
+ """Extend the scale to match the given aspect ratio.
157
+
158
+ Args:
159
+ scale (np.ndarray): The image scale (w, h) in shape (2, )
160
+ aspect_ratio (float): The ratio of ``w/h``
161
+
162
+ Returns:
163
+ np.ndarray: The reshaped image scale in (2, )
164
+ """
165
+ w, h = np.hsplit(bbox_scale, [1])
166
+ bbox_scale = np.where(
167
+ w > h * aspect_ratio,
168
+ np.hstack([w, w / aspect_ratio]),
169
+ np.hstack([h * aspect_ratio, h]),
170
+ )
171
+ return bbox_scale
172
+
173
+
174
+ def _rotate_point(pt: np.ndarray, angle_rad: float) -> np.ndarray:
175
+ """Rotate a point by an angle.
176
+
177
+ Args:
178
+ pt (np.ndarray): 2D point coordinates (x, y) in shape (2, )
179
+ angle_rad (float): rotation angle in radian
180
+
181
+ Returns:
182
+ np.ndarray: Rotated point in shape (2, )
183
+ """
184
+ sn, cs = np.sin(angle_rad), np.cos(angle_rad)
185
+ rot_mat = np.array([[cs, -sn], [sn, cs]])
186
+ return rot_mat @ pt
187
+
188
+
189
+ def _get_3rd_point(a: np.ndarray, b: np.ndarray) -> np.ndarray:
190
+ """To calculate the affine matrix, three pairs of points are required. This
191
+ function is used to get the 3rd point, given 2D points a & b.
192
+
193
+ The 3rd point is defined by rotating vector `a - b` by 90 degrees
194
+ anticlockwise, using b as the rotation center.
195
+
196
+ Args:
197
+ a (np.ndarray): The 1st point (x,y) in shape (2, )
198
+ b (np.ndarray): The 2nd point (x,y) in shape (2, )
199
+
200
+ Returns:
201
+ np.ndarray: The 3rd point.
202
+ """
203
+ direction = a - b
204
+ c = b + np.r_[-direction[1], direction[0]]
205
+ return c
206
+
207
+
208
+ def get_warp_matrix(
209
+ center: np.ndarray,
210
+ scale: np.ndarray,
211
+ rot: float,
212
+ output_size: Tuple[int, int],
213
+ shift: Tuple[float, float] = (0.0, 0.0),
214
+ inv: bool = False,
215
+ ) -> np.ndarray:
216
+ """Calculate the affine transformation matrix that can warp the bbox area
217
+ in the input image to the output size.
218
+
219
+ Args:
220
+ center (np.ndarray[2, ]): Center of the bounding box (x, y).
221
+ scale (np.ndarray[2, ]): Scale of the bounding box
222
+ wrt [width, height].
223
+ rot (float): Rotation angle (degree).
224
+ output_size (np.ndarray[2, ] | list(2,)): Size of the
225
+ destination heatmaps.
226
+ shift (0-100%): Shift translation ratio wrt the width/height.
227
+ Default (0., 0.).
228
+ inv (bool): Option to inverse the affine transform direction.
229
+ (inv=False: src->dst or inv=True: dst->src)
230
+
231
+ Returns:
232
+ np.ndarray: A 2x3 transformation matrix
233
+ """
234
+ shift = np.array(shift)
235
+ src_w = scale[0]
236
+ dst_w = output_size[0]
237
+ dst_h = output_size[1]
238
+
239
+ # compute transformation matrix
240
+ rot_rad = np.deg2rad(rot)
241
+ src_dir = _rotate_point(np.array([0.0, src_w * -0.5]), rot_rad)
242
+ dst_dir = np.array([0.0, dst_w * -0.5])
243
+
244
+ # get four corners of the src rectangle in the original image
245
+ src = np.zeros((3, 2), dtype=np.float32)
246
+ src[0, :] = center + scale * shift
247
+ src[1, :] = center + src_dir + scale * shift
248
+ src[2, :] = _get_3rd_point(src[0, :], src[1, :])
249
+
250
+ # get four corners of the dst rectangle in the input image
251
+ dst = np.zeros((3, 2), dtype=np.float32)
252
+ dst[0, :] = [dst_w * 0.5, dst_h * 0.5]
253
+ dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5]) + dst_dir
254
+ dst[2, :] = _get_3rd_point(dst[0, :], dst[1, :])
255
+
256
+ if inv:
257
+ warp_mat = cv2.getAffineTransform(np.float32(dst), np.float32(src))
258
+ else:
259
+ warp_mat = cv2.getAffineTransform(np.float32(src), np.float32(dst))
260
+
261
+ return warp_mat
262
+
263
+
264
+ def top_down_affine(
265
+ input_size: dict, bbox_scale: dict, bbox_center: dict, img: np.ndarray
266
+ ) -> Tuple[np.ndarray, np.ndarray]:
267
+ """Get the bbox image as the model input by affine transform.
268
+
269
+ Args:
270
+ input_size (dict): The input size of the model.
271
+ bbox_scale (dict): The bbox scale of the img.
272
+ bbox_center (dict): The bbox center of the img.
273
+ img (np.ndarray): The original image.
274
+
275
+ Returns:
276
+ tuple: A tuple containing center and scale.
277
+ - np.ndarray[float32]: img after affine transform.
278
+ - np.ndarray[float32]: bbox scale after affine transform.
279
+ """
280
+ w, h = input_size
281
+ warp_size = (int(w), int(h))
282
+
283
+ # reshape bbox to fixed aspect ratio
284
+ bbox_scale = _fix_aspect_ratio(bbox_scale, aspect_ratio=w / h)
285
+
286
+ # get the affine matrix
287
+ center = bbox_center
288
+ scale = bbox_scale
289
+ rot = 0
290
+ warp_mat = get_warp_matrix(center, scale, rot, output_size=(w, h))
291
+
292
+ # do affine transform
293
+ img = cv2.warpAffine(img, warp_mat, warp_size, flags=cv2.INTER_LINEAR)
294
+
295
+ return img, bbox_scale
296
+
297
+
298
+ def get_simcc_maximum(
299
+ simcc_x: np.ndarray, simcc_y: np.ndarray
300
+ ) -> Tuple[np.ndarray, np.ndarray]:
301
+ """Get maximum response location and value from simcc representations.
302
+
303
+ Note:
304
+ instance number: N
305
+ num_keypoints: K
306
+ heatmap height: H
307
+ heatmap width: W
308
+
309
+ Args:
310
+ simcc_x (np.ndarray): x-axis SimCC in shape (K, Wx) or (N, K, Wx)
311
+ simcc_y (np.ndarray): y-axis SimCC in shape (K, Wy) or (N, K, Wy)
312
+
313
+ Returns:
314
+ tuple:
315
+ - locs (np.ndarray): locations of maximum heatmap responses in shape
316
+ (K, 2) or (N, K, 2)
317
+ - vals (np.ndarray): values of maximum heatmap responses in shape
318
+ (K,) or (N, K)
319
+ """
320
+ N, K, Wx = simcc_x.shape
321
+ simcc_x = simcc_x.reshape(N * K, -1)
322
+ simcc_y = simcc_y.reshape(N * K, -1)
323
+
324
+ # get maximum value locations
325
+ x_locs = np.argmax(simcc_x, axis=1)
326
+ y_locs = np.argmax(simcc_y, axis=1)
327
+ locs = np.stack((x_locs, y_locs), axis=-1).astype(np.float32)
328
+ max_val_x = np.amax(simcc_x, axis=1)
329
+ max_val_y = np.amax(simcc_y, axis=1)
330
+
331
+ # get maximum value across x and y axis
332
+ mask = max_val_x > max_val_y
333
+ max_val_x[mask] = max_val_y[mask]
334
+ vals = max_val_x
335
+ locs[vals <= 0.0] = -1
336
+
337
+ # reshape
338
+ locs = locs.reshape(N, K, 2)
339
+ vals = vals.reshape(N, K)
340
+
341
+ return locs, vals
342
+
343
+
344
+ def decode(
345
+ simcc_x: np.ndarray, simcc_y: np.ndarray, simcc_split_ratio
346
+ ) -> Tuple[np.ndarray, np.ndarray]:
347
+ """Modulate simcc distribution with Gaussian.
348
+
349
+ Args:
350
+ simcc_x (np.ndarray[K, Wx]): model predicted simcc in x.
351
+ simcc_y (np.ndarray[K, Wy]): model predicted simcc in y.
352
+ simcc_split_ratio (int): The split ratio of simcc.
353
+
354
+ Returns:
355
+ tuple: A tuple containing center and scale.
356
+ - np.ndarray[float32]: keypoints in shape (K, 2) or (n, K, 2)
357
+ - np.ndarray[float32]: scores in shape (K,) or (n, K)
358
+ """
359
+ keypoints, scores = get_simcc_maximum(simcc_x, simcc_y)
360
+ keypoints /= simcc_split_ratio
361
+
362
+ return keypoints, scores
363
+
364
+
365
+ def inference_pose(session, out_bbox, oriImg):
366
+ h, w = session.get_inputs()[0].shape[2:]
367
+ model_input_size = (w, h)
368
+ resized_img, center, scale = preprocess(oriImg, out_bbox, model_input_size)
369
+ outputs = inference(session, resized_img)
370
+ keypoints, scores = postprocess(outputs, model_input_size, center, scale)
371
+
372
+ return keypoints, scores
373
+
374
+
375
+ def inference_pose_batch(session, out_bbox, oriImg):
376
+ h, w = session.get_inputs()[0].shape[2:]
377
+ model_input_size = (w, h)
378
+ # breakpoint()
379
+ num_boxes = [box.shape[0] if len(box)>=1 else 1 for box in out_bbox]
380
+ resized_img, center, scale = preprocess_batch(oriImg, out_bbox, model_input_size)
381
+ outputs = inference_batch(session, resized_img) # nparray 第0维为(\sum_{batch_num} num_box)
382
+ keypoints, scores = postprocess_batch(outputs, num_boxes, model_input_size, center, scale)
383
+ # keypoints和scores在postprocess中从(\sum_{batch_num} num_box) 恢复为 a list of batch_num of nparray[num_box, ...]
384
+
385
+ return keypoints, scores
386
+
387
+
388
+ def preprocess_batch(
389
+ img_list: List[np.ndarray], out_bbox_list, input_size: Tuple[int, int] = (192, 256)
390
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
391
+ # img_list shape: batch_num * nparray[C, H, W]
392
+ # out_bbox_list shape: batch_bum * nparray[num_box, 4]
393
+ resized_imgs = []
394
+ centers = []
395
+ scales = []
396
+
397
+ for img, out_bbox in zip(img_list, out_bbox_list):
398
+ resized_img, center, scale = preprocess(img, out_bbox, input_size)
399
+ # 有的out_bbox是两个
400
+ # resized_img: list of array, num_box*nparray [C, H', W']
401
+ resized_imgs.extend(resized_img)
402
+ centers.extend(center)
403
+ scales.extend(scale)
404
+ # resized_imgs 为 nparray 第0维为(\sum_{batch_num} num_box)
405
+ # centers和scales 为 list of nparray len为(\sum_{batch_num} num_box)
406
+ return np.stack(resized_imgs), centers, scales
407
+
408
+
409
+ def inference_batch(sess: ort.InferenceSession, batch_img):
410
+ """Inference RTMPose model in batch."""
411
+ # build input
412
+ input = batch_img.transpose(0, 3, 1, 2).astype(np.float32)
413
+
414
+ # build output
415
+ sess_input = {sess.get_inputs()[0].name: input}
416
+ sess_output = []
417
+ for out in sess.get_outputs():
418
+ sess_output.append(out.name)
419
+
420
+ outputs = sess.run(sess_output, sess_input)
421
+ num = outputs[0].shape[0]
422
+ split_0 = np.split(outputs[0], num, axis=0)
423
+ split_1 = np.split(outputs[1], num, axis=0)
424
+ # 创建最终的输出列表,其中每个元素是一个包含两个 nparray 的列表
425
+ final_output = [[split_0[i], split_1[i]] for i in range(num)]
426
+ return final_output # list of nparray 长度为(\sum_{batch_num} num_box)
427
+
428
+ def postprocess_batch(
429
+ outputs: List[np.ndarray],
430
+ num_boxes: List[int],
431
+ model_input_size: Tuple[int, int],
432
+ center: List[Tuple[int, int]],
433
+ scale: List[Tuple[int, int]],
434
+ simcc_split_ratio: float = 2.0,
435
+ ) -> Tuple[np.ndarray, np.ndarray]:
436
+ # keypoints和scores在postprocess中根据num_boxes从(\sum_{batch_num} num_box) 恢复为 a list of batch_num of nparray[num_box, ...]
437
+ all_key, all_score = postprocess(outputs, model_input_size, center, scale, simcc_split_ratio)
438
+ relist_all_key, relist_all_score = [], []
439
+ start_idx = 0
440
+ for num_box in num_boxes:
441
+ # 按照每个 batch 的 num_box 从 all_key 和 all_score 中恢复出对应的部分
442
+ split_indices = np.cumsum(num_boxes)
443
+ split_indices = np.insert(split_indices, 0, 0) # 在开头插入0 -> 0, len1, len1+len2...
444
+ # 恢复每个batch的 keypoints 和 scores
445
+ relist_all_key = [all_key[split_indices[i]:split_indices[i + 1]] for i in range(len(split_indices) - 1)]
446
+ relist_all_score = [all_score[split_indices[i]:split_indices[i + 1]] for i in range(len(split_indices) - 1)]
447
+ return relist_all_key, relist_all_score # a list of batch_num of nparray[num_box, ...]
448
+
449
+
450
+ # Main function for testing inference_pose_batch
451
+ def main():
452
+ # 创建一个简单的测试图像和假边界框
453
+ img = np.random.rand(256, 192, 3).astype(np.float32) # 一张随机图像
454
+ out_bbox = np.array([[50, 50, 150, 150]])
455
+
456
+ img_list = [np.random.rand(256, 192, 3).astype(np.float32) for _ in range(3)] # 3张随机图像
457
+ out_bbox_list = [
458
+ np.array([[50, 50, 150, 150]]), # 第一个批次: 1个框
459
+ np.array([[30, 30, 130, 130], [50, 50, 200, 200]]), # 第二个批次: 2个框
460
+ np.array([[10, 10, 100, 100]]), # 第三个批次: 1个框
461
+ ]
462
+
463
+ # 加载 ONNX 模型
464
+ model_path = "/workspace/yanwenhao/Moore-AnimateAnyone/pretrained_weights/DWPose/dw-ll_ucoco_384.onnx" # 在此替换为实际的模型路径
465
+ sess = ort.InferenceSession(model_path)
466
+
467
+ # 调用 inference_pose 进行单张图像推理
468
+ keypoints, scores = inference_pose(sess, out_bbox, img)
469
+
470
+ # 输出结果
471
+ print("single infer test")
472
+ print("Keypoints:", keypoints.shape)
473
+ print("Scores:", scores.shape)
474
+
475
+ # 调用 inference_pose_batch 进行推理
476
+ keypoints, scores = inference_pose_batch(sess, out_bbox_list, img_list)
477
+
478
+ # 输出结果
479
+ print("batch infer test")
480
+ for i, (keypoint, score) in enumerate(zip(keypoints, scores)):
481
+ print(f"Batch_index {i + 1}:")
482
+ print("Keypoints:", keypoint.shape)
483
+ print("Scores:", score.shape)
484
+
485
+ # 调用 main 函数
486
+ if __name__ == "__main__":
487
+ main()
SCAIL-Pose/DWPoseProcess/dwpose/util.py ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://github.com/IDEA-Research/DWPose
2
+ import math
3
+ import numpy as np
4
+ import matplotlib
5
+ import cv2
6
+
7
+
8
+ eps = 0.01
9
+
10
+
11
+ def smart_resize(x, s):
12
+ Ht, Wt = s
13
+ if x.ndim == 2:
14
+ Ho, Wo = x.shape
15
+ Co = 1
16
+ else:
17
+ Ho, Wo, Co = x.shape
18
+ if Co == 3 or Co == 1:
19
+ k = float(Ht + Wt) / float(Ho + Wo)
20
+ return cv2.resize(
21
+ x,
22
+ (int(Wt), int(Ht)),
23
+ interpolation=cv2.INTER_AREA if k < 1 else cv2.INTER_LANCZOS4,
24
+ )
25
+ else:
26
+ return np.stack([smart_resize(x[:, :, i], s) for i in range(Co)], axis=2)
27
+
28
+
29
+ def smart_resize_k(x, fx, fy):
30
+ if x.ndim == 2:
31
+ Ho, Wo = x.shape
32
+ Co = 1
33
+ else:
34
+ Ho, Wo, Co = x.shape
35
+ Ht, Wt = Ho * fy, Wo * fx
36
+ if Co == 3 or Co == 1:
37
+ k = float(Ht + Wt) / float(Ho + Wo)
38
+ return cv2.resize(
39
+ x,
40
+ (int(Wt), int(Ht)),
41
+ interpolation=cv2.INTER_AREA if k < 1 else cv2.INTER_LANCZOS4,
42
+ )
43
+ else:
44
+ return np.stack([smart_resize_k(x[:, :, i], fx, fy) for i in range(Co)], axis=2)
45
+
46
+
47
+ def padRightDownCorner(img, stride, padValue):
48
+ h = img.shape[0]
49
+ w = img.shape[1]
50
+
51
+ pad = 4 * [None]
52
+ pad[0] = 0 # up
53
+ pad[1] = 0 # left
54
+ pad[2] = 0 if (h % stride == 0) else stride - (h % stride) # down
55
+ pad[3] = 0 if (w % stride == 0) else stride - (w % stride) # right
56
+
57
+ img_padded = img
58
+ pad_up = np.tile(img_padded[0:1, :, :] * 0 + padValue, (pad[0], 1, 1))
59
+ img_padded = np.concatenate((pad_up, img_padded), axis=0)
60
+ pad_left = np.tile(img_padded[:, 0:1, :] * 0 + padValue, (1, pad[1], 1))
61
+ img_padded = np.concatenate((pad_left, img_padded), axis=1)
62
+ pad_down = np.tile(img_padded[-2:-1, :, :] * 0 + padValue, (pad[2], 1, 1))
63
+ img_padded = np.concatenate((img_padded, pad_down), axis=0)
64
+ pad_right = np.tile(img_padded[:, -2:-1, :] * 0 + padValue, (1, pad[3], 1))
65
+ img_padded = np.concatenate((img_padded, pad_right), axis=1)
66
+
67
+ return img_padded, pad
68
+
69
+
70
+ def transfer(model, model_weights):
71
+ transfered_model_weights = {}
72
+ for weights_name in model.state_dict().keys():
73
+ transfered_model_weights[weights_name] = model_weights[
74
+ ".".join(weights_name.split(".")[1:])
75
+ ]
76
+ return transfered_model_weights
77
+
78
+
79
+ def draw_bodypose(canvas, candidate, subset):
80
+ H, W, C = canvas.shape
81
+ candidate = np.array(candidate)
82
+ subset = np.array(subset)
83
+
84
+ stickwidth = 4
85
+
86
+ limbSeq = [
87
+ [2, 3],
88
+ [2, 6],
89
+ [3, 4],
90
+ [4, 5],
91
+ [6, 7],
92
+ [7, 8],
93
+ [2, 9],
94
+ [9, 10],
95
+ [10, 11],
96
+ [2, 12],
97
+ [12, 13],
98
+ [13, 14],
99
+ [2, 1],
100
+ [1, 15],
101
+ [15, 17],
102
+ [1, 16],
103
+ [16, 18],
104
+ [3, 17],
105
+ [6, 18],
106
+ ]
107
+
108
+ colors = [
109
+ [255, 0, 0],
110
+ [255, 85, 0],
111
+ [255, 170, 0],
112
+ [255, 255, 0],
113
+ [170, 255, 0],
114
+ [85, 255, 0],
115
+ [0, 255, 0],
116
+ [0, 255, 85],
117
+ [0, 255, 170],
118
+ [0, 255, 255],
119
+ [0, 170, 255],
120
+ [0, 85, 255],
121
+ [0, 0, 255],
122
+ [85, 0, 255],
123
+ [170, 0, 255],
124
+ [255, 0, 255],
125
+ [255, 0, 170],
126
+ [255, 0, 85],
127
+ ]
128
+
129
+ for i in range(17):
130
+ for n in range(len(subset)):
131
+ index = subset[n][np.array(limbSeq[i]) - 1]
132
+ if -1 in index:
133
+ continue
134
+ Y = candidate[index.astype(int), 0] * float(W)
135
+ X = candidate[index.astype(int), 1] * float(H)
136
+ mX = np.mean(X)
137
+ mY = np.mean(Y)
138
+ length = ((X[0] - X[1]) ** 2 + (Y[0] - Y[1]) ** 2) ** 0.5
139
+ angle = math.degrees(math.atan2(X[0] - X[1], Y[0] - Y[1]))
140
+ polygon = cv2.ellipse2Poly(
141
+ (int(mY), int(mX)), (int(length / 2), stickwidth), int(angle), 0, 360, 1
142
+ )
143
+ cv2.fillConvexPoly(canvas, polygon, colors[i])
144
+
145
+ canvas = (canvas * 0.6).astype(np.uint8)
146
+
147
+ for i in range(18):
148
+ for n in range(len(subset)):
149
+ index = int(subset[n][i])
150
+ if index == -1:
151
+ continue
152
+ x, y = candidate[index][0:2]
153
+ x = int(x * W)
154
+ y = int(y * H)
155
+ cv2.circle(canvas, (int(x), int(y)), 4, colors[i], thickness=-1)
156
+
157
+ return canvas
158
+
159
+
160
+ def draw_handpose(canvas, all_hand_peaks):
161
+ H, W, C = canvas.shape
162
+
163
+ edges = [
164
+ [0, 1],
165
+ [1, 2],
166
+ [2, 3],
167
+ [3, 4],
168
+ [0, 5],
169
+ [5, 6],
170
+ [6, 7],
171
+ [7, 8],
172
+ [0, 9],
173
+ [9, 10],
174
+ [10, 11],
175
+ [11, 12],
176
+ [0, 13],
177
+ [13, 14],
178
+ [14, 15],
179
+ [15, 16],
180
+ [0, 17],
181
+ [17, 18],
182
+ [18, 19],
183
+ [19, 20],
184
+ ]
185
+
186
+ for peaks in all_hand_peaks:
187
+ peaks = np.array(peaks)
188
+
189
+ for ie, e in enumerate(edges):
190
+ x1, y1 = peaks[e[0]]
191
+ x2, y2 = peaks[e[1]]
192
+ x1 = int(x1 * W)
193
+ y1 = int(y1 * H)
194
+ x2 = int(x2 * W)
195
+ y2 = int(y2 * H)
196
+ if x1 > eps and y1 > eps and x2 > eps and y2 > eps:
197
+ cv2.line(
198
+ canvas,
199
+ (x1, y1),
200
+ (x2, y2),
201
+ matplotlib.colors.hsv_to_rgb([ie / float(len(edges)), 1.0, 1.0])
202
+ * 255,
203
+ thickness=2,
204
+ )
205
+
206
+ for i, keyponit in enumerate(peaks):
207
+ x, y = keyponit
208
+ x = int(x * W)
209
+ y = int(y * H)
210
+ if x > eps and y > eps:
211
+ cv2.circle(canvas, (x, y), 2, (0, 0, 255), thickness=-1)
212
+ return canvas
213
+
214
+
215
+ def draw_facepose(canvas, all_lmks):
216
+ H, W, C = canvas.shape
217
+ for lmks in all_lmks:
218
+ lmks = np.array(lmks)
219
+ for lmk in lmks:
220
+ x, y = lmk
221
+ x = int(x * W)
222
+ y = int(y * H)
223
+ if x > eps and y > eps:
224
+ cv2.circle(canvas, (x, y), 3, (255, 255, 255), thickness=-1)
225
+ return canvas
226
+
227
+
228
+ # detect hand according to body pose keypoints
229
+ # please refer to https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/src/openpose/hand/handDetector.cpp
230
+ def handDetect(candidate, subset, oriImg):
231
+ # right hand: wrist 4, elbow 3, shoulder 2
232
+ # left hand: wrist 7, elbow 6, shoulder 5
233
+ ratioWristElbow = 0.33
234
+ detect_result = []
235
+ image_height, image_width = oriImg.shape[0:2]
236
+ for person in subset.astype(int):
237
+ # if any of three not detected
238
+ has_left = np.sum(person[[5, 6, 7]] == -1) == 0
239
+ has_right = np.sum(person[[2, 3, 4]] == -1) == 0
240
+ if not (has_left or has_right):
241
+ continue
242
+ hands = []
243
+ # left hand
244
+ if has_left:
245
+ left_shoulder_index, left_elbow_index, left_wrist_index = person[[5, 6, 7]]
246
+ x1, y1 = candidate[left_shoulder_index][:2]
247
+ x2, y2 = candidate[left_elbow_index][:2]
248
+ x3, y3 = candidate[left_wrist_index][:2]
249
+ hands.append([x1, y1, x2, y2, x3, y3, True])
250
+ # right hand
251
+ if has_right:
252
+ right_shoulder_index, right_elbow_index, right_wrist_index = person[
253
+ [2, 3, 4]
254
+ ]
255
+ x1, y1 = candidate[right_shoulder_index][:2]
256
+ x2, y2 = candidate[right_elbow_index][:2]
257
+ x3, y3 = candidate[right_wrist_index][:2]
258
+ hands.append([x1, y1, x2, y2, x3, y3, False])
259
+
260
+ for x1, y1, x2, y2, x3, y3, is_left in hands:
261
+ # pos_hand = pos_wrist + ratio * (pos_wrist - pos_elbox) = (1 + ratio) * pos_wrist - ratio * pos_elbox
262
+ # handRectangle.x = posePtr[wrist*3] + ratioWristElbow * (posePtr[wrist*3] - posePtr[elbow*3]);
263
+ # handRectangle.y = posePtr[wrist*3+1] + ratioWristElbow * (posePtr[wrist*3+1] - posePtr[elbow*3+1]);
264
+ # const auto distanceWristElbow = getDistance(poseKeypoints, person, wrist, elbow);
265
+ # const auto distanceElbowShoulder = getDistance(poseKeypoints, person, elbow, shoulder);
266
+ # handRectangle.width = 1.5f * fastMax(distanceWristElbow, 0.9f * distanceElbowShoulder);
267
+ x = x3 + ratioWristElbow * (x3 - x2)
268
+ y = y3 + ratioWristElbow * (y3 - y2)
269
+ distanceWristElbow = math.sqrt((x3 - x2) ** 2 + (y3 - y2) ** 2)
270
+ distanceElbowShoulder = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
271
+ width = 1.5 * max(distanceWristElbow, 0.9 * distanceElbowShoulder)
272
+ # x-y refers to the center --> offset to topLeft point
273
+ # handRectangle.x -= handRectangle.width / 2.f;
274
+ # handRectangle.y -= handRectangle.height / 2.f;
275
+ x -= width / 2
276
+ y -= width / 2 # width = height
277
+ # overflow the image
278
+ if x < 0:
279
+ x = 0
280
+ if y < 0:
281
+ y = 0
282
+ width1 = width
283
+ width2 = width
284
+ if x + width > image_width:
285
+ width1 = image_width - x
286
+ if y + width > image_height:
287
+ width2 = image_height - y
288
+ width = min(width1, width2)
289
+ # the max hand box value is 20 pixels
290
+ if width >= 20:
291
+ detect_result.append([int(x), int(y), int(width), is_left])
292
+
293
+ """
294
+ return value: [[x, y, w, True if left hand else False]].
295
+ width=height since the network require squared input.
296
+ x, y is the coordinate of top left
297
+ """
298
+ return detect_result
299
+
300
+
301
+ # Written by Lvmin
302
+ def faceDetect(candidate, subset, oriImg):
303
+ # left right eye ear 14 15 16 17
304
+ detect_result = []
305
+ image_height, image_width = oriImg.shape[0:2]
306
+ for person in subset.astype(int):
307
+ has_head = person[0] > -1
308
+ if not has_head:
309
+ continue
310
+
311
+ has_left_eye = person[14] > -1
312
+ has_right_eye = person[15] > -1
313
+ has_left_ear = person[16] > -1
314
+ has_right_ear = person[17] > -1
315
+
316
+ if not (has_left_eye or has_right_eye or has_left_ear or has_right_ear):
317
+ continue
318
+
319
+ head, left_eye, right_eye, left_ear, right_ear = person[[0, 14, 15, 16, 17]]
320
+
321
+ width = 0.0
322
+ x0, y0 = candidate[head][:2]
323
+
324
+ if has_left_eye:
325
+ x1, y1 = candidate[left_eye][:2]
326
+ d = max(abs(x0 - x1), abs(y0 - y1))
327
+ width = max(width, d * 3.0)
328
+
329
+ if has_right_eye:
330
+ x1, y1 = candidate[right_eye][:2]
331
+ d = max(abs(x0 - x1), abs(y0 - y1))
332
+ width = max(width, d * 3.0)
333
+
334
+ if has_left_ear:
335
+ x1, y1 = candidate[left_ear][:2]
336
+ d = max(abs(x0 - x1), abs(y0 - y1))
337
+ width = max(width, d * 1.5)
338
+
339
+ if has_right_ear:
340
+ x1, y1 = candidate[right_ear][:2]
341
+ d = max(abs(x0 - x1), abs(y0 - y1))
342
+ width = max(width, d * 1.5)
343
+
344
+ x, y = x0, y0
345
+
346
+ x -= width
347
+ y -= width
348
+
349
+ if x < 0:
350
+ x = 0
351
+
352
+ if y < 0:
353
+ y = 0
354
+
355
+ width1 = width * 2
356
+ width2 = width * 2
357
+
358
+ if x + width > image_width:
359
+ width1 = image_width - x
360
+
361
+ if y + width > image_height:
362
+ width2 = image_height - y
363
+
364
+ width = min(width1, width2)
365
+
366
+ if width >= 20:
367
+ detect_result.append([int(x), int(y), int(width)])
368
+
369
+ return detect_result
370
+
371
+
372
+ # get max index of 2d array
373
+ def npmax(array):
374
+ arrayindex = array.argmax(1)
375
+ arrayvalue = array.max(1)
376
+ i = arrayvalue.argmax()
377
+ j = arrayindex[i]
378
+ return i, j
SCAIL-Pose/DWPoseProcess/dwpose/wholebody.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://github.com/IDEA-Research/DWPose
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import onnxruntime as ort
7
+ import time
8
+ from .onnxdet import inference_detector
9
+ from .onnxpose import inference_pose, inference_pose_batch
10
+
11
+ ModelDataPathPrefix = Path("./pretrained_weights")
12
+
13
+
14
+ class Wholebody:
15
+ def __init__(self, device, use_batch=False):
16
+ providers = [("CUDAExecutionProvider", {
17
+ "device_id": device
18
+ })]
19
+ # providers = [("CPUExecutionProvider", {})]
20
+ onnx_det = ModelDataPathPrefix.joinpath("DWPose/yolox_l.onnx")
21
+ onnx_pose = ModelDataPathPrefix.joinpath("DWPose/dw-ll_ucoco_384.onnx")
22
+
23
+ self.session_det = ort.InferenceSession(
24
+ path_or_bytes=onnx_det, providers=providers
25
+ )
26
+ self.session_pose = ort.InferenceSession(
27
+ path_or_bytes=onnx_pose, providers=providers
28
+ )
29
+ self.use_batch = use_batch
30
+
31
+ def _get_result_from_det_pose(self, det_result, keypoints, scores):
32
+ keypoints_info = np.concatenate((keypoints, scores[..., None]), axis=-1) # (1, 133, 3)
33
+ # compute neck joint
34
+ neck = np.mean(keypoints_info[:, [5, 6]], axis=1) # (1, 3),对第五第六个点做平均
35
+ # neck score when visualizing pred
36
+ neck[:, 2:4] = np.logical_and(
37
+ keypoints_info[:, 5, 2:4] > 0.3, keypoints_info[:, 6, 2:4] > 0.3
38
+ ).astype(int) # 从第二个开始切片,这里维度为3,只切一片
39
+ new_keypoints_info = np.insert(keypoints_info, 17, neck, axis=1) # 在17索引处插入neck
40
+ # 调换骨骼索引
41
+ mmpose_idx = [17, 6, 8, 10, 7, 9, 12, 14, 16, 13, 15, 2, 1, 4, 3]
42
+ openpose_idx = [1, 2, 3, 4, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17] # openpose需要检测17点+1脖子关键点
43
+ new_keypoints_info[:, openpose_idx] = new_keypoints_info[:, mmpose_idx]
44
+ keypoints_info = new_keypoints_info
45
+
46
+ keypoints, scores = keypoints_info[..., :2], keypoints_info[..., 2]
47
+
48
+ return keypoints, scores, det_result
49
+
50
+
51
+ def __call__(self, oriImg):
52
+ if not self.use_batch:
53
+ det_result = inference_detector(self.session_det, oriImg)
54
+ keypoints, scores = inference_pose(self.session_pose, det_result, oriImg) # keypoints: (n_bbox, 133, 2) scores: (n_bbox, 133) 不管输入是什么初步提取的关键点数量都是一致的
55
+ return self._get_result_from_det_pose(det_result=det_result, keypoints=keypoints, scores=scores)
56
+
57
+ else:
58
+ raise NotImplementedError("DWposeDetector does not support batch mode")
SCAIL-Pose/DWPoseProcess/extractUtils.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import copy
3
+
4
+ def get_bbox_area(bbox):
5
+ x1, y1, x2, y2 = bbox
6
+ return (x2 - x1) * (y2 - y1)
7
+
8
+ def check_single_human_requirements(det_result):
9
+ # filter results
10
+ if len(det_result) > 3 or len(det_result) == 0:
11
+ return False
12
+ elif len(det_result) == 1:
13
+ return True
14
+ elif len(det_result) > 1: # [2, 3]
15
+ bbox_areas = [get_bbox_area(bbox) for bbox in det_result]
16
+ # 获取最大 bbox 面积的索引
17
+ max_ind = max(range(len(bbox_areas)), key=lambda i: bbox_areas[i])
18
+ # 获取次大面积(需要排除 max_ind)
19
+ other_indices = [i for i in range(len(bbox_areas)) if i != max_ind]
20
+ second_max_area = max([bbox_areas[i] for i in other_indices])
21
+
22
+ max_area = bbox_areas[max_ind]
23
+ if max_area < 2 * second_max_area:
24
+ return False
25
+ else:
26
+ return True
27
+
28
+ def human_select(poses, det_results, multi_person):
29
+ new_poses = []
30
+ new_det_results = []
31
+ for pose, det_result in zip(poses, det_results):
32
+ if multi_person:
33
+ new_pose, new_det_result = get_multi_human(pose, det_result)
34
+ else:
35
+ new_pose, new_det_result = get_single_human(pose, det_result)
36
+ new_poses.append(new_pose)
37
+ new_det_results.append(new_det_result)
38
+ return new_poses, new_det_results
39
+
40
+
41
+ def get_single_human(pose, det_result):
42
+ if len(det_result) <= 1:
43
+ return pose, det_result
44
+ else:
45
+ bbox_areas = [get_bbox_area(bbox) for bbox in det_result]
46
+ max_ind = max(range(len(bbox_areas)), key=lambda i: bbox_areas[i])
47
+ pose_copy = copy.deepcopy(pose)
48
+ pose_copy['bodies']['candidate'] = pose_copy['bodies']['candidate'][max_ind:max_ind+1]
49
+ pose_copy['bodies']['subset'] = pose_copy['bodies']['subset'][max_ind:max_ind+1]
50
+ pose_copy['hands'] = pose_copy['hands'][2*max_ind:2*max_ind+2]
51
+ pose_copy['faces'] = pose_copy['faces'][max_ind:max_ind+1]
52
+ return pose_copy, det_result[max_ind:max_ind+1]
53
+
54
+ def check_multi_human_requirements(det_result):
55
+ # filter results
56
+ if len(det_result) < 2 or len(det_result) > 4: # 2-4个人
57
+ return False
58
+ else: # [3, 6]
59
+ bbox_areas = [get_bbox_area(bbox) for bbox in det_result]
60
+ # 获取最大 bbox 面积的索引
61
+ max_ind = max(range(len(bbox_areas)), key=lambda i: bbox_areas[i])
62
+ max_area = bbox_areas[max_ind]
63
+
64
+ # 选择面积大于等于最大面积50%的bbox
65
+ selected_indices = [i for i in range(len(bbox_areas)) if bbox_areas[i] >= 0.5 * max_area] # 包含max_ind
66
+
67
+ # 检查选中的bbox数量是否大于等于2
68
+ if len(selected_indices) >= 2:
69
+ return True
70
+ else:
71
+ return False
72
+
73
+ def get_multi_human(pose, det_result):
74
+ # 后续再筛比较好,后续从65帧里面筛的时候可以把背景里的人的筛掉
75
+ return pose, det_result
SCAIL-Pose/LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2025 W Yan, S Ye, Z Yang, J Teng, ZH Dong, K Wen, X Gu, YJ Liu, J Tang
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
SCAIL-Pose/NLFPoseExtract/align3d.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sympy import beta
2
+ import torch
3
+ import numpy as np
4
+ from scipy.optimize import minimize
5
+
6
+
7
+ def solve_new_camera_params_central(three_d_points, focal_length, imshape, new_2d_points):
8
+ """
9
+ 通过最小化原始2D投影点和新的2D投影点之间的误差,求解新的相机参数。
10
+
11
+ 参数:
12
+ three_d_points (torch.Tensor): N*3 的 3D 点
13
+ focal_length (float): 原始相机的焦距
14
+ imshape (tuple): 图像的尺寸,例如 [512, 896]
15
+ original_2d_points (torch.Tensor): N*2 的原始2D投影点
16
+ new_2d_points (torch.Tensor): N*2 的新的2D投影点
17
+
18
+ 返回:
19
+ m, n, p, q: 新的相机内参中的参数
20
+ """
21
+
22
+ # 原始相机内参矩阵
23
+ K_orig = np.array([
24
+ [focal_length, 0, imshape[1] / 2],
25
+ [0, focal_length, imshape[0] / 2],
26
+ [0, 0, 1]
27
+ ])
28
+
29
+ # 目标函数:最小化原始投影点和新的投影点之间的误差
30
+ def objective(params):
31
+ m, s, p, q = params
32
+ # 构建新的相机内参矩阵
33
+ K_new = np.array([
34
+ [focal_length * m , 0, imshape[1] / 2 + p],
35
+ [0, focal_length * m * s, imshape[0] / 2 + q],
36
+ [0, 0, 1]
37
+ ])
38
+
39
+ # 计算新的2D投影点
40
+ new_projections = []
41
+ for point in three_d_points:
42
+ X, Y, Z = point
43
+ u = (K_new[0, 0] * X / Z) + K_new[0, 2]
44
+ v = (K_new[1, 1] * Y / Z) + K_new[1, 2]
45
+ new_projections.append([u, v])
46
+ new_projections = np.array(new_projections)
47
+
48
+ # 计算原始2D投影点和新的投影点之间的误差
49
+ # 第0个投影点特殊处理
50
+ error0 = np.sum((new_2d_points[:1] - new_projections[:1]) ** 2)
51
+ error = np.sum((new_2d_points[1:] - new_projections[1:]) ** 2)
52
+ return error0 * 8 + error
53
+
54
+ # 初始化参数 m, beta, p, q
55
+ initial_params = [1.0, 1.0, 0.0, 0.0] # 初始值
56
+
57
+ # 使用最小二乘法求解 p, q)
58
+ result = minimize(objective, initial_params, bounds=[(0.7, 1.4), (0.8, 1.15), (-imshape[1], imshape[1]), (-imshape[0], imshape[0])])
59
+
60
+ # 输出求解结果
61
+ m, s, p, q = result.x
62
+ print(f"debug: solved camera params m={m}, s={s}, p={p}, q={q}")
63
+
64
+ K_final = np.array([
65
+ [focal_length * m, 0, imshape[1] / 2 + p],
66
+ [0, focal_length * m * s, imshape[0] / 2 + q],
67
+ [0, 0, 1]
68
+ ])
69
+
70
+
71
+ return K_final, m, s
72
+
73
+
74
+ def solve_new_camera_params_down(three_d_points, focal_length, imshape, new_2d_points):
75
+ """
76
+ 通过最小化原始2D投影点和新的2D投影点之间的误差,求解新的相机参数。
77
+
78
+ 参数:
79
+ three_d_points (torch.Tensor): N*3 的 3D 点
80
+ focal_length (float): 原始相机的焦距
81
+ imshape (tuple): 图像的尺寸,例如 [512, 896]
82
+ original_2d_points (torch.Tensor): N*2 的原始2D投影点
83
+ new_2d_points (torch.Tensor): N*2 的新的2D投影点
84
+
85
+ 返回:
86
+ m, n, p, q: 新的相机内参中的参数
87
+ """
88
+
89
+ # 原始相机内参矩阵
90
+ K_orig = np.array([
91
+ [focal_length, 0, imshape[1] / 2],
92
+ [0, focal_length, imshape[0] / 2],
93
+ [0, 0, 1]
94
+ ])
95
+
96
+ # 目标函数:最小化原始投影点和新的投影点之间的误差
97
+ def objective(params):
98
+ m, s, p, q = params
99
+ # 构建新的相机内参矩阵
100
+ K_new = np.array([
101
+ [focal_length * m , 0, imshape[1] / 2 + p],
102
+ [0, focal_length * m * s, imshape[0] / 2 + q],
103
+ [0, 0, 1]
104
+ ])
105
+
106
+ # 计算新的2D投影点
107
+ new_projections = []
108
+ for point in three_d_points:
109
+ X, Y, Z = point
110
+ u = (K_new[0, 0] * X / Z) + K_new[0, 2]
111
+ v = (K_new[1, 1] * Y / Z) + K_new[1, 2]
112
+ new_projections.append([u, v])
113
+ new_projections = np.array(new_projections)
114
+
115
+ # 计算原始2D投影点和新的投影点之间的误差
116
+ # 第0个投影点特殊处理
117
+ error0 = np.sum((new_2d_points[:1] - new_projections[:1]) ** 2)
118
+ error = np.sum((new_2d_points[1:] - new_projections[1:]) ** 2)
119
+ return error0 + error * 4
120
+
121
+ # 初始化参数 m, beta, p, q
122
+ initial_params = [1.0, 1.0, 0.0, 0.0] # 初始值
123
+
124
+ # 使用最小二乘法求解 p, q)
125
+ result = minimize(objective, initial_params, bounds=[(0.7, 1.4), (0.8, 1.15), (-imshape[1], imshape[1]), (-imshape[0], imshape[0])])
126
+
127
+ # 输出求解结果
128
+ m, s, p, q = result.x
129
+ print(f"debug: solved camera params m={m}, s={s}, p={p}, q={q}")
130
+
131
+ K_final = np.array([
132
+ [focal_length * m, 0, imshape[1] / 2 + p],
133
+ [0, focal_length * m * s, imshape[0] / 2 + q],
134
+ [0, 0, 1]
135
+ ])
136
+
137
+
138
+ return K_final, m, s
SCAIL-Pose/NLFPoseExtract/debug_nlf.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ smpl_path = "/workspace/ywh_data/DataProcessNew/dongman/smpl/ca8637a1f14435234a9f1f9bc31ca4bb.pkl"
2
+
3
+ import pickle
4
+
5
+ with open(smpl_path, 'rb') as f:
6
+ smpl_data = pickle.load(f)
7
+ data = smpl_data['pose']['joints3d_nonparam']
8
+
9
+ breakpoint()
10
+ print(data)
SCAIL-Pose/NLFPoseExtract/extract_nlfpose_batch.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # 动态添加项目根目录到 sys.path,这样就不需要 export PYTHONPATH
5
+ current_dir = os.path.dirname(os.path.abspath(__file__))
6
+ project_root = os.path.dirname(current_dir) # SCAIL_Pose 目录
7
+ if project_root not in sys.path:
8
+ sys.path.insert(0, project_root)
9
+
10
+ import random
11
+ from pathlib import Path
12
+ import multiprocessing
13
+ import numpy as np
14
+ import time
15
+ from DWPoseProcess.checkUtils import *
16
+ from collections import deque
17
+ import shutil
18
+ import torch
19
+ import yaml
20
+ import webdataset as wds
21
+ from torch.utils.data import DataLoader
22
+ from tqdm import tqdm
23
+ from functools import partial
24
+ import threading
25
+ import time
26
+ from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED, ALL_COMPLETED, TimeoutError
27
+ from decord import VideoReader
28
+ from fractions import Fraction
29
+ import io
30
+ import gc
31
+ from PIL import Image
32
+ from multiprocessing import Process
33
+ import json
34
+ import jsonlines
35
+ from webdataset import TarWriter
36
+ import math
37
+ import glob
38
+ import pickle
39
+ import copy
40
+ import decord
41
+
42
+
43
+ def process_video_nlf(model, vr_frames, bboxes):
44
+ # Ensure output directory exists
45
+ # pose_results = {
46
+ # 'joints3d_nonparam': [],
47
+ # }
48
+ pose_meta_list = []
49
+ vr_frames = vr_frames.cuda()
50
+ height, width = vr_frames.shape[1], vr_frames.shape[2]
51
+ result_list = []
52
+
53
+ batch_size = 64
54
+ buffer = torch.zeros(
55
+ (batch_size, height, width, 3),
56
+ dtype=vr_frames.dtype,
57
+ device='cuda'
58
+ )
59
+ buffer_count = 0
60
+ with torch.inference_mode(), torch.device('cuda'):
61
+ for frame, bbox_list in zip(vr_frames, bboxes):
62
+ for bbox in bbox_list:
63
+ x1, y1, x2, y2 = bbox
64
+ x1_px = max(0, math.floor(x1 * width - width * 0.025))
65
+ y1_px = max(0, math.floor(y1 * height - height * 0.05))
66
+ x2_px = min(width, math.ceil(x2 * width + width * 0.025))
67
+ y2_px = min(height, math.ceil(y2 * height + height * 0.05))
68
+
69
+ cropped_region = frame[y1_px:y2_px, x1_px:x2_px, :]
70
+ buffer[buffer_count, y1_px:y2_px, x1_px:x2_px, :] = cropped_region
71
+ buffer_count += 1
72
+
73
+ # 一旦 buffer 满了,推理并清空
74
+ if buffer_count == batch_size:
75
+ frame_batch = buffer.permute(0, 3, 1, 2)
76
+ pred = model.detect_smpl_batched(frame_batch)
77
+ if 'joints3d_nonparam' in pred:
78
+ result_list.extend(pred['joints3d_nonparam'])
79
+ else:
80
+ result_list.extend([None] * buffer_count)
81
+
82
+ buffer.zero_()
83
+ buffer_count = 0
84
+
85
+ # 处理最后不满一批的残余
86
+ if buffer_count > 0:
87
+ frame_batch = buffer[:buffer_count].permute(0, 3, 1, 2)
88
+ pred = model.detect_smpl_batched(frame_batch)
89
+ if 'joints3d_nonparam' in pred:
90
+ result_list.extend(pred['joints3d_nonparam'])
91
+ else:
92
+ result_list.extend([None] * buffer_count)
93
+
94
+ index = 0
95
+ for bbox_list in bboxes:
96
+ n = len(bbox_list)
97
+ pose_meta_list.append({"video_height": height, "video_width": width, "bboxes": bbox_list, "nlfpose": result_list[index : index + n]})
98
+ index += n
99
+
100
+ del buffer # 删除 Python 引用
101
+ torch.cuda.empty_cache()
102
+ return pose_meta_list
103
+
104
+
105
+ def process_video_multi_nlf(model, vr_frames_list): # vr_frames_list里支持1-3人
106
+ # Ensure output directory exists
107
+ # pose_results = {
108
+ # 'joints3d_nonparam': [],
109
+ # }
110
+ pose_meta_list = []
111
+ vr_frames_first = vr_frames_list[0].cuda()
112
+ # vr_frames_second = vr_frames_second.cuda()
113
+ height, width = vr_frames_first.shape[1], vr_frames_first.shape[2]
114
+ result_list = []
115
+
116
+ batch_size = 64
117
+ buffer = torch.zeros(
118
+ (batch_size, height, width, 3),
119
+ dtype=vr_frames_first.dtype,
120
+ device='cuda'
121
+ )
122
+ buffer_count = 0
123
+ with torch.inference_mode(), torch.device('cuda'):
124
+ for frame_idx in range(len(vr_frames_first)):
125
+ for person_idx in range(len(vr_frames_list)):
126
+ buffer[buffer_count, :, :, :] = vr_frames_first[frame_idx] if person_idx == 0 else vr_frames_list[person_idx][frame_idx]
127
+ buffer_count += 1
128
+ # 一旦 buffer 满了,推理并清空
129
+ if buffer_count == batch_size:
130
+ frame_batch = buffer.permute(0, 3, 1, 2)
131
+ pred = model.detect_smpl_batched(frame_batch)
132
+ if 'joints3d_nonparam' in pred:
133
+ result_list.extend(pred['joints3d_nonparam'])
134
+ else:
135
+ result_list.extend([None] * buffer_count)
136
+
137
+ buffer.zero_()
138
+ buffer_count = 0
139
+
140
+ # 处理最后不满一批的残余
141
+ if buffer_count > 0:
142
+ frame_batch = buffer[:buffer_count].permute(0, 3, 1, 2)
143
+ pred = model.detect_smpl_batched(frame_batch)
144
+ if 'joints3d_nonparam' in pred:
145
+ result_list.extend(pred['joints3d_nonparam'])
146
+ else:
147
+ result_list.extend([None] * buffer_count)
148
+
149
+ index = 0
150
+ length_step = len(vr_frames_list)
151
+ for _ in range(len(vr_frames_first)):
152
+ pose_meta_list.append({"video_height": height, "video_width": width, "bboxes": None, "nlfpose": result_list[index : index + length_step]})
153
+ index += length_step
154
+
155
+ del buffer # 删除 Python 引用
156
+ torch.cuda.empty_cache()
157
+ return pose_meta_list
158
+
159
+
160
+ def process_video_nlf_original(model, vr_frames):
161
+ # Ensure output directory exists
162
+ # pose_results = {
163
+ # 'joints3d_nonparam': [],
164
+ # }
165
+ pose_meta_list = []
166
+ vr_frames = vr_frames.cuda()
167
+ height, width = vr_frames.shape[1], vr_frames.shape[2]
168
+ result_list = []
169
+ people_count_list = []
170
+
171
+ batch_size = 64
172
+ buffer = torch.zeros(
173
+ (batch_size, height, width, 3),
174
+ dtype=vr_frames.dtype,
175
+ device='cuda'
176
+ )
177
+ buffer_count = 0
178
+ with torch.inference_mode(), torch.device('cuda'):
179
+ for frame in vr_frames:
180
+ buffer[buffer_count] = frame
181
+ buffer_count += 1
182
+
183
+ # 一旦 buffer 满了,推理并清空
184
+ if buffer_count == batch_size:
185
+ frame_batch = buffer.permute(0, 3, 1, 2)
186
+ pred = model.detect_smpl_batched(frame_batch)
187
+ if 'joints3d_nonparam' in pred:
188
+ result_list.extend(pred['joints3d_nonparam'])
189
+ else:
190
+ result_list.extend([None] * buffer_count)
191
+
192
+ buffer.zero_()
193
+ buffer_count = 0
194
+
195
+ # 处理最后不满一批的残余
196
+ if buffer_count > 0:
197
+ frame_batch = buffer[:buffer_count].permute(0, 3, 1, 2)
198
+ pred = model.detect_smpl_batched(frame_batch)
199
+ if 'joints3d_nonparam' in pred:
200
+ result_list.extend(pred['joints3d_nonparam'])
201
+ else:
202
+ result_list.extend([None] * buffer_count)
203
+
204
+ index = 0
205
+ for index in range(len(vr_frames)):
206
+ pose_meta_list.append({"video_height": height, "video_width": width, "bboxes": None, "nlfpose": result_list[index]})
207
+
208
+ del buffer # 删除 Python 引用
209
+ torch.cuda.empty_cache()
210
+ return pose_meta_list
211
+
212
+
213
+ def process_fn_video(src, bbox_dir):
214
+ worker_info = torch.utils.data.get_worker_info()
215
+ for i, r in enumerate(src):
216
+ if worker_info is not None:
217
+ if i % worker_info.num_workers != worker_info.id:
218
+ continue
219
+ key = r['__key__']
220
+ mp4_bytes = r.get("mp4", None)
221
+
222
+ try:
223
+ decord.bridge.set_bridge("torch")
224
+ vr = VideoReader(io.BytesIO(mp4_bytes)) # 这里都是原视频,没有动的
225
+ frames = vr.get_batch(range(len(vr)))
226
+ frames = torch.from_numpy(frames) if type(frames) is not torch.Tensor else frames
227
+ bbox_path = os.path.join(bbox_dir, key + '.pt')
228
+ if os.path.exists(bbox_path):
229
+ bboxes = torch.load(bbox_path)
230
+ else:
231
+ print('no bboxes file: ', key)
232
+ continue
233
+ except Exception as e:
234
+ print(e)
235
+ print('load video error: ', key)
236
+ continue
237
+ item = {'__key__': key, 'frames': frames, 'bboxes': bboxes}
238
+ yield item
239
+
240
+
241
+ def producer_worker_wds(tar_paths, save_dir_bbox, task_queue):
242
+ for tar_path in tar_paths:
243
+ produce_nlfpose(tar_path, save_dir_bbox, task_queue)
244
+
245
+
246
+ def produce_nlfpose(wds_path, save_dir_bbox, task_queue):
247
+ dataset = wds.DataPipeline(
248
+ wds.SimpleShardList(wds_path, seed=None),
249
+ wds.tarfile_to_samples(),
250
+ partial(process_fn_video, bbox_dir=save_dir_bbox),
251
+ )
252
+ dataloader = DataLoader(dataset, batch_size=1, num_workers=4, shuffle=False, collate_fn=lambda x: x[0])
253
+ for data in tqdm(dataloader):
254
+ task_queue.put(data)
255
+
256
+ def gpu_worker(task_queue, save_dir_smpl):
257
+ model = torch.jit.load("/workspace/yanwenhao/dwpose_draw/NLFPoseExtract/nlf_l_multi_0.3.2.torchscript").cuda().eval()
258
+ while True:
259
+ item = task_queue.get()
260
+ if item is None:
261
+ break
262
+ try:
263
+ frames = item['frames']
264
+ key = item['__key__']
265
+ bboxes = item['bboxes']
266
+ output_data = process_video_nlf(model, frames, bboxes)
267
+
268
+ with open(os.path.join(save_dir_smpl, key + '.pkl'), 'wb') as f:
269
+ pickle.dump(output_data, f)
270
+ except Exception as e:
271
+ print(f"Task failed: {e}")
272
+
273
+
274
+ def load_config(config_path):
275
+ with open(config_path, 'r') as f:
276
+ config = yaml.safe_load(f)
277
+ return config
278
+
279
+
280
+ # def process_tar_debug(wds_path):
281
+ # model = torch.jit.load("/workspace/yanwenhao/dwpose_draw/NLFPoseExtract/nlf_l_multi_0.3.2.torchscript").cuda().eval()
282
+ # dataset = wds.DataPipeline(
283
+ # wds.SimpleShardList(wds_path, seed=None),
284
+ # wds.tarfile_to_samples(),
285
+ # partial(process_fn_video),
286
+ # )
287
+ # dataloader = DataLoader(dataset, batch_size=1, num_workers=4, shuffle=False, collate_fn=lambda x: x[0])
288
+ # for data in tqdm(dataloader):
289
+ # item = data
290
+ # if item is None:
291
+ # break
292
+ # try:
293
+ # frames = item['frames']
294
+ # key = item['__key__']
295
+ # bboxes = torch.load(os.path.join(save_dir_bbox, key + '.pt'))
296
+ # output_data = process_video_nlf(model, frames, bboxes)
297
+
298
+ # with open(os.path.join(save_dir_smpl, key + '.pkl'), 'wb') as f:
299
+ # pickle.dump(output_data, f)
300
+
301
+ # except Exception as e:
302
+ # print(f"Task failed: {e}")
303
+
304
+
305
+ if __name__ == "__main__":
306
+ import argparse
307
+
308
+ parser = argparse.ArgumentParser()
309
+ parser.add_argument('--config', type=str, default='video_directories.yaml',
310
+ help='Path to YAML configuration file')
311
+ parser.add_argument('--input_root', type=str, default='/workspace/ywh_data/pose_pack_wds_0923add_step1',
312
+ help='Input root')
313
+ parser.add_argument('--local_rank', type=int, default=0,
314
+ help='Local rank')
315
+ parser.add_argument('--world_size', type=int, default=1,
316
+ help='World size')
317
+
318
+ args = parser.parse_args()
319
+ config = load_config(args.config)
320
+ os.environ['CUDA_VISIBLE_DEVICES'] = str(args.local_rank)
321
+
322
+ video_root = config.get('video_root', '')
323
+
324
+
325
+ save_dir_smpl = os.path.join(video_root, 'smpl')
326
+ save_dir_bbox = os.path.join(video_root, 'bboxes')
327
+ os.makedirs(save_dir_smpl, exist_ok=True)
328
+
329
+
330
+ processes = [] # 存储进程的列表
331
+ max_queue_size = 32
332
+ task_queue = multiprocessing.Queue(maxsize=max_queue_size)
333
+ # Split wds_list into chunks
334
+ input_root = os.path.join(args.input_root, os.path.basename(os.path.normpath(video_root)))
335
+ input_tar_paths = glob.glob(os.path.join(input_root, "**", "*.tar"), recursive=True)
336
+ input_tar_paths = sorted(input_tar_paths)
337
+ input_tar_paths_for_the_rank = input_tar_paths[args.local_rank::args.world_size]
338
+
339
+ # 并行流程
340
+ p = multiprocessing.Process(target=gpu_worker, args=(task_queue, save_dir_smpl))
341
+ p.start()
342
+
343
+ producer_worker_wds(input_tar_paths_for_the_rank, save_dir_bbox, task_queue)
344
+ for _ in range(max_queue_size):
345
+ task_queue.put(None)
346
+
347
+ p.join(timeout=6000)
348
+ if p.is_alive():
349
+ print("Warning: GPU worker process did not finish within the expected time")
350
+ p.terminate()
351
+
352
+ # 串行debug
353
+ # for wds_path in input_tar_paths_for_the_rank:
354
+ # process_tar_debug(wds_path)
355
+
356
+
357
+
358
+
359
+
360
+
361
+
362
+
363
+
364
+
365
+
SCAIL-Pose/NLFPoseExtract/nlf_draw.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import math
4
+ from PIL import Image
5
+ from DWPoseProcess.dwpose.util import draw_bodypose
6
+
7
+
8
+ def process_data_to_COCO_format(joints):
9
+ """Args:
10
+ joints: numpy array of shape (24, 2) or (24, 3)
11
+ Returns:
12
+ new_joints: numpy array of shape (17, 2) or (17, 3)
13
+ """
14
+ if joints.ndim != 2:
15
+ raise ValueError(f"Expected shape (24,2) or (24,3), got {joints.shape}")
16
+
17
+ dim = joints.shape[1] # 2D or 3D
18
+
19
+ mapping = {
20
+ 15: 0, # head
21
+ 12: 1, # neck
22
+ 17: 2, # left shoulder
23
+ 16: 5, # right shoulder
24
+ 19: 3, # left elbow
25
+ 18: 6, # right elbow
26
+ 21: 4, # left hand
27
+ 20: 7, # right hand
28
+ 2: 8, # left pelvis
29
+ 1: 11, # right pelvis
30
+ 5: 9, # left knee
31
+ 4: 12, # right knee
32
+ 8: 10, # left feet
33
+ 7: 13, # right feet
34
+ }
35
+
36
+ new_joints = np.zeros((18, dim), dtype=joints.dtype)
37
+ for src, dst in mapping.items():
38
+ new_joints[dst] = joints[src]
39
+
40
+ return new_joints
41
+
42
+
43
+ def intrinsic_matrix_from_field_of_view(imshape, fov_degrees:float =55): # nlf default fov_degrees 55
44
+ imshape = np.array(imshape)
45
+ fov_radians = fov_degrees * np.array(np.pi / 180)
46
+ larger_side = np.max(imshape)
47
+ focal_length = larger_side / (np.tan(fov_radians / 2) * 2)
48
+ # intrinsic_matrix 3*3
49
+ return np.array([
50
+ [focal_length, 0, imshape[1] / 2],
51
+ [0, focal_length, imshape[0] / 2],
52
+ [0, 0, 1],
53
+ ])
54
+
55
+
56
+ def p3d_to_p2d(point_3d, height, width): # point3d n*num_points*3
57
+ camera_matrix = intrinsic_matrix_from_field_of_view((height,width))
58
+ camera_matrix = np.expand_dims(camera_matrix, axis=0)
59
+ camera_matrix = np.expand_dims(camera_matrix, axis=0) # 1*1*3*3
60
+ point_3d = np.expand_dims(point_3d,axis=-1) # n*num_points*3*1
61
+ point_2d = (camera_matrix@point_3d).squeeze(-1) # n*num_points*3
62
+ point_2d[:,:,:2] = point_2d[:,:,:2]/point_2d[:,:,2:3] # 相对位置
63
+ return point_2d[:,:,:] # n*num_points*2
64
+
65
+ def preview_nlf_2d(data):
66
+ """ return a list of images """
67
+ height, width = data['video_height'], data['video_width']
68
+ offset = [height, width, 0]
69
+ np_images = []
70
+ for image_result in data['pose']['joints3d_nonparam']:
71
+ final_canvas = np.zeros(shape=(offset[0], offset[1], 3), dtype=np.uint8)
72
+ for joints3d in image_result: # 每个人的pose
73
+ canvas = np.zeros(shape=(offset[0], offset[1], 3), dtype=np.uint8)
74
+ joints3d = joints3d.cpu().numpy()
75
+ joints2d = p3d_to_p2d(joints3d, offset[0], offset[1])
76
+ joints2d = joints2d[0][:, :2]
77
+ joints2d[:, 0] = joints2d[:, 0] / offset[1] # x坐标归一化
78
+ joints2d[:, 1] = joints2d[:, 1] / offset[0] # y坐标归一化
79
+ joints2d = process_data_to_COCO_format(joints2d)
80
+ subset = np.expand_dims(np.concatenate([np.arange(14), [-1, -1, -1, -1]]), axis=0)
81
+ canvas = draw_bodypose(canvas, joints2d, subset)
82
+ final_canvas = final_canvas + canvas
83
+ np_images.append(final_canvas)
84
+
85
+ return np_images
86
+
87
+
88
+
89
+ def preview_nlf_2d_ori(nlf_results):
90
+ """ return a list of images """
91
+ height, width = nlf_results[0]['video_height'], nlf_results[0]['video_width']
92
+ offset = [height, width, 0]
93
+ np_images = []
94
+ for single_result in nlf_results:
95
+ image_result = single_result['nlfpose']
96
+ final_canvas = np.zeros(shape=(offset[0], offset[1], 3), dtype=np.uint8)
97
+ for joints3d in image_result: # 每个人的pose
98
+ canvas = np.zeros(shape=(offset[0], offset[1], 3), dtype=np.uint8)
99
+ joints3d = joints3d.cpu().numpy()
100
+ joints2d = p3d_to_p2d(joints3d, offset[0], offset[1])
101
+ joints2d = joints2d[0][:, :2]
102
+ joints2d[:, 0] = joints2d[:, 0] / offset[1] # x坐标归一化
103
+ joints2d[:, 1] = joints2d[:, 1] / offset[0] # y坐标归一化
104
+ joints2d = process_data_to_COCO_format(joints2d)
105
+ subset = np.expand_dims(np.concatenate([np.arange(14), [-1, -1, -1, -1]]), axis=0)
106
+ canvas = draw_bodypose(canvas, joints2d, subset)
107
+ final_canvas = final_canvas + canvas
108
+ np_images.append(final_canvas)
109
+
110
+ return np_images
111
+
112
+
113
+ def preview_nlf_2d_new(nlf_results):
114
+ """ return a list of images """
115
+ height, width = nlf_results[0]['video_height'], nlf_results[0]['video_width']
116
+ offset = [height, width, 0]
117
+ np_images = []
118
+ for bbox_result in nlf_results:
119
+ final_canvas = np.zeros(shape=(offset[0], offset[1], 3), dtype=np.uint8)
120
+ bbox_result = bbox_result['nlfpose']
121
+ for image_result in bbox_result:
122
+ for joints3d in image_result: # 每个人的pose
123
+ canvas = np.zeros(shape=(offset[0], offset[1], 3), dtype=np.uint8)
124
+ joints3d = joints3d.cpu().numpy()
125
+ joints2d = p3d_to_p2d(joints3d, offset[0], offset[1])
126
+ joints2d = joints2d[0][:, :2]
127
+ joints2d[:, 0] = joints2d[:, 0] / offset[1] # x坐标归一化
128
+ joints2d[:, 1] = joints2d[:, 1] / offset[0] # y坐标归一化
129
+ joints2d = process_data_to_COCO_format(joints2d)
130
+ subset = np.expand_dims(np.concatenate([np.arange(14), [-1, -1, -1, -1]]), axis=0)
131
+ canvas = draw_bodypose(canvas, joints2d, subset)
132
+ final_canvas = final_canvas + canvas
133
+ np_images.append(final_canvas)
134
+
135
+ return np_images
SCAIL-Pose/NLFPoseExtract/nlf_render.py ADDED
@@ -0,0 +1,658 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import math
4
+ from PIL import Image
5
+ from render_3d.taichi_cylinder import render_whole
6
+ from NLFPoseExtract.nlf_draw import intrinsic_matrix_from_field_of_view, process_data_to_COCO_format, preview_nlf_2d, p3d_to_p2d
7
+ from concurrent.futures import ProcessPoolExecutor, as_completed
8
+ from pose_draw.draw_pose_utils import draw_pose_to_canvas_np, scale_image_hw_keep_size
9
+ import pose_draw.draw_utils as draw_utils
10
+ import torch.multiprocessing as mp
11
+ import os
12
+ os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
13
+ import copy
14
+ import random
15
+ import torch
16
+ try:
17
+ import moviepy.editor as mpy
18
+ except Exception:
19
+ import moviepy as mpy
20
+
21
+ def p3d_single_p2d(points, intrinsic_matrix):
22
+ X, Y, Z = points[0], points[1], points[2]
23
+ u = (intrinsic_matrix[0, 0] * X / Z) + intrinsic_matrix[0, 2]
24
+ v = (intrinsic_matrix[1, 1] * Y / Z) + intrinsic_matrix[1, 2]
25
+ u_np = u.cpu().numpy()
26
+ v_np = v.cpu().numpy()
27
+ return np.array([u_np, v_np])
28
+
29
+ def scale_around_center(points, center, dim, scale=1.0):
30
+ return (points[:, dim] - center[dim]) * scale + center[dim]
31
+
32
+ def shift_dwpose_according_to_nlf(smpl_poses, aligned_poses, ori_intrinstics, modified_intrinstics, height, width, scale_x = 1.0, scale_y = 1.0):
33
+ ########## warning: 会改变body; shift 之后 body是不准的 ##########
34
+ for i in range(len(smpl_poses)):
35
+ persons_joints_list = smpl_poses[i]
36
+ poses_list = aligned_poses[i]
37
+ # 对里面每一个人,取关节并进行变形;并且修改2d;如果3d不存在,把2d的手/脸也去掉
38
+ for person_idx, person_joints in enumerate(persons_joints_list):
39
+ face = poses_list["faces"][person_idx]
40
+ right_hand = poses_list["hands"][2 * person_idx]
41
+ left_hand = poses_list["hands"][2 * person_idx + 1]
42
+ candidate = poses_list["bodies"]["candidate"][person_idx]
43
+ # 注意,这里不是coco format
44
+ person_joint_15_2d_shift = p3d_single_p2d(person_joints[15], modified_intrinstics) - p3d_single_p2d(person_joints[15], ori_intrinstics) if person_joints[15, 2] > 0.01 else np.array([0.0, 0.0]) # face
45
+ person_joint_20_2d_shift = p3d_single_p2d(person_joints[20], modified_intrinstics) - p3d_single_p2d(person_joints[20], ori_intrinstics) if person_joints[20, 2] > 0.01 else np.array([0.0, 0.0]) # right hand
46
+ person_joint_21_2d_shift = p3d_single_p2d(person_joints[21], modified_intrinstics) - p3d_single_p2d(person_joints[21], ori_intrinstics) if person_joints[21, 2] > 0.01 else np.array([0.0, 0.0]) # left hand
47
+
48
+ face[:, 0] += person_joint_15_2d_shift[0] / width
49
+ face[:, 1] += person_joint_15_2d_shift[1] / height
50
+ right_hand[:, 0] += person_joint_20_2d_shift[0] / width
51
+ right_hand[:, 1] += person_joint_20_2d_shift[1] / height
52
+ left_hand[:, 0] += person_joint_21_2d_shift[0] / width
53
+ left_hand[:, 1] += person_joint_21_2d_shift[1] / height
54
+ candidate[:, 0] += person_joint_15_2d_shift[0] / width
55
+ candidate[:, 1] += person_joint_15_2d_shift[1] / height
56
+
57
+ scales = [scale_x, scale_y]
58
+ # apply camera scale around wrist (hand[0]).
59
+ for dim in [0,1]:
60
+ right_hand[:, dim] = scale_around_center(right_hand, right_hand[0, :], dim=dim, scale=scales[dim])
61
+ left_hand[:, dim] = scale_around_center(left_hand, left_hand[0, :], dim=dim, scale=scales[dim])
62
+
63
+ def get_single_pose_cylinder_specs(args):
64
+ """渲染单个pose的辅助函数,用于并行处理"""
65
+ idx, pose, focal, princpt, height, width, colors, limb_seq, draw_seq = args
66
+ cylinder_specs = []
67
+
68
+ for joints3d in pose: # 多人
69
+ joints3d = joints3d.cpu().numpy()
70
+ joints3d = process_data_to_COCO_format(joints3d)
71
+ for line_idx in draw_seq:
72
+ line = limb_seq[line_idx]
73
+ start, end = line[0], line[1]
74
+ if np.sum(joints3d[start]) == 0 or np.sum(joints3d[end]) == 0:
75
+ continue
76
+ else:
77
+ cylinder_specs.append((joints3d[start], joints3d[end], colors[line_idx]))
78
+ return cylinder_specs
79
+
80
+ def get_single_pose_cylinder_specs_mono(args):
81
+ """渲染单个pose的辅助函数,用于并行处理"""
82
+ idx, pose, ori_pose, binary_frame, intrinsic_matrix, height, width, limb_seq, draw_seq = args
83
+ cylinder_specs = []
84
+
85
+ for joints3d, ori_joint3d in zip(pose, ori_pose): # 多人
86
+ joints3d = joints3d.cpu().numpy()
87
+ joints3d = process_data_to_COCO_format(joints3d)
88
+ ori_joint3d = ori_joint3d.cpu().numpy()
89
+ ori_joint3d = process_data_to_COCO_format(ori_joint3d)
90
+ specific_color = locate_binary_color(binary_frame, ori_joint3d, height, width) # 通过3D点的2D投影的像素位置,计算原本这个人对应的颜色
91
+ for line_idx in draw_seq:
92
+ line = limb_seq[line_idx]
93
+ start, end = line[0], line[1]
94
+ if np.sum(joints3d[start]) == 0 or np.sum(joints3d[end]) == 0:
95
+ continue
96
+ else:
97
+ cylinder_specs.append((joints3d[start], joints3d[end], specific_color))
98
+ return cylinder_specs
99
+
100
+ def get_single_pose_cylinder_specs_colored(args):
101
+ """直接使用传入的每人颜色渲染,不做颜色查找。"""
102
+ idx, pose, person_colors, limb_seq, draw_seq = args
103
+ cylinder_specs = []
104
+ for person_idx, joints3d in enumerate(pose):
105
+ joints3d = joints3d.cpu().numpy()
106
+ joints3d = process_data_to_COCO_format(joints3d)
107
+ color = person_colors[person_idx] if person_idx < len(person_colors) else [0, 0, 0, 1]
108
+ for line_idx in draw_seq:
109
+ line = limb_seq[line_idx]
110
+ start, end = line[0], line[1]
111
+ if np.sum(joints3d[start]) == 0 or np.sum(joints3d[end]) == 0:
112
+ continue
113
+ cylinder_specs.append((joints3d[start], joints3d[end], color))
114
+ return cylinder_specs
115
+
116
+
117
+ def locate_binary_color(binary_frame, ori_joint3d, height, width):
118
+ """通过3D点的2D投影的像素位置,计算原本这个人对应的颜色。
119
+ ori_joint3d: COCO format (18, 3) numpy array
120
+ binary_frame: H x W x 3,BGR,像素颜色只有6种纯色之一
121
+ """
122
+ key_joint_indices = [1, 2, 5, 8, 11] # neck, left shoulder, right shoulder, left pelvis, right pelvis
123
+ key_joints_3d = ori_joint3d[key_joint_indices] # (5, 3)
124
+ valid_flag = key_joints_3d[:, 2] > 0.0001
125
+
126
+ point_2d = p3d_to_p2d(key_joints_3d[np.newaxis], height, width)[0] # (5, 3)
127
+
128
+ sampled = []
129
+ for is_valid, p2d in zip(valid_flag, point_2d):
130
+ if not is_valid:
131
+ continue
132
+ u = int(round(p2d[0]))
133
+ v = int(round(p2d[1]))
134
+ if 0 <= u < width and 0 <= v < height:
135
+ sampled.append(binary_frame[v, u].astype(np.float32))
136
+
137
+ if len(sampled) == 0:
138
+ return [0, 0, 0, 1]
139
+
140
+ avg_color = np.mean(sampled, axis=0)
141
+ binarized = (avg_color > 127).astype(np.float32) * 1.0
142
+
143
+ return [binarized[0], binarized[1], binarized[2], 1]
144
+
145
+ def collect_smpl_poses(data):
146
+ uncollected_smpl_poses = [item['nlfpose'] for item in data]
147
+ smpl_poses = [[] for _ in range(len(uncollected_smpl_poses))]
148
+ for frame_idx in range(len(uncollected_smpl_poses)):
149
+ for person_idx in range(len(uncollected_smpl_poses[frame_idx])): # 每个人(每个bbox)只给出一个pose
150
+ if len(uncollected_smpl_poses[frame_idx][person_idx]) > 0: # 有返回的骨骼
151
+ smpl_poses[frame_idx].append(uncollected_smpl_poses[frame_idx][person_idx][0])
152
+ else:
153
+ smpl_poses[frame_idx].append(torch.zeros((24, 3), dtype=torch.float32)) # 没有检测到人,就放一个全0的
154
+
155
+ return smpl_poses
156
+
157
+
158
+
159
+ def collect_smpl_poses_samurai(data):
160
+ uncollected_smpl_poses = [item['nlfpose'] for item in data]
161
+ smpl_poses_first = [[] for _ in range(len(uncollected_smpl_poses))]
162
+ smpl_poses_second = [[] for _ in range(len(uncollected_smpl_poses))]
163
+
164
+ for frame_idx in range(len(uncollected_smpl_poses)):
165
+ for person_idx in range(len(uncollected_smpl_poses[frame_idx])): # 每个人(每个bbox)只给出一个pose
166
+ if len(uncollected_smpl_poses[frame_idx][person_idx]) > 0: # 有返回的骨骼
167
+ if person_idx == 0:
168
+ smpl_poses_first[frame_idx].append(uncollected_smpl_poses[frame_idx][person_idx][0])
169
+ elif person_idx == 1:
170
+ smpl_poses_second[frame_idx].append(uncollected_smpl_poses[frame_idx][person_idx][0])
171
+ else:
172
+ if person_idx == 0:
173
+ smpl_poses_first[frame_idx].append(torch.zeros((24, 3), dtype=torch.float32)) # 没有检测到人,就放一个全0的
174
+ elif person_idx == 1:
175
+ smpl_poses_second[frame_idx].append(torch.zeros((24, 3), dtype=torch.float32))
176
+
177
+ return smpl_poses_first, smpl_poses_second
178
+
179
+
180
+
181
+
182
+ def render_nlf_as_images(data, poses, reshape_pool=None, intrinsic_matrix=None, draw_2d=True, aug_2d=False, aug_cam=False, binary_mask=None, person_colors=None, palette_offset=0):
183
+ """ return a list of images """
184
+ height, width = data[0]['video_height'], data[0]['video_width']
185
+ video_length = len(data)
186
+
187
+ base_colors_255_dict = {
188
+ # Warm Colors for Right Side (R.) - Red, Orange, Yellow
189
+ "Red": [255, 0, 0],
190
+ "Orange": [255, 85, 0],
191
+ "Golden Orange": [255, 170, 0],
192
+ "Yellow": [255, 240, 0],
193
+ "Yellow-Green": [180, 255, 0],
194
+ # Cool Colors for Left Side (L.) - Green, Blue, Purple
195
+ "Bright Green": [0, 255, 0],
196
+ "Light Green-Blue": [0, 255, 85],
197
+ "Aqua": [0, 255, 170],
198
+ "Cyan": [0, 255, 255],
199
+ "Sky Blue": [0, 170, 255],
200
+ "Medium Blue": [0, 85, 255],
201
+ "Pure Blue": [0, 0, 255],
202
+ "Purple-Blue": [85, 0, 255],
203
+ "Medium Purple": [170, 0, 255],
204
+ # Neutral/Central Colors (e.g., for Neck, Nose, Eyes, Ears)
205
+ "Grey": [150, 150, 150],
206
+ "Pink-Magenta": [255, 0, 170],
207
+ "Dark Pink": [255, 0, 85],
208
+ "Violet": [100, 0, 255],
209
+ "Dark Violet": [50, 0, 255],
210
+ }
211
+
212
+ ordered_colors_255 = [
213
+ base_colors_255_dict["Red"], # Neck -> R. Shoulder (Red)
214
+ base_colors_255_dict["Cyan"], # Neck -> L. Shoulder (Cyan)
215
+ base_colors_255_dict["Orange"], # R. Shoulder -> R. Elbow (Orange)
216
+ base_colors_255_dict["Golden Orange"], # R. Elbow -> R. Wrist (Golden Orange)
217
+ base_colors_255_dict["Sky Blue"], # L. Shoulder -> L. Elbow (Sky Blue)
218
+ base_colors_255_dict["Medium Blue"], # L. Elbow -> L. Wrist (Medium Blue)
219
+ base_colors_255_dict["Yellow-Green"], # Neck -> R. Hip ( Yellow-Green)
220
+ base_colors_255_dict["Bright Green"], # R. Hip -> R. Knee (Bright Green - transitioning warm to cool spectrum)
221
+ base_colors_255_dict["Light Green-Blue"], # R. Knee -> R. Ankle (Light Green-Blue - transitioning)
222
+ base_colors_255_dict["Pure Blue"], # Neck -> L. Hip (Pure Blue)
223
+ base_colors_255_dict["Purple-Blue"], # L. Hip -> L. Knee (Purple-Blue)
224
+ base_colors_255_dict["Medium Purple"], # L. Knee -> L. Ankle (Medium Purple)
225
+ base_colors_255_dict["Grey"], # Neck -> Nose (Grey)
226
+ base_colors_255_dict["Pink-Magenta"], # Nose -> R. Eye (Pink/Magenta)
227
+ base_colors_255_dict["Dark Violet"], # R. Eye -> R. Ear (Dark Pink)
228
+ base_colors_255_dict["Pink-Magenta"], # Nose -> L. Eye (Violet)
229
+ base_colors_255_dict["Dark Violet"], # L. Eye -> L. Ear (Dark Violet)
230
+ ]
231
+
232
+ limb_seq = [
233
+ [1, 2], # 0 Neck -> R. Shoulder
234
+ [1, 5], # 1 Neck -> L. Shoulder
235
+ [2, 3], # 2 R. Shoulder -> R. Elbow
236
+ [3, 4], # 3 R. Elbow -> R. Wrist
237
+ [5, 6], # 4 L. Shoulder -> L. Elbow
238
+ [6, 7], # 5 L. Elbow -> L. Wrist
239
+ [1, 8], # 6 Neck -> R. Hip
240
+ [8, 9], # 7 R. Hip -> R. Knee
241
+ [9, 10], # 8 R. Knee -> R. Ankle
242
+ [1, 11], # 9 Neck -> L. Hip
243
+ [11, 12], # 10 L. Hip -> L. Knee
244
+ [12, 13], # 11 L. Knee -> L. Ankle
245
+ [1, 0], # 12 Neck -> Nose
246
+ [0, 14], # 13 Nose -> R. Eye
247
+ [14, 16], # 14 R. Eye -> R. Ear
248
+ [0, 15], # 15 Nose -> L. Eye
249
+ [15, 17], # 16 L. Eye -> L. Ear
250
+ ]
251
+
252
+ draw_seq = [0, 2, 3, # Neck -> R. Shoulder -> R. Elbow -> R. Wrist
253
+ 1, 4, 5, # Neck -> L. Shoulder -> L. Elbow -> L. Wrist
254
+ 6, 7, 8, # Neck -> R. Hip -> R. Knee -> R. Ankle
255
+ 9, 10, 11, # Neck -> L. Hip -> L. Knee -> L. Ankle
256
+ 12, # Neck -> Nose
257
+ 13, 14, # Nose -> R. Eye -> R. Ear
258
+ 15, 16, # Nose -> L. Eye -> L. Ear
259
+ ] # 从近心端往外扩展
260
+
261
+ colors = [[c / 300 + 0.15 for c in color_rgb] + [0.8] for color_rgb in ordered_colors_255]
262
+
263
+
264
+
265
+ # smpl_poses 会在这里被修改
266
+ if poses is not None or binary_mask is not None or person_colors is not None:
267
+ # 重新收集poses
268
+ smpl_poses = collect_smpl_poses(data)
269
+ if binary_mask is not None:
270
+ original_smpl_poses = copy.deepcopy(smpl_poses)
271
+ if poses is not None:
272
+ aligned_poses = copy.deepcopy(poses) # 2d poses
273
+ if reshape_pool is not None:
274
+ for i in range(video_length):
275
+ persons_joints_list = smpl_poses[i]
276
+ poses_list = aligned_poses[i]
277
+ # 对里面每一个人,取关节并进行变形;并且修改2d;如果3d不存在,把2d的手/脸也去掉
278
+ for person_idx, person_joints in enumerate(persons_joints_list):
279
+ candidate = poses_list['bodies']['candidate'][person_idx]
280
+ subset = poses_list['bodies']['subset'][person_idx]
281
+ face = poses_list["faces"][person_idx]
282
+ right_hand = poses_list["hands"][2 * person_idx]
283
+ left_hand = poses_list["hands"][2 * person_idx + 1]
284
+ reshape_pool.apply_random_reshapes(person_joints, candidate, left_hand, right_hand, face, subset)
285
+ else:
286
+ smpl_poses = [item['nlfpose'] for item in data] # 主要为了兼容多人评测集;搭配process_video_nlf_original
287
+
288
+
289
+ if intrinsic_matrix is None:
290
+ intrinsic_matrix = intrinsic_matrix_from_field_of_view((height, width))
291
+ focal_x = intrinsic_matrix[0,0]
292
+ focal_y = intrinsic_matrix[1,1]
293
+ princpt = (intrinsic_matrix[0,2], intrinsic_matrix[1,2]) # 主点 (cx, cy)
294
+ if aug_cam and random.random() < 0.3:
295
+ w_shift_factor = random.uniform(-0.04, 0.04)
296
+ h_shift_factor = random.uniform(-0.04, 0.04)
297
+ princpt = (princpt[0] - w_shift_factor * width, princpt[1] - h_shift_factor * height) # princpt变化和点的变化相反
298
+ new_intrinsic_matrix = copy.deepcopy(intrinsic_matrix)
299
+ new_intrinsic_matrix[0,2] = princpt[0]
300
+ new_intrinsic_matrix[1,2] = princpt[1]
301
+ shift_dwpose_according_to_nlf(smpl_poses, aligned_poses, intrinsic_matrix, new_intrinsic_matrix, height, width)
302
+
303
+ # person_colors 传入时,为每人生成独立肢体颜色方案(同 render_multi_nlf_as_images 的两套配色)
304
+ if person_colors is not None:
305
+ _palettes_255 = [
306
+ # Person 0: 浅色调
307
+ [[255,150,150],[180,230,240],[255,180,140],[255,215,150],[160,200,255],[100,120,255],
308
+ [200,255,100],[100,255,100],[140,255,180],[120,140,255],[180, 90,255],[190,120,255],
309
+ [210,210,210],[255,120,200],[130, 80,255],[255,120,200],[130, 80,255]],
310
+ # Person 1: 饱和色调
311
+ [[255, 20, 20],[ 0,230,255],[255, 60, 0],[255,110, 0],[ 0,130,255],[ 0, 70,255],
312
+ [160,255, 40],[ 0,255, 50],[ 0,255,100],[ 0, 0,255],[ 80, 0,255],[160, 0,255],
313
+ [130,130,130],[255, 0,150],[ 60, 0,255],[255, 0,150],[ 60, 0,255]],
314
+ ]
315
+ colors_per_person = [
316
+ [[c / 300 + 0.15 for c in rgb] + [0.8]
317
+ for rgb in _palettes_255[(p + palette_offset) % len(_palettes_255)]]
318
+ for p in range(len(person_colors))
319
+ ]
320
+
321
+ # 串行获取每一帧的cylinder_specs
322
+ cylinder_specs_list = []
323
+ cylinder_specs_list_mono = []
324
+ for i in range(video_length):
325
+ if person_colors is not None:
326
+ cylinder_specs = []
327
+ for p_idx, person_pose in enumerate(smpl_poses[i]):
328
+ p_limb_colors = colors_per_person[p_idx] if p_idx < len(colors_per_person) else colors
329
+ cylinder_specs.extend(get_single_pose_cylinder_specs(
330
+ (i, [person_pose], None, None, None, None, p_limb_colors, limb_seq, draw_seq)))
331
+ else:
332
+ cylinder_specs = get_single_pose_cylinder_specs((i, smpl_poses[i], None, None, None, None, colors, limb_seq, draw_seq))
333
+ cylinder_specs_list.append(cylinder_specs)
334
+ if person_colors is not None:
335
+ cylinder_specs_colored = get_single_pose_cylinder_specs_colored((i, smpl_poses[i], person_colors, limb_seq, draw_seq))
336
+ cylinder_specs_list_mono.append(cylinder_specs_colored)
337
+ elif binary_mask is not None:
338
+ cylinder_specs_mono = get_single_pose_cylinder_specs_mono((i, smpl_poses[i], original_smpl_poses[i], binary_mask[i], intrinsic_matrix, height, width, limb_seq, draw_seq))
339
+ cylinder_specs_list_mono.append(cylinder_specs_mono)
340
+
341
+
342
+ frames_np_rgba = render_whole(cylinder_specs_list, H=height, W=width, fx=focal_x, fy=focal_y, cx=princpt[0], cy=princpt[1])
343
+ frames_np_rgba_mono = render_whole(cylinder_specs_list_mono, H=height, W=width, fx=focal_x, fy=focal_y, cx=princpt[0], cy=princpt[1], use_specular=False) if (binary_mask is not None or person_colors is not None) else None
344
+
345
+ bg_color = np.array([0, 0, 0], dtype=np.uint8)
346
+ for frame in frames_np_rgba:
347
+ bg_mask = frame[:, :, 3] == 0
348
+ frame[:, :, :3][bg_mask] = bg_color
349
+
350
+ scale_h = random.uniform(0.85, 1.15)
351
+ scale_w = random.uniform(0.85, 1.15)
352
+ rescale_flag = random.random() < 0.4 if reshape_pool is not None else False
353
+
354
+ if poses is not None and draw_2d:
355
+ canvas_2d = draw_pose_to_canvas_np(aligned_poses, pool=None, H=height, W=width, reshape_scale=0, show_feet_flag=False, show_body_flag=False, show_cheek_flag=True, dw_hand=True)
356
+ for i in range(len(frames_np_rgba)):
357
+ frame_img = frames_np_rgba[i]
358
+ canvas_img = canvas_2d[i]
359
+ mask = canvas_img != 0
360
+ frame_img[:, :, :3][mask] = canvas_img[mask]
361
+ frames_np_rgba[i] = frame_img # no alpha blending
362
+ # 在 mono 版上用每人的颜色画 cheek/hand/face 2D 关键点
363
+ if frames_np_rgba_mono is not None and person_colors is not None:
364
+ poses_list = aligned_poses[i]
365
+ n_draw = min(len(poses_list['bodies']['candidate']), len(person_colors))
366
+ for p_idx in range(n_draw):
367
+ temp_canvas = np.zeros((height, width, 3), dtype=np.uint8)
368
+ p_candidate = poses_list['bodies']['candidate'][p_idx]
369
+ p_subset = poses_list['bodies']['subset'][p_idx:p_idx+1]
370
+ p_faces = poses_list['faces'][p_idx:p_idx+1]
371
+ p_hands = poses_list['hands'][2*p_idx:2*p_idx+2]
372
+ temp_canvas = draw_utils.draw_bodypose_augmentation(temp_canvas, p_candidate, p_subset, drop_aug=False, shift_aug=False, all_cheek_aug=True)
373
+ temp_canvas = draw_utils.draw_handpose(temp_canvas, p_hands)
374
+ temp_canvas = draw_utils.draw_facepose(temp_canvas, p_faces, optimized_face=True)
375
+ mask_2d = np.any(temp_canvas != 0, axis=-1)
376
+ mono_color = [int(c * 255) for c in person_colors[p_idx][:3]]
377
+ frames_np_rgba_mono[i][:, :, :3][mask_2d] = mono_color
378
+ if aug_2d:
379
+ if rescale_flag:
380
+ frames_np_rgba[i] = scale_image_hw_keep_size(frames_np_rgba[i], scale_h, scale_w)
381
+ border_mask = frames_np_rgba[i][:, :, 3] == 0
382
+ frames_np_rgba[i][:, :, :3][border_mask] = bg_color
383
+ if reshape_pool is not None and random.random() < 0.04:
384
+ # 4%的概率完全消除某些帧,两组同步
385
+ frames_np_rgba[i][:, :, :3] = bg_color
386
+ if frames_np_rgba_mono is not None:
387
+ frames_np_rgba_mono[i][:, :, 0:3] = 0
388
+ if frames_np_rgba_mono is not None and rescale_flag:
389
+ frames_np_rgba_mono[i] = scale_image_hw_keep_size(frames_np_rgba_mono[i], scale_h, scale_w)
390
+ else:
391
+ for i in range(len(frames_np_rgba)):
392
+ if aug_2d:
393
+ if rescale_flag:
394
+ frames_np_rgba[i] = scale_image_hw_keep_size(frames_np_rgba[i], scale_h, scale_w)
395
+ border_mask = frames_np_rgba[i][:, :, 3] == 0
396
+ frames_np_rgba[i][:, :, :3][border_mask] = bg_color
397
+ if reshape_pool is not None and random.random() < 0.04:
398
+ # 4%的概率完全消除某些帧,两组同步
399
+ frames_np_rgba[i][:, :, :3] = bg_color
400
+ if frames_np_rgba_mono is not None:
401
+ frames_np_rgba_mono[i][:, :, 0:3] = 0
402
+ if frames_np_rgba_mono is not None and rescale_flag:
403
+ frames_np_rgba_mono[i] = scale_image_hw_keep_size(frames_np_rgba_mono[i], scale_h, scale_w)
404
+
405
+ if binary_mask is not None or person_colors is not None:
406
+ return frames_np_rgba, frames_np_rgba_mono
407
+ return frames_np_rgba
408
+
409
+
410
+
411
+
412
+
413
+
414
+
415
+ def render_multi_nlf_as_images(data, poses, reshape_pool=None, intrinsic_matrix=None, draw_2d=True, aug_2d=False, aug_cam=False):
416
+ """ return a list of images """
417
+ height, width = data[0]['video_height'], data[0]['video_width']
418
+ video_length = len(data)
419
+
420
+ second_person_base_colors_255_dict = {
421
+ # Warm Colors for Right Side (R.) - Red, Orange, Yellow
422
+ "Red": [255, 20, 20],
423
+ "Orange": [255, 60, 0],
424
+ "Golden Orange": [255, 110, 0],
425
+ "Yellow": [255, 200, 0],
426
+ "Yellow-Green": [160, 255, 40],
427
+
428
+ # Cool Colors for Left Side (L.) - Green, Blue, Purple
429
+ "Bright Green": [0, 255, 50],
430
+ "Light Green-Blue": [0, 255, 100],
431
+ "Aqua": [0, 255, 200],
432
+ "Cyan": [0, 230, 255],
433
+ "Sky Blue": [0, 130, 255],
434
+ "Medium Blue": [0, 70, 255],
435
+ "Pure Blue": [0, 0, 255],
436
+ "Purple-Blue": [80, 0, 255],
437
+ "Medium Purple": [160, 0, 255],
438
+
439
+ # Neutral/Central Colors (e.g., for Neck, Nose, Eyes, Ears)
440
+ "Grey": [130, 130, 130],
441
+ "Pink-Magenta": [255, 0, 150],
442
+ "Dark Pink": [255, 0, 100],
443
+ "Violet": [120, 0, 255],
444
+ "Dark Violet": [60, 0, 255],
445
+ }
446
+
447
+ first_person_base_colors_255_dict = {
448
+ # Warm Colors for Right Side (R.) - Red, Orange, Yellow
449
+ "Red": [255, 150, 150],
450
+ "Orange": [255, 180, 140],
451
+ "Golden Orange": [255, 215, 150],
452
+ "Yellow": [255, 240, 170],
453
+ "Yellow-Green": [200, 255, 100],
454
+
455
+ # Cool Colors for Left Side (L.) - Green, Blue, Purple
456
+ "Bright Green": [100, 255, 100],
457
+ "Light Green-Blue": [140, 255, 180],
458
+ "Aqua": [150, 240, 200],
459
+ "Cyan": [180, 230, 240],
460
+ "Sky Blue": [160, 200, 255],
461
+ "Medium Blue": [100, 120, 255],
462
+ "Pure Blue": [120, 140, 255],
463
+ "Purple-Blue": [180, 90, 255],
464
+ "Medium Purple": [190, 120, 255],
465
+
466
+ # Neutral/Central Colors (e.g., for Neck, Nose, Eyes, Ears)
467
+ "Grey": [210, 210, 210],
468
+ "Pink-Magenta": [255, 120, 200],
469
+ "Dark Pink": [255, 150, 180],
470
+ "Violet": [200, 90, 255],
471
+ "Dark Violet": [130, 80, 255],
472
+ }
473
+
474
+ base_colors_255_dict_list = [first_person_base_colors_255_dict, second_person_base_colors_255_dict]
475
+ ordered_colors_255_list = [[
476
+ base_colors_255_dict["Red"], # Neck -> R. Shoulder (Red)
477
+ base_colors_255_dict["Cyan"], # Neck -> L. Shoulder (Cyan)
478
+ base_colors_255_dict["Orange"], # R. Shoulder -> R. Elbow (Orange)
479
+ base_colors_255_dict["Golden Orange"], # R. Elbow -> R. Wrist (Golden Orange)
480
+ base_colors_255_dict["Sky Blue"], # L. Shoulder -> L. Elbow (Sky Blue)
481
+ base_colors_255_dict["Medium Blue"], # L. Elbow -> L. Wrist (Medium Blue)
482
+ base_colors_255_dict["Yellow-Green"], # Neck -> R. Hip ( Yellow-Green)
483
+ base_colors_255_dict["Bright Green"], # R. Hip -> R. Knee (Bright Green - transitioning warm to cool spectrum)
484
+ base_colors_255_dict["Light Green-Blue"], # R. Knee -> R. Ankle (Light Green-Blue - transitioning)
485
+ base_colors_255_dict["Pure Blue"], # Neck -> L. Hip (Pure Blue)
486
+ base_colors_255_dict["Purple-Blue"], # L. Hip -> L. Knee (Purple-Blue)
487
+ base_colors_255_dict["Medium Purple"], # L. Knee -> L. Ankle (Medium Purple)
488
+ base_colors_255_dict["Grey"], # Neck -> Nose (Grey)
489
+ base_colors_255_dict["Pink-Magenta"], # Nose -> R. Eye (Pink/Magenta)
490
+ base_colors_255_dict["Dark Violet"], # R. Eye -> R. Ear (Dark Pink)
491
+ base_colors_255_dict["Pink-Magenta"], # Nose -> L. Eye (Violet)
492
+ base_colors_255_dict["Dark Violet"], # L. Eye -> L. Ear (Dark Violet)
493
+ ] for base_colors_255_dict in base_colors_255_dict_list]
494
+
495
+ limb_seq = [
496
+ [1, 2], # 0 Neck -> R. Shoulder
497
+ [1, 5], # 1 Neck -> L. Shoulder
498
+ [2, 3], # 2 R. Shoulder -> R. Elbow
499
+ [3, 4], # 3 R. Elbow -> R. Wrist
500
+ [5, 6], # 4 L. Shoulder -> L. Elbow
501
+ [6, 7], # 5 L. Elbow -> L. Wrist
502
+ [1, 8], # 6 Neck -> R. Hip
503
+ [8, 9], # 7 R. Hip -> R. Knee
504
+ [9, 10], # 8 R. Knee -> R. Ankle
505
+ [1, 11], # 9 Neck -> L. Hip
506
+ [11, 12], # 10 L. Hip -> L. Knee
507
+ [12, 13], # 11 L. Knee -> L. Ankle
508
+ [1, 0], # 12 Neck -> Nose
509
+ [0, 14], # 13 Nose -> R. Eye
510
+ [14, 16], # 14 R. Eye -> R. Ear
511
+ [0, 15], # 15 Nose -> L. Eye
512
+ [15, 17], # 16 L. Eye -> L. Ear
513
+ ]
514
+
515
+ draw_seq = [0, 2, 3, # Neck -> R. Shoulder -> R. Elbow -> R. Wrist
516
+ 1, 4, 5, # Neck -> L. Shoulder -> L. Elbow -> L. Wrist
517
+ 6, 7, 8, # Neck -> R. Hip -> R. Knee -> R. Ankle
518
+ 9, 10, 11, # Neck -> L. Hip -> L. Knee -> L. Ankle
519
+ 12, # Neck -> Nose
520
+ 13, 14, # Nose -> R. Eye -> R. Ear
521
+ 15, 16, # Nose -> L. Eye -> L. Ear
522
+ ] # 从近心端往外扩展
523
+
524
+ colors_first = [[c / 300 + 0.15 for c in color_rgb] + [0.8] for color_rgb in ordered_colors_255_list[0]]
525
+ colors_second = [[c / 300 + 0.15 for c in color_rgb] + [0.8] for color_rgb in ordered_colors_255_list[1]]
526
+
527
+ smpl_poses_first, smpl_poses_second = collect_smpl_poses_samurai(data)
528
+
529
+
530
+ if intrinsic_matrix is None:
531
+ intrinsic_matrix = intrinsic_matrix_from_field_of_view((height, width))
532
+ focal_x = intrinsic_matrix[0,0]
533
+ focal_y = intrinsic_matrix[1,1]
534
+ princpt = (intrinsic_matrix[0,2], intrinsic_matrix[1,2]) # 主点 (cx, cy)
535
+
536
+ # 串行获取每一帧的cylinder_specs
537
+ cylinder_specs_list = []
538
+ for i in range(video_length):
539
+ cylinder_specs_first = get_single_pose_cylinder_specs((i, smpl_poses_first[i], None, None, None, None, colors_first, limb_seq, draw_seq))
540
+ cylinder_specs_second = get_single_pose_cylinder_specs((i, smpl_poses_second[i], None, None, None, None, colors_second, limb_seq, draw_seq))
541
+ cylinder_specs = cylinder_specs_first + cylinder_specs_second
542
+ cylinder_specs_list.append(cylinder_specs)
543
+
544
+
545
+ frames_np_rgba = render_whole(cylinder_specs_list, H=height, W=width, fx=focal_x, fy=focal_y, cx=princpt[0], cy=princpt[1])
546
+ if poses is not None and draw_2d:
547
+ aligned_poses = copy.deepcopy(poses)
548
+ canvas_2d = draw_pose_to_canvas_np(aligned_poses, pool=None, H=height, W=width, reshape_scale=0, show_feet_flag=False, show_body_flag=False, show_cheek_flag=True, dw_hand=True)
549
+ for i in range(len(frames_np_rgba)):
550
+ frame_img = frames_np_rgba[i]
551
+ canvas_img = canvas_2d[i]
552
+ mask = canvas_img != 0
553
+ frame_img[:, :, :3][mask] = canvas_img[mask]
554
+ frames_np_rgba[i] = frame_img
555
+
556
+ return frames_np_rgba
557
+
558
+
559
+ def run_nlf_from_masks(video_frames, masks, colors, model_nlf, nlf_render_path,
560
+ nlf_render_mask_path, fps=16, detector=None):
561
+ """对每个人用墨绿色背景隔离后提取 NLF 姿态,再分别渲染普通和 mono 结果并保存为 MP4。
562
+
563
+ Args:
564
+ video_frames: (T, H, W, 3) uint8 numpy array, RGB
565
+ masks: list of (T, H, W) bool ndarray,每人一个
566
+ colors: list of BGR color tuples,与 masks 一一对应
567
+ model_nlf: TorchScript NLF 模型
568
+ nlf_render_path: 普通渲染输出路径(含 2D 关键点叠加)
569
+ nlf_render_mask_path: mono 渲染输出路径
570
+ fps: 输出帧率
571
+ detector: DWposeDetector,对原始帧提取多人 2D 关键点
572
+ """
573
+ from NLFPoseExtract.extract_nlfpose_batch import process_video_multi_nlf
574
+
575
+ if len(masks) == 0:
576
+ print("No masks provided, skipping.")
577
+ return
578
+
579
+ T, H, W, C = video_frames.shape
580
+ dark_green = np.array([0, 100, 0], dtype=np.uint8)
581
+
582
+ vr_frames_list = []
583
+ for mask in masks:
584
+ person_frames = np.full((T, H, W, C), dark_green, dtype=np.uint8)
585
+ person_frames[mask] = video_frames[mask]
586
+ vr_frames_list.append(torch.from_numpy(person_frames))
587
+
588
+ nlf_results = process_video_multi_nlf(model_nlf, vr_frames_list)
589
+
590
+ poses = None
591
+ if detector is not None:
592
+ # Per-person DWpose: run detector on each SAM3 person's dark_green-bg crop so the
593
+ # 2D keypoints (face/hands/body) align with SAM3 person order. Stack the per-person
594
+ # single-person dicts back into multi-person dicts per frame, in SAM3 order.
595
+ N = len(masks)
596
+ EMPTY_BODY = np.full((24, 2), -1.0, dtype=np.float32)
597
+ EMPTY_SUBSET = np.full((24,), -1.0, dtype=np.float32)
598
+ EMPTY_FACE = np.full((68, 2), -1.0, dtype=np.float32)
599
+ EMPTY_HAND = np.full((21, 2), -1.0, dtype=np.float32)
600
+
601
+ per_person_per_frame = [[None] * T for _ in range(N)]
602
+ for p_idx in range(N):
603
+ person_frames_np = vr_frames_list[p_idx].numpy() # (T, H, W, 3) RGB, dark_green bg
604
+ for t in range(T):
605
+ pose_dict, _, _ = detector(Image.fromarray(person_frames_np[t]))
606
+ per_person_per_frame[p_idx][t] = pose_dict
607
+
608
+ poses = []
609
+ for t in range(T):
610
+ cand_rows, sub_rows, face_rows = [], [], []
611
+ hand_rows = []
612
+ for p_idx in range(N):
613
+ pd = per_person_per_frame[p_idx][t]
614
+ cands = pd['bodies']['candidate']
615
+ if cands is not None and len(cands) > 0:
616
+ cand_rows.append(cands[0])
617
+ sub_rows.append(pd['bodies']['subset'][0])
618
+ face_rows.append(pd['faces'][0])
619
+ hand_rows.append(pd['hands'][0])
620
+ hand_rows.append(pd['hands'][1])
621
+ else:
622
+ cand_rows.append(EMPTY_BODY)
623
+ sub_rows.append(EMPTY_SUBSET)
624
+ face_rows.append(EMPTY_FACE)
625
+ hand_rows.append(EMPTY_HAND)
626
+ hand_rows.append(EMPTY_HAND)
627
+ poses.append({
628
+ 'bodies': {
629
+ 'candidate': np.stack(cand_rows, axis=0),
630
+ 'subset': np.stack(sub_rows, axis=0),
631
+ },
632
+ 'faces': np.stack(face_rows, axis=0),
633
+ 'hands': np.stack(hand_rows, axis=0),
634
+ })
635
+
636
+ person_colors_rgba = []
637
+ for bgr in colors:
638
+ b, g, r = bgr[0] / 255.0, bgr[1] / 255.0, bgr[2] / 255.0
639
+ person_colors_rgba.append([r, g, b, 1.0])
640
+
641
+ palette_offset = 1 if len(masks) == 1 else 0
642
+ frames_regular, frames_mono = render_nlf_as_images(
643
+ copy.deepcopy(nlf_results), poses=copy.deepcopy(poses),
644
+ reshape_pool=None, intrinsic_matrix=None,
645
+ draw_2d=True, aug_2d=False, aug_cam=False,
646
+ person_colors=person_colors_rgba, palette_offset=palette_offset,
647
+ )
648
+
649
+ for out_path in (nlf_render_path, nlf_render_mask_path):
650
+ out_dir = os.path.dirname(out_path)
651
+ if out_dir:
652
+ os.makedirs(out_dir, exist_ok=True)
653
+
654
+ frames_regular_rgb = [f[:, :, :3] for f in frames_regular]
655
+ frames_mono_rgb = [f[:, :, :3] for f in frames_mono]
656
+
657
+ mpy.ImageSequenceClip(frames_regular_rgb, fps=fps).write_videofile(nlf_render_path)
658
+ mpy.ImageSequenceClip(frames_mono_rgb, fps=fps).write_videofile(nlf_render_mask_path)
SCAIL-Pose/NLFPoseExtract/process_animation_aio.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # 动态添加项目根目录到 sys.path,这样就不需要 export PYTHONPATH
5
+ current_dir = os.path.dirname(os.path.abspath(__file__))
6
+ project_root = os.path.dirname(current_dir) # SCAIL_Pose 目录
7
+ if project_root not in sys.path:
8
+ sys.path.insert(0, project_root)
9
+
10
+ import argparse
11
+ import glob
12
+ import shutil
13
+ import time
14
+ import traceback
15
+
16
+ import numpy as np
17
+ import torch
18
+ from decord import VideoReader, cpu
19
+
20
+ from NLFPoseExtract.v2_helper import (
21
+ find_ref_image,
22
+ match_driving_to_ref_by_center,
23
+ save_colored_mask_image,
24
+ write_colored_mask_video,
25
+ write_kept_driving_video,
26
+ )
27
+
28
+
29
+ def process_one(subdir, video_name, e2e_mode, crop_kind, max_persons, text,
30
+ model_nlf, detector, predictor, image_predictor,
31
+ crop_margin=0.05):
32
+ from TrackSam3.track import get_mask_from_image_via_video, get_mask_from_video
33
+
34
+ if crop_kind is not None and not e2e_mode:
35
+ raise ValueError("--crop_e2e_bbox / --crop_e2e_mask / --crop_e2e_steady_bbox require --e2e_mode")
36
+
37
+ mp4_path = os.path.join(subdir, video_name)
38
+ if not os.path.exists(mp4_path):
39
+ raise FileNotFoundError(f"No {video_name} found in {subdir}")
40
+
41
+ ref_image_path = find_ref_image(subdir)
42
+
43
+ out_path_rendered = os.path.join(subdir, 'rendered_v2.mp4')
44
+ out_path_mask = os.path.join(subdir, 'rendered_mask_v2.mp4')
45
+ ref_mask_path = os.path.join(subdir, 'ref_mask.jpg')
46
+
47
+ # 1) Driving is the canonical authority for person count (video tracker is reliable;
48
+ # SAM3 image mode tends to return duplicate masks for the same character).
49
+ print(f"Getting driving masks from {mp4_path} (text={text})...")
50
+ drv_masks, drv_colors = get_mask_from_video(
51
+ mp4_path, predictor, max_targets=max_persons, sort_by='x', fixed_colors=None,
52
+ text=text,
53
+ )
54
+ if len(drv_masks) == 0:
55
+ raise RuntimeError(f"No valid persons detected in driving {mp4_path}")
56
+ N = len(drv_masks)
57
+ print(f"Driving defines {N} person(s) (capped at --max_persons={max_persons}); colors={drv_colors}")
58
+
59
+ # 2) Ref provides per-person colors; cap at N and sort left-to-right to match driving.
60
+ # Route ref through the video predictor (single-frame mp4 wrapper) — image-mode SAM3
61
+ # often misses small / distant subjects that the video pipeline picks up reliably.
62
+ print(f"Getting ref masks from {ref_image_path} (text={text})...")
63
+ ref_masks, ref_colors = get_mask_from_image_via_video(
64
+ ref_image_path, predictor, max_targets=N, sort_by='x', fixed_colors=None,
65
+ text=text,
66
+ )
67
+ if len(ref_masks) < N:
68
+ print(f" Ref has {len(ref_masks)} person(s) but driving has {N}; "
69
+ f"matching driving subset to ref by normalized center ...")
70
+ drv_masks, drv_colors = match_driving_to_ref_by_center(
71
+ drv_masks, drv_colors, ref_masks)
72
+ if len(drv_masks) != len(ref_masks):
73
+ raise RuntimeError(
74
+ f"Center matching produced {len(drv_masks)} driving tracks but "
75
+ f"ref has {len(ref_masks)}; cannot align."
76
+ )
77
+ N = len(drv_masks)
78
+ print(f" Reduced driving to {N} person(s).")
79
+ save_colored_mask_image(ref_masks, ref_colors, ref_mask_path, bg_color=(255, 255, 255))
80
+ print(f" Ref mask saved: {ref_mask_path}")
81
+
82
+ # 3) Read driving frames and fps once
83
+ vr = VideoReader(mp4_path, ctx=cpu(0))
84
+ fps = vr.get_avg_fps()
85
+ fps_int = max(1, int(round(fps)))
86
+ video_frames_np = vr.get_batch(list(range(len(vr)))).asnumpy() # (T, H, W, 3) RGB
87
+
88
+ # 4) Branch on e2e_mode (+ optional crop_kind for rendered_v2 only)
89
+ if e2e_mode:
90
+ if crop_kind is not None:
91
+ print(f"[e2e_mode+crop_e2e_{crop_kind}] writing kept driving as rendered_v2.mp4 "
92
+ f"(bbox_margin={crop_margin}) ...")
93
+ write_kept_driving_video(video_frames_np, drv_masks, out_path_rendered,
94
+ fps_int, crop_kind=crop_kind, bbox_margin=crop_margin)
95
+ else:
96
+ print("[e2e_mode] copying driving as rendered_v2.mp4 ...")
97
+ shutil.copyfile(mp4_path, out_path_rendered)
98
+ print("[e2e_mode] writing colored mask video as rendered_mask_v2.mp4 ...")
99
+ write_colored_mask_video(drv_masks, drv_colors, out_path_mask, fps_int)
100
+ else:
101
+ from NLFPoseExtract.nlf_render import run_nlf_from_masks
102
+ print("Running NLF and rendering skeletons ...")
103
+ run_nlf_from_masks(
104
+ video_frames=video_frames_np,
105
+ masks=drv_masks,
106
+ colors=drv_colors,
107
+ model_nlf=model_nlf,
108
+ nlf_render_path=out_path_rendered,
109
+ nlf_render_mask_path=out_path_mask,
110
+ fps=fps_int,
111
+ detector=detector,
112
+ )
113
+
114
+ print("Done!")
115
+ print(f" Rendered: {out_path_rendered}")
116
+ print(f" Rendered mask: {out_path_mask}")
117
+ print(f" Ref mask: {ref_mask_path}")
118
+
119
+
120
+ if __name__ == '__main__':
121
+ parser = argparse.ArgumentParser(
122
+ description='SCAIL pose AIO: SAM3 mask + (optional) NLF skeleton render, with '
123
+ 'ref-aligned multi-person colors. Pass exactly one of --subdir '
124
+ '(single example) OR --input_root (batch over a directory of examples).'
125
+ )
126
+ src = parser.add_mutually_exclusive_group(required=True)
127
+ src.add_argument('--subdir', type=str, default=None,
128
+ help='Single-example mode: path to one subdir containing driving video + ref_image. '
129
+ 'Mutually exclusive with --input_root.')
130
+ src.add_argument('--input_root', type=str, default=None,
131
+ help='Batch mode: directory whose immediate subdirs are each an example. '
132
+ 'Models load once and every subdir is processed in one invocation. '
133
+ 'Mutually exclusive with --subdir.')
134
+ parser.add_argument('--video_name', type=str, default='driving.mp4',
135
+ choices=['driving.mp4', 'GT.mp4', 'raw.mp4'],
136
+ help='Filename of the driving video inside each subdir.')
137
+ parser.add_argument('--e2e_mode', action='store_true',
138
+ help='If set, skip pose extraction: rendered_v2.mp4 is a copy of the driving video, '
139
+ 'and rendered_mask_v2.mp4 is the colored SAM3 mask video. Recommanded setting to True as it\'s more accurate and easy-to-use than pose-driven for most cases.'
140
+ 'Default pose-driven provides more control and interpretability, you can adopt pose-driven for extremely challenging inputs.')
141
+ crop_group = parser.add_mutually_exclusive_group()
142
+ crop_group.add_argument('--crop_e2e_mask', action='store_true',
143
+ help='Sub-flag of --e2e_mode. rendered_v2.mp4 keeps only pixels inside '
144
+ 'each person\'s mask silhouette; rest is black. The behaviour is between pose-driven and crop_e2e_bbox.'
145
+ 'For 512p e2e runs or portrait videos, as the main training is in under this resolution, typically the function is not needed.'
146
+ 'For 704p horizontal (especially multi-human scenarios as it\'s zero-shot), we notice the crop can be an inference optimization to reduce artifacts.'
147
+ 'For other cases the crop may also not be necessary, as the default full e2e is usually better especially in terms of human-object interactions.')
148
+ crop_group.add_argument('--crop_e2e_bbox', action='store_true',
149
+ help='Sub-flag of --e2e_mode. rendered_v2.mp4 keeps only pixels inside '
150
+ 'each person\'s mask bbox (margin-padded, overlap-merged); rest is black. ')
151
+ crop_group.add_argument('--crop_e2e_steady_bbox', action='store_true',
152
+ help='Sub-flag of --e2e_mode. One static bbox from the union of all person masks '
153
+ 'across all frames (with margin); the kept rectangle never moves. Usually not needed unless you specifically want behaviour between full e2e and crop_e2e_bbox. ')
154
+ # All the expected behaviour of the 4 cropping alternatives (including not cropping, i.e full e2e) are based on 50 steps with cfg 4.0, when using lightx2v the results may be different
155
+ # Anyway, the cropping is generally a harmless optimization to reduce artifacts, and as long as it can cover the main body movements and interactions with objects, it should be fine.
156
+ parser.add_argument('--crop_margin', type=float, default=0.05,
157
+ help='Fractional padding added to bboxes in --crop_e2e_bbox / '
158
+ '--crop_e2e_steady_bbox (default 0.05).')
159
+ parser.add_argument('--max_persons', type=int, default=2,
160
+ help='Maximum number of persons to track from ref / driving (default 2).')
161
+ parser.add_argument('--text', type=str, nargs='+',
162
+ default=['human', 'character'],
163
+ help='Text prompts passed to SAM3 for both driving and ref. Add extras '
164
+ 'like "robot arm" "gripper" if the subject is a non-human character '
165
+ '(e.g. a robotic arm in egocentric/animation data).')
166
+ parser.add_argument('--skip_existing', action='store_true',
167
+ help='In --input_root mode, skip subdirs whose rendered_mask_v2.mp4 already exists.')
168
+ parser.add_argument('--model_path', type=str,
169
+ default='pretrained_weights/nlf_l_multi_0.3.2.torchscript',
170
+ help='Path to NLF TorchScript model (only used when --e2e_mode is not set).')
171
+ parser.add_argument('--sam3_model', type=str,
172
+ default='pretrained_weights/sam3.pt',
173
+ help='Path to SAM3 model weights.')
174
+ args = parser.parse_args()
175
+
176
+ crop_kind = ('bbox' if args.crop_e2e_bbox else
177
+ 'mask' if args.crop_e2e_mask else
178
+ 'steady_bbox' if args.crop_e2e_steady_bbox else None)
179
+ if crop_kind is not None and not args.e2e_mode:
180
+ parser.error("--crop_e2e_bbox / --crop_e2e_mask / --crop_e2e_steady_bbox require --e2e_mode")
181
+
182
+ from ultralytics.models.sam import SAM3SemanticPredictor, SAM3VideoSemanticPredictor
183
+
184
+ print("Initializing SAM3 video predictor...")
185
+ overrides = dict(
186
+ conf=0.25, task="segment", mode="predict", imgsz=640,
187
+ model=args.sam3_model, half=True, save=False, verbose=False,
188
+ )
189
+ predictor = SAM3VideoSemanticPredictor(overrides=overrides, new_det_thresh=1.0)
190
+
191
+ print("Initializing SAM3 image predictor...")
192
+ image_predictor = SAM3SemanticPredictor(overrides=overrides)
193
+
194
+ if args.e2e_mode:
195
+ model_nlf = None
196
+ detector = None
197
+ else:
198
+ from DWPoseProcess.dwpose import DWposeDetector
199
+ print("Loading NLF model...")
200
+ model_nlf = torch.jit.load(args.model_path).cuda().eval()
201
+ print("Loading DWpose detector...")
202
+ detector = DWposeDetector(use_batch=False).to(0)
203
+ print("All models loaded.")
204
+
205
+ if args.subdir is not None:
206
+ subdirs = [args.subdir]
207
+ else:
208
+ subdirs = sorted(d for d in glob.glob(os.path.join(args.input_root, '*'))
209
+ if os.path.isdir(d))
210
+ if not subdirs:
211
+ print(f"No subdirs found under {args.input_root}")
212
+ sys.exit(0)
213
+
214
+ n_ok, n_skip, n_err = 0, 0, 0
215
+ for i, subdir in enumerate(subdirs):
216
+ if args.skip_existing and os.path.exists(os.path.join(subdir, 'rendered_mask_v2.mp4')):
217
+ print(f"[{i+1}/{len(subdirs)}] skip (already done): {subdir}")
218
+ n_skip += 1
219
+ continue
220
+
221
+ print(f"\n{'='*60}")
222
+ print(f"[{i+1}/{len(subdirs)}] {subdir} (video_name={args.video_name}, e2e_mode={args.e2e_mode}, crop_kind={crop_kind})")
223
+ print(f"{'='*60}")
224
+ t0 = time.time()
225
+ try:
226
+ process_one(subdir, args.video_name, args.e2e_mode, crop_kind,
227
+ args.max_persons, args.text, model_nlf, detector,
228
+ predictor, image_predictor, crop_margin=args.crop_margin)
229
+ n_ok += 1
230
+ print(f" -> ok ({time.time() - t0:.1f}s)")
231
+ except Exception as e:
232
+ n_err += 1
233
+ print(f" -> FAILED: {e}")
234
+ traceback.print_exc()
235
+
236
+ print(f"\nDone. ok={n_ok} skipped={n_skip} failed={n_err} total={len(subdirs)}")
SCAIL-Pose/NLFPoseExtract/process_replacement.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ current_dir = os.path.dirname(os.path.abspath(__file__))
5
+ project_root = os.path.dirname(current_dir)
6
+ if project_root not in sys.path:
7
+ sys.path.insert(0, project_root)
8
+
9
+ import argparse
10
+ import glob
11
+ import shutil
12
+ import time
13
+ import traceback
14
+
15
+ import cv2
16
+ import numpy as np
17
+
18
+ from NLFPoseExtract.v2_helper import (
19
+ find_ref_image,
20
+ save_colored_mask_image,
21
+ save_real_pixel_mask_image,
22
+ write_colored_mask_video,
23
+ )
24
+
25
+
26
+ def _select_closest_to_ref(drv_masks, drv_colors, ref_masks):
27
+ """matchnearest: among multiple driving tracks, pick the one whose first-frame
28
+ mask has the highest IoU with the ref mask, after resizing ref to driving
29
+ resolution. Returns ([selected_mask], [selected_color]).
30
+ """
31
+ if len(drv_masks) <= 1:
32
+ return drv_masks, drv_colors
33
+
34
+ H_drv, W_drv = drv_masks[0].shape[1:]
35
+ ref_u8 = ref_masks[0][0].astype(np.uint8) * 255
36
+ ref_resized = cv2.resize(ref_u8, (W_drv, H_drv), interpolation=cv2.INTER_NEAREST) > 127
37
+
38
+ best_iou, best_idx = -1.0, 0
39
+ for i, mask in enumerate(drv_masks):
40
+ drv_first = mask[0]
41
+ inter = int(np.logical_and(ref_resized, drv_first).sum())
42
+ union = int(np.logical_or(ref_resized, drv_first).sum())
43
+ iou = inter / max(union, 1)
44
+ print(f" matchnearest IoU track {i} (color={drv_colors[i]}): {iou:.4f}")
45
+ if iou > best_iou:
46
+ best_iou, best_idx = iou, i
47
+
48
+ print(f" matchnearest selected track {best_idx} (IoU={best_iou:.4f})")
49
+ return [drv_masks[best_idx]], [drv_colors[best_idx]]
50
+
51
+
52
+ def _union_masks(masks, colors):
53
+ """Combine N masks into one via logical OR; reuse the first track's color.
54
+ Used for egocentric mode where left/right arms are detected as separate SAM3
55
+ instances but should be treated as a single actor."""
56
+ if len(masks) <= 1:
57
+ return masks, colors
58
+ combined = np.logical_or.reduce(masks)
59
+ print(f" egocentric: unioned {len(masks)} masks into 1 (color={colors[0]})")
60
+ return [combined], [colors[0]]
61
+
62
+
63
+ def process_one(subdir, video_name, test_mode, matchnearest, egocentric,
64
+ predictor, image_predictor, text):
65
+ from TrackSam3.track import get_mask_from_image, get_mask_from_video
66
+
67
+ mp4_path = os.path.join(subdir, video_name)
68
+ if not os.path.exists(mp4_path):
69
+ raise FileNotFoundError(f"No {video_name} found in {subdir}")
70
+
71
+ out_path_rendered = os.path.join(subdir, 'rendered_v2.mp4')
72
+ out_path_mask = os.path.join(subdir, 'replace_mask.mp4')
73
+ ref_image_out_path = os.path.join(subdir, 'ref_image.png')
74
+ ref_mask_path = os.path.join(subdir, 'ref_mask.png')
75
+
76
+ # 1) Read fps + first frame via cv2 — decord VideoReader corrupts CUDA fds before/between
77
+ # SAM3 calls regardless of ordering; cv2 is CUDA-agnostic and safe at any point.
78
+ cap = cv2.VideoCapture(mp4_path)
79
+ fps = cap.get(cv2.CAP_PROP_FPS)
80
+ fps_int = max(1, int(round(fps)))
81
+ ret, first_frame_bgr = cap.read()
82
+ cap.release()
83
+ if not ret:
84
+ raise RuntimeError(f"Could not read first frame from {mp4_path}")
85
+ first_frame_rgb = first_frame_bgr[:, :, ::-1]
86
+
87
+ # 2) Driving → full-video masks. matchnearest allows 2 tracks then picks via IoU;
88
+ # egocentric allows 2 tracks then unions them.
89
+ max_drv = 2 if (matchnearest or egocentric) else 1
90
+ print(f"Getting driving masks from {mp4_path} (max_targets={max_drv}, text={text})...")
91
+ drv_masks, drv_colors = get_mask_from_video(
92
+ mp4_path, predictor, max_targets=max_drv, sort_by='x', fixed_colors=None, text=text,
93
+ )
94
+ if len(drv_masks) == 0:
95
+ raise RuntimeError(f"No valid persons detected in driving {mp4_path}")
96
+ print(f"Driving detected: {len(drv_masks)} person(s); colors={drv_colors}")
97
+
98
+ if egocentric:
99
+ drv_masks, drv_colors = _union_masks(drv_masks, drv_colors)
100
+
101
+ # 3) Resolve ref_image_path: test_mode auto-generates from first driving frame
102
+ if test_mode:
103
+ save_real_pixel_mask_image([drv_masks[0][0:1]], first_frame_rgb, ref_image_out_path)
104
+ print(f"[test_mode] Ref image saved (real pixels, black bg): {ref_image_out_path}")
105
+ ref_image_path = ref_image_out_path
106
+ else:
107
+ ref_image_path = find_ref_image(subdir)
108
+
109
+ # 4) Get ref masks from ref_image (same path for both modes)
110
+ max_ref = 2 if egocentric else 1
111
+ print(f"Getting ref masks from {ref_image_path} (max_targets={max_ref})...")
112
+ ref_masks, ref_colors = get_mask_from_image(
113
+ ref_image_path, image_predictor, max_targets=max_ref,
114
+ sort_by='x', fixed_colors=None, text=text,
115
+ )
116
+ if len(ref_masks) == 0:
117
+ raise RuntimeError(f"No qualifying person found in ref image {ref_image_path}")
118
+
119
+ if egocentric:
120
+ ref_masks, ref_colors = _union_masks(ref_masks, ref_colors)
121
+
122
+ # ref_mask.png: solid-color mask on black bg
123
+ save_colored_mask_image(ref_masks, ref_colors, ref_mask_path, bg_color=(0, 0, 0))
124
+ print(f" Ref mask saved: {ref_mask_path}")
125
+
126
+ # 4.5) matchnearest: pick the driving track closest to ref by IoU
127
+ if matchnearest:
128
+ drv_masks, drv_colors = _select_closest_to_ref(drv_masks, drv_colors, ref_masks)
129
+
130
+ # 4) rendered_v2.mp4 is always a copy of driving
131
+ shutil.copyfile(mp4_path, out_path_rendered)
132
+ print(f" Copied driving → {out_path_rendered}")
133
+
134
+ # 5) replace_mask.mp4 — white background
135
+ print("Writing replace_mask.mp4 (white bg)...")
136
+ write_colored_mask_video(drv_masks, drv_colors, out_path_mask, fps_int,
137
+ bg_color=(255, 255, 255))
138
+
139
+ print("Done!")
140
+ print(f" Rendered: {out_path_rendered}")
141
+ print(f" Replace mask: {out_path_mask}")
142
+ print(f" Ref mask: {ref_mask_path}")
143
+
144
+
145
+ if __name__ == '__main__':
146
+ parser = argparse.ArgumentParser(
147
+ description='SCAIL replacement pipeline: SAM3 mask extraction for character replacement. '
148
+ 'Outputs rendered_v2.mp4 (driving copy), replace_mask.mp4 (white bg), '
149
+ 'ref_mask.png (black bg). Pass exactly one of --subdir or --input_root.'
150
+ )
151
+ src = parser.add_mutually_exclusive_group(required=True)
152
+ src.add_argument('--subdir', type=str, default=None,
153
+ help='Single-example mode: path to one subdir. '
154
+ 'Mutually exclusive with --input_root.')
155
+ src.add_argument('--input_root', type=str, default=None,
156
+ help='Batch mode: directory whose immediate subdirs are each an example. '
157
+ 'Mutually exclusive with --subdir.')
158
+ parser.add_argument('--video_name', type=str, default='driving.mp4',
159
+ choices=['driving.mp4', 'GT.mp4'],
160
+ help='Filename of the driving video inside each subdir.')
161
+ parser.add_argument('--test_mode', action='store_true',
162
+ help='Use driving first frame as ref: saves ref_image.png with real '
163
+ 'pixels inside mask area (black outside). No ref_image file needed.')
164
+ parser.add_argument('--matchnearest', action='store_true',
165
+ help='Driving may contain 2 persons; ref has 1. Picks the driving '
166
+ 'track whose first-frame mask has highest IoU with the ref mask '
167
+ '(after resizing ref to driving resolution). Other tracks are dropped.')
168
+ parser.add_argument('--egocentric', action='store_true',
169
+ help='ONLY for egocentric/first-person data where the actor appears as '
170
+ 'multiple disconnected parts (e.g. left + right arms or grippers). '
171
+ 'Sets max_targets=2 for both driving and ref, then unions the '
172
+ 'resulting masks into one (same color), treating both arms as a '
173
+ 'single actor. Do NOT use on normal third-person data. '
174
+ 'Mutually exclusive with --matchnearest.')
175
+ parser.add_argument('--text', type=str, nargs='+',
176
+ default=['human', 'character'],
177
+ help='Text prompts passed to SAM3 for both driving and ref. Add extras '
178
+ 'like "bear" if the subject is a non-human character.')
179
+ parser.add_argument('--skip_existing', action='store_true',
180
+ help='In --input_root mode, skip subdirs whose replace_mask.mp4 already exists.')
181
+ parser.add_argument('--sam3_model', type=str,
182
+ default='pretrained_weights/sam3.pt',
183
+ help='Path to SAM3 model weights.')
184
+ args = parser.parse_args()
185
+
186
+ if args.matchnearest and args.egocentric:
187
+ parser.error("--matchnearest and --egocentric are mutually exclusive: "
188
+ "the first picks one track out of many, the second unions multiple "
189
+ "tracks into one.")
190
+
191
+ from ultralytics.models.sam import SAM3SemanticPredictor, SAM3VideoSemanticPredictor
192
+
193
+ print("Initializing SAM3 video predictor...")
194
+ overrides = dict(
195
+ conf=0.25, task="segment", mode="predict", imgsz=640,
196
+ model=args.sam3_model, half=True, save=False, verbose=False,
197
+ )
198
+ predictor = SAM3VideoSemanticPredictor(overrides=overrides, new_det_thresh=1.0)
199
+
200
+ print("Initializing SAM3 image predictor...")
201
+ image_predictor = SAM3SemanticPredictor(overrides=overrides)
202
+
203
+ print("All models loaded.")
204
+
205
+ if args.subdir is not None:
206
+ subdirs = [args.subdir]
207
+ else:
208
+ subdirs = sorted(d for d in glob.glob(os.path.join(args.input_root, '*'))
209
+ if os.path.isdir(d))
210
+ if not subdirs:
211
+ print(f"No subdirs found under {args.input_root}")
212
+ sys.exit(0)
213
+
214
+ n_ok, n_skip, n_err = 0, 0, 0
215
+ for i, subdir in enumerate(subdirs):
216
+ if args.skip_existing and os.path.exists(os.path.join(subdir, 'replace_mask.mp4')):
217
+ print(f"[{i+1}/{len(subdirs)}] skip (already done): {subdir}")
218
+ n_skip += 1
219
+ continue
220
+
221
+ print(f"\n{'='*60}")
222
+ print(f"[{i+1}/{len(subdirs)}] {subdir} (video_name={args.video_name}, test_mode={args.test_mode}, matchnearest={args.matchnearest}, egocentric={args.egocentric})")
223
+ print(f"{'='*60}")
224
+ t0 = time.time()
225
+ try:
226
+ process_one(subdir, args.video_name, args.test_mode, args.matchnearest,
227
+ args.egocentric, predictor, image_predictor, args.text)
228
+ n_ok += 1
229
+ print(f" -> ok ({time.time() - t0:.1f}s)")
230
+ except Exception as e:
231
+ n_err += 1
232
+ print(f" -> FAILED: {e}")
233
+ traceback.print_exc()
234
+
235
+ print(f"\nDone. ok={n_ok} skipped={n_skip} failed={n_err} total={len(subdirs)}")
SCAIL-Pose/NLFPoseExtract/reshape_utils_3d.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import random
3
+ from NLFPoseExtract.nlf_draw import intrinsic_matrix_from_field_of_view, process_data_to_COCO_format, p3d_to_p2d
4
+ import torch
5
+
6
+
7
+
8
+ # reshapePool只负责形变,骨骼偏移、丢弃等得从draw层来做
9
+ class reshapePool3d:
10
+ def __init__(self, reshape_type, height, width): # 对每个视频只初始化一次
11
+ self.reshape_type = reshape_type
12
+ self.height = height
13
+ self.width = width
14
+ self.shoulder_alpha = 0
15
+ self.upper_arm_alpha = 0
16
+ self.forearm_alpha = 0
17
+ self.body_alpha = 0
18
+ self.thigh_alpha = 0
19
+ self.calf_alpha = 0
20
+ self.face_alpha = random.choices([-0.4, -0.2, 0, 0.2, 0.4], weights=[0.2, 0.15, 0.3, 0.15, 0.2], k=1)[0]
21
+
22
+
23
+ self.body_reshape_methods = [
24
+ self.reshape_body,
25
+ self.reshape_arm,
26
+ self.reshape_leg,
27
+ self.reshape_shoulder,
28
+ ]
29
+
30
+ self.face_reshape_methods = [
31
+ self.reshape_face,
32
+ ]
33
+
34
+ self.body_offset_selected_methods = []
35
+
36
+ options = ["normal_human", "dwarf", "slender", "elf", "random_long_arm_long_leg", "king-kong"]
37
+ if self.reshape_type == "low":
38
+ weights = [0.8, 0, 0, 0.1, 0.1, 0]
39
+ self.body_offset_selected_methods = []
40
+ if self.reshape_type == "normal":
41
+ weights = [0.4, 0.1, 0.1, 0.1, 0.2, 0.1]
42
+ elif self.reshape_type == "high":
43
+ weights = [0.3, 0.2, 0.1, 0.1, 0.2, 0.1]
44
+ elif self.reshape_type == "dongman":
45
+ weights = [0.1, 0.2, 0.2, 0.2, 0.2, 0.1]
46
+ self.face_alpha = random.choices([-0.4, -0.2, 0.4], weights=[0.4, 0.2, 0.4], k=1)[0]
47
+ choice = random.choices(options, weights=weights, k=1)[0]
48
+ self.aug_init(choice)
49
+
50
+
51
+ def pose_reshape_2d_for_face(self, alpha, candidate, face, subset, body_anchor_point, body_affected_points):
52
+ anchor_x, anchor_y = candidate[body_anchor_point]
53
+ if subset[body_anchor_point] == -1:
54
+ return
55
+
56
+ for face_point_idx in range(len(face)):
57
+ face_point_x, face_point_y = face[face_point_idx]
58
+ if face_point_x == -1 or face_point_y == -1:
59
+ continue
60
+ vector_x = face_point_x - anchor_x
61
+ vector_y = face_point_y - anchor_y
62
+ offset_x = vector_x * alpha
63
+ offset_y = vector_y * alpha
64
+ face[face_point_idx] = [face_point_x + offset_x, face_point_y + offset_y]
65
+
66
+ for body_affected_point_idx in body_affected_points:
67
+ body_point_x, body_point_y = candidate[body_affected_point_idx]
68
+ if subset[body_affected_point_idx] == -1 or body_point_x == -1 or body_point_y == -1:
69
+ continue
70
+ vector_x = body_point_x - anchor_x
71
+ vector_y = body_point_y - anchor_y
72
+ offset_x = vector_x * alpha
73
+ offset_y = vector_y * alpha
74
+ candidate[body_affected_point_idx] = [body_point_x + offset_x, body_point_y + offset_y]
75
+
76
+ def pose_reshape_3d(self, alpha, smpl_joints, candidate, subset, left_hand, right_hand, face,
77
+ anchor_point, end_point,
78
+ affected_body_points):
79
+ """
80
+ # joints3d: n, 24, 3
81
+ alpha: 变化比例
82
+ anchor_point: 起始点
83
+ anchor_part: 起始点所属的身体部分,0代表body candidate, 1代表face, 2代表hand
84
+ end_point: 中止点
85
+ """
86
+ if torch.sum(smpl_joints[anchor_point]) == 0:
87
+ right_hand[:] = -1
88
+ left_hand[:] = -1
89
+ face[:] = -1
90
+ subset[:] = -1
91
+ return
92
+
93
+
94
+ anchor_x, anchor_y, anchor_z = smpl_joints[anchor_point]
95
+ end_x, end_y, end_z = smpl_joints[end_point]
96
+
97
+ vector_x = end_x - anchor_x
98
+ vector_y = end_y - anchor_y
99
+ vector_z = end_z - anchor_z
100
+ offset_x = (vector_x * alpha).item()
101
+ offset_y = (vector_y * alpha).item()
102
+ offset_z = (vector_z * alpha).item()
103
+
104
+ map_to_2d = {}
105
+ map_to_2d[4] = 11
106
+ map_to_2d[7] = 12
107
+ map_to_2d[10] = 13
108
+ map_to_2d[5] = 8
109
+ map_to_2d[8] = 9
110
+ map_to_2d[11] = 10
111
+ map_to_2d[20] = 6
112
+ map_to_2d[22] = 7
113
+ map_to_2d[21] = 3
114
+ map_to_2d[23] = 4
115
+ map_to_2d[18] = 5
116
+ map_to_2d[19] = 2
117
+
118
+
119
+ for affected_body_point in affected_body_points:
120
+ if torch.sum(smpl_joints[affected_body_point]) == 0:
121
+ continue
122
+ new_smpl_joint = smpl_joints[affected_body_point] + torch.tensor([offset_x, offset_y, offset_z]).to(smpl_joints.device)
123
+ new_smpl_joint_2d_offset = p3d_to_p2d(new_smpl_joint.reshape(1,1,3).cpu().numpy(), self.height, self.width)[0][0] - p3d_to_p2d(smpl_joints[affected_body_point].reshape(1,1,3).cpu().numpy(), self.height, self.width)[0][0]
124
+ new_smpl_joint_2d_offset = np.array([new_smpl_joint_2d_offset[0] / self.width, new_smpl_joint_2d_offset[1] / self.height])
125
+ smpl_joints[affected_body_point] = new_smpl_joint
126
+ if affected_body_point in map_to_2d.keys():
127
+ affected_candidate_point_idx = map_to_2d[affected_body_point]
128
+ if subset[affected_candidate_point_idx] != -1 and candidate[affected_candidate_point_idx][0] != -1 and candidate[affected_candidate_point_idx][1] != -1:
129
+ candidate[affected_candidate_point_idx] = candidate[affected_candidate_point_idx] + new_smpl_joint_2d_offset # 2d的也移动这么多
130
+ if affected_candidate_point_idx == 4: # dwpose 右手 (反的
131
+ left_hand[:] = left_hand + new_smpl_joint_2d_offset
132
+ if affected_candidate_point_idx == 7: # dwpose 左手 (反的
133
+ right_hand[:] = right_hand + new_smpl_joint_2d_offset
134
+
135
+
136
+
137
+ def aug_init(self, body_type):
138
+ print(f"augmentation: using body_type: {body_type}")
139
+ self.shoulder_alpha = 0
140
+ self.upper_arm_alpha = 0
141
+ self.forearm_alpha = 0
142
+ self.body_alpha = 0
143
+ self.thigh_alpha = 0
144
+ self.calf_alpha = 0
145
+ if body_type == "normal_human":
146
+ self.body_reshape_selected_methods = []
147
+ elif body_type == "dwarf": # body不动
148
+ self.upper_arm_alpha = random.uniform(-0.3, -0.2)
149
+ self.forearm_alpha = self.upper_arm_alpha
150
+ self.shoulder_alpha = -0.2
151
+ self.thigh_alpha = random.uniform(-0.3, -0.2)
152
+ self.calf_alpha = self.thigh_alpha
153
+ self.body_reshape_selected_methods = [self.reshape_shoulder, self.reshape_arm, self.reshape_leg]
154
+ self.face_alpha = 0.2
155
+ elif body_type == "slender":
156
+ self.upper_arm_alpha = 0.3
157
+ self.forearm_alpha = 0.2
158
+ self.shoulder_alpha = 0.2
159
+ self.thigh_alpha = 0.1
160
+ self.calf_alpha = 0.1
161
+ self.body_reshape_selected_methods = [self.reshape_shoulder, self.reshape_arm, self.reshape_leg]
162
+ self.face_alpha = -0.2
163
+ elif body_type == "elf":
164
+ self.body_alpha = random.uniform(-0.2, 0.2)
165
+ self.shoulder_alpha = 0.1
166
+ self.upper_arm_alpha = 0.1
167
+ self.forearm_alpha = 0.1
168
+ self.thigh_alpha = 0.25
169
+ self.calf_alpha = 0.25
170
+ self.body_reshape_selected_methods = [self.reshape_body, self.reshape_shoulder, self.reshape_arm, self.reshape_leg]
171
+ self.face_alpha = 0
172
+ elif body_type == "king-kong":
173
+ self.body_alpha = 0.1
174
+ self.thigh_alpha = -0.25
175
+ self.calf_alpha = -0.25
176
+ self.upper_arm_alpha = 0.2
177
+ self.forearm_alpha = 0.2
178
+ self.shoulder_alpha = 0.3
179
+ self.body_reshape_selected_methods = [self.reshape_body, self.reshape_shoulder, self.reshape_arm, self.reshape_leg]
180
+ self.face_alpha = 0
181
+ elif body_type == "random_long_arm_long_leg":
182
+ self.upper_arm_alpha = random.uniform(-0.2, 0.2)
183
+ self.forearm_alpha = random.uniform(-0.2, 0.2)
184
+ self.thigh_alpha = random.uniform(-0.2, 0.2)
185
+ self.calf_alpha = random.uniform(-0.2, 0.2)
186
+ self.body_alpha = random.uniform(-0.1, 0.1)
187
+ self.body_reshape_selected_methods = [self.reshape_body, self.reshape_arm, self.reshape_leg]
188
+ elif body_type == "test_case_1":
189
+ self.upper_arm_alpha = -0.4
190
+ self.forearm_alpha = -0.4
191
+ self.shoulder_alpha = -0.3
192
+ self.thigh_alpha = 0.2
193
+ self.calf_alpha = 0.2
194
+ self.body_alpha = 0.1
195
+ self.face_alpha = 0.4
196
+ self.body_reshape_selected_methods = [self.reshape_body, self.reshape_shoulder, self.reshape_arm, self.reshape_leg]
197
+ elif body_type == "test_case_2":
198
+ self.upper_arm_alpha = 0.4
199
+ self.forearm_alpha = 0.4
200
+ self.shoulder_alpha = 0.3
201
+ self.thigh_alpha = -0.2
202
+ self.calf_alpha = -0.25
203
+ self.body_alpha = -0.2
204
+ self.face_alpha = -0.2
205
+ self.body_reshape_selected_methods = [self.reshape_body, self.reshape_shoulder, self.reshape_arm, self.reshape_leg]
206
+ elif body_type == "test_case_3":
207
+ self.upper_arm_alpha = 0.2
208
+ self.forearm_alpha = 0.2
209
+ self.shoulder_alpha = 0.2
210
+ self.thigh_alpha = 0.4
211
+ self.calf_alpha = 0.4
212
+ self.body_alpha = 0.3
213
+ self.face_alpha = 0.15
214
+ self.body_reshape_selected_methods = [self.reshape_body, self.reshape_shoulder, self.reshape_arm, self.reshape_leg]
215
+ elif body_type == "normal_human_test":
216
+ self.body_reshape_selected_methods = []
217
+ self.face_alpha = 0
218
+
219
+
220
+
221
+ def apply_random_reshapes(self, smpl_joints_list, candidate, left_hand, right_hand, face, subset):
222
+ # Apply the two selected reshape methods
223
+ for method in self.body_reshape_selected_methods:
224
+ method(smpl_joints_list, candidate, subset, left_hand, right_hand, face)
225
+
226
+ for method in self.body_offset_selected_methods:
227
+ method(smpl_joints_list, candidate, left_hand, right_hand, face)
228
+
229
+ for method in self.face_reshape_methods:
230
+ method(candidate, face, subset)
231
+
232
+
233
+
234
+
235
+ def reshape_body(self, smpl_joints_list, candidate, subset, left_hand, right_hand, face):
236
+ self.pose_reshape_3d(self.body_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
237
+ 12, 1,
238
+ [1, 4, 7, 10]
239
+ )
240
+ self.pose_reshape_3d(self.body_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
241
+ 12, 2,
242
+ [2, 5, 8, 11]
243
+ )
244
+
245
+ def reshape_arm(self, smpl_joints_list, candidate, subset, left_hand, right_hand, face):
246
+ self.pose_reshape_3d(self.upper_arm_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
247
+ 16, 18,
248
+ [18, 20, 22]
249
+ )
250
+ self.pose_reshape_3d(self.upper_arm_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
251
+ 17, 19,
252
+ [19, 21, 23]
253
+ )
254
+ self.pose_reshape_3d(self.forearm_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
255
+ 18, 20,
256
+ [20, 22]
257
+ )
258
+ self.pose_reshape_3d(self.forearm_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
259
+ 19, 21,
260
+ [21, 23]
261
+ )
262
+
263
+
264
+ def reshape_leg(self, smpl_joints_list, candidate, subset, left_hand, right_hand, face):
265
+ self.pose_reshape_3d(self.thigh_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
266
+ 1, 4,
267
+ [4, 7, 10]
268
+ )
269
+ self.pose_reshape_3d(self.thigh_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
270
+ 2, 5,
271
+ [5, 8, 11]
272
+ )
273
+ self.pose_reshape_3d(self.calf_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
274
+ 4, 7,
275
+ [7, 10]
276
+ )
277
+ self.pose_reshape_3d(self.calf_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
278
+ 5, 8,
279
+ [8, 11]
280
+ )
281
+
282
+
283
+ def reshape_shoulder(self, smpl_joints_list, candidate, subset, left_hand, right_hand, face):
284
+ self.pose_reshape_3d(self.shoulder_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
285
+ 12, 16,
286
+ [16, 18, 20, 22]
287
+ )
288
+ self.pose_reshape_3d(self.shoulder_alpha, smpl_joints_list, candidate, subset, left_hand, right_hand, face,
289
+ 12, 17,
290
+ [17, 19, 21, 23]
291
+ )
292
+
293
+ # def offset_3d_all(self, smpl_joints_list, candidate, left_hand, right_hand, face):
294
+ # smpl_joints_list = smpl_joints_list + torch.tensor([self.offset_3d_x, self.offset_3d_y, self.offset_3d_z]).to(smpl_joints_list.device)
295
+ # offset_2d = p3d_to_p2d(np.array([[[self.offset_3d_x, self.offset_3d_y, self.offset_3d_z]]]), self.height, self.width)[0][0]
296
+ # candidate = candidate + np.array([offset_2d[0], offset_2d[1]])
297
+ # left_hand = left_hand + np.array([offset_2d[0], offset_2d[1]])
298
+ # right_hand = right_hand + np.array([offset_2d[0], offset_2d[1]])
299
+ # face = face + np.array([offset_2d[0], offset_2d[1]])
300
+
301
+
302
+ def reshape_face(self, candidate, face, subset):
303
+ self.pose_reshape_2d_for_face(alpha=self.face_alpha, candidate=candidate, face=face, subset=subset,
304
+ body_anchor_point=0, body_affected_points=[14, 15, 16, 17])
SCAIL-Pose/NLFPoseExtract/v1_process_pose.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # 动态添加项目根目录到 sys.path,这样就不需要 export PYTHONPATH
5
+ current_dir = os.path.dirname(os.path.abspath(__file__))
6
+ project_root = os.path.dirname(current_dir) # SCAIL_Pose 目录
7
+ if project_root not in sys.path:
8
+ sys.path.insert(0, project_root)
9
+
10
+ import cv2
11
+ import torch
12
+ import pickle
13
+ import torchvision
14
+ import shutil
15
+ import glob
16
+ import random
17
+ from tqdm import tqdm
18
+ import decord
19
+ from decord import VideoReader, cpu, gpu
20
+ from torchvision.transforms import ToPILImage
21
+ from PIL import Image
22
+ import numpy as np
23
+ import argparse
24
+ from NLFPoseExtract.nlf_render import render_nlf_as_images, collect_smpl_poses, shift_dwpose_according_to_nlf, p3d_single_p2d
25
+ from NLFPoseExtract.nlf_draw import intrinsic_matrix_from_field_of_view, process_data_to_COCO_format, p3d_to_p2d
26
+ from DWPoseProcess.dwpose import DWposeDetector
27
+ from concurrent.futures import ProcessPoolExecutor, as_completed
28
+ import multiprocessing
29
+ import traceback
30
+ from NLFPoseExtract.extract_nlfpose_batch import process_video_nlf
31
+ from NLFPoseExtract.reshape_utils_3d import reshapePool3d
32
+ try:
33
+ import moviepy.editor as mpy
34
+ except:
35
+ import moviepy as mpy
36
+ from torchvision.transforms.functional import center_crop, resize
37
+ from torchvision.transforms import InterpolationMode
38
+ import torchvision.transforms as TT
39
+ import copy
40
+ from NLFPoseExtract.align3d import solve_new_camera_params_central, solve_new_camera_params_down
41
+
42
+
43
+ def recollect_nlf(data):
44
+ new_data = []
45
+ for item in data:
46
+ new_item = item.copy()
47
+ if len(item['bboxes']) > 0:
48
+ new_item['bboxes'] = item['bboxes'][:1]
49
+ new_item['nlfpose'] = item['nlfpose'][:1]
50
+ new_data.append(new_item)
51
+ return new_data
52
+
53
+ def recollect_dwposes(poses):
54
+ new_poses = []
55
+ for pose in poses:
56
+ new_pose = pose.copy()
57
+ for i in range(1):
58
+ bodies = pose["bodies"]
59
+ faces = pose["faces"][i:i+1]
60
+ hands = pose["hands"][2*i:2*i+2]
61
+ candidate = bodies["candidate"][i:i+1] # candidate是所有点的坐标和置信度
62
+ subset = bodies["subset"][i:i+1] # subset是认为的有效点
63
+ new_pose = {
64
+ "bodies": {
65
+ "candidate": candidate,
66
+ "subset": subset
67
+ },
68
+ "faces": faces,
69
+ "hands": hands
70
+ }
71
+ new_poses.append(new_pose)
72
+ return new_poses
73
+
74
+
75
+
76
+ def resize_for_rectangle_crop(arr, image_size, reshape_mode='random'):
77
+ if arr.shape[3] / arr.shape[2] > image_size[1] / image_size[0]:
78
+ arr = resize(arr, size=[image_size[0], int(arr.shape[3] * image_size[0] / arr.shape[2])], interpolation=InterpolationMode.BICUBIC)
79
+ else:
80
+ arr = resize(arr, size=[int(arr.shape[2] * image_size[1] / arr.shape[3]), image_size[1]], interpolation=InterpolationMode.BICUBIC)
81
+
82
+ h, w = arr.shape[2], arr.shape[3]
83
+
84
+ delta_h = h - image_size[0]
85
+ delta_w = w - image_size[1]
86
+
87
+ if reshape_mode == 'random' or reshape_mode == 'none':
88
+ top = np.random.randint(0, delta_h + 1)
89
+ left = np.random.randint(0, delta_w + 1)
90
+ elif reshape_mode == 'center':
91
+ top, left = delta_h // 2, delta_w // 2
92
+ else:
93
+ raise NotImplementedError
94
+ arr = TT.functional.crop(
95
+ arr, top=top, left=left, height=image_size[0], width=image_size[1]
96
+ )
97
+ return arr
98
+
99
+ def scale_faces(poses, pose_2d_ref):
100
+ # 输入:两个list of dict,poses[0]['faces'].shape: 1, 68, 2 , poses_ref[0]['faces'].shape: 1, 68, 2
101
+ # 根据脸部的中心点,对poses中的脸部关键点进行缩放
102
+ # 也即:计算ref里面脸部中心点(idx: 30)到其他脸部关键点的中心距离, 计算poses里面脸部中心点到其他脸部关键点的中心距离,得到scale_n
103
+ # 对scale_n 取一下0.8-1.5的上下界,然后应用在poses上
104
+ # 注意:需要inplace改变poses
105
+
106
+ ref = pose_2d_ref[0]
107
+ pose_0 = poses[0]
108
+
109
+
110
+ face_0 = pose_0['faces'] # shape: (1, 68, 2)
111
+ face_ref = ref['faces']
112
+
113
+ # 提取 numpy 数组
114
+ face_0 = np.array(face_0[0]) # (68, 2)
115
+ face_ref = np.array(face_ref[0])
116
+
117
+ # 中心点(鼻尖或面部中心)
118
+ center_idx = 30
119
+ center_0 = face_0[center_idx]
120
+ center_ref = face_ref[center_idx]
121
+
122
+ # 计算到中心点的距离
123
+ dist = np.linalg.norm(face_0 - center_0, axis=1)
124
+ dist_ref = np.linalg.norm(face_ref - center_ref, axis=1)
125
+
126
+ # 避免中心点自身的 0 距离影响
127
+ dist = np.delete(dist, center_idx)
128
+ dist_ref = np.delete(dist_ref, center_idx)
129
+
130
+ mean_dist = np.mean(dist)
131
+ mean_dist_ref = np.mean(dist_ref)
132
+
133
+ if mean_dist < 1e-6:
134
+ scale_n = 1.0
135
+ else:
136
+ scale_n = mean_dist_ref / mean_dist
137
+
138
+ # 限制在 [0.8, 1.5]
139
+ scale_n = np.clip(scale_n, 0.8, 1.5)
140
+
141
+ for i, pose in enumerate(poses):
142
+ face = pose['faces']
143
+ # 提取 numpy 数组
144
+ face = np.array(face[0]) # (68, 2)
145
+ center = face[center_idx]
146
+ scaled_face = (face - center) * scale_n + center
147
+ poses[i]['faces'][0] = scaled_face
148
+
149
+ body = pose['bodies']
150
+ candidate = body['candidate']
151
+ candidate_np = np.array(candidate[0]) # (14, 2)
152
+ body_center = candidate_np[0]
153
+ scaled_candidate = (candidate_np - body_center) * scale_n + body_center
154
+ poses[i]['bodies']['candidate'][0] = scaled_candidate
155
+
156
+ # inplace 修改
157
+ pose['faces'][0] = scaled_face
158
+
159
+ return scale_n
160
+
161
+
162
+ if __name__ == '__main__':
163
+ parser = argparse.ArgumentParser(description='Process video with NLF pose estimation')
164
+ parser.add_argument('--subdir', type=str, default="../examples/001", help='Path to the subdirectory to process')
165
+ parser.add_argument('--model_path', type=str, default='pretrained_weights/nlf_l_multi_0.3.2.torchscript',
166
+ help='Path to NLF model')
167
+ parser.add_argument('--use_align', action='store_true', help='Whether to use 2D keypoints from reference image for alignment')
168
+ parser.add_argument('--resolution', type=int, nargs=2, default=[512, 896],
169
+ metavar=('HEIGHT', 'WIDTH'),
170
+ help='Target resolution as [height, width], currently only [512, 896] are supported')
171
+ args = parser.parse_args()
172
+
173
+ subdir = args.subdir
174
+ model_nlf = torch.jit.load(args.model_path).cuda().eval()
175
+ decord.bridge.set_bridge("torch")
176
+
177
+ # 设置路径
178
+ mp4_path = os.path.join(subdir, 'driving.mp4')
179
+ if not os.path.exists(mp4_path):
180
+ raise FileNotFoundError(f"No video file found in {subdir}")
181
+
182
+ if args.use_align:
183
+ out_path_aligned = os.path.join(subdir, 'rendered_aligned.mp4')
184
+ else:
185
+ out_path_aligned = os.path.join(subdir, 'rendered.mp4')
186
+
187
+ ref_image_path = os.path.join(subdir, 'ref_image.jpg')
188
+ if not os.path.exists(ref_image_path):
189
+ ref_image_path = os.path.join(subdir, 'ref_image.png')
190
+ if not os.path.exists(ref_image_path):
191
+ ref_image_path = os.path.join(subdir, 'ref.jpg')
192
+ if not os.path.exists(ref_image_path):
193
+ raise FileNotFoundError(f"No reference image found in {subdir}")
194
+
195
+ print(f"Processing: {subdir}")
196
+ print(f"Video: {mp4_path}")
197
+ print(f"Reference: {ref_image_path}")
198
+ print(f"Resolution: {args.resolution}")
199
+
200
+ # 读取视频
201
+ vr = VideoReader(mp4_path)
202
+ vr_frames = vr.get_batch(list(range(len(vr)))) # T H W C
203
+ sampling_image_size = args.resolution
204
+ if vr_frames.shape[1] < vr_frames.shape[2]:
205
+ target_H, target_W = sampling_image_size
206
+ else:
207
+ target_W, target_H = sampling_image_size
208
+ vr_frames = resize_for_rectangle_crop(vr_frames.permute(0, 3, 1, 2), [target_H, target_W], reshape_mode='center').permute(0, 2, 3, 1) # T H W C ->T C H W -> T H W C
209
+
210
+ # 读取参考图片
211
+ img_ref = cv2.imread(ref_image_path)
212
+ img_ref = cv2.cvtColor(img_ref, cv2.COLOR_BGR2RGB)
213
+ vr_frames_ref = torch.from_numpy(img_ref).unsqueeze(0)
214
+ vr_frames_ref = resize_for_rectangle_crop(vr_frames_ref.permute(0, 3, 1, 2), [target_H, target_W], reshape_mode='center').permute(0, 2, 3, 1) # 1 H W C ->1 C H W -> 1 H W C
215
+
216
+ # 初始化检测器
217
+ detector = DWposeDetector(use_batch=False).to(0)
218
+
219
+ # 处理Driving视频
220
+ print("Processing driving video...")
221
+ detector_return_list = []
222
+ pil_frames = []
223
+ for i in tqdm(range(len(vr_frames)), desc="Detecting poses in video"):
224
+ pil_frame = Image.fromarray(vr_frames[i].numpy())
225
+ pil_frames.append(pil_frame)
226
+ detector_result = detector(pil_frame)
227
+ detector_return_list.append(detector_result)
228
+
229
+ W, H = pil_frames[0].size
230
+ poses, scores, det_results = zip(*detector_return_list)
231
+
232
+ print("Running NLF on driving video...")
233
+ nlf_results = process_video_nlf(model_nlf, vr_frames, det_results)
234
+
235
+ # 处理ref图片
236
+ print("Processing reference image...")
237
+ detector_return_list_ref = []
238
+ pil_frames_ref = []
239
+ for i in range(len(vr_frames_ref)):
240
+ pil_frame = Image.fromarray(vr_frames_ref[i].numpy())
241
+ pil_frames_ref.append(pil_frame)
242
+ detector_result = detector(pil_frame)
243
+ detector_return_list_ref.append(detector_result)
244
+
245
+ poses_ref, scores_ref, det_results_ref = zip(*detector_return_list_ref)
246
+
247
+ print("Running NLF on reference image...")
248
+ nlf_results_ref = process_video_nlf(model_nlf, vr_frames_ref, det_results_ref)
249
+
250
+ # 进行对齐和渲染
251
+ print("Aligning and rendering...")
252
+ ori_camera_pose = intrinsic_matrix_from_field_of_view([target_H, target_W])
253
+ ori_focal = ori_camera_pose[0, 0]
254
+ pose_3d_first_driving_frame = nlf_results[0]['nlfpose'][0][0].cpu().numpy() # 3D点 frame-idx bbox-idx detect-idx
255
+ pose_3d_coco_first_driving_frame = process_data_to_COCO_format(pose_3d_first_driving_frame)
256
+
257
+ if args.use_align:
258
+ poses_2d_ref = poses_ref[0]['bodies']['candidate'][0][:14]
259
+ poses_2d_ref[:, 0] = poses_2d_ref[:, 0] * target_W
260
+ poses_2d_ref[:, 1] = poses_2d_ref[:, 1] * target_H
261
+
262
+ poses_2d_subset = poses_ref[0]['bodies']['subset'][0][:14]
263
+ pose_3d_coco_first_driving_frame = pose_3d_coco_first_driving_frame[:14]
264
+
265
+ valid_indices = []
266
+ valid_upper_indices = []
267
+ valid_lower_indices = []
268
+ upper_body_indices = [0, 2, 3, 5, 6]
269
+ lower_body_indices = [9, 10, 12, 13]
270
+ excluded_indices = [3, 4, 6, 7] # 去除手
271
+ for i in range(len(poses_2d_subset)):
272
+ if poses_2d_subset[i] != -1.0 and np.sum(pose_3d_coco_first_driving_frame[i]) != 0:
273
+ if i in upper_body_indices:
274
+ valid_upper_indices.append(i)
275
+ if i in lower_body_indices:
276
+ valid_lower_indices.append(i)
277
+
278
+ if len(valid_lower_indices) >= 4:
279
+ print("Align feet")
280
+ valid_indices = [1] + valid_lower_indices
281
+ else:
282
+ print("Align body")
283
+ valid_indices = [1] + valid_upper_indices
284
+
285
+ pose_2d_ref = poses_2d_ref[valid_indices]
286
+ pose_3d_coco_first_driving_frame = pose_3d_coco_first_driving_frame[valid_indices]
287
+
288
+ if len(valid_lower_indices) >= 4:
289
+ new_camera_intrinsics, scale_m, scale_s = solve_new_camera_params_down(pose_3d_coco_first_driving_frame, ori_focal, [target_H, target_W], pose_2d_ref)
290
+ else:
291
+ new_camera_intrinsics, scale_m, scale_s = solve_new_camera_params_central(pose_3d_coco_first_driving_frame, ori_focal, [target_H, target_W], pose_2d_ref)
292
+
293
+ # m 代表缩放了多少
294
+ scale_face = scale_faces(list(poses), list(poses_ref)) # poses[0]['faces'].shape: 1, 68, 2 , poses_ref[0]['faces'].shape: 1, 68, 2
295
+
296
+ print(f"Scale - m: {scale_m}, face: {scale_face}")
297
+
298
+ nlf_results = recollect_nlf(nlf_results)
299
+ poses = recollect_dwposes(list(poses))
300
+ shift_dwpose_according_to_nlf(collect_smpl_poses(nlf_results), poses, ori_camera_pose, new_camera_intrinsics, target_H, target_W, scale_x=scale_m, scale_y=scale_m*scale_s)
301
+
302
+ print("Rendering final video...")
303
+ frames_np = render_nlf_as_images(nlf_results, poses, reshape_pool=None, intrinsic_matrix=new_camera_intrinsics)
304
+
305
+ else:
306
+ nlf_results = recollect_nlf(nlf_results)
307
+ print("Rendering final video...")
308
+ frames_np = render_nlf_as_images(nlf_results, poses, reshape_pool=None, intrinsic_matrix=ori_camera_pose)
309
+
310
+ mpy.ImageSequenceClip(frames_np, fps=16).write_videofile(out_path_aligned)
311
+ print(f"Done! Output saved to: {out_path_aligned}")
312
+
313
+
SCAIL-Pose/NLFPoseExtract/v1_process_pose_multi.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import os.path as osp
4
+ import shutil
5
+ import numpy as np
6
+ import torch
7
+ import gc
8
+ import sys
9
+ import cv2
10
+ from tqdm import tqdm
11
+ from PIL import Image
12
+ import decord
13
+ from decord import VideoReader, cpu
14
+ try:
15
+ import moviepy.editor as mpy
16
+ except ImportError:
17
+ import moviepy as mpy
18
+ import copy
19
+ import glob
20
+
21
+ # Add project root to sys.path
22
+ current_dir = os.path.dirname(os.path.abspath(__file__))
23
+ project_root = os.path.dirname(current_dir)
24
+ if project_root not in sys.path:
25
+ sys.path.insert(0, project_root)
26
+
27
+ sys.path.append(os.path.join(project_root, "sam2"))
28
+ from sam2.build_sam import build_sam2_video_predictor
29
+
30
+ from DWPoseProcess.dwpose import DWposeDetector
31
+ from NLFPoseExtract.extract_nlfpose_batch import process_video_multi_nlf
32
+ from NLFPoseExtract.nlf_render import render_multi_nlf_as_images
33
+
34
+ def get_largest_bbox_indices(bboxes, num_bboxes=2):
35
+ # 计算每个bbox的面积
36
+ def calculate_area(bbox):
37
+ x1, y1, x2, y2 = bbox
38
+ return (x2 - x1) * (y2 - y1)
39
+
40
+ # 计算每个bbox的面积,并保留原索引
41
+ bboxes_with_area = [(i, calculate_area(bbox)) for i, bbox in enumerate(bboxes)]
42
+
43
+ # 根据面积从大到小排序
44
+ bboxes_with_area.sort(key=lambda x: x[1], reverse=True)
45
+
46
+ # 取出面积最大的 num_bboxes 个索引
47
+ largest_indices = [idx for idx, _ in bboxes_with_area[:num_bboxes]]
48
+
49
+ return largest_indices
50
+
51
+ def change_poses_to_limit_num(poses, bboxes, num_bboxes=2):
52
+ bboxes = list(bboxes) # ✅ 转换为可变列表
53
+ for idx, (pose, bbox) in enumerate(zip(poses, bboxes)):
54
+ if len(bbox) == 0:
55
+ continue
56
+ largest_indices = get_largest_bbox_indices(bbox, num_bboxes)
57
+
58
+ # 过滤 subset、hands、faces
59
+ pose['bodies']['subset'] = pose['bodies']['subset'][largest_indices]
60
+
61
+ new_hands = []
62
+ for i in largest_indices:
63
+ if 2*i+1 < len(pose['hands']):
64
+ new_hands.append(pose['hands'][2*i])
65
+ new_hands.append(pose['hands'][2*i+1])
66
+ pose['hands'] = new_hands
67
+
68
+ pose['faces'] = [pose['faces'][i] for i in largest_indices if i < len(pose['faces'])]
69
+
70
+ bboxes[idx] = [bbox[i] for i in largest_indices]
71
+
72
+ return poses, bboxes
73
+
74
+ def get_samurai_crop_video(video_input_path, video_output_root, bboxes_0, final_keypoints_list, predictor=None, use_green_background=True):
75
+ decord.bridge.set_bridge("torch")
76
+ # 用 decord 读取视频帧
77
+ if video_input_path.endswith(".mp4"):
78
+ vr = VideoReader(video_input_path)
79
+ loaded_frames = vr.get_batch(list(range(len(vr)))).numpy()
80
+ height, width = loaded_frames[0].shape[:2]
81
+
82
+ # 每个人一个输出视频
83
+ num_persons = len(final_keypoints_list)
84
+ print(f"Detected {num_persons} persons, will save {num_persons} videos.")
85
+
86
+ prompts = {fid: ((x1, y1, x2, y2), 0) for fid, (x1, y1, x2, y2) in enumerate(bboxes_0)}
87
+ with torch.inference_mode(), torch.autocast("cuda", dtype=torch.float16):
88
+ for person_idx in range(num_persons):
89
+ print(f"Processing person {person_idx + 1}/{num_persons}...")
90
+
91
+ state = predictor.init_state(video_input_path, offload_video_to_cpu=True)
92
+ bbox, track_label = prompts[person_idx]
93
+ bbox = (bbox[0] * width, bbox[1] * height, bbox[2] * width, bbox[3] * height)
94
+ points = copy.deepcopy(final_keypoints_list[person_idx])
95
+ points[:, 0] *= width
96
+ points[:, 1] *= height
97
+
98
+ _, _, masks = predictor.add_new_points_or_box(state, box=bbox, points=points, labels=np.ones(points.shape[0]), frame_idx=0, obj_id=0)
99
+
100
+ output_frames = []
101
+ output_mask_frames = []
102
+ repeat_flag = False
103
+
104
+ for frame_idx, object_ids, masks in predictor.propagate_in_video(state):
105
+ img = loaded_frames[frame_idx].copy()
106
+ for obj_id, mask in zip(object_ids, masks):
107
+ mask = mask[0].cpu().numpy() > 0.0 # 更新 mask
108
+ mask_log = np.zeros_like(img)
109
+ if use_green_background:
110
+ mask_img = np.full_like(img, (30, 60, 30))
111
+ else:
112
+ mask_img = np.zeros_like(img)
113
+ mask_img[mask] = img[mask]
114
+ mask_log[mask] = 255
115
+ output_frames.append(mask_img) # mask_img: array of [h, w, 3]
116
+ output_mask_frames.append(mask_log) # mask: array of [h, w]
117
+
118
+ del state
119
+ gc.collect()
120
+ torch.cuda.empty_cache()
121
+
122
+ # 用 moviepy 保存视频
123
+ output_name = os.path.join(video_output_root, f"{person_idx+1}.mp4")
124
+ clip = mpy.ImageSequenceClip(output_frames, fps=16)
125
+ clip.write_videofile(output_name, codec="libx264", audio=False)
126
+ print(f"Saved {output_name}")
127
+
128
+ # del predictor # Do not delete predictor here as it might be reused or managed outside
129
+ gc.collect()
130
+ torch.clear_autocast_cache()
131
+ torch.cuda.empty_cache()
132
+
133
+ if __name__ == "__main__":
134
+ parser = argparse.ArgumentParser()
135
+ parser.add_argument('--subdir', type=str, required=True, help='Path to the subdirectory containing GT.mp4')
136
+ parser.add_argument('--model_path', type=str, default='pretrained_weights/nlf_l_multi_0.3.2.torchscript',
137
+ help='Path to NLF model')
138
+ parser.add_argument('--resolution', type=int, nargs=2, default=[512, 512], help='Resolution [H, W]')
139
+ args = parser.parse_args()
140
+
141
+ subdir = args.subdir
142
+ model_path = args.model_path
143
+ resolution = args.resolution
144
+
145
+ video_input_path = osp.join(subdir, "driving.mp4")
146
+ if not osp.exists(video_input_path):
147
+ print(f"Error: {video_input_path} does not exist.")
148
+
149
+ # 1. Extract DWpose and BBoxes
150
+ print("Extracting DWpose and BBoxes...")
151
+ detector = DWposeDetector(use_batch=False).to(0)
152
+
153
+ vr = VideoReader(video_input_path)
154
+ vr_frames = vr.get_batch(list(range(len(vr)))).asnumpy() # T H W C
155
+
156
+ # Resize if needed? process_pose.py does resize.
157
+ # But run_samurai_mp4.py seems to use original video for samurai.
158
+ # process_multinlf_after_samurai.py uses samurai output which is same resolution as input?
159
+ # Let's stick to original resolution for extraction to match run_samurai_mp4 logic which uses original video.
160
+
161
+ detector_return_list = []
162
+ pil_frames = []
163
+ for i in tqdm(range(len(vr_frames)), desc="Detecting poses"):
164
+ pil_frame = Image.fromarray(vr_frames[i])
165
+ pil_frames.append(pil_frame)
166
+ detector_result = detector(pil_frame)
167
+ detector_return_list.append(detector_result)
168
+
169
+ poses, scores, det_results = zip(*detector_return_list)
170
+ # poses is tuple of dicts, det_results is tuple of lists of bboxes
171
+
172
+ # Save meta if needed, or just use in memory.
173
+ # run_samurai_mp4.py saves to meta/keypoints.pt and meta/bboxes.pt
174
+ meta_dir = osp.join(subdir, "meta")
175
+ os.makedirs(meta_dir, exist_ok=True)
176
+ torch.save(poses, osp.join(meta_dir, "keypoints.pt"))
177
+ torch.save(det_results, osp.join(meta_dir, "bboxes.pt"))
178
+
179
+ # 2. Run Samurai Segmentation
180
+ print("Running Samurai Segmentation...")
181
+ samurai_output_root = osp.join(subdir, "samurai")
182
+ if osp.exists(samurai_output_root):
183
+ shutil.rmtree(samurai_output_root)
184
+ os.makedirs(samurai_output_root, exist_ok=True)
185
+
186
+ device = "cuda:0"
187
+ predictor = build_sam2_video_predictor("configs/sam2.1/sam2.1_hiera_l.yaml", "sam2/checkpoints/sam2.1_hiera_large.pt", device=device)
188
+
189
+ # Prepare inputs for samurai
190
+ bboxes_0 = det_results[0]
191
+ indices = get_largest_bbox_indices(bboxes_0)
192
+ bboxes_0 = [bboxes_0[index] for index in indices]
193
+
194
+ keypoints_0 = poses[0]['bodies']['candidate']
195
+ subset_0 = poses[0]['bodies']['subset']
196
+ chosen_keypoints = keypoints_0[indices]
197
+
198
+ final_keypoints_list = []
199
+ for i in range(len(chosen_keypoints)):
200
+ keypoints_for_person = chosen_keypoints[i]
201
+ subset_for_person = subset_0[i]
202
+ considered_points = [0, 1, 14, 15]
203
+ # Create a copy to avoid modifying original if needed, though subset_for_person is from tensor
204
+ subset_for_person_mod = subset_for_person.copy()
205
+ for k in range(len(subset_for_person_mod)):
206
+ if k not in considered_points:
207
+ subset_for_person_mod[k] = -1
208
+ new_keypoints = keypoints_for_person[subset_for_person_mod != -1]
209
+ final_keypoints_list.append(new_keypoints)
210
+
211
+ get_samurai_crop_video(video_input_path, samurai_output_root, bboxes_0, final_keypoints_list, predictor=predictor)
212
+
213
+ del predictor
214
+ gc.collect()
215
+ torch.cuda.empty_cache()
216
+
217
+ # 3. Render Multi NLF
218
+ print("Rendering Multi NLF...")
219
+ model_nlf = torch.jit.load(model_path).cuda().eval()
220
+ decord.bridge.set_bridge("torch")
221
+
222
+ vr_frames_list = []
223
+ for samurai_mp4_path in sorted(glob.glob(osp.join(samurai_output_root, '*.mp4'))):
224
+ vr_tmp = VideoReader(samurai_mp4_path)
225
+ vr_frames_tmp = vr_tmp.get_batch(list(range(len(vr_tmp))))
226
+ vr_frames_list.append(vr_frames_tmp)
227
+
228
+ # Filter poses for rendering
229
+ # change_poses_to_limit_num modifies poses in-place or returns new ones?
230
+ # It returns poses, bboxes. And it modifies the lists passed to it?
231
+ # poses is a tuple from zip, convert to list
232
+ poses_list = list(poses)
233
+ det_results_list = list(det_results)
234
+
235
+ poses_list, det_results_list = change_poses_to_limit_num(poses_list, det_results_list)
236
+
237
+ nlf_results = process_video_multi_nlf(model_nlf, vr_frames_list)
238
+ frames_ori_np = render_multi_nlf_as_images(nlf_results, poses_list, reshape_pool=None)
239
+
240
+ out_path = osp.join(subdir, 'rendered.mp4')
241
+ mpy.ImageSequenceClip(frames_ori_np, fps=16).write_videofile(out_path)
242
+ print(f"Done! Output saved to: {out_path}")
243
+
SCAIL-Pose/NLFPoseExtract/v2_helper.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helpers shared by process_animation_aio.py and process_replacement.py.
2
+
3
+ Small, side-effect-free utilities for finding ref images, rendering SAM3 masks
4
+ (image + video), and writing the e2e "kept-region" driving video used by
5
+ --crop_e2e_bbox / --crop_e2e_mask in process_animation_aio.
6
+ """
7
+ import os
8
+
9
+ import cv2
10
+ import numpy as np
11
+
12
+ try:
13
+ import moviepy.editor as mpy
14
+ except Exception:
15
+ import moviepy as mpy
16
+
17
+
18
+ def find_ref_image(subdir):
19
+ for name in ('ref_image.jpg', 'ref_image.png', 'ref.jpg', 'ref.png'):
20
+ p = os.path.join(subdir, name)
21
+ if os.path.exists(p):
22
+ return p
23
+ raise FileNotFoundError(f"No reference image (ref_image.jpg/png or ref.jpg) found in {subdir}")
24
+
25
+
26
+ def imread_bgr(image_path):
27
+ """Robust BGR uint8 image reader. Tries cv2.imread first, falls back to PIL
28
+ when cv2 returns None — handles cases where libpng's global state gets
29
+ corrupted by SAM3/torch deps and rejects PNGs that PIL still decodes fine.
30
+
31
+ Raises FileNotFoundError if both readers fail."""
32
+ img = cv2.imread(str(image_path))
33
+ if img is not None:
34
+ return img
35
+ try:
36
+ from PIL import Image
37
+ pil = Image.open(image_path).convert('RGB')
38
+ print(f" [warn] cv2.imread failed for {image_path}; used PIL fallback")
39
+ return np.array(pil)[:, :, ::-1].copy() # RGB -> BGR
40
+ except Exception as e:
41
+ raise FileNotFoundError(f"Cannot read image: {image_path} ({e})")
42
+
43
+
44
+ def save_colored_mask_image(masks, colors, out_path, bg_color=(0, 0, 0)):
45
+ """Render the first frame of each person's mask onto a single BGR image.
46
+ bg_color is BGR; default black."""
47
+ H, W = masks[0].shape[1:]
48
+ frame = np.full((H, W, 3), bg_color, dtype=np.uint8)
49
+ for mask_t, color in zip(masks, colors):
50
+ frame[mask_t[0]] = color
51
+ cv2.imwrite(out_path, frame)
52
+
53
+
54
+ def save_real_pixel_mask_image(masks, frame_rgb, out_path):
55
+ """Black bg; mask regions show real pixels from frame_rgb (H,W,3 RGB)."""
56
+ H, W = masks[0].shape[1:]
57
+ canvas = np.zeros((H, W, 3), dtype=np.uint8)
58
+ frame_bgr = frame_rgb[:, :, ::-1]
59
+ for mask_t in masks:
60
+ canvas[mask_t[0]] = frame_bgr[mask_t[0]]
61
+ cv2.imwrite(out_path, canvas)
62
+
63
+
64
+ def write_colored_mask_video(masks, colors, out_path, fps, bg_color=(0, 0, 0)):
65
+ """Per-frame colored mask mp4 (each person painted in their BGR color).
66
+ bg_color is BGR; default black."""
67
+ T = masks[0].shape[0]
68
+ H, W = masks[0].shape[1:]
69
+ rgb_colors = [(int(c[2]), int(c[1]), int(c[0])) for c in colors] # BGR -> RGB for moviepy
70
+ bg_rgb = (int(bg_color[2]), int(bg_color[1]), int(bg_color[0]))
71
+
72
+ frames = []
73
+ for t in range(T):
74
+ frame = np.full((H, W, 3), bg_rgb, dtype=np.uint8)
75
+ for mask_t, rgb in zip(masks, rgb_colors):
76
+ frame[mask_t[t]] = rgb
77
+ frames.append(frame)
78
+
79
+ out_dir = os.path.dirname(out_path)
80
+ if out_dir:
81
+ os.makedirs(out_dir, exist_ok=True)
82
+ mpy.ImageSequenceClip(frames, fps=fps).write_videofile(out_path)
83
+
84
+
85
+ def _mask_to_numpy_bool(m_t):
86
+ if hasattr(m_t, 'cpu'):
87
+ m_t = m_t.cpu().numpy()
88
+ return np.asarray(m_t).astype(bool)
89
+
90
+
91
+ def match_driving_to_ref_by_center(drv_masks, drv_colors, ref_masks):
92
+ """Greedy nearest-center matching for the one-to-many case where SAM3 finds
93
+ more driving persons than ref (often false positives like instruments/props
94
+ matching the 'character' prompt). For each ref person, pick the not-yet-used
95
+ driving person whose first-frame mask centroid (normalized to [0,1]^2) is
96
+ closest. Returns (matched_masks, matched_colors) in ref order, len == len(ref).
97
+ """
98
+ def norm_center(mask2d, H, W):
99
+ mt = _mask_to_numpy_bool(mask2d)
100
+ ys, xs = np.where(mt)
101
+ if len(xs) == 0:
102
+ return None
103
+ return (float(xs.mean()) / W, float(ys.mean()) / H)
104
+
105
+ H_d, W_d = drv_masks[0].shape[1:]
106
+ H_r, W_r = ref_masks[0].shape[1:]
107
+ drv_c = [norm_center(m[0], H_d, W_d) for m in drv_masks]
108
+ ref_c = [norm_center(m[0], H_r, W_r) for m in ref_masks]
109
+
110
+ used = set()
111
+ out_masks, out_colors = [], []
112
+ for ri, rc in enumerate(ref_c):
113
+ if rc is None:
114
+ continue
115
+ best_i, best_d = None, float('inf')
116
+ for di, dc in enumerate(drv_c):
117
+ if di in used or dc is None:
118
+ continue
119
+ d = (dc[0] - rc[0]) ** 2 + (dc[1] - rc[1]) ** 2
120
+ if d < best_d:
121
+ best_d, best_i = d, di
122
+ if best_i is None:
123
+ continue
124
+ used.add(best_i)
125
+ out_masks.append(drv_masks[best_i])
126
+ out_colors.append(drv_colors[best_i])
127
+ print(f" match: ref[{ri}] center={rc} -> drv[{best_i}] "
128
+ f"center={drv_c[best_i]} (d^2={best_d:.4f})")
129
+ return out_masks, out_colors
130
+
131
+
132
+ def _merge_overlapping_bboxes(boxes):
133
+ """Iteratively merge any pair of overlapping bboxes into their bounding union
134
+ until no overlaps remain. O(N^2) — fine since N <= max_persons (typically 2)."""
135
+ def overlaps(a, b):
136
+ return not (a[2] <= b[0] or b[2] <= a[0] or a[3] <= b[1] or b[3] <= a[1])
137
+ def union(a, b):
138
+ return (min(a[0], b[0]), min(a[1], b[1]), max(a[2], b[2]), max(a[3], b[3]))
139
+ boxes = list(boxes)
140
+ changed = True
141
+ while changed:
142
+ changed = False
143
+ for i in range(len(boxes)):
144
+ for j in range(i + 1, len(boxes)):
145
+ if overlaps(boxes[i], boxes[j]):
146
+ boxes[i] = union(boxes[i], boxes[j])
147
+ boxes.pop(j)
148
+ changed = True
149
+ break
150
+ if changed:
151
+ break
152
+ return boxes
153
+
154
+
155
+ def _frame_keep_mask(masks_t, H, W, crop_kind, bbox_margin):
156
+ """(H, W) bool mask of pixels to keep this frame.
157
+ crop_kind='mask': union of per-person silhouettes.
158
+ crop_kind='bbox': union of per-person bboxes (margin-padded, overlap-merged)."""
159
+ if crop_kind == 'mask':
160
+ keep = np.zeros((H, W), bool)
161
+ for m_t in masks_t:
162
+ keep |= _mask_to_numpy_bool(m_t)
163
+ return keep
164
+ boxes = []
165
+ for m_t in masks_t:
166
+ ys, xs = np.where(_mask_to_numpy_bool(m_t))
167
+ if len(xs) == 0:
168
+ continue
169
+ x0, x1 = int(xs.min()), int(xs.max()) + 1
170
+ y0, y1 = int(ys.min()), int(ys.max()) + 1
171
+ mx = int(round((x1 - x0) * bbox_margin))
172
+ my = int(round((y1 - y0) * bbox_margin))
173
+ boxes.append((max(0, x0 - mx), max(0, y0 - my),
174
+ min(W, x1 + mx), min(H, y1 + my)))
175
+ keep = np.zeros((H, W), bool)
176
+ for x0, y0, x1, y1 in _merge_overlapping_bboxes(boxes):
177
+ keep[y0:y1, x0:x1] = True
178
+ return keep
179
+
180
+
181
+ def _compute_steady_bbox(masks, H, W, bbox_margin):
182
+ """Single bbox covering the union of every per-person mask across all frames,
183
+ with fractional margin. Returns (x0, y0, x1, y1) or None if all masks empty."""
184
+ T = masks[0].shape[0]
185
+ union = np.zeros((H, W), bool)
186
+ for m in masks:
187
+ for t in range(T):
188
+ union |= _mask_to_numpy_bool(m[t])
189
+ ys, xs = np.where(union)
190
+ if len(xs) == 0:
191
+ return None
192
+ x0, x1 = int(xs.min()), int(xs.max()) + 1
193
+ y0, y1 = int(ys.min()), int(ys.max()) + 1
194
+ mx = int(round((x1 - x0) * bbox_margin))
195
+ my = int(round((y1 - y0) * bbox_margin))
196
+ return (max(0, x0 - mx), max(0, y0 - my),
197
+ min(W, x1 + mx), min(H, y1 + my))
198
+
199
+
200
+ def write_kept_driving_video(video_frames_rgb, masks, out_path, fps,
201
+ crop_kind, bbox_margin=0.05):
202
+ """Same dims as the driving video; per-frame keep only the region selected by
203
+ crop_kind ('bbox' | 'mask' | 'steady_bbox') and blacken the rest.
204
+
205
+ 'steady_bbox' uses one static bbox = union of all masks across all frames
206
+ (with margin), so the kept rectangle never moves — useful when the driving
207
+ has camera motion and you want a stable crop window."""
208
+ T = masks[0].shape[0]
209
+ H, W = video_frames_rgb.shape[1:3]
210
+
211
+ static_keep = None
212
+ if crop_kind == 'steady_bbox':
213
+ bbox = _compute_steady_bbox(masks, H, W, bbox_margin)
214
+ static_keep = np.zeros((H, W), bool)
215
+ if bbox is not None:
216
+ x0, y0, x1, y1 = bbox
217
+ static_keep[y0:y1, x0:x1] = True
218
+ print(f" steady_bbox: {bbox} (W={x1-x0}, H={y1-y0}) in {W}x{H} frame")
219
+
220
+ frames = []
221
+ for t in range(T):
222
+ if static_keep is not None:
223
+ keep = static_keep
224
+ else:
225
+ keep = _frame_keep_mask([masks[i][t] for i in range(len(masks))],
226
+ H, W, crop_kind, bbox_margin)
227
+ out = np.zeros_like(video_frames_rgb[t])
228
+ out[keep] = video_frames_rgb[t][keep]
229
+ frames.append(out)
230
+ out_dir = os.path.dirname(out_path)
231
+ if out_dir:
232
+ os.makedirs(out_dir, exist_ok=True)
233
+ mpy.ImageSequenceClip(frames, fps=fps).write_videofile(out_path)
SCAIL-Pose/README.md ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <h1>Official Code for Processing Driving Videos for SCAIL Series</h1>
2
+ <div align="center">
3
+ <a href='https://arxiv.org/abs/2512.05905'><img src='https://img.shields.io/badge/📖 arXiv-2512.05905-red'></a>
4
+ <a href='https://teal024.github.io/SCAIL/'><img src='https://img.shields.io/badge/🌐 Project Page-green'></a>
5
+ <a href="https://github.com/zai-org/SCAIL">
6
+ <img src="https://img.shields.io/badge/%20Main GitHub Repo-181717?logo=github">
7
+ </a>
8
+ </div>
9
+
10
+
11
+ This repository contains the code to process driving videos for **SCAIL**, a series of frameworks towards Studio-Grade Character Animation via In-Context Learning. The frameworks enable complex animation under diverse and challenging
12
+ conditions, including large motion variations and multi-character interactions. The main repo is at [zai-org/SCAIL](https://github.com/zai-org/SCAIL).
13
+ <p align="center">
14
+ <img src="resources/pose_teaser.png" alt="teaser" width="90%"><br>
15
+ <b>SCAIL</b>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <img src="resources/teaser.png" alt="teaser" width="90%"><br>
20
+ <b>SCAIL-2</b>
21
+ </p>
22
+
23
+
24
+ ## 📋 Methods
25
+ **SCAIL** is a series of frameworks towards Studio-Grade Character Animation via In-Context Learning. The first open-source work of this series is SCAIL-Preview, a pose-driven animation framework. We develop a 3D skeleton for the pose representation to be fully identity agnostic and depth-aware. The representation can process multi-human interactions, yielding robust results from [NLFPose](https://github.com/isarandi/nlf)’s reliable depth estimation.
26
+
27
+ <p align="center">
28
+ <img src='resources/pose_result.png' alt='Teaser' width='95%'>
29
+ </p>
30
+
31
+ Despite current progress, skeleton maps suffer from inherent ambiguity under complex scenarios. As intermediates, skeleton maps suffer from inherent ambiguity under complex scenarios. Further, it restricts the driving source to be exocentric human movements and thus cannot handle driving sources like animals. Character replacement and multi-character animation suffers from similar issues, where state-of-the-art methods use inpainting masks, but such masks are still a form of intermediates and limits the application and bounds the performance.
32
+
33
+ <p align="center">
34
+ <img src='resources/preteaser.png' alt='Preteaser' width='70%'>
35
+ </p>
36
+
37
+ Our latest **SCAIL-2** is an end-to-end framework to bypass the pose estimation to obtain more reliable and expressive motion, utilizing the inherent in-context learning capability in the diffusion transformer. We adopt a unification design to support both Animation Mode and Replacement Mode, using
38
+ [SAM3](https://github.com/facebookresearch/sam3) to extract the explicit mask for both the reference image and the driving sequence to augment the conditioning. Benefiting from the end-to-end unification, SCAIL-2 supports diverse driving tasks. You can directly use the full driving video to drive the reference image, or use pose-driven just like SCAIL-Preview. We will elaborate different ways of driving in lateral usage instructions.
39
+
40
+
41
+ ## 🚀 Getting Started
42
+
43
+ Make sure you have already clone the main repo, this repo should be cloned under the main repo folder:
44
+ ```
45
+ SCAIL/ (or SCAIL-2/)
46
+ ├── examples
47
+ ├── sat
48
+ ├── configs
49
+ ├── ...
50
+ ├── SCAIL-Pose
51
+ ```
52
+
53
+ Change dir to this pose extraction & rendering folder:
54
+
55
+ ```
56
+ cd SCAIL-Pose/
57
+ ```
58
+
59
+ ### Environment Setup
60
+
61
+ We recommend using [mmpose](https://github.com/open-mmlab) for the environment setup. You can refer to the official
62
+ mmpose [installation guide](https://mmpose.readthedocs.io/en/latest/installation.html). Note that the example in the guide uses python 3.8, however we recommend using python>=3.10 for better compatibility with SAM models.
63
+ The following commands are used to install the required packages once you have setup the environment.
64
+
65
+ ```bash
66
+ conda activate openmmlab
67
+ pip install -r requirements.txt
68
+
69
+ # [Optional] SAM2 is only for multi-human extraction of SCAIL-Preview, for SCAIL-2 we use SAM3
70
+ git clone https://github.com/facebookresearch/sam2.git && cd sam2
71
+ pip install -e .
72
+ cd checkpoints && \
73
+ ./download_ckpts.sh && \
74
+ cd ../..
75
+ ```
76
+
77
+
78
+
79
+ ### Weights Download
80
+
81
+ First, download pretrained weights for pose extraction & rendering. The script below
82
+ downloads [NLFPose](https://github.com/isarandi/nlf) (torchscript), [DWPose](https://github.com/IDEA-Research/DWPose) (
83
+ onnx) and [YOLOX](https://github.com/Megvii-BaseDetection/YOLOX) (onnx) weights. You can also download the weights
84
+ manually and put them into the `pretrained_weights` folder.
85
+
86
+ ```bash
87
+ mkdir pretrained_weights && cd pretrained_weights
88
+ # download NLFPose Model Weights
89
+ wget https://github.com/isarandi/nlf/releases/download/v0.3.2/nlf_l_multi_0.3.2.torchscript
90
+ # download DWPose Model Weights & Detection Model Weights
91
+ mkdir DWPose
92
+ wget -O DWPose/dw-ll_ucoco_384.onnx \
93
+ https://huggingface.co/yzd-v/DWPose/resolve/main/dw-ll_ucoco_384.onnx
94
+ wget -O DWPose/yolox_l.onnx \
95
+ https://huggingface.co/yzd-v/DWPose/resolve/main/yolox_l.onnx
96
+ cd ..
97
+ ```
98
+
99
+ For **SCAIL-2**, you additionally need the SAM3 weights. SAM3 is gated on HuggingFace,
100
+ so you must first request access at [facebook/sam3](https://huggingface.co/facebook/sam3)
101
+ and agree to Meta's license. Once approved, download `sam3.pt` into `pretrained_weights/`:
102
+
103
+ ```bash
104
+ # After being granted access on HuggingFace
105
+ huggingface-cli login
106
+ huggingface-cli download facebook/sam3 sam3.pt --local-dir pretrained_weights/
107
+ ```
108
+
109
+ The weights should be formatted as follows:
110
+
111
+ ```
112
+ pretrained_weights/
113
+ ├── nlf_l_multi_0.3.2.torchscript
114
+ ├── sam3.pt
115
+ └── DWPose/
116
+ ├── dw-ll_ucoco_384.onnx
117
+ └── yolox_l.onnx
118
+ ```
119
+
120
+
121
+ ## 🦾 Usage
122
+
123
+ ### SCAIL-Preview
124
+
125
+ ```
126
+ # Single Character w/o 3D Retarget
127
+ python NLFPoseExtract/v1_process_pose.py --subdir <path_to_the_example_pair> --resolution [512, 896]
128
+
129
+ # Single Character w/ 3D Retarget
130
+ python NLFPoseExtract/v1_process_pose.py --subdir <path_to_the_example_pair> --use_align --resolution [512, 896]
131
+
132
+ # Multi-Human
133
+ python NLFPoseExtract/v1_process_pose_multi.py --subdir <path_to_the_example_pair> --resolution [512, 896]
134
+ ```
135
+
136
+ ### SCAIL-2
137
+ For SCAIL-2, two entrypoints cover the two tasks: **Animation** (`process_animation_aio.py`) and **Replacement** (`process_replacement.py`).
138
+
139
+ #### Animation Mode
140
+
141
+ ```bash
142
+ # (Recommended) End-to-end: rendered_v2.mp4 = driving copy, mask video is colored SAM3 masks.
143
+ # More accurate and easier than pose-driven for most cases.
144
+ python NLFPoseExtract/process_animation_aio.py --subdir <example_dir> --e2e_mode
145
+
146
+ # Pose-driven (no --e2e_mode): runs NLF + DWpose, rendered_v2.mp4 is the skeleton render.
147
+ # More interpretable / controllable; use it for extremely challenging inputs.
148
+ python NLFPoseExtract/process_animation_aio.py --subdir <example_dir>
149
+
150
+ ## Following options allow behaviours between pose-driven and full-e2e. Useful for 704p horizontal / multi-human inputs where the zero-shot resolution gap causes artifacts
151
+
152
+ # E2E + per-frame mask silhouette crop.
153
+ python NLFPoseExtract/process_animation_aio.py --subdir <example_dir> --e2e_mode --crop_e2e_mask
154
+
155
+ # E2E + per-frame bbox crop.
156
+ python NLFPoseExtract/process_animation_aio.py --subdir <example_dir> --e2e_mode --crop_e2e_bbox
157
+
158
+
159
+ ```
160
+
161
+ Other useful flags: `--max_persons N` (default 2), `--text human character ...` (extra SAM3 prompts, e.g. add `"robot arm" "gripper"` for egocentric/robotic subjects), `--sam3_model <path>` (override the default `pretrained_weights/sam3.pt` location). The same `--sam3_model` flag is also accepted by `process_replacement.py`.
162
+
163
+ #### Replacement Mode
164
+
165
+ ```bash
166
+ # Standard: ref image in the subdir, driving has 1 actor matching the ref.
167
+ python NLFPoseExtract/process_replacement.py --subdir <example_dir>
168
+
169
+ # Driving has 2 persons but you only want to replace 1: pick the driving track whose first-frame mask has
170
+ # highest IoU with the ref mask; drop the other.
171
+ python NLFPoseExtract/process_replacement.py --subdir <example_dir> --matchnearest
172
+ ```
173
+
174
+ Examples are in the main repo folder; you can also use your own images or videos. After extraction the results live in the example folder and can be fed straight into the main repo to generate character animations.
175
+
176
+ #### Notes for Animation
177
+ Although our model supports a variety of driving modalities, end-to-end driving typically achieves the best results, as the model has access to the complete visual information. This is especially evident in cases involving object interactions.
178
+
179
+ <p align="center">
180
+ <img src='resources/animation.png' alt='Preteaser' width='70%'>
181
+ </p>
182
+
183
+
184
+
185
+
186
+ ## 📄 Citation
187
+
188
+ If you find this work useful in your research, please cite:
189
+
190
+ ```bibtex
191
+ @article{yan2025scail,
192
+ title={SCAIL: Towards Studio-Grade Character Animation via In-Context Learning of 3D-Consistent Pose Representations},
193
+ author={Yan, Wenhao and Ye, Sheng and Yang, Zhuoyi and Teng, Jiayan and Dong, ZhenHui and Wen, Kairui and Gu, Xiaotao and Liu, Yong-Jin and Tang, Jie},
194
+ journal={arXiv preprint arXiv:2512.05905},
195
+ year={2025}
196
+ }
197
+ ```
SCAIL-Pose/TrackSam3/track.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from random import shuffle
3
+
4
+ import cv2
5
+ import numpy as np
6
+ from decord import VideoReader
7
+
8
+
9
+ # Minimum mask ratio threshold (percentage of frame). Override via env var
10
+ # SCAIL_MIN_MASK_RATIO for small-subject scenes (e.g. paper figures where the
11
+ # subject occupies <1% of the frame) without editing this file.
12
+ MIN_MASK_RATIO = float(os.environ.get('SCAIL_MIN_MASK_RATIO', '1.0'))
13
+
14
+ # Default cap on number of targets when caller does not override
15
+ DEFAULT_MAX_TARGETS = 4
16
+
17
+ # Deterministic BGR palette used when callers want stable colors across runs.
18
+ DEFAULT_PALETTE_BGR = [
19
+ (255, 0, 0), # Blue
20
+ (0, 0, 255), # Red
21
+ (0, 255, 0), # Green
22
+ (255, 0, 255), # Magenta
23
+ (255, 255, 0), # Cyan
24
+ (0, 255, 255), # Yellow
25
+ ]
26
+
27
+
28
+ def remove_small_tracks_from_predictor(predictor, invalid_track_ids):
29
+ """Remove invalid track IDs from predictor's internal tracker state."""
30
+ if not invalid_track_ids:
31
+ return
32
+
33
+ metadata = predictor.inference_state.get("tracker_metadata", {})
34
+ if not metadata:
35
+ return
36
+
37
+ obj_ids = metadata.get("obj_ids_all_gpu", np.array([]))
38
+ if len(obj_ids) == 0:
39
+ return
40
+
41
+ keep_mask = np.array([int(oid) not in invalid_track_ids for oid in obj_ids])
42
+ metadata["obj_ids_all_gpu"] = obj_ids[keep_mask]
43
+
44
+ arrays_to_filter = [
45
+ "obj_id_to_score", "obj_id_to_cls", "obj_id_to_tracker_score"
46
+ ]
47
+ for key in arrays_to_filter:
48
+ if key in metadata and isinstance(metadata[key], dict):
49
+ metadata[key] = {k: v for k, v in metadata[key].items() if int(k) not in invalid_track_ids}
50
+
51
+ tracker_states = predictor.inference_state.get("tracker_inference_states", [])
52
+ if tracker_states:
53
+ for state in tracker_states:
54
+ if hasattr(state, 'obj_ids') and state.obj_ids is not None:
55
+ state_keep = np.array([int(oid) not in invalid_track_ids for oid in state.obj_ids])
56
+ state.obj_ids = state.obj_ids[state_keep]
57
+
58
+ print(f"Removed track IDs {invalid_track_ids} from tracker state")
59
+
60
+
61
+ def visualize_and_save_mask(results, width, height, predictor, new_indices, full_length,
62
+ max_targets=DEFAULT_MAX_TARGETS, shuffle_colors=True,
63
+ direct_return=False):
64
+ """Run through SAM3 streaming results and gather per-track binary masks.
65
+
66
+ Returns (valid_track_ids_ordered, mask_arrays, track_colors) ordered by descending
67
+ mask area in the first frame; or None if no valid track is detected.
68
+ """
69
+ colors = list(DEFAULT_PALETTE_BGR)
70
+ if shuffle_colors:
71
+ shuffle(colors)
72
+
73
+ frame_idx = 0
74
+ valid_track_ids = None
75
+ total_pixels = height * width
76
+ valid_track_ids_ordered = []
77
+ mask_arrays = {}
78
+ track_colors = {}
79
+ _color_counter = 0
80
+
81
+ for result_idx, result in enumerate(results):
82
+ index_result = new_indices[result_idx]
83
+ if result.masks is not None:
84
+ masks = result.masks.data.cpu().numpy() # (N, H, W)
85
+ track_ids = result.boxes.id.cpu().numpy() if result.boxes.id is not None else np.arange(len(masks))
86
+
87
+ if frame_idx == 0:
88
+ valid_track_ids = set()
89
+ invalid_track_ids = set()
90
+ candidates = []
91
+ for i, (mask, track_id) in enumerate(zip(masks, track_ids)):
92
+ if mask.shape[:2] != (height, width):
93
+ mask_resized = cv2.resize(mask.astype(np.float32), (width, height))
94
+ else:
95
+ mask_resized = mask
96
+ mask_bool = mask_resized > 0.5
97
+ mask_ratio = np.sum(mask_bool) / total_pixels * 100
98
+ if mask_ratio >= MIN_MASK_RATIO:
99
+ candidates.append((int(track_id), mask_ratio))
100
+ else:
101
+ invalid_track_ids.add(int(track_id))
102
+
103
+ candidates.sort(key=lambda x: x[1], reverse=True)
104
+
105
+ if len(candidates) == 0 and direct_return:
106
+ print(f" No valid candidates (all < MIN_MASK_RATIO={MIN_MASK_RATIO}%) in first frame, return")
107
+ return
108
+
109
+ if len(candidates) > max_targets:
110
+ if direct_return:
111
+ print(f" Found {len(candidates)} candidates, return")
112
+ return
113
+ print(f" Found {len(candidates)} candidates, limiting to top {max_targets}")
114
+ kept_candidates = candidates[:max_targets]
115
+ dropped_candidates = candidates[max_targets:]
116
+ for track_id, _ in kept_candidates:
117
+ valid_track_ids.add(track_id)
118
+ for track_id, _ in dropped_candidates:
119
+ invalid_track_ids.add(track_id)
120
+ else:
121
+ kept_candidates = candidates
122
+ for track_id, _ in candidates:
123
+ valid_track_ids.add(track_id)
124
+
125
+ if kept_candidates and direct_return:
126
+ max_ratio = kept_candidates[0][1]
127
+ if max_ratio < 1.5 or max_ratio > 50:
128
+ print(f" Max mask ratio {max_ratio:.2f}% out of valid range [1.5, 50], return")
129
+ return
130
+
131
+ if len(kept_candidates) >= 2 and direct_return:
132
+ max_ratio = kept_candidates[0][1]
133
+ min_ratio = kept_candidates[-1][1]
134
+ if min_ratio < max_ratio / 3:
135
+ print(f" Smallest person ({min_ratio:.2f}%) < 1/3 of largest ({max_ratio:.2f}%), return")
136
+ return
137
+
138
+ valid_track_ids_ordered = [tid for tid, _ in kept_candidates]
139
+ mask_arrays = {tid: np.zeros((full_length, height, width), dtype=bool)
140
+ for tid in valid_track_ids_ordered}
141
+
142
+ if invalid_track_ids:
143
+ remove_small_tracks_from_predictor(predictor, invalid_track_ids)
144
+
145
+ for i, (mask, track_id) in enumerate(zip(masks, track_ids)):
146
+ if valid_track_ids is not None and int(track_id) not in valid_track_ids:
147
+ continue
148
+
149
+ tid = int(track_id)
150
+ if tid not in track_colors:
151
+ track_colors[tid] = colors[_color_counter % len(colors)]
152
+ _color_counter += 1
153
+
154
+ if mask.shape[:2] != (height, width):
155
+ mask = cv2.resize(mask.astype(np.float32), (width, height))
156
+ mask_bool = mask > 0.5
157
+
158
+ if tid in mask_arrays:
159
+ mask_arrays[tid][index_result] = mask_bool
160
+
161
+ frame_idx += 1
162
+
163
+ if not valid_track_ids_ordered:
164
+ return None
165
+ return valid_track_ids_ordered, mask_arrays, track_colors
166
+
167
+
168
+ def _centroid_x(mask_2d):
169
+ """X-coordinate of the centroid of a 2D bool mask. Returns +inf if mask is empty."""
170
+ cols = np.where(mask_2d.any(axis=0))[0]
171
+ if len(cols) == 0:
172
+ return float('inf')
173
+ rows = np.where(mask_2d.any(axis=1))[0]
174
+ # use bounding-box center (cheap and stable)
175
+ return 0.5 * (cols[0] + cols[-1])
176
+
177
+
178
+ def _reorder_and_color(valid_track_ids_ordered, mask_arrays, sort_by, fixed_colors):
179
+ """Apply left-to-right sort and deterministic color assignment.
180
+
181
+ Returns (masks, colors) where masks is a list of (T, H, W) bool ndarray and
182
+ colors is a list of BGR tuples, both in the chosen ordering.
183
+ """
184
+ if sort_by == 'x':
185
+ ordered = sorted(valid_track_ids_ordered,
186
+ key=lambda tid: _centroid_x(mask_arrays[tid][0]))
187
+ elif sort_by == 'area':
188
+ ordered = list(valid_track_ids_ordered)
189
+ else:
190
+ raise ValueError(f"unknown sort_by: {sort_by}")
191
+
192
+ n = len(ordered)
193
+ if fixed_colors is not None:
194
+ if len(fixed_colors) < n:
195
+ raise ValueError(f"fixed_colors has {len(fixed_colors)} entries but {n} tracks")
196
+ colors = [tuple(c) for c in fixed_colors[:n]]
197
+ else:
198
+ colors = [DEFAULT_PALETTE_BGR[i % len(DEFAULT_PALETTE_BGR)] for i in range(n)]
199
+
200
+ masks = [mask_arrays[tid] for tid in ordered]
201
+ return masks, colors
202
+
203
+
204
+ def get_mask_from_video(video_path, predictor, max_targets=DEFAULT_MAX_TARGETS,
205
+ sort_by='area', fixed_colors=None,
206
+ text=("human", "character")):
207
+ """Run SAM3 tracking on a video file and return per-person binary masks and colors.
208
+
209
+ Args:
210
+ video_path: path to input video (str or Path).
211
+ predictor: SAM3VideoSemanticPredictor instance (state will be reset).
212
+ max_targets: cap on the number of tracked persons (kept by descending area).
213
+ sort_by: 'area' (default, descending area) or 'x' (left-to-right by first-frame
214
+ centroid x).
215
+ fixed_colors: optional list of BGR tuples assigned to ordered tracks instead of
216
+ the default palette. Must have at least len(tracks) entries.
217
+
218
+ Returns:
219
+ masks: list of (T, H, W) bool ndarray, one per tracked person.
220
+ colors: list of BGR color tuples corresponding to each person.
221
+ Both lists are empty if no valid persons are detected.
222
+ """
223
+ video_path = str(video_path)
224
+
225
+ predictor.inference_state = {}
226
+ if hasattr(predictor, 'dataset'):
227
+ predictor.dataset = None
228
+
229
+ vr = VideoReader(video_path)
230
+ full_length = len(vr)
231
+ height, width = vr[0].asnumpy().shape[:2]
232
+ del vr
233
+
234
+ results = predictor(source=video_path, text=list(text), stream=True)
235
+ ret = visualize_and_save_mask(
236
+ results, width, height, predictor,
237
+ new_indices=np.arange(full_length), full_length=full_length,
238
+ max_targets=max_targets, shuffle_colors=fixed_colors is None,
239
+ direct_return=False,
240
+ )
241
+ if ret is None:
242
+ return [], []
243
+ valid_track_ids_ordered, mask_arrays, _ = ret
244
+ return _reorder_and_color(valid_track_ids_ordered, mask_arrays, sort_by, fixed_colors)
245
+
246
+
247
+ def get_mask_from_image_via_video(image_path, video_predictor, max_targets=DEFAULT_MAX_TARGETS,
248
+ sort_by='x', fixed_colors=None,
249
+ text=("human", "character"), n_repeat=4, fps=8):
250
+ """Detect persons in a still image by wrapping it as a tiny mp4 and routing through
251
+ SAM3VideoSemanticPredictor. Workaround for image-mode SAM3 missing small / distant
252
+ subjects that the video pipeline picks up reliably.
253
+
254
+ Returns (masks, colors) with each mask shaped (1, H, W) bool — only the first frame
255
+ of the synthetic clip is kept.
256
+ """
257
+ import tempfile
258
+ from NLFPoseExtract.v2_helper import imread_bgr
259
+ image_path = str(image_path)
260
+ img = imread_bgr(image_path)
261
+ H, W = img.shape[:2]
262
+
263
+ tmp_fd, tmp_path = tempfile.mkstemp(suffix='.mp4')
264
+ os.close(tmp_fd)
265
+ try:
266
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
267
+ vw = cv2.VideoWriter(tmp_path, fourcc, float(fps), (W, H))
268
+ if not vw.isOpened():
269
+ raise RuntimeError(f"cv2.VideoWriter failed to open {tmp_path}")
270
+ for _ in range(n_repeat):
271
+ vw.write(img)
272
+ vw.release()
273
+
274
+ masks, colors = get_mask_from_video(
275
+ tmp_path, video_predictor,
276
+ max_targets=max_targets, sort_by=sort_by,
277
+ fixed_colors=fixed_colors, text=text,
278
+ )
279
+ finally:
280
+ try:
281
+ os.unlink(tmp_path)
282
+ except OSError:
283
+ pass
284
+
285
+ masks = [m[:1] for m in masks]
286
+ return masks, colors
287
+
288
+
289
+ def get_mask_from_image(image_path, predictor, max_targets=DEFAULT_MAX_TARGETS,
290
+ sort_by='x', fixed_colors=None,
291
+ text=("human", "character")):
292
+ """Run SAM3SemanticPredictor (image variant) on a single image.
293
+
294
+ Args:
295
+ image_path: path to input image (str or Path).
296
+ predictor: SAM3SemanticPredictor instance.
297
+ max_targets: cap on number of persons (kept by descending area).
298
+ sort_by: 'x' (default, left-to-right) or 'area'.
299
+ fixed_colors: optional list of BGR tuples assigned in order; otherwise the
300
+ deterministic palette is used.
301
+
302
+ Returns:
303
+ masks: list of (1, H, W) bool ndarray, one per detected person.
304
+ colors: list of BGR color tuples corresponding to each person.
305
+ """
306
+ image_path = str(image_path)
307
+ results = predictor(source=image_path, text=list(text))
308
+ if not results:
309
+ return [], []
310
+ result = results[0]
311
+ if result.masks is None or len(result.masks) == 0:
312
+ return [], []
313
+
314
+ masks_NHW = result.masks.data.cpu().numpy() # (N, H, W)
315
+ masks_NHW = masks_NHW > 0.5
316
+
317
+ return _filter_image_masks(masks_NHW, max_targets, sort_by, fixed_colors)
318
+
319
+
320
+ def _filter_image_masks(masks_NHW, max_targets, sort_by, fixed_colors):
321
+ """Apply MIN_MASK_RATIO + max_targets filter to per-image SAM masks, then order
322
+ and color them. Returns (masks_list, colors) where each mask is (1, H, W) bool.
323
+ """
324
+ N, H, W = masks_NHW.shape
325
+ total_pixels = H * W
326
+
327
+ candidates = [] # (idx, mask_ratio)
328
+ for i in range(N):
329
+ ratio = float(np.sum(masks_NHW[i])) / total_pixels * 100
330
+ if ratio >= MIN_MASK_RATIO:
331
+ candidates.append((i, ratio))
332
+
333
+ candidates.sort(key=lambda x: x[1], reverse=True)
334
+ candidates = candidates[:max_targets]
335
+ if not candidates:
336
+ return [], []
337
+
338
+ kept = [masks_NHW[idx] for idx, _ in candidates] # list of (H, W) bool
339
+
340
+ if sort_by == 'x':
341
+ order = sorted(range(len(kept)), key=lambda i: _centroid_x(kept[i]))
342
+ kept = [kept[i] for i in order]
343
+ elif sort_by != 'area':
344
+ raise ValueError(f"unknown sort_by: {sort_by}")
345
+
346
+ n = len(kept)
347
+ if fixed_colors is not None:
348
+ if len(fixed_colors) < n:
349
+ raise ValueError(f"fixed_colors has {len(fixed_colors)} entries but {n} masks")
350
+ colors = [tuple(c) for c in fixed_colors[:n]]
351
+ else:
352
+ colors = [DEFAULT_PALETTE_BGR[i % len(DEFAULT_PALETTE_BGR)] for i in range(n)]
353
+
354
+ masks_out = [m[None] for m in kept] # add T=1 axis
355
+ return masks_out, colors
SCAIL-Pose/pose_draw/draw_3d_utils.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ def convert_3dpose_to_2dpose_body(body_keypoints, face_keypoints):
4
+ """
5
+ 将20点的3D坐标映射到18点的2D坐标。
6
+ :param poses: 输入的20点坐标列表,每个点为 [x, y, z]
7
+ :return: 映射得到的18点坐标列表,每个点为 [x, y]
8
+ """
9
+ # 映射关系:索引位置
10
+ body_mapping = {
11
+ 0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 8, 9: 9, 10: 10, 11: 23, 13: 22, 12: 21,
12
+ 14: 11, 15: 12, 16: 13, 17: 20, 18: 18, 19: 19
13
+ }
14
+ face_mapping = {
15
+ 1: 16, 8: 14, 4: 0, 7: 15, 0: 17
16
+ }
17
+
18
+ # 初始化18点坐标列表,默认值为 [-1, -1]
19
+ result = [[-1, -1] for _ in range(24)]
20
+
21
+ # 遍历映射关系,将对应的20点坐标映射到18点坐标
22
+ for src_idx, dst_idx in body_mapping.items():
23
+ if src_idx < len(body_keypoints): # 确保索引不越界
24
+ result[dst_idx] = [body_keypoints[src_idx][1],body_keypoints[src_idx][0]] # 提取 x, y 坐标
25
+ for src_idx, dst_idx in face_mapping.items():
26
+ if src_idx < len(face_keypoints):
27
+ result[dst_idx] = [face_keypoints[src_idx][1], face_keypoints[src_idx][0]]
28
+ return result
29
+
30
+ def convert_3dpose_to_2dpose_hand(left_hand_keypoints, right_hand_keypoints, body_keypoints):
31
+ """
32
+ 将20点的3D坐标映射到18点的2D坐标。
33
+ :param poses: 输入的20点坐标列表,每个点为 [x, y, z]
34
+ :return: 映射得到的18点坐标列表,每个点为 [x, y]
35
+ """
36
+ # 映射关系:索引位置
37
+ hand_mapping = {
38
+ 0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10,
39
+ 10: 11, 11: 12, 12: 13, 13: 14, 14: 15, 15: 16, 16: 17, 17: 18,
40
+ 18: 19, 19: 20
41
+ }
42
+
43
+ body_mapping_left = {3: 0}
44
+ body_mapping_right = {6: 0}
45
+
46
+ # 初始化18点坐标列表,默认值为 [-1, -1]
47
+ left_result = [[-1, -1] for _ in range(21)]
48
+ right_result = [[-1, -1] for _ in range(21)]
49
+
50
+ # 遍历映射关系,将对应的20点坐标映射到18点坐标
51
+ for src_idx, dst_idx in hand_mapping.items():
52
+ if src_idx < len(left_hand_keypoints): # 确保索引不越界
53
+ left_result[dst_idx] = [left_hand_keypoints[src_idx][1], left_hand_keypoints[src_idx][0]] # 提取 x, y 坐标
54
+ right_result[dst_idx] = [right_hand_keypoints[src_idx][1], right_hand_keypoints[src_idx][0]]
55
+
56
+ for src_idx, dst_idx in body_mapping_left.items():
57
+ if src_idx < len(body_keypoints):
58
+ left_result[dst_idx] = [body_keypoints[src_idx][1], body_keypoints[src_idx][0]]
59
+ for src_idx, dst_idx in body_mapping_right.items():
60
+ if src_idx < len(body_keypoints):
61
+ right_result[dst_idx] = [body_keypoints[src_idx][1], body_keypoints[src_idx][0]]
62
+
63
+ return [left_result, right_result]
64
+
65
+ def convert_3dpose_to_2dpose_face(face_keypoints):
66
+ result = [[-1, -1] if i in [0, 1, 4, 5, 6, 7, 8] else [pt[1], pt[0]] for i, pt in enumerate(face_keypoints)]
67
+ return result
68
+
69
+ def correct_lift_end_kpt_by_phmr(start, end, dwpose_kpts, lift_start, lift_end, phmr_start, phmr_end):
70
+ '''
71
+ 检查另一端是否符合要求, 符合要求则返回lift后结果,不然返回phmr结果
72
+ '''
73
+ if dwpose_kpts[start][0] == -1:
74
+ return
75
+ lift_vec = np.array(lift_end) - np.array(lift_start)
76
+ phmr_vec = np.array(phmr_end) - np.array(phmr_start)
77
+ start_distance = np.linalg.norm(np.array(lift_start) - np.array(phmr_start))
78
+ end_distance = np.linalg.norm(np.array(lift_end) - np.array(phmr_end))
79
+ lift_vec_len = np.linalg.norm(lift_vec)
80
+ phmr_vec_len = np.linalg.norm(phmr_vec)
81
+ if start_distance + end_distance > phmr_vec_len:
82
+ dwpose_kpts[end] = [-1, -1]
83
+ theta = np.arccos(np.dot(lift_vec, phmr_vec) / (lift_vec_len * phmr_vec_len))
84
+ if lift_vec_len > phmr_vec_len * 1.65 or lift_vec_len < phmr_vec_len * 0.4 or theta > np.pi / 4:
85
+ dwpose_kpts[end] = [-1, -1]
86
+ return
87
+
88
+
89
+
90
+ def mix_3d_poses(poses_dwpose, poses_3dpose):
91
+ '''
92
+ 组合两种pose,用3dPose的身体,DWPose的face和hand
93
+ '''
94
+ poses = []
95
+ for pose_dwpose, pose_3dpose in zip(poses_dwpose, poses_3dpose):
96
+ pose = {
97
+ "bodies": {
98
+ "candidate": pose_3dpose["bodies"]["candidate"],
99
+ "subset": pose_dwpose["bodies"]["subset"]
100
+ },
101
+ "faces": pose_dwpose["faces"],
102
+ "hands": pose_dwpose["hands"]
103
+ }
104
+ poses.append(pose)
105
+ return poses
106
+
107
+ def correct_hand_from_3d(hand_keypoints_dwpose, hand_keypoints_3dpose):
108
+ '''
109
+ 如果dwpose的手部关节点和3dpose的手部关节点相差过大,则去掉最远的那一端
110
+ '''
111
+ edges_palm = [
112
+ [1, 2], [2, 3], [3, 4],
113
+ [5, 6], [6, 7], [7, 8],
114
+ [9, 10], [10, 11], [11, 12],
115
+ [13, 14], [14, 15], [15, 16],
116
+ [17, 18], [18, 19], [19, 20],
117
+ ]
118
+ edges_finger = [[0, 1], [0, 5], [0, 9], [0, 13], [0, 17]]
119
+ max_length_palm = 0
120
+ max_length_finger = 0
121
+ for edge in edges_palm:
122
+ limb_length_3dpose = np.linalg.norm(np.array(hand_keypoints_3dpose[edge[0]]) - np.array(hand_keypoints_3dpose[edge[1]]))
123
+ if limb_length_3dpose > max_length_palm:
124
+ max_length_palm = limb_length_3dpose
125
+ for edge in edges_finger:
126
+ limb_length_3dpose = np.linalg.norm(np.array(hand_keypoints_3dpose[edge[0]]) - np.array(hand_keypoints_3dpose[edge[1]]))
127
+ if limb_length_3dpose > max_length_finger:
128
+ max_length_finger = limb_length_3dpose
129
+ for edge in edges_palm:
130
+ limb_length_dwpose = np.linalg.norm(np.array(hand_keypoints_dwpose[edge[0]]) - np.array(hand_keypoints_dwpose[edge[1]]))
131
+ if limb_length_dwpose > max_length_palm * 1.5:
132
+ if -1 in hand_keypoints_dwpose[edge[0]] or -1 in hand_keypoints_dwpose[edge[1]] or -1 in hand_keypoints_3dpose[edge[0]] or -1 in hand_keypoints_3dpose[edge[1]]:
133
+ continue
134
+ distance_point_0 = np.linalg.norm(np.array(hand_keypoints_dwpose[edge[0]]) - np.array(hand_keypoints_3dpose[edge[0]]))
135
+ distance_point_1 = np.linalg.norm(np.array(hand_keypoints_dwpose[edge[1]]) - np.array(hand_keypoints_3dpose[edge[1]]))
136
+ if distance_point_0 > distance_point_1:
137
+ hand_keypoints_dwpose[edge[1]] = [-1, -1]
138
+ else:
139
+ hand_keypoints_dwpose[edge[0]] = [-1, -1]
140
+ for edge in edges_finger:
141
+ limb_length_dwpose = np.linalg.norm(np.array(hand_keypoints_dwpose[edge[0]]) - np.array(hand_keypoints_dwpose[edge[1]]))
142
+ if limb_length_dwpose > max_length_finger * 1.5:
143
+ if -1 in hand_keypoints_dwpose[edge[0]] or -1 in hand_keypoints_dwpose[edge[1]] or -1 in hand_keypoints_3dpose[edge[0]] or -1 in hand_keypoints_3dpose[edge[1]]:
144
+ continue
145
+ distance_point_0 = np.linalg.norm(np.array(hand_keypoints_dwpose[edge[0]]) - np.array(hand_keypoints_3dpose[edge[0]]))
146
+ distance_point_1 = np.linalg.norm(np.array(hand_keypoints_dwpose[edge[1]]) - np.array(hand_keypoints_3dpose[edge[1]]))
147
+ if distance_point_0 > distance_point_1:
148
+ hand_keypoints_dwpose[edge[1]] = [-1, -1]
149
+ else:
150
+ hand_keypoints_dwpose[edge[0]] = [-1, -1]
151
+ return hand_keypoints_dwpose
152
+
153
+ def correct_body_from_3d(body_keypoints_dwpose, body_keypoints_3dpose, subset_dwpose, subset_3dpose):
154
+ '''
155
+ 如果dwpose的骨骼长度和3dpose的骨骼长度相差过大,则去掉最远的那一端
156
+ '''
157
+ limbSeq = [
158
+ [2, 3],
159
+ [2, 6],
160
+ [3, 4],
161
+ [4, 5],
162
+ [6, 7],
163
+ [7, 8],
164
+ [2, 9],
165
+ [9, 10],
166
+ [10, 11],
167
+ [2, 12],
168
+ [12, 13],
169
+ [13, 14],
170
+ [2, 1],
171
+ [1, 15],
172
+ [15, 17],
173
+ [1, 16],
174
+ [16, 18],
175
+ [3, 17],
176
+ [6, 18],
177
+ ]
178
+
179
+ for ori_limb in limbSeq:
180
+ limb = [ori_limb[0] - 1, ori_limb[1] - 1]
181
+ limb_length_dwpose = np.linalg.norm(np.array(body_keypoints_dwpose[limb[0]]) - np.array(body_keypoints_dwpose[limb[1]]))
182
+ limb_length_3dpose = np.linalg.norm(np.array(body_keypoints_3dpose[limb[0]]) - np.array(body_keypoints_3dpose[limb[1]]))
183
+ if subset_dwpose[0][limb[0]] == -1 or subset_dwpose[0][limb[1]] == -1 or subset_3dpose[0][limb[0]] == -1 or subset_3dpose[0][limb[1]] == -1:
184
+ continue
185
+ if limb_length_dwpose > limb_length_3dpose * 2:
186
+ # 判断较远端
187
+ distance_point_0 = np.linalg.norm(np.array(body_keypoints_dwpose[limb[0]]) - np.array(body_keypoints_3dpose[limb[0]]))
188
+ distance_point_1 = np.linalg.norm(np.array(body_keypoints_dwpose[limb[1]]) - np.array(body_keypoints_3dpose[limb[1]]))
189
+ if distance_point_0 > distance_point_1:
190
+ if limb[1] == 1: # 核心
191
+ continue
192
+ body_keypoints_dwpose[limb[1]] = [-1, -1]
193
+ subset_dwpose[0][limb[1]] = -1
194
+ else:
195
+ if limb[0] == 1: # 核心
196
+ continue
197
+ body_keypoints_dwpose[limb[0]] = [-1, -1]
198
+ subset_dwpose[0][limb[0]] = -1
199
+ return body_keypoints_dwpose, subset_dwpose
200
+
201
+ def correct_full_pose_from_3d(poses_dwpose, poses_3dpose):
202
+ '''
203
+ 如果dwpose的骨骼长度和3dpose的骨骼长度相差过大,则去掉离3d pose最远的那一端
204
+ '''
205
+ poses = []
206
+ for pose_dwpose, pose_3dpose in zip(poses_dwpose, poses_3dpose):
207
+ new_candidate, new_subset = correct_body_from_3d(pose_dwpose["bodies"]["candidate"], pose_3dpose["bodies"]["candidate"], pose_dwpose["bodies"]["subset"], pose_3dpose["bodies"]["subset"])
208
+ new_hands_0 = correct_hand_from_3d(pose_dwpose["hands"][0], pose_3dpose["hands"][0])
209
+ new_hands_1 = correct_hand_from_3d(pose_dwpose["hands"][1], pose_3dpose["hands"][1])
210
+ pose = {
211
+ "bodies": {
212
+ "candidate": new_candidate,
213
+ "subset": new_subset
214
+ },
215
+ "faces": pose_dwpose["faces"],
216
+ "hands": [new_hands_0, new_hands_1]
217
+ }
218
+ poses.append(pose)
219
+
220
+ return poses
SCAIL-Pose/pose_draw/draw_pose_utils.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from PIL import Image
4
+ import torch
5
+ import sys
6
+ import os
7
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+ import pose_draw.draw_utils as util
9
+ from pose_draw.draw_3d_utils import *
10
+ from pose_draw.reshape_utils import *
11
+ from DWPoseProcess.AAUtils import read_frames_and_fps_as_np, save_videos_from_pil
12
+ from DWPoseProcess.checkUtils import *
13
+ import random
14
+ import shutil
15
+ import argparse
16
+ import yaml # Add this import
17
+ import os
18
+ from tqdm import tqdm
19
+ from multiprocessing import Pool, cpu_count
20
+ from decord import VideoReader
21
+ import copy
22
+
23
+
24
+ def draw_pose(pose, H, W, show_feet=False, show_body=True, show_hand=True, show_face=True, show_cheek=False, dw_bgr=False, dw_hand=False, aug_body_draw=False, optimized_face=False):
25
+ final_canvas = np.zeros(shape=(H, W, 3), dtype=np.uint8)
26
+ for i in range(len(pose["bodies"]["candidate"])):
27
+ canvas = np.zeros(shape=(H, W, 3), dtype=np.uint8)
28
+ bodies = pose["bodies"]
29
+ faces = pose["faces"][i:i+1]
30
+ hands = pose["hands"][2*i:2*i+2]
31
+ candidate = bodies["candidate"][i]
32
+ subset = bodies["subset"][i:i+1] # subset是认为的有效点
33
+
34
+ if show_body:
35
+ if len(subset[0]) <= 18 or show_feet == False:
36
+ if aug_body_draw:
37
+ raise NotImplementedError("aug_body_draw is not implemented yet")
38
+ else:
39
+ canvas = util.draw_bodypose(canvas, candidate, subset)
40
+ else:
41
+ canvas = util.draw_bodypose_with_feet(canvas, candidate, subset)
42
+ if dw_bgr:
43
+ canvas = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
44
+ if show_cheek:
45
+ assert show_body == False, "show_cheek and show_body cannot be True at the same time"
46
+ canvas = util.draw_bodypose_augmentation(canvas, candidate, subset, drop_aug=True, shift_aug=False, all_cheek_aug=True)
47
+ if show_hand:
48
+ if not dw_hand:
49
+ canvas = util.draw_handpose_lr(canvas, hands)
50
+ else:
51
+ canvas = util.draw_handpose(canvas, hands)
52
+ if show_face:
53
+ canvas = util.draw_facepose(canvas, faces, optimized_face=optimized_face)
54
+ final_canvas = final_canvas + canvas
55
+ return final_canvas
56
+
57
+
58
+ def scale_image_hw_keep_size(img, scale_h, scale_w):
59
+ """分别按 scale_h, scale_w 缩放图像,保持输出尺寸不变。"""
60
+ H, W = img.shape[:2]
61
+ new_H, new_W = int(H * scale_h), int(W * scale_w)
62
+ scaled = cv2.resize(img, (new_W, new_H), interpolation=cv2.INTER_LINEAR)
63
+
64
+ result = np.zeros_like(img)
65
+
66
+ # 计算在目标图上的放置范围
67
+ # --- Y方向 ---
68
+ if new_H >= H:
69
+ y_start_src = (new_H - H) // 2
70
+ y_end_src = y_start_src + H
71
+ y_start_dst = 0
72
+ y_end_dst = H
73
+ else:
74
+ y_start_src = 0
75
+ y_end_src = new_H
76
+ y_start_dst = (H - new_H) // 2
77
+ y_end_dst = y_start_dst + new_H
78
+
79
+ # --- X方向 ---
80
+ if new_W >= W:
81
+ x_start_src = (new_W - W) // 2
82
+ x_end_src = x_start_src + W
83
+ x_start_dst = 0
84
+ x_end_dst = W
85
+ else:
86
+ x_start_src = 0
87
+ x_end_src = new_W
88
+ x_start_dst = (W - new_W) // 2
89
+ x_end_dst = x_start_dst + new_W
90
+
91
+ # 将 scaled 映射到 result
92
+ result[y_start_dst:y_end_dst, x_start_dst:x_end_dst] = scaled[y_start_src:y_end_src, x_start_src:x_end_src]
93
+
94
+ return result
95
+
96
+ def draw_pose_to_canvas_np(poses, pool, H, W, reshape_scale, show_feet_flag=False, show_body_flag=True, show_hand_flag=True, show_face_flag=True, show_cheek_flag=False, dw_bgr=False, dw_hand=False, aug_body_draw=False):
97
+ canvas_np_lst = []
98
+ for pose in poses:
99
+ if reshape_scale > 0:
100
+ pool.apply_random_reshapes(pose)
101
+ canvas = draw_pose(pose, H, W, show_feet_flag, show_body_flag, show_hand_flag, show_face_flag, show_cheek_flag, dw_bgr, dw_hand, aug_body_draw, optimized_face=True)
102
+ canvas_np_lst.append(canvas)
103
+ return canvas_np_lst
104
+
105
+
106
+ def draw_pose_to_canvas(poses, pool, H, W, reshape_scale, points_only_flag, show_feet_flag, show_body_flag=True, show_hand_flag=True, show_face_flag=True, show_cheek_flag=False, dw_bgr=False, dw_hand=False, aug_body_draw=False):
107
+ canvas_lst = []
108
+ for pose in poses:
109
+ if reshape_scale > 0:
110
+ pool.apply_random_reshapes(pose)
111
+ canvas = draw_pose(pose, H, W, show_feet_flag, show_body_flag, show_hand_flag, show_face_flag, show_cheek_flag, dw_bgr, dw_hand, aug_body_draw, optimized_face=False)
112
+ canvas_img = Image.fromarray(canvas)
113
+ canvas_lst.append(canvas_img)
114
+ return canvas_lst
115
+
116
+
117
+ def get_mp4_filenames_from_directory(dwpose_keypoints_dir):
118
+ mp4_filenames_dwpose = []
119
+ # 通过keypoints和mp4的交集取所有可用的mp4
120
+ if dwpose_keypoints_dir:
121
+ for root, dirs, files in os.walk(dwpose_keypoints_dir):
122
+ for file in files:
123
+ if file.lower().endswith('.pt'): # 只查找 .mp4 文件
124
+ mp4_filenames_dwpose.append(file.replace(".pt", ".mp4")) # 获取绝对路径
125
+ return mp4_filenames_dwpose
126
+
127
+ def project_dwpose_to_3d(dwpose_keypoint, original_threed_keypoint, focal, princpt, H, W):
128
+ # 相机内参
129
+ # fx, fy = focal, focal
130
+ fx, fy = focal
131
+ cx, cy = princpt
132
+
133
+ # 2D 关键点坐标
134
+ x_2d, y_2d = dwpose_keypoint[0] * W, dwpose_keypoint[1] * H
135
+
136
+ # 原始 3D 点(相机坐标系下)
137
+ ori_x, ori_y, ori_z = original_threed_keypoint
138
+
139
+ # 使用新的 2D 点和原始深度反投影计算新的 3D 点
140
+ # 公式: x = (u - cx) * z / fx
141
+ new_x = (x_2d - cx) * ori_z / fx
142
+ new_y = (y_2d - cy) * ori_z / fy
143
+ new_z = ori_z # 保持深度不变
144
+
145
+ return [new_x, new_y, new_z]
146
+
147
+
148
+
149
+ def process_video(mp4_path, dwpose_keypoint_path, threed_keypoint_pair, reshape_scale, points_only_flag, show_feet_flag, wanted_fps=None, output_dirname=None, pose_type="dwpose"):
150
+ frames, fps = read_frames_and_fps_as_np(mp4_path)
151
+ initial_frame = frames[0]
152
+ output_path = os.path.join(output_dirname, os.path.basename(mp4_path))
153
+ os.makedirs(output_dirname, exist_ok=True)
154
+
155
+ if "3dpose" in pose_type:
156
+ raise NotImplementedError("3dpose is not implemented")
157
+ else:
158
+ poses = torch.load(dwpose_keypoint_path)
159
+ pool = reshapePool(alpha=reshape_scale)
160
+ canvas_lst = draw_pose_to_canvas(poses, pool, initial_frame.shape[0], initial_frame.shape[1], reshape_scale, points_only_flag, show_feet_flag, show_body_flag=True)
161
+ save_videos_from_pil(canvas_lst, output_path, wanted_fps)
162
+
163
+
164
+ def load_config(config_path):
165
+ with open(config_path, 'r') as f:
166
+ config = yaml.safe_load(f)
167
+ return config
168
+
169
+
SCAIL-Pose/pose_draw/draw_utils.py ADDED
@@ -0,0 +1,658 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://github.com/IDEA-Research/DWPose
2
+ import math
3
+ import numpy as np
4
+ import matplotlib
5
+ import cv2
6
+ import random
7
+
8
+ eps = 0.01
9
+
10
+
11
+ def smart_resize(x, s):
12
+ Ht, Wt = s
13
+ if x.ndim == 2:
14
+ Ho, Wo = x.shape
15
+ Co = 1
16
+ else:
17
+ Ho, Wo, Co = x.shape
18
+ if Co == 3 or Co == 1:
19
+ k = float(Ht + Wt) / float(Ho + Wo)
20
+ return cv2.resize(
21
+ x,
22
+ (int(Wt), int(Ht)),
23
+ interpolation=cv2.INTER_AREA if k < 1 else cv2.INTER_LANCZOS4,
24
+ )
25
+ else:
26
+ return np.stack([smart_resize(x[:, :, i], s) for i in range(Co)], axis=2)
27
+
28
+
29
+ def smart_resize_k(x, fx, fy):
30
+ if x.ndim == 2:
31
+ Ho, Wo = x.shape
32
+ Co = 1
33
+ else:
34
+ Ho, Wo, Co = x.shape
35
+ Ht, Wt = Ho * fy, Wo * fx
36
+ if Co == 3 or Co == 1:
37
+ k = float(Ht + Wt) / float(Ho + Wo)
38
+ return cv2.resize(
39
+ x,
40
+ (int(Wt), int(Ht)),
41
+ interpolation=cv2.INTER_AREA if k < 1 else cv2.INTER_LANCZOS4,
42
+ )
43
+ else:
44
+ return np.stack([smart_resize_k(x[:, :, i], fx, fy) for i in range(Co)], axis=2)
45
+
46
+
47
+ def padRightDownCorner(img, stride, padValue):
48
+ h = img.shape[0]
49
+ w = img.shape[1]
50
+
51
+ pad = 4 * [None]
52
+ pad[0] = 0 # up
53
+ pad[1] = 0 # left
54
+ pad[2] = 0 if (h % stride == 0) else stride - (h % stride) # down
55
+ pad[3] = 0 if (w % stride == 0) else stride - (w % stride) # right
56
+
57
+ img_padded = img
58
+ pad_up = np.tile(img_padded[0:1, :, :] * 0 + padValue, (pad[0], 1, 1))
59
+ img_padded = np.concatenate((pad_up, img_padded), axis=0)
60
+ pad_left = np.tile(img_padded[:, 0:1, :] * 0 + padValue, (1, pad[1], 1))
61
+ img_padded = np.concatenate((pad_left, img_padded), axis=1)
62
+ pad_down = np.tile(img_padded[-2:-1, :, :] * 0 + padValue, (pad[2], 1, 1))
63
+ img_padded = np.concatenate((img_padded, pad_down), axis=0)
64
+ pad_right = np.tile(img_padded[:, -2:-1, :] * 0 + padValue, (1, pad[3], 1))
65
+ img_padded = np.concatenate((img_padded, pad_right), axis=1)
66
+
67
+ return img_padded, pad
68
+
69
+
70
+ def transfer(model, model_weights):
71
+ transfered_model_weights = {}
72
+ for weights_name in model.state_dict().keys():
73
+ transfered_model_weights[weights_name] = model_weights[
74
+ ".".join(weights_name.split(".")[1:])
75
+ ]
76
+ return transfered_model_weights
77
+
78
+ def draw_bodypose_with_feet(canvas, candidate, subset):
79
+ H, W, C = canvas.shape
80
+ candidate = np.array(candidate)
81
+ subset = np.array(subset)
82
+
83
+ stickwidth = 4
84
+
85
+ # 原始18个关节点的连接顺序(和 OpenPose 的 COCO 模型一致)
86
+ limbSeq = [
87
+ [2, 3],
88
+ [2, 6],
89
+ [3, 4],
90
+ [4, 5],
91
+ [6, 7],
92
+ [7, 8],
93
+ [2, 9],
94
+ [9, 10],
95
+ [10, 11],
96
+ [2, 12],
97
+ [12, 13],
98
+ [13, 14],
99
+ [2, 1],
100
+ [1, 15],
101
+ [15, 17],
102
+ [1, 16],
103
+ [16, 18],
104
+ [3, 17],
105
+ [6, 18],
106
+ ]
107
+
108
+ # 添加脚部连接线:10->18, 10->19, 10->20;13->21, 13->22, 13->23
109
+ foot_limbSeq = [
110
+ [14, 19],
111
+ [14, 20],
112
+ [14, 21],
113
+ [11, 22],
114
+ [11, 23],
115
+ [11, 24],
116
+ ]
117
+
118
+ # 生成颜色(原始18条颜色 + 6条新颜色)
119
+ colors = [
120
+ [255, 0, 0],
121
+ [255, 85, 0],
122
+ [255, 170, 0],
123
+ [255, 255, 0],
124
+ [170, 255, 0],
125
+ [85, 255, 0],
126
+ [0, 255, 0],
127
+ [0, 255, 85],
128
+ [0, 255, 170],
129
+ [0, 255, 255],
130
+ [0, 170, 255],
131
+ [0, 85, 255],
132
+ [0, 0, 255],
133
+ [85, 0, 255],
134
+ [170, 0, 255],
135
+ [255, 0, 255],
136
+ [255, 0, 170],
137
+ [255, 0, 85],
138
+ ]
139
+
140
+ colors_feet = [
141
+ [100, 0, 215], [80, 0, 235], [60, 0, 255],
142
+ [0, 235, 150], [0, 215, 170], [0, 195, 190],
143
+ ]
144
+
145
+ colors = colors + colors_feet
146
+
147
+ for i in range(17):
148
+ for n in range(len(subset)):
149
+ index = subset[n][np.array(limbSeq[i]) - 1]
150
+ if -1 in index:
151
+ continue
152
+ Y = candidate[index.astype(int), 0] * float(W)
153
+ X = candidate[index.astype(int), 1] * float(H)
154
+ mX = np.mean(X)
155
+ mY = np.mean(Y)
156
+ length = ((X[0] - X[1]) ** 2 + (Y[0] - Y[1]) ** 2) ** 0.5
157
+ angle = math.degrees(math.atan2(X[0] - X[1], Y[0] - Y[1]))
158
+ polygon = cv2.ellipse2Poly(
159
+ (int(mY), int(mX)), (int(length / 2), stickwidth), int(angle), 0, 360, 1
160
+ )
161
+ cv2.fillConvexPoly(canvas, polygon, colors[i])
162
+
163
+ for i in range(6):
164
+ for n in range(len(subset)):
165
+ index = subset[n][np.array(foot_limbSeq[i]) - 1]
166
+ if -1 in index:
167
+ continue
168
+ Y = candidate[index.astype(int), 0] * float(W)
169
+ X = candidate[index.astype(int), 1] * float(H)
170
+ mX = np.mean(X)
171
+ mY = np.mean(Y)
172
+ length = ((X[0] - X[1]) ** 2 + (Y[0] - Y[1]) ** 2) ** 0.5
173
+ angle = math.degrees(math.atan2(X[0] - X[1], Y[0] - Y[1]))
174
+ polygon = cv2.ellipse2Poly(
175
+ (int(mY), int(mX)), (int(length / 2), stickwidth), int(angle), 0, 360, 1
176
+ )
177
+ cv2.fillConvexPoly(canvas, polygon, colors_feet[i])
178
+
179
+
180
+ canvas = (canvas * 0.6).astype(np.uint8)
181
+
182
+ # 画关键点
183
+ for i in range(24):
184
+ for n in range(len(subset)):
185
+ index = int(subset[n][i])
186
+ if index == -1:
187
+ continue
188
+ x, y = candidate[index][0:2]
189
+ x = int(x * W)
190
+ y = int(y * H)
191
+ cv2.circle(canvas, (int(x), int(y)), 4, colors[i], thickness=-1)
192
+ return canvas
193
+
194
+
195
+ def draw_bodypose_augmentation(canvas, candidate, subset, drop_aug=True, shift_aug=False, all_cheek_aug=False):
196
+ H, W, C = canvas.shape
197
+ candidate = np.array(candidate)
198
+ subset = np.array(subset)
199
+
200
+ stickwidth = 4
201
+
202
+ limbSeq = [
203
+ [2, 3], # 1->2 左肩 0
204
+ [2, 6], # 1->5 右肩 1
205
+ [3, 4], # 2->3 左臂 2
206
+ [4, 5], # 3->4 左肘 3
207
+ [6, 7], # 5->6 右臂 4
208
+ [7, 8], # 6->7 右肘 5
209
+ [2, 9], # 6
210
+ [9, 10], # 7
211
+ [10, 11], # 8
212
+ [2, 12], # 9
213
+ [12, 13], # 10
214
+ [13, 14], # 11
215
+ [2, 1], # 12
216
+ [1, 15], # 13 cheek
217
+ [15, 17], # 14 cheek
218
+ [1, 16], # 15 cheek
219
+ [16, 18], # 16 cheek
220
+ [3, 17],
221
+ [6, 18],
222
+ ]
223
+
224
+ colors = [
225
+ [255, 0, 0],
226
+ [255, 85, 0],
227
+ [255, 170, 0],
228
+ [255, 255, 0],
229
+ [170, 255, 0],
230
+ [85, 255, 0],
231
+ [0, 255, 0],
232
+ [0, 255, 85],
233
+ [0, 255, 170],
234
+ [0, 255, 255],
235
+ [0, 170, 255],
236
+ [0, 85, 255],
237
+ [0, 0, 255],
238
+ [85, 0, 255],
239
+ [170, 0, 255],
240
+ [255, 0, 255],
241
+ [255, 0, 170],
242
+ [255, 0, 85],
243
+ ]
244
+
245
+ # 随机选0-2根骨骼进行丢弃
246
+ if drop_aug:
247
+ arr_drop = list(range(17))
248
+ k_drop = random.choices([0, 1, 2], weights=[0.5, 0.3, 0.2])[0]
249
+ drop_indices = random.sample(arr_drop, k_drop)
250
+ else:
251
+ drop_indices = []
252
+ if shift_aug:
253
+ shift_indices = random.sample(list(range(17)), 2)
254
+ else:
255
+ shift_indices = []
256
+ if all_cheek_aug:
257
+ drop_indices = list(range(13)) # 0-12对应的骨骼都扔掉
258
+
259
+ for i in range(17):
260
+ for n in range(len(subset)):
261
+ index = subset[n][np.array(limbSeq[i]) - 1]
262
+ if -1 in index:
263
+ continue
264
+ Y = candidate[index.astype(int), 0] * float(W)
265
+ X = candidate[index.astype(int), 1] * float(H)
266
+
267
+ if i in drop_indices:
268
+ continue
269
+
270
+ mX = np.mean(X) # 计算两个关节点之间的中点
271
+ mY = np.mean(Y)
272
+ length = ((X[0] - X[1]) ** 2 + (Y[0] - Y[1]) ** 2) ** 0.5
273
+ if i in shift_indices:
274
+ mX = mX + random.uniform(-length/4, length/4)
275
+ mY = mY + random.uniform(-length/4, length/4)
276
+ angle = math.degrees(math.atan2(X[0] - X[1], Y[0] - Y[1]))
277
+ polygon = cv2.ellipse2Poly(
278
+ (int(mY), int(mX)), (int(length / 2), stickwidth), int(angle), 0, 360, 1
279
+ )
280
+ cv2.fillConvexPoly(canvas, polygon, colors[i])
281
+
282
+ canvas = (canvas * 0.6).astype(np.uint8)
283
+
284
+ for i in range(18):
285
+ if all_cheek_aug:
286
+ if not i in [0, 14, 15, 16, 17]:
287
+ continue
288
+ for n in range(len(subset)):
289
+ index = int(subset[n][i])
290
+ if index == -1:
291
+ continue
292
+ x, y = candidate[index][0:2]
293
+ x = int(x * W)
294
+ y = int(y * H)
295
+ cv2.circle(canvas, (int(x), int(y)), 4, colors[i], thickness=-1)
296
+
297
+ return canvas
298
+
299
+ def draw_bodypose(canvas, candidate, subset):
300
+ H, W, C = canvas.shape
301
+ candidate = np.array(candidate)
302
+ subset = np.array(subset)
303
+
304
+ stickwidth = 4
305
+
306
+ limbSeq = [
307
+ [2, 3],
308
+ [2, 6],
309
+ [3, 4],
310
+ [4, 5],
311
+ [6, 7],
312
+ [7, 8],
313
+ [2, 9],
314
+ [9, 10],
315
+ [10, 11],
316
+ [2, 12],
317
+ [12, 13],
318
+ [13, 14],
319
+ [2, 1],
320
+ [1, 15],
321
+ [15, 17],
322
+ [1, 16],
323
+ [16, 18],
324
+ [3, 17],
325
+ [6, 18],
326
+ ]
327
+
328
+ colors = [
329
+ [255, 0, 0],
330
+ [255, 85, 0],
331
+ [255, 170, 0],
332
+ [255, 255, 0],
333
+ [170, 255, 0],
334
+ [85, 255, 0],
335
+ [0, 255, 0],
336
+ [0, 255, 85],
337
+ [0, 255, 170],
338
+ [0, 255, 255],
339
+ [0, 170, 255],
340
+ [0, 85, 255],
341
+ [0, 0, 255],
342
+ [85, 0, 255],
343
+ [170, 0, 255],
344
+ [255, 0, 255],
345
+ [255, 0, 170],
346
+ [255, 0, 85],
347
+ ]
348
+
349
+ for i in range(17):
350
+ for n in range(len(subset)):
351
+ index = subset[n][np.array(limbSeq[i]) - 1]
352
+ if -1 in index:
353
+ continue
354
+ Y = candidate[index.astype(int), 0] * float(W)
355
+ X = candidate[index.astype(int), 1] * float(H)
356
+ mX = np.mean(X)
357
+ mY = np.mean(Y)
358
+ length = ((X[0] - X[1]) ** 2 + (Y[0] - Y[1]) ** 2) ** 0.5
359
+ angle = math.degrees(math.atan2(X[0] - X[1], Y[0] - Y[1]))
360
+ polygon = cv2.ellipse2Poly(
361
+ (int(mY), int(mX)), (int(length / 2), stickwidth), int(angle), 0, 360, 1
362
+ )
363
+ cv2.fillConvexPoly(canvas, polygon, colors[i])
364
+
365
+ canvas = (canvas * 0.6).astype(np.uint8)
366
+
367
+ for i in range(18):
368
+ for n in range(len(subset)):
369
+ index = int(subset[n][i])
370
+ if index == -1:
371
+ continue
372
+ x, y = candidate[index][0:2]
373
+ x = int(x * W)
374
+ y = int(y * H)
375
+ cv2.circle(canvas, (int(x), int(y)), 4, colors[i], thickness=-1)
376
+
377
+ return canvas
378
+
379
+ def draw_handpose_lr(canvas, all_hand_peaks):
380
+ H, W, C = canvas.shape
381
+
382
+ # 连接顺序:21个关键点的骨架连线
383
+ edges = [
384
+ [0, 1], [1, 2], [2, 3], [3, 4],
385
+ [0, 5], [5, 6], [6, 7], [7, 8],
386
+ [0, 9], [9, 10], [10, 11], [11, 12],
387
+ [0, 13], [13, 14], [14, 15], [15, 16],
388
+ [0, 17], [17, 18], [18, 19], [19, 20],
389
+ ]
390
+
391
+ all_num_hands = len(all_hand_peaks)
392
+ for peaks_idx, peaks in enumerate(all_hand_peaks):
393
+ left_or_right = not (peaks_idx >= all_num_hands / 2)
394
+ base_hue = 0 if left_or_right == 0 else 0.3
395
+ peaks = np.array(peaks)
396
+
397
+ for ie, e in enumerate(edges):
398
+ x1, y1 = peaks[e[0]]
399
+ x2, y2 = peaks[e[1]]
400
+ x1 = int(x1 * W)
401
+ y1 = int(y1 * H)
402
+ x2 = int(x2 * W)
403
+ y2 = int(y2 * H)
404
+ if x1 > eps and y1 > eps and x2 > eps and y2 > eps:
405
+ if left_or_right == 0:
406
+ hsv_color = [ (base_hue + ie / float(len(edges)) * 0.8), 0.9, 0.9 ]
407
+ else:
408
+ hsv_color = [ (base_hue + ie / float(len(edges)) * 0.8), 0.8, 1 ]
409
+ rgb_color = matplotlib.colors.hsv_to_rgb(hsv_color) * 255
410
+ cv2.line(
411
+ canvas,
412
+ (x1, y1),
413
+ (x2, y2),
414
+ rgb_color,
415
+ thickness=2,
416
+ )
417
+
418
+ for i, keypoint in enumerate(peaks):
419
+ x, y = keypoint
420
+ x = int(x * W)
421
+ y = int(y * H)
422
+ if x > eps and y > eps:
423
+ # 关键点也用淡色标注(左手蓝、右手红)
424
+ point_color = (245, 100, 100) if left_or_right == 0 else (100, 100, 255)
425
+ cv2.circle(canvas, (x, y), 4, point_color, thickness=-1)
426
+
427
+ return canvas
428
+
429
+ def draw_handpose(canvas, all_hand_peaks):
430
+ H, W, C = canvas.shape
431
+ stickwidth_thin = min(max(int(min(H, W) / 300), 1), 2)
432
+
433
+ edges = [
434
+ [0, 1],
435
+ [1, 2],
436
+ [2, 3],
437
+ [3, 4],
438
+ [0, 5],
439
+ [5, 6],
440
+ [6, 7],
441
+ [7, 8],
442
+ [0, 9],
443
+ [9, 10],
444
+ [10, 11],
445
+ [11, 12],
446
+ [0, 13],
447
+ [13, 14],
448
+ [14, 15],
449
+ [15, 16],
450
+ [0, 17],
451
+ [17, 18],
452
+ [18, 19],
453
+ [19, 20],
454
+ ]
455
+
456
+ for peaks in all_hand_peaks:
457
+ peaks = np.array(peaks)
458
+
459
+ for ie, e in enumerate(edges):
460
+ x1, y1 = peaks[e[0]]
461
+ x2, y2 = peaks[e[1]]
462
+ x1 = int(x1 * W)
463
+ y1 = int(y1 * H)
464
+ x2 = int(x2 * W)
465
+ y2 = int(y2 * H)
466
+ if x1 > eps and y1 > eps and x2 > eps and y2 > eps:
467
+ cv2.line(
468
+ canvas,
469
+ (x1, y1),
470
+ (x2, y2),
471
+ matplotlib.colors.hsv_to_rgb([ie / float(len(edges)), 1.0, 1.0])
472
+ * 255,
473
+ thickness=stickwidth_thin,
474
+ )
475
+
476
+ for i, keyponit in enumerate(peaks):
477
+ x, y = keyponit
478
+ x = int(x * W)
479
+ y = int(y * H)
480
+ if x > eps and y > eps:
481
+ cv2.circle(canvas, (x, y), stickwidth_thin, (0, 0, 255), thickness=-1)
482
+ return canvas
483
+
484
+
485
+ def draw_facepose(canvas, all_lmks, optimized_face=True):
486
+ H, W, C = canvas.shape
487
+ stickwidth = min(max(int(min(H, W) / 200), 1), 3)
488
+ stickwidth_thin = min(max(int(min(H, W) / 300), 1), 2)
489
+
490
+ for lmks in all_lmks:
491
+ lmks = np.array(lmks)
492
+ for lmk_idx, lmk in enumerate(lmks):
493
+ x, y = lmk
494
+ x = int(x * W)
495
+ y = int(y * H)
496
+ if x > eps and y > eps:
497
+ if optimized_face:
498
+ if lmk_idx in list(range(17, 27)) + list(range(36, 70)):
499
+ cv2.circle(canvas, (x, y), stickwidth_thin, (255, 255, 255), thickness=-1)
500
+ else:
501
+ cv2.circle(canvas, (x, y), stickwidth, (255, 255, 255), thickness=-1)
502
+ return canvas
503
+
504
+
505
+
506
+
507
+
508
+ # detect hand according to body pose keypoints
509
+ # please refer to https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/src/openpose/hand/handDetector.cpp
510
+ def handDetect(candidate, subset, oriImg):
511
+ # right hand: wrist 4, elbow 3, shoulder 2
512
+ # left hand: wrist 7, elbow 6, shoulder 5
513
+ ratioWristElbow = 0.33
514
+ detect_result = []
515
+ image_height, image_width = oriImg.shape[0:2]
516
+ for person in subset.astype(int):
517
+ # if any of three not detected
518
+ has_left = np.sum(person[[5, 6, 7]] == -1) == 0
519
+ has_right = np.sum(person[[2, 3, 4]] == -1) == 0
520
+ if not (has_left or has_right):
521
+ continue
522
+ hands = []
523
+ # left hand
524
+ if has_left:
525
+ left_shoulder_index, left_elbow_index, left_wrist_index = person[[5, 6, 7]]
526
+ x1, y1 = candidate[left_shoulder_index][:2]
527
+ x2, y2 = candidate[left_elbow_index][:2]
528
+ x3, y3 = candidate[left_wrist_index][:2]
529
+ hands.append([x1, y1, x2, y2, x3, y3, True])
530
+ # right hand
531
+ if has_right:
532
+ right_shoulder_index, right_elbow_index, right_wrist_index = person[
533
+ [2, 3, 4]
534
+ ]
535
+ x1, y1 = candidate[right_shoulder_index][:2]
536
+ x2, y2 = candidate[right_elbow_index][:2]
537
+ x3, y3 = candidate[right_wrist_index][:2]
538
+ hands.append([x1, y1, x2, y2, x3, y3, False])
539
+
540
+ for x1, y1, x2, y2, x3, y3, is_left in hands:
541
+ # pos_hand = pos_wrist + ratio * (pos_wrist - pos_elbox) = (1 + ratio) * pos_wrist - ratio * pos_elbox
542
+ # handRectangle.x = posePtr[wrist*3] + ratioWristElbow * (posePtr[wrist*3] - posePtr[elbow*3]);
543
+ # handRectangle.y = posePtr[wrist*3+1] + ratioWristElbow * (posePtr[wrist*3+1] - posePtr[elbow*3+1]);
544
+ # const auto distanceWristElbow = getDistance(poseKeypoints, person, wrist, elbow);
545
+ # const auto distanceElbowShoulder = getDistance(poseKeypoints, person, elbow, shoulder);
546
+ # handRectangle.width = 1.5f * fastMax(distanceWristElbow, 0.9f * distanceElbowShoulder);
547
+ x = x3 + ratioWristElbow * (x3 - x2)
548
+ y = y3 + ratioWristElbow * (y3 - y2)
549
+ distanceWristElbow = math.sqrt((x3 - x2) ** 2 + (y3 - y2) ** 2)
550
+ distanceElbowShoulder = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
551
+ width = 1.5 * max(distanceWristElbow, 0.9 * distanceElbowShoulder)
552
+ # x-y refers to the center --> offset to topLeft point
553
+ # handRectangle.x -= handRectangle.width / 2.f;
554
+ # handRectangle.y -= handRectangle.height / 2.f;
555
+ x -= width / 2
556
+ y -= width / 2 # width = height
557
+ # overflow the image
558
+ if x < 0:
559
+ x = 0
560
+ if y < 0:
561
+ y = 0
562
+ width1 = width
563
+ width2 = width
564
+ if x + width > image_width:
565
+ width1 = image_width - x
566
+ if y + width > image_height:
567
+ width2 = image_height - y
568
+ width = min(width1, width2)
569
+ # the max hand box value is 20 pixels
570
+ if width >= 20:
571
+ detect_result.append([int(x), int(y), int(width), is_left])
572
+
573
+ """
574
+ return value: [[x, y, w, True if left hand else False]].
575
+ width=height since the network require squared input.
576
+ x, y is the coordinate of top left
577
+ """
578
+ return detect_result
579
+
580
+
581
+ # Written by Lvmin
582
+ def faceDetect(candidate, subset, oriImg):
583
+ # left right eye ear 14 15 16 17
584
+ detect_result = []
585
+ image_height, image_width = oriImg.shape[0:2]
586
+ for person in subset.astype(int):
587
+ has_head = person[0] > -1
588
+ if not has_head:
589
+ continue
590
+
591
+ has_left_eye = person[14] > -1
592
+ has_right_eye = person[15] > -1
593
+ has_left_ear = person[16] > -1
594
+ has_right_ear = person[17] > -1
595
+
596
+ if not (has_left_eye or has_right_eye or has_left_ear or has_right_ear):
597
+ continue
598
+
599
+ head, left_eye, right_eye, left_ear, right_ear = person[[0, 14, 15, 16, 17]]
600
+
601
+ width = 0.0
602
+ x0, y0 = candidate[head][:2]
603
+
604
+ if has_left_eye:
605
+ x1, y1 = candidate[left_eye][:2]
606
+ d = max(abs(x0 - x1), abs(y0 - y1))
607
+ width = max(width, d * 3.0)
608
+
609
+ if has_right_eye:
610
+ x1, y1 = candidate[right_eye][:2]
611
+ d = max(abs(x0 - x1), abs(y0 - y1))
612
+ width = max(width, d * 3.0)
613
+
614
+ if has_left_ear:
615
+ x1, y1 = candidate[left_ear][:2]
616
+ d = max(abs(x0 - x1), abs(y0 - y1))
617
+ width = max(width, d * 1.5)
618
+
619
+ if has_right_ear:
620
+ x1, y1 = candidate[right_ear][:2]
621
+ d = max(abs(x0 - x1), abs(y0 - y1))
622
+ width = max(width, d * 1.5)
623
+
624
+ x, y = x0, y0
625
+
626
+ x -= width
627
+ y -= width
628
+
629
+ if x < 0:
630
+ x = 0
631
+
632
+ if y < 0:
633
+ y = 0
634
+
635
+ width1 = width * 2
636
+ width2 = width * 2
637
+
638
+ if x + width > image_width:
639
+ width1 = image_width - x
640
+
641
+ if y + width > image_height:
642
+ width2 = image_height - y
643
+
644
+ width = min(width1, width2)
645
+
646
+ if width >= 20:
647
+ detect_result.append([int(x), int(y), int(width)])
648
+
649
+ return detect_result
650
+
651
+
652
+ # get max index of 2d array
653
+ def npmax(array):
654
+ arrayindex = array.argmax(1)
655
+ arrayvalue = array.max(1)
656
+ i = arrayvalue.argmax()
657
+ j = arrayindex[i]
658
+ return i, j
SCAIL-Pose/pose_draw/reshape_utils.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import random
3
+
4
+ def pose_offset(offset_x, offset_y, pose, vitposes):
5
+ for i in range(len(pose["bodies"]["candidate"])):
6
+ bodies = pose["bodies"]
7
+ faces = pose["faces"][i]
8
+ right_hand = pose["hands"][2 * i]
9
+ left_hand = pose["hands"][2 * i + 1]
10
+ candidate = bodies["candidate"][i]
11
+ subset = bodies["subset"][i]
12
+
13
+ for face_point in faces:
14
+ if face_point[0] == -1 and face_point[1] == -1:
15
+ continue
16
+ face_point[0] = face_point[0] + offset_x
17
+ face_point[1] = face_point[1] + offset_y
18
+ for left_hand_point in left_hand:
19
+ if left_hand_point[0] == -1 and left_hand_point[1] == -1:
20
+ continue
21
+ left_hand_point[0] = left_hand_point[0] + offset_x
22
+ left_hand_point[1] = left_hand_point[1] + offset_y
23
+ for right_hand_point in right_hand:
24
+ if right_hand_point[0] == -1 and right_hand_point[1] == -1:
25
+ continue
26
+ right_hand_point[0] = right_hand_point[0] + offset_x
27
+ right_hand_point[1] = right_hand_point[1] + offset_y
28
+
29
+ assert len(candidate) == len(subset), f"candidate, length: {len(candidate)} and subset, length: {len(subset)} must have the same length"
30
+ for idx, body_point in enumerate(candidate):
31
+ if subset[idx] == -1:
32
+ continue
33
+ if body_point[0] == -1 and body_point[1] == -1:
34
+ continue
35
+ body_point[0] = body_point[0] + offset_x
36
+ body_point[1] = body_point[1] + offset_y
37
+
38
+ if vitposes is not None:
39
+ for vitpose in vitposes:
40
+ vitpose['keypoints_body'] = vitpose['keypoints_body'] + np.array([offset_x, offset_y, 0])
41
+ vitpose['keypoints_left_hand'] = vitpose['keypoints_left_hand'] + np.array([offset_x, offset_y, 0])
42
+ vitpose['keypoints_right_hand'] = vitpose['keypoints_right_hand'] + np.array([offset_x, offset_y, 0])
43
+
44
+
45
+ def pose_whole_scale(scale_x, scale_y, pose, vitposes): # 目前仅能用于单人
46
+ # 获取最左端的x 最右端的x 最上端的y 最下端的y
47
+ min_x = float('inf')
48
+ max_x = float('-inf')
49
+ min_y = float('inf')
50
+ max_y = float('-inf')
51
+ if len(pose["bodies"]["candidate"]) > 1:
52
+ return
53
+ for i in range(len(pose["bodies"]["candidate"])):
54
+ bodies = pose["bodies"]
55
+ faces = pose["faces"][i]
56
+ right_hand = pose["hands"][2 * i]
57
+ left_hand = pose["hands"][2 * i + 1]
58
+ candidate = bodies["candidate"][i]
59
+ subset = bodies["subset"][i]
60
+
61
+ for face_point in faces:
62
+ if face_point[0] == -1 and face_point[1] == -1:
63
+ continue
64
+ min_x = min(min_x, face_point[0])
65
+ max_x = max(max_x, face_point[0])
66
+ min_y = min(min_y, face_point[1])
67
+ max_y = max(max_y, face_point[1])
68
+ for left_hand_point in left_hand:
69
+ if left_hand_point[0] == -1 and left_hand_point[1] == -1:
70
+ continue
71
+ min_x = min(min_x, left_hand_point[0])
72
+ max_x = max(max_x, left_hand_point[0])
73
+ min_y = min(min_y, left_hand_point[1])
74
+ max_y = max(max_y, left_hand_point[1])
75
+ for right_hand_point in right_hand:
76
+ if right_hand_point[0] == -1 and right_hand_point[1] == -1:
77
+ continue
78
+ min_x = min(min_x, right_hand_point[0])
79
+ max_x = max(max_x, right_hand_point[0])
80
+ min_y = min(min_y, right_hand_point[1])
81
+ max_y = max(max_y, right_hand_point[1])
82
+ assert len(candidate) == len(subset), "candidate and subset must have the same length"
83
+ for idx, body_point in enumerate(candidate):
84
+ if subset[idx] == -1:
85
+ continue
86
+ if body_point[0] == -1 and body_point[1] == -1:
87
+ continue
88
+ min_x = min(min_x, body_point[0])
89
+ max_x = max(max_x, body_point[0])
90
+ min_y = min(min_y, body_point[1])
91
+ max_y = max(max_y, body_point[1])
92
+
93
+ # 计算缩放比例
94
+ # 根据bbox中心进行dilate, 倍数为x_scale, y_scale
95
+ bbox_center_x = (min_x + max_x) / 2
96
+ bbox_center_y = (min_y + max_y) / 2
97
+ for i in range(len(pose["bodies"]["candidate"])):
98
+ bodies = pose["bodies"]
99
+ candidate = bodies["candidate"][i]
100
+ for body_point in candidate:
101
+ if body_point[0] == -1 and body_point[1] == -1:
102
+ continue
103
+ body_point[0] = body_point[0] + (body_point[0] - bbox_center_x) * scale_x # scale越大,越远离中心,变化越大
104
+ body_point[1] = body_point[1] + (body_point[1] - bbox_center_y) * scale_y
105
+ for face_point in faces:
106
+ if face_point[0] == -1 and face_point[1] == -1:
107
+ continue
108
+ face_point[0] = face_point[0] + (face_point[0] - bbox_center_x) * scale_x
109
+ face_point[1] = face_point[1] + (face_point[1] - bbox_center_y) * scale_y
110
+ for left_hand_point in left_hand:
111
+ if left_hand_point[0] == -1 and left_hand_point[1] == -1:
112
+ continue
113
+ left_hand_point[0] = left_hand_point[0] + (left_hand_point[0] - bbox_center_x) * scale_x
114
+ left_hand_point[1] = left_hand_point[1] + (left_hand_point[1] - bbox_center_y) * scale_y
115
+ for right_hand_point in right_hand:
116
+ if right_hand_point[0] == -1 and right_hand_point[1] == -1:
117
+ continue
118
+ right_hand_point[0] = right_hand_point[0] + (right_hand_point[0] - bbox_center_x) * scale_x
119
+ right_hand_point[1] = right_hand_point[1] + (right_hand_point[1] - bbox_center_y) * scale_y
120
+
121
+ if vitposes is not None:
122
+ min_x = float('inf')
123
+ max_x = float('-inf')
124
+ min_y = float('inf')
125
+ max_y = float('-inf')
126
+ if len(vitposes) > 1:
127
+ return
128
+ for vitpose in vitposes:
129
+ for body_point in vitpose['keypoints_body']:
130
+ if body_point[2] < 0.4:
131
+ continue
132
+ min_x = min(min_x, body_point[0])
133
+ max_x = max(max_x, body_point[0])
134
+ min_y = min(min_y, body_point[1])
135
+ max_y = max(max_y, body_point[1])
136
+ for left_hand_point in vitpose['keypoints_left_hand']:
137
+ if left_hand_point[2] < 0.4:
138
+ continue
139
+ min_x = min(min_x, left_hand_point[0])
140
+ max_x = max(max_x, left_hand_point[0])
141
+ min_y = min(min_y, left_hand_point[1])
142
+ max_y = max(max_y, left_hand_point[1])
143
+ for right_hand_point in vitpose['keypoints_right_hand']:
144
+ if right_hand_point[2] < 0.4:
145
+ continue
146
+ min_x = min(min_x, right_hand_point[0])
147
+ max_x = max(max_x, right_hand_point[0])
148
+ min_y = min(min_y, right_hand_point[1])
149
+ max_y = max(max_y, right_hand_point[1])
150
+ bbox_center_x = (min_x + max_x) / 2
151
+ bbox_center_y = (min_y + max_y) / 2
152
+ for body_point in vitpose['keypoints_body']:
153
+ body_point[0] = body_point[0] + (body_point[0] - bbox_center_x) * scale_x
154
+ body_point[1] = body_point[1] + (body_point[1] - bbox_center_y) * scale_y
155
+ for left_hand_point in vitpose['keypoints_left_hand']:
156
+ left_hand_point[0] = left_hand_point[0] + (left_hand_point[0] - bbox_center_x) * scale_x
157
+ left_hand_point[1] = left_hand_point[1] + (left_hand_point[1] - bbox_center_y) * scale_y
158
+ for right_hand_point in vitpose['keypoints_right_hand']:
159
+ right_hand_point[0] = right_hand_point[0] + (right_hand_point[0] - bbox_center_x) * scale_x
160
+ right_hand_point[1] = right_hand_point[1] + (right_hand_point[1] - bbox_center_y) * scale_y
161
+
162
+
163
+
164
+
165
+ # 如果做多人增强,需要修改逻辑,每个人都有随机性,现在都是做单人增强,增强方法一致
166
+ def pose_reshape(alpha, pose, vitposes,
167
+ anchor_point, anchor_part, end_point, end_part,
168
+ affected_body_points, affected_faces_points, affected_left_hands_points, affected_right_hands_points):
169
+ """
170
+ 直接修改原始pose中的某些点的位置,通过根据一个不动点和端点之间的向量偏移被影响点的位置。
171
+ 一次只能有一个anchor_point, 一个end_point,多个affected_points
172
+ alpha: 变化比例
173
+ anchor_point: 起始点
174
+ anchor_part: 起始点所属的身体部分,0代表body candidate, 1代表face, 2代表hand
175
+ end_point: 中止点
176
+ end_part: 中止点所属的身体部分,0代表body candidate, 1代表face, 2代表hand
177
+ """
178
+ for i in range(len(pose["bodies"]["candidate"])):
179
+ bodies = pose["bodies"]
180
+ # faces = pose["faces"][i:i+1]
181
+ faces = pose["faces"][i]
182
+ # hands = pose["hands"][2*i:2*i+2]
183
+ right_hand = pose["hands"][2 * i]
184
+ left_hand = pose["hands"][2 * i + 1]
185
+ candidate = bodies["candidate"][i]
186
+ # subset = bodies["subset"][i:i+1]
187
+ subset = bodies["subset"][i]
188
+ assert anchor_part == 0, "anchor part must belong to the body"
189
+ anchor_x, anchor_y = candidate[anchor_point]
190
+ if subset[anchor_point] == -1:
191
+ continue
192
+
193
+ if end_part == 0:
194
+ end_x, end_y = candidate[end_point]
195
+ if subset[end_point] == -1:
196
+ continue
197
+ elif end_part == 1:
198
+ end_x, end_y = faces[end_point]
199
+ # 不考虑Hands
200
+ if end_x == -1 and end_y == -1:
201
+ continue
202
+
203
+ vector_x = end_x - anchor_x
204
+ vector_y = end_y - anchor_y
205
+ offset_x = vector_x * alpha
206
+ offset_y = vector_y * alpha
207
+
208
+ for affected_body_point in affected_body_points:
209
+ if subset[affected_body_point] == -1:
210
+ continue
211
+ affected_x, affected_y = candidate[affected_body_point]
212
+ candidate[affected_body_point] = [affected_x + offset_x, affected_y + offset_y]
213
+ for affected_faces_point in affected_faces_points:
214
+ affected_x, affected_y = faces[affected_faces_point]
215
+ if affected_x == -1 and affected_y == -1:
216
+ continue
217
+ faces[affected_faces_point] = [affected_x + offset_x, affected_y + offset_y]
218
+ for affected_hands_point in affected_left_hands_points:
219
+ affected_x, affected_y = left_hand[affected_hands_point]
220
+ if affected_x == -1 and affected_y == -1:
221
+ continue
222
+ left_hand[affected_hands_point] = [affected_x + offset_x, affected_y + offset_y]
223
+ for affected_hands_point in affected_right_hands_points:
224
+ affected_x, affected_y = right_hand[affected_hands_point]
225
+ if affected_x == -1 and affected_y == -1:
226
+ continue
227
+ right_hand[affected_hands_point] = [affected_x + offset_x, affected_y + offset_y]
228
+
229
+ if vitposes is not None:
230
+ vit_visible_threshold = 0.5
231
+ for vitpose in vitposes:
232
+ assert anchor_part == 0, "anchor part must belong to the body"
233
+ anchor_x, anchor_y, _ = vitpose['keypoints_body'][anchor_point]
234
+ if vitpose['keypoints_body'][anchor_point][2] < vit_visible_threshold:
235
+ continue
236
+ if end_part == 0:
237
+ end_x, end_y, _ = vitpose['keypoints_body'][end_point]
238
+ if vitpose['keypoints_body'][end_point][2] < vit_visible_threshold:
239
+ continue
240
+ # 不考虑hands
241
+ elif end_part == 1:
242
+ continue # ViTPose的脸不动
243
+
244
+ vector_x = end_x - anchor_x
245
+ vector_y = end_y - anchor_y
246
+ offset_x = vector_x * alpha
247
+ offset_y = vector_y * alpha
248
+
249
+ for affected_body_point in affected_body_points:
250
+ if vitpose['keypoints_body'][affected_body_point][2] < vit_visible_threshold:
251
+ continue
252
+ vitpose['keypoints_body'][affected_body_point][0] = vitpose['keypoints_body'][affected_body_point][0] + offset_x
253
+ vitpose['keypoints_body'][affected_body_point][1] = vitpose['keypoints_body'][affected_body_point][1] + offset_y
254
+ # 左右标记相反,需要互换
255
+ for affected_left_hand_point in affected_left_hands_points:
256
+ if vitpose['keypoints_right_hand'][affected_left_hand_point][2] < vit_visible_threshold:
257
+ continue
258
+ vitpose['keypoints_right_hand'][affected_left_hand_point][0] = vitpose['keypoints_right_hand'][affected_left_hand_point][0] + offset_x
259
+ vitpose['keypoints_right_hand'][affected_left_hand_point][1] = vitpose['keypoints_right_hand'][affected_left_hand_point][1] + offset_y
260
+ for affected_right_hand_point in affected_right_hands_points:
261
+ if vitpose['keypoints_left_hand'][affected_right_hand_point][2] < vit_visible_threshold:
262
+ continue
263
+ vitpose['keypoints_left_hand'][affected_right_hand_point][0] = vitpose['keypoints_left_hand'][affected_right_hand_point][0] + offset_x
264
+ vitpose['keypoints_left_hand'][affected_right_hand_point][1] = vitpose['keypoints_left_hand'][affected_right_hand_point][1] + offset_y
265
+
266
+
267
+
268
+ # reshapePool只负责形变,骨骼偏移、丢弃等得从draw层来做
269
+ class reshapePool:
270
+ def __init__(self, alpha): # 对每个视频只初始化一次
271
+ self.faces_indices = np.arange(0, 68)
272
+ self.left_hands_indices = np.arange(0, 21)
273
+ self.right_hands_indices = np.arange(0, 21)
274
+ self.alpha = alpha # 0.1
275
+ self.offset_x = random.uniform(-1/16, 1/16)
276
+ self.offset_y = random.uniform(-1/16, 1/16)
277
+ self.scale_x = random.uniform(-alpha/2, alpha/2)
278
+ self.scale_y = random.uniform(-alpha/2, alpha/2)
279
+
280
+ self.body_reshape_methods = [
281
+ self.extend_body,
282
+ self.extend_arm,
283
+ self.extend_leg,
284
+ self.shrink_body,
285
+ self.shrink_arm,
286
+ self.shrink_leg,
287
+ ]
288
+ self.scale_reshape_methods = [
289
+ self.offset_wholebody,
290
+ # self.scale_wholebody,
291
+ self.dilate_face,
292
+ self.shrink_face,
293
+ ]
294
+ self.selected_methods = random.sample(self.body_reshape_methods, 2) + random.sample(self.scale_reshape_methods, random.choice([0, 1]))
295
+
296
+ def apply_random_reshapes(self, pose, vitposes=None):
297
+ # Apply the two selected reshape methods
298
+ for method in self.selected_methods:
299
+ method(pose, vitposes)
300
+
301
+ def offset_wholebody(self, pose, vitposes):
302
+ pose_offset(self.offset_x, self.offset_y, pose, vitposes)
303
+
304
+ def scale_wholebody(self, pose, vitposes):
305
+ pose_whole_scale(self.scale_x, self.scale_y, pose, vitposes)
306
+
307
+
308
+ def extend_body(self, pose, vitposes):
309
+ pose_reshape(self.alpha, pose, vitposes,
310
+ 1, 0, 8, 0,
311
+ [8, 9, 10], [], [], []
312
+ )
313
+ pose_reshape(self.alpha, pose, vitposes,
314
+ 1, 0, 11, 0,
315
+ [11, 12, 13], [], [], []
316
+ )
317
+
318
+ def shrink_body(self, pose, vitposes):
319
+ pose_reshape(-self.alpha, pose, vitposes,
320
+ 1, 0, 8, 0,
321
+ [8, 9, 10], [], [], []
322
+ )
323
+ pose_reshape(-self.alpha, pose, vitposes,
324
+ 1, 0, 11, 0,
325
+ [11, 12, 13], [], [], []
326
+ )
327
+
328
+ def extend_arm(self, pose, vitposes):
329
+ pose_reshape(self.alpha, pose, vitposes,
330
+ 2, 0, 3, 0,
331
+ [3, 4], [], self.left_hands_indices, []
332
+ )
333
+ pose_reshape(self.alpha, pose, vitposes,
334
+ 5, 0, 6, 0,
335
+ [6, 7], [], [], self.right_hands_indices
336
+ )
337
+ pose_reshape(self.alpha, pose, vitposes,
338
+ 3, 0, 4, 0,
339
+ [4], [], self.left_hands_indices, []
340
+ )
341
+ pose_reshape(self.alpha, pose, vitposes,
342
+ 6, 0, 7, 0,
343
+ [7], [], [], self.right_hands_indices
344
+ )
345
+
346
+ def shrink_arm(self, pose, vitposes):
347
+ pose_reshape(-self.alpha, pose, vitposes,
348
+ 2, 0, 3, 0,
349
+ [3, 4], [], self.left_hands_indices, []
350
+ )
351
+ pose_reshape(-self.alpha, pose, vitposes,
352
+ 5, 0, 6, 0,
353
+ [6, 7], [], [], self.right_hands_indices
354
+ )
355
+ pose_reshape(-self.alpha, pose, vitposes,
356
+ 3, 0, 4, 0,
357
+ [4], [], self.left_hands_indices, []
358
+ )
359
+ pose_reshape(-self.alpha, pose, vitposes,
360
+ 6, 0, 7, 0,
361
+ [7], [], [], self.right_hands_indices
362
+ )
363
+
364
+ def extend_leg(self, pose, vitposes):
365
+ pose_reshape(self.alpha, pose, vitposes,
366
+ 8, 0, 9, 0,
367
+ [9, 10], [], [], []
368
+ )
369
+ pose_reshape(self.alpha, pose, vitposes,
370
+ 11, 0, 12, 0,
371
+ [12, 13], [], [], []
372
+ )
373
+ pose_reshape(self.alpha, pose, vitposes,
374
+ 9, 0, 10, 0,
375
+ [10], [], [], []
376
+ )
377
+ pose_reshape(self.alpha, pose, vitposes,
378
+ 12, 0, 13, 0,
379
+ [13], [], [], []
380
+ )
381
+
382
+ def shrink_leg(self, pose, vitposes):
383
+ pose_reshape(-self.alpha, pose, vitposes,
384
+ 8, 0, 9, 0,
385
+ [9, 10], [], [], []
386
+ )
387
+ pose_reshape(-self.alpha, pose, vitposes,
388
+ 11, 0, 12, 0,
389
+ [12, 13], [], [], []
390
+ )
391
+ pose_reshape(-self.alpha, pose, vitposes,
392
+ 9, 0, 10, 0,
393
+ [10], [], [], []
394
+ )
395
+ pose_reshape(-self.alpha, pose, vitposes,
396
+ 12, 0, 13, 0,
397
+ [13], [], [], []
398
+ )
399
+
400
+ def extend_shoulder(self, pose, vitposes):
401
+ pose_reshape(self.alpha, pose, vitposes,
402
+ 1, 0, 2, 0,
403
+ [2, 3, 4], [], self.left_hands_indices, []
404
+ )
405
+ pose_reshape(self.alpha, pose, vitposes,
406
+ 1, 0, 5, 0,
407
+ [5, 6, 7], [], [], self.right_hands_indices
408
+ )
409
+
410
+ def shrink_shoulder(self, pose, vitposes):
411
+ pose_reshape(-self.alpha, pose, vitposes,
412
+ 1, 0, 2, 0,
413
+ [2, 3, 4], [], self.left_hands_indices, []
414
+ )
415
+ pose_reshape(-self.alpha, pose, vitposes,
416
+ 1, 0, 5, 0,
417
+ [5, 6, 7], [], [], self.right_hands_indices
418
+ )
419
+
420
+
421
+ def dilate_face(self, pose, vitposes):
422
+ for i in self.faces_indices:
423
+ pose_reshape(self.alpha, pose, vitposes,
424
+ 0, 0, i, 1,
425
+ [], [i], [], []
426
+ )
427
+ for i in [14, 15, 16, 17]:
428
+ pose_reshape(self.alpha, pose, vitposes,
429
+ 0, 0, i, 0,
430
+ [i], [], [], []
431
+ )
432
+
433
+ def shrink_face(self, pose, vitposes): # 缩小脸会看不清
434
+ for i in self.faces_indices:
435
+ pose_reshape(-self.alpha / 2, pose, vitposes,
436
+ 0, 0, i, 1,
437
+ [], [i], [], []
438
+ )
439
+
440
+ for i in [14, 15, 16, 17]:
441
+ pose_reshape(-self.alpha / 2, pose, vitposes,
442
+ 0, 0, i, 0,
443
+ [i], [], [], []
444
+ )
SCAIL-Pose/render_3d/render_cylinder.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+ import cv2
6
+ from PIL import Image
7
+
8
+
9
+ def render_colored_cylinders(cylinder_specs, focal, princpt, image_size=(1280, 1280), img=None):
10
+ os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
11
+ import pyrender
12
+ import trimesh
13
+ H, W = image_size
14
+ if isinstance(focal, float) or isinstance(focal, int):
15
+ fx, fy = focal, focal
16
+ else:
17
+ fx, fy = focal[0], focal[1]
18
+ cx, cy = princpt
19
+
20
+ # 初始化场景
21
+ scene = pyrender.Scene(bg_color=[0, 0, 0, 0], ambient_light=[0.1, 0.1, 0.1])
22
+
23
+ # 设置相机
24
+ camera = pyrender.IntrinsicsCamera(fx=fx, fy=fy, cx=cx, cy=cy, znear=0.5, zfar=10000)
25
+ pyrender2opencv = np.array([[1.0, 0, 0, 0],
26
+ [0, -1, 0, 0],
27
+ [0, 0, -1, 0],
28
+ [0, 0, 0, 1]])
29
+ cam_pose = pyrender2opencv @ np.eye(4)
30
+ scene.add(camera, pose=cam_pose)
31
+
32
+ # 添加光源
33
+ light = pyrender.DirectionalLight(color=np.ones(3), intensity=3.0)
34
+ scene.add(light, pose=cam_pose)
35
+
36
+ points_to_draw = []
37
+
38
+ for start, end, color in cylinder_specs:
39
+ start = np.array(start)
40
+ end = np.array(end)
41
+ vec = end - start
42
+ height = np.linalg.norm(vec)
43
+ if height == 0:
44
+ continue
45
+
46
+ tm = trimesh.creation.cylinder(radius=12, height=height, sections=16)
47
+
48
+ # 旋转对齐z轴
49
+ z_axis = np.array([0, 0, 1])
50
+ axis = np.cross(z_axis, vec)
51
+ if np.linalg.norm(axis) > 1e-6:
52
+ axis = axis / np.linalg.norm(axis)
53
+ angle = np.arccos(np.dot(z_axis, vec) / height)
54
+ rot = trimesh.transformations.rotation_matrix(angle, axis)
55
+ tm.apply_transform(rot)
56
+
57
+ tm.apply_translation(start + vec / 2)
58
+
59
+ # 材质颜色(支持 RGBA)
60
+ rgba = np.array(color)
61
+ material = pyrender.MetallicRoughnessMaterial(
62
+ metallicFactor=0.1,
63
+ roughnessFactor=0.5,
64
+ baseColorFactor=rgba
65
+ )
66
+
67
+ mesh = pyrender.Mesh.from_trimesh(tm, material=material)
68
+ scene.add(mesh)
69
+
70
+ # 投影点用于可视化,检查投射是否正确
71
+ x1 = fx * (start[0] / start[2]) + cx
72
+ y1 = fy * (start[1] / start[2]) + cy
73
+ x2 = fx * (end[0] / end[2]) + cx
74
+ y2 = fy * (end[1] / end[2]) + cy
75
+ points_to_draw.append((x1, y1))
76
+ points_to_draw.append((x2, y2))
77
+
78
+ # 渲染
79
+ r = pyrender.OffscreenRenderer(viewport_width=W, viewport_height=H, point_size=1.0)
80
+ color, _ = r.render(scene, flags=pyrender.RenderFlags.RGBA)
81
+
82
+ # 后处理
83
+ color = color.astype(np.float32) / 255.0
84
+ # 转 uint8
85
+ final_img = (color * 255).astype(np.uint8)
86
+
87
+ # 画点,检查投射是否正确
88
+ for (x, y) in points_to_draw:
89
+ print(f" debug point: {x}, {y}")
90
+ x_draw = int(x)
91
+ y_draw = int(y)
92
+ cv2.circle(final_img, (x_draw, y_draw), radius=4, color=(0, 255, 0), thickness=-1)
93
+
94
+ return Image.fromarray(final_img)
95
+
96
+
97
+ # test
98
+ if __name__ == "__main__":
99
+ # 构造一个空白背景
100
+ H, W = 480, 640
101
+ img = np.zeros((H, W, 3), dtype=np.uint8) + 255 # 白色背景
102
+
103
+ # 构造几组3D点对和颜色
104
+ cylinder_specs = [
105
+ # 起点 (0,0,100), Y轴方向的红色圆柱终点 Y 调整为 40
106
+ (np.array([0, 20, 120]), np.array([0, 40, 100]), [1.0, 0.0, 0.0, 1.0]), # 红色
107
+
108
+ # 起点 (0,0,100), X轴方向的绿色圆柱终点 X 调整为 60
109
+ (np.array([0, 0, 100]), np.array([60, 40, 100]), [0.0, 1.0, 0.0, 1.0]), # 绿色
110
+
111
+ # Z轴方向的蓝色圆柱长度调整为50 (从100到150)
112
+ (np.array([0, 0, 100]), np.array([0, 0, 150]), [0.0, 0.0, 1.0, 1.0]), # 蓝色
113
+ ]
114
+
115
+ # 简单的相机参数
116
+ fx, fy = 500, 500
117
+ cx, cy = W // 2, H // 2
118
+
119
+ # 调用渲染函数
120
+ img_pil = render_colored_cylinders(
121
+ cylinder_specs=cylinder_specs,
122
+ focal=(fx, fy),
123
+ princpt=(cx, cy),
124
+ image_size=(H, W),
125
+ img=img
126
+ )
127
+
128
+ # 显示或保存结果
129
+ img_pil.save("test_render_cylinder.png")
SCAIL-Pose/render_3d/taichi_cylinder.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import taichi as ti
2
+ import numpy as np
3
+ from PIL import Image
4
+ import random
5
+ import math
6
+ import time
7
+ import imageio
8
+ ti.init(arch=ti.cuda)
9
+
10
+ def flatten_specs(specs_list):
11
+ """把 specs_list 拉平为 numpy 数组 + 索引表"""
12
+ starts, ends, colors = [], [], []
13
+ frame_offset, frame_count = [], []
14
+ offset = 0
15
+ for specs in specs_list:
16
+ frame_offset.append(offset)
17
+ frame_count.append(len(specs))
18
+ for (s, e, c) in specs:
19
+ starts.append(s)
20
+ ends.append(e)
21
+ colors.append(c)
22
+ offset += len(specs)
23
+ return (
24
+ np.array(starts, dtype=np.float32),
25
+ np.array(ends, dtype=np.float32),
26
+ np.array(colors, dtype=np.float32),
27
+ np.array(frame_offset, dtype=np.int32),
28
+ np.array(frame_count, dtype=np.int32),
29
+ )
30
+
31
+ def render_whole(specs_list, H=480, W=640, fx=500, fy=500, cx=240, cy=320, radius=21.5, use_specular=True):
32
+ img = ti.Vector.field(4, dtype=ti.f32, shape=(H, W))
33
+ starts, ends, colors, frame_offset, frame_count = flatten_specs(specs_list)
34
+ total_cyl = len(starts)
35
+ n_frames = len(specs_list)
36
+ z_min = min(starts[:, 2].min(), ends[:, 2].min())
37
+ z_max = max(starts[:, 2].max(), ends[:, 2].max())
38
+
39
+ # ========= 相机内参 =========
40
+ znear = 0.1
41
+ zfar = max(min(z_max, 25000), 10000)
42
+ C = ti.Vector([0.0, 0.0, 0.0]) # 相机中心
43
+ light_dir = ti.Vector([0.0, 0.0, 1.0])
44
+
45
+ c_start = ti.Vector.field(3, dtype=ti.f32, shape=total_cyl)
46
+ c_end = ti.Vector.field(3, dtype=ti.f32, shape=total_cyl)
47
+ c_rgba = ti.Vector.field(4, dtype=ti.f32, shape=total_cyl)
48
+ n_cyl = ti.field(dtype=ti.i32, shape=()) # 实际数量
49
+ f_offset = ti.field(dtype=ti.i32, shape=n_frames)
50
+ f_count = ti.field(dtype=ti.i32, shape=n_frames)
51
+ frame_id = ti.field(dtype=ti.i32, shape=()) # 当前帧号
52
+ z_min_field = ti.field(dtype=ti.f32, shape=())
53
+ z_max_field = ti.field(dtype=ti.f32, shape=())
54
+ use_spec_field = ti.field(dtype=ti.i32, shape=())
55
+
56
+ z_min_field[None] = z_min
57
+ z_max_field[None] = z_max
58
+ use_spec_field[None] = 1 if use_specular else 0
59
+
60
+ # # ====== 拷贝数据一次 ======
61
+ c_start.from_numpy(starts)
62
+ c_end.from_numpy(ends)
63
+ c_rgba.from_numpy(colors)
64
+ f_offset.from_numpy(frame_offset)
65
+ f_count.from_numpy(frame_count)
66
+
67
+ @ti.func
68
+ def sd_cylinder(p, a, b, r):
69
+ pa = p - a
70
+ ba = b - a
71
+ h = ba.norm()
72
+ eps = 1e-8
73
+ res = 0.0
74
+ if h < eps:
75
+ res = pa.norm() - r
76
+ else:
77
+ ba_n = ba / h
78
+ proj = pa.dot(ba_n)
79
+ proj_clamped = min(max(proj, 0.0), h)
80
+ res = (pa - proj_clamped * ba_n).norm() - r
81
+ return res
82
+
83
+ @ti.func
84
+ def scene_sdf(p):
85
+ best_d = 1e6
86
+ best_col = ti.Vector([0.0, 0.0, 0.0, 0.0])
87
+ fid = frame_id[None] # 从 field 里读出来,变成一个普通 int
88
+ off = f_offset[fid]
89
+ cnt = f_count[fid]
90
+ for i in range(cnt): # 只遍历实际数量
91
+ a = c_start[off + i]
92
+ b = c_end[off + i]
93
+ r = radius
94
+ col = c_rgba[off + i]
95
+ d = sd_cylinder(p, a, b, r)
96
+ if d < best_d:
97
+ best_d = d
98
+ best_col = col
99
+ return best_d, best_col
100
+
101
+ @ti.func
102
+ def get_normal(p):
103
+ e = 1e-3
104
+ dx = scene_sdf(p + ti.Vector([e, 0.0, 0.0]))[0] - scene_sdf(p - ti.Vector([e, 0.0, 0.0]))[0]
105
+ dy = scene_sdf(p + ti.Vector([0.0, e, 0.0]))[0] - scene_sdf(p - ti.Vector([0.0, e, 0.0]))[0]
106
+ dz = scene_sdf(p + ti.Vector([0.0, 0.0, e]))[0] - scene_sdf(p - ti.Vector([0.0, 0.0, e]))[0]
107
+ n = ti.Vector([dx, dy, dz])
108
+ return n.normalized()
109
+
110
+ @ti.func
111
+ def pixel_to_ray(xi, yi):
112
+ u = (xi - cx) / fx
113
+ v = (yi - cy) / fy
114
+ dir_cam = ti.Vector([u, v, 1.0]).normalized()
115
+ Rcw = ti.Matrix.identity(ti.f32, 3)
116
+ rd_world = Rcw @ dir_cam
117
+ ro_world = C
118
+ return ro_world, rd_world
119
+
120
+ @ti.kernel
121
+ def render():
122
+ depth_near, depth_far = ti.max(z_min_field[None], 0.1), ti.min(z_max_field[None] + 6000, 20000) # 能渲染出来的点,最大12000
123
+ for y, x in img:
124
+ ro, rd = pixel_to_ray(x, y)
125
+ t = znear
126
+ col_out = ti.Vector([0.0, 0.0, 0.0, 0.0])
127
+ for _ in range(300):
128
+ p = ro + rd * t
129
+ d, col = scene_sdf(p)
130
+ if d < 1e-3:
131
+ # n = get_normal(p)
132
+ # diff = max(n.dot(-light_dir), 0.0)
133
+ # lit = 0.3 + 0.7 * diff
134
+ # col_out = ti.Vector([col.x * lit, col.y * lit, col.z * lit, col.w])
135
+ # break
136
+
137
+ if use_spec_field[None] == 1:
138
+ n = get_normal(p)
139
+ diff = max(n.dot(-light_dir), 0.0)
140
+
141
+ depth_factor = 1.0 - (p.z - depth_near) / (depth_far - znear)
142
+ depth_factor = ti.max(0.0, ti.min(1.0, depth_factor))
143
+
144
+ # diffuse/ambient 光照
145
+ diffuse_term = 0.3 + 0.7 * diff
146
+ base = col.xyz * diffuse_term * depth_factor
147
+
148
+ # === Blinn-Phong 镜面反射 ===
149
+ view_dir = -rd.normalized()
150
+ half_dir = (view_dir + -light_dir).normalized()
151
+ spec = max(n.dot(half_dir), 0.0) ** 32 # shininess=32,越小越散,越大越锐
152
+ highlight = ti.Vector([1.0, 1.0, 1.0]) * (0.5 * spec) * depth_factor
153
+ col_out = ti.Vector([base.x + highlight.x,
154
+ base.y + highlight.y,
155
+ base.z + highlight.z,
156
+ col.w])
157
+ else:
158
+ # mono:无光照,纯平色
159
+ col_out = ti.Vector([col.x, col.y, col.z, col.w])
160
+ break
161
+
162
+ if t > zfar:
163
+ break
164
+ t += max(d, 1e-4)
165
+ img[y, x] = col_out
166
+
167
+ frames_np_rgba = []
168
+ for f in range(len(specs_list)):
169
+ # start_time = time.time()
170
+ frame_id[None] = f
171
+ render()
172
+ arr = np.clip(img.to_numpy(), 0, 1)
173
+ # end_time = time.time()
174
+ # print(f"Frame {f} time: {end_time - start_time} seconds")
175
+ arr8 = (arr * 255).astype(np.uint8)
176
+ frames_np_rgba.append(arr8)
177
+
178
+ return frames_np_rgba
179
+
180
+
181
+ def random_cylinder():
182
+ """生成一根随机圆柱 (start, end, color)。"""
183
+ # 起点 [-200,200]^2, z 在 [-300,-100]
184
+ ax = random.uniform(-200, 200)
185
+ ay = random.uniform(-200, 200)
186
+ az = random.uniform(300, 400)
187
+ start = [ax, ay, az]
188
+
189
+ # 随机方向和长度
190
+ theta = random.uniform(0, 2*math.pi)
191
+ phi = random.uniform(-math.pi/4, math.pi/4) # 倾斜角
192
+ L = 100
193
+ dx = math.cos(phi) * math.cos(theta)
194
+ dy = math.cos(phi) * math.sin(theta)
195
+ dz = math.sin(phi)
196
+ end = [ax + dx * L, ay + dy * L, az + dz * L]
197
+
198
+ # 随机颜色 (RGB + alpha=1)
199
+ color = [random.random(), random.random(), random.random(), 1.0]
200
+
201
+ return (start, end, color)
202
+
203
+ def generate_specs_list(num_frames=120, min_cyl=10, max_cyl=120):
204
+ """生成 specs_list,每帧有若干随机圆柱."""
205
+ specs_list = []
206
+ for _ in range(num_frames):
207
+ n_cyl = random.randint(min_cyl, max_cyl)
208
+ specs = [random_cylinder() for _ in range(n_cyl)]
209
+ specs_x_shift = [([spec[0][0] + 50, spec[0][1], spec[0][2]], [spec[1][0] + 50, spec[1][1], spec[1][2]], spec[2]) for spec in specs]
210
+ specs_list.append(specs)
211
+ specs_list.append(specs_x_shift)
212
+ return specs_list
213
+
214
+
215
+ if __name__ == "__main__":
216
+ specs_list = generate_specs_list(num_frames=24, min_cyl=10, max_cyl=120)
217
+ frames = render_whole(specs_list)
SCAIL-Pose/requirements.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ onnxruntime-gpu>=1.23.2
2
+ moviepy>=1.0.3
3
+ taichi>=1.7.4
4
+ openmim>=0.3.9
5
+ torch
6
+ torchvision
7
+ mmcv>=2.2.0
8
+ pre-commit>=4.5.0
9
+ matplotlib==3.7
10
+ tikzplotlib
11
+ jpeg4py
12
+ opencv-python
13
+ lmdb
14
+ pandas
15
+ scipy
16
+ loguru
17
+ moviepy
18
+ decord
19
+ controlnet_aux
20
+ webdataset
21
+ jsonlines
22
+ av
23
+ ultralytics
SCAIL-Pose/resources/animation.png ADDED

Git LFS Details

  • SHA256: d5eddd7caf6fbbbd40f56d861a7d00cc7f4a174df1e124fa825836c87ff721f5
  • Pointer size: 132 Bytes
  • Size of remote file: 2.28 MB
SCAIL-Pose/resources/data.png ADDED

Git LFS Details

  • SHA256: 1235a1ba44c76988250536d83870a648d68646c2c8a2d4fcbaab462d1ac13b67
  • Pointer size: 131 Bytes
  • Size of remote file: 643 kB
SCAIL-Pose/resources/pose_comp.png ADDED

Git LFS Details

  • SHA256: 72efc184058c7dfedc5d986d6e9f927cb39bc7566bdd1f2b7a9f278107f51d78
  • Pointer size: 132 Bytes
  • Size of remote file: 1.17 MB
SCAIL-Pose/resources/pose_result.png ADDED

Git LFS Details

  • SHA256: 56ae0cfd546b0797cef43e8006ab57f0baf83ea7a1c382eae737666223cae3f2
  • Pointer size: 131 Bytes
  • Size of remote file: 706 kB
SCAIL-Pose/resources/pose_teaser.png ADDED

Git LFS Details

  • SHA256: 2bf5e8413cbd7afad6d050099b7a55d349552c24df917e9b3135db550a2357f0
  • Pointer size: 132 Bytes
  • Size of remote file: 3.46 MB
SCAIL-Pose/resources/preteaser.png ADDED

Git LFS Details

  • SHA256: 04d1eaa51b780fbda7e2b097412eecf32d2c3b1b1deab573012034a0935d2225
  • Pointer size: 131 Bytes
  • Size of remote file: 163 kB
SCAIL-Pose/resources/teaser.png ADDED

Git LFS Details

  • SHA256: 56c3c1745976ad37bae932a77623482becc09936a548c4cad30295402baabb1e
  • Pointer size: 132 Bytes
  • Size of remote file: 4.51 MB
configs/config-1.3b.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "WanSCAILModel",
3
+ "_diffusers_version": "0.30.0",
4
+ "dim": 1536,
5
+ "eps": 1e-06,
6
+ "ffn_dim": 8960,
7
+ "freq_dim": 256,
8
+ "in_dim": 20,
9
+ "model_type": "i2v",
10
+ "num_heads": 12,
11
+ "num_layers": 30,
12
+ "out_dim": 16,
13
+ "text_len": 512
14
+ }
configs/config-14b.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "WanSCAILModel",
3
+ "_diffusers_version": "0.30.0",
4
+ "dim": 5120,
5
+ "eps": 1e-06,
6
+ "ffn_dim": 13824,
7
+ "freq_dim": 256,
8
+ "in_dim": 20,
9
+ "mask_dim": 28,
10
+ "model_type": "i2v",
11
+ "num_heads": 40,
12
+ "num_layers": 40,
13
+ "out_dim": 16,
14
+ "text_len": 512
15
+ }
convert.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import torch
3
+ from safetensors.torch import save_file
4
+ from typing import Dict
5
+ import os
6
+
7
+ class ModuleParser():
8
+ def __init__(self, key: str):
9
+ self.key = key
10
+ self.modules = key.split('.')
11
+ self.idx = 0
12
+
13
+ def match(self, pattern: str) -> bool:
14
+ patterns = pattern.split('.')
15
+ for j, p in enumerate(patterns):
16
+ if self.idx + j < len(self.modules) and self.modules[self.idx+j] == p:
17
+ continue
18
+ else:
19
+ return False
20
+ self.idx += len(patterns)
21
+ return True
22
+
23
+ def step(self) -> str:
24
+ m = self.modules[self.idx]
25
+ self.idx += 1
26
+ return m
27
+
28
+ def eof(self) -> bool:
29
+ return self.idx == len(self.modules)
30
+
31
+ def get_new_mappings(key: str, param: torch.Tensor) -> Dict[str, torch.Tensor]:
32
+ modules = []
33
+ parser = ModuleParser(key)
34
+ sat_prefix = "model.diffusion_model"
35
+ assert parser.match(sat_prefix), key
36
+ if parser.match("mixins"):
37
+ if parser.match("adaln_layer"):
38
+ if parser.match("adaLN_modulations"):
39
+ modules.append("blocks")
40
+ modules.append(parser.step())
41
+ modules.append("modulation")
42
+ elif parser.match("query_layernorm_list"):
43
+ modules.append("blocks")
44
+ modules.append(parser.step())
45
+ modules.append("self_attn.norm_q")
46
+ modules.append(parser.step())
47
+ elif parser.match("key_layernorm_list"):
48
+ modules.append("blocks")
49
+ modules.append(parser.step())
50
+ modules.append("self_attn.norm_k")
51
+ modules.append(parser.step())
52
+ elif parser.match("cross_query_layernorm_list"):
53
+ modules.append("blocks")
54
+ modules.append(parser.step())
55
+ modules.append("cross_attn.norm_q")
56
+ modules.append(parser.step())
57
+ elif parser.match("cross_key_layernorm_list"):
58
+ modules.append("blocks")
59
+ modules.append(parser.step())
60
+ modules.append("cross_attn.norm_k")
61
+ modules.append(parser.step())
62
+ elif parser.match("clip_feature_key_layernorm_list"):
63
+ modules.append("blocks")
64
+ modules.append(parser.step())
65
+ modules.append("cross_attn.norm_k_img")
66
+ modules.append(parser.step())
67
+ elif parser.match("clip_feature_key_value_list"):
68
+ modules.append("blocks")
69
+ modules.append(parser.step())
70
+ modules.append("cross_attn")
71
+ prefix = '.'.join(modules)
72
+ suffix = parser.step() # weight or bias
73
+ key_param, value_param = param.chunk(2, dim=0)
74
+ return {
75
+ ".".join([prefix, "k_img", suffix]): key_param,
76
+ ".".join([prefix, "v_img", suffix]): value_param,
77
+ }
78
+ else:
79
+ raise ValueError(key)
80
+ elif parser.match("final_layer"):
81
+ modules.append("head")
82
+ if parser.match("adaLN_modulation"):
83
+ modules.append("modulation")
84
+ elif parser.match("linear"):
85
+ modules.append("head")
86
+ modules.append(parser.step()) # weight or bias
87
+ elif parser.match("patch_embed"):
88
+ if parser.match("proj"):
89
+ modules.append("patch_embedding")
90
+ elif parser.match("proj_pose"):
91
+ modules.append("patch_embedding_pose")
92
+ elif parser.match("proj_mask"):
93
+ modules.append("patch_embedding_mask")
94
+ else:
95
+ raise ValueError(key)
96
+ modules.append(parser.step())
97
+ else:
98
+ raise ValueError(key)
99
+ elif parser.match("transformer.layers"):
100
+ modules.append("blocks")
101
+ modules.append(parser.step())
102
+ if parser.match("attention"):
103
+ modules.append("self_attn")
104
+ if parser.match("dense"):
105
+ modules.append("o")
106
+ modules.append(parser.step())
107
+ elif parser.match("query_key_value"):
108
+ prefix = '.'.join(modules)
109
+ suffix = parser.step()
110
+ query_param, key_param, value_param = param.chunk(3, dim=0)
111
+ return {
112
+ ".".join([prefix, "q", suffix]): query_param,
113
+ ".".join([prefix, "k", suffix]): key_param,
114
+ ".".join([prefix, "v", suffix]): value_param,
115
+ }
116
+ else:
117
+ raise ValueError(key)
118
+ elif parser.match("cross_attention"):
119
+ modules.append("cross_attn")
120
+ if parser.match("dense"):
121
+ modules.append("o")
122
+ modules.append(parser.step())
123
+ elif parser.match("query"):
124
+ modules.append("q")
125
+ modules.append(parser.step())
126
+ elif parser.match("key_value"):
127
+ prefix = '.'.join(modules)
128
+ suffix = parser.step()
129
+ key_param, value_param = param.chunk(2, dim=0)
130
+ return {
131
+ ".".join([prefix, "k", suffix]): key_param,
132
+ ".".join([prefix, "v", suffix]): value_param,
133
+ }
134
+ else:
135
+ raise ValueError(key)
136
+ elif parser.match("post_cross_attention_layernorm"):
137
+ modules.append("norm3")
138
+ modules.append(parser.step()) # weight or bias
139
+ elif parser.match("mlp"):
140
+ modules.append("ffn")
141
+ if parser.match("dense_h_to_4h"):
142
+ modules.append("0")
143
+ elif parser.match("dense_4h_to_h"):
144
+ modules.append("2")
145
+ else:
146
+ raise ValueError(key)
147
+ modules.append(parser.step()) # weight or bias
148
+ else:
149
+ raise ValueError(key)
150
+ elif parser.match("time_embed"):
151
+ modules.append("time_embedding")
152
+ modules.append(parser.step())
153
+ modules.append(parser.step())
154
+ elif parser.match("adaln_projection"):
155
+ modules.append("time_projection")
156
+ modules.append(parser.step())
157
+ modules.append(parser.step())
158
+ elif parser.match("text_embedding"):
159
+ modules.append("text_embedding")
160
+ modules.append(parser.step())
161
+ modules.append(parser.step())
162
+ elif parser.match("clip_proj"):
163
+ assert parser.match("proj"), key
164
+ modules.append("img_emb.proj")
165
+ modules.append(parser.step())
166
+ modules.append(parser.step())
167
+ else:
168
+ raise ValueError(key)
169
+ assert parser.eof(), key
170
+ return {'.'.join(modules): param}
171
+
172
+ def get_new_state_dict(old: Dict[str, torch.Tensor]):
173
+ new = dict()
174
+ for key, value in old.items():
175
+ map = get_new_mappings(key, value)
176
+ for new_key, new_value in map.items():
177
+ if new_key in new:
178
+ print(f"Warning: duplicate new key {new_key} converted from {key}!")
179
+ new[new_key] = new_value
180
+ return new
181
+
182
+ def main(args):
183
+ pt_file_path = os.path.join(args.scail_dir, args.sat_model_path)
184
+ print(f"Loading from {pt_file_path}...")
185
+ checkpoint = torch.load(pt_file_path)
186
+ state_dict = checkpoint["module"]
187
+ new_state_dict = get_new_state_dict(state_dict)
188
+ print(f"Saving to {args.save_path}...")
189
+ save_file(new_state_dict, args.save_path)
190
+ print("Done.")
191
+
192
+ import argparse
193
+ if __name__ == "__main__":
194
+ parser = argparse.ArgumentParser()
195
+ parser.add_argument("--scail-dir", default="SCAIL-2/")
196
+ parser.add_argument("--sat-model-path", default="model/1/fsdp2_rank_0000_checkpoint.pt")
197
+ parser.add_argument("--save-path", default="SCAIL-2.safetensors")
198
+ args = parser.parse_args()
199
+ main(args)
examples/animation_001/combined.gif ADDED

Git LFS Details

  • SHA256: 089acb223778fc3b26e1273abda72640ff9a310cdd0d46067ceb7dc15be91e74
  • Pointer size: 131 Bytes
  • Size of remote file: 299 kB
examples/animation_001/driving.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a6395c6c0f927fa6d6f1f7e761aa902f31c90ae112f4ca38457baa2455108b6a
3
+ size 517884
examples/animation_001/ref.jpg ADDED

Git LFS Details

  • SHA256: 3485a50ddf5cf73d81da87ff7a9da4a16e4ad3e51cb7c8e1f2393d2dea79f781
  • Pointer size: 131 Bytes
  • Size of remote file: 224 kB
examples/animation_001/ref_mask.jpg ADDED
examples/animation_001/rendered_mask_v2.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b8dca4f6148feb0ea4cd225ea5ab5d7500747f240e89312f1092ec88d68b5bba
3
+ size 206989