Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e31449c99a | ||
|
|
ad78464341 | ||
|
|
ae639c69c1 | ||
|
|
959dcfa062 | ||
|
|
aa35c8feb0 | ||
|
|
6b1b83b4a5 | ||
|
|
3b16771cf7 | ||
|
|
29cd12358e | ||
|
|
5818bdf812 | ||
|
|
7cb0d7de12 | ||
|
|
7f60e01b9a | ||
|
|
f70b527f7c | ||
|
|
4f45656976 | ||
|
|
310f576108 | ||
|
|
849bba41ad | ||
|
|
cb7726221e | ||
|
|
e91f331652 | ||
|
|
2de40a517e | ||
|
|
fc2b20ad38 | ||
|
|
ab262513df | ||
|
|
9e8434a7a8 | ||
|
|
e804976c75 | ||
|
|
f8023feac8 | ||
|
|
3d0c12bdff | ||
|
|
fd35bb8e62 | ||
|
|
c92488f903 | ||
|
|
c614f70082 |
@@ -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 }}
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
This project currently contains two nodes for generating QR codes.
|
||||
|
||||
### QR Code
|
||||
### QR Code and QR Code (Split)
|
||||
|
||||
A dynamically sized QR code without any resampling scaled by the text length, the module size, and the error correction level.
|
||||
A dynamically sized QR code without any resampling scaled by the text length, the module size, and the error correction level. Use the `QR Code `for simple workflows and the `QR Code (Split)` if you want to build more advanced pipelines with additional outputs for the `MODULE_LAYER`, `FINDER_LAYER`, or `FINDER_MASK`.
|
||||
|
||||

|
||||
|
||||
#### Inputs
|
||||
|
||||
@@ -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)
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
|
||||

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

|
||||
|
||||
Any workflow in the example that ends with "validated" (and a few image examples) assume the installation of the [scanning pack](https://gitlab.com/sofuego-comfy-nodes/ComfyQR-scanning-nodes) as well.
|
||||
|
||||
## 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 |
|
||||
|:---: | :---: | :---: | :---: | :---: | :---: |
|
||||
|  |  |  |  |  |  |
|
||||
|
||||
**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.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .qr_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 924 KiB |
|
After Width: | Height: | Size: 907 KiB |
|
After Width: | Height: | Size: 961 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_node_id": 13,
|
||||
"last_link_id": 13,
|
||||
"last_node_id": 14,
|
||||
"last_link_id": 14,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 7,
|
||||
@@ -59,7 +59,7 @@
|
||||
{
|
||||
"name": "pixels",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
"link": 14
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
@@ -240,51 +240,6 @@
|
||||
"1.5/v1-5-pruned-emaonly.safetensors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "comfy-qr-by-image-size",
|
||||
"pos": [
|
||||
202,
|
||||
581
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 264
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
4
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "QR_VERSION",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "comfy-qr-by-image-size"
|
||||
},
|
||||
"widgets_values": [
|
||||
"Https",
|
||||
"gitlab.com/sofuego-comfy-nodes/comfy-qr",
|
||||
512,
|
||||
"#222222",
|
||||
"#DDDDDD",
|
||||
"High",
|
||||
1,
|
||||
"Nearest"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "KSampler",
|
||||
@@ -336,7 +291,7 @@
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
321184522216123,
|
||||
1077540641933383,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
@@ -344,6 +299,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -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 = ""
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
qrcode >= 7.1
|
||||
qrcode >= 7.1
|
||||
pillow >= 9.0
|
||||
|
||||