fixtracks/fixtracks/widgets/detectiontimeline.py

345 lines
12 KiB
Python

import logging
import numpy as np
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QSizePolicy, QLabel
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsRectItem, QGraphicsLineItem, QGraphicsEllipseItem
from PySide6.QtCore import Qt, QRectF, QRectF
from PySide6.QtGui import QBrush, QColor, QPen, QFont
from fixtracks.utils.signals import DetectionTimelineSignals
from fixtracks.utils.trackingdata import TrackingData
class Window(QGraphicsRectItem):
signals = DetectionTimelineSignals()
def __init__(self, x, y, width, height, pen, brush):
super().__init__(x, y, width, height)
self._width = width
self.setPen(pen)
self.setBrush(brush)
self.setZValue(1.0)
self.setAcceptHoverEvents(True) # Enable hover events if needed
self.setFlags(
QGraphicsItem.ItemIsMovable | # Enables item dragging
QGraphicsItem.ItemIsSelectable # Enables item selection
)
self._y = y
def setWindowX(self, newx):
logging.debug("timeline.window: set position to %.3f", newx)
self.setX(newx)
# self.signals.windowMoved.emit()
def setWindowWidth(self, newwidth):
logging.debug("timeline.window: update window width to %f", newwidth)
self._width = newwidth
r = self.rect()
r.setWidth(newwidth)
self.setRect(r)
# self.signals.windowMoved.emit()
def setWindow(self, newx:float, newwidth:float):
"""
Update the window to the specified range.
Parameters
----------
newx : float
The new x-coordinate of the window.
newwidth : float
The new width of the window.
Returns
-------
None
"""
logging.debug("timeline.window: update window to range %.5f to %.5f", newx, newwidth)
self._width = newwidth
r = self.rect()
self.setRect(newx, r.y(), self._width, r.height())
self.update()
# self.signals.windowMoved.emit()
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
def mousePressEvent(self, event):
self.setCursor(Qt.ClosedHandCursor)
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
logging.debug("Timeline.Window:MouseRelease event!")
r = self.sceneBoundingRect()
if r.left() < 0:
self.setX(0.)
if r.right() > self.scene().width():
self.setX(self.scene().width() - self._width)
if r.y() != self._y:
self.setY(self._y)
super().mouseReleaseEvent(event)
self.signals.manualMove.emit()
def hoverEnterEvent(self, event):
super().hoverEnterEvent(event)
class DetectionTimeline(QWidget):
signals = DetectionTimelineSignals()
def __init__(self, trackone_id=1, tracktwo_id=2, parent=None):
super().__init__(parent)
self._trackone = trackone_id
self._tracktwo = tracktwo_id
self._data = None
self._rangeStart = 0.0
self._rangeStop = 0.005
self._total_width = 2000
self._stepCount = 1000
self._bg_brush = QBrush(QColor(20, 20, 20, 255))
transparent_brush = QBrush(QColor(200, 200, 200, 64))
self._white_pen = QPen(QColor.fromString("white"))
self._white_pen.setWidth(0.1)
self._t1_pen = QPen(QColor.fromString("orange"))
self._t1_pen.setWidth(1)
self._t2_pen = QPen(QColor(0, 255, 0, 255))
self._t2_pen.setWidth(1)
self._other_pen = QPen(QColor.fromString("red"))
self._other_pen.setWidth(1)
window_pen = QPen(QColor.fromString("white"))
window_pen.setWidth(2)
self._user_brush = QBrush(QColor.fromString("white"))
user_pen = QPen(QColor.fromString("white"))
user_pen.setWidth(2)
font = QFont()
font.setPointSize(15)
font.setBold(True)
self._window = Window(0, 0, 100, 60, window_pen, transparent_brush)
self._window.signals.manualMove.connect(self.on_windowMoved)
self._scene = QGraphicsScene(QRectF(0, 0, self._total_width, 85.))
self._scene.setBackgroundBrush(self._bg_brush)
self._scene.addItem(self._window)
self._view = QGraphicsView()
# self._view.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
self._view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self._view.setScene(self._scene)
self._view.fitInView(self._scene.sceneRect(), aspectRadioMode=Qt.AspectRatioMode.KeepAspectRatio)
self.fit_scene_to_view()
t1_label = self._scene.addText("track 1", font)
t1_label.setDefaultTextColor(self._t1_pen.color())
t1_label.setPos(0, 50)
t2_label = self._scene.addText("track 2", font)
t2_label.setFont(font)
t2_label.setDefaultTextColor(self._t2_pen.color())
t2_label.setPos(100, 50)
other_label = self._scene.addText("unassigned", font)
other_label.setFont(font)
other_label.setDefaultTextColor(self._other_pen.color())
other_label.setPos(200, 50)
user_label = self._scene.addText("user-labeled", font)
user_label.setFont(font)
user_label.setDefaultTextColor(user_pen.color())
user_label.setPos(350, 50)
self._position_label = QLabel("")
f = self._position_label.font()
f.setPointSize(9)
self._position_label.setFont(f)
layout = QVBoxLayout()
layout.addWidget(self._view)
layout.addWidget(self._position_label, Qt.AlignmentFlag.AlignRight)
self.setLayout(layout)
# self.setMaximumHeight(100)
# self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
def clear(self):
for i in self._scene.items():
if isinstance(i, (QGraphicsLineItem, QGraphicsEllipseItem)):
self._scene.removeItem(i)
def setData(self, data:TrackingData):
logging.debug("Timeline: setData!")
self._data = data
self.update()
def update(self):
self.clear()
self.draw_coverage()
def draw_coverage(self):
logging.debug("Timeline: drawCoverage!")
if isinstance(self._data, TrackingData):
maxframe = np.max(self._data["frame"])
bins = np.linspace(0, maxframe, self._stepCount)
pos = np.linspace(0, self._scene.width(), self._stepCount) # of the vertical dashes is this correct?
track1_frames = self._data["frame"][self._data["track"] == self._trackone]
track2_frames = self._data["frame"][self._data["track"] == self._tracktwo]
other_frames = self._data["frame"][(self._data["track"] != self._trackone) &
(self._data["track"] != self._tracktwo)]
userlabeled = self._data["frame"][self._data["userlabeled"]]
else:
print("Data is not trackingdata")
return
t1_coverage, _ = np.histogram(track1_frames, bins=bins)
t2_coverage, _ = np.histogram(track2_frames, bins=bins)
other_coverage, _ = np.histogram(other_frames, bins=bins)
labeled_coverage, _ = np.histogram(userlabeled, bins=bins)
for i in range(len(bins)-1):
if t1_coverage[i]: self._scene.addLine(pos[i], 0, pos[i], 15., pen=self._t1_pen)
if t2_coverage[i]: self._scene.addLine(pos[i], 17, pos[i], 32., pen=self._t2_pen)
if other_coverage[i]: self._scene.addLine(pos[i], 34, pos[i], 49., pen=self._other_pen)
if other_coverage[i]: self._scene.addLine(pos[i], 34, pos[i], 49., pen=self._other_pen)
if labeled_coverage[i]: self._scene.addEllipse(pos[i]-2, 52, 4, 4, brush=self._user_brush)
def updatePositionLabel(self):
start = np.round(self._rangeStart * 100, 4)
stop = np.round(self._rangeStop * 100, 4)
self._position_label.setText(f"Current position: {start}% to {stop}% of data.")
@property
def rangeStart(self):
return self._rangeStart
@property
def rangeStop(self):
return self._rangeStop
def fit_scene_to_view(self):
"""Scale the image to fit the QGraphicsView."""
logging.debug("Timeline: fit scene to view")
self._view.fitInView(self._scene.sceneRect(), Qt.KeepAspectRatio)
def resizeEvent(self, event):
"""Handle window resizing to fit the image."""
super().resizeEvent(event)
self.fit_scene_to_view()
def on_windowMoved(self):
scene_width = self._scene.width()
self._rangeStart = self._window.sceneBoundingRect().left() / scene_width
self._rangeStop = self._window.sceneBoundingRect().right() / scene_width
logging.debug("Timeline: WindowUpdated positions start: %.3f end: %.3f", self.rangeStart, self.rangeStop)
self.updatePositionLabel()
self.signals.windowMoved.emit()
def setWindowPos(self, newx: float):
"""Set the x-position of the selection window.
Parameters
----------
newx : float
The new x position of the selection window given in percent of the data.
"""
if newx < 0.0:
newx = 0.0
elif newx > 1.0:
newx = 1.0
logging.debug("Timeline:setWindow to new position %.4f", newx)
x_rel = np.round(newx * self._total_width)
self._window.setWindowX(x_rel)
def setWindowWidth(self, width: float):
"""Set the width of the selection window.
Parameters
----------
width : float
The width in a range 0.0 to 1.0 (aka 0% to 100% of the span.)
"""
logging.debug("Set window width to new value %.5f of %i total width", width, self._total_width)
span = np.round(width * self._total_width)
self._window.setWindowWidth(np.round(span))
def setWindow(self, xpos:float, width:float):
"""
Set the window position and width.
Parameters
----------
xpos : float
The x position of the window as a fraction of the total data.
Must be between 0.0 and 1.0. Values outside this range will be clamped.
width : float
The width of the window as a fraction of the total data.
Returns
-------
None
"""
if xpos < 0.0:
xpos = 0.0
elif xpos > 1.0:
xpos = 1.0
xstart = xpos * self._total_width
span = width * self._total_width
self._window.setWindow(xstart, span)
def windowBounds(self):
return self._rangeStart, self._rangeStop
def main():
def back(start_x):
view.setWindowPos(start_x)
print(view.windowBounds())
def fwd(start_x):
view.setWindowPos(start_x)
print(view.windowBounds())
def zero():
view.setWindowPos(0.0)
print(view.windowBounds())
def as_dict(df):
d = {c: df[c].values for c in df.columns}
d["index"] = df.index.values
return d
import pickle
import numpy as np
from PySide6.QtWidgets import QApplication, QPushButton, QHBoxLayout
from fixtracks.info import PACKAGE_ROOT
logging.basicConfig(level=logging.DEBUG, force=True)
datafile = PACKAGE_ROOT / "data/merged_small.pkl"
with open(datafile, "rb") as f:
df = pickle.load(f)
data = TrackingData()
data.setData(as_dict(df))
data.setSelection(np.arange(0,100, 1))
data.setUserLabeledStatus(True)
start_x = 0.1
app = QApplication([])
window = QWidget()
window.setMinimumSize(200, 75)
view = DetectionTimeline()
view.setData(data)
fwdBtn = QPushButton(">>")
fwdBtn.clicked.connect(lambda: fwd(0.5))
zeroBtn = QPushButton("0->|")
zeroBtn.clicked.connect(zero)
backBtn = QPushButton("<<")
backBtn.clicked.connect(lambda: back(0.2))
btnLyt = QHBoxLayout()
btnLyt.addWidget(backBtn)
btnLyt.addWidget(zeroBtn)
btnLyt.addWidget(fwdBtn)
view.setWindowPos(start_x)
layout = QVBoxLayout()
layout.addWidget(view)
layout.addLayout(btnLyt)
window.setLayout(layout)
window.show()
app.exec()
if __name__ == "__main__":
main()