PerceptionLabPortable / app /nodes /CorticalReconstructionNode.py
Aluode's picture
Upload folder using huggingface_hub
3bb804c verified
"""
CorticalReconstructionNode - Attempts to visualize "brain images" from EEG signals.
---------------------------------------------------------------------------------
This node takes raw EEG or specific frequency band powers and projects them
onto a 2D cortical map, synthesizing a visual representation (reconstructed qualia)
based on brain-inspired principles of spatial organization and dynamic attention.
Inspired by:
- How different frequencies (alpha, theta, gamma) correspond to spatial processing
(Lobe Emergence node).
- Dynamic scanning and gating mechanisms in perception (Theta-Gamma Scanner node).
- The idea of a holographic/fractal memory map encoding visual information.
- The "signal-centric" view where temporal dynamics are crucial for representation.
This is a speculative node for exploring the *concept* of brain-to-image
reconstruction within the Perception Lab's framework.
Place this file in the 'nodes' folder
"""
import numpy as np
import cv2
import __main__
BaseNode = __main__.BaseNode
PA_INSTANCE = getattr(__main__, "PA_INSTANCE", None)
QtGui = __main__.QtGui
try:
from scipy.ndimage import gaussian_filter
SCIPY_AVAILABLE = True
except ImportError:
SCIPY_AVAILABLE = False
print("Warning: CorticalReconstructionNode requires scipy")
class CorticalReconstructionNode(BaseNode):
NODE_CATEGORY = "Visualization" # Or "Cognitive"
NODE_COLOR = QtGui.QColor(100, 50, 200) # Deep Purple
def __init__(self, output_size=128, decay_rate=0.95, alpha_influence=0.3, theta_influence=0.5, gamma_influence=0.8, noise_level=0.01):
super().__init__()
self.node_title = "Cortical Reconstruction"
self.inputs = {
'raw_eeg_signal': 'signal', # Main EEG signal (e.g., raw_signal from EEG node)
'alpha_power': 'signal', # Alpha power (e.g., alpha from EEG node)
'theta_power': 'signal', # Theta power
'gamma_power': 'signal', # Gamma power
'attention_focus': 'image', # Optional: an image mask to bias reconstruction focus
}
self.outputs = {
'reconstructed_image': 'image', # The synthesized "brain image"
'alpha_contribution': 'image', # Visualizing alpha's part
'theta_contribution': 'image', # Visualizing theta's part
'gamma_contribution': 'image', # Visualizing gamma's part
'current_focus': 'image' # Where the node is 'looking'
}
if not SCIPY_AVAILABLE or QtGui is None:
self.node_title = "Cortical Reconstruction (ERROR)"
self._error = True
return
self._error = False
self.output_size = int(output_size)
self.decay_rate = float(decay_rate)
self.alpha_influence = float(alpha_influence) # Higher influence -> more visual output from this band
self.theta_influence = float(theta_influence)
self.gamma_influence = float(gamma_influence)
self.noise_level = float(noise_level)
# Internal 2D "mental canvas"
self.reconstructed_image = np.zeros((self.output_size, self.output_size), dtype=np.float32)
# Initialize some simple spatial filters for each band
# These are highly speculative and can be made more complex
self.alpha_filter = self._create_spatial_filter(self.output_size, 'smooth')
self.theta_filter = self._create_spatial_filter(self.output_size, 'directional')
self.gamma_filter = self._create_spatial_filter(self.output_size, 'detail')
self.alpha_map = np.zeros_like(self.reconstructed_image)
self.theta_map = np.zeros_like(self.reconstructed_image)
self.gamma_map = np.zeros_like(self.reconstructed_image)
self.current_focus_map = np.zeros_like(self.reconstructed_image)
def _create_spatial_filter(self, size, type):
"""Creates a speculative spatial pattern for EEG band influence."""
filter_map = np.zeros((size, size), dtype=np.float32)
if type == 'smooth':
filter_map = gaussian_filter(np.random.rand(size, size), sigma=size/8)
elif type == 'directional':
x = np.linspace(-1, 1, size)
y = np.linspace(-1, 1, size)
X, Y = np.meshgrid(x, y)
angle = np.random.uniform(0, 2 * np.pi)
filter_map = np.cos(X * np.cos(angle) * np.pi * 5 + Y * np.sin(angle) * np.pi * 5)
filter_map = (filter_map + 1) / 2 # Normalize to 0-1
elif type == 'detail':
filter_map = np.random.rand(size, size)
filter_map = cv2.Canny((filter_map * 255).astype(np.uint8), 50, 150) / 255.0 # Edge detection
return filter_map / (filter_map.max() + 1e-9) # Normalize
def step(self):
if self._error: return
# 1. Get EEG band powers (normalized roughly)
raw_eeg = self.get_blended_input('raw_eeg_signal', 'sum') or 0.0
alpha_power = self.get_blended_input('alpha_power', 'sum') or 0.0
theta_power = self.get_blended_input('theta_power', 'sum') or 0.0
gamma_power = self.get_blended_input('gamma_power', 'sum') or 0.0
attention_focus_in = self.get_blended_input('attention_focus', 'mean')
# Basic normalization for input signals (adjust as needed for real EEG ranges)
alpha_power = np.clip(alpha_power, 0, 1) # Assuming 0-1 range for simplicity
theta_power = np.clip(theta_power, 0, 1)
gamma_power = np.clip(gamma_power, 0, 1)
raw_eeg_norm = np.clip(raw_eeg + 0.5, 0, 1) # Roughly center 0 and scale to 0-1
# 2. Update internal "mental canvas" based on EEG bands
# Alpha: Influences smooth, global background or overall brightness
self.alpha_map = self.alpha_filter * alpha_power * self.alpha_influence
# Theta: Influences dynamic, directional elements or larger structures
# We can make theta shift the filter dynamically based on raw_eeg
# (This is a simplified way to model theta's role in "scanning" and memory)
theta_shift_x = int((raw_eeg_norm - 0.5) * 10) # Raw EEG shifts the pattern
theta_shifted_filter = np.roll(self.theta_filter, theta_shift_x, axis=1)
self.theta_map = theta_shifted_filter * theta_power * self.theta_influence
# Gamma: Influences fine details, edges, and sharp features
self.gamma_map = self.gamma_filter * gamma_power * self.gamma_influence
# Combine contributions
current_reconstruction = (self.alpha_map + self.theta_map + self.gamma_map)
# 3. Apply Attention Focus (if provided)
if attention_focus_in is not None:
if attention_focus_in.shape[0] != self.output_size:
attention_focus_in = cv2.resize(attention_focus_in, (self.output_size, self.output_size))
if attention_focus_in.ndim == 3:
attention_focus_in = np.mean(attention_focus_in, axis=2)
# Normalize attention mask
attention_focus_in = attention_focus_in / (attention_focus_in.max() + 1e-9)
self.current_focus_map = gaussian_filter(attention_focus_in, sigma=self.output_size / 20)
# Only parts under focus are strongly reconstructed
current_reconstruction *= (0.5 + 0.5 * self.current_focus_map) # Bias towards focused areas
else:
self.current_focus_map.fill(1.0) # Full attention if no input
# Add some baseline noise for organic feel
current_reconstruction += np.random.rand(self.output_size, self.output_size) * self.noise_level
# Update the main reconstructed image with decay and new input
self.reconstructed_image = self.reconstructed_image * self.decay_rate + current_reconstruction
np.clip(self.reconstructed_image, 0, 1, out=self.reconstructed_image)
# Apply a light gaussian blur for smoother "qualia"
self.reconstructed_image = gaussian_filter(self.reconstructed_image, sigma=0.5)
def get_output(self, port_name):
if self._error: return None
if port_name == 'reconstructed_image':
return self.reconstructed_image
elif port_name == 'alpha_contribution':
return self.alpha_map
elif port_name == 'theta_contribution':
return self.theta_map
elif port_name == 'gamma_contribution':
return self.gamma_map
elif port_name == 'current_focus':
return self.current_focus_map
return None
def get_display_image(self):
if self._error: return None
display_w = 512
display_h = 256
display = np.zeros((display_h, display_w, 3), dtype=np.uint8)
# Left side: Reconstructed Image
reco_u8 = (np.clip(self.reconstructed_image, 0, 1) * 255).astype(np.uint8)
reco_color = cv2.cvtColor(reco_u8, cv2.COLOR_GRAY2RGB)
reco_resized = cv2.resize(reco_color, (display_h, display_h), interpolation=cv2.INTER_LINEAR)
display[:, :display_h] = reco_resized
# Right side: Band Contributions and Focus (blended for visualization)
# Alpha: Green, Theta: Blue, Gamma: Red
contributions_rgb = np.zeros((self.output_size, self.output_size, 3), dtype=np.float32)
contributions_rgb[:, :, 0] = self.gamma_map # Red for Gamma (details)
contributions_rgb[:, :, 1] = self.alpha_map # Green for Alpha (smoothness)
contributions_rgb[:, :, 2] = self.theta_map # Blue for Theta (motion/structure)
# Overlay focus map as an intensity
focus_overlay = np.stack([self.current_focus_map]*3, axis=-1)
contributions_rgb = (contributions_rgb * (0.5 + 0.5 * focus_overlay)) # Dim if not focused
contr_u8 = (np.clip(contributions_rgb, 0, 1) * 255).astype(np.uint8)
contr_resized = cv2.resize(contr_u8, (display_h, display_h), interpolation=cv2.INTER_LINEAR)
display[:, display_w-display_h:] = contr_resized
# Add dividing line
display[:, display_h-1:display_h+1] = [255, 255, 255]
# Add labels
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(display, 'RECONSTRUCTED QUALIA', (10, 20), font, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
cv2.putText(display, 'BAND CONTRIBUTIONS & FOCUS', (display_h + 10, 20), font, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
# Add input values for context
alpha_val = self.get_blended_input('alpha_power', 'sum') or 0.0
theta_val = self.get_blended_input('theta_power', 'sum') or 0.0
gamma_val = self.get_blended_input('gamma_power', 'sum') or 0.0
cv2.putText(display, f"ALPHA: {alpha_val:.2f}", (10, display_h - 40), font, 0.4, (0, 255, 0), 1, cv2.LINE_AA)
cv2.putText(display, f"THETA: {theta_val:.2f}", (10, display_h - 25), font, 0.4, (255, 0, 0), 1, cv2.LINE_AA)
cv2.putText(display, f"GAMMA: {gamma_val:.2f}", (10, display_h - 10), font, 0.4, (0, 0, 255), 1, cv2.LINE_AA) # Changed to blue for theta, red for gamma
display = np.ascontiguousarray(display)
return QtGui.QImage(display.data, display_w, display_h, 3*display_w, QtGui.QImage.Format.Format_RGB888)
def get_config_options(self):
return [
("Output Size", "output_size", self.output_size, None),
("Decay Rate", "decay_rate", self.decay_rate, None),
("Alpha Influence", "alpha_influence", self.alpha_influence, None),
("Theta Influence", "theta_influence", self.theta_influence, None),
("Gamma Influence", "gamma_influence", self.gamma_influence, None),
("Noise Level", "noise_level", self.noise_level, None),
]