Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f45656976 | ||
|
|
310f576108 | ||
|
|
849bba41ad | ||
|
|
cb7726221e | ||
|
|
e91f331652 | ||
|
|
2de40a517e | ||
|
|
fc2b20ad38 | ||
|
|
ab262513df |
@@ -1,12 +1,12 @@
|
||||
# comfy-qr
|
||||
# ComfyQR
|
||||
|
||||
A set of ComfyUI nodes to dynamically create QR image layers for generative QR art without the hassle of leaving the webui.
|
||||
|
||||
Everything in this pack is primarily focused on the creation of patterns. While the `Mask QR Errors` does some basic statistics on the differences bwtween pixels, a more definative node on determining scannability can be found in the companion project [Comfy QR Validation Nodes](https://gitlab.com/sofuego-comfy-nodes/comfy-qr-validation-nodes) which uses additional dependencies for the reading of QRs.
|
||||
Everything in this pack is primarily focused on the creation of patterns. While the `Mask QR Errors` does some basic statistics on the differences between pixels, a more definative node on determining scannability can be found in the companion project [ComfyQR-scanning-nodes](https://gitlab.com/sofuego-comfy-nodes/ComfyQR-scanning-nodes) which uses additional dependencies for the reading of QRs.
|
||||
|
||||
This repository is managed publicly on [Gitlab](https://gitlab.com/sofuego-comfy-nodes/comfy-qr), but also mirrored on [Github](https://github.com/coreyryanhanson/comfy-qr). Please submit any [issues](https://gitlab.com/sofuego-comfy-nodes/comfy-qr/-/issues) or [pull requests](https://gitlab.com/sofuego-comfy-nodes/comfy-qr/-/merge_requests) to the gitlab repo.
|
||||
This repository is managed publicly on [Gitlab](https://gitlab.com/sofuego-comfy-nodes/ComfyQR), but also mirrored on [Github](https://github.com/coreyryanhanson/ComfyQR). Please submit any [issues](https://gitlab.com/sofuego-comfy-nodes/ComfyQR/-/issues) or [pull requests](https://gitlab.com/sofuego-comfy-nodes/ComfyQR/-/merge_requests) to the gitlab repo.
|
||||
|
||||

|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -99,6 +99,7 @@ A node that that will analyze the differences between a modified QR and create a
|
||||
* `module_size` - The size in pixels of each QR module (the individual blocks that make up a qr code) in the provided images.
|
||||
* `grayscale_method` - How to convert the RGB color channels to a single grayscale value.
|
||||
* `mean` - A simple average of the RGB values.
|
||||
* `luminance` - Converts colors to grayscale using a [more advanced formula](https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale) to better match percieved brightness.
|
||||
* `aggregate_method` - How to aggregate the many pixels that make up a QR module into a single value.
|
||||
* `mean` - Within each module a simple average is taken of all constituent pixels.
|
||||
* `evaluate` - Which part of the QR to examine.
|
||||
@@ -107,6 +108,7 @@ A node that that will analyze the differences between a modified QR and create a
|
||||
* `finder_pattern` - Ignores the modules while examining only the 3 finder patterns at the corners of the QR.
|
||||
* `error_difficulty` - It is possible that while using the same reader a QR code could be easily scannable on one monitor, but not on another. The way the colors are displayed can have a major effect on unsability. By increasing this threshold, you are simulating inability to distinguish midrange pixels for both the `PERCENT_ERROR` and `QR_ERROR_MASK` outputs. It ranges from 0 to 1 where 0 only automatically fails perfect grays while 1 accepts only exactly matched modules that are perfectly black or white.
|
||||
* `inverted_pattern` - If set to `True` this assumes that the QR code has inverted colors (a black background with white modules.)
|
||||
* `gamma` - If using the `grayscale` method based on `luminance`, this will influence the formula by appending the gamma expansion and compression formulas before and after the color conversion. A gamma of 2.2 will use the [formula specific to srgb](qr_nodes.py#0.04045), while other gamma values will simply use the gamma amount as an exponent.
|
||||
|
||||
#### Outputs
|
||||
|
||||
@@ -125,7 +127,7 @@ And then aggressively increasing the strength of the ControlNet on only the erro
|
||||
|
||||

|
||||
|
||||
Any workflow in the example that ends with "validated" (and a few image examples) assume the installation of the [validation pack](https://gitlab.com/sofuego-comfy-nodes/comfy-qr-validation-nodes) as well.
|
||||
Any workflow in the example that ends with "validated" (and a few image examples) assume the installation of the [scanning pack](https://gitlab.com/sofuego-comfy-nodes/ComfyQR-scanning-nodes) as well.
|
||||
|
||||
## Future updates
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 924 KiB After Width: | Height: | Size: 924 KiB |
@@ -1,151 +1,7 @@
|
||||
{
|
||||
"last_node_id": 50,
|
||||
"last_link_id": 110,
|
||||
"last_node_id": 51,
|
||||
"last_link_id": 113,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "ControlNetLoader",
|
||||
"pos": [
|
||||
-692.0780999999998,
|
||||
618.3673000000019
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONTROL_NET",
|
||||
"type": "CONTROL_NET",
|
||||
"links": [
|
||||
13
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ControlNetLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"control_v1p_sd15_qrcode_monster_v2.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "ImageCompositeMasked",
|
||||
"pos": [
|
||||
-332.0780999999991,
|
||||
-91.63270000000001
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
146
|
||||
],
|
||||
"flags": {},
|
||||
"order": 18,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "destination",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "x",
|
||||
"type": "INT",
|
||||
"link": 107,
|
||||
"widget": {
|
||||
"name": "x",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 8192,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"type": "INT",
|
||||
"link": 108,
|
||||
"widget": {
|
||||
"name": "y",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 8192,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
4,
|
||||
12,
|
||||
25
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageCompositeMasked"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
16,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
877.9219000000076,
|
||||
18.367300000000753
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 246
|
||||
},
|
||||
"flags": {},
|
||||
"order": 25,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "PreviewImage",
|
||||
@@ -158,7 +14,7 @@
|
||||
"1": 246
|
||||
},
|
||||
"flags": {},
|
||||
"order": 32,
|
||||
"order": 33,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
@@ -183,13 +39,13 @@
|
||||
"1": 26
|
||||
},
|
||||
"flags": {},
|
||||
"order": 30,
|
||||
"order": 31,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 26
|
||||
"link": 113
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
@@ -299,7 +155,7 @@
|
||||
"1": 98
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -351,7 +207,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -397,7 +253,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -442,7 +298,7 @@
|
||||
"1": 76
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -562,7 +418,7 @@
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
944021096056392,
|
||||
507775986242591,
|
||||
"randomize",
|
||||
25,
|
||||
8,
|
||||
@@ -614,10 +470,10 @@
|
||||
-970,
|
||||
294
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 14,
|
||||
"mode": 0,
|
||||
@@ -827,49 +683,6 @@
|
||||
"Node name for S&R": "SetLatentNoiseMask"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1039,
|
||||
464
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 27,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 17
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
24,
|
||||
37,
|
||||
44
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"type": "ImagePadForOutpaint",
|
||||
@@ -882,7 +695,7 @@
|
||||
"1": 174
|
||||
},
|
||||
"flags": {},
|
||||
"order": 29,
|
||||
"order": 28,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
@@ -999,7 +812,7 @@
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 31,
|
||||
"order": 30,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
@@ -1174,73 +987,6 @@
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "comfy-qr-quantify-match",
|
||||
"pos": [
|
||||
1253,
|
||||
129
|
||||
],
|
||||
"size": {
|
||||
"0": 317.4000244140625,
|
||||
"1": 238
|
||||
},
|
||||
"flags": {},
|
||||
"order": 28,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 25
|
||||
},
|
||||
{
|
||||
"name": "modified_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 24
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "PERCENT_ERROR",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "CORRELATION",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "RMSE",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "QR_ERROR_MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
26
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-quantify-match"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
"mean",
|
||||
"mean",
|
||||
"module_pattern",
|
||||
0,
|
||||
"False"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "comfy-qr-quantify-match",
|
||||
@@ -1305,7 +1051,7 @@
|
||||
"mean",
|
||||
"module_pattern",
|
||||
0.5,
|
||||
"False"
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1320,7 +1066,7 @@
|
||||
"1": 314.0000305175781
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1360,7 +1106,7 @@
|
||||
"1": 200
|
||||
},
|
||||
"flags": {},
|
||||
"order": 33,
|
||||
"order": 32,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
@@ -1429,7 +1175,7 @@
|
||||
"widgets_values": [
|
||||
"Https",
|
||||
"gitlab.com/sofuego-comfy-nodes/comfy-qr-validation-nodes",
|
||||
"False"
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1470,7 +1216,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1514,7 +1260,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1558,7 +1304,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1602,7 +1348,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1641,10 +1387,10 @@
|
||||
-994,
|
||||
455
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 15,
|
||||
"mode": 0,
|
||||
@@ -1712,10 +1458,10 @@
|
||||
-635,
|
||||
413
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
126
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 126
|
||||
},
|
||||
"flags": {},
|
||||
"order": 17,
|
||||
"mode": 0,
|
||||
@@ -1825,6 +1571,261 @@
|
||||
16,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
881,
|
||||
17
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 246
|
||||
},
|
||||
"flags": {},
|
||||
"order": 25,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "ImageCompositeMasked",
|
||||
"pos": [
|
||||
-332.0780999999991,
|
||||
-91.63270000000001
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 146
|
||||
},
|
||||
"flags": {},
|
||||
"order": 18,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "destination",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "x",
|
||||
"type": "INT",
|
||||
"link": 107,
|
||||
"widget": {
|
||||
"name": "x",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 8192,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"type": "INT",
|
||||
"link": 108,
|
||||
"widget": {
|
||||
"name": "y",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 8192,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
4,
|
||||
12,
|
||||
111
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageCompositeMasked"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
16,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1039,
|
||||
464
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 27,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": 17
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
37,
|
||||
44,
|
||||
112
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"type": "comfy-qr-mask_errors",
|
||||
"pos": [
|
||||
1194,
|
||||
15
|
||||
],
|
||||
"size": {
|
||||
"0": 317.4000244140625,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 29,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 111
|
||||
},
|
||||
{
|
||||
"name": "modified_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 112
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "QR_ERROR_MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
113
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "PERCENT_ERROR",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "CORRELATION",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "RMSE",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-mask_errors"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
"luminance",
|
||||
"mean",
|
||||
"module_pattern",
|
||||
0,
|
||||
false,
|
||||
2.2
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "ControlNetLoader",
|
||||
"pos": [
|
||||
-692.0780999999998,
|
||||
618.3673000000019
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONTROL_NET",
|
||||
"type": "CONTROL_NET",
|
||||
"links": [
|
||||
13
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ControlNetLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1.5/control_v1p_sd15_qrcode_monster_v2.safetensors"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -1948,30 +1949,6 @@
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
24,
|
||||
17,
|
||||
0,
|
||||
18,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
25,
|
||||
6,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
26,
|
||||
18,
|
||||
3,
|
||||
20,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
27,
|
||||
20,
|
||||
@@ -2251,6 +2228,30 @@
|
||||
38,
|
||||
3,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
111,
|
||||
6,
|
||||
0,
|
||||
51,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
112,
|
||||
17,
|
||||
0,
|
||||
51,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
113,
|
||||
51,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"MASK"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
|
||||
@@ -1,151 +1,7 @@
|
||||
{
|
||||
"last_node_id": 50,
|
||||
"last_link_id": 111,
|
||||
"last_node_id": 51,
|
||||
"last_link_id": 114,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 12,
|
||||
"type": "ControlNetLoader",
|
||||
"pos": [
|
||||
-692.0780999999998,
|
||||
618.3673000000019
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONTROL_NET",
|
||||
"type": "CONTROL_NET",
|
||||
"links": [
|
||||
13
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ControlNetLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"control_v1p_sd15_qrcode_monster_v2.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "ImageCompositeMasked",
|
||||
"pos": [
|
||||
-332.0780999999991,
|
||||
-91.63270000000001
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
146
|
||||
],
|
||||
"flags": {},
|
||||
"order": 17,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "destination",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "x",
|
||||
"type": "INT",
|
||||
"link": 107,
|
||||
"widget": {
|
||||
"name": "x",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 8192,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"type": "INT",
|
||||
"link": 108,
|
||||
"widget": {
|
||||
"name": "y",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 8192,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
4,
|
||||
12,
|
||||
25
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageCompositeMasked"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
16,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
877.9219000000076,
|
||||
18.367300000000753
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 246
|
||||
},
|
||||
"flags": {},
|
||||
"order": 24,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "PreviewImage",
|
||||
@@ -171,6 +27,42 @@
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
1637.9218999999982,
|
||||
148.3672999999998
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 26
|
||||
},
|
||||
"flags": {},
|
||||
"order": 29,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 113
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
27
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "CLIPTextEncode",
|
||||
@@ -263,7 +155,7 @@
|
||||
"1": 98
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -315,7 +207,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -359,7 +251,7 @@
|
||||
"1": 76
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -478,7 +370,7 @@
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
927301192808348,
|
||||
364823902739253,
|
||||
"randomize",
|
||||
25,
|
||||
8,
|
||||
@@ -530,10 +422,10 @@
|
||||
-970,
|
||||
294
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
@@ -891,73 +783,6 @@
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "comfy-qr-quantify-match",
|
||||
"pos": [
|
||||
1253,
|
||||
129
|
||||
],
|
||||
"size": {
|
||||
"0": 317.4000244140625,
|
||||
"1": 238
|
||||
},
|
||||
"flags": {},
|
||||
"order": 27,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 25
|
||||
},
|
||||
{
|
||||
"name": "modified_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 24
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "PERCENT_ERROR",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "CORRELATION",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "RMSE",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "QR_ERROR_MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
26
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-quantify-match"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
"mean",
|
||||
"mean",
|
||||
"module_pattern",
|
||||
0,
|
||||
"False"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "comfy-qr-quantify-match",
|
||||
@@ -1022,7 +847,7 @@
|
||||
"mean",
|
||||
"module_pattern",
|
||||
0.5,
|
||||
"False"
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1037,7 +862,7 @@
|
||||
"1": 314.0000305175781
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1077,7 +902,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1121,7 +946,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1165,7 +990,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1209,7 +1034,7 @@
|
||||
"1": 82
|
||||
},
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
@@ -1248,10 +1073,10 @@
|
||||
-994,
|
||||
455
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
106
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 14,
|
||||
"mode": 0,
|
||||
@@ -1319,10 +1144,10 @@
|
||||
-635,
|
||||
413
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
126
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 126
|
||||
},
|
||||
"flags": {},
|
||||
"order": 16,
|
||||
"mode": 0,
|
||||
@@ -1434,24 +1259,93 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "MaskToImage",
|
||||
"id": 7,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1644,
|
||||
142
|
||||
881,
|
||||
17
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 26
|
||||
"1": 246
|
||||
},
|
||||
"flags": {},
|
||||
"order": 29,
|
||||
"order": 24,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "ImageCompositeMasked",
|
||||
"pos": [
|
||||
-332.0780999999991,
|
||||
-91.63270000000001
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 146
|
||||
},
|
||||
"flags": {},
|
||||
"order": 17,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "destination",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 26
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "x",
|
||||
"type": "INT",
|
||||
"link": 107,
|
||||
"widget": {
|
||||
"name": "x",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 8192,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "y",
|
||||
"type": "INT",
|
||||
"link": 108,
|
||||
"widget": {
|
||||
"name": "y",
|
||||
"config": [
|
||||
"INT",
|
||||
{
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 8192,
|
||||
"step": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
@@ -1459,22 +1353,29 @@
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
27
|
||||
4,
|
||||
12,
|
||||
111
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
"Node name for S&R": "ImageCompositeMasked"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
16,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "VAEDecode",
|
||||
"pos": [
|
||||
1039,
|
||||
475
|
||||
464
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
@@ -1500,8 +1401,8 @@
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
24,
|
||||
111
|
||||
112,
|
||||
114
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
@@ -1511,12 +1412,112 @@
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"type": "comfy-qr-mask_errors",
|
||||
"pos": [
|
||||
1194,
|
||||
15
|
||||
],
|
||||
"size": {
|
||||
"0": 317.4000244140625,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 27,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 111
|
||||
},
|
||||
{
|
||||
"name": "modified_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 112
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "QR_ERROR_MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
113
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "PERCENT_ERROR",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "CORRELATION",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "RMSE",
|
||||
"type": "FLOAT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-mask_errors"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
"luminance",
|
||||
"mean",
|
||||
"module_pattern",
|
||||
0,
|
||||
false,
|
||||
2.2
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "ControlNetLoader",
|
||||
"pos": [
|
||||
-692.0780999999998,
|
||||
618.3673000000019
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONTROL_NET",
|
||||
"type": "CONTROL_NET",
|
||||
"links": [
|
||||
13
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ControlNetLoader"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1.5/control_v1p_sd15_qrcode_monster_v2.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1388,
|
||||
501
|
||||
1473,
|
||||
510
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
@@ -1529,7 +1530,7 @@
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 111
|
||||
"link": 114
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
@@ -1659,30 +1660,6 @@
|
||||
1,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
24,
|
||||
17,
|
||||
0,
|
||||
18,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
25,
|
||||
6,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
26,
|
||||
18,
|
||||
3,
|
||||
20,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
27,
|
||||
20,
|
||||
@@ -1877,6 +1854,30 @@
|
||||
],
|
||||
[
|
||||
111,
|
||||
6,
|
||||
0,
|
||||
51,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
112,
|
||||
17,
|
||||
0,
|
||||
51,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
113,
|
||||
51,
|
||||
0,
|
||||
20,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
114,
|
||||
17,
|
||||
0,
|
||||
23,
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
@@ -1,458 +1,496 @@
|
||||
import numpy as np
|
||||
import qrcode
|
||||
from qrcode.compat.pil import Image
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class QRBase:
|
||||
def __init__(self):
|
||||
self.text = ""
|
||||
self.fill = None
|
||||
self.back = None
|
||||
|
||||
FUNCTION = "generate_qr"
|
||||
CATEGORY = "Comfy-QR"
|
||||
|
||||
def _get_error_correction_constant(self, error_correction_string):
|
||||
if error_correction_string == "Low":
|
||||
return qrcode.constants.ERROR_CORRECT_L
|
||||
if error_correction_string == "Medium":
|
||||
return qrcode.constants.ERROR_CORRECT_M
|
||||
if error_correction_string == "Quartile":
|
||||
return qrcode.constants.ERROR_CORRECT_Q
|
||||
return qrcode.constants.ERROR_CORRECT_H
|
||||
|
||||
def _img_to_tensor(self, img):
|
||||
out_image = np.array(img, dtype=np.uint8).astype(np.float32) / 255
|
||||
return torch.from_numpy(out_image).unsqueeze(0)
|
||||
|
||||
def _make_qr(self, qr, fill_hexcolor, back_hexcolor):
|
||||
self.fill = self._parse_hexcolor_string(fill_hexcolor, "fill_hexcolor")
|
||||
self.back = self._parse_hexcolor_string(back_hexcolor, "back_hexcolor")
|
||||
qr.make(fit=True)
|
||||
return qr.make_image(fill_color=self.fill, back_color=self.back)
|
||||
|
||||
def _parse_hexcolor_string(self, s, parameter):
|
||||
if s.startswith("#"):
|
||||
s = s[1:]
|
||||
if len(s) == 3:
|
||||
rgb = (c + c for c in s)
|
||||
elif len(s) == 6:
|
||||
rgb = (s[i] + s[i+1] for i in range(0, 6, 2))
|
||||
else:
|
||||
raise ValueError(f"{parameter} must be 3 or 6 characters long")
|
||||
try:
|
||||
return tuple(int(channel, 16) for channel in rgb)
|
||||
except ValueError:
|
||||
raise ValueError(f"{parameter} contains invalid hexadecimal characters")
|
||||
|
||||
def _validate_qr_size(self, size, max_size):
|
||||
if size > max_size:
|
||||
raise RuntimeError(f"QR dimensions of {size} exceed max size of {max_size}.")
|
||||
|
||||
def update_text(self, protocol, text):
|
||||
"""This function takes input from a text box and a chosen internet
|
||||
protocol and stores a full address within an instance variable.
|
||||
Backslashes will invalidate text box input and this acts as a
|
||||
workaround to be able to use them when required in QR strings.
|
||||
|
||||
Args:
|
||||
protocol: A categorical variable of one of the available internet
|
||||
protocols.
|
||||
text: The input from the text box.
|
||||
"""
|
||||
if protocol == "Https":
|
||||
prefix = "https://"
|
||||
elif protocol == "Http":
|
||||
prefix = "http://"
|
||||
elif protocol == "None":
|
||||
prefix = ""
|
||||
self.text = prefix + text
|
||||
|
||||
|
||||
class QRByImageSize(QRBase):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"protocol": (["Http", "Https", "None"], {"default": "Https"}),
|
||||
"text": ("STRING", {"multiline": True}),
|
||||
"image_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),
|
||||
"fill_hexcolor": ("STRING", {"multiline": False, "default": "#000000"}),
|
||||
"back_hexcolor": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
|
||||
"error_correction": (["Low", "Medium", "Quartile", "High"], {"default": "High"}),
|
||||
"border": ("INT", {"default": 1, "min": 0, "max": 100, "step": 1}),
|
||||
"resampling": (["Bicubic", "Bilinear", "Box", "Hamming", "Lanczos", "Nearest"], {"default": "Nearest"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT")
|
||||
RETURN_NAMES = ("QR_CODE", "QR_VERSION")
|
||||
|
||||
def _select_resampling_method(self, resampling_string):
|
||||
if resampling_string == "Nearest":
|
||||
return Image.NEAREST
|
||||
if resampling_string == "Bicubic":
|
||||
return Image.BICUBIC
|
||||
if resampling_string == "Bilinear":
|
||||
return Image.BILINEAR
|
||||
if resampling_string == "Lanczos":
|
||||
return Image.LANCZOS
|
||||
if resampling_string == "Box":
|
||||
return Image.BOX
|
||||
if resampling_string == "Hamming":
|
||||
return Image.HAMMING
|
||||
raise ValueError(f"Resampling method of {resampling_string} not supported")
|
||||
|
||||
def generate_qr(
|
||||
self,
|
||||
protocol,
|
||||
text,
|
||||
image_size,
|
||||
fill_hexcolor,
|
||||
back_hexcolor,
|
||||
error_correction,
|
||||
border,
|
||||
resampling
|
||||
):
|
||||
resampling_method = self._select_resampling_method(resampling)
|
||||
error_level = self._get_error_correction_constant(error_correction)
|
||||
self.update_text(protocol, text)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=error_level,
|
||||
box_size=1,
|
||||
border=border)
|
||||
qr.add_data(self.text)
|
||||
img = self._make_qr(qr, fill_hexcolor, back_hexcolor)
|
||||
self._validate_qr_size(img.pixel_size, image_size)
|
||||
img = img.resize((image_size, image_size), resample=resampling_method)
|
||||
return (self._img_to_tensor(img), qr.version)
|
||||
|
||||
|
||||
class QRByModuleSize(QRBase):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"protocol": (["Http", "Https", "None"], {"default": "Https"}),
|
||||
"text": ("STRING", {"multiline": True}),
|
||||
"module_size": ("INT", {"default": 16, "min": 1, "max": 64, "step": 1}),
|
||||
"max_image_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),
|
||||
"fill_hexcolor": ("STRING", {"multiline": False, "default": "#000000"}),
|
||||
"back_hexcolor": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
|
||||
"error_correction": (["Low", "Medium", "Quartile", "High"], {"default": "High"}),
|
||||
"border": ("INT", {"default": 1, "min": 0, "max": 100, "step": 1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT")
|
||||
RETURN_NAMES = ("QR_CODE", "QR_VERSION", "IMAGE_SIZE")
|
||||
|
||||
def generate_qr(
|
||||
self,
|
||||
protocol,
|
||||
text,
|
||||
module_size,
|
||||
max_image_size,
|
||||
fill_hexcolor,
|
||||
back_hexcolor,
|
||||
error_correction,
|
||||
border
|
||||
):
|
||||
self.update_text(protocol, text)
|
||||
error_level = self._get_error_correction_constant(error_correction)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=error_level,
|
||||
box_size=module_size,
|
||||
border=border)
|
||||
qr.add_data(self.text)
|
||||
img = self._make_qr(qr, fill_hexcolor, back_hexcolor)
|
||||
self._validate_qr_size(img.pixel_size, max_image_size)
|
||||
return (self._img_to_tensor(img), qr.version, img.pixel_size)
|
||||
|
||||
|
||||
class QRByModuleSizeSplitFunctionPatterns(QRBase):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"protocol": (["Http", "Https", "None"], {"default": "Https"}),
|
||||
"text": ("STRING", {"multiline": True}),
|
||||
"module_size": ("INT", {"default": 16, "min": 1, "max": 64, "step": 1}),
|
||||
"max_image_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),
|
||||
"fill_hexcolor": ("STRING", {"multiline": False, "default": "#000000"}),
|
||||
"back_hexcolor": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
|
||||
"error_correction": (["Low", "Medium", "Quartile", "High"], {"default": "High"}),
|
||||
"border": ("INT", {"default": 1, "min": 0, "max": 100, "step": 1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "MASK", "INT", "INT")
|
||||
RETURN_NAMES = ("QR_CODE", "MODULE_LAYER", "FINDER_LAYER", "FINDER_MASK", "QR_VERSION", "IMAGE_SIZE")
|
||||
|
||||
def _generate_finder_pattern_ranges(self, module_size, border_size):
|
||||
outer = module_size * border_size
|
||||
inner = 7 * module_size + outer
|
||||
# Alternate behavior is required to prevent bugs from 0 border_size.
|
||||
far_outer = -outer if border_size else None
|
||||
return [
|
||||
(outer, inner, outer, inner),
|
||||
(outer, inner, -inner, far_outer),
|
||||
(-inner, far_outer, outer, inner)
|
||||
]
|
||||
|
||||
def _generate_finder_pattern_mask(self, pixel_size, module_size, border_size):
|
||||
mask = np.zeros((pixel_size, pixel_size), dtype=bool)
|
||||
for x_min, x_max, y_min, y_max in self._generate_finder_pattern_ranges(module_size, border_size):
|
||||
mask[y_min:y_max, x_min:x_max] = True
|
||||
return mask
|
||||
|
||||
def _apply_fill_to_mask(self, img, mask):
|
||||
array = np.array(img).copy()
|
||||
indices = np.nonzero(mask)
|
||||
array[indices[0], indices[1], :] = self.back
|
||||
return Image.fromarray(array)
|
||||
|
||||
def _mask_to_tensor(self, mask):
|
||||
out_image = mask.astype(np.float32)
|
||||
return torch.from_numpy(out_image)
|
||||
|
||||
def generate_qr(
|
||||
self,
|
||||
protocol,
|
||||
text,
|
||||
module_size,
|
||||
max_image_size,
|
||||
fill_hexcolor,
|
||||
back_hexcolor,
|
||||
error_correction,
|
||||
border
|
||||
):
|
||||
self.update_text(protocol, text)
|
||||
error_level = self._get_error_correction_constant(error_correction)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=error_level,
|
||||
box_size=module_size,
|
||||
border=border)
|
||||
qr.add_data(self.text)
|
||||
img = self._make_qr(qr, fill_hexcolor, back_hexcolor)
|
||||
pixel_size = img.pixel_size
|
||||
self._validate_qr_size(pixel_size, max_image_size)
|
||||
mask = self._generate_finder_pattern_mask(pixel_size, module_size, border)
|
||||
module_image = self._apply_fill_to_mask(img, mask)
|
||||
function_image = self._apply_fill_to_mask(img, ~mask)
|
||||
return (
|
||||
self._img_to_tensor(img),
|
||||
self._img_to_tensor(module_image),
|
||||
self._img_to_tensor(function_image),
|
||||
self._mask_to_tensor(mask),
|
||||
qr.version,
|
||||
pixel_size,
|
||||
)
|
||||
|
||||
|
||||
class QRErrorMasker:
|
||||
def __init__(self):
|
||||
self.module_size = None
|
||||
self.canvas_shape = None
|
||||
self.qr_bounds = None
|
||||
|
||||
FUNCTION = "find_qr_errors"
|
||||
CATEGORY = "Comfy-QR"
|
||||
RETURN_TYPES = ("MASK", "FLOAT", "FLOAT", "FLOAT")
|
||||
RETURN_NAMES = ("QR_ERROR_MASK", "PERCENT_ERROR", "CORRELATION", "RMSE")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"source_qr": ("IMAGE",),
|
||||
"modified_qr": ("IMAGE",),
|
||||
"module_size": ("INT", {"default": 16, "min": 1, "max": 64, "step": 1}),
|
||||
"grayscale_method": (["mean",], {"default": "mean"}),
|
||||
"aggregate_method": (["mean",], {"default": "mean"}),
|
||||
"evaluate": (["full_qr", "module_pattern", "finder_pattern"], {"default": "module_pattern"}),
|
||||
"error_difficulty": ("FLOAT", {"default": 0, "min": 0, "max": 1, "step": .01}),
|
||||
"inverted_pattern": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
}
|
||||
|
||||
def _get_qr_bounds(self, tensor, invert):
|
||||
module_color = 1.0 if invert else 0.0
|
||||
module_pixels = (tensor == module_color)
|
||||
indices = torch.nonzero(module_pixels, as_tuple=True)
|
||||
# The viewer patterns will guarentee a module pixel in the upper left
|
||||
# The bottom right does not have that guarentee so max is used.
|
||||
return indices[0][0], indices[0].max() + 1, indices[1][0], indices[1].max() + 1
|
||||
|
||||
def _extract_pattern_from_bounds(self, tensor):
|
||||
return tensor[self.qr_bounds[0]:self.qr_bounds[1], self.qr_bounds[2]:self.qr_bounds[3]]
|
||||
|
||||
def _trim_to_qr_area(self, source_qr, modified_qr, inverted_pattern):
|
||||
self.qr_bounds = self._get_qr_bounds(source_qr, inverted_pattern)
|
||||
self._check_bounds_and_module_size()
|
||||
source_qr = self._extract_pattern_from_bounds(source_qr)
|
||||
modified_qr = self._extract_pattern_from_bounds(modified_qr)
|
||||
return source_qr, modified_qr
|
||||
|
||||
def _reshape_tensor_to_modules(self, tensor):
|
||||
if len(tensor.shape) != 2:
|
||||
raise RuntimeError("Module reshaping requires a 2 dimensional array.")
|
||||
length = tensor.shape[0] // self.module_size
|
||||
reshaped_tensor = tensor.view(length, self.module_size, length, self.module_size)
|
||||
rehaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
|
||||
return rehaped_tensor.view(length, length, self.module_size ** 2)
|
||||
|
||||
def _check_bounds_and_module_size(self):
|
||||
height = self.qr_bounds[1] - self.qr_bounds[0]
|
||||
width = self.qr_bounds[3] - self.qr_bounds[2]
|
||||
color_warning = "Make sure that qr_fill and back colors have exact #FFFFFFF and #000000 values (and that module color values do not occur outside the QR) and invert is set correctly."
|
||||
if width != height:
|
||||
raise RuntimeError(f"Source QR dimensions are {width} x {height}. They must be a perfect square. {color_warning}")
|
||||
if width % self.module_size:
|
||||
raise RuntimeError(f"QR width of {width} does not fit module_size of {self.module_size}. It must be perfectly divisible. {color_warning}")
|
||||
|
||||
def _squeeze_by_mean(self, tensor):
|
||||
return torch.mean(tensor, dim=-1)
|
||||
|
||||
def _squeeze_to_modules(self, tensor, method):
|
||||
tensor = self._reshape_tensor_to_modules(tensor)
|
||||
if method == "mean":
|
||||
return self._squeeze_by_mean(tensor)
|
||||
raise RuntimeError("Module aggregation currently only supports the mean.")
|
||||
|
||||
def _reduce_to_modules(
|
||||
self,
|
||||
source_qr,
|
||||
modified_qr,
|
||||
module_size,
|
||||
grayscale_method,
|
||||
aggregate_method,
|
||||
inverted_pattern
|
||||
):
|
||||
if source_qr.shape != modified_qr.shape:
|
||||
raise ValueError("Source and modified QR must have the same dimensions.")
|
||||
self.module_size = module_size
|
||||
self.canvas_shape = (source_qr.shape[1], source_qr.shape[2])
|
||||
# Ignore batch dimension
|
||||
source_qr, modified_qr = source_qr[0], modified_qr[0]
|
||||
# Processed first for simplified indexing of QR bounds.
|
||||
source_qr = self._squeeze_by_mean(source_qr)
|
||||
source_qr, modified_qr = self._trim_to_qr_area(source_qr,
|
||||
modified_qr,
|
||||
inverted_pattern
|
||||
)
|
||||
if grayscale_method == "mean":
|
||||
modified_qr = self._squeeze_by_mean(modified_qr)
|
||||
else:
|
||||
raise ValueError("Currently only mean is supported for rgb to grayscale conversion.")
|
||||
source_qr = self._squeeze_to_modules(source_qr, "mean")
|
||||
modified_qr = self._squeeze_to_modules(modified_qr, aggregate_method)
|
||||
return source_qr, modified_qr
|
||||
|
||||
def _create_finder_pattern_mask(self, width, inverted):
|
||||
mask = np.zeros((width, width), dtype=bool)
|
||||
# When borders are trimmed and QR code has module size of 1, results
|
||||
# are consistent.
|
||||
finder_coords = [[0, 7, 0, 7], [0, 7, -7, None], [-7, None, 0, 7]]
|
||||
for x_min, x_max, y_min, y_max in finder_coords:
|
||||
mask[y_min:y_max, x_min:x_max] = True
|
||||
return ~mask if inverted else mask
|
||||
|
||||
def _create_qr_mask(self, tensor, evaluate):
|
||||
if evaluate == "module_pattern":
|
||||
return self._create_finder_pattern_mask(tensor, True)
|
||||
if evaluate == "finder_pattern":
|
||||
return self._create_finder_pattern_mask(tensor, False)
|
||||
return None
|
||||
|
||||
def _bin_tensor_to_threshold(self, tensor, contrast_difficulty):
|
||||
tensor = tensor.clone()
|
||||
threshold = contrast_difficulty / 2
|
||||
# Since we are only interested in value matches and there is a clear
|
||||
# stable dividing line of .5, bringing in the other array is
|
||||
# unneccessary and the binning process can be simplified.
|
||||
bin_condition = (tensor + threshold <= .5) & (tensor != .5)
|
||||
tensor[bin_condition] = 0.0
|
||||
bin_condition = (tensor - threshold >= .5) & (tensor != .5)
|
||||
tensor[bin_condition] = 1.0
|
||||
return tensor
|
||||
|
||||
def _replace_qr_to_canvas(self, tensor):
|
||||
length = tensor.shape[0] * self.module_size
|
||||
bounds = self.qr_bounds
|
||||
tensor = F.interpolate(tensor.unsqueeze(0).unsqueeze(0), size=(length, length), mode='nearest')
|
||||
canvas = torch.zeros(self.canvas_shape, dtype=torch.float32)
|
||||
canvas[bounds[0]:bounds[1], bounds[2]:bounds[3]] = tensor.squeeze()
|
||||
return canvas
|
||||
|
||||
def _compare_modules(
|
||||
self,
|
||||
source_qr,
|
||||
modified_qr,
|
||||
mask,
|
||||
error_difficulty
|
||||
):
|
||||
modified_qr = self._bin_tensor_to_threshold(modified_qr, error_difficulty)
|
||||
error = source_qr != modified_qr
|
||||
percent_error = error[mask].sum().item() / error[mask].numel()
|
||||
if mask is not None:
|
||||
error[~mask] = False
|
||||
return self._replace_qr_to_canvas((error).to(torch.float32)), percent_error
|
||||
|
||||
def _qr_correlation(self, source_qr, modified_qr, mask):
|
||||
source_qr = source_qr[mask].numpy().reshape((-1))
|
||||
modified_qr = modified_qr[mask].numpy().reshape((-1))
|
||||
return np.corrcoef(source_qr, modified_qr)[0, 1]
|
||||
|
||||
def _qr_rmse(self, source_qr, modified_qr, mask):
|
||||
diff = source_qr[mask].numpy() - modified_qr[mask].numpy()
|
||||
return np.sqrt((diff ** 2).mean())
|
||||
|
||||
def find_qr_errors(
|
||||
self,
|
||||
source_qr,
|
||||
modified_qr,
|
||||
module_size,
|
||||
grayscale_method,
|
||||
aggregate_method,
|
||||
evaluate,
|
||||
error_difficulty,
|
||||
inverted_pattern,
|
||||
):
|
||||
source_qr, modified_qr = self._reduce_to_modules(source_qr,
|
||||
modified_qr,
|
||||
module_size,
|
||||
grayscale_method,
|
||||
aggregate_method,
|
||||
inverted_pattern
|
||||
)
|
||||
mask = self._create_qr_mask(source_qr.shape[0], evaluate)
|
||||
error_mask, percent_error = self._compare_modules(source_qr,
|
||||
modified_qr,
|
||||
mask,
|
||||
error_difficulty
|
||||
)
|
||||
correlation = self._qr_correlation(source_qr, modified_qr, mask)
|
||||
rmse = self._qr_rmse(source_qr, modified_qr, mask)
|
||||
return (error_mask, percent_error, correlation, rmse)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"comfy-qr-by-module-size": QRByModuleSize,
|
||||
"comfy-qr-by-image-size": QRByImageSize,
|
||||
"comfy-qr-by-module-split": QRByModuleSizeSplitFunctionPatterns,
|
||||
"comfy-qr-mask_errors": QRErrorMasker,
|
||||
}
|
||||
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"comfy-qr-by-module-size": "QR Code",
|
||||
"comfy-qr-by-image-size": "QR Code (Conformed to Image Size)",
|
||||
"comfy-qr-by-module-split": "QR Code (Split)",
|
||||
"comfy-qr-mask_errors": "Mask QR Errors",
|
||||
}
|
||||
import numpy as np
|
||||
import qrcode
|
||||
from qrcode.compat.pil import Image
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class QRBase:
|
||||
def __init__(self):
|
||||
self.text = ""
|
||||
self.fill = None
|
||||
self.back = None
|
||||
|
||||
FUNCTION = "generate_qr"
|
||||
CATEGORY = "ComfyQR"
|
||||
|
||||
def _get_error_correction_constant(self, error_correction_string):
|
||||
if error_correction_string == "Low":
|
||||
return qrcode.constants.ERROR_CORRECT_L
|
||||
if error_correction_string == "Medium":
|
||||
return qrcode.constants.ERROR_CORRECT_M
|
||||
if error_correction_string == "Quartile":
|
||||
return qrcode.constants.ERROR_CORRECT_Q
|
||||
return qrcode.constants.ERROR_CORRECT_H
|
||||
|
||||
def _img_to_tensor(self, img):
|
||||
out_image = np.array(img, dtype=np.uint8).astype(np.float32) / 255
|
||||
return torch.from_numpy(out_image).unsqueeze(0)
|
||||
|
||||
def _make_qr(self, qr, fill_hexcolor, back_hexcolor):
|
||||
self.fill = self._parse_hexcolor_string(fill_hexcolor, "fill_hexcolor")
|
||||
self.back = self._parse_hexcolor_string(back_hexcolor, "back_hexcolor")
|
||||
qr.make(fit=True)
|
||||
return qr.make_image(fill_color=self.fill, back_color=self.back)
|
||||
|
||||
def _parse_hexcolor_string(self, s, parameter):
|
||||
if s.startswith("#"):
|
||||
s = s[1:]
|
||||
if len(s) == 3:
|
||||
rgb = (c + c for c in s)
|
||||
elif len(s) == 6:
|
||||
rgb = (s[i] + s[i+1] for i in range(0, 6, 2))
|
||||
else:
|
||||
raise ValueError(f"{parameter} must be 3 or 6 characters long")
|
||||
try:
|
||||
return tuple(int(channel, 16) for channel in rgb)
|
||||
except ValueError:
|
||||
raise ValueError(f"{parameter} contains invalid hexadecimal characters")
|
||||
|
||||
def _validate_qr_size(self, size, max_size):
|
||||
if size > max_size:
|
||||
raise RuntimeError(f"QR dimensions of {size} exceed max size of {max_size}.")
|
||||
|
||||
def update_text(self, protocol, text):
|
||||
"""This function takes input from a text box and a chosen internet
|
||||
protocol and stores a full address within an instance variable.
|
||||
Backslashes will invalidate text box input and this acts as a
|
||||
workaround to be able to use them when required in QR strings.
|
||||
|
||||
Args:
|
||||
protocol: A categorical variable of one of the available internet
|
||||
protocols.
|
||||
text: The input from the text box.
|
||||
"""
|
||||
if protocol == "Https":
|
||||
prefix = "https://"
|
||||
elif protocol == "Http":
|
||||
prefix = "http://"
|
||||
elif protocol == "None":
|
||||
prefix = ""
|
||||
self.text = prefix + text
|
||||
|
||||
|
||||
class QRByImageSize(QRBase):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"protocol": (["Http", "Https", "None"], {"default": "Https"}),
|
||||
"text": ("STRING", {"multiline": True}),
|
||||
"image_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),
|
||||
"fill_hexcolor": ("STRING", {"multiline": False, "default": "#000000"}),
|
||||
"back_hexcolor": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
|
||||
"error_correction": (["Low", "Medium", "Quartile", "High"], {"default": "High"}),
|
||||
"border": ("INT", {"default": 1, "min": 0, "max": 100, "step": 1}),
|
||||
"resampling": (["Bicubic", "Bilinear", "Box", "Hamming", "Lanczos", "Nearest"], {"default": "Nearest"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT")
|
||||
RETURN_NAMES = ("QR_CODE", "QR_VERSION")
|
||||
|
||||
def _select_resampling_method(self, resampling_string):
|
||||
if resampling_string == "Nearest":
|
||||
return Image.NEAREST
|
||||
if resampling_string == "Bicubic":
|
||||
return Image.BICUBIC
|
||||
if resampling_string == "Bilinear":
|
||||
return Image.BILINEAR
|
||||
if resampling_string == "Lanczos":
|
||||
return Image.LANCZOS
|
||||
if resampling_string == "Box":
|
||||
return Image.BOX
|
||||
if resampling_string == "Hamming":
|
||||
return Image.HAMMING
|
||||
raise ValueError(f"Resampling method of {resampling_string} not supported")
|
||||
|
||||
def generate_qr(
|
||||
self,
|
||||
protocol,
|
||||
text,
|
||||
image_size,
|
||||
fill_hexcolor,
|
||||
back_hexcolor,
|
||||
error_correction,
|
||||
border,
|
||||
resampling
|
||||
):
|
||||
resampling_method = self._select_resampling_method(resampling)
|
||||
error_level = self._get_error_correction_constant(error_correction)
|
||||
self.update_text(protocol, text)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=error_level,
|
||||
box_size=1,
|
||||
border=border)
|
||||
qr.add_data(self.text)
|
||||
img = self._make_qr(qr, fill_hexcolor, back_hexcolor)
|
||||
self._validate_qr_size(img.pixel_size, image_size)
|
||||
img = img.resize((image_size, image_size), resample=resampling_method)
|
||||
return (self._img_to_tensor(img), qr.version)
|
||||
|
||||
|
||||
class QRByModuleSize(QRBase):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"protocol": (["Http", "Https", "None"], {"default": "Https"}),
|
||||
"text": ("STRING", {"multiline": True}),
|
||||
"module_size": ("INT", {"default": 16, "min": 1, "max": 64, "step": 1}),
|
||||
"max_image_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),
|
||||
"fill_hexcolor": ("STRING", {"multiline": False, "default": "#000000"}),
|
||||
"back_hexcolor": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
|
||||
"error_correction": (["Low", "Medium", "Quartile", "High"], {"default": "High"}),
|
||||
"border": ("INT", {"default": 1, "min": 0, "max": 100, "step": 1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT")
|
||||
RETURN_NAMES = ("QR_CODE", "QR_VERSION", "IMAGE_SIZE")
|
||||
|
||||
def generate_qr(
|
||||
self,
|
||||
protocol,
|
||||
text,
|
||||
module_size,
|
||||
max_image_size,
|
||||
fill_hexcolor,
|
||||
back_hexcolor,
|
||||
error_correction,
|
||||
border
|
||||
):
|
||||
self.update_text(protocol, text)
|
||||
error_level = self._get_error_correction_constant(error_correction)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=error_level,
|
||||
box_size=module_size,
|
||||
border=border)
|
||||
qr.add_data(self.text)
|
||||
img = self._make_qr(qr, fill_hexcolor, back_hexcolor)
|
||||
self._validate_qr_size(img.pixel_size, max_image_size)
|
||||
return (self._img_to_tensor(img), qr.version, img.pixel_size)
|
||||
|
||||
|
||||
class QRByModuleSizeSplitFunctionPatterns(QRBase):
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"protocol": (["Http", "Https", "None"], {"default": "Https"}),
|
||||
"text": ("STRING", {"multiline": True}),
|
||||
"module_size": ("INT", {"default": 16, "min": 1, "max": 64, "step": 1}),
|
||||
"max_image_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),
|
||||
"fill_hexcolor": ("STRING", {"multiline": False, "default": "#000000"}),
|
||||
"back_hexcolor": ("STRING", {"multiline": False, "default": "#FFFFFF"}),
|
||||
"error_correction": (["Low", "Medium", "Quartile", "High"], {"default": "High"}),
|
||||
"border": ("INT", {"default": 1, "min": 0, "max": 100, "step": 1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "MASK", "INT", "INT")
|
||||
RETURN_NAMES = ("QR_CODE", "MODULE_LAYER", "FINDER_LAYER", "FINDER_MASK", "QR_VERSION", "IMAGE_SIZE")
|
||||
|
||||
def _generate_finder_pattern_ranges(self, module_size, border_size):
|
||||
outer = module_size * border_size
|
||||
inner = 7 * module_size + outer
|
||||
# Alternate behavior is required to prevent bugs from 0 border_size.
|
||||
far_outer = -outer if border_size else None
|
||||
return [
|
||||
(outer, inner, outer, inner),
|
||||
(outer, inner, -inner, far_outer),
|
||||
(-inner, far_outer, outer, inner)
|
||||
]
|
||||
|
||||
def _generate_finder_pattern_mask(self, pixel_size, module_size, border_size):
|
||||
mask = np.zeros((pixel_size, pixel_size), dtype=bool)
|
||||
for x_min, x_max, y_min, y_max in self._generate_finder_pattern_ranges(module_size, border_size):
|
||||
mask[y_min:y_max, x_min:x_max] = True
|
||||
return mask
|
||||
|
||||
def _apply_fill_to_mask(self, img, mask):
|
||||
array = np.array(img).copy()
|
||||
indices = np.nonzero(mask)
|
||||
array[indices[0], indices[1], :] = self.back
|
||||
return Image.fromarray(array)
|
||||
|
||||
def _mask_to_tensor(self, mask):
|
||||
out_image = mask.astype(np.float32)
|
||||
return torch.from_numpy(out_image).unsqueeze(0)
|
||||
|
||||
def generate_qr(
|
||||
self,
|
||||
protocol,
|
||||
text,
|
||||
module_size,
|
||||
max_image_size,
|
||||
fill_hexcolor,
|
||||
back_hexcolor,
|
||||
error_correction,
|
||||
border
|
||||
):
|
||||
self.update_text(protocol, text)
|
||||
error_level = self._get_error_correction_constant(error_correction)
|
||||
qr = qrcode.QRCode(
|
||||
error_correction=error_level,
|
||||
box_size=module_size,
|
||||
border=border)
|
||||
qr.add_data(self.text)
|
||||
img = self._make_qr(qr, fill_hexcolor, back_hexcolor)
|
||||
pixel_size = img.pixel_size
|
||||
self._validate_qr_size(pixel_size, max_image_size)
|
||||
mask = self._generate_finder_pattern_mask(pixel_size, module_size, border)
|
||||
module_image = self._apply_fill_to_mask(img, mask)
|
||||
function_image = self._apply_fill_to_mask(img, ~mask)
|
||||
return (
|
||||
self._img_to_tensor(img),
|
||||
self._img_to_tensor(module_image),
|
||||
self._img_to_tensor(function_image),
|
||||
self._mask_to_tensor(mask),
|
||||
qr.version,
|
||||
pixel_size,
|
||||
)
|
||||
|
||||
|
||||
class QRErrorMasker:
|
||||
def __init__(self):
|
||||
self.module_size = None
|
||||
self.canvas_shape = None
|
||||
self.qr_bounds = None
|
||||
|
||||
FUNCTION = "find_qr_errors"
|
||||
CATEGORY = "ComfyQR"
|
||||
RETURN_TYPES = ("MASK", "FLOAT", "FLOAT", "FLOAT")
|
||||
RETURN_NAMES = ("QR_ERROR_MASK", "PERCENT_ERROR", "CORRELATION", "RMSE")
|
||||
OUTPUT_IS_LIST = (False, True, True, True)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"source_qr": ("IMAGE",),
|
||||
"modified_qr": ("IMAGE",),
|
||||
"module_size": ("INT", {"default": 16, "min": 1, "max": 64, "step": 1}),
|
||||
"grayscale_method": (["mean", "luminance"], {"default": "luminance"}),
|
||||
"aggregate_method": (["mean",], {"default": "mean"}),
|
||||
"evaluate": (["full_qr", "module_pattern", "finder_pattern"], {"default": "module_pattern"}),
|
||||
"error_difficulty": ("FLOAT", {"default": 0, "min": 0, "max": 1, "step": .01}),
|
||||
"inverted_pattern": ("BOOLEAN", {"default": False}),
|
||||
"gamma": ("FLOAT", {"default": 2.2, "min": .1, "max": 2.8, "step": .1}),
|
||||
},
|
||||
}
|
||||
|
||||
def _get_qr_bounds(self, tensor, invert):
|
||||
module_color = 1.0 if invert else 0.0
|
||||
module_pixels = (tensor == module_color)
|
||||
indices = torch.nonzero(module_pixels, as_tuple=True)
|
||||
# The viewer patterns will guarentee a module pixel in the upper left
|
||||
# The bottom right does not have that guarentee so max is used.
|
||||
return indices[0][0], indices[0].max() + 1, indices[1][0], indices[1].max() + 1
|
||||
|
||||
def _extract_pattern_from_bounds(self, tensor):
|
||||
return tensor[self.qr_bounds[0]:self.qr_bounds[1], self.qr_bounds[2]:self.qr_bounds[3]]
|
||||
|
||||
def _trim_to_qr_area(self, source_qr, modified_qr, inverted_pattern):
|
||||
self.qr_bounds = self._get_qr_bounds(source_qr, inverted_pattern)
|
||||
self._check_bounds_and_module_size()
|
||||
source_qr = self._extract_pattern_from_bounds(source_qr)
|
||||
modified_qr = self._extract_pattern_from_bounds(modified_qr)
|
||||
return source_qr, modified_qr
|
||||
|
||||
def _reshape_tensor_to_modules(self, tensor):
|
||||
if len(tensor.shape) != 2:
|
||||
raise RuntimeError("Module reshaping requires a 2 dimensional array.")
|
||||
length = tensor.shape[0] // self.module_size
|
||||
reshaped_tensor = tensor.view(length, self.module_size, length, self.module_size)
|
||||
rehaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
|
||||
return rehaped_tensor.view(length, length, self.module_size ** 2)
|
||||
|
||||
def _check_bounds_and_module_size(self):
|
||||
height = self.qr_bounds[1] - self.qr_bounds[0]
|
||||
width = self.qr_bounds[3] - self.qr_bounds[2]
|
||||
color_warning = "Make sure that qr_fill and back colors have exact #FFFFFFF and #000000 values (and that module color values do not occur outside the QR) and invert is set correctly."
|
||||
if width != height:
|
||||
raise RuntimeError(f"Source QR dimensions are {width} x {height}. They must be a perfect square. {color_warning}")
|
||||
if width % self.module_size:
|
||||
raise RuntimeError(f"QR width of {width} does not fit module_size of {self.module_size}. It must be perfectly divisible. {color_warning}")
|
||||
|
||||
def _check_equal_shape(self, source_qr, modified_qr):
|
||||
if source_qr.shape != modified_qr.shape:
|
||||
raise ValueError("Source and modified QR must have the same batch size and dimensions.")
|
||||
|
||||
def _squeeze_by_mean(self, tensor):
|
||||
return torch.mean(tensor, dim=-1)
|
||||
|
||||
def _gamma_expansion(self, tensor, gamma):
|
||||
if gamma == 1:
|
||||
return tensor
|
||||
if gamma == 2.2:
|
||||
return torch.where(tensor <= 0.04045, tensor / 12.92, ((tensor + 0.055) / 1.055) ** 2.4)
|
||||
return tensor ** gamma
|
||||
|
||||
def _gamma_compression(self, tensor, gamma):
|
||||
if gamma == 1:
|
||||
return tensor
|
||||
if gamma == 2.2:
|
||||
return torch.where(tensor <= .0031308, tensor * 12.92, 1.055 * tensor ** (1/2.4) - 0.055)
|
||||
return tensor ** (1/gamma)
|
||||
|
||||
def _grayscale_by_luminance(self, tensor, gamma):
|
||||
weights = torch.tensor([0.2125, 0.7154, 0.0721], dtype=torch.float32)
|
||||
tensor = self._gamma_expansion(tensor, gamma)
|
||||
tensor = tensor @ weights
|
||||
if gamma != 1:
|
||||
tensor = tensor ** gamma
|
||||
return self._gamma_compression(tensor, gamma)
|
||||
|
||||
def _squeeze_to_modules(self, tensor, method):
|
||||
tensor = self._reshape_tensor_to_modules(tensor)
|
||||
if method == "mean":
|
||||
return self._squeeze_by_mean(tensor)
|
||||
raise RuntimeError("Module aggregation currently only supports the mean.")
|
||||
|
||||
def _reduce_to_modules(
|
||||
self,
|
||||
source_qr,
|
||||
modified_qr,
|
||||
module_size,
|
||||
grayscale_method,
|
||||
aggregate_method,
|
||||
inverted_pattern,
|
||||
gamma
|
||||
):
|
||||
self.module_size = module_size
|
||||
self.canvas_shape = (source_qr.shape[0], source_qr.shape[1])
|
||||
# Processed first for simplified indexing of QR bounds.
|
||||
source_qr = self._squeeze_by_mean(source_qr)
|
||||
source_qr, modified_qr = self._trim_to_qr_area(source_qr,
|
||||
modified_qr,
|
||||
inverted_pattern
|
||||
)
|
||||
if grayscale_method == "mean":
|
||||
modified_qr = self._squeeze_by_mean(modified_qr)
|
||||
elif grayscale_method == "luminance":
|
||||
modified_qr = self._grayscale_by_luminance(modified_qr, gamma)
|
||||
else:
|
||||
raise ValueError("Currently only mean is supported for rgb to grayscale conversion.")
|
||||
source_qr = self._squeeze_to_modules(source_qr, "mean")
|
||||
modified_qr = self._squeeze_to_modules(modified_qr, aggregate_method)
|
||||
return source_qr, modified_qr
|
||||
|
||||
def _create_finder_pattern_mask(self, width, inverted):
|
||||
mask = torch.zeros((width, width), dtype=torch.bool)
|
||||
# When borders are trimmed and QR code has module size of 1, results
|
||||
# are consistent.
|
||||
finder_coords = [[0, 7, 0, 7], [0, 7, -7, None], [-7, None, 0, 7]]
|
||||
for x_min, x_max, y_min, y_max in finder_coords:
|
||||
mask[y_min:y_max, x_min:x_max] = True
|
||||
return ~mask if inverted else mask
|
||||
|
||||
def _create_qr_mask(self, tensor, evaluate):
|
||||
if evaluate == "module_pattern":
|
||||
return self._create_finder_pattern_mask(tensor, True)
|
||||
if evaluate == "finder_pattern":
|
||||
return self._create_finder_pattern_mask(tensor, False)
|
||||
return None
|
||||
|
||||
def _bin_tensor_to_threshold(self, tensor, contrast_difficulty):
|
||||
tensor = tensor.clone()
|
||||
threshold = contrast_difficulty / 2
|
||||
# Since we are only interested in value matches and there is a clear
|
||||
# stable dividing line of .5, bringing in the other array is
|
||||
# unneccessary and the binning process can be simplified.
|
||||
bin_condition = (tensor + threshold <= .5) & (tensor != .5)
|
||||
tensor[bin_condition] = 0.0
|
||||
bin_condition = (tensor - threshold >= .5) & (tensor != .5)
|
||||
tensor[bin_condition] = 1.0
|
||||
return tensor
|
||||
|
||||
def _replace_qr_to_canvas(self, tensor):
|
||||
length = tensor.shape[0] * self.module_size
|
||||
bounds = self.qr_bounds
|
||||
tensor = F.interpolate(tensor.unsqueeze(0).unsqueeze(0), size=(length, length), mode='nearest')
|
||||
canvas = torch.zeros(self.canvas_shape, dtype=torch.float32)
|
||||
canvas[bounds[0]:bounds[1], bounds[2]:bounds[3]] = tensor.squeeze()
|
||||
return canvas
|
||||
|
||||
def _compare_modules(
|
||||
self,
|
||||
source_qr,
|
||||
modified_qr,
|
||||
mask,
|
||||
error_difficulty
|
||||
):
|
||||
modified_qr = self._bin_tensor_to_threshold(modified_qr, error_difficulty)
|
||||
error = source_qr != modified_qr
|
||||
percent_error = error[mask].sum().item() / error[mask].numel()
|
||||
if mask is not None:
|
||||
error[~mask] = False
|
||||
return self._replace_qr_to_canvas((error).to(torch.float32)), percent_error
|
||||
|
||||
def _qr_correlation(self, source_qr, modified_qr, mask):
|
||||
source_qr = source_qr[mask].numpy().reshape((-1))
|
||||
modified_qr = modified_qr[mask].numpy().reshape((-1))
|
||||
return np.corrcoef(source_qr, modified_qr)[0, 1]
|
||||
|
||||
def _qr_rmse(self, source_qr, modified_qr, mask):
|
||||
diff = source_qr[mask].numpy() - modified_qr[mask].numpy()
|
||||
return np.sqrt((diff ** 2).mean())
|
||||
|
||||
def find_qr_errors(
|
||||
self,
|
||||
source_qr,
|
||||
modified_qr,
|
||||
module_size,
|
||||
grayscale_method,
|
||||
aggregate_method,
|
||||
evaluate,
|
||||
error_difficulty,
|
||||
inverted_pattern,
|
||||
gamma,
|
||||
):
|
||||
self._check_equal_shape(source_qr, modified_qr)
|
||||
error_masks, error_percents, correlations, rmses = [], [], [], []
|
||||
for i in range(source_qr.shape[0]):
|
||||
qr_s, qr_m = source_qr[i], modified_qr[i]
|
||||
qr_s, qr_m = self._reduce_to_modules(qr_s,
|
||||
qr_m,
|
||||
module_size,
|
||||
grayscale_method,
|
||||
aggregate_method,
|
||||
inverted_pattern,
|
||||
gamma
|
||||
)
|
||||
mask = self._create_qr_mask(qr_s.shape[0], evaluate)
|
||||
error_mask, percent_error = self._compare_modules(qr_s,
|
||||
qr_m,
|
||||
mask,
|
||||
error_difficulty
|
||||
)
|
||||
correlation = self._qr_correlation(qr_s, qr_m, mask)
|
||||
rmse = self._qr_rmse(qr_s, qr_m, mask)
|
||||
error_masks.append(error_mask)
|
||||
error_percents.append(percent_error)
|
||||
correlations.append(correlation)
|
||||
rmses.append(rmse)
|
||||
error_masks = torch.stack(error_masks, dim=0)
|
||||
return (error_masks, error_percents, correlations, rmses)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"comfy-qr-by-module-size": QRByModuleSize,
|
||||
"comfy-qr-by-image-size": QRByImageSize,
|
||||
"comfy-qr-by-module-split": QRByModuleSizeSplitFunctionPatterns,
|
||||
"comfy-qr-mask_errors": QRErrorMasker,
|
||||
}
|
||||
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"comfy-qr-by-module-size": "QR Code",
|
||||
"comfy-qr-by-image-size": "QR Code (Conformed to Image Size)",
|
||||
"comfy-qr-by-module-split": "QR Code (Split)",
|
||||
"comfy-qr-mask_errors": "Mask QR Errors",
|
||||
}
|
||||
|
||||