From b35f8ee9ea7b1b51a7a1981859b0890b1b7840d9 Mon Sep 17 00:00:00 2001 From: drumph Date: Tue, 10 May 2022 13:56:40 -0700 Subject: [PATCH 1/3] Update mainWindow ui design --- src/mainWindow.ui | 57 ++++++++++++++++++++++++--------------- src/mainWindow_ui.py | 64 ++++++++++++++++++++++++++------------------ 2 files changed, 74 insertions(+), 47 deletions(-) diff --git a/src/mainWindow.ui b/src/mainWindow.ui index 1cde407..95ea43a 100644 --- a/src/mainWindow.ui +++ b/src/mainWindow.ui @@ -6,12 +6,12 @@ 0 0 - 460 - 328 + 604 + 409 - + 0 0 @@ -24,8 +24,8 @@ - 457 - 328 + 10000 + 10000 @@ -70,6 +70,9 @@ Qt::ScrollBarAlwaysOff + + Qt::ScrollBarAlwaysOn + QGraphicsView::AnchorViewCenter @@ -129,48 +132,60 @@ - + - + - Previous + /2 - + - Next + 1x - - - - - + - /2 + * 2 - + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - 1x + Previous - + - * 2 + Next + + + @@ -219,7 +234,7 @@ 0 0 - 460 + 604 24 diff --git a/src/mainWindow_ui.py b/src/mainWindow_ui.py index 0a05075..af34345 100644 --- a/src/mainWindow_ui.py +++ b/src/mainWindow_ui.py @@ -3,30 +3,37 @@ ################################################################################ ## Form generated from reading UI file 'mainWindow.ui' ## -## Created by: Qt User Interface Compiler version 6.1.2 +## Created by: Qt User Interface Compiler version 6.2.2 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from qtpy.QtCore import * # type: ignore -from qtpy.QtGui import * # type: ignore -from qtpy.QtWidgets import * # type: ignore +from qtpy.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from qtpy.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from qtpy.QtWidgets import (QApplication, QComboBox, QGraphicsView, QHBoxLayout, + QLabel, QMainWindow, QMenuBar, QPushButton, + QSizePolicy, QSpacerItem, QStatusBar, QVBoxLayout, + QWidget) from widgets.annotationsWidget import AnnotationsView - class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(460, 328) - sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + MainWindow.resize(604, 409) + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) MainWindow.setSizePolicy(sizePolicy) MainWindow.setMinimumSize(QSize(460, 328)) - MainWindow.setMaximumSize(QSize(457, 328)) + MainWindow.setMaximumSize(QSize(10000, 10000)) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") sizePolicy1 = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) @@ -52,6 +59,7 @@ def setupUi(self, MainWindow): self.annotationsView.setMinimumSize(QSize(0, 50)) self.annotationsView.setAcceptDrops(False) self.annotationsView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.annotationsView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.annotationsView.setResizeAnchor(QGraphicsView.AnchorViewCenter) self.verticalLayout.addWidget(self.annotationsView) @@ -96,21 +104,6 @@ def setupUi(self, MainWindow): self.verticalLayout.addLayout(self.controlButtonLayout) - self.nextPrevLayout = QHBoxLayout() - self.nextPrevLayout.setObjectName(u"nextPrevLayout") - self.previousButton = QPushButton(self.centralwidget) - self.previousButton.setObjectName(u"previousButton") - - self.nextPrevLayout.addWidget(self.previousButton) - - self.nextButton = QPushButton(self.centralwidget) - self.nextButton.setObjectName(u"nextButton") - - self.nextPrevLayout.addWidget(self.nextButton) - - - self.verticalLayout.addLayout(self.nextPrevLayout) - self.playbackSpeedLayout = QHBoxLayout() self.playbackSpeedLayout.setObjectName(u"playbackSpeedLayout") self.halveFrameRateButton = QPushButton(self.centralwidget) @@ -128,9 +121,28 @@ def setupUi(self, MainWindow): self.playbackSpeedLayout.addWidget(self.doubleFrameRateButton) + self.horizontalSpacer_4 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.playbackSpeedLayout.addItem(self.horizontalSpacer_4) + + self.previousButton = QPushButton(self.centralwidget) + self.previousButton.setObjectName(u"previousButton") + + self.playbackSpeedLayout.addWidget(self.previousButton) + + self.nextButton = QPushButton(self.centralwidget) + self.nextButton.setObjectName(u"nextButton") + + self.playbackSpeedLayout.addWidget(self.nextButton) + self.verticalLayout.addLayout(self.playbackSpeedLayout) + self.nextPrevLayout = QHBoxLayout() + self.nextPrevLayout.setObjectName(u"nextPrevLayout") + + self.verticalLayout.addLayout(self.nextPrevLayout) + self.mainButtonLayout = QHBoxLayout() self.mainButtonLayout.setObjectName(u"mainButtonLayout") self.channelComboBox = QComboBox(self.centralwidget) @@ -163,7 +175,7 @@ def setupUi(self, MainWindow): MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 460, 24)) + self.menubar.setGeometry(QRect(0, 0, 604, 24)) MainWindow.setMenuBar(self.menubar) self.statusbar = QStatusBar(MainWindow) self.statusbar.setObjectName(u"statusbar") @@ -185,11 +197,11 @@ def retranslateUi(self, MainWindow): self.stepButton.setText(QCoreApplication.translate("MainWindow", u">", None)) self.ffButton.setText(QCoreApplication.translate("MainWindow", u">>", None)) self.toEndButton.setText(QCoreApplication.translate("MainWindow", u">|", None)) - self.previousButton.setText(QCoreApplication.translate("MainWindow", u"Previous", None)) - self.nextButton.setText(QCoreApplication.translate("MainWindow", u"Next", None)) self.halveFrameRateButton.setText(QCoreApplication.translate("MainWindow", u"/2", None)) self.oneXFrameRateButton.setText(QCoreApplication.translate("MainWindow", u"1x", None)) self.doubleFrameRateButton.setText(QCoreApplication.translate("MainWindow", u"* 2", None)) + self.previousButton.setText(QCoreApplication.translate("MainWindow", u"Previous", None)) + self.nextButton.setText(QCoreApplication.translate("MainWindow", u"Next", None)) self.newChannelPushButton.setText(QCoreApplication.translate("MainWindow", u"New Channel", None)) self.trialPushButton.setText(QCoreApplication.translate("MainWindow", u"Select Trial...", None)) self.quitButton.setText(QCoreApplication.translate("MainWindow", u"Quit", None)) From cdd2c13143ef6e1eae29a7be7daf5d75c8d4573c Mon Sep 17 00:00:00 2001 From: drumph Date: Mon, 13 Jun 2022 13:29:27 -0700 Subject: [PATCH 2/3] Development checkpoint --- .gitignore | 3 +- src/bento.py | 1 + src/mainWindow.py | 1 - src/mainWindow.ui | 2 +- src/mainWindow_ui.py | 2 +- src/neural/neuralFrame.py | 1 - src/utils/__init__.py | 2 +- src/video/seqIo.py | 9 ++-- src/widgets/annotationsWidget.py | 73 +++++++++++++++++++++----------- 9 files changed, 60 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 6b5e089..f9b4cb6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /.vscode/ .DS_Store timecode-1.3.1/ -timecode-1.3.1.tar \ No newline at end of file +timecode-1.3.1.tar +*.h5 diff --git a/src/bento.py b/src/bento.py index d552910..4222550 100644 --- a/src/bento.py +++ b/src/bento.py @@ -197,6 +197,7 @@ def load_or_init_annotations(self, fn, sample_rate = 30., start_time = None): self.annotationsScene.loaded = True self.annotations.active_annotations_changed.connect(self.noteAnnotationsChanged) self.set_time(self.time_start) + self.annotationsScene.sceneRectChanged.emit(self.annotationsScene.sceneRect()) @Slot() def newChannel(self): diff --git a/src/mainWindow.py b/src/mainWindow.py index 877480f..e692b26 100644 --- a/src/mainWindow.py +++ b/src/mainWindow.py @@ -39,7 +39,6 @@ def __init__(self, bento): self.ui.annotationsView.setScene(bento.annotationsScene) bento.annotationsScene.sceneRectChanged.connect(self.ui.annotationsView.update) self.ui.annotationsView.scale(10., self.ui.annotationsView.height()) - self.ui.annotationsView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) bento.annotationsSceneHeightChanged.connect(self.ui.annotationsView.setVScaleAndShow) self.populateChannelsCombo() self.ui.channelComboBox.currentTextChanged.connect(bento.setActiveChannel) diff --git a/src/mainWindow.ui b/src/mainWindow.ui index 95ea43a..c3c53b3 100644 --- a/src/mainWindow.ui +++ b/src/mainWindow.ui @@ -7,7 +7,7 @@ 0 0 604 - 409 + 328 diff --git a/src/mainWindow_ui.py b/src/mainWindow_ui.py index af34345..2bd712f 100644 --- a/src/mainWindow_ui.py +++ b/src/mainWindow_ui.py @@ -26,7 +26,7 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(604, 409) + MainWindow.resize(604, 328) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) diff --git a/src/neural/neuralFrame.py b/src/neural/neuralFrame.py index 49986ce..9141419 100644 --- a/src/neural/neuralFrame.py +++ b/src/neural/neuralFrame.py @@ -40,7 +40,6 @@ def __init__(self, bento): self.ui.annotationsView.setScene(bento.annotationsScene) self.ui.annotationsView.scale(10., self.ui.annotationsView.height()) self.ui.annotationsView.setVScaleAndShow(bento.annotationsScene.sceneRect().height()) - self.ui.annotationsView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.ui.neuralView.hScaleChanged.connect(self.ui.annotationsView.setHScaleAndShow) bento.annotationsSceneHeightChanged.connect(self.ui.annotationsView.setVScaleAndShow) self.annotations = self.bento.annotations diff --git a/src/utils/__init__.py b/src/utils/__init__.py index ef0ed5e..17ccca0 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -8,7 +8,7 @@ def fix_path(path): return abspath(path.replace('\\', sep).replace('/', sep)) SCENE_PADDING = 200. -def padded_rectf(rectf: QRectF): +def padded_rectf(rectf: QRectF) -> QRectF: return rectf + QMarginsF(SCENE_PADDING, 0., SCENE_PADDING, 0.) cm_data_parula = [ diff --git a/src/video/seqIo.py b/src/video/seqIo.py index c231411..dfd586c 100755 --- a/src/video/seqIo.py +++ b/src/video/seqIo.py @@ -442,7 +442,7 @@ def __init__(self,filename,info=[],buildTable=True): info.numFrames=0 if buildTable: print("buildTable was True, so calling buildSeekTable()") - self.buildSeekTable(False) + self.buildSeekTable(True) def readHeader(self): @@ -526,7 +526,7 @@ def readHeader(self): self.timestamp_length = int(self.header['trueImageSize'] \ - (self.bit_depth / 8 * (self.header['height'] * self.header['width']))) - def buildSeekTable(self,memoize=False): + def buildSeekTable(self,memoize=True): """Build a seek table containing the offset and frame size for every frame in the video.""" print("in seqIo_reader.buildSeekTable()") pickle_name = self.filename.strip(".seq") + ".seek" @@ -588,7 +588,10 @@ def buildSeekTable(self,memoize=False): else: self.seek_table=seek_table if memoize: - pickle.dump(seek_table,open(pickle_name,'wb')) + try: + pickle.dump(seek_table,open(pickle_name,'wb')) + except OSError as e: + print(f"Problem writing seek table file {e.filename}, maybe no write permission?") #compute frame rate from timestamps as stored fps may be incorrect # if n==1: return diff --git a/src/widgets/annotationsWidget.py b/src/widgets/annotationsWidget.py index 2eb4a69..f1c003f 100644 --- a/src/widgets/annotationsWidget.py +++ b/src/widgets/annotationsWidget.py @@ -27,9 +27,15 @@ def __init__(self, parent=None): #self.v_factor = self.height() self.scale(self.scale_v, self.scale_h) self.sample_rate = 30. + self.disablePositionUpdates = False self.time_x = Timecode(str(self.sample_rate), '0:0:0:1') - self.horizontalScrollBar().sliderReleased.connect(self.updateFromScroll) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + # NB: Unfortunately, we can't turn on scrollBar tracking, because it causes + # an infinite loop setting the current time -> moving the scroll bar -> + # updating the time, etc. + self.horizontalScrollBar().sliderPressed.connect(self.startHScroll) + self.horizontalScrollBar().sliderMoved.connect(self.updateFromScroll) + self.horizontalScrollBar().sliderReleased.connect(self.endHScroll) + self.horizontalScrollBar().setTracking(True) self.ticksScale = 1. self.setInteractive(False) self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) @@ -42,6 +48,10 @@ def set_bento(self, bento): @Slot(Timecode) def updatePosition(self, t): + if self.disablePositionUpdates: + # break infinite signal loop when we're the source of the + # time update + return pt = QPointF(t.float, self.scene().height/2.) self.centerOn(pt) self.show() @@ -119,14 +129,26 @@ def mouseMoveEvent(self, event): )) event.accept() + @Slot() + def startHScroll(self): + self.disablePositionUpdates = True + + @Slot(int) def updateFromScroll(self): assert self.bento + viewrect = self.viewport().rect() + print(f"viewrect width: {viewrect.width()}") center = self.viewport().rect().center() sceneCenter = self.mapToScene(center) - self.bento.set_time(Timecode( - self.time_x.framerate, - start_seconds=sceneCenter.x() - )) + newTime = Timecode( + framerate=self.time_x.framerate, + start_seconds=sceneCenter.x()) + self.bento.set_time(newTime) + + @Slot() + def endHScroll(self): + self.disablePositionUpdates = False + self.updateFromScroll() def maybeDrawPendingBout(self, painter, rect): bout = self.bento.pending_bout @@ -198,21 +220,21 @@ def __init__(self, sample_rate=30.): self.chan_map = {} self.loaded = False - def addBout(self, bout, chan): - """ - Add a bout to the scene according to its timecode and channel name or number. - """ - if isinstance(chan, int): - chan_num = chan - elif isinstance(chan, str): - if chan not in self.chan_map.keys(): - self.chan_map[chan] = len(self.chan_map.keys()) # add the new channel - chan_num = self.chan_map[chan] - else: - raise RuntimeError(f"addBout: expected int or str, but got {type(chan)}") - color = bout.color - self.addRect(bout.start().float, float(chan_num), bout.len().float, 1., QPen(QBrush(), 0, s=Qt.NoPen), QBrush(color())) - self.loaded = True + # def addBout(self, bout, chan): + # """ + # Add a bout to the scene according to its timecode and channel name or number. + # """ + # if isinstance(chan, int): + # chan_num = chan + # elif isinstance(chan, str): + # if chan not in self.chan_map.keys(): + # self.chan_map[chan] = len(self.chan_map.keys()) # add the new channel + # chan_num = self.chan_map[chan] + # else: + # raise RuntimeError(f"addBout: expected int or str, but got {type(chan)}") + # color = bout.color + # self.addRect(bout.start().float, float(chan_num), bout.len().float, 1., QPen(QBrush(), 0, s=Qt.NoPen), QBrush(color())) + # self.loaded = True def loadAnnotations(self, annotations, activeChannels, sample_rate): self.setSampleRate(sample_rate) @@ -226,10 +248,11 @@ def loadAnnotations(self, annotations, activeChannels, sample_rate): # self.loadBouts(annotations.channel(chan), ix) self.loaded = True - def loadBouts(self, channel, chan_num): - print(f"Loading bouts for channel {chan_num}") - for bout in channel: - self.addBout(bout, chan_num) + + # def loadBouts(self, channel, chan_num): + # print(f"Loading bouts for channel {chan_num}") + # for bout in channel: + # self.addBout(bout, chan_num) def setSampleRate(self, sample_rate): self.sample_rate = sample_rate From 70107ddcc91fe6beebafbe0b26f8932519731232 Mon Sep 17 00:00:00 2001 From: drumph Date: Tue, 5 Jul 2022 11:25:52 -0700 Subject: [PATCH 3/3] Limit data to only the frame range specified This should also improve performance in cases where there are data from multiple trials in the same file. The issue remains how to display the data, which has no padding, in the neural viewer. Either we pad the scene, or we change the viewer behavior to accommodate the edges of the data. --- src/widgets/neuralWidget.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/widgets/neuralWidget.py b/src/widgets/neuralWidget.py index c344375..dc31dcf 100644 --- a/src/widgets/neuralWidget.py +++ b/src/widgets/neuralWidget.py @@ -284,14 +284,18 @@ def loadNeural(self, ca_file, sample_rate, start_frame, stop_frame, time_start, warnings.simplefilter('ignore', category=UserWarning) mat = pmr.read_mat(ca_file) try: - data = mat['results']['C_raw'] + # restrict data to just the range for this trial + data = mat['results']['C_raw'][:,start_frame:stop_frame] except Exception as e: QMessageBox.about(self, "Load Error", f"Error loading neural data from file {ca_file}: {e}") return - self.range = data.max() - data.min() + + # set up some values needed inside normalize() + self.data_min = data.min() + self.data_range = data.max() - self.data_min # Provide for a little space between traces - self.minimum = data.min() + self.range * 0.05 - self.range *= 0.9 + self.plot_min = 0.05 + self.plot_range = 0.9 self.sample_rate = sample_rate self.start_frame = start_frame @@ -308,16 +312,19 @@ def loadNeural(self, ca_file, sample_rate, start_frame, stop_frame, time_start, self.heatmapImage = self.colorMapper.mappedImage(data) self.heatmap = self.addPixmap(QPixmap.fromImageInPlace(self.heatmapImage, Qt.NoFormatConversion)) - # Scale the heatmap's time axis by the 1 / sample rate so that it corresponds correctly - # to the time scale transform = QTransform() + # Scale the heatmap's time axis by 1 / sample rate so that it corresponds correctly + # to the time scale (unit seconds) transform.scale(1. / self.sample_rate, 1.) + # Move the heatmap's origin to correspond to the time_start of this data + transform.translate(self.time_start.float, 0.) self.heatmap.setTransform(transform) self.heatmap.setOpacity(0.5) # finally, add the traces on top of everything self.addItem(self.traces) # pad some time on left and right to allow centering - sceneRect = padded_rectf(self.sceneRect()) + # sceneRect = padded_rectf(self.sceneRect()) + sceneRect = self.sceneRect() sceneRect.setHeight(float(self.num_chans) + 1.) self.setSceneRect(sceneRect) if isinstance(self.traces, QGraphicsItem): @@ -332,16 +339,17 @@ def loadNeural(self, ca_file, sample_rate, start_frame, stop_frame, time_start, self.annotations.setVisible(showAnnotations) def loadChannel(self, data, chan): + # at this point, the data is already clipped to [start_frame : stop_frame] pen = QPen() pen.setWidth(0) trace = QPainterPath() - trace.reserve(self.stop_frame - self.start_frame + 1) - y = float(chan+0.5) + self.normalize(data[chan][self.start_frame]) - trace.moveTo(self.time_start.float, y) + trace.reserve(data.shape[1] + 1) + y = float(chan) + self.normalize(data[chan][0]) time_start_float = self.time_start.float + trace.moveTo(time_start_float, y) - for ix in range(self.start_frame + 1, self.stop_frame): - t = (ix - self.start_frame)/self.sample_rate + time_start_float + for ix in range(1, self.stop_frame - self.start_frame): + t = (ix/self.sample_rate) + time_start_float val = self.normalize(data[chan][ix]) # Add a section to the trace path y = float(chan+0.5) + val @@ -351,7 +359,7 @@ def loadChannel(self, data, chan): self.traces.addToGroup(traceItem) def normalize(self, y_val): - return 1.0 - (y_val - self.minimum) / self.range + return ((1.0 - ((y_val - self.data_min) / self.data_range)) * self.plot_range) + self.plot_min def overlayAnnotations(self, annotationsScene, parentScene, annotations): self.annotations = QGraphicsSubSceneItem(annotationsScene, parentScene, annotations)