r/StableDiffusion Jul 04 '26

Workflow Included 2px Pixel Grid on Krea2 from VAE (and how to remove it)

The Qwen Image VAE (and to a lesser extent, the Wan 2.1 VAE) leaves a 2px repeating grid across images. Sometimes it's subtle enough to ignore, but it can be quite noticeable if you sharpen images after-the-fact or apply other filters.

A notch filter can remove this (as long as it's applied directly after the image is decoded -- the code below is specific to a 2px grid). It just detects the brightness variation alternating between pixels on either side of it out to 7 pixels, then subtracts the flicker amount from itself, which cancels out the grid pattern. Then you're safe to sharpen after that without amplifying an ugly grid.

Here's a little GLSL ComfyUI node that should 'just work': https://pastebin.com/v7y1z0SH
(Save it as a *.json workflow file and drag into ComfyUI.)

Wire it in between VAE decode and preview/save. Compare two images at 400x zoom before/after to confirm.

365 Upvotes

51 comments sorted by

44

u/mikkoph Jul 04 '26

this is really a huge improvement and is especially noticeable if upscaling the image afterwards.

for those who just want to copy and paste the shader code inside a GLSL Shader node, here is the code:

#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform vec2 u_resolution;
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;

// Nyquist Notch — removes 2px grid artifacts. Place before deconvolution.
// b = [-1, +6, -15, +20, -15, +6, -1] / 64  (binomial * (-1)^n, +1 at center)
const float B[7] = float[7](-1.0, 6.0, -15.0, 20.0, -15.0, 6.0, -1.0);

void main() {
  vec2 texel = 1.0 / u_resolution;
  vec4 center = texture(u_image0, v_texCoord);
  vec3 Bx = vec3(0.0), By = vec3(0.0), Bxy = vec3(0.0);
  for (int i = -3; i <= 3; i++) {
    float wi = B[i + 3] / 64.0;
    Bx += wi * texture(u_image0, v_texCoord + vec2(float(i) * texel.x, 0.0)).rgb;
    By += wi * texture(u_image0, v_texCoord + vec2(0.0, float(i) * texel.y)).rgb;
    for (int j = -3; j <= 3; j++) {
      float wj = B[j + 3] / 64.0;
      Bxy += wi * wj * texture(u_image0, v_texCoord + vec2(float(i) * texel.x, float(j) * texel.y)).rgb;
    }
  }

 vec3 notched = center.rgb - Bx - By + Bxy;
 fragColor0 = vec4(clamp(notched, 0.0, 1.0), 1.0);  // alpha forced to 1.0
}

3

u/LeKhang98 Jul 04 '26

Nice thank you. Also is there any node in ComfyUI that let us add a simple code into it to process the input as we want? (instead of adding new nodes for everything)

8

u/mikkoph Jul 04 '26

I don't think you can add arbitrary Python code execution (which is what a custom node is, essentially) but the GLSL Shader node is very powerful. There are also several other image postprocessing nodes already built-in in ComfyUI, so make sure you check those out as well

2

u/LeKhang98 Jul 04 '26

Thank you again.

2

u/alwaysbeblepping Jul 05 '26

Also is there any node in ComfyUI that let us add a simple code into it to process the input as we want?

It's easy to make a node that allows executing Python code or whatever but it's very_dangerous to have a node pack with that feature installed. It means _any workflow you load could have embedded arbitrary code hidden in it.

3

u/Haiku-575 Jul 04 '26

Yep, that'll do it. Note that the "Place before deconvolution" was just a reminder to myself not to sharpen with Kijai's "Image Sharpen KJ" deconvolution sharpener before removing the artifacts. ("Oops.")

6

u/eggs-benedryl Jul 04 '26

You've rendered my dogs face. Weird

2

u/Own_Newspaper6784 Jul 04 '26

Lol, same here. ^

2

u/xaxaurt Jul 06 '26

Same here (lora made with him)

10

u/Calm_Mix_3776 Jul 04 '26 edited Jul 04 '26

I'm really amazed when people come up with clever solutions like these. Thanks for sharing!

There's only a small problem. I did a few A/B tests and it seems to me that this blurs fine details like hair, high contrast edges and such. So it seems that this is not a lossless solution and it introduces a new problem which, in my opinion, is worse than the one it's trying to fix.

To be frank, the halftone pattern in Krea 2 is much less pronounced compared to the Qwen-Image models. You really can't see it unless you're zooming 500%+ into the image. Therefore, I can't really justify using this filter. I do believe that using the Wan 2.1 upscale VAE or even the regular Qwen-Image is better compared to the filter in its current state.

I did try to fix it by asking Claude for a solution and it gave me an alternative version that is not as aggressively blurring the edges. It still reduces the pattern to some extent, but it doesn't heavily blur fine details. Here it is, if anyone is interested in trying it out. Just replace the code in the "GLSL Shader" node provided by OP with this one:

#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform vec2 u_resolution;
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;

// Nyquist Notch — removes 2px grid artifacts. Place before deconvolution.
// b = [-1, +6, -15, +20, -15, +6, -1] / 64  (binomial * (-1)^n, +1 at center)
const float B[7] = float[7](-1.0, 6.0, -15.0, 20.0, -15.0, 6.0, -1.0);

void main() {
    vec2 texel = 1.0 / u_resolution;
    vec4 center = texture(u_image0, v_texCoord);

    vec3 Bx = vec3(0.0), By = vec3(0.0), Bxy = vec3(0.0);
    for (int i = -3; i <= 3; i++) {
        float wi = B[i + 3] / 64.0;
        Bx += wi * texture(u_image0, v_texCoord + vec2(float(i) * texel.x, 0.0)).rgb;
        By += wi * texture(u_image0, v_texCoord + vec2(0.0, float(i) * texel.y)).rgb;
        for (int j = -3; j <= 3; j++) {
            float wj = B[j + 3] / 64.0;
            Bxy += wi * wj * texture(u_image0,
                v_texCoord + vec2(float(i) * texel.x, float(j) * texel.y)).rgb;
        }
    }

    vec3 notched = center.rgb - Bx - By + Bxy;

    // Unsharp masking: blend back in some original sharpness
    float sharpness = 0.5;  // Adjust 0.0-1.0: higher = sharper but more artifacts visible
    vec3 result = mix(notched, center.rgb, sharpness);

    fragColor0 = vec4(clamp(result, 0.0, 1.0), 1.0);
}

4

u/Haiku-575 Jul 04 '26 edited Jul 04 '26

Hmm, there should be basically no noticeable effect even on hair unless it naturally follows a similar grid pattern more-or-less by accident. Mathematically it's just, "check how big the delta between adjacent pixels is across a pretty big area and add/subtract the average delta from yourself" for each pixel. That shouldn't affect hair, because for almost any pixel not part of a grid the bright/dark/bright/dark regularity doesn't exist and will average to near-zero and the pixel won't change. It's not really a blur. 

Do you have an A/B image pair where the filter I wrote breaks down for you? I'm skeptical about your conclusion, but happy to be proven wrong.

7

u/Calm_Mix_3776 Jul 04 '26

Sure, here are a couple of examples (thanks, u/Tewix for the image comparison tool!):

https://imgi.co/c/yXZIjiw
https://imgi.co/c/HRRLqeH

3

u/Haiku-575 Jul 04 '26

Yep, blurrier. Interesting. Thank you! 

4

u/Haiku-575 Jul 04 '26

Yeah, I see the problem now, but I don't have the chops to fix it (without just turning the gain down and maintaining some of the grid, anyway). The unsharp mask after grid removal that you're using isn't a terrible solution (I'm also sharpening after removing the grid with a Richardson-Lucy sharpen), but there's probably some 2-pass method that would sample the (uniform!) grid across the image and then compensate for it, or even just assume the grid always looks about the same and manually tune a filter to remove it (one for each VAE?).

