190 lines
6.1 KiB
Python
190 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
import sys
|
|
|
|
import torch
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
SRC = ROOT / "src"
|
|
if str(SRC) not in sys.path:
|
|
sys.path.insert(0, str(SRC))
|
|
|
|
from flux2_resolution_guard.inference.image import (
|
|
analytic_compand_correction_tensor,
|
|
load_model_checkpoint,
|
|
)
|
|
|
|
|
|
def _comfy_image_to_bchw(image: torch.Tensor) -> torch.Tensor:
|
|
if image.dim() == 3:
|
|
image = image.unsqueeze(0)
|
|
# Comfy IMAGE is usually BHWC
|
|
if image.shape[-1] == 3:
|
|
return image.permute(0, 3, 1, 2).contiguous()
|
|
if image.shape[1] == 3:
|
|
return image.contiguous()
|
|
raise ValueError(f"Unsupported IMAGE tensor shape: {tuple(image.shape)}")
|
|
|
|
|
|
def _bchw_to_comfy_image(image: torch.Tensor) -> torch.Tensor:
|
|
if image.dim() == 3:
|
|
image = image.unsqueeze(0)
|
|
return image.permute(0, 2, 3, 1).contiguous()
|
|
|
|
|
|
def _normalize_mask(mask: Optional[torch.Tensor], height: int, width: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
|
|
if mask is None:
|
|
return torch.ones((1, 1, height, width), device=device, dtype=dtype)
|
|
if mask.dim() == 2:
|
|
mask = mask.unsqueeze(0).unsqueeze(0)
|
|
elif mask.dim() == 3:
|
|
if mask.shape[-1] == 1:
|
|
mask = mask.permute(0, 3, 1, 2)
|
|
else:
|
|
mask = mask.unsqueeze(1)
|
|
elif mask.dim() == 4 and mask.shape[-1] == 1:
|
|
mask = mask.permute(0, 3, 1, 2)
|
|
mask = mask.to(device=device, dtype=dtype)
|
|
if mask.shape[-2:] != (height, width):
|
|
mask = torch.nn.functional.interpolate(mask, size=(height, width), mode="bilinear", align_corners=False)
|
|
return mask.clamp(0.0, 1.0)
|
|
|
|
|
|
@dataclass
|
|
class RGModelHandle:
|
|
model: torch.nn.Module
|
|
device: str
|
|
checkpoint_path: str
|
|
|
|
|
|
class Flux2RGLoadModel:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"checkpoint_path": ("STRING", {"default": "checkpoints/smic_best.pt"}),
|
|
"device": (["cpu", "cuda"], {"default": "cpu"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("RG_MODEL",)
|
|
RETURN_NAMES = ("model",)
|
|
FUNCTION = "load_model"
|
|
CATEGORY = "FLUX2/Resolution Guard"
|
|
|
|
def load_model(self, checkpoint_path: str, device: str):
|
|
model = load_model_checkpoint(checkpoint_path, device=device)
|
|
return (RGModelHandle(model=model, device=device, checkpoint_path=checkpoint_path),)
|
|
|
|
|
|
class Flux2RGApplyCorrection:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model": ("RG_MODEL",),
|
|
"image": ("IMAGE",),
|
|
"mp_ratio": ("FLOAT", {"default": 1.5, "min": 0.5, "max": 8.0, "step": 0.01}),
|
|
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3.0, "step": 0.01}),
|
|
},
|
|
"optional": {
|
|
"anchor_image": ("IMAGE",),
|
|
"mask": ("MASK",),
|
|
"original_image": ("IMAGE",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("image",)
|
|
FUNCTION = "apply"
|
|
CATEGORY = "FLUX2/Resolution Guard"
|
|
|
|
def apply(
|
|
self,
|
|
model: RGModelHandle,
|
|
image: torch.Tensor,
|
|
mp_ratio: float,
|
|
strength: float,
|
|
anchor_image: Optional[torch.Tensor] = None,
|
|
mask: Optional[torch.Tensor] = None,
|
|
original_image: Optional[torch.Tensor] = None,
|
|
):
|
|
net = model.model
|
|
device = next(net.parameters()).device
|
|
image_b = _comfy_image_to_bchw(image).to(device)
|
|
anchor_b = _comfy_image_to_bchw(anchor_image).to(device) if anchor_image is not None else image_b
|
|
original_b = _comfy_image_to_bchw(original_image).to(device) if original_image is not None else image_b
|
|
mask_b = _normalize_mask(mask, image_b.shape[-2], image_b.shape[-1], device, image_b.dtype)
|
|
|
|
with torch.no_grad():
|
|
outputs = net(
|
|
edited=image_b,
|
|
original=original_b,
|
|
anchor=anchor_b,
|
|
mask=mask_b,
|
|
mp_ratio=mp_ratio,
|
|
strength=strength,
|
|
)
|
|
return (_bchw_to_comfy_image(outputs["corrected"].cpu()),)
|
|
|
|
|
|
class Flux2RGAnalyticCompand:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"mp_ratio": ("FLOAT", {"default": 1.5, "min": 0.5, "max": 8.0, "step": 0.01}),
|
|
"strength": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 3.0, "step": 0.01}),
|
|
"blur_sigma": ("FLOAT", {"default": 5.0, "min": 0.1, "max": 32.0, "step": 0.1}),
|
|
},
|
|
"optional": {
|
|
"anchor_image": ("IMAGE",),
|
|
"mask": ("MASK",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("image",)
|
|
FUNCTION = "apply"
|
|
CATEGORY = "FLUX2/Resolution Guard"
|
|
|
|
def apply(
|
|
self,
|
|
image: torch.Tensor,
|
|
mp_ratio: float,
|
|
strength: float,
|
|
blur_sigma: float,
|
|
anchor_image: Optional[torch.Tensor] = None,
|
|
mask: Optional[torch.Tensor] = None,
|
|
):
|
|
image_b = _comfy_image_to_bchw(image)
|
|
anchor_b = _comfy_image_to_bchw(anchor_image) if anchor_image is not None else None
|
|
mask_b = _normalize_mask(mask, image_b.shape[-2], image_b.shape[-1], image_b.device, image_b.dtype) if mask is not None else None
|
|
with torch.no_grad():
|
|
corrected = analytic_compand_correction_tensor(
|
|
image=image_b,
|
|
anchor_image=anchor_b,
|
|
mask=mask_b,
|
|
mp_ratio=mp_ratio,
|
|
strength=strength,
|
|
blur_sigma=blur_sigma,
|
|
)
|
|
return (_bchw_to_comfy_image(corrected.cpu()),)
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"Flux2RGLoadModel": Flux2RGLoadModel,
|
|
"Flux2RGApplyCorrection": Flux2RGApplyCorrection,
|
|
"Flux2RGAnalyticCompand": Flux2RGAnalyticCompand,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"Flux2RGLoadModel": "FLUX2 RG Load Model",
|
|
"Flux2RGApplyCorrection": "FLUX2 RG Apply Correction",
|
|
"Flux2RGAnalyticCompand": "FLUX2 RG Analytic Compand",
|
|
}
|