Skip to main content
Ctrl+K
NeuroKit2 0.2.12 documentation - Home

Menu

  • Overview
  • Installation
  • Authors
  • Cite us
  • Codebook
  • Examples
    • Event-related Analysis
    • Interval-related Analysis
    • Customize your Processing Pipeline
    • Heart Rate Variability (HRV)
    • Extract and Visualize Individual Heartbeats
    • Locate P, Q, S and T waves in ECG
    • ECG-Derived Respiration (EDR)
    • Generating Abnormal 12-leads ECG
    • Analyze Electrodermal Activity (EDA)
    • Respiratory Rate Variability (RRV)
    • Analyze Electrooculography (EOG)
    • Simulate Artificial Physiological Signals
    • Save Preprocessing Reports
    • EEG Power in Frequency Bands
    • EEG Complexity Analysis
    • EEG Microstates
    • Fit a function to a non-linear pattern
    • Create epochs
  • Functions
    • Bio
    • ECG
    • PPG
    • HRV
    • RSP
    • EDA
    • EMG
    • EOG
    • EEG
    • Video Analysis
    • Microstates
    • Signal
    • Events
    • Epochs
    • Data
    • Complexity
    • Markov Chains
    • Stats
    • Misc
    • Benchmarking
  • Resources
    • Learn Python in 10 minutes
    • Contributing guide
    • Recording good quality signals
    • Additional Resources
  • Studies
    • HRV Review
    • HRV Indices
    • HRV Tutorial
    • Complexity Review
    • Complexity Indices
    • EEG Complexity: Parameters Selection
    • EEG Analysis with GAMs
    • ECG Benchmark
    • EOG blink template
  • Repository
  • Repository
  • Suggest edit
  • Open issue
  • .rst

Blink Template Estimation for Electrooculography (EOG)

Contents

  • Introduction
  • Study 1: Initial Estimation
    • Initial Estimation Methods
      • Define Functions
    • Initial Estimation Results
  • Study 2: Difference between Template and EOG Events
    • Template vs. EOG Event Methods
    • Template vs. EOG Event Results
  • Study 3: Optimize the Parameters
    • Parameter Optimization Methods
    • Parameter Optimization Results
  • References

Blink Template Estimation for Electrooculography (EOG)#

This study can be referenced by citing the package.

We’d like to publish this study, but unfortunately we currently don’t have the time. If you want to help to make it happen, please contact us!

Introduction#

The goal of this study is to identify a template for blinks in vertical EOG recordings. For this, we will optimize to candidate functions, a Gamma distribution and the SCR function proposed by Bach, Flandin, Friston, & Dolan (2010) to model skin conductance responses.

In the first study, we will obtain the optimized parameters for a large numbers of “events” detected in the vEOG signal (for different subjects under different tasks), assuming that blinks are the prevalent type of detected events.

In the second study, we will see how these two blink templates (obtained from the optimized parameters of study 1 for the two functions) perform by computing their difference (RMSE) with each EOG event. The assumption is that not all of the detected events are blinks, and so we will observe a bi-modal distribution, with events closely matching the template and other events differing from them.

In the third study, after identifying a reasonable RMSE threshold to identify and keep only the “blink”-like events, we will then re-optimize the functions parameters on this cleaner subset of events.

Study 1: Initial Estimation#

Initial Estimation Methods#

Define Functions#

# import neurokit2 as nk
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.signal


def fit_gamma(x, loc, a, scale):
    x = nk.rescale(x, to=[0, 10])
    gamma = scipy.stats.gamma.pdf(x, a=a, loc=loc, scale=scale)
    y = gamma / np.max(gamma)
    return y


def fit_scr(x, time_peak, rise, decay1, decay2):
    x = nk.rescale(x, to=[0, 10])
    gt = np.exp(-((x - time_peak) ** 2) / (2 * rise ** 2))
    ht = np.exp(-x / decay1) + np.exp(-x / decay2)

    ft = np.convolve(gt, ht)
    ft = ft[0 : len(x)]
    y = ft / np.max(ft)
    return y

