File size: 5,475 Bytes
3bb804c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""

Antti's Wave Mirror - Learns an image, then evolves it.

Inspired by mirror.py

Place this file in the 'nodes' folder

"""

import numpy as np
from PyQt6 import QtGui
import cv2
import time

import sys
import os
# --- This is the new, correct block ---
import __main__
BaseNode = __main__.BaseNode
PA_INSTANCE = getattr(__main__, "PA_INSTANCE", None)
# ------------------------------------

class WaveNeuron:
    """Simplified WaveNeuron class from mirror.py"""
    def __init__(self, w, h):
        # WaveNeuron is designed for grayscale/single-channel data (w, h)
        self.frequency = np.random.uniform(0.1, 1.0, (h, w)).astype(np.float32)
        self.amplitude = np.random.uniform(0.5, 1.0, (h, w)).astype(np.float32)
        self.phase = np.random.uniform(0, 2 * np.pi, (h, w)).astype(np.float32)
        
    def activate(self, input_signal, t):
        # Vectorized activation
        return self.amplitude * np.sin(2 * np.pi * self.frequency * t + self.phase) + input_signal
        
    def train(self, target, t, learning_rate):
        # Target must be (h, w) shape to match output
        output = self.activate(0, t) # Get internal activation
        error = target - output
        
        sin_term = np.sin(2 * np.pi * self.frequency * t + self.phase)
        cos_term = np.cos(2 * np.pi * self.frequency * t + self.phase)
        
        self.amplitude += learning_rate * error * sin_term
        self.phase += learning_rate * error * self.amplitude * cos_term
        self.frequency += learning_rate * error * self.amplitude * (2 * np.pi * t) * cos_term
        
        # Clamp values to reasonable ranges
        self.amplitude = np.clip(self.amplitude, 0.1, 2.0)
        self.frequency = np.clip(self.frequency, 0.01, 2.0)

class WaveMirrorNode(BaseNode):
    NODE_CATEGORY = "Transform"
    NODE_COLOR = QtGui.QColor(60, 180, 160) # A teal/aqua color
    
    def __init__(self, width=80, height=60, training_duration=300):
        super().__init__()
        self.node_title = "Antti's Mirror"
        
        self.inputs = {'image_in': 'image'}
        self.outputs = {'image_out': 'image'}
        
        self.w, self.h = int(width), int(height)
        self.training_duration = int(training_duration)
        self.learning_rate = 0.01
        
        # Internal state
        self.wnn = WaveNeuron(self.w, self.h)
        self.output_image = np.zeros((self.h, self.w), dtype=np.float32)
        self.training_counter = 0
        self.start_time = time.time()
        self.is_trained = False

    def step(self):
        t = time.time() - self.start_time
        input_image = self.get_blended_input('image_in', 'mean')
        
        if input_image is None:
            input_image = np.zeros((self.h, self.w), dtype=np.float32)
        else:
            # 1. Resize the input
            input_image = cv2.resize(input_image, (self.w, self.h), interpolation=cv2.INTER_AREA)

            # 2. FIX: Convert to Grayscale if the input is color (ndim == 3)
            if input_image.ndim == 3:
                # Convert BGR/RGB to Grayscale (assuming input is float 0-1)
                input_image = cv2.cvtColor(input_image.astype(np.float32), cv2.COLOR_BGR2GRAY)
            
        if self.training_counter < self.training_duration:
            # --- Training Phase ---
            self.wnn.train(input_image, t, self.learning_rate)
            self.training_counter += 1
            # Show the input image while training
            self.output_image = input_image
            self.is_trained = False
        else:
            # --- Evolution Phase ---
            if not self.is_trained:
                self.is_trained = True
                print("WaveMirror: Training complete. Entering evolution phase.")
                
            # "Lives its own life" by using 0 as input
            input_signal = np.zeros((self.h, self.w), dtype=np.float32)
            self.output_image = self.wnn.activate(input_signal, t)

    def get_output(self, port_name):
        if port_name == 'image_out':
            # Normalize for output
            out = self.output_image - np.min(self.output_image)
            out_max = np.max(out)
            if out_max > 1e-6:
                out = out / out_max
            return out
        return None
        
    def get_display_image(self):
        # Display internal state
        out_img = self.get_output('image_out')
        if out_img is None:
            out_img = np.zeros((self.h, self.w), dtype=np.float32)
            
        img_u8 = (np.clip(out_img, 0, 1) * 255).astype(np.uint8)
        
        # Add status bar
        if not self.is_trained:
            status_color = (0, 255, 0) # Green for training
            progress = int((self.training_counter / self.training_duration) * self.w)
            cv2.rectangle(img_u8, (0, self.h - 5), (progress, self.h - 1), status_color, -1)
        else:
            status_color = (0, 0, 255) # Red for evolving
            cv2.rectangle(img_u8, (0, self.h - 5), (self.w - 1, self.h - 1), status_color, -1)

        img_u8 = np.ascontiguousarray(img_u8)
        return QtGui.QImage(img_u8.data, self.w, self.h, self.w, QtGui.QImage.Format.Format_Grayscale8)

    def get_config_options(self):
        return [
            ("Training Frames", "training_duration", self.training_duration, None)
        ]