13 Commits
23 changed files with 881 additions and 973 deletions
+22
View File
@@ -0,0 +1,22 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+12
View File
@@ -35,6 +35,7 @@ A dynamically sized QR code without any resampling scaled by the text length, th
* `Quartile` - 25% error correction.
* `High` - 30% error correction.
* `border` - The border size (In multiples of `module` widths)
* `module_drawer` - The shape the QR code modules should be. The default is `square`, but see [Alternate Module Drawers](#alternate-module-drawers) below for new examples.
#### Outputs
@@ -74,6 +75,7 @@ A QR code fixed to specific output dimensions through image resampling.
* `Hamming` - Hamming interpolation
* `Lanczos` - Lanczos interpolation
* `Nearest` - Nearest Neighbor interpolation
* `module_drawer` - The shape the QR code modules should be. The default is `square`, but see [Alternate Module Drawers](#alternate-module-drawers) below for new examples.
#### Outputs
@@ -129,6 +131,16 @@ And then aggressively increasing the strength of the ControlNet on only the erro
Any workflow in the example that ends with "validated" (and a few image examples) assume the installation of the [scanning pack](https://gitlab.com/sofuego-comfy-nodes/ComfyQR-scanning-nodes) as well.
## Alternate Module Drawers
The QR generation nodes now support alternate module styles. Experiment using different ones for greater flexibility during generation.
| Square | Gapped Square | Circle | Rounded | Vertical bars | Horizontal bars |
|:---: | :---: | :---: | :---: | :---: | :---: |
| ![Example QR](/img/square.png) | ![Example QR](img/gapped_square.png) | ![Example QR](img/circle.png) | ![Example QR](img/rounded.png) | ![Example QR](img/vertical-bars.png) | ![Example QR](img/horizontal-bars.png) |
**Note** _Even unaltered non square styles will inherently register small deviations from RMSE values and possible minute deviations for correlations when using the [Mask QR Errors](#mask-qr-errors) node. When information is extracted, the `source_qr` values are rounded so that they can only be 0 or 1 (to allow alternate drawers to be used as inputs) and matched against the unrounded aggregation of the `modified_qr` values._
## Future updates
If the solution is clean enough and if it can definitively improve scannability, there may be additional plans for the seperation of alignment patterns (based on `module_size`, `border`, and `QR_VERSION`) for more advanced workflows.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 909 KiB

After

Width:  |  Height:  |  Size: 907 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 KiB

After

Width:  |  Height:  |  Size: 961 KiB

File diff suppressed because it is too large Load Diff
+257 -382
View File
@@ -1,6 +1,6 @@
{
"last_node_id": 51,
"last_link_id": 114,
"last_node_id": 52,
"last_link_id": 117,
"nodes": [
{
"id": 19,
@@ -204,7 +204,7 @@
],
"size": {
"0": 210,
"1": 82
"1": 106
},
"flags": {},
"order": 1,
@@ -212,31 +212,24 @@
"outputs": [
{
"name": "COMBO",
"type": "Http,Https,None",
"type": "COMBO",
"links": [
46
],
"slot_index": 0,
"widget": {
"name": "protocol",
"config": [
[
"Http",
"Https",
"None"
],
{
"default": "Https"
}
]
"name": "protocol"
}
}
],
"title": "Protocol",
"properties": {},
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
"Https",
"fixed"
"fixed",
""
]
},
{
@@ -262,18 +255,14 @@
],
"slot_index": 0,
"widget": {
"name": "text",
"config": [
"STRING",
{
"multiline": true
}
]
"name": "text"
}
}
],
"title": "URL",
"properties": {},
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
"gitlab.com/sofuego-comfy-nodes/comfy-qr-validation-nodes"
]
@@ -290,7 +279,7 @@
"1": 46
},
"flags": {},
"order": 12,
"order": 14,
"mode": 0,
"inputs": [
{
@@ -326,10 +315,10 @@
647.9219000000045,
458.3673000000005
],
"size": [
315,
474.00000762939453
],
"size": {
"0": 315,
"1": 474
},
"flags": {},
"order": 25,
"mode": 0,
@@ -427,7 +416,7 @@
"1": 106
},
"flags": {},
"order": 13,
"order": 12,
"mode": 0,
"inputs": [
{
@@ -435,16 +424,7 @@
"type": "INT",
"link": 103,
"widget": {
"name": "width",
"config": [
"INT",
{
"default": 512,
"min": 1,
"max": 8192,
"step": 1
}
]
"name": "width"
}
},
{
@@ -452,16 +432,7 @@
"type": "INT",
"link": 104,
"widget": {
"name": "height",
"config": [
"INT",
{
"default": 512,
"min": 1,
"max": 8192,
"step": 1
}
]
"name": "height"
}
}
],
@@ -503,20 +474,10 @@
"inputs": [
{
"name": "protocol",
"type": "Http,Https,None",
"type": "COMBO",
"link": 46,
"widget": {
"name": "protocol",
"config": [
[
"Http",
"Https",
"None"
],
{
"default": "Https"
}
]
"name": "protocol"
}
},
{
@@ -524,13 +485,7 @@
"type": "STRING",
"link": 47,
"widget": {
"name": "text",
"config": [
"STRING",
{
"multiline": true
}
]
"name": "text"
}
}
],
@@ -591,7 +546,8 @@
"#000000",
"#FFFFFF",
"High",
1
1,
"Square"
]
},
{
@@ -647,7 +603,7 @@
"1": 166
},
"flags": {},
"order": 20,
"order": 19,
"mode": 0,
"inputs": [
{
@@ -718,7 +674,7 @@
{
"name": "destination",
"type": "MASK",
"link": 91
"link": 115
},
{
"name": "source",
@@ -783,113 +739,6 @@
"Node name for S&R": "MaskToImage"
}
},
{
"id": 3,
"type": "comfy-qr-quantify-match",
"pos": [
97.9219000000007,
-111.63269999999997
],
"size": {
"0": 317.4000244140625,
"1": 238
},
"flags": {},
"order": 19,
"mode": 0,
"inputs": [
{
"name": "source_qr",
"type": "IMAGE",
"link": 4
},
{
"name": "modified_qr",
"type": "IMAGE",
"link": 5
}
],
"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": [
91
],
"shape": 3,
"slot_index": 3
}
],
"properties": {
"Node name for S&R": "comfy-qr-quantify-match"
},
"widgets_values": [
16,
"mean",
"mean",
"module_pattern",
0.5,
false
]
},
{
"id": 2,
"type": "LoadImage",
"pos": [
-288,
115
],
"size": {
"0": 315,
"1": 314.0000305175781
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
5,
87
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"unscannable_00001_ (1).png",
"image"
]
},
{
"id": 47,
"type": "PrimitiveNode",
@@ -902,7 +751,7 @@
"1": 82
},
"flags": {},
"order": 4,
"order": 3,
"mode": 0,
"outputs": [
{
@@ -914,21 +763,14 @@
],
"slot_index": 0,
"widget": {
"name": "height",
"config": [
"INT",
{
"default": 512,
"min": 1,
"max": 8192,
"step": 1
}
]
"name": "height"
}
}
],
"title": "Height",
"properties": {},
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
768,
"fixed"
@@ -946,7 +788,7 @@
"1": 82
},
"flags": {},
"order": 5,
"order": 4,
"mode": 0,
"outputs": [
{
@@ -958,21 +800,14 @@
],
"slot_index": 0,
"widget": {
"name": "width",
"config": [
"INT",
{
"default": 512,
"min": 1,
"max": 8192,
"step": 1
}
]
"name": "width"
}
}
],
"title": "Width",
"properties": {},
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
768,
"fixed"
@@ -990,7 +825,7 @@
"1": 82
},
"flags": {},
"order": 6,
"order": 5,
"mode": 0,
"outputs": [
{
@@ -1002,21 +837,14 @@
],
"slot_index": 0,
"widget": {
"name": "x",
"config": [
"INT",
{
"default": 0,
"min": 0,
"max": 8192,
"step": 1
}
]
"name": "x"
}
}
],
"title": "X Offset",
"properties": {},
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
16,
"fixed"
@@ -1034,7 +862,7 @@
"1": 82
},
"flags": {},
"order": 7,
"order": 6,
"mode": 0,
"outputs": [
{
@@ -1046,21 +874,14 @@
],
"slot_index": 0,
"widget": {
"name": "y",
"config": [
"INT",
{
"default": 0,
"min": 0,
"max": 8192,
"step": 1
}
]
"name": "y"
}
}
],
"title": "Y Offset",
"properties": {},
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
16,
"fixed"
@@ -1078,7 +899,7 @@
"1": 106
},
"flags": {},
"order": 14,
"order": 13,
"mode": 0,
"inputs": [
{
@@ -1086,16 +907,7 @@
"type": "INT",
"link": 105,
"widget": {
"name": "width",
"config": [
"INT",
{
"default": 512,
"min": 1,
"max": 8192,
"step": 1
}
]
"name": "width"
}
},
{
@@ -1103,16 +915,7 @@
"type": "INT",
"link": 106,
"widget": {
"name": "height",
"config": [
"INT",
{
"default": 512,
"min": 1,
"max": 8192,
"step": 1
}
]
"name": "height"
}
}
],
@@ -1167,16 +970,7 @@
"type": "INT",
"link": 109,
"widget": {
"name": "x",
"config": [
"INT",
{
"default": 0,
"min": 0,
"max": 8192,
"step": 1
}
]
"name": "x"
}
},
{
@@ -1184,16 +978,7 @@
"type": "INT",
"link": 110,
"widget": {
"name": "y",
"config": [
"INT",
{
"default": 0,
"min": 0,
"max": 8192,
"step": 1
}
]
"name": "y"
}
}
],
@@ -1283,93 +1068,6 @@
"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": 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",
@@ -1492,7 +1190,7 @@
"1": 58
},
"flags": {},
"order": 8,
"order": 7,
"mode": 0,
"outputs": [
{
@@ -1537,6 +1235,183 @@
"widgets_values": [
"ComfyUI"
]
},
{
"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": null
},
{
"name": "x",
"type": "INT",
"link": 107,
"widget": {
"name": "x"
}
},
{
"name": "y",
"type": "INT",
"link": 108,
"widget": {
"name": "y"
}
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
12,
111,
116
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ImageCompositeMasked"
},
"widgets_values": [
16,
16,
false
]
},
{
"id": 2,
"type": "LoadImage",
"pos": [
-288,
115
],
"size": {
"0": 315,
"1": 314.0000305175781
},
"flags": {},
"order": 8,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
87,
117
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"unscannable_00001_ (1).png",
"image"
]
},
{
"id": 52,
"type": "comfy-qr-mask_errors",
"pos": [
120,
-146
],
"size": {
"0": 317.4000244140625,
"1": 262
},
"flags": {},
"order": 20,
"mode": 0,
"inputs": [
{
"name": "source_qr",
"type": "IMAGE",
"link": 116
},
{
"name": "modified_qr",
"type": "IMAGE",
"link": 117
}
],
"outputs": [
{
"name": "QR_ERROR_MASK",
"type": "MASK",
"links": [
115
],
"shape": 3,
"slot_index": 0
},
{
"name": "PERCENT_ERROR",
"type": "FLOAT",
"links": null,
"shape": 6
},
{
"name": "CORRELATION",
"type": "FLOAT",
"links": null,
"shape": 6
},
{
"name": "RMSE",
"type": "FLOAT",
"links": null,
"shape": 6
}
],
"properties": {
"Node name for S&R": "comfy-qr-mask_errors"
},
"widgets_values": [
16,
"mean",
"mean",
"module_pattern",
0.5,
false,
2.2
]
}
],
"links": [
@@ -1564,22 +1439,6 @@
0,
"IMAGE"
],
[
4,
6,
0,
3,
0,
"IMAGE"
],
[
5,
2,
0,
3,
1,
"IMAGE"
],
[
7,
8,
@@ -1714,7 +1573,7 @@
0,
1,
0,
"Http,Https,None"
"COMBO"
],
[
47,
@@ -1748,14 +1607,6 @@
1,
"MASK"
],
[
91,
3,
3,
39,
0,
"MASK"
],
[
92,
39,
@@ -1883,6 +1734,30 @@
23,
0,
"IMAGE"
],
[
115,
52,
0,
39,
0,
"MASK"
],
[
116,
6,
0,
52,
0,
"IMAGE"
],
[
117,
2,
0,
52,
1,
"IMAGE"
]
],
"groups": [],
+3 -2
View File
@@ -342,7 +342,8 @@
"#DDDDDD",
"High",
1,
"Nearest"
"Nearest",
"Square"
]
}
],
@@ -440,4 +441,4 @@
"config": {},
"extra": {},
"version": 0.4
}
}
+2 -1
View File
@@ -477,7 +477,8 @@
"#000000",
"#FFFFFF",
"Medium",
1
1,
"Square"
]
}
],
@@ -524,7 +524,8 @@
"#000000",
"#FFFFFF",
"High",
1
1,
"Square"
]
},
{
+2 -1
View File
@@ -394,7 +394,8 @@
"#000000",
"#FFFFFF",
"High",
1
1,
"Square"
]
},
{
+3 -2
View File
@@ -357,7 +357,8 @@
"#000000",
"#FFFFFF",
"High",
1
1,
"Square"
]
}
],
@@ -423,4 +424,4 @@
"config": {},
"extra": {},
"version": 0.4
}
}
+3 -2
View File
@@ -70,7 +70,8 @@
"FF0",
"High",
1,
"Nearest"
"Nearest",
"Circle"
]
}
],
@@ -88,4 +89,4 @@
"config": {},
"extra": {},
"version": 0.4
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 59 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+16
View File
@@ -0,0 +1,16 @@
[project]
name = "comfyqr"
description = "QR generation within ComfyUI. Contains nodes suitable for workflows from generating basic QR images to techniques with advanced QR masking."
version = "0.4.1"
license = "LICENSE"
dependencies = ["qrcode >= 7.1"]
[project.urls]
Repository = "https://github.com/coreyryanhanson/ComfyQR"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "coreyryanhanson"
DisplayName = "ComfyQR"
Icon = ""
+240 -81
View File
@@ -1,6 +1,13 @@
import numpy as np
import qrcode
from qrcode.compat.pil import Image
from qrcode.image.styles.moduledrawers import (GappedSquareModuleDrawer,
CircleModuleDrawer,
RoundedModuleDrawer,
VerticalBarsDrawer,
HorizontalBarsDrawer)
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.colormasks import SolidFillColorMask
from PIL import Image
import torch
import torch.nn.functional as F
@@ -27,11 +34,19 @@ class QRBase:
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):
def _make_qr(self, qr, fill_hexcolor, back_hexcolor, module_drawer):
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)
if module_drawer == "Square":
# Keeps using Square QR generation the old way for faster speeds.
return qr.make_image(fill_color=self.fill, back_color=self.back)
color_mask = SolidFillColorMask(back_color=self.back,
front_color=self.fill)
module_drawing_method = self._select_module_drawer(module_drawer)
return qr.make_image(image_factory=StyledPilImage,
color_mask=color_mask,
module_drawer=module_drawing_method)
def _parse_hexcolor_string(self, s, parameter):
if s.startswith("#"):
@@ -45,11 +60,31 @@ class QRBase:
try:
return tuple(int(channel, 16) for channel in rgb)
except ValueError:
raise ValueError(f"{parameter} contains invalid hexadecimal characters")
raise ValueError(f"{parameter} contains invalid hexadecimal "
f"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}.")
raise RuntimeError(f"QR dimensions of {size} exceed max size of "
f"{max_size}.")
def _select_module_drawer(self, module_drawer_string):
"""Square is not included in the results, for a speed optimization
applying color masks. Current version of python-qr code suffers a
slowdown when using custom colors combined with custom module drawers.
By bypassing square QRs, non standard colors will load faster."""
if module_drawer_string == "Gapped square":
return GappedSquareModuleDrawer()
if module_drawer_string == "Circle":
return CircleModuleDrawer()
if module_drawer_string == "Rounded":
return RoundedModuleDrawer()
if module_drawer_string == "Vertical bars":
return VerticalBarsDrawer()
if module_drawer_string == "Horizontal bars":
return HorizontalBarsDrawer()
raise ValueError(f"Module drawing method of {module_drawer_string} "
f"not supported")
def update_text(self, protocol, text):
"""This function takes input from a text box and a chosen internet
@@ -79,12 +114,34 @@ class QRByImageSize(QRBase):
"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"}),
"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"}),
"module_drawer": (["Square",
"Gapped square",
"Circle",
"Rounded",
"Vertical bars",
"Horizontal bars"
], {"default": "Square"})
},
}
@@ -104,7 +161,8 @@ class QRByImageSize(QRBase):
return Image.BOX
if resampling_string == "Hamming":
return Image.HAMMING
raise ValueError(f"Resampling method of {resampling_string} not supported")
raise ValueError(f"Resampling method of {resampling_string} not "
f"supported")
def generate_qr(
self,
@@ -115,18 +173,18 @@ class QRByImageSize(QRBase):
back_hexcolor,
error_correction,
border,
resampling
resampling,
module_drawer
):
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,
box_size=16,
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 = self._make_qr(qr, fill_hexcolor, back_hexcolor, module_drawer)
img = img.resize((image_size, image_size), resample=resampling_method)
return (self._img_to_tensor(img), qr.version)
@@ -139,12 +197,31 @@ class QRByModuleSize(QRBase):
"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}),
"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}),
"module_drawer": (["Square",
"Gapped square",
"Circle",
"Rounded",
"Vertical bars",
"Horizontal bars"
], {"default": "Square"})
},
}
@@ -160,7 +237,8 @@ class QRByModuleSize(QRBase):
fill_hexcolor,
back_hexcolor,
error_correction,
border
border,
module_drawer
):
self.update_text(protocol, text)
error_level = self._get_error_correction_constant(error_correction)
@@ -169,7 +247,7 @@ class QRByModuleSize(QRBase):
box_size=module_size,
border=border)
qr.add_data(self.text)
img = self._make_qr(qr, fill_hexcolor, back_hexcolor)
img = self._make_qr(qr, fill_hexcolor, back_hexcolor, module_drawer)
self._validate_qr_size(img.pixel_size, max_image_size)
return (self._img_to_tensor(img), qr.version, img.pixel_size)
@@ -182,17 +260,41 @@ class QRByModuleSizeSplitFunctionPatterns(QRBase):
"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}),
"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}),
"module_drawer": (["Square",
"Gapped square",
"Circle",
"Rounded",
"Vertical bars",
"Horizontal bars"
], {"default": "Square"})
},
}
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "MASK", "INT", "INT")
RETURN_NAMES = ("QR_CODE", "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
@@ -205,9 +307,16 @@ class QRByModuleSizeSplitFunctionPatterns(QRBase):
(-inner, far_outer, outer, inner)
]
def _generate_finder_pattern_mask(self, pixel_size, module_size, border_size):
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):
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
@@ -219,7 +328,7 @@ class QRByModuleSizeSplitFunctionPatterns(QRBase):
def _mask_to_tensor(self, mask):
out_image = mask.astype(np.float32)
return torch.from_numpy(out_image)
return torch.from_numpy(out_image).unsqueeze(0)
def generate_qr(
self,
@@ -230,7 +339,8 @@ class QRByModuleSizeSplitFunctionPatterns(QRBase):
fill_hexcolor,
back_hexcolor,
error_correction,
border
border,
module_drawer
):
self.update_text(protocol, text)
error_level = self._get_error_correction_constant(error_correction)
@@ -239,10 +349,12 @@ class QRByModuleSizeSplitFunctionPatterns(QRBase):
box_size=module_size,
border=border)
qr.add_data(self.text)
img = self._make_qr(qr, fill_hexcolor, back_hexcolor)
img = self._make_qr(qr, fill_hexcolor, back_hexcolor, module_drawer)
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)
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 (
@@ -265,6 +377,7 @@ class QRErrorMasker:
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):
@@ -272,13 +385,24 @@ class QRErrorMasker:
"required": {
"source_qr": ("IMAGE",),
"modified_qr": ("IMAGE",),
"module_size": ("INT", {"default": 16, "min": 1, "max": 64, "step": 1}),
"grayscale_method": (["mean", "luminance"], {"default": "luminance"}),
"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}),
"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}),
"gamma": ("FLOAT", {"default": 2.2,
"min": .1,
"max": 2.8,
"step": .1}),
},
}
@@ -288,10 +412,13 @@ class QRErrorMasker:
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
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]]
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)
@@ -302,20 +429,35 @@ class QRErrorMasker:
def _reshape_tensor_to_modules(self, tensor):
if len(tensor.shape) != 2:
raise RuntimeError("Module reshaping requires a 2 dimensional array.")
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)
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."
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}")
raise RuntimeError(f"Source QR dimensions are {width} x {height}. "
f"They must be a perfect square. "
f"{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}")
raise RuntimeError(f"QR width of {width} does not fit module_size "
f"of {self.module_size}. It must be perfectly "
f"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)
@@ -324,14 +466,18 @@ class QRErrorMasker:
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 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 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):
@@ -346,7 +492,8 @@ class QRErrorMasker:
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.")
raise RuntimeError("Module aggregation currently only supports the "
"mean.")
def _reduce_to_modules(
self,
@@ -358,12 +505,8 @@ class QRErrorMasker:
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]
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,
@@ -375,8 +518,9 @@ class QRErrorMasker:
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")
raise ValueError("Currently only mean is supported for rgb to "
"grayscale conversion.")
source_qr = torch.round(self._squeeze_to_modules(source_qr, "mean"))
modified_qr = self._squeeze_to_modules(modified_qr, aggregate_method)
return source_qr, modified_qr
@@ -411,7 +555,9 @@ class QRErrorMasker:
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')
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
@@ -423,12 +569,14 @@ class QRErrorMasker:
mask,
error_difficulty
):
modified_qr = self._bin_tensor_to_threshold(modified_qr, 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
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))
@@ -451,36 +599,47 @@ class QRErrorMasker:
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)
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-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-image-size": "QR Code (Conformed "
"to Image Size)",
"comfy-qr-by-module-split": "QR Code (Split)",
"comfy-qr-mask_errors": "Mask QR Errors",
}
+2 -1
View File
@@ -1 +1,2 @@
qrcode >= 7.1
qrcode >= 7.1
pillow >= 9.0