Files
bemoregt-ComfyUI_PhaseStret…/pst_node.py
T

160 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
import torch
from scipy import ndimage
def phase_stretch_transform(img_gray: np.ndarray,
lpf_sigma: float = 0.21,
phase_strength: float = 0.48,
warp_strength: float = 12.14,
morph_flag: bool = True,
min_thresh: float = -1.0,
max_thresh: float = 0.003) -> tuple[np.ndarray, np.ndarray]:
"""
Phase Stretch Transform (PST)
Returns:
phase_image : raw phase output (float, roughly -pi..pi)
edge_image : binary or soft edge map (float 0..1)
"""
H, W = img_gray.shape
L = 0.5
fx = np.linspace(-L, L, W)
fy = np.linspace(-L, L, H)
FX, FY = np.meshgrid(fx, fy)
R = np.sqrt(FX ** 2 + FY ** 2) # radial frequency grid
# ── Low-pass filter (Gaussian) ───────────────────────────────────────────
lpf = np.exp(-0.5 * (R / (lpf_sigma + 1e-9)) ** 2)
img_fft = np.fft.fftshift(np.fft.fft2(img_gray.astype(np.float64)))
img_filtered = img_fft * lpf
# ── PST kernel (warped phase ramp) ───────────────────────────────────────
W_val = warp_strength
S_val = phase_strength
pst_kernel = (W_val * R / (1.0 + W_val * R)) * np.exp(-1j * S_val * R / (1.0 + R))
img_pst = np.fft.ifft2(np.fft.ifftshift(img_filtered * pst_kernel))
# ── Phase angle ──────────────────────────────────────────────────────────
phase = np.angle(img_pst) # range: -π … π
if morph_flag:
edges = ((phase > max_thresh) | (phase < min_thresh)).astype(np.float64)
# small morphological clean-up
edges = ndimage.binary_closing(edges, structure=np.ones((3, 3))).astype(np.float64)
edges = ndimage.binary_opening(edges, structure=np.ones((3, 3))).astype(np.float64)
else:
# soft edge map – normalise phase to 0..1
edges = (phase - phase.min()) / (phase.max() - phase.min() + 1e-9)
return phase, edges
class PhaseStretchTransformNode:
"""ComfyUI node – Phase Stretch Transform edge detector."""
CATEGORY = "image/processing"
RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("edge_image", "phase_image")
FUNCTION = "run"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"lpf_sigma": ("FLOAT", {
"default": 0.21, "min": 0.01, "max": 1.0, "step": 0.01,
"tooltip": "Low-pass filter sigma – higher = more smoothing before PST"
}),
"phase_strength": ("FLOAT", {
"default": 0.48, "min": 0.0, "max": 3.0, "step": 0.01,
"tooltip": "PST phase strength (S)"
}),
"warp_strength": ("FLOAT", {
"default": 12.14, "min": 0.0, "max": 100.0, "step": 0.1,
"tooltip": "PST warp strength (W)"
}),
"min_thresh": ("FLOAT", {
"default": -1.0, "min": -3.15, "max": 0.0, "step": 0.001,
"tooltip": "Lower phase threshold for binary edge map"
}),
"max_thresh": ("FLOAT", {
"default": 0.003, "min": 0.0, "max": 3.15, "step": 0.001,
"tooltip": "Upper phase threshold for binary edge map"
}),
"morph_flag": ("BOOLEAN", {
"default": True,
"tooltip": "Apply morphological clean-up (binary edges); "
"False returns normalised phase image"
}),
"output_mode": (["grayscale", "rgb_overlay"], {
"default": "grayscale",
"tooltip": "grayscale = white edges on black; "
"rgb_overlay = edges overlaid on original"
}),
},
}
# ------------------------------------------------------------------
def run(self, image: torch.Tensor,
lpf_sigma: float,
phase_strength: float,
warp_strength: float,
min_thresh: float,
max_thresh: float,
morph_flag: bool,
output_mode: str):
"""
image shape: (B, H, W, C) – ComfyUI convention, float32 0..1
"""
B, H, W, C = image.shape
edge_frames = []
phase_frames = []
for b in range(B):
frame = image[b].cpu().numpy() # (H, W, C)
# Convert to grayscale for PST (luminance weights)
if C == 1:
gray = frame[..., 0]
else:
gray = 0.2126 * frame[..., 0] + \
0.7152 * frame[..., 1] + \
0.0722 * frame[..., 2]
phase, edges = phase_stretch_transform(
gray,
lpf_sigma=lpf_sigma,
phase_strength=phase_strength,
warp_strength=warp_strength,
morph_flag=morph_flag,
min_thresh=min_thresh,
max_thresh=max_thresh,
)
# ── edge output ──────────────────────────────────────────────────
if output_mode == "rgb_overlay":
overlay = frame.copy()
mask = edges > 0.5
overlay[mask] = np.array([1.0, 0.2, 0.2], dtype=np.float32)
edge_out = overlay.astype(np.float32)
else:
edge_rgb = np.stack([edges] * 3, axis=-1).astype(np.float32)
edge_out = edge_rgb
# ── phase output (normalised to 0..1, RGB) ───────────────────────
phase_norm = (phase - phase.min()) / (phase.max() - phase.min() + 1e-9)
phase_out = np.stack([phase_norm] * 3, axis=-1).astype(np.float32)
edge_frames.append(torch.from_numpy(edge_out))
phase_frames.append(torch.from_numpy(phase_out))
edge_tensor = torch.stack(edge_frames, dim=0) # (B, H, W, 3)
phase_tensor = torch.stack(phase_frames, dim=0) # (B, H, W, 3)
return edge_tensor, phase_tensor