8 Commits
10 changed files with 1090 additions and 1048 deletions
+7 -5
View File
@@ -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.
![QR code with repo link](img/badgers_levels_adjusted.png)
![QR code with repo link](example_generations/main_qr_inpainted.png)
## 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
![Fixed Scan Example](example_generations/unscannable_00001_fixed_.png)
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 924 KiB

After

Width:  |  Height:  |  Size: 924 KiB

+311 -310
View File
@@ -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": [],
+276 -275
View File
@@ -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

+496 -458
View File
@@ -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",
}