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 1cde407..c3c53b3 100644 --- a/src/mainWindow.ui +++ b/src/mainWindow.ui @@ -6,12 +6,12 @@ 0 0 - 460 + 604 328 - + 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..2bd712f 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, 328) + 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)) 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 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)