Spaces:
Running
Running
| """ | |
| Moiré Interference Node - Generates a 2D moiré pattern by interfering | |
| two perpendicular sine waves. The frequencies of the waves are | |
| controlled by the signal inputs. | |
| Ported from moire_microscope.html | |
| """ | |
| import numpy as np | |
| from PyQt6 import QtGui | |
| import cv2 | |
| import __main__ | |
| BaseNode = __main__.BaseNode | |
| PA_INSTANCE = getattr(__main__, "PA_INSTANCE", None) | |
| QtGui = __main__.QtGui | |
| class MoireInterferenceNode(BaseNode): | |
| NODE_CATEGORY = "Transform" | |
| NODE_COLOR = QtGui.QColor(100, 180, 180) # Moiré Teal | |
| def __init__(self, size=128, base_phase_1=0.0, base_phase_2=0.0): | |
| super().__init__() | |
| self.node_title = "Moiré Interference" | |
| self.size = int(size) | |
| self.base_phase_1 = float(base_phase_1) | |
| self.base_phase_2 = float(base_phase_2) | |
| self.inputs = { | |
| 'freq_1': 'signal', # Controls frequency of horizontal wave | |
| 'freq_2': 'signal' # Controls frequency of vertical wave | |
| } | |
| self.outputs = {'image': 'image'} | |
| # Pre-calculate coordinate grids | |
| self._init_grids() | |
| self.output_image = np.zeros((self.size, self.size), dtype=np.float32) | |
| def _init_grids(self): | |
| """Creates normalized coordinate grids [0, 1]""" | |
| if self.size == 0: self.size = 1 # Avoid division by zero | |
| u_vec = np.linspace(0, 1, self.size, dtype=np.float32) | |
| v_vec = np.linspace(0, 1, self.size, dtype=np.float32) | |
| # V (rows, 0->1), U (cols, 0->1) | |
| self.U, self.V = np.meshgrid(u_vec, v_vec) | |
| self.output_image = np.zeros((self.size, self.size), dtype=np.float32) | |
| def step(self): | |
| # Check if size changed from config | |
| if self.U.shape[0] != self.size: | |
| self._init_grids() | |
| # 1. Get frequency inputs | |
| # We map the input signal (range -1 to 1) to a k-value (frequency) | |
| # e.g., mapping to a range of [5, 45] | |
| k1 = ((self.get_blended_input('freq_1', 'sum') or 0.0) + 1.0) * 20.0 + 5.0 | |
| k2 = ((self.get_blended_input('freq_2', 'sum') or 0.0) + 1.0) * 20.0 + 5.0 | |
| # 2. Port the core math from moire_microscope.html | |
| # const field1 = Math.sin(u * 20 * Math.PI + phase1); | |
| # const field2 = Math.cos(v * 20 * Math.PI + phase2); | |
| # const moireValue = Math.cos(field1 * Math.PI - field2 * Math.PI); | |
| # We use U (horizontal grid) for field 1 and V (vertical grid) for field 2 | |
| field1 = np.sin(self.U * k1 * np.pi + self.base_phase_1) | |
| field2 = np.cos(self.V * k2 * np.pi + self.base_phase_2) | |
| # The interference pattern | |
| moire_value = np.cos(field1 * np.pi - field2 * np.pi) | |
| # 3. Normalize [-1, 1] to [0, 1] for image output | |
| self.output_image = (moire_value + 1.0) / 2.0 | |
| def get_output(self, port_name): | |
| if port_name == 'image': | |
| return self.output_image | |
| return None | |
| def get_display_image(self): | |
| img_u8 = (np.clip(self.output_image, 0, 1) * 255).astype(np.uint8) | |
| img_u8 = np.ascontiguousarray(img_u8) | |
| return QtGui.QImage(img_u8.data, self.size, self.size, self.size, QtGui.QImage.Format.Format_Grayscale8) | |
| def get_config_options(self): | |
| return [ | |
| ("Resolution", "size", self.size, None), | |
| ("Base Phase 1", "base_phase_1", self.base_phase_1, None), | |
| ("Base Phase 2", "base_phase_2", self.base_phase_2, None), | |
| ] |