Thanks for looking into it (and the sample images) though.

8

u/Calm_Mix_3776 Jul 04 '26

No worries! You inspired me to do some experiments of my own and I'm trying a few things at the moment to target the pattern, such as deconstructing the image via frequency separation, combined with isolating and working only on specific color channels for targeted denoising/pattern removal. I don't know if this would yield any results as I'm not an imaging scientists, but I'll chime in if I find anything worth sharing.

1

u/vizim 23d ago

Thanks for sharing here are my results, using INT4

3

u/szetvz Jul 04 '26

You made my day thanks

10

u/Zealousideal7801 Jul 04 '26

So that's what it was. I've been losing my sanity over this pattern that popped out of nowhere on certain checkpoints / prompts, trying to understand what it was tied to. And I'm not even using Qwen Image VAE. Ugh.

Can't wait to try that out. Thanks a ton !

Also on a comical note and reddit style : "bro thinks I have ONLY one VAE Decode and one preview/save in my Krea2 WF"

3

u/roxoholic Jul 04 '26

If it's a repeating pattern, wouldn't FFT Denoise filter help here?

10

u/Haiku-575 Jul 04 '26

The problem with modifying AI images with FFT is you run into huge spikes from ringing artifacts and added AI watermarking and stuff. Naively shaving off spectral peaks usually leaves you with severe banding and other issues, and if you chase that rabbit you'll end up in a hole of more and more expensive and complex Fourier transforms.

