Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
698d958085 | ||
|
|
0adab07d1a | ||
|
|
ee5589c9ea | ||
|
|
d8b8c637fb | ||
|
|
ba9153cb06 | ||
|
|
e2ce0843d1 | ||
|
|
b37122a9fd | ||
|
|
6dfd2c2420 | ||
|
|
e435e999e4 | ||
|
|
876a6dd292 | ||
|
|
2f7300dc54 | ||
|
|
87d0cf42db | ||
|
|
dafbcae4e6 | ||
|
|
331260d908 | ||
|
|
984c7bb276 | ||
|
|
504044f181 | ||
|
|
d382efd7e7 | ||
|
|
fbdb08f9d6 | ||
|
|
a6b867b63a | ||
|
|
b7f337fe3a | ||
|
|
37a0973b47 | ||
|
|
0d909572e2 | ||
|
|
0d2334de6d | ||
|
|
ad37ce656c | ||
|
|
ab8cac5396 | ||
|
|
730edb08c1 | ||
|
|
fca4aa2a15 | ||
|
|
d9425173e7 | ||
|
|
f20cb4c108 | ||
|
|
4549812bcc | ||
|
|
9ea455afd6 | ||
|
|
22e722bafe | ||
|
|
cabfcb2245 | ||
|
|
479cde218b | ||
|
|
5c19bd835a | ||
|
|
6a0c755c56 | ||
|
|
ee0eb6696b | ||
|
|
40d9d84b68 | ||
|
|
1f653a8d96 | ||
|
|
ff49e1b01f | ||
|
|
6363376a17 | ||
|
|
fbc779a7bd | ||
|
|
f7eb33abc8 | ||
|
|
24f10ca90c | ||
|
|
f9118e0bbb | ||
|
|
ff98412479 | ||
|
|
b5e654103a | ||
|
|
b560966027 | ||
|
|
7afb0f906a | ||
|
|
d584c711a3 | ||
|
|
aeab1a7de5 | ||
|
|
ed8294d7fb | ||
|
|
1fbf4805f1 | ||
|
|
3f62cf2849 | ||
|
|
dfe7a909a4 | ||
|
|
e96a028254 | ||
|
|
5736669288 | ||
|
|
87b8f85103 | ||
|
|
f35bb4d336 | ||
|
|
9682804efb | ||
|
|
cebd0fc1c5 | ||
|
|
76d67a963e | ||
|
|
ea5482a6ee | ||
|
|
42c7641776 | ||
|
|
daadc345b8 | ||
|
|
a879d15208 | ||
|
|
08a22951ed | ||
|
|
0b78aedaee | ||
|
|
9a52b7dfd2 | ||
|
|
b95b79ee82 | ||
|
|
37eb7bddcb | ||
|
|
07b804cb3f | ||
|
|
16f60e53e5 | ||
|
|
44565e9bff | ||
|
|
aca7916352 | ||
|
|
12a64667ae | ||
|
|
5dcda71011 | ||
|
|
bec42252c6 | ||
|
|
ca07b9dadc | ||
|
|
cc043fcac7 | ||
|
|
6a35eefdd5 | ||
|
|
bfb6d973fe | ||
|
|
b7e5b6f1e2 | ||
|
|
c3dc82108a | ||
|
|
c3f0a55adf | ||
|
|
086ac449a2 | ||
|
|
8dac94d9d9 | ||
|
|
4d9c73ed46 | ||
|
|
3e3a1a8aac | ||
|
|
366b866f8e | ||
|
|
9903cf078e | ||
|
|
d399bc559d | ||
|
|
d41ad755ef | ||
|
|
58668a44cc | ||
|
|
9026379046 | ||
|
|
8ecf5cd05e | ||
|
|
d57154c3a8 | ||
|
|
c8f5fc0760 | ||
|
|
6c997afd5f | ||
|
|
5f16aeec88 | ||
|
|
26aea1889d | ||
|
|
9215c919b1 | ||
|
|
402124d5c4 | ||
|
|
91bdcfdf60 | ||
|
|
843805b1fb | ||
|
|
57195496fa | ||
|
|
2aa4da0f58 | ||
|
|
18a5565b5c | ||
|
|
59bd92ff49 | ||
|
|
0addfc6101 | ||
|
|
916461c432 | ||
|
|
fc59fff1b5 | ||
|
|
58a06698f0 | ||
|
|
b3269479f2 | ||
|
|
52c2e31a90 | ||
|
|
f77c0dc5d8 | ||
|
|
b1ec996ba3 | ||
|
|
a5bd3c86c8 | ||
|
|
62ee13ef76 | ||
|
|
89fb17ae84 | ||
|
|
751b0bae62 | ||
|
|
e19285e5d1 | ||
|
|
e3b4bcf21e | ||
|
|
71351c0984 | ||
|
|
393ec896f7 | ||
|
|
bb154eb71f | ||
|
|
7c488a16ef | ||
|
|
6b7eeebe44 | ||
|
|
c0f9894dc5 | ||
|
|
02c76c88b4 | ||
|
|
51e792ccdf | ||
|
|
c19ad34916 | ||
|
|
1016861aec | ||
|
|
a5b39a8f1a | ||
|
|
46f9ec2c24 | ||
|
|
29a77e764a | ||
|
|
7ecb190ef9 | ||
|
|
a4b9fd36da | ||
|
|
51b9efe0a1 | ||
|
|
63966e3483 | ||
|
|
263961539e | ||
|
|
665f59fae3 | ||
|
|
28d1fbda34 | ||
|
|
79d9aab5e7 | ||
|
|
773fab5a07 | ||
|
|
68db110554 | ||
|
|
d126b62ceb | ||
|
|
8ba503c0a5 | ||
|
|
d835ac9604 | ||
|
|
fa6d20eeb3 | ||
|
|
4d8cd3daa4 | ||
|
|
11a3744c3d | ||
|
|
0a99e6799a | ||
|
|
60abdef03e | ||
|
|
3b0d20532f | ||
|
|
d00082f648 | ||
|
|
9a15e22f5e | ||
|
|
6a16be7f21 | ||
|
|
46b76b8d7c | ||
|
|
9dc737e560 | ||
|
|
ced72635c7 | ||
|
|
82272ef448 | ||
|
|
dc482957d8 | ||
|
|
97d20e27e5 | ||
|
|
1a4259f052 | ||
|
|
24bb774432 | ||
|
|
4b3009e4bf | ||
|
|
69ec71d2bd | ||
|
|
9273d23916 | ||
|
|
8f3cc622a8 | ||
|
|
302fd1bbe6 | ||
|
|
14430c118d | ||
|
|
f3d931a630 | ||
|
|
8950c5fe67 | ||
|
|
f653a8e45e | ||
|
|
095c8d4b52 | ||
|
|
56979210c7 | ||
|
|
2129789f93 | ||
|
|
fb03b43414 | ||
|
|
e98b612b2b | ||
|
|
cb0f055a12 | ||
|
|
80977db1ea | ||
|
|
a0eebe8c9d | ||
|
|
86b5453a5c | ||
|
|
c5d23a7c38 | ||
|
|
bfe72cc964 | ||
|
|
a22b269242 | ||
|
|
9a3434ffa3 | ||
|
|
854da433f4 | ||
|
|
a8aeef670b | ||
|
|
e9a3170c72 | ||
|
|
e11695a305 | ||
|
|
23103dffde | ||
|
|
81b7d71454 | ||
|
|
6c4aa273c8 | ||
|
|
188793f2ba | ||
|
|
3a62577717 | ||
|
|
fa6964b96c | ||
|
|
df7edf7893 | ||
|
|
75a990b40d | ||
|
|
2abf557e3d | ||
|
|
bd2546b137 | ||
|
|
3f141b8f1c | ||
|
|
7b36368a74 | ||
|
|
ae5a6a92d6 | ||
|
|
690268ee27 | ||
|
|
c9c8dcd5e7 | ||
|
|
5b8d419c6f | ||
|
|
3d6b72f28d | ||
|
|
28f0470a9a | ||
|
|
3adcc529f2 | ||
|
|
7364ce7d2d | ||
|
|
5a15eea036 | ||
|
|
4d0e5cf240 | ||
|
|
833cda9fa2 | ||
|
|
31cb7c1d14 | ||
|
|
4c5c26a2c9 | ||
|
|
7a190ce8c1 | ||
|
|
f38b89ba7f | ||
|
|
3c6bb87083 | ||
|
|
cdbd38213f | ||
|
|
f5ce7d017b | ||
|
|
973ceb6ca8 | ||
|
|
d089395bb5 | ||
|
|
8c590fd5a0 | ||
|
|
6eb9211091 | ||
|
|
1a39ccde72 | ||
|
|
28381262d1 | ||
|
|
f4a0f7fc6e | ||
|
|
22231d401a | ||
|
|
ebd5915d8e | ||
|
|
7341ae0096 | ||
|
|
1bc5c99f5a | ||
|
|
5920419f44 | ||
|
|
8f057eb563 | ||
|
|
44620cb566 | ||
|
|
fb5aa296ae | ||
|
|
e2e8e7ebe5 | ||
|
|
f4f43d12e7 | ||
|
|
5d7aaf93ce | ||
|
|
67c54b31e8 | ||
|
|
31639b4c79 | ||
|
|
1dbb38d63d | ||
|
|
2cabc3eaa3 | ||
|
|
5f032e3b8f | ||
|
|
75e647560f | ||
|
|
a982a31956 | ||
|
|
3f903091b3 | ||
|
|
3b065864fa | ||
|
|
ba33d02198 | ||
|
|
cdf8ca8298 | ||
|
|
1614e1d932 | ||
|
|
365e0699b1 | ||
|
|
8cfbbaf29e | ||
|
|
3e11fff5f5 | ||
|
|
1eaa2a1928 | ||
|
|
d9191b4c1d | ||
|
|
f6ba944afc | ||
|
|
fe5fbb03ff | ||
|
|
530c5d7eaf | ||
|
|
f59e410568 | ||
|
|
19ec49ae2b | ||
|
|
1ec5810868 | ||
|
|
0227f7b77f | ||
|
|
b3e5108ad4 | ||
|
|
eb8ce5a574 | ||
|
|
971bf375e5 | ||
|
|
bbb28a0fc9 | ||
|
|
6842699f9e | ||
|
|
7c1228a5a3 | ||
|
|
2263b8cb41 | ||
|
|
14f6d5fb51 | ||
|
|
3df9f978f7 | ||
|
|
74c335b6ef | ||
|
|
f123b36d28 | ||
|
|
579f0b4050 | ||
|
|
d1848eb402 | ||
|
|
fb2f09651b | ||
|
|
2fbed0575d | ||
|
|
b5419c853c | ||
|
|
c31fa9f438 | ||
|
|
6d119fda33 | ||
|
|
71904a4eca | ||
|
|
13242fc3bf | ||
|
|
912cbd2ab6 | ||
|
|
ca0e1ad28d | ||
|
|
999be76780 | ||
|
|
3dacf166df | ||
|
|
7cb775ce40 | ||
|
|
8c456d5307 | ||
|
|
2230682bfb | ||
|
|
7aa591b3a6 | ||
|
|
181f153120 | ||
|
|
a219601b2a | ||
|
|
ab526783d2 | ||
|
|
d85a3c92a8 | ||
|
|
0defb731ac | ||
|
|
bdb65e5635 | ||
|
|
e362c926d3 | ||
|
|
8fc4794f88 | ||
|
|
a175adfe73 | ||
|
|
326d5945b7 | ||
|
|
d02a3bf46f | ||
|
|
8a2ea3fa79 | ||
|
|
47a6da5f62 | ||
|
|
bbf3db8760 | ||
|
|
1bfdb8a1a2 | ||
|
|
d7f91f2c65 | ||
|
|
87084633be | ||
|
|
11c2155138 | ||
|
|
7e7a73191c | ||
|
|
a23d734fa0 | ||
|
|
bff39a4e9c | ||
|
|
7e989daae3 | ||
|
|
eaed0d3593 | ||
|
|
7235a3dccf | ||
|
|
b751e34383 | ||
|
|
48cf28c7d0 | ||
|
|
58f234d544 | ||
|
|
a6f358ac29 | ||
|
|
e549bd91bc | ||
|
|
2c3d2b9832 | ||
|
|
855aef479f | ||
|
|
0bc25cfe55 | ||
|
|
20aab31d83 | ||
|
|
9bb1e47ba7 | ||
|
|
98c1fb17c5 | ||
|
|
c3f6dcd850 | ||
|
|
9349d318ba | ||
|
|
2e7129fdb9 | ||
|
|
1afb92dee2 | ||
|
|
70301c5e54 | ||
|
|
c08c858374 | ||
|
|
dc479dc2ef | ||
|
|
df5e0d49e4 | ||
|
|
d9bef9692a | ||
|
|
cc6196c25f | ||
|
|
3c0bc27343 | ||
|
|
6f82bca98d | ||
|
|
42741f578c | ||
|
|
286f8ea4a3 | ||
|
|
5229aaa2c0 | ||
|
|
4a2d499a5b | ||
|
|
17f8d8db60 | ||
|
|
dee4e8f1eb | ||
|
|
5ccd427bd8 | ||
|
|
27cfddc379 | ||
|
|
0888ece9b0 | ||
|
|
ad60519856 | ||
|
|
7191a1fbbb | ||
|
|
7ea33f68fc | ||
|
|
adb35ee233 | ||
|
|
5568cff68a | ||
|
|
6e3af6e7b9 | ||
|
|
bc4890f798 | ||
|
|
8df4ea78cb | ||
|
|
44f7e559b9 | ||
|
|
86dc22a028 | ||
|
|
0905db846d | ||
|
|
8560348572 | ||
|
|
6e084c0f9b | ||
|
|
ffafc9c2c6 | ||
|
|
296a1beb66 | ||
|
|
3d7577f316 | ||
|
|
2ead4fae1d | ||
|
|
83095a51ba | ||
|
|
2c9acabaa5 | ||
|
|
86a1125ae1 | ||
|
|
fa10533f8c | ||
|
|
d6b69f56bb | ||
|
|
05b4c3c4b7 | ||
|
|
1f03079338 | ||
|
|
2fb0ee4934 | ||
|
|
97cefb427a | ||
|
|
6f37e62dd6 | ||
|
|
c98c94125e | ||
|
|
36a8633aff | ||
|
|
3ba3ddf0b1 | ||
|
|
07288d1b6f | ||
|
|
36e3b6f66a | ||
|
|
cdd507de43 | ||
|
|
5430564e7f | ||
|
|
b6193451b3 | ||
|
|
2c7e8613e0 | ||
|
|
a15d79d5dc | ||
|
|
0690bff42c | ||
|
|
63e6b81aee | ||
|
|
bba419b07b | ||
|
|
a8f6110828 | ||
|
|
a7141dc5ca | ||
|
|
d8cf496704 | ||
|
|
2f843db54e | ||
|
|
fca2e82853 | ||
|
|
cb6864c7c8 | ||
|
|
2f6e38220c | ||
|
|
c4c63fce73 | ||
|
|
7e6bd8d14a | ||
|
|
f741ef0252 | ||
|
|
33ef974370 | ||
|
|
6ca2bb2708 | ||
|
|
68471f65b3 | ||
|
|
17a6b358af | ||
|
|
b2d5ab3fcd | ||
|
|
4812eff6e5 | ||
|
|
0ef2b86b28 | ||
|
|
22f166008f | ||
|
|
d24d1c64d2 | ||
|
|
5631cd0146 | ||
|
|
9991155130 | ||
|
|
668496d85f | ||
|
|
218e95c8c9 | ||
|
|
d2a44569b2 | ||
|
|
bee351960d | ||
|
|
d165ae2542 | ||
|
|
26ca2925c6 | ||
|
|
030b6a20b5 | ||
|
|
3260038935 | ||
|
|
cba1cb92c1 | ||
|
|
3652e8eee2 | ||
|
|
148c805a15 | ||
|
|
916241840a | ||
|
|
26fbf1f03d | ||
|
|
46e91987be | ||
|
|
928e5cc778 | ||
|
|
4f9d3f4142 | ||
|
|
147c4505d1 | ||
|
|
855f9993b9 | ||
|
|
d12a011943 | ||
|
|
24f27c5b82 |
@@ -0,0 +1,2 @@
|
|||||||
|
github: [kijai]
|
||||||
|
custom: ["https://www.paypal.me/kijaidesign"]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Publish to Comfy registry
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "pyproject.toml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-node:
|
||||||
|
name: Publish Custom Node to registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository_owner == 'kijai' }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Publish Custom Node
|
||||||
|
uses: Comfy-Org/publish-node-action@v1
|
||||||
|
with:
|
||||||
|
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||||
|
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
/venv
|
/venv
|
||||||
|
*.code-workspace
|
||||||
|
.history
|
||||||
.vscode
|
.vscode
|
||||||
*.ckpt
|
*.ckpt
|
||||||
*.pth
|
*.pth
|
||||||
types
|
types
|
||||||
models
|
models
|
||||||
jsconfig.json
|
jsconfig.json
|
||||||
|
custom_dimensions.json
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ This is still work in progress, like everything else.
|
|||||||
## Javascript
|
## Javascript
|
||||||
|
|
||||||
### browserstatus.js
|
### browserstatus.js
|
||||||
Sets the favicon to green circle when not processing anything, sets it to red when processing and shows progress percentage and the lenghth of your queue.
|
Sets the favicon to green circle when not processing anything, sets it to red when processing and shows progress percentage and the length of your queue.
|
||||||
Default off, needs to be enabled from options, overrides Custom-Scripts favicon when enabled.
|
Default off, needs to be enabled from options, overrides Custom-Scripts favicon when enabled.
|
||||||
|
|
||||||
## Nodes:
|
## Nodes:
|
||||||
|
|||||||
+102
-4
@@ -5,8 +5,11 @@ from .nodes.audioscheduler_nodes import *
|
|||||||
from .nodes.image_nodes import *
|
from .nodes.image_nodes import *
|
||||||
from .nodes.intrinsic_lora_nodes import *
|
from .nodes.intrinsic_lora_nodes import *
|
||||||
from .nodes.mask_nodes import *
|
from .nodes.mask_nodes import *
|
||||||
|
from .nodes.model_optimization_nodes import *
|
||||||
|
from .nodes.lora_nodes import *
|
||||||
NODE_CONFIG = {
|
NODE_CONFIG = {
|
||||||
#constants
|
#constants
|
||||||
|
"BOOLConstant": {"class": BOOLConstant, "name": "BOOL Constant"},
|
||||||
"INTConstant": {"class": INTConstant, "name": "INT Constant"},
|
"INTConstant": {"class": INTConstant, "name": "INT Constant"},
|
||||||
"FloatConstant": {"class": FloatConstant, "name": "Float Constant"},
|
"FloatConstant": {"class": FloatConstant, "name": "Float Constant"},
|
||||||
"StringConstant": {"class": StringConstant, "name": "String Constant"},
|
"StringConstant": {"class": StringConstant, "name": "String Constant"},
|
||||||
@@ -19,6 +22,7 @@ NODE_CONFIG = {
|
|||||||
"ConditioningSetMaskAndCombine5": {"class": ConditioningSetMaskAndCombine5, "name": "ConditioningSetMaskAndCombine5"},
|
"ConditioningSetMaskAndCombine5": {"class": ConditioningSetMaskAndCombine5, "name": "ConditioningSetMaskAndCombine5"},
|
||||||
"CondPassThrough": {"class": CondPassThrough},
|
"CondPassThrough": {"class": CondPassThrough},
|
||||||
#masking
|
#masking
|
||||||
|
"DownloadAndLoadCLIPSeg": {"class": DownloadAndLoadCLIPSeg, "name": "(Down)load CLIPSeg"},
|
||||||
"BatchCLIPSeg": {"class": BatchCLIPSeg, "name": "Batch CLIPSeg"},
|
"BatchCLIPSeg": {"class": BatchCLIPSeg, "name": "Batch CLIPSeg"},
|
||||||
"ColorToMask": {"class": ColorToMask, "name": "Color To Mask"},
|
"ColorToMask": {"class": ColorToMask, "name": "Color To Mask"},
|
||||||
"CreateGradientMask": {"class": CreateGradientMask, "name": "Create Gradient Mask"},
|
"CreateGradientMask": {"class": CreateGradientMask, "name": "Create Gradient Mask"},
|
||||||
@@ -37,31 +41,64 @@ NODE_CONFIG = {
|
|||||||
"RemapMaskRange": {"class": RemapMaskRange, "name": "Remap Mask Range"},
|
"RemapMaskRange": {"class": RemapMaskRange, "name": "Remap Mask Range"},
|
||||||
"ResizeMask": {"class": ResizeMask, "name": "Resize Mask"},
|
"ResizeMask": {"class": ResizeMask, "name": "Resize Mask"},
|
||||||
"RoundMask": {"class": RoundMask, "name": "Round Mask"},
|
"RoundMask": {"class": RoundMask, "name": "Round Mask"},
|
||||||
|
"SeparateMasks": {"class": SeparateMasks, "name": "Separate Masks"},
|
||||||
#images
|
#images
|
||||||
"AddLabel": {"class": AddLabel, "name": "Add Label"},
|
"AddLabel": {"class": AddLabel, "name": "Add Label"},
|
||||||
"ColorMatch": {"class": ColorMatch, "name": "Color Match"},
|
"ColorMatch": {"class": ColorMatch, "name": "Color Match"},
|
||||||
|
"ImageTensorList": {"class": ImageTensorList, "name": "Image Tensor List"},
|
||||||
"CrossFadeImages": {"class": CrossFadeImages, "name": "Cross Fade Images"},
|
"CrossFadeImages": {"class": CrossFadeImages, "name": "Cross Fade Images"},
|
||||||
"GetImageRangeFromBatch": {"class": GetImageRangeFromBatch, "name": "Get Image Range From Batch"},
|
"CrossFadeImagesMulti": {"class": CrossFadeImagesMulti, "name": "Cross Fade Images Multi"},
|
||||||
|
"GetImagesFromBatchIndexed": {"class": GetImagesFromBatchIndexed, "name": "Get Images From Batch Indexed"},
|
||||||
|
"GetImageRangeFromBatch": {"class": GetImageRangeFromBatch, "name": "Get Image or Mask Range From Batch"},
|
||||||
|
"GetLatentRangeFromBatch": {"class": GetLatentRangeFromBatch, "name": "Get Latent Range From Batch"},
|
||||||
|
"GetLatentSizeAndCount": {"class": GetLatentSizeAndCount, "name": "Get Latent Size & Count"},
|
||||||
"GetImageSizeAndCount": {"class": GetImageSizeAndCount, "name": "Get Image Size & Count"},
|
"GetImageSizeAndCount": {"class": GetImageSizeAndCount, "name": "Get Image Size & Count"},
|
||||||
|
"FastPreview": {"class": FastPreview, "name": "Fast Preview"},
|
||||||
|
"ImageBatchFilter": {"class": ImageBatchFilter, "name": "Image Batch Filter"},
|
||||||
"ImageAndMaskPreview": {"class": ImageAndMaskPreview},
|
"ImageAndMaskPreview": {"class": ImageAndMaskPreview},
|
||||||
|
"ImageAddMulti": {"class": ImageAddMulti, "name": "Image Add Multi"},
|
||||||
|
"ImageBatchJoinWithTransition": {"class": ImageBatchJoinWithTransition, "name": "Image Batch Join With Transition"},
|
||||||
"ImageBatchMulti": {"class": ImageBatchMulti, "name": "Image Batch Multi"},
|
"ImageBatchMulti": {"class": ImageBatchMulti, "name": "Image Batch Multi"},
|
||||||
"ImageBatchRepeatInterleaving": {"class": ImageBatchRepeatInterleaving},
|
"ImageBatchRepeatInterleaving": {"class": ImageBatchRepeatInterleaving},
|
||||||
"ImageBatchTestPattern": {"class": ImageBatchTestPattern, "name": "Image Batch Test Pattern"},
|
"ImageBatchTestPattern": {"class": ImageBatchTestPattern, "name": "Image Batch Test Pattern"},
|
||||||
"ImageConcanate": {"class": ImageConcanate, "name": "Image Concatenate"},
|
"ImageConcanate": {"class": ImageConcanate, "name": "Image Concatenate"},
|
||||||
|
"ImageConcatFromBatch": {"class": ImageConcatFromBatch, "name": "Image Concatenate From Batch"},
|
||||||
|
"ImageConcatMulti": {"class": ImageConcatMulti, "name": "Image Concatenate Multi"},
|
||||||
|
"ImageCropByMask": {"class": ImageCropByMask, "name": "Image Crop By Mask"},
|
||||||
|
"ImageCropByMaskAndResize": {"class": ImageCropByMaskAndResize, "name": "Image Crop By Mask And Resize"},
|
||||||
|
"ImageCropByMaskBatch": {"class": ImageCropByMaskBatch, "name": "Image Crop By Mask Batch"},
|
||||||
|
"ImageUncropByMask": {"class": ImageUncropByMask, "name": "Image Uncrop By Mask"},
|
||||||
"ImageGrabPIL": {"class": ImageGrabPIL, "name": "Image Grab PIL"},
|
"ImageGrabPIL": {"class": ImageGrabPIL, "name": "Image Grab PIL"},
|
||||||
"ImageGridComposite2x2": {"class": ImageGridComposite2x2, "name": "Image Grid Composite 2x2"},
|
"ImageGridComposite2x2": {"class": ImageGridComposite2x2, "name": "Image Grid Composite 2x2"},
|
||||||
"ImageGridComposite3x3": {"class": ImageGridComposite3x3, "name": "Image Grid Composite 3x3"},
|
"ImageGridComposite3x3": {"class": ImageGridComposite3x3, "name": "Image Grid Composite 3x3"},
|
||||||
|
"ImageGridtoBatch": {"class": ImageGridtoBatch, "name": "Image Grid To Batch"},
|
||||||
|
"ImageNoiseAugmentation": {"class": ImageNoiseAugmentation, "name": "Image Noise Augmentation"},
|
||||||
"ImageNormalize_Neg1_To_1": {"class": ImageNormalize_Neg1_To_1, "name": "Image Normalize -1 to 1"},
|
"ImageNormalize_Neg1_To_1": {"class": ImageNormalize_Neg1_To_1, "name": "Image Normalize -1 to 1"},
|
||||||
"ImagePass": {"class": ImagePass},
|
"ImagePass": {"class": ImagePass},
|
||||||
|
"ImagePadKJ": {"class": ImagePadKJ, "name": "ImagePad KJ"},
|
||||||
"ImagePadForOutpaintMasked": {"class": ImagePadForOutpaintMasked, "name": "Image Pad For Outpaint Masked"},
|
"ImagePadForOutpaintMasked": {"class": ImagePadForOutpaintMasked, "name": "Image Pad For Outpaint Masked"},
|
||||||
|
"ImagePadForOutpaintTargetSize": {"class": ImagePadForOutpaintTargetSize, "name": "Image Pad For Outpaint Target Size"},
|
||||||
|
"ImagePrepForICLora": {"class": ImagePrepForICLora, "name": "Image Prep For ICLora"},
|
||||||
|
"ImageResizeKJ": {"class": ImageResizeKJ, "name": "Resize Image (deprecated)"},
|
||||||
|
"ImageResizeKJv2": {"class": ImageResizeKJv2, "name": "Resize Image v2"},
|
||||||
"ImageUpscaleWithModelBatched": {"class": ImageUpscaleWithModelBatched, "name": "Image Upscale With Model Batched"},
|
"ImageUpscaleWithModelBatched": {"class": ImageUpscaleWithModelBatched, "name": "Image Upscale With Model Batched"},
|
||||||
"InsertImagesToBatchIndexed": {"class": InsertImagesToBatchIndexed, "name": "Insert Images To Batch Indexed"},
|
"InsertImagesToBatchIndexed": {"class": InsertImagesToBatchIndexed, "name": "Insert Images To Batch Indexed"},
|
||||||
|
"InsertLatentToIndexed": {"class": InsertLatentToIndex, "name": "Insert Latent To Index"},
|
||||||
|
"LoadAndResizeImage": {"class": LoadAndResizeImage, "name": "Load & Resize Image"},
|
||||||
|
"LoadImagesFromFolderKJ": {"class": LoadImagesFromFolderKJ, "name": "Load Images From Folder (KJ)"},
|
||||||
|
"LoadVideosFromFolder": {"class": LoadVideosFromFolder, "name": "Load Videos From Folder"},
|
||||||
"MergeImageChannels": {"class": MergeImageChannels, "name": "Merge Image Channels"},
|
"MergeImageChannels": {"class": MergeImageChannels, "name": "Merge Image Channels"},
|
||||||
|
"PadImageBatchInterleaved": {"class": PadImageBatchInterleaved, "name": "Pad Image Batch Interleaved"},
|
||||||
|
"PreviewAnimation": {"class": PreviewAnimation, "name": "Preview Animation"},
|
||||||
"RemapImageRange": {"class": RemapImageRange, "name": "Remap Image Range"},
|
"RemapImageRange": {"class": RemapImageRange, "name": "Remap Image Range"},
|
||||||
"ReverseImageBatch": {"class": ReverseImageBatch, "name": "Reverse Image Batch"},
|
"ReverseImageBatch": {"class": ReverseImageBatch, "name": "Reverse Image Batch"},
|
||||||
"ReplaceImagesInBatch": {"class": ReplaceImagesInBatch, "name": "Replace Images In Batch"},
|
"ReplaceImagesInBatch": {"class": ReplaceImagesInBatch, "name": "Replace Images In Batch"},
|
||||||
"SaveImageWithAlpha": {"class": SaveImageWithAlpha, "name": "Save Image With Alpha"},
|
"SaveImageWithAlpha": {"class": SaveImageWithAlpha, "name": "Save Image With Alpha"},
|
||||||
|
"SaveImageKJ": {"class": SaveImageKJ, "name": "Save Image KJ"},
|
||||||
|
"ShuffleImageBatch": {"class": ShuffleImageBatch, "name": "Shuffle Image Batch"},
|
||||||
"SplitImageChannels": {"class": SplitImageChannels, "name": "Split Image Channels"},
|
"SplitImageChannels": {"class": SplitImageChannels, "name": "Split Image Channels"},
|
||||||
|
"TransitionImagesMulti": {"class": TransitionImagesMulti, "name": "Transition Images Multi"},
|
||||||
|
"TransitionImagesInBatch": {"class": TransitionImagesInBatch, "name": "Transition Images In Batch"},
|
||||||
#batch cropping
|
#batch cropping
|
||||||
"BatchCropFromMask": {"class": BatchCropFromMask, "name": "Batch Crop From Mask"},
|
"BatchCropFromMask": {"class": BatchCropFromMask, "name": "Batch Crop From Mask"},
|
||||||
"BatchCropFromMaskAdvanced": {"class": BatchCropFromMaskAdvanced, "name": "Batch Crop From Mask Advanced"},
|
"BatchCropFromMaskAdvanced": {"class": BatchCropFromMaskAdvanced, "name": "Batch Crop From Mask Advanced"},
|
||||||
@@ -78,34 +115,52 @@ NODE_CONFIG = {
|
|||||||
"InjectNoiseToLatent": {"class": InjectNoiseToLatent, "name": "Inject Noise To Latent"},
|
"InjectNoiseToLatent": {"class": InjectNoiseToLatent, "name": "Inject Noise To Latent"},
|
||||||
"CustomSigmas": {"class": CustomSigmas, "name": "Custom Sigmas"},
|
"CustomSigmas": {"class": CustomSigmas, "name": "Custom Sigmas"},
|
||||||
#utility
|
#utility
|
||||||
|
"StringToFloatList": {"class": StringToFloatList, "name": "String to Float List"},
|
||||||
"WidgetToString": {"class": WidgetToString, "name": "Widget To String"},
|
"WidgetToString": {"class": WidgetToString, "name": "Widget To String"},
|
||||||
"DummyLatentOut": {"class": DummyLatentOut, "name": "Dummy Latent Out"},
|
"SaveStringKJ": {"class": SaveStringKJ, "name": "Save String KJ"},
|
||||||
|
"DummyOut": {"class": DummyOut, "name": "Dummy Out"},
|
||||||
"GetLatentsFromBatchIndexed": {"class": GetLatentsFromBatchIndexed, "name": "Get Latents From Batch Indexed"},
|
"GetLatentsFromBatchIndexed": {"class": GetLatentsFromBatchIndexed, "name": "Get Latents From Batch Indexed"},
|
||||||
"ScaleBatchPromptSchedule": {"class": ScaleBatchPromptSchedule, "name": "Scale Batch Prompt Schedule"},
|
"ScaleBatchPromptSchedule": {"class": ScaleBatchPromptSchedule, "name": "Scale Batch Prompt Schedule"},
|
||||||
"CameraPoseVisualizer": {"class": CameraPoseVisualizer, "name": "Camera Pose Visualizer"},
|
"CameraPoseVisualizer": {"class": CameraPoseVisualizer, "name": "Camera Pose Visualizer"},
|
||||||
|
"AppendStringsToList": {"class": AppendStringsToList, "name": "Append Strings To List"},
|
||||||
"JoinStrings": {"class": JoinStrings, "name": "Join Strings"},
|
"JoinStrings": {"class": JoinStrings, "name": "Join Strings"},
|
||||||
"JoinStringMulti": {"class": JoinStringMulti, "name": "Join String Multi"},
|
"JoinStringMulti": {"class": JoinStringMulti, "name": "Join String Multi"},
|
||||||
|
"SomethingToString": {"class": SomethingToString, "name": "Something To String"},
|
||||||
"Sleep": {"class": Sleep, "name": "Sleep"},
|
"Sleep": {"class": Sleep, "name": "Sleep"},
|
||||||
"VRAM_Debug": {"class": VRAM_Debug, "name": "VRAM Debug"},
|
"VRAM_Debug": {"class": VRAM_Debug, "name": "VRAM Debug"},
|
||||||
"SomethingToString": {"class": SomethingToString, "name": "Something To String"},
|
"SomethingToString": {"class": SomethingToString, "name": "Something To String"},
|
||||||
"EmptyLatentImagePresets": {"class": EmptyLatentImagePresets, "name": "Empty Latent Image Presets"},
|
"EmptyLatentImagePresets": {"class": EmptyLatentImagePresets, "name": "Empty Latent Image Presets"},
|
||||||
|
"EmptyLatentImageCustomPresets": {"class": EmptyLatentImageCustomPresets, "name": "Empty Latent Image Custom Presets"},
|
||||||
|
"ModelPassThrough": {"class": ModelPassThrough, "name": "ModelPass"},
|
||||||
|
"ModelSaveKJ": {"class": ModelSaveKJ, "name": "Model Save KJ"},
|
||||||
|
"SetShakkerLabsUnionControlNetType": {"class": SetShakkerLabsUnionControlNetType, "name": "Set Shakker Labs Union ControlNet Type"},
|
||||||
|
"StyleModelApplyAdvanced": {"class": StyleModelApplyAdvanced, "name": "Style Model Apply Advanced"},
|
||||||
|
"DiffusionModelSelector": {"class": DiffusionModelSelector, "name": "Diffusion Model Selector"},
|
||||||
|
"LazySwitchKJ": {"class": LazySwitchKJ, "name": "Lazy Switch KJ"},
|
||||||
#audioscheduler stuff
|
#audioscheduler stuff
|
||||||
"NormalizedAmplitudeToMask": {"class": NormalizedAmplitudeToMask},
|
"NormalizedAmplitudeToMask": {"class": NormalizedAmplitudeToMask},
|
||||||
"NormalizedAmplitudeToFloatList": {"class": NormalizedAmplitudeToFloatList},
|
"NormalizedAmplitudeToFloatList": {"class": NormalizedAmplitudeToFloatList},
|
||||||
"OffsetMaskByNormalizedAmplitude": {"class": OffsetMaskByNormalizedAmplitude},
|
"OffsetMaskByNormalizedAmplitude": {"class": OffsetMaskByNormalizedAmplitude},
|
||||||
"ImageTransformByNormalizedAmplitude": {"class": ImageTransformByNormalizedAmplitude},
|
"ImageTransformByNormalizedAmplitude": {"class": ImageTransformByNormalizedAmplitude},
|
||||||
|
"AudioConcatenate": {"class": AudioConcatenate},
|
||||||
#curve nodes
|
#curve nodes
|
||||||
"SplineEditor": {"class": SplineEditor, "name": "Spline Editor"},
|
"SplineEditor": {"class": SplineEditor, "name": "Spline Editor"},
|
||||||
|
"CreateShapeImageOnPath": {"class": CreateShapeImageOnPath, "name": "Create Shape Image On Path"},
|
||||||
"CreateShapeMaskOnPath": {"class": CreateShapeMaskOnPath, "name": "Create Shape Mask On Path"},
|
"CreateShapeMaskOnPath": {"class": CreateShapeMaskOnPath, "name": "Create Shape Mask On Path"},
|
||||||
|
"CreateTextOnPath": {"class": CreateTextOnPath, "name": "Create Text On Path"},
|
||||||
|
"CreateGradientFromCoords": {"class": CreateGradientFromCoords, "name": "Create Gradient From Coords"},
|
||||||
|
"CutAndDragOnPath": {"class": CutAndDragOnPath, "name": "Cut And Drag On Path"},
|
||||||
|
"GradientToFloat": {"class": GradientToFloat, "name": "Gradient To Float"},
|
||||||
"WeightScheduleExtend": {"class": WeightScheduleExtend, "name": "Weight Schedule Extend"},
|
"WeightScheduleExtend": {"class": WeightScheduleExtend, "name": "Weight Schedule Extend"},
|
||||||
"MaskOrImageToWeight": {"class": MaskOrImageToWeight, "name": "Mask Or Image To Weight"},
|
"MaskOrImageToWeight": {"class": MaskOrImageToWeight, "name": "Mask Or Image To Weight"},
|
||||||
"WeightScheduleConvert": {"class": WeightScheduleConvert, "name": "Weight Schedule Convert"},
|
"WeightScheduleConvert": {"class": WeightScheduleConvert, "name": "Weight Schedule Convert"},
|
||||||
"FloatToMask": {"class": FloatToMask, "name": "Float To Mask"},
|
"FloatToMask": {"class": FloatToMask, "name": "Float To Mask"},
|
||||||
"FloatToSigmas": {"class": FloatToSigmas, "name": "Float To Sigmas"},
|
"FloatToSigmas": {"class": FloatToSigmas, "name": "Float To Sigmas"},
|
||||||
|
"SigmasToFloat": {"class": SigmasToFloat, "name": "Sigmas To Float"},
|
||||||
"PlotCoordinates": {"class": PlotCoordinates, "name": "Plot Coordinates"},
|
"PlotCoordinates": {"class": PlotCoordinates, "name": "Plot Coordinates"},
|
||||||
"InterpolateCoords": {"class": InterpolateCoords, "name": "Interpolate Coords"},
|
"InterpolateCoords": {"class": InterpolateCoords, "name": "Interpolate Coords"},
|
||||||
|
"PointsEditor": {"class": PointsEditor, "name": "Points Editor"},
|
||||||
#experimental
|
#experimental
|
||||||
"StabilityAPI_SD3": {"class": StabilityAPI_SD3, "name": "Stability API SD3"},
|
|
||||||
"SoundReactive": {"class": SoundReactive, "name": "Sound Reactive"},
|
"SoundReactive": {"class": SoundReactive, "name": "Sound Reactive"},
|
||||||
"StableZero123_BatchSchedule": {"class": StableZero123_BatchSchedule, "name": "Stable Zero123 Batch Schedule"},
|
"StableZero123_BatchSchedule": {"class": StableZero123_BatchSchedule, "name": "Stable Zero123 Batch Schedule"},
|
||||||
"SV3D_BatchSchedule": {"class": SV3D_BatchSchedule, "name": "SV3D Batch Schedule"},
|
"SV3D_BatchSchedule": {"class": SV3D_BatchSchedule, "name": "SV3D Batch Schedule"},
|
||||||
@@ -113,10 +168,51 @@ NODE_CONFIG = {
|
|||||||
"Superprompt": {"class": Superprompt, "name": "Superprompt"},
|
"Superprompt": {"class": Superprompt, "name": "Superprompt"},
|
||||||
"GLIGENTextBoxApplyBatchCoords": {"class": GLIGENTextBoxApplyBatchCoords},
|
"GLIGENTextBoxApplyBatchCoords": {"class": GLIGENTextBoxApplyBatchCoords},
|
||||||
"Intrinsic_lora_sampling": {"class": Intrinsic_lora_sampling, "name": "Intrinsic Lora Sampling"},
|
"Intrinsic_lora_sampling": {"class": Intrinsic_lora_sampling, "name": "Intrinsic Lora Sampling"},
|
||||||
|
"CheckpointPerturbWeights": {"class": CheckpointPerturbWeights, "name": "CheckpointPerturbWeights"},
|
||||||
|
"Screencap_mss": {"class": Screencap_mss, "name": "Screencap mss"},
|
||||||
|
"WebcamCaptureCV2": {"class": WebcamCaptureCV2, "name": "Webcam Capture CV2"},
|
||||||
|
"DifferentialDiffusionAdvanced": {"class": DifferentialDiffusionAdvanced, "name": "Differential Diffusion Advanced"},
|
||||||
|
"DiTBlockLoraLoader": {"class": DiTBlockLoraLoader, "name": "DiT Block Lora Loader"},
|
||||||
|
"FluxBlockLoraSelect": {"class": FluxBlockLoraSelect, "name": "Flux Block Lora Select"},
|
||||||
|
"HunyuanVideoBlockLoraSelect": {"class": HunyuanVideoBlockLoraSelect, "name": "Hunyuan Video Block Lora Select"},
|
||||||
|
"Wan21BlockLoraSelect": {"class": Wan21BlockLoraSelect, "name": "Wan21 Block Lora Select"},
|
||||||
|
"CustomControlNetWeightsFluxFromList": {"class": CustomControlNetWeightsFluxFromList, "name": "Custom ControlNet Weights Flux From List"},
|
||||||
|
"CheckpointLoaderKJ": {"class": CheckpointLoaderKJ, "name": "CheckpointLoaderKJ"},
|
||||||
|
"DiffusionModelLoaderKJ": {"class": DiffusionModelLoaderKJ, "name": "Diffusion Model Loader KJ"},
|
||||||
|
"TorchCompileModelFluxAdvanced": {"class": TorchCompileModelFluxAdvanced, "name": "TorchCompileModelFluxAdvanced"},
|
||||||
|
"TorchCompileModelFluxAdvancedV2": {"class": TorchCompileModelFluxAdvancedV2, "name": "TorchCompileModelFluxAdvancedV2"},
|
||||||
|
"TorchCompileModelHyVideo": {"class": TorchCompileModelHyVideo, "name": "TorchCompileModelHyVideo"},
|
||||||
|
"TorchCompileVAE": {"class": TorchCompileVAE, "name": "TorchCompileVAE"},
|
||||||
|
"TorchCompileControlNet": {"class": TorchCompileControlNet, "name": "TorchCompileControlNet"},
|
||||||
|
"PatchModelPatcherOrder": {"class": PatchModelPatcherOrder, "name": "Patch Model Patcher Order"},
|
||||||
|
"TorchCompileLTXModel": {"class": TorchCompileLTXModel, "name": "TorchCompileLTXModel"},
|
||||||
|
"TorchCompileCosmosModel": {"class": TorchCompileCosmosModel, "name": "TorchCompileCosmosModel"},
|
||||||
|
"TorchCompileModelQwenImage": {"class": TorchCompileModelQwenImage, "name": "TorchCompileModelQwenImage"},
|
||||||
|
"TorchCompileModelWanVideo": {"class": TorchCompileModelWanVideo, "name": "TorchCompileModelWanVideo"},
|
||||||
|
"TorchCompileModelWanVideoV2": {"class": TorchCompileModelWanVideoV2, "name": "TorchCompileModelWanVideoV2"},
|
||||||
|
"PathchSageAttentionKJ": {"class": PathchSageAttentionKJ, "name": "Patch Sage Attention KJ"},
|
||||||
|
"LeapfusionHunyuanI2VPatcher": {"class": LeapfusionHunyuanI2V, "name": "Leapfusion Hunyuan I2V Patcher"},
|
||||||
|
"VAELoaderKJ": {"class": VAELoaderKJ, "name": "VAELoader KJ"},
|
||||||
|
"ScheduledCFGGuidance": {"class": ScheduledCFGGuidance, "name": "Scheduled CFG Guidance"},
|
||||||
|
"ApplyRifleXRoPE_HunuyanVideo": {"class": ApplyRifleXRoPE_HunuyanVideo, "name": "Apply RifleXRoPE HunuyanVideo"},
|
||||||
|
"ApplyRifleXRoPE_WanVideo": {"class": ApplyRifleXRoPE_WanVideo, "name": "Apply RifleXRoPE WanVideo"},
|
||||||
|
"WanVideoTeaCacheKJ": {"class": WanVideoTeaCacheKJ, "name": "WanVideo Tea Cache (native)"},
|
||||||
|
"WanVideoEnhanceAVideoKJ": {"class": WanVideoEnhanceAVideoKJ, "name": "WanVideo Enhance A Video (native)"},
|
||||||
|
"SkipLayerGuidanceWanVideo": {"class": SkipLayerGuidanceWanVideo, "name": "Skip Layer Guidance WanVideo"},
|
||||||
|
"TimerNodeKJ": {"class": TimerNodeKJ, "name": "Timer Node KJ"},
|
||||||
|
"HunyuanVideoEncodeKeyframesToCond": {"class": HunyuanVideoEncodeKeyframesToCond, "name": "HunyuanVideo Encode Keyframes To Cond"},
|
||||||
|
"CFGZeroStarAndInit": {"class": CFGZeroStarAndInit, "name": "CFG Zero Star/Init"},
|
||||||
|
"ModelPatchTorchSettings": {"class": ModelPatchTorchSettings, "name": "Model Patch Torch Settings"},
|
||||||
|
"WanVideoNAG": {"class": WanVideoNAG, "name": "WanVideoNAG"},
|
||||||
|
|
||||||
#instance diffusion
|
#instance diffusion
|
||||||
"CreateInstanceDiffusionTracking": {"class": CreateInstanceDiffusionTracking},
|
"CreateInstanceDiffusionTracking": {"class": CreateInstanceDiffusionTracking},
|
||||||
"AppendInstanceDiffusionTracking": {"class": AppendInstanceDiffusionTracking},
|
"AppendInstanceDiffusionTracking": {"class": AppendInstanceDiffusionTracking},
|
||||||
"DrawInstanceDiffusionTracking": {"class": DrawInstanceDiffusionTracking},
|
"DrawInstanceDiffusionTracking": {"class": DrawInstanceDiffusionTracking},
|
||||||
|
|
||||||
|
#lora
|
||||||
|
"LoraExtractKJ": {"class": LoraExtractKJ, "name": "LoraExtractKJ"},
|
||||||
|
"LoraReduceRankKJ": {"class": LoraReduceRank, "name": "LoraReduceRank"}
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_node_mappings(node_config):
|
def generate_node_mappings(node_config):
|
||||||
@@ -140,9 +236,11 @@ from server import PromptServer
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
if hasattr(PromptServer, "instance"):
|
if hasattr(PromptServer, "instance"):
|
||||||
|
try:
|
||||||
# NOTE: we add an extra static path to avoid comfy mechanism
|
# NOTE: we add an extra static path to avoid comfy mechanism
|
||||||
# that loads every script in web.
|
# that loads every script in web.
|
||||||
PromptServer.instance.app.add_routes(
|
PromptServer.instance.app.add_routes(
|
||||||
[web.static("/kjweb_async", (Path(__file__).parent.absolute() / "kjweb_async").as_posix())]
|
[web.static("/kjweb_async", (Path(__file__).parent.absolute() / "kjweb_async").as_posix())]
|
||||||
)
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"sai_api_key": "your_api_key_here"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "SD",
|
||||||
|
"value": "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "HD",
|
||||||
|
"value": "768x768"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Full HD",
|
||||||
|
"value": "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "4k",
|
||||||
|
"value": "2048x2048"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "SVD",
|
||||||
|
"value": "1024x576"
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -694,6 +694,7 @@ class BboxVisualize:
|
|||||||
"images": ("IMAGE",),
|
"images": ("IMAGE",),
|
||||||
"bboxes": ("BBOX",),
|
"bboxes": ("BBOX",),
|
||||||
"line_width": ("INT", {"default": 1,"min": 1, "max": 10, "step": 1}),
|
"line_width": ("INT", {"default": 1,"min": 1, "max": 10, "step": 1}),
|
||||||
|
"bbox_format": (["xywh", "xyxy"], {"default": "xywh"}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,31 +707,56 @@ Visualizes the specified bbox on the image.
|
|||||||
|
|
||||||
CATEGORY = "KJNodes/masking"
|
CATEGORY = "KJNodes/masking"
|
||||||
|
|
||||||
def visualizebbox(self, bboxes, images, line_width):
|
def visualizebbox(self, bboxes, images, line_width, bbox_format):
|
||||||
image_list = []
|
image_list = []
|
||||||
for image, bbox in zip(images, bboxes):
|
for image, bbox in zip(images, bboxes):
|
||||||
|
if bbox_format == "xywh":
|
||||||
x_min, y_min, width, height = bbox
|
x_min, y_min, width, height = bbox
|
||||||
|
elif bbox_format == "xyxy":
|
||||||
|
x_min, y_min, x_max, y_max = bbox
|
||||||
|
width = x_max - x_min
|
||||||
|
height = y_max - y_min
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown bbox_format: {bbox_format}")
|
||||||
|
|
||||||
|
# Ensure bbox coordinates are integers
|
||||||
|
x_min = int(x_min)
|
||||||
|
y_min = int(y_min)
|
||||||
|
width = int(width)
|
||||||
|
height = int(height)
|
||||||
|
|
||||||
|
# Permute the image dimensions
|
||||||
image = image.permute(2, 0, 1)
|
image = image.permute(2, 0, 1)
|
||||||
|
|
||||||
|
# Clone the image to draw bounding boxes
|
||||||
img_with_bbox = image.clone()
|
img_with_bbox = image.clone()
|
||||||
|
|
||||||
# Define the color for the bbox, e.g., red
|
# Define the color for the bbox, e.g., red
|
||||||
color = torch.tensor([1, 0, 0], dtype=torch.float32)
|
color = torch.tensor([1, 0, 0], dtype=torch.float32)
|
||||||
|
|
||||||
|
# Ensure color tensor matches the image channels
|
||||||
|
if color.shape[0] != img_with_bbox.shape[0]:
|
||||||
|
color = color.unsqueeze(1).expand(-1, line_width)
|
||||||
|
|
||||||
# Draw lines for each side of the bbox with the specified line width
|
# Draw lines for each side of the bbox with the specified line width
|
||||||
for lw in range(line_width):
|
for lw in range(line_width):
|
||||||
# Top horizontal line
|
# Top horizontal line
|
||||||
|
if y_min + lw < img_with_bbox.shape[1]:
|
||||||
img_with_bbox[:, y_min + lw, x_min:x_min + width] = color[:, None]
|
img_with_bbox[:, y_min + lw, x_min:x_min + width] = color[:, None]
|
||||||
|
|
||||||
# Bottom horizontal line
|
# Bottom horizontal line
|
||||||
|
if y_min + height - lw < img_with_bbox.shape[1]:
|
||||||
img_with_bbox[:, y_min + height - lw, x_min:x_min + width] = color[:, None]
|
img_with_bbox[:, y_min + height - lw, x_min:x_min + width] = color[:, None]
|
||||||
|
|
||||||
# Left vertical line
|
# Left vertical line
|
||||||
|
if x_min + lw < img_with_bbox.shape[2]:
|
||||||
img_with_bbox[:, y_min:y_min + height, x_min + lw] = color[:, None]
|
img_with_bbox[:, y_min:y_min + height, x_min + lw] = color[:, None]
|
||||||
|
|
||||||
# Right vertical line
|
# Right vertical line
|
||||||
|
if x_min + width - lw < img_with_bbox.shape[2]:
|
||||||
img_with_bbox[:, y_min:y_min + height, x_min + width - lw] = color[:, None]
|
img_with_bbox[:, y_min:y_min + height, x_min + width - lw] = color[:, None]
|
||||||
|
|
||||||
|
# Permute the image dimensions back
|
||||||
img_with_bbox = img_with_bbox.permute(1, 2, 0).unsqueeze(0)
|
img_with_bbox = img_with_bbox.permute(1, 2, 0).unsqueeze(0)
|
||||||
image_list.append(img_with_bbox)
|
image_list.append(img_with_bbox)
|
||||||
|
|
||||||
|
|||||||
+719
-35
@@ -1,10 +1,49 @@
|
|||||||
import torch
|
import torch
|
||||||
from torchvision import transforms
|
from torchvision import transforms
|
||||||
import json
|
import json
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont, ImageColor, ImageFilter, ImageChops
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from ..utility.utility import pil2tensor
|
from ..utility.utility import pil2tensor, tensor2pil
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from comfy.utils import common_upscale
|
||||||
|
|
||||||
|
def parse_color(color):
|
||||||
|
if isinstance(color, str) and ',' in color:
|
||||||
|
return tuple(int(c.strip()) for c in color.split(','))
|
||||||
|
return color
|
||||||
|
|
||||||
|
def parse_json_tracks(tracks):
|
||||||
|
tracks_data = []
|
||||||
|
try:
|
||||||
|
# If tracks is a string, try to parse it as JSON
|
||||||
|
if isinstance(tracks, str):
|
||||||
|
parsed = json.loads(tracks.replace("'", '"'))
|
||||||
|
tracks_data.extend(parsed)
|
||||||
|
else:
|
||||||
|
# If tracks is a list of strings, parse each one
|
||||||
|
for track_str in tracks:
|
||||||
|
parsed = json.loads(track_str.replace("'", '"'))
|
||||||
|
tracks_data.append(parsed)
|
||||||
|
|
||||||
|
# Check if we have a single track (dict with x,y) or a list of tracks
|
||||||
|
if tracks_data and isinstance(tracks_data[0], dict) and 'x' in tracks_data[0]:
|
||||||
|
# Single track detected, wrap it in a list
|
||||||
|
tracks_data = [tracks_data]
|
||||||
|
elif tracks_data and isinstance(tracks_data[0], list) and tracks_data[0] and isinstance(tracks_data[0][0], dict) and 'x' in tracks_data[0][0]:
|
||||||
|
# Already a list of tracks, nothing to do
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Unexpected format
|
||||||
|
print(f"Warning: Unexpected track format: {type(tracks_data[0])}")
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"Error parsing tracks JSON: {e}")
|
||||||
|
tracks_data = []
|
||||||
|
|
||||||
|
return tracks_data
|
||||||
|
|
||||||
def plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, prompt):
|
def plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, prompt):
|
||||||
import matplotlib
|
import matplotlib
|
||||||
@@ -91,7 +130,9 @@ Plots coordinates to sequence of images using Matplotlib.
|
|||||||
coordinates = json.loads(coordinates.replace("'", '"'))
|
coordinates = json.loads(coordinates.replace("'", '"'))
|
||||||
coordinates = [(coord['x'], coord['y']) for coord in coordinates]
|
coordinates = [(coord['x'], coord['y']) for coord in coordinates]
|
||||||
batch_size = len(coordinates)
|
batch_size = len(coordinates)
|
||||||
if len(size_multiplier) != batch_size:
|
if not size_multiplier or len(size_multiplier) != batch_size:
|
||||||
|
size_multiplier = [0] * batch_size
|
||||||
|
else:
|
||||||
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
|
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
|
||||||
|
|
||||||
plot_image_tensor = plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, text)
|
plot_image_tensor = plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, text)
|
||||||
@@ -113,6 +154,8 @@ class SplineEditor:
|
|||||||
[
|
[
|
||||||
'path',
|
'path',
|
||||||
'time',
|
'time',
|
||||||
|
'controlpoints',
|
||||||
|
'speed'
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
"default": 'time'
|
"default": 'time'
|
||||||
@@ -146,11 +189,12 @@ class SplineEditor:
|
|||||||
"optional": {
|
"optional": {
|
||||||
"min_value": ("FLOAT", {"default": 0.0, "min": -10000.0, "max": 10000.0, "step": 0.01}),
|
"min_value": ("FLOAT", {"default": 0.0, "min": -10000.0, "max": 10000.0, "step": 0.01}),
|
||||||
"max_value": ("FLOAT", {"default": 1.0, "min": -10000.0, "max": 10000.0, "step": 0.01}),
|
"max_value": ("FLOAT", {"default": 1.0, "min": -10000.0, "max": 10000.0, "step": 0.01}),
|
||||||
|
"bg_image": ("IMAGE", ),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("MASK", "STRING", "FLOAT", "INT")
|
RETURN_TYPES = ("MASK", "STRING", "FLOAT", "INT", "STRING",)
|
||||||
RETURN_NAMES = ("mask", "coord_str", "float", "count")
|
RETURN_NAMES = ("mask", "coord_str", "float", "count", "normalized_str",)
|
||||||
FUNCTION = "splinedata"
|
FUNCTION = "splinedata"
|
||||||
CATEGORY = "KJNodes/weights"
|
CATEGORY = "KJNodes/weights"
|
||||||
DESCRIPTION = """
|
DESCRIPTION = """
|
||||||
@@ -168,6 +212,16 @@ guaranteed!!
|
|||||||
Note that you can't delete from start/end.
|
Note that you can't delete from start/end.
|
||||||
|
|
||||||
Right click on canvas for context menu:
|
Right click on canvas for context menu:
|
||||||
|
NEW!:
|
||||||
|
- Add new spline
|
||||||
|
- Creates a new spline on same canvas, currently these paths are only outputed
|
||||||
|
as coordinates.
|
||||||
|
- Add single point
|
||||||
|
- Creates a single point that only returns it's current position coords
|
||||||
|
- Delete spline
|
||||||
|
- Deletes the currently selected spline, you can select a spline by clicking on
|
||||||
|
it's path, or cycle through them with the 'Next spline' -option.
|
||||||
|
|
||||||
These are purely visual options, doesn't affect the output:
|
These are purely visual options, doesn't affect the output:
|
||||||
- Toggle handles visibility
|
- Toggle handles visibility
|
||||||
- Display sample points: display the points to be returned.
|
- Display sample points: display the points to be returned.
|
||||||
@@ -178,6 +232,7 @@ actual control points, so the interpolation type matters.
|
|||||||
sampling_method:
|
sampling_method:
|
||||||
- time: samples along the time axis, used for schedules
|
- time: samples along the time axis, used for schedules
|
||||||
- path: samples along the path itself, useful for coordinates
|
- path: samples along the path itself, useful for coordinates
|
||||||
|
- controlpoints: samples only the control points themselves
|
||||||
|
|
||||||
output types:
|
output types:
|
||||||
- mask batch
|
- mask batch
|
||||||
@@ -192,36 +247,77 @@ output types:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def splinedata(self, mask_width, mask_height, coordinates, float_output_type, interpolation,
|
def splinedata(self, mask_width, mask_height, coordinates, float_output_type, interpolation,
|
||||||
points_to_sample, sampling_method, points_store, tension, repeat_output, min_value=0.0, max_value=1.0):
|
points_to_sample, sampling_method, points_store, tension, repeat_output,
|
||||||
|
min_value=0.0, max_value=1.0, bg_image=None):
|
||||||
|
|
||||||
coordinates = json.loads(coordinates)
|
coordinates = json.loads(coordinates)
|
||||||
for coord in coordinates:
|
|
||||||
|
# Handle nested list structure if present
|
||||||
|
all_normalized = []
|
||||||
|
all_normalized_y_values = []
|
||||||
|
|
||||||
|
# Check if we have a nested list structure
|
||||||
|
if isinstance(coordinates, list) and len(coordinates) > 0 and isinstance(coordinates[0], list):
|
||||||
|
# Process each list of coordinates in the nested structure
|
||||||
|
coordinate_sets = coordinates
|
||||||
|
else:
|
||||||
|
# If not nested, treat as a single list of coordinates
|
||||||
|
coordinate_sets = [coordinates]
|
||||||
|
|
||||||
|
# Process each set of coordinates
|
||||||
|
for coord_set in coordinate_sets:
|
||||||
|
normalized = []
|
||||||
|
normalized_y_values = []
|
||||||
|
|
||||||
|
for coord in coord_set:
|
||||||
coord['x'] = int(round(coord['x']))
|
coord['x'] = int(round(coord['x']))
|
||||||
coord['y'] = int(round(coord['y']))
|
coord['y'] = int(round(coord['y']))
|
||||||
|
norm_x = (1.0 - (coord['x'] / mask_height) - 0.0) * (max_value - min_value) + min_value
|
||||||
|
norm_y = (1.0 - (coord['y'] / mask_height) - 0.0) * (max_value - min_value) + min_value
|
||||||
|
normalized_y_values.append(norm_y)
|
||||||
|
normalized.append({'x':norm_x, 'y':norm_y})
|
||||||
|
|
||||||
normalized_y_values = [
|
all_normalized.extend(normalized)
|
||||||
(1.0 - (point['y'] / mask_height) - 0.0) * (max_value - min_value) + min_value
|
all_normalized_y_values.extend(normalized_y_values)
|
||||||
for point in coordinates
|
|
||||||
]
|
# Use the combined normalized values for output
|
||||||
if float_output_type == 'list':
|
if float_output_type == 'list':
|
||||||
out_floats = normalized_y_values * repeat_output
|
out_floats = all_normalized_y_values * repeat_output
|
||||||
elif float_output_type == 'pandas series':
|
elif float_output_type == 'pandas series':
|
||||||
try:
|
try:
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
except:
|
except:
|
||||||
raise Exception("MaskOrImageToWeight: pandas is not installed. Please install pandas to use this output_type")
|
raise Exception("MaskOrImageToWeight: pandas is not installed. Please install pandas to use this output_type")
|
||||||
out_floats = pd.Series(normalized_y_values * repeat_output),
|
out_floats = pd.Series(all_normalized_y_values * repeat_output),
|
||||||
elif float_output_type == 'tensor':
|
elif float_output_type == 'tensor':
|
||||||
out_floats = torch.tensor(normalized_y_values * repeat_output, dtype=torch.float32)
|
out_floats = torch.tensor(all_normalized_y_values * repeat_output, dtype=torch.float32)
|
||||||
|
|
||||||
# Create a color map for grayscale intensities
|
# Create a color map for grayscale intensities
|
||||||
color_map = lambda y: torch.full((mask_height, mask_width, 3), y, dtype=torch.float32)
|
color_map = lambda y: torch.full((mask_height, mask_width, 3), y, dtype=torch.float32)
|
||||||
|
|
||||||
# Create image tensors for each normalized y value
|
# Create image tensors for each normalized y value
|
||||||
mask_tensors = [color_map(y) for y in normalized_y_values]
|
mask_tensors = [color_map(y) for y in all_normalized_y_values]
|
||||||
masks_out = torch.stack(mask_tensors)
|
masks_out = torch.stack(mask_tensors)
|
||||||
masks_out = masks_out.repeat(repeat_output, 1, 1, 1)
|
masks_out = masks_out.repeat(repeat_output, 1, 1, 1)
|
||||||
masks_out = masks_out.mean(dim=-1)
|
masks_out = masks_out.mean(dim=-1)
|
||||||
return (masks_out, str(coordinates), out_floats, len(out_floats))
|
|
||||||
|
if bg_image is None:
|
||||||
|
return (masks_out, json.dumps(coordinates if len(coordinates) > 1 else coordinates[0]), out_floats, len(out_floats), json.dumps(all_normalized))
|
||||||
|
else:
|
||||||
|
transform = transforms.ToPILImage()
|
||||||
|
image = transform(bg_image[0].permute(2, 0, 1))
|
||||||
|
buffered = io.BytesIO()
|
||||||
|
image.save(buffered, format="JPEG", quality=75)
|
||||||
|
|
||||||
|
# Encode the image bytes to a Base64 string
|
||||||
|
img_bytes = buffered.getvalue()
|
||||||
|
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ui": {"bg_image": [img_base64]},
|
||||||
|
"result": (masks_out, json.dumps(coordinates if len(coordinates) > 1 else coordinates[0]), out_floats, len(out_floats), json.dumps(all_normalized))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreateShapeMaskOnPath:
|
class CreateShapeMaskOnPath:
|
||||||
|
|
||||||
@@ -232,8 +328,8 @@ class CreateShapeMaskOnPath:
|
|||||||
DESCRIPTION = """
|
DESCRIPTION = """
|
||||||
Creates a mask or batch of masks with the specified shape.
|
Creates a mask or batch of masks with the specified shape.
|
||||||
Locations are center locations.
|
Locations are center locations.
|
||||||
Grow value is the amount to grow the shape on each frame, creating animated masks.
|
|
||||||
"""
|
"""
|
||||||
|
DEPRECATED = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
@@ -266,7 +362,9 @@ Grow value is the amount to grow the shape on each frame, creating animated mask
|
|||||||
batch_size = len(coordinates)
|
batch_size = len(coordinates)
|
||||||
out = []
|
out = []
|
||||||
color = "white"
|
color = "white"
|
||||||
if len(size_multiplier) != batch_size:
|
if not size_multiplier or len(size_multiplier) != batch_size:
|
||||||
|
size_multiplier = [0] * batch_size
|
||||||
|
else:
|
||||||
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
|
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
|
||||||
for i, coord in enumerate(coordinates):
|
for i, coord in enumerate(coordinates):
|
||||||
image = Image.new("RGB", (frame_width, frame_height), "black")
|
image = Image.new("RGB", (frame_width, frame_height), "black")
|
||||||
@@ -303,6 +401,320 @@ Grow value is the amount to grow the shape on each frame, creating animated mask
|
|||||||
outstack = torch.cat(out, dim=0)
|
outstack = torch.cat(out, dim=0)
|
||||||
return (outstack, 1.0 - outstack,)
|
return (outstack, 1.0 - outstack,)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CreateShapeImageOnPath:
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE", "MASK",)
|
||||||
|
RETURN_NAMES = ("image","mask", )
|
||||||
|
FUNCTION = "createshapemask"
|
||||||
|
CATEGORY = "KJNodes/image"
|
||||||
|
DESCRIPTION = """
|
||||||
|
Creates an image or batch of images with the specified shape.
|
||||||
|
Locations are center locations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"shape": (
|
||||||
|
[ 'circle',
|
||||||
|
'square',
|
||||||
|
'triangle',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"default": 'circle'
|
||||||
|
}),
|
||||||
|
"coordinates": ("STRING", {"forceInput": True}),
|
||||||
|
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
|
||||||
|
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
|
||||||
|
"shape_width": ("INT", {"default": 128,"min": 2, "max": 4096, "step": 1}),
|
||||||
|
"shape_height": ("INT", {"default": 128,"min": 2, "max": 4096, "step": 1}),
|
||||||
|
"shape_color": ("STRING", {"default": 'white'}),
|
||||||
|
"bg_color": ("STRING", {"default": 'black'}),
|
||||||
|
"blur_radius": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100, "step": 0.1}),
|
||||||
|
"intensity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True}),
|
||||||
|
"trailing": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
|
||||||
|
"border_width": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
|
||||||
|
"border_color": ("STRING", {"default": 'black'}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def createshapemask(self, coordinates, frame_width, frame_height, shape_width, shape_height, shape_color,
|
||||||
|
bg_color, blur_radius, shape, intensity, size_multiplier=[1.0], trailing=1.0, border_width=0, border_color='black'):
|
||||||
|
|
||||||
|
shape_color = parse_color(shape_color)
|
||||||
|
border_color = parse_color(border_color)
|
||||||
|
bg_color = parse_color(bg_color)
|
||||||
|
coords_list = parse_json_tracks(coordinates)
|
||||||
|
|
||||||
|
batch_size = len(coords_list[0])
|
||||||
|
images_list = []
|
||||||
|
masks_list = []
|
||||||
|
|
||||||
|
if not size_multiplier or len(size_multiplier) != batch_size:
|
||||||
|
size_multiplier = [1] * batch_size
|
||||||
|
else:
|
||||||
|
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
|
||||||
|
|
||||||
|
previous_output = None
|
||||||
|
|
||||||
|
for i in range(batch_size):
|
||||||
|
image = Image.new("RGB", (frame_width, frame_height), bg_color)
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
|
# Calculate the size for this frame and ensure it's not less than 0
|
||||||
|
current_width = shape_width * size_multiplier[i]
|
||||||
|
current_height = shape_height * size_multiplier[i]
|
||||||
|
|
||||||
|
for coords in coords_list:
|
||||||
|
location_x = coords[i]['x']
|
||||||
|
location_y = coords[i]['y']
|
||||||
|
|
||||||
|
if shape == 'circle' or shape == 'square':
|
||||||
|
# Define the bounding box for the shape
|
||||||
|
left_up_point = (location_x - current_width // 2, location_y - current_height // 2)
|
||||||
|
right_down_point = (location_x + current_width // 2, location_y + current_height // 2)
|
||||||
|
two_points = [left_up_point, right_down_point]
|
||||||
|
|
||||||
|
if shape == 'circle':
|
||||||
|
if border_width > 0:
|
||||||
|
draw.ellipse(two_points, fill=shape_color, outline=border_color, width=border_width)
|
||||||
|
else:
|
||||||
|
draw.ellipse(two_points, fill=shape_color)
|
||||||
|
elif shape == 'square':
|
||||||
|
if border_width > 0:
|
||||||
|
draw.rectangle(two_points, fill=shape_color, outline=border_color, width=border_width)
|
||||||
|
else:
|
||||||
|
draw.rectangle(two_points, fill=shape_color)
|
||||||
|
|
||||||
|
elif shape == 'triangle':
|
||||||
|
# Define the points for the triangle
|
||||||
|
left_up_point = (location_x - current_width // 2, location_y + current_height // 2) # bottom left
|
||||||
|
right_down_point = (location_x + current_width // 2, location_y + current_height // 2) # bottom right
|
||||||
|
top_point = (location_x, location_y - current_height // 2) # top point
|
||||||
|
|
||||||
|
if border_width > 0:
|
||||||
|
draw.polygon([top_point, left_up_point, right_down_point], fill=shape_color, outline=border_color, width=border_width)
|
||||||
|
else:
|
||||||
|
draw.polygon([top_point, left_up_point, right_down_point], fill=shape_color)
|
||||||
|
|
||||||
|
if blur_radius != 0:
|
||||||
|
image = image.filter(ImageFilter.GaussianBlur(blur_radius))
|
||||||
|
# Blend the current image with the accumulated image
|
||||||
|
|
||||||
|
image = pil2tensor(image)
|
||||||
|
if trailing != 1.0 and previous_output is not None:
|
||||||
|
# Add the decayed previous output to the current frame
|
||||||
|
image += trailing * previous_output
|
||||||
|
image = image / image.max()
|
||||||
|
previous_output = image
|
||||||
|
image = image * intensity
|
||||||
|
mask = image[:, :, :, 0]
|
||||||
|
masks_list.append(mask)
|
||||||
|
images_list.append(image)
|
||||||
|
out_images = torch.cat(images_list, dim=0).cpu().float()
|
||||||
|
out_masks = torch.cat(masks_list, dim=0)
|
||||||
|
return (out_images, out_masks)
|
||||||
|
|
||||||
|
class CreateTextOnPath:
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE", "MASK", "MASK",)
|
||||||
|
RETURN_NAMES = ("image", "mask", "mask_inverted",)
|
||||||
|
FUNCTION = "createtextmask"
|
||||||
|
CATEGORY = "KJNodes/masking/generate"
|
||||||
|
DESCRIPTION = """
|
||||||
|
Creates a mask or batch of masks with the specified text.
|
||||||
|
Locations are center locations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"coordinates": ("STRING", {"forceInput": True}),
|
||||||
|
"text": ("STRING", {"default": 'text', "multiline": True}),
|
||||||
|
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
|
||||||
|
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
|
||||||
|
"font": (folder_paths.get_filename_list("kjnodes_fonts"), ),
|
||||||
|
"font_size": ("INT", {"default": 42}),
|
||||||
|
"alignment": (
|
||||||
|
[ 'left',
|
||||||
|
'center',
|
||||||
|
'right'
|
||||||
|
],
|
||||||
|
{"default": 'center'}
|
||||||
|
),
|
||||||
|
"text_color": ("STRING", {"default": 'white'}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def createtextmask(self, coordinates, frame_width, frame_height, font, font_size, text, text_color, alignment, size_multiplier=[1.0]):
|
||||||
|
coordinates = coordinates.replace("'", '"')
|
||||||
|
coordinates = json.loads(coordinates)
|
||||||
|
|
||||||
|
batch_size = len(coordinates)
|
||||||
|
mask_list = []
|
||||||
|
image_list = []
|
||||||
|
color = parse_color(text_color)
|
||||||
|
font_path = folder_paths.get_full_path("kjnodes_fonts", font)
|
||||||
|
|
||||||
|
if len(size_multiplier) != batch_size:
|
||||||
|
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
|
||||||
|
|
||||||
|
for i, coord in enumerate(coordinates):
|
||||||
|
image = Image.new("RGB", (frame_width, frame_height), "black")
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
lines = text.split('\n') # Split the text into lines
|
||||||
|
# Apply the size multiplier to the font size for this iteration
|
||||||
|
current_font_size = int(font_size * size_multiplier[i])
|
||||||
|
current_font = ImageFont.truetype(font_path, current_font_size)
|
||||||
|
line_heights = [current_font.getbbox(line)[3] for line in lines] # List of line heights
|
||||||
|
total_text_height = sum(line_heights) # Total height of text block
|
||||||
|
|
||||||
|
# Calculate the starting Y position to center the block of text
|
||||||
|
start_y = coord['y'] - total_text_height // 2
|
||||||
|
for j, line in enumerate(lines):
|
||||||
|
text_width, text_height = current_font.getbbox(line)[2], line_heights[j]
|
||||||
|
if alignment == 'left':
|
||||||
|
location_x = coord['x']
|
||||||
|
elif alignment == 'center':
|
||||||
|
location_x = int(coord['x'] - text_width // 2)
|
||||||
|
elif alignment == 'right':
|
||||||
|
location_x = int(coord['x'] - text_width)
|
||||||
|
|
||||||
|
location_y = int(start_y + sum(line_heights[:j]))
|
||||||
|
text_position = (location_x, location_y)
|
||||||
|
# Draw the text
|
||||||
|
try:
|
||||||
|
draw.text(text_position, line, fill=color, font=current_font, features=['-liga'])
|
||||||
|
except:
|
||||||
|
draw.text(text_position, line, fill=color, font=current_font)
|
||||||
|
|
||||||
|
image = pil2tensor(image)
|
||||||
|
non_black_pixels = (image > 0).any(dim=-1)
|
||||||
|
mask = non_black_pixels.to(image.dtype)
|
||||||
|
mask_list.append(mask)
|
||||||
|
image_list.append(image)
|
||||||
|
|
||||||
|
out_images = torch.cat(image_list, dim=0).cpu().float()
|
||||||
|
out_masks = torch.cat(mask_list, dim=0)
|
||||||
|
return (out_images, out_masks, 1.0 - out_masks,)
|
||||||
|
|
||||||
|
class CreateGradientFromCoords:
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE", )
|
||||||
|
RETURN_NAMES = ("image", )
|
||||||
|
FUNCTION = "generate"
|
||||||
|
CATEGORY = "KJNodes/image"
|
||||||
|
DESCRIPTION = """
|
||||||
|
Creates a gradient image from coordinates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"coordinates": ("STRING", {"forceInput": True}),
|
||||||
|
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
|
||||||
|
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
|
||||||
|
"start_color": ("STRING", {"default": 'white'}),
|
||||||
|
"end_color": ("STRING", {"default": 'black'}),
|
||||||
|
"multiplier": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate(self, coordinates, frame_width, frame_height, start_color, end_color, multiplier):
|
||||||
|
# Parse the coordinates
|
||||||
|
coordinates = json.loads(coordinates.replace("'", '"'))
|
||||||
|
|
||||||
|
# Create an image
|
||||||
|
image = Image.new("RGB", (frame_width, frame_height))
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
|
# Extract start and end points for the gradient
|
||||||
|
start_coord = coordinates[0]
|
||||||
|
end_coord = coordinates[1]
|
||||||
|
|
||||||
|
start_color = parse_color(start_color)
|
||||||
|
end_color = parse_color(end_color)
|
||||||
|
|
||||||
|
# Calculate the gradient direction (vector)
|
||||||
|
gradient_direction = (end_coord['x'] - start_coord['x'], end_coord['y'] - start_coord['y'])
|
||||||
|
gradient_length = (gradient_direction[0] ** 2 + gradient_direction[1] ** 2) ** 0.5
|
||||||
|
|
||||||
|
# Iterate over each pixel in the image
|
||||||
|
for y in range(frame_height):
|
||||||
|
for x in range(frame_width):
|
||||||
|
# Calculate the projection of the point on the gradient line
|
||||||
|
point_vector = (x - start_coord['x'], y - start_coord['y'])
|
||||||
|
projection = (point_vector[0] * gradient_direction[0] + point_vector[1] * gradient_direction[1]) / gradient_length
|
||||||
|
projection = max(min(projection, gradient_length), 0) # Clamp the projection value
|
||||||
|
|
||||||
|
# Calculate the blend factor for the current pixel
|
||||||
|
blend = projection * multiplier / gradient_length
|
||||||
|
|
||||||
|
# Determine the color of the current pixel
|
||||||
|
color = (
|
||||||
|
int(start_color[0] + (end_color[0] - start_color[0]) * blend),
|
||||||
|
int(start_color[1] + (end_color[1] - start_color[1]) * blend),
|
||||||
|
int(start_color[2] + (end_color[2] - start_color[2]) * blend)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the pixel color
|
||||||
|
draw.point((x, y), fill=color)
|
||||||
|
|
||||||
|
# Convert the PIL image to a tensor (assuming such a function exists in your context)
|
||||||
|
image_tensor = pil2tensor(image)
|
||||||
|
|
||||||
|
return (image_tensor,)
|
||||||
|
|
||||||
|
class GradientToFloat:
|
||||||
|
|
||||||
|
RETURN_TYPES = ("FLOAT", "FLOAT",)
|
||||||
|
RETURN_NAMES = ("float_x", "float_y", )
|
||||||
|
FUNCTION = "sample"
|
||||||
|
CATEGORY = "KJNodes/image"
|
||||||
|
DESCRIPTION = """
|
||||||
|
Calculates list of floats from image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE", ),
|
||||||
|
"steps": ("INT", {"default": 10, "min": 2, "max": 10000, "step": 1}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def sample(self, image, steps):
|
||||||
|
# Assuming image is a tensor with shape [B, H, W, C]
|
||||||
|
B, H, W, C = image.shape
|
||||||
|
|
||||||
|
# Sample along the width axis (W)
|
||||||
|
w_intervals = torch.linspace(0, W - 1, steps=steps, dtype=torch.int64)
|
||||||
|
# Assuming we're sampling from the first batch and the first channel
|
||||||
|
w_sampled = image[0, :, w_intervals, 0]
|
||||||
|
|
||||||
|
# Sample along the height axis (H)
|
||||||
|
h_intervals = torch.linspace(0, H - 1, steps=steps, dtype=torch.int64)
|
||||||
|
# Assuming we're sampling from the first batch and the first channel
|
||||||
|
h_sampled = image[0, h_intervals, :, 0]
|
||||||
|
|
||||||
|
# Taking the mean across the height for width sampling, and across the width for height sampling
|
||||||
|
w_values = w_sampled.mean(dim=0).tolist()
|
||||||
|
h_values = h_sampled.mean(dim=1).tolist()
|
||||||
|
|
||||||
|
return (w_values, h_values)
|
||||||
|
|
||||||
class MaskOrImageToWeight:
|
class MaskOrImageToWeight:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -347,7 +759,7 @@ and returns that as the selected output type.
|
|||||||
|
|
||||||
# Convert mean_values to the specified output_type
|
# Convert mean_values to the specified output_type
|
||||||
if output_type == 'list':
|
if output_type == 'list':
|
||||||
out = mean_values,
|
out = mean_values
|
||||||
elif output_type == 'pandas series':
|
elif output_type == 'pandas series':
|
||||||
try:
|
try:
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -604,6 +1016,25 @@ Creates a sigmas tensor from list of float values.
|
|||||||
def customsigmas(self, float_list):
|
def customsigmas(self, float_list):
|
||||||
return torch.tensor(float_list, dtype=torch.float32),
|
return torch.tensor(float_list, dtype=torch.float32),
|
||||||
|
|
||||||
|
class SigmasToFloat:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {"required":
|
||||||
|
{
|
||||||
|
"sigmas": ("SIGMAS",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RETURN_TYPES = ("FLOAT",)
|
||||||
|
RETURN_NAMES = ("float",)
|
||||||
|
CATEGORY = "KJNodes/noise"
|
||||||
|
FUNCTION = "customsigmas"
|
||||||
|
DESCRIPTION = """
|
||||||
|
Creates a float list from sigmas tensors.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def customsigmas(self, sigmas):
|
||||||
|
return sigmas.tolist(),
|
||||||
|
|
||||||
class GLIGENTextBoxApplyBatchCoords:
|
class GLIGENTextBoxApplyBatchCoords:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
@@ -732,7 +1163,9 @@ for example:
|
|||||||
batch_size = len(coordinates)
|
batch_size = len(coordinates)
|
||||||
# Initialize a list to hold the coordinates for the current ID
|
# Initialize a list to hold the coordinates for the current ID
|
||||||
id_coordinates = []
|
id_coordinates = []
|
||||||
if len(size_multiplier) != batch_size:
|
if not size_multiplier or len(size_multiplier) != batch_size:
|
||||||
|
size_multiplier = [0] * batch_size
|
||||||
|
else:
|
||||||
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
|
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
|
||||||
for i, coord in enumerate(coordinates):
|
for i, coord in enumerate(coordinates):
|
||||||
x = coord['x']
|
x = coord['x']
|
||||||
@@ -751,6 +1184,13 @@ for example:
|
|||||||
top_left_y = max(0, top_left_y)
|
top_left_y = max(0, top_left_y)
|
||||||
bottom_right_x = min(width, bottom_right_x)
|
bottom_right_x = min(width, bottom_right_x)
|
||||||
bottom_right_y = min(height, bottom_right_y)
|
bottom_right_y = min(height, bottom_right_y)
|
||||||
|
# Ensure width and height are positive
|
||||||
|
adjusted_bbox_width = max(1, bottom_right_x - top_left_x)
|
||||||
|
adjusted_bbox_height = max(1, bottom_right_y - top_left_y)
|
||||||
|
|
||||||
|
# Update the coordinates with the new width and height
|
||||||
|
bottom_right_x = top_left_x + adjusted_bbox_width
|
||||||
|
bottom_right_y = top_left_y + adjusted_bbox_height
|
||||||
|
|
||||||
# Append the top left and bottom right coordinates to the list for the current ID
|
# Append the top left and bottom right coordinates to the list for the current ID
|
||||||
id_coordinates.append([top_left_x, top_left_y, bottom_right_x, bottom_right_y, width, height])
|
id_coordinates.append([top_left_x, top_left_y, bottom_right_x, bottom_right_y, width, height])
|
||||||
@@ -837,41 +1277,44 @@ Interpolates coordinates based on a curve.
|
|||||||
coordinates = [(coord['x'], coord['y']) for coord in coordinates]
|
coordinates = [(coord['x'], coord['y']) for coord in coordinates]
|
||||||
|
|
||||||
# Calculate the total length of the original path
|
# Calculate the total length of the original path
|
||||||
path_length = sum(np.linalg.norm(np.array(coordinates[i]) - np.array(coordinates[i-1])) for i in range(1, len(coordinates)))
|
path_length = sum(np.linalg.norm(np.array(coordinates[i]) - np.array(coordinates[i-1]))
|
||||||
|
for i in range(1, len(coordinates)))
|
||||||
# Normalize the interpolation curve
|
|
||||||
normalized_curve = [x / path_length for x in interpolation_curve]
|
|
||||||
|
|
||||||
# Initialize variables for interpolation
|
# Initialize variables for interpolation
|
||||||
interpolated_coords = []
|
interpolated_coords = []
|
||||||
current_length = 0
|
current_length = 0
|
||||||
current_index = 1
|
current_index = 0
|
||||||
|
|
||||||
# Iterate over the normalized curve
|
# Iterate over the normalized curve
|
||||||
for target_length in normalized_curve:
|
for normalized_length in interpolation_curve:
|
||||||
target_length *= path_length # Convert back to the original scale
|
target_length = normalized_length * path_length # Convert to the original scale
|
||||||
while current_length < target_length and current_index < len(coordinates):
|
while current_index < len(coordinates) - 1:
|
||||||
segment_length = np.linalg.norm(np.array(coordinates[current_index]) - np.array(coordinates[current_index-1]))
|
segment_start, segment_end = np.array(coordinates[current_index]), np.array(coordinates[current_index + 1])
|
||||||
|
segment_length = np.linalg.norm(segment_end - segment_start)
|
||||||
|
if current_length + segment_length >= target_length:
|
||||||
|
break
|
||||||
current_length += segment_length
|
current_length += segment_length
|
||||||
current_index += 1
|
current_index += 1
|
||||||
|
|
||||||
# Interpolate between the last two points
|
# Interpolate between the last two points
|
||||||
if current_index == 1:
|
if current_index < len(coordinates) - 1:
|
||||||
interpolated_coords.append(coordinates[0])
|
p1, p2 = np.array(coordinates[current_index]), np.array(coordinates[current_index + 1])
|
||||||
else:
|
|
||||||
p1, p2 = np.array(coordinates[current_index-2]), np.array(coordinates[current_index-1])
|
|
||||||
segment_length = np.linalg.norm(p2 - p1)
|
segment_length = np.linalg.norm(p2 - p1)
|
||||||
if segment_length > 0:
|
if segment_length > 0:
|
||||||
t = (target_length - (current_length - segment_length)) / segment_length
|
t = (target_length - current_length) / segment_length
|
||||||
interpolated_point = p1 + t * (p2 - p1)
|
interpolated_point = p1 + t * (p2 - p1)
|
||||||
interpolated_coords.append(interpolated_point.tolist())
|
interpolated_coords.append(interpolated_point.tolist())
|
||||||
else:
|
else:
|
||||||
interpolated_coords.append(p1.tolist())
|
interpolated_coords.append(p1.tolist())
|
||||||
|
else:
|
||||||
|
# If the target_length is at or beyond the end of the path, add the last coordinate
|
||||||
|
interpolated_coords.append(coordinates[-1])
|
||||||
|
|
||||||
# Convert back to string format if necessary
|
# Convert back to string format if necessary
|
||||||
interpolated_coords_str = "[" + ", ".join([f"{{'x': {round(coord[0])}, 'y': {round(coord[1])}}}" for coord in interpolated_coords]) + "]"
|
interpolated_coords_str = "[" + ", ".join([f"{{'x': {round(coord[0])}, 'y': {round(coord[1])}}}" for coord in interpolated_coords]) + "]"
|
||||||
|
print(interpolated_coords_str)
|
||||||
|
|
||||||
return (interpolated_coords_str, )
|
return (interpolated_coords_str,)
|
||||||
|
|
||||||
class DrawInstanceDiffusionTracking:
|
class DrawInstanceDiffusionTracking:
|
||||||
|
|
||||||
@@ -950,3 +1393,244 @@ CreateInstanceDiffusionTracking -node.
|
|||||||
image_tensor_batch = torch.stack(modified_images).cpu().float()
|
image_tensor_batch = torch.stack(modified_images).cpu().float()
|
||||||
|
|
||||||
return image_tensor_batch,
|
return image_tensor_batch,
|
||||||
|
|
||||||
|
class PointsEditor:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"points_store": ("STRING", {"multiline": False}),
|
||||||
|
"coordinates": ("STRING", {"multiline": False}),
|
||||||
|
"neg_coordinates": ("STRING", {"multiline": False}),
|
||||||
|
"bbox_store": ("STRING", {"multiline": False}),
|
||||||
|
"bboxes": ("STRING", {"multiline": False}),
|
||||||
|
"bbox_format": (
|
||||||
|
[
|
||||||
|
'xyxy',
|
||||||
|
'xywh',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}),
|
||||||
|
"height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}),
|
||||||
|
"normalize": ("BOOLEAN", {"default": False}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"bg_image": ("IMAGE", ),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING", "BBOX", "MASK", "IMAGE")
|
||||||
|
RETURN_NAMES = ("positive_coords", "negative_coords", "bbox", "bbox_mask", "cropped_image")
|
||||||
|
FUNCTION = "pointdata"
|
||||||
|
CATEGORY = "KJNodes/experimental"
|
||||||
|
DESCRIPTION = """
|
||||||
|
# WORK IN PROGRESS
|
||||||
|
Do not count on this as part of your workflow yet,
|
||||||
|
probably contains lots of bugs and stability is not
|
||||||
|
guaranteed!!
|
||||||
|
|
||||||
|
## Graphical editor to create coordinates
|
||||||
|
|
||||||
|
**Shift + click** to add a positive (green) point.
|
||||||
|
**Shift + right click** to add a negative (red) point.
|
||||||
|
**Ctrl + click** to draw a box.
|
||||||
|
**Right click on a point** to delete it.
|
||||||
|
Note that you can't delete from start/end of the points array.
|
||||||
|
|
||||||
|
To add an image select the node and copy/paste or drag in the image.
|
||||||
|
Or from the bg_image input on queue (first frame of the batch).
|
||||||
|
|
||||||
|
**THE IMAGE IS SAVED TO THE NODE AND WORKFLOW METADATA**
|
||||||
|
you can clear the image from the context menu by right clicking on the canvas
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def pointdata(self, points_store, bbox_store, width, height, coordinates, neg_coordinates, normalize, bboxes, bbox_format="xyxy", bg_image=None):
|
||||||
|
coordinates = json.loads(coordinates)
|
||||||
|
pos_coordinates = []
|
||||||
|
for coord in coordinates:
|
||||||
|
coord['x'] = int(round(coord['x']))
|
||||||
|
coord['y'] = int(round(coord['y']))
|
||||||
|
if normalize:
|
||||||
|
norm_x = coord['x'] / width
|
||||||
|
norm_y = coord['y'] / height
|
||||||
|
pos_coordinates.append({'x': norm_x, 'y': norm_y})
|
||||||
|
else:
|
||||||
|
pos_coordinates.append({'x': coord['x'], 'y': coord['y']})
|
||||||
|
|
||||||
|
if neg_coordinates:
|
||||||
|
coordinates = json.loads(neg_coordinates)
|
||||||
|
neg_coordinates = []
|
||||||
|
for coord in coordinates:
|
||||||
|
coord['x'] = int(round(coord['x']))
|
||||||
|
coord['y'] = int(round(coord['y']))
|
||||||
|
if normalize:
|
||||||
|
norm_x = coord['x'] / width
|
||||||
|
norm_y = coord['y'] / height
|
||||||
|
neg_coordinates.append({'x': norm_x, 'y': norm_y})
|
||||||
|
else:
|
||||||
|
neg_coordinates.append({'x': coord['x'], 'y': coord['y']})
|
||||||
|
|
||||||
|
# Create a blank mask
|
||||||
|
mask = np.zeros((height, width), dtype=np.uint8)
|
||||||
|
bboxes = json.loads(bboxes)
|
||||||
|
print(bboxes)
|
||||||
|
valid_bboxes = []
|
||||||
|
for bbox in bboxes:
|
||||||
|
if (bbox.get("startX") is None or
|
||||||
|
bbox.get("startY") is None or
|
||||||
|
bbox.get("endX") is None or
|
||||||
|
bbox.get("endY") is None):
|
||||||
|
continue # Skip this bounding box if any value is None
|
||||||
|
else:
|
||||||
|
# Ensure that endX and endY are greater than startX and startY
|
||||||
|
x_min = min(int(bbox["startX"]), int(bbox["endX"]))
|
||||||
|
y_min = min(int(bbox["startY"]), int(bbox["endY"]))
|
||||||
|
x_max = max(int(bbox["startX"]), int(bbox["endX"]))
|
||||||
|
y_max = max(int(bbox["startY"]), int(bbox["endY"]))
|
||||||
|
|
||||||
|
valid_bboxes.append((x_min, y_min, x_max, y_max))
|
||||||
|
|
||||||
|
bboxes_xyxy = []
|
||||||
|
for bbox in valid_bboxes:
|
||||||
|
x_min, y_min, x_max, y_max = bbox
|
||||||
|
bboxes_xyxy.append((x_min, y_min, x_max, y_max))
|
||||||
|
mask[y_min:y_max, x_min:x_max] = 1 # Fill the bounding box area with 1s
|
||||||
|
|
||||||
|
if bbox_format == "xywh":
|
||||||
|
bboxes_xywh = []
|
||||||
|
for bbox in valid_bboxes:
|
||||||
|
x_min, y_min, x_max, y_max = bbox
|
||||||
|
width = x_max - x_min
|
||||||
|
height = y_max - y_min
|
||||||
|
bboxes_xywh.append((x_min, y_min, width, height))
|
||||||
|
bboxes = bboxes_xywh
|
||||||
|
else:
|
||||||
|
bboxes = bboxes_xyxy
|
||||||
|
|
||||||
|
mask_tensor = torch.from_numpy(mask)
|
||||||
|
mask_tensor = mask_tensor.unsqueeze(0).float().cpu()
|
||||||
|
|
||||||
|
if bg_image is not None and len(valid_bboxes) > 0:
|
||||||
|
x_min, y_min, x_max, y_max = bboxes[0]
|
||||||
|
cropped_image = bg_image[:, y_min:y_max, x_min:x_max, :]
|
||||||
|
|
||||||
|
elif bg_image is not None:
|
||||||
|
cropped_image = bg_image
|
||||||
|
|
||||||
|
if bg_image is None:
|
||||||
|
return (json.dumps(pos_coordinates), json.dumps(neg_coordinates), bboxes, mask_tensor)
|
||||||
|
else:
|
||||||
|
transform = transforms.ToPILImage()
|
||||||
|
image = transform(bg_image[0].permute(2, 0, 1))
|
||||||
|
buffered = io.BytesIO()
|
||||||
|
image.save(buffered, format="JPEG", quality=75)
|
||||||
|
|
||||||
|
# Step 3: Encode the image bytes to a Base64 string
|
||||||
|
img_bytes = buffered.getvalue()
|
||||||
|
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ui": {"bg_image": [img_base64]},
|
||||||
|
"result": (json.dumps(pos_coordinates), json.dumps(neg_coordinates), bboxes, mask_tensor, cropped_image)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CutAndDragOnPath:
|
||||||
|
RETURN_TYPES = ("IMAGE", "MASK",)
|
||||||
|
RETURN_NAMES = ("image","mask", )
|
||||||
|
FUNCTION = "cutanddrag"
|
||||||
|
CATEGORY = "KJNodes/image"
|
||||||
|
DESCRIPTION = """
|
||||||
|
Cuts the masked area from the image, and drags it along the path. If inpaint is enabled, and no bg_image is provided, the cut area is filled using cv2 TELEA algorithm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE",),
|
||||||
|
"coordinates": ("STRING", {"forceInput": True}),
|
||||||
|
"mask": ("MASK",),
|
||||||
|
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
|
||||||
|
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
|
||||||
|
"inpaint": ("BOOLEAN", {"default": True}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"bg_image": ("IMAGE",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def cutanddrag(self, image, coordinates, mask, frame_width, frame_height, inpaint, bg_image=None):
|
||||||
|
# Parse coordinates
|
||||||
|
coords_list = parse_json_tracks(coordinates)
|
||||||
|
|
||||||
|
batch_size = len(coords_list[0])
|
||||||
|
images_list = []
|
||||||
|
masks_list = []
|
||||||
|
|
||||||
|
# Convert input image and mask to PIL
|
||||||
|
input_image = tensor2pil(image)[0]
|
||||||
|
input_mask = tensor2pil(mask)[0]
|
||||||
|
|
||||||
|
# Find masked region bounds
|
||||||
|
mask_array = np.array(input_mask)
|
||||||
|
y_indices, x_indices = np.where(mask_array > 0)
|
||||||
|
if len(x_indices) == 0 or len(y_indices) == 0:
|
||||||
|
return (image, mask)
|
||||||
|
|
||||||
|
x_min, x_max = x_indices.min(), x_indices.max()
|
||||||
|
y_min, y_max = y_indices.min(), y_indices.max()
|
||||||
|
|
||||||
|
# Cut out the masked region
|
||||||
|
cut_width = x_max - x_min
|
||||||
|
cut_height = y_max - y_min
|
||||||
|
cut_image = input_image.crop((x_min, y_min, x_max, y_max))
|
||||||
|
cut_mask = input_mask.crop((x_min, y_min, x_max, y_max))
|
||||||
|
|
||||||
|
# Create inpainted background
|
||||||
|
if bg_image is None:
|
||||||
|
background = input_image.copy()
|
||||||
|
# Inpaint the cut area
|
||||||
|
if inpaint:
|
||||||
|
import cv2
|
||||||
|
border = 5 # Create small border around cut area for better inpainting
|
||||||
|
fill_mask = Image.new("L", background.size, 0)
|
||||||
|
draw = ImageDraw.Draw(fill_mask)
|
||||||
|
draw.rectangle([x_min-border, y_min-border, x_max+border, y_max+border], fill=255)
|
||||||
|
background = cv2.inpaint(
|
||||||
|
np.array(background),
|
||||||
|
np.array(fill_mask),
|
||||||
|
inpaintRadius=3,
|
||||||
|
flags=cv2.INPAINT_TELEA
|
||||||
|
)
|
||||||
|
background = Image.fromarray(background)
|
||||||
|
else:
|
||||||
|
background = tensor2pil(bg_image)[0]
|
||||||
|
|
||||||
|
# Create batch of images with cut region at different positions
|
||||||
|
for i in range(batch_size):
|
||||||
|
# Create new image
|
||||||
|
new_image = background.copy()
|
||||||
|
new_mask = Image.new("L", (frame_width, frame_height), 0)
|
||||||
|
|
||||||
|
# Get target position from coordinates
|
||||||
|
for coords in coords_list:
|
||||||
|
target_x = int(coords[i]['x'] - cut_width/2)
|
||||||
|
target_y = int(coords[i]['y'] - cut_height/2)
|
||||||
|
|
||||||
|
# Paste cut region at new position
|
||||||
|
new_image.paste(cut_image, (target_x, target_y), cut_mask)
|
||||||
|
new_mask.paste(cut_mask, (target_x, target_y))
|
||||||
|
|
||||||
|
# Convert to tensor and append
|
||||||
|
image_tensor = pil2tensor(new_image)
|
||||||
|
mask_tensor = pil2tensor(new_mask)
|
||||||
|
|
||||||
|
images_list.append(image_tensor)
|
||||||
|
masks_list.append(mask_tensor)
|
||||||
|
|
||||||
|
# Stack tensors into batches
|
||||||
|
out_images = torch.cat(images_list, dim=0).cpu().float()
|
||||||
|
out_masks = torch.cat(masks_list, dim=0)
|
||||||
|
|
||||||
|
return (out_images, out_masks)
|
||||||
+2855
-134
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import comfy.sample
|
|||||||
from nodes import CLIPTextEncode
|
from nodes import CLIPTextEncode
|
||||||
|
|
||||||
script_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
script_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
folder_paths.add_model_folder_path("intristic_loras", os.path.join(script_directory, "intristic_loras"))
|
folder_paths.add_model_folder_path("intrinsic_loras", os.path.join(script_directory, "intrinsic_loras"))
|
||||||
|
|
||||||
class Intrinsic_lora_sampling:
|
class Intrinsic_lora_sampling:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -16,7 +16,7 @@ class Intrinsic_lora_sampling:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {"required": { "model": ("MODEL",),
|
return {"required": { "model": ("MODEL",),
|
||||||
"lora_name": (folder_paths.get_filename_list("intristic_loras"), ),
|
"lora_name": (folder_paths.get_filename_list("intrinsic_loras"), ),
|
||||||
"task": (
|
"task": (
|
||||||
[
|
[
|
||||||
'depth map',
|
'depth map',
|
||||||
@@ -81,7 +81,7 @@ with this node pack.
|
|||||||
|
|
||||||
#load lora
|
#load lora
|
||||||
model_clone = model.clone()
|
model_clone = model.clone()
|
||||||
lora_path = folder_paths.get_full_path("intristic_loras", lora_name)
|
lora_path = folder_paths.get_full_path("intrinsic_loras", lora_name)
|
||||||
lora = load_torch_file(lora_path, safe_load=True)
|
lora = load_torch_file(lora_path, safe_load=True)
|
||||||
self.loaded_lora = (lora_path, lora)
|
self.loaded_lora = (lora_path, lora)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,552 @@
|
|||||||
|
import torch
|
||||||
|
import comfy.model_management
|
||||||
|
import comfy.utils
|
||||||
|
import folder_paths
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from tqdm import tqdm
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
device = comfy.model_management.get_torch_device()
|
||||||
|
|
||||||
|
CLAMP_QUANTILE = 0.99
|
||||||
|
|
||||||
|
def extract_lora(diff, key, rank, algorithm, lora_type, lowrank_iters=7, adaptive_param=1.0, clamp_quantile=True):
|
||||||
|
"""
|
||||||
|
Extracts LoRA weights from a weight difference tensor using SVD.
|
||||||
|
"""
|
||||||
|
conv2d = (len(diff.shape) == 4)
|
||||||
|
kernel_size = None if not conv2d else diff.size()[2:4]
|
||||||
|
conv2d_3x3 = conv2d and kernel_size != (1, 1)
|
||||||
|
out_dim, in_dim = diff.size()[0:2]
|
||||||
|
|
||||||
|
if conv2d:
|
||||||
|
if conv2d_3x3:
|
||||||
|
diff = diff.flatten(start_dim=1)
|
||||||
|
else:
|
||||||
|
diff = diff.squeeze()
|
||||||
|
|
||||||
|
diff_float = diff.float()
|
||||||
|
if algorithm == "svd_lowrank":
|
||||||
|
U, S, V = torch.svd_lowrank(diff_float, q=min(rank, in_dim, out_dim), niter=lowrank_iters)
|
||||||
|
U = U @ torch.diag(S)
|
||||||
|
Vh = V.t()
|
||||||
|
else:
|
||||||
|
#torch.linalg.svdvals()
|
||||||
|
U, S, Vh = torch.linalg.svd(diff_float)
|
||||||
|
# Flexible rank selection logic like locon: https://github.com/KohakuBlueleaf/LyCORIS/blob/main/tools/extract_locon.py
|
||||||
|
if "adaptive" in lora_type:
|
||||||
|
if lora_type == "adaptive_ratio":
|
||||||
|
min_s = torch.max(S) * adaptive_param
|
||||||
|
lora_rank = torch.sum(S > min_s).item()
|
||||||
|
elif lora_type == "adaptive_energy":
|
||||||
|
energy = torch.cumsum(S**2, dim=0)
|
||||||
|
total_energy = torch.sum(S**2)
|
||||||
|
threshold = adaptive_param * total_energy # e.g., adaptive_param=0.95 for 95%
|
||||||
|
lora_rank = torch.sum(energy < threshold).item() + 1
|
||||||
|
elif lora_type == "adaptive_quantile":
|
||||||
|
s_cum = torch.cumsum(S, dim=0)
|
||||||
|
min_cum_sum = adaptive_param * torch.sum(S)
|
||||||
|
lora_rank = torch.sum(s_cum < min_cum_sum).item()
|
||||||
|
print(f"{key} Extracted LoRA rank: {lora_rank}")
|
||||||
|
else:
|
||||||
|
lora_rank = rank
|
||||||
|
|
||||||
|
lora_rank = max(1, lora_rank)
|
||||||
|
lora_rank = min(out_dim, in_dim, lora_rank)
|
||||||
|
|
||||||
|
U = U[:, :lora_rank]
|
||||||
|
S = S[:lora_rank]
|
||||||
|
U = U @ torch.diag(S)
|
||||||
|
Vh = Vh[:lora_rank, :]
|
||||||
|
|
||||||
|
if clamp_quantile:
|
||||||
|
dist = torch.cat([U.flatten(), Vh.flatten()])
|
||||||
|
if dist.numel() > 100_000:
|
||||||
|
# Sample 100,000 elements for quantile estimation
|
||||||
|
idx = torch.randperm(dist.numel(), device=dist.device)[:100_000]
|
||||||
|
dist_sample = dist[idx]
|
||||||
|
hi_val = torch.quantile(dist_sample, CLAMP_QUANTILE)
|
||||||
|
else:
|
||||||
|
hi_val = torch.quantile(dist, CLAMP_QUANTILE)
|
||||||
|
low_val = -hi_val
|
||||||
|
|
||||||
|
U = U.clamp(low_val, hi_val)
|
||||||
|
Vh = Vh.clamp(low_val, hi_val)
|
||||||
|
if conv2d:
|
||||||
|
U = U.reshape(out_dim, lora_rank, 1, 1)
|
||||||
|
Vh = Vh.reshape(lora_rank, in_dim, kernel_size[0], kernel_size[1])
|
||||||
|
return (U, Vh)
|
||||||
|
|
||||||
|
|
||||||
|
def calc_lora_model(model_diff, rank, prefix_model, prefix_lora, output_sd, lora_type, algorithm, lowrank_iters, out_dtype, bias_diff=False, adaptive_param=1.0, clamp_quantile=True):
|
||||||
|
comfy.model_management.load_models_gpu([model_diff], force_patch_weights=True)
|
||||||
|
model_diff.model.diffusion_model.cpu()
|
||||||
|
sd = model_diff.model_state_dict(filter_prefix=prefix_model)
|
||||||
|
del model_diff
|
||||||
|
comfy.model_management.soft_empty_cache()
|
||||||
|
for k, v in sd.items():
|
||||||
|
if isinstance(v, torch.Tensor):
|
||||||
|
sd[k] = v.cpu()
|
||||||
|
|
||||||
|
# Get total number of keys to process for progress bar
|
||||||
|
total_keys = len([k for k in sd if k.endswith(".weight") or (bias_diff and k.endswith(".bias"))])
|
||||||
|
|
||||||
|
# Create progress bar
|
||||||
|
progress_bar = tqdm(total=total_keys, desc=f"Extracting LoRA ({prefix_lora.strip('.')})")
|
||||||
|
comfy_pbar = comfy.utils.ProgressBar(total_keys)
|
||||||
|
|
||||||
|
for k in sd:
|
||||||
|
if k.endswith(".weight"):
|
||||||
|
weight_diff = sd[k]
|
||||||
|
if weight_diff.ndim == 5:
|
||||||
|
logging.info(f"Skipping 5D tensor for key {k}") #skip patch embed
|
||||||
|
progress_bar.update(1)
|
||||||
|
comfy_pbar.update(1)
|
||||||
|
continue
|
||||||
|
if lora_type != "full":
|
||||||
|
if weight_diff.ndim < 2:
|
||||||
|
if bias_diff:
|
||||||
|
output_sd["{}{}.diff".format(prefix_lora, k[len(prefix_model):-7])] = weight_diff.contiguous().to(out_dtype).cpu()
|
||||||
|
progress_bar.update(1)
|
||||||
|
comfy_pbar.update(1)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
out = extract_lora(weight_diff.to(device), k, rank, algorithm, lora_type, lowrank_iters=lowrank_iters, adaptive_param=adaptive_param, clamp_quantile=clamp_quantile)
|
||||||
|
output_sd["{}{}.lora_up.weight".format(prefix_lora, k[len(prefix_model):-7])] = out[0].contiguous().to(out_dtype).cpu()
|
||||||
|
output_sd["{}{}.lora_down.weight".format(prefix_lora, k[len(prefix_model):-7])] = out[1].contiguous().to(out_dtype).cpu()
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Could not generate lora weights for key {k}, error {e}")
|
||||||
|
else:
|
||||||
|
output_sd["{}{}.diff".format(prefix_lora, k[len(prefix_model):-7])] = weight_diff.contiguous().to(out_dtype).cpu()
|
||||||
|
|
||||||
|
progress_bar.update(1)
|
||||||
|
comfy_pbar.update(1)
|
||||||
|
|
||||||
|
elif bias_diff and k.endswith(".bias"):
|
||||||
|
output_sd["{}{}.diff_b".format(prefix_lora, k[len(prefix_model):-5])] = sd[k].contiguous().to(out_dtype).cpu()
|
||||||
|
progress_bar.update(1)
|
||||||
|
comfy_pbar.update(1)
|
||||||
|
progress_bar.close()
|
||||||
|
return output_sd
|
||||||
|
|
||||||
|
class LoraExtractKJ:
|
||||||
|
def __init__(self):
|
||||||
|
self.output_dir = folder_paths.get_output_directory()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {"required":
|
||||||
|
{
|
||||||
|
"finetuned_model": ("MODEL",),
|
||||||
|
"original_model": ("MODEL",),
|
||||||
|
"filename_prefix": ("STRING", {"default": "loras/ComfyUI_extracted_lora"}),
|
||||||
|
"rank": ("INT", {"default": 8, "min": 1, "max": 4096, "step": 1}),
|
||||||
|
"lora_type": (["standard", "full", "adaptive_ratio", "adaptive_quantile", "adaptive_energy"],),
|
||||||
|
"algorithm": (["svd_linalg", "svd_lowrank"], {"default": "svd_linalg", "tooltip": "SVD algorithm to use, svd_lowrank is faster but less accurate."}),
|
||||||
|
"lowrank_iters": ("INT", {"default": 7, "min": 1, "max": 100, "step": 1, "tooltip": "The number of subspace iterations for lowrank SVD algorithm."}),
|
||||||
|
"output_dtype": (["fp16", "bf16", "fp32"], {"default": "fp16"}),
|
||||||
|
"bias_diff": ("BOOLEAN", {"default": True}),
|
||||||
|
"adaptive_param": ("FLOAT", {"default": 0.15, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "For ratio mode, this is the ratio of the maximum singular value. For quantile mode, this is the quantile of the singular values."}),
|
||||||
|
"clamp_quantile": ("BOOLEAN", {"default": True}),
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
FUNCTION = "save"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
|
||||||
|
CATEGORY = "KJNodes/lora"
|
||||||
|
|
||||||
|
def save(self, finetuned_model, original_model, filename_prefix, rank, lora_type, algorithm, lowrank_iters, output_dtype, bias_diff, adaptive_param, clamp_quantile):
|
||||||
|
if algorithm == "svd_lowrank" and lora_type != "standard":
|
||||||
|
raise ValueError("svd_lowrank algorithm is only supported for standard LoRA extraction.")
|
||||||
|
|
||||||
|
dtype = {"fp8_e4m3fn": torch.float8_e4m3fn, "bf16": torch.bfloat16, "fp16": torch.float16, "fp16_fast": torch.float16, "fp32": torch.float32}[output_dtype]
|
||||||
|
m = finetuned_model.clone()
|
||||||
|
kp = original_model.get_key_patches("diffusion_model.")
|
||||||
|
for k in kp:
|
||||||
|
m.add_patches({k: kp[k]}, - 1.0, 1.0)
|
||||||
|
model_diff = m
|
||||||
|
|
||||||
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
|
||||||
|
|
||||||
|
output_sd = {}
|
||||||
|
if model_diff is not None:
|
||||||
|
output_sd = calc_lora_model(model_diff, rank, "diffusion_model.", "diffusion_model.", output_sd, lora_type, algorithm, lowrank_iters, dtype, bias_diff=bias_diff, adaptive_param=adaptive_param, clamp_quantile=clamp_quantile)
|
||||||
|
if "adaptive" in lora_type:
|
||||||
|
rank_str = f"{lora_type}_{adaptive_param:.2f}"
|
||||||
|
else:
|
||||||
|
rank_str = rank
|
||||||
|
output_checkpoint = f"{filename}_rank_{rank_str}_{output_dtype}_{counter:05}_.safetensors"
|
||||||
|
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
|
||||||
|
|
||||||
|
comfy.utils.save_torch_file(output_sd, output_checkpoint, metadata=None)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"LoraExtractKJ": LoraExtractKJ
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"LoraExtractKJ": "LoraExtractKJ"
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoraReduceRank:
|
||||||
|
def __init__(self):
|
||||||
|
self.output_dir = folder_paths.get_output_directory()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {"required":
|
||||||
|
{
|
||||||
|
"lora_name": (folder_paths.get_filename_list("loras"), {"tooltip": "The name of the LoRA."}),
|
||||||
|
"new_rank": ("INT", {"default": 8, "min": 1, "max": 4096, "step": 1, "tooltip": "The new rank to resize the LoRA. Acts as max rank when using dynamic_method."}),
|
||||||
|
"dynamic_method": (["disabled", "sv_ratio", "sv_cumulative", "sv_fro"], {"default": "disabled", "tooltip": "Method to use for dynamically determining new alphas and dims"}),
|
||||||
|
"dynamic_param": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Method to use for dynamically determining new alphas and dims"}),
|
||||||
|
"output_dtype": (["match_original", "fp16", "bf16", "fp32"], {"default": "match_original", "tooltip": "Data type to save the LoRA as."}),
|
||||||
|
"verbose": ("BOOLEAN", {"default": True}),
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
FUNCTION = "save"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
EXPERIMENTAL = True
|
||||||
|
DESCRIPTION = "Resize a LoRA model by reducing it's rank. Based on kohya's sd-scripts: https://github.com/kohya-ss/sd-scripts/blob/main/networks/resize_lora.py"
|
||||||
|
|
||||||
|
CATEGORY = "KJNodes/lora"
|
||||||
|
|
||||||
|
def save(self, lora_name, new_rank, output_dtype, dynamic_method, dynamic_param, verbose):
|
||||||
|
|
||||||
|
lora_path = folder_paths.get_full_path("loras", lora_name)
|
||||||
|
lora_sd, metadata = comfy.utils.load_torch_file(lora_path, return_metadata=True)
|
||||||
|
|
||||||
|
if output_dtype == "fp16":
|
||||||
|
save_dtype = torch.float16
|
||||||
|
elif output_dtype == "bf16":
|
||||||
|
save_dtype = torch.bfloat16
|
||||||
|
elif output_dtype == "fp32":
|
||||||
|
save_dtype = torch.float32
|
||||||
|
elif output_dtype == "match_original":
|
||||||
|
first_weight_key = next(k for k in lora_sd if k.endswith(".weight") and isinstance(lora_sd[k], torch.Tensor))
|
||||||
|
save_dtype = lora_sd[first_weight_key].dtype
|
||||||
|
|
||||||
|
new_lora_sd = {}
|
||||||
|
for k, v in lora_sd.items():
|
||||||
|
new_lora_sd[k.replace(".default", "")] = v
|
||||||
|
del lora_sd
|
||||||
|
print("Resizing Lora...")
|
||||||
|
output_sd, old_dim, new_alpha, rank_list = resize_lora_model(new_lora_sd, new_rank, save_dtype, device, dynamic_method, dynamic_param, verbose)
|
||||||
|
|
||||||
|
# update metadata
|
||||||
|
if metadata is None:
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
comment = metadata.get("ss_training_comment", "")
|
||||||
|
|
||||||
|
if dynamic_method == "disabled":
|
||||||
|
metadata["ss_training_comment"] = f"dimension is resized from {old_dim} to {new_rank}; {comment}"
|
||||||
|
metadata["ss_network_dim"] = str(new_rank)
|
||||||
|
metadata["ss_network_alpha"] = str(new_alpha)
|
||||||
|
else:
|
||||||
|
metadata["ss_training_comment"] = f"Dynamic resize with {dynamic_method}: {dynamic_param} from {old_dim}; {comment}"
|
||||||
|
metadata["ss_network_dim"] = "Dynamic"
|
||||||
|
metadata["ss_network_alpha"] = "Dynamic"
|
||||||
|
|
||||||
|
# cast to save_dtype before calculating hashes
|
||||||
|
for key in list(output_sd.keys()):
|
||||||
|
value = output_sd[key]
|
||||||
|
if type(value) == torch.Tensor and value.dtype.is_floating_point and value.dtype != save_dtype:
|
||||||
|
output_sd[key] = value.to(save_dtype)
|
||||||
|
|
||||||
|
output_filename_prefix = "loras/" + lora_name
|
||||||
|
|
||||||
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(output_filename_prefix, self.output_dir)
|
||||||
|
output_dtype_str = f"_{output_dtype}" if output_dtype != "match_original" else ""
|
||||||
|
average_rank = str(int(np.mean(rank_list)))
|
||||||
|
rank_str = new_rank if dynamic_method == "disabled" else f"dynamic_{average_rank}"
|
||||||
|
output_checkpoint = f"{filename.replace('.safetensors', '')}_resized_from_{old_dim}_to_{rank_str}{output_dtype_str}_{counter:05}_.safetensors"
|
||||||
|
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
|
||||||
|
print(f"Saving resized LoRA to {output_checkpoint}")
|
||||||
|
|
||||||
|
comfy.utils.save_torch_file(output_sd, output_checkpoint, metadata=metadata)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"LoraExtractKJ": LoraExtractKJ
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"LoraExtractKJ": "LoraExtractKJ"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert LoRA to different rank approximation (should only be used to go to lower rank)
|
||||||
|
# This code is based off the extract_lora_from_models.py file which is based on https://github.com/cloneofsimo/lora/blob/develop/lora_diffusion/cli_svd.py
|
||||||
|
# Thanks to cloneofsimo
|
||||||
|
|
||||||
|
# This version is based on
|
||||||
|
# https://github.com/kohya-ss/sd-scripts/blob/main/networks/resize_lora.py
|
||||||
|
|
||||||
|
MIN_SV = 1e-6
|
||||||
|
|
||||||
|
LORA_DOWN_UP_FORMATS = [
|
||||||
|
("lora_down", "lora_up"), # sd-scripts LoRA
|
||||||
|
("lora_A", "lora_B"), # PEFT LoRA
|
||||||
|
("down", "up"), # ControlLoRA
|
||||||
|
]
|
||||||
|
|
||||||
|
# Indexing functions
|
||||||
|
def index_sv_cumulative(S, target):
|
||||||
|
original_sum = float(torch.sum(S))
|
||||||
|
cumulative_sums = torch.cumsum(S, dim=0) / original_sum
|
||||||
|
index = int(torch.searchsorted(cumulative_sums, target)) + 1
|
||||||
|
index = max(1, min(index, len(S) - 1))
|
||||||
|
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
def index_sv_fro(S, target):
|
||||||
|
S_squared = S.pow(2)
|
||||||
|
S_fro_sq = float(torch.sum(S_squared))
|
||||||
|
sum_S_squared = torch.cumsum(S_squared, dim=0) / S_fro_sq
|
||||||
|
index = int(torch.searchsorted(sum_S_squared, target**2)) + 1
|
||||||
|
index = max(1, min(index, len(S) - 1))
|
||||||
|
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
def index_sv_ratio(S, target):
|
||||||
|
max_sv = S[0]
|
||||||
|
min_sv = max_sv / target
|
||||||
|
index = int(torch.sum(S > min_sv).item())
|
||||||
|
index = max(1, min(index, len(S) - 1))
|
||||||
|
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
# Modified from Kohaku-blueleaf's extract/merge functions
|
||||||
|
def extract_conv(weight, lora_rank, dynamic_method, dynamic_param, device, scale=1):
|
||||||
|
out_size, in_size, kernel_size, _ = weight.size()
|
||||||
|
if weight.dtype != torch.float32:
|
||||||
|
weight = weight.to(torch.float32)
|
||||||
|
U, S, Vh = torch.linalg.svd(weight.reshape(out_size, -1).to(device))
|
||||||
|
|
||||||
|
param_dict = rank_resize(S, lora_rank, dynamic_method, dynamic_param, scale)
|
||||||
|
lora_rank = param_dict["new_rank"]
|
||||||
|
|
||||||
|
U = U[:, :lora_rank]
|
||||||
|
S = S[:lora_rank]
|
||||||
|
U = U @ torch.diag(S)
|
||||||
|
Vh = Vh[:lora_rank, :]
|
||||||
|
|
||||||
|
param_dict["lora_down"] = Vh.reshape(lora_rank, in_size, kernel_size, kernel_size).cpu()
|
||||||
|
param_dict["lora_up"] = U.reshape(out_size, lora_rank, 1, 1).cpu()
|
||||||
|
del U, S, Vh, weight
|
||||||
|
return param_dict
|
||||||
|
|
||||||
|
|
||||||
|
def extract_linear(weight, lora_rank, dynamic_method, dynamic_param, device, scale=1):
|
||||||
|
out_size, in_size = weight.size()
|
||||||
|
|
||||||
|
if weight.dtype != torch.float32:
|
||||||
|
weight = weight.to(torch.float32)
|
||||||
|
U, S, Vh = torch.linalg.svd(weight.to(device))
|
||||||
|
|
||||||
|
param_dict = rank_resize(S, lora_rank, dynamic_method, dynamic_param, scale)
|
||||||
|
lora_rank = param_dict["new_rank"]
|
||||||
|
|
||||||
|
U = U[:, :lora_rank]
|
||||||
|
S = S[:lora_rank]
|
||||||
|
U = U @ torch.diag(S)
|
||||||
|
Vh = Vh[:lora_rank, :]
|
||||||
|
|
||||||
|
param_dict["lora_down"] = Vh.reshape(lora_rank, in_size).cpu()
|
||||||
|
param_dict["lora_up"] = U.reshape(out_size, lora_rank).cpu()
|
||||||
|
del U, S, Vh, weight
|
||||||
|
return param_dict
|
||||||
|
|
||||||
|
|
||||||
|
def merge_conv(lora_down, lora_up, device):
|
||||||
|
in_rank, in_size, kernel_size, k_ = lora_down.shape
|
||||||
|
out_size, out_rank, _, _ = lora_up.shape
|
||||||
|
assert in_rank == out_rank and kernel_size == k_, f"rank {in_rank} {out_rank} or kernel {kernel_size} {k_} mismatch"
|
||||||
|
|
||||||
|
lora_down = lora_down.to(device)
|
||||||
|
lora_up = lora_up.to(device)
|
||||||
|
|
||||||
|
merged = lora_up.reshape(out_size, -1) @ lora_down.reshape(in_rank, -1)
|
||||||
|
weight = merged.reshape(out_size, in_size, kernel_size, kernel_size)
|
||||||
|
del lora_up, lora_down
|
||||||
|
return weight
|
||||||
|
|
||||||
|
|
||||||
|
def merge_linear(lora_down, lora_up, device):
|
||||||
|
in_rank, in_size = lora_down.shape
|
||||||
|
out_size, out_rank = lora_up.shape
|
||||||
|
assert in_rank == out_rank, f"rank {in_rank} {out_rank} mismatch"
|
||||||
|
|
||||||
|
lora_down = lora_down.to(device)
|
||||||
|
lora_up = lora_up.to(device)
|
||||||
|
|
||||||
|
weight = lora_up @ lora_down
|
||||||
|
del lora_up, lora_down
|
||||||
|
return weight
|
||||||
|
|
||||||
|
|
||||||
|
# Calculate new rank
|
||||||
|
|
||||||
|
|
||||||
|
def rank_resize(S, rank, dynamic_method, dynamic_param, scale=1):
|
||||||
|
param_dict = {}
|
||||||
|
|
||||||
|
if dynamic_method == "sv_ratio":
|
||||||
|
# Calculate new dim and alpha based off ratio
|
||||||
|
new_rank = index_sv_ratio(S, dynamic_param) + 1
|
||||||
|
new_alpha = float(scale * new_rank)
|
||||||
|
|
||||||
|
elif dynamic_method == "sv_cumulative":
|
||||||
|
# Calculate new dim and alpha based off cumulative sum
|
||||||
|
new_rank = index_sv_cumulative(S, dynamic_param) + 1
|
||||||
|
new_alpha = float(scale * new_rank)
|
||||||
|
|
||||||
|
elif dynamic_method == "sv_fro":
|
||||||
|
# Calculate new dim and alpha based off sqrt sum of squares
|
||||||
|
new_rank = index_sv_fro(S, dynamic_param) + 1
|
||||||
|
new_alpha = float(scale * new_rank)
|
||||||
|
else:
|
||||||
|
new_rank = rank
|
||||||
|
new_alpha = float(scale * new_rank)
|
||||||
|
|
||||||
|
if S[0] <= MIN_SV: # Zero matrix, set dim to 1
|
||||||
|
new_rank = 1
|
||||||
|
new_alpha = float(scale * new_rank)
|
||||||
|
elif new_rank > rank: # cap max rank at rank
|
||||||
|
new_rank = rank
|
||||||
|
new_alpha = float(scale * new_rank)
|
||||||
|
|
||||||
|
# Calculate resize info
|
||||||
|
s_sum = torch.sum(torch.abs(S))
|
||||||
|
s_rank = torch.sum(torch.abs(S[:new_rank]))
|
||||||
|
|
||||||
|
S_squared = S.pow(2)
|
||||||
|
s_fro = torch.sqrt(torch.sum(S_squared))
|
||||||
|
s_red_fro = torch.sqrt(torch.sum(S_squared[:new_rank]))
|
||||||
|
fro_percent = float(s_red_fro / s_fro)
|
||||||
|
|
||||||
|
param_dict["new_rank"] = new_rank
|
||||||
|
param_dict["new_alpha"] = new_alpha
|
||||||
|
param_dict["sum_retained"] = (s_rank) / s_sum
|
||||||
|
param_dict["fro_retained"] = fro_percent
|
||||||
|
param_dict["max_ratio"] = S[0] / S[new_rank - 1]
|
||||||
|
|
||||||
|
return param_dict
|
||||||
|
|
||||||
|
|
||||||
|
def resize_lora_model(lora_sd, new_rank, save_dtype, device, dynamic_method, dynamic_param, verbose):
|
||||||
|
max_old_rank = None
|
||||||
|
new_alpha = None
|
||||||
|
verbose_str = "\n"
|
||||||
|
fro_list = []
|
||||||
|
rank_list = []
|
||||||
|
|
||||||
|
if dynamic_method:
|
||||||
|
print(f"Dynamically determining new alphas and dims based off {dynamic_method}: {dynamic_param}, max rank is {new_rank}")
|
||||||
|
|
||||||
|
lora_down_weight = None
|
||||||
|
lora_up_weight = None
|
||||||
|
|
||||||
|
o_lora_sd = lora_sd.copy()
|
||||||
|
block_down_name = None
|
||||||
|
block_up_name = None
|
||||||
|
|
||||||
|
total_keys = len([k for k in lora_sd if k.endswith(".weight")])
|
||||||
|
|
||||||
|
pbar = comfy.utils.ProgressBar(total_keys)
|
||||||
|
for key, value in tqdm(lora_sd.items()):
|
||||||
|
key_parts = key.split(".")
|
||||||
|
block_down_name = None
|
||||||
|
for _format in LORA_DOWN_UP_FORMATS:
|
||||||
|
# Currently we only match lora_down_name in the last two parts of key
|
||||||
|
# because ("down", "up") are general words and may appear in block_down_name
|
||||||
|
if len(key_parts) >= 2 and _format[0] == key_parts[-2]:
|
||||||
|
block_down_name = ".".join(key_parts[:-2])
|
||||||
|
lora_down_name = "." + _format[0]
|
||||||
|
lora_up_name = "." + _format[1]
|
||||||
|
weight_name = "." + key_parts[-1]
|
||||||
|
break
|
||||||
|
if len(key_parts) >= 1 and _format[0] == key_parts[-1]:
|
||||||
|
block_down_name = ".".join(key_parts[:-1])
|
||||||
|
lora_down_name = "." + _format[0]
|
||||||
|
lora_up_name = "." + _format[1]
|
||||||
|
weight_name = ""
|
||||||
|
break
|
||||||
|
|
||||||
|
if block_down_name is None:
|
||||||
|
# This parameter is not lora_down
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Now weight_name can be ".weight" or ""
|
||||||
|
# Find corresponding lora_up and alpha
|
||||||
|
block_up_name = block_down_name
|
||||||
|
lora_down_weight = value
|
||||||
|
lora_up_weight = lora_sd.get(block_up_name + lora_up_name + weight_name, None)
|
||||||
|
lora_alpha = lora_sd.get(block_down_name + ".alpha", None)
|
||||||
|
|
||||||
|
weights_loaded = lora_down_weight is not None and lora_up_weight is not None
|
||||||
|
|
||||||
|
if weights_loaded:
|
||||||
|
|
||||||
|
conv2d = len(lora_down_weight.size()) == 4
|
||||||
|
old_rank = lora_down_weight.size()[0]
|
||||||
|
max_old_rank = max(max_old_rank or 0, old_rank)
|
||||||
|
|
||||||
|
|
||||||
|
if lora_alpha is None:
|
||||||
|
scale = 1.0
|
||||||
|
else:
|
||||||
|
scale = lora_alpha / old_rank
|
||||||
|
|
||||||
|
if conv2d:
|
||||||
|
full_weight_matrix = merge_conv(lora_down_weight, lora_up_weight, device)
|
||||||
|
param_dict = extract_conv(full_weight_matrix, new_rank, dynamic_method, dynamic_param, device, scale)
|
||||||
|
else:
|
||||||
|
full_weight_matrix = merge_linear(lora_down_weight, lora_up_weight, device)
|
||||||
|
param_dict = extract_linear(full_weight_matrix, new_rank, dynamic_method, dynamic_param, device, scale)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
max_ratio = param_dict["max_ratio"]
|
||||||
|
sum_retained = param_dict["sum_retained"]
|
||||||
|
fro_retained = param_dict["fro_retained"]
|
||||||
|
if not np.isnan(fro_retained):
|
||||||
|
fro_list.append(float(fro_retained))
|
||||||
|
|
||||||
|
verbose_str += f"{block_down_name:75} | "
|
||||||
|
verbose_str += f"sum(S) retained: {sum_retained:.1%}, fro retained: {fro_retained:.1%}, max(S) ratio: {max_ratio:0.1f}"
|
||||||
|
print(verbose_str)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if verbose and dynamic_method:
|
||||||
|
verbose_str += f", dynamic | dim: {param_dict['new_rank']}, alpha: {param_dict['new_alpha']}\n"
|
||||||
|
else:
|
||||||
|
verbose_str += "\n"
|
||||||
|
|
||||||
|
new_alpha = param_dict["new_alpha"]
|
||||||
|
o_lora_sd[block_down_name + lora_down_name + weight_name] = param_dict["lora_down"].to(save_dtype).contiguous()
|
||||||
|
o_lora_sd[block_up_name + lora_up_name + weight_name] = param_dict["lora_up"].to(save_dtype).contiguous()
|
||||||
|
o_lora_sd[block_down_name + ".alpha"] = torch.tensor(param_dict["new_alpha"]).to(save_dtype)
|
||||||
|
|
||||||
|
block_down_name = None
|
||||||
|
block_up_name = None
|
||||||
|
lora_down_weight = None
|
||||||
|
lora_up_weight = None
|
||||||
|
weights_loaded = False
|
||||||
|
rank_list.append(param_dict["new_rank"])
|
||||||
|
del param_dict
|
||||||
|
pbar.update(1)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(verbose_str)
|
||||||
|
print(f"Average Frobenius norm retention: {np.mean(fro_list):.2%} | std: {np.std(fro_list):0.3f}")
|
||||||
|
return o_lora_sd, max_old_rank, new_alpha, rank_list
|
||||||
+320
-59
@@ -4,13 +4,12 @@ from torchvision.transforms import functional as TF
|
|||||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||||
import scipy.ndimage
|
import scipy.ndimage
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import model_management
|
from comfy import model_management
|
||||||
from comfy.utils import ProgressBar
|
from comfy.utils import ProgressBar
|
||||||
|
from comfy.utils import common_upscale
|
||||||
from nodes import MAX_RESOLUTION
|
from nodes import MAX_RESOLUTION
|
||||||
|
|
||||||
import folder_paths
|
import folder_paths
|
||||||
@@ -31,73 +30,155 @@ class BatchCLIPSeg:
|
|||||||
{
|
{
|
||||||
"images": ("IMAGE",),
|
"images": ("IMAGE",),
|
||||||
"text": ("STRING", {"multiline": False}),
|
"text": ("STRING", {"multiline": False}),
|
||||||
"threshold": ("FLOAT", {"default": 0.1,"min": 0.0, "max": 10.0, "step": 0.001}),
|
"threshold": ("FLOAT", {"default": 0.5,"min": 0.0, "max": 10.0, "step": 0.001}),
|
||||||
"binary_mask": ("BOOLEAN", {"default": True}),
|
"binary_mask": ("BOOLEAN", {"default": True}),
|
||||||
"combine_mask": ("BOOLEAN", {"default": False}),
|
"combine_mask": ("BOOLEAN", {"default": False}),
|
||||||
"use_cuda": ("BOOLEAN", {"default": True}),
|
"use_cuda": ("BOOLEAN", {"default": True}),
|
||||||
},
|
},
|
||||||
|
"optional":
|
||||||
|
{
|
||||||
|
"blur_sigma": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}),
|
||||||
|
"opt_model": ("CLIPSEGMODEL", ),
|
||||||
|
"prev_mask": ("MASK", {"default": None}),
|
||||||
|
"image_bg_level": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||||
|
"invert": ("BOOLEAN", {"default": False}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CATEGORY = "KJNodes/masking"
|
CATEGORY = "KJNodes/masking"
|
||||||
RETURN_TYPES = ("MASK",)
|
RETURN_TYPES = ("MASK", "IMAGE", )
|
||||||
RETURN_NAMES = ("Mask",)
|
RETURN_NAMES = ("Mask", "Image", )
|
||||||
FUNCTION = "segment_image"
|
FUNCTION = "segment_image"
|
||||||
DESCRIPTION = """
|
DESCRIPTION = """
|
||||||
Segments an image or batch of images using CLIPSeg.
|
Segments an image or batch of images using CLIPSeg.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def segment_image(self, images, text, threshold, binary_mask, combine_mask, use_cuda):
|
def segment_image(self, images, text, threshold, binary_mask, combine_mask, use_cuda, blur_sigma=0.0, opt_model=None, prev_mask=None, invert= False, image_bg_level=0.5):
|
||||||
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
|
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
|
||||||
out = []
|
import torchvision.transforms as transforms
|
||||||
height, width, _ = images[0].shape
|
offload_device = model_management.unet_offload_device()
|
||||||
if use_cuda and torch.cuda.is_available():
|
device = model_management.get_torch_device()
|
||||||
device = torch.device("cuda")
|
if not use_cuda:
|
||||||
else:
|
|
||||||
device = torch.device("cpu")
|
device = torch.device("cpu")
|
||||||
dtype = model_management.unet_dtype()
|
dtype = model_management.unet_dtype()
|
||||||
model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined")
|
|
||||||
model.to(dtype)
|
if opt_model is None:
|
||||||
model.to(device)
|
checkpoint_path = os.path.join(folder_paths.models_dir,'clip_seg', 'clipseg-rd64-refined-fp16')
|
||||||
|
if not hasattr(self, "model"):
|
||||||
|
try:
|
||||||
|
if not os.path.exists(checkpoint_path):
|
||||||
|
from huggingface_hub import snapshot_download
|
||||||
|
snapshot_download(repo_id="Kijai/clipseg-rd64-refined-fp16", local_dir=checkpoint_path, local_dir_use_symlinks=False)
|
||||||
|
self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path)
|
||||||
|
except:
|
||||||
|
checkpoint_path = "CIDAS/clipseg-rd64-refined"
|
||||||
|
self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path)
|
||||||
|
processor = CLIPSegProcessor.from_pretrained(checkpoint_path)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.model = opt_model['model']
|
||||||
|
processor = opt_model['processor']
|
||||||
|
|
||||||
|
self.model.to(dtype).to(device)
|
||||||
|
|
||||||
|
B, H, W, C = images.shape
|
||||||
images = images.to(device)
|
images = images.to(device)
|
||||||
processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined")
|
|
||||||
pbar = ProgressBar(images.shape[0])
|
|
||||||
autocast_condition = (dtype != torch.float32) and not model_management.is_device_mps(device)
|
autocast_condition = (dtype != torch.float32) and not model_management.is_device_mps(device)
|
||||||
with torch.autocast(model_management.get_autocast_device(device), dtype=dtype) if autocast_condition else nullcontext():
|
with torch.autocast(model_management.get_autocast_device(device), dtype=dtype) if autocast_condition else nullcontext():
|
||||||
for image in images:
|
|
||||||
image = (image* 255).type(torch.uint8)
|
PIL_images = [Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) for image in images ]
|
||||||
prompt = text
|
prompt = [text] * len(images)
|
||||||
input_prc = processor(text=prompt, images=image, return_tensors="pt")
|
input_prc = processor(text=prompt, images=PIL_images, return_tensors="pt")
|
||||||
# Move the processed input to the device
|
|
||||||
for key in input_prc:
|
for key in input_prc:
|
||||||
input_prc[key] = input_prc[key].to(device)
|
input_prc[key] = input_prc[key].to(device)
|
||||||
|
outputs = self.model(**input_prc)
|
||||||
|
|
||||||
outputs = model(**input_prc)
|
mask_tensor = torch.sigmoid(outputs.logits)
|
||||||
|
mask_tensor = (mask_tensor - mask_tensor.min()) / (mask_tensor.max() - mask_tensor.min())
|
||||||
|
mask_tensor = torch.where(mask_tensor > (threshold), mask_tensor, torch.tensor(0, dtype=torch.float))
|
||||||
|
print(mask_tensor.shape)
|
||||||
|
if len(mask_tensor.shape) == 2:
|
||||||
|
mask_tensor = mask_tensor.unsqueeze(0)
|
||||||
|
mask_tensor = F.interpolate(mask_tensor.unsqueeze(1), size=(H, W), mode='nearest')
|
||||||
|
mask_tensor = mask_tensor.squeeze(1)
|
||||||
|
|
||||||
tensor = torch.sigmoid(outputs[0])
|
self.model.to(offload_device)
|
||||||
tensor_thresholded = torch.where(tensor > threshold, tensor, torch.tensor(0, dtype=torch.float))
|
|
||||||
tensor_normalized = (tensor_thresholded - tensor_thresholded.min()) / (tensor_thresholded.max() - tensor_thresholded.min())
|
|
||||||
tensor = tensor_normalized
|
|
||||||
|
|
||||||
# Resize the mask
|
|
||||||
if len(tensor.shape) == 3:
|
|
||||||
tensor = tensor.unsqueeze(0)
|
|
||||||
resized_tensor = F.interpolate(tensor, size=(height, width), mode='nearest')
|
|
||||||
|
|
||||||
# Remove the extra dimensions
|
|
||||||
resized_tensor = resized_tensor[0, 0, :, :]
|
|
||||||
pbar.update(1)
|
|
||||||
out.append(resized_tensor)
|
|
||||||
|
|
||||||
results = torch.stack(out).cpu().float()
|
|
||||||
|
|
||||||
if combine_mask:
|
|
||||||
combined_results = torch.max(results, dim=0)[0]
|
|
||||||
results = combined_results.unsqueeze(0).repeat(len(images),1,1)
|
|
||||||
|
|
||||||
if binary_mask:
|
if binary_mask:
|
||||||
results = results.round()
|
mask_tensor = (mask_tensor > 0).float()
|
||||||
|
if blur_sigma > 0:
|
||||||
|
kernel_size = int(6 * int(blur_sigma) + 1)
|
||||||
|
blur = transforms.GaussianBlur(kernel_size=(kernel_size, kernel_size), sigma=(blur_sigma, blur_sigma))
|
||||||
|
mask_tensor = blur(mask_tensor)
|
||||||
|
|
||||||
return results,
|
if combine_mask:
|
||||||
|
mask_tensor = torch.max(mask_tensor, dim=0)[0]
|
||||||
|
mask_tensor = mask_tensor.unsqueeze(0).repeat(len(images),1,1)
|
||||||
|
|
||||||
|
del outputs
|
||||||
|
model_management.soft_empty_cache()
|
||||||
|
|
||||||
|
if prev_mask is not None:
|
||||||
|
if prev_mask.shape != mask_tensor.shape:
|
||||||
|
prev_mask = F.interpolate(prev_mask.unsqueeze(1), size=(H, W), mode='nearest')
|
||||||
|
mask_tensor = mask_tensor + prev_mask.to(device)
|
||||||
|
torch.clamp(mask_tensor, min=0.0, max=1.0)
|
||||||
|
|
||||||
|
if invert:
|
||||||
|
mask_tensor = 1 - mask_tensor
|
||||||
|
|
||||||
|
image_tensor = images * mask_tensor.unsqueeze(-1) + (1 - mask_tensor.unsqueeze(-1)) * image_bg_level
|
||||||
|
image_tensor = torch.clamp(image_tensor, min=0.0, max=1.0).cpu().float()
|
||||||
|
|
||||||
|
mask_tensor = mask_tensor.cpu().float()
|
||||||
|
|
||||||
|
return mask_tensor, image_tensor,
|
||||||
|
|
||||||
|
class DownloadAndLoadCLIPSeg:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
|
||||||
|
return {"required":
|
||||||
|
{
|
||||||
|
"model": (
|
||||||
|
[ 'Kijai/clipseg-rd64-refined-fp16',
|
||||||
|
'CIDAS/clipseg-rd64-refined',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CATEGORY = "KJNodes/masking"
|
||||||
|
RETURN_TYPES = ("CLIPSEGMODEL",)
|
||||||
|
RETURN_NAMES = ("clipseg_model",)
|
||||||
|
FUNCTION = "segment_image"
|
||||||
|
DESCRIPTION = """
|
||||||
|
Downloads and loads CLIPSeg model with huggingface_hub,
|
||||||
|
to ComfyUI/models/clip_seg
|
||||||
|
"""
|
||||||
|
|
||||||
|
def segment_image(self, model):
|
||||||
|
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
|
||||||
|
checkpoint_path = os.path.join(folder_paths.models_dir,'clip_seg', os.path.basename(model))
|
||||||
|
if not hasattr(self, "model"):
|
||||||
|
if not os.path.exists(checkpoint_path):
|
||||||
|
from huggingface_hub import snapshot_download
|
||||||
|
snapshot_download(repo_id=model, local_dir=checkpoint_path, local_dir_use_symlinks=False)
|
||||||
|
self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path)
|
||||||
|
|
||||||
|
processor = CLIPSegProcessor.from_pretrained(checkpoint_path)
|
||||||
|
|
||||||
|
clipseg_model = {}
|
||||||
|
clipseg_model['model'] = self.model
|
||||||
|
clipseg_model['processor'] = processor
|
||||||
|
|
||||||
|
return clipseg_model,
|
||||||
|
|
||||||
class CreateTextMask:
|
class CreateTextMask:
|
||||||
|
|
||||||
@@ -277,7 +358,7 @@ class CreateFluidMask:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"invert": ("BOOLEAN", {"default": False}),
|
"invert": ("BOOLEAN", {"default": False}),
|
||||||
"frames": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}),
|
"frames": ("INT", {"default": 1,"min": 1, "max": 4096, "step": 1}),
|
||||||
"width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
|
"width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
|
||||||
"height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
|
"height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
|
||||||
"inflow_count": ("INT", {"default": 3,"min": 0, "max": 255, "step": 1}),
|
"inflow_count": ("INT", {"default": 3,"min": 0, "max": 255, "step": 1}),
|
||||||
@@ -290,6 +371,9 @@ class CreateFluidMask:
|
|||||||
#using code from https://github.com/GregTJ/stable-fluids
|
#using code from https://github.com/GregTJ/stable-fluids
|
||||||
def createfluidmask(self, frames, width, height, invert, inflow_count, inflow_velocity, inflow_radius, inflow_padding, inflow_duration):
|
def createfluidmask(self, frames, width, height, invert, inflow_count, inflow_velocity, inflow_radius, inflow_padding, inflow_duration):
|
||||||
from ..utility.fluid import Fluid
|
from ..utility.fluid import Fluid
|
||||||
|
try:
|
||||||
|
from scipy.special import erf
|
||||||
|
except:
|
||||||
from scipy.spatial import erf
|
from scipy.spatial import erf
|
||||||
out = []
|
out = []
|
||||||
masks = []
|
masks = []
|
||||||
@@ -444,7 +528,7 @@ class CreateFadeMask:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"invert": ("BOOLEAN", {"default": False}),
|
"invert": ("BOOLEAN", {"default": False}),
|
||||||
"frames": ("INT", {"default": 2,"min": 2, "max": 255, "step": 1}),
|
"frames": ("INT", {"default": 2,"min": 2, "max": 10000, "step": 1}),
|
||||||
"width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
|
"width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
|
||||||
"height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
|
"height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
|
||||||
"interpolation": (["linear", "ease_in", "ease_out", "ease_in_out"],),
|
"interpolation": (["linear", "ease_in", "ease_out", "ease_in_out"],),
|
||||||
@@ -531,10 +615,10 @@ and interpolating from that to fully black at the 16th frame.
|
|||||||
"required": {
|
"required": {
|
||||||
"points_string": ("STRING", {"default": "0:(0.0),\n7:(1.0),\n15:(0.0)\n", "multiline": True}),
|
"points_string": ("STRING", {"default": "0:(0.0),\n7:(1.0),\n15:(0.0)\n", "multiline": True}),
|
||||||
"invert": ("BOOLEAN", {"default": False}),
|
"invert": ("BOOLEAN", {"default": False}),
|
||||||
"frames": ("INT", {"default": 16,"min": 2, "max": 255, "step": 1}),
|
"frames": ("INT", {"default": 16,"min": 2, "max": 10000, "step": 1}),
|
||||||
"width": ("INT", {"default": 512,"min": 1, "max": 4096, "step": 1}),
|
"width": ("INT", {"default": 512,"min": 1, "max": 4096, "step": 1}),
|
||||||
"height": ("INT", {"default": 512,"min": 1, "max": 4096, "step": 1}),
|
"height": ("INT", {"default": 512,"min": 1, "max": 4096, "step": 1}),
|
||||||
"interpolation": (["linear", "ease_in", "ease_out", "ease_in_out"],),
|
"interpolation": (["linear", "ease_in", "ease_out", "ease_in_out", "none", "default_to_black"],),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,7 +642,7 @@ and interpolating from that to fully black at the 16th frame.
|
|||||||
points.append((frame, color))
|
points.append((frame, color))
|
||||||
|
|
||||||
# Check if the last frame is already in the points
|
# Check if the last frame is already in the points
|
||||||
if len(points) == 0 or points[-1][0] != frames - 1:
|
if (interpolation != "default_to_black") and (len(points) == 0 or points[-1][0] != frames - 1):
|
||||||
# If not, add it with the color of the last specified frame
|
# If not, add it with the color of the last specified frame
|
||||||
points.append((frames - 1, points[-1][1] if points else 0))
|
points.append((frames - 1, points[-1][1] if points else 0))
|
||||||
|
|
||||||
@@ -578,6 +662,27 @@ and interpolating from that to fully black at the 16th frame.
|
|||||||
|
|
||||||
# Interpolate between the previous point and the next point
|
# Interpolate between the previous point and the next point
|
||||||
prev_point = next_point - 1
|
prev_point = next_point - 1
|
||||||
|
|
||||||
|
if interpolation == "none":
|
||||||
|
exact_match = False
|
||||||
|
for p in points:
|
||||||
|
if p[0] == i: # Exact frame match
|
||||||
|
color = p[1]
|
||||||
|
exact_match = True
|
||||||
|
break
|
||||||
|
if not exact_match:
|
||||||
|
color = points[prev_point][1]
|
||||||
|
|
||||||
|
elif interpolation == "default_to_black":
|
||||||
|
exact_match = False
|
||||||
|
for p in points:
|
||||||
|
if p[0] == i: # Exact frame match
|
||||||
|
color = p[1]
|
||||||
|
exact_match = True
|
||||||
|
break
|
||||||
|
if not exact_match:
|
||||||
|
color = 0
|
||||||
|
else:
|
||||||
t = (i - points[prev_point][0]) / (points[next_point][0] - points[prev_point][0])
|
t = (i - points[prev_point][0]) / (points[next_point][0] - points[prev_point][0])
|
||||||
if interpolation == "ease_in":
|
if interpolation == "ease_in":
|
||||||
t = ease_in(t)
|
t = ease_in(t)
|
||||||
@@ -589,6 +694,7 @@ and interpolating from that to fully black at the 16th frame.
|
|||||||
pass # No need to modify `t` for linear interpolation
|
pass # No need to modify `t` for linear interpolation
|
||||||
|
|
||||||
color = points[prev_point][1] - t * (points[prev_point][1] - points[next_point][1])
|
color = points[prev_point][1] - t * (points[prev_point][1] - points[next_point][1])
|
||||||
|
|
||||||
color = np.clip(color, 0, 255)
|
color = np.clip(color, 0, 255)
|
||||||
image = np.full((height, width), color, dtype=np.float32)
|
image = np.full((height, width), color, dtype=np.float32)
|
||||||
image_batch[i] = image
|
image_batch[i] = image
|
||||||
@@ -624,6 +730,7 @@ class CreateMagicMask:
|
|||||||
|
|
||||||
def createmagicmask(self, frames, transitions, depth, distortion, seed, frame_width, frame_height):
|
def createmagicmask(self, frames, transitions, depth, distortion, seed, frame_width, frame_height):
|
||||||
from ..utility.magictex import coordinate_grid, random_transform, magic
|
from ..utility.magictex import coordinate_grid, random_transform, magic
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
rng = np.random.default_rng(seed)
|
rng = np.random.default_rng(seed)
|
||||||
out = []
|
out = []
|
||||||
coords = coordinate_grid((frame_width, frame_height))
|
coords = coordinate_grid((frame_width, frame_height))
|
||||||
@@ -903,7 +1010,7 @@ class GrowMaskWithBlur:
|
|||||||
previous_output = None
|
previous_output = None
|
||||||
current_expand = expand
|
current_expand = expand
|
||||||
for m in growmask:
|
for m in growmask:
|
||||||
output = m.numpy()
|
output = m.numpy().astype(np.float32)
|
||||||
for _ in range(abs(round(current_expand))):
|
for _ in range(abs(round(current_expand))):
|
||||||
if current_expand < 0:
|
if current_expand < 0:
|
||||||
output = scipy.ndimage.grey_erosion(output, footprint=kernel)
|
output = scipy.ndimage.grey_erosion(output, footprint=kernel)
|
||||||
@@ -1095,14 +1202,17 @@ Rounds the mask or batch of masks to a binary mask.
|
|||||||
return (mask,)
|
return (mask,)
|
||||||
|
|
||||||
class ResizeMask:
|
class ResizeMask:
|
||||||
|
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"mask": ("MASK",),
|
"mask": ("MASK",),
|
||||||
"width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "display": "number" }),
|
"width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, "display": "number" }),
|
||||||
"height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "display": "number" }),
|
"height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, "display": "number" }),
|
||||||
"keep_proportions": ("BOOLEAN", { "default": False }),
|
"keep_proportions": ("BOOLEAN", { "default": False }),
|
||||||
|
"upscale_method": (s.upscale_methods,),
|
||||||
|
"crop": (["disabled","center"],),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,20 +1224,21 @@ class ResizeMask:
|
|||||||
Resizes the mask or batch of masks to the specified width and height.
|
Resizes the mask or batch of masks to the specified width and height.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def resize(self, mask, width, height, keep_proportions):
|
def resize(self, mask, width, height, keep_proportions, upscale_method,crop):
|
||||||
if keep_proportions:
|
if keep_proportions:
|
||||||
_, oh, ow, _ = mask.shape
|
_, oh, ow = mask.shape
|
||||||
width = ow if width == 0 else width
|
width = ow if width == 0 else width
|
||||||
height = oh if height == 0 else height
|
height = oh if height == 0 else height
|
||||||
ratio = min(width / ow, height / oh)
|
ratio = min(width / ow, height / oh)
|
||||||
width = round(ow*ratio)
|
width = round(ow*ratio)
|
||||||
height = round(oh*ratio)
|
height = round(oh*ratio)
|
||||||
|
|
||||||
outputs = mask.unsqueeze(0) # Add an extra dimension for batch size
|
if upscale_method == "lanczos":
|
||||||
outputs = F.interpolate(outputs, size=(height, width), mode="nearest")
|
out_mask = common_upscale(mask.unsqueeze(1).repeat(1, 3, 1, 1), width, height, upscale_method, crop=crop).movedim(1,-1)[:, :, :, 0]
|
||||||
outputs = outputs.squeeze(0) # Remove the extra dimension after interpolation
|
else:
|
||||||
|
out_mask = common_upscale(mask.unsqueeze(1), width, height, upscale_method, crop=crop).squeeze(1)
|
||||||
|
|
||||||
return(outputs, outputs.shape[2], outputs.shape[1],)
|
return(out_mask, out_mask.shape[2], out_mask.shape[1],)
|
||||||
|
|
||||||
class RemapMaskRange:
|
class RemapMaskRange:
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1164,3 +1275,153 @@ Sets new min and max values for the mask.
|
|||||||
scaled_mask = torch.clamp(scaled_mask, min=0.0, max=1.0)
|
scaled_mask = torch.clamp(scaled_mask, min=0.0, max=1.0)
|
||||||
|
|
||||||
return (scaled_mask, )
|
return (scaled_mask, )
|
||||||
|
|
||||||
|
|
||||||
|
def get_mask_polygon(self, mask_np):
|
||||||
|
import cv2
|
||||||
|
"""Helper function to get polygon points from mask"""
|
||||||
|
# Find contours
|
||||||
|
contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
if not contours:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get the largest contour
|
||||||
|
largest_contour = max(contours, key=cv2.contourArea)
|
||||||
|
|
||||||
|
# Approximate polygon
|
||||||
|
epsilon = 0.02 * cv2.arcLength(largest_contour, True)
|
||||||
|
polygon = cv2.approxPolyDP(largest_contour, epsilon, True)
|
||||||
|
|
||||||
|
return polygon.squeeze()
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
class SeparateMasks:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"mask": ("MASK", ),
|
||||||
|
"size_threshold_width" : ("INT", {"default": 256, "min": 0.0, "max": 4096, "step": 1}),
|
||||||
|
"size_threshold_height" : ("INT", {"default": 256, "min": 0.0, "max": 4096, "step": 1}),
|
||||||
|
"mode": (["convex_polygons", "area", "box"],),
|
||||||
|
"max_poly_points": ("INT", {"default": 8, "min": 3, "max": 32, "step": 1}),
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("MASK",)
|
||||||
|
RETURN_NAMES = ("mask",)
|
||||||
|
FUNCTION = "separate"
|
||||||
|
CATEGORY = "KJNodes/masking"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
DESCRIPTION = "Separates a mask into multiple masks based on the size of the connected components."
|
||||||
|
|
||||||
|
def polygon_to_mask(self, polygon, shape):
|
||||||
|
mask = np.zeros((shape[0], shape[1]), dtype=np.uint8) # Fixed shape handling
|
||||||
|
|
||||||
|
if len(polygon.shape) == 2: # Check if polygon points are valid
|
||||||
|
polygon = polygon.astype(np.int32)
|
||||||
|
cv2.fillPoly(mask, [polygon], 1)
|
||||||
|
return mask
|
||||||
|
|
||||||
|
def get_mask_polygon(self, mask_np, max_points):
|
||||||
|
contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
if not contours:
|
||||||
|
return None
|
||||||
|
|
||||||
|
largest_contour = max(contours, key=cv2.contourArea)
|
||||||
|
hull = cv2.convexHull(largest_contour)
|
||||||
|
|
||||||
|
# Initialize with smaller epsilon for more points
|
||||||
|
perimeter = cv2.arcLength(hull, True)
|
||||||
|
epsilon = perimeter * 0.01 # Start smaller
|
||||||
|
|
||||||
|
min_eps = perimeter * 0.001 # Much smaller minimum
|
||||||
|
max_eps = perimeter * 0.2 # Smaller maximum
|
||||||
|
|
||||||
|
best_approx = None
|
||||||
|
best_diff = float('inf')
|
||||||
|
max_iterations = 20
|
||||||
|
|
||||||
|
#print(f"Target points: {max_points}, Perimeter: {perimeter}")
|
||||||
|
|
||||||
|
for i in range(max_iterations):
|
||||||
|
curr_eps = (min_eps + max_eps) / 2
|
||||||
|
approx = cv2.approxPolyDP(hull, curr_eps, True)
|
||||||
|
points_diff = len(approx) - max_points
|
||||||
|
|
||||||
|
#print(f"Iteration {i}: points={len(approx)}, eps={curr_eps:.4f}")
|
||||||
|
|
||||||
|
if abs(points_diff) < best_diff:
|
||||||
|
best_approx = approx
|
||||||
|
best_diff = abs(points_diff)
|
||||||
|
|
||||||
|
if len(approx) > max_points:
|
||||||
|
min_eps = curr_eps * 1.1 # More gradual adjustment
|
||||||
|
elif len(approx) < max_points:
|
||||||
|
max_eps = curr_eps * 0.9 # More gradual adjustment
|
||||||
|
else:
|
||||||
|
return approx.squeeze()
|
||||||
|
|
||||||
|
if abs(max_eps - min_eps) < perimeter * 0.0001: # Relative tolerance
|
||||||
|
break
|
||||||
|
|
||||||
|
# If we didn't find exact match, return best approximation
|
||||||
|
return best_approx.squeeze() if best_approx is not None else hull.squeeze()
|
||||||
|
|
||||||
|
def separate(self, mask: torch.Tensor, size_threshold_width: int, size_threshold_height: int, max_poly_points: int, mode: str):
|
||||||
|
from scipy.ndimage import label, center_of_mass
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
B, H, W = mask.shape
|
||||||
|
separated = []
|
||||||
|
|
||||||
|
mask = mask.round()
|
||||||
|
|
||||||
|
for b in range(B):
|
||||||
|
mask_np = mask[b].cpu().numpy().astype(np.uint8)
|
||||||
|
structure = np.ones((3, 3), dtype=np.int8)
|
||||||
|
labeled, ncomponents = label(mask_np, structure=structure)
|
||||||
|
pbar = ProgressBar(ncomponents)
|
||||||
|
|
||||||
|
for component in range(1, ncomponents + 1):
|
||||||
|
component_mask_np = (labeled == component).astype(np.uint8)
|
||||||
|
|
||||||
|
rows = np.any(component_mask_np, axis=1)
|
||||||
|
cols = np.any(component_mask_np, axis=0)
|
||||||
|
y_min, y_max = np.where(rows)[0][[0, -1]]
|
||||||
|
x_min, x_max = np.where(cols)[0][[0, -1]]
|
||||||
|
|
||||||
|
width = x_max - x_min + 1
|
||||||
|
height = y_max - y_min + 1
|
||||||
|
centroid_x = (x_min + x_max) / 2 # Calculate x centroid
|
||||||
|
print(f"Component {component}: width={width}, height={height}, x_pos={centroid_x}")
|
||||||
|
|
||||||
|
if width >= size_threshold_width and height >= size_threshold_height:
|
||||||
|
if mode == "convex_polygons":
|
||||||
|
polygon = self.get_mask_polygon(component_mask_np, max_poly_points)
|
||||||
|
if polygon is not None:
|
||||||
|
poly_mask = self.polygon_to_mask(polygon, (H, W))
|
||||||
|
poly_mask = torch.tensor(poly_mask, device=mask.device)
|
||||||
|
separated.append((centroid_x, poly_mask))
|
||||||
|
elif mode == "box":
|
||||||
|
# Create bounding box mask
|
||||||
|
box_mask = np.zeros((H, W), dtype=np.uint8)
|
||||||
|
box_mask[y_min:y_max+1, x_min:x_max+1] = 1
|
||||||
|
box_mask = torch.tensor(box_mask, device=mask.device)
|
||||||
|
separated.append((centroid_x, box_mask))
|
||||||
|
else:
|
||||||
|
area_mask = torch.tensor(component_mask_np, device=mask.device)
|
||||||
|
separated.append((centroid_x, area_mask))
|
||||||
|
pbar.update(1)
|
||||||
|
|
||||||
|
if len(separated) > 0:
|
||||||
|
# Sort by x position and extract only the masks
|
||||||
|
separated.sort(key=lambda x: x[0])
|
||||||
|
separated = [x[1] for x in separated]
|
||||||
|
out_masks = torch.stack(separated, dim=0)
|
||||||
|
return out_masks,
|
||||||
|
else:
|
||||||
|
return torch.empty((1, 64, 64), device=mask.device),
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
+1209
-216
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
[project]
|
||||||
|
name = "comfyui-kjnodes"
|
||||||
|
description = "Various quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability."
|
||||||
|
version = "1.1.4"
|
||||||
|
license = {file = "LICENSE"}
|
||||||
|
dependencies = ["librosa", "numpy", "pillow>=10.3.0", "scipy", "color-matcher", "matplotlib", "huggingface_hub"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/kijai/ComfyUI-KJNodes"
|
||||||
|
# Used by Comfy Registry https://comfyregistry.org
|
||||||
|
|
||||||
|
[tool.comfy]
|
||||||
|
PublisherId = "kijai"
|
||||||
|
DisplayName = "ComfyUI-KJNodes"
|
||||||
|
Icon = "https://avatars.githubusercontent.com/u/40791699"
|
||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
librosa
|
|
||||||
numpy
|
|
||||||
pillow>=10.3.0
|
pillow>=10.3.0
|
||||||
scipy
|
scipy
|
||||||
color-matcher
|
color-matcher
|
||||||
matplotlib
|
matplotlib
|
||||||
|
huggingface_hub
|
||||||
|
mss
|
||||||
|
opencv-python
|
||||||
@@ -47,7 +47,9 @@ app.registerExtension({
|
|||||||
)
|
)
|
||||||
if (pythongossFeed) {
|
if (pythongossFeed) {
|
||||||
console.warn("KJNodes - Overriding pysssss.FaviconStatus")
|
console.warn("KJNodes - Overriding pysssss.FaviconStatus")
|
||||||
app.extensions = app.extensions.filter(item => item !== pythongossFeed);
|
pythongossFeed.setup = function() {
|
||||||
|
console.warn("Disabled by KJNodes")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
+46
-51
@@ -48,105 +48,100 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setup(app) {
|
async setup(app) {
|
||||||
const onChange = (value) => {
|
const updateSlots = (value) => {
|
||||||
if (value) {
|
|
||||||
const valuesToAddToIn = ["GetNode"];
|
const valuesToAddToIn = ["GetNode"];
|
||||||
const valuesToAddToOut = ["SetNode"];
|
const valuesToAddToOut = ["SetNode"];
|
||||||
|
// Remove entries if they exist
|
||||||
for (const arr of Object.values(LiteGraph.slot_types_default_in)) {
|
for (const arr of Object.values(LiteGraph.slot_types_default_in)) {
|
||||||
for (const valueToAdd of valuesToAddToIn) {
|
for (const valueToAdd of valuesToAddToIn) {
|
||||||
const idx = arr.indexOf(valueToAdd);
|
const idx = arr.indexOf(valueToAdd);
|
||||||
if (idx !== 0) {
|
if (idx !== -1) {
|
||||||
arr.splice(idx, 1);
|
arr.splice(idx, 1);
|
||||||
}
|
}
|
||||||
arr.unshift(valueToAdd);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const arr of Object.values(LiteGraph.slot_types_default_out)) {
|
for (const arr of Object.values(LiteGraph.slot_types_default_out)) {
|
||||||
for (const valueToAdd of valuesToAddToOut) {
|
for (const valueToAdd of valuesToAddToOut) {
|
||||||
const idx = arr.indexOf(valueToAdd);
|
const idx = arr.indexOf(valueToAdd);
|
||||||
if (idx !== 0) {
|
if (idx !== -1) {
|
||||||
arr.splice(idx, 1);
|
arr.splice(idx, 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value!="disabled") {
|
||||||
|
for (const arr of Object.values(LiteGraph.slot_types_default_in)) {
|
||||||
|
for (const valueToAdd of valuesToAddToIn) {
|
||||||
|
const idx = arr.indexOf(valueToAdd);
|
||||||
|
if (idx !== -1) {
|
||||||
|
arr.splice(idx, 1);
|
||||||
|
}
|
||||||
|
if (value === "top") {
|
||||||
arr.unshift(valueToAdd);
|
arr.unshift(valueToAdd);
|
||||||
|
} else {
|
||||||
|
arr.push(valueToAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const arr of Object.values(LiteGraph.slot_types_default_out)) {
|
||||||
|
for (const valueToAdd of valuesToAddToOut) {
|
||||||
|
const idx = arr.indexOf(valueToAdd);
|
||||||
|
if (idx !== -1) {
|
||||||
|
arr.splice(idx, 1);
|
||||||
|
}
|
||||||
|
if (value === "top") {
|
||||||
|
arr.unshift(valueToAdd);
|
||||||
|
} else {
|
||||||
|
arr.push(valueToAdd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "🦛 KJNodes.SetGetMenu",
|
id: "KJNodes.SetGetMenu",
|
||||||
name: "🦛 KJNodes: Make Set/Get -nodes defaults (turn off and reload to disable)",
|
name: "KJNodes: Make Set/Get -nodes defaults",
|
||||||
defaultValue: false,
|
tooltip: 'Adds Set/Get nodes to the top or bottom of the list of available node suggestions.',
|
||||||
type: "boolean",
|
options: ['disabled', 'top', 'bottom'],
|
||||||
options: (value) => [
|
defaultValue: 'disabled',
|
||||||
{
|
type: "combo",
|
||||||
value: true,
|
onChange: updateSlots,
|
||||||
text: "On",
|
|
||||||
selected: value === true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: false,
|
|
||||||
text: "Off",
|
|
||||||
selected: value === false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onChange: onChange,
|
|
||||||
|
|
||||||
});
|
});
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "KJNodes.DisableMiddleClickDefault",
|
id: "KJNodes.MiddleClickDefault",
|
||||||
name: "🦛 KJNodes: Middle click default node adding",
|
name: "KJNodes: Middle click default node adding",
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
options: (value) => [
|
|
||||||
{ value: true, text: "On", selected: value === true },
|
|
||||||
{ value: false, text: "Off", selected: value === false },
|
|
||||||
],
|
|
||||||
onChange: (value) => {
|
onChange: (value) => {
|
||||||
LiteGraph.middle_click_slot_add_default_node = value;
|
LiteGraph.middle_click_slot_add_default_node = value;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "KJNodes.nodeAutoColor",
|
id: "KJNodes.nodeAutoColor",
|
||||||
name: "🦛 KJNodes: Automatically set node colors",
|
name: "KJNodes: Automatically set node colors",
|
||||||
defaultValue: true,
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
options: (value) => [
|
defaultValue: true,
|
||||||
{ value: true, text: "On", selected: value === true },
|
|
||||||
{ value: false, text: "Off", selected: value === false },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "KJNodes.helpPopup",
|
id: "KJNodes.helpPopup",
|
||||||
name: "🦛 KJNodes: Help popups",
|
name: "KJNodes: Help popups",
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
options: (value) => [
|
|
||||||
{ value: true, text: "On", selected: value === true },
|
|
||||||
{ value: false, text: "Off", selected: value === false },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "KJNodes.disablePrefix",
|
id: "KJNodes.disablePrefix",
|
||||||
name: "🦛 KJNodes: Disable automatic Set_ and Get_ prefix",
|
name: "KJNodes: Disable automatic Set_ and Get_ prefix",
|
||||||
defaultValue: false,
|
defaultValue: true,
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
options: (value) => [
|
|
||||||
{ value: true, text: "On", selected: value === true },
|
|
||||||
{ value: false, text: "Off", selected: value === false },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "KJNodes.browserStatus",
|
id: "KJNodes.browserStatus",
|
||||||
name: "🦛 KJNodes: 🟢 Stoplight browser status icon 🔴",
|
name: "KJNodes: 🟢 Stoplight browser status icon 🔴",
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
options: (value) => [
|
|
||||||
{ value: true, text: "On", selected: value === true },
|
|
||||||
{ value: false, text: "Off", selected: value === false },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { app } from '../../../scripts/app.js'
|
||||||
|
|
||||||
|
//from melmass
|
||||||
|
export function makeUUID() {
|
||||||
|
let dt = new Date().getTime()
|
||||||
|
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = ((dt + Math.random() * 16) % 16) | 0
|
||||||
|
dt = Math.floor(dt / 16)
|
||||||
|
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||||
|
})
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
function chainCallback(object, property, callback) {
|
||||||
|
if (object == undefined) {
|
||||||
|
//This should not happen.
|
||||||
|
console.error("Tried to add callback to non-existant object")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (property in object) {
|
||||||
|
const callback_orig = object[property]
|
||||||
|
object[property] = function () {
|
||||||
|
const r = callback_orig.apply(this, arguments);
|
||||||
|
callback.apply(this, arguments);
|
||||||
|
return r
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
object[property] = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.registerExtension({
|
||||||
|
name: 'KJNodes.FastPreview',
|
||||||
|
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
|
if (nodeData?.name === 'FastPreview') {
|
||||||
|
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
||||||
|
|
||||||
|
var element = document.createElement("div");
|
||||||
|
this.uuid = makeUUID()
|
||||||
|
element.id = `fast-preview-${this.uuid}`
|
||||||
|
|
||||||
|
this.previewWidget = this.addDOMWidget(nodeData.name, "FastPreviewWidget", element, {
|
||||||
|
serialize: false,
|
||||||
|
hideOnZoom: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.previewer = new Previewer(this);
|
||||||
|
|
||||||
|
this.setSize([550, 550]);
|
||||||
|
this.resizable = false;
|
||||||
|
this.previewWidget.parentEl = document.createElement("div");
|
||||||
|
this.previewWidget.parentEl.className = "fast-preview";
|
||||||
|
this.previewWidget.parentEl.id = `fast-preview-${this.uuid}`
|
||||||
|
element.appendChild(this.previewWidget.parentEl);
|
||||||
|
|
||||||
|
chainCallback(this, "onExecuted", function (message) {
|
||||||
|
let bg_image = message["bg_image"];
|
||||||
|
this.properties.imgData = {
|
||||||
|
name: "bg_image",
|
||||||
|
base64: bg_image
|
||||||
|
};
|
||||||
|
this.previewer.refreshBackgroundImage(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}); // onAfterGraphConfigured
|
||||||
|
}//node created
|
||||||
|
} //before register
|
||||||
|
})//register
|
||||||
|
|
||||||
|
class Previewer {
|
||||||
|
constructor(context) {
|
||||||
|
this.node = context;
|
||||||
|
this.previousWidth = null;
|
||||||
|
this.previousHeight = null;
|
||||||
|
}
|
||||||
|
refreshBackgroundImage = () => {
|
||||||
|
const imgData = this.node?.properties?.imgData;
|
||||||
|
if (imgData?.base64) {
|
||||||
|
const base64String = imgData.base64;
|
||||||
|
const imageUrl = `data:${imgData.type};base64,${base64String}`;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = imageUrl;
|
||||||
|
img.onload = () => {
|
||||||
|
const { width, height } = img;
|
||||||
|
if (width !== this.previousWidth || height !== this.previousHeight) {
|
||||||
|
this.node.setSize([width, height]);
|
||||||
|
this.previousWidth = width;
|
||||||
|
this.previousHeight = height;
|
||||||
|
}
|
||||||
|
this.node.previewWidget.element.style.backgroundImage = `url(${imageUrl})`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -38,14 +38,14 @@ export const loadScript = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadScript('/kjweb_async/marked.min.js').catch((e) => {
|
loadScript('kjweb_async/marked.min.js').catch((e) => {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
})
|
})
|
||||||
loadScript('/kjweb_async/purify.min.js').catch((e) => {
|
loadScript('kjweb_async/purify.min.js').catch((e) => {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
const categories = ["KJNodes", "SUPIR", "VoiceCraft", "Marigold"];
|
const categories = ["KJNodes", "SUPIR", "VoiceCraft", "Marigold", "IC-Light", "WanVideoWrapper"];
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "KJNodes.HelpPopup",
|
name: "KJNodes.HelpPopup",
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
@@ -257,12 +257,13 @@ const create_documentation_stylesheet = () => {
|
|||||||
|
|
||||||
const scale = new DOMMatrix()
|
const scale = new DOMMatrix()
|
||||||
.scaleSelf(transform.a, transform.d);
|
.scaleSelf(transform.a, transform.d);
|
||||||
|
const bcr = app.canvas.canvas.getBoundingClientRect()
|
||||||
|
|
||||||
const styleObject = {
|
const styleObject = {
|
||||||
transformOrigin: '0 0',
|
transformOrigin: '0 0',
|
||||||
transform: scale,
|
transform: scale,
|
||||||
left: `${transform.a + transform.e}px`,
|
left: `${transform.a + bcr.x + transform.e}px`,
|
||||||
top: `${transform.d + transform.f}px`,
|
top: `${transform.d + bcr.y + transform.f}px`,
|
||||||
};
|
};
|
||||||
Object.assign(docElement.style, styleObject);
|
Object.assign(docElement.style, styleObject);
|
||||||
}
|
}
|
||||||
|
|||||||
+187
-50
@@ -1,4 +1,5 @@
|
|||||||
import { app } from "../../../scripts/app.js";
|
import { app } from "../../../scripts/app.js";
|
||||||
|
import { applyTextReplacements } from "../../../scripts/utils.js";
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "KJNodes.jsnodes",
|
name: "KJNodes.jsnodes",
|
||||||
@@ -9,87 +10,158 @@ app.registerExtension({
|
|||||||
switch (nodeData.name) {
|
switch (nodeData.name) {
|
||||||
case "ConditioningMultiCombine":
|
case "ConditioningMultiCombine":
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
this.cond_type = "CONDITIONING"
|
this._type = "CONDITIONING"
|
||||||
this.inputs_offset = nodeData.name.includes("selective")?1:0
|
this.inputs_offset = nodeData.name.includes("selective")?1:0
|
||||||
this.addWidget("button", "Update inputs", null, () => {
|
this.addWidget("button", "Update inputs", null, () => {
|
||||||
if (!this.inputs) {
|
if (!this.inputs) {
|
||||||
this.inputs = [];
|
this.inputs = [];
|
||||||
}
|
}
|
||||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||||
if(target_number_of_inputs===this.inputs.length)return; // already set, do nothing
|
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||||
|
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||||
|
|
||||||
if(target_number_of_inputs < this.inputs.length){
|
if(target_number_of_inputs < num_inputs){
|
||||||
for(let i = this.inputs.length; i>=this.inputs_offset+target_number_of_inputs; i--)
|
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||||
this.removeInput(i)
|
for(let i = 0; i < inputs_to_remove; i++) {
|
||||||
|
this.removeInput(this.inputs.length - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
for(let i = this.inputs.length+1-this.inputs_offset; i <= target_number_of_inputs; ++i)
|
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||||
this.addInput(`conditioning_${i}`, this.cond_type)
|
this.addInput(`conditioning_${i}`, this._type)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "ImageBatchMulti":
|
case "ImageBatchMulti":
|
||||||
|
case "ImageAddMulti":
|
||||||
|
case "ImageConcatMulti":
|
||||||
|
case "CrossFadeImagesMulti":
|
||||||
|
case "TransitionImagesMulti":
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
this._type = "IMAGE"
|
this._type = "IMAGE"
|
||||||
this.inputs_offset = nodeData.name.includes("selective")?1:0
|
|
||||||
this.addWidget("button", "Update inputs", null, () => {
|
this.addWidget("button", "Update inputs", null, () => {
|
||||||
if (!this.inputs) {
|
if (!this.inputs) {
|
||||||
this.inputs = [];
|
this.inputs = [];
|
||||||
}
|
}
|
||||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||||
if(target_number_of_inputs===this.inputs.length)return; // already set, do nothing
|
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||||
|
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||||
|
|
||||||
if(target_number_of_inputs < this.inputs.length){
|
if(target_number_of_inputs < num_inputs){
|
||||||
for(let i = this.inputs.length; i>=this.inputs_offset+target_number_of_inputs; i--)
|
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||||
this.removeInput(i)
|
for(let i = 0; i < inputs_to_remove; i++) {
|
||||||
|
this.removeInput(this.inputs.length - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
for(let i = this.inputs.length+1-this.inputs_offset; i <= target_number_of_inputs; ++i)
|
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||||
this.addInput(`image_${i}`, this._type)
|
this.addInput(`image_${i}`, this._type, {shape: 7});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "MaskBatchMulti":
|
case "MaskBatchMulti":
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
this._type = "MASK"
|
this._type = "MASK"
|
||||||
this.inputs_offset = nodeData.name.includes("selective")?1:0
|
|
||||||
this.addWidget("button", "Update inputs", null, () => {
|
this.addWidget("button", "Update inputs", null, () => {
|
||||||
if (!this.inputs) {
|
if (!this.inputs) {
|
||||||
this.inputs = [];
|
this.inputs = [];
|
||||||
}
|
}
|
||||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||||
if(target_number_of_inputs===this.inputs.length)return; // already set, do nothing
|
const num_inputs = this.inputs.filter(input => input.type === this._type).length
|
||||||
|
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
|
||||||
|
|
||||||
if(target_number_of_inputs < this.inputs.length){
|
if(target_number_of_inputs < num_inputs){
|
||||||
for(let i = this.inputs.length; i>=this.inputs_offset+target_number_of_inputs; i--)
|
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||||
this.removeInput(i)
|
for(let i = 0; i < inputs_to_remove; i++) {
|
||||||
|
this.removeInput(this.inputs.length - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
for(let i = this.inputs.length+1-this.inputs_offset; i <= target_number_of_inputs; ++i)
|
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||||
this.addInput(`mask_${i}`, this._type)
|
this.addInput(`mask_${i}`, this._type)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "FluxBlockLoraSelect":
|
||||||
|
case "HunyuanVideoBlockLoraSelect":
|
||||||
|
case "Wan21BlockLoraSelect":
|
||||||
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
|
this.addWidget("button", "Set all", null, () => {
|
||||||
|
const userInput = prompt("Enter the values to set for widgets (e.g., s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0):", "");
|
||||||
|
if (userInput) {
|
||||||
|
const regex = /([sd])?(\d+(?:,\d+|-?\d+)*?)?=(\d+(\.\d+)?)/;
|
||||||
|
const match = userInput.match(regex);
|
||||||
|
if (match) {
|
||||||
|
const type = match[1];
|
||||||
|
const indicesPart = match[2];
|
||||||
|
const value = parseFloat(match[3]);
|
||||||
|
|
||||||
|
let targetWidgets = [];
|
||||||
|
if (type === 's') {
|
||||||
|
targetWidgets = this.widgets.filter(widget => widget.name.includes("single"));
|
||||||
|
} else if (type === 'd') {
|
||||||
|
targetWidgets = this.widgets.filter(widget => widget.name.includes("double"));
|
||||||
|
} else {
|
||||||
|
targetWidgets = this.widgets; // No type specified, all widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indicesPart) {
|
||||||
|
const indices = indicesPart.split(',').flatMap(part => {
|
||||||
|
if (part.includes('-')) {
|
||||||
|
const [start, end] = part.split('-').map(Number);
|
||||||
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||||
|
}
|
||||||
|
return Number(part);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const index of indices) {
|
||||||
|
if (index < targetWidgets.length) {
|
||||||
|
targetWidgets[index].value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No indices provided, set value for all target widgets
|
||||||
|
for (const widget of targetWidgets) {
|
||||||
|
widget.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!isNaN(parseFloat(userInput))) {
|
||||||
|
// Single value provided, set it for all widgets
|
||||||
|
const value = parseFloat(userInput);
|
||||||
|
for (const widget of this.widgets) {
|
||||||
|
widget.value = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Invalid input format. Please use the format s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Invalid input. Please enter a value.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
case "GetMaskSizeAndCount":
|
case "GetMaskSizeAndCount":
|
||||||
const onGetMaskSizeConnectInput = nodeType.prototype.onConnectInput;
|
const onGetMaskSizeConnectInput = nodeType.prototype.onConnectInput;
|
||||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||||
const v = onGetMaskSizeConnectInput?.(this, arguments);
|
const v = onGetMaskSizeConnectInput? onGetMaskSizeConnectInput.apply(this, arguments): undefined
|
||||||
targetSlot.outputs[1]["name"] = "width"
|
this.outputs[1]["label"] = "width"
|
||||||
targetSlot.outputs[2]["name"] = "height"
|
this.outputs[2]["label"] = "height"
|
||||||
targetSlot.outputs[3]["name"] = "count"
|
this.outputs[3]["label"] = "count"
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
const onGetMaskSizeExecuted = nodeType.prototype.onExecuted;
|
const onGetMaskSizeExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||||
nodeType.prototype.onExecuted = function(message) {
|
nodeType.prototype.onExecuted = function(message) {
|
||||||
const r = onGetMaskSizeExecuted? onGetMaskSizeExecuted.apply(this,arguments): undefined
|
const r = onGetMaskSizeExecuted? onGetMaskSizeExecuted.apply(this,arguments): undefined
|
||||||
let values = message["text"].toString().split('x').map(Number);
|
let values = message["text"].toString().split('x').map(Number);
|
||||||
this.outputs[1]["name"] = values[1] + " width"
|
this.outputs[1]["label"] = values[1] + " width"
|
||||||
this.outputs[2]["name"] = values[2] + " height"
|
this.outputs[2]["label"] = values[2] + " height"
|
||||||
this.outputs[3]["name"] = values[0] + " count"
|
this.outputs[3]["label"] = values[0] + " count"
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -97,58 +169,112 @@ app.registerExtension({
|
|||||||
case "GetImageSizeAndCount":
|
case "GetImageSizeAndCount":
|
||||||
const onGetImageSizeConnectInput = nodeType.prototype.onConnectInput;
|
const onGetImageSizeConnectInput = nodeType.prototype.onConnectInput;
|
||||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||||
const v = onGetImageSizeConnectInput?.(this, arguments);
|
console.log(this)
|
||||||
targetSlot.outputs[1]["name"] = "width"
|
const v = onGetImageSizeConnectInput? onGetImageSizeConnectInput.apply(this, arguments): undefined
|
||||||
targetSlot.outputs[2]["name"] = "height"
|
//console.log(this)
|
||||||
targetSlot.outputs[3]["name"] = "count"
|
this.outputs[1]["label"] = "width"
|
||||||
|
this.outputs[2]["label"] = "height"
|
||||||
|
this.outputs[3]["label"] = "count"
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
|
//const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
|
||||||
|
const onGetImageSizeExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||||
nodeType.prototype.onExecuted = function(message) {
|
nodeType.prototype.onExecuted = function(message) {
|
||||||
|
console.log(this)
|
||||||
const r = onGetImageSizeExecuted? onGetImageSizeExecuted.apply(this,arguments): undefined
|
const r = onGetImageSizeExecuted? onGetImageSizeExecuted.apply(this,arguments): undefined
|
||||||
let values = message["text"].toString().split('x').map(Number);
|
let values = message["text"].toString().split('x').map(Number);
|
||||||
this.outputs[1]["name"] = values[1] + " width"
|
console.log(values)
|
||||||
this.outputs[2]["name"] = values[2] + " height"
|
this.outputs[1]["label"] = values[1] + " width"
|
||||||
this.outputs[3]["name"] = values[0] + " count"
|
this.outputs[2]["label"] = values[2] + " height"
|
||||||
|
this.outputs[3]["label"] = values[0] + " count"
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "GetLatentSizeAndCount":
|
||||||
|
const onGetLatentConnectInput = nodeType.prototype.onConnectInput;
|
||||||
|
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||||
|
console.log(this)
|
||||||
|
const v = onGetLatentConnectInput? onGetLatentConnectInput.apply(this, arguments): undefined
|
||||||
|
//console.log(this)
|
||||||
|
this.outputs[1]["label"] = "width"
|
||||||
|
this.outputs[2]["label"] = "height"
|
||||||
|
this.outputs[3]["label"] = "count"
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
//const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
|
||||||
|
const onGetLatentSizeExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||||
|
nodeType.prototype.onExecuted = function(message) {
|
||||||
|
console.log(this)
|
||||||
|
const r = onGetLatentSizeExecuted? onGetLatentSizeExecuted.apply(this,arguments): undefined
|
||||||
|
let values = message["text"].toString().split('x').map(Number);
|
||||||
|
console.log(values)
|
||||||
|
this.outputs[1]["label"] = values[0] + " batch"
|
||||||
|
this.outputs[2]["label"] = values[1] + " channels"
|
||||||
|
this.outputs[3]["label"] = values[2] + " frames"
|
||||||
|
this.outputs[4]["label"] = values[3] + " height"
|
||||||
|
this.outputs[5]["label"] = values[4] + " width"
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "PreviewAnimation":
|
||||||
|
const onPreviewAnimationConnectInput = nodeType.prototype.onConnectInput;
|
||||||
|
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||||
|
const v = onPreviewAnimationConnectInput? onPreviewAnimationConnectInput.apply(this, arguments): undefined
|
||||||
|
this.title = "Preview Animation"
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
const onPreviewAnimationExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||||
|
nodeType.prototype.onExecuted = function(message) {
|
||||||
|
const r = onPreviewAnimationExecuted? onPreviewAnimationExecuted.apply(this,arguments): undefined
|
||||||
|
let values = message["text"].toString();
|
||||||
|
this.title = "Preview Animation " + values
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "VRAM_Debug":
|
case "VRAM_Debug":
|
||||||
const onVRAM_DebugConnectInput = nodeType.prototype.onConnectInput;
|
const onVRAM_DebugConnectInput = nodeType.prototype.onConnectInput;
|
||||||
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
|
||||||
const v = onVRAM_DebugConnectInput?.(this, arguments);
|
const v = onVRAM_DebugConnectInput? onVRAM_DebugConnectInput.apply(this, arguments): undefined
|
||||||
targetSlot.outputs[3]["name"] = "freemem_before"
|
this.outputs[3]["label"] = "freemem_before"
|
||||||
targetSlot.outputs[4]["name"] = "freemem_after"
|
this.outputs[4]["label"] = "freemem_after"
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
const onVRAM_DebugExecuted = nodeType.prototype.onExecuted;
|
const onVRAM_DebugExecuted = nodeType.prototype.onAfterExecuteNode;
|
||||||
nodeType.prototype.onExecuted = function(message) {
|
nodeType.prototype.onExecuted = function(message) {
|
||||||
const r = onVRAM_DebugExecuted? onVRAM_DebugExecuted.apply(this,arguments): undefined
|
const r = onVRAM_DebugExecuted? onVRAM_DebugExecuted.apply(this,arguments): undefined
|
||||||
let values = message["text"].toString().split('x');
|
let values = message["text"].toString().split('x');
|
||||||
this.outputs[3]["name"] = values[0] + " freemem_before"
|
this.outputs[3]["label"] = values[0] + " freemem_before"
|
||||||
this.outputs[4]["name"] = values[1] + " freemem_after"
|
this.outputs[4]["label"] = values[1] + " freemem_after"
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "JoinStringMulti":
|
case "JoinStringMulti":
|
||||||
|
const originalOnNodeCreated = nodeType.prototype.onNodeCreated || function() {};
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
this._type = "STRING"
|
originalOnNodeCreated.apply(this, arguments);
|
||||||
this.inputs_offset = nodeData.name.includes("selective")?1:0
|
|
||||||
|
this._type = "STRING";
|
||||||
this.addWidget("button", "Update inputs", null, () => {
|
this.addWidget("button", "Update inputs", null, () => {
|
||||||
if (!this.inputs) {
|
if (!this.inputs) {
|
||||||
this.inputs = [];
|
this.inputs = [];
|
||||||
}
|
}
|
||||||
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
|
||||||
if(target_number_of_inputs===this.inputs.length)return; // already set, do nothing
|
const num_inputs = this.inputs.filter(input => input.name && input.name.toLowerCase().includes("string_")).length
|
||||||
|
if (target_number_of_inputs === num_inputs) return; // already set, do nothing
|
||||||
|
|
||||||
if(target_number_of_inputs < this.inputs.length){
|
if(target_number_of_inputs < num_inputs){
|
||||||
for(let i = this.inputs.length; i>=this.inputs_offset+target_number_of_inputs; i--)
|
const inputs_to_remove = num_inputs - target_number_of_inputs;
|
||||||
this.removeInput(i)
|
for(let i = 0; i < inputs_to_remove; i++) {
|
||||||
|
this.removeInput(this.inputs.length - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
for(let i = this.inputs.length+1-this.inputs_offset; i <= target_number_of_inputs; ++i)
|
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
|
||||||
this.addInput(`string_${i}`, this._type)
|
this.addInput(`string_${i}`, this._type, {shape: 7});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -255,6 +381,17 @@ app.registerExtension({
|
|||||||
this.addWidget("button", "Stop mic capture", null, stopMicrophoneCapture);
|
this.addWidget("button", "Stop mic capture", null, stopMicrophoneCapture);
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case "SaveImageKJ":
|
||||||
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
|
nodeType.prototype.onNodeCreated = function() {
|
||||||
|
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : void 0;
|
||||||
|
const widget = this.widgets.find((w) => w.name === "filename_prefix");
|
||||||
|
widget.serializeValue = () => {
|
||||||
|
return applyTextReplacements(app, widget.value);
|
||||||
|
};
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,734 @@
|
|||||||
|
import { app } from '../../../scripts/app.js'
|
||||||
|
|
||||||
|
//from melmass
|
||||||
|
export function makeUUID() {
|
||||||
|
let dt = new Date().getTime()
|
||||||
|
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = ((dt + Math.random() * 16) % 16) | 0
|
||||||
|
dt = Math.floor(dt / 16)
|
||||||
|
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||||
|
})
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadScript = (
|
||||||
|
FILE_URL,
|
||||||
|
async = true,
|
||||||
|
type = 'text/javascript',
|
||||||
|
) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Check if the script already exists
|
||||||
|
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
|
||||||
|
if (existingScript) {
|
||||||
|
resolve({ status: true, message: 'Script already loaded' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scriptEle = document.createElement('script')
|
||||||
|
scriptEle.type = type
|
||||||
|
scriptEle.async = async
|
||||||
|
scriptEle.src = FILE_URL
|
||||||
|
|
||||||
|
scriptEle.addEventListener('load', (ev) => {
|
||||||
|
resolve({ status: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
scriptEle.addEventListener('error', (ev) => {
|
||||||
|
reject({
|
||||||
|
status: false,
|
||||||
|
message: `Failed to load the script ${FILE_URL}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
document.body.appendChild(scriptEle)
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const create_documentation_stylesheet = () => {
|
||||||
|
const tag = 'kj-pointseditor-stylesheet'
|
||||||
|
|
||||||
|
let styleTag = document.head.querySelector(tag)
|
||||||
|
|
||||||
|
if (!styleTag) {
|
||||||
|
styleTag = document.createElement('style')
|
||||||
|
styleTag.type = 'text/css'
|
||||||
|
styleTag.id = tag
|
||||||
|
styleTag.innerHTML = `
|
||||||
|
.points-editor {
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
font: 12px monospace;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 10px;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
document.head.appendChild(styleTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadScript('kjweb_async/svg-path-properties.min.js').catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
loadScript('kjweb_async/protovis.min.js').catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
create_documentation_stylesheet()
|
||||||
|
|
||||||
|
function chainCallback(object, property, callback) {
|
||||||
|
if (object == undefined) {
|
||||||
|
//This should not happen.
|
||||||
|
console.error("Tried to add callback to non-existant object")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (property in object) {
|
||||||
|
const callback_orig = object[property]
|
||||||
|
object[property] = function () {
|
||||||
|
const r = callback_orig.apply(this, arguments);
|
||||||
|
callback.apply(this, arguments);
|
||||||
|
return r
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
object[property] = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.registerExtension({
|
||||||
|
name: 'KJNodes.PointEditor',
|
||||||
|
|
||||||
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
|
if (nodeData?.name === 'PointsEditor') {
|
||||||
|
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
||||||
|
|
||||||
|
hideWidgetForGood(this, this.widgets.find(w => w.name === "coordinates"))
|
||||||
|
hideWidgetForGood(this, this.widgets.find(w => w.name === "neg_coordinates"))
|
||||||
|
hideWidgetForGood(this, this.widgets.find(w => w.name === "bboxes"))
|
||||||
|
|
||||||
|
var element = document.createElement("div");
|
||||||
|
this.uuid = makeUUID()
|
||||||
|
element.id = `points-editor-${this.uuid}`
|
||||||
|
|
||||||
|
this.previewMediaType = 'image'
|
||||||
|
|
||||||
|
this.pointsEditor = this.addDOMWidget(nodeData.name, "PointsEditorWidget", element, {
|
||||||
|
serialize: false,
|
||||||
|
hideOnZoom: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// context menu
|
||||||
|
this.contextMenu = document.createElement("div");
|
||||||
|
this.contextMenu.id = "context-menu";
|
||||||
|
this.contextMenu.style.display = "none";
|
||||||
|
this.contextMenu.style.position = "absolute";
|
||||||
|
this.contextMenu.style.backgroundColor = "#202020";
|
||||||
|
this.contextMenu.style.minWidth = "100px";
|
||||||
|
this.contextMenu.style.boxShadow = "0px 8px 16px 0px rgba(0,0,0,0.2)";
|
||||||
|
this.contextMenu.style.zIndex = "100";
|
||||||
|
this.contextMenu.style.padding = "5px";
|
||||||
|
|
||||||
|
function styleMenuItem(menuItem) {
|
||||||
|
menuItem.style.display = "block";
|
||||||
|
menuItem.style.padding = "5px";
|
||||||
|
menuItem.style.color = "#FFF";
|
||||||
|
menuItem.style.fontFamily = "Arial, sans-serif";
|
||||||
|
menuItem.style.fontSize = "16px";
|
||||||
|
menuItem.style.textDecoration = "none";
|
||||||
|
menuItem.style.marginBottom = "5px";
|
||||||
|
}
|
||||||
|
function createMenuItem(id, textContent) {
|
||||||
|
let menuItem = document.createElement("a");
|
||||||
|
menuItem.href = "#";
|
||||||
|
menuItem.id = `menu-item-${id}`;
|
||||||
|
menuItem.textContent = textContent;
|
||||||
|
styleMenuItem(menuItem);
|
||||||
|
return menuItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an array of menu items using the createMenuItem function
|
||||||
|
this.menuItems = [
|
||||||
|
createMenuItem(0, "Load Image"),
|
||||||
|
createMenuItem(1, "Clear Image"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add mouseover and mouseout event listeners to each menu item for styling
|
||||||
|
this.menuItems.forEach(menuItem => {
|
||||||
|
menuItem.addEventListener('mouseover', function () {
|
||||||
|
this.style.backgroundColor = "gray";
|
||||||
|
});
|
||||||
|
|
||||||
|
menuItem.addEventListener('mouseout', function () {
|
||||||
|
this.style.backgroundColor = "#202020";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append each menu item to the context menu
|
||||||
|
this.menuItems.forEach(menuItem => {
|
||||||
|
this.contextMenu.appendChild(menuItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(this.contextMenu);
|
||||||
|
|
||||||
|
this.addWidget("button", "New canvas", null, () => {
|
||||||
|
if (!this.properties || !("points" in this.properties)) {
|
||||||
|
this.editor = new PointsEditor(this);
|
||||||
|
this.addProperty("points", this.constructor.type, "string");
|
||||||
|
this.addProperty("neg_points", this.constructor.type, "string");
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.editor = new PointsEditor(this, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setSize([550, 550]);
|
||||||
|
this.resizable = false;
|
||||||
|
this.pointsEditor.parentEl = document.createElement("div");
|
||||||
|
this.pointsEditor.parentEl.className = "points-editor";
|
||||||
|
this.pointsEditor.parentEl.id = `points-editor-${this.uuid}`
|
||||||
|
element.appendChild(this.pointsEditor.parentEl);
|
||||||
|
|
||||||
|
chainCallback(this, "onConfigure", function () {
|
||||||
|
try {
|
||||||
|
this.editor = new PointsEditor(this);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("An error occurred while configuring the editor:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chainCallback(this, "onExecuted", function (message) {
|
||||||
|
let bg_image = message["bg_image"];
|
||||||
|
this.properties.imgData = {
|
||||||
|
name: "bg_image",
|
||||||
|
base64: bg_image
|
||||||
|
};
|
||||||
|
this.editor.refreshBackgroundImage(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
}); // onAfterGraphConfigured
|
||||||
|
}//node created
|
||||||
|
} //before register
|
||||||
|
})//register
|
||||||
|
|
||||||
|
class PointsEditor {
|
||||||
|
constructor(context, reset = false) {
|
||||||
|
this.node = context;
|
||||||
|
this.reset = reset;
|
||||||
|
const self = this; // Keep a reference to the main class context
|
||||||
|
|
||||||
|
console.log("creatingPointEditor")
|
||||||
|
|
||||||
|
this.node.pasteFile = (file) => {
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
this.handleImageFile(file);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.node.onDragOver = function (e) {
|
||||||
|
if (e.dataTransfer && e.dataTransfer.items) {
|
||||||
|
return [...e.dataTransfer.items].some(f => f.kind === "file" && f.type.startsWith("image/"));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// On drop upload files
|
||||||
|
this.node.onDragDrop = (e) => {
|
||||||
|
console.log("onDragDrop called");
|
||||||
|
let handled = false;
|
||||||
|
for (const file of e.dataTransfer.files) {
|
||||||
|
if (file.type.startsWith("image/")) {
|
||||||
|
this.handleImageFile(file);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// context menu
|
||||||
|
this.createContextMenu();
|
||||||
|
|
||||||
|
if (reset && context.pointsEditor.element) {
|
||||||
|
context.pointsEditor.element.innerHTML = ''; // Clear the container
|
||||||
|
}
|
||||||
|
this.pos_coordWidget = context.widgets.find(w => w.name === "coordinates");
|
||||||
|
this.neg_coordWidget = context.widgets.find(w => w.name === "neg_coordinates");
|
||||||
|
this.pointsStoreWidget = context.widgets.find(w => w.name === "points_store");
|
||||||
|
this.widthWidget = context.widgets.find(w => w.name === "width");
|
||||||
|
this.heightWidget = context.widgets.find(w => w.name === "height");
|
||||||
|
this.bboxStoreWidget = context.widgets.find(w => w.name === "bbox_store");
|
||||||
|
this.bboxWidget = context.widgets.find(w => w.name === "bboxes");
|
||||||
|
|
||||||
|
//widget callbacks
|
||||||
|
this.widthWidget.callback = () => {
|
||||||
|
this.width = this.widthWidget.value;
|
||||||
|
if (this.width > 256) {
|
||||||
|
context.setSize([this.width + 45, context.size[1]]);
|
||||||
|
}
|
||||||
|
this.vis.width(this.width);
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
this.heightWidget.callback = () => {
|
||||||
|
this.height = this.heightWidget.value
|
||||||
|
this.vis.height(this.height)
|
||||||
|
context.setSize([context.size[0], this.height + 300]);
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
this.pointsStoreWidget.callback = () => {
|
||||||
|
this.points = JSON.parse(pointsStoreWidget.value).positive;
|
||||||
|
this.neg_points = JSON.parse(pointsStoreWidget.value).negative;
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
this.bboxStoreWidget.callback = () => {
|
||||||
|
this.bbox = JSON.parse(bboxStoreWidget.value)
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.width = this.widthWidget.value;
|
||||||
|
this.height = this.heightWidget.value;
|
||||||
|
var i = 3;
|
||||||
|
this.points = [];
|
||||||
|
this.neg_points = [];
|
||||||
|
this.bbox = [{}];
|
||||||
|
var drawing = false;
|
||||||
|
|
||||||
|
// Initialize or reset points array
|
||||||
|
if (!reset && this.pointsStoreWidget.value != "") {
|
||||||
|
this.points = JSON.parse(this.pointsStoreWidget.value).positive;
|
||||||
|
this.neg_points = JSON.parse(this.pointsStoreWidget.value).negative;
|
||||||
|
this.bbox = JSON.parse(this.bboxStoreWidget.value);
|
||||||
|
console.log(this.bbox)
|
||||||
|
} else {
|
||||||
|
this.points = [
|
||||||
|
{
|
||||||
|
x: this.width / 2, // Middle point horizontally centered
|
||||||
|
y: this.height / 2 // Middle point vertically centered
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.neg_points = [
|
||||||
|
{
|
||||||
|
x: 0, // Middle point horizontally centered
|
||||||
|
y: 0 // Middle point vertically centered
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const combinedPoints = {
|
||||||
|
positive: this.points,
|
||||||
|
negative: this.neg_points,
|
||||||
|
};
|
||||||
|
this.pointsStoreWidget.value = JSON.stringify(combinedPoints);
|
||||||
|
this.bboxStoreWidget.value = JSON.stringify(this.bbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
//create main canvas panel
|
||||||
|
this.vis = new pv.Panel()
|
||||||
|
.width(this.width)
|
||||||
|
.height(this.height)
|
||||||
|
.fillStyle("#222")
|
||||||
|
.strokeStyle("gray")
|
||||||
|
.lineWidth(2)
|
||||||
|
.antialias(false)
|
||||||
|
.margin(10)
|
||||||
|
.event("mousedown", function () {
|
||||||
|
if (pv.event.shiftKey && pv.event.button === 2) { // Use pv.event to access the event object
|
||||||
|
let scaledMouse = {
|
||||||
|
x: this.mouse().x / app.canvas.ds.scale,
|
||||||
|
y: this.mouse().y / app.canvas.ds.scale
|
||||||
|
};
|
||||||
|
i = self.neg_points.push(scaledMouse) - 1;
|
||||||
|
self.updateData();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
else if (pv.event.shiftKey) {
|
||||||
|
let scaledMouse = {
|
||||||
|
x: this.mouse().x / app.canvas.ds.scale,
|
||||||
|
y: this.mouse().y / app.canvas.ds.scale
|
||||||
|
};
|
||||||
|
i = self.points.push(scaledMouse) - 1;
|
||||||
|
self.updateData();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
else if (pv.event.ctrlKey) {
|
||||||
|
console.log("start drawing at " + this.mouse().x / app.canvas.ds.scale + ", " + this.mouse().y / app.canvas.ds.scale);
|
||||||
|
drawing = true;
|
||||||
|
self.bbox[0].startX = this.mouse().x / app.canvas.ds.scale;
|
||||||
|
self.bbox[0].startY = this.mouse().y / app.canvas.ds.scale;
|
||||||
|
}
|
||||||
|
else if (pv.event.button === 2) {
|
||||||
|
self.node.contextMenu.style.display = 'block';
|
||||||
|
self.node.contextMenu.style.left = `${pv.event.clientX}px`;
|
||||||
|
self.node.contextMenu.style.top = `${pv.event.clientY}px`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.event("mousemove", function () {
|
||||||
|
if (drawing) {
|
||||||
|
self.bbox[0].endX = this.mouse().x / app.canvas.ds.scale;
|
||||||
|
self.bbox[0].endY = this.mouse().y / app.canvas.ds.scale;
|
||||||
|
self.vis.render();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.event("mouseup", function () {
|
||||||
|
console.log("end drawing at " + this.mouse().x / app.canvas.ds.scale + ", " + this.mouse().y / app.canvas.ds.scale);
|
||||||
|
drawing = false;
|
||||||
|
self.updateData();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.backgroundImage = this.vis.add(pv.Image).visible(false)
|
||||||
|
|
||||||
|
//create bounding box
|
||||||
|
this.bounding_box = this.vis.add(pv.Area)
|
||||||
|
.data(function () {
|
||||||
|
if (drawing || (self.bbox && self.bbox[0] && Object.keys(self.bbox[0]).length > 0)) {
|
||||||
|
return [self.bbox[0].startX, self.bbox[0].endX];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.bottom(function () {return self.height - Math.max(self.bbox[0].startY, self.bbox[0].endY); })
|
||||||
|
.left(function (d) {return d; })
|
||||||
|
.height(function () {return Math.abs(self.bbox[0].startY - self.bbox[0].endY);})
|
||||||
|
.fillStyle("rgba(70, 130, 180, 0.5)")
|
||||||
|
.strokeStyle("steelblue")
|
||||||
|
.visible(function () {return drawing || Object.keys(self.bbox[0]).length > 0; })
|
||||||
|
.add(pv.Dot)
|
||||||
|
.visible(function () {return drawing || Object.keys(self.bbox[0]).length > 0; })
|
||||||
|
.data(() => {
|
||||||
|
if (self.bbox && Object.keys(self.bbox[0]).length > 0) {
|
||||||
|
return [{
|
||||||
|
x: self.bbox[0].endX,
|
||||||
|
y: self.bbox[0].endY
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.left(d => d.x)
|
||||||
|
.top(d => d.y)
|
||||||
|
.radius(Math.log(Math.min(self.width, self.height)) * 1)
|
||||||
|
.shape("square")
|
||||||
|
.cursor("move")
|
||||||
|
.strokeStyle("steelblue")
|
||||||
|
.lineWidth(2)
|
||||||
|
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
|
||||||
|
.event("mousedown", pv.Behavior.drag())
|
||||||
|
.event("drag", function () {
|
||||||
|
let adjustedX = this.mouse().x / app.canvas.ds.scale; // Adjust the new position by the inverse of the scale factor
|
||||||
|
let adjustedY = this.mouse().y / app.canvas.ds.scale;
|
||||||
|
|
||||||
|
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
|
||||||
|
adjustedX = Math.max(0, Math.min(self.vis.width(), adjustedX));
|
||||||
|
adjustedY = Math.max(0, Math.min(self.vis.height(), adjustedY));
|
||||||
|
self.bbox[0].endX = this.mouse().x / app.canvas.ds.scale;
|
||||||
|
self.bbox[0].endY = this.mouse().y / app.canvas.ds.scale;
|
||||||
|
self.vis.render();
|
||||||
|
})
|
||||||
|
.event("dragend", function () {
|
||||||
|
self.updateData();
|
||||||
|
});
|
||||||
|
|
||||||
|
//create positive points
|
||||||
|
this.vis.add(pv.Dot)
|
||||||
|
.data(() => this.points)
|
||||||
|
.left(d => d.x)
|
||||||
|
.top(d => d.y)
|
||||||
|
.radius(Math.log(Math.min(self.width, self.height)) * 4)
|
||||||
|
.shape("circle")
|
||||||
|
.cursor("move")
|
||||||
|
.strokeStyle(function () { return i == this.index ? "#07f907" : "#139613"; })
|
||||||
|
.lineWidth(4)
|
||||||
|
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
|
||||||
|
.event("mousedown", pv.Behavior.drag())
|
||||||
|
.event("dragstart", function () {
|
||||||
|
i = this.index;
|
||||||
|
})
|
||||||
|
.event("dragend", function () {
|
||||||
|
if (pv.event.button === 2 && i !== 0 && i !== self.points.length - 1) {
|
||||||
|
this.index = i;
|
||||||
|
self.points.splice(i--, 1);
|
||||||
|
}
|
||||||
|
self.updateData();
|
||||||
|
|
||||||
|
})
|
||||||
|
.event("drag", function () {
|
||||||
|
let adjustedX = this.mouse().x / app.canvas.ds.scale; // Adjust the new X position by the inverse of the scale factor
|
||||||
|
let adjustedY = this.mouse().y / app.canvas.ds.scale; // Adjust the new Y position by the inverse of the scale factor
|
||||||
|
// Determine the bounds of the vis.Panel
|
||||||
|
const panelWidth = self.vis.width();
|
||||||
|
const panelHeight = self.vis.height();
|
||||||
|
|
||||||
|
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
|
||||||
|
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
|
||||||
|
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
|
||||||
|
self.points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
|
||||||
|
self.vis.render(); // Re-render the visualization to reflect the new position
|
||||||
|
})
|
||||||
|
|
||||||
|
.anchor("center")
|
||||||
|
.add(pv.Label)
|
||||||
|
.left(d => d.x < this.width / 2 ? d.x + 30 : d.x - 35) // Shift label to right if on left half, otherwise shift to left
|
||||||
|
.top(d => d.y < this.height / 2 ? d.y + 25 : d.y - 25) // Shift label down if on top half, otherwise shift up
|
||||||
|
.font(25 + "px sans-serif")
|
||||||
|
.text(d => {return this.points.indexOf(d); })
|
||||||
|
.textStyle("#139613")
|
||||||
|
.textShadow("2px 2px 2px black")
|
||||||
|
.add(pv.Dot) // Add smaller point in the center
|
||||||
|
.data(() => this.points)
|
||||||
|
.left(d => d.x)
|
||||||
|
.top(d => d.y)
|
||||||
|
.radius(2) // Smaller radius for the center point
|
||||||
|
.shape("circle")
|
||||||
|
.fillStyle("red") // Color for the center point
|
||||||
|
.lineWidth(1); // Stroke thickness for the center point
|
||||||
|
|
||||||
|
//create negative points
|
||||||
|
this.vis.add(pv.Dot)
|
||||||
|
.data(() => this.neg_points)
|
||||||
|
.left(d => d.x)
|
||||||
|
.top(d => d.y)
|
||||||
|
.radius(Math.log(Math.min(self.width, self.height)) * 4)
|
||||||
|
.shape("circle")
|
||||||
|
.cursor("move")
|
||||||
|
.strokeStyle(function () { return i == this.index ? "#f91111" : "#891616"; })
|
||||||
|
.lineWidth(4)
|
||||||
|
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
|
||||||
|
.event("mousedown", pv.Behavior.drag())
|
||||||
|
.event("dragstart", function () {
|
||||||
|
i = this.index;
|
||||||
|
})
|
||||||
|
.event("dragend", function () {
|
||||||
|
if (pv.event.button === 2 && i !== 0 && i !== self.neg_points.length - 1) {
|
||||||
|
this.index = i;
|
||||||
|
self.neg_points.splice(i--, 1);
|
||||||
|
}
|
||||||
|
self.updateData();
|
||||||
|
|
||||||
|
})
|
||||||
|
.event("drag", function () {
|
||||||
|
let adjustedX = this.mouse().x / app.canvas.ds.scale; // Adjust the new X position by the inverse of the scale factor
|
||||||
|
let adjustedY = this.mouse().y / app.canvas.ds.scale; // Adjust the new Y position by the inverse of the scale factor
|
||||||
|
// Determine the bounds of the vis.Panel
|
||||||
|
const panelWidth = self.vis.width();
|
||||||
|
const panelHeight = self.vis.height();
|
||||||
|
|
||||||
|
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
|
||||||
|
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
|
||||||
|
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
|
||||||
|
self.neg_points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
|
||||||
|
self.vis.render(); // Re-render the visualization to reflect the new position
|
||||||
|
})
|
||||||
|
.anchor("center")
|
||||||
|
.add(pv.Label)
|
||||||
|
.left(d => d.x < this.width / 2 ? d.x + 30 : d.x - 35) // Shift label to right if on left half, otherwise shift to left
|
||||||
|
.top(d => d.y < this.height / 2 ? d.y + 25 : d.y - 25) // Shift label down if on top half, otherwise shift up
|
||||||
|
.font(25 + "px sans-serif")
|
||||||
|
.text(d => {return this.neg_points.indexOf(d); })
|
||||||
|
.textStyle("red")
|
||||||
|
.textShadow("2px 2px 2px black")
|
||||||
|
.add(pv.Dot) // Add smaller point in the center
|
||||||
|
.data(() => this.neg_points)
|
||||||
|
.left(d => d.x)
|
||||||
|
.top(d => d.y)
|
||||||
|
.radius(2) // Smaller radius for the center point
|
||||||
|
.shape("circle")
|
||||||
|
.fillStyle("red") // Color for the center point
|
||||||
|
.lineWidth(1); // Stroke thickness for the center point
|
||||||
|
|
||||||
|
if (this.points.length != 0) {
|
||||||
|
this.vis.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
var svgElement = this.vis.canvas();
|
||||||
|
svgElement.style['zIndex'] = "2"
|
||||||
|
svgElement.style['position'] = "relative"
|
||||||
|
this.node.pointsEditor.element.appendChild(svgElement);
|
||||||
|
|
||||||
|
if (this.width > 256) {
|
||||||
|
this.node.setSize([this.width + 45, this.node.size[1]]);
|
||||||
|
}
|
||||||
|
this.node.setSize([this.node.size[0], this.height + 300]);
|
||||||
|
this.updateData();
|
||||||
|
this.refreshBackgroundImage();
|
||||||
|
|
||||||
|
}//end constructor
|
||||||
|
|
||||||
|
updateData = () => {
|
||||||
|
if (!this.points || this.points.length === 0) {
|
||||||
|
console.log("no points");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const combinedPoints = {
|
||||||
|
positive: this.points,
|
||||||
|
negative: this.neg_points,
|
||||||
|
};
|
||||||
|
this.pointsStoreWidget.value = JSON.stringify(combinedPoints);
|
||||||
|
this.pos_coordWidget.value = JSON.stringify(this.points);
|
||||||
|
this.neg_coordWidget.value = JSON.stringify(this.neg_points);
|
||||||
|
|
||||||
|
if (this.bbox.length != 0) {
|
||||||
|
let bboxString = JSON.stringify(this.bbox);
|
||||||
|
this.bboxStoreWidget.value = bboxString;
|
||||||
|
this.bboxWidget.value = bboxString;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vis.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleImageLoad = (img, file, base64String) => {
|
||||||
|
console.log(img.width, img.height); // Access width and height here
|
||||||
|
this.widthWidget.value = img.width;
|
||||||
|
this.heightWidget.value = img.height;
|
||||||
|
|
||||||
|
if (img.width != this.vis.width() || img.height != this.vis.height()) {
|
||||||
|
if (img.width > 256) {
|
||||||
|
this.node.setSize([img.width + 45, this.node.size[1]]);
|
||||||
|
}
|
||||||
|
this.node.setSize([this.node.size[0], img.height + 300]);
|
||||||
|
this.vis.width(img.width);
|
||||||
|
this.vis.height(img.height);
|
||||||
|
this.height = img.height;
|
||||||
|
this.width = img.width;
|
||||||
|
this.updateData();
|
||||||
|
}
|
||||||
|
this.backgroundImage.url(file ? URL.createObjectURL(file) : `data:${this.node.properties.imgData.type};base64,${base64String}`).visible(true).root.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
processImage = (img, file) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const maxWidth = 800; // maximum width
|
||||||
|
const maxHeight = 600; // maximum height
|
||||||
|
let width = img.width;
|
||||||
|
let height = img.height;
|
||||||
|
|
||||||
|
// Calculate the new dimensions while preserving the aspect ratio
|
||||||
|
if (width > height) {
|
||||||
|
if (width > maxWidth) {
|
||||||
|
height *= maxWidth / width;
|
||||||
|
width = maxWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (height > maxHeight) {
|
||||||
|
width *= maxHeight / height;
|
||||||
|
height = maxHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
// Get the compressed image data as a Base64 string
|
||||||
|
const base64String = canvas.toDataURL('image/jpeg', 0.5).replace('data:', '').replace(/^.+,/, ''); // 0.5 is the quality from 0 to 1
|
||||||
|
|
||||||
|
this.node.properties.imgData = {
|
||||||
|
name: file.name,
|
||||||
|
lastModified: file.lastModified,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
base64: base64String
|
||||||
|
};
|
||||||
|
handleImageLoad(img, file, base64String);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleImageFile = (file) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = reader.result;
|
||||||
|
img.onload = () => processImage(img, file);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
const imageUrl = URL.createObjectURL(file);
|
||||||
|
const img = new Image();
|
||||||
|
img.src = imageUrl;
|
||||||
|
img.onload = () => this.handleImageLoad(img, file, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshBackgroundImage = () => {
|
||||||
|
if (this.node.properties.imgData && this.node.properties.imgData.base64) {
|
||||||
|
const base64String = this.node.properties.imgData.base64;
|
||||||
|
const imageUrl = `data:${this.node.properties.imgData.type};base64,${base64String}`;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = imageUrl;
|
||||||
|
img.onload = () => this.handleImageLoad(img, null, base64String);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createContextMenu = () => {
|
||||||
|
self = this;
|
||||||
|
document.addEventListener('contextmenu', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (!self.node.contextMenu.contains(e.target)) {
|
||||||
|
self.node.contextMenu.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.node.menuItems.forEach((menuItem, index) => {
|
||||||
|
self = this;
|
||||||
|
menuItem.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
// Create file input element
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = 'image/*'; // Accept only image files
|
||||||
|
|
||||||
|
// Listen for file selection
|
||||||
|
fileInput.addEventListener('change', function (event) {
|
||||||
|
const file = event.target.files[0]; // Get the selected file
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const imageUrl = URL.createObjectURL(file);
|
||||||
|
let img = new Image();
|
||||||
|
img.src = imageUrl;
|
||||||
|
img.onload = () => self.handleImageLoad(img, file, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.click();
|
||||||
|
|
||||||
|
self.node.contextMenu.style.display = 'none';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
self.backgroundImage.visible(false).root.render();
|
||||||
|
self.node.properties.imgData = null;
|
||||||
|
self.node.contextMenu.style.display = 'none';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}//end createContextMenu
|
||||||
|
}//end class
|
||||||
|
|
||||||
|
|
||||||
|
//from melmass
|
||||||
|
export function hideWidgetForGood(node, widget, suffix = '') {
|
||||||
|
widget.origType = widget.type
|
||||||
|
widget.origComputeSize = widget.computeSize
|
||||||
|
widget.origSerializeValue = widget.serializeValue
|
||||||
|
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
|
||||||
|
widget.type = "converted-widget" + suffix
|
||||||
|
// widget.serializeValue = () => {
|
||||||
|
// // Prevent serializing the widget if we have no input linked
|
||||||
|
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
|
||||||
|
// if (w?.link == null) {
|
||||||
|
// return undefined;
|
||||||
|
// }
|
||||||
|
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Hide any linked widgets, e.g. seed+seedControl
|
||||||
|
if (widget.linkedWidgets) {
|
||||||
|
for (const w of widget.linkedWidgets) {
|
||||||
|
hideWidgetForGood(node, w, ':' + widget.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-20
@@ -12,8 +12,13 @@ function setColorAndBgColor(type) {
|
|||||||
"IMAGE": LGraphCanvas.node_colors.pale_blue,
|
"IMAGE": LGraphCanvas.node_colors.pale_blue,
|
||||||
"CLIP": LGraphCanvas.node_colors.yellow,
|
"CLIP": LGraphCanvas.node_colors.yellow,
|
||||||
"FLOAT": LGraphCanvas.node_colors.green,
|
"FLOAT": LGraphCanvas.node_colors.green,
|
||||||
"MASK": LGraphCanvas.node_colors.cyan,
|
"MASK": { color: "#1c5715", bgcolor: "#1f401b"},
|
||||||
"INT": { color: "#1b4669", bgcolor: "#29699c"},
|
"INT": { color: "#1b4669", bgcolor: "#29699c"},
|
||||||
|
"CONTROL_NET": { color: "#156653", bgcolor: "#1c453b"},
|
||||||
|
"NOISE": { color: "#2e2e2e", bgcolor: "#242121"},
|
||||||
|
"GUIDER": { color: "#3c7878", bgcolor: "#1c453b"},
|
||||||
|
"SAMPLER": { color: "#614a4a", bgcolor: "#3b2c2c"},
|
||||||
|
"SIGMAS": { color: "#485248", bgcolor: "#272e27"},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,20 +28,21 @@ function setColorAndBgColor(type) {
|
|||||||
this.bgcolor = colors.bgcolor;
|
this.bgcolor = colors.bgcolor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let isAlertShown = false;
|
|
||||||
let disablePrefix = app.ui.settings.getSettingValue("KJNodes.disablePrefix")
|
let disablePrefix = app.ui.settings.getSettingValue("KJNodes.disablePrefix")
|
||||||
|
const LGraphNode = LiteGraph.LGraphNode
|
||||||
|
|
||||||
function showAlertWithThrottle(message, delay) {
|
function showAlert(message) {
|
||||||
if (!isAlertShown) {
|
app.extensionManager.toast.add({
|
||||||
isAlertShown = true;
|
severity: 'warn',
|
||||||
alert(message);
|
summary: "KJ Get/Set",
|
||||||
setTimeout(() => isAlertShown = false, delay);
|
detail: `${message}. Most likely you're missing custom nodes`,
|
||||||
}
|
life: 5000,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "SetNode",
|
name: "SetNode",
|
||||||
registerCustomNodes() {
|
registerCustomNodes() {
|
||||||
class SetNode {
|
class SetNode extends LGraphNode {
|
||||||
defaultVisibility = true;
|
defaultVisibility = true;
|
||||||
serialize_widgets = true;
|
serialize_widgets = true;
|
||||||
drawConnection = false;
|
drawConnection = false;
|
||||||
@@ -45,7 +51,8 @@ app.registerExtension({
|
|||||||
canvas = app.canvas;
|
canvas = app.canvas;
|
||||||
menuEntry = "Show connections";
|
menuEntry = "Show connections";
|
||||||
|
|
||||||
constructor() {
|
constructor(title) {
|
||||||
|
super(title)
|
||||||
if (!this.properties) {
|
if (!this.properties) {
|
||||||
this.properties = {
|
this.properties = {
|
||||||
"previousName": ""
|
"previousName": ""
|
||||||
@@ -89,9 +96,10 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (slotType == 2 && !isChangeConnect) {
|
if (slotType == 2 && !isChangeConnect) {
|
||||||
|
if (this.outputs && this.outputs[slot]) {
|
||||||
this.outputs[slot].type = '*';
|
this.outputs[slot].type = '*';
|
||||||
this.outputs[slot].name = '*';
|
this.outputs[slot].name = '*';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//On Connect
|
//On Connect
|
||||||
if (link_info && node.graph && slotType == 1 && isChangeConnect) {
|
if (link_info && node.graph && slotType == 1 && isChangeConnect) {
|
||||||
@@ -115,7 +123,7 @@ app.registerExtension({
|
|||||||
setColorAndBgColor.call(this, type);
|
setColorAndBgColor.call(this, type);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert("Error: Set node input undefined. Most likely you're missing custom nodes");
|
showAlert("node input undefined.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (link_info && node.graph && slotType == 2 && isChangeConnect) {
|
if (link_info && node.graph && slotType == 2 && isChangeConnect) {
|
||||||
@@ -127,7 +135,7 @@ app.registerExtension({
|
|||||||
this.outputs[0].type = type;
|
this.outputs[0].type = type;
|
||||||
this.outputs[0].name = type;
|
this.outputs[0].name = type;
|
||||||
} else {
|
} else {
|
||||||
alert("Error: Get Set node output undefined. Most likely you're missing custom nodes");
|
showAlert('node output undefined');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,6 +328,8 @@ app.registerExtension({
|
|||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
// Provide a default link object with necessary properties, to avoid errors as link can't be null anymore
|
||||||
|
const defaultLink = { type: 'default', color: this.slotColor };
|
||||||
|
|
||||||
for (const getter of this.currentGetters) {
|
for (const getter of this.currentGetters) {
|
||||||
if (!this.flags.collapsed) {
|
if (!this.flags.collapsed) {
|
||||||
@@ -340,7 +350,7 @@ app.registerExtension({
|
|||||||
ctx,
|
ctx,
|
||||||
start_node_slotpos,
|
start_node_slotpos,
|
||||||
end_node_slotpos,
|
end_node_slotpos,
|
||||||
null,
|
defaultLink,
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
this.slotColor,
|
this.slotColor,
|
||||||
@@ -365,7 +375,7 @@ app.registerExtension({
|
|||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "GetNode",
|
name: "GetNode",
|
||||||
registerCustomNodes() {
|
registerCustomNodes() {
|
||||||
class GetNode {
|
class GetNode extends LGraphNode {
|
||||||
|
|
||||||
defaultVisibility = true;
|
defaultVisibility = true;
|
||||||
serialize_widgets = true;
|
serialize_widgets = true;
|
||||||
@@ -374,7 +384,8 @@ app.registerExtension({
|
|||||||
currentSetter = null;
|
currentSetter = null;
|
||||||
canvas = app.canvas;
|
canvas = app.canvas;
|
||||||
|
|
||||||
constructor() {
|
constructor(title) {
|
||||||
|
super(title)
|
||||||
if (!this.properties) {
|
if (!this.properties) {
|
||||||
this.properties = {};
|
this.properties = {};
|
||||||
}
|
}
|
||||||
@@ -439,7 +450,7 @@ app.registerExtension({
|
|||||||
if (this.outputs[0].type !== '*' && this.outputs[0].links) {
|
if (this.outputs[0].type !== '*' && this.outputs[0].links) {
|
||||||
this.outputs[0].links.filter(linkId => {
|
this.outputs[0].links.filter(linkId => {
|
||||||
const link = node.graph.links[linkId];
|
const link = node.graph.links[linkId];
|
||||||
return link && (link.type !== this.outputs[0].type && link.type !== '*');
|
return link && (!link.type.split(",").includes(this.outputs[0].type) && link.type !== '*');
|
||||||
}).forEach(linkId => {
|
}).forEach(linkId => {
|
||||||
node.graph.removeLink(linkId);
|
node.graph.removeLink(linkId);
|
||||||
});
|
});
|
||||||
@@ -470,6 +481,9 @@ app.registerExtension({
|
|||||||
|
|
||||||
getInputLink(slot) {
|
getInputLink(slot) {
|
||||||
const setter = this.findSetter(this.graph);
|
const setter = this.findSetter(this.graph);
|
||||||
|
if (this.mode !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (setter) {
|
if (setter) {
|
||||||
const slotInfo = setter.inputs[slot];
|
const slotInfo = setter.inputs[slot];
|
||||||
@@ -477,8 +491,8 @@ app.registerExtension({
|
|||||||
return link;
|
return link;
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = "No SetNode found for " + this.widgets[0].value + "(" + this.type + ")";
|
const errorMessage = "No SetNode found for " + this.widgets[0].value + "(" + this.type + ")";
|
||||||
showAlertWithThrottle(errorMessage, 5000);
|
showAlert(errorMessage);
|
||||||
throw new Error(errorMessage);
|
//throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAdded(graph) {
|
onAdded(graph) {
|
||||||
@@ -509,6 +523,11 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDrawForeground(ctx, lGraphCanvas) {
|
onDrawForeground(ctx, lGraphCanvas) {
|
||||||
|
if (this.mode === 4) {
|
||||||
|
console.log(`Mode is ${this.mode}, setting to disabled`)
|
||||||
|
this.mode = 2;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (this.drawConnection) {
|
if (this.drawConnection) {
|
||||||
this._drawVirtualLink(lGraphCanvas, ctx);
|
this._drawVirtualLink(lGraphCanvas, ctx);
|
||||||
}
|
}
|
||||||
@@ -521,6 +540,9 @@ app.registerExtension({
|
|||||||
_drawVirtualLink(lGraphCanvas, ctx) {
|
_drawVirtualLink(lGraphCanvas, ctx) {
|
||||||
if (!this.currentSetter) return;
|
if (!this.currentSetter) return;
|
||||||
|
|
||||||
|
// Provide a default link object with necessary properties, to avoid errors as link can't be null anymore
|
||||||
|
const defaultLink = { type: 'default', color: this.slotColor };
|
||||||
|
|
||||||
let start_node_slotpos = this.currentSetter.getConnectionPos(false, 0);
|
let start_node_slotpos = this.currentSetter.getConnectionPos(false, 0);
|
||||||
start_node_slotpos = [
|
start_node_slotpos = [
|
||||||
start_node_slotpos[0] - this.pos[0],
|
start_node_slotpos[0] - this.pos[0],
|
||||||
@@ -531,7 +553,7 @@ app.registerExtension({
|
|||||||
ctx,
|
ctx,
|
||||||
start_node_slotpos,
|
start_node_slotpos,
|
||||||
end_node_slotpos,
|
end_node_slotpos,
|
||||||
null,
|
defaultLink,
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
this.slotColor
|
this.slotColor
|
||||||
|
|||||||
+1002
-330
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user