Reading higher frequency data in thread and plotting graph in real-time with Tkinter

◇◆丶佛笑我妖孽 提交于 2021-01-28 12:25:19

问题


In the last couple of weeks, I've been trying to make an application that can read EEG data from OpenBCI Cyton (@250Hz) and plot a graph in 'real-time'. What seems to work better here are threads. I applied the tips I found here 1 to communicate the thread with Tkinter, but the application still doesn't work (gives me the error RecursionError: maximum recursion depth exceeded while calling a Python object). Maybe I'm doing something wrong because I'm trying to use multiple .py files? See below the main parts of my code and a few more comments in context:

###FILE main.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from AppWindow import *

window = AppWindow()
window.start()
###FILE AppWindow.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk
from tkinter.scrolledtext import ScrolledText
import scroller as scrl
import logging
import requests
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import random
from pandas import DataFrame
import stream_lsl_eeg as leeg


#Definitions
H = 720
W = 1280
#Color palette>> https://www.color-hex.com/color-palette/92077
bg_color = "#c4ac93"
sc_color = "#bba58e"
tx_color = "#313843"
dt_color = "#987f62"
wn_color = "#6b553b"


class AppWindow:

    #Other Functions

    def plotGraph(self, x, y):
        self.ax.clear()
        self.ax.plot(x,y, color = tx_color)
        plt.subplots_adjust(bottom=0.31, left=0.136, top=0.9, right=0.99)
        plt.ylabel('Magnitude', fontsize = 9, color = tx_color)
        plt.xlabel('Freq', fontsize = 9, color = tx_color)
        self.figure.canvas.draw()

    def __init__(self):
        self.root = tk.Tk() #start of application
        self.root.wm_title("Hybrid BCI - SSVEP and Eye Tracker")

        #Other Graphical Elements

        #Button that calls function
        self.btn_ReceiveEEG = tk.Button(self.EEG_frame, text = "Receive EEG signal", bg = bg_color, fg = tx_color, state = tk.DISABLED, command = lambda: leeg.getEEGstream(self))
        self.btn_ReceiveEEG.place(anchor = 'nw', relx = 0.52, rely = 0.5, width = 196, height = 40)

        #Other Graphical Elements

    def start(self):
        self.root.mainloop() #end of application
### FILE stream_lsl_eeg.py
from pylsl import StreamInlet, resolve_stream
import tkinter as tk
import AppWindow as app
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import threading
import queue
import time


class myThread(threading.Thread):
    def __init__(self, name, q, f):
        threading.Thread.__init__(self)
        self.name = name
        self.q = q
        self.f = f

    def run(self):
        print("Starting ", self.name)
        pullSamples(self.q, self.f) #place where function is called


def getInlet(app): #this is triggered by another button and it's working fine
    global inlet
    app.logger.warn('Looking for an EEG strean...')
    streams = resolve_stream('type', 'EEG')
    inlet = StreamInlet(streams[0])
    app.logger.warn('Connected')
    app.btn_ReceiveEEG.config(state = tk.NORMAL)


def pullSamples(q):
    i = 0
    while i<1000:
        sample, timestamp = inlet.pull_sample()
        threadLock.acquire()    #thread locks to put info in the queue
        q.put([sample,timestamp]) #data is put in the queue for other threads to access
        threadLock.release()    #thread unlocks after info is in
        i += 1
    stopcollecting = 1
    print("Exit flag on")

def plotSamples(app, kounter): #Outside Thread
    if not stopcollecting: #testing if stream reception stopped
        while dataqueue.qsize(  ):
            try:
                kounter += 1
                sample, timestamp = dataqueue.get(0)
                samples.append(sample[0]) #getting just channel 1 (0)
                timestamps.append(timestamp)
                show_samples = samples[-250:]
                show_timestamps = timestamps[-250:]
                app.plotGraph(show_timestamps,show_samples)
                print(counter) #this was just a control to count if the right amount of samples was coming out of the queue
            except dataqueue.Empty:
                pass #still not implemented, but will return to the main application
        app.root.after(60, plotSamples(flag,app,kounter)) #60 chosen because plot should update every 15 samples (15/250 = 0,06s)   


