Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- ComfyUI_ExtraModels/.gitignore +160 -0
- ComfyUI_ExtraModels/LICENSE +201 -0
- ComfyUI_ExtraModels/README.md +287 -0
- ComfyUI_ExtraModels/__init__.py +51 -0
- ComfyUI_ExtraModels/requirements.txt +7 -0
- ComfyUI_JPS-Nodes/README.md +81 -0
- ComfyUI_JPS-Nodes/__init__.py +10 -0
- ComfyUI_JPS-Nodes/jps_nodes.py +0 -0
- ComfyUI_Qwen2-VL-Instruct/.gitignore +162 -0
- ComfyUI_Qwen2-VL-Instruct/LICENSE +201 -0
- ComfyUI_Qwen2-VL-Instruct/README.md +37 -0
- ComfyUI_Qwen2-VL-Instruct/__init__.py +18 -0
- ComfyUI_Qwen2-VL-Instruct/favicon.ico +0 -0
- ComfyUI_Qwen2-VL-Instruct/node_helpers.py +37 -0
- ComfyUI_Qwen2-VL-Instruct/nodes.py +195 -0
- ComfyUI_Qwen2-VL-Instruct/path_nodes.py +61 -0
- ComfyUI_Qwen2-VL-Instruct/pyproject.toml +14 -0
- ComfyUI_Qwen2-VL-Instruct/requirements.txt +14 -0
- ComfyUI_Qwen2-VL-Instruct/util_nodes.py +80 -0
- ComfyUI_essentials/.gitignore +6 -0
- ComfyUI_essentials/LICENSE +21 -0
- ComfyUI_essentials/README.md +49 -0
- ComfyUI_essentials/__init__.py +36 -0
- ComfyUI_essentials/carve.py +454 -0
- ComfyUI_essentials/conditioning.py +280 -0
- ComfyUI_essentials/histogram_matching.py +87 -0
- ComfyUI_essentials/image.py +1770 -0
- ComfyUI_essentials/mask.py +596 -0
- ComfyUI_essentials/misc.py +574 -0
- ComfyUI_essentials/pyproject.toml +15 -0
- ComfyUI_essentials/requirements.txt +5 -0
- ComfyUI_essentials/sampling.py +811 -0
- ComfyUI_essentials/segmentation.py +89 -0
- ComfyUI_essentials/text.py +113 -0
- ComfyUI_essentials/utils.py +89 -0
- ComfyUI_essentials/workflow_all_nodes.json +994 -0
- comfyui_controlnet_aux/README.md +252 -0
- comfyui_controlnet_aux/UPDATES.md +44 -0
- comfyui_controlnet_aux/__init__.py +214 -0
- comfyui_controlnet_aux/log.py +80 -0
- comfyui_controlnet_aux/requirements.txt +25 -0
- comfyui_controlnet_aux/search_hf_assets.py +56 -0
- comfyui_controlnet_aux/utils.py +250 -0
- comfyui_layerstyle/.gitignore +6 -0
- comfyui_layerstyle/LICENSE +21 -0
- comfyui_layerstyle/README.MD +0 -0
- comfyui_layerstyle/README_CN.MD +0 -0
- comfyui_layerstyle/__init__.py +48 -0
- 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
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 |
+

|
| 67 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+
[](https://kaiber.ai/) [](https://www.instasd.com/)
|
| 33 |
+
|
| 34 |
+
### :tada: Silver sponsors
|
| 35 |
+
|
| 36 |
+
[](https://openart.ai/workflows) [](https://www.finetuners.ai/) [](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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 179 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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
|