27 Commits
Author SHA1 Message Date
Corey Hanson e31449c99a update pyproject.toml 2025-01-26 08:53:29 -06:00
Corey Hanson ad78464341 fix qrcode pil deprecation bug 2025-01-26 08:44:18 -06:00
Corey Hanson ae639c69c1 restore workflow to to require change to pyproject.toml 2024-07-05 00:11:16 -04:00
Corey 959dcfa062 Trigger Registry Build 2024-07-05 00:09:22 -04:00
Corey Hanson aa35c8feb0 main repo overwritten the github action ensuring change is on both 2024-07-05 00:08:22 -04:00
Corey Hanson 6b1b83b4a5 changed license in pyproject.toml to string 2024-07-04 23:55:37 -04:00
Corey Hanson 3b16771cf7 added pyproject.toml 2024-07-04 23:06:19 -04:00
Corey Hanson 29cd12358e fix typo 2024-03-17 22:12:38 -04:00
Corey Hanson 5818bdf812 shorten line lengths 2024-03-17 22:08:59 -04:00
Corey Ryan Hanson 7cb0d7de12 Merge branch 'complete/add-module-drawer' into 'main'
Add custom module drawers

See merge request sofuego-comfy-nodes/ComfyQR!3
2024-03-18 00:43:54 +00:00
Corey Ryan Hanson 7f60e01b9a Add custom module drawers 2024-03-18 00:43:54 +00:00
Corey Hanson f70b527f7c fix broken links in example png metadata 2024-03-09 13:48:32 -05:00
Corey Hanson 4f45656976 support batch operations on qr error mask 2023-10-14 22:24:40 -04:00
Corey Hanson 310f576108 fix for bug that occurs when masking torch tensors with numpy array 2023-09-14 15:00:11 -04:00
Corey Hanson 849bba41ad rename old pictures. Section off bulky upscales for easier pruning later if repo requires it 2023-08-24 21:34:27 -04:00
Corey Hanson cb7726221e update supplemental materials 2023-08-24 20:51:04 -04:00
Corey Hanson e91f331652 added luminance based grayscale conversion 2023-08-24 00:07:01 -04:00
Corey Ryan Hanson 2de40a517e Missed one name in readme 2023-08-22 03:16:42 +00:00
Corey Ryan Hanson fc2b20ad38 update links and group name to reflect name format 2023-08-22 03:15:11 +00:00
Corey Ryan Hanson ab262513df Still early enough to improve project name without being too disruptive. To whomever has managed to find this plugin sorry for breaking the link this one time. 2023-08-22 02:57:26 +00:00
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
Corey Hanson f8023feac8 fix array selection bug which misses 2 finder patterns when border size is 0 2023-08-14 10:26:08 -04:00
Corey Ryan Hanson 3d0c12bdff Merge branch 'dev-split-function-patterns' into 'main'
Dev split function patterns

