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()