# Starting parameters
plt.plot(fit_gamma(np.arange(100), 3, 3, 0.5), linewidth=2, linestyle='-', color="#4CAF50", label='Gamma')
plt.plot(fit_scr(np.arange(100), 3.5, 0.5, 1, 1), linewidth=2, linestyle='-', color="#9C27B0", label='SCR')
plt.savefig("figures/fig1.png")
plt.clf()

fig1 #### Optimize Parameters

The eogdb dataset is placed in the NeuroKit/data/eogdb/ folder.

params_gamma = pd.DataFrame(columns=["loc", "a", "scale", "Participant", "Task"])
params_scr = pd.DataFrame(columns=["time_peak", "rise", "decay1", "decay2", "Participant", "Task"])

for i in range(4):
    print("Task: " + str(i))

    data = pd.read_csv("../../data/eogdb/eogdb_task" + str(i + 1) + ".csv")

    for j, participant in enumerate(np.unique(data["Participant"])):
        print("  - " + str(j + 1))

        segment = data[data["Participant"] == participant]
        signal = segment["vEOG"]

        cleaned = nk.eog_clean(signal, sampling_rate=200, method='neurokit')
        blinks = nk.signal_findpeaks(cleaned, relative_height_min=1.5)["Peaks"]

        events = nk.epochs_create(cleaned, blinks, sampling_rate=200, epochs_start=-0.4, epochs_end=0.6)
        events = nk.epochs_to_array(events)  # Convert to 2D array

        x = np.linspace(0, 100, num=len(events))

        p_gamma = np.full((events.shape[1], 3), np.nan)
        p_bateman = np.full((events.shape[1], 3), np.nan)
        p_scr = np.full((events.shape[1], 4), np.nan)

        for i in range(events.shape[1]):
            if np.isnan(events[:, i]).any():
                break
            events[:, i] = nk.rescale(events[:, i], to=[0, 1])  # Reshape to 0-1 scale
            try:
                p_gamma[i, :], _ = scipy.optimize.curve_fit(fit_gamma, x, events[:, i], p0=[3, 3, 0.5])
                p_scr[i, :], _ = scipy.optimize.curve_fit(fit_scr, x, events[:, i], p0=[3.5, 0.5, 1, 1])
            except RuntimeError:
                pass

        p_gamma = pd.DataFrame(p_gamma[~np.isnan(p_gamma).any(axis=1)], columns=["loc", "a", "scale"])
        p_gamma["Participant"] = participant
        p_gamma["Task"] = data["Task"][0]
        params_gamma = pd.concat([params_gamma, p_gamma], axis=0)
        p_scr = pd.DataFrame(p_scr[~np.isnan(p_scr).any(axis=1)], columns=["time_peak", "rise", "decay1", "decay2"])
        p_scr["Participant"] = participant
        p_scr["Task"] = data["Task"][0]
        params_scr = pd.concat([params_scr, p_scr], axis=0)

Initial Estimation Results#

Visualize the optimal templates for one task.

data = pd.read_csv("../../data/eogdb/eogdb_task3.csv")
cleaned = nk.eog_clean(data["vEOG"], sampling_rate=200, method='neurokit')
blinks = nk.signal_findpeaks(cleaned, relative_height_min=1.5)["Peaks"][:-1]
events = nk.epochs_create(cleaned, blinks, sampling_rate=200, epochs_start=-0.4, epochs_end=0.6)
events = nk.epochs_to_array(events)

for i in range(events.shape[1]):
    events[:, i] = nk.rescale(events[:, i], to=[0, 1])  # Reshape to 0-1 scale

x = np.linspace(0, 100, num=len(events))
template_gamma = fit_gamma(x, *np.nanmedian(params_gamma.iloc[:, [0, 1, 2]], axis=0))
template_scr = fit_scr(x, *np.nanmedian(params_scr.iloc[:, [0, 1, 2, 3]], axis=0))

plt.plot(events, linewidth=0.02, color="black")
plt.plot(template_gamma, linewidth=2, linestyle='-', color="#4CAF50", label='Gamma')
plt.plot(template_scr, linewidth=2, linestyle='-', color="#9C27B0", label='SCR')
plt.legend(loc="upper right")
plt.savefig("figures/fig2.png")
plt.clf()

