visbrain icon indicating copy to clipboard operation
visbrain copied to clipboard

Distinguishing N3 from N2 by computing length of slow waves with a line

Open SvennoNito opened this issue 4 years ago • 10 comments

Hey everybody, first of all congrats to such a nice toolbox! My team is considering using your toolbox as the new default program for sleep scoring data. However, for distinguishing between N2 and N3, we need some kind of ruler functionality, with which we can measure the length of slow waves (20% of slow waves in an epoch --> N3). The amplitude is easy to determine with the grid. But how do you determine whether 20% of the epoch contains slow waves? Our old program had a ruler or line (in green) that we could draw from positive to positive peak of a slow wave, which then added up the time in seconds of counted slow waves (top right corner). When 6s were reached, we staged the epoch as N3. Can such a feature be implemented? Thanks a lot!

example (Note that negative is up in this recording)

SvennoNito avatar Mar 14 '20 10:03 SvennoNito

@SvennoNito

Hey everybody, first of all congrats to such a nice toolbox!

Thanks, it's always pleasant to hear that !

However, for distinguishing between N2 and N3, we need some kind of ruler functionality, with which we can measure the length of slow waves (20% of slow waves in an epoch --> N3). The amplitude is easy to determine with the grid. But how do you determine whether 20% of the epoch contains slow waves?

@raphaelvallat and @TomBugnon are the two sleep experts. Could it be interesting to add it in Sleep?

Can such a feature be implemented?

Well, yes, since visbrain is an open source toolbox, you can fork it, make your changes and then we can discuss for a pull request to integrate it. One thing that should be noticed is that you can change the default detection algorithms of Sleep using your own detections. See replace_detections

EtienneCmb avatar Mar 14 '20 10:03 EtienneCmb

Hey @EtienneCmb, thanks for your quick response. I wish I was that fluent in python that I could simply implement it, but I'll let you know if I accomplish it nevertheless. It's not a change in the detection algorithm though, but rather a new feature than when you click and hold at the recording, that you draw a horizontal line that measures time in seconds. If there's another way to distinguish N2 from N3, I'm of course also happy to hear that, the suggested idea was just the way we have done it so far and I haven't seen any alternative feature in the toolbox.

SvennoNito avatar Mar 14 '20 14:03 SvennoNito

Hi @SvennoNito! I've worked with several sleep scoring software and never seen such a ruler to calculate the 20% threshold, but I can only admire such exactitude in determining the sleep stages!

I think it's gonna require some fundamental changes in the toolbox though, which also means a lot of time, which I'm not sure we have right now...That said, one feature that I've always wanted to add to sleep, and that could perhaps represent a first step here would be a similar "click-and-hold" marker to visually mark sleep spindles, arousals, etc.. Once we have that, I think it will be easy to just create a textbox that represents the cumulated duration of marked data ("label: slow-waves") in the current epoch. Does that make any sense? Any thoughts @EtienneCmb @TomBugnon @SvennoNito

Thanks again, Raphael

raphaelvallat avatar Mar 14 '20 21:03 raphaelvallat

Hey @raphaelvallat. Yes such a feature would totally satisfy our need to compute the duration of slow waves per epoch. And it would be even more powerful in case you want to count other events. I like the idea. Especially if you could optionally export those event later on.

Best, Sven

SvennoNito avatar Mar 18 '20 10:03 SvennoNito

Hey @raphaelvallat, just wanted to ask whether there has been any steps made into the direction of this feature? I think our lab is going to decide soon on a new program and I still would like to advert SLEEP ;-)

Best, Sven

SvennoNito avatar Apr 23 '20 09:04 SvennoNito

Hi @SvennoNito! I am so sorry but I doubt we will implement this feature in the near future... I've too many other projects at the time.

Thanks, Raphael

raphaelvallat avatar Apr 23 '20 16:04 raphaelvallat

Thank you @raphaelvallat, I can understand that, no worries.

Best, Sven

SvennoNito avatar Apr 23 '20 22:04 SvennoNito

Dear @raphaelvallat,

I am still eager to introduce the toolbox to our lab and now tried to implement this feature myself, counting the length of slow waves per epoch. I managed to write code that effectively draws horizontal green lines between a mouse click and a mouse release and to show the accumulated length on the top right corner. What I terribly fail at, however, is integrating it into your toolbox. Maybe you could give me a hint?

lines

