chaiNNer
chaiNNer copied to clipboard
Implement "Add Image Diff" node
This PR implements an "Add Image Diff" node. The intended usage looks like this:
- I have a high-res image "HQ.png" and a low-res image "LQ.png", with identical content.
- I also have a low-res image "LQ-plus.png", which is identical in size and content to "LQ.png" except that it has a translucent overlay applied to it.
- I want to apply the translucent overlay to "HQ.png" without losing the high-res detail that's already in "HQ.png".
- I upscale both of the low-res images with ESRGAN to match the resolution of "HQ.png".
- I run the "Add Image Diff" node. The input image is "HQ.png"; the reference initial image is the upscaled "LQ.png"; the reference goal image is the upscaled "LQ-plus.png".
- The result is that the difference between "LQ-Plus.png" and "LQ.png" (which is equivalent to the overlay by itself) has been added to "HQ.png", thus the overlay has been applied without losing any high-res detail in "HQ.png".
Not sure what the Pyright error is about; it looks like it's unhappy about this line:
invalid_mask = result_alpha <= 0
Is that a false positive? The code seems to work OK.
Just add a # type: ignore
comment. Pyright complains about using math ops on numpy arrays even though they're perfectly valid.
It kinda sounds like what you want is this:
It kinda sounds like what you want is this:
@RunDevelopment You're suggesting applying the High Pass node to the HQ.png
? That's definitely an interesting approach, and it does look mildly better than the original LQ-plus.png
, but for my test images it doesn't look nearly as good as what this PR produces (unless I'm doing it wrong).
I think I understand what you want to do now. Essentially, you have an LR image L
, an HR image H
, and an LR image with an overlay o
. So you have L
, H
, and L+o
as images. You then upscale L
and L+o
to get up(L)
and up(L+o)
. Finally, this node is supposed to output the result of H + up(L+o) - up(L)
, which is approximately H + up(o)
(but not really because upscaling in finicky).
This would also explain why my suggestion with High Pass kinda works. High Pass can somewhat approximate the up(L+o) - up(L)
term.
We can split what your node is doing in 2 parts: calculating up(L+o) - up(L)
and adding that to H
.
-
up(L+o) - up(L)
: This is very similar to what High Pass is doing. Given an imageA
, High Pass just essentially returnsA - blur(A) + 0.5
. If instead of blurring, we configured High Pass to take another image as the "blurred image", then we can calculateup(L+o) - up(L)
no problem. -
H + x
: This operation is already implemented by the Overlay blend mode in Blend Images. Kinda.
Of course, my above suggestions assume that I understood your use case correctly. So did I get that correctly? If so, what do you think about my suggestions?
Actually, I think I'm gonna implement custom "blurred" images for High Pass quickly myself. It's a feature I wanted before anyway.
@RunDevelopment Yes, I think your description of my use case is accurate (unless I'm misreading, which I suppose is possible). The main reason I combined them into one node was that I wasn't sure if there was any practical use for a diff image besides adding it to another image, and also I wasn't sure if passing around negative RGB values between nodes would break anything. If passing around negative RGB values between nodes is OK, then yes, I think splitting this node in two is OK.
One potential caveat: my implementation has special-case logic for fully transparent pixels, which was necessary for RGBA images to look correct. I'm not sure whether High Pass and Overlay can do that out of the box, nor am I sure whether adding that special-case logic to High Pass and Overlay will break other use cases.
The main reason I combined them into one node was that I wasn't sure if there was any practical use for a diff image besides adding it to another image
I have an entire folder of just these high frequency details :) They are super useful to have around when you want to add more crunch to a texture.
I wasn't sure if passing around negative RGB values between nodes would break anything.
I would. We only allow passing 0-1 values. That's why High Pass does the + 0.5
. This gets rid of the negatives. Almost. We still clip values outside 0-1, so it's a bit lossy. You can change the contrast to fix this though.
One potential caveat: my implementation has special-case logic for fully transparent pixels, which was necessary for RGBA images to look correct. I'm not sure whether High Pass and Overlay can do that out of the box, nor am I sure whether adding that special-case logic to High Pass and Overlay will break other use cases.
The alpha logic in High Pass is just something tacked on. It's kinda broken rn tbh. We might as well fix it...
It's the same old alpha == 0 -> diff = 0, right?
I would. We only allow passing 0-1 values. That's why High Pass does the
+ 0.5
. This gets rid of the negatives. Almost. We still clip values outside 0-1, so it's a bit lossy. You can change the contrast to fix this though.
@RunDevelopment Hmm, if potentially lossy workarounds are needed for passing between nodes, I think I'd prefer to keep this within one node, so that users don't have to analyze whether they're hitting the lossy condition. Unless you have another way to fix that?
It's the same old alpha == 0 -> diff = 0, right?
This PR copies the reference goal pixel (i.e. it discards the reference init pixel and the input pixel) if the input pixel is fully transparent. This is the correct behavior for my use case, but I don't know whether that behavior would break other use cases.