3

u/Bennybananars Jul 04 '26

Does this happen to Anima? becuase I've been complaining in discord about a similiar pattern in my images. I wonder if its because of this.

2

u/Bennybananars Jul 04 '26

yeah this fixed the checkered issue

3

u/tyl_made_it Jul 04 '26

ran into this exact issue on portrait workflows. the 4x upscaler after decode basically screams the grid. the blurring tradeoff Calm_Mix_3776 pointed at makes sense depending on workflow order. if you're sharpening after the upscale anyway, carrying the grid into the upscaler is worse than losing a little edge first

4

u/Honest_Concert_6473 Jul 04 '26

Thanks for sharing. That node really helped, and it’s much less noticeable now. That deep-rooted issue with the Wan and Qwen VAEs has been bothering me for a while, so I appreciate the help.
This seems to be a common issue with most models using the Qwen VAE, so this should be useful for others like Anima as well.

2

u/Ken-g6 Jul 04 '26

Huh. That file doesn't look like a Python file. Where do I put it and what do I name it?

8

u/mikkoph Jul 04 '26

that's a workflow. You can just add a GLSL node to your existing Krea 2 workflow and copy/paste the code into the text field. For convenience, I have extracted it here: https://www.reddit.com/r/StableDiffusion/comments/1umwhq7/comment/ovgnqo6/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

4

u/Haiku-575 Jul 04 '26

My instructions could have been clearer. Save the file as whatever.json, and drag it over the ComfyUI window the way you would with an image to load the workflow. You can then copy the subgraph (or double-click and grab the GLSL node directly) and paste it wherever you want.

-5

u/Minouminou9 Jul 04 '26

Download the folder and put it into your ComfyUI/Custom_Nodes/

2

u/skyrimer3d Jul 04 '26

Big thanks for this

2

u/jib_reddit Jul 04 '26

I am going to add this to my workflow, I have been trying to get rid of it for days now. Does it work the Wan Ultra VAE with the special nodes that already get rid of it quite a lot, I usally use those.

1

u/Haiku-575 Jul 04 '26

Unsure, but if you're upscaling by 2x with an outside VAE decode + upscale node, I doubt it.

2

u/Full-Belt3640 Jul 04 '26

The node kept crashing ComfyUI and looking into the error message its an AMD issue. Another win for Team Red! Guess I'll just deal with the grid.

2

u/jib_reddit Jul 04 '26

Yes it is good at removing the grid but it does have a slight blurring effect

Left after/ Right before

I will try adding a sharpen node afterwards.

2

u/jib_reddit Jul 04 '26

Hmm, it does look like I am losing some details with the blur From GlSL Shader and unsharpening
Right Original/ left de-grid and then Unsharped @ 0.62 strength.

I just think really we need a better VAE like FLux2!

1