See merge request sofuego-comfy-nodes/comfy-qr!1
2023-08-14 02:36:20 +00:00
Corey Ryan Hanson fd35bb8e62 Ability to seperate function patterns 2023-08-14 02:36:20 +00:00
Corey Hanson c92488f903 update README
update README
2023-08-13 22:15:29 -04:00
Corey Hanson c614f70082 Forgot to push init file 2023-08-05 17:05:03 -04:00
31 changed files with 10709 additions and 353 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 }}
+79 -7
View File
@@ -1,16 +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.
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.
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/ComfyQR), but also mirrored on [Github](https://github.com/coreyryanhanson/ComfyQR). Please submit any [issues](https://gitlab.com/sofuego-comfy-nodes/ComfyQR/-/issues) or [pull requests](https://gitlab.com/sofuego-comfy-nodes/ComfyQR/-/merge_requests) to the gitlab repo.
![QR code with repo link](example_generations/main_qr_inpainted.png)
## 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`.
![Split QR Code Node Example](img/node-qr-code-split.png)
#### Inputs
@@ -29,15 +35,21 @@ 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
* `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
@@ -63,12 +75,72 @@ 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
* `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.
### Mask QR Errors
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
* `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_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 [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, I plan on implementing a node that will automatically generate a mask to extract the 3 QR markers (based on `module_size`, `border`, and `QR_VERSION`) for more advanced workflows.
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.
+3
View File
@@ -0,0 +1,3 @@
from .qr_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 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
+59 -58
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,52 @@
"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",
"Square"
]
}
],
"links": [
@@ -363,14 +364,6 @@
0,
"CLIP"
],
[
4,
4,
0,
9,
0,
"IMAGE"
],
[
5,
6,
@@ -434,10 +427,18 @@
11,
2,
"CONDITIONING"
],
[
14,
14,
0,
9,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
}
+92 -91
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,
@@ -127,10 +127,10 @@
678,
604
],
"size": [
315,
106
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 5,
"mode": 0,
@@ -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": [
679629273700059,
123662286732457,
"randomize",
20,
8,
@@ -321,61 +321,6 @@
1
]
},
{
"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",
"#999999",
"Medium",
1
]
},
{
"id": 8,
"type": "CLIPTextEncode",
@@ -419,7 +364,7 @@
"id": 17,
"type": "ControlNetApplyAdvanced",
"pos": [
808,
804,
796
],
"size": {
@@ -448,7 +393,7 @@
{
"name": "image",
"type": "IMAGE",
"link": 20
"link": 25
}
],
"outputs": [
@@ -475,10 +420,66 @@
"Node name for S&R": "ControlNetApplyAdvanced"
},
"widgets_values": [
2.49999999999999,
1.6,
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,
"Square"
]
}
],
"links": [
@@ -530,22 +531,6 @@
0,
"IMAGE"
],
[
14,
14,
2,
15,
0,
"INT"
],
[
15,
14,
2,
15,
1,
"INT"
],
[
16,
15,
@@ -570,14 +555,6 @@
2,
"CONTROL_NET"
],
[
20,
14,
0,
17,
3,
"IMAGE"
],
[
22,
17,
@@ -601,6 +578,30 @@
11,
1,
"CONDITIONING"
],
[
25,
19,
0,
17,
3,
"IMAGE"
],
[
26,
19,
2,
15,
0,
"INT"
],
[
27,
19,
2,
15,
1,
"INT"
]
],
"groups": [],
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+427
View File
@@ -0,0 +1,427 @@
{
"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,
"Square"
]
}
],
"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
}
+14 -13
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,19 +65,20 @@
"widgets_values": [
"Https",
"gitlab.com/sofuego-comfy-nodes/comfy-qr",
768,
512,
"00F",
"FF0",
"High",
1,
"Nearest"
"Nearest",
"Circle"
]
}
],
"links": [
[
1,
1,
2,
3,
0,
2,
0,
@@ -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: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

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.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

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 = ""
+645 -183
View File
@@ -1,183 +1,645 @@
import numpy as np
import qrcode
from qrcode.compat.pil import Image
import torch
class QRBase:
def __init__(self):
self.text = ""
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):
fill_color = self._parse_hexcolor_string(fill_hexcolor, "fill_hexcolor")
back_color = self._parse_hexcolor_string(back_hexcolor, "back_hexcolor")
qr.make(fit=True)
return qr.make_image(fill_color=fill_color, back_color=back_color)
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)
NODE_CLASS_MAPPINGS = {
"comfy-qr-by-module-size": QRByModuleSize,
"comfy-qr-by-image-size": QRByImageSize,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"comfy-qr-by-module-size": "QR Code",
"comfy-qr-by-image-size": "QR Code (Conformed to Image Size)",
}
import numpy as np
import qrcode
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
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, 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)
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("#"):
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 "
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 "
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
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"}),
"module_drawer": (["Square",
"Gapped square",
"Circle",
"Rounded",
"Vertical bars",
"Horizontal bars"
], {"default": "Square"})
},
}
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 "
f"supported")
def generate_qr(
self,
protocol,
text,
image_size,
fill_hexcolor,
back_hexcolor,
error_correction,
border,
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=16,
border=border)
qr.add_data(self.text)
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)
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}),
"module_drawer": (["Square",
"Gapped square",
"Circle",
"Rounded",
"Vertical bars",
"Horizontal bars"
], {"default": "Square"})
},
}
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,
module_drawer
):
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, module_drawer)
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}),
"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")
def _generate_finder_pattern_ranges(self, module_size, border_size):
outer = module_size * border_size
inner = 7 * module_size + outer
# Alternate behavior is required to prevent bugs from 0 border_size.
far_outer = -outer if border_size else None
return [
(outer, inner, outer, inner),
(outer, inner, -inner, far_outer),
(-inner, far_outer, outer, inner)
]
def _generate_finder_pattern_mask(self,
pixel_size,
module_size,
border_size):
mask = np.zeros((pixel_size, pixel_size), dtype=bool)
for (x_min,
x_max,
y_min,
y_max) in self._generate_finder_pattern_ranges(module_size,
border_size):
mask[y_min:y_max, x_min:x_max] = True
return mask
def _apply_fill_to_mask(self, img, mask):
array = np.array(img).copy()
indices = np.nonzero(mask)
array[indices[0], indices[1], :] = self.back
return Image.fromarray(array)
def _mask_to_tensor(self, mask):
out_image = mask.astype(np.float32)
return torch.from_numpy(out_image).unsqueeze(0)
def generate_qr(
self,
protocol,
text,
module_size,
max_image_size,
fill_hexcolor,
back_hexcolor,
error_correction,
border,
module_drawer
):
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, 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)
module_image = self._apply_fill_to_mask(img, mask)
function_image = self._apply_fill_to_mask(img, ~mask)
return (
self._img_to_tensor(img),
self._img_to_tensor(module_image),
self._img_to_tensor(function_image),
self._mask_to_tensor(mask),
qr.version,
pixel_size,
)
class QRErrorMasker:
def __init__(self):
self.module_size = None
self.canvas_shape = None
self.qr_bounds = None
FUNCTION = "find_qr_errors"
CATEGORY = "ComfyQR"
RETURN_TYPES = ("MASK", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("QR_ERROR_MASK", "PERCENT_ERROR", "CORRELATION", "RMSE")
OUTPUT_IS_LIST = (False, True, True, True)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"source_qr": ("IMAGE",),
"modified_qr": ("IMAGE",),
"module_size": ("INT", {"default": 16,
"min": 1,
"max": 64,
"step": 1}),
"grayscale_method": (["mean", "luminance"],
{"default": "luminance"}),
"aggregate_method": (["mean",], {"default": "mean"}),
"evaluate": (["full_qr", "module_pattern", "finder_pattern"],
{"default": "module_pattern"}),
"error_difficulty": ("FLOAT", {"default": 0,
"min": 0,
"max": 1,
"step": .01}),
"inverted_pattern": ("BOOLEAN", {"default": False}),
"gamma": ("FLOAT", {"default": 2.2,
"min": .1,
"max": 2.8,
"step": .1}),
},
}
def _get_qr_bounds(self, tensor, invert):
module_color = 1.0 if invert else 0.0
module_pixels = (tensor == module_color)
indices = torch.nonzero(module_pixels, as_tuple=True)
# The viewer patterns will guarentee a module pixel in the upper left
# The bottom right does not have that guarentee so max is used.
return (indices[0][0],
indices[0].max() + 1,
indices[1][0], indices[1].max() + 1)
def _extract_pattern_from_bounds(self, tensor):
return tensor[self.qr_bounds[0]: self.qr_bounds[1],
self.qr_bounds[2]: self.qr_bounds[3]]
def _trim_to_qr_area(self, source_qr, modified_qr, inverted_pattern):
self.qr_bounds = self._get_qr_bounds(source_qr, inverted_pattern)
self._check_bounds_and_module_size()
source_qr = self._extract_pattern_from_bounds(source_qr)
modified_qr = self._extract_pattern_from_bounds(modified_qr)
return source_qr, modified_qr
def _reshape_tensor_to_modules(self, tensor):
if len(tensor.shape) != 2:
raise RuntimeError("Module reshaping requires a 2 dimensional "
"array.")
length = tensor.shape[0] // self.module_size
reshaped_tensor = tensor.view(length,
self.module_size,
length,
self.module_size)
rehaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
return rehaped_tensor.view(length, length, self.module_size ** 2)
def _check_bounds_and_module_size(self):
height = self.qr_bounds[1] - self.qr_bounds[0]
width = self.qr_bounds[3] - self.qr_bounds[2]
color_warning = "Make sure that qr_fill and back colors have exact "
"#FFFFFFF and #000000 values (and that module color values do not "
"occur outside the QR) and invert is set correctly."
if width != height:
raise RuntimeError(f"Source QR dimensions are {width} x {height}. "
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 "
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)
def _gamma_expansion(self, tensor, gamma):
if gamma == 1:
return tensor
if gamma == 2.2:
return torch.where(tensor <= 0.04045,
tensor / 12.92,
(tensor + 0.055) / 1.055) ** 2.4
return tensor ** gamma
def _gamma_compression(self, tensor, gamma):
if gamma == 1:
return tensor
if gamma == 2.2:
return torch.where(tensor <= .0031308,
tensor * 12.92,
1.055 * tensor ** (1/2.4) - 0.055)
return tensor ** (1/gamma)
def _grayscale_by_luminance(self, tensor, gamma):
weights = torch.tensor([0.2125, 0.7154, 0.0721], dtype=torch.float32)
tensor = self._gamma_expansion(tensor, gamma)
tensor = tensor @ weights
if gamma != 1:
tensor = tensor ** gamma
return self._gamma_compression(tensor, gamma)
def _squeeze_to_modules(self, tensor, method):
tensor = self._reshape_tensor_to_modules(tensor)
if method == "mean":
return self._squeeze_by_mean(tensor)
raise RuntimeError("Module aggregation currently only supports the "
"mean.")
def _reduce_to_modules(
self,
source_qr,
modified_qr,
module_size,
grayscale_method,
aggregate_method,
inverted_pattern,
gamma
):
self.module_size = module_size
self.canvas_shape = (source_qr.shape[0], source_qr.shape[1])
# Processed first for simplified indexing of QR bounds.
source_qr = self._squeeze_by_mean(source_qr)
source_qr, modified_qr = self._trim_to_qr_area(source_qr,
modified_qr,
inverted_pattern
)
if grayscale_method == "mean":
modified_qr = self._squeeze_by_mean(modified_qr)
elif grayscale_method == "luminance":
modified_qr = self._grayscale_by_luminance(modified_qr, gamma)
else:
raise ValueError("Currently only mean is supported for rgb to "
"grayscale conversion.")
source_qr = torch.round(self._squeeze_to_modules(source_qr, "mean"))
modified_qr = self._squeeze_to_modules(modified_qr, aggregate_method)
return source_qr, modified_qr
def _create_finder_pattern_mask(self, width, inverted):
mask = torch.zeros((width, width), dtype=torch.bool)
# When borders are trimmed and QR code has module size of 1, results
# are consistent.
finder_coords = [[0, 7, 0, 7], [0, 7, -7, None], [-7, None, 0, 7]]
for x_min, x_max, y_min, y_max in finder_coords:
mask[y_min:y_max, x_min:x_max] = True
return ~mask if inverted else mask
def _create_qr_mask(self, tensor, evaluate):
if evaluate == "module_pattern":
return self._create_finder_pattern_mask(tensor, True)
if evaluate == "finder_pattern":
return self._create_finder_pattern_mask(tensor, False)
return None
def _bin_tensor_to_threshold(self, tensor, contrast_difficulty):
tensor = tensor.clone()
threshold = contrast_difficulty / 2
# Since we are only interested in value matches and there is a clear
# stable dividing line of .5, bringing in the other array is
# unneccessary and the binning process can be simplified.
bin_condition = (tensor + threshold <= .5) & (tensor != .5)
tensor[bin_condition] = 0.0
bin_condition = (tensor - threshold >= .5) & (tensor != .5)
tensor[bin_condition] = 1.0
return tensor
def _replace_qr_to_canvas(self, tensor):
length = tensor.shape[0] * self.module_size
bounds = self.qr_bounds
tensor = F.interpolate(tensor.unsqueeze(0).unsqueeze(0),
size=(length, length),
mode='nearest')
canvas = torch.zeros(self.canvas_shape, dtype=torch.float32)
canvas[bounds[0]:bounds[1], bounds[2]:bounds[3]] = tensor.squeeze()
return canvas
def _compare_modules(
self,
source_qr,
modified_qr,
mask,
error_difficulty
):
modified_qr = self._bin_tensor_to_threshold(modified_qr,
error_difficulty)
error = source_qr != modified_qr
percent_error = error[mask].sum().item() / error[mask].numel()
if mask is not None:
error[~mask] = False
return (self._replace_qr_to_canvas((error).to(torch.float32)),
percent_error)
def _qr_correlation(self, source_qr, modified_qr, mask):
source_qr = source_qr[mask].numpy().reshape((-1))
modified_qr = modified_qr[mask].numpy().reshape((-1))
return np.corrcoef(source_qr, modified_qr)[0, 1]
def _qr_rmse(self, source_qr, modified_qr, mask):
diff = source_qr[mask].numpy() - modified_qr[mask].numpy()
return np.sqrt((diff ** 2).mean())
def find_qr_errors(
self,
source_qr,
modified_qr,
module_size,
grayscale_method,
aggregate_method,
evaluate,
error_difficulty,
inverted_pattern,
gamma,
):
self._check_equal_shape(source_qr, modified_qr)
error_masks, error_percents, correlations, rmses = [], [], [], []
for i in range(source_qr.shape[0]):
qr_s, qr_m = source_qr[i], modified_qr[i]
qr_s, qr_m = self._reduce_to_modules(qr_s,
qr_m,
module_size,
grayscale_method,
aggregate_method,
inverted_pattern,
gamma
)
mask = self._create_qr_mask(qr_s.shape[0], evaluate)
error_mask, percent_error = self._compare_modules(qr_s,
qr_m,
mask,
error_difficulty
)
correlation = self._qr_correlation(qr_s, qr_m, mask)
rmse = self._qr_rmse(qr_s, qr_m, mask)
error_masks.append(error_mask)
error_percents.append(percent_error)
correlations.append(correlation)
rmses.append(rmse)
error_masks = torch.stack(error_masks, dim=0)
return (error_masks, error_percents, correlations, rmses)
NODE_CLASS_MAPPINGS = {
"comfy-qr-by-module-size": QRByModuleSize,
"comfy-qr-by-image-size": QRByImageSize,
"comfy-qr-by-module-split":
QRByModuleSizeSplitFunctionPatterns,
"comfy-qr-mask_errors": QRErrorMasker,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"comfy-qr-by-module-size": "QR Code",
"comfy-qr-by-image-size": "QR Code (Conformed "
"to Image Size)",
"comfy-qr-by-module-split": "QR Code (Split)",
"comfy-qr-mask_errors": "Mask QR Errors",
}
+2 -1
View File
@@ -1 +1,2 @@
qrcode >= 7.1
qrcode >= 7.1
pillow >= 9.0