Cyton values streamed from OpenBCI_GUI don't match those in GUI

Hello,
I'm writing a Python script using Brainflow to accept the data streamed over Network from the OpenBCI_GUI, perform some processing, and then forward it through a websocket to an iPhone app I'm displaying it in.

I've got the script to work perfectly with the synthetic board, so that the logged uVRMS value and frequency is exactly that of the synthetic channels displayed in the GUI.

However, when trying to do the same with my Cyton board the values vary wildly from what's displayed. I'm sure I am just missing a (or adding an extra) crucial preprocessing step, so what should I change in my script to match the signal amplitudes printed with those displayed in the GUI?

General Questions:
1. Should I have the scaling from ADC counts to uV, or is the Cyton masterboard streaming uV already (either way, the values are significantly off)
2. Should filtering be done manually in the script, or are all the filters applied in the GUI applied before data is streamed (either way, the values are significantly off)

Here is a minimal version of the script and video showing the live comparison between the data and GUI readings.

Code: https://codeshare.io/5zloJD
Live comparison: https://streamable.com/iexvfd

Thanks in advance for the help!

Comments

  • wjcroftwjcroft Mount Shasta, CA

    @haccr said:
    ...
    However, when trying to do the same with my Cyton board the values vary wildly from what's displayed. I'm sure I am just missing a (or adding an extra) crucial preprocessing step, so what should I change in my script to match the signal amplitudes printed with those displayed in the GUI?

    The data stream and recordings from the GUI are unfiltered, whereas the GUI display panels show filtered data. Because Cyton is a DC-coupled-amplifier, the raw data contains a slowly moving DC-offset.

    https://openbci.com/forum/index.php?p=/discussion/201/large-millivolt-data-values-fbeeg-full-band-eeg

    William

  • Thanks for getting back to me, William. It's very helpful to know that the data from the streaming board is unfiltered.

    Still, I thought the detrending and filtering (both notch and bandpass) in my script would be enough to align my data.

    Can you please confirm that this is the correct preprocessing pipeline done in the GUI:
    0. (maybe?) Converting incoming signal from ADC counts to uV - (4.5 / 24) / (2**23 - 1) * 1e6 (don't think this is necessary)
    1. Detrending data - DataFilter.detrend(data, DetrendOperations.CONSTANT.value)
    2. Butterworth bandpass from 5 to 50Hz at order 4 (what my filter settings are currently at)
    3. Notch filter at 50 and 60Hz at order 4 (butterworth bandstop at 48-52 and 58-68)
    4. RMS calculation with last second of filtered uV data

    I have tested without 0 and 1 since those are the steps I'm least confident about. Still, I can't ever get the numbers logged by my script to match the values in the GUI.

    Any help is appreciated!

  • wjcroftwjcroft Mount Shasta, CA

    @haccr said:
    Thanks for getting back to me, William. It's very helpful to know that the data from the streaming board is unfiltered.

    Still, I thought the detrending and filtering (both notch and bandpass) in my script would be enough to align my data.

    Can you please confirm that this is the correct preprocessing pipeline done in the GUI:
    0. (maybe?) Converting incoming signal from ADC counts to uV - (4.5 / 24) / (2**23 - 1) * 1e6 (don't think this is necessary)

    Correct, sample values are already in microvolts.

    1. Detrending data - DataFilter.detrend(data, DetrendOperations.CONSTANT.value)
    2. Butterworth bandpass from 5 to 50Hz at order 4 (what my filter settings are currently at)

    I suggest the filter first, you probably will not need to 'detrend'.

    1. Notch filter at 50 and 60Hz at order 4 (butterworth bandstop at 48-52 and 58-68)

    You only need the notch at your local mains frequency.

    1. RMS calculation with last second of filtered uV data

    I have tested without 0 and 1 since those are the steps I'm least confident about. Still, I can't ever get the numbers logged by my script to match the values in the GUI.

    If you are matching the filter values in the Filters section of the GUI, your time series should be pretty close.

    https://docs.openbci.com/Software/OpenBCISoftware/GUIWidgets/#filters

    Any help is appreciated!

    Can you be more specific about what is "not matching"?

  • edited June 2025

    @wjcroft said:
    Can you be more specific about what is "not matching"?

    Check out this video: https://streamable.com/25szl3
    It illustrates the discrepancy between uVrms displayed in the GUI vs. measured in my script. In the GUI I'm using a bandpass from 5 to 50 and the notch at 50 + 60Hz. You can see the values are an order of magnitude off.

    My current preprocessing pipeline is exactly as you suggested:

    def filter_data(data, fs):
        DataFilter.perform_bandpass(
            data, fs, 5, 50.0, 4, FilterTypes.BUTTERWORTH_ZERO_PHASE.value, 0
        )
        DataFilter.remove_environmental_noise(
            data, fs, NoiseTypes.FIFTY_AND_SIXTY.value)
        return data
    # ....................
    for _ in range(50):  # 5s
        data = board.get_current_board_data(buf_len)
        if data.shape[1]:
            eeg_uV = data[eeg_ch, :].astype(np.float64)
            filter_data(eeg_uV[log_channel - 1], fs)
            ch_data = eeg_uV[log_channel - 1]
            rms = np.sqrt(np.mean(ch_data**2))
            peak = np.max(ch_data)
            print(f"RMS: {rms:.2f} µVrms | Peak: {peak:.2f} µV")
        time.sleep(0.1)
    
    1. 4th order butterworth zero phase bandpass from 5 to 50Hz
    2. Environmental notch filters
    3. Calculate rms on last chunk of samples

    What's going wrong here?

  • wjcroftwjcroft Mount Shasta, CA

    Your time series shows a HUGE amount periodic non-EEG signal. Is it being contaminated with either ECG signal or external EMF (electromagnetic field noise) in the environment?

    RMS computation generally depends on a 'running average' type of calculation, that involves a time constant.

    Are you following the tutorial steps to try something simple, like measuring eyes close alpha?

    https://docs.openbci.com/GettingStarted/Biosensing-Setups/EEGSetup/#4-alpha-brain-waves-eeg

  • @wjcroft said:
    Your time series shows a HUGE amount periodic non-EEG signal. Is it being contaminated with either ECG signal or external EMF (electromagnetic field noise) in the environment?

    Yeah, the EEG is super sporadic. That above clip was with the channel 2 electrode against my finger (and reference on earlobe), so not at all a real measurement.

    Here is a real signal with channel 2 between Fp1 and Fp2: https://streamable.com/v8wr2a.

    My main concern is the mismatch in readings, though, as the signal quality is a whole other thing to debug.

  • wjcroftwjcroft Mount Shasta, CA

    Can you please try one of the tutorial tests, such as SRB2 on one ear lobe, Bias/Ground on the other, and O1 or O2 or Pz for measuring alpha. Then try the eyes closed alpha test. The impedance shown is seems on the high side. What electrodes are you using?

    I would not fixate on the RMS calculation values. As you probably know EEG is hugely impacted by head movement, muscle tension, eye blinks, jaw tightness, etc.

    What is your project attempting to do with the EEG? Other than forwarding to the phone?

Sign In or Register to comment.