Skip to content

Commit

Permalink
add eye blink post filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
gisogrimm committed Feb 18, 2025
1 parent 67ba8d7 commit b5ef1a1
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 5 deletions.
2 changes: 1 addition & 1 deletion examples/reclevelanalyzer.tsc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modules>
<route channels="2" name="rla" connect="system:capture_[12]">
<plugins>
<reclevelanalyzer/>
<reclevelanalyzer triggered="true"/>
</plugins>
</route>
<oscrelay path="/reclevelanalyzer" url="osc.udp://localhost:19877"/>
Expand Down
5 changes: 5 additions & 0 deletions libtascar/include/filterclass.h
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ namespace TASCAR {

protected:
float a1_, a2_, b0_, b1_, b2_;

public:
float z1, z2;
};

Expand Down Expand Up @@ -482,6 +484,9 @@ namespace TASCAR {
*/
class o1_ar_filter_t : public TASCAR::wave_t {
public:
o1_ar_filter_t();
o1_ar_filter_t(const o1_ar_filter_t&);
o1_ar_filter_t& operator=(const o1_ar_filter_t&);
/**
\brief Constructor, setting all taus to zero.
Expand Down
31 changes: 31 additions & 0 deletions libtascar/src/filterclass.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,37 @@ void TASCAR::o1_ar_filter_t::set_tau_release(unsigned int ch, float tau)
o1_lp_coeffs(tau, fs, c1_r[ch], c2_r[ch]);
}

TASCAR::o1_ar_filter_t::o1_ar_filter_t()
: TASCAR::wave_t(1), c1_a(1), c2_a(1), c1_r(1),
c2_r(1), fs(1)
{
d[0] = 0.0f;
set_tau_attack(0, 1);
set_tau_release(0, 1);
}

TASCAR::o1_ar_filter_t::o1_ar_filter_t(const o1_ar_filter_t& src)
:TASCAR::wave_t(src.n), c1_a(src.c1_a), c2_a(src.c2_a), c1_r(src.c1_r),
c2_r(src.c2_r), fs(src.fs)
{
}

TASCAR::o1_ar_filter_t& TASCAR::o1_ar_filter_t::operator=(const TASCAR::o1_ar_filter_t& src)
{
resize(src.n);
c1_a.resize(src.n);
c2_a.resize(src.n);
c1_r.resize(src.n);
c2_r.resize(src.n);
fs = src.fs;
c1_a.copy(src.c1_a);
c2_a.copy(src.c2_a);
c1_r.copy(src.c1_r);
c2_r.copy(src.c2_r);
fs = src.fs;
return *this;
}

/*
* Local Variables:
* mode: c++
Expand Down
65 changes: 61 additions & 4 deletions plugins/src/tascarmod_osceog.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ class osceog_t : public TASCAR::module_base_t {
public:
osceog_t(const TASCAR::module_cfg_t& cfg);
virtual ~osceog_t();
static int osc_update(const char*, const char*, lo_arg**, int argc,
static int osc_update(const char*, const char*, lo_arg** argv, int argc,
lo_message, void* user_data)
{
if(user_data && (argc == 3))
// first data element is time, discarded for now
((osceog_t*)user_data)->update_eog();
((osceog_t*)user_data)->update_eog(argv[0]->f, argv[1]->f, argv[2]->f);
return 1;
}
void update_eog();
void update_eog(double t, float eog_hor, float eog_vert);

private:
void connect();
Expand All @@ -56,6 +56,17 @@ class osceog_t : public TASCAR::module_base_t {
std::string targetip;
// sample rate
uint32_t srate = 128;
// post-filter parameters:
std::string pf_path = "";
// cut-off frequency in Hz of low-pass filter:
float pf_fcut = 20.0f;
// minimum tracker time constant in seconds:
float pf_tau_min = 2.0f;
// maximum tracker time constant in seconds:
float pf_tau_max = 10.0f;
// minimum blink amplitude in Volt:
float pf_a_min = 1e-4f;
bool pf_reset = true;

// measure time since last callback, for reconnection attempts if no data
// arrives:
Expand All @@ -68,6 +79,12 @@ class osceog_t : public TASCAR::module_base_t {
std::atomic<bool> run_service;
// shortcut for name prefix:
std::string p;

// post-filter of EOG:
TASCAR::biquadf_t pf_lp;
TASCAR::o1_ar_filter_t pf_minmaxtrack;
lo_message msg;
lo_arg** oscmsgargv;
};

void osceog_t::connect()
Expand Down Expand Up @@ -103,18 +120,58 @@ osceog_t::osceog_t(const TASCAR::module_cfg_t& cfg)
if(connectwlan && (wlanssid.size() == 0))
throw TASCAR::ErrMsg(
"If sensor is to be connected to WLAN, the SSID must be provided");
// postfilter settings:
GET_ATTRIBUTE(
pf_path, "",
"Path name of filtered eye blink data, or empty for no postfilter.");
GET_ATTRIBUTE(pf_fcut, "Hz", "cut-off frequency of low-pass filter");
GET_ATTRIBUTE(pf_tau_min, "s", "minimum tracker time constant");
GET_ATTRIBUTE(pf_tau_max, "s", "maximum tracker time constant");
GET_ATTRIBUTE(pf_a_min, "V", "minimum blink amplitude");
// end postfilter settings.
// create OSC message for postfiltered data:
msg = lo_message_new();
lo_message_add_double(msg, 0.0f); // sender time stamp
lo_message_add_float(msg, 0.0f); // filtered blink
lo_message_add_float(msg, 0.0f); // lowpass vertical EOG
lo_message_add_float(msg, 0.0f); // minimum EOG
lo_message_add_float(msg, 0.0f); // maximum EOG
oscmsgargv = lo_message_get_argv(msg);
pf_lp.set_butterworth(pf_fcut, (float)srate);
pf_minmaxtrack = TASCAR::o1_ar_filter_t(2, (float)srate, {pf_tau_min, 0.0f},
{0.0f, pf_tau_max});
//
tictoc.tic();
connect();
run_service = true;
connectsrv = std::thread(&osceog_t::connectservice, this);
if(name.size())
p = "/" + name;
session->add_method(p + eogpath, "dff", &osc_update, this);
session->add_bool_true(p + "/reset", &pf_reset, "Reset post filter state");
}

void osceog_t::update_eog()
void osceog_t::update_eog(double t, float, float eog_vert)
{
tictoc.tic();
if(!pf_path.empty()) {
if(pf_reset) {
for(uint32_t k = 0; k < srate; ++k)
pf_lp.filter(eog_vert);
pf_minmaxtrack[0] = eog_vert;
pf_minmaxtrack[1] = eog_vert + pf_a_min;
pf_reset = false;
}
float eog_lp = pf_lp.filter(eog_vert);
float eog_min = pf_minmaxtrack(0, eog_lp);
float eog_max = pf_minmaxtrack(1, std::max(eog_min + pf_a_min, eog_lp));
oscmsgargv[0]->d = t;
oscmsgargv[1]->f = (eog_lp - eog_min) / (eog_max - eog_min);
oscmsgargv[2]->f = eog_lp;
oscmsgargv[3]->f = eog_min;
oscmsgargv[4]->f = eog_max;
session->dispatch_data_message(pf_path.c_str(), msg);
}
}

osceog_t::~osceog_t()
Expand Down

0 comments on commit b5ef1a1

Please sign in to comment.