fig2

Study 2: Difference between Template and EOG Events#

Template vs. EOG Event Methods#

data_rmse = pd.DataFrame(columns=["RMSE", "Index", "Participant", "Task", "Function"])
for i in range(4):
    data = pd.read_csv("../../data/eogdb/eogdb_task" + str(i + 1) + ".csv")

    for j, participant in enumerate(np.unique(data["Participant"])):
        segment = data[data["Participant"] == participant]
        signal = segment["vEOG"]

        cleaned = nk.eog_clean(signal, sampling_rate=200, method='neurokit')
        blinks = nk.signal_findpeaks(cleaned, relative_height_min=1.5)["Peaks"]

        events = nk.epochs_create(cleaned, blinks, sampling_rate=200, epochs_start=-0.4, epochs_end=0.6)
        events = nk.epochs_to_array(events)  # Convert to 2D array

        # Rescale
        for i in range(events.shape[1]):
            events[:, i] = nk.rescale(events[:, i], to=[0, 1])  # Reshape to 0-1 scale

        # RMSE - Gamma
        rmse = pd.DataFrame({"RMSE": [nk.fit_rmse(events[:, i], template_gamma) for i in range(events.shape[1])],
                             "Index": range(events.shape[1]),
                             "Participant": [participant]*events.shape[1],
                             "Task": [data["Task"][0]]*events.shape[1],
                             "Function": ["Gamma"] * events.shape[1]})
        rmse["Index"] = rmse["Participant"] + "_" + rmse["Task"] + "_" + rmse["Index"].astype(str)
        data_rmse = pd.concat([data_rmse, rmse], axis=0)

        # RMSE - SCR
        rmse = pd.DataFrame({"RMSE": [nk.fit_rmse(events[:, i], template_scr) for i in range(events.shape[1])],
                             "Index": range(events.shape[1]),
                             "Participant": [participant]*events.shape[1],
                             "Task": [data["Task"][0]]*events.shape[1],
                             "Function": ["SCR"] * events.shape[1]})
        rmse["Index"] = rmse["Participant"] + "_" + rmse["Task"] + "_" + rmse["Index"].astype(str)
        data_rmse = pd.concat([data_rmse, rmse], axis=0)

Template vs. EOG Event Results#

p = data_rmse.pivot(index='Index', columns='Function', values='RMSE').plot.kde()
p.set_xlim(0, 1)
p.axvline(x=0.25, color="red")
plt.savefig("figures/fig3.png")
plt.clf()

fig3

Study 3: Optimize the Parameters#

Parameter Optimization Methods#

optimal_gamma = np.nanmedian(params_gamma.iloc[:, [0, 1, 2]], axis=0)
optimal_scr = np.nanmedian(params_scr.iloc[:, [0, 1, 2, 3]], axis=0)

params_gamma = pd.DataFrame(columns=["loc", "a", "scale", "Participant", "Task"])
params_scr = pd.DataFrame(columns=["time_peak", "rise", "decay1", "decay2", "Participant", "Task"])

