2 Commits
Author SHA1 Message Date
Corey Ryan Hanson 9e8434a7a8 Merge branch 'dev-qr-error-mask' into 'main'
New QR Error Mask Node

See merge request sofuego-comfy-nodes/comfy-qr!2
2023-08-18 05:58:21 +00:00
Corey Ryan Hanson e804976c75 New QR Error Mask Node 2023-08-18 05:58:21 +00:00
14 changed files with 4974 additions and 184 deletions
+51 -28
View File
@@ -2,7 +2,7 @@
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 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.
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.
@@ -12,9 +12,11 @@ This repository is managed publicly on [Gitlab](https://gitlab.com/sofuego-comfy
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`.
![Split QR Code Node Example](img/node-qr-code-split.png)
#### 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)
![Conformed QR Code Node Example](img/node-qr-code-conformed.png)
A QR code fixed to specific output dimensions through image resampling.
#### Inputs
@@ -70,39 +77,55 @@ 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.
![Mask QR Errors Node Example](img/node-mask-qr-errors.png)
**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.
* `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.)
#### 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.
![Bad Scan Example](example_generations/unscannable_00001_.png)
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.
![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.
## Future updates
Binary file not shown.

After

Width:  |  Height:  |  Size: 924 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+57 -57
View File
@@ -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": [],
+85 -85
View File
@@ -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": [],
+426
View File
@@ -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
}
+11 -11
View File
@@ -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,
Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+193 -3
View File
@@ -2,6 +2,7 @@ import numpy as np
import qrcode
from qrcode.compat.pil import Image
import torch
import torch.nn.functional as F
class QRBase:
@@ -88,7 +89,7 @@ class QRByImageSize(QRBase):
}
RETURN_TYPES = ("IMAGE", "INT")
RETURN_NAMES = ("IMAGE", "QR_VERSION")
RETURN_NAMES = ("QR_CODE", "QR_VERSION")
def _select_resampling_method(self, resampling_string):
if resampling_string == "Nearest":
@@ -148,7 +149,7 @@ class QRByModuleSize(QRBase):
}
RETURN_TYPES = ("IMAGE", "INT", "INT")
RETURN_NAMES = ("IMAGE", "QR_VERSION", "IMAGE_SIZE")
RETURN_NAMES = ("QR_CODE", "QR_VERSION", "IMAGE_SIZE")
def generate_qr(
self,
@@ -191,7 +192,7 @@ class QRByModuleSizeSplitFunctionPatterns(QRBase):
}
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "MASK", "INT", "INT")
RETURN_NAMES = ("QR_FLATTENED", "MODULE_LAYER", "FINDER_LAYER", "FINDER_MASK", "QR_VERSION", "IMAGE_SIZE")
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
@@ -254,10 +255,198 @@ class QRByModuleSizeSplitFunctionPatterns(QRBase):
)
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,
}
@@ -265,4 +454,5 @@ 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",
}