Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
849bba41ad | ||
|
|
cb7726221e | ||
|
|
e91f331652 | ||
|
|
2de40a517e | ||
|
|
fc2b20ad38 | ||
|
|
ab262513df | ||
|
|
9e8434a7a8 | ||
|
|
e804976c75 |
@@ -1,20 +1,22 @@
|
||||
# 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 for the creation of patterns. If you would like to evaluate rendered patterns for scannability, check out the companion project [Comfy QR Validation Nodes](https://gitlab.com/sofuego-comfy-nodes/comfy-qr-validation-nodes). Any workflow in the example that ends with "validated" assume the installation of the validation pack as well.
|
||||
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
|
||||
|
||||
This project currently contains two nodes for generating QR codes.
|
||||
|
||||
### QR Code
|
||||
### QR Code and QR Code (Split)
|
||||
|
||||
A dynamically sized QR code without any resampling scaled by the text length, the module size, and the error correction level.
|
||||
A dynamically sized QR code without any resampling scaled by the text length, the module size, and the error correction level. Use the `QR Code `for simple workflows and the `QR Code (Split)` if you want to build more advanced pipelines with additional outputs for the `MODULE_LAYER`, `FINDER_LAYER`, or `FINDER_MASK`.
|
||||
|
||||

|
||||
|
||||
#### Inputs
|
||||
|
||||
@@ -36,12 +38,17 @@ A dynamically sized QR code without any resampling scaled by the text length, th
|
||||
|
||||
#### Outputs
|
||||
|
||||
* `IMAGE` - The QR code
|
||||
* `QR_CODE` - The QR code
|
||||
* `MODULE_LAYER` - The QR code with the `back_hexcolor` filling in the function patterns (to remove their influence from the early diffusion steps).
|
||||
* `FINDER_LAYER` - The isolated finder patterns over the `back_hexcolor`.
|
||||
* `FINDER_MASK` - A mask that isolates the locations of the 3 finder patterns.
|
||||
* `QR_VERSION` - An integer of the QR version that was selected based on the text length and error correction level.
|
||||
* `IMAGE_SIZE` - An integer based on the pixel width of the resulting QR code.
|
||||
|
||||
### QR Code (Conformed to Image Size)
|
||||
|
||||

|
||||
|
||||
A QR code fixed to specific output dimensions through image resampling.
|
||||
|
||||
#### Inputs
|
||||
@@ -70,39 +77,57 @@ A QR code fixed to specific output dimensions through image resampling.
|
||||
|
||||
#### Outputs
|
||||
|
||||
* `IMAGE` - The QR code
|
||||
* `QR_CODE` - The QR code
|
||||
* `QR_VERSION` - An integer of the QR version that was selected based on the text length and error correction level.
|
||||
|
||||
### QR Code (Split)
|
||||
### Mask QR Errors
|
||||
|
||||
A version of the `QR Code` node that also outputs split layers for the finder patterns along with an accompanying mask.
|
||||
A node that that will analyze the differences between a modified QR and create a mask of the estimated errors.
|
||||
|
||||

