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)