for i in range(4):
    print("Task: " + str(i))
    data = pd.read_csv("../../data/eogdb/eogdb_task" + str(i + 1) + ".csv")

    for j, participant in enumerate(np.unique(data["Participant"])):
        print("  - " + str(j + 1))

        segment = data[data["Participant"] == participant]
        signal = segment["vEOG"]

        cleaned = nk.eog_clean(signal, sampling_rate=200, method='neurokit')
        blinks = nk.signal_findpeaks(cleaned, relative_height_min=1.5)["Peaks"]

        events = nk.epochs_create(cleaned, blinks, sampling_rate=200, epochs_start=-0.4, epochs_end=0.6)
        events = nk.epochs_to_array(events)  # Convert to 2D array

        x = np.linspace(0, 100, num=len(events))

        p_gamma = np.full((events.shape[1], 3), np.nan)
        p_scr = np.full((events.shape[1], 4), np.nan)

        for i in range(events.shape[1]):
            if np.isnan(events[:, i]).any():
                break
            events[:, i] = nk.rescale(events[:, i], to=[0, 1])  # Reshape to 0-1 scale

            if nk.fit_rmse(events[:, i], template_gamma) < 0.25:
                try:
                    p_gamma[i, :], _ = scipy.optimize.curve_fit(fit_gamma, x, events[:, i], p0=optimal_gamma)
                except RuntimeError:
                    pass

            if nk.fit_rmse(events[:, i], template_scr) < 0.25:
                try:
                    p_scr[i, :], _ = scipy.optimize.curve_fit(fit_scr, x, events[:, i], p0=optimal_scr)
                except RuntimeError:
                    pass

        p_gamma = pd.DataFrame(p_gamma[~np.isnan(p_gamma).any(axis=1)], columns=["loc", "a", "scale"])
        p_gamma["Participant"] = participant
        p_gamma["Task"] = data["Task"][0]
        params_gamma = pd.concat([params_gamma, p_gamma], axis=0)

        p_scr = pd.DataFrame(p_scr[~np.isnan(p_scr).any(axis=1)], columns=["time_peak", "rise", "decay1", "decay2"])
        p_scr["Participant"] = participant
        p_scr["Task"] = data["Task"][0]
        params_scr = pd.concat([params_scr, p_scr], axis=0)
data_rmse = pd.DataFrame(columns=["RMSE", "Index", "Participant", "Task", "Function"])
for i in range(4):
    data = pd.read_csv("../../data/eogdb/eogdb_task" + str(i + 1) + ".csv")

    for j, participant in enumerate(np.unique(data["Participant"])):
        segment = data[data["Participant"] == participant]
        signal = segment["vEOG"]

        cleaned = nk.eog_clean(signal, sampling_rate=200, method='neurokit')
        blinks = nk.signal_findpeaks(cleaned, relative_height_min=1.5)["Peaks"]

        events = nk.epochs_create(cleaned, blinks, sampling_rate=200, epochs_start=-0.4, epochs_end=0.6)
        events = nk.epochs_to_array(events)  # Convert to 2D array

        # Rescale
        for i in range(events.shape[1]):
            events[:, i] = nk.rescale(events[:, i], to=[0, 1])  # Reshape to 0-1 scale

        # RMSE - Gamma
        rmse = pd.DataFrame({"RMSE": [nk.fit_rmse(events[:, i], template_gamma) for i in range(events.shape[1])],
                             "Index": range(events.shape[1]),
                             "Participant": [participant]*events.shape[1],
                             "Task": [data["Task"][0]]*events.shape[1],
                             "Function": ["Gamma"] * events.shape[1]})
        rmse["Index"] = rmse["Participant"] + "_" + rmse["Task"] + "_" + rmse["Index"].astype(str)
        data_rmse = pd.concat([data_rmse, rmse], axis=0)

        # RMSE - SCR
        rmse = pd.DataFrame({"RMSE": [nk.fit_rmse(events[:, i], template_scr) for i in range(events.shape[1])],
                             "Index": range(events.shape[1]),
                             "Participant": [participant]*events.shape[1],
                             "Task": [data["Task"][0]]*events.shape[1],
                             "Function": ["SCR"] * events.shape[1]})
        rmse["Index"] = rmse["Participant"] + "_" + rmse["Task"] + "_" + rmse["Index"].astype(str)
        data_rmse = pd.concat([data_rmse, rmse], axis=0)

df = data_rmse.pivot(index='Index', columns='Function', values='RMSE')
print(df.median(axis=0))
## Function
## Gamma    0.112794
## SCR      0.114369
## dtype: float64

Parameter Optimization Results#

data = pd.read_csv("../../data/eogdb/eogdb_task3.csv")
cleaned = nk.eog_clean(data["vEOG"], sampling_rate=200, method='neurokit')
blinks = nk.signal_findpeaks(cleaned, relative_height_min=1.5)["Peaks"]
events = nk.epochs_create(cleaned, blinks, sampling_rate=200, epochs_start=-0.4, epochs_end=0.6)
events = nk.epochs_to_array(events)
for i in range(events.shape[1]):
        events[:, i] = nk.rescale(events[:, i], to=[0, 1])  # Reshape to 0-1 scale

