"""Immerse detail — overlay high-resolution detail onto lower-resolution overview.""" from __future__ import annotations import numpy as np from backend.node_registry import register_node from backend.data_types import DataField @register_node(display_name="Immerse Detail") class ImmerseDetail: @classmethod def INPUT_TYPES(cls): return { "required": { "overview": ("DATA_FIELD",), "detail": ("DATA_FIELD",), "blend": (["replace", "average"], {"default": "replace"}), } } OUTPUTS = ( ('DATA_FIELD', 'combined'), ) FUNCTION = "process" DESCRIPTION = ( "Overlay a high-resolution detail scan onto a lower-resolution overview " "image using cross-correlation to find the best position. " ) KEYWORDS = ("inset", "zoom", "merge", "overlay") def process(self, overview: DataField, detail: DataField, blend: str) -> tuple: ov = np.asarray(overview.data, dtype=np.float64) dt = np.asarray(detail.data, dtype=np.float64) # Resample detail to overview pixel size if needed scale_x = detail.dx / overview.dx scale_y = detail.dy / overview.dy if abs(scale_x - 1.0) > 0.01 or abs(scale_y - 1.0) > 0.01: from scipy.ndimage import zoom dt = zoom(dt, (scale_y, scale_x), order=1) dy_res, dx_res = dt.shape oy_res, ox_res = ov.shape if dy_res >= oy_res or dx_res >= ox_res: # Detail is larger than overview, just return overview return (overview,) # Cross-correlate to find best position # Use a sliding window approach for small detail best_score = -np.inf best_y, best_x = 0, 0 dt_norm = dt - dt.mean() dt_std = dt.std() if dt_std < 1e-30: dt_std = 1.0 # Coarse search with stride stride = max(1, min(dy_res, dx_res) // 4) for iy in range(0, oy_res - dy_res + 1, stride): for ix in range(0, ox_res - dx_res + 1, stride): patch = ov[iy:iy + dy_res, ix:ix + dx_res] score = np.sum((patch - patch.mean()) * dt_norm) / (patch.std() * dt_std + 1e-30) if score > best_score: best_score = score best_y, best_x = iy, ix # Fine search around best position for iy in range(max(0, best_y - stride), min(oy_res - dy_res + 1, best_y + stride + 1)): for ix in range(max(0, best_x - stride), min(ox_res - dx_res + 1, best_x + stride + 1)): patch = ov[iy:iy + dy_res, ix:ix + dx_res] score = np.sum((patch - patch.mean()) * dt_norm) / (patch.std() * dt_std + 1e-30) if score > best_score: best_score = score best_y, best_x = iy, ix # Place detail into overview result = ov.copy() if blend == "replace": result[best_y:best_y + dy_res, best_x:best_x + dx_res] = dt else: # average result[best_y:best_y + dy_res, best_x:best_x + dx_res] = \ 0.5 * (ov[best_y:best_y + dy_res, best_x:best_x + dx_res] + dt) return (overview.replace(data=result),)