345 lines
12 KiB
Python
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() |