def getEEGstream(app): #function called by button
    app.logger.warn('Starting thread...')
    #
    kounter = 0
    start = time.perf_counter()
    thread1.start()
    ##
    plotSamples(flag, app, kounter)
    ##
    thread1.join() #I don't know if I need this...
    finish = time.perf_counter()
    #
    print(f'Sizes: Samples [{len(samples)}, {len(samples[0])}], {len(timestamps)} timestamps')
    print(f'Sucessfully streamed in {round(finish-start,3)}s!')

###
threadLock = threading.Lock()
dataqueue = queue.Queue()
stopcollecting = 0
kounter = []
flag = queue.Queue() #secondary queue for flags not used at the moment
flag.put(0)
thread1 = myThread("Thread-1", dataqueue,flag)
samples,timestamps = [],[]
show_samples, show_timestamps = [],[]

As I found here 2, a function should not call itself, but it's basically what here 1 does. Also, I don't think I'm calling root.mainloop() multiple times like done in here 3.

After executing, python gives me the following error/output:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File "C:\Users\robotics\Documents\gitDocuments\SSVEP_EyeGaze_py\AppWindow.py", line 109, in <lambda>
    self.btn_ReceiveEEG = tk.Button(self.EEG_frame, text = "Receive EEG signal", bg = bg_color, fg = tx_color, state = tk.DISABLED, command = lambda: leeg.getEEGstream(self))
  File "C:\Users\robotics\Documents\gitDocuments\SSVEP_EyeGaze_py\stream_lsl_eeg.py", line 118, in getEEGstream
    plotSamples(flag, app, kounter)
  File "C:\Users\robotics\Documents\gitDocuments\SSVEP_EyeGaze_py\stream_lsl_eeg.py", line 104, in plotSamples
    app.root.after(60, plotSamples(flag,app,kounter))
  File "C:\Users\robotics\Documents\gitDocuments\SSVEP_EyeGaze_py\stream_lsl_eeg.py", line 104, in plotSamples
    app.root.after(60, plotSamples(flag,app,kounter))
  File "C:\Users\robotics\Documents\gitDocuments\SSVEP_EyeGaze_py\stream_lsl_eeg.py", line 104, in plotSamples
    app.root.after(60, plotSamples(flag,app,kounter))
  [Previous line repeated 986 more times]
  File "C:\Users\robotics\Documents\gitDocuments\SSVEP_EyeGaze_py\stream_lsl_eeg.py", line 92, in plotSamples
    while dataqueue.qsize(  ): # if not dataqueue.empty():
  File "C:\Users\robotics\AppData\Local\Continuum\anaconda3\envs\psychopy\lib\queue.py", line 87, in qsize
    with self.mutex:
RecursionError: maximum recursion depth exceeded while calling a Python object
Exit flag on

This means the thread is being successfully executed, apparently, but the plotSamples() is crashing.

Any advice??


回答1:


after() (similar to button's command= and bind()) needs function's name without () and without argument - it is called callback - and after sends it to mainloop and mainloop later uses () to run it.

You use function with ()

 app.root.after(60, plotSamples(flag,app,kounter))

so it runs it at once (it doesn't send it to mainloop) and this function runs at once again the same function which runs at once the same function, etc. - so you create recursion.

It works like

 result = plotSamples(flag,app,kounter) # run at once
 app.root.after(60, result)

If you have to use function with arguments then you can do

 app.root.after(60, plotSamples, flag, app, kounter)

Eventually you can use lambda to create function without argument

 app.root.after(60, lambda:plotSamples(flag,app,kounter) )


来源:https://stackoverflow.com/questions/62273244/reading-higher-frequency-data-in-thread-and-plotting-graph-in-real-time-with-tki

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!