Real-time Brain Wave Processing System: Data Visualization


Introduction
Welcome to the third and final article in our series on building a real-time brain wave processing system. In the Unlock the Secrets of Your Mind: Building a Real-time Brain Wave System, we explored the overall architecture and design philosophy. In the Inside Your Brain's Control Room: Generating and Decoding Neural Signals, we delved into the implementation details of the Data Generator and Data Analyzer components.
Now, we'll focus on the Data Visualizer component, which creates an interactive web dashboard for visualizing brain wave analysis results in real-time. This component is crucial for making the complex frequency data accessible and meaningful to users.
For the complete source code and to contribute, visit our GitHub repository.
Data visualizer implementation
The Data Visualizer component receives analysis results from the Data Analyzer and displays them in an interactive web dashboard using the Dash framework. This allows users to monitor brain wave activity in real-time and observe changes in brain states.
Class structure
The main class, BrainWaveVisualizer
, handles the reception of analysis results and the creation of the web dashboard:
import zmq
import dash
import dash_bootstrap_components as dbc
from collections import deque
import time
import json
import threading
# Constants
ANALYZER_PORT = 5556 # Default port for data analyzer
BUFFER_SIZE = 100 # Default buffer size for visualization
class BrainWaveVisualizer:
"""Brain wave data visualization class"""
def __init__(self, port: int = ANALYZER_PORT):
"""
Initialization
Args:
port: ZeroMQ port number of the data analyzer
"""
self.port = port
self.running = False
# Data buffer
self.timestamps = deque(maxlen=BUFFER_SIZE)
self.delta_powers = deque(maxlen=BUFFER_SIZE)
self.theta_powers = deque(maxlen=BUFFER_SIZE)
self.alpha_powers = deque(maxlen=BUFFER_SIZE)
self.beta_powers = deque(maxlen=BUFFER_SIZE)
# Current brain wave state
self.current_state = "normal"
# ZeroMQ initialization
self.context = zmq.Context()
self.socket = self.context.socket(zmq.SUB)
self.socket.connect(f"tcp://localhost:{self.port}")
self.socket.setsockopt_string(zmq.SUBSCRIBE, "") # Subscribe to all messages
# Dash application initialization
self.app = dash.Dash(
__name__,
external_stylesheets=[dbc.themes.BOOTSTRAP],
update_title=None # Hide title during updates
)
# Layout settings
self.setup_layout()
# Callback settings
self.setup_callbacks()
The constructor initializes the visualizer with a configurable port for receiving analysis results. It also sets up:
Data buffers for storing time series data (using
deque
with a fixed maximum length)ZeroMQ subscriber socket for receiving analysis results
Dash application with Bootstrap styling
Dashboard layout and callbacks
Dashboard layout
The setup_layout
method defines the structure and appearance of the web dashboard:
def setup_layout(self):
"""Set up the layout of the Dash application"""
self.app.layout = dbc.Container([
dbc.Row([
dbc.Col([
html.H1("Real-time Brain Wave Analysis Dashboard", className="text-center my-4"),
html.Hr(),
], width=12)
]),
dbc.Row([
dbc.Col([
# Brain wave power graph
dbc.Card([
dbc.CardHeader(html.H4("Brain Wave Power Trends", className="text-center")),
dbc.CardBody([
dcc.Graph(
id='brain-wave-graph',
config={'displayModeBar': False},
figure=self.create_empty_figure()
),
dcc.Interval(
id='graph-update-interval',
interval=500, # Update every 500 milliseconds
n_intervals=0
)
])
]),
], width=12)
]),
dbc.Row([
dbc.Col([
# Current brain wave state
dbc.Card([
dbc.CardHeader(html.H4("Current State", className="text-center")),
dbc.CardBody([
html.Div(id='brain-state-display', className="text-center h3")
])
]),
], width=12, className="mt-4")
]),
dbc.Row([
dbc.Col([
# Display of each brain wave power
dbc.Card([
dbc.CardHeader(html.H4("Brain Wave Powers", className="text-center")),
dbc.CardBody([
dbc.Row([
dbc.Col([
html.Div([
html.Span("Delta: ", className="h5"),
html.Span(id='delta-power', className="h5 text-primary")
], className="mb-2"),
html.Div([
html.Span("Theta: ", className="h5"),
html.Span(id='theta-power', className="h5 text-warning")
], className="mb-2"),
], width=6),
dbc.Col([
html.Div([
html.Span("Alpha: ", className="h5"),
html.Span(id='alpha-power', className="h5 text-success")
], className="mb-2"),
html.Div([
html.Span("Beta: ", className="h5"),
html.Span(id='beta-power', className="h5 text-danger")
], className="mb-2"),
], width=6),
])
])
]),
], width=12, className="mt-4 mb-4")
])
], fluid=True)
The dashboard layout consists of:
A header with the dashboard title
A graph showing the power trends for each brain wave band
A display of the current brain wave state
A display of the relative power (%) for each brain wave band
The layout uses Bootstrap components (via dash-bootstrap-components
) for responsive design and consistent styling.
Interactive callbacks
The setup_callbacks
method defines the interactive behavior of the dashboard:
def setup_callbacks(self):
"""Set up callbacks for the Dash application"""
# Graph update callback
@self.app.callback(
Output('brain-wave-graph', 'figure'),
Input('graph-update-interval', 'n_intervals')
)
def update_graph(n):
return self.create_figure()
# Brain wave state display update callback
@self.app.callback(
Output('brain-state-display', 'children'),
Input('graph-update-interval', 'n_intervals')
)
def update_brain_state(n):
state_text = BRAIN_STATES.get(self.current_state, "Unknown")
return f"{state_text}"
# Update callback for each brain wave power display
@self.app.callback(
[
Output('delta-power', 'children'),
Output('theta-power', 'children'),
Output('alpha-power', 'children'),
Output('beta-power', 'children')
],
Input('graph-update-interval', 'n_intervals')
)
def update_power_values(n):
if len(self.delta_powers) > 0:
delta = self.delta_powers[-1]
theta = self.theta_powers[-1]
alpha = self.alpha_powers[-1]
beta = self.beta_powers[-1]
# Normalize to calculate relative power
total = delta + theta + alpha + beta
if total > 0:
delta_percent = (delta / total) * 100
theta_percent = (theta / total) * 100
alpha_percent = (alpha / total) * 100
beta_percent = (beta / total) * 100
# Determine brain wave state
self.determine_brain_state(delta_percent, theta_percent, alpha_percent, beta_percent)
return [
f"{delta_percent:.1f}%",
f"{theta_percent:.1f}%",
f"{alpha_percent:.1f}%",
f"{beta_percent:.1f}%"
]
return ["0.0%", "0.0%", "0.0%", "0.0%"]
This method sets up three main callbacks:
update_graph
: Updates the brain wave power trends graph every 500 millisecondsupdate_brain_state
: Updates the display of the current brain wave stateupdate_power_values
: Updates the display of the relative power for each brain wave band and determines the current brain state
The callbacks are triggered by the graph-update-interval
component, which fires every 500 milliseconds.
Brain wave state determination
The determine_brain_state
method analyzes the relative power in each frequency band to determine the current brain state:
def determine_brain_state(self, delta, theta, alpha, beta):
"""
Determine current state from brain wave power distribution
Args:
delta: Delta wave power (%)
theta: Theta wave power (%)
alpha: Alpha wave power (%)
beta: Beta wave power (%)
"""
if delta > 50:
self.current_state = "deep_sleep"
elif alpha > 40:
self.current_state = "relaxed"
elif beta > 40:
self.current_state = "focused"
elif theta > 30 and alpha > 20:
self.current_state = "meditative"
else:
self.current_state = "normal"
This method implements a simple rule-based approach to determine the brain state based on the relative power in each frequency band.
Creating interactive graphs
The create_figure
method generates the interactive graph for displaying brain wave power trends:
import plotly.graph_objects as go
# Define colors for each wave type
WAVE_COLORS = {
'delta': '#0000FF', # Blue
'theta': '#FFA500', # Orange
'alpha': '#00FF00', # Green
'beta': '#FF0000' # Red
}
def create_figure(self):
"""Create a graph with current data"""
fig = go.Figure()
# Add traces for each brain wave
fig.add_trace(go.Scatter(
x=list(self.timestamps),
y=list(self.delta_powers),
name='Delta',
mode='lines',
line=dict(color=WAVE_COLORS['delta'], width=2)
))
fig.add_trace(go.Scatter(
x=list(self.timestamps),
y=list(self.theta_powers),
name='Theta',
mode='lines',
line=dict(color=WAVE_COLORS['theta'], width=2)
))
fig.add_trace(go.Scatter(
x=list(self.timestamps),
y=list(self.alpha_powers),
name='Alpha',
mode='lines',
line=dict(color=WAVE_COLORS['alpha'], width=2)
))
fig.add_trace(go.Scatter(
x=list(self.timestamps),
y=list(self.beta_powers),
name='Beta',
mode='lines',
line=dict(color=WAVE_COLORS['beta'], width=2)
))
# Layout settings
fig.update_layout(
title=None,
xaxis=dict(
title='Measurement Time',
showgrid=True,
gridcolor='rgba(211, 211, 211, 0.5)',
# Display as time
type='date',
tickformat='%H:%M:%S'
),
yaxis=dict(
title='Power',
showgrid=True,
gridcolor='rgba(211, 211, 211, 0.5)'
),
margin=dict(l=40, r=20, t=10, b=40),
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="center",
x=0.5
),
plot_bgcolor='rgba(255, 255, 255, 1)',
paper_bgcolor='rgba(255, 255, 255, 1)',
hovermode='x unified'
)
# Dynamically adjust X-axis range and convert to date-time format
if len(self.timestamps) > 1:
# Convert timestamps to JavaScript date-time format (milliseconds)
date_times = [self.unix_to_js_time(t) for t in self.timestamps]
# Update data in date-time format
for i in range(len(fig.data)):
fig.data[i].x = date_times
# Set X-axis range
fig.update_xaxes(range=[min(date_times), max(date_times)])
return fig
This method:
Creates a Plotly figure with separate traces for each brain wave band
Configures the layout for optimal visualization
Converts UNIX timestamps to JavaScript date-time format for proper display
Dynamically adjusts the X-axis range to show all available data
Data reception thread
The receive_data
method runs in a separate thread to receive analysis results without blocking the web server:
def receive_data(self):
"""Data receiving thread"""
print(f"Starting data reception (port: {self.port})")
while self.running:
try:
# Receive data non-blocking
message = self.socket.recv_json(flags=zmq.NOBLOCK)
# Get timestamp and each brain wave power
timestamp = message.get('timestamp')
delta = message.get('delta_power')
theta = message.get('theta_power')
alpha = message.get('alpha_power')
beta = message.get('beta_power')
# Add to buffer if data is valid
if timestamp is not None and delta is not None and theta is not None and alpha is not None and beta is not None:
self.timestamps.append(timestamp)
self.delta_powers.append(delta)
self.theta_powers.append(theta)
self.alpha_powers.append(alpha)
self.beta_powers.append(beta)
except zmq.ZMQError as e:
if e.errno == zmq.EAGAIN:
# Wait a bit if no data yet
time.sleep(0.1)
else:
print(f"ZeroMQ error: {e}")
time.sleep(1)
except json.JSONDecodeError as e:
print(f"JSON decode error: {e}")
# Skip this message and continue
except Exception as e:
print(f"An unexpected error occurred: {e}")
import traceback
traceback.print_exc()
time.sleep(1)
This method:
Receives analysis results from the Data Analyzer via ZeroMQ
Extracts the timestamp and power values for each frequency band
Adds the data to the buffers if it's valid
Handles various error conditions gracefully
Starting the visualizer
The start
method launches the data reception thread and the Dash web server:
def start(self, host='0.0.0.0', port=8050):
"""
Start the visualizer
Args:
host: Dash server host
port: Dash server port
"""
self.running = True
# Start data receiving thread
self.receiver_thread = threading.Thread(target=self.receive_data)
self.receiver_thread.daemon = True
self.receiver_thread.start()
# Display access methods
print(f"Brain wave visualization dashboard started")
print(f"Please access the following URLs:")
print(f" * Local access: http://localhost:{port}/")
print(f" * Network access: http://<IP address>:{port}/")
# Start Dash application
self.app.run_server(debug=False, host=host, port=port)
This method:
Starts the data reception thread as a daemon thread (so it will terminate when the main thread exits)
Displays information about how to access the dashboard
Starts the Dash web server
Technical Challenges and Solutions
Challenge 1: Real-time visualization performance
Updating visualizations in real-time can be resource-intensive and may lead to performance issues. The system addresses this challenge through:
Fixed-size Data Buffers: Using
collections.deque
with a fixed maximum length (default: 100 points) to limit memory usage and rendering complexity.Efficient Graph Updates: Using Plotly's efficient update mechanisms to modify only the necessary parts of the graph.
Throttled Updates: Updating the dashboard every 500 milliseconds instead of on every data point, balancing responsiveness and performance.
Challenge 2: Multi-threaded architecture
The visualizer needs to receive data and update the web dashboard simultaneously, which requires a multi-threaded approach:
Separate Data Reception Thread: Running the data reception in a separate thread to avoid blocking the web server.
Thread-safe Data Structures: Using thread-safe data structures (
deque
) to avoid race conditions.Non-blocking ZeroMQ Reception: Using non-blocking reception to avoid hanging the data thread.
Challenge 3: User experience
Creating an intuitive and responsive user interface for complex brain wave data:
Clear Visual Hierarchy: Organizing the dashboard with a clear visual hierarchy to highlight the most important information.
Color Coding: Using consistent colors for each brain wave band to make the graph easier to interpret.
Responsive Design: Using Bootstrap components for a responsive layout that works on different screen sizes.
Extensibility and real-world applications
The visualization component can be extended in several ways to support real-world applications:
1. Advanced visualization techniques
The current implementation uses simple line graphs to display brain wave power trends. This could be extended with:
Topographic Mapping: Creating 2D or 3D visualizations of brain activity across the scalp.
Spectrograms: Showing the full frequency spectrum over time using color-coded intensity maps.
Coherence Visualization: Displaying the synchronization between different brain regions.
2. Integration with real EEG devices
The system can be extended to work with real EEG devices by:
Device-specific Adapters: Creating adapters for popular EEG devices like OpenBCI, Muse, or Emotiv.
Real-time Artifact Rejection: Implementing algorithms to detect and remove artifacts from eye blinks, muscle movement, etc.
Calibration Interfaces: Adding interfaces for device calibration and signal quality monitoring.
3. Clinical and research applications
The visualization system can be adapted for various clinical and research applications:
Sleep Analysis: Extended visualization for sleep stage classification and sleep disorder diagnosis.
Neurofeedback Training: Interactive visualizations that respond to specific brain states for training purposes.
Cognitive Workload Assessment: Visualizations that highlight changes in cognitive load during tasks.
Meditation Assistance: Specialized displays for meditation practice that emphasize relevant brain states.
4. Consumer Applications
The system can also be adapted for consumer applications:
Mobile Interfaces: Creating mobile-friendly visualizations for portable EEG devices.
Gamification: Adding game-like elements to make brain wave monitoring more engaging.
Integration with Smart Home: Using brain states to control smart home devices or ambient environments.
Conclusion
In this article, we've explored the implementation details of the Data Visualizer component of our real-time brain wave processing system. This component creates an interactive web dashboard that makes complex brain wave data accessible and meaningful to users.
The visualizer uses the Dash framework to create a responsive and interactive dashboard that displays brain wave power trends, the current brain state, and the relative power in each frequency band. It communicates with the Data Analyzer component via ZeroMQ and updates the dashboard in real-time.
Throughout this three-part series, we've explored the architecture, design, and implementation of a complete real-time brain wave processing system. The system demonstrates how to build a modular, loosely coupled framework for processing and visualizing brain wave data in real-time.
By open-sourcing this system, I hope to contribute to the growing ecosystem of neurotechnology tools and enable researchers, developers, and enthusiasts to build innovative brain-computer interface applications.
Note: This article describes a simplified version of brain wave processing systems I've developed for major electronics manufacturers. The actual implementation details of proprietary systems remain confidential.
Subscribe to my newsletter
Read articles from Aine LLC. directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