|
||||
|
||||
**Note**: _This is an extremly powerful node but relies on several assumptions in order to be used._
|
||||
1. A QR with a fixed module size that has not been resampled irregularly or distorted.
|
||||
2. Shared allignment between the `source_qr` and `modified_qr`.
|
||||
3. Fill and back colors must be perfectly white and black (`#FFFFFF`, `#000000`) for the `source_qr`.
|
||||
4. The canvas can be bigger than the QR code, but on the `source_qr`, there should be no pixels the same color as the module color outside of the QR exterior.
|
||||
|
||||
#### Inputs
|
||||
|
||||
* `protocol` - If enabled this will prefix the textbox input with a preset to represent the internet protocol. This is included both for convenience and as a workaround for the textbox clipping strings with this character combination.
|
||||
* `Http` - Adds "http://" before the text.
|
||||
* `Https` - Adds "https://" before the text.
|
||||
* `None` - Uses only the contents of the `text` box.
|
||||
* `text` - What text to build your QR code with. If `protocol` is specified, this textbox will be combined it with the selected option.
|
||||
* `module_size` - The pixel width of the smallest unit of a QR code.
|
||||
* `max_image_size` - The maximum size of the resulting QR code. If the combination of `text`, `module_size`, and `error_correction` create dimensions that exceed this, an error will halt the pipeline.
|
||||
* `fill_hexcolor` - A string of characters representing the hex value of the QR units. Can be 3 or 6 hexadecimal characters with an optional # before.
|
||||
* `back_hexcolor` - A string of characters representing the hex value of the space around the QR units. Can be 3 or 6 hexadecimal characters with an optional # before.
|
||||
* `error_correction` - The level of error correction to apply.
|
||||
* `Low` - 7% error correction.
|
||||
* `Medium` - 15% error correction.
|
||||
* `Quartile` - 25% error correction.
|
||||
* `High` - 30% error correction.
|
||||
* `border` - The border size (In multiples of `module` widths)
|
||||
* `source_qr` - The original QR code used to generate the image (with the same placement and dimensions as `modified_qr`).
|
||||
* `modified_qr` - The generated QR art to evaluate (with the same placement and dimensions as `source_qr`).
|
||||
* `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.
|
||||
* `full_qr` - The entire QR code is evaluated.
|
||||
* `module_pattern` - Has the node focus only on the modules while excluding the finder pattern.
|
||||
* `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
|
||||
|
||||
* `QR_FLATTENED` - The QR code
|
||||
* `MODULE_LAYER` - The QR code with the `back_hexcolor` filling in the function patterns (to remove their influence from the early diffusion steps).
|
||||
* `FINDER_LAYER` - The isolated finder patterns over the `back_hexcolor`.
|
||||
* `FINDER_MASK` - A mask that isolates the locations of the 3 finder patterns.
|
||||
* `QR_VERSION` - An integer of the QR version that was selected based on the text length and error correction level.
|
||||
* `IMAGE_SIZE` - An integer based on the pixel width of the resulting QR code.
|
||||
* `QR_ERROR_MASK` - A mask with white areas falling on locations of modules with errors (precision set by `error_difficulty`). This mask can be used for streamlined img2img operations to salvage unscannable QRs.
|
||||
* `PERCENT_ERROR` - Number of modules that do not fall into the acceptable threshold (set by `error_difficulty`) divided by the total number of modules.
|
||||
* `CORRELATION` - The Pearson correlation coefficient between the QR patterns on the `source_qr` and `modified_qr`
|
||||
* `RMSE` - The root mean squared error between both QR codes. In this case, a value of 0 indicates an identical match to the `source_qr`. A value of 1 indicates a perfect inversion where each expected color is the complete opposite.
|
||||
|
||||
## Examples
|
||||
|
||||
There is an assortment of workflow examples that can be found in the `examples` directory or in the metadata of the images in the `example_generations` folder. For example taking this image.
|
||||
|
||||

|
||||
|
||||
And then aggressively increasing the strength of the ControlNet on only the error pixels to make something previously completely unscannable more reliably across different monitors and QR readers.
|
||||
|
||||

