Real-time Brain Wave Processing System: Data Visualization

Aine LLC.Aine LLC.
10 min read

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:

  1. A header with the dashboard title

  2. A graph showing the power trends for each brain wave band

  3. A display of the current brain wave state

  4. 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:

  1. update_graph: Updates the brain wave power trends graph every 500 milliseconds

  2. update_brain_state: Updates the display of the current brain wave state

  3. update_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:

  1. Creates a Plotly figure with separate traces for each brain wave band

  2. Configures the layout for optimal visualization

  3. Converts UNIX timestamps to JavaScript date-time format for proper display

  4. 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:

  1. Receives analysis results from the Data Analyzer via ZeroMQ

  2. Extracts the timestamp and power values for each frequency band

  3. Adds the data to the buffers if it's valid

  4. 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:

  1. Starts the data reception thread as a daemon thread (so it will terminate when the main thread exits)

  2. Displays information about how to access the dashboard

  3. 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:

  1. Fixed-size Data Buffers: Using collections.deque with a fixed maximum length (default: 100 points) to limit memory usage and rendering complexity.

  2. Efficient Graph Updates: Using Plotly's efficient update mechanisms to modify only the necessary parts of the graph.

  3. 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:

  1. Separate Data Reception Thread: Running the data reception in a separate thread to avoid blocking the web server.

  2. Thread-safe Data Structures: Using thread-safe data structures (deque) to avoid race conditions.

  3. 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:

  1. Clear Visual Hierarchy: Organizing the dashboard with a clear visual hierarchy to highlight the most important information.

  2. Color Coding: Using consistent colors for each brain wave band to make the graph easier to interpret.

  3. 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:

  1. Topographic Mapping: Creating 2D or 3D visualizations of brain activity across the scalp.

  2. Spectrograms: Showing the full frequency spectrum over time using color-coded intensity maps.

  3. 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:

  1. Device-specific Adapters: Creating adapters for popular EEG devices like OpenBCI, Muse, or Emotiv.

  2. Real-time Artifact Rejection: Implementing algorithms to detect and remove artifacts from eye blinks, muscle movement, etc.

  3. 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:

  1. Sleep Analysis: Extended visualization for sleep stage classification and sleep disorder diagnosis.

  2. Neurofeedback Training: Interactive visualizations that respond to specific brain states for training purposes.

  3. Cognitive Workload Assessment: Visualizations that highlight changes in cognitive load during tasks.

  4. Meditation Assistance: Specialized displays for meditation practice that emphasize relevant brain states.

4. Consumer Applications

The system can also be adapted for consumer applications:

  1. Mobile Interfaces: Creating mobile-friendly visualizations for portable EEG devices.

  2. Gamification: Adding game-like elements to make brain wave monitoring more engaging.

  3. 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.

0
Subscribe to my newsletter

Read articles from Aine LLC. directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aine LLC.
Aine LLC.