x = np.linspace(0, 100, num=len(events))
template_gamma2 = fit_gamma(x, *np.nanmedian(params_gamma.iloc[:, [0, 1, 2]], axis=0))
template_scr2 = fit_scr(x, *np.nanmedian(params_scr.iloc[:, [0, 1, 2, 3]], axis=0))

plt.plot(events, linewidth=0.02, color="black")
plt.plot(template_gamma, linewidth=2, linestyle='-', color="#4CAF50", label='Gamma')
plt.plot(template_gamma2, linewidth=2, linestyle='-', color="#2196F3", label='Gamma (optimized)')
plt.plot(template_scr, linewidth=2, linestyle='-', color="#9C27B0", label='SCR')
plt.plot(template_scr2, linewidth=2, linestyle='-', color="#E91E63", label='SCR (optimized)')
plt.legend(loc="upper right")
plt.savefig("figures/fig4.png")
plt.clf()

fig4

data = pd.read_csv("../../data/eogdb/eogdb_task3.csv")
cleaned = nk.eog_clean(data[(data["Participant"] == "S1") | (data["Participant"] == "S2")]["vEOG"], sampling_rate=200, method='neurokit')
blinks = nk.signal_findpeaks(cleaned, relative_height_min=1.5)["Peaks"]
events = nk.epochs_create(cleaned, blinks, sampling_rate=200, epochs_start=-0.4, epochs_end=0.6)
events = nk.epochs_to_array(events)

for i in range(events.shape[1]):
        events[:, i] = nk.rescale(events[:, i], to=[0, 1])  # Reshape to 0-1 scale
rmse = np.array([nk.fit_rmse(events[:, i], template_gamma2) for i in range(events.shape[1])])

plt.plot(events[:, rmse < 0.25], linewidth=0.2, color="black")
plt.plot(events[:, rmse >= 0.25], linewidth=0.2, color="red")
plt.plot(template_gamma2, linewidth=2, linestyle='-', color="#2196F3", label='Gamma (optimized)')
plt.plot(template_scr2, linewidth=2, linestyle='-', color="#E91E63", label='SCR (optimized)')
plt.legend(loc="upper right")
plt.savefig("figures/fig5.png")
plt.clf()

fig5

The optimal blink template using a gamma distribution has the following parameters:

gamma = np.nanmedian(params_gamma.iloc[:, [0, 1, 2]], axis=0)
print(
  "- location: " + str(np.round(gamma[0], 3)) +
  "\n- alpha: " + str(np.round(gamma[1], 3)) +
  "\n- scale: " + str(np.round(gamma[2], 3)))
## - location: 2.659
## - alpha: 5.172
## - scale: 0.317

The optimal blink template using the SCR function by Bach et al. (2010) has the following parameters:

scr = np.nanmedian(params_scr.iloc[:, [0, 1, 2, 3]], axis=0)
print(
  "- time_peak: " + str(np.round(scr[0], 3)) +
  "\n- rise: " + str(np.round(scr[1], 3)) +
  "\n- decay1: " + str(np.round(scr[2], 3)) +
  "\n- decay2: " + str(np.round(scr[3], 3)))
## - time_peak: 3.644
## - rise: 0.422
## - decay1: 0.356
## - decay2: 0.943

References#

Bach, D. R., Flandin, G., Friston, K. J., & Dolan, R. J. (2010). Modelling event-related skin conductance responses. International Journal of Psychophysiology, 75(3), 349–356.

previous

Benchmarking of ECG Preprocessing Methods

Contents
  • Introduction
  • Study 1: Initial Estimation
    • Initial Estimation Methods
      • Define Functions
    • Initial Estimation Results
  • Study 2: Difference between Template and EOG Events
    • Template vs. EOG Event Methods
    • Template vs. EOG Event Results
  • Study 3: Optimize the Parameters
    • Parameter Optimization Methods
    • Parameter Optimization Results
  • References

By Dominique Makowski and the Team. This documentation is licensed under a CC BY 4.0 license.

© Copyright 2020–2025.