I found that in visbrain\gui\sleep\visuals\visuals.py, the class ChannelPlot(PrepareData) seems to plot the EEG data. I tried to somehow implement the functions to draw a line in this class, but it's probably not the right way. Maybe it's already obvious for you where such a feature needs to go?

Here's the code:

import sys
from PyQt5.QtWidgets import QDialog, QApplication
from PyQt5.QtGui import QPainter, QPen, QPixmap
from PyQt5.QtCore import Qt, QLine, QPoint
from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.resize(500, 340) # Build the surface
        self.labelRelease = QtWidgets.QLabel(Dialog) # Dialog probably needs to be replaced with something from the toolbox
        self.labelRelease.setGeometry(QtCore.QRect(450, 10, 45, 40)) # Size of green rectancle around the text showing the length of all lines drawn  
        font = QtGui.QFont()
        font.setPointSize(12) # Font Size
        self.labelRelease.setFont(font)
        self.labelRelease.setText("") # Initialize empty text
        self.labelRelease.setStyleSheet("border: 3px solid green;") # Green rectangle witch a thickness of 3 px    

class GreenLines(QDialog):

    def __init__(self):
        super().__init__()
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)  # The setupUi() method sets up the widgets; it creates the widgets that you use while defining the user interface in Qt Designer.
        self.pos1 = [0,0] # Mouse position at mouse click
        self.pos2 = [0,0] # Mouse position at mouse release
        self.totalLineLength = 0 # Length of all lines drawn
        self.lines = [] # Stores all lines drawn
        self.eraseLinesKeys = ['n', 'b', 'a', 'w', '1', '2', '3'] # Pressing one of these buttons will erase all lines drawn (should be adaptive if the user changes those keys)
        self.reset = 0 # If 1, reset erase lines drawn
        self.show()

    def paintEvent(self, event):
        # Draw horizontal lines between mouse click and mouse release
        qp = QPainter()
        qp.begin(self)
        pen = QPen(Qt.darkGreen, 5) # Dark green line of width 5 px
        pen.setStyle(Qt.SolidLine)
        qp.setPen(pen)
        if self.reset == 1: 
            qp.drawLine(0,0,0,0) # Erase all lines
            self.ui.labelRelease.setText("") # Erase text showing length of lines
            self.reset = 0
        else:                          
            p1       = QPoint(self.pos1[0], self.pos2[1]) # Line start
            p2       = QPoint(self.pos2[0], self.pos2[1]) # Line end
            self.lines.append(QLine(p1, p2)) # Store this line
            for line in self.lines:
                qp.drawLine(line) # Draw all lines
        qp.end()

    def mousePressEvent(self, event):
        # Get mouse position at button press
        if event.buttons() & QtCore.Qt.LeftButton:
            self.pos1[0], self.pos1[1] = event.pos().x(), event.pos().y() # Mouse position

    def mouseReleaseEvent(self, event):
        # Get mouse position at button release
            self.pos2[0], self.pos2[1] = event.pos().x(), event.pos().y() # Mouse position
            self.totalLineLength += self.pos2[0] - self.pos1[0] # Length between button release and button press (still needs to converted into seconds)
            self.ui.labelRelease.setText("{0}s".format(self.totalLineLength)) # Show line length
            self.update() # Evoke paintEvent

    def keyPressEvent(self, event):
        # When keys are pressed, evoke the following functions
        if event.text() in self.eraseLinesKeys:
            self.resetLines()

    def resetLines(self):
        # Erases all lines drawn
        self.pos1 = [0,0] # Reset mouse position at click
        self.pos2 = [0,0] # Reset mouse position at release
        self.lines = [] # Erases all lines drawn
        self.totalLineLength = 0 # Set length of lines to 0
        self.reset = 1 # To erase all lines in paintEvent
        self.update() # Evple paintEvent

if __name__=="__main__":
    app = QApplication(sys.argv)
    w = GreenLines()
    w.show()
    sys.exit(app.exec_())    

Thanks! I'm looking forward to your ideas and all best from Zurich, Sven

SvennoNito avatar Aug 19 '20 09:08 SvennoNito

Hi @SvennoNito, thanks for sharing! The main PyQt interface was designed by @EtienneCmb so he will probably know better than me where such a code snippet should be integrated.

Best, Raphael

raphaelvallat avatar Aug 20 '20 20:08 raphaelvallat

Thanks @raphaelvallat . What do you think @EtienneCmb ?

SvennoNito avatar Sep 14 '20 08:09 SvennoNito