Compare commits
No commits in common. "master" and "main" have entirely different histories.
34 changed files with 56 additions and 3761 deletions
1366
EStiMo_GUI.py
1366
EStiMo_GUI.py
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
|||
full_cap_file_path: C:/Users/Basics/Desktop/new_super_important_study/TMS Trains/Up to date version/estimo-master (1)/estimo-master/settings/easycap-M10_63_NO.txt
|
||||
cap_file_path: C:/Users/Basics/Desktop/new_super_important_study/TMS Trains/Up to date version/estimo-master (1)/estimo-master/settings/easycap-M10_16_NO.txt
|
207
README.md
207
README.md
|
@ -1,187 +1,92 @@
|
|||
# EStiMo
|
||||
|
||||
## Table of Contents
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Installation](#installation)
|
||||
- [Running EStiMo for the First Time](#running-estimo-for-the-first-time)
|
||||
- [Establishing Connection with NeurOne or Brain Products](#establishing-connection-with-neurone-or-brain-products)
|
||||
- [Start EStiMo](#start-estimo)
|
||||
- [Configuration Window](#configuration-window)
|
||||
- [Calibration Process](#calibration-process)
|
||||
- [Running Main Recording](#running-main-recording)
|
||||
- [Configuration and Electrode Montage Files](#configuration-and-electrode-montage-files)
|
||||
- [TMS Protocol Structure](#tms-protocol-structure)
|
||||
- [Electrode Selection Structure](#electrode-selection-structure)
|
||||
- [Spatial Locations File Structure](#spatial-locations-file-structure)
|
||||
- [Montage File Structure](#montage-file-structure)
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
## Open-source toolbox for EEG-based Stimulation Monitoring (EStiMo) of brain states during TMS burst delivery
|
||||
https://doi.org/10.1016/j.brs.2024.12.001
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
|
||||
## Overview:
|
||||
EStiMo is an open-source Python toolbox for real-time EEG monitoring during Transcranial Magnetic Stimulation (TMS) sessions. It performs real-time analysis of Electroencephalography (EEG) signals, computing features online and visually representing them via a user-friendly graphical interface. These computations occur during the intervals between TMS pulse trains, providing valuable insights into brain activity.
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
|
||||
### Features:
|
||||
* Real-Time EEG Analysis: Visualizes cortical activity during TMS sessions.
|
||||
* Flexible Customization: Allows the use of custom montages, channel configurations, and feature settings.
|
||||
* Seamless Integration: Compatible with NeurOne and Brain Products systems for data acquisition.
|
||||
## Add your files
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
## How It Works:
|
||||
EStiMo operates in three main steps:
|
||||
|
||||
* Data Acquisition: EEG signals are streamed from NeurOne or Brain Products RDA systems.
|
||||
* Feature Computation: Real-time calculation of up to six EEG features.
|
||||
* Visualization: Processed data is visualized in an interactive GUI for easy monitoring.
|
||||
|
||||
### Installation
|
||||
EStiMo is designed to be conveniently portable and as such, does not necessitate a typical installation procedure. To use the software, ensure a Python 3 environment (recommended: Python 3.9) and follow these steps:
|
||||
|
||||
Clone the repository:
|
||||
```
|
||||
git clone https://nugit.drcmr.dk/Tools/EStiMo.git
|
||||
cd EStiMo
|
||||
```
|
||||
Install dependencies:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Troubleshooting:
|
||||
If you encounter issues with dependencies, ensure that your Python environment is correctly configured (consider using virtual environments).
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
----------------------------------------------------------------------
|
||||
|
||||
|
||||
### Running EStiMo for the First Time
|
||||
|
||||
#### Establishing Connection with NeurOne OR Brain Products Systems (RDA):
|
||||
|
||||
__NeuroOne:__
|
||||
The EStiMo software connects to NeurOne utilizing a serial port. The application anticipates data as input from the device. The last channel is intended to function as a stimulus trigger channel, which returns a value of 0 in the absence of triggers and alternate values when triggers are present. For further specifics regarding the connection setup, kindly refer to Bittium NeurOne real-time DigiOut functionality of NeurOne user manual (https://www.bittium.com/medical/bittium-neurone/).
|
||||
|
||||
|
||||
|
||||
__Brain Product:__
|
||||
The software forms a connection with Brain Products systems via the Remote Data Access (RDA) protocol, which is an integral part of the Brain Products Recorder. Hence, the Recorder is a necessary requirement. The connection is made via the ethernet port. Detailed information about the RDA protocol and connection process can be found in the Brain Product user manual (https://pressrelease.brainproducts.com/real-time-eeg/).
|
||||
|
||||
__Quick connection setup__: on the ___server computer___ where Brain Recorder software is running, Enable the RDA option in Configuration > Preferences …, select the Remote Data Access tab and tick the Enable Remote Data Access.
|
||||
Next, to enable receiving the data through Ethernet cable (maybe known as LAN cable), on the ___client computer___ which EstiMo will run, please go to Control Panel > Network and Sharing Center and open the Network Connection Details of the newly established Ethernet Connection. You need to make sure that the IPv4 address of the client computer is same as server computer. Do not change the IPv4 address on the server computer, only change it on the client computer and make sure they are following the same IPv4 address.
|
||||
|
||||
|
||||
default configurations for the TMS_protocol.txt and electrode_selection.txt files for first-time users
|
||||
|
||||
#### Start EStiMo:
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
||||
|
||||
```
|
||||
python EStiMo_GUI.py
|
||||
cd existing_repo
|
||||
git remote add origin https://git.drcmr.dk/adamr/estimo.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```
|
||||
|
||||
##### 1. Configuration Window:
|
||||
## Integrate with your tools
|
||||
|
||||
- [ ] [Set up project integrations](https://git.drcmr.dk/adamr/estimo/-/settings/integrations)
|
||||
|
||||
## Collaborate with your team
|
||||
|
||||
Upon execution, the initial settings window will appear. These settings can be manually altered, or a configuration file (a txt file with a specific structure) could be imported instead. The montage can be adjusted by importing a csv file containing a matrix of size (n_channels, n_channels).
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
||||
|
||||
EEG channels can be manually selected or deselected to include or exclude them from the feature extraction process (please check [Configuration and Electrode Montage Files](#configuration-and-electrode-montage-files)). Selected channels are highlighted in blue.
|
||||
On the right side of the interface, the channels are displayed. Their positions correspond to the actual positions on the cap, guided by the file indicated at the top of the screen. This layout can be changed if needed. It should be noted that when changing, two files must be selected: one depicting the original cap layout, and the other containing only the channels used for feature computation. If all channels are needed, the same file can be loaded twice.
|
||||
## Test and Deploy
|
||||
|
||||

|
||||
Use the built-in continuous integration in GitLab.
|
||||
|
||||
At the top of the window, the "Features and connection" bar can be selected, offering a choice of features for the connection setup. Once the settings are appropriately adjusted, the "Run program" button can be clicked to start the software.
|
||||
Up to 6 features among X number of features can be selected. The Threshold is also adjustable on the right side of each feature. Details are available in the EstiMo publication. (https://doi.org/10.1016/j.brs.2024.12.001)
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
|
||||

|
||||
***
|
||||
|
||||
# Editing this README
|
||||
|
||||
After selecting the preferred features and establishing the connection, the user can start streaming by clicking the 'Run Program' button.
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
|
||||
## Suggestions for a good README
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
##### 2. Calibration Process
|
||||
Perform a baseline recording of EEG data during rest. The system will compute average values for each feature. The feature measurements are averaged across channels and displayed as plots on the right side of the screen.
|
||||
Note: Recalibration may be required if the montage, channels, or environment changes.
|
||||
The software includes a calibration function. During this process, it looks at the data to set a baseline for all used features. During calibration, the software calculates the mean value of all channels every second. Once calibration is over, it sets the thresholds (by default) at 10% of the distance between min a max, over, and below the maximum and minimum value recorded during the calibration.
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
##### 3. Running main recording
|
||||
Following the calibration phase, the main recording and feature measurements can start by Start/Stop button. Each plot's background will turn red if any of the thresholds are crossed. Deatils can be found in the EstiMo paper. (https://doi.org/10.1016/j.brs.2024.12.001).
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||

|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Configuration and Electrode Montage Files:
|
||||
### TMS_Protocol.txt structure
|
||||
This file includes several settings that can be changed by adjusting the value that follows the colon. The settings you can change are:
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
- time_between_trains: Specifies the interval between consecutive trains in seconds.
|
||||
- cut_time: Defines the duration (in seconds) of the signal segment to ignore between trains. The signal is cut symmetrically, removing half of this value from both ends. This value should be set in seconds.
|
||||
- number_of_channels: Sets the total number of EEG channels used. This would be the EEG channels excluding EOG and EMG channels. For NeuroOne system exclude trigger indicator channels as well. For BrainProducts system: number of streamed channels from Brain Recorder - 2
|
||||
- number_of_lines: Indicates the number of past segment measurements displayed during the readout phase.
|
||||
- eog_channel: Specifies the index of the EOG (electrooculogram) channel. Negative indices can be used to count from the end. Note: The last channel (-1) is always reserved for the trigger indicator.
|
||||
- emg_channel: Specifies the index of the EMG (electromyogram) channel, following the same indexing rules as eog_channel.
|
||||
- included_channels: These are the indexes of channels that are used to calculate features, from 0 (which is the first channel) to N (N represents the number of EEG channels). EOG, EMG, and trigger indicator channels are not included. Indexes correspond to the order of channels streamed from the EEG system. This should be filled as a list of integers.
|
||||
- names: These are the names of the channels that are streamed. This should be filled as a list of integers or strings.
|
||||
- alpha_range: Defines the frequency range for the alpha band (in Hz) as a list of integers (e.g., [8, 15]).
|
||||
- beta_range: Defines the frequency range for the beta band (in Hz) as a list of integers (e.g., [16, 30]).
|
||||
- theta_range: Defines the frequency range for the theta band (in Hz) as a list of integers (e.g., [4, 8]).
|
||||
- expected_triggers: Specifies the number of triggers expected within a single train.
|
||||
- expected_time: This is the time of the single train, measured in milliseconds.
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
> ✅ **Example**:
|
||||
```
|
||||
time_between_trains: 8
|
||||
cut_time: 1
|
||||
number_of_channels: 18
|
||||
number_of_lines: 4
|
||||
eog_channel: -3
|
||||
emg_channel: -2
|
||||
included_channels: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
|
||||
names: [1,3,7,8,9,11,14,17,19,23,26,29,32,43,50,52, "EOG", "EMG"]
|
||||
alpha_range: [8,15]
|
||||
beta_range: [16,30]
|
||||
theta_range: [4,8]
|
||||
threshold_parameter: 2
|
||||
expected_triggers: 10
|
||||
expected_time: 2000
|
||||
plot_len: 4
|
||||
```
|
||||
|
||||
> ✅ **Example**: From 10-10 EEG montage, only 12 channels are used. In that case, as a first file the whole 10-10 montage should be loaded, and as a second a file containing only chosen 12 electrodes.
|
||||
|
||||
### Structure of the Electrode_selection.txt
|
||||
You can set paths for the files that contain spatial information in this file. The following are the available settings:
|
||||
- full_cap_file_path: This is the path to the file showing the spatial location for the full cap.
|
||||
- cap_file_path: This is the path to the file showing the spatial location only for the streamed channels.
|
||||
|
||||
The requirement for both files is purely based on practical reasons to make potential edits to the protocol easier. It's designed for users who only want to use a selected number of channels from a larger EEG cap montage.
|
||||
|
||||
> :warning: **Please note**: If you're using the entire cap, set both settings to the same path. If you don't have access to any of these files, use one of them for both options. However, this might require manual adjustment in the program settings.
|
||||
|
||||
### Spatial Locations file structure
|
||||
The files with spatial locations are managed using the 'mne' library from Python. The function called mne.channels.read_custom_montage is utilized for this purpose. The coordinates are transformed into 2D space using the same library. The way this function reads the file depends on the file format:
|
||||
```
|
||||
eeglab: '.loc', '.locs', '.eloc'
|
||||
hydrocel: '.sfp'
|
||||
matlab: '.csd'
|
||||
asa electrode: '.elc'
|
||||
generic (Theta-phi in degrees): '.txt'
|
||||
standard BESA spherical: '.elp'
|
||||
brainvision: '.bvef'
|
||||
```
|
||||
> :warning: **Please note**: The software was tested only using generic format.
|
||||
|
||||
### Montage file structure
|
||||
The montage file is an N by N array that functions as an array multiplying the signal (array multiplication). This means the signal can be adjusted according to the user's needs, for example by setting a specific type of reference. The file should be in .csv format, and values should be separated by a comma. The indexes follow the Python standard, which is horizontally from left to right and vertically from top to bottom.
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,199 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Tue Aug 16 09:15:49 2016
|
||||
|
||||
@author: KHM
|
||||
"""
|
||||
|
||||
import socket,time,sys,datetime
|
||||
import numpy as np
|
||||
#import scipy.io
|
||||
if sys.version_info[0]>=3:
|
||||
import queue
|
||||
else:
|
||||
import Queue as queue
|
||||
|
||||
import threading,platform
|
||||
if platform.system()=='Windows':
|
||||
tfunc=time.time
|
||||
else:
|
||||
tfunc=time.time
|
||||
|
||||
def bytes_to_int32(databuf):
|
||||
in_data=np.frombuffer(databuf,dtype=np.dtype([('1','i1'),('2','>u2')]))
|
||||
data=in_data['1'].astype('i4')
|
||||
data <<= 16
|
||||
data |= in_data['2']
|
||||
return data
|
||||
|
||||
def sampleLoop(no):
|
||||
firstpackage=True
|
||||
# databuf = np.empty((no.bufsiz,), dtype=np.uint8)
|
||||
databuf = bytearray(b' ' * no.bufsiz)
|
||||
readdump=False
|
||||
if not no.readdump is None:
|
||||
fnin=open(no.readdump,'rb')
|
||||
readdump=True
|
||||
tsamp=tfunc()+no.si
|
||||
sampidx=0
|
||||
firstsamp=0
|
||||
oldsampidx=0
|
||||
droppeds=0
|
||||
timeout=False
|
||||
if not no.dump is None:
|
||||
if no.dump==True:
|
||||
fn=open('dump_'+datetime.datetime.now().strftime('%Y%m%d_%H-%M-%S')+'.raw','wb')
|
||||
else:
|
||||
fn=open(no.dump,'wb')
|
||||
while not no.stop:
|
||||
try:
|
||||
if readdump:
|
||||
while tsamp>tfunc():
|
||||
#time.sleep(tsamp-tfunc())
|
||||
time.sleep(0.)
|
||||
tsamp+=no.si
|
||||
tsin=np.frombuffer(fnin.read(10),dtype=np.uint16)
|
||||
nbytes=tsin[-1]
|
||||
tsin=tsin[:-1].view(np.float64)[0]
|
||||
databuf[:nbytes]=np.frombuffer(fnin.read(nbytes),dtype=np.uint8)
|
||||
else:
|
||||
nbytes=no.sock.recv_into(databuf)
|
||||
t0=tfunc()
|
||||
if timeout:
|
||||
print('Connection re-established.')
|
||||
timeout=False
|
||||
except:
|
||||
nbytes=0
|
||||
if not timeout:
|
||||
print('Timeout - package too small, will keep retrying every second.')
|
||||
timeout=True
|
||||
time.sleep(1.)
|
||||
if nbytes>28:
|
||||
#spacket = databuf[0]
|
||||
#mainunit = databuf[1]
|
||||
packetno = np.frombuffer(databuf[4:8],'>u4').copy()
|
||||
nch = np.frombuffer(databuf[8:10],'>u2').copy()
|
||||
nsamp = np.frombuffer(databuf[10:12],'>u2').copy()
|
||||
sampidx = np.frombuffer(databuf[12:20],'>u8').copy()
|
||||
tstamp = np.frombuffer(databuf[20:28],'>u8').copy()
|
||||
if oldsampidx!=0:
|
||||
ds=sampidx-oldsampidx
|
||||
if ds>nsamp:
|
||||
droppeds += ds-nsamp
|
||||
print('Dropped %i samples'%(ds-nsamp,))
|
||||
elif ds!=nsamp:
|
||||
print('delta samp %i, samples %i'%(ds,nsamp))
|
||||
else:
|
||||
firstsamp=sampidx
|
||||
oldsampidx=sampidx
|
||||
data=bytes_to_int32(databuf[28:28 + 3 * nch[0] * nsamp[0]]).reshape((nsamp[0],nch[0]))
|
||||
t1=tfunc()
|
||||
if not no.ringbuffer is None:
|
||||
if no.avgPackets:
|
||||
data1=data.mean(axis=0, keepdims=True)
|
||||
data1[:,-1]=np.max(data[:,-1])
|
||||
no.updateRingBuffer(data1,sampidx,(tstamp,t1))
|
||||
else:
|
||||
no.updateRingBuffer(data,sampidx,(tstamp,t1))
|
||||
if no.sendqueue:
|
||||
no.queue.put((data,(packetno,sampidx,tstamp,t0,t1)))
|
||||
if firstpackage:
|
||||
no.tstamp0=(tstamp,t1)
|
||||
if no.dump:
|
||||
fn.write(np.array(t1).tostring())
|
||||
fn.write(np.array(nbytes,dtype=np.uint16).tostring())
|
||||
fn.write(databuf[:nbytes])
|
||||
try:
|
||||
fn.close()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
fnin.close()
|
||||
except:
|
||||
pass
|
||||
totals=sampidx-firstsamp
|
||||
if totals>0:
|
||||
if droppeds>0:
|
||||
print('Dropped %i out of %i samples (%.1f%%)'%(droppeds,totals,droppeds/totals*100.))
|
||||
else:
|
||||
print('Acquired %i samples none were dropped.'%(totals,))
|
||||
else:
|
||||
print('No samples acquired.')
|
||||
|
||||
class NO():
|
||||
def __init__(self,ip='127.0.0.1',port=50000,buffersize=2**10,ringbuffersize=None,sendqueue=False,\
|
||||
ringbuf_factor=2,dump=None,readdump=None,si=1./1000.,avgPackets=False):
|
||||
if readdump is None:
|
||||
self.sock=socket.socket(socket.AF_INET, # Internet
|
||||
socket.SOCK_DGRAM) #UDP
|
||||
self.sock.bind((ip, port))
|
||||
self.sock.settimeout(2.)
|
||||
self.avgPackets = avgPackets
|
||||
self.bufsiz=buffersize
|
||||
self.ip=ip
|
||||
self.port=port
|
||||
self.sampidx=0
|
||||
self.tstamp=None
|
||||
self.tstamp0=None
|
||||
self.queue=queue.Queue()
|
||||
self.A=None
|
||||
self.stop=False
|
||||
self.dump=dump
|
||||
self.readdump=readdump
|
||||
if ringbuffersize is None:
|
||||
self.ringbuffer=None
|
||||
else:
|
||||
self.ringbuffer=True
|
||||
self.idx=0
|
||||
self.ringbufferinit=True
|
||||
self.ringbuffersize=ringbuffersize
|
||||
self.ringbuf_factor=ringbuf_factor
|
||||
self.sendqueue=sendqueue
|
||||
self.lock=threading.RLock()
|
||||
self.si=si
|
||||
|
||||
def updateRingBuffer(self,data,i=None,tstamp=None):
|
||||
if self.ringbufferinit:
|
||||
self.ringbuffer=np.zeros((self.ringbuffersize*self.ringbuf_factor,data.shape[1]),dtype=np.float32)
|
||||
self.ringbufferinit=False
|
||||
ringbuf=self.ringbuffer
|
||||
wlen=self.ringbuffersize
|
||||
self.lock.acquire()
|
||||
if (self.idx+data.shape[0])<=ringbuf.shape[0]:
|
||||
ringbuf[self.idx:self.idx+data.shape[0],:]=data
|
||||
self.idx+=data.shape[0]
|
||||
else:
|
||||
ringbuf[0:wlen-data.shape[0],:]=ringbuf[self.idx-wlen+data.shape[0]:self.idx,:]
|
||||
self.idx=wlen
|
||||
ringbuf[wlen-data.shape[0]:wlen,:]=data
|
||||
self.datawindow=ringbuf[self.idx-wlen:self.idx]
|
||||
if not i is None:
|
||||
self.sampidx=i
|
||||
if not tstamp is None:
|
||||
self.tstamp=tstamp
|
||||
self.lock.release()
|
||||
|
||||
def getBuffer(self,returnIdx=False):
|
||||
self.lock.acquire()
|
||||
try:
|
||||
out=self.datawindow.copy()
|
||||
except:
|
||||
out=None
|
||||
#print self.sampleno
|
||||
if returnIdx:
|
||||
out=(out,self.sampidx)
|
||||
self.lock.release()
|
||||
return out
|
||||
|
||||
def start(self):
|
||||
self.thread=threading.Thread(target=sampleLoop,args=(self,))
|
||||
self.thread.start()
|
||||
|
||||
def stopit(self):
|
||||
self.stop=True
|
||||
self.thread.join()
|
||||
|
||||
try:
|
||||
self.sock.close()
|
||||
except:
|
||||
pass
|
|
@ -1,304 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Wed Nov 16 15:00:23 2022
|
||||
|
||||
@author: s202442
|
||||
"""
|
||||
|
||||
|
||||
# needs socket and struct library
|
||||
from socket import socket, AF_INET, SOCK_STREAM
|
||||
from struct import unpack
|
||||
import sys
|
||||
import numpy as np
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
|
||||
"""Packets are received every 20 ms in the size that it fits the sampling rate
|
||||
|
||||
e.g.:
|
||||
for 1000 Hz packet size will be 20, because 20*50=1000
|
||||
for 2500 Hz packet size will be 50, because 50*50=2500
|
||||
for 50 kHz it will be 1000, because 1000*50=50000
|
||||
"""
|
||||
def average(arr, n, mode='mean'):
|
||||
if mode=='max':
|
||||
end = n * int(len(arr)/n)
|
||||
return np.max(arr[:end].reshape(-1, n), 1)
|
||||
arr = arr.T
|
||||
data_raw_new = np.zeros((arr.shape[0], int(arr.shape[1]/n)))
|
||||
for i in range(arr.shape[0]):
|
||||
a = arr[i]
|
||||
data_raw_new[i,:] = a.reshape(-1, n).mean(1)
|
||||
return data_raw_new.T
|
||||
|
||||
# Marker class for storing marker information
|
||||
class Marker:
|
||||
def __init__(self):
|
||||
self.position = 0
|
||||
self.points = 0
|
||||
self.channel = -1
|
||||
self.type = ""
|
||||
self.description = ""
|
||||
|
||||
# Helper function for receiving whole message
|
||||
def RecvData(socket, requestedSize):
|
||||
returnStream = bytes()
|
||||
while len(returnStream) < requestedSize:
|
||||
databytes = socket.recv(requestedSize - len(returnStream))
|
||||
if databytes == '':
|
||||
raise RuntimeError
|
||||
# print(databytes)
|
||||
returnStream += databytes
|
||||
|
||||
return returnStream
|
||||
|
||||
|
||||
# Helper function for splitting a raw array of
|
||||
# zero terminated strings (C) into an array of python strings
|
||||
def SplitString(raw):
|
||||
stringlist = []
|
||||
s = bytes()
|
||||
for i in range(len(raw)):
|
||||
if raw[i] != 0: #'\x00':
|
||||
s = s + raw[i].to_bytes(1, sys.byteorder)
|
||||
else:
|
||||
stringlist.append(s.decode())
|
||||
s = bytes()
|
||||
|
||||
return stringlist
|
||||
|
||||
|
||||
# Helper function for extracting eeg properties from a raw data array
|
||||
# read from tcpip socket
|
||||
def GetProperties(rawdata):
|
||||
|
||||
# Extract numerical data
|
||||
(channelCount, samplingInterval) = unpack('<Ld', rawdata[:12])
|
||||
|
||||
# Extract resolutions
|
||||
resolutions = []
|
||||
for c in range(channelCount):
|
||||
index = 12 + c * 8
|
||||
restuple = unpack('<d', rawdata[index:index+8])
|
||||
resolutions.append(restuple[0])
|
||||
|
||||
# Extract channel names
|
||||
print(type(rawdata))
|
||||
channelNames = SplitString(rawdata[12 + 8 * channelCount:])
|
||||
print(rawdata[12 + 8 * channelCount:])
|
||||
print('-----')
|
||||
print(channelNames)
|
||||
|
||||
return (channelCount, samplingInterval, resolutions, channelNames)
|
||||
|
||||
# Helper function for extracting eeg and marker data from a raw data array
|
||||
# read from tcpip socket
|
||||
def GetData(rawdata, channelCount):
|
||||
|
||||
# Extract numerical data
|
||||
(block, points, markerCount) = unpack('<LLL', rawdata[:12])
|
||||
# Extract eeg data as array of floats
|
||||
data = []
|
||||
for i in range(points * channelCount):
|
||||
index = 12 + 4 * i
|
||||
value = unpack('<f', rawdata[index:index+4])
|
||||
data.append(value[0])
|
||||
|
||||
# Extract markers
|
||||
markers = []
|
||||
index = 12 + 4 * points * channelCount
|
||||
for m in range(markerCount):
|
||||
markersize = unpack('<L', rawdata[index:index+4])
|
||||
ma = Marker()
|
||||
(ma.position, ma.points, ma.channel) = unpack('<LLl', rawdata[index+4:index+16])
|
||||
|
||||
typedesc = SplitString(rawdata[index+16:index+markersize[0]])
|
||||
ma.type = typedesc[0]
|
||||
ma.description = typedesc[1]
|
||||
|
||||
markers.append(ma)
|
||||
index = index + markersize[0]
|
||||
|
||||
return (block, points, markerCount, data, markers)
|
||||
|
||||
|
||||
|
||||
def sampleLoop(obj):
|
||||
# Get message header as raw array of chars
|
||||
firstpackage=True
|
||||
# databuf = np.empty((no.bufsiz,), dtype=np.uint8)
|
||||
databuf = bytearray(b' ' * obj.bufsiz)
|
||||
|
||||
block=0
|
||||
firstblock=0
|
||||
oldblock=0
|
||||
droppeds=0
|
||||
timeout=False
|
||||
data1s = []
|
||||
|
||||
while not obj.stop:
|
||||
|
||||
# Get message header as raw array of chars
|
||||
rawhdr = RecvData(obj.sock, 24)
|
||||
|
||||
# Split array into usefull information id1 to id4 are constants
|
||||
(id1, id2, id3, id4, msgsize, msgtype) = unpack('<llllLL', rawhdr)
|
||||
|
||||
# Get data part of message, which is of variable size
|
||||
rawdata = RecvData(obj.sock, msgsize - 24)
|
||||
if msgtype == 1:
|
||||
# Start message, extract eeg properties and display them
|
||||
(channelCount, samplingInterval, resolutions, channelNames) = GetProperties(rawdata)
|
||||
# reset block counter
|
||||
lastBlock = -1
|
||||
|
||||
print("Start")
|
||||
print("Number of channels: " + str(channelCount))
|
||||
print("Sampling interval: " + str(samplingInterval))
|
||||
print("Resolutions: " + str(resolutions))
|
||||
print("Channel Names: " + str(channelNames))
|
||||
|
||||
elif msgtype == 4:
|
||||
# Data message, extract data and markers
|
||||
(block, points, markerCount, data, markers) = GetData(rawdata, channelCount)
|
||||
if block!=0:
|
||||
ds=block-oldblock
|
||||
if ds!=1:
|
||||
droppeds += ds
|
||||
print('Dropped %i blocks'%(ds,))
|
||||
else:
|
||||
firstblock=block
|
||||
oldblock=block
|
||||
|
||||
# Check for overflow
|
||||
if lastBlock != -1 and block > lastBlock + 1:
|
||||
print("*** Overflow with " + str(block - lastBlock) + " datablocks ***" )
|
||||
lastBlock = block
|
||||
|
||||
data1s.extend(data)
|
||||
data1s = np.array(data1s)
|
||||
|
||||
# Print markers, if there are some in actual block
|
||||
marker_sig = np.zeros([1, int(len(data1s)/channelCount)])
|
||||
if markerCount > 0:
|
||||
for m in range(markerCount):
|
||||
print("Marker " + markers[m].description + " of type " + markers[m].type)
|
||||
marker_sig[0][markers[m].position] = 1
|
||||
t1 = time.time()
|
||||
# Put data at the end of actual buffer
|
||||
data_array = data1s.reshape([int(len(data1s)/channelCount), channelCount]) * np.array(resolutions)
|
||||
data_array = np.vstack([data_array.T, marker_sig]).T #isn't that too slow?
|
||||
if obj.avgPackets:
|
||||
resampling_coef = int((len(data)/channelCount)/20)
|
||||
data1=average(data_array, resampling_coef, 'mean')
|
||||
data1[:,-1]=average(data_array[:,-1], resampling_coef, 'max')
|
||||
obj.updateRingBuffer(data1,block)
|
||||
else:
|
||||
obj.updateRingBuffer(data_array,block)
|
||||
data1s = []
|
||||
|
||||
|
||||
elif msgtype == 3:
|
||||
# Stop message, terminate program
|
||||
print("Stop")
|
||||
finish = True
|
||||
obj.sock.close()
|
||||
|
||||
|
||||
##############################################################################################
|
||||
#
|
||||
# Main RDA routine
|
||||
#
|
||||
##############################################################################################
|
||||
|
||||
|
||||
class RDA():
|
||||
def __init__(self,ip='127.0.0.1', port=51244, buffersize=2**10, sendqueue=False,
|
||||
si=1/1000, ringbuffersize = 2**12, avgPackets=True):
|
||||
# Create a tcpip socket
|
||||
#con = socket(AF_INET, SOCK_STREAM)
|
||||
# Connect to recorder host via 32Bit RDA-port
|
||||
# adapt to your host, if recorder is not running on local machine
|
||||
# change port to 51234 to connect to 16Bit RDA-port
|
||||
|
||||
#ip_client = "169.254.200.198 "#.96.224"
|
||||
# ip_server = "169.254.252.66"
|
||||
# port = 51244
|
||||
# con.connect((ip_server, port))
|
||||
self.sock=socket(AF_INET, # Internet
|
||||
SOCK_STREAM) #UDP
|
||||
#self.sock.bind((ip_server, port))
|
||||
self.sock.connect((ip, port))
|
||||
self.sock.settimeout(2.)
|
||||
# s = socket(AF_INET, SOCK_DGRAM)
|
||||
# s.bind((ip_client, port))
|
||||
# s.settimeout(5)
|
||||
# print(s.recvfrom(1024))
|
||||
# con.settimeout(5)
|
||||
# Flag for main loop
|
||||
#finish = False
|
||||
|
||||
self.avgPackets = avgPackets
|
||||
self.bufsiz=buffersize
|
||||
self.ip=ip
|
||||
self.port=port
|
||||
self.sampidx=0
|
||||
self.tstamp=None
|
||||
self.tstamp0=None
|
||||
self.queue=queue.Queue()
|
||||
self.A=None
|
||||
self.stop=False
|
||||
self.idx=0
|
||||
self.ringbufferinit=True
|
||||
self.ringbuffersize=ringbuffersize
|
||||
self.sendqueue=sendqueue
|
||||
self.lock=threading.RLock()
|
||||
self.si=si
|
||||
|
||||
def updateRingBuffer(self,data,i=None,tstamp=None):
|
||||
if self.ringbufferinit:
|
||||
self.ringbuffer=np.zeros((self.ringbuffersize ,data.shape[1]),dtype=np.float32)
|
||||
self.ringbufferinit=False
|
||||
ringbuf=self.ringbuffer
|
||||
wlen=self.ringbuffersize
|
||||
self.lock.acquire()
|
||||
if (self.idx+data.shape[0])<=ringbuf.shape[0]:
|
||||
ringbuf[self.idx:self.idx+data.shape[0],:]=data
|
||||
self.idx+=data.shape[0]
|
||||
else:
|
||||
ringbuf[0:wlen-data.shape[0],:]=ringbuf[self.idx-wlen+data.shape[0]:self.idx,:]
|
||||
self.idx=wlen
|
||||
ringbuf[wlen-data.shape[0]:wlen,:]=data
|
||||
self.datawindow=ringbuf[self.idx-wlen:self.idx]
|
||||
if not i is None:
|
||||
self.sampidx=i
|
||||
if not tstamp is None:
|
||||
self.tstamp=tstamp
|
||||
self.lock.release()
|
||||
|
||||
def getBuffer(self,returnIdx=False):
|
||||
self.lock.acquire()
|
||||
try:
|
||||
out=self.datawindow.copy()
|
||||
except:
|
||||
out=None
|
||||
if returnIdx:
|
||||
out=(out,self.sampidx)
|
||||
self.lock.release()
|
||||
return out
|
||||
|
||||
def start(self):
|
||||
self.thread=threading.Thread(target=sampleLoop,args=(self,))
|
||||
self.thread.start()
|
||||
|
||||
def stopit(self):
|
||||
self.stop=True
|
||||
self.thread.join()
|
||||
|
||||
try:
|
||||
self.sock.close()
|
||||
except:
|
||||
pass
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import sys, os
|
||||
# sys.path.append("..")
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,9 +0,0 @@
|
|||
matplotlib
|
||||
mne
|
||||
numpy
|
||||
scipy
|
||||
pandas
|
||||
datetime
|
||||
PyWavelets
|
||||
cycler
|
||||
PyQt5
|
|
@ -1,2 +0,0 @@
|
|||
full_cap_file_path: settings/easycap-M10_63_NO.txt
|
||||
cap_file_path: settings/easycap-M10_16_NO.txt
|
|
@ -1,15 +0,0 @@
|
|||
time_between_trains: 8
|
||||
cut_time: 1
|
||||
number_of_channels: 18
|
||||
number_of_lines: 4
|
||||
eog_channel: -3
|
||||
emg_channel: -2
|
||||
included_channels: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
|
||||
names: [1,3,7,8,9,11,14,17,19,23,26,29,32,43,50,52, "EOG", "EMG"]
|
||||
alpha_range: [8,15]
|
||||
beta_range: [16,30]
|
||||
theta_range: [4,8]
|
||||
threshold_parameter: 2
|
||||
expected_triggers: 10
|
||||
expected_time: 2000
|
||||
plot_len: 4
|
|
@ -1,18 +0,0 @@
|
|||
Site Theta Phi
|
||||
1 0 0
|
||||
3 23 30
|
||||
7 -23 -30
|
||||
8 46 90
|
||||
9 46 66
|
||||
11 46 0
|
||||
14 -46 90
|
||||
17 -46 0
|
||||
19 -46 -66
|
||||
23 69 18
|
||||
26 69 -54
|
||||
29 -69 54
|
||||
32 -69 -18
|
||||
43 92 90
|
||||
50 92 -68
|
||||
52 -92 68
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
Site Theta Phi
|
||||
1 0 0
|
||||
2 23 90
|
||||
3 23 30
|
||||
4 23 -30
|
||||
5 -23 90
|
||||
6 -23 30
|
||||
7 -23 -30
|
||||
8 46 90
|
||||
9 46 66
|
||||
10 46 33
|
||||
11 46 0
|
||||
12 46 -33
|
||||
13 46 -66
|
||||
14 -46 90
|
||||
15 -46 66
|
||||
16 -46 33
|
||||
17 -46 0
|
||||
18 -46 -33
|
||||
19 -46 -66
|
||||
20 69 90
|
||||
21 69 66
|
||||
22 69 42
|
||||
23 69 18
|
||||
24 69 -6
|
||||
25 69 -30
|
||||
26 69 -54
|
||||
27 69 -78
|
||||
28 -69 78
|
||||
29 -69 54
|
||||
30 -69 30
|
||||
31 -69 6
|
||||
32 -69 -18
|
||||
41 -69 -42
|
||||
42 -69 -66
|
||||
43 92 90
|
||||
44 92 68
|
||||
45 92 45
|
||||
46 92 22
|
||||
47 92 0
|
||||
48 92 -22
|
||||
49 92 -45
|
||||
50 92 -68
|
||||
51 -92 90
|
||||
52 -92 68
|
||||
53 -92 45
|
||||
54 -92 22
|
||||
55 -92 0
|
||||
56 -92 -22
|
||||
57 -92 -45
|
||||
58 -92 -68
|
||||
59 115.000000000000 35
|
||||
60 115.000000000000 10
|
||||
61 115.000000000000 -15
|
||||
62 115.000000000000 -40
|
||||
63 115.000000000000 -65
|
||||
64 -115.000000000000 90
|
||||
65 -115.000000000000 65
|
||||
66 -115.000000000000 40
|
||||
67 -115.000000000000 15
|
||||
68 -115.000000000000 -10
|
||||
69 -115.000000000000 -35
|
||||
70 -135 -24
|
||||
71 135 24
|
|
@ -1,18 +0,0 @@
|
|||
1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0
|
||||
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0
|
|
|
@ -1,326 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Settings</class>
|
||||
<widget class="QWidget" name="Settings">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>680</width>
|
||||
<height>331</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>11</x>
|
||||
<y>11</y>
|
||||
<width>330</width>
|
||||
<height>309</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>measure 2 ax limits</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="label_13">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>7</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>y-axis caiibration</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="2">
|
||||
<widget class="QPushButton" name="pushButton_2">
|
||||
<property name="text">
|
||||
<string>Apply</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_5"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>measure 3 ax limits</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_4"/>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>measure 4 ax limits</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QLineEdit" name="lineEdit_13"/>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>measure 6 ax limits</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_7"/>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="QLineEdit" name="lineEdit_11"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLabel" name="label_14">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>7</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>x-axis calibration
|
||||
y-axis readout</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>measure 1 ax limits</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_2"/>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_3"/>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QLineEdit" name="lineEdit_10"/>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="3" alignment="Qt::AlignHCenter">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>plot limits</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLineEdit" name="lineEdit_9"/>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<widget class="QLineEdit" name="lineEdit_8"/>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>measure 5 ax limits</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="lineEdit_6"/>
|
||||
</item>
|
||||
<item row="7" column="2">
|
||||
<widget class="QLineEdit" name="lineEdit_12"/>
|
||||
</item>
|
||||
<item row="8" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>6</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>All values should be set like: [min, max]</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="checkBox">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>7</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use auto y-axis limits for readout</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>348</x>
|
||||
<y>11</y>
|
||||
<width>321</width>
|
||||
<height>151</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>321</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_12">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>7</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>No file loaded</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Montage</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Select file with montage</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="acceptDrops">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>File path: </string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="pushButton">
|
||||
<property name="text">
|
||||
<string>Load</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>348</x>
|
||||
<y>169</y>
|
||||
<width>321</width>
|
||||
<height>130</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>321</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="pushButton_4">
|
||||
<property name="text">
|
||||
<string>Restart without applying</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="pushButton_3">
|
||||
<property name="text">
|
||||
<string>Apply montage and restart</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_2">
|
||||
<property name="text">
|
||||
<string>Display max of each measurments after
|
||||
calibration</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<zorder>groupBox_3</zorder>
|
||||
<zorder>groupBox_2</zorder>
|
||||
<zorder>groupBox</zorder>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
1032
utils/FirstWindow.py
1032
utils/FirstWindow.py
File diff suppressed because it is too large
Load diff
|
@ -1,214 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Mon Jan 24 11:48:34 2022
|
||||
|
||||
@author: Basics
|
||||
"""
|
||||
import numpy as np
|
||||
import time
|
||||
import scipy.signal as ss
|
||||
import scipy
|
||||
|
||||
def apply_montage(data, matrix):
|
||||
#print(matrix)
|
||||
if data.shape[0]!=matrix.shape[0]:
|
||||
if data.shape[0]==matrix.shape[0]+1:
|
||||
data = data[:-1]
|
||||
times = time.time()
|
||||
new_data = np.zeros(data.shape)
|
||||
# for i in range(matrix.shape[0]):
|
||||
# for j in range(matrix.shape[1]):
|
||||
# if float(abs(matrix[i,j]))>0:
|
||||
# new_data[i,:]+=data[j]*float(matrix[i,j])
|
||||
# print(time.time()-times)
|
||||
print(data.shape, matrix.shape)
|
||||
new_data = (data.T@matrix.T).T
|
||||
return new_data
|
||||
|
||||
def eye_reg(eeg, eog, regg = True, Fs=1000):
|
||||
if regg:
|
||||
print(eeg.shape)
|
||||
eeg = eeg.T
|
||||
#eeg = ss.detrend(eeg.T)
|
||||
print(eog.shape)
|
||||
[A,B] = ss.butter(2, 40/(Fs/2), 'lowpass')
|
||||
eog = ss.filtfilt(A, B, eog)
|
||||
eogt = ss.detrend(eog.reshape([len(eog),1]),axis=0)
|
||||
#eogt = eog.reshape([len(eog),1])
|
||||
data_reg_eog = eeg - np.dot(eogt, np.linalg.lstsq(eogt,eeg, rcond=None)[0])
|
||||
print('EYE REG ZROBIONEEEE hmmm')
|
||||
print(data_reg_eog.shape, eogt.shape,eeg.shape)
|
||||
# plt.figure()
|
||||
# plt.plot(data_reg_eog[:,-5],c='b')
|
||||
# plt.plot(eeg[:,-5], c='orange')
|
||||
# plt.plot(eogt, c='green')
|
||||
# plt.show()
|
||||
return(data_reg_eog).T
|
||||
else:
|
||||
return(eeg)
|
||||
|
||||
def most_frequent(arr):
|
||||
"""returns most frequent item in the array
|
||||
|
||||
Parameters
|
||||
------------
|
||||
arr: array type
|
||||
|
||||
Returns
|
||||
------------
|
||||
most frequent item in the list or 0
|
||||
"""
|
||||
try:
|
||||
counts = np.bincount(arr)
|
||||
return np.argmax(counts)
|
||||
#return max(set(List), key = List.count)
|
||||
except:
|
||||
return 0
|
||||
|
||||
def connect_sig(data1, data2, fs):
|
||||
"""Knowing that signal doesn't updates perfectly every x seconds, I was looking for a way
|
||||
to connect it in proper time, but finally other approach have been used
|
||||
|
||||
Parameters
|
||||
-------------
|
||||
data1, data2: numpy array types with MxN size
|
||||
fs: int
|
||||
sampling rate
|
||||
"""
|
||||
print(data1.shape, data2.shape)
|
||||
if all(data1[:, -fs] == None):
|
||||
print(data2[:, -fs])
|
||||
data_ret = data1.copy()
|
||||
data_ret[:, :-fs] = data2[:, -fs].reshape(-1, 1)
|
||||
data_ret[:,-fs:] = data2[:, -fs:]
|
||||
return data_ret, 800
|
||||
|
||||
print(data2.shape)
|
||||
data2 = data2
|
||||
startt = time.time()
|
||||
num = data1.shape[0]
|
||||
size = data1.shape[1]
|
||||
pts = list()
|
||||
data_ret = np.zeros(data1.shape)
|
||||
data_ret[:, :-fs] = data1[:,fs:]
|
||||
|
||||
if data2.shape[0]==0 or data2.shape[1]==0:
|
||||
return data2
|
||||
if fs<2000:
|
||||
for i in range(num):
|
||||
try:
|
||||
pts.extend(np.where(data1[i,-1]==data2[i])[0].tolist())
|
||||
except:
|
||||
print("ARBEJDE IKKEEE")
|
||||
# data_ret = np.concatenate((data1, data2[:,-int(size):]),1)
|
||||
data_ret[:,-fs:] = data2[:, -fs:]
|
||||
return data_ret, 800
|
||||
#print('hehe', time.time()-startt)
|
||||
most_fr = most_frequent(np.array(pts))
|
||||
#print('hehe', time.time()-startt)
|
||||
print(list(pts).count(most_fr)>num//2, most_fr)
|
||||
if fs<2000 and list(pts).count(most_fr)>num//2:
|
||||
most_fr = most_fr+1
|
||||
#print(data2[:,int(pts[i]):int(pts[i]+size)].shape)
|
||||
print('hehe', time.time()-startt)
|
||||
#data_ret = np.concatenate((data1, data2[:,int(most_fr)+1:int(most_fr+size)+1]),1)
|
||||
data_ret[:,-fs:] = data2[:, most_fr:most_fr+fs]
|
||||
print('connect:', time.time()-startt)
|
||||
return data_ret, most_fr
|
||||
else:
|
||||
data_ret[:,-fs:] = data2[:, -fs:]
|
||||
print("NEEEJJJJJJJ")
|
||||
print('connect:', time.time()-startt)
|
||||
return data_ret, 800
|
||||
|
||||
def set_to_gray(lines):
|
||||
for i in range(lines.shape[0]):
|
||||
for j in range(lines.shape[1]):
|
||||
if type(lines[i,j]) != int:
|
||||
lines[i,j][0].set_color("gray")
|
||||
|
||||
def update_stem(line, data, ax, relim=False):
|
||||
x = adjust_hist(data[1])
|
||||
y = data[0]
|
||||
|
||||
line[0].set_ydata(y)
|
||||
line[0].set_xdata(x) # not necessary for constant x
|
||||
|
||||
# stemlines
|
||||
# line[1].set_paths([np.array([[xx, 0], [xx, yy]]) for (xx, yy) in zip(x, y)])
|
||||
# line[2].set_xdata([np.min(x), np.max(x)])
|
||||
# line[2].set_ydata([0, 0]) # not necessary for constant bottom
|
||||
if relim:
|
||||
ax.relim()
|
||||
# update ax.viewLim using the new dataLim
|
||||
ax.autoscale_view(scalex=True)
|
||||
|
||||
|
||||
def adjust_hist(data):
|
||||
new_data = np.zeros(len(data)-1)
|
||||
for i in range(len(new_data)):
|
||||
new_data[i] = np.mean([data[i], data[i+1]])
|
||||
return new_data
|
||||
|
||||
def min_zero(thr):
|
||||
if thr[0]<0:
|
||||
return [0, thr[1]]
|
||||
else:
|
||||
return thr
|
||||
|
||||
def pentropy(signal, Fs, nperseg=None, fmin=None, fmax=None):
|
||||
#I think it's good to put own nperseg value, because default one is not always good in this case
|
||||
f, time, S = ss.spectrogram(signal, Fs, nperseg=nperseg) #spectrogram of signal
|
||||
if fmin and fmax:
|
||||
idxs = np.where((f<fmax) & (f>fmin)) #choosing only bands that we are interested in
|
||||
S = S[idxs]
|
||||
P = np.zeros(S.shape)
|
||||
H = np.zeros(S.shape[1])
|
||||
for t in range(S.shape[1]):
|
||||
for m in range(S.shape[0]):
|
||||
P[m,t] = S[m,t]/np.sum(S[:,t],0) #according to matlab instruction
|
||||
H[t] += -P[m,t]*np.log2(P[m,t]+0.000001)/np.log2(S.shape[0])
|
||||
return f, S,time, H
|
||||
|
||||
def entropy(S, bins=100):
|
||||
histo = np.histogram(S, bins)[0]
|
||||
p = scipy.special.entr(histo/histo.sum())
|
||||
ent = sum(p)
|
||||
return ent
|
||||
|
||||
def check_integrity(stim):
|
||||
"""If the list contains two consecutive values, the function leaves only the first.
|
||||
It was needed because NeurOne sometimes returns two stimuli pulses78.
|
||||
|
||||
Parameters
|
||||
-----------------
|
||||
stim: list or array type with numbers, in our case indexes of stimuli
|
||||
|
||||
Returns
|
||||
-----------------
|
||||
ent: array with removed values
|
||||
|
||||
"""
|
||||
#copy the list, I need two, because from one values are removed, and other gives
|
||||
#proper index values
|
||||
stim_c = list(stim.copy())
|
||||
stim = list(stim)
|
||||
for ind in range(len(stim_c)-1):
|
||||
if stim_c[ind]==(stim_c[ind+1]-1):
|
||||
stim.remove(stim_c[ind+1])
|
||||
return np.array(stim)
|
||||
|
||||
def doubleMADsfromMedian(y,thresh=3.5):
|
||||
# warning: this function does not check for NAs
|
||||
# nor does it address issues when
|
||||
# more than 50% of your data have identical values
|
||||
m = np.median(y)
|
||||
abs_dev = np.abs(y - m)
|
||||
left_mad = np.median(abs_dev[y <= m])
|
||||
right_mad = np.median(abs_dev[y >= m])
|
||||
y_mad = left_mad * np.ones(len(y))
|
||||
y_mad[y > m] = right_mad
|
||||
modified_z_score = 0.6745 * abs_dev / y_mad
|
||||
modified_z_score[y == m] = 0
|
||||
return modified_z_score > thresh
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Mon Jan 24 11:26:23 2022
|
||||
|
||||
@author: Basics
|
||||
"""
|
||||
from PyQt5.QtWidgets import (QLabel, QDialog, QDialogButtonBox,
|
||||
QVBoxLayout)
|
||||
|
||||
class Waiting_window(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle("Wait...")
|
||||
|
||||
QBtn = QDialogButtonBox.Ok
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.message = QLabel('Waiting for the data (should take up to 30 sec)')
|
||||
self.layout.addWidget(self.message)
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
self.setLayout(self.layout)
|
||||
self.show()
|
||||
def setText(self,text):
|
||||
self.message.setText(text)
|
|
@ -1,3 +0,0 @@
|
|||
import sys, os
|
||||
# sys.path.append("..")
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Wed Jan 18 17:01:32 2023
|
||||
|
||||
@author: adamr
|
||||
"""
|
Binary file not shown.
Before Width: | Height: | Size: 252 KiB |
Binary file not shown.
Before Width: | Height: | Size: 233 KiB |
Binary file not shown.
Before Width: | Height: | Size: 347 KiB |
Loading…
Reference in a new issue