Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8618d8ca56 | ||
|
|
46dde36185 | ||
|
|
a29ca76fe8 | ||
|
|
2908eac20c |
@@ -2,6 +2,17 @@
|
||||
|
||||
All notable changes to ComfyUI-AusBoss are documented here.
|
||||
|
||||
## 2.25.1 - 2026-11-13
|
||||
|
||||
- **Edit Lock Restore 🆎 takes out the pink tint.** MiniMax H3 Image gives
|
||||
every edit a faint pink, slightly lighter tint over the whole picture.
|
||||
Edit Lock already removed it wherever it locked, but when it stepped
|
||||
aside (a new hair color, a new background, rain) the tint stayed. A new
|
||||
**Remove tint** switch, on by default, reads the tint from the part of
|
||||
the picture the edit left alone and takes it out of the whole picture.
|
||||
Edits meant to recolor everything, such as golden hour or red light, are
|
||||
left as they are. Turn it off to get the old behavior.
|
||||
|
||||
## 2.25.0 - 2026-11-12
|
||||
|
||||
- **New node: Load Image.** Loads a picture and sets its size in the same
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="20" role="img" aria-label="release: v2.25.0">
|
||||
<title>release: v2.25.0</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="20" role="img" aria-label="release: v2.25.1">
|
||||
<title>release: v2.25.1</title>
|
||||
<path fill="#555" d="M0 0h48v20H0z"/>
|
||||
<path fill="#00b4aa" d="M48 0h57v20H48z"/>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
||||
<text x="24" y="14">release</text>
|
||||
<text x="76.5" y="14">v2.25.0</text>
|
||||
<text x="76.5" y="14">v2.25.1</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 425 B |
@@ -1,6 +1,7 @@
|
||||
# Edit Lock Restore
|
||||
|
||||
Puts your picture's own pixels back in every part the edit did not change.
|
||||
Puts your picture's own pixels back in every part the edit did not change,
|
||||
and takes out the faint pink tint some edit models add to the whole picture.
|
||||
|
||||
**Edit Lock 🆎** keeps the untouched parts in place while the picture is
|
||||
made. They still pass through the VAE, which softens fine detail a little.
|
||||
@@ -22,6 +23,14 @@ your picture pixel for pixel. Only the edit is new.
|
||||
- **Feather** (`feather`): how softly your original fades into the edit,
|
||||
in pixels. 16 is the default. The fade lies outside the edit, so the
|
||||
edit itself is never mixed with the old picture.
|
||||
- **Remove tint** (`remove_tint`): on by default. MiniMax H3 Image makes
|
||||
every edit a little pinker and lighter all over. This reads that tint
|
||||
from the part of the picture the edit left alone and takes it out of the
|
||||
whole picture, the edit included. It matters most when Edit Lock stepped
|
||||
aside, such as for a new hair color, a new background or rain, because
|
||||
then nothing else removes it. Edits meant to recolor everything (golden
|
||||
hour, red light, a new art style) are left alone. Turn it off if a color
|
||||
change you asked for comes out weaker.
|
||||
|
||||
## Outputs
|
||||
|
||||
@@ -30,13 +39,15 @@ your picture pixel for pixel. Only the edit is new.
|
||||
what the lock decided, or for a later step that should only touch the
|
||||
edit.
|
||||
- **report**: one line, such as "Edit Lock kept 82% of your picture as it
|
||||
was and left 18% to the edit".
|
||||
was and left 18% to the edit". When the tint was taken out it adds a
|
||||
sentence that says so.
|
||||
|
||||
## When nothing was locked
|
||||
|
||||
The picture passes through unchanged, the mask is all white, and the
|
||||
Your original pixels are not pasted back, the mask is all white, and the
|
||||
report says why: the edit changed the whole picture, the lock was off, or
|
||||
the picture and the result were different sizes.
|
||||
the picture and the result were different sizes. Remove tint still works
|
||||
here, so the picture can come out with the tint taken out.
|
||||
|
||||
## Technical details
|
||||
|
||||
@@ -46,3 +57,17 @@ by the same amount, and used to blend the decoded picture with your
|
||||
original. If the decoded picture is not the size of your original (after
|
||||
an upscale), the original is resized to fit first, which costs it some
|
||||
sharpness. That is why this node goes before an upscaler.
|
||||
|
||||
**Remove tint.** The edit is lined up with your original (edits often come
|
||||
back about one percent larger or a little shifted), both are averaged
|
||||
into coarse cells of about 8 px at 1 megapixel, and their difference is
|
||||
taken in Lab color. The tint is the even shift most cells share: it starts
|
||||
at the median and keeps only cells within 4 Lab units of it, five times
|
||||
over, so a new background or new hair drops out of the count. It is taken
|
||||
out only when it looks like the model's own tint: a between +0.5 and +10,
|
||||
b between -8 and 0, L between -1 and +6, read on at least 15% of the
|
||||
picture. On 21 test edits at 16 megapixels it took out the tint on the 5
|
||||
it should (a +2.6 to +6.5 down to +0.4 or less) and left the other 16
|
||||
alone, golden hour, red light and an anime restyle included. It cannot
|
||||
tell this tint from an edit that asks for a slightly pinker, lighter look
|
||||
over the whole picture; turn it off for one of those.
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { app } from "../../../scripts/app.js";
|
||||
// Must match `version` in pyproject.toml — scripts/release_preflight.py
|
||||
// enforces it. The backend serves its copy at /ausboss/pack_version; a
|
||||
// mismatch means the browser cached JavaScript from an older install.
|
||||
export const AUSBOSS_JS_VERSION = "2.25.0";
|
||||
export const AUSBOSS_JS_VERSION = "2.25.1";
|
||||
|
||||
export const BRAND = "#00b4aa"; // AusBoss teal — keep node accents consistent.
|
||||
export const BRAND_DARK = "#007f78";
|
||||
|
||||
@@ -133,7 +133,13 @@ const CARDS = {
|
||||
{ widget: "tolerance", label: "Tolerance", decimals: 2, suffix: "×" },
|
||||
],
|
||||
},
|
||||
AUSBOSS_NODES_EditLockRestore: { minWidth: 300, rows: [{ widget: "feather", label: "Feather", suffix: "px" }] },
|
||||
AUSBOSS_NODES_EditLockRestore: {
|
||||
minWidth: 300,
|
||||
rows: [
|
||||
{ widget: "feather", label: "Feather", suffix: "px" },
|
||||
{ widget: "remove_tint", label: "Remove tint" },
|
||||
],
|
||||
},
|
||||
AUSBOSS_NODES_ColorMatch: {
|
||||
minWidth: 300,
|
||||
rows: [
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Edit Lock Restore's tint fix: take out the even tint an edit model adds to the whole picture.
|
||||
|
||||
MiniMax H3 Image gives every edit one even color shift: a little lighter,
|
||||
pinker and bluer (Lab L +1 to +3, a +2 to +7, b -1 to -6 at 16 MP). Edit
|
||||
Lock removes it wherever it locks, because those parts are your picture
|
||||
again. When the lock steps aside (new hair color, new background, rain) the
|
||||
shift stays. Here it is read from the part of the picture the edit left
|
||||
alone and subtracted from every pixel.
|
||||
|
||||
A shift is only taken out when it looks like that tint. An edit that is
|
||||
meant to recolor everything (golden hour, red light, a restyle) moves the
|
||||
colors much further or the other way, or leaves too little of the picture
|
||||
alone to read anything, and passes through untouched.
|
||||
|
||||
Pure torch, no ComfyUI imports.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from ._realign_helpers import measure, warp_to_source
|
||||
|
||||
# The pictures are compared at this long side; the fit is sub-pixel there.
|
||||
WORK = 1536
|
||||
# Cells closer than this to the running estimate count as "left alone".
|
||||
NEAR = 4.0
|
||||
# The tint as measured on 57 edits: lighter, pinker, bluer, within these
|
||||
# bounds (Lab units). Smaller than MIN_A is not worth touching.
|
||||
MIN_A = 0.5
|
||||
MAX_A = 10.0
|
||||
MAX_B = 0.0
|
||||
MIN_B = -8.0
|
||||
MIN_L = -1.0
|
||||
MAX_L = 6.0
|
||||
# At least this share of the picture must be left alone to read the tint.
|
||||
MIN_SHARE = 0.15
|
||||
|
||||
_D65 = (0.95047, 1.0, 1.08883)
|
||||
_TO_XYZ = ((0.4124564, 0.3575761, 0.1804375),
|
||||
(0.2126729, 0.7151522, 0.0721750),
|
||||
(0.0193339, 0.1191920, 0.9503041))
|
||||
_TO_RGB = ((3.2404542, -1.5371385, -0.4985314),
|
||||
(-0.9692660, 1.8760108, 0.0415560),
|
||||
(0.0556434, -0.2040259, 1.0572252))
|
||||
|
||||
|
||||
def rgb_to_lab(rgb: torch.Tensor) -> torch.Tensor:
|
||||
"""sRGB in 0..1, [..., 3] -> CIE Lab, float64."""
|
||||
x = rgb.double().clamp(0.0, 1.0)
|
||||
linear = torch.where(x > 0.04045, ((x + 0.055) / 1.055) ** 2.4, x / 12.92)
|
||||
xyz = linear @ torch.tensor(_TO_XYZ, dtype=torch.float64).T / torch.tensor(_D65, dtype=torch.float64)
|
||||
f = torch.where(xyz > 0.008856, xyz.clamp(min=1e-9) ** (1.0 / 3.0), 7.787 * xyz + 16.0 / 116.0)
|
||||
return torch.stack([116.0 * f[..., 1] - 16.0, 500.0 * (f[..., 0] - f[..., 1]),
|
||||
200.0 * (f[..., 1] - f[..., 2])], -1)
|
||||
|
||||
|
||||
def lab_to_rgb(lab: torch.Tensor) -> torch.Tensor:
|
||||
"""CIE Lab, [..., 3] -> sRGB in 0..1, float64."""
|
||||
fy = (lab[..., 0] + 16.0) / 116.0
|
||||
f = torch.stack([fy + lab[..., 1] / 500.0, fy, fy - lab[..., 2] / 200.0], -1)
|
||||
xyz = torch.where(f > 0.206893, f ** 3, (f - 16.0 / 116.0) / 7.787) * torch.tensor(_D65, dtype=torch.float64)
|
||||
linear = (xyz @ torch.tensor(_TO_RGB, dtype=torch.float64).T).clamp(0.0, 1.0)
|
||||
return torch.where(linear > 0.0031308, 1.055 * linear ** (1.0 / 2.4) - 0.055, 12.92 * linear).clamp(0.0, 1.0)
|
||||
|
||||
|
||||
def _fit(image: torch.Tensor, long_side: int) -> torch.Tensor:
|
||||
"""HWC float at most long_side px on its long side."""
|
||||
height, width = int(image.shape[0]), int(image.shape[1])
|
||||
scale = long_side / max(height, width)
|
||||
if scale >= 1.0:
|
||||
return image.float()
|
||||
size = (max(1, round(height * scale)), max(1, round(width * scale)))
|
||||
return F.interpolate(image.movedim(-1, 0)[None].float(), size=size, mode="bilinear",
|
||||
align_corners=False, antialias=True)[0].movedim(0, -1)
|
||||
|
||||
|
||||
def measure_tint(result: torch.Tensor, source: torch.Tensor) -> tuple[list[float] | None, float]:
|
||||
"""The even shift in (L, a, b) from source to result, read where the edit left the picture alone.
|
||||
|
||||
result, source: HWC float in 0..1, any sizes. Returns (shift or None, the
|
||||
share of the picture it was read on). The result is lined up with the
|
||||
source first, since edits often come back slightly zoomed or moved.
|
||||
"""
|
||||
src = _fit(source[..., :3], WORK)
|
||||
edit = _fit(result[..., :3], WORK)
|
||||
fit = measure(src, edit, mode="zoom + shift")
|
||||
if not fit.reliable:
|
||||
return None, 0.0
|
||||
height, width = int(src.shape[0]), int(src.shape[1])
|
||||
scaled = F.interpolate(edit.movedim(-1, 0)[None], size=(height, width), mode="bilinear",
|
||||
align_corners=False)[0].movedim(0, -1)
|
||||
try:
|
||||
warped = warp_to_source(scaled, fit.matrix, width, height)
|
||||
warped = warped[0] if isinstance(warped, tuple) else warped
|
||||
except Exception:
|
||||
warped = scaled
|
||||
# coarse cells of about 8 px at 1 MP, so grain and small detail drop out
|
||||
cell = max(4, round(8 / math.sqrt(1e6 / (width * height))))
|
||||
|
||||
def cells(img: torch.Tensor) -> torch.Tensor:
|
||||
return F.avg_pool2d(img.movedim(-1, 0)[None], cell, stride=cell)[0].movedim(0, -1)
|
||||
|
||||
diff = rgb_to_lab(cells(warped)) - rgb_to_lab(cells(src))
|
||||
margin = max(1, int(diff.shape[0]) // 12) # the edges carry the warp's filled strip
|
||||
diff = diff[margin:-margin, margin:-margin].reshape(-1, 3)
|
||||
if diff.shape[0] < 50:
|
||||
return None, 0.0
|
||||
centre = diff.median(0).values
|
||||
share = 1.0
|
||||
# start at the median change, then keep only the cells near it: a new
|
||||
# background or new hair is a big change, but not the same one everywhere
|
||||
for _ in range(5):
|
||||
near = (diff - centre).norm(dim=1) < NEAR
|
||||
if int(near.sum()) < 50:
|
||||
break
|
||||
centre = diff[near].mean(0)
|
||||
share = float(near.double().mean())
|
||||
return [float(v) for v in centre], share
|
||||
|
||||
|
||||
def looks_like_tint(shift: list[float] | None, share: float) -> bool:
|
||||
"""True for the model's own lighter-pinker-bluer shift; False for an intended recolor."""
|
||||
if shift is None or share < MIN_SHARE:
|
||||
return False
|
||||
light, a, b = shift
|
||||
return MIN_A <= a <= MAX_A and MIN_B <= b <= MAX_B and MIN_L <= light <= MAX_L
|
||||
|
||||
|
||||
def remove_tint(image: torch.Tensor, shift: list[float]) -> torch.Tensor:
|
||||
"""Subtract one (L, a, b) shift from every pixel. image: [B, H, W, C] in 0..1."""
|
||||
lab = rgb_to_lab(image[..., :3]) - torch.tensor(shift, dtype=torch.float64)
|
||||
out = image.clone()
|
||||
out[..., :3] = lab_to_rgb(lab).to(image.dtype)
|
||||
return out
|
||||
|
||||
|
||||
def take_out_tint(images: torch.Tensor, source: torch.Tensor) -> tuple[torch.Tensor, list[float] | None]:
|
||||
"""Each picture of the batch with its tint taken out when it looks like the model's tint.
|
||||
|
||||
Returns the pictures and the largest shift taken out (None when none was)."""
|
||||
out = images.clone()
|
||||
taken: list[float] | None = None
|
||||
for index in range(int(images.shape[0])):
|
||||
shift, share = measure_tint(images[index], source[0])
|
||||
if looks_like_tint(shift, share):
|
||||
out[index:index + 1] = remove_tint(images[index:index + 1], shift)
|
||||
if taken is None or abs(shift[1]) > abs(taken[1]):
|
||||
taken = shift
|
||||
return out, taken
|
||||
+34
-5
@@ -19,6 +19,7 @@ from ._edit_lock_helpers import (
|
||||
describe,
|
||||
restore_pixels,
|
||||
)
|
||||
from ._edit_tint_helpers import take_out_tint
|
||||
|
||||
|
||||
class AusBossEditLock:
|
||||
@@ -141,8 +142,10 @@ class AusBossEditLockRestore:
|
||||
"Puts your picture's own pixels back in every part the edit did not "
|
||||
"change. Use it right after the VAE decode, with the edit_lock "
|
||||
"output of Edit Lock 🆎. The untouched parts then match your "
|
||||
"original pixel for pixel; only the edit is new. When the lock "
|
||||
"stepped aside the picture passes through unchanged."
|
||||
"original pixel for pixel; only the edit is new. It also takes out "
|
||||
"the faint pink tint some edit models add to the whole picture, "
|
||||
"which matters most when the lock stepped aside (a new hair color, "
|
||||
"background or weather)."
|
||||
)
|
||||
SEARCH_ALIASES = [
|
||||
"edit lock restore",
|
||||
@@ -178,6 +181,19 @@ class AusBossEditLockRestore:
|
||||
},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"remove_tint": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": True,
|
||||
"tooltip": (
|
||||
"Takes out the faint pink tint the edit model adds "
|
||||
"to the whole picture. Edits meant to recolor "
|
||||
"everything, like golden hour, are left alone."
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "STRING")
|
||||
@@ -189,17 +205,30 @@ class AusBossEditLockRestore:
|
||||
)
|
||||
FUNCTION = "restore"
|
||||
|
||||
def restore(self, image, edit_lock, feather):
|
||||
def restore(self, image, edit_lock, feather, remove_tint=True):
|
||||
if image.ndim != 4:
|
||||
raise ValueError("Edit Lock Restore: image must be an IMAGE batch.")
|
||||
if not isinstance(edit_lock, EditLock):
|
||||
raise ValueError("Edit Lock Restore: edit_lock needs the output of Edit Lock.")
|
||||
height, width = int(image.shape[1]), int(image.shape[2])
|
||||
report = describe(edit_lock)
|
||||
if remove_tint and edit_lock.source is not None:
|
||||
# before the paste, so the edit itself loses the tint too and meets the original evenly
|
||||
image, taken = take_out_tint(image, edit_lock.source)
|
||||
if taken is not None:
|
||||
report += " " + tint_report(taken)
|
||||
mask = edit_lock.pixel_mask(height, width)
|
||||
if mask is None or edit_lock.source is None:
|
||||
return (image, torch.ones((int(image.shape[0]), height, width)), describe(edit_lock))
|
||||
return (image, torch.ones((int(image.shape[0]), height, width)), report)
|
||||
result, edit_mask = restore_pixels(image, edit_lock.source, mask, int(feather))
|
||||
return (result, edit_mask, describe(edit_lock))
|
||||
return (result, edit_mask, report)
|
||||
|
||||
|
||||
def tint_report(shift):
|
||||
"""One plain sentence; the numbers are Lab units, for anyone who wants them."""
|
||||
light, a, b = shift
|
||||
return (f"It also took out the faint pink tint the model added to the whole picture "
|
||||
f"(L {light:+.1f}, a {a:+.1f}, b {b:+.1f}).")
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ name = "ausboss-nodes"
|
||||
# Manager/registry search matches against this text, so it names the actual
|
||||
# nodes instead of describing the collection abstractly.
|
||||
description = "ComfyUI-AusBoss (AusBoss nodes): image and video nodes with compact controls: Image and Video Crop + Rotate + Pad (frame or clip), Load Image, Load Image + Pad, Image Folder, Image Resize, Align Image, Image Size, Color Match, Image Compare A/B, Realign to Source (experimental), Save Image (PNG, WebP/JXL, captions), Load Video, Load Audio, Save Video, Select Frame, Frame Interpolate, Mask Refine, Mask by Name and Mask by Touch, LaMa Inpaint, Crop For Inpaint, Stitch Inpaint, Tiled Upscale and Tiled Upscale Stitch, FlashVSR Upscale, Edit Lock and Edit Lock Restore, LoRA Loader, Krea 2 Encode and Outpaint Model Patch, Latent Size, Layout Boxes, Seed, Select Every Nth, Split Batch, Merge Batches, Text, Text Lines, Prompt Variations, H3 Prompt Writer, Integer, Float, Math Expression, Show Text, Free Memory, Workflow Note, Workflow Note Tile, Callout, Callout Tile, Workflow Switches, Run Timer, and Step Preview. Example workflows for Qwen Image 2.1, Krea 2, Klein 9B and LTX 2.3."
|
||||
version = "2.25.0"
|
||||
version = "2.25.1"
|
||||
# comfy-cli serializes this to JSON verbatim, so `file` publishes the literal
|
||||
# string {"file": "LICENSE"} to the registry listing. `text` puts a readable
|
||||
# name there instead. Both forms are supported; the LICENSE file is unchanged.
|
||||
|
||||
Vendored
+3
-1
@@ -144,7 +144,9 @@
|
||||
"edit_lock",
|
||||
"feather"
|
||||
],
|
||||
"optional": [],
|
||||
"optional": [
|
||||
"remove_tint"
|
||||
],
|
||||
"outputs": [
|
||||
[
|
||||
"IMAGE",
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Edit Lock Restore's tint fix: the model's pink tint goes, an intended recolor stays."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
if "nodes" in sys.modules and not hasattr(sys.modules["nodes"], "__path__"):
|
||||
del sys.modules["nodes"]
|
||||
|
||||
from nodes._edit_lock_helpers import EditLock # noqa: E402
|
||||
from nodes._edit_tint_helpers import ( # noqa: E402
|
||||
lab_to_rgb,
|
||||
looks_like_tint,
|
||||
measure_tint,
|
||||
remove_tint,
|
||||
rgb_to_lab,
|
||||
take_out_tint,
|
||||
)
|
||||
from nodes.node_edit_lock import AusBossEditLockRestore # noqa: E402
|
||||
|
||||
TINT = [2.0, 5.0, -3.0] # what MiniMax H3 Image adds: lighter, pinker, bluer
|
||||
|
||||
|
||||
def picture(seed: int = 0, size: int = 384) -> torch.Tensor:
|
||||
"""A textured photo stand-in, [1, H, W, 3], mid tones so a shift never clips."""
|
||||
gen = torch.Generator().manual_seed(seed)
|
||||
coarse = torch.rand(1, 3, size // 16, size // 16, generator=gen)
|
||||
fine = torch.rand(1, 3, size // 4, size // 4, generator=gen)
|
||||
img = 0.6 * F.interpolate(coarse, size=(size, size), mode="bicubic", align_corners=False)
|
||||
img = img + 0.4 * F.interpolate(fine, size=(size, size), mode="bilinear", align_corners=False)
|
||||
return (0.2 + 0.6 * img).clamp(0.0, 1.0).movedim(1, -1)
|
||||
|
||||
|
||||
def shifted(image: torch.Tensor, shift) -> torch.Tensor:
|
||||
return lab_to_rgb(rgb_to_lab(image) + torch.tensor(shift, dtype=torch.float64)).float()
|
||||
|
||||
|
||||
def edited(source: torch.Tensor, shift) -> torch.Tensor:
|
||||
"""A local edit (a new patch) on top of an even shift of the whole picture."""
|
||||
out = shifted(source, shift)
|
||||
out[:, 120:260, 140:280] = torch.tensor([0.85, 0.85, 0.88])
|
||||
return out
|
||||
|
||||
|
||||
class TintTests(unittest.TestCase):
|
||||
def test_lab_round_trip(self):
|
||||
img = picture(1)
|
||||
self.assertLess(float((lab_to_rgb(rgb_to_lab(img)).float() - img).abs().max()), 1e-4)
|
||||
|
||||
def test_reads_the_tint_on_the_untouched_part(self):
|
||||
source = picture(2)
|
||||
shift, share = measure_tint(edited(source, TINT)[0], source[0])
|
||||
self.assertIsNotNone(shift)
|
||||
for got, want in zip(shift, TINT):
|
||||
self.assertAlmostEqual(got, want, delta=0.4)
|
||||
self.assertGreater(share, 0.5)
|
||||
|
||||
def test_takes_the_tint_out(self):
|
||||
source = picture(3)
|
||||
out, taken = take_out_tint(edited(source, TINT), source)
|
||||
self.assertIsNotNone(taken)
|
||||
outside = (slice(0, 100), slice(0, 384))
|
||||
self.assertLess(float((out[0][outside] - source[0][outside]).abs().mean()), 0.004)
|
||||
|
||||
def test_leaves_an_intended_recolor(self):
|
||||
source = picture(4)
|
||||
golden = [6.0, 20.0, 30.0] # golden hour: warmer and much yellower
|
||||
image = edited(source, golden)
|
||||
out, taken = take_out_tint(image, source)
|
||||
self.assertIsNone(taken)
|
||||
self.assertTrue(torch.equal(out, image))
|
||||
|
||||
def test_the_tint_rule(self):
|
||||
self.assertTrue(looks_like_tint([2.6, 6.3, -3.4], 0.49)) # silver hair
|
||||
self.assertTrue(looks_like_tint([1.8, 3.5, -1.8], 0.62)) # rain
|
||||
self.assertFalse(looks_like_tint([-2.0, 29.4, 2.9], 0.05)) # red light
|
||||
self.assertFalse(looks_like_tint([19.5, 2.5, -5.5], 0.07)) # anime restyle
|
||||
self.assertFalse(looks_like_tint([7.8, 23.9, 31.3], 1.0)) # golden hour
|
||||
self.assertFalse(looks_like_tint([0.0, 0.1, -0.1], 0.9)) # already clean
|
||||
self.assertFalse(looks_like_tint(None, 0.0))
|
||||
|
||||
def test_noise_is_left_alone(self):
|
||||
gen = torch.Generator().manual_seed(5)
|
||||
image = torch.rand(1, 64, 64, 3, generator=gen)
|
||||
out, taken = take_out_tint(image, torch.rand(1, 64, 64, 3, generator=gen))
|
||||
self.assertIsNone(taken)
|
||||
self.assertTrue(torch.equal(out, image))
|
||||
|
||||
def test_restore_node_takes_it_out_when_the_lock_stepped_aside(self):
|
||||
source = picture(6)
|
||||
lock = EditLock()
|
||||
lock.status = "whole picture"
|
||||
lock.source = source
|
||||
image = edited(source, TINT)
|
||||
out, mask, report = AusBossEditLockRestore().restore(image, lock, 16)
|
||||
self.assertLess(float((out[0, :100] - source[0, :100]).abs().mean()), 0.004)
|
||||
self.assertIn("tint", report)
|
||||
self.assertTrue(torch.all(mask == 1.0))
|
||||
# switched off, it passes through as before
|
||||
out, _, report = AusBossEditLockRestore().restore(image, lock, 16, remove_tint=False)
|
||||
self.assertTrue(torch.equal(out, image))
|
||||
self.assertNotIn("tint", report)
|
||||
|
||||
def test_remove_tint_keeps_alpha(self):
|
||||
image = torch.cat([picture(7), torch.ones(1, 384, 384, 1)], -1)
|
||||
out = remove_tint(image, TINT)
|
||||
self.assertTrue(torch.equal(out[..., 3], image[..., 3]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user