|
||||
|
||||
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 |
|
After Width: | Height: | Size: 924 KiB |
|
After Width: | Height: | Size: 909 KiB |
|
After Width: | Height: | Size: 961 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_node_id": 13,
|
||||
"last_link_id": 13,
|
||||
"last_node_id": 14,
|
||||
"last_link_id": 14,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
@@ -59,7 +59,7 @@
|
||||
{
|
||||
"name": "pixels",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
"link": 14
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
@@ -240,51 +240,6 @@
|
||||
"1.5/v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "comfy-qr-by-image-size",
|
||||
"pos": [
|
||||
202,
|
||||
581
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 264
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
4
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "QR_VERSION",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-by-image-size"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Https",
|
||||
"gitlab.com/sofuego-comfy-nodes/comfy-qr",
|
||||
512,
|
||||
"#222222",
|
||||
"#DDDDDD",
|
||||
"High",
|
||||
1,
|
||||
"Nearest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "KSampler",
|
||||
@@ -336,7 +291,7 @@
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
321184522216123,
|
||||
1077540641933383,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
@@ -344,6 +299,51 @@
|
||||
"normal",
|
||||
0.6499999999999999
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "comfy-qr-by-image-size",
|
||||
"pos": [
|
||||
264,
|
||||
659
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 264
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "QR_CODE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
14
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "QR_VERSION",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-by-image-size"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Https",
|
||||
"gitlab.com/sofuego-comfy-nodes/comfy-qr",
|
||||
512,
|
||||
"#000000",
|
||||
"#DDDDDD",
|
||||
"High",
|
||||
1,
|
||||
"Nearest"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -363,14 +363,6 @@
|
||||
0,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
4,
|
||||
4,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
6,
|
||||
@@ -434,6 +426,14 @@
|
||||
11,
|
||||
2,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
14,
|
||||
14,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_node_id": 18,
|
||||
"last_link_id": 24,
|
||||
"last_node_id": 19,
|
||||
"last_link_id": 27,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 13,
|
||||
@@ -138,7 +138,7 @@
|
||||
{
|
||||
"name": "width",
|
||||
"type": "INT",
|
||||
"link": 14,
|
||||
"link": 26,
|
||||
"widget": {
|
||||
"name": "width",
|
||||
"config": [
|
||||
@@ -155,7 +155,7 @@
|
||||
{
|
||||
"name": "height",
|
||||
"type": "INT",
|
||||
"link": 15,
|
||||
"link": 27,
|
||||
"widget": {
|
||||
"name": "height",
|
||||
"config": [
|
||||
@@ -312,7 +312,7 @@
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
483918444996335,
|
||||
123662286732457,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
@@ -360,61 +360,6 @@
|
||||
"worst quality"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "comfy-qr-by-module-size",
|
||||
"pos": [
|
||||
169,
|
||||
540
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 284
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
20
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "QR_VERSION",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "IMAGE_SIZE",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
14,
|
||||
15
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-by-module-size"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Https",
|
||||
"gitlab.com/sofuego-comfy-nodes",
|
||||
16,
|
||||
512,
|
||||
"#000000",
|
||||
"#FFFFFF",
|
||||
"Medium",
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "ControlNetApplyAdvanced",
|
||||
@@ -448,7 +393,7 @@
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 20
|
||||
"link": 25
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
@@ -479,6 +424,61 @@
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"type": "comfy-qr-by-module-size",
|
||||
"pos": [
|
||||
191,
|
||||
629
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 284
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "QR_CODE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
25
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "QR_VERSION",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "IMAGE_SIZE",
|
||||
"type": "INT",
|
||||
"links": [
|
||||
26,
|
||||
27
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-by-module-size"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Https",
|
||||
"gitlab.com/sofuego-comfy-nodes",
|
||||
16,
|
||||
512,
|
||||
"#000000",
|
||||
"#FFFFFF",
|
||||
"Medium",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
@@ -530,22 +530,6 @@
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
14,
|
||||
14,
|
||||
2,
|
||||
15,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
15,
|
||||
14,
|
||||
2,
|
||||
15,
|
||||
1,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
16,
|
||||
15,
|
||||
@@ -570,14 +554,6 @@
|
||||
2,
|
||||
"CONTROL_NET"
|
||||
],
|
||||
[
|
||||
20,
|
||||
14,
|
||||
0,
|
||||
17,
|
||||
3,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
22,
|
||||
17,
|
||||
@@ -601,6 +577,30 @@
|
||||
11,
|
||||
1,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
25,
|
||||
19,
|
||||
0,
|
||||
17,
|
||||
3,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
26,
|
||||
19,
|
||||
2,
|
||||
15,
|
||||
0,
|
||||
"INT"
|
||||
],
|
||||
[
|
||||
27,
|
||||
19,
|
||||
2,
|
||||
15,
|
||||
1,
|
||||
"INT"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
{
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 12,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 5,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
420,
|
||||
740
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 26
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
3
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "ImageCompositeMasked",
|
||||
"pos": [
|
||||
640,
|
||||
280
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 146
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "destination",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"type": "IMAGE",
|
||||
"link": 12
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
8
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageCompositeMasked"
|
||||
},
|
||||
"widgets_values": [
|
||||
16,
|
||||
16,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
680,
|
||||
520
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314.0000305175781
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
9
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"unscannable_00001_.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "comfy-qr-mask_errors",
|
||||
"pos": [
|
||||
1030,
|
||||
310
|
||||
],
|
||||
"size": {
|
||||
"0": 317.4000244140625,
|
||||
"1": 238
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 8
|
||||
},
|
||||
{
|
||||
"name": "modified_qr",
|
||||
"type": "IMAGE",
|
||||
"link": 9
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "QR_ERROR_MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
10
|
||||
],
|
||||
"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,
|
||||
"mean",
|
||||
"mean",
|
||||
"module_pattern",
|
||||
0,
|
||||
false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"type": "MaskToImage",
|
||||
"pos": [
|
||||
1399,
|
||||
333
|
||||
],
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 26
|
||||
},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 10
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
11
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "MaskToImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "SaveImage",
|
||||
"pos": [
|
||||
1574,
|
||||
474
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 270
|
||||
},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "SolidMask",
|
||||
"pos": [
|
||||
38,
|
||||
733
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
1
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SolidMask"
|
||||
},
|
||||
"widgets_values": [
|
||||
0.5,
|
||||
768,
|
||||
768
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "comfy-qr-by-module-split",
|
||||
"pos": [
|
||||
111,
|
||||
258
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 344
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "QR_CODE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
12
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MODULE_LAYER",
|
||||
"type": "IMAGE",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "FINDER_LAYER",
|
||||
"type": "IMAGE",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "FINDER_MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "QR_VERSION",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "IMAGE_SIZE",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-by-module-split"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Https",
|
||||
"gitlab.com/sofuego-comfy-nodes/comfy-qr-validation-nodes",
|
||||
16,
|
||||
768,
|
||||
"#000000",
|
||||
"#FFFFFF",
|
||||
"High",
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
3,
|
||||
5,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
8,
|
||||
6,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
9,
|
||||
2,
|
||||
0,
|
||||
9,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
10,
|
||||
9,
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
11,
|
||||
8,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
12,
|
||||
11,
|
||||
0,
|
||||
6,
|
||||
1,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 1,
|
||||
"last_node_id": 3,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
@@ -20,7 +20,7 @@
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
@@ -29,11 +29,11 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"id": 3,
|
||||
"type": "comfy-qr-by-image-size",
|
||||
"pos": [
|
||||
429,
|
||||
492
|
||||
516,
|
||||
479
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
@@ -44,10 +44,10 @@
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"name": "QR_CODE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1
|
||||
2
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
@@ -65,7 +65,7 @@
|
||||
"widgets_values": [
|
||||
"Https",
|
||||
"gitlab.com/sofuego-comfy-nodes/comfy-qr",
|
||||
768,
|
||||
512,
|
||||
"00F",
|
||||
"FF0",
|
||||
"High",
|
||||
@@ -76,8 +76,8 @@
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
|
||||
|
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 |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -1,268 +1,486 @@
|
||||
import numpy as np
|
||||
import qrcode
|
||||
from qrcode.compat.pil import Image
|
||||
import torch
|
||||
|
||||
|
||||
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 = ("IMAGE", "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 = ("IMAGE", "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_FLATTENED", "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,
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"comfy-qr-by-module-size": QRByModuleSize,
|
||||
"comfy-qr-by-image-size": QRByImageSize,
|
||||
"comfy-qr-by-module-split": QRByModuleSizeSplitFunctionPatterns,
|
||||
}
|
||||
|
||||
|
||||
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)",
|
||||
}
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
@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 _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
|
||||
):
|
||||
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)
|
||||
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 = 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,
|
||||
gamma,
|
||||
):
|
||||
source_qr, modified_qr = self._reduce_to_modules(source_qr,
|
||||
modified_qr,
|
||||
module_size,
|
||||
grayscale_method,
|
||||
aggregate_method,
|
||||
inverted_pattern,
|
||||
gamma
|
||||
)
|
||||
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",
|
||||
}
|
||||
|
||||