keyon857 commited on
Commit
d2f1e4b
·
1 Parent(s): aa6f83d

Add files using upload-large-folder tool

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 +2 -0
  2. ComfyUI_ExtraModels/.gitignore +160 -0
  3. ComfyUI_ExtraModels/LICENSE +201 -0
  4. ComfyUI_ExtraModels/README.md +287 -0
  5. ComfyUI_ExtraModels/__init__.py +51 -0
  6. ComfyUI_ExtraModels/requirements.txt +7 -0
  7. ComfyUI_JPS-Nodes/README.md +81 -0
  8. ComfyUI_JPS-Nodes/__init__.py +10 -0
  9. ComfyUI_JPS-Nodes/jps_nodes.py +0 -0
  10. ComfyUI_Qwen2-VL-Instruct/.gitignore +162 -0
  11. ComfyUI_Qwen2-VL-Instruct/LICENSE +201 -0
  12. ComfyUI_Qwen2-VL-Instruct/README.md +37 -0
  13. ComfyUI_Qwen2-VL-Instruct/__init__.py +18 -0
  14. ComfyUI_Qwen2-VL-Instruct/favicon.ico +0 -0
  15. ComfyUI_Qwen2-VL-Instruct/node_helpers.py +37 -0
  16. ComfyUI_Qwen2-VL-Instruct/nodes.py +195 -0
  17. ComfyUI_Qwen2-VL-Instruct/path_nodes.py +61 -0
  18. ComfyUI_Qwen2-VL-Instruct/pyproject.toml +14 -0
  19. ComfyUI_Qwen2-VL-Instruct/requirements.txt +14 -0
  20. ComfyUI_Qwen2-VL-Instruct/util_nodes.py +80 -0
  21. ComfyUI_essentials/.gitignore +6 -0
  22. ComfyUI_essentials/LICENSE +21 -0
  23. ComfyUI_essentials/README.md +49 -0
  24. ComfyUI_essentials/__init__.py +36 -0
  25. ComfyUI_essentials/carve.py +454 -0
  26. ComfyUI_essentials/conditioning.py +280 -0
  27. ComfyUI_essentials/histogram_matching.py +87 -0
  28. ComfyUI_essentials/image.py +1770 -0
  29. ComfyUI_essentials/mask.py +596 -0
  30. ComfyUI_essentials/misc.py +574 -0
  31. ComfyUI_essentials/pyproject.toml +15 -0
  32. ComfyUI_essentials/requirements.txt +5 -0
  33. ComfyUI_essentials/sampling.py +811 -0
  34. ComfyUI_essentials/segmentation.py +89 -0
  35. ComfyUI_essentials/text.py +113 -0
  36. ComfyUI_essentials/utils.py +89 -0
  37. ComfyUI_essentials/workflow_all_nodes.json +994 -0
  38. comfyui_controlnet_aux/README.md +252 -0
  39. comfyui_controlnet_aux/UPDATES.md +44 -0
  40. comfyui_controlnet_aux/__init__.py +214 -0
  41. comfyui_controlnet_aux/log.py +80 -0
  42. comfyui_controlnet_aux/requirements.txt +25 -0
  43. comfyui_controlnet_aux/search_hf_assets.py +56 -0
  44. comfyui_controlnet_aux/utils.py +250 -0
  45. comfyui_layerstyle/.gitignore +6 -0
  46. comfyui_layerstyle/LICENSE +21 -0
  47. comfyui_layerstyle/README.MD +0 -0
  48. comfyui_layerstyle/README_CN.MD +0 -0
  49. comfyui_layerstyle/__init__.py +48 -0
  50. comfyui_layerstyle/custom_size.ini.example +10 -0
.gitattributes CHANGED
@@ -35,3 +35,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  ComfyUI-Florence-2/workflow_seg_crop.png filter=lfs diff=lfs merge=lfs -text
37
  ComfyUI-Florence-2/workflow_bbox.png filter=lfs diff=lfs merge=lfs -text
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  ComfyUI-Florence-2/workflow_seg_crop.png filter=lfs diff=lfs merge=lfs -text
37
  ComfyUI-Florence-2/workflow_bbox.png filter=lfs diff=lfs merge=lfs -text
38
+ comfyui-inpaint-cropandstitch/inpaint-cropandstitch_example_workflow.png filter=lfs diff=lfs merge=lfs -text
39
+ comfyui-inpaint-cropandstitch/inpaint-cropandstitch_flux_example_workflow.png filter=lfs diff=lfs merge=lfs -text
ComfyUI_ExtraModels/.gitignore ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
ComfyUI_ExtraModels/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 [yyyy] [name of copyright owner]
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.
ComfyUI_ExtraModels/README.md ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Extra Models for ComfyUI
2
+
3
+ This repository aims to add support for various different image diffusion models to ComfyUI.
4
+
5
+ ## Installation
6
+
7
+ Simply clone this repo to your custom_nodes folder using the following command:
8
+
9
+ `git clone https://github.com/city96/ComfyUI_ExtraModels custom_nodes/ComfyUI_ExtraModels`
10
+
11
+ You will also have to install the requirements from the provided file by running `pip install -r requirements.txt` inside your VENV/conda env. If you downloaded the standalone version of ComfyUI, then follow the steps below.
12
+
13
+ ### Standalone ComfyUI
14
+
15
+ I haven't tested this completely, so if you know what you're doing, use the regular venv/`git clone` install option when installing ComfyUI.
16
+
17
+ Go to the where you unpacked `ComfyUI_windows_portable` to (where your run_nvidia_gpu.bat file is) and open a command line window. Press `CTRL+SHIFT+Right click` in an empty space and click "Open PowerShell window here".
18
+
19
+ Clone the repository to your custom nodes folder, assuming haven't installed in through the manager.
20
+
21
+ `git clone https://github.com/city96/ComfyUI_ExtraModels .\ComfyUI\custom_nodes\ComfyUI_ExtraModels`
22
+
23
+ To install the requirements on windows, run these commands in the same window:
24
+ ```
25
+ .\python_embeded\python.exe -s -m pip install -r .\ComfyUI\custom_nodes\ComfyUI_ExtraModels\requirements.txt
26
+ ```
27
+
28
+ To update, open the command line window like before and run the following commands:
29
+
30
+ ```
31
+ cd .\ComfyUI\custom_nodes\ComfyUI_ExtraModels\
32
+ git pull
33
+ ```
34
+
35
+ Alternatively, use the manager, assuming it has an update function.
36
+
37
+
38
+ ## Sana
39
+
40
+ [Original Repo](https://github.com/NVlabs/Sana)
41
+
42
+ > [!CAUTION]
43
+ > As many people have had issues with Sana, it's for now recommended to try the fork by the Sana devs, which auto downloads all models:
44
+ >
45
+ > [Readme](https://github.com/NVlabs/Sana/blob/main/asset/docs/ComfyUI/comfyui.md) | [Fork repo](https://github.com/Efficient-Large-Model/ComfyUI_ExtraModels)
46
+
47
+ A full rewrite to have better integration is in progress in [this PR](https://github.com/city96/ComfyUI_ExtraModels/pull/92) but isn't ready yet.
48
+
49
+ https://github.com/NVlabs/Sana/blob/main/asset/docs/ComfyUI/comfyui.md
50
+ https://github.com/Efficient-Large-Model/ComfyUI_ExtraModels
51
+
52
+ ### Model info / implementation
53
+ - Uses Gemma2 2B as the text encoder
54
+ - Multiple resolutions and models available
55
+ - Compressed latent space (32 channels, /32 compression) - needs custom VAE
56
+
57
+ ### Usage
58
+ 1. Download the model weights from the [Sana HF repo](https://huggingface.co/Efficient-Large-Model/Sana_1600M_1024px/tree/main/checkpoints) - the HF account has alternative models available too.
59
+ 2. Place them in your checkpoints folder
60
+ 3. Load them with the correct PixArt checkpoint loader
61
+ 4. Use the "Gemma Loader" node - it should automatically download the requested model from Huggingface - Recommended to use the 4bit quantized model on CPU when low on memory.
62
+ 5. Download the VAE from [here](https://huggingface.co/Efficient-Large-Model/Sana_1600M_1024px_diffusers/blob/main/vae/diffusion_pytorch_model.safetensors) or [here](https://huggingface.co/mit-han-lab/dc-ae-f32c32-sana-1.0/blob/main/model.safetensors) and place it in your VAE folder after renaming it.
63
+ 6. Use either the "Empty Sana Latent Image" or "Empty DCAE Latent Image" node for the latent input when doing txt2img.
64
+
65
+ [Sample workflow](https://github.com/user-attachments/files/18027854/SanaV1.json)
66
+
67
+ ![Sana](https://github.com/user-attachments/assets/e4334352-e894-416a-b200-dff096027481)
68
+
69
+
70
+ ## PixArt
71
+
72
+ [Original Repo](https://github.com/PixArt-alpha/PixArt-alpha)
73
+
74
+ ### Model info / implementation
75
+ - Uses T5 text encoder instead of clip
76
+ - Available in 512 and 1024 versions, needs specific pre-defined resolutions to work correctly
77
+ - Same latent space as SD1.5 (works with the SD1.5 VAE)
78
+ - Attention needs optimization, images look worse without xformers.
79
+
80
+ ### Usage
81
+
82
+ 1. Download the model weights from the [PixArt alpha repo](https://huggingface.co/PixArt-alpha/PixArt-alpha/tree/main) - you most likely want the 1024px one - `PixArt-XL-2-1024-MS.pth`
83
+ 3. Place them in your checkpoints folder
84
+ 4. Load them with the correct PixArt checkpoint loader
85
+ 5. **Follow the T5v11 section of this readme** to set up the T5 text encoder
86
+
87
+ > [!TIP]
88
+ > You should be able to use the model with the default KSampler if you're on the latest version of the node.
89
+ > In theory, this should allow you to use longer prompts as well as things like doing img2img.
90
+
91
+ Limitations:
92
+ - `PixArt DPM Sampler` requires the negative prompt to be shorter than the positive prompt.
93
+ - `PixArt DPM Sampler` can only work with a batch size of 1.
94
+ - `PixArt T5 Text Encode` is from the reference implementation, therefore it doesn't support weights. `T5 Text Encode` support weights, but I can't attest to the correctness of the implementation.
95
+
96
+ > [!IMPORTANT]
97
+ > Installing `xformers` is optional but strongly recommended as torch SDP is only partially implemented, if that.
98
+
99
+ [Sample workflow here](https://github.com/city96/ComfyUI_ExtraModels/files/13617463/PixArtV3.json)
100
+
101
+ ![PixArtT12](https://github.com/city96/ComfyUI_ExtraModels/assets/125218114/eb1a02f9-6114-47eb-a066-261c39c55615)
102
+
103
+ ### PixArt Sigma
104
+
105
+ The Sigma models work just like the normal ones. Out of the released checkpoints, the 512, 1024 and 2K one are supported.
106
+
107
+ You can find the [1024 checkpoint here](https://huggingface.co/PixArt-alpha/PixArt-Sigma/blob/main/PixArt-Sigma-XL-2-1024-MS.pth). Place it in your models folder and **select the appropriate type in the model loader / resolution selection node.**
108
+
109
+ > [!IMPORTANT]
110
+ > Make sure to select an SDXL VAE for PixArt Sigma!
111
+
112
+ ### PixArt LCM
113
+
114
+ The LCM model also works if you're on the latest version. To use it:
115
+
116
+ 1. Download the [PixArt LCM model](https://huggingface.co/PixArt-alpha/PixArt-LCM-XL-2-1024-MS/blob/main/transformer/diffusion_pytorch_model.safetensors) and place it in your checkpoints folder.
117
+ 2. Add a `ModelSamplingDiscrete` node and set "sampling" to "lcm"
118
+ 3. Adjust the KSampler settings - Set the sampler to "lcm". Your CFG should be fairly low (1.1-1.5), your steps should be around 5.
119
+
120
+ Everything else can be the same the same as in the example above.
121
+
122
+ ![PixArtLCM](https://github.com/city96/ComfyUI_ExtraModels/assets/125218114/558f8b30-449b-4973-ad7e-6aa69832adcb)
123
+
124
+
125
+
126
+ ## HunYuan DiT
127
+
128
+ WIP implementation of [HunYuan DiT by Tencent](https://github.com/Tencent/HunyuanDiT)
129
+
130
+ The initial work on this was done by [chaojie](https://github.com/chaojie) in [this PR](https://github.com/city96/ComfyUI_ExtraModels/pull/37).
131
+
132
+ Instructions:
133
+ - Download the [first text encoder from here](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT/blob/main/t2i/clip_text_encoder/pytorch_model.bin) and place it in `ComfyUI/models/clip` - rename to "chinese-roberta-wwm-ext-large.bin"
134
+ - Download the [second text encoder from here](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT/blob/main/t2i/mt5/pytorch_model.bin) and place it in `ComfyUI/models/t5` - rename it to "mT5-xl.bin"
135
+ - Download the [model file from here](https://huggingface.co/Tencent-Hunyuan/HunyuanDiT/blob/main/t2i/model/pytorch_model_module.pt) and place it in `ComfyUI/checkpoints` - rename it to "HunYuanDiT.pt"
136
+ - Download/use any SDXL VAE, for example [this one](https://huggingface.co/madebyollin/sdxl-vae-fp16-fix)
137
+
138
+ You may also try the following alternate model files for faster loading speed/smaller file size:
139
+ - converted [second text encoder](https://huggingface.co/city96/mt5-xl-encoder-fp16/blob/main/model.safetensors) - rename to `mT5-xl-encoder-fp16.safetensors` and placed in `ComfyUI/models/t5`
140
+
141
+ You can use the "simple" text encode node to only use one prompt, or you can use the regular one to pass different text to CLIP/T5.
142
+
143
+ [Sample Workflow](https://github.com/city96/ComfyUI_ExtraModels/files/15444231/HyDiTV1.json)
144
+
145
+ ![image](https://github.com/city96/ComfyUI_ExtraModels/assets/125218114/7a9d6e34-d3f4-4f67-a17f-4f2d6795e54e)
146
+
147
+
148
+
149
+ ## DiT
150
+
151
+ [Original Repo](https://github.com/facebookresearch/DiT)
152
+
153
+ ### Model info / implementation
154
+ - Uses class labels instead of prompts
155
+ - Limited to 256x256 or 512x512 images
156
+ - Same latent space as SD1.5 (works with the SD1.5 VAE)
157
+ - Works in FP16, but no other optimization
158
+
159
+ ### Usage
160
+
161
+ 1. Download the original model weights from the [DiT Repo](https://github.com/facebookresearch/DiT) or the converted [FP16 safetensor ones from Huggingface](https://huggingface.co/city96/DiT/tree/main).
162
+ 2. Place them in your checkpoints folder. (You may need to move them if you had them in `ComfyUI\models\dit` before)
163
+ 3. Load the model and select the class labels as shown in the image below
164
+ 4. **Make sure to use the Empty label conditioning for the Negative input of the KSampler!**
165
+
166
+ ConditioningCombine nodes *should* work for combining multiple labels. The area ones don't since the model currently can't handle dynamic input dimensions.
167
+
168
+ [Sample workflow here](https://github.com/city96/ComfyUI_ExtraModels/files/13619259/DiTV2.json)
169
+
170
+ ![DIT_WORKFLOW_IMG](https://github.com/city96/ComfyUI_ExtraModels/assets/125218114/cdd4ec94-b0eb-436a-bf23-a3bcef8d7b90)
171
+
172
+
173
+
174
+ ## T5
175
+
176
+ ### T5v11
177
+
178
+ The model files can be downloaded from the [DeepFloyd/t5-v1_1-xxl](https://huggingface.co/DeepFloyd/t5-v1_1-xxl/tree/main) repository.
179
+
180
+ You will need to download the following 4 files:
181
+ - `config.json`
182
+ - `pytorch_model-00001-of-00002.bin`
183
+ - `pytorch_model-00002-of-00002.bin`
184
+ - `pytorch_model.bin.index.json`
185
+
186
+ Place them in your `ComfyUI/models/t5` folder. You can put them in a subfolder called "t5-v1.1-xxl" though it doesn't matter. There are int8 safetensor files in the other DeepFloyd repo, thought they didn't work for me.
187
+
188
+ For faster loading/smaller file sizes, you may pick one of the following alternative downloads:
189
+ - [FP16 converted version](https://huggingface.co/theunlikely/t5-v1_1-xxl-fp16/tree/main) - Same layout as the original, download both safetensor files as well as the `*.index.json` and `config.json` files.
190
+ - [BF16 converter version](https://huggingface.co/city96/t5-v1_1-xxl-encoder-bf16/tree/main) - Merged into a single safetensor, only `model.safetensors` (+`config.json` for folder mode) are reqired.
191
+
192
+ To move T5 to a different drive/folder, do the same as you would when moving checkpoints, but add ` t5: t5` to `extra_model_paths.yaml` and create a directory called "t5" in the alternate path specified in the `base_path` variable.
193
+
194
+ ### Usage
195
+
196
+ Loaded onto the CPU, it'll use about 22GBs of system RAM. Depending on which weights you use, it might use slightly more during loading.
197
+
198
+ If you have a second GPU, selecting "cuda:1" as the device will allow you to use it for T5, freeing at least some VRAM/System RAM. Using FP16 as the dtype is recommended.
199
+
200
+ Loaded in bnb4bit mode, it only takes around 6GB VRAM, making it work with 12GB cards. The only drawback is that it'll constantly stay in VRAM since BitsAndBytes doesn't allow moving the weights to the system RAM temporarily. Switching to a different workflow *should* still release the VRAM as expected. Pascal cards (1080ti, P40) seem to struggle with 4bit. Select "cpu" if you encounter issues.
201
+
202
+ On windows, you may need a newer version of bitsandbytes for 4bit. Try `python -m pip install bitsandbytes`
203
+
204
+ > [!IMPORTANT]
205
+ > You may also need to upgrade transformers and install spiece for the tokenizer. `pip install -r requirements.txt`
206
+
207
+
208
+
209
+ ## MiaoBi
210
+
211
+ ### Original from:
212
+
213
+ - Author: Github [ShineChen1024](https://github.com/ShineChen1024) | Hugging Face [ShineChen1024](https://huggingface.co/ShineChen1024)
214
+ - https://github.com/ShineChen1024/MiaoBi
215
+ - https://huggingface.co/ShineChen1024/MiaoBi
216
+
217
+ ### Instructions
218
+ - Download the [clip model](https://huggingface.co/ShineChen1024/MiaoBi/blob/main/miaobi_beta0.9/text_encoder/model.safetensors) and rename it to "MiaoBi_CLIP.safetensors" or any you like, then place it in `ComfyUI/models/clip`.
219
+ - Download the [unet model](https://huggingface.co/ShineChen1024/MiaoBi/blob/main/miaobi_beta0.9/unet/diffusion_pytorch_model.safetensors) and rename it to "MiaoBi.safetensors", then place it in `ComfyUI/models/unet`.
220
+ - Alternatively, clone/download the entire huggingface repo to `ComfyUI/models/diffusers` and use the MiaoBi diffusers loader.
221
+
222
+ 这是妙笔的测试版本。妙笔,一个中文文生图模型,与经典的stable-diffusion 1.5版本拥有一致的结构,兼容现有的lora,controlnet,T2I-Adapter等主流插件及其权重。
223
+
224
+ This is the beta version of MiaoBi, a chinese text-to-image model, following the classical structure of sd-v1.5, compatible with existing mainstream plugins such as Lora, Controlnet, T2I Adapter, etc.
225
+
226
+ Example Prompts:
227
+ - 一只精致的陶瓷猫咪雕像,全身绘有精美的传统花纹,眼睛仿佛会发光。
228
+ - 动漫风格的风景画,有山脉、湖泊,也有繁华的小镇子,色彩鲜艳,光影效果明显。
229
+ - 极具真实感的复杂农村的老人肖像,黑白。
230
+ - 红烧狮子头
231
+ - 车水马龙的上海街道,春节,舞龙舞狮。
232
+ - 枯藤老树昏鸦,小桥流水人家。水墨画。
233
+
234
+ [Example Workflow](https://github.com/city96/ComfyUI_ExtraModels/files/15389380/MiaoBiV1.json)
235
+
236
+ [Example Workflow (diffusers)](https://github.com/city96/ComfyUI_ExtraModels/files/15389381/MiaoBiV1D.json)
237
+
238
+ ![MiaoBi](https://github.com/city96/ComfyUI_ExtraModels/assets/125218114/d9e4ab7d-f61b-407f-b7dd-af5859627d0e)
239
+
240
+
241
+
242
+ ## VAE
243
+
244
+ A few custom VAE models are supported. The option to select a different dtype when loading is also possible, which can be useful for testing/comparisons. You can load the models listed below using the "ExtraVAELoader" node.
245
+
246
+ **Models like PixArt/DiT do NOT need a special VAE. Unless mentioned, use one of the following as you would with any other model:**
247
+ - [VAE for SD1.X, DiT and PixArt alpha](https://huggingface.co/stabilityai/sd-vae-ft-mse-original/blob/main/vae-ft-mse-840000-ema-pruned.safetensors).
248
+ - [VAE for SDXL and PixArt sigma](https://huggingface.co/madebyollin/sdxl-vae-fp16-fix/blob/main/diffusion_pytorch_model.safetensors)
249
+
250
+ ### Consistency Decoder
251
+
252
+ [Original Repo](https://github.com/openai/consistencydecoder)
253
+
254
+ This now works thanks to the work of @mrsteyk and @madebyollin - [Gist with more info](https://gist.github.com/madebyollin/865fa6a18d9099351ddbdfbe7299ccbf).
255
+
256
+ - Download the converted safetensor VAE from [this HF repository](https://huggingface.co/mrsteyk/consistency-decoder-sd15/blob/main/stk_consistency_decoder_amalgamated.safetensors). If you downloaded the OpenAI model before, it won't work, as it is a TorchScript file. Feel free to delete it.
257
+ - Put the file in your VAE folder
258
+ - Load it with the ExtraVAELoader
259
+ - Set it to fp16 or bf16 to not run out of VRAM
260
+ - Use tiled VAE decode if required
261
+
262
+ ### Deflickering Decoder / VideoDecoder
263
+
264
+ This is the VAE that comes baked into the [Stable Video Diffusion](https://stability.ai/news/stable-video-diffusion-open-ai-video-model) model.
265
+
266
+ It doesn't seem particularly good as a normal VAE (color issues, pretty bad with finer details).
267
+
268
+ Still for completeness sake the code to run it is mostly implemented. To obtain the weights just extract them from the sdv model:
269
+
270
+ ```py
271
+ from safetensors.torch import load_file, save_file
272
+
273
+ pf = "first_stage_model." # Key prefix
274
+ sd = load_file("svd_xt.safetensors")
275
+ vae = {k.replace(pf, ''):v for k,v in sd.items() if k.startswith(pf)}
276
+ save_file(vae, "svd_xt_vae.safetensors")
277
+ ```
278
+
279
+ ### AutoencoderKL / VQModel
280
+
281
+ `kl-f4/8/16/32` from the [compvis/latent diffusion repo](https://github.com/CompVis/latent-diffusion/tree/main#pretrained-autoencoding-models).
282
+
283
+ `vq-f4/8/16` from the taming transformers repo, weights for both vq and kl models available [here](https://ommer-lab.com/files/latent-diffusion/)
284
+
285
+ `vq-f8` can accepts latents from the SD unet but just like xl with v1 latents, output largely garbage. The rest are completely useless without a matching UNET that uses the correct channel count.
286
+
287
+ ![VAE_TEST](https://github.com/city96/ComfyUI_ExtraModels/assets/125218114/316c7029-ee78-4ff7-a46a-b56ef91477eb)
ComfyUI_ExtraModels/__init__.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # only import if running as a custom node
2
+ try:
3
+ import comfy.utils
4
+ except ImportError:
5
+ pass
6
+ else:
7
+ NODE_CLASS_MAPPINGS = {}
8
+
9
+ # Deci Diffusion
10
+ # from .DeciDiffusion.nodes import NODE_CLASS_MAPPINGS as DeciDiffusion_Nodes
11
+ # NODE_CLASS_MAPPINGS.update(DeciDiffusion_Nodes)
12
+
13
+ # DiT
14
+ from .DiT.nodes import NODE_CLASS_MAPPINGS as DiT_Nodes
15
+ NODE_CLASS_MAPPINGS.update(DiT_Nodes)
16
+
17
+ # PixArt
18
+ from .PixArt.nodes import NODE_CLASS_MAPPINGS as PixArt_Nodes
19
+ NODE_CLASS_MAPPINGS.update(PixArt_Nodes)
20
+
21
+ # T5
22
+ from .T5.nodes import NODE_CLASS_MAPPINGS as T5_Nodes
23
+ NODE_CLASS_MAPPINGS.update(T5_Nodes)
24
+
25
+ # HYDiT
26
+ from .HunYuanDiT.nodes import NODE_CLASS_MAPPINGS as HunYuanDiT_Nodes
27
+ NODE_CLASS_MAPPINGS.update(HunYuanDiT_Nodes)
28
+
29
+ # VAE
30
+ from .VAE.nodes import NODE_CLASS_MAPPINGS as VAE_Nodes
31
+ NODE_CLASS_MAPPINGS.update(VAE_Nodes)
32
+
33
+ # MiaoBi
34
+ from .MiaoBi.nodes import NODE_CLASS_MAPPINGS as MiaoBi_Nodes
35
+ NODE_CLASS_MAPPINGS.update(MiaoBi_Nodes)
36
+
37
+ # Extra
38
+ from .utils.nodes import NODE_CLASS_MAPPINGS as Extra_Nodes
39
+ NODE_CLASS_MAPPINGS.update(Extra_Nodes)
40
+
41
+ # Sana
42
+ from .Sana.nodes import NODE_CLASS_MAPPINGS as Sana_Nodes
43
+ NODE_CLASS_MAPPINGS.update(Sana_Nodes)
44
+
45
+ # Gemma
46
+ from .Gemma.nodes import NODE_CLASS_MAPPINGS as Gemma_Nodes
47
+ NODE_CLASS_MAPPINGS.update(Gemma_Nodes)
48
+
49
+ NODE_DISPLAY_NAME_MAPPINGS = {k:v.TITLE for k,v in NODE_CLASS_MAPPINGS.items()}
50
+ __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
51
+
ComfyUI_ExtraModels/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ timm>=0.6.13
2
+ sentencepiece>=0.1.97
3
+ transformers>=4.34.1
4
+ accelerate>=0.23.0
5
+ einops>=0.6.0
6
+ protobuf>=3.20.3
7
+ bitsandbytes>=0.41.0
ComfyUI_JPS-Nodes/README.md ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # JPS Custom Nodes for ComfyUI
2
+
3
+ These nodes were originally made for use in JPS ComfyUI Workflows.
4
+
5
+ The nodes can be used in any ComfyUI workflow.
6
+
7
+ # Installation
8
+
9
+ If you have a previous version of the "JPS Custom Nodes for ComfyUI", please delete this before installing these nodes.
10
+
11
+ 1. cd custom_nodes
12
+ 2. git clone https://github.com/JPS-GER/ComfyUI_JPS-Nodes.git
13
+ 3. Restart ComfyUI
14
+
15
+ You can also install the nodes using the following methods:
16
+ * install using [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager)
17
+
18
+ # List of Custom Nodes
19
+
20
+ __IO__
21
+ * Lora Loader - Lora Loader with On/Off Switch - output is 1 or 2, so it works with most "x to 1"-switches (while some other alternatives use boolean 0 or 1 and need corresponding switches or additional math nodes)
22
+
23
+ __Settings__
24
+ * SDXL Resolutions - small node that offers recommended SDXL resolutions and outputs height and width values
25
+ * SDXL Basic Settings - menu node for basic SDXL settings, required for most SDXL workflows (connect to "SDXL Basic Settings Pipe" to access the values), includes FreeU options now
26
+ * Generation TXT IMG Settings - menu node to switch between TXT2IMG and IMG2IMG
27
+ * Generation Settings - menu node to switch between, TXT2IMG, IMG2IMG, Control Net Canny, Control Net Depth, Inpainting (conntect to "Generation Settings Pipe" to access the values)
28
+ * IP Adapter Settings - menu node to turn on/off five IP adapter input images and settings (conntect to "IP Adapter Settings Pipe" to access the values)
29
+ * Revision Settings - menu node to turn on/off two revision input images and settings (conntect to "Revision Settings Pipe" to access the values)
30
+ * Sampler Scheduler Settings - menu node for sampler + scheduler settings, can also be used as pipe
31
+
32
+ __Switches__
33
+ * Integer Switch - "5 to 1"-switch for integer values
34
+ * Image Switch - "5 to 1"-switch for images
35
+ * Latent Switch - "5 to 1"-switch for latent images
36
+ * Conditioning Switch - "5 to 1"-switch for conditioning
37
+ * Model Switch - "5 to 1"-switch for models
38
+ * VAE Switch - "5 to 1"-switch for VAE
39
+ * ControlNet Switch - "5 to 1"-switch for ControlNet
40
+ * Disable Enable Switch - input for nodes that use "disable/enable" types of input (for example KSampler) - useful to switch those values in combinaton with other switches
41
+ * Enable Disable Switch - input for nodes that use "enable/disable" types of input (for example KSampler) - useful to switch those values in combinaton with other switches
42
+
43
+ __Pipes__
44
+ * SDXL Basic Settings Pipe - used to access data from "SDXL Basic Settings" menu node - place outside of the menu structure of your workflow
45
+ * Generation Settings Pipe - used to access data from "Generation Settings" menu node - place outside of the menu structure of your workflow
46
+ * IP Adapter Settings Pipe - used to access data from "IP Adapter Settings" menu node - place outside of the menu structure of your workflow
47
+ * Revision Settings Pipe - used to access data from "Revision Settings" menu node - place outside of the menu structure of your workflow
48
+ * SDXL Fundamentals MultiPipe - used to build a pipe for basic SDXL settings, has input/outputs for all supported types, so you can access/change values more easily than classic "from/to/edit"-pipes
49
+ * Images Masks MultiPipe - used to build a pipe for various images and masks used in my workflow, has input/outputs for all images, so you can access/change images and masks more easily than classic "from/to/edit"-pipes
50
+
51
+ __Math__
52
+ * SDXL Recommended Resolution Calc - gives you the closest recommended SDXL resolution for the width and height values, useful for IMG2IMG and ControlNet input images, to bring them in line with SDXL workflows
53
+ * Resolution Multiply - multily height and width by some factor - useful to get 2x or 4x values for upscaling or SDXL target width and SDXL target height
54
+ * Largest Int - input two integer values, output will be the larger value
55
+ * Multiply Int Int - multiply two integer inputs, output is available as integer and float, so you can save an extra node converting to the required type
56
+ * Multiply Int Float - multiply integer and float inputs, output is available as integer and float, so you can save an extra node converting to the required type
57
+ * Multiply Float Float - multiply two flout inputs, output is available as integer and float, so you can save an extra node converting to the required type
58
+ * Substract Int Int - subscract one integer input from another integer input, output is available as integer and float, so you can save an extra node converting to the required type
59
+
60
+ __Text__
61
+ * Text Concatenate - combine multiple input strings to one output string
62
+ * Get Date Time String - get current date/time (has extra code to make sure it will not use cached data)
63
+ * SDXL Prompt Handling - control how text_g and text_l input will be handled (many options)
64
+ * SDXL Prompt Handling Plus - control how text_g and text_l input will be handled (many options), option to add an "universal negative" prompt
65
+
66
+ ![image](https://github.com/JPS-GER/ComfyUI_JPS-Nodes/assets/142158778/66da22f7-e4d6-4898-ae30-7b123a268615)
67
+ ![image](https://github.com/JPS-GER/ComfyUI_JPS-Nodes/assets/142158778/c5abb960-0c6c-448a-a2f2-72d857dddc70)
68
+
69
+ __Image__
70
+ * Get Image Size - get width and height value from an input image, useful in combination with "Resolution Multiply" and "SDXL Recommended Resolution Calc" nodes
71
+ * Crop Image Square - crop images to a square aspect ratio - choose between center, top, bottom, left and right part of the image and fine tune with offset option, optional: resize image to target size (useful for Clip Vision input images, like IP-Adapter or Revision)
72
+
73
+ __Style__
74
+ * SDXL Prompt Styler - add artists, movies and general styles to your text prompt, option to add an "universal negative" prompt - uses json files, so you can extend the available options
75
+
76
+ ![image](https://github.com/JPS-GER/ComfyUI_JPS-Nodes/assets/142158778/486e2e32-1a06-4a79-b85d-0d21e4013016)
77
+
78
+ # Credits
79
+
80
+ SDXL Prompt Styler is an extended version of SDXL Prompt Styler by twri - https://github.com/twri/sdxl_prompt_styler
81
+
ComfyUI_JPS-Nodes/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: JPS
3
+ @title: JPS Custom Nodes for ComfyUI
4
+ @nickname: JPS Custom Nodes
5
+ @description: Various nodes to handle SDXL Resolutions, SDXL Basic Settings, IP Adapter Settings, Revision Settings, SDXL Prompt Styler, Crop Image to Square, Crop Image to Target Size, Get Date-Time String, Resolution Multiply, Largest Integer, 5-to-1 Switches for Integer, Images, Latents, Conditioning, Model, VAE, ControlNet
6
+ """
7
+
8
+ from .jps_nodes import NODE_CLASS_MAPPINGS
9
+
10
+ __all__ = ['NODE_CLASS_MAPPINGS']
ComfyUI_JPS-Nodes/jps_nodes.py ADDED
The diff for this file is too large to render. See raw diff
 
ComfyUI_Qwen2-VL-Instruct/.gitignore ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110
+ .pdm.toml
111
+ .pdm-python
112
+ .pdm-build/
113
+
114
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115
+ __pypackages__/
116
+
117
+ # Celery stuff
118
+ celerybeat-schedule
119
+ celerybeat.pid
120
+
121
+ # SageMath parsed files
122
+ *.sage.py
123
+
124
+ # Environments
125
+ .env
126
+ .venv
127
+ env/
128
+ venv/
129
+ ENV/
130
+ env.bak/
131
+ venv.bak/
132
+
133
+ # Spyder project settings
134
+ .spyderproject
135
+ .spyproject
136
+
137
+ # Rope project settings
138
+ .ropeproject
139
+
140
+ # mkdocs documentation
141
+ /site
142
+
143
+ # mypy
144
+ .mypy_cache/
145
+ .dmypy.json
146
+ dmypy.json
147
+
148
+ # Pyre type checker
149
+ .pyre/
150
+
151
+ # pytype static type analyzer
152
+ .pytype/
153
+
154
+ # Cython debug symbols
155
+ cython_debug/
156
+
157
+ # PyCharm
158
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
161
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162
+ #.idea/
ComfyUI_Qwen2-VL-Instruct/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 2024 QwenLM
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.
ComfyUI_Qwen2-VL-Instruct/README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ComfyUI_Qwen2-VL-Instruct
2
+
3
+ This is an implementation of [Qwen2-VL-Instruct](https://github.com/QwenLM/Qwen2-VL) by [ComfyUI](https://github.com/comfyanonymous/ComfyUI), which includes, but is not limited to, support for text-based queries, video queries, single-image queries, and multi-image queries to generate captions or responses.
4
+
5
+ ---
6
+
7
+ ## Basic Workflow
8
+
9
+ - **Text-based Query**: Users can submit textual queries to request information or generate descriptions. For instance, a user might input a description like "What is the meaning of life?"
10
+
11
+ ![Chat_with_text_workflow preview](examples/Chat_with_text_workflow.png)
12
+
13
+ - **Video Query**: When a user uploads a video, the system can analyze the content and generate a detailed caption for each frame or a summary of the entire video. For example, "Generate a caption for the given video."
14
+
15
+ ![Chat_with_video_workflow preview](examples/Chat_with_video_workflow.png)
16
+
17
+ - **Single-Image Query**: This workflow supports generating a caption for an individual image. A user could upload a photo and ask, "What does this image show?" resulting in a caption such as "A majestic lion pride relaxing on the savannah."
18
+
19
+ ![Chat_with_single_image_workflow preview](examples/Chat_with_single_image_workflow.png)
20
+
21
+ - **Multi-Image Query**: For multiple images, the system can provide a collective description or a narrative that ties the images together. For example, "Create a story from the following series of images: one of a couple at a beach, another at a wedding ceremony, and the last one at a baby's christening."
22
+
23
+ ![Chat_with_multiple_images_workflow preview](examples/Chat_with_multiple_images_workflow.png)
24
+
25
+ ## Installation
26
+
27
+ - Install from [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) (search for `Qwen2`)
28
+
29
+ - Download or git clone this repository into the `ComfyUI\custom_nodes\` directory and run:
30
+
31
+ ```python
32
+ pip install -r requirements.txt
33
+ ```
34
+
35
+ ## Download Models
36
+
37
+ All the models will be downloaded automatically when running the workflow if they are not found in the `ComfyUI\models\prompt_generator\` directory.
ComfyUI_Qwen2-VL-Instruct/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .nodes import Qwen2_VQA
2
+ from .util_nodes import ImageLoader
3
+ from .path_nodes import MultiplePathsInput
4
+ WEB_DIRECTORY = "./web"
5
+ # A dictionary that contains all nodes you want to export with their names
6
+ # NOTE: names should be globally unique
7
+ NODE_CLASS_MAPPINGS = {
8
+ "Qwen2_VQA": Qwen2_VQA,
9
+ "ImageLoader": ImageLoader,
10
+ "MultiplePathsInput": MultiplePathsInput,
11
+ }
12
+
13
+ # A dictionary that contains the friendly/humanly readable titles for the nodes
14
+ NODE_DISPLAY_NAME_MAPPINGS = {
15
+ "Qwen2_VQA": "Qwen2 VQA",
16
+ "ImageLoader": "Load Image Advanced",
17
+ "MultiplePathsInput": "Multiple Paths Input",
18
+ }
ComfyUI_Qwen2-VL-Instruct/favicon.ico ADDED
ComfyUI_Qwen2-VL-Instruct/node_helpers.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+
3
+ from comfy.cli_args import args
4
+
5
+ from PIL import ImageFile, UnidentifiedImageError
6
+
7
+ def conditioning_set_values(conditioning, values={}):
8
+ c = []
9
+ for t in conditioning:
10
+ n = [t[0], t[1].copy()]
11
+ for k in values:
12
+ n[1][k] = values[k]
13
+ c.append(n)
14
+
15
+ return c
16
+
17
+ def pillow(fn, arg):
18
+ prev_value = None
19
+ try:
20
+ x = fn(arg)
21
+ except (OSError, UnidentifiedImageError, ValueError): #PIL issues #4472 and #2445, also fixes ComfyUI issue #3416
22
+ prev_value = ImageFile.LOAD_TRUNCATED_IMAGES
23
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
24
+ x = fn(arg)
25
+ finally:
26
+ if prev_value is not None:
27
+ ImageFile.LOAD_TRUNCATED_IMAGES = prev_value
28
+ return x
29
+
30
+ def hasher():
31
+ hashfuncs = {
32
+ "md5": hashlib.md5,
33
+ "sha1": hashlib.sha1,
34
+ "sha256": hashlib.sha256,
35
+ "sha512": hashlib.sha512
36
+ }
37
+ return hashfuncs[args.default_hashing_function]
ComfyUI_Qwen2-VL-Instruct/nodes.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import folder_paths
4
+ from transformers import (
5
+ Qwen2VLForConditionalGeneration,
6
+ AutoProcessor,
7
+ BitsAndBytesConfig,
8
+ )
9
+ from qwen_vl_utils import process_vision_info
10
+
11
+
12
+ class Qwen2_VQA:
13
+ def __init__(self):
14
+ self.model_checkpoint = None
15
+ self.processor = None
16
+ self.model = None
17
+ self.device = (
18
+ torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
19
+ )
20
+ self.bf16_support = (
21
+ torch.cuda.is_available()
22
+ and torch.cuda.get_device_capability(self.device)[0] >= 8
23
+ )
24
+
25
+ @classmethod
26
+ def INPUT_TYPES(s):
27
+ return {
28
+ "required": {
29
+ "text": ("STRING", {"default": "", "multiline": True}),
30
+ "model": (
31
+ [
32
+ "Qwen2-VL-2B-Instruct-GPTQ-Int4",
33
+ "Qwen2-VL-2B-Instruct-GPTQ-Int8",
34
+ "Qwen2-VL-2B-Instruct",
35
+ "Qwen2-VL-7B-Instruct-GPTQ-Int4",
36
+ "Qwen2-VL-7B-Instruct-GPTQ-Int8",
37
+ "Qwen2-VL-7B-Instruct",
38
+ ],
39
+ {"default": "Qwen2-VL-2B-Instruct"},
40
+ ),
41
+ "quantization": (
42
+ ["none", "4bit", "8bit"],
43
+ {"default": "none"},
44
+ ), # add quantization type selection
45
+ "keep_model_loaded": ("BOOLEAN", {"default": False}),
46
+ "temperature": (
47
+ "FLOAT",
48
+ {"default": 0.7, "min": 0, "max": 1, "step": 0.1},
49
+ ),
50
+ "max_new_tokens": (
51
+ "INT",
52
+ {"default": 2048, "min": 128, "max": 2048, "step": 1},
53
+ ),
54
+ "min_pixels": (
55
+ "INT",
56
+ {
57
+ "default": 256 * 28 * 28,
58
+ "min": 4 * 28 * 28,
59
+ "max": 16384 * 28 * 28,
60
+ "step": 28 * 28,
61
+ },
62
+ ),
63
+ "max_pixels": (
64
+ "INT",
65
+ {
66
+ "default": 1280 * 28 * 28,
67
+ "min": 4 * 28 * 28,
68
+ "max": 16384 * 28 * 28,
69
+ "step": 28 * 28,
70
+ },
71
+ ),
72
+ "seed": ("INT", {"default": -1}), # add seed parameter, default is -1
73
+ },
74
+ "optional": {
75
+ "source_path": ("PATH",),
76
+ },
77
+ }
78
+
79
+ RETURN_TYPES = ("STRING",)
80
+ FUNCTION = "inference"
81
+ CATEGORY = "Comfyui_Qwen2-VL-Instruct"
82
+
83
+ def inference(
84
+ self,
85
+ text,
86
+ model,
87
+ keep_model_loaded,
88
+ temperature,
89
+ max_new_tokens,
90
+ min_pixels,
91
+ max_pixels,
92
+ seed,
93
+ quantization,
94
+ source_path=None,
95
+ ):
96
+ if seed != -1:
97
+ torch.manual_seed(seed)
98
+ model_id = f"qwen/{model}"
99
+ self.model_checkpoint = os.path.join(
100
+ folder_paths.models_dir, "prompt_generator", os.path.basename(model_id)
101
+ )
102
+
103
+ if not os.path.exists(self.model_checkpoint):
104
+ from huggingface_hub import snapshot_download
105
+
106
+ snapshot_download(
107
+ repo_id=model_id,
108
+ local_dir=self.model_checkpoint,
109
+ local_dir_use_symlinks=False,
110
+ )
111
+
112
+ if self.processor is None:
113
+ # The default range for the number of visual tokens per image in the model is 4-16384. You can set min_pixels and max_pixels according to your needs, such as a token count range of 256-1280, to balance speed and memory usage.
114
+ self.processor = AutoProcessor.from_pretrained(
115
+ self.model_checkpoint, min_pixels=min_pixels, max_pixels=max_pixels
116
+ )
117
+
118
+ if self.model is None:
119
+ # Load the model on the available device(s)
120
+ if quantization == "4bit":
121
+ quantization_config = BitsAndBytesConfig(
122
+ load_in_4bit=True,
123
+ )
124
+ elif quantization == "8bit":
125
+ quantization_config = BitsAndBytesConfig(
126
+ load_in_8bit=True,
127
+ )
128
+ else:
129
+ quantization_config = None
130
+
131
+ self.model = Qwen2VLForConditionalGeneration.from_pretrained(
132
+ self.model_checkpoint,
133
+ torch_dtype=torch.bfloat16 if self.bf16_support else torch.float16,
134
+ device_map="auto",
135
+ attn_implementation="sdpa",
136
+ quantization_config=quantization_config,
137
+ )
138
+
139
+ with torch.no_grad():
140
+ if source_path:
141
+ messages = [
142
+ {
143
+ "role": "user",
144
+ "content": source_path
145
+ + [
146
+ {"type": "text", "text": text},
147
+ ],
148
+ }
149
+ ]
150
+ else:
151
+ messages = [
152
+ {
153
+ "role": "user",
154
+ "content": [
155
+ {"type": "text", "text": text},
156
+ ],
157
+ }
158
+ ]
159
+ # raise ValueError("Either image or video must be provided")
160
+
161
+ # Preparation for inference
162
+ text = self.processor.apply_chat_template(
163
+ messages, tokenize=False, add_generation_prompt=True
164
+ )
165
+ image_inputs, video_inputs = process_vision_info(messages)
166
+ inputs = self.processor(
167
+ text=[text],
168
+ images=image_inputs,
169
+ videos=video_inputs,
170
+ padding=True,
171
+ return_tensors="pt",
172
+ )
173
+ inputs = inputs.to("cuda")
174
+ # Inference: Generation of the output
175
+ generated_ids = self.model.generate(**inputs, max_new_tokens=max_new_tokens)
176
+ generated_ids_trimmed = [
177
+ out_ids[len(in_ids) :]
178
+ for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
179
+ ]
180
+ result = self.processor.batch_decode(
181
+ generated_ids_trimmed,
182
+ skip_special_tokens=True,
183
+ clean_up_tokenization_spaces=False,
184
+ temperature=temperature,
185
+ )
186
+
187
+ if not keep_model_loaded:
188
+ del self.processor # release processor memory
189
+ del self.model # release model memory
190
+ self.processor = None # set processor to None
191
+ self.model = None # set model to None
192
+ torch.cuda.empty_cache() # release GPU memory
193
+ torch.cuda.ipc_collect()
194
+
195
+ return (result,)
ComfyUI_Qwen2-VL-Instruct/path_nodes.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from decord import VideoReader, cpu # pip install decord
2
+ class MultiplePathsInput:
3
+ @classmethod
4
+ def INPUT_TYPES(s):
5
+ return {
6
+ "required": {
7
+ "inputcount": ("INT", {"default": 1, "min": 1, "max": 1000, "step": 1}),
8
+ "path_1": ("PATH",),
9
+ },
10
+ }
11
+
12
+ RETURN_TYPES = ("PATH",)
13
+ RETURN_NAMES = ("paths",)
14
+ FUNCTION = "combine"
15
+ CATEGORY = "Comfyui_Qwen2-VL-Instruct"
16
+ DESCRIPTION = """
17
+ Creates a path batch from multiple paths.
18
+ You can set how many inputs the node has,
19
+ with the **inputcount** and clicking update.
20
+ """
21
+
22
+
23
+
24
+ @staticmethod
25
+ def convert_path_to_json(file_path):
26
+ ext = file_path.split('.')[-1].lower()
27
+
28
+ if ext in ["jpg", "jpeg", "png", "bmp", "tiff", "webp"]:
29
+ return {"type": "image", "image": f"{file_path}"}
30
+ elif ext in ["mp4", "mkv", "mov", "avi", "flv", "wmv", "webm", "m4v"]:
31
+ print("source_video_path:", file_path)
32
+ vr = VideoReader(file_path, ctx=cpu(0))
33
+ total_frames = len(vr) + 1
34
+ print("Total frames:", total_frames)
35
+ avg_fps = vr.get_avg_fps()
36
+ print("Get average FPS(frame per second):", avg_fps)
37
+ duration = len(vr) / avg_fps
38
+ print("Total duration:", duration, "seconds")
39
+ width = vr[0].shape[1]
40
+ height = vr[0].shape[0]
41
+ print("Video resolution(width x height):", width, "x", height)
42
+ return {
43
+ "type": "video",
44
+ "video": f"{file_path}",
45
+ "fps": 1.0,
46
+ }
47
+ else:
48
+ return None
49
+
50
+
51
+
52
+ def combine(self, inputcount, **kwargs):
53
+ path_list = []
54
+ for c in range(inputcount):
55
+ path = kwargs[f"path_{c + 1}"]
56
+ path = self.convert_path_to_json(path)
57
+ print(path)
58
+ path_list.append(path)
59
+ print(path_list)
60
+ result = path_list
61
+ return (result,)
ComfyUI_Qwen2-VL-Instruct/pyproject.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "ComfyUI_Qwen2-VL-Instruct"
3
+ description = "This is an implementation of [Qwen2-VL-Instruct](https://github.com/QwenLM/Qwen2-VL) by [ComfyUI](https://github.com/comfyanonymous/ComfyUI), which includes, but is not limited to, support for text-based queries, video queries, single-image queries, and multi-image queries to generate captions or responses."
4
+ version = "1.0.0"
5
+ license = "LICENSE"
6
+ dependencies = ["torch", "torchvision", "numpy", "pillow", "huggingface_hub", "transformers", "decord", "bitsandbytes","accelerate","qwen-vl-utils","optimum","av"]
7
+
8
+ [project.urls]
9
+ Repository = "https://github.com/IuvenisSapiens/ComfyUI_Qwen2-VL-Instruct"
10
+
11
+ [tool.comfy]
12
+ PublisherId = "IuvenisSapiens"
13
+ DisplayName = "ComfyUI_Qwen2-VL-Instruct"
14
+ Icon = "favicon.ico"
ComfyUI_Qwen2-VL-Instruct/requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ torchvision
3
+ torchaudio
4
+ numpy
5
+ pillow
6
+ huggingface_hub
7
+ decord
8
+ accelerate
9
+ qwen-vl-utils[decord]
10
+ optimum
11
+ av
12
+ decord
13
+ auto-gptq
14
+ transformers>=4.45.0
ComfyUI_Qwen2-VL-Instruct/util_nodes.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import os
3
+ import folder_paths
4
+ import numpy as np
5
+ import torch
6
+ import node_helpers
7
+ from PIL import Image, ImageOps, ImageSequence
8
+
9
+
10
+ class ImageLoader:
11
+ @classmethod
12
+ def INPUT_TYPES(s):
13
+ input_dir = folder_paths.get_input_directory()
14
+ files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f)) and f.split('.')[-1] in ['jpg', 'jpeg', 'png', 'bmp', 'tiff', 'webp']]
15
+ return {"required":
16
+ {"image": (sorted(files), {"image_upload": True})},
17
+ }
18
+
19
+ CATEGORY = "Comfyui_Qwen2-VL-Instruct"
20
+
21
+ RETURN_TYPES = ("IMAGE", "MASK", "PATH")
22
+ FUNCTION = "load_image"
23
+ def load_image(self, image):
24
+ image_path = folder_paths.get_annotated_filepath(image)
25
+
26
+ img = node_helpers.pillow(Image.open, image_path)
27
+
28
+ output_images = []
29
+ output_masks = []
30
+ w, h = None, None
31
+
32
+ excluded_formats = ['MPO']
33
+
34
+ for i in ImageSequence.Iterator(img):
35
+ i = node_helpers.pillow(ImageOps.exif_transpose, i)
36
+
37
+ if i.mode == 'I':
38
+ i = i.point(lambda i: i * (1 / 255))
39
+ image = i.convert("RGB")
40
+
41
+ if len(output_images) == 0:
42
+ w = image.size[0]
43
+ h = image.size[1]
44
+
45
+ if image.size[0] != w or image.size[1] != h:
46
+ continue
47
+
48
+ image = np.array(image).astype(np.float32) / 255.0
49
+ image = torch.from_numpy(image)[None,]
50
+ if 'A' in i.getbands():
51
+ mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
52
+ mask = 1. - torch.from_numpy(mask)
53
+ else:
54
+ mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
55
+ output_images.append(image)
56
+ output_masks.append(mask.unsqueeze(0))
57
+
58
+ if len(output_images) > 1 and img.format not in excluded_formats:
59
+ output_image = torch.cat(output_images, dim=0)
60
+ output_mask = torch.cat(output_masks, dim=0)
61
+ else:
62
+ output_image = output_images[0]
63
+ output_mask = output_masks[0]
64
+
65
+ return (output_image, output_mask, image_path)
66
+
67
+ @classmethod
68
+ def IS_CHANGED(s, image):
69
+ image_path = folder_paths.get_annotated_filepath(image)
70
+ m = hashlib.sha256()
71
+ with open(image_path, 'rb') as f:
72
+ m.update(f.read())
73
+ return m.digest().hex()
74
+
75
+ @classmethod
76
+ def VALIDATE_INPUTS(s, image):
77
+ if not folder_paths.exists_annotated_filepath(image):
78
+ return "Invalid image file: {}".format(image)
79
+
80
+ return True
ComfyUI_essentials/.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ /__pycache__/
2
+ /luts/*.cube
3
+ /luts/*.CUBE
4
+ /fonts/*.ttf
5
+ /fonts/*.otf
6
+ !/fonts/ShareTechMono-Regular.ttf
ComfyUI_essentials/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Matteo Spinelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ComfyUI_essentials/README.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # :wrench: ComfyUI Essentials
2
+
3
+ Essential nodes that are weirdly missing from ComfyUI core. With few exceptions they are new features and not commodities. I hope this will be just a temporary repository until the nodes get included into ComfyUI.
4
+
5
+ # Sponsorship
6
+
7
+ <div align="center">
8
+
9
+ **[:heart: Github Sponsor](https://github.com/sponsors/cubiq) | [:coin: Paypal](https://paypal.me/matt3o)**
10
+
11
+ </div>
12
+
13
+ If you like my work and wish to see updates and new features please consider sponsoring my projects.
14
+
15
+ - [ComfyUI IPAdapter Plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus)
16
+ - [ComfyUI InstantID (Native)](https://github.com/cubiq/ComfyUI_InstantID)
17
+ - [ComfyUI Essentials](https://github.com/cubiq/ComfyUI_essentials)
18
+ - [ComfyUI FaceAnalysis](https://github.com/cubiq/ComfyUI_FaceAnalysis)
19
+
20
+ Not to mention the documentation and videos tutorials. Check my **ComfyUI Advanced Understanding** videos on YouTube for example, [part 1](https://www.youtube.com/watch?v=_C7kR2TFIX0) and [part 2](https://www.youtube.com/watch?v=ijqXnW_9gzc)
21
+
22
+ The only way to keep the code open and free is by sponsoring its development. The more sponsorships the more time I can dedicate to my open source projects.
23
+
24
+ Please consider a [Github Sponsorship](https://github.com/sponsors/cubiq) or [PayPal donation](https://paypal.me/matt3o) (Matteo "matt3o" Spinelli). For sponsorships of $50+, let me know if you'd like to be mentioned in this readme file, you can find me on [Discord](https://latent.vision/discord) or _matt3o :snail: gmail.com_.
25
+
26
+ ## Current sponsors
27
+
28
+ It's only thanks to generous sponsors that **the whole community** can enjoy open and free software. Please join me in thanking the following companies and individuals!
29
+
30
+ ### :trophy: Gold sponsors
31
+
32
+ [![Kaiber.ai](https://f.latent.vision/imgs/kaiber.png)](https://kaiber.ai/)&nbsp; &nbsp;[![InstaSD](https://f.latent.vision/imgs/instasd.png)](https://www.instasd.com/)
33
+
34
+ ### :tada: Silver sponsors
35
+
36
+ [![OperArt.ai](https://f.latent.vision/imgs/openart.png?r=1)](https://openart.ai/workflows)&nbsp; &nbsp;[![Finetuners](https://f.latent.vision/imgs/finetuners.png)](https://www.finetuners.ai/)&nbsp; &nbsp;[![Comfy.ICU](https://f.latent.vision/imgs/comfyicu.png?r=1)](https://comfy.icu/)
37
+
38
+ ### Other companies supporting my projects
39
+
40
+ - [RunComfy](https://www.runcomfy.com/) (ComfyUI Cloud)
41
+
42
+ ### Esteemed individuals
43
+
44
+ - [Øystein Ø. Olsen](https://github.com/FireNeslo)
45
+ - [Jack Gane](https://github.com/ganeJackS)
46
+ - [Nathan Shipley](https://www.nathanshipley.com/)
47
+ - [Dkdnzia](https://github.com/Dkdnzia)
48
+
49
+ [And all my public and private sponsors!](https://github.com/sponsors/cubiq)
ComfyUI_essentials/__init__.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #from .essentials import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
2
+ from .image import IMAGE_CLASS_MAPPINGS, IMAGE_NAME_MAPPINGS
3
+ from .mask import MASK_CLASS_MAPPINGS, MASK_NAME_MAPPINGS
4
+ from .sampling import SAMPLING_CLASS_MAPPINGS, SAMPLING_NAME_MAPPINGS
5
+ from .segmentation import SEG_CLASS_MAPPINGS, SEG_NAME_MAPPINGS
6
+ from .misc import MISC_CLASS_MAPPINGS, MISC_NAME_MAPPINGS
7
+ from .conditioning import COND_CLASS_MAPPINGS, COND_NAME_MAPPINGS
8
+ from .text import TEXT_CLASS_MAPPINGS, TEXT_NAME_MAPPINGS
9
+
10
+ WEB_DIRECTORY = "./js"
11
+
12
+ NODE_CLASS_MAPPINGS = {}
13
+ NODE_DISPLAY_NAME_MAPPINGS = {}
14
+
15
+ NODE_CLASS_MAPPINGS.update(COND_CLASS_MAPPINGS)
16
+ NODE_DISPLAY_NAME_MAPPINGS.update(COND_NAME_MAPPINGS)
17
+
18
+ NODE_CLASS_MAPPINGS.update(IMAGE_CLASS_MAPPINGS)
19
+ NODE_DISPLAY_NAME_MAPPINGS.update(IMAGE_NAME_MAPPINGS)
20
+
21
+ NODE_CLASS_MAPPINGS.update(MASK_CLASS_MAPPINGS)
22
+ NODE_DISPLAY_NAME_MAPPINGS.update(MASK_NAME_MAPPINGS)
23
+
24
+ NODE_CLASS_MAPPINGS.update(SAMPLING_CLASS_MAPPINGS)
25
+ NODE_DISPLAY_NAME_MAPPINGS.update(SAMPLING_NAME_MAPPINGS)
26
+
27
+ NODE_CLASS_MAPPINGS.update(SEG_CLASS_MAPPINGS)
28
+ NODE_DISPLAY_NAME_MAPPINGS.update(SEG_NAME_MAPPINGS)
29
+
30
+ NODE_CLASS_MAPPINGS.update(TEXT_CLASS_MAPPINGS)
31
+ NODE_DISPLAY_NAME_MAPPINGS.update(TEXT_NAME_MAPPINGS)
32
+
33
+ NODE_CLASS_MAPPINGS.update(MISC_CLASS_MAPPINGS)
34
+ NODE_DISPLAY_NAME_MAPPINGS.update(MISC_NAME_MAPPINGS)
35
+
36
+ __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', "WEB_DIRECTORY"]
ComfyUI_essentials/carve.py ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT licensed code from https://github.com/li-plus/seam-carving/
2
+
3
+ from enum import Enum
4
+ from typing import Optional, Tuple
5
+
6
+ import numba as nb
7
+ import numpy as np
8
+ from scipy.ndimage import sobel
9
+
10
+ DROP_MASK_ENERGY = 1e5
11
+ KEEP_MASK_ENERGY = 1e3
12
+
13
+
14
+ class OrderMode(str, Enum):
15
+ WIDTH_FIRST = "width-first"
16
+ HEIGHT_FIRST = "height-first"
17
+
18
+
19
+ class EnergyMode(str, Enum):
20
+ FORWARD = "forward"
21
+ BACKWARD = "backward"
22
+
23
+
24
+ def _list_enum(enum_class) -> Tuple:
25
+ return tuple(x.value for x in enum_class)
26
+
27
+
28
+ def _rgb2gray(rgb: np.ndarray) -> np.ndarray:
29
+ """Convert an RGB image to a grayscale image"""
30
+ coeffs = np.array([0.2125, 0.7154, 0.0721], dtype=np.float32)
31
+ return (rgb @ coeffs).astype(rgb.dtype)
32
+
33
+
34
+ def _get_seam_mask(src: np.ndarray, seam: np.ndarray) -> np.ndarray:
35
+ """Convert a list of seam column indices to a mask"""
36
+ return np.eye(src.shape[1], dtype=bool)[seam]
37
+
38
+
39
+ def _remove_seam_mask(src: np.ndarray, seam_mask: np.ndarray) -> np.ndarray:
40
+ """Remove a seam from the source image according to the given seam_mask"""
41
+ if src.ndim == 3:
42
+ h, w, c = src.shape
43
+ seam_mask = np.broadcast_to(seam_mask[:, :, None], src.shape)
44
+ dst = src[~seam_mask].reshape((h, w - 1, c))
45
+ else:
46
+ h, w = src.shape
47
+ dst = src[~seam_mask].reshape((h, w - 1))
48
+ return dst
49
+
50
+
51
+ def _get_energy(gray: np.ndarray) -> np.ndarray:
52
+ """Get backward energy map from the source image"""
53
+ assert gray.ndim == 2
54
+
55
+ gray = gray.astype(np.float32)
56
+ grad_x = sobel(gray, axis=1)
57
+ grad_y = sobel(gray, axis=0)
58
+ energy = np.abs(grad_x) + np.abs(grad_y)
59
+ return energy
60
+
61
+
62
+ @nb.njit(nb.int32[:](nb.float32[:, :]), cache=True)
63
+ def _get_backward_seam(energy: np.ndarray) -> np.ndarray:
64
+ """Compute the minimum vertical seam from the backward energy map"""
65
+ h, w = energy.shape
66
+ inf = np.array([np.inf], dtype=np.float32)
67
+ cost = np.concatenate((inf, energy[0], inf))
68
+ parent = np.empty((h, w), dtype=np.int32)
69
+ base_idx = np.arange(-1, w - 1, dtype=np.int32)
70
+
71
+ for r in range(1, h):
72
+ choices = np.vstack((cost[:-2], cost[1:-1], cost[2:]))
73
+ min_idx = np.argmin(choices, axis=0) + base_idx
74
+ parent[r] = min_idx
75
+ cost[1:-1] = cost[1:-1][min_idx] + energy[r]
76
+
77
+ c = np.argmin(cost[1:-1])
78
+ seam = np.empty(h, dtype=np.int32)
79
+ for r in range(h - 1, -1, -1):
80
+ seam[r] = c
81
+ c = parent[r, c]
82
+
83
+ return seam
84
+
85
+
86
+ def _get_backward_seams(
87
+ gray: np.ndarray, num_seams: int, aux_energy: Optional[np.ndarray]
88
+ ) -> np.ndarray:
89
+ """Compute the minimum N vertical seams using backward energy"""
90
+ h, w = gray.shape
91
+ seams = np.zeros((h, w), dtype=bool)
92
+ rows = np.arange(h, dtype=np.int32)
93
+ idx_map = np.broadcast_to(np.arange(w, dtype=np.int32), (h, w))
94
+ energy = _get_energy(gray)
95
+ if aux_energy is not None:
96
+ energy += aux_energy
97
+ for _ in range(num_seams):
98
+ seam = _get_backward_seam(energy)
99
+ seams[rows, idx_map[rows, seam]] = True
100
+
101
+ seam_mask = _get_seam_mask(gray, seam)
102
+ gray = _remove_seam_mask(gray, seam_mask)
103
+ idx_map = _remove_seam_mask(idx_map, seam_mask)
104
+ if aux_energy is not None:
105
+ aux_energy = _remove_seam_mask(aux_energy, seam_mask)
106
+
107
+ # Only need to re-compute the energy in the bounding box of the seam
108
+ _, cur_w = energy.shape
109
+ lo = max(0, np.min(seam) - 1)
110
+ hi = min(cur_w, np.max(seam) + 1)
111
+ pad_lo = 1 if lo > 0 else 0
112
+ pad_hi = 1 if hi < cur_w - 1 else 0
113
+ mid_block = gray[:, lo - pad_lo : hi + pad_hi]
114
+ _, mid_w = mid_block.shape
115
+ mid_energy = _get_energy(mid_block)[:, pad_lo : mid_w - pad_hi]
116
+ if aux_energy is not None:
117
+ mid_energy += aux_energy[:, lo:hi]
118
+ energy = np.hstack((energy[:, :lo], mid_energy, energy[:, hi + 1 :]))
119
+
120
+ return seams
121
+
122
+
123
+ @nb.njit(
124
+ [
125
+ nb.int32[:](nb.float32[:, :], nb.none),
126
+ nb.int32[:](nb.float32[:, :], nb.float32[:, :]),
127
+ ],
128
+ cache=True,
129
+ )
130
+ def _get_forward_seam(gray: np.ndarray, aux_energy: Optional[np.ndarray]) -> np.ndarray:
131
+ """Compute the minimum vertical seam using forward energy"""
132
+ h, w = gray.shape
133
+
134
+ gray = np.hstack((gray[:, :1], gray, gray[:, -1:]))
135
+
136
+ inf = np.array([np.inf], dtype=np.float32)
137
+ dp = np.concatenate((inf, np.abs(gray[0, 2:] - gray[0, :-2]), inf))
138
+
139
+ parent = np.empty((h, w), dtype=np.int32)
140
+ base_idx = np.arange(-1, w - 1, dtype=np.int32)
141
+
142
+ inf = np.array([np.inf], dtype=np.float32)
143
+ for r in range(1, h):
144
+ curr_shl = gray[r, 2:]
145
+ curr_shr = gray[r, :-2]
146
+ cost_mid = np.abs(curr_shl - curr_shr)
147
+ if aux_energy is not None:
148
+ cost_mid += aux_energy[r]
149
+
150
+ prev_mid = gray[r - 1, 1:-1]
151
+ cost_left = cost_mid + np.abs(prev_mid - curr_shr)
152
+ cost_right = cost_mid + np.abs(prev_mid - curr_shl)
153
+
154
+ dp_mid = dp[1:-1]
155
+ dp_left = dp[:-2]
156
+ dp_right = dp[2:]
157
+
158
+ choices = np.vstack(
159
+ (cost_left + dp_left, cost_mid + dp_mid, cost_right + dp_right)
160
+ )
161
+ min_idx = np.argmin(choices, axis=0)
162
+ parent[r] = min_idx + base_idx
163
+ # numba does not support specifying axis in np.min, below loop is equivalent to:
164
+ # `dp_mid[:] = np.min(choices, axis=0)` or `dp_mid[:] = choices[min_idx, np.arange(w)]`
165
+ for j, i in enumerate(min_idx):
166
+ dp_mid[j] = choices[i, j]
167
+
168
+ c = np.argmin(dp[1:-1])
169
+ seam = np.empty(h, dtype=np.int32)
170
+ for r in range(h - 1, -1, -1):
171
+ seam[r] = c
172
+ c = parent[r, c]
173
+
174
+ return seam
175
+
176
+
177
+ def _get_forward_seams(
178
+ gray: np.ndarray, num_seams: int, aux_energy: Optional[np.ndarray]
179
+ ) -> np.ndarray:
180
+ """Compute minimum N vertical seams using forward energy"""
181
+ h, w = gray.shape
182
+ seams = np.zeros((h, w), dtype=bool)
183
+ rows = np.arange(h, dtype=np.int32)
184
+ idx_map = np.broadcast_to(np.arange(w, dtype=np.int32), (h, w))
185
+ for _ in range(num_seams):
186
+ seam = _get_forward_seam(gray, aux_energy)
187
+ seams[rows, idx_map[rows, seam]] = True
188
+ seam_mask = _get_seam_mask(gray, seam)
189
+ gray = _remove_seam_mask(gray, seam_mask)
190
+ idx_map = _remove_seam_mask(idx_map, seam_mask)
191
+ if aux_energy is not None:
192
+ aux_energy = _remove_seam_mask(aux_energy, seam_mask)
193
+
194
+ return seams
195
+
196
+
197
+ def _get_seams(
198
+ gray: np.ndarray, num_seams: int, energy_mode: str, aux_energy: Optional[np.ndarray]
199
+ ) -> np.ndarray:
200
+ """Get the minimum N seams from the grayscale image"""
201
+ gray = np.asarray(gray, dtype=np.float32)
202
+ if energy_mode == EnergyMode.BACKWARD:
203
+ return _get_backward_seams(gray, num_seams, aux_energy)
204
+ elif energy_mode == EnergyMode.FORWARD:
205
+ return _get_forward_seams(gray, num_seams, aux_energy)
206
+ else:
207
+ raise ValueError(
208
+ f"expect energy_mode to be one of {_list_enum(EnergyMode)}, got {energy_mode}"
209
+ )
210
+
211
+
212
+ def _reduce_width(
213
+ src: np.ndarray,
214
+ delta_width: int,
215
+ energy_mode: str,
216
+ aux_energy: Optional[np.ndarray],
217
+ ) -> Tuple[np.ndarray, Optional[np.ndarray]]:
218
+ """Reduce the width of image by delta_width pixels"""
219
+ assert src.ndim in (2, 3) and delta_width >= 0
220
+ if src.ndim == 2:
221
+ gray = src
222
+ src_h, src_w = src.shape
223
+ dst_shape: Tuple[int, ...] = (src_h, src_w - delta_width)
224
+ else:
225
+ gray = _rgb2gray(src)
226
+ src_h, src_w, src_c = src.shape
227
+ dst_shape = (src_h, src_w - delta_width, src_c)
228
+
229
+ to_keep = ~_get_seams(gray, delta_width, energy_mode, aux_energy)
230
+ dst = src[to_keep].reshape(dst_shape)
231
+ if aux_energy is not None:
232
+ aux_energy = aux_energy[to_keep].reshape(dst_shape[:2])
233
+ return dst, aux_energy
234
+
235
+
236
+ @nb.njit(
237
+ nb.float32[:, :, :](nb.float32[:, :, :], nb.boolean[:, :], nb.int32), cache=True
238
+ )
239
+ def _insert_seams_kernel(
240
+ src: np.ndarray, seams: np.ndarray, delta_width: int
241
+ ) -> np.ndarray:
242
+ """The numba kernel for inserting seams"""
243
+ src_h, src_w, src_c = src.shape
244
+ dst = np.empty((src_h, src_w + delta_width, src_c), dtype=src.dtype)
245
+ for row in range(src_h):
246
+ dst_col = 0
247
+ for src_col in range(src_w):
248
+ if seams[row, src_col]:
249
+ left = src[row, max(src_col - 1, 0)]
250
+ right = src[row, src_col]
251
+ dst[row, dst_col] = (left + right) / 2
252
+ dst_col += 1
253
+ dst[row, dst_col] = src[row, src_col]
254
+ dst_col += 1
255
+ return dst
256
+
257
+
258
+ def _insert_seams(src: np.ndarray, seams: np.ndarray, delta_width: int) -> np.ndarray:
259
+ """Insert multiple seams into the source image"""
260
+ dst = src.astype(np.float32)
261
+ if dst.ndim == 2:
262
+ dst = dst[:, :, None]
263
+ dst = _insert_seams_kernel(dst, seams, delta_width).astype(src.dtype)
264
+ if src.ndim == 2:
265
+ dst = dst.squeeze(-1)
266
+ return dst
267
+
268
+
269
+ def _expand_width(
270
+ src: np.ndarray,
271
+ delta_width: int,
272
+ energy_mode: str,
273
+ aux_energy: Optional[np.ndarray],
274
+ step_ratio: float,
275
+ ) -> Tuple[np.ndarray, Optional[np.ndarray]]:
276
+ """Expand the width of image by delta_width pixels"""
277
+ assert src.ndim in (2, 3) and delta_width >= 0
278
+ if not 0 < step_ratio <= 1:
279
+ raise ValueError(f"expect `step_ratio` to be between (0,1], got {step_ratio}")
280
+
281
+ dst = src
282
+ while delta_width > 0:
283
+ max_step_size = max(1, round(step_ratio * dst.shape[1]))
284
+ step_size = min(max_step_size, delta_width)
285
+ gray = dst if dst.ndim == 2 else _rgb2gray(dst)
286
+ seams = _get_seams(gray, step_size, energy_mode, aux_energy)
287
+ dst = _insert_seams(dst, seams, step_size)
288
+ if aux_energy is not None:
289
+ aux_energy = _insert_seams(aux_energy, seams, step_size)
290
+ delta_width -= step_size
291
+
292
+ return dst, aux_energy
293
+
294
+
295
+ def _resize_width(
296
+ src: np.ndarray,
297
+ width: int,
298
+ energy_mode: str,
299
+ aux_energy: Optional[np.ndarray],
300
+ step_ratio: float,
301
+ ) -> Tuple[np.ndarray, Optional[np.ndarray]]:
302
+ """Resize the width of image by removing vertical seams"""
303
+ assert src.size > 0 and src.ndim in (2, 3)
304
+ assert width > 0
305
+
306
+ src_w = src.shape[1]
307
+ if src_w < width:
308
+ dst, aux_energy = _expand_width(
309
+ src, width - src_w, energy_mode, aux_energy, step_ratio
310
+ )
311
+ else:
312
+ dst, aux_energy = _reduce_width(src, src_w - width, energy_mode, aux_energy)
313
+ return dst, aux_energy
314
+
315
+
316
+ def _transpose_image(src: np.ndarray) -> np.ndarray:
317
+ """Transpose a source image in rgb or grayscale format"""
318
+ if src.ndim == 3:
319
+ dst = src.transpose((1, 0, 2))
320
+ else:
321
+ dst = src.T
322
+ return dst
323
+
324
+
325
+ def _resize_height(
326
+ src: np.ndarray,
327
+ height: int,
328
+ energy_mode: str,
329
+ aux_energy: Optional[np.ndarray],
330
+ step_ratio: float,
331
+ ) -> Tuple[np.ndarray, Optional[np.ndarray]]:
332
+ """Resize the height of image by removing horizontal seams"""
333
+ assert src.ndim in (2, 3) and height > 0
334
+ if aux_energy is not None:
335
+ aux_energy = aux_energy.T
336
+ src = _transpose_image(src)
337
+ src, aux_energy = _resize_width(src, height, energy_mode, aux_energy, step_ratio)
338
+ src = _transpose_image(src)
339
+ if aux_energy is not None:
340
+ aux_energy = aux_energy.T
341
+ return src, aux_energy
342
+
343
+
344
+ def _check_mask(mask: np.ndarray, shape: Tuple[int, ...]) -> np.ndarray:
345
+ """Ensure the mask to be a 2D grayscale map of specific shape"""
346
+ mask = np.asarray(mask, dtype=bool)
347
+ if mask.ndim != 2:
348
+ raise ValueError(f"expect mask to be a 2d binary map, got shape {mask.shape}")
349
+ if mask.shape != shape:
350
+ raise ValueError(
351
+ f"expect the shape of mask to match the image, got {mask.shape} vs {shape}"
352
+ )
353
+ return mask
354
+
355
+
356
+ def _check_src(src: np.ndarray) -> np.ndarray:
357
+ """Ensure the source to be RGB or grayscale"""
358
+ src = np.asarray(src)
359
+ if src.size == 0 or src.ndim not in (2, 3):
360
+ raise ValueError(
361
+ f"expect a 3d rgb image or a 2d grayscale image, got image in shape {src.shape}"
362
+ )
363
+ return src
364
+
365
+
366
+ def seam_carving(
367
+ src: np.ndarray,
368
+ size: Optional[Tuple[int, int]] = None,
369
+ energy_mode: str = "backward",
370
+ order: str = "width-first",
371
+ keep_mask: Optional[np.ndarray] = None,
372
+ drop_mask: Optional[np.ndarray] = None,
373
+ step_ratio: float = 0.5,
374
+ ) -> np.ndarray:
375
+ """Resize the image using the content-aware seam-carving algorithm.
376
+
377
+ :param src: A source image in RGB or grayscale format.
378
+ :param size: The target size in pixels, as a 2-tuple (width, height).
379
+ :param energy_mode: Policy to compute energy for the source image. Could be
380
+ one of ``backward`` or ``forward``. If ``backward``, compute the energy
381
+ as the gradient at each pixel. If ``forward``, compute the energy as the
382
+ distances between adjacent pixels after each pixel is removed.
383
+ :param order: The order to remove horizontal and vertical seams. Could be
384
+ one of ``width-first`` or ``height-first``. In ``width-first`` mode, we
385
+ remove or insert all vertical seams first, then the horizontal ones,
386
+ while ``height-first`` is the opposite.
387
+ :param keep_mask: An optional mask where the foreground is protected from
388
+ seam removal. If not specified, no area will be protected.
389
+ :param drop_mask: An optional binary object mask to remove. If given, the
390
+ object will be removed before resizing the image to the target size.
391
+ :param step_ratio: The maximum size expansion ratio in one seam carving step.
392
+ The image will be expanded in multiple steps if target size is too large.
393
+ :return: A resized copy of the source image.
394
+ """
395
+ src = _check_src(src)
396
+
397
+ if order not in _list_enum(OrderMode):
398
+ raise ValueError(
399
+ f"expect order to be one of {_list_enum(OrderMode)}, got {order}"
400
+ )
401
+
402
+ aux_energy = None
403
+
404
+ if keep_mask is not None:
405
+ keep_mask = _check_mask(keep_mask, src.shape[:2])
406
+
407
+ aux_energy = np.zeros(src.shape[:2], dtype=np.float32)
408
+ aux_energy[keep_mask] += KEEP_MASK_ENERGY
409
+
410
+ # remove object if `drop_mask` is given
411
+ if drop_mask is not None:
412
+ drop_mask = _check_mask(drop_mask, src.shape[:2])
413
+
414
+ if aux_energy is None:
415
+ aux_energy = np.zeros(src.shape[:2], dtype=np.float32)
416
+ aux_energy[drop_mask] -= DROP_MASK_ENERGY
417
+
418
+ if order == OrderMode.HEIGHT_FIRST:
419
+ src = _transpose_image(src)
420
+ aux_energy = aux_energy.T
421
+
422
+ num_seams = (aux_energy < 0).sum(1).max()
423
+ while num_seams > 0:
424
+ src, aux_energy = _reduce_width(src, num_seams, energy_mode, aux_energy)
425
+ num_seams = (aux_energy < 0).sum(1).max()
426
+
427
+ if order == OrderMode.HEIGHT_FIRST:
428
+ src = _transpose_image(src)
429
+ aux_energy = aux_energy.T
430
+
431
+ # resize image if `size` is given
432
+ if size is not None:
433
+ width, height = size
434
+ width = round(width)
435
+ height = round(height)
436
+ if width <= 0 or height <= 0:
437
+ raise ValueError(f"expect target size to be positive, got {size}")
438
+
439
+ if order == OrderMode.WIDTH_FIRST:
440
+ src, aux_energy = _resize_width(
441
+ src, width, energy_mode, aux_energy, step_ratio
442
+ )
443
+ src, aux_energy = _resize_height(
444
+ src, height, energy_mode, aux_energy, step_ratio
445
+ )
446
+ else:
447
+ src, aux_energy = _resize_height(
448
+ src, height, energy_mode, aux_energy, step_ratio
449
+ )
450
+ src, aux_energy = _resize_width(
451
+ src, width, energy_mode, aux_energy, step_ratio
452
+ )
453
+
454
+ return src
ComfyUI_essentials/conditioning.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from nodes import MAX_RESOLUTION, ConditioningZeroOut, ConditioningSetTimestepRange, ConditioningCombine
2
+ import re
3
+
4
+ class CLIPTextEncodeSDXLSimplified:
5
+ @classmethod
6
+ def INPUT_TYPES(s):
7
+ return {"required": {
8
+ "width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
9
+ "height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
10
+ "size_cond_factor": ("INT", {"default": 4, "min": 1, "max": 16 }),
11
+ "text": ("STRING", {"multiline": True, "dynamicPrompts": True, "default": ""}),
12
+ "clip": ("CLIP", ),
13
+ }}
14
+ RETURN_TYPES = ("CONDITIONING",)
15
+ FUNCTION = "execute"
16
+ CATEGORY = "essentials/conditioning"
17
+
18
+ def execute(self, clip, width, height, size_cond_factor, text):
19
+ crop_w = 0
20
+ crop_h = 0
21
+ width = width*size_cond_factor
22
+ height = height*size_cond_factor
23
+ target_width = width
24
+ target_height = height
25
+ text_g = text_l = text
26
+
27
+ tokens = clip.tokenize(text_g)
28
+ tokens["l"] = clip.tokenize(text_l)["l"]
29
+ if len(tokens["l"]) != len(tokens["g"]):
30
+ empty = clip.tokenize("")
31
+ while len(tokens["l"]) < len(tokens["g"]):
32
+ tokens["l"] += empty["l"]
33
+ while len(tokens["l"]) > len(tokens["g"]):
34
+ tokens["g"] += empty["g"]
35
+ cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
36
+ return ([[cond, {"pooled_output": pooled, "width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}]], )
37
+
38
+ class ConditioningCombineMultiple:
39
+ @classmethod
40
+ def INPUT_TYPES(s):
41
+ return {
42
+ "required": {
43
+ "conditioning_1": ("CONDITIONING",),
44
+ "conditioning_2": ("CONDITIONING",),
45
+ }, "optional": {
46
+ "conditioning_3": ("CONDITIONING",),
47
+ "conditioning_4": ("CONDITIONING",),
48
+ "conditioning_5": ("CONDITIONING",),
49
+ },
50
+ }
51
+ RETURN_TYPES = ("CONDITIONING",)
52
+ FUNCTION = "execute"
53
+ CATEGORY = "essentials/conditioning"
54
+
55
+ def execute(self, conditioning_1, conditioning_2, conditioning_3=None, conditioning_4=None, conditioning_5=None):
56
+ c = conditioning_1 + conditioning_2
57
+
58
+ if conditioning_3 is not None:
59
+ c += conditioning_3
60
+ if conditioning_4 is not None:
61
+ c += conditioning_4
62
+ if conditioning_5 is not None:
63
+ c += conditioning_5
64
+
65
+ return (c,)
66
+
67
+ class SD3NegativeConditioning:
68
+ @classmethod
69
+ def INPUT_TYPES(s):
70
+ return {"required": {
71
+ "conditioning": ("CONDITIONING",),
72
+ "end": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.001 }),
73
+ }}
74
+ RETURN_TYPES = ("CONDITIONING",)
75
+ FUNCTION = "execute"
76
+ CATEGORY = "essentials/conditioning"
77
+
78
+ def execute(self, conditioning, end):
79
+ zero_c = ConditioningZeroOut().zero_out(conditioning)[0]
80
+
81
+ if end == 0:
82
+ return (zero_c, )
83
+
84
+ c = ConditioningSetTimestepRange().set_range(conditioning, 0, end)[0]
85
+ zero_c = ConditioningSetTimestepRange().set_range(zero_c, end, 1.0)[0]
86
+ c = ConditioningCombine().combine(zero_c, c)[0]
87
+
88
+ return (c, )
89
+
90
+ class FluxAttentionSeeker:
91
+ @classmethod
92
+ def INPUT_TYPES(s):
93
+ return {"required": {
94
+ "clip": ("CLIP",),
95
+ "apply_to_query": ("BOOLEAN", { "default": True }),
96
+ "apply_to_key": ("BOOLEAN", { "default": True }),
97
+ "apply_to_value": ("BOOLEAN", { "default": True }),
98
+ "apply_to_out": ("BOOLEAN", { "default": True }),
99
+ **{f"clip_l_{s}": ("FLOAT", { "display": "slider", "default": 1.0, "min": 0, "max": 5, "step": 0.05 }) for s in range(12)},
100
+ **{f"t5xxl_{s}": ("FLOAT", { "display": "slider", "default": 1.0, "min": 0, "max": 5, "step": 0.05 }) for s in range(24)},
101
+ }}
102
+
103
+ RETURN_TYPES = ("CLIP",)
104
+ FUNCTION = "execute"
105
+
106
+ CATEGORY = "essentials/conditioning"
107
+
108
+ def execute(self, clip, apply_to_query, apply_to_key, apply_to_value, apply_to_out, **values):
109
+ if not apply_to_key and not apply_to_query and not apply_to_value and not apply_to_out:
110
+ return (clip, )
111
+
112
+ m = clip.clone()
113
+ sd = m.patcher.model_state_dict()
114
+
115
+ for k in sd:
116
+ if "self_attn" in k:
117
+ layer = re.search(r"\.layers\.(\d+)\.", k)
118
+ layer = int(layer.group(1)) if layer else None
119
+
120
+ if layer is not None and values[f"clip_l_{layer}"] != 1.0:
121
+ if (apply_to_query and "q_proj" in k) or (apply_to_key and "k_proj" in k) or (apply_to_value and "v_proj" in k) or (apply_to_out and "out_proj" in k):
122
+ m.add_patches({k: (None,)}, 0.0, values[f"clip_l_{layer}"])
123
+ elif "SelfAttention" in k:
124
+ block = re.search(r"\.block\.(\d+)\.", k)
125
+ block = int(block.group(1)) if block else None
126
+
127
+ if block is not None and values[f"t5xxl_{block}"] != 1.0:
128
+ if (apply_to_query and ".q." in k) or (apply_to_key and ".k." in k) or (apply_to_value and ".v." in k) or (apply_to_out and ".o." in k):
129
+ m.add_patches({k: (None,)}, 0.0, values[f"t5xxl_{block}"])
130
+
131
+ return (m, )
132
+
133
+ class SD3AttentionSeekerLG:
134
+ @classmethod
135
+ def INPUT_TYPES(s):
136
+ return {"required": {
137
+ "clip": ("CLIP",),
138
+ "apply_to_query": ("BOOLEAN", { "default": True }),
139
+ "apply_to_key": ("BOOLEAN", { "default": True }),
140
+ "apply_to_value": ("BOOLEAN", { "default": True }),
141
+ "apply_to_out": ("BOOLEAN", { "default": True }),
142
+ **{f"clip_l_{s}": ("FLOAT", { "display": "slider", "default": 1.0, "min": 0, "max": 5, "step": 0.05 }) for s in range(12)},
143
+ **{f"clip_g_{s}": ("FLOAT", { "display": "slider", "default": 1.0, "min": 0, "max": 5, "step": 0.05 }) for s in range(32)},
144
+ }}
145
+
146
+ RETURN_TYPES = ("CLIP",)
147
+ FUNCTION = "execute"
148
+
149
+ CATEGORY = "essentials/conditioning"
150
+
151
+ def execute(self, clip, apply_to_query, apply_to_key, apply_to_value, apply_to_out, **values):
152
+ if not apply_to_key and not apply_to_query and not apply_to_value and not apply_to_out:
153
+ return (clip, )
154
+
155
+ m = clip.clone()
156
+ sd = m.patcher.model_state_dict()
157
+
158
+ for k in sd:
159
+ if "self_attn" in k:
160
+ layer = re.search(r"\.layers\.(\d+)\.", k)
161
+ layer = int(layer.group(1)) if layer else None
162
+
163
+ if layer is not None:
164
+ if "clip_l" in k and values[f"clip_l_{layer}"] != 1.0:
165
+ if (apply_to_query and "q_proj" in k) or (apply_to_key and "k_proj" in k) or (apply_to_value and "v_proj" in k) or (apply_to_out and "out_proj" in k):
166
+ m.add_patches({k: (None,)}, 0.0, values[f"clip_l_{layer}"])
167
+ elif "clip_g" in k and values[f"clip_g_{layer}"] != 1.0:
168
+ if (apply_to_query and "q_proj" in k) or (apply_to_key and "k_proj" in k) or (apply_to_value and "v_proj" in k) or (apply_to_out and "out_proj" in k):
169
+ m.add_patches({k: (None,)}, 0.0, values[f"clip_g_{layer}"])
170
+
171
+ return (m, )
172
+
173
+ class SD3AttentionSeekerT5:
174
+ @classmethod
175
+ def INPUT_TYPES(s):
176
+ return {"required": {
177
+ "clip": ("CLIP",),
178
+ "apply_to_query": ("BOOLEAN", { "default": True }),
179
+ "apply_to_key": ("BOOLEAN", { "default": True }),
180
+ "apply_to_value": ("BOOLEAN", { "default": True }),
181
+ "apply_to_out": ("BOOLEAN", { "default": True }),
182
+ **{f"t5xxl_{s}": ("FLOAT", { "display": "slider", "default": 1.0, "min": 0, "max": 5, "step": 0.05 }) for s in range(24)},
183
+ }}
184
+
185
+ RETURN_TYPES = ("CLIP",)
186
+ FUNCTION = "execute"
187
+
188
+ CATEGORY = "essentials/conditioning"
189
+
190
+ def execute(self, clip, apply_to_query, apply_to_key, apply_to_value, apply_to_out, **values):
191
+ if not apply_to_key and not apply_to_query and not apply_to_value and not apply_to_out:
192
+ return (clip, )
193
+
194
+ m = clip.clone()
195
+ sd = m.patcher.model_state_dict()
196
+
197
+ for k in sd:
198
+ if "SelfAttention" in k:
199
+ block = re.search(r"\.block\.(\d+)\.", k)
200
+ block = int(block.group(1)) if block else None
201
+
202
+ if block is not None and values[f"t5xxl_{block}"] != 1.0:
203
+ if (apply_to_query and ".q." in k) or (apply_to_key and ".k." in k) or (apply_to_value and ".v." in k) or (apply_to_out and ".o." in k):
204
+ m.add_patches({k: (None,)}, 0.0, values[f"t5xxl_{block}"])
205
+
206
+ return (m, )
207
+
208
+ class FluxBlocksBuster:
209
+ @classmethod
210
+ def INPUT_TYPES(s):
211
+ return {"required": {
212
+ "model": ("MODEL",),
213
+ "blocks": ("STRING", {"default": "## 0 = 1.0\n## 1 = 1.0\n## 2 = 1.0\n## 3 = 1.0\n## 4 = 1.0\n## 5 = 1.0\n## 6 = 1.0\n## 7 = 1.0\n## 8 = 1.0\n## 9 = 1.0\n## 10 = 1.0\n## 11 = 1.0\n## 12 = 1.0\n## 13 = 1.0\n## 14 = 1.0\n## 15 = 1.0\n## 16 = 1.0\n## 17 = 1.0\n## 18 = 1.0\n# 0 = 1.0\n# 1 = 1.0\n# 2 = 1.0\n# 3 = 1.0\n# 4 = 1.0\n# 5 = 1.0\n# 6 = 1.0\n# 7 = 1.0\n# 8 = 1.0\n# 9 = 1.0\n# 10 = 1.0\n# 11 = 1.0\n# 12 = 1.0\n# 13 = 1.0\n# 14 = 1.0\n# 15 = 1.0\n# 16 = 1.0\n# 17 = 1.0\n# 18 = 1.0\n# 19 = 1.0\n# 20 = 1.0\n# 21 = 1.0\n# 22 = 1.0\n# 23 = 1.0\n# 24 = 1.0\n# 25 = 1.0\n# 26 = 1.0\n# 27 = 1.0\n# 28 = 1.0\n# 29 = 1.0\n# 30 = 1.0\n# 31 = 1.0\n# 32 = 1.0\n# 33 = 1.0\n# 34 = 1.0\n# 35 = 1.0\n# 36 = 1.0\n# 37 = 1.0", "multiline": True, "dynamicPrompts": True}),
214
+ #**{f"double_block_{s}": ("FLOAT", { "display": "slider", "default": 1.0, "min": 0, "max": 5, "step": 0.05 }) for s in range(19)},
215
+ #**{f"single_block_{s}": ("FLOAT", { "display": "slider", "default": 1.0, "min": 0, "max": 5, "step": 0.05 }) for s in range(38)},
216
+ }}
217
+ RETURN_TYPES = ("MODEL", "STRING")
218
+ RETURN_NAMES = ("MODEL", "patched_blocks")
219
+ FUNCTION = "patch"
220
+
221
+ CATEGORY = "essentials/conditioning"
222
+
223
+ def patch(self, model, blocks):
224
+ if blocks == "":
225
+ return (model, )
226
+
227
+ m = model.clone()
228
+ sd = model.model_state_dict()
229
+ patched_blocks = []
230
+
231
+ """
232
+ Also compatible with the following format:
233
+
234
+ double_blocks\.0\.(img|txt)_(mod|attn|mlp)\.(lin|qkv|proj|0|2)\.(weight|bias)=1.1
235
+ single_blocks\.0\.(linear[12]|modulation\.lin)\.(weight|bias)=1.1
236
+
237
+ The regex is used to match the block names
238
+ """
239
+
240
+ blocks = blocks.split("\n")
241
+ blocks = [b.strip() for b in blocks if b.strip()]
242
+
243
+ for k in sd:
244
+ for block in blocks:
245
+ block = block.split("=")
246
+ value = float(block[1].strip()) if len(block) > 1 else 1.0
247
+ block = block[0].strip()
248
+ if block.startswith("##"):
249
+ block = r"double_blocks\." + block[2:].strip() + r"\.(img|txt)_(mod|attn|mlp)\.(lin|qkv|proj|0|2)\.(weight|bias)"
250
+ elif block.startswith("#"):
251
+ block = r"single_blocks\." + block[1:].strip() + r"\.(linear[12]|modulation\.lin)\.(weight|bias)"
252
+
253
+ if value != 1.0 and re.search(block, k):
254
+ m.add_patches({k: (None,)}, 0.0, value)
255
+ patched_blocks.append(f"{k}: {value}")
256
+
257
+ patched_blocks = "\n".join(patched_blocks)
258
+
259
+ return (m, patched_blocks,)
260
+
261
+
262
+ COND_CLASS_MAPPINGS = {
263
+ "CLIPTextEncodeSDXL+": CLIPTextEncodeSDXLSimplified,
264
+ "ConditioningCombineMultiple+": ConditioningCombineMultiple,
265
+ "SD3NegativeConditioning+": SD3NegativeConditioning,
266
+ "FluxAttentionSeeker+": FluxAttentionSeeker,
267
+ "SD3AttentionSeekerLG+": SD3AttentionSeekerLG,
268
+ "SD3AttentionSeekerT5+": SD3AttentionSeekerT5,
269
+ "FluxBlocksBuster+": FluxBlocksBuster,
270
+ }
271
+
272
+ COND_NAME_MAPPINGS = {
273
+ "CLIPTextEncodeSDXL+": "🔧 SDXL CLIPTextEncode",
274
+ "ConditioningCombineMultiple+": "🔧 Cond Combine Multiple",
275
+ "SD3NegativeConditioning+": "🔧 SD3 Negative Conditioning",
276
+ "FluxAttentionSeeker+": "🔧 Flux Attention Seeker",
277
+ "SD3AttentionSeekerLG+": "🔧 SD3 Attention Seeker L/G",
278
+ "SD3AttentionSeekerT5+": "🔧 SD3 Attention Seeker T5",
279
+ "FluxBlocksBuster+": "🔧 Flux Model Blocks Buster",
280
+ }
ComfyUI_essentials/histogram_matching.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # from MIT licensed https://github.com/nemodleo/pytorch-histogram-matching
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+
6
+ class Histogram_Matching(nn.Module):
7
+ def __init__(self, differentiable=False):
8
+ super(Histogram_Matching, self).__init__()
9
+ self.differentiable = differentiable
10
+
11
+ def forward(self, dst, ref):
12
+ # B C
13
+ B, C, H, W = dst.size()
14
+ # assertion
15
+ assert dst.device == ref.device
16
+ # [B*C 256]
17
+ hist_dst = self.cal_hist(dst)
18
+ hist_ref = self.cal_hist(ref)
19
+ # [B*C 256]
20
+ tables = self.cal_trans_batch(hist_dst, hist_ref)
21
+ # [B C H W]
22
+ rst = dst.clone()
23
+ for b in range(B):
24
+ for c in range(C):
25
+ rst[b,c] = tables[b*c, (dst[b,c] * 255).long()]
26
+ # [B C H W]
27
+ rst /= 255.
28
+ return rst
29
+
30
+ def cal_hist(self, img):
31
+ B, C, H, W = img.size()
32
+ # [B*C 256]
33
+ if self.differentiable:
34
+ hists = self.soft_histc_batch(img * 255, bins=256, min=0, max=256, sigma=3*25)
35
+ else:
36
+ hists = torch.stack([torch.histc(img[b,c] * 255, bins=256, min=0, max=255) for b in range(B) for c in range(C)])
37
+ hists = hists.float()
38
+ hists = F.normalize(hists, p=1)
39
+ # BC 256
40
+ bc, n = hists.size()
41
+ # [B*C 256 256]
42
+ triu = torch.ones(bc, n, n, device=hists.device).triu()
43
+ # [B*C 256]
44
+ hists = torch.bmm(hists[:,None,:], triu)[:,0,:]
45
+ return hists
46
+
47
+ def soft_histc_batch(self, x, bins=256, min=0, max=256, sigma=3*25):
48
+ # B C H W
49
+ B, C, H, W = x.size()
50
+ # [B*C H*W]
51
+ x = x.view(B*C, -1)
52
+ # 1
53
+ delta = float(max - min) / float(bins)
54
+ # [256]
55
+ centers = float(min) + delta * (torch.arange(bins, device=x.device, dtype=torch.bfloat16) + 0.5)
56
+ # [B*C 1 H*W]
57
+ x = torch.unsqueeze(x, 1)
58
+ # [1 256 1]
59
+ centers = centers[None,:,None]
60
+ # [B*C 256 H*W]
61
+ x = x - centers
62
+ # [B*C 256 H*W]
63
+ x = x.type(torch.bfloat16)
64
+ # [B*C 256 H*W]
65
+ x = torch.sigmoid(sigma * (x + delta/2)) - torch.sigmoid(sigma * (x - delta/2))
66
+ # [B*C 256]
67
+ x = x.sum(dim=2)
68
+ # [B*C 256]
69
+ x = x.type(torch.float32)
70
+ # prevent oom
71
+ # torch.cuda.empty_cache()
72
+ return x
73
+
74
+ def cal_trans_batch(self, hist_dst, hist_ref):
75
+ # [B*C 256 256]
76
+ hist_dst = hist_dst[:,None,:].repeat(1,256,1)
77
+ # [B*C 256 256]
78
+ hist_ref = hist_ref[:,:,None].repeat(1,1,256)
79
+ # [B*C 256 256]
80
+ table = hist_dst - hist_ref
81
+ # [B*C 256 256]
82
+ table = torch.where(table>=0, 1., 0.)
83
+ # [B*C 256]
84
+ table = torch.sum(table, dim=1) - 1
85
+ # [B*C 256]
86
+ table = torch.clamp(table, min=0, max=255)
87
+ return table
ComfyUI_essentials/image.py ADDED
@@ -0,0 +1,1770 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .utils import max_, min_
2
+ from nodes import MAX_RESOLUTION
3
+ import comfy.utils
4
+ from nodes import SaveImage
5
+ from node_helpers import pillow
6
+ from PIL import Image, ImageOps
7
+
8
+ import kornia
9
+ import torch
10
+ import torch.nn.functional as F
11
+ import torchvision.transforms.v2 as T
12
+
13
+ #import warnings
14
+ #warnings.filterwarnings('ignore', module="torchvision")
15
+ import math
16
+ import os
17
+ import numpy as np
18
+ import folder_paths
19
+ from pathlib import Path
20
+ import random
21
+
22
+ """
23
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24
+ Image analysis
25
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26
+ """
27
+
28
+ class ImageEnhanceDifference:
29
+ @classmethod
30
+ def INPUT_TYPES(s):
31
+ return {
32
+ "required": {
33
+ "image1": ("IMAGE",),
34
+ "image2": ("IMAGE",),
35
+ "exponent": ("FLOAT", { "default": 0.75, "min": 0.00, "max": 1.00, "step": 0.05, }),
36
+ }
37
+ }
38
+
39
+ RETURN_TYPES = ("IMAGE",)
40
+ FUNCTION = "execute"
41
+ CATEGORY = "essentials/image analysis"
42
+
43
+ def execute(self, image1, image2, exponent):
44
+ if image1.shape[1:] != image2.shape[1:]:
45
+ image2 = comfy.utils.common_upscale(image2.permute([0,3,1,2]), image1.shape[2], image1.shape[1], upscale_method='bicubic', crop='center').permute([0,2,3,1])
46
+
47
+ diff_image = image1 - image2
48
+ diff_image = torch.pow(diff_image, exponent)
49
+ diff_image = torch.clamp(diff_image, 0, 1)
50
+
51
+ return(diff_image,)
52
+
53
+ """
54
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
55
+ Batch tools
56
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
57
+ """
58
+
59
+ class ImageBatchMultiple:
60
+ @classmethod
61
+ def INPUT_TYPES(s):
62
+ return {
63
+ "required": {
64
+ "image_1": ("IMAGE",),
65
+ "method": (["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], { "default": "lanczos" }),
66
+ }, "optional": {
67
+ "image_2": ("IMAGE",),
68
+ "image_3": ("IMAGE",),
69
+ "image_4": ("IMAGE",),
70
+ "image_5": ("IMAGE",),
71
+ },
72
+ }
73
+ RETURN_TYPES = ("IMAGE",)
74
+ FUNCTION = "execute"
75
+ CATEGORY = "essentials/image batch"
76
+
77
+ def execute(self, image_1, method, image_2=None, image_3=None, image_4=None, image_5=None):
78
+ out = image_1
79
+
80
+ if image_2 is not None:
81
+ if image_1.shape[1:] != image_2.shape[1:]:
82
+ image_2 = comfy.utils.common_upscale(image_2.movedim(-1,1), image_1.shape[2], image_1.shape[1], method, "center").movedim(1,-1)
83
+ out = torch.cat((image_1, image_2), dim=0)
84
+ if image_3 is not None:
85
+ if image_1.shape[1:] != image_3.shape[1:]:
86
+ image_3 = comfy.utils.common_upscale(image_3.movedim(-1,1), image_1.shape[2], image_1.shape[1], method, "center").movedim(1,-1)
87
+ out = torch.cat((out, image_3), dim=0)
88
+ if image_4 is not None:
89
+ if image_1.shape[1:] != image_4.shape[1:]:
90
+ image_4 = comfy.utils.common_upscale(image_4.movedim(-1,1), image_1.shape[2], image_1.shape[1], method, "center").movedim(1,-1)
91
+ out = torch.cat((out, image_4), dim=0)
92
+ if image_5 is not None:
93
+ if image_1.shape[1:] != image_5.shape[1:]:
94
+ image_5 = comfy.utils.common_upscale(image_5.movedim(-1,1), image_1.shape[2], image_1.shape[1], method, "center").movedim(1,-1)
95
+ out = torch.cat((out, image_5), dim=0)
96
+
97
+ return (out,)
98
+
99
+
100
+ class ImageExpandBatch:
101
+ @classmethod
102
+ def INPUT_TYPES(s):
103
+ return {
104
+ "required": {
105
+ "image": ("IMAGE",),
106
+ "size": ("INT", { "default": 16, "min": 1, "step": 1, }),
107
+ "method": (["expand", "repeat all", "repeat first", "repeat last"],)
108
+ }
109
+ }
110
+
111
+ RETURN_TYPES = ("IMAGE",)
112
+ FUNCTION = "execute"
113
+ CATEGORY = "essentials/image batch"
114
+
115
+ def execute(self, image, size, method):
116
+ orig_size = image.shape[0]
117
+
118
+ if orig_size == size:
119
+ return (image,)
120
+
121
+ if size <= 1:
122
+ return (image[:size],)
123
+
124
+ if 'expand' in method:
125
+ out = torch.empty([size] + list(image.shape)[1:], dtype=image.dtype, device=image.device)
126
+ if size < orig_size:
127
+ scale = (orig_size - 1) / (size - 1)
128
+ for i in range(size):
129
+ out[i] = image[min(round(i * scale), orig_size - 1)]
130
+ else:
131
+ scale = orig_size / size
132
+ for i in range(size):
133
+ out[i] = image[min(math.floor((i + 0.5) * scale), orig_size - 1)]
134
+ elif 'all' in method:
135
+ out = image.repeat([math.ceil(size / image.shape[0])] + [1] * (len(image.shape) - 1))[:size]
136
+ elif 'first' in method:
137
+ if size < image.shape[0]:
138
+ out = image[:size]
139
+ else:
140
+ out = torch.cat([image[:1].repeat(size-image.shape[0], 1, 1, 1), image], dim=0)
141
+ elif 'last' in method:
142
+ if size < image.shape[0]:
143
+ out = image[:size]
144
+ else:
145
+ out = torch.cat((image, image[-1:].repeat((size-image.shape[0], 1, 1, 1))), dim=0)
146
+
147
+ return (out,)
148
+
149
+ class ImageFromBatch:
150
+ @classmethod
151
+ def INPUT_TYPES(s):
152
+ return {
153
+ "required": {
154
+ "image": ("IMAGE", ),
155
+ "start": ("INT", { "default": 0, "min": 0, "step": 1, }),
156
+ "length": ("INT", { "default": -1, "min": -1, "step": 1, }),
157
+ }
158
+ }
159
+
160
+ RETURN_TYPES = ("IMAGE",)
161
+ FUNCTION = "execute"
162
+ CATEGORY = "essentials/image batch"
163
+
164
+ def execute(self, image, start, length):
165
+ if length<0:
166
+ length = image.shape[0]
167
+ start = min(start, image.shape[0]-1)
168
+ length = min(image.shape[0]-start, length)
169
+ return (image[start:start + length], )
170
+
171
+
172
+ class ImageListToBatch:
173
+ @classmethod
174
+ def INPUT_TYPES(s):
175
+ return {
176
+ "required": {
177
+ "image": ("IMAGE",),
178
+ }
179
+ }
180
+
181
+ RETURN_TYPES = ("IMAGE",)
182
+ FUNCTION = "execute"
183
+ INPUT_IS_LIST = True
184
+ CATEGORY = "essentials/image batch"
185
+
186
+ def execute(self, image):
187
+ shape = image[0].shape[1:3]
188
+ out = []
189
+
190
+ for i in range(len(image)):
191
+ img = image[i]
192
+ if image[i].shape[1:3] != shape:
193
+ img = comfy.utils.common_upscale(img.permute([0,3,1,2]), shape[1], shape[0], upscale_method='bicubic', crop='center').permute([0,2,3,1])
194
+ out.append(img)
195
+
196
+ out = torch.cat(out, dim=0)
197
+
198
+ return (out,)
199
+
200
+ class ImageBatchToList:
201
+ @classmethod
202
+ def INPUT_TYPES(s):
203
+ return {
204
+ "required": {
205
+ "image": ("IMAGE",),
206
+ }
207
+ }
208
+
209
+ RETURN_TYPES = ("IMAGE",)
210
+ OUTPUT_IS_LIST = (True,)
211
+ FUNCTION = "execute"
212
+ CATEGORY = "essentials/image batch"
213
+
214
+ def execute(self, image):
215
+ return ([image[i].unsqueeze(0) for i in range(image.shape[0])], )
216
+
217
+
218
+ """
219
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
220
+ Image manipulation
221
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
222
+ """
223
+
224
+ class ImageCompositeFromMaskBatch:
225
+ @classmethod
226
+ def INPUT_TYPES(s):
227
+ return {
228
+ "required": {
229
+ "image_from": ("IMAGE", ),
230
+ "image_to": ("IMAGE", ),
231
+ "mask": ("MASK", )
232
+ }
233
+ }
234
+
235
+ RETURN_TYPES = ("IMAGE",)
236
+ FUNCTION = "execute"
237
+ CATEGORY = "essentials/image manipulation"
238
+
239
+ def execute(self, image_from, image_to, mask):
240
+ frames = mask.shape[0]
241
+
242
+ if image_from.shape[1] != image_to.shape[1] or image_from.shape[2] != image_to.shape[2]:
243
+ image_to = comfy.utils.common_upscale(image_to.permute([0,3,1,2]), image_from.shape[2], image_from.shape[1], upscale_method='bicubic', crop='center').permute([0,2,3,1])
244
+
245
+ if frames < image_from.shape[0]:
246
+ image_from = image_from[:frames]
247
+ elif frames > image_from.shape[0]:
248
+ image_from = torch.cat((image_from, image_from[-1].unsqueeze(0).repeat(frames-image_from.shape[0], 1, 1, 1)), dim=0)
249
+
250
+ mask = mask.unsqueeze(3).repeat(1, 1, 1, 3)
251
+
252
+ if image_from.shape[1] != mask.shape[1] or image_from.shape[2] != mask.shape[2]:
253
+ mask = comfy.utils.common_upscale(mask.permute([0,3,1,2]), image_from.shape[2], image_from.shape[1], upscale_method='bicubic', crop='center').permute([0,2,3,1])
254
+
255
+ out = mask * image_to + (1 - mask) * image_from
256
+
257
+ return (out, )
258
+
259
+ class ImageComposite:
260
+ @classmethod
261
+ def INPUT_TYPES(s):
262
+ return {
263
+ "required": {
264
+ "destination": ("IMAGE",),
265
+ "source": ("IMAGE",),
266
+ "x": ("INT", { "default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1 }),
267
+ "y": ("INT", { "default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1 }),
268
+ "offset_x": ("INT", { "default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1 }),
269
+ "offset_y": ("INT", { "default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1 }),
270
+ },
271
+ "optional": {
272
+ "mask": ("MASK",),
273
+ }
274
+ }
275
+
276
+ RETURN_TYPES = ("IMAGE",)
277
+ FUNCTION = "execute"
278
+ CATEGORY = "essentials/image manipulation"
279
+
280
+ def execute(self, destination, source, x, y, offset_x, offset_y, mask=None):
281
+ if mask is None:
282
+ mask = torch.ones_like(source)[:,:,:,0]
283
+
284
+ mask = mask.unsqueeze(-1).repeat(1, 1, 1, 3)
285
+
286
+ if mask.shape[1:3] != source.shape[1:3]:
287
+ mask = F.interpolate(mask.permute([0, 3, 1, 2]), size=(source.shape[1], source.shape[2]), mode='bicubic')
288
+ mask = mask.permute([0, 2, 3, 1])
289
+
290
+ if mask.shape[0] > source.shape[0]:
291
+ mask = mask[:source.shape[0]]
292
+ elif mask.shape[0] < source.shape[0]:
293
+ mask = torch.cat((mask, mask[-1:].repeat((source.shape[0]-mask.shape[0], 1, 1, 1))), dim=0)
294
+
295
+ if destination.shape[0] > source.shape[0]:
296
+ destination = destination[:source.shape[0]]
297
+ elif destination.shape[0] < source.shape[0]:
298
+ destination = torch.cat((destination, destination[-1:].repeat((source.shape[0]-destination.shape[0], 1, 1, 1))), dim=0)
299
+
300
+ if not isinstance(x, list):
301
+ x = [x]
302
+ if not isinstance(y, list):
303
+ y = [y]
304
+
305
+ if len(x) < destination.shape[0]:
306
+ x = x + [x[-1]] * (destination.shape[0] - len(x))
307
+ if len(y) < destination.shape[0]:
308
+ y = y + [y[-1]] * (destination.shape[0] - len(y))
309
+
310
+ x = [i + offset_x for i in x]
311
+ y = [i + offset_y for i in y]
312
+
313
+ output = []
314
+ for i in range(destination.shape[0]):
315
+ d = destination[i].clone()
316
+ s = source[i]
317
+ m = mask[i]
318
+
319
+ if x[i]+source.shape[2] > destination.shape[2]:
320
+ s = s[:, :, :destination.shape[2]-x[i], :]
321
+ m = m[:, :, :destination.shape[2]-x[i], :]
322
+ if y[i]+source.shape[1] > destination.shape[1]:
323
+ s = s[:, :destination.shape[1]-y[i], :, :]
324
+ m = m[:destination.shape[1]-y[i], :, :]
325
+
326
+ #output.append(s * m + d[y[i]:y[i]+s.shape[0], x[i]:x[i]+s.shape[1], :] * (1 - m))
327
+ d[y[i]:y[i]+s.shape[0], x[i]:x[i]+s.shape[1], :] = s * m + d[y[i]:y[i]+s.shape[0], x[i]:x[i]+s.shape[1], :] * (1 - m)
328
+ output.append(d)
329
+
330
+ output = torch.stack(output)
331
+
332
+ # apply the source to the destination at XY position using the mask
333
+ #for i in range(destination.shape[0]):
334
+ # output[i, y[i]:y[i]+source.shape[1], x[i]:x[i]+source.shape[2], :] = source * mask + destination[i, y[i]:y[i]+source.shape[1], x[i]:x[i]+source.shape[2], :] * (1 - mask)
335
+
336
+ #for x_, y_ in zip(x, y):
337
+ # output[:, y_:y_+source.shape[1], x_:x_+source.shape[2], :] = source * mask + destination[:, y_:y_+source.shape[1], x_:x_+source.shape[2], :] * (1 - mask)
338
+
339
+ #output[:, y:y+source.shape[1], x:x+source.shape[2], :] = source * mask + destination[:, y:y+source.shape[1], x:x+source.shape[2], :] * (1 - mask)
340
+ #output = destination * (1 - mask) + source * mask
341
+
342
+ return (output,)
343
+
344
+ class ImageResize:
345
+ @classmethod
346
+ def INPUT_TYPES(s):
347
+ return {
348
+ "required": {
349
+ "image": ("IMAGE",),
350
+ "width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, }),
351
+ "height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, }),
352
+ "interpolation": (["nearest", "bilinear", "bicubic", "area", "nearest-exact", "lanczos"],),
353
+ "method": (["stretch", "keep proportion", "fill / crop", "pad"],),
354
+ "condition": (["always", "downscale if bigger", "upscale if smaller", "if bigger area", "if smaller area"],),
355
+ "multiple_of": ("INT", { "default": 0, "min": 0, "max": 512, "step": 1, }),
356
+ }
357
+ }
358
+
359
+ RETURN_TYPES = ("IMAGE", "INT", "INT",)
360
+ RETURN_NAMES = ("IMAGE", "width", "height",)
361
+ FUNCTION = "execute"
362
+ CATEGORY = "essentials/image manipulation"
363
+
364
+ def execute(self, image, width, height, method="stretch", interpolation="nearest", condition="always", multiple_of=0, keep_proportion=False):
365
+ _, oh, ow, _ = image.shape
366
+ x = y = x2 = y2 = 0
367
+ pad_left = pad_right = pad_top = pad_bottom = 0
368
+
369
+ if keep_proportion:
370
+ method = "keep proportion"
371
+
372
+ if multiple_of > 1:
373
+ width = width - (width % multiple_of)
374
+ height = height - (height % multiple_of)
375
+
376
+ if method == 'keep proportion' or method == 'pad':
377
+ if width == 0 and oh < height:
378
+ width = MAX_RESOLUTION
379
+ elif width == 0 and oh >= height:
380
+ width = ow
381
+
382
+ if height == 0 and ow < width:
383
+ height = MAX_RESOLUTION
384
+ elif height == 0 and ow >= width:
385
+ height = oh
386
+
387
+ ratio = min(width / ow, height / oh)
388
+ new_width = round(ow*ratio)
389
+ new_height = round(oh*ratio)
390
+
391
+ if method == 'pad':
392
+ pad_left = (width - new_width) // 2
393
+ pad_right = width - new_width - pad_left
394
+ pad_top = (height - new_height) // 2
395
+ pad_bottom = height - new_height - pad_top
396
+
397
+ width = new_width
398
+ height = new_height
399
+ elif method.startswith('fill'):
400
+ width = width if width > 0 else ow
401
+ height = height if height > 0 else oh
402
+
403
+ ratio = max(width / ow, height / oh)
404
+ new_width = round(ow*ratio)
405
+ new_height = round(oh*ratio)
406
+ x = (new_width - width) // 2
407
+ y = (new_height - height) // 2
408
+ x2 = x + width
409
+ y2 = y + height
410
+ if x2 > new_width:
411
+ x -= (x2 - new_width)
412
+ if x < 0:
413
+ x = 0
414
+ if y2 > new_height:
415
+ y -= (y2 - new_height)
416
+ if y < 0:
417
+ y = 0
418
+ width = new_width
419
+ height = new_height
420
+ else:
421
+ width = width if width > 0 else ow
422
+ height = height if height > 0 else oh
423
+
424
+ if "always" in condition \
425
+ or ("downscale if bigger" == condition and (oh > height or ow > width)) or ("upscale if smaller" == condition and (oh < height or ow < width)) \
426
+ or ("bigger area" in condition and (oh * ow > height * width)) or ("smaller area" in condition and (oh * ow < height * width)):
427
+
428
+ outputs = image.permute(0,3,1,2)
429
+
430
+ if interpolation == "lanczos":
431
+ outputs = comfy.utils.lanczos(outputs, width, height)
432
+ else:
433
+ outputs = F.interpolate(outputs, size=(height, width), mode=interpolation)
434
+
435
+ if method == 'pad':
436
+ if pad_left > 0 or pad_right > 0 or pad_top > 0 or pad_bottom > 0:
437
+ outputs = F.pad(outputs, (pad_left, pad_right, pad_top, pad_bottom), value=0)
438
+
439
+ outputs = outputs.permute(0,2,3,1)
440
+
441
+ if method.startswith('fill'):
442
+ if x > 0 or y > 0 or x2 > 0 or y2 > 0:
443
+ outputs = outputs[:, y:y2, x:x2, :]
444
+ else:
445
+ outputs = image
446
+
447
+ if multiple_of > 1 and (outputs.shape[2] % multiple_of != 0 or outputs.shape[1] % multiple_of != 0):
448
+ width = outputs.shape[2]
449
+ height = outputs.shape[1]
450
+ x = (width % multiple_of) // 2
451
+ y = (height % multiple_of) // 2
452
+ x2 = width - ((width % multiple_of) - x)
453
+ y2 = height - ((height % multiple_of) - y)
454
+ outputs = outputs[:, y:y2, x:x2, :]
455
+
456
+ outputs = torch.clamp(outputs, 0, 1)
457
+
458
+ return(outputs, outputs.shape[2], outputs.shape[1],)
459
+
460
+ class ImageFlip:
461
+ @classmethod
462
+ def INPUT_TYPES(s):
463
+ return {
464
+ "required": {
465
+ "image": ("IMAGE",),
466
+ "axis": (["x", "y", "xy"],),
467
+ }
468
+ }
469
+
470
+ RETURN_TYPES = ("IMAGE",)
471
+ FUNCTION = "execute"
472
+ CATEGORY = "essentials/image manipulation"
473
+
474
+ def execute(self, image, axis):
475
+ dim = ()
476
+ if "y" in axis:
477
+ dim += (1,)
478
+ if "x" in axis:
479
+ dim += (2,)
480
+ image = torch.flip(image, dim)
481
+
482
+ return(image,)
483
+
484
+ class ImageCrop:
485
+ @classmethod
486
+ def INPUT_TYPES(s):
487
+ return {
488
+ "required": {
489
+ "image": ("IMAGE",),
490
+ "width": ("INT", { "default": 256, "min": 0, "max": MAX_RESOLUTION, "step": 8, }),
491
+ "height": ("INT", { "default": 256, "min": 0, "max": MAX_RESOLUTION, "step": 8, }),
492
+ "position": (["top-left", "top-center", "top-right", "right-center", "bottom-right", "bottom-center", "bottom-left", "left-center", "center"],),
493
+ "x_offset": ("INT", { "default": 0, "min": -99999, "step": 1, }),
494
+ "y_offset": ("INT", { "default": 0, "min": -99999, "step": 1, }),
495
+ }
496
+ }
497
+
498
+ RETURN_TYPES = ("IMAGE","INT","INT",)
499
+ RETURN_NAMES = ("IMAGE","x","y",)
500
+ FUNCTION = "execute"
501
+ CATEGORY = "essentials/image manipulation"
502
+
503
+ def execute(self, image, width, height, position, x_offset, y_offset):
504
+ _, oh, ow, _ = image.shape
505
+
506
+ width = min(ow, width)
507
+ height = min(oh, height)
508
+
509
+ if "center" in position:
510
+ x = round((ow-width) / 2)
511
+ y = round((oh-height) / 2)
512
+ if "top" in position:
513
+ y = 0
514
+ if "bottom" in position:
515
+ y = oh-height
516
+ if "left" in position:
517
+ x = 0
518
+ if "right" in position:
519
+ x = ow-width
520
+
521
+ x += x_offset
522
+ y += y_offset
523
+
524
+ x2 = x+width
525
+ y2 = y+height
526
+
527
+ if x2 > ow:
528
+ x2 = ow
529
+ if x < 0:
530
+ x = 0
531
+ if y2 > oh:
532
+ y2 = oh
533
+ if y < 0:
534
+ y = 0
535
+
536
+ image = image[:, y:y2, x:x2, :]
537
+
538
+ return(image, x, y, )
539
+
540
+ class ImageTile:
541
+ @classmethod
542
+ def INPUT_TYPES(s):
543
+ return {
544
+ "required": {
545
+ "image": ("IMAGE",),
546
+ "rows": ("INT", { "default": 2, "min": 1, "max": 256, "step": 1, }),
547
+ "cols": ("INT", { "default": 2, "min": 1, "max": 256, "step": 1, }),
548
+ "overlap": ("FLOAT", { "default": 0, "min": 0, "max": 0.5, "step": 0.01, }),
549
+ "overlap_x": ("INT", { "default": 0, "min": 0, "max": MAX_RESOLUTION//2, "step": 1, }),
550
+ "overlap_y": ("INT", { "default": 0, "min": 0, "max": MAX_RESOLUTION//2, "step": 1, }),
551
+ }
552
+ }
553
+
554
+ RETURN_TYPES = ("IMAGE", "INT", "INT", "INT", "INT")
555
+ RETURN_NAMES = ("IMAGE", "tile_width", "tile_height", "overlap_x", "overlap_y",)
556
+ FUNCTION = "execute"
557
+ CATEGORY = "essentials/image manipulation"
558
+
559
+ def execute(self, image, rows, cols, overlap, overlap_x, overlap_y):
560
+ h, w = image.shape[1:3]
561
+ tile_h = h // rows
562
+ tile_w = w // cols
563
+ h = tile_h * rows
564
+ w = tile_w * cols
565
+ overlap_h = int(tile_h * overlap) + overlap_y
566
+ overlap_w = int(tile_w * overlap) + overlap_x
567
+
568
+ # max overlap is half of the tile size
569
+ overlap_h = min(tile_h // 2, overlap_h)
570
+ overlap_w = min(tile_w // 2, overlap_w)
571
+
572
+ if rows == 1:
573
+ overlap_h = 0
574
+ if cols == 1:
575
+ overlap_w = 0
576
+
577
+ tiles = []
578
+ for i in range(rows):
579
+ for j in range(cols):
580
+ y1 = i * tile_h
581
+ x1 = j * tile_w
582
+
583
+ if i > 0:
584
+ y1 -= overlap_h
585
+ if j > 0:
586
+ x1 -= overlap_w
587
+
588
+ y2 = y1 + tile_h + overlap_h
589
+ x2 = x1 + tile_w + overlap_w
590
+
591
+ if y2 > h:
592
+ y2 = h
593
+ y1 = y2 - tile_h - overlap_h
594
+ if x2 > w:
595
+ x2 = w
596
+ x1 = x2 - tile_w - overlap_w
597
+
598
+ tiles.append(image[:, y1:y2, x1:x2, :])
599
+ tiles = torch.cat(tiles, dim=0)
600
+
601
+ return(tiles, tile_w+overlap_w, tile_h+overlap_h, overlap_w, overlap_h,)
602
+
603
+ class ImageUntile:
604
+ @classmethod
605
+ def INPUT_TYPES(s):
606
+ return {
607
+ "required": {
608
+ "tiles": ("IMAGE",),
609
+ "overlap_x": ("INT", { "default": 0, "min": 0, "max": MAX_RESOLUTION//2, "step": 1, }),
610
+ "overlap_y": ("INT", { "default": 0, "min": 0, "max": MAX_RESOLUTION//2, "step": 1, }),
611
+ "rows": ("INT", { "default": 2, "min": 1, "max": 256, "step": 1, }),
612
+ "cols": ("INT", { "default": 2, "min": 1, "max": 256, "step": 1, }),
613
+ }
614
+ }
615
+
616
+ RETURN_TYPES = ("IMAGE",)
617
+ FUNCTION = "execute"
618
+ CATEGORY = "essentials/image manipulation"
619
+
620
+ def execute(self, tiles, overlap_x, overlap_y, rows, cols):
621
+ tile_h, tile_w = tiles.shape[1:3]
622
+ tile_h -= overlap_y
623
+ tile_w -= overlap_x
624
+ out_w = cols * tile_w
625
+ out_h = rows * tile_h
626
+
627
+ out = torch.zeros((1, out_h, out_w, tiles.shape[3]), device=tiles.device, dtype=tiles.dtype)
628
+
629
+ for i in range(rows):
630
+ for j in range(cols):
631
+ y1 = i * tile_h
632
+ x1 = j * tile_w
633
+
634
+ if i > 0:
635
+ y1 -= overlap_y
636
+ if j > 0:
637
+ x1 -= overlap_x
638
+
639
+ y2 = y1 + tile_h + overlap_y
640
+ x2 = x1 + tile_w + overlap_x
641
+
642
+ if y2 > out_h:
643
+ y2 = out_h
644
+ y1 = y2 - tile_h - overlap_y
645
+ if x2 > out_w:
646
+ x2 = out_w
647
+ x1 = x2 - tile_w - overlap_x
648
+
649
+ mask = torch.ones((1, tile_h+overlap_y, tile_w+overlap_x), device=tiles.device, dtype=tiles.dtype)
650
+
651
+ # feather the overlap on top
652
+ if i > 0 and overlap_y > 0:
653
+ mask[:, :overlap_y, :] *= torch.linspace(0, 1, overlap_y, device=tiles.device, dtype=tiles.dtype).unsqueeze(1)
654
+ # feather the overlap on bottom
655
+ #if i < rows - 1:
656
+ # mask[:, -overlap_y:, :] *= torch.linspace(1, 0, overlap_y, device=tiles.device, dtype=tiles.dtype).unsqueeze(1)
657
+ # feather the overlap on left
658
+ if j > 0 and overlap_x > 0:
659
+ mask[:, :, :overlap_x] *= torch.linspace(0, 1, overlap_x, device=tiles.device, dtype=tiles.dtype).unsqueeze(0)
660
+ # feather the overlap on right
661
+ #if j < cols - 1:
662
+ # mask[:, :, -overlap_x:] *= torch.linspace(1, 0, overlap_x, device=tiles.device, dtype=tiles.dtype).unsqueeze(0)
663
+
664
+ mask = mask.unsqueeze(-1).repeat(1, 1, 1, tiles.shape[3])
665
+ tile = tiles[i * cols + j] * mask
666
+ out[:, y1:y2, x1:x2, :] = out[:, y1:y2, x1:x2, :] * (1 - mask) + tile
667
+ return(out, )
668
+
669
+ class ImageSeamCarving:
670
+ @classmethod
671
+ def INPUT_TYPES(cls):
672
+ return {
673
+ "required": {
674
+ "image": ("IMAGE",),
675
+ "width": ("INT", { "default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1, }),
676
+ "height": ("INT", { "default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1, }),
677
+ "energy": (["backward", "forward"],),
678
+ "order": (["width-first", "height-first"],),
679
+ },
680
+ "optional": {
681
+ "keep_mask": ("MASK",),
682
+ "drop_mask": ("MASK",),
683
+ }
684
+ }
685
+
686
+ RETURN_TYPES = ("IMAGE",)
687
+ CATEGORY = "essentials/image manipulation"
688
+ FUNCTION = "execute"
689
+
690
+ def execute(self, image, width, height, energy, order, keep_mask=None, drop_mask=None):
691
+ from .carve import seam_carving
692
+
693
+ img = image.permute([0, 3, 1, 2])
694
+
695
+ if keep_mask is not None:
696
+ #keep_mask = keep_mask.reshape((-1, 1, keep_mask.shape[-2], keep_mask.shape[-1])).movedim(1, -1)
697
+ keep_mask = keep_mask.unsqueeze(1)
698
+
699
+ if keep_mask.shape[2] != img.shape[2] or keep_mask.shape[3] != img.shape[3]:
700
+ keep_mask = F.interpolate(keep_mask, size=(img.shape[2], img.shape[3]), mode="bilinear")
701
+ if drop_mask is not None:
702
+ drop_mask = drop_mask.unsqueeze(1)
703
+
704
+ if drop_mask.shape[2] != img.shape[2] or drop_mask.shape[3] != img.shape[3]:
705
+ drop_mask = F.interpolate(drop_mask, size=(img.shape[2], img.shape[3]), mode="bilinear")
706
+
707
+ out = []
708
+ for i in range(img.shape[0]):
709
+ resized = seam_carving(
710
+ T.ToPILImage()(img[i]),
711
+ size=(width, height),
712
+ energy_mode=energy,
713
+ order=order,
714
+ keep_mask=T.ToPILImage()(keep_mask[i]) if keep_mask is not None else None,
715
+ drop_mask=T.ToPILImage()(drop_mask[i]) if drop_mask is not None else None,
716
+ )
717
+ out.append(T.ToTensor()(resized))
718
+
719
+ out = torch.stack(out).permute([0, 2, 3, 1])
720
+
721
+ return(out, )
722
+
723
+ class ImageRandomTransform:
724
+ @classmethod
725
+ def INPUT_TYPES(s):
726
+ return {
727
+ "required": {
728
+ "image": ("IMAGE",),
729
+ "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
730
+ "repeat": ("INT", { "default": 1, "min": 1, "max": 256, "step": 1, }),
731
+ "variation": ("FLOAT", { "default": 0.1, "min": 0.0, "max": 1.0, "step": 0.05, }),
732
+ }
733
+ }
734
+
735
+ RETURN_TYPES = ("IMAGE",)
736
+ FUNCTION = "execute"
737
+ CATEGORY = "essentials/image manipulation"
738
+
739
+ def execute(self, image, seed, repeat, variation):
740
+ h, w = image.shape[1:3]
741
+ image = image.repeat(repeat, 1, 1, 1).permute([0, 3, 1, 2])
742
+
743
+ distortion = 0.2 * variation
744
+ rotation = 5 * variation
745
+ brightness = 0.5 * variation
746
+ contrast = 0.5 * variation
747
+ saturation = 0.5 * variation
748
+ hue = 0.2 * variation
749
+ scale = 0.5 * variation
750
+
751
+ torch.manual_seed(seed)
752
+
753
+ out = []
754
+ for i in image:
755
+ tramsforms = T.Compose([
756
+ T.RandomPerspective(distortion_scale=distortion, p=0.5),
757
+ T.RandomRotation(degrees=rotation, interpolation=T.InterpolationMode.BILINEAR, expand=True),
758
+ T.ColorJitter(brightness=brightness, contrast=contrast, saturation=saturation, hue=(-hue, hue)),
759
+ T.RandomHorizontalFlip(p=0.5),
760
+ T.RandomResizedCrop((h, w), scale=(1-scale, 1+scale), ratio=(w/h, w/h), interpolation=T.InterpolationMode.BICUBIC),
761
+ ])
762
+ out.append(tramsforms(i.unsqueeze(0)))
763
+
764
+ out = torch.cat(out, dim=0).permute([0, 2, 3, 1]).clamp(0, 1)
765
+
766
+ return (out,)
767
+
768
+ class RemBGSession:
769
+ @classmethod
770
+ def INPUT_TYPES(s):
771
+ return {
772
+ "required": {
773
+ "model": (["u2net: general purpose", "u2netp: lightweight general purpose", "u2net_human_seg: human segmentation", "u2net_cloth_seg: cloths Parsing", "silueta: very small u2net", "isnet-general-use: general purpose", "isnet-anime: anime illustrations", "sam: general purpose"],),
774
+ "providers": (['CPU', 'CUDA', 'ROCM', 'DirectML', 'OpenVINO', 'CoreML', 'Tensorrt', 'Azure'],),
775
+ },
776
+ }
777
+
778
+ RETURN_TYPES = ("REMBG_SESSION",)
779
+ FUNCTION = "execute"
780
+ CATEGORY = "essentials/image manipulation"
781
+
782
+ def execute(self, model, providers):
783
+ from rembg import new_session, remove
784
+
785
+ model = model.split(":")[0]
786
+
787
+ class Session:
788
+ def __init__(self, model, providers):
789
+ self.session = new_session(model, providers=[providers+"ExecutionProvider"])
790
+ def process(self, image):
791
+ return remove(image, session=self.session)
792
+
793
+ return (Session(model, providers),)
794
+
795
+ class TransparentBGSession:
796
+ @classmethod
797
+ def INPUT_TYPES(s):
798
+ return {
799
+ "required": {
800
+ "mode": (["base", "fast", "base-nightly"],),
801
+ "use_jit": ("BOOLEAN", { "default": True }),
802
+ },
803
+ }
804
+
805
+ RETURN_TYPES = ("REMBG_SESSION",)
806
+ FUNCTION = "execute"
807
+ CATEGORY = "essentials/image manipulation"
808
+
809
+ def execute(self, mode, use_jit):
810
+ from transparent_background import Remover
811
+
812
+ class Session:
813
+ def __init__(self, mode, use_jit):
814
+ self.session = Remover(mode=mode, jit=use_jit)
815
+ def process(self, image):
816
+ return self.session.process(image)
817
+
818
+ return (Session(mode, use_jit),)
819
+
820
+ class ImageRemoveBackground:
821
+ @classmethod
822
+ def INPUT_TYPES(s):
823
+ return {
824
+ "required": {
825
+ "rembg_session": ("REMBG_SESSION",),
826
+ "image": ("IMAGE",),
827
+ },
828
+ }
829
+
830
+ RETURN_TYPES = ("IMAGE", "MASK",)
831
+ FUNCTION = "execute"
832
+ CATEGORY = "essentials/image manipulation"
833
+
834
+ def execute(self, rembg_session, image):
835
+ image = image.permute([0, 3, 1, 2])
836
+ output = []
837
+ for img in image:
838
+ img = T.ToPILImage()(img)
839
+ img = rembg_session.process(img)
840
+ output.append(T.ToTensor()(img))
841
+
842
+ output = torch.stack(output, dim=0)
843
+ output = output.permute([0, 2, 3, 1])
844
+ mask = output[:, :, :, 3] if output.shape[3] == 4 else torch.ones_like(output[:, :, :, 0])
845
+ # output = output[:, :, :, :3]
846
+
847
+ return(output, mask,)
848
+
849
+ """
850
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
851
+ Image processing
852
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
853
+ """
854
+
855
+ class ImageDesaturate:
856
+ @classmethod
857
+ def INPUT_TYPES(s):
858
+ return {
859
+ "required": {
860
+ "image": ("IMAGE",),
861
+ "factor": ("FLOAT", { "default": 1.00, "min": 0.00, "max": 1.00, "step": 0.05, }),
862
+ "method": (["luminance (Rec.709)", "luminance (Rec.601)", "average", "lightness"],),
863
+ }
864
+ }
865
+
866
+ RETURN_TYPES = ("IMAGE",)
867
+ FUNCTION = "execute"
868
+ CATEGORY = "essentials/image processing"
869
+
870
+ def execute(self, image, factor, method):
871
+ if method == "luminance (Rec.709)":
872
+ grayscale = 0.2126 * image[..., 0] + 0.7152 * image[..., 1] + 0.0722 * image[..., 2]
873
+ elif method == "luminance (Rec.601)":
874
+ grayscale = 0.299 * image[..., 0] + 0.587 * image[..., 1] + 0.114 * image[..., 2]
875
+ elif method == "average":
876
+ grayscale = image.mean(dim=3)
877
+ elif method == "lightness":
878
+ grayscale = (torch.max(image, dim=3)[0] + torch.min(image, dim=3)[0]) / 2
879
+
880
+ grayscale = (1.0 - factor) * image + factor * grayscale.unsqueeze(-1).repeat(1, 1, 1, 3)
881
+ grayscale = torch.clamp(grayscale, 0, 1)
882
+
883
+ return(grayscale,)
884
+
885
+ class PixelOEPixelize:
886
+ @classmethod
887
+ def INPUT_TYPES(s):
888
+ return {
889
+ "required": {
890
+ "image": ("IMAGE",),
891
+ "downscale_mode": (["contrast", "bicubic", "nearest", "center", "k-centroid"],),
892
+ "target_size": ("INT", { "default": 128, "min": 0, "max": MAX_RESOLUTION, "step": 8 }),
893
+ "patch_size": ("INT", { "default": 16, "min": 4, "max": 32, "step": 2 }),
894
+ "thickness": ("INT", { "default": 2, "min": 1, "max": 16, "step": 1 }),
895
+ "color_matching": ("BOOLEAN", { "default": True }),
896
+ "upscale": ("BOOLEAN", { "default": True }),
897
+ #"contrast": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1 }),
898
+ #"saturation": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1 }),
899
+ },
900
+ }
901
+
902
+ RETURN_TYPES = ("IMAGE",)
903
+ FUNCTION = "execute"
904
+ CATEGORY = "essentials/image processing"
905
+
906
+ def execute(self, image, downscale_mode, target_size, patch_size, thickness, color_matching, upscale):
907
+ from pixeloe.pixelize import pixelize
908
+
909
+ image = image.clone().mul(255).clamp(0, 255).byte().cpu().numpy()
910
+ output = []
911
+ for img in image:
912
+ img = pixelize(img,
913
+ mode=downscale_mode,
914
+ target_size=target_size,
915
+ patch_size=patch_size,
916
+ thickness=thickness,
917
+ contrast=1.0,
918
+ saturation=1.0,
919
+ color_matching=color_matching,
920
+ no_upscale=not upscale)
921
+ output.append(T.ToTensor()(img))
922
+
923
+ output = torch.stack(output, dim=0).permute([0, 2, 3, 1])
924
+
925
+ return(output,)
926
+
927
+ class ImagePosterize:
928
+ @classmethod
929
+ def INPUT_TYPES(s):
930
+ return {
931
+ "required": {
932
+ "image": ("IMAGE",),
933
+ "threshold": ("FLOAT", { "default": 0.50, "min": 0.00, "max": 1.00, "step": 0.05, }),
934
+ }
935
+ }
936
+
937
+ RETURN_TYPES = ("IMAGE",)
938
+ FUNCTION = "execute"
939
+ CATEGORY = "essentials/image processing"
940
+
941
+ def execute(self, image, threshold):
942
+ image = image.mean(dim=3, keepdim=True)
943
+ image = (image > threshold).float()
944
+ image = image.repeat(1, 1, 1, 3)
945
+
946
+ return(image,)
947
+
948
+ # From https://github.com/yoonsikp/pycubelut/blob/master/pycubelut.py (MIT license)
949
+ class ImageApplyLUT:
950
+ @classmethod
951
+ def INPUT_TYPES(s):
952
+ return {
953
+ "required": {
954
+ "image": ("IMAGE",),
955
+ "lut_file": (folder_paths.get_filename_list("luts"),),
956
+ "gamma_correction": ("BOOLEAN", { "default": True }),
957
+ "clip_values": ("BOOLEAN", { "default": True }),
958
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1 }),
959
+ }}
960
+
961
+ RETURN_TYPES = ("IMAGE",)
962
+ FUNCTION = "execute"
963
+ CATEGORY = "essentials/image processing"
964
+
965
+ # TODO: check if we can do without numpy
966
+ def execute(self, image, lut_file, gamma_correction, clip_values, strength):
967
+ lut_file_path = folder_paths.get_full_path("luts", lut_file)
968
+ if not lut_file_path or not Path(lut_file_path).exists():
969
+ print(f"Could not find LUT file: {lut_file_path}")
970
+ return (image,)
971
+
972
+ from colour.io.luts.iridas_cube import read_LUT_IridasCube
973
+
974
+ device = image.device
975
+ lut = read_LUT_IridasCube(lut_file_path)
976
+ lut.name = lut_file
977
+
978
+ if clip_values:
979
+ if lut.domain[0].max() == lut.domain[0].min() and lut.domain[1].max() == lut.domain[1].min():
980
+ lut.table = np.clip(lut.table, lut.domain[0, 0], lut.domain[1, 0])
981
+ else:
982
+ if len(lut.table.shape) == 2: # 3x1D
983
+ for dim in range(3):
984
+ lut.table[:, dim] = np.clip(lut.table[:, dim], lut.domain[0, dim], lut.domain[1, dim])
985
+ else: # 3D
986
+ for dim in range(3):
987
+ lut.table[:, :, :, dim] = np.clip(lut.table[:, :, :, dim], lut.domain[0, dim], lut.domain[1, dim])
988
+
989
+ out = []
990
+ for img in image: # TODO: is this more resource efficient? should we use a batch instead?
991
+ lut_img = img.cpu().numpy().copy()
992
+
993
+ is_non_default_domain = not np.array_equal(lut.domain, np.array([[0., 0., 0.], [1., 1., 1.]]))
994
+ dom_scale = None
995
+ if is_non_default_domain:
996
+ dom_scale = lut.domain[1] - lut.domain[0]
997
+ lut_img = lut_img * dom_scale + lut.domain[0]
998
+ if gamma_correction:
999
+ lut_img = lut_img ** (1/2.2)
1000
+ lut_img = lut.apply(lut_img)
1001
+ if gamma_correction:
1002
+ lut_img = lut_img ** (2.2)
1003
+ if is_non_default_domain:
1004
+ lut_img = (lut_img - lut.domain[0]) / dom_scale
1005
+
1006
+ lut_img = torch.from_numpy(lut_img).to(device)
1007
+ if strength < 1.0:
1008
+ lut_img = strength * lut_img + (1 - strength) * img
1009
+ out.append(lut_img)
1010
+
1011
+ out = torch.stack(out)
1012
+
1013
+ return (out, )
1014
+
1015
+ # From https://github.com/Jamy-L/Pytorch-Contrast-Adaptive-Sharpening/
1016
+ class ImageCAS:
1017
+ @classmethod
1018
+ def INPUT_TYPES(cls):
1019
+ return {
1020
+ "required": {
1021
+ "image": ("IMAGE",),
1022
+ "amount": ("FLOAT", {"default": 0.8, "min": 0, "max": 1, "step": 0.05}),
1023
+ },
1024
+ }
1025
+
1026
+ RETURN_TYPES = ("IMAGE",)
1027
+ CATEGORY = "essentials/image processing"
1028
+ FUNCTION = "execute"
1029
+
1030
+ def execute(self, image, amount):
1031
+ epsilon = 1e-5
1032
+ img = F.pad(image.permute([0,3,1,2]), pad=(1, 1, 1, 1))
1033
+
1034
+ a = img[..., :-2, :-2]
1035
+ b = img[..., :-2, 1:-1]
1036
+ c = img[..., :-2, 2:]
1037
+ d = img[..., 1:-1, :-2]
1038
+ e = img[..., 1:-1, 1:-1]
1039
+ f = img[..., 1:-1, 2:]
1040
+ g = img[..., 2:, :-2]
1041
+ h = img[..., 2:, 1:-1]
1042
+ i = img[..., 2:, 2:]
1043
+
1044
+ # Computing contrast
1045
+ cross = (b, d, e, f, h)
1046
+ mn = min_(cross)
1047
+ mx = max_(cross)
1048
+
1049
+ diag = (a, c, g, i)
1050
+ mn2 = min_(diag)
1051
+ mx2 = max_(diag)
1052
+ mx = mx + mx2
1053
+ mn = mn + mn2
1054
+
1055
+ # Computing local weight
1056
+ inv_mx = torch.reciprocal(mx + epsilon)
1057
+ amp = inv_mx * torch.minimum(mn, (2 - mx))
1058
+
1059
+ # scaling
1060
+ amp = torch.sqrt(amp)
1061
+ w = - amp * (amount * (1/5 - 1/8) + 1/8)
1062
+ div = torch.reciprocal(1 + 4*w)
1063
+
1064
+ output = ((b + d + f + h)*w + e) * div
1065
+ output = output.clamp(0, 1)
1066
+ #output = torch.nan_to_num(output)
1067
+
1068
+ output = output.permute([0,2,3,1])
1069
+
1070
+ return (output,)
1071
+
1072
+ class ImageSmartSharpen:
1073
+ @classmethod
1074
+ def INPUT_TYPES(s):
1075
+ return {
1076
+ "required": {
1077
+ "image": ("IMAGE",),
1078
+ "noise_radius": ("INT", { "default": 7, "min": 1, "max": 25, "step": 1, }),
1079
+ "preserve_edges": ("FLOAT", { "default": 0.75, "min": 0.0, "max": 1.0, "step": 0.05 }),
1080
+ "sharpen": ("FLOAT", { "default": 5.0, "min": 0.0, "max": 25.0, "step": 0.5 }),
1081
+ "ratio": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1 }),
1082
+ }}
1083
+
1084
+ RETURN_TYPES = ("IMAGE",)
1085
+ CATEGORY = "essentials/image processing"
1086
+ FUNCTION = "execute"
1087
+
1088
+ def execute(self, image, noise_radius, preserve_edges, sharpen, ratio):
1089
+ import cv2
1090
+
1091
+ output = []
1092
+ #diagonal = np.sqrt(image.shape[1]**2 + image.shape[2]**2)
1093
+ if preserve_edges > 0:
1094
+ preserve_edges = max(1 - preserve_edges, 0.05)
1095
+
1096
+ for img in image:
1097
+ if noise_radius > 1:
1098
+ sigma = 0.3 * ((noise_radius - 1) * 0.5 - 1) + 0.8 # this is what pytorch uses for blur
1099
+ #sigma_color = preserve_edges * (diagonal / 2048)
1100
+ blurred = cv2.bilateralFilter(img.cpu().numpy(), noise_radius, preserve_edges, sigma)
1101
+ blurred = torch.from_numpy(blurred)
1102
+ else:
1103
+ blurred = img
1104
+
1105
+ if sharpen > 0:
1106
+ sharpened = kornia.enhance.sharpness(img.permute(2,0,1), sharpen).permute(1,2,0)
1107
+ else:
1108
+ sharpened = img
1109
+
1110
+ img = ratio * sharpened + (1 - ratio) * blurred
1111
+ img = torch.clamp(img, 0, 1)
1112
+ output.append(img)
1113
+
1114
+ del blurred, sharpened
1115
+ output = torch.stack(output)
1116
+
1117
+ return (output,)
1118
+
1119
+
1120
+ class ExtractKeyframes:
1121
+ @classmethod
1122
+ def INPUT_TYPES(s):
1123
+ return {
1124
+ "required": {
1125
+ "image": ("IMAGE",),
1126
+ "threshold": ("FLOAT", { "default": 0.85, "min": 0.00, "max": 1.00, "step": 0.01, }),
1127
+ }
1128
+ }
1129
+
1130
+ RETURN_TYPES = ("IMAGE", "STRING")
1131
+ RETURN_NAMES = ("KEYFRAMES", "indexes")
1132
+
1133
+ FUNCTION = "execute"
1134
+ CATEGORY = "essentials"
1135
+
1136
+ def execute(self, image, threshold):
1137
+ window_size = 2
1138
+
1139
+ variations = torch.sum(torch.abs(image[1:] - image[:-1]), dim=[1, 2, 3])
1140
+ #variations = torch.sum((image[1:] - image[:-1]) ** 2, dim=[1, 2, 3])
1141
+ threshold = torch.quantile(variations.float(), threshold).item()
1142
+
1143
+ keyframes = []
1144
+ for i in range(image.shape[0] - window_size + 1):
1145
+ window = image[i:i + window_size]
1146
+ variation = torch.sum(torch.abs(window[-1] - window[0])).item()
1147
+
1148
+ if variation > threshold:
1149
+ keyframes.append(i + window_size - 1)
1150
+
1151
+ return (image[keyframes], ','.join(map(str, keyframes)),)
1152
+
1153
+ class ImageColorMatch:
1154
+ @classmethod
1155
+ def INPUT_TYPES(s):
1156
+ return {
1157
+ "required": {
1158
+ "image": ("IMAGE",),
1159
+ "reference": ("IMAGE",),
1160
+ "color_space": (["LAB", "YCbCr", "RGB", "LUV", "YUV", "XYZ"],),
1161
+ "factor": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05, }),
1162
+ "device": (["auto", "cpu", "gpu"],),
1163
+ "batch_size": ("INT", { "default": 0, "min": 0, "max": 1024, "step": 1, }),
1164
+ },
1165
+ "optional": {
1166
+ "reference_mask": ("MASK",),
1167
+ }
1168
+ }
1169
+
1170
+ RETURN_TYPES = ("IMAGE",)
1171
+ FUNCTION = "execute"
1172
+ CATEGORY = "essentials/image processing"
1173
+
1174
+ def execute(self, image, reference, color_space, factor, device, batch_size, reference_mask=None):
1175
+ if "gpu" == device:
1176
+ device = comfy.model_management.get_torch_device()
1177
+ elif "auto" == device:
1178
+ device = comfy.model_management.intermediate_device()
1179
+ else:
1180
+ device = 'cpu'
1181
+
1182
+ image = image.permute([0, 3, 1, 2])
1183
+ reference = reference.permute([0, 3, 1, 2]).to(device)
1184
+
1185
+ # Ensure reference_mask is in the correct format and on the right device
1186
+ if reference_mask is not None:
1187
+ assert reference_mask.ndim == 3, f"Expected reference_mask to have 3 dimensions, but got {reference_mask.ndim}"
1188
+ assert reference_mask.shape[0] == reference.shape[0], f"Frame count mismatch: reference_mask has {reference_mask.shape[0]} frames, but reference has {reference.shape[0]}"
1189
+
1190
+ # Reshape mask to (batch, 1, height, width)
1191
+ reference_mask = reference_mask.unsqueeze(1).to(device)
1192
+
1193
+ # Ensure the mask is binary (0 or 1)
1194
+ reference_mask = (reference_mask > 0.5).float()
1195
+
1196
+ # Ensure spatial dimensions match
1197
+ if reference_mask.shape[2:] != reference.shape[2:]:
1198
+ reference_mask = comfy.utils.common_upscale(
1199
+ reference_mask,
1200
+ reference.shape[3], reference.shape[2],
1201
+ upscale_method='bicubic',
1202
+ crop='center'
1203
+ )
1204
+
1205
+ if batch_size == 0 or batch_size > image.shape[0]:
1206
+ batch_size = image.shape[0]
1207
+
1208
+ if "LAB" == color_space:
1209
+ reference = kornia.color.rgb_to_lab(reference)
1210
+ elif "YCbCr" == color_space:
1211
+ reference = kornia.color.rgb_to_ycbcr(reference)
1212
+ elif "LUV" == color_space:
1213
+ reference = kornia.color.rgb_to_luv(reference)
1214
+ elif "YUV" == color_space:
1215
+ reference = kornia.color.rgb_to_yuv(reference)
1216
+ elif "XYZ" == color_space:
1217
+ reference = kornia.color.rgb_to_xyz(reference)
1218
+
1219
+ reference_mean, reference_std = self.compute_mean_std(reference, reference_mask)
1220
+
1221
+ image_batch = torch.split(image, batch_size, dim=0)
1222
+ output = []
1223
+
1224
+ for image in image_batch:
1225
+ image = image.to(device)
1226
+
1227
+ if color_space == "LAB":
1228
+ image = kornia.color.rgb_to_lab(image)
1229
+ elif color_space == "YCbCr":
1230
+ image = kornia.color.rgb_to_ycbcr(image)
1231
+ elif color_space == "LUV":
1232
+ image = kornia.color.rgb_to_luv(image)
1233
+ elif color_space == "YUV":
1234
+ image = kornia.color.rgb_to_yuv(image)
1235
+ elif color_space == "XYZ":
1236
+ image = kornia.color.rgb_to_xyz(image)
1237
+
1238
+ image_mean, image_std = self.compute_mean_std(image)
1239
+
1240
+ matched = torch.nan_to_num((image - image_mean) / image_std) * torch.nan_to_num(reference_std) + reference_mean
1241
+ matched = factor * matched + (1 - factor) * image
1242
+
1243
+ if color_space == "LAB":
1244
+ matched = kornia.color.lab_to_rgb(matched)
1245
+ elif color_space == "YCbCr":
1246
+ matched = kornia.color.ycbcr_to_rgb(matched)
1247
+ elif color_space == "LUV":
1248
+ matched = kornia.color.luv_to_rgb(matched)
1249
+ elif color_space == "YUV":
1250
+ matched = kornia.color.yuv_to_rgb(matched)
1251
+ elif color_space == "XYZ":
1252
+ matched = kornia.color.xyz_to_rgb(matched)
1253
+
1254
+ out = matched.permute([0, 2, 3, 1]).clamp(0, 1).to(comfy.model_management.intermediate_device())
1255
+ output.append(out)
1256
+
1257
+ out = None
1258
+ output = torch.cat(output, dim=0)
1259
+ return (output,)
1260
+
1261
+ def compute_mean_std(self, tensor, mask=None):
1262
+ if mask is not None:
1263
+ # Apply mask to the tensor
1264
+ masked_tensor = tensor * mask
1265
+
1266
+ # Calculate the sum of the mask for each channel
1267
+ mask_sum = mask.sum(dim=[2, 3], keepdim=True)
1268
+
1269
+ # Avoid division by zero
1270
+ mask_sum = torch.clamp(mask_sum, min=1e-6)
1271
+
1272
+ # Calculate mean and std only for masked area
1273
+ mean = torch.nan_to_num(masked_tensor.sum(dim=[2, 3], keepdim=True) / mask_sum)
1274
+ std = torch.sqrt(torch.nan_to_num(((masked_tensor - mean) ** 2 * mask).sum(dim=[2, 3], keepdim=True) / mask_sum))
1275
+ else:
1276
+ mean = tensor.mean(dim=[2, 3], keepdim=True)
1277
+ std = tensor.std(dim=[2, 3], keepdim=True)
1278
+ return mean, std
1279
+
1280
+ class ImageColorMatchAdobe(ImageColorMatch):
1281
+ @classmethod
1282
+ def INPUT_TYPES(s):
1283
+ return {
1284
+ "required": {
1285
+ "image": ("IMAGE",),
1286
+ "reference": ("IMAGE",),
1287
+ "color_space": (["RGB", "LAB"],),
1288
+ "luminance_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05}),
1289
+ "color_intensity_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05}),
1290
+ "fade_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
1291
+ "neutralization_factor": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05}),
1292
+ "device": (["auto", "cpu", "gpu"],),
1293
+ },
1294
+ "optional": {
1295
+ "reference_mask": ("MASK",),
1296
+ }
1297
+ }
1298
+
1299
+ RETURN_TYPES = ("IMAGE",)
1300
+ FUNCTION = "execute"
1301
+ CATEGORY = "essentials/image processing"
1302
+
1303
+ def analyze_color_statistics(self, image, mask=None):
1304
+ # Assuming image is in RGB format
1305
+ l, a, b = kornia.color.rgb_to_lab(image).chunk(3, dim=1)
1306
+
1307
+ if mask is not None:
1308
+ # Ensure mask is binary and has the same spatial dimensions as the image
1309
+ mask = F.interpolate(mask, size=image.shape[2:], mode='nearest')
1310
+ mask = (mask > 0.5).float()
1311
+
1312
+ # Apply mask to each channel
1313
+ l = l * mask
1314
+ a = a * mask
1315
+ b = b * mask
1316
+
1317
+ # Compute masked mean and std
1318
+ num_pixels = mask.sum()
1319
+ mean_l = (l * mask).sum() / num_pixels
1320
+ mean_a = (a * mask).sum() / num_pixels
1321
+ mean_b = (b * mask).sum() / num_pixels
1322
+ std_l = torch.sqrt(((l - mean_l)**2 * mask).sum() / num_pixels)
1323
+ var_ab = ((a - mean_a)**2 + (b - mean_b)**2) * mask
1324
+ std_ab = torch.sqrt(var_ab.sum() / num_pixels)
1325
+ else:
1326
+ mean_l = l.mean()
1327
+ std_l = l.std()
1328
+ mean_a = a.mean()
1329
+ mean_b = b.mean()
1330
+ std_ab = torch.sqrt(a.var() + b.var())
1331
+
1332
+ return mean_l, std_l, mean_a, mean_b, std_ab
1333
+
1334
+ def apply_color_transformation(self, image, source_stats, dest_stats, L, C, N):
1335
+ l, a, b = kornia.color.rgb_to_lab(image).chunk(3, dim=1)
1336
+
1337
+ # Unpack statistics
1338
+ src_mean_l, src_std_l, src_mean_a, src_mean_b, src_std_ab = source_stats
1339
+ dest_mean_l, dest_std_l, dest_mean_a, dest_mean_b, dest_std_ab = dest_stats
1340
+
1341
+ # Adjust luminance
1342
+ l_new = (l - dest_mean_l) * (src_std_l / dest_std_l) * L + src_mean_l
1343
+
1344
+ # Neutralize color cast
1345
+ a = a - N * dest_mean_a
1346
+ b = b - N * dest_mean_b
1347
+
1348
+ # Adjust color intensity
1349
+ a_new = a * (src_std_ab / dest_std_ab) * C
1350
+ b_new = b * (src_std_ab / dest_std_ab) * C
1351
+
1352
+ # Combine channels
1353
+ lab_new = torch.cat([l_new, a_new, b_new], dim=1)
1354
+
1355
+ # Convert back to RGB
1356
+ rgb_new = kornia.color.lab_to_rgb(lab_new)
1357
+
1358
+ return rgb_new
1359
+
1360
+ def execute(self, image, reference, color_space, luminance_factor, color_intensity_factor, fade_factor, neutralization_factor, device, reference_mask=None):
1361
+ if "gpu" == device:
1362
+ device = comfy.model_management.get_torch_device()
1363
+ elif "auto" == device:
1364
+ device = comfy.model_management.intermediate_device()
1365
+ else:
1366
+ device = 'cpu'
1367
+
1368
+ # Ensure image and reference are in the correct shape (B, C, H, W)
1369
+ image = image.permute(0, 3, 1, 2).to(device)
1370
+ reference = reference.permute(0, 3, 1, 2).to(device)
1371
+
1372
+ # Handle reference_mask (if provided)
1373
+ if reference_mask is not None:
1374
+ # Ensure reference_mask is 4D (B, 1, H, W)
1375
+ if reference_mask.ndim == 2:
1376
+ reference_mask = reference_mask.unsqueeze(0).unsqueeze(0)
1377
+ elif reference_mask.ndim == 3:
1378
+ reference_mask = reference_mask.unsqueeze(1)
1379
+ reference_mask = reference_mask.to(device)
1380
+
1381
+ # Analyze color statistics
1382
+ source_stats = self.analyze_color_statistics(reference, reference_mask)
1383
+ dest_stats = self.analyze_color_statistics(image)
1384
+
1385
+ # Apply color transformation
1386
+ transformed = self.apply_color_transformation(
1387
+ image, source_stats, dest_stats,
1388
+ luminance_factor, color_intensity_factor, neutralization_factor
1389
+ )
1390
+
1391
+ # Apply fade factor
1392
+ result = fade_factor * transformed + (1 - fade_factor) * image
1393
+
1394
+ # Convert back to (B, H, W, C) format and ensure values are in [0, 1] range
1395
+ result = result.permute(0, 2, 3, 1).clamp(0, 1).to(comfy.model_management.intermediate_device())
1396
+
1397
+ return (result,)
1398
+
1399
+
1400
+ class ImageHistogramMatch:
1401
+ @classmethod
1402
+ def INPUT_TYPES(s):
1403
+ return {
1404
+ "required": {
1405
+ "image": ("IMAGE",),
1406
+ "reference": ("IMAGE",),
1407
+ "method": (["pytorch", "skimage"],),
1408
+ "factor": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05, }),
1409
+ "device": (["auto", "cpu", "gpu"],),
1410
+ }
1411
+ }
1412
+
1413
+ RETURN_TYPES = ("IMAGE",)
1414
+ FUNCTION = "execute"
1415
+ CATEGORY = "essentials/image processing"
1416
+
1417
+ def execute(self, image, reference, method, factor, device):
1418
+ if "gpu" == device:
1419
+ device = comfy.model_management.get_torch_device()
1420
+ elif "auto" == device:
1421
+ device = comfy.model_management.intermediate_device()
1422
+ else:
1423
+ device = 'cpu'
1424
+
1425
+ if "pytorch" in method:
1426
+ from .histogram_matching import Histogram_Matching
1427
+
1428
+ image = image.permute([0, 3, 1, 2]).to(device)
1429
+ reference = reference.permute([0, 3, 1, 2]).to(device)[0].unsqueeze(0)
1430
+ image.requires_grad = True
1431
+ reference.requires_grad = True
1432
+
1433
+ out = []
1434
+
1435
+ for i in image:
1436
+ i = i.unsqueeze(0)
1437
+ hm = Histogram_Matching(differentiable=True)
1438
+ out.append(hm(i, reference))
1439
+ out = torch.cat(out, dim=0)
1440
+ out = factor * out + (1 - factor) * image
1441
+ out = out.permute([0, 2, 3, 1]).clamp(0, 1)
1442
+ else:
1443
+ from skimage.exposure import match_histograms
1444
+
1445
+ out = torch.from_numpy(match_histograms(image.cpu().numpy(), reference.cpu().numpy(), channel_axis=3)).to(device)
1446
+ out = factor * out + (1 - factor) * image.to(device)
1447
+
1448
+ return (out.to(comfy.model_management.intermediate_device()),)
1449
+
1450
+ """
1451
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1452
+ Utilities
1453
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1454
+ """
1455
+
1456
+ class ImageToDevice:
1457
+ @classmethod
1458
+ def INPUT_TYPES(s):
1459
+ return {
1460
+ "required": {
1461
+ "image": ("IMAGE",),
1462
+ "device": (["auto", "cpu", "gpu"],),
1463
+ }
1464
+ }
1465
+
1466
+ RETURN_TYPES = ("IMAGE",)
1467
+ FUNCTION = "execute"
1468
+ CATEGORY = "essentials/image utils"
1469
+
1470
+ def execute(self, image, device):
1471
+ if "gpu" == device:
1472
+ device = comfy.model_management.get_torch_device()
1473
+ elif "auto" == device:
1474
+ device = comfy.model_management.intermediate_device()
1475
+ else:
1476
+ device = 'cpu'
1477
+
1478
+ image = image.clone().to(device)
1479
+ torch.cuda.empty_cache()
1480
+
1481
+ return (image,)
1482
+
1483
+ class GetImageSize:
1484
+ @classmethod
1485
+ def INPUT_TYPES(s):
1486
+ return {
1487
+ "required": {
1488
+ "image": ("IMAGE",),
1489
+ }
1490
+ }
1491
+
1492
+ RETURN_TYPES = ("INT", "INT", "INT",)
1493
+ RETURN_NAMES = ("width", "height", "count")
1494
+ FUNCTION = "execute"
1495
+ CATEGORY = "essentials/image utils"
1496
+
1497
+ def execute(self, image):
1498
+ return (image.shape[2], image.shape[1], image.shape[0])
1499
+
1500
+ class ImageRemoveAlpha:
1501
+ @classmethod
1502
+ def INPUT_TYPES(s):
1503
+ return {
1504
+ "required": {
1505
+ "image": ("IMAGE",),
1506
+ },
1507
+ }
1508
+
1509
+ RETURN_TYPES = ("IMAGE",)
1510
+ FUNCTION = "execute"
1511
+ CATEGORY = "essentials/image utils"
1512
+
1513
+ def execute(self, image):
1514
+ if image.shape[3] == 4:
1515
+ image = image[..., :3]
1516
+ return (image,)
1517
+
1518
+ class ImagePreviewFromLatent(SaveImage):
1519
+ def __init__(self):
1520
+ self.output_dir = folder_paths.get_temp_directory()
1521
+ self.type = "temp"
1522
+ self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
1523
+ self.compress_level = 1
1524
+
1525
+ @classmethod
1526
+ def INPUT_TYPES(s):
1527
+ return {
1528
+ "required": {
1529
+ "latent": ("LATENT",),
1530
+ "vae": ("VAE", ),
1531
+ "tile_size": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 64})
1532
+ }, "optional": {
1533
+ "image": (["none"], {"image_upload": False}),
1534
+ }, "hidden": {
1535
+ "prompt": "PROMPT",
1536
+ "extra_pnginfo": "EXTRA_PNGINFO",
1537
+ },
1538
+ }
1539
+
1540
+ RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT",)
1541
+ RETURN_NAMES = ("IMAGE", "MASK", "width", "height",)
1542
+ FUNCTION = "execute"
1543
+ CATEGORY = "essentials/image utils"
1544
+
1545
+ def execute(self, latent, vae, tile_size, prompt=None, extra_pnginfo=None, image=None, filename_prefix="ComfyUI"):
1546
+ mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
1547
+ ui = None
1548
+
1549
+ if image.startswith("clipspace"):
1550
+ image_path = folder_paths.get_annotated_filepath(image)
1551
+ if not os.path.exists(image_path):
1552
+ raise ValueError(f"Clipspace image does not exist anymore, select 'none' in the image field.")
1553
+
1554
+ img = pillow(Image.open, image_path)
1555
+ img = pillow(ImageOps.exif_transpose, img)
1556
+ if img.mode == "I":
1557
+ img = img.point(lambda i: i * (1 / 255))
1558
+ image = img.convert("RGB")
1559
+ image = np.array(image).astype(np.float32) / 255.0
1560
+ image = torch.from_numpy(image)[None,]
1561
+ if "A" in img.getbands():
1562
+ mask = np.array(img.getchannel('A')).astype(np.float32) / 255.0
1563
+ mask = 1. - torch.from_numpy(mask)
1564
+ ui = {
1565
+ "filename": os.path.basename(image_path),
1566
+ "subfolder": os.path.dirname(image_path),
1567
+ "type": "temp",
1568
+ }
1569
+ else:
1570
+ if tile_size > 0:
1571
+ tile_size = max(tile_size, 320)
1572
+ image = vae.decode_tiled(latent["samples"], tile_x=tile_size // 8, tile_y=tile_size // 8, )
1573
+ else:
1574
+ image = vae.decode(latent["samples"])
1575
+ ui = self.save_images(image, filename_prefix, prompt, extra_pnginfo)
1576
+
1577
+ out = {**ui, "result": (image, mask, image.shape[2], image.shape[1],)}
1578
+ return out
1579
+
1580
+ class NoiseFromImage:
1581
+ @classmethod
1582
+ def INPUT_TYPES(s):
1583
+ return {
1584
+ "required": {
1585
+ "image": ("IMAGE",),
1586
+ "noise_strenght": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01 }),
1587
+ "noise_size": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01 }),
1588
+ "color_noise": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01 }),
1589
+ "mask_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01 }),
1590
+ "mask_scale_diff": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01 }),
1591
+ "mask_contrast": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1 }),
1592
+ "saturation": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 100.0, "step": 0.1 }),
1593
+ "contrast": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1 }),
1594
+ "blur": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1 }),
1595
+ },
1596
+ "optional": {
1597
+ "noise_mask": ("IMAGE",),
1598
+ }
1599
+ }
1600
+
1601
+ RETURN_TYPES = ("IMAGE",)
1602
+ FUNCTION = "execute"
1603
+ CATEGORY = "essentials/image utils"
1604
+
1605
+ def execute(self, image, noise_size, color_noise, mask_strength, mask_scale_diff, mask_contrast, noise_strenght, saturation, contrast, blur, noise_mask=None):
1606
+ torch.manual_seed(0)
1607
+
1608
+ elastic_alpha = max(image.shape[1], image.shape[2])# * noise_size
1609
+ elastic_sigma = elastic_alpha / 400 * noise_size
1610
+
1611
+ blur_size = int(6 * blur+1)
1612
+ if blur_size % 2 == 0:
1613
+ blur_size+= 1
1614
+
1615
+ if noise_mask is None:
1616
+ noise_mask = image
1617
+
1618
+ # increase contrast of the mask
1619
+ if mask_contrast != 1:
1620
+ noise_mask = T.ColorJitter(contrast=(mask_contrast,mask_contrast))(noise_mask.permute([0, 3, 1, 2])).permute([0, 2, 3, 1])
1621
+
1622
+ # Ensure noise mask is the same size as the image
1623
+ if noise_mask.shape[1:] != image.shape[1:]:
1624
+ noise_mask = F.interpolate(noise_mask.permute([0, 3, 1, 2]), size=(image.shape[1], image.shape[2]), mode='bicubic', align_corners=False)
1625
+ noise_mask = noise_mask.permute([0, 2, 3, 1])
1626
+ # Ensure we have the same number of masks and images
1627
+ if noise_mask.shape[0] > image.shape[0]:
1628
+ noise_mask = noise_mask[:image.shape[0]]
1629
+ else:
1630
+ noise_mask = torch.cat((noise_mask, noise_mask[-1:].repeat((image.shape[0]-noise_mask.shape[0], 1, 1, 1))), dim=0)
1631
+
1632
+ # Convert mask to grayscale mask
1633
+ noise_mask = noise_mask.mean(dim=3).unsqueeze(-1)
1634
+
1635
+ # add color noise
1636
+ imgs = image.clone().permute([0, 3, 1, 2])
1637
+ if color_noise > 0:
1638
+ color_noise = torch.normal(torch.zeros_like(imgs), std=color_noise)
1639
+ color_noise *= (imgs - imgs.min()) / (imgs.max() - imgs.min())
1640
+
1641
+ imgs = imgs + color_noise
1642
+ imgs = imgs.clamp(0, 1)
1643
+
1644
+ # create fine and coarse noise
1645
+ fine_noise = []
1646
+ for n in imgs:
1647
+ avg_color = n.mean(dim=[1,2])
1648
+
1649
+ tmp_noise = T.ElasticTransform(alpha=elastic_alpha, sigma=elastic_sigma, fill=avg_color.tolist())(n)
1650
+ if blur > 0:
1651
+ tmp_noise = T.GaussianBlur(blur_size, blur)(tmp_noise)
1652
+ tmp_noise = T.ColorJitter(contrast=(contrast,contrast), saturation=(saturation,saturation))(tmp_noise)
1653
+ fine_noise.append(tmp_noise)
1654
+
1655
+ imgs = None
1656
+ del imgs
1657
+
1658
+ fine_noise = torch.stack(fine_noise, dim=0)
1659
+ fine_noise = fine_noise.permute([0, 2, 3, 1])
1660
+ #fine_noise = torch.stack(fine_noise, dim=0)
1661
+ #fine_noise = pb(fine_noise)
1662
+ mask_scale_diff = min(mask_scale_diff, 0.99)
1663
+ if mask_scale_diff > 0:
1664
+ coarse_noise = F.interpolate(fine_noise.permute([0, 3, 1, 2]), scale_factor=1-mask_scale_diff, mode='area')
1665
+ coarse_noise = F.interpolate(coarse_noise, size=(fine_noise.shape[1], fine_noise.shape[2]), mode='bilinear', align_corners=False)
1666
+ coarse_noise = coarse_noise.permute([0, 2, 3, 1])
1667
+ else:
1668
+ coarse_noise = fine_noise
1669
+
1670
+ output = (1 - noise_mask) * coarse_noise + noise_mask * fine_noise
1671
+
1672
+ if mask_strength < 1:
1673
+ noise_mask = noise_mask.pow(mask_strength)
1674
+ noise_mask = torch.nan_to_num(noise_mask).clamp(0, 1)
1675
+ output = noise_mask * output + (1 - noise_mask) * image
1676
+
1677
+ # apply noise to image
1678
+ output = output * noise_strenght + image * (1 - noise_strenght)
1679
+ output = output.clamp(0, 1)
1680
+
1681
+ return (output, )
1682
+
1683
+ IMAGE_CLASS_MAPPINGS = {
1684
+ # Image analysis
1685
+ "ImageEnhanceDifference+": ImageEnhanceDifference,
1686
+
1687
+ # Image batch
1688
+ "ImageBatchMultiple+": ImageBatchMultiple,
1689
+ "ImageExpandBatch+": ImageExpandBatch,
1690
+ "ImageFromBatch+": ImageFromBatch,
1691
+ "ImageListToBatch+": ImageListToBatch,
1692
+ "ImageBatchToList+": ImageBatchToList,
1693
+
1694
+ # Image manipulation
1695
+ "ImageCompositeFromMaskBatch+": ImageCompositeFromMaskBatch,
1696
+ "ImageComposite+": ImageComposite,
1697
+ "ImageCrop+": ImageCrop,
1698
+ "ImageFlip+": ImageFlip,
1699
+ "ImageRandomTransform+": ImageRandomTransform,
1700
+ "ImageRemoveAlpha+": ImageRemoveAlpha,
1701
+ "ImageRemoveBackground+": ImageRemoveBackground,
1702
+ "ImageResize+": ImageResize,
1703
+ "ImageSeamCarving+": ImageSeamCarving,
1704
+ "ImageTile+": ImageTile,
1705
+ "ImageUntile+": ImageUntile,
1706
+ "RemBGSession+": RemBGSession,
1707
+ "TransparentBGSession+": TransparentBGSession,
1708
+
1709
+ # Image processing
1710
+ "ImageApplyLUT+": ImageApplyLUT,
1711
+ "ImageCASharpening+": ImageCAS,
1712
+ "ImageDesaturate+": ImageDesaturate,
1713
+ "PixelOEPixelize+": PixelOEPixelize,
1714
+ "ImagePosterize+": ImagePosterize,
1715
+ "ImageColorMatch+": ImageColorMatch,
1716
+ "ImageColorMatchAdobe+": ImageColorMatchAdobe,
1717
+ "ImageHistogramMatch+": ImageHistogramMatch,
1718
+ "ImageSmartSharpen+": ImageSmartSharpen,
1719
+
1720
+ # Utilities
1721
+ "GetImageSize+": GetImageSize,
1722
+ "ImageToDevice+": ImageToDevice,
1723
+ "ImagePreviewFromLatent+": ImagePreviewFromLatent,
1724
+ "NoiseFromImage+": NoiseFromImage,
1725
+ #"ExtractKeyframes+": ExtractKeyframes,
1726
+ }
1727
+
1728
+ IMAGE_NAME_MAPPINGS = {
1729
+ # Image analysis
1730
+ "ImageEnhanceDifference+": "🔧 Image Enhance Difference",
1731
+
1732
+ # Image batch
1733
+ "ImageBatchMultiple+": "🔧 Images Batch Multiple",
1734
+ "ImageExpandBatch+": "🔧 Image Expand Batch",
1735
+ "ImageFromBatch+": "🔧 Image From Batch",
1736
+ "ImageListToBatch+": "🔧 Image List To Batch",
1737
+ "ImageBatchToList+": "🔧 Image Batch To List",
1738
+
1739
+ # Image manipulation
1740
+ "ImageCompositeFromMaskBatch+": "🔧 Image Composite From Mask Batch",
1741
+ "ImageComposite+": "🔧 Image Composite",
1742
+ "ImageCrop+": "🔧 Image Crop",
1743
+ "ImageFlip+": "🔧 Image Flip",
1744
+ "ImageRandomTransform+": "🔧 Image Random Transform",
1745
+ "ImageRemoveAlpha+": "🔧 Image Remove Alpha",
1746
+ "ImageRemoveBackground+": "🔧 Image Remove Background",
1747
+ "ImageResize+": "🔧 Image Resize",
1748
+ "ImageSeamCarving+": "🔧 Image Seam Carving",
1749
+ "ImageTile+": "🔧 Image Tile",
1750
+ "ImageUntile+": "🔧 Image Untile",
1751
+ "RemBGSession+": "🔧 RemBG Session",
1752
+ "TransparentBGSession+": "🔧 InSPyReNet TransparentBG",
1753
+
1754
+ # Image processing
1755
+ "ImageApplyLUT+": "🔧 Image Apply LUT",
1756
+ "ImageCASharpening+": "🔧 Image Contrast Adaptive Sharpening",
1757
+ "ImageDesaturate+": "🔧 Image Desaturate",
1758
+ "PixelOEPixelize+": "🔧 Pixelize",
1759
+ "ImagePosterize+": "🔧 Image Posterize",
1760
+ "ImageColorMatch+": "🔧 Image Color Match",
1761
+ "ImageColorMatchAdobe+": "🔧 Image Color Match Adobe",
1762
+ "ImageHistogramMatch+": "🔧 Image Histogram Match",
1763
+ "ImageSmartSharpen+": "🔧 Image Smart Sharpen",
1764
+
1765
+ # Utilities
1766
+ "GetImageSize+": "🔧 Get Image Size",
1767
+ "ImageToDevice+": "🔧 Image To Device",
1768
+ "ImagePreviewFromLatent+": "🔧 Image Preview From Latent",
1769
+ "NoiseFromImage+": "🔧 Noise From Image",
1770
+ }
ComfyUI_essentials/mask.py ADDED
@@ -0,0 +1,596 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from nodes import SaveImage
2
+ import torch
3
+ import torchvision.transforms.v2 as T
4
+ import random
5
+ import folder_paths
6
+ import comfy.utils
7
+ from .image import ImageExpandBatch
8
+ from .utils import AnyType
9
+ import numpy as np
10
+ import scipy
11
+ from PIL import Image
12
+ from nodes import MAX_RESOLUTION
13
+ import math
14
+
15
+ any = AnyType("*")
16
+
17
+ class MaskBlur:
18
+ @classmethod
19
+ def INPUT_TYPES(s):
20
+ return {
21
+ "required": {
22
+ "mask": ("MASK",),
23
+ "amount": ("INT", { "default": 6, "min": 0, "max": 256, "step": 1, }),
24
+ "device": (["auto", "cpu", "gpu"],),
25
+ }
26
+ }
27
+
28
+ RETURN_TYPES = ("MASK",)
29
+ FUNCTION = "execute"
30
+ CATEGORY = "essentials/mask"
31
+
32
+ def execute(self, mask, amount, device):
33
+ if amount == 0:
34
+ return (mask,)
35
+
36
+ if "gpu" == device:
37
+ mask = mask.to(comfy.model_management.get_torch_device())
38
+ elif "cpu" == device:
39
+ mask = mask.to('cpu')
40
+
41
+ if amount % 2 == 0:
42
+ amount+= 1
43
+
44
+ if mask.dim() == 2:
45
+ mask = mask.unsqueeze(0)
46
+
47
+ mask = T.functional.gaussian_blur(mask.unsqueeze(1), amount).squeeze(1)
48
+
49
+ if "gpu" == device or "cpu" == device:
50
+ mask = mask.to(comfy.model_management.intermediate_device())
51
+
52
+ return(mask,)
53
+
54
+ class MaskFlip:
55
+ @classmethod
56
+ def INPUT_TYPES(s):
57
+ return {
58
+ "required": {
59
+ "mask": ("MASK",),
60
+ "axis": (["x", "y", "xy"],),
61
+ }
62
+ }
63
+
64
+ RETURN_TYPES = ("MASK",)
65
+ FUNCTION = "execute"
66
+ CATEGORY = "essentials/mask"
67
+
68
+ def execute(self, mask, axis):
69
+ if mask.dim() == 2:
70
+ mask = mask.unsqueeze(0)
71
+
72
+ dim = ()
73
+ if "y" in axis:
74
+ dim += (1,)
75
+ if "x" in axis:
76
+ dim += (2,)
77
+ mask = torch.flip(mask, dims=dim)
78
+
79
+ return(mask,)
80
+
81
+ class MaskPreview(SaveImage):
82
+ def __init__(self):
83
+ self.output_dir = folder_paths.get_temp_directory()
84
+ self.type = "temp"
85
+ self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
86
+ self.compress_level = 4
87
+
88
+ @classmethod
89
+ def INPUT_TYPES(s):
90
+ return {
91
+ "required": {"mask": ("MASK",), },
92
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
93
+ }
94
+
95
+ FUNCTION = "execute"
96
+ CATEGORY = "essentials/mask"
97
+
98
+ def execute(self, mask, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
99
+ preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
100
+ return self.save_images(preview, filename_prefix, prompt, extra_pnginfo)
101
+
102
+ class MaskBatch:
103
+ @classmethod
104
+ def INPUT_TYPES(s):
105
+ return {
106
+ "required": {
107
+ "mask1": ("MASK",),
108
+ "mask2": ("MASK",),
109
+ }
110
+ }
111
+
112
+ RETURN_TYPES = ("MASK",)
113
+ FUNCTION = "execute"
114
+ CATEGORY = "essentials/mask batch"
115
+
116
+ def execute(self, mask1, mask2):
117
+ if mask1.shape[1:] != mask2.shape[1:]:
118
+ mask2 = comfy.utils.common_upscale(mask2.unsqueeze(1).expand(-1,3,-1,-1), mask1.shape[2], mask1.shape[1], upscale_method='bicubic', crop='center')[:,0,:,:]
119
+
120
+ return (torch.cat((mask1, mask2), dim=0),)
121
+
122
+ class MaskExpandBatch:
123
+ @classmethod
124
+ def INPUT_TYPES(s):
125
+ return {
126
+ "required": {
127
+ "mask": ("MASK",),
128
+ "size": ("INT", { "default": 16, "min": 1, "step": 1, }),
129
+ "method": (["expand", "repeat all", "repeat first", "repeat last"],)
130
+ }
131
+ }
132
+
133
+ RETURN_TYPES = ("MASK",)
134
+ FUNCTION = "execute"
135
+ CATEGORY = "essentials/mask batch"
136
+
137
+ def execute(self, mask, size, method):
138
+ return (ImageExpandBatch().execute(mask.unsqueeze(1).expand(-1,3,-1,-1), size, method)[0][:,0,:,:],)
139
+
140
+
141
+ class MaskBoundingBox:
142
+ @classmethod
143
+ def INPUT_TYPES(s):
144
+ return {
145
+ "required": {
146
+ "mask": ("MASK",),
147
+ "padding": ("INT", { "default": 0, "min": 0, "max": 4096, "step": 1, }),
148
+ "blur": ("INT", { "default": 0, "min": 0, "max": 256, "step": 1, }),
149
+ },
150
+ "optional": {
151
+ "image_optional": ("IMAGE",),
152
+ }
153
+ }
154
+
155
+ RETURN_TYPES = ("MASK", "IMAGE", "INT", "INT", "INT", "INT")
156
+ RETURN_NAMES = ("MASK", "IMAGE", "x", "y", "width", "height")
157
+ FUNCTION = "execute"
158
+ CATEGORY = "essentials/mask"
159
+
160
+ def execute(self, mask, padding, blur, image_optional=None):
161
+ if mask.dim() == 2:
162
+ mask = mask.unsqueeze(0)
163
+
164
+ if image_optional is None:
165
+ image_optional = mask.unsqueeze(3).repeat(1, 1, 1, 3)
166
+
167
+ # resize the image if it's not the same size as the mask
168
+ if image_optional.shape[1:] != mask.shape[1:]:
169
+ image_optional = comfy.utils.common_upscale(image_optional.permute([0,3,1,2]), mask.shape[2], mask.shape[1], upscale_method='bicubic', crop='center').permute([0,2,3,1])
170
+
171
+ # match batch size
172
+ if image_optional.shape[0] < mask.shape[0]:
173
+ image_optional = torch.cat((image_optional, image_optional[-1].unsqueeze(0).repeat(mask.shape[0]-image_optional.shape[0], 1, 1, 1)), dim=0)
174
+ elif image_optional.shape[0] > mask.shape[0]:
175
+ image_optional = image_optional[:mask.shape[0]]
176
+
177
+ # blur the mask
178
+ if blur > 0:
179
+ if blur % 2 == 0:
180
+ blur += 1
181
+ mask = T.functional.gaussian_blur(mask.unsqueeze(1), blur).squeeze(1)
182
+
183
+ _, y, x = torch.where(mask)
184
+ x1 = max(0, x.min().item() - padding)
185
+ x2 = min(mask.shape[2], x.max().item() + 1 + padding)
186
+ y1 = max(0, y.min().item() - padding)
187
+ y2 = min(mask.shape[1], y.max().item() + 1 + padding)
188
+
189
+ # crop the mask
190
+ mask = mask[:, y1:y2, x1:x2]
191
+ image_optional = image_optional[:, y1:y2, x1:x2, :]
192
+
193
+ return (mask, image_optional, x1, y1, x2 - x1, y2 - y1)
194
+
195
+
196
+ class MaskFromColor:
197
+ @classmethod
198
+ def INPUT_TYPES(s):
199
+ return {
200
+ "required": {
201
+ "image": ("IMAGE", ),
202
+ "red": ("INT", { "default": 255, "min": 0, "max": 255, "step": 1, }),
203
+ "green": ("INT", { "default": 255, "min": 0, "max": 255, "step": 1, }),
204
+ "blue": ("INT", { "default": 255, "min": 0, "max": 255, "step": 1, }),
205
+ "threshold": ("INT", { "default": 0, "min": 0, "max": 127, "step": 1, }),
206
+ }
207
+ }
208
+
209
+ RETURN_TYPES = ("MASK",)
210
+ FUNCTION = "execute"
211
+ CATEGORY = "essentials/mask"
212
+
213
+ def execute(self, image, red, green, blue, threshold):
214
+ temp = (torch.clamp(image, 0, 1.0) * 255.0).round().to(torch.int)
215
+ color = torch.tensor([red, green, blue])
216
+ lower_bound = (color - threshold).clamp(min=0)
217
+ upper_bound = (color + threshold).clamp(max=255)
218
+ lower_bound = lower_bound.view(1, 1, 1, 3)
219
+ upper_bound = upper_bound.view(1, 1, 1, 3)
220
+ mask = (temp >= lower_bound) & (temp <= upper_bound)
221
+ mask = mask.all(dim=-1)
222
+ mask = mask.float()
223
+
224
+ return (mask, )
225
+
226
+
227
+ class MaskFromSegmentation:
228
+ @classmethod
229
+ def INPUT_TYPES(s):
230
+ return {
231
+ "required": {
232
+ "image": ("IMAGE", ),
233
+ "segments": ("INT", { "default": 6, "min": 1, "max": 16, "step": 1, }),
234
+ "remove_isolated_pixels": ("INT", { "default": 0, "min": 0, "max": 32, "step": 1, }),
235
+ "remove_small_masks": ("FLOAT", { "default": 0.0, "min": 0., "max": 1., "step": 0.01, }),
236
+ "fill_holes": ("BOOLEAN", { "default": False }),
237
+ }
238
+ }
239
+
240
+ RETURN_TYPES = ("MASK",)
241
+ FUNCTION = "execute"
242
+ CATEGORY = "essentials/mask"
243
+
244
+ def execute(self, image, segments, remove_isolated_pixels, fill_holes, remove_small_masks):
245
+ im = image[0] # we only work on the first image in the batch
246
+ im = Image.fromarray((im * 255).to(torch.uint8).cpu().numpy(), mode="RGB")
247
+ im = im.quantize(palette=im.quantize(colors=segments), dither=Image.Dither.NONE)
248
+ im = torch.tensor(np.array(im.convert("RGB"))).float() / 255.0
249
+
250
+ colors = im.reshape(-1, im.shape[-1])
251
+ colors = torch.unique(colors, dim=0)
252
+
253
+ masks = []
254
+ for color in colors:
255
+ mask = (im == color).all(dim=-1).float()
256
+ # remove isolated pixels
257
+ if remove_isolated_pixels > 0:
258
+ mask = torch.from_numpy(scipy.ndimage.binary_opening(mask.cpu().numpy(), structure=np.ones((remove_isolated_pixels, remove_isolated_pixels))))
259
+
260
+ # fill holes
261
+ if fill_holes:
262
+ mask = torch.from_numpy(scipy.ndimage.binary_fill_holes(mask.cpu().numpy()))
263
+
264
+ # if the mask is too small, it's probably noise
265
+ if mask.sum() / (mask.shape[0]*mask.shape[1]) > remove_small_masks:
266
+ masks.append(mask)
267
+
268
+ if masks == []:
269
+ masks.append(torch.zeros_like(im)[:,:,0]) # return an empty mask if no masks were found, prevents errors
270
+
271
+ mask = torch.stack(masks, dim=0).float()
272
+
273
+ return (mask, )
274
+
275
+
276
+ class MaskFix:
277
+ @classmethod
278
+ def INPUT_TYPES(s):
279
+ return {
280
+ "required": {
281
+ "mask": ("MASK",),
282
+ "erode_dilate": ("INT", { "default": 0, "min": -256, "max": 256, "step": 1, }),
283
+ "fill_holes": ("INT", { "default": 0, "min": 0, "max": 128, "step": 1, }),
284
+ "remove_isolated_pixels": ("INT", { "default": 0, "min": 0, "max": 32, "step": 1, }),
285
+ "smooth": ("INT", { "default": 0, "min": 0, "max": 256, "step": 1, }),
286
+ "blur": ("INT", { "default": 0, "min": 0, "max": 256, "step": 1, }),
287
+ }
288
+ }
289
+
290
+ RETURN_TYPES = ("MASK",)
291
+ FUNCTION = "execute"
292
+ CATEGORY = "essentials/mask"
293
+
294
+ def execute(self, mask, erode_dilate, smooth, remove_isolated_pixels, blur, fill_holes):
295
+ masks = []
296
+ for m in mask:
297
+ # erode and dilate
298
+ if erode_dilate != 0:
299
+ if erode_dilate < 0:
300
+ m = torch.from_numpy(scipy.ndimage.grey_erosion(m.cpu().numpy(), size=(-erode_dilate, -erode_dilate)))
301
+ else:
302
+ m = torch.from_numpy(scipy.ndimage.grey_dilation(m.cpu().numpy(), size=(erode_dilate, erode_dilate)))
303
+
304
+ # fill holes
305
+ if fill_holes > 0:
306
+ #m = torch.from_numpy(scipy.ndimage.binary_fill_holes(m.cpu().numpy(), structure=np.ones((fill_holes,fill_holes)))).float()
307
+ m = torch.from_numpy(scipy.ndimage.grey_closing(m.cpu().numpy(), size=(fill_holes, fill_holes)))
308
+
309
+ # remove isolated pixels
310
+ if remove_isolated_pixels > 0:
311
+ m = torch.from_numpy(scipy.ndimage.grey_opening(m.cpu().numpy(), size=(remove_isolated_pixels, remove_isolated_pixels)))
312
+
313
+ # smooth the mask
314
+ if smooth > 0:
315
+ if smooth % 2 == 0:
316
+ smooth += 1
317
+ m = T.functional.gaussian_blur((m > 0.5).unsqueeze(0), smooth).squeeze(0)
318
+
319
+ # blur the mask
320
+ if blur > 0:
321
+ if blur % 2 == 0:
322
+ blur += 1
323
+ m = T.functional.gaussian_blur(m.float().unsqueeze(0), blur).squeeze(0)
324
+
325
+ masks.append(m.float())
326
+
327
+ masks = torch.stack(masks, dim=0).float()
328
+
329
+ return (masks, )
330
+
331
+ class MaskSmooth:
332
+ @classmethod
333
+ def INPUT_TYPES(s):
334
+ return {
335
+ "required": {
336
+ "mask": ("MASK",),
337
+ "amount": ("INT", { "default": 0, "min": 0, "max": 127, "step": 1, }),
338
+ }
339
+ }
340
+
341
+ RETURN_TYPES = ("MASK",)
342
+ FUNCTION = "execute"
343
+ CATEGORY = "essentials/mask"
344
+
345
+ def execute(self, mask, amount):
346
+ if amount == 0:
347
+ return (mask,)
348
+
349
+ if amount % 2 == 0:
350
+ amount += 1
351
+
352
+ mask = mask > 0.5
353
+ mask = T.functional.gaussian_blur(mask.unsqueeze(1), amount).squeeze(1).float()
354
+
355
+ return (mask,)
356
+
357
+ class MaskFromBatch:
358
+ @classmethod
359
+ def INPUT_TYPES(s):
360
+ return {
361
+ "required": {
362
+ "mask": ("MASK", ),
363
+ "start": ("INT", { "default": 0, "min": 0, "step": 1, }),
364
+ "length": ("INT", { "default": 1, "min": 1, "step": 1, }),
365
+ }
366
+ }
367
+
368
+ RETURN_TYPES = ("MASK",)
369
+ FUNCTION = "execute"
370
+ CATEGORY = "essentials/mask batch"
371
+
372
+ def execute(self, mask, start, length):
373
+ if length > mask.shape[0]:
374
+ length = mask.shape[0]
375
+
376
+ start = min(start, mask.shape[0]-1)
377
+ length = min(mask.shape[0]-start, length)
378
+ return (mask[start:start + length], )
379
+
380
+ class MaskFromList:
381
+ @classmethod
382
+ def INPUT_TYPES(s):
383
+ return {
384
+ "required": {
385
+ "width": ("INT", { "default": 32, "min": 0, "max": MAX_RESOLUTION, "step": 8, }),
386
+ "height": ("INT", { "default": 32, "min": 0, "max": MAX_RESOLUTION, "step": 8, }),
387
+ }, "optional": {
388
+ "values": (any, { "default": 0.0, "min": 0.0, "max": 1.0, }),
389
+ "str_values": ("STRING", { "default": "", "multiline": True, "placeholder": "0.0, 0.5, 1.0",}),
390
+ }
391
+ }
392
+
393
+ RETURN_TYPES = ("MASK",)
394
+ FUNCTION = "execute"
395
+ CATEGORY = "essentials/mask"
396
+
397
+ def execute(self, width, height, values=None, str_values=""):
398
+ out = []
399
+
400
+ if values is not None:
401
+ if not isinstance(values, list):
402
+ out = [values]
403
+ else:
404
+ out.extend([float(v) for v in values])
405
+
406
+ if str_values != "":
407
+ str_values = [float(v) for v in str_values.split(",")]
408
+ out.extend(str_values)
409
+
410
+ if out == []:
411
+ raise ValueError("No values provided")
412
+
413
+ out = torch.tensor(out).float().clamp(0.0, 1.0)
414
+ out = out.view(-1, 1, 1).expand(-1, height, width)
415
+
416
+ values = None
417
+ str_values = ""
418
+
419
+ return (out, )
420
+
421
+ class MaskFromRGBCMYBW:
422
+ @classmethod
423
+ def INPUT_TYPES(s):
424
+ return {
425
+ "required": {
426
+ "image": ("IMAGE", ),
427
+ "threshold_r": ("FLOAT", { "default": 0.15, "min": 0.0, "max": 1, "step": 0.01, }),
428
+ "threshold_g": ("FLOAT", { "default": 0.15, "min": 0.0, "max": 1, "step": 0.01, }),
429
+ "threshold_b": ("FLOAT", { "default": 0.15, "min": 0.0, "max": 1, "step": 0.01, }),
430
+ }
431
+ }
432
+
433
+ RETURN_TYPES = ("MASK","MASK","MASK","MASK","MASK","MASK","MASK","MASK",)
434
+ RETURN_NAMES = ("red","green","blue","cyan","magenta","yellow","black","white",)
435
+ FUNCTION = "execute"
436
+ CATEGORY = "essentials/mask"
437
+
438
+ def execute(self, image, threshold_r, threshold_g, threshold_b):
439
+ red = ((image[..., 0] >= 1-threshold_r) & (image[..., 1] < threshold_g) & (image[..., 2] < threshold_b)).float()
440
+ green = ((image[..., 0] < threshold_r) & (image[..., 1] >= 1-threshold_g) & (image[..., 2] < threshold_b)).float()
441
+ blue = ((image[..., 0] < threshold_r) & (image[..., 1] < threshold_g) & (image[..., 2] >= 1-threshold_b)).float()
442
+
443
+ cyan = ((image[..., 0] < threshold_r) & (image[..., 1] >= 1-threshold_g) & (image[..., 2] >= 1-threshold_b)).float()
444
+ magenta = ((image[..., 0] >= 1-threshold_r) & (image[..., 1] < threshold_g) & (image[..., 2] > 1-threshold_b)).float()
445
+ yellow = ((image[..., 0] >= 1-threshold_r) & (image[..., 1] >= 1-threshold_g) & (image[..., 2] < threshold_b)).float()
446
+
447
+ black = ((image[..., 0] <= threshold_r) & (image[..., 1] <= threshold_g) & (image[..., 2] <= threshold_b)).float()
448
+ white = ((image[..., 0] >= 1-threshold_r) & (image[..., 1] >= 1-threshold_g) & (image[..., 2] >= 1-threshold_b)).float()
449
+
450
+ return (red, green, blue, cyan, magenta, yellow, black, white,)
451
+
452
+ class TransitionMask:
453
+ @classmethod
454
+ def INPUT_TYPES(s):
455
+ return {
456
+ "required": {
457
+ "width": ("INT", { "default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1, }),
458
+ "height": ("INT", { "default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1, }),
459
+ "frames": ("INT", { "default": 16, "min": 1, "max": 9999, "step": 1, }),
460
+ "start_frame": ("INT", { "default": 0, "min": 0, "step": 1, }),
461
+ "end_frame": ("INT", { "default": 9999, "min": 0, "step": 1, }),
462
+ "transition_type": (["horizontal slide", "vertical slide", "horizontal bar", "vertical bar", "center box", "horizontal door", "vertical door", "circle", "fade"],),
463
+ "timing_function": (["linear", "in", "out", "in-out"],)
464
+ }
465
+ }
466
+
467
+ RETURN_TYPES = ("MASK",)
468
+ FUNCTION = "execute"
469
+ CATEGORY = "essentials/mask"
470
+
471
+ def linear(self, i, t):
472
+ return i/t
473
+ def ease_in(self, i, t):
474
+ return pow(i/t, 2)
475
+ def ease_out(self, i, t):
476
+ return 1 - pow(1 - i/t, 2)
477
+ def ease_in_out(self, i, t):
478
+ if i < t/2:
479
+ return pow(i/(t/2), 2) / 2
480
+ else:
481
+ return 1 - pow(1 - (i - t/2)/(t/2), 2) / 2
482
+
483
+ def execute(self, width, height, frames, start_frame, end_frame, transition_type, timing_function):
484
+ if timing_function == 'in':
485
+ timing_function = self.ease_in
486
+ elif timing_function == 'out':
487
+ timing_function = self.ease_out
488
+ elif timing_function == 'in-out':
489
+ timing_function = self.ease_in_out
490
+ else:
491
+ timing_function = self.linear
492
+
493
+ out = []
494
+
495
+ end_frame = min(frames, end_frame)
496
+ transition = end_frame - start_frame
497
+
498
+ if start_frame > 0:
499
+ out = out + [torch.full((height, width), 0.0, dtype=torch.float32, device="cpu")] * start_frame
500
+
501
+ for i in range(transition):
502
+ frame = torch.full((height, width), 0.0, dtype=torch.float32, device="cpu")
503
+ progress = timing_function(i, transition-1)
504
+
505
+ if "horizontal slide" in transition_type:
506
+ pos = round(width*progress)
507
+ frame[:, :pos] = 1.0
508
+ elif "vertical slide" in transition_type:
509
+ pos = round(height*progress)
510
+ frame[:pos, :] = 1.0
511
+ elif "box" in transition_type:
512
+ box_w = round(width*progress)
513
+ box_h = round(height*progress)
514
+ x1 = (width - box_w) // 2
515
+ y1 = (height - box_h) // 2
516
+ x2 = x1 + box_w
517
+ y2 = y1 + box_h
518
+ frame[y1:y2, x1:x2] = 1.0
519
+ elif "circle" in transition_type:
520
+ radius = math.ceil(math.sqrt(pow(width,2)+pow(height,2))*progress/2)
521
+ c_x = width // 2
522
+ c_y = height // 2
523
+ # is this real life? Am I hallucinating?
524
+ x = torch.arange(0, width, dtype=torch.float32, device="cpu")
525
+ y = torch.arange(0, height, dtype=torch.float32, device="cpu")
526
+ y, x = torch.meshgrid((y, x), indexing="ij")
527
+ circle = ((x - c_x) ** 2 + (y - c_y) ** 2) <= (radius ** 2)
528
+ frame[circle] = 1.0
529
+ elif "horizontal bar" in transition_type:
530
+ bar = round(height*progress)
531
+ y1 = (height - bar) // 2
532
+ y2 = y1 + bar
533
+ frame[y1:y2, :] = 1.0
534
+ elif "vertical bar" in transition_type:
535
+ bar = round(width*progress)
536
+ x1 = (width - bar) // 2
537
+ x2 = x1 + bar
538
+ frame[:, x1:x2] = 1.0
539
+ elif "horizontal door" in transition_type:
540
+ bar = math.ceil(height*progress/2)
541
+ if bar > 0:
542
+ frame[:bar, :] = 1.0
543
+ frame[-bar:, :] = 1.0
544
+ elif "vertical door" in transition_type:
545
+ bar = math.ceil(width*progress/2)
546
+ if bar > 0:
547
+ frame[:, :bar] = 1.0
548
+ frame[:, -bar:] = 1.0
549
+ elif "fade" in transition_type:
550
+ frame[:,:] = progress
551
+
552
+ out.append(frame)
553
+
554
+ if end_frame < frames:
555
+ out = out + [torch.full((height, width), 1.0, dtype=torch.float32, device="cpu")] * (frames - end_frame)
556
+
557
+ out = torch.stack(out, dim=0)
558
+
559
+ return (out, )
560
+
561
+ MASK_CLASS_MAPPINGS = {
562
+ "MaskBlur+": MaskBlur,
563
+ "MaskBoundingBox+": MaskBoundingBox,
564
+ "MaskFix+": MaskFix,
565
+ "MaskFlip+": MaskFlip,
566
+ "MaskFromColor+": MaskFromColor,
567
+ "MaskFromList+": MaskFromList,
568
+ "MaskFromRGBCMYBW+": MaskFromRGBCMYBW,
569
+ "MaskFromSegmentation+": MaskFromSegmentation,
570
+ "MaskPreview+": MaskPreview,
571
+ "MaskSmooth+": MaskSmooth,
572
+ "TransitionMask+": TransitionMask,
573
+
574
+ # Batch
575
+ "MaskBatch+": MaskBatch,
576
+ "MaskExpandBatch+": MaskExpandBatch,
577
+ "MaskFromBatch+": MaskFromBatch,
578
+ }
579
+
580
+ MASK_NAME_MAPPINGS = {
581
+ "MaskBlur+": "🔧 Mask Blur",
582
+ "MaskFix+": "🔧 Mask Fix",
583
+ "MaskFlip+": "🔧 Mask Flip",
584
+ "MaskFromColor+": "🔧 Mask From Color",
585
+ "MaskFromList+": "🔧 Mask From List",
586
+ "MaskFromRGBCMYBW+": "🔧 Mask From RGB/CMY/BW",
587
+ "MaskFromSegmentation+": "🔧 Mask From Segmentation",
588
+ "MaskPreview+": "🔧 Mask Preview",
589
+ "MaskBoundingBox+": "🔧 Mask Bounding Box",
590
+ "MaskSmooth+": "🔧 Mask Smooth",
591
+ "TransitionMask+": "🔧 Transition Mask",
592
+
593
+ "MaskBatch+": "🔧 Mask Batch",
594
+ "MaskExpandBatch+": "🔧 Mask Expand Batch",
595
+ "MaskFromBatch+": "🔧 Mask From Batch",
596
+ }
ComfyUI_essentials/misc.py ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ from .utils import AnyType
4
+ import comfy.model_management
5
+ from nodes import MAX_RESOLUTION
6
+ import time
7
+
8
+ any = AnyType("*")
9
+
10
+ class SimpleMathFloat:
11
+ @classmethod
12
+ def INPUT_TYPES(s):
13
+ return {
14
+ "required": {
15
+ "value": ("FLOAT", { "default": 0.0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 0.05 }),
16
+ },
17
+ }
18
+
19
+ RETURN_TYPES = ("FLOAT", )
20
+ FUNCTION = "execute"
21
+ CATEGORY = "essentials/utilities"
22
+
23
+ def execute(self, value):
24
+ return (float(value), )
25
+
26
+ class SimpleMathPercent:
27
+ @classmethod
28
+ def INPUT_TYPES(s):
29
+ return {
30
+ "required": {
31
+ "value": ("FLOAT", { "default": 0.0, "min": 0, "max": 1, "step": 0.05 }),
32
+ },
33
+ }
34
+
35
+ RETURN_TYPES = ("FLOAT", )
36
+ FUNCTION = "execute"
37
+ CATEGORY = "essentials/utilities"
38
+
39
+ def execute(self, value):
40
+ return (float(value), )
41
+
42
+ class SimpleMathInt:
43
+ @classmethod
44
+ def INPUT_TYPES(s):
45
+ return {
46
+ "required": {
47
+ "value": ("INT", { "default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1 }),
48
+ },
49
+ }
50
+
51
+ RETURN_TYPES = ("INT",)
52
+ FUNCTION = "execute"
53
+ CATEGORY = "essentials/utilities"
54
+
55
+ def execute(self, value):
56
+ return (int(value), )
57
+
58
+ class SimpleMathSlider:
59
+ @classmethod
60
+ def INPUT_TYPES(s):
61
+ return {
62
+ "required": {
63
+ "value": ("FLOAT", { "display": "slider", "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.001 }),
64
+ "min": ("FLOAT", { "default": 0.0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 0.001 }),
65
+ "max": ("FLOAT", { "default": 1.0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 0.001 }),
66
+ "rounding": ("INT", { "default": 0, "min": 0, "max": 10, "step": 1 }),
67
+ },
68
+ }
69
+
70
+ RETURN_TYPES = ("FLOAT", "INT",)
71
+ FUNCTION = "execute"
72
+ CATEGORY = "essentials/utilities"
73
+
74
+ def execute(self, value, min, max, rounding):
75
+ value = min + value * (max - min)
76
+
77
+ if rounding > 0:
78
+ value = round(value, rounding)
79
+
80
+ return (value, int(value), )
81
+
82
+ class SimpleMathSliderLowRes:
83
+ @classmethod
84
+ def INPUT_TYPES(s):
85
+ return {
86
+ "required": {
87
+ "value": ("INT", { "display": "slider", "default": 5, "min": 0, "max": 10, "step": 1 }),
88
+ "min": ("FLOAT", { "default": 0.0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 0.001 }),
89
+ "max": ("FLOAT", { "default": 1.0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 0.001 }),
90
+ "rounding": ("INT", { "default": 0, "min": 0, "max": 10, "step": 1 }),
91
+ },
92
+ }
93
+
94
+ RETURN_TYPES = ("FLOAT", "INT",)
95
+ FUNCTION = "execute"
96
+ CATEGORY = "essentials/utilities"
97
+
98
+ def execute(self, value, min, max, rounding):
99
+ value = 0.1 * value
100
+ value = min + value * (max - min)
101
+ if rounding > 0:
102
+ value = round(value, rounding)
103
+
104
+ return (value, )
105
+
106
+ class SimpleMathBoolean:
107
+ @classmethod
108
+ def INPUT_TYPES(s):
109
+ return {
110
+ "required": {
111
+ "value": ("BOOLEAN", { "default": False }),
112
+ },
113
+ }
114
+
115
+ RETURN_TYPES = ("BOOLEAN",)
116
+ FUNCTION = "execute"
117
+ CATEGORY = "essentials/utilities"
118
+
119
+ def execute(self, value):
120
+ return (value, int(value), )
121
+
122
+ class SimpleMath:
123
+ @classmethod
124
+ def INPUT_TYPES(s):
125
+ return {
126
+ "optional": {
127
+ "a": (any, { "default": 0.0 }),
128
+ "b": (any, { "default": 0.0 }),
129
+ "c": (any, { "default": 0.0 }),
130
+ },
131
+ "required": {
132
+ "value": ("STRING", { "multiline": False, "default": "" }),
133
+ },
134
+ }
135
+
136
+ RETURN_TYPES = ("INT", "FLOAT", )
137
+ FUNCTION = "execute"
138
+ CATEGORY = "essentials/utilities"
139
+
140
+ def execute(self, value, a = 0.0, b = 0.0, c = 0.0, d = 0.0):
141
+ import ast
142
+ import operator as op
143
+
144
+ h, w = 0.0, 0.0
145
+ if hasattr(a, 'shape'):
146
+ a = list(a.shape)
147
+ if hasattr(b, 'shape'):
148
+ b = list(b.shape)
149
+ if hasattr(c, 'shape'):
150
+ c = list(c.shape)
151
+ if hasattr(d, 'shape'):
152
+ d = list(d.shape)
153
+
154
+ if isinstance(a, str):
155
+ a = float(a)
156
+ if isinstance(b, str):
157
+ b = float(b)
158
+ if isinstance(c, str):
159
+ c = float(c)
160
+ if isinstance(d, str):
161
+ d = float(d)
162
+
163
+ operators = {
164
+ ast.Add: op.add,
165
+ ast.Sub: op.sub,
166
+ ast.Mult: op.mul,
167
+ ast.Div: op.truediv,
168
+ ast.FloorDiv: op.floordiv,
169
+ ast.Pow: op.pow,
170
+ #ast.BitXor: op.xor,
171
+ #ast.BitOr: op.or_,
172
+ #ast.BitAnd: op.and_,
173
+ ast.USub: op.neg,
174
+ ast.Mod: op.mod,
175
+ ast.Eq: op.eq,
176
+ ast.NotEq: op.ne,
177
+ ast.Lt: op.lt,
178
+ ast.LtE: op.le,
179
+ ast.Gt: op.gt,
180
+ ast.GtE: op.ge,
181
+ ast.And: lambda x, y: x and y,
182
+ ast.Or: lambda x, y: x or y,
183
+ ast.Not: op.not_
184
+ }
185
+
186
+ op_functions = {
187
+ 'min': min,
188
+ 'max': max,
189
+ 'round': round,
190
+ 'sum': sum,
191
+ 'len': len,
192
+ }
193
+
194
+ def eval_(node):
195
+ if isinstance(node, ast.Num): # number
196
+ return node.n
197
+ elif isinstance(node, ast.Name): # variable
198
+ if node.id == "a":
199
+ return a
200
+ if node.id == "b":
201
+ return b
202
+ if node.id == "c":
203
+ return c
204
+ if node.id == "d":
205
+ return d
206
+ elif isinstance(node, ast.BinOp): # <left> <operator> <right>
207
+ return operators[type(node.op)](eval_(node.left), eval_(node.right))
208
+ elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
209
+ return operators[type(node.op)](eval_(node.operand))
210
+ elif isinstance(node, ast.Compare): # comparison operators
211
+ left = eval_(node.left)
212
+ for op, comparator in zip(node.ops, node.comparators):
213
+ if not operators[type(op)](left, eval_(comparator)):
214
+ return 0
215
+ return 1
216
+ elif isinstance(node, ast.BoolOp): # boolean operators (And, Or)
217
+ values = [eval_(value) for value in node.values]
218
+ return operators[type(node.op)](*values)
219
+ elif isinstance(node, ast.Call): # custom function
220
+ if node.func.id in op_functions:
221
+ args =[eval_(arg) for arg in node.args]
222
+ return op_functions[node.func.id](*args)
223
+ elif isinstance(node, ast.Subscript): # indexing or slicing
224
+ value = eval_(node.value)
225
+ if isinstance(node.slice, ast.Constant):
226
+ return value[node.slice.value]
227
+ else:
228
+ return 0
229
+ else:
230
+ return 0
231
+
232
+ result = eval_(ast.parse(value, mode='eval').body)
233
+
234
+ if math.isnan(result):
235
+ result = 0.0
236
+
237
+ return (round(result), result, )
238
+
239
+ class SimpleMathDual:
240
+ @classmethod
241
+ def INPUT_TYPES(s):
242
+ return {
243
+ "optional": {
244
+ "a": (any, { "default": 0.0 }),
245
+ "b": (any, { "default": 0.0 }),
246
+ "c": (any, { "default": 0.0 }),
247
+ "d": (any, { "default": 0.0 }),
248
+ },
249
+ "required": {
250
+ "value_1": ("STRING", { "multiline": False, "default": "" }),
251
+ "value_2": ("STRING", { "multiline": False, "default": "" }),
252
+ },
253
+ }
254
+
255
+ RETURN_TYPES = ("INT", "FLOAT", "INT", "FLOAT", )
256
+ RETURN_NAMES = ("int_1", "float_1", "int_2", "float_2" )
257
+ FUNCTION = "execute"
258
+ CATEGORY = "essentials/utilities"
259
+
260
+ def execute(self, value_1, value_2, a = 0.0, b = 0.0, c = 0.0, d = 0.0):
261
+ return SimpleMath().execute(value_1, a, b, c, d) + SimpleMath().execute(value_2, a, b, c, d)
262
+
263
+ class SimpleMathCondition:
264
+ @classmethod
265
+ def INPUT_TYPES(s):
266
+ return {
267
+ "optional": {
268
+ "a": (any, { "default": 0.0 }),
269
+ "b": (any, { "default": 0.0 }),
270
+ "c": (any, { "default": 0.0 }),
271
+ },
272
+ "required": {
273
+ "evaluate": (any, {"default": 0}),
274
+ "on_true": ("STRING", { "multiline": False, "default": "" }),
275
+ "on_false": ("STRING", { "multiline": False, "default": "" }),
276
+ },
277
+ }
278
+
279
+ RETURN_TYPES = ("INT", "FLOAT", )
280
+ FUNCTION = "execute"
281
+ CATEGORY = "essentials/utilities"
282
+
283
+ def execute(self, evaluate, on_true, on_false, a = 0.0, b = 0.0, c = 0.0):
284
+ return SimpleMath().execute(on_true if evaluate else on_false, a, b, c)
285
+
286
+ class SimpleCondition:
287
+ def __init__(self):
288
+ pass
289
+
290
+ @classmethod
291
+ def INPUT_TYPES(cls):
292
+ return {
293
+ "required": {
294
+ "evaluate": (any, {"default": 0}),
295
+ "on_true": (any, {"default": 0}),
296
+ },
297
+ "optional": {
298
+ "on_false": (any, {"default": None}),
299
+ },
300
+ }
301
+
302
+ RETURN_TYPES = (any,)
303
+ RETURN_NAMES = ("result",)
304
+ FUNCTION = "execute"
305
+
306
+ CATEGORY = "essentials/utilities"
307
+
308
+ def execute(self, evaluate, on_true, on_false=None):
309
+ from comfy_execution.graph import ExecutionBlocker
310
+ if not evaluate:
311
+ return (on_false if on_false is not None else ExecutionBlocker(None),)
312
+
313
+ return (on_true,)
314
+
315
+ class SimpleComparison:
316
+ def __init__(self):
317
+ pass
318
+
319
+ @classmethod
320
+ def INPUT_TYPES(cls):
321
+ return {
322
+ "required": {
323
+ "a": (any, {"default": 0}),
324
+ "b": (any, {"default": 0}),
325
+ "comparison": (["==", "!=", "<", "<=", ">", ">="],),
326
+ },
327
+ }
328
+
329
+ RETURN_TYPES = ("BOOLEAN",)
330
+ FUNCTION = "execute"
331
+
332
+ CATEGORY = "essentials/utilities"
333
+
334
+ def execute(self, a, b, comparison):
335
+ if comparison == "==":
336
+ return (a == b,)
337
+ elif comparison == "!=":
338
+ return (a != b,)
339
+ elif comparison == "<":
340
+ return (a < b,)
341
+ elif comparison == "<=":
342
+ return (a <= b,)
343
+ elif comparison == ">":
344
+ return (a > b,)
345
+ elif comparison == ">=":
346
+ return (a >= b,)
347
+
348
+ class ConsoleDebug:
349
+ @classmethod
350
+ def INPUT_TYPES(s):
351
+ return {
352
+ "required": {
353
+ "value": (any, {}),
354
+ },
355
+ "optional": {
356
+ "prefix": ("STRING", { "multiline": False, "default": "Value:" })
357
+ }
358
+ }
359
+
360
+ RETURN_TYPES = ()
361
+ FUNCTION = "execute"
362
+ CATEGORY = "essentials/utilities"
363
+ OUTPUT_NODE = True
364
+
365
+ def execute(self, value, prefix):
366
+ print(f"\033[96m{prefix} {value}\033[0m")
367
+
368
+ return (None,)
369
+
370
+ class DebugTensorShape:
371
+ @classmethod
372
+ def INPUT_TYPES(s):
373
+ return {
374
+ "required": {
375
+ "tensor": (any, {}),
376
+ },
377
+ }
378
+
379
+ RETURN_TYPES = ()
380
+ FUNCTION = "execute"
381
+ CATEGORY = "essentials/utilities"
382
+ OUTPUT_NODE = True
383
+
384
+ def execute(self, tensor):
385
+ shapes = []
386
+ def tensorShape(tensor):
387
+ if isinstance(tensor, dict):
388
+ for k in tensor:
389
+ tensorShape(tensor[k])
390
+ elif isinstance(tensor, list):
391
+ for i in range(len(tensor)):
392
+ tensorShape(tensor[i])
393
+ elif hasattr(tensor, 'shape'):
394
+ shapes.append(list(tensor.shape))
395
+
396
+ tensorShape(tensor)
397
+
398
+ print(f"\033[96mShapes found: {shapes}\033[0m")
399
+
400
+ return (None,)
401
+
402
+ class BatchCount:
403
+ @classmethod
404
+ def INPUT_TYPES(s):
405
+ return {
406
+ "required": {
407
+ "batch": (any, {}),
408
+ },
409
+ }
410
+
411
+ RETURN_TYPES = ("INT",)
412
+ FUNCTION = "execute"
413
+ CATEGORY = "essentials/utilities"
414
+
415
+ def execute(self, batch):
416
+ count = 0
417
+ if hasattr(batch, 'shape'):
418
+ count = batch.shape[0]
419
+ elif isinstance(batch, dict) and 'samples' in batch:
420
+ count = batch['samples'].shape[0]
421
+ elif isinstance(batch, list) or isinstance(batch, dict):
422
+ count = len(batch)
423
+
424
+ return (count, )
425
+
426
+ class ModelCompile():
427
+ @classmethod
428
+ def INPUT_TYPES(s):
429
+ return {
430
+ "required": {
431
+ "model": ("MODEL",),
432
+ "fullgraph": ("BOOLEAN", { "default": False }),
433
+ "dynamic": ("BOOLEAN", { "default": False }),
434
+ "mode": (["default", "reduce-overhead", "max-autotune", "max-autotune-no-cudagraphs"],),
435
+ },
436
+ }
437
+
438
+ RETURN_TYPES = ("MODEL", )
439
+ FUNCTION = "execute"
440
+ CATEGORY = "essentials/utilities"
441
+
442
+ def execute(self, model, fullgraph, dynamic, mode):
443
+ work_model = model.clone()
444
+ torch._dynamo.config.suppress_errors = True
445
+ work_model.add_object_patch("diffusion_model", torch.compile(model=work_model.get_model_object("diffusion_model"), dynamic=dynamic, fullgraph=fullgraph, mode=mode))
446
+ return (work_model, )
447
+
448
+ class RemoveLatentMask:
449
+ @classmethod
450
+ def INPUT_TYPES(s):
451
+ return {"required": { "samples": ("LATENT",),}}
452
+ RETURN_TYPES = ("LATENT",)
453
+ FUNCTION = "execute"
454
+
455
+ CATEGORY = "essentials/utilities"
456
+
457
+ def execute(self, samples):
458
+ s = samples.copy()
459
+ if "noise_mask" in s:
460
+ del s["noise_mask"]
461
+
462
+ return (s,)
463
+
464
+ class SDXLEmptyLatentSizePicker:
465
+ def __init__(self):
466
+ self.device = comfy.model_management.intermediate_device()
467
+
468
+ @classmethod
469
+ def INPUT_TYPES(s):
470
+ return {"required": {
471
+ "resolution": (["704x1408 (0.5)","704x1344 (0.52)","768x1344 (0.57)","768x1280 (0.6)","832x1216 (0.68)","832x1152 (0.72)","896x1152 (0.78)","896x1088 (0.82)","960x1088 (0.88)","960x1024 (0.94)","1024x1024 (1.0)","1024x960 (1.07)","1088x960 (1.13)","1088x896 (1.21)","1152x896 (1.29)","1152x832 (1.38)","1216x832 (1.46)","1280x768 (1.67)","1344x768 (1.75)","1344x704 (1.91)","1408x704 (2.0)","1472x704 (2.09)","1536x640 (2.4)","1600x640 (2.5)","1664x576 (2.89)","1728x576 (3.0)",], {"default": "1024x1024 (1.0)"}),
472
+ "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
473
+ "width_override": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
474
+ "height_override": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),
475
+ }}
476
+
477
+ RETURN_TYPES = ("LATENT","INT","INT",)
478
+ RETURN_NAMES = ("LATENT","width","height",)
479
+ FUNCTION = "execute"
480
+ CATEGORY = "essentials/utilities"
481
+
482
+ def execute(self, resolution, batch_size, width_override=0, height_override=0):
483
+ width, height = resolution.split(" ")[0].split("x")
484
+ width = width_override if width_override > 0 else int(width)
485
+ height = height_override if height_override > 0 else int(height)
486
+
487
+ latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device)
488
+
489
+ return ({"samples":latent}, width, height,)
490
+
491
+ class DisplayAny:
492
+ def __init__(self):
493
+ pass
494
+
495
+ @classmethod
496
+ def INPUT_TYPES(s):
497
+ return {
498
+ "required": {
499
+ "input": (("*",{})),
500
+ "mode": (["raw value", "tensor shape"],),
501
+ },
502
+ }
503
+
504
+ @classmethod
505
+ def VALIDATE_INPUTS(s, input_types):
506
+ return True
507
+
508
+ RETURN_TYPES = ("STRING",)
509
+ FUNCTION = "execute"
510
+ OUTPUT_NODE = True
511
+
512
+ CATEGORY = "essentials/utilities"
513
+
514
+ def execute(self, input, mode):
515
+ if mode == "tensor shape":
516
+ text = []
517
+ def tensorShape(tensor):
518
+ if isinstance(tensor, dict):
519
+ for k in tensor:
520
+ tensorShape(tensor[k])
521
+ elif isinstance(tensor, list):
522
+ for i in range(len(tensor)):
523
+ tensorShape(tensor[i])
524
+ elif hasattr(tensor, 'shape'):
525
+ text.append(list(tensor.shape))
526
+
527
+ tensorShape(input)
528
+ input = text
529
+
530
+ text = str(input)
531
+
532
+ return {"ui": {"text": text}, "result": (text,)}
533
+
534
+ MISC_CLASS_MAPPINGS = {
535
+ "BatchCount+": BatchCount,
536
+ "ConsoleDebug+": ConsoleDebug,
537
+ "DebugTensorShape+": DebugTensorShape,
538
+ "DisplayAny": DisplayAny,
539
+ "ModelCompile+": ModelCompile,
540
+ "RemoveLatentMask+": RemoveLatentMask,
541
+ "SDXLEmptyLatentSizePicker+": SDXLEmptyLatentSizePicker,
542
+ "SimpleComparison+": SimpleComparison,
543
+ "SimpleCondition+": SimpleCondition,
544
+ "SimpleMath+": SimpleMath,
545
+ "SimpleMathDual+": SimpleMathDual,
546
+ "SimpleMathCondition+": SimpleMathCondition,
547
+ "SimpleMathBoolean+": SimpleMathBoolean,
548
+ "SimpleMathFloat+": SimpleMathFloat,
549
+ "SimpleMathInt+": SimpleMathInt,
550
+ "SimpleMathPercent+": SimpleMathPercent,
551
+ "SimpleMathSlider+": SimpleMathSlider,
552
+ "SimpleMathSliderLowRes+": SimpleMathSliderLowRes,
553
+ }
554
+
555
+ MISC_NAME_MAPPINGS = {
556
+ "BatchCount+": "🔧 Batch Count",
557
+ "ConsoleDebug+": "🔧 Console Debug",
558
+ "DebugTensorShape+": "🔧 Debug Tensor Shape",
559
+ "DisplayAny": "🔧 Display Any",
560
+ "ModelCompile+": "🔧 Model Compile",
561
+ "RemoveLatentMask+": "🔧 Remove Latent Mask",
562
+ "SDXLEmptyLatentSizePicker+": "🔧 Empty Latent Size Picker",
563
+ "SimpleComparison+": "🔧 Simple Comparison",
564
+ "SimpleCondition+": "🔧 Simple Condition",
565
+ "SimpleMath+": "🔧 Simple Math",
566
+ "SimpleMathDual+": "🔧 Simple Math Dual",
567
+ "SimpleMathCondition+": "🔧 Simple Math Condition",
568
+ "SimpleMathBoolean+": "🔧 Simple Math Boolean",
569
+ "SimpleMathFloat+": "🔧 Simple Math Float",
570
+ "SimpleMathInt+": "🔧 Simple Math Int",
571
+ "SimpleMathPercent+": "🔧 Simple Math Percent",
572
+ "SimpleMathSlider+": "🔧 Simple Math Slider",
573
+ "SimpleMathSliderLowRes+": "🔧 Simple Math Slider low-res",
574
+ }
ComfyUI_essentials/pyproject.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "comfyui_essentials"
3
+ description = "Essential nodes that are weirdly missing from ComfyUI core. With few exceptions they are new features and not commodities."
4
+ version = "1.1.0"
5
+ license = { file = "LICENSE" }
6
+ dependencies = ["numba", "colour-science", "rembg", "pixeloe"]
7
+
8
+ [project.urls]
9
+ Repository = "https://github.com/cubiq/ComfyUI_essentials"
10
+ # Used by Comfy Registry https://comfyregistry.org
11
+
12
+ [tool.comfy]
13
+ PublisherId = "matteo"
14
+ DisplayName = "ComfyUI_essentials"
15
+ Icon = ""
ComfyUI_essentials/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ numba
2
+ colour-science
3
+ rembg
4
+ pixeloe
5
+ transparent-background
ComfyUI_essentials/sampling.py ADDED
@@ -0,0 +1,811 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import comfy.samplers
3
+ import comfy.sample
4
+ import torch
5
+ from nodes import common_ksampler, CLIPTextEncode
6
+ from comfy.utils import ProgressBar
7
+ from .utils import expand_mask, FONTS_DIR, parse_string_to_list
8
+ import torchvision.transforms.v2 as T
9
+ import torch.nn.functional as F
10
+ import logging
11
+ import folder_paths
12
+
13
+ # From https://github.com/BlenderNeko/ComfyUI_Noise/
14
+ def slerp(val, low, high):
15
+ dims = low.shape
16
+
17
+ low = low.reshape(dims[0], -1)
18
+ high = high.reshape(dims[0], -1)
19
+
20
+ low_norm = low/torch.norm(low, dim=1, keepdim=True)
21
+ high_norm = high/torch.norm(high, dim=1, keepdim=True)
22
+
23
+ low_norm[low_norm != low_norm] = 0.0
24
+ high_norm[high_norm != high_norm] = 0.0
25
+
26
+ omega = torch.acos((low_norm*high_norm).sum(1))
27
+ so = torch.sin(omega)
28
+ res = (torch.sin((1.0-val)*omega)/so).unsqueeze(1)*low + (torch.sin(val*omega)/so).unsqueeze(1) * high
29
+
30
+ return res.reshape(dims)
31
+
32
+ class KSamplerVariationsWithNoise:
33
+ @classmethod
34
+ def INPUT_TYPES(s):
35
+ return {"required": {
36
+ "model": ("MODEL", ),
37
+ "latent_image": ("LATENT", ),
38
+ "main_seed": ("INT:seed", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
39
+ "steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
40
+ "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}),
41
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
42
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
43
+ "positive": ("CONDITIONING", ),
44
+ "negative": ("CONDITIONING", ),
45
+ "variation_strength": ("FLOAT", {"default": 0.17, "min": 0.0, "max": 1.0, "step":0.01, "round": 0.01}),
46
+ #"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
47
+ #"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
48
+ #"return_with_leftover_noise": (["disable", "enable"], ),
49
+ "variation_seed": ("INT:seed", {"default": 12345, "min": 0, "max": 0xffffffffffffffff}),
50
+ "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step":0.01, "round": 0.01}),
51
+ }}
52
+
53
+ RETURN_TYPES = ("LATENT",)
54
+ FUNCTION = "execute"
55
+ CATEGORY = "essentials/sampling"
56
+
57
+ def prepare_mask(self, mask, shape):
58
+ mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(shape[2], shape[3]), mode="bilinear")
59
+ mask = mask.expand((-1,shape[1],-1,-1))
60
+ if mask.shape[0] < shape[0]:
61
+ mask = mask.repeat((shape[0] -1) // mask.shape[0] + 1, 1, 1, 1)[:shape[0]]
62
+ return mask
63
+
64
+ def execute(self, model, latent_image, main_seed, steps, cfg, sampler_name, scheduler, positive, negative, variation_strength, variation_seed, denoise):
65
+ if main_seed == variation_seed:
66
+ variation_seed += 1
67
+
68
+ end_at_step = steps #min(steps, end_at_step)
69
+ start_at_step = round(end_at_step - end_at_step * denoise)
70
+
71
+ force_full_denoise = True
72
+ disable_noise = True
73
+
74
+ device = comfy.model_management.get_torch_device()
75
+
76
+ # Generate base noise
77
+ batch_size, _, height, width = latent_image["samples"].shape
78
+ generator = torch.manual_seed(main_seed)
79
+ base_noise = torch.randn((1, 4, height, width), dtype=torch.float32, device="cpu", generator=generator).repeat(batch_size, 1, 1, 1).cpu()
80
+
81
+ # Generate variation noise
82
+ generator = torch.manual_seed(variation_seed)
83
+ variation_noise = torch.randn((batch_size, 4, height, width), dtype=torch.float32, device="cpu", generator=generator).cpu()
84
+
85
+ slerp_noise = slerp(variation_strength, base_noise, variation_noise)
86
+
87
+ # Calculate sigma
88
+ comfy.model_management.load_model_gpu(model)
89
+ sampler = comfy.samplers.KSampler(model, steps=steps, device=device, sampler=sampler_name, scheduler=scheduler, denoise=1.0, model_options=model.model_options)
90
+ sigmas = sampler.sigmas
91
+ sigma = sigmas[start_at_step] - sigmas[end_at_step]
92
+ sigma /= model.model.latent_format.scale_factor
93
+ sigma = sigma.detach().cpu().item()
94
+
95
+ work_latent = latent_image.copy()
96
+ work_latent["samples"] = latent_image["samples"].clone() + slerp_noise * sigma
97
+
98
+ # if there's a mask we need to expand it to avoid artifacts, 5 pixels should be enough
99
+ if "noise_mask" in latent_image:
100
+ noise_mask = self.prepare_mask(latent_image["noise_mask"], latent_image['samples'].shape)
101
+ work_latent["samples"] = noise_mask * work_latent["samples"] + (1-noise_mask) * latent_image["samples"]
102
+ work_latent['noise_mask'] = expand_mask(latent_image["noise_mask"].clone(), 5, True)
103
+
104
+ return common_ksampler(model, main_seed, steps, cfg, sampler_name, scheduler, positive, negative, work_latent, denoise=1.0, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)
105
+
106
+
107
+ class KSamplerVariationsStochastic:
108
+ @classmethod
109
+ def INPUT_TYPES(s):
110
+ return {"required":{
111
+ "model": ("MODEL",),
112
+ "latent_image": ("LATENT", ),
113
+ "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
114
+ "steps": ("INT", {"default": 25, "min": 1, "max": 10000}),
115
+ "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}),
116
+ "sampler": (comfy.samplers.KSampler.SAMPLERS, ),
117
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
118
+ "positive": ("CONDITIONING", ),
119
+ "negative": ("CONDITIONING", ),
120
+ "variation_seed": ("INT:seed", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
121
+ "variation_strength": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step":0.05, "round": 0.01}),
122
+ #"variation_sampler": (comfy.samplers.KSampler.SAMPLERS, ),
123
+ "cfg_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step":0.05, "round": 0.01}),
124
+ }}
125
+
126
+ RETURN_TYPES = ("LATENT", )
127
+ FUNCTION = "execute"
128
+ CATEGORY = "essentials/sampling"
129
+
130
+ def execute(self, model, latent_image, noise_seed, steps, cfg, sampler, scheduler, positive, negative, variation_seed, variation_strength, cfg_scale, variation_sampler="dpmpp_2m_sde"):
131
+ # Stage 1: composition sampler
132
+ force_full_denoise = False # return with leftover noise = "enable"
133
+ disable_noise = False # add noise = "enable"
134
+
135
+ end_at_step = max(int(steps * (1-variation_strength)), 1)
136
+ start_at_step = 0
137
+
138
+ work_latent = latent_image.copy()
139
+ batch_size = work_latent["samples"].shape[0]
140
+ work_latent["samples"] = work_latent["samples"][0].unsqueeze(0)
141
+
142
+ stage1 = common_ksampler(model, noise_seed, steps, cfg, sampler, scheduler, positive, negative, work_latent, denoise=1.0, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)[0]
143
+
144
+ if batch_size > 1:
145
+ stage1["samples"] = stage1["samples"].clone().repeat(batch_size, 1, 1, 1)
146
+
147
+ # Stage 2: variation sampler
148
+ force_full_denoise = True
149
+ disable_noise = True
150
+ cfg = max(cfg * cfg_scale, 1.0)
151
+ start_at_step = end_at_step
152
+ end_at_step = steps
153
+
154
+ return common_ksampler(model, variation_seed, steps, cfg, variation_sampler, scheduler, positive, negative, stage1, denoise=1.0, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)
155
+
156
+ class InjectLatentNoise:
157
+ @classmethod
158
+ def INPUT_TYPES(s):
159
+ return {"required": {
160
+ "latent": ("LATENT", ),
161
+ "noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
162
+ "noise_strength": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step":0.01, "round": 0.01}),
163
+ "normalize": (["false", "true"], {"default": "false"}),
164
+ },
165
+ "optional": {
166
+ "mask": ("MASK", ),
167
+ }}
168
+
169
+ RETURN_TYPES = ("LATENT",)
170
+ FUNCTION = "execute"
171
+ CATEGORY = "essentials/sampling"
172
+
173
+ def execute(self, latent, noise_seed, noise_strength, normalize="false", mask=None):
174
+ torch.manual_seed(noise_seed)
175
+ noise_latent = latent.copy()
176
+ original_samples = noise_latent["samples"].clone()
177
+ random_noise = torch.randn_like(original_samples)
178
+
179
+ if normalize == "true":
180
+ mean = original_samples.mean()
181
+ std = original_samples.std()
182
+ random_noise = random_noise * std + mean
183
+
184
+ random_noise = original_samples + random_noise * noise_strength
185
+
186
+ if mask is not None:
187
+ mask = F.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(random_noise.shape[2], random_noise.shape[3]), mode="bilinear")
188
+ mask = mask.expand((-1,random_noise.shape[1],-1,-1)).clamp(0.0, 1.0)
189
+ if mask.shape[0] < random_noise.shape[0]:
190
+ mask = mask.repeat((random_noise.shape[0] -1) // mask.shape[0] + 1, 1, 1, 1)[:random_noise.shape[0]]
191
+ elif mask.shape[0] > random_noise.shape[0]:
192
+ mask = mask[:random_noise.shape[0]]
193
+ random_noise = mask * random_noise + (1-mask) * original_samples
194
+
195
+ noise_latent["samples"] = random_noise
196
+
197
+ return (noise_latent, )
198
+
199
+ class TextEncodeForSamplerParams:
200
+ @classmethod
201
+ def INPUT_TYPES(s):
202
+ return {
203
+ "required": {
204
+ "text": ("STRING", {"multiline": True, "dynamicPrompts": True, "default": "Separate prompts with at least three dashes\n---\nLike so"}),
205
+ "clip": ("CLIP", )
206
+ }}
207
+
208
+ RETURN_TYPES = ("CONDITIONING", )
209
+ FUNCTION = "execute"
210
+ CATEGORY = "essentials/sampling"
211
+
212
+ def execute(self, text, clip):
213
+ import re
214
+ output_text = []
215
+ output_encoded = []
216
+ text = re.sub(r'[-*=~]{4,}\n', '---\n', text)
217
+ text = text.split("---\n")
218
+
219
+ for t in text:
220
+ t = t.strip()
221
+ if t:
222
+ output_text.append(t)
223
+ output_encoded.append(CLIPTextEncode().encode(clip, t)[0])
224
+
225
+ #if len(output_encoded) == 1:
226
+ # output = output_encoded[0]
227
+ #else:
228
+ output = {"text": output_text, "encoded": output_encoded}
229
+
230
+ return (output, )
231
+
232
+ class SamplerSelectHelper:
233
+ @classmethod
234
+ def INPUT_TYPES(s):
235
+ return {"required": {
236
+ **{s: ("BOOLEAN", { "default": False }) for s in comfy.samplers.KSampler.SAMPLERS},
237
+ }}
238
+
239
+ RETURN_TYPES = ("STRING", )
240
+ FUNCTION = "execute"
241
+ CATEGORY = "essentials/sampling"
242
+
243
+ def execute(self, **values):
244
+ values = [v for v in values if values[v]]
245
+ values = ", ".join(values)
246
+
247
+ return (values, )
248
+
249
+ class SchedulerSelectHelper:
250
+ @classmethod
251
+ def INPUT_TYPES(s):
252
+ return {"required": {
253
+ **{s: ("BOOLEAN", { "default": False }) for s in comfy.samplers.KSampler.SCHEDULERS},
254
+ }}
255
+
256
+ RETURN_TYPES = ("STRING", )
257
+ FUNCTION = "execute"
258
+ CATEGORY = "essentials/sampling"
259
+
260
+ def execute(self, **values):
261
+ values = [v for v in values if values[v]]
262
+ values = ", ".join(values)
263
+
264
+ return (values, )
265
+
266
+ class LorasForFluxParams:
267
+ @classmethod
268
+ def INPUT_TYPES(s):
269
+ optional_loras = ['none'] + folder_paths.get_filename_list("loras")
270
+ return {
271
+ "required": {
272
+ "lora_1": (folder_paths.get_filename_list("loras"), {"tooltip": "The name of the LoRA."}),
273
+ "strength_model_1": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "1.0" }),
274
+ },
275
+ #"optional": {
276
+ # "lora_2": (optional_loras, ),
277
+ # "strength_lora_2": ("STRING", { "multiline": False, "dynamicPrompts": False }),
278
+ # "lora_3": (optional_loras, ),
279
+ # "strength_lora_3": ("STRING", { "multiline": False, "dynamicPrompts": False }),
280
+ # "lora_4": (optional_loras, ),
281
+ # "strength_lora_4": ("STRING", { "multiline": False, "dynamicPrompts": False }),
282
+ #}
283
+ }
284
+
285
+ RETURN_TYPES = ("LORA_PARAMS", )
286
+ FUNCTION = "execute"
287
+ CATEGORY = "essentials/sampling"
288
+
289
+ def execute(self, lora_1, strength_model_1, lora_2="none", strength_lora_2="", lora_3="none", strength_lora_3="", lora_4="none", strength_lora_4=""):
290
+ output = { "loras": [], "strengths": [] }
291
+ output["loras"].append(lora_1)
292
+ output["strengths"].append(parse_string_to_list(strength_model_1))
293
+
294
+ if lora_2 != "none":
295
+ output["loras"].append(lora_2)
296
+ if strength_lora_2 == "":
297
+ strength_lora_2 = "1.0"
298
+ output["strengths"].append(parse_string_to_list(strength_lora_2))
299
+ if lora_3 != "none":
300
+ output["loras"].append(lora_3)
301
+ if strength_lora_3 == "":
302
+ strength_lora_3 = "1.0"
303
+ output["strengths"].append(parse_string_to_list(strength_lora_3))
304
+ if lora_4 != "none":
305
+ output["loras"].append(lora_4)
306
+ if strength_lora_4 == "":
307
+ strength_lora_4 = "1.0"
308
+ output["strengths"].append(parse_string_to_list(strength_lora_4))
309
+
310
+ return (output,)
311
+
312
+
313
+ class FluxSamplerParams:
314
+ def __init__(self):
315
+ self.loraloader = None
316
+ self.lora = (None, None)
317
+
318
+ @classmethod
319
+ def INPUT_TYPES(s):
320
+ return {"required": {
321
+ "model": ("MODEL", ),
322
+ "conditioning": ("CONDITIONING", ),
323
+ "latent_image": ("LATENT", ),
324
+
325
+ "seed": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "?" }),
326
+ "sampler": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "euler" }),
327
+ "scheduler": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "simple" }),
328
+ "steps": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "20" }),
329
+ "guidance": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "3.5" }),
330
+ "max_shift": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "" }),
331
+ "base_shift": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "" }),
332
+ "denoise": ("STRING", { "multiline": False, "dynamicPrompts": False, "default": "1.0" }),
333
+ },
334
+ "optional": {
335
+ "loras": ("LORA_PARAMS",),
336
+ }}
337
+
338
+ RETURN_TYPES = ("LATENT","SAMPLER_PARAMS")
339
+ RETURN_NAMES = ("latent", "params")
340
+ FUNCTION = "execute"
341
+ CATEGORY = "essentials/sampling"
342
+
343
+ def execute(self, model, conditioning, latent_image, seed, sampler, scheduler, steps, guidance, max_shift, base_shift, denoise, loras=None):
344
+ import random
345
+ import time
346
+ from comfy_extras.nodes_custom_sampler import Noise_RandomNoise, BasicScheduler, BasicGuider, SamplerCustomAdvanced
347
+ from comfy_extras.nodes_latent import LatentBatch
348
+ from comfy_extras.nodes_model_advanced import ModelSamplingFlux, ModelSamplingAuraFlow
349
+ from node_helpers import conditioning_set_values
350
+ from nodes import LoraLoader
351
+
352
+ is_schnell = model.model.model_type == comfy.model_base.ModelType.FLOW
353
+
354
+ noise = seed.replace("\n", ",").split(",")
355
+ noise = [random.randint(0, 999999) if "?" in n else int(n) for n in noise]
356
+ if not noise:
357
+ noise = [random.randint(0, 999999)]
358
+
359
+ if sampler == '*':
360
+ sampler = comfy.samplers.KSampler.SAMPLERS
361
+ elif sampler.startswith("!"):
362
+ sampler = sampler.replace("\n", ",").split(",")
363
+ sampler = [s.strip("! ") for s in sampler]
364
+ sampler = [s for s in comfy.samplers.KSampler.SAMPLERS if s not in sampler]
365
+ else:
366
+ sampler = sampler.replace("\n", ",").split(",")
367
+ sampler = [s.strip() for s in sampler if s.strip() in comfy.samplers.KSampler.SAMPLERS]
368
+ if not sampler:
369
+ sampler = ['ipndm']
370
+
371
+ if scheduler == '*':
372
+ scheduler = comfy.samplers.KSampler.SCHEDULERS
373
+ elif scheduler.startswith("!"):
374
+ scheduler = scheduler.replace("\n", ",").split(",")
375
+ scheduler = [s.strip("! ") for s in scheduler]
376
+ scheduler = [s for s in comfy.samplers.KSampler.SCHEDULERS if s not in scheduler]
377
+ else:
378
+ scheduler = scheduler.replace("\n", ",").split(",")
379
+ scheduler = [s.strip() for s in scheduler]
380
+ scheduler = [s for s in scheduler if s in comfy.samplers.KSampler.SCHEDULERS]
381
+ if not scheduler:
382
+ scheduler = ['simple']
383
+
384
+ if steps == "":
385
+ if is_schnell:
386
+ steps = "4"
387
+ else:
388
+ steps = "20"
389
+ steps = parse_string_to_list(steps)
390
+
391
+ denoise = "1.0" if denoise == "" else denoise
392
+ denoise = parse_string_to_list(denoise)
393
+
394
+ guidance = "3.5" if guidance == "" else guidance
395
+ guidance = parse_string_to_list(guidance)
396
+
397
+ if not is_schnell:
398
+ max_shift = "1.15" if max_shift == "" else max_shift
399
+ base_shift = "0.5" if base_shift == "" else base_shift
400
+ else:
401
+ max_shift = "0"
402
+ base_shift = "1.0" if base_shift == "" else base_shift
403
+
404
+ max_shift = parse_string_to_list(max_shift)
405
+ base_shift = parse_string_to_list(base_shift)
406
+
407
+ cond_text = None
408
+ if isinstance(conditioning, dict) and "encoded" in conditioning:
409
+ cond_text = conditioning["text"]
410
+ cond_encoded = conditioning["encoded"]
411
+ else:
412
+ cond_encoded = [conditioning]
413
+
414
+ out_latent = None
415
+ out_params = []
416
+
417
+ basicschedueler = BasicScheduler()
418
+ basicguider = BasicGuider()
419
+ samplercustomadvanced = SamplerCustomAdvanced()
420
+ latentbatch = LatentBatch()
421
+ modelsamplingflux = ModelSamplingFlux() if not is_schnell else ModelSamplingAuraFlow()
422
+ width = latent_image["samples"].shape[3]*8
423
+ height = latent_image["samples"].shape[2]*8
424
+
425
+ lora_strength_len = 1
426
+ if loras:
427
+ lora_model = loras["loras"]
428
+ lora_strength = loras["strengths"]
429
+ lora_strength_len = sum(len(i) for i in lora_strength)
430
+
431
+ if self.loraloader is None:
432
+ self.loraloader = LoraLoader()
433
+
434
+ # count total number of samples
435
+ total_samples = len(cond_encoded) * len(noise) * len(max_shift) * len(base_shift) * len(guidance) * len(sampler) * len(scheduler) * len(steps) * len(denoise) * lora_strength_len
436
+ current_sample = 0
437
+ if total_samples > 1:
438
+ pbar = ProgressBar(total_samples)
439
+
440
+ lora_strength_len = 1
441
+ if loras:
442
+ lora_strength_len = len(lora_strength[0])
443
+
444
+ for los in range(lora_strength_len):
445
+ if loras:
446
+ patched_model = self.loraloader.load_lora(model, None, lora_model[0], lora_strength[0][los], 0)[0]
447
+ else:
448
+ patched_model = model
449
+
450
+ for i in range(len(cond_encoded)):
451
+ conditioning = cond_encoded[i]
452
+ ct = cond_text[i] if cond_text else None
453
+ for n in noise:
454
+ randnoise = Noise_RandomNoise(n)
455
+ for ms in max_shift:
456
+ for bs in base_shift:
457
+ if is_schnell:
458
+ work_model = modelsamplingflux.patch_aura(patched_model, bs)[0]
459
+ else:
460
+ work_model = modelsamplingflux.patch(patched_model, ms, bs, width, height)[0]
461
+ for g in guidance:
462
+ cond = conditioning_set_values(conditioning, {"guidance": g})
463
+ guider = basicguider.get_guider(work_model, cond)[0]
464
+ for s in sampler:
465
+ samplerobj = comfy.samplers.sampler_object(s)
466
+ for sc in scheduler:
467
+ for st in steps:
468
+ for d in denoise:
469
+ sigmas = basicschedueler.get_sigmas(work_model, sc, st, d)[0]
470
+ current_sample += 1
471
+ log = f"Sampling {current_sample}/{total_samples} with seed {n}, sampler {s}, scheduler {sc}, steps {st}, guidance {g}, max_shift {ms}, base_shift {bs}, denoise {d}"
472
+ lora_name = None
473
+ lora_str = 0
474
+ if loras:
475
+ lora_name = lora_model[0]
476
+ lora_str = lora_strength[0][los]
477
+ log += f", lora {lora_name}, lora_strength {lora_str}"
478
+ logging.info(log)
479
+ start_time = time.time()
480
+ latent = samplercustomadvanced.sample(randnoise, guider, samplerobj, sigmas, latent_image)[1]
481
+ elapsed_time = time.time() - start_time
482
+ out_params.append({"time": elapsed_time,
483
+ "seed": n,
484
+ "width": width,
485
+ "height": height,
486
+ "sampler": s,
487
+ "scheduler": sc,
488
+ "steps": st,
489
+ "guidance": g,
490
+ "max_shift": ms,
491
+ "base_shift": bs,
492
+ "denoise": d,
493
+ "prompt": ct,
494
+ "lora": lora_name,
495
+ "lora_strength": lora_str})
496
+
497
+ if out_latent is None:
498
+ out_latent = latent
499
+ else:
500
+ out_latent = latentbatch.batch(out_latent, latent)[0]
501
+ if total_samples > 1:
502
+ pbar.update(1)
503
+
504
+ return (out_latent, out_params)
505
+
506
+ class PlotParameters:
507
+ @classmethod
508
+ def INPUT_TYPES(s):
509
+ return {"required": {
510
+ "images": ("IMAGE", ),
511
+ "params": ("SAMPLER_PARAMS", ),
512
+ "order_by": (["none", "time", "seed", "steps", "denoise", "sampler", "scheduler", "guidance", "max_shift", "base_shift", "lora_strength"], ),
513
+ "cols_value": (["none", "time", "seed", "steps", "denoise", "sampler", "scheduler", "guidance", "max_shift", "base_shift", "lora_strength"], ),
514
+ "cols_num": ("INT", {"default": -1, "min": -1, "max": 1024 }),
515
+ "add_prompt": (["false", "true", "excerpt"], ),
516
+ "add_params": (["false", "true", "changes only"], {"default": "true"}),
517
+ }}
518
+
519
+ RETURN_TYPES = ("IMAGE", )
520
+ FUNCTION = "execute"
521
+ CATEGORY = "essentials/sampling"
522
+
523
+ def execute(self, images, params, order_by, cols_value, cols_num, add_prompt, add_params):
524
+ from PIL import Image, ImageDraw, ImageFont
525
+ import math
526
+ import textwrap
527
+
528
+ if images.shape[0] != len(params):
529
+ raise ValueError("Number of images and number of parameters do not match.")
530
+
531
+ _params = params.copy()
532
+
533
+ if order_by != "none":
534
+ sorted_params = sorted(_params, key=lambda x: x[order_by])
535
+ indices = [_params.index(item) for item in sorted_params]
536
+ images = images[torch.tensor(indices)]
537
+ _params = sorted_params
538
+
539
+ if cols_value != "none" and cols_num > -1:
540
+ groups = {}
541
+ for p in _params:
542
+ value = p[cols_value]
543
+ if value not in groups:
544
+ groups[value] = []
545
+ groups[value].append(p)
546
+ cols_num = len(groups)
547
+
548
+ sorted_params = []
549
+ groups = list(groups.values())
550
+ for g in zip(*groups):
551
+ sorted_params.extend(g)
552
+
553
+ indices = [_params.index(item) for item in sorted_params]
554
+ images = images[torch.tensor(indices)]
555
+ _params = sorted_params
556
+ elif cols_num == 0:
557
+ cols_num = int(math.sqrt(images.shape[0]))
558
+ cols_num = max(1, min(cols_num, 1024))
559
+
560
+ width = images.shape[2]
561
+ out_image = []
562
+
563
+ font = ImageFont.truetype(os.path.join(FONTS_DIR, 'ShareTechMono-Regular.ttf'), min(48, int(32*(width/1024))))
564
+ text_padding = 3
565
+ line_height = font.getmask('Q').getbbox()[3] + font.getmetrics()[1] + text_padding*2
566
+ char_width = font.getbbox('M')[2]+1 # using monospace font
567
+
568
+ if add_params == "changes only":
569
+ value_tracker = {}
570
+ for p in _params:
571
+ for key, value in p.items():
572
+ if key != "time":
573
+ if key not in value_tracker:
574
+ value_tracker[key] = set()
575
+ value_tracker[key].add(value)
576
+ changing_keys = {key for key, values in value_tracker.items() if len(values) > 1 or key == "prompt"}
577
+
578
+ result = []
579
+ for p in _params:
580
+ changing_params = {key: value for key, value in p.items() if key in changing_keys}
581
+ result.append(changing_params)
582
+
583
+ _params = result
584
+
585
+ for (image, param) in zip(images, _params):
586
+ image = image.permute(2, 0, 1)
587
+
588
+ if add_params != "false":
589
+ if add_params == "changes only":
590
+ text = "\n".join([f"{key}: {value}" for key, value in param.items() if key != "prompt"])
591
+ else:
592
+ text = f"time: {param['time']:.2f}s, seed: {param['seed']}, steps: {param['steps']}, size: {param['width']}×{param['height']}\ndenoise: {param['denoise']}, sampler: {param['sampler']}, sched: {param['scheduler']}\nguidance: {param['guidance']}, max/base shift: {param['max_shift']}/{param['base_shift']}"
593
+ if 'lora' in param and param['lora']:
594
+ text += f"\nLoRA: {param['lora'][:32]}, str: {param['lora_strength']}"
595
+
596
+ lines = text.split("\n")
597
+ text_height = line_height * len(lines)
598
+ text_image = Image.new('RGB', (width, text_height), color=(0, 0, 0))
599
+
600
+ for i, line in enumerate(lines):
601
+ draw = ImageDraw.Draw(text_image)
602
+ draw.text((text_padding, i * line_height + text_padding), line, font=font, fill=(255, 255, 255))
603
+
604
+ text_image = T.ToTensor()(text_image).to(image.device)
605
+ image = torch.cat([image, text_image], 1)
606
+
607
+ if 'prompt' in param and param['prompt'] and add_prompt != "false":
608
+ prompt = param['prompt']
609
+ if add_prompt == "excerpt":
610
+ prompt = " ".join(param['prompt'].split()[:64])
611
+ prompt += "..."
612
+
613
+ cols = math.ceil(width / char_width)
614
+ prompt_lines = textwrap.wrap(prompt, width=cols)
615
+ prompt_height = line_height * len(prompt_lines)
616
+ prompt_image = Image.new('RGB', (width, prompt_height), color=(0, 0, 0))
617
+
618
+ for i, line in enumerate(prompt_lines):
619
+ draw = ImageDraw.Draw(prompt_image)
620
+ draw.text((text_padding, i * line_height + text_padding), line, font=font, fill=(255, 255, 255))
621
+
622
+ prompt_image = T.ToTensor()(prompt_image).to(image.device)
623
+ image = torch.cat([image, prompt_image], 1)
624
+
625
+ # a little cleanup
626
+ image = torch.nan_to_num(image, nan=0.0).clamp(0.0, 1.0)
627
+ out_image.append(image)
628
+
629
+ # ensure all images have the same height
630
+ if add_prompt != "false" or add_params == "changes only":
631
+ max_height = max([image.shape[1] for image in out_image])
632
+ out_image = [F.pad(image, (0, 0, 0, max_height - image.shape[1])) for image in out_image]
633
+
634
+ out_image = torch.stack(out_image, 0).permute(0, 2, 3, 1)
635
+
636
+ # merge images
637
+ if cols_num > -1:
638
+ cols = min(cols_num, out_image.shape[0])
639
+ b, h, w, c = out_image.shape
640
+ rows = math.ceil(b / cols)
641
+
642
+ # Pad the tensor if necessary
643
+ if b % cols != 0:
644
+ padding = cols - (b % cols)
645
+ out_image = F.pad(out_image, (0, 0, 0, 0, 0, 0, 0, padding))
646
+ b = out_image.shape[0]
647
+
648
+ # Reshape and transpose
649
+ out_image = out_image.reshape(rows, cols, h, w, c)
650
+ out_image = out_image.permute(0, 2, 1, 3, 4)
651
+ out_image = out_image.reshape(rows * h, cols * w, c).unsqueeze(0)
652
+
653
+ """
654
+ width = out_image.shape[2]
655
+ # add the title and notes on top
656
+ if title and export_labels:
657
+ title_font = ImageFont.truetype(os.path.join(FONTS_DIR, 'ShareTechMono-Regular.ttf'), 48)
658
+ title_width = title_font.getbbox(title)[2]
659
+ title_padding = 6
660
+ title_line_height = title_font.getmask(title).getbbox()[3] + title_font.getmetrics()[1] + title_padding*2
661
+ title_text_height = title_line_height
662
+ title_text_image = Image.new('RGB', (width, title_text_height), color=(0, 0, 0, 0))
663
+
664
+ draw = ImageDraw.Draw(title_text_image)
665
+ draw.text((width//2 - title_width//2, title_padding), title, font=title_font, fill=(255, 255, 255))
666
+
667
+ title_text_image = T.ToTensor()(title_text_image).unsqueeze(0).permute([0,2,3,1]).to(out_image.device)
668
+ out_image = torch.cat([title_text_image, out_image], 1)
669
+ """
670
+
671
+ return (out_image, )
672
+
673
+ class GuidanceTimestepping:
674
+ @classmethod
675
+ def INPUT_TYPES(s):
676
+ return {
677
+ "required": {
678
+ "model": ("MODEL",),
679
+ "value": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 100.0, "step": 0.05}),
680
+ "start_at": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
681
+ "end_at": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.01}),
682
+ }
683
+ }
684
+
685
+ RETURN_TYPES = ("MODEL",)
686
+ FUNCTION = "execute"
687
+ CATEGORY = "essentials/sampling"
688
+
689
+ def execute(self, model, value, start_at, end_at):
690
+ sigma_start = model.get_model_object("model_sampling").percent_to_sigma(start_at)
691
+ sigma_end = model.get_model_object("model_sampling").percent_to_sigma(end_at)
692
+
693
+ def apply_apg(args):
694
+ cond = args["cond"]
695
+ uncond = args["uncond"]
696
+ cond_scale = args["cond_scale"]
697
+ sigma = args["sigma"]
698
+
699
+ sigma = sigma.detach().cpu()[0].item()
700
+
701
+ if sigma <= sigma_start and sigma > sigma_end:
702
+ cond_scale = value
703
+
704
+ return uncond + (cond - uncond) * cond_scale
705
+
706
+ m = model.clone()
707
+ m.set_model_sampler_cfg_function(apply_apg)
708
+ return (m,)
709
+
710
+ class ModelSamplingDiscreteFlowCustom(torch.nn.Module):
711
+ def __init__(self, model_config=None):
712
+ super().__init__()
713
+ if model_config is not None:
714
+ sampling_settings = model_config.sampling_settings
715
+ else:
716
+ sampling_settings = {}
717
+
718
+ self.set_parameters(shift=sampling_settings.get("shift", 1.0), multiplier=sampling_settings.get("multiplier", 1000))
719
+
720
+ def set_parameters(self, shift=1.0, timesteps=1000, multiplier=1000, cut_off=1.0, shift_multiplier=0):
721
+ self.shift = shift
722
+ self.multiplier = multiplier
723
+ self.cut_off = cut_off
724
+ self.shift_multiplier = shift_multiplier
725
+ ts = self.sigma((torch.arange(1, timesteps + 1, 1) / timesteps) * multiplier)
726
+ self.register_buffer('sigmas', ts)
727
+
728
+ @property
729
+ def sigma_min(self):
730
+ return self.sigmas[0]
731
+
732
+ @property
733
+ def sigma_max(self):
734
+ return self.sigmas[-1]
735
+
736
+ def timestep(self, sigma):
737
+ return sigma * self.multiplier
738
+
739
+ def sigma(self, timestep):
740
+ shift = self.shift
741
+ if timestep.dim() == 0:
742
+ t = timestep.cpu().item() / self.multiplier
743
+ if t <= self.cut_off:
744
+ shift = shift * self.shift_multiplier
745
+
746
+ return comfy.model_sampling.time_snr_shift(shift, timestep / self.multiplier)
747
+
748
+ def percent_to_sigma(self, percent):
749
+ if percent <= 0.0:
750
+ return 1.0
751
+ if percent >= 1.0:
752
+ return 0.0
753
+ return 1.0 - percent
754
+
755
+ class ModelSamplingSD3Advanced:
756
+ @classmethod
757
+ def INPUT_TYPES(s):
758
+ return {"required": { "model": ("MODEL",),
759
+ "shift": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 100.0, "step":0.01}),
760
+ "cut_off": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step":0.05}),
761
+ "shift_multiplier": ("FLOAT", {"default": 2, "min": 0, "max": 10, "step":0.05}),
762
+ }}
763
+
764
+ RETURN_TYPES = ("MODEL",)
765
+ FUNCTION = "execute"
766
+
767
+ CATEGORY = "essentials/sampling"
768
+
769
+ def execute(self, model, shift, multiplier=1000, cut_off=1.0, shift_multiplier=0):
770
+ m = model.clone()
771
+
772
+
773
+ sampling_base = ModelSamplingDiscreteFlowCustom
774
+ sampling_type = comfy.model_sampling.CONST
775
+
776
+ class ModelSamplingAdvanced(sampling_base, sampling_type):
777
+ pass
778
+
779
+ model_sampling = ModelSamplingAdvanced(model.model.model_config)
780
+ model_sampling.set_parameters(shift=shift, multiplier=multiplier, cut_off=cut_off, shift_multiplier=shift_multiplier)
781
+ m.add_object_patch("model_sampling", model_sampling)
782
+
783
+ return (m, )
784
+
785
+ SAMPLING_CLASS_MAPPINGS = {
786
+ "KSamplerVariationsStochastic+": KSamplerVariationsStochastic,
787
+ "KSamplerVariationsWithNoise+": KSamplerVariationsWithNoise,
788
+ "InjectLatentNoise+": InjectLatentNoise,
789
+ "FluxSamplerParams+": FluxSamplerParams,
790
+ "GuidanceTimestepping+": GuidanceTimestepping,
791
+ "PlotParameters+": PlotParameters,
792
+ "TextEncodeForSamplerParams+": TextEncodeForSamplerParams,
793
+ "SamplerSelectHelper+": SamplerSelectHelper,
794
+ "SchedulerSelectHelper+": SchedulerSelectHelper,
795
+ "LorasForFluxParams+": LorasForFluxParams,
796
+ "ModelSamplingSD3Advanced+": ModelSamplingSD3Advanced,
797
+ }
798
+
799
+ SAMPLING_NAME_MAPPINGS = {
800
+ "KSamplerVariationsStochastic+": "🔧 KSampler Stochastic Variations",
801
+ "KSamplerVariationsWithNoise+": "🔧 KSampler Variations with Noise Injection",
802
+ "InjectLatentNoise+": "🔧 Inject Latent Noise",
803
+ "FluxSamplerParams+": "🔧 Flux Sampler Parameters",
804
+ "GuidanceTimestepping+": "🔧 Guidance Timestep (experimental)",
805
+ "PlotParameters+": "🔧 Plot Sampler Parameters",
806
+ "TextEncodeForSamplerParams+": "🔧Text Encode for Sampler Params",
807
+ "SamplerSelectHelper+": "🔧 Sampler Select Helper",
808
+ "SchedulerSelectHelper+": "🔧 Scheduler Select Helper",
809
+ "LorasForFluxParams+": "🔧 LoRA for Flux Parameters",
810
+ "ModelSamplingSD3Advanced+": "🔧 Model Sampling SD3 Advanced",
811
+ }
ComfyUI_essentials/segmentation.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torchvision.transforms.v2 as T
3
+ import torch.nn.functional as F
4
+ from .utils import expand_mask
5
+
6
+ class LoadCLIPSegModels:
7
+ @classmethod
8
+ def INPUT_TYPES(s):
9
+ return {
10
+ "required": {},
11
+ }
12
+
13
+ RETURN_TYPES = ("CLIP_SEG",)
14
+ FUNCTION = "execute"
15
+ CATEGORY = "essentials/segmentation"
16
+
17
+ def execute(self):
18
+ from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
19
+ processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined")
20
+ model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined")
21
+
22
+ return ((processor, model),)
23
+
24
+ class ApplyCLIPSeg:
25
+ @classmethod
26
+ def INPUT_TYPES(s):
27
+ return {
28
+ "required": {
29
+ "clip_seg": ("CLIP_SEG",),
30
+ "image": ("IMAGE",),
31
+ "prompt": ("STRING", { "multiline": False, "default": "" }),
32
+ "threshold": ("FLOAT", { "default": 0.4, "min": 0.0, "max": 1.0, "step": 0.05 }),
33
+ "smooth": ("INT", { "default": 9, "min": 0, "max": 32, "step": 1 }),
34
+ "dilate": ("INT", { "default": 0, "min": -32, "max": 32, "step": 1 }),
35
+ "blur": ("INT", { "default": 0, "min": 0, "max": 64, "step": 1 }),
36
+ },
37
+ }
38
+
39
+ RETURN_TYPES = ("MASK",)
40
+ FUNCTION = "execute"
41
+ CATEGORY = "essentials/segmentation"
42
+
43
+ def execute(self, image, clip_seg, prompt, threshold, smooth, dilate, blur):
44
+ processor, model = clip_seg
45
+
46
+ imagenp = image.mul(255).clamp(0, 255).byte().cpu().numpy()
47
+
48
+ outputs = []
49
+ for i in imagenp:
50
+ inputs = processor(text=prompt, images=[i], return_tensors="pt")
51
+ out = model(**inputs)
52
+ out = out.logits.unsqueeze(1)
53
+ out = torch.sigmoid(out[0][0])
54
+ out = (out > threshold)
55
+ outputs.append(out)
56
+
57
+ del imagenp
58
+
59
+ outputs = torch.stack(outputs, dim=0)
60
+
61
+ if smooth > 0:
62
+ if smooth % 2 == 0:
63
+ smooth += 1
64
+ outputs = T.functional.gaussian_blur(outputs, smooth)
65
+
66
+ outputs = outputs.float()
67
+
68
+ if dilate != 0:
69
+ outputs = expand_mask(outputs, dilate, True)
70
+
71
+ if blur > 0:
72
+ if blur % 2 == 0:
73
+ blur += 1
74
+ outputs = T.functional.gaussian_blur(outputs, blur)
75
+
76
+ # resize to original size
77
+ outputs = F.interpolate(outputs.unsqueeze(1), size=(image.shape[1], image.shape[2]), mode='bicubic').squeeze(1)
78
+
79
+ return (outputs,)
80
+
81
+ SEG_CLASS_MAPPINGS = {
82
+ "ApplyCLIPSeg+": ApplyCLIPSeg,
83
+ "LoadCLIPSegModels+": LoadCLIPSegModels,
84
+ }
85
+
86
+ SEG_NAME_MAPPINGS = {
87
+ "ApplyCLIPSeg+": "🔧 Apply CLIPSeg",
88
+ "LoadCLIPSegModels+": "🔧 Load CLIPSeg Models",
89
+ }
ComfyUI_essentials/text.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ from nodes import MAX_RESOLUTION
4
+ import torchvision.transforms.v2 as T
5
+ from .utils import FONTS_DIR
6
+
7
+ class DrawText:
8
+ @classmethod
9
+ def INPUT_TYPES(s):
10
+ return {
11
+ "required": {
12
+ "text": ("STRING", { "multiline": True, "dynamicPrompts": True, "default": "Hello, World!" }),
13
+ "font": (sorted([f for f in os.listdir(FONTS_DIR) if f.endswith('.ttf') or f.endswith('.otf')]), ),
14
+ "size": ("INT", { "default": 56, "min": 1, "max": 9999, "step": 1 }),
15
+ "color": ("STRING", { "multiline": False, "default": "#FFFFFF" }),
16
+ "background_color": ("STRING", { "multiline": False, "default": "#00000000" }),
17
+ "shadow_distance": ("INT", { "default": 0, "min": 0, "max": 100, "step": 1 }),
18
+ "shadow_blur": ("INT", { "default": 0, "min": 0, "max": 100, "step": 1 }),
19
+ "shadow_color": ("STRING", { "multiline": False, "default": "#000000" }),
20
+ "horizontal_align": (["left", "center", "right"],),
21
+ "vertical_align": (["top", "center", "bottom"],),
22
+ "offset_x": ("INT", { "default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1 }),
23
+ "offset_y": ("INT", { "default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1 }),
24
+ "direction": (["ltr", "rtl"],),
25
+ },
26
+ "optional": {
27
+ "img_composite": ("IMAGE",),
28
+ },
29
+ }
30
+
31
+ RETURN_TYPES = ("IMAGE", "MASK",)
32
+ FUNCTION = "execute"
33
+ CATEGORY = "essentials/text"
34
+
35
+ def execute(self, text, font, size, color, background_color, shadow_distance, shadow_blur, shadow_color, horizontal_align, vertical_align, offset_x, offset_y, direction, img_composite=None):
36
+ from PIL import Image, ImageDraw, ImageFont, ImageColor, ImageFilter
37
+
38
+ font = ImageFont.truetype(os.path.join(FONTS_DIR, font), size)
39
+
40
+ lines = text.split("\n")
41
+ if direction == "rtl":
42
+ lines = [line[::-1] for line in lines]
43
+
44
+ # Calculate the width and height of the text
45
+ text_width = max(font.getbbox(line)[2] for line in lines)
46
+ line_height = font.getmask(text).getbbox()[3] + font.getmetrics()[1] # add descent to height
47
+ text_height = line_height * len(lines)
48
+
49
+ if img_composite is not None:
50
+ img_composite = T.ToPILImage()(img_composite.permute([0,3,1,2])[0]).convert('RGBA')
51
+ width = img_composite.width
52
+ height = img_composite.height
53
+ image = Image.new('RGBA', (width, height), color=background_color)
54
+ else:
55
+ width = text_width
56
+ height = text_height
57
+ background_color = ImageColor.getrgb(background_color)
58
+ image = Image.new('RGBA', (width + shadow_distance, height + shadow_distance), color=background_color)
59
+
60
+ image_shadow = None
61
+ if shadow_distance > 0:
62
+ image_shadow = image.copy()
63
+ #image_shadow = Image.new('RGBA', (width + shadow_distance, height + shadow_distance), color=background_color)
64
+
65
+ for i, line in enumerate(lines):
66
+ line_width = font.getbbox(line)[2]
67
+ #text_height =font.getbbox(line)[3]
68
+ if horizontal_align == "left":
69
+ x = 0
70
+ elif horizontal_align == "center":
71
+ x = (width - line_width) / 2
72
+ elif horizontal_align == "right":
73
+ x = width - line_width
74
+
75
+ if vertical_align == "top":
76
+ y = 0
77
+ elif vertical_align == "center":
78
+ y = (height - text_height) / 2
79
+ elif vertical_align == "bottom":
80
+ y = height - text_height
81
+
82
+ x += offset_x
83
+ y += i * line_height + offset_y
84
+
85
+ draw = ImageDraw.Draw(image)
86
+ draw.text((x, y), line, font=font, fill=color)
87
+
88
+ if image_shadow is not None:
89
+ draw = ImageDraw.Draw(image_shadow)
90
+ draw.text((x + shadow_distance, y + shadow_distance), line, font=font, fill=shadow_color)
91
+
92
+ if image_shadow is not None:
93
+ image_shadow = image_shadow.filter(ImageFilter.GaussianBlur(shadow_blur))
94
+ image = Image.alpha_composite(image_shadow, image)
95
+
96
+ #image = T.ToTensor()(image).unsqueeze(0).permute([0,2,3,1])
97
+ mask = T.ToTensor()(image).unsqueeze(0).permute([0,2,3,1])
98
+ mask = mask[:, :, :, 3] if mask.shape[3] == 4 else torch.ones_like(mask[:, :, :, 0])
99
+
100
+ if img_composite is not None:
101
+ image = Image.alpha_composite(img_composite, image)
102
+
103
+ image = T.ToTensor()(image).unsqueeze(0).permute([0,2,3,1])
104
+
105
+ return (image[:, :, :, :3], mask,)
106
+
107
+ TEXT_CLASS_MAPPINGS = {
108
+ "DrawText+": DrawText,
109
+ }
110
+
111
+ TEXT_NAME_MAPPINGS = {
112
+ "DrawText+": "🔧 Draw Text",
113
+ }
ComfyUI_essentials/utils.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ import scipy
4
+ import os
5
+ #import re
6
+ from pathlib import Path
7
+ import folder_paths
8
+
9
+ FONTS_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts")
10
+
11
+ SCRIPT_DIR = Path(__file__).parent
12
+ folder_paths.add_model_folder_path("luts", (SCRIPT_DIR / "luts").as_posix())
13
+ folder_paths.add_model_folder_path(
14
+ "luts", (Path(folder_paths.models_dir) / "luts").as_posix()
15
+ )
16
+
17
+ # from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
18
+ class AnyType(str):
19
+ def __ne__(self, __value: object) -> bool:
20
+ return False
21
+
22
+ def min_(tensor_list):
23
+ # return the element-wise min of the tensor list.
24
+ x = torch.stack(tensor_list)
25
+ mn = x.min(axis=0)[0]
26
+ return torch.clamp(mn, min=0)
27
+
28
+ def max_(tensor_list):
29
+ # return the element-wise max of the tensor list.
30
+ x = torch.stack(tensor_list)
31
+ mx = x.max(axis=0)[0]
32
+ return torch.clamp(mx, max=1)
33
+
34
+ def expand_mask(mask, expand, tapered_corners):
35
+ c = 0 if tapered_corners else 1
36
+ kernel = np.array([[c, 1, c],
37
+ [1, 1, 1],
38
+ [c, 1, c]])
39
+ mask = mask.reshape((-1, mask.shape[-2], mask.shape[-1]))
40
+ out = []
41
+ for m in mask:
42
+ output = m.numpy()
43
+ for _ in range(abs(expand)):
44
+ if expand < 0:
45
+ output = scipy.ndimage.grey_erosion(output, footprint=kernel)
46
+ else:
47
+ output = scipy.ndimage.grey_dilation(output, footprint=kernel)
48
+ output = torch.from_numpy(output)
49
+ out.append(output)
50
+
51
+ return torch.stack(out, dim=0)
52
+
53
+ def parse_string_to_list(s):
54
+ elements = s.split(',')
55
+ result = []
56
+
57
+ def parse_number(s):
58
+ try:
59
+ if '.' in s:
60
+ return float(s)
61
+ else:
62
+ return int(s)
63
+ except ValueError:
64
+ return 0
65
+
66
+ def decimal_places(s):
67
+ if '.' in s:
68
+ return len(s.split('.')[1])
69
+ return 0
70
+
71
+ for element in elements:
72
+ element = element.strip()
73
+ if '...' in element:
74
+ start, rest = element.split('...')
75
+ end, step = rest.split('+')
76
+ decimals = decimal_places(step)
77
+ start = parse_number(start)
78
+ end = parse_number(end)
79
+ step = parse_number(step)
80
+ current = start
81
+ if (start > end and step > 0) or (start < end and step < 0):
82
+ step = -step
83
+ while current <= end:
84
+ result.append(round(current, decimals))
85
+ current += step
86
+ else:
87
+ result.append(round(parse_number(element), decimal_places(element)))
88
+
89
+ return result
ComfyUI_essentials/workflow_all_nodes.json ADDED
@@ -0,0 +1,994 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "last_node_id": 42,
3
+ "last_link_id": 61,
4
+ "nodes": [
5
+ {
6
+ "id": 9,
7
+ "type": "ConsoleDebug+",
8
+ "pos": [
9
+ 720,
10
+ 140
11
+ ],
12
+ "size": {
13
+ "0": 210,
14
+ "1": 60
15
+ },
16
+ "flags": {},
17
+ "order": 12,
18
+ "mode": 0,
19
+ "inputs": [
20
+ {
21
+ "name": "value",
22
+ "type": "*",
23
+ "link": 3
24
+ }
25
+ ],
26
+ "properties": {
27
+ "Node name for S&R": "ConsoleDebug+"
28
+ },
29
+ "widgets_values": [
30
+ "Height:"
31
+ ]
32
+ },
33
+ {
34
+ "id": 28,
35
+ "type": "PreviewImage",
36
+ "pos": [
37
+ 860,
38
+ 1180
39
+ ],
40
+ "size": {
41
+ "0": 210,
42
+ "1": 246
43
+ },
44
+ "flags": {},
45
+ "order": 17,
46
+ "mode": 0,
47
+ "inputs": [
48
+ {
49
+ "name": "images",
50
+ "type": "IMAGE",
51
+ "link": 23
52
+ }
53
+ ],
54
+ "properties": {
55
+ "Node name for S&R": "PreviewImage"
56
+ }
57
+ },
58
+ {
59
+ "id": 12,
60
+ "type": "PreviewImage",
61
+ "pos": [
62
+ 860,
63
+ 580
64
+ ],
65
+ "size": {
66
+ "0": 210,
67
+ "1": 246
68
+ },
69
+ "flags": {},
70
+ "order": 15,
71
+ "mode": 0,
72
+ "inputs": [
73
+ {
74
+ "name": "images",
75
+ "type": "IMAGE",
76
+ "link": 11
77
+ }
78
+ ],
79
+ "properties": {
80
+ "Node name for S&R": "PreviewImage"
81
+ }
82
+ },
83
+ {
84
+ "id": 14,
85
+ "type": "PreviewImage",
86
+ "pos": [
87
+ 860,
88
+ 880
89
+ ],
90
+ "size": {
91
+ "0": 210,
92
+ "1": 246
93
+ },
94
+ "flags": {},
95
+ "order": 16,
96
+ "mode": 0,
97
+ "inputs": [
98
+ {
99
+ "name": "images",
100
+ "type": "IMAGE",
101
+ "link": 13
102
+ }
103
+ ],
104
+ "properties": {
105
+ "Node name for S&R": "PreviewImage"
106
+ }
107
+ },
108
+ {
109
+ "id": 18,
110
+ "type": "MaskPreview+",
111
+ "pos": [
112
+ 2100,
113
+ 90
114
+ ],
115
+ "size": {
116
+ "0": 210,
117
+ "1": 246
118
+ },
119
+ "flags": {},
120
+ "order": 20,
121
+ "mode": 0,
122
+ "inputs": [
123
+ {
124
+ "name": "mask",
125
+ "type": "MASK",
126
+ "link": 19
127
+ }
128
+ ],
129
+ "properties": {
130
+ "Node name for S&R": "MaskPreview+"
131
+ }
132
+ },
133
+ {
134
+ "id": 1,
135
+ "type": "GetImageSize+",
136
+ "pos": [
137
+ 450,
138
+ 80
139
+ ],
140
+ "size": {
141
+ "0": 210,
142
+ "1": 46
143
+ },
144
+ "flags": {},
145
+ "order": 2,
146
+ "mode": 0,
147
+ "inputs": [
148
+ {
149
+ "name": "image",
150
+ "type": "IMAGE",
151
+ "link": 1
152
+ }
153
+ ],
154
+ "outputs": [
155
+ {
156
+ "name": "width",
157
+ "type": "INT",
158
+ "links": [
159
+ 2
160
+ ],
161
+ "shape": 3,
162
+ "slot_index": 0
163
+ },
164
+ {
165
+ "name": "height",
166
+ "type": "INT",
167
+ "links": [
168
+ 3
169
+ ],
170
+ "shape": 3,
171
+ "slot_index": 1
172
+ }
173
+ ],
174
+ "properties": {
175
+ "Node name for S&R": "GetImageSize+"
176
+ }
177
+ },
178
+ {
179
+ "id": 8,
180
+ "type": "ConsoleDebug+",
181
+ "pos": [
182
+ 720,
183
+ 40
184
+ ],
185
+ "size": {
186
+ "0": 210,
187
+ "1": 60
188
+ },
189
+ "flags": {},
190
+ "order": 11,
191
+ "mode": 0,
192
+ "inputs": [
193
+ {
194
+ "name": "value",
195
+ "type": "*",
196
+ "link": 2
197
+ }
198
+ ],
199
+ "properties": {
200
+ "Node name for S&R": "ConsoleDebug+"
201
+ },
202
+ "widgets_values": [
203
+ "Width:"
204
+ ]
205
+ },
206
+ {
207
+ "id": 10,
208
+ "type": "PreviewImage",
209
+ "pos": [
210
+ 860,
211
+ 280
212
+ ],
213
+ "size": {
214
+ "0": 210,
215
+ "1": 246
216
+ },
217
+ "flags": {},
218
+ "order": 13,
219
+ "mode": 0,
220
+ "inputs": [
221
+ {
222
+ "name": "images",
223
+ "type": "IMAGE",
224
+ "link": 9
225
+ }
226
+ ],
227
+ "properties": {
228
+ "Node name for S&R": "PreviewImage"
229
+ }
230
+ },
231
+ {
232
+ "id": 36,
233
+ "type": "SimpleMath+",
234
+ "pos": [
235
+ 1650,
236
+ 780
237
+ ],
238
+ "size": {
239
+ "0": 210,
240
+ "1": 80
241
+ },
242
+ "flags": {},
243
+ "order": 14,
244
+ "mode": 0,
245
+ "inputs": [
246
+ {
247
+ "name": "a",
248
+ "type": "INT,FLOAT",
249
+ "link": 44
250
+ },
251
+ {
252
+ "name": "b",
253
+ "type": "INT,FLOAT",
254
+ "link": 45
255
+ }
256
+ ],
257
+ "outputs": [
258
+ {
259
+ "name": "INT",
260
+ "type": "INT",
261
+ "links": [
262
+ 46
263
+ ],
264
+ "shape": 3,
265
+ "slot_index": 0
266
+ },
267
+ {
268
+ "name": "FLOAT",
269
+ "type": "FLOAT",
270
+ "links": null,
271
+ "shape": 3
272
+ }
273
+ ],
274
+ "properties": {
275
+ "Node name for S&R": "SimpleMath+"
276
+ },
277
+ "widgets_values": [
278
+ "a*b"
279
+ ]
280
+ },
281
+ {
282
+ "id": 23,
283
+ "type": "ConsoleDebug+",
284
+ "pos": [
285
+ 1920,
286
+ 780
287
+ ],
288
+ "size": {
289
+ "0": 210,
290
+ "1": 60
291
+ },
292
+ "flags": {},
293
+ "order": 22,
294
+ "mode": 0,
295
+ "inputs": [
296
+ {
297
+ "name": "value",
298
+ "type": "*",
299
+ "link": 46
300
+ }
301
+ ],
302
+ "properties": {
303
+ "Node name for S&R": "ConsoleDebug+"
304
+ },
305
+ "widgets_values": [
306
+ "Value:"
307
+ ]
308
+ },
309
+ {
310
+ "id": 2,
311
+ "type": "ImageResize+",
312
+ "pos": [
313
+ 430,
314
+ 340
315
+ ],
316
+ "size": {
317
+ "0": 310,
318
+ "1": 170
319
+ },
320
+ "flags": {},
321
+ "order": 3,
322
+ "mode": 0,
323
+ "inputs": [
324
+ {
325
+ "name": "image",
326
+ "type": "IMAGE",
327
+ "link": 4
328
+ }
329
+ ],
330
+ "outputs": [
331
+ {
332
+ "name": "IMAGE",
333
+ "type": "IMAGE",
334
+ "links": [
335
+ 9
336
+ ],
337
+ "shape": 3,
338
+ "slot_index": 0
339
+ },
340
+ {
341
+ "name": "width",
342
+ "type": "INT",
343
+ "links": [
344
+ 44
345
+ ],
346
+ "shape": 3,
347
+ "slot_index": 1
348
+ },
349
+ {
350
+ "name": "height",
351
+ "type": "INT",
352
+ "links": [
353
+ 45
354
+ ],
355
+ "shape": 3,
356
+ "slot_index": 2
357
+ }
358
+ ],
359
+ "properties": {
360
+ "Node name for S&R": "ImageResize+"
361
+ },
362
+ "widgets_values": [
363
+ 256,
364
+ 64,
365
+ "lanczos",
366
+ true
367
+ ]
368
+ },
369
+ {
370
+ "id": 4,
371
+ "type": "ImageFlip+",
372
+ "pos": [
373
+ 430,
374
+ 800
375
+ ],
376
+ "size": {
377
+ "0": 310,
378
+ "1": 60
379
+ },
380
+ "flags": {},
381
+ "order": 4,
382
+ "mode": 0,
383
+ "inputs": [
384
+ {
385
+ "name": "image",
386
+ "type": "IMAGE",
387
+ "link": 6
388
+ }
389
+ ],
390
+ "outputs": [
391
+ {
392
+ "name": "IMAGE",
393
+ "type": "IMAGE",
394
+ "links": [
395
+ 11
396
+ ],
397
+ "shape": 3,
398
+ "slot_index": 0
399
+ }
400
+ ],
401
+ "properties": {
402
+ "Node name for S&R": "ImageFlip+"
403
+ },
404
+ "widgets_values": [
405
+ "xy"
406
+ ]
407
+ },
408
+ {
409
+ "id": 6,
410
+ "type": "ImagePosterize+",
411
+ "pos": [
412
+ 430,
413
+ 1000
414
+ ],
415
+ "size": {
416
+ "0": 310,
417
+ "1": 60
418
+ },
419
+ "flags": {},
420
+ "order": 5,
421
+ "mode": 0,
422
+ "inputs": [
423
+ {
424
+ "name": "image",
425
+ "type": "IMAGE",
426
+ "link": 8
427
+ }
428
+ ],
429
+ "outputs": [
430
+ {
431
+ "name": "IMAGE",
432
+ "type": "IMAGE",
433
+ "links": [
434
+ 13
435
+ ],
436
+ "shape": 3,
437
+ "slot_index": 0
438
+ }
439
+ ],
440
+ "properties": {
441
+ "Node name for S&R": "ImagePosterize+"
442
+ },
443
+ "widgets_values": [
444
+ 0.5
445
+ ]
446
+ },
447
+ {
448
+ "id": 27,
449
+ "type": "ImageCASharpening+",
450
+ "pos": [
451
+ 430,
452
+ 1110
453
+ ],
454
+ "size": {
455
+ "0": 310.79998779296875,
456
+ "1": 60
457
+ },
458
+ "flags": {},
459
+ "order": 6,
460
+ "mode": 0,
461
+ "inputs": [
462
+ {
463
+ "name": "image",
464
+ "type": "IMAGE",
465
+ "link": 22
466
+ }
467
+ ],
468
+ "outputs": [
469
+ {
470
+ "name": "IMAGE",
471
+ "type": "IMAGE",
472
+ "links": [
473
+ 23
474
+ ],
475
+ "shape": 3,
476
+ "slot_index": 0
477
+ }
478
+ ],
479
+ "properties": {
480
+ "Node name for S&R": "ImageCASharpening+"
481
+ },
482
+ "widgets_values": [
483
+ 0.8
484
+ ]
485
+ },
486
+ {
487
+ "id": 15,
488
+ "type": "MaskBlur+",
489
+ "pos": [
490
+ 1690,
491
+ 130
492
+ ],
493
+ "size": {
494
+ "0": 310,
495
+ "1": 82
496
+ },
497
+ "flags": {},
498
+ "order": 9,
499
+ "mode": 0,
500
+ "inputs": [
501
+ {
502
+ "name": "mask",
503
+ "type": "MASK",
504
+ "link": 14
505
+ }
506
+ ],
507
+ "outputs": [
508
+ {
509
+ "name": "MASK",
510
+ "type": "MASK",
511
+ "links": [
512
+ 19
513
+ ],
514
+ "shape": 3,
515
+ "slot_index": 0
516
+ }
517
+ ],
518
+ "properties": {
519
+ "Node name for S&R": "MaskBlur+"
520
+ },
521
+ "widgets_values": [
522
+ 45,
523
+ 28.5
524
+ ]
525
+ },
526
+ {
527
+ "id": 16,
528
+ "type": "MaskFlip+",
529
+ "pos": [
530
+ 1690,
531
+ 270
532
+ ],
533
+ "size": {
534
+ "0": 310,
535
+ "1": 60
536
+ },
537
+ "flags": {},
538
+ "order": 10,
539
+ "mode": 0,
540
+ "inputs": [
541
+ {
542
+ "name": "mask",
543
+ "type": "MASK",
544
+ "link": 15
545
+ }
546
+ ],
547
+ "outputs": [
548
+ {
549
+ "name": "MASK",
550
+ "type": "MASK",
551
+ "links": [
552
+ 18
553
+ ],
554
+ "shape": 3,
555
+ "slot_index": 0
556
+ }
557
+ ],
558
+ "properties": {
559
+ "Node name for S&R": "MaskFlip+"
560
+ },
561
+ "widgets_values": [
562
+ "xy"
563
+ ]
564
+ },
565
+ {
566
+ "id": 13,
567
+ "type": "PreviewImage",
568
+ "pos": [
569
+ 1100,
570
+ 760
571
+ ],
572
+ "size": {
573
+ "0": 210,
574
+ "1": 246
575
+ },
576
+ "flags": {},
577
+ "order": 18,
578
+ "mode": 0,
579
+ "inputs": [
580
+ {
581
+ "name": "images",
582
+ "type": "IMAGE",
583
+ "link": 49
584
+ }
585
+ ],
586
+ "properties": {
587
+ "Node name for S&R": "PreviewImage"
588
+ }
589
+ },
590
+ {
591
+ "id": 37,
592
+ "type": "ImageDesaturate+",
593
+ "pos": [
594
+ 500,
595
+ 920
596
+ ],
597
+ "size": {
598
+ "0": 190,
599
+ "1": 30
600
+ },
601
+ "flags": {},
602
+ "order": 7,
603
+ "mode": 0,
604
+ "inputs": [
605
+ {
606
+ "name": "image",
607
+ "type": "IMAGE",
608
+ "link": 48
609
+ }
610
+ ],
611
+ "outputs": [
612
+ {
613
+ "name": "IMAGE",
614
+ "type": "IMAGE",
615
+ "links": [
616
+ 49
617
+ ],
618
+ "shape": 3,
619
+ "slot_index": 0
620
+ }
621
+ ],
622
+ "properties": {
623
+ "Node name for S&R": "ImageDesaturate+"
624
+ }
625
+ },
626
+ {
627
+ "id": 7,
628
+ "type": "LoadImage",
629
+ "pos": [
630
+ -90,
631
+ 650
632
+ ],
633
+ "size": {
634
+ "0": 315,
635
+ "1": 314
636
+ },
637
+ "flags": {},
638
+ "order": 0,
639
+ "mode": 0,
640
+ "outputs": [
641
+ {
642
+ "name": "IMAGE",
643
+ "type": "IMAGE",
644
+ "links": [
645
+ 1,
646
+ 4,
647
+ 6,
648
+ 8,
649
+ 22,
650
+ 48,
651
+ 57
652
+ ],
653
+ "shape": 3,
654
+ "slot_index": 0
655
+ },
656
+ {
657
+ "name": "MASK",
658
+ "type": "MASK",
659
+ "links": null,
660
+ "shape": 3
661
+ }
662
+ ],
663
+ "properties": {
664
+ "Node name for S&R": "LoadImage"
665
+ },
666
+ "widgets_values": [
667
+ "venere.jpg",
668
+ "image"
669
+ ]
670
+ },
671
+ {
672
+ "id": 11,
673
+ "type": "PreviewImage",
674
+ "pos": [
675
+ 1100,
676
+ 450
677
+ ],
678
+ "size": {
679
+ "0": 210,
680
+ "1": 246
681
+ },
682
+ "flags": {},
683
+ "order": 19,
684
+ "mode": 0,
685
+ "inputs": [
686
+ {
687
+ "name": "images",
688
+ "type": "IMAGE",
689
+ "link": 58
690
+ }
691
+ ],
692
+ "properties": {
693
+ "Node name for S&R": "PreviewImage"
694
+ }
695
+ },
696
+ {
697
+ "id": 40,
698
+ "type": "ImageCrop+",
699
+ "pos": [
700
+ 430,
701
+ 560
702
+ ],
703
+ "size": {
704
+ "0": 310,
705
+ "1": 194
706
+ },
707
+ "flags": {},
708
+ "order": 8,
709
+ "mode": 0,
710
+ "inputs": [
711
+ {
712
+ "name": "image",
713
+ "type": "IMAGE",
714
+ "link": 57
715
+ }
716
+ ],
717
+ "outputs": [
718
+ {
719
+ "name": "IMAGE",
720
+ "type": "IMAGE",
721
+ "links": [
722
+ 58
723
+ ],
724
+ "shape": 3,
725
+ "slot_index": 0
726
+ },
727
+ {
728
+ "name": "x",
729
+ "type": "INT",
730
+ "links": null,
731
+ "shape": 3
732
+ },
733
+ {
734
+ "name": "y",
735
+ "type": "INT",
736
+ "links": null,
737
+ "shape": 3
738
+ }
739
+ ],
740
+ "properties": {
741
+ "Node name for S&R": "ImageCrop+"
742
+ },
743
+ "widgets_values": [
744
+ 256,
745
+ 256,
746
+ "center",
747
+ 0,
748
+ 0
749
+ ]
750
+ },
751
+ {
752
+ "id": 20,
753
+ "type": "LoadImageMask",
754
+ "pos": [
755
+ 1400,
756
+ 260
757
+ ],
758
+ "size": {
759
+ "0": 220.70516967773438,
760
+ "1": 318
761
+ },
762
+ "flags": {},
763
+ "order": 1,
764
+ "mode": 0,
765
+ "outputs": [
766
+ {
767
+ "name": "MASK",
768
+ "type": "MASK",
769
+ "links": [
770
+ 14,
771
+ 15
772
+ ],
773
+ "shape": 3,
774
+ "slot_index": 0
775
+ }
776
+ ],
777
+ "properties": {
778
+ "Node name for S&R": "LoadImageMask"
779
+ },
780
+ "widgets_values": [
781
+ "cwf_inpaint_example_mask.png",
782
+ "alpha",
783
+ "image"
784
+ ]
785
+ },
786
+ {
787
+ "id": 21,
788
+ "type": "MaskPreview+",
789
+ "pos": [
790
+ 2100,
791
+ 380
792
+ ],
793
+ "size": {
794
+ "0": 210,
795
+ "1": 246
796
+ },
797
+ "flags": {},
798
+ "order": 21,
799
+ "mode": 0,
800
+ "inputs": [
801
+ {
802
+ "name": "mask",
803
+ "type": "MASK",
804
+ "link": 18
805
+ }
806
+ ],
807
+ "properties": {
808
+ "Node name for S&R": "MaskPreview+"
809
+ }
810
+ }
811
+ ],
812
+ "links": [
813
+ [
814
+ 1,
815
+ 7,
816
+ 0,
817
+ 1,
818
+ 0,
819
+ "IMAGE"
820
+ ],
821
+ [
822
+ 2,
823
+ 1,
824
+ 0,
825
+ 8,
826
+ 0,
827
+ "*"
828
+ ],
829
+ [
830
+ 3,
831
+ 1,
832
+ 1,
833
+ 9,
834
+ 0,
835
+ "*"
836
+ ],
837
+ [
838
+ 4,
839
+ 7,
840
+ 0,
841
+ 2,
842
+ 0,
843
+ "IMAGE"
844
+ ],
845
+ [
846
+ 6,
847
+ 7,
848
+ 0,
849
+ 4,
850
+ 0,
851
+ "IMAGE"
852
+ ],
853
+ [
854
+ 8,
855
+ 7,
856
+ 0,
857
+ 6,
858
+ 0,
859
+ "IMAGE"
860
+ ],
861
+ [
862
+ 9,
863
+ 2,
864
+ 0,
865
+ 10,
866
+ 0,
867
+ "IMAGE"
868
+ ],
869
+ [
870
+ 11,
871
+ 4,
872
+ 0,
873
+ 12,
874
+ 0,
875
+ "IMAGE"
876
+ ],
877
+ [
878
+ 13,
879
+ 6,
880
+ 0,
881
+ 14,
882
+ 0,
883
+ "IMAGE"
884
+ ],
885
+ [
886
+ 14,
887
+ 20,
888
+ 0,
889
+ 15,
890
+ 0,
891
+ "MASK"
892
+ ],
893
+ [
894
+ 15,
895
+ 20,
896
+ 0,
897
+ 16,
898
+ 0,
899
+ "MASK"
900
+ ],
901
+ [
902
+ 18,
903
+ 16,
904
+ 0,
905
+ 21,
906
+ 0,
907
+ "MASK"
908
+ ],
909
+ [
910
+ 19,
911
+ 15,
912
+ 0,
913
+ 18,
914
+ 0,
915
+ "MASK"
916
+ ],
917
+ [
918
+ 22,
919
+ 7,
920
+ 0,
921
+ 27,
922
+ 0,
923
+ "IMAGE"
924
+ ],
925
+ [
926
+ 23,
927
+ 27,
928
+ 0,
929
+ 28,
930
+ 0,
931
+ "IMAGE"
932
+ ],
933
+ [
934
+ 44,
935
+ 2,
936
+ 1,
937
+ 36,
938
+ 0,
939
+ "INT,FLOAT"
940
+ ],
941
+ [
942
+ 45,
943
+ 2,
944
+ 2,
945
+ 36,
946
+ 1,
947
+ "INT,FLOAT"
948
+ ],
949
+ [
950
+ 46,
951
+ 36,
952
+ 0,
953
+ 23,
954
+ 0,
955
+ "*"
956
+ ],
957
+ [
958
+ 48,
959
+ 7,
960
+ 0,
961
+ 37,
962
+ 0,
963
+ "IMAGE"
964
+ ],
965
+ [
966
+ 49,
967
+ 37,
968
+ 0,
969
+ 13,
970
+ 0,
971
+ "IMAGE"
972
+ ],
973
+ [
974
+ 57,
975
+ 7,
976
+ 0,
977
+ 40,
978
+ 0,
979
+ "IMAGE"
980
+ ],
981
+ [
982
+ 58,
983
+ 40,
984
+ 0,
985
+ 11,
986
+ 0,
987
+ "IMAGE"
988
+ ]
989
+ ],
990
+ "groups": [],
991
+ "config": {},
992
+ "extra": {},
993
+ "version": 0.4
994
+ }
comfyui_controlnet_aux/README.md ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ComfyUI's ControlNet Auxiliary Preprocessors
2
+ Plug-and-play [ComfyUI](https://github.com/comfyanonymous/ComfyUI) node sets for making [ControlNet](https://github.com/lllyasviel/ControlNet/) hint images
3
+
4
+ "anime style, a protest in the street, cyberpunk city, a woman with pink hair and golden eyes (looking at the viewer) is holding a sign with the text "ComfyUI ControlNet Aux" in bold, neon pink" on Flux.1 Dev
5
+
6
+ ![](./examples/CNAuxBanner.jpg)
7
+
8
+ The code is copy-pasted from the respective folders in https://github.com/lllyasviel/ControlNet/tree/main/annotator and connected to [the 🤗 Hub](https://huggingface.co/lllyasviel/Annotators).
9
+
10
+ All credit & copyright goes to https://github.com/lllyasviel.
11
+
12
+ # Updates
13
+ Go to [Update page](./UPDATES.md) to follow updates
14
+
15
+ # Installation:
16
+ ## Using ComfyUI Manager (recommended):
17
+ Install [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) and do steps introduced there to install this repo.
18
+
19
+ ## Alternative:
20
+ If you're running on Linux, or non-admin account on windows you'll want to ensure `/ComfyUI/custom_nodes` and `comfyui_controlnet_aux` has write permissions.
21
+
22
+ There is now a **install.bat** you can run to install to portable if detected. Otherwise it will default to system and assume you followed ConfyUI's manual installation steps.
23
+
24
+ If you can't run **install.bat** (e.g. you are a Linux user). Open the CMD/Shell and do the following:
25
+ - Navigate to your `/ComfyUI/custom_nodes/` folder
26
+ - Run `git clone https://github.com/Fannovel16/comfyui_controlnet_aux/`
27
+ - Navigate to your `comfyui_controlnet_aux` folder
28
+ - Portable/venv:
29
+ - Run `path/to/ComfUI/python_embeded/python.exe -s -m pip install -r requirements.txt`
30
+ - With system python
31
+ - Run `pip install -r requirements.txt`
32
+ - Start ComfyUI
33
+
34
+ # Nodes
35
+ Please note that this repo only supports preprocessors making hint images (e.g. stickman, canny edge, etc).
36
+ All preprocessors except Inpaint are intergrated into `AIO Aux Preprocessor` node.
37
+ This node allow you to quickly get the preprocessor but a preprocessor's own threshold parameters won't be able to set.
38
+ You need to use its node directly to set thresholds.
39
+
40
+ # Nodes (sections are categories in Comfy menu)
41
+ ## Line Extractors
42
+ | Preprocessor Node | sd-webui-controlnet/other | ControlNet/T2I-Adapter |
43
+ |-----------------------------|---------------------------|-------------------------------------------|
44
+ | Binary Lines | binary | control_scribble |
45
+ | Canny Edge | canny | control_v11p_sd15_canny <br> control_canny <br> t2iadapter_canny |
46
+ | HED Soft-Edge Lines | hed | control_v11p_sd15_softedge <br> control_hed |
47
+ | Standard Lineart | standard_lineart | control_v11p_sd15_lineart |
48
+ | Realistic Lineart | lineart (or `lineart_coarse` if `coarse` is enabled) | control_v11p_sd15_lineart |
49
+ | Anime Lineart | lineart_anime | control_v11p_sd15s2_lineart_anime |
50
+ | Manga Lineart | lineart_anime_denoise | control_v11p_sd15s2_lineart_anime |
51
+ | M-LSD Lines | mlsd | control_v11p_sd15_mlsd <br> control_mlsd |
52
+ | PiDiNet Soft-Edge Lines | pidinet | control_v11p_sd15_softedge <br> control_scribble |
53
+ | Scribble Lines | scribble | control_v11p_sd15_scribble <br> control_scribble |
54
+ | Scribble XDoG Lines | scribble_xdog | control_v11p_sd15_scribble <br> control_scribble |
55
+ | Fake Scribble Lines | scribble_hed | control_v11p_sd15_scribble <br> control_scribble |
56
+ | TEED Soft-Edge Lines | teed | [controlnet-sd-xl-1.0-softedge-dexined](https://huggingface.co/SargeZT/controlnet-sd-xl-1.0-softedge-dexined/blob/main/controlnet-sd-xl-1.0-softedge-dexined.safetensors) <br> control_v11p_sd15_softedge (Theoretically)
57
+ | Scribble PiDiNet Lines | scribble_pidinet | control_v11p_sd15_scribble <br> control_scribble |
58
+ | AnyLine Lineart | | mistoLine_fp16.safetensors <br> mistoLine_rank256 <br> control_v11p_sd15s2_lineart_anime <br> control_v11p_sd15_lineart |
59
+
60
+ ## Normal and Depth Estimators
61
+ | Preprocessor Node | sd-webui-controlnet/other | ControlNet/T2I-Adapter |
62
+ |-----------------------------|---------------------------|-------------------------------------------|
63
+ | MiDaS Depth Map | (normal) depth | control_v11f1p_sd15_depth <br> control_depth <br> t2iadapter_depth |
64
+ | LeReS Depth Map | depth_leres | control_v11f1p_sd15_depth <br> control_depth <br> t2iadapter_depth |
65
+ | Zoe Depth Map | depth_zoe | control_v11f1p_sd15_depth <br> control_depth <br> t2iadapter_depth |
66
+ | MiDaS Normal Map | normal_map | control_normal |
67
+ | BAE Normal Map | normal_bae | control_v11p_sd15_normalbae |
68
+ | MeshGraphormer Hand Refiner ([HandRefinder](https://github.com/wenquanlu/HandRefiner)) | depth_hand_refiner | [control_sd15_inpaint_depth_hand_fp16](https://huggingface.co/hr16/ControlNet-HandRefiner-pruned/blob/main/control_sd15_inpaint_depth_hand_fp16.safetensors) |
69
+ | Depth Anything | depth_anything | [Depth-Anything](https://huggingface.co/spaces/LiheYoung/Depth-Anything/blob/main/checkpoints_controlnet/diffusion_pytorch_model.safetensors) |
70
+ | Zoe Depth Anything <br> (Basically Zoe but the encoder is replaced with DepthAnything) | depth_anything | [Depth-Anything](https://huggingface.co/spaces/LiheYoung/Depth-Anything/blob/main/checkpoints_controlnet/diffusion_pytorch_model.safetensors) |
71
+ | Normal DSINE | | control_normal/control_v11p_sd15_normalbae |
72
+ | Metric3D Depth | | control_v11f1p_sd15_depth <br> control_depth <br> t2iadapter_depth |
73
+ | Metric3D Normal | | control_v11p_sd15_normalbae |
74
+ | Depth Anything V2 | | [Depth-Anything](https://huggingface.co/spaces/LiheYoung/Depth-Anything/blob/main/checkpoints_controlnet/diffusion_pytorch_model.safetensors) |
75
+
76
+ ## Faces and Poses Estimators
77
+ | Preprocessor Node | sd-webui-controlnet/other | ControlNet/T2I-Adapter |
78
+ |-----------------------------|---------------------------|-------------------------------------------|
79
+ | DWPose Estimator | dw_openpose_full | control_v11p_sd15_openpose <br> control_openpose <br> t2iadapter_openpose |
80
+ | OpenPose Estimator | openpose (detect_body) <br> openpose_hand (detect_body + detect_hand) <br> openpose_faceonly (detect_face) <br> openpose_full (detect_hand + detect_body + detect_face) | control_v11p_sd15_openpose <br> control_openpose <br> t2iadapter_openpose |
81
+ | MediaPipe Face Mesh | mediapipe_face | controlnet_sd21_laion_face_v2 |
82
+ | Animal Estimator | animal_openpose | [control_sd15_animal_openpose_fp16](https://huggingface.co/huchenlei/animal_openpose/blob/main/control_sd15_animal_openpose_fp16.pth) |
83
+
84
+ ## Optical Flow Estimators
85
+ | Preprocessor Node | sd-webui-controlnet/other | ControlNet/T2I-Adapter |
86
+ |-----------------------------|---------------------------|-------------------------------------------|
87
+ | Unimatch Optical Flow | | [DragNUWA](https://github.com/ProjectNUWA/DragNUWA) |
88
+
89
+ ### How to get OpenPose-format JSON?
90
+ #### User-side
91
+ This workflow will save images to ComfyUI's output folder (the same location as output images). If you haven't found `Save Pose Keypoints` node, update this extension
92
+ ![](./examples/example_save_kps.png)
93
+
94
+ #### Dev-side
95
+ An array of [OpenPose-format JSON](https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/02_output.md#json-output-format) corresponsding to each frame in an IMAGE batch can be gotten from DWPose and OpenPose using `app.nodeOutputs` on the UI or `/history` API endpoint. JSON output from AnimalPose uses a kinda similar format to OpenPose JSON:
96
+ ```
97
+ [
98
+ {
99
+ "version": "ap10k",
100
+ "animals": [
101
+ [[x1, y1, 1], [x2, y2, 1],..., [x17, y17, 1]],
102
+ [[x1, y1, 1], [x2, y2, 1],..., [x17, y17, 1]],
103
+ ...
104
+ ],
105
+ "canvas_height": 512,
106
+ "canvas_width": 768
107
+ },
108
+ ...
109
+ ]
110
+ ```
111
+
112
+ For extension developers (e.g. Openpose editor):
113
+ ```js
114
+ const poseNodes = app.graph._nodes.filter(node => ["OpenposePreprocessor", "DWPreprocessor", "AnimalPosePreprocessor"].includes(node.type))
115
+ for (const poseNode of poseNodes) {
116
+ const openposeResults = JSON.parse(app.nodeOutputs[poseNode.id].openpose_json[0])
117
+ console.log(openposeResults) //An array containing Openpose JSON for each frame
118
+ }
119
+ ```
120
+
121
+ For API users:
122
+ Javascript
123
+ ```js
124
+ import fetch from "node-fetch" //Remember to add "type": "module" to "package.json"
125
+ async function main() {
126
+ const promptId = '792c1905-ecfe-41f4-8114-83e6a4a09a9f' //Too lazy to POST /queue
127
+ let history = await fetch(`http://127.0.0.1:8188/history/${promptId}`).then(re => re.json())
128
+ history = history[promptId]
129
+ const nodeOutputs = Object.values(history.outputs).filter(output => output.openpose_json)
130
+ for (const nodeOutput of nodeOutputs) {
131
+ const openposeResults = JSON.parse(nodeOutput.openpose_json[0])
132
+ console.log(openposeResults) //An array containing Openpose JSON for each frame
133
+ }
134
+ }
135
+ main()
136
+ ```
137
+
138
+ Python
139
+ ```py
140
+ import json, urllib.request
141
+
142
+ server_address = "127.0.0.1:8188"
143
+ prompt_id = '' #Too lazy to POST /queue
144
+
145
+ def get_history(prompt_id):
146
+ with urllib.request.urlopen("http://{}/history/{}".format(server_address, prompt_id)) as response:
147
+ return json.loads(response.read())
148
+
149
+ history = get_history(prompt_id)[prompt_id]
150
+ for o in history['outputs']:
151
+ for node_id in history['outputs']:
152
+ node_output = history['outputs'][node_id]
153
+ if 'openpose_json' in node_output:
154
+ print(json.loads(node_output['openpose_json'][0])) #An list containing Openpose JSON for each frame
155
+ ```
156
+ ## Semantic Segmentation
157
+ | Preprocessor Node | sd-webui-controlnet/other | ControlNet/T2I-Adapter |
158
+ |-----------------------------|---------------------------|-------------------------------------------|
159
+ | OneFormer ADE20K Segmentor | oneformer_ade20k | control_v11p_sd15_seg |
160
+ | OneFormer COCO Segmentor | oneformer_coco | control_v11p_sd15_seg |
161
+ | UniFormer Segmentor | segmentation |control_sd15_seg <br> control_v11p_sd15_seg|
162
+
163
+ ## T2IAdapter-only
164
+ | Preprocessor Node | sd-webui-controlnet/other | ControlNet/T2I-Adapter |
165
+ |-----------------------------|---------------------------|-------------------------------------------|
166
+ | Color Pallete | color | t2iadapter_color |
167
+ | Content Shuffle | shuffle | t2iadapter_style |
168
+
169
+ ## Recolor
170
+ | Preprocessor Node | sd-webui-controlnet/other | ControlNet/T2I-Adapter |
171
+ |-----------------------------|---------------------------|-------------------------------------------|
172
+ | Image Luminance | recolor_luminance | [ioclab_sd15_recolor](https://huggingface.co/lllyasviel/sd_control_collection/resolve/main/ioclab_sd15_recolor.safetensors) <br> [sai_xl_recolor_256lora](https://huggingface.co/lllyasviel/sd_control_collection/resolve/main/sai_xl_recolor_256lora.safetensors) <br> [bdsqlsz_controlllite_xl_recolor_luminance](https://huggingface.co/bdsqlsz/qinglong_controlnet-lllite/resolve/main/bdsqlsz_controlllite_xl_recolor_luminance.safetensors) |
173
+ | Image Intensity | recolor_intensity | Idk. Maybe same as above? |
174
+
175
+ # Examples
176
+ > A picture is worth a thousand words
177
+
178
+ ![](./examples/ExecuteAll1.jpg)
179
+ ![](./examples/ExecuteAll2.jpg)
180
+
181
+ # Testing workflow
182
+ https://github.com/Fannovel16/comfyui_controlnet_aux/blob/main/examples/ExecuteAll.png
183
+ Input image: https://github.com/Fannovel16/comfyui_controlnet_aux/blob/main/examples/comfyui-controlnet-aux-logo.png
184
+
185
+ # Q&A:
186
+ ## Why some nodes doesn't appear after I installed this repo?
187
+
188
+ This repo has a new mechanism which will skip any custom node can't be imported. If you meet this case, please create a issue on [Issues tab](https://github.com/Fannovel16/comfyui_controlnet_aux/issues) with the log from the command line.
189
+
190
+ ## DWPose/AnimalPose only uses CPU so it's so slow. How can I make it use GPU?
191
+ There are two ways to speed-up DWPose: using TorchScript checkpoints (.torchscript.pt) checkpoints or ONNXRuntime (.onnx). TorchScript way is little bit slower than ONNXRuntime but doesn't require any additional library and still way way faster than CPU.
192
+
193
+ A torchscript bbox detector is compatiable with an onnx pose estimator and vice versa.
194
+ ### TorchScript
195
+ Set `bbox_detector` and `pose_estimator` according to this picture. You can try other bbox detector endings with `.torchscript.pt` to reduce bbox detection time if input images are ideal.
196
+ ![](./examples/example_torchscript.png)
197
+ ### ONNXRuntime
198
+ If onnxruntime is installed successfully and the checkpoint used endings with `.onnx`, it will replace default cv2 backend to take advantage of GPU. Note that if you are using NVidia card, this method currently can only works on CUDA 11.8 (ComfyUI_windows_portable_nvidia_cu118_or_cpu.7z) unless you compile onnxruntime yourself.
199
+
200
+ 1. Know your onnxruntime build:
201
+ * * NVidia CUDA 11.x or bellow/AMD GPU: `onnxruntime-gpu`
202
+ * * NVidia CUDA 12.x: `onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/`
203
+ * * DirectML: `onnxruntime-directml`
204
+ * * OpenVINO: `onnxruntime-openvino`
205
+
206
+ Note that if this is your first time using ComfyUI, please test if it can run on your device before doing next steps.
207
+
208
+ 2. Add it into `requirements.txt`
209
+
210
+ 3. Run `install.bat` or pip command mentioned in Installation
211
+
212
+ ![](./examples/example_onnx.png)
213
+
214
+ # Assets files of preprocessors
215
+ * anime_face_segment: [bdsqlsz/qinglong_controlnet-lllite/Annotators/UNet.pth](https://huggingface.co/bdsqlsz/qinglong_controlnet-lllite/blob/main/Annotators/UNet.pth), [anime-seg/isnetis.ckpt](https://huggingface.co/skytnt/anime-seg/blob/main/isnetis.ckpt)
216
+ * densepose: [LayerNorm/DensePose-TorchScript-with-hint-image/densepose_r50_fpn_dl.torchscript](https://huggingface.co/LayerNorm/DensePose-TorchScript-with-hint-image/blob/main/densepose_r50_fpn_dl.torchscript)
217
+ * dwpose:
218
+ * * bbox_detector: Either [yzd-v/DWPose/yolox_l.onnx](https://huggingface.co/yzd-v/DWPose/blob/main/yolox_l.onnx), [hr16/yolox-onnx/yolox_l.torchscript.pt](https://huggingface.co/hr16/yolox-onnx/blob/main/yolox_l.torchscript.pt), [hr16/yolo-nas-fp16/yolo_nas_l_fp16.onnx](https://huggingface.co/hr16/yolo-nas-fp16/blob/main/yolo_nas_l_fp16.onnx), [hr16/yolo-nas-fp16/yolo_nas_m_fp16.onnx](https://huggingface.co/hr16/yolo-nas-fp16/blob/main/yolo_nas_m_fp16.onnx), [hr16/yolo-nas-fp16/yolo_nas_s_fp16.onnx](https://huggingface.co/hr16/yolo-nas-fp16/blob/main/yolo_nas_s_fp16.onnx)
219
+ * * pose_estimator: Either [hr16/DWPose-TorchScript-BatchSize5/dw-ll_ucoco_384_bs5.torchscript.pt](https://huggingface.co/hr16/DWPose-TorchScript-BatchSize5/blob/main/dw-ll_ucoco_384_bs5.torchscript.pt), [yzd-v/DWPose/dw-ll_ucoco_384.onnx](https://huggingface.co/yzd-v/DWPose/blob/main/dw-ll_ucoco_384.onnx)
220
+ * animal_pose (ap10k):
221
+ * * bbox_detector: Either [yzd-v/DWPose/yolox_l.onnx](https://huggingface.co/yzd-v/DWPose/blob/main/yolox_l.onnx), [hr16/yolox-onnx/yolox_l.torchscript.pt](https://huggingface.co/hr16/yolox-onnx/blob/main/yolox_l.torchscript.pt), [hr16/yolo-nas-fp16/yolo_nas_l_fp16.onnx](https://huggingface.co/hr16/yolo-nas-fp16/blob/main/yolo_nas_l_fp16.onnx), [hr16/yolo-nas-fp16/yolo_nas_m_fp16.onnx](https://huggingface.co/hr16/yolo-nas-fp16/blob/main/yolo_nas_m_fp16.onnx), [hr16/yolo-nas-fp16/yolo_nas_s_fp16.onnx](https://huggingface.co/hr16/yolo-nas-fp16/blob/main/yolo_nas_s_fp16.onnx)
222
+ * * pose_estimator: Either [hr16/DWPose-TorchScript-BatchSize5/rtmpose-m_ap10k_256_bs5.torchscript.pt](https://huggingface.co/hr16/DWPose-TorchScript-BatchSize5/blob/main/rtmpose-m_ap10k_256_bs5.torchscript.pt), [hr16/UnJIT-DWPose/rtmpose-m_ap10k_256.onnx](https://huggingface.co/hr16/UnJIT-DWPose/blob/main/rtmpose-m_ap10k_256.onnx)
223
+ * hed: [lllyasviel/Annotators/ControlNetHED.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/ControlNetHED.pth)
224
+ * leres: [lllyasviel/Annotators/res101.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/res101.pth), [lllyasviel/Annotators/latest_net_G.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/latest_net_G.pth)
225
+ * lineart: [lllyasviel/Annotators/sk_model.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/sk_model.pth), [lllyasviel/Annotators/sk_model2.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/sk_model2.pth)
226
+ * lineart_anime: [lllyasviel/Annotators/netG.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/netG.pth)
227
+ * manga_line: [lllyasviel/Annotators/erika.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/erika.pth)
228
+ * mesh_graphormer: [hr16/ControlNet-HandRefiner-pruned/graphormer_hand_state_dict.bin](https://huggingface.co/hr16/ControlNet-HandRefiner-pruned/blob/main/graphormer_hand_state_dict.bin), [hr16/ControlNet-HandRefiner-pruned/hrnetv2_w64_imagenet_pretrained.pth](https://huggingface.co/hr16/ControlNet-HandRefiner-pruned/blob/main/hrnetv2_w64_imagenet_pretrained.pth)
229
+ * midas: [lllyasviel/Annotators/dpt_hybrid-midas-501f0c75.pt](https://huggingface.co/lllyasviel/Annotators/blob/main/dpt_hybrid-midas-501f0c75.pt)
230
+ * mlsd: [lllyasviel/Annotators/mlsd_large_512_fp32.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/mlsd_large_512_fp32.pth)
231
+ * normalbae: [lllyasviel/Annotators/scannet.pt](https://huggingface.co/lllyasviel/Annotators/blob/main/scannet.pt)
232
+ * oneformer: [lllyasviel/Annotators/250_16_swin_l_oneformer_ade20k_160k.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/250_16_swin_l_oneformer_ade20k_160k.pth)
233
+ * open_pose: [lllyasviel/Annotators/body_pose_model.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/body_pose_model.pth), [lllyasviel/Annotators/hand_pose_model.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/hand_pose_model.pth), [lllyasviel/Annotators/facenet.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/facenet.pth)
234
+ * pidi: [lllyasviel/Annotators/table5_pidinet.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/table5_pidinet.pth)
235
+ * sam: [dhkim2810/MobileSAM/mobile_sam.pt](https://huggingface.co/dhkim2810/MobileSAM/blob/main/mobile_sam.pt)
236
+ * uniformer: [lllyasviel/Annotators/upernet_global_small.pth](https://huggingface.co/lllyasviel/Annotators/blob/main/upernet_global_small.pth)
237
+ * zoe: [lllyasviel/Annotators/ZoeD_M12_N.pt](https://huggingface.co/lllyasviel/Annotators/blob/main/ZoeD_M12_N.pt)
238
+ * teed: [bdsqlsz/qinglong_controlnet-lllite/7_model.pth](https://huggingface.co/bdsqlsz/qinglong_controlnet-lllite/blob/main/Annotators/7_model.pth)
239
+ * depth_anything: Either [LiheYoung/Depth-Anything/checkpoints/depth_anything_vitl14.pth](https://huggingface.co/spaces/LiheYoung/Depth-Anything/blob/main/checkpoints/depth_anything_vitl14.pth), [LiheYoung/Depth-Anything/checkpoints/depth_anything_vitb14.pth](https://huggingface.co/spaces/LiheYoung/Depth-Anything/blob/main/checkpoints/depth_anything_vitb14.pth) or [LiheYoung/Depth-Anything/checkpoints/depth_anything_vits14.pth](https://huggingface.co/spaces/LiheYoung/Depth-Anything/blob/main/checkpoints/depth_anything_vits14.pth)
240
+ * diffusion_edge: Either [hr16/Diffusion-Edge/diffusion_edge_indoor.pt](https://huggingface.co/hr16/Diffusion-Edge/blob/main/diffusion_edge_indoor.pt), [hr16/Diffusion-Edge/diffusion_edge_urban.pt](https://huggingface.co/hr16/Diffusion-Edge/blob/main/diffusion_edge_urban.pt) or [hr16/Diffusion-Edge/diffusion_edge_natrual.pt](https://huggingface.co/hr16/Diffusion-Edge/blob/main/diffusion_edge_natrual.pt)
241
+ * unimatch: Either [hr16/Unimatch/gmflow-scale2-regrefine6-mixdata.pth](https://huggingface.co/hr16/Unimatch/blob/main/gmflow-scale2-regrefine6-mixdata.pth), [hr16/Unimatch/gmflow-scale2-mixdata.pth](https://huggingface.co/hr16/Unimatch/blob/main/gmflow-scale2-mixdata.pth) or [hr16/Unimatch/gmflow-scale1-mixdata.pth](https://huggingface.co/hr16/Unimatch/blob/main/gmflow-scale1-mixdata.pth)
242
+ * zoe_depth_anything: Either [LiheYoung/Depth-Anything/checkpoints_metric_depth/depth_anything_metric_depth_indoor.pt](https://huggingface.co/spaces/LiheYoung/Depth-Anything/blob/main/checkpoints_metric_depth/depth_anything_metric_depth_indoor.pt) or [LiheYoung/Depth-Anything/checkpoints_metric_depth/depth_anything_metric_depth_outdoor.pt](https://huggingface.co/spaces/LiheYoung/Depth-Anything/blob/main/checkpoints_metric_depth/depth_anything_metric_depth_outdoor.pt)
243
+ # 2000 Stars 😄
244
+ <a href="https://star-history.com/#Fannovel16/comfyui_controlnet_aux&Date">
245
+ <picture>
246
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Fannovel16/comfyui_controlnet_aux&type=Date&theme=dark" />
247
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Fannovel16/comfyui_controlnet_aux&type=Date" />
248
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Fannovel16/comfyui_controlnet_aux&type=Date" />
249
+ </picture>
250
+ </a>
251
+
252
+ Thanks for yalls supports. I never thought the graph for stars would be linear lol.
comfyui_controlnet_aux/UPDATES.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * `AIO Aux Preprocessor` intergrating all loadable aux preprocessors as dropdown options. Easy to copy, paste and get the preprocessor faster.
2
+ * Added OpenPose-format JSON output from OpenPose Preprocessor and DWPose Preprocessor. Checks [here](#faces-and-poses).
3
+ * Fixed wrong model path when downloading DWPose.
4
+ * Make hint images less blurry.
5
+ * Added `resolution` option, `PixelPerfectResolution` and `HintImageEnchance` nodes (TODO: Documentation).
6
+ * Added `RAFT Optical Flow Embedder` for TemporalNet2 (TODO: Workflow example).
7
+ * Fixed opencv's conflicts between this extension, [ReActor](https://github.com/Gourieff/comfyui-reactor-node) and Roop. Thanks `Gourieff` for [the solution](https://github.com/Fannovel16/comfyui_controlnet_aux/issues/7#issuecomment-1734319075)!
8
+ * RAFT is removed as the code behind it doesn't match what what the original code does
9
+ * Changed `lineart`'s display name from `Normal Lineart` to `Realistic Lineart`. This change won't affect old workflows
10
+ * Added support for `onnxruntime` to speed-up DWPose (see the Q&A)
11
+ * Fixed TypeError: expected size to be one of int or Tuple[int] or Tuple[int, int] or Tuple[int, int, int], but got size with types [<class 'numpy.int64'>, <class 'numpy.int64'>]: [Issue](https://github.com/Fannovel16/comfyui_controlnet_aux/issues/2), [PR](https://github.com/Fannovel16/comfyui_controlnet_aux/pull/71))
12
+ * Fixed ImageGenResolutionFromImage mishape (https://github.com/Fannovel16/comfyui_controlnet_aux/pull/74)
13
+ * Fixed LeRes and MiDaS's incomatipility with MPS device
14
+ * Fixed checking DWPose onnxruntime session multiple times: https://github.com/Fannovel16/comfyui_controlnet_aux/issues/89)
15
+ * Added `Anime Face Segmentor` (in `ControlNet Preprocessors/Semantic Segmentation`) for [ControlNet AnimeFaceSegmentV2](https://huggingface.co/bdsqlsz/qinglong_controlnet-lllite#animefacesegmentv2). Checks [here](#anime-face-segmentor)
16
+ * Change download functions and fix [download error](https://github.com/Fannovel16/comfyui_controlnet_aux/issues/39): [PR](https://github.com/Fannovel16/comfyui_controlnet_aux/pull/96)
17
+ * Caching DWPose Onnxruntime during the first use of DWPose node instead of ComfyUI startup
18
+ * Added alternative YOLOX models for faster speed when using DWPose
19
+ * Added alternative DWPose models
20
+ * Implemented the preprocessor for [AnimalPose ControlNet](https://github.com/abehonest/ControlNet_AnimalPose/tree/main). Check [Animal Pose AP-10K](#animal-pose-ap-10k)
21
+ * Added YOLO-NAS models which are drop-in replacements of YOLOX
22
+ * Fixed Openpose Face/Hands no longer detecting: https://github.com/Fannovel16/comfyui_controlnet_aux/issues/54
23
+ * Added TorchScript implementation of DWPose and AnimalPose
24
+ * Added TorchScript implementation of DensePose from [Colab notebook](https://colab.research.google.com/drive/16hcaaKs210ivpxjoyGNuvEXZD4eqOOSQ) which doesn't require detectron2. [Example](#densepose). Thanks [@LayerNome](https://github.com/Layer-norm) for fixing bugs related.
25
+ * Added Standard Lineart Preprocessor
26
+ * Fixed OpenPose misplacements in some cases
27
+ * Added Mesh Graphormer - Hand Depth Map & Mask
28
+ * Misaligned hands bug from MeshGraphormer was fixed
29
+ * Added more mask options for MeshGraphormer
30
+ * Added Save Pose Keypoint node for editing
31
+ * Added Unimatch Optical Flow
32
+ * Added Depth Anything & Zoe Depth Anything
33
+ * Removed resolution field from Unimatch Optical Flow as that interpolating optical flow seems unstable
34
+ * Added TEED Soft-Edge Preprocessor
35
+ * Added DiffusionEdge
36
+ * Added Image Luminance and Image Intensity
37
+ * Added Normal DSINE
38
+ * Added TTPlanet Tile (09/05/2024, DD/MM/YYYY)
39
+ * Added AnyLine, Metric3D (18/05/2024)
40
+ * Added Depth Anything V2 (16/06/2024)
41
+ * Added Union model of ControlNet and preprocessors
42
+ ![345832280-edf41dab-7619-494c-9f60-60ec1f8789cb](https://github.com/user-attachments/assets/aa55f57c-cad7-48e6-84d3-8f506d847989)
43
+ * Refactor INPUT_TYPES and add Execute All node during the process of learning [Execution Model Inversion](https://github.com/comfyanonymous/ComfyUI/pull/2666)
44
+ * Added scale_stick_for_xinsr_cn (https://github.com/Fannovel16/comfyui_controlnet_aux/issues/447) (09/04/2024)
comfyui_controlnet_aux/__init__.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys, os
2
+ from .utils import here, define_preprocessor_inputs, INPUT
3
+ from pathlib import Path
4
+ import traceback
5
+ import importlib
6
+ from .log import log, blue_text, cyan_text, get_summary, get_label
7
+ from .hint_image_enchance import NODE_CLASS_MAPPINGS as HIE_NODE_CLASS_MAPPINGS
8
+ from .hint_image_enchance import NODE_DISPLAY_NAME_MAPPINGS as HIE_NODE_DISPLAY_NAME_MAPPINGS
9
+ #Ref: https://github.com/comfyanonymous/ComfyUI/blob/76d53c4622fc06372975ed2a43ad345935b8a551/nodes.py#L17
10
+ sys.path.insert(0, str(Path(here, "src").resolve()))
11
+ for pkg_name in ["custom_controlnet_aux", "custom_mmpkg"]:
12
+ sys.path.append(str(Path(here, "src", pkg_name).resolve()))
13
+
14
+ #Enable CPU fallback for ops not being supported by MPS like upsample_bicubic2d.out
15
+ #https://github.com/pytorch/pytorch/issues/77764
16
+ #https://github.com/Fannovel16/comfyui_controlnet_aux/issues/2#issuecomment-1763579485
17
+ os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = os.getenv("PYTORCH_ENABLE_MPS_FALLBACK", '1')
18
+
19
+
20
+ def load_nodes():
21
+ shorted_errors = []
22
+ full_error_messages = []
23
+ node_class_mappings = {}
24
+ node_display_name_mappings = {}
25
+
26
+ for filename in (here / "node_wrappers").iterdir():
27
+ module_name = filename.stem
28
+ if module_name.startswith('.'): continue #Skip hidden files created by the OS (e.g. [.DS_Store](https://en.wikipedia.org/wiki/.DS_Store))
29
+ try:
30
+ module = importlib.import_module(
31
+ f".node_wrappers.{module_name}", package=__package__
32
+ )
33
+ node_class_mappings.update(getattr(module, "NODE_CLASS_MAPPINGS"))
34
+ if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"):
35
+ node_display_name_mappings.update(getattr(module, "NODE_DISPLAY_NAME_MAPPINGS"))
36
+
37
+ log.debug(f"Imported {module_name} nodes")
38
+
39
+ except AttributeError:
40
+ pass # wip nodes
41
+ except Exception:
42
+ error_message = traceback.format_exc()
43
+ full_error_messages.append(error_message)
44
+ error_message = error_message.splitlines()[-1]
45
+ shorted_errors.append(
46
+ f"Failed to import module {module_name} because {error_message}"
47
+ )
48
+
49
+ if len(shorted_errors) > 0:
50
+ full_err_log = '\n\n'.join(full_error_messages)
51
+ print(f"\n\nFull error log from comfyui_controlnet_aux: \n{full_err_log}\n\n")
52
+ log.info(
53
+ f"Some nodes failed to load:\n\t"
54
+ + "\n\t".join(shorted_errors)
55
+ + "\n\n"
56
+ + "Check that you properly installed the dependencies.\n"
57
+ + "If you think this is a bug, please report it on the github page (https://github.com/Fannovel16/comfyui_controlnet_aux/issues)"
58
+ )
59
+ return node_class_mappings, node_display_name_mappings
60
+
61
+ AUX_NODE_MAPPINGS, AUX_DISPLAY_NAME_MAPPINGS = load_nodes()
62
+
63
+ #For nodes not mapping image to image or has special requirements
64
+ AIO_NOT_SUPPORTED = ["InpaintPreprocessor", "MeshGraphormer+ImpactDetector-DepthMapPreprocessor", "DiffusionEdge_Preprocessor"]
65
+ AIO_NOT_SUPPORTED += ["SavePoseKpsAsJsonFile", "FacialPartColoringFromPoseKps", "UpperBodyTrackingFromPoseKps", "RenderPeopleKps", "RenderAnimalKps"]
66
+ AIO_NOT_SUPPORTED += ["Unimatch_OptFlowPreprocessor", "MaskOptFlow"]
67
+
68
+ def preprocessor_options():
69
+ auxs = list(AUX_NODE_MAPPINGS.keys())
70
+ auxs.insert(0, "none")
71
+ for name in AIO_NOT_SUPPORTED:
72
+ if name in auxs:
73
+ auxs.remove(name)
74
+ return auxs
75
+
76
+
77
+ PREPROCESSOR_OPTIONS = preprocessor_options()
78
+
79
+ class AIO_Preprocessor:
80
+ @classmethod
81
+ def INPUT_TYPES(s):
82
+ return define_preprocessor_inputs(
83
+ preprocessor=INPUT.COMBO(PREPROCESSOR_OPTIONS, default="none"),
84
+ resolution=INPUT.RESOLUTION()
85
+ )
86
+
87
+ RETURN_TYPES = ("IMAGE",)
88
+ FUNCTION = "execute"
89
+
90
+ CATEGORY = "ControlNet Preprocessors"
91
+
92
+ def execute(self, preprocessor, image, resolution=512):
93
+ if preprocessor == "none":
94
+ return (image, )
95
+ else:
96
+ aux_class = AUX_NODE_MAPPINGS[preprocessor]
97
+ input_types = aux_class.INPUT_TYPES()
98
+ input_types = {
99
+ **input_types["required"],
100
+ **(input_types["optional"] if "optional" in input_types else {})
101
+ }
102
+ params = {}
103
+ for name, input_type in input_types.items():
104
+ if name == "image":
105
+ params[name] = image
106
+ continue
107
+
108
+ if name == "resolution":
109
+ params[name] = resolution
110
+ continue
111
+
112
+ if len(input_type) == 2 and ("default" in input_type[1]):
113
+ params[name] = input_type[1]["default"]
114
+ continue
115
+
116
+ default_values = { "INT": 0, "FLOAT": 0.0 }
117
+ if input_type[0] in default_values:
118
+ params[name] = default_values[input_type[0]]
119
+
120
+ return getattr(aux_class(), aux_class.FUNCTION)(**params)
121
+
122
+ class ControlNetAuxSimpleAddText:
123
+ @classmethod
124
+ def INPUT_TYPES(s):
125
+ return dict(
126
+ required=dict(image=INPUT.IMAGE(), text=INPUT.STRING())
127
+ )
128
+
129
+ RETURN_TYPES = ("IMAGE",)
130
+ FUNCTION = "execute"
131
+ CATEGORY = "ControlNet Preprocessors"
132
+ def execute(self, image, text):
133
+ from PIL import Image, ImageDraw, ImageFont
134
+ import numpy as np
135
+ import torch
136
+
137
+ font = ImageFont.truetype(str((here / "NotoSans-Regular.ttf").resolve()), 40)
138
+ img = Image.fromarray(image[0].cpu().numpy().__mul__(255.).astype(np.uint8))
139
+ ImageDraw.Draw(img).text((0,0), text, fill=(0,255,0), font=font)
140
+ return (torch.from_numpy(np.array(img)).unsqueeze(0) / 255.,)
141
+
142
+ class ExecuteAllControlNetPreprocessors:
143
+ @classmethod
144
+ def INPUT_TYPES(s):
145
+ return define_preprocessor_inputs(resolution=INPUT.RESOLUTION())
146
+ RETURN_TYPES = ("IMAGE",)
147
+ FUNCTION = "execute"
148
+
149
+ CATEGORY = "ControlNet Preprocessors"
150
+
151
+ def execute(self, image, resolution=512):
152
+ try:
153
+ from comfy_execution.graph_utils import GraphBuilder
154
+ except:
155
+ raise RuntimeError("ExecuteAllControlNetPreprocessor requries [Execution Model Inversion](https://github.com/comfyanonymous/ComfyUI/commit/5cfe38). Update ComfyUI/SwarmUI to get this feature")
156
+
157
+ graph = GraphBuilder()
158
+ curr_outputs = []
159
+ for preprocc in PREPROCESSOR_OPTIONS:
160
+ preprocc_node = graph.node("AIO_Preprocessor", preprocessor=preprocc, image=image, resolution=resolution)
161
+ hint_img = preprocc_node.out(0)
162
+ add_text_node = graph.node("ControlNetAuxSimpleAddText", image=hint_img, text=preprocc)
163
+ curr_outputs.append(add_text_node.out(0))
164
+
165
+ while len(curr_outputs) > 1:
166
+ _outputs = []
167
+ for i in range(0, len(curr_outputs), 2):
168
+ if i+1 < len(curr_outputs):
169
+ image_batch = graph.node("ImageBatch", image1=curr_outputs[i], image2=curr_outputs[i+1])
170
+ _outputs.append(image_batch.out(0))
171
+ else:
172
+ _outputs.append(curr_outputs[i])
173
+ curr_outputs = _outputs
174
+
175
+ return {
176
+ "result": (curr_outputs[0],),
177
+ "expand": graph.finalize(),
178
+ }
179
+
180
+ class ControlNetPreprocessorSelector:
181
+ @classmethod
182
+ def INPUT_TYPES(s):
183
+ return {
184
+ "required": {
185
+ "preprocessor": (PREPROCESSOR_OPTIONS,),
186
+ }
187
+ }
188
+
189
+ RETURN_TYPES = (PREPROCESSOR_OPTIONS,)
190
+ RETURN_NAMES = ("preprocessor",)
191
+ FUNCTION = "get_preprocessor"
192
+
193
+ CATEGORY = "ControlNet Preprocessors"
194
+
195
+ def get_preprocessor(self, preprocessor: str):
196
+ return (preprocessor,)
197
+
198
+
199
+ NODE_CLASS_MAPPINGS = {
200
+ **AUX_NODE_MAPPINGS,
201
+ "AIO_Preprocessor": AIO_Preprocessor,
202
+ "ControlNetPreprocessorSelector": ControlNetPreprocessorSelector,
203
+ **HIE_NODE_CLASS_MAPPINGS,
204
+ "ExecuteAllControlNetPreprocessors": ExecuteAllControlNetPreprocessors,
205
+ "ControlNetAuxSimpleAddText": ControlNetAuxSimpleAddText
206
+ }
207
+
208
+ NODE_DISPLAY_NAME_MAPPINGS = {
209
+ **AUX_DISPLAY_NAME_MAPPINGS,
210
+ "AIO_Preprocessor": "AIO Aux Preprocessor",
211
+ "ControlNetPreprocessorSelector": "Preprocessor Selector",
212
+ **HIE_NODE_DISPLAY_NAME_MAPPINGS,
213
+ "ExecuteAllControlNetPreprocessors": "Execute All ControlNet Preprocessors"
214
+ }
comfyui_controlnet_aux/log.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #Cre: https://github.com/melMass/comfy_mtb/blob/main/log.py
2
+ import logging
3
+ import re
4
+ import os
5
+
6
+ base_log_level = logging.INFO
7
+
8
+
9
+ # Custom object that discards the output
10
+ class NullWriter:
11
+ def write(self, text):
12
+ pass
13
+
14
+
15
+ class Formatter(logging.Formatter):
16
+ grey = "\x1b[38;20m"
17
+ cyan = "\x1b[36;20m"
18
+ purple = "\x1b[35;20m"
19
+ yellow = "\x1b[33;20m"
20
+ red = "\x1b[31;20m"
21
+ bold_red = "\x1b[31;1m"
22
+ reset = "\x1b[0m"
23
+ # format = "%(asctime)s - [%(name)s] - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
24
+ format = "[%(name)s] | %(levelname)s -> %(message)s"
25
+
26
+ FORMATS = {
27
+ logging.DEBUG: purple + format + reset,
28
+ logging.INFO: cyan + format + reset,
29
+ logging.WARNING: yellow + format + reset,
30
+ logging.ERROR: red + format + reset,
31
+ logging.CRITICAL: bold_red + format + reset,
32
+ }
33
+
34
+ def format(self, record):
35
+ log_fmt = self.FORMATS.get(record.levelno)
36
+ formatter = logging.Formatter(log_fmt)
37
+ return formatter.format(record)
38
+
39
+
40
+ def mklog(name, level=base_log_level):
41
+ logger = logging.getLogger(name)
42
+ logger.setLevel(level)
43
+
44
+ for handler in logger.handlers:
45
+ logger.removeHandler(handler)
46
+
47
+ ch = logging.StreamHandler()
48
+ ch.setLevel(level)
49
+ ch.setFormatter(Formatter())
50
+ logger.addHandler(ch)
51
+
52
+ # Disable log propagation
53
+ logger.propagate = False
54
+
55
+ return logger
56
+
57
+
58
+ # - The main app logger
59
+ log = mklog(__package__, base_log_level)
60
+
61
+
62
+ def log_user(arg):
63
+ print("\033[34mComfyUI ControlNet AUX:\033[0m {arg}")
64
+
65
+
66
+ def get_summary(docstring):
67
+ return docstring.strip().split("\n\n", 1)[0]
68
+
69
+
70
+ def blue_text(text):
71
+ return f"\033[94m{text}\033[0m"
72
+
73
+
74
+ def cyan_text(text):
75
+ return f"\033[96m{text}\033[0m"
76
+
77
+
78
+ def get_label(label):
79
+ words = re.findall(r"(?:^|[A-Z])[a-z]*", label)
80
+ return " ".join(words).strip()
comfyui_controlnet_aux/requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ importlib_metadata
3
+ huggingface_hub
4
+ scipy
5
+ opencv-python>=4.7.0.72
6
+ filelock
7
+ numpy
8
+ Pillow
9
+ einops
10
+ torchvision
11
+ pyyaml
12
+ scikit-image
13
+ python-dateutil
14
+ mediapipe
15
+ svglib
16
+ fvcore
17
+ yapf
18
+ omegaconf
19
+ ftfy
20
+ addict
21
+ yacs
22
+ trimesh[easy]
23
+ albumentations
24
+ scikit-learn
25
+ matplotlib
comfyui_controlnet_aux/search_hf_assets.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import os
3
+ import re
4
+ #Thanks ChatGPT
5
+ pattern = r'\bfrom_pretrained\(.*?pretrained_model_or_path\s*=\s*(.*?)(?:,|\))|filename\s*=\s*(.*?)(?:,|\))|(\w+_filename)\s*=\s*(.*?)(?:,|\))'
6
+ aux_dir = Path(__file__).parent / 'src' / 'custom_controlnet_aux'
7
+ VAR_DICT = dict(
8
+ HF_MODEL_NAME = "lllyasviel/Annotators",
9
+ DWPOSE_MODEL_NAME = "yzd-v/DWPose",
10
+ BDS_MODEL_NAME = "bdsqlsz/qinglong_controlnet-lllite",
11
+ DENSEPOSE_MODEL_NAME = "LayerNorm/DensePose-TorchScript-with-hint-image",
12
+ MESH_GRAPHORMER_MODEL_NAME = "hr16/ControlNet-HandRefiner-pruned",
13
+ SAM_MODEL_NAME = "dhkim2810/MobileSAM",
14
+ UNIMATCH_MODEL_NAME = "hr16/Unimatch",
15
+ DEPTH_ANYTHING_MODEL_NAME = "LiheYoung/Depth-Anything", #HF Space
16
+ DIFFUSION_EDGE_MODEL_NAME = "hr16/Diffusion-Edge"
17
+ )
18
+ re_result_dict = {}
19
+ for preprocc in os.listdir(aux_dir):
20
+ if preprocc in ["__pycache__", 'tests']: continue
21
+ if '.py' in preprocc: continue
22
+ f = open(aux_dir / preprocc / '__init__.py', 'r')
23
+ code = f.read()
24
+ matches = re.findall(pattern, code)
25
+ result = [match[0] or match[1] or match[3] for match in matches]
26
+ if not len(result):
27
+ print(preprocc)
28
+ continue
29
+ result = [el.replace("'", '').replace('"', '') for el in result]
30
+ result = [VAR_DICT.get(el, el) for el in result]
31
+ re_result_dict[preprocc] = result
32
+ f.close()
33
+
34
+ for preprocc, re_result in re_result_dict.items():
35
+ model_name, filenames = re_result[0], re_result[1:]
36
+ print(f"* {preprocc}: ", end=' ')
37
+ assests_md = ', '.join([f"[{model_name}/{filename}](https://huggingface.co/{model_name}/blob/main/{filename})" for filename in filenames])
38
+ print(assests_md)
39
+
40
+ preprocc = "dwpose"
41
+ model_name, filenames = VAR_DICT['DWPOSE_MODEL_NAME'], ["yolox_l.onnx", "dw-ll_ucoco_384.onnx"]
42
+ print(f"* {preprocc}: ", end=' ')
43
+ assests_md = ', '.join([f"[{model_name}/{filename}](https://huggingface.co/{model_name}/blob/main/{filename})" for filename in filenames])
44
+ print(assests_md)
45
+
46
+ preprocc = "yolo-nas"
47
+ model_name, filenames = "hr16/yolo-nas-fp16", ["yolo_nas_l_fp16.onnx", "yolo_nas_m_fp16.onnx", "yolo_nas_s_fp16.onnx"]
48
+ print(f"* {preprocc}: ", end=' ')
49
+ assests_md = ', '.join([f"[{model_name}/{filename}](https://huggingface.co/{model_name}/blob/main/{filename})" for filename in filenames])
50
+ print(assests_md)
51
+
52
+ preprocc = "dwpose-torchscript"
53
+ model_name, filenames = "hr16/DWPose-TorchScript-BatchSize5", ["dw-ll_ucoco_384_bs5.torchscript.pt", "rtmpose-m_ap10k_256_bs5.torchscript.pt"]
54
+ print(f"* {preprocc}: ", end=' ')
55
+ assests_md = ', '.join([f"[{model_name}/{filename}](https://huggingface.co/{model_name}/blob/main/{filename})" for filename in filenames])
56
+ print(assests_md)
comfyui_controlnet_aux/utils.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ import os
4
+ import cv2
5
+ import yaml
6
+ from pathlib import Path
7
+ from enum import Enum
8
+ from .log import log
9
+ import subprocess
10
+ import threading
11
+ import comfy
12
+ import tempfile
13
+
14
+ here = Path(__file__).parent.resolve()
15
+
16
+ config_path = Path(here, "config.yaml")
17
+
18
+ if os.path.exists(config_path):
19
+ config = yaml.load(open(config_path, "r"), Loader=yaml.FullLoader)
20
+
21
+ annotator_ckpts_path = str(Path(here, config["annotator_ckpts_path"]))
22
+ TEMP_DIR = config["custom_temp_path"]
23
+ USE_SYMLINKS = config["USE_SYMLINKS"]
24
+ ORT_PROVIDERS = config["EP_list"]
25
+
26
+ if USE_SYMLINKS is None or type(USE_SYMLINKS) != bool:
27
+ log.error("USE_SYMLINKS must be a boolean. Using False by default.")
28
+ USE_SYMLINKS = False
29
+
30
+ if TEMP_DIR is None:
31
+ TEMP_DIR = tempfile.gettempdir()
32
+ elif not os.path.isdir(TEMP_DIR):
33
+ try:
34
+ os.makedirs(TEMP_DIR)
35
+ except:
36
+ log.error("Failed to create custom temp directory. Using default.")
37
+ TEMP_DIR = tempfile.gettempdir()
38
+
39
+ if not os.path.isdir(annotator_ckpts_path):
40
+ try:
41
+ os.makedirs(annotator_ckpts_path)
42
+ except:
43
+ log.error("Failed to create config ckpts directory. Using default.")
44
+ annotator_ckpts_path = str(Path(here, "./ckpts"))
45
+ else:
46
+ annotator_ckpts_path = str(Path(here, "./ckpts"))
47
+ TEMP_DIR = tempfile.gettempdir()
48
+ USE_SYMLINKS = False
49
+ ORT_PROVIDERS = ["CUDAExecutionProvider", "DirectMLExecutionProvider", "OpenVINOExecutionProvider", "ROCMExecutionProvider", "CPUExecutionProvider", "CoreMLExecutionProvider"]
50
+
51
+ os.environ['AUX_ANNOTATOR_CKPTS_PATH'] = os.getenv('AUX_ANNOTATOR_CKPTS_PATH', annotator_ckpts_path)
52
+ os.environ['AUX_TEMP_DIR'] = os.getenv('AUX_TEMP_DIR', str(TEMP_DIR))
53
+ os.environ['AUX_USE_SYMLINKS'] = os.getenv('AUX_USE_SYMLINKS', str(USE_SYMLINKS))
54
+ os.environ['AUX_ORT_PROVIDERS'] = os.getenv('AUX_ORT_PROVIDERS', str(",".join(ORT_PROVIDERS)))
55
+
56
+ log.info(f"Using ckpts path: {annotator_ckpts_path}")
57
+ log.info(f"Using symlinks: {USE_SYMLINKS}")
58
+ log.info(f"Using ort providers: {ORT_PROVIDERS}")
59
+
60
+ # Sync with theoritical limit from Comfy base
61
+ # https://github.com/comfyanonymous/ComfyUI/blob/eecd69b53a896343775bcb02a4f8349e7442ffd1/nodes.py#L45
62
+ MAX_RESOLUTION=16384
63
+
64
+ def common_annotator_call(model, tensor_image, input_batch=False, show_pbar=True, **kwargs):
65
+ if "detect_resolution" in kwargs:
66
+ del kwargs["detect_resolution"] #Prevent weird case?
67
+
68
+ if "resolution" in kwargs:
69
+ detect_resolution = kwargs["resolution"] if type(kwargs["resolution"]) == int and kwargs["resolution"] >= 64 else 512
70
+ del kwargs["resolution"]
71
+ else:
72
+ detect_resolution = 512
73
+
74
+ if input_batch:
75
+ np_images = np.asarray(tensor_image * 255., dtype=np.uint8)
76
+ np_results = model(np_images, output_type="np", detect_resolution=detect_resolution, **kwargs)
77
+ return torch.from_numpy(np_results.astype(np.float32) / 255.0)
78
+
79
+ batch_size = tensor_image.shape[0]
80
+ if show_pbar:
81
+ pbar = comfy.utils.ProgressBar(batch_size)
82
+ out_tensor = None
83
+ for i, image in enumerate(tensor_image):
84
+ np_image = np.asarray(image.cpu() * 255., dtype=np.uint8)
85
+ np_result = model(np_image, output_type="np", detect_resolution=detect_resolution, **kwargs)
86
+ out = torch.from_numpy(np_result.astype(np.float32) / 255.0)
87
+ if out_tensor is None:
88
+ out_tensor = torch.zeros(batch_size, *out.shape, dtype=torch.float32)
89
+ out_tensor[i] = out
90
+ if show_pbar:
91
+ pbar.update(1)
92
+ return out_tensor
93
+
94
+ def define_preprocessor_inputs(**arguments):
95
+ return dict(
96
+ required=dict(image=INPUT.IMAGE()),
97
+ optional=arguments
98
+ )
99
+
100
+ class INPUT(Enum):
101
+ def IMAGE():
102
+ return ("IMAGE",)
103
+ def LATENT():
104
+ return ("LATENT",)
105
+ def MASK():
106
+ return ("MASK",)
107
+ def SEED(default=0):
108
+ return ("INT", dict(default=default, min=0, max=0xffffffffffffffff))
109
+ def RESOLUTION(default=512, min=64, max=MAX_RESOLUTION, step=64):
110
+ return ("INT", dict(default=default, min=min, max=max, step=step))
111
+ def INT(default=0, min=0, max=MAX_RESOLUTION, step=1):
112
+ return ("INT", dict(default=default, min=min, max=max, step=step))
113
+ def FLOAT(default=0, min=0, max=1, step=0.01):
114
+ return ("FLOAT", dict(default=default, min=min, max=max, step=step))
115
+ def STRING(default='', multiline=False):
116
+ return ("STRING", dict(default=default, multiline=multiline))
117
+ def COMBO(values, default=None):
118
+ return (values, dict(default=values[0] if default is None else default))
119
+ def BOOLEAN(default=True):
120
+ return ("BOOLEAN", dict(default=default))
121
+
122
+
123
+
124
+ class ResizeMode(Enum):
125
+ """
126
+ Resize modes for ControlNet input images.
127
+ """
128
+
129
+ RESIZE = "Just Resize"
130
+ INNER_FIT = "Crop and Resize"
131
+ OUTER_FIT = "Resize and Fill"
132
+
133
+ def int_value(self):
134
+ if self == ResizeMode.RESIZE:
135
+ return 0
136
+ elif self == ResizeMode.INNER_FIT:
137
+ return 1
138
+ elif self == ResizeMode.OUTER_FIT:
139
+ return 2
140
+ assert False, "NOTREACHED"
141
+
142
+ #https://github.com/Mikubill/sd-webui-controlnet/blob/e67e017731aad05796b9615dc6eadce911298ea1/internal_controlnet/external_code.py#L89
143
+ #Replaced logger with internal log
144
+ def pixel_perfect_resolution(
145
+ image: np.ndarray,
146
+ target_H: int,
147
+ target_W: int,
148
+ resize_mode: ResizeMode,
149
+ ) -> int:
150
+ """
151
+ Calculate the estimated resolution for resizing an image while preserving aspect ratio.
152
+
153
+ The function first calculates scaling factors for height and width of the image based on the target
154
+ height and width. Then, based on the chosen resize mode, it either takes the smaller or the larger
155
+ scaling factor to estimate the new resolution.
156
+
157
+ If the resize mode is OUTER_FIT, the function uses the smaller scaling factor, ensuring the whole image
158
+ fits within the target dimensions, potentially leaving some empty space.
159
+
160
+ If the resize mode is not OUTER_FIT, the function uses the larger scaling factor, ensuring the target
161
+ dimensions are fully filled, potentially cropping the image.
162
+
163
+ After calculating the estimated resolution, the function prints some debugging information.
164
+
165
+ Args:
166
+ image (np.ndarray): A 3D numpy array representing an image. The dimensions represent [height, width, channels].
167
+ target_H (int): The target height for the image.
168
+ target_W (int): The target width for the image.
169
+ resize_mode (ResizeMode): The mode for resizing.
170
+
171
+ Returns:
172
+ int: The estimated resolution after resizing.
173
+ """
174
+ raw_H, raw_W, _ = image.shape
175
+
176
+ k0 = float(target_H) / float(raw_H)
177
+ k1 = float(target_W) / float(raw_W)
178
+
179
+ if resize_mode == ResizeMode.OUTER_FIT:
180
+ estimation = min(k0, k1) * float(min(raw_H, raw_W))
181
+ else:
182
+ estimation = max(k0, k1) * float(min(raw_H, raw_W))
183
+
184
+ log.debug(f"Pixel Perfect Computation:")
185
+ log.debug(f"resize_mode = {resize_mode}")
186
+ log.debug(f"raw_H = {raw_H}")
187
+ log.debug(f"raw_W = {raw_W}")
188
+ log.debug(f"target_H = {target_H}")
189
+ log.debug(f"target_W = {target_W}")
190
+ log.debug(f"estimation = {estimation}")
191
+
192
+ return int(np.round(estimation))
193
+
194
+ #https://github.com/Mikubill/sd-webui-controlnet/blob/e67e017731aad05796b9615dc6eadce911298ea1/scripts/controlnet.py#L404
195
+ def safe_numpy(x):
196
+ # A very safe method to make sure that Apple/Mac works
197
+ y = x
198
+
199
+ # below is very boring but do not change these. If you change these Apple or Mac may fail.
200
+ y = y.copy()
201
+ y = np.ascontiguousarray(y)
202
+ y = y.copy()
203
+ return y
204
+
205
+ #https://github.com/Mikubill/sd-webui-controlnet/blob/e67e017731aad05796b9615dc6eadce911298ea1/scripts/utils.py#L140
206
+ def get_unique_axis0(data):
207
+ arr = np.asanyarray(data)
208
+ idxs = np.lexsort(arr.T)
209
+ arr = arr[idxs]
210
+ unique_idxs = np.empty(len(arr), dtype=np.bool_)
211
+ unique_idxs[:1] = True
212
+ unique_idxs[1:] = np.any(arr[:-1, :] != arr[1:, :], axis=-1)
213
+ return arr[unique_idxs]
214
+
215
+ #Ref: https://github.com/ltdrdata/ComfyUI-Manager/blob/284e90dc8296a2e1e4f14b4b2d10fba2f52f0e53/__init__.py#L14
216
+ def handle_stream(stream, prefix):
217
+ for line in stream:
218
+ print(prefix, line, end="")
219
+
220
+
221
+ def run_script(cmd, cwd='.'):
222
+ process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
223
+
224
+ stdout_thread = threading.Thread(target=handle_stream, args=(process.stdout, ""))
225
+ stderr_thread = threading.Thread(target=handle_stream, args=(process.stderr, "[!]"))
226
+
227
+ stdout_thread.start()
228
+ stderr_thread.start()
229
+
230
+ stdout_thread.join()
231
+ stderr_thread.join()
232
+
233
+ return process.wait()
234
+
235
+ def nms(x, t, s):
236
+ x = cv2.GaussianBlur(x.astype(np.float32), (0, 0), s)
237
+
238
+ f1 = np.array([[0, 0, 0], [1, 1, 1], [0, 0, 0]], dtype=np.uint8)
239
+ f2 = np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8)
240
+ f3 = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.uint8)
241
+ f4 = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=np.uint8)
242
+
243
+ y = np.zeros_like(x)
244
+
245
+ for f in [f1, f2, f3, f4]:
246
+ np.putmask(y, cv2.dilate(x, kernel=f) == x, x)
247
+
248
+ z = np.zeros_like(y, dtype=np.uint8)
249
+ z[y > t] = 255
250
+ return z
comfyui_layerstyle/.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ _test_*.*
2
+ __pycache__
3
+ .venv
4
+ .idea
5
+ *.pth
6
+ *.ini
comfyui_layerstyle/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 chflame163
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
comfyui_layerstyle/README.MD ADDED
The diff for this file is too large to render. See raw diff
 
comfyui_layerstyle/README_CN.MD ADDED
The diff for this file is too large to render. See raw diff
 
comfyui_layerstyle/__init__.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib.util
2
+ import os
3
+ import sys
4
+ import json
5
+
6
+ NODE_CLASS_MAPPINGS = {}
7
+ NODE_DISPLAY_NAME_MAPPINGS = {}
8
+
9
+ python = sys.executable
10
+
11
+ def get_ext_dir(subpath=None, mkdir=False):
12
+ dir = os.path.dirname(__file__)
13
+ if subpath is not None:
14
+ dir = os.path.join(dir, subpath)
15
+
16
+ dir = os.path.abspath(dir)
17
+
18
+ if mkdir and not os.path.exists(dir):
19
+ os.makedirs(dir)
20
+ return dir
21
+
22
+ def serialize(obj):
23
+ if isinstance(obj, (str, int, float, bool, list, dict, type(None))):
24
+ return obj
25
+ return str(obj) # 转为字符串
26
+
27
+
28
+ py = get_ext_dir("py")
29
+ files = os.listdir(py)
30
+ all_nodes = {}
31
+ for file in files:
32
+ if not file.endswith(".py"):
33
+ continue
34
+ name = os.path.splitext(file)[0]
35
+ imported_module = importlib.import_module(".py.{}".format(name), __name__)
36
+ try:
37
+ NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **imported_module.NODE_CLASS_MAPPINGS}
38
+ NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **imported_module.NODE_DISPLAY_NAME_MAPPINGS}
39
+ serialized_CLASS_MAPPINGS = {k: serialize(v) for k, v in imported_module.NODE_CLASS_MAPPINGS.items()}
40
+ serialized_DISPLAY_NAME_MAPPINGS = {k: serialize(v) for k, v in imported_module.NODE_DISPLAY_NAME_MAPPINGS.items()}
41
+ all_nodes[file]={"NODE_CLASS_MAPPINGS": serialized_CLASS_MAPPINGS, "NODE_DISPLAY_NAME_MAPPINGS": serialized_DISPLAY_NAME_MAPPINGS}
42
+ except:
43
+ pass
44
+
45
+
46
+ WEB_DIRECTORY = "./js"
47
+
48
+ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
comfyui_layerstyle/custom_size.ini.example ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # LayerStyle Custom_size
2
+ 1024 x 1024
3
+ 768 x 512
4
+ 512 x 768
5
+ 1280 x 720
6
+ 720 x 1280
7
+ 1344 x 768
8
+ 768 x 1344
9
+ 1536 x 640
10
+ 640 x 1536