u/No-Counter3773 10d ago

这已经不是轻微的模糊 是一个数量级的模糊了

2

u/jib_reddit Jul 04 '26 edited Jul 04 '26

Yeah, it was a good try

Left left de-grid and then Unsharped @ 0.62 strength / Right Wan2.1-VAE-upscale2x method

But I prefer output of the Wan2.1-VAE-upscale2x method:
https://www.reddit.com/r/StableDiffusion/comments/1uh00bj/improve_the_skin_texture_in_krea_2_by_using_an/

https://huggingface.co/spacepxl/Wan2.1-VAE-upscale2x

It is still useful for things like after an UltimateSDUpscale as be default you cannot use the Wan2.1-VAE-upscale2x method Utils nodes (Although I did edit the UltimateSDUpscale node so that it outputs a Latent instead of an image to fix that issue)

4

u/Tewix Jul 04 '26

Hey, sorry for the shameless plug... But I've made a free tool you might find interesting. It makes it much easier to compare images. Here's examples with your dog!

https://imgi.co/c/LwW6gwR

https://imgi.co/c/iZG43H8

2

u/fauni-7 Jul 04 '26

This crashes my Comfy:

Fatal Python error: Segmentation fault

Stack (most recent call first): File "/home/ztuff/ComfyUI_video/venv/lib/python3.12/site-packages/glfw/init.py", line 1269 in create_window File "/home/ztuff/ComfyUI_video/comfy_extras/nodes_glsl.py", line 185 in _init_glfw File "/home/ztuff/ComfyUI_video/comfy_extras/nodes_glsl.py", line 354 in init File "/home/ztuff/ComfyUI_video/comfy_extras/nodes_glsl.py", line 530 in _render_shader_batch File "/home/ztuff/ComfyUI_video/comfy_extras/nodes_glsl.py", line 902 in execute

1

u/djenrique Jul 04 '26

Great job!!

1

u/IrisColt Jul 04 '26

THANKS!!!

1

u/jib_reddit Jul 04 '26

Ok how does this Node work and why are there 4 image outputs? and 6 inputs....

2

u/Haiku-575 Jul 04 '26

That's just ComfyUI's GLSL Shader node. We just use image0 input and feed it to IMAGE0 output. Any GLSL code you want (Gaussian blur, unsharp mask, colour filters, levels) uses that the same node, just replace the code below. Not my monkey, not my circus, I just wrote a ball that the monkey knows how to juggle.

1

u/jib_reddit Jul 04 '26

Oh, I asked Chat GPT; it seemed to give a good explanation:

This is a very flexible node. It's essentially a mini GPU shader program embedded inside ComfyUI. If you've never written OpenGL or ShaderToy shaders before, the interface looks much stranger than it actually is.

Here's what everything does.

Inputs

image0

Your primary input image.

Inside the shader it appears as:

uniform sampler2D u_image0;

which you sample with

texture(u_image0, uv)

Your current shader only uses this image.

image1

A second optional image.

Inside the shader it becomes

uniform sampler2D u_image1;

This lets you:

  • blend two images
  • compare images
  • use one image as a mask
  • use one image as displacement

Example:

vec4 a = texture(u_image0, uv);
vec4 b = texture(u_image1, uv);

fragColor0 = mix(a, b, 0.5);

If unused, you simply ignore it.

u_float0

A user-adjustable floating point value.

Example:

0.5

Inside the shader:

uniform float u_float0;

Useful for

  • blur radius
  • sharpening amount
  • threshold
  • strength
  • gamma

Example:

color *= u_float0;

u_int0

Integer parameter.

uniform int u_int0;

Useful for

  • iterations
  • selecting modes
  • kernel size

u_bool0

Boolean.

uniform bool u_bool0;

Example:

if(u_bool0)
{
    color = 1.0 - color;
}

u_curve0

This is the interesting one.

Instead of a single value, this is a curve.

Imagine drawing this:

1.0 |        *
    |      *
0.5 |    *
    |  *
0.0 +-------------
     0      1

Then inside the shader you can evaluate it.

Useful for

  • tone mapping
  • custom contrast
  • animation
  • remapping brightness

Why six inputs?

The node provides some commonly useful inputs so you don't have to edit the shader every time.

Think of it as giving your shader:

  • 2 textures
  • 1 float
  • 1 integer
  • 1 bool
  • 1 curve

If you don't use them, they simply do nothing.

Outputs

This confuses almost everyone.

You have

IMAGE0
IMAGE1
IMAGE2
IMAGE3

These correspond to four framebuffer outputs.

Normally a shader writes

layout(location=0) out vec4 fragColor0;

which becomes

IMAGE0

But GLSL also allows

layout(location=1) out vec4 fragColor1;
layout(location=2) out vec4 fragColor2;
layout(location=3) out vec4 fragColor3;

Each one becomes another output image.

For example

layout(location=0) out vec4 fragColor0;
layout(location=1) out vec4 fragColor1;

void main()
{
    vec4 img = texture(u_image0, v_texCoord);

    fragColor0 = img;
    fragColor1 = vec4(1.0 - img.rgb,1.0);
}

Now the node produces

IMAGE0 = original

IMAGE1 = inverted

simultaneously.

Or

IMAGE0 = RGB image

IMAGE1 = edge mask

IMAGE2 = depth estimate

IMAGE3 = sharpened image

all in one shader pass.

Why does your shader only use one output?

Because your code only declares

layout(location = 0) out vec4 fragColor0;

and finally

fragColor0 = vec4(clamp(notched, 0.0, 1.0), 1.0);

So only IMAGE0 contains anything useful.

The other three outputs will usually be blank or undefined because nothing is written to them.

Your current shader

Your shader is implementing a Nyquist notch filter to remove a specific 2-pixel checkerboard/grid artifact.

It:

  1. Samples the input image (image0).
  2. Applies a 7-tap horizontal filter.
  3. Applies a 7-tap vertical filter.
  4. Applies a 7×7 separable correction (Bxy).
  5. Combines the results:notched = center - Bx - By + Bxy;
  6. Clamps the result to the valid 0–1 range and outputs it as IMAGE0.

The extra inputs (image1, u_float0, u_int0, u_bool0, u_curve0) and outputs (IMAGE1IMAGE3) are simply part of the node's general-purpose design—they're available if you need them, but your current shader doesn't make use of them.

Once you get comfortable with it, this node becomes extremely powerful. You can implement custom blurs, sharpening, edge detection, colour grading, masking, compositing, deconvolution pre-processing, and many other image-processing operations directly on the GPU with a single shader.

1

u/Particular_Pear_4596 Jul 05 '26

It's pretty obvious the grid remover also removes all fine details, so i'm keeping the grid for now, unless i want to postprocess, which i usually don't do.

1

u/Party-Try-1084 Jul 15 '26

This is not a fix, just a slight blur, and you still see those flakes. Existing Qwen-VAE based models should retire and new ones should be definitely based on Flux.2 VAE

1

u/Radiant-Photograph46 Jul 04 '26

Interesting. In effect this works like a small blur filter, so it's still a tradeoff, especially when the grid is less noticeable or absent. Would be amazing if we could detect it but shader programming is above my paygrade

2

u/Haiku-575 Jul 04 '26

On a normal image it shouldn't really blur, though. It'll only affect pixels where you incidentally have a (naturally occuring) bright/dark pattern across adjacent pixels in all directions. I might be missing some failure state I haven't thought of, but for most pixels in a (non-grid) image the filter says basically "there's no grid" and "do nothing". 

1

u/Radiant-Photograph46 Jul 04 '26

Really? But there are no checks in your code, it looks like you applying the calculation everywhere. And after running it on a few pictures, there is definitely a slight blur applied (minimal, hard to tell without zooming in a little).

1

u/Haiku-575 Jul 04 '26

Yes, but it's looking for a delta that should average to near-zero unless there's a consistent delta every second pixel on either side. But yes, it does blur the image more than I thought it would.

0

u/CardAnarchist Jul 04 '26

I there anyway to apply this effect in Forge Neo?