diff --git a/fixtracks/__init__.py b/fixtracks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fixtracks/centralwidget.py b/fixtracks/centralwidget.py new file mode 100644 index 0000000..8566b6f --- /dev/null +++ b/fixtracks/centralwidget.py @@ -0,0 +1,43 @@ +import logging +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QStackedLayout, QPushButton +from PyQt6.QtWidgets import QFileDialog +from PyQt6.QtCore import Qt, pyqtSignal + +from fixtracks.taskwidgets import MergeDetections, FixTracks, TasksWidget + + + + +class CentralWidget(QWidget): + + def __init__(self, parent=None): + super().__init__(parent) + layout = QStackedLayout() + self._tw = TasksWidget() + self._tw.clicked.connect(self._select_task) + self._tasksindex = layout.addWidget(self._tw) + self._mergewidget = MergeDetections(self) + self._trackwidget = FixTracks(self) + self._mergeindex = layout.addWidget(self._mergewidget) + self._trackindex = layout.addWidget(self._trackwidget) + + self.setLayout(layout) + + def _select_task(self, s): + logging.debug("Centralwidget: Selected task: %s", s) + if "merge" in s.lower(): + self.layout().setCurrentIndex(self._mergeindex) + self._mergewidget.fileList = self._tw.fileList + elif "tracks" in s.lower(): + self.layout().setCurrentIndex(self._trackindex) + self._trackwidget.fileList = self._tw.fileList + else: + logging.warning("Centralwidget: got invalid task request: %s", s) + + # # @Slot(None) + # def on_exit(self): + # self.exit_signal.emit() + + # # @Slot(None) + # def on_new(self): + # self._view.setScene(self._gamescene) diff --git a/fixtracks/fixtracks.py b/fixtracks/fixtracks.py new file mode 100644 index 0000000..de86d9d --- /dev/null +++ b/fixtracks/fixtracks.py @@ -0,0 +1,124 @@ +from PyQt6.QtCore import QSize, Qt +from PyQt6.QtWidgets import QMainWindow, QWidget, QToolBar, QMenu, QMenuBar, QSizePolicy, QFileDialog +from PyQt6.QtGui import QKeySequence, QAction, QIcon + +from fixtracks.centralwidget import CentralWidget + +# Subclass QMainWindow to customize your application's main window + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.create_actions() + self._toolbar = None + cw = CentralWidget() + self.setCentralWidget(cw) + # cw.exit_signal.connect(self.exit_request) + + def create_actions(self): + self._file_open_action = QAction(QIcon(":/icons/file_open"), "Open", self) + self._file_open_action.setStatusTip("Open nix file") + self._file_open_action.setShortcut(QKeySequence("Ctrl+o")) + self._file_open_action.triggered.connect(self.on_file_open) + + # self._file_close_action = QAction(QIcon(":/icons/file_close"), "Close", self) + # self._file_close_action.setStatusTip("Close current nix file") + # self._file_close_action.setShortcut(QKeySequence("Ctrl+w")) + # self._file_close_action.setEnabled(False) + # self._file_close_action.triggered.connect(self.on_file_close) + + self._quit_action = QAction(QIcon(":quit"), "Quit", self) + self._quit_action.setStatusTip("Close current file and quit") + self._quit_action.setShortcut(QKeySequence("Ctrl+q")) + self._quit_action.triggered.connect(self.exit_request) + + # self._plot_action = QAction(QIcon(":/icons/show_plot"), "Plot", self) + # self._plot_action.setStatusTip("Plot currently selected entity") + # self._plot_action.setShortcut(QKeySequence("Ctrl+p")) + # self._plot_action.setEnabled(False) + # self._plot_action.triggered.connect(self.on_item_plot) + + # self._table_action = QAction(QIcon(":/icons/show_table"), "Show table", self) + # self._table_action.setStatusTip("Show data as table") + # self._table_action.setShortcut(QKeySequence("Ctrl+t")) + # self._table_action.setEnabled(False) + # self._table_action.triggered.connect(self.on_item_show_table) + + self._about_action = QAction("about") + self._about_action.setStatusTip("Show about dialog") + self._about_action.setEnabled(True) + self._about_action.triggered.connect(self.on_about) + + self._help_action = QAction(QIcon(":help"), "help") + self._help_action.setStatusTip("Show help dialog") + self._help_action.setShortcut(QKeySequence("F1")) + + self._help_action.setEnabled(True) + self._help_action.triggered.connect(self.on_help) + + self.create_toolbar() + self.create_menu() + + def create_menu(self): + menu = self.menuBar() + file_menu = menu.addMenu("&File") + file_menu.addAction(self._file_open_action) + # file_menu.addAction(self._file_close_action) + file_menu.addSeparator() + file_menu.addAction(self._quit_action) + + plot_menu = menu.addMenu("&Plot") + # plot_menu.addAction(self._plot_action) + # plot_menu.addAction(self._table_action) + + help_menu = menu.addMenu("&Help") + help_menu.addAction(self._about_action) + help_menu.addAction(self._help_action) + self.setMenuBar(menu) + + def create_toolbar(self): + self._toolbar = QToolBar("My main toolbar") + #self._toolbar.setStyleSheet("QToolButton:!hover {background-color:none}") + self._toolbar.setAllowedAreas(Qt.ToolBarArea.LeftToolBarArea | Qt.ToolBarArea.TopToolBarArea) + self._toolbar.setFloatable(False) + self._toolbar.setIconSize(QSize(32, 32)) + + self._toolbar.addAction(self._file_open_action) + # self._toolbar.addAction(self._file_close_action) + self._toolbar.addSeparator() + # self._toolbar.addAction(self._plot_action) + # self._toolbar.addAction(self._table_action) + self._toolbar.addAction(self._help_action) + + empty = QWidget() + empty.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self._toolbar.addWidget(empty) + self._toolbar.addSeparator() + self._toolbar.addAction(self._quit_action) + + # settings = QSettings(info.ORGANIZATION, info.NAME) + # tb_orientation = settings.value("app/toolbar_area", "left") + # self.addToolBar(Qt.LeftToolBarArea if tb_orientation == "left" else Qt.TopToolBarArea, self._toolbar) + # self._toolbar.topLevelChanged.connect(self.tb_changed) + # del settings + + + def on_file_open(self, s): + QFileDialog.getExistingDirectory() + # dlg = QFileDialog(self, 'Open dataset folder data file', '', "NIX files (*.h5 *.nix)") + # dlg.setFileMode(QFileDialog.ExistingFile) + # filenames = None + # if dlg.exec_(): + # filenames = dlg.selectedFiles() + # self.open_file(filenames[0]) + + def on_about(self, s): + pass + + def on_help(self, s): + pass + + # @Slot(None) + def exit_request(self): + self.close() diff --git a/fixtracks/info.py b/fixtracks/info.py new file mode 100644 index 0000000..a37d737 --- /dev/null +++ b/fixtracks/info.py @@ -0,0 +1,5 @@ +from packaging.version import Version + +organization_name = "de.bendalab" +application_name = "FixTracks" +application_version = Version("0.0.1") diff --git a/fixtracks/taskwidgets.py b/fixtracks/taskwidgets.py new file mode 100644 index 0000000..eea9f71 --- /dev/null +++ b/fixtracks/taskwidgets.py @@ -0,0 +1,171 @@ +import logging +import pathlib +import cv2 as cv + +from PyQt6.QtWidgets import QWidget, QGridLayout, QVBoxLayout, QLabel, QPushButton, QFileDialog, QHBoxLayout, QComboBox, QSizePolicy +from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThreadPool +from PyQt6.QtGui import QImage, QPixmap + +from fixtracks.util import ImageReader + +class TasksWidget(QWidget): + clicked = pyqtSignal((str,)) + + def __init__(self, parent = None): + super().__init__(parent) + l = QVBoxLayout() + l.addWidget(QLabel("Tasks:")) + folderBtn = QPushButton("Select data folder") + folderBtn.setEnabled(True) + folderBtn.clicked.connect(self._open_folder) + l.addWidget(folderBtn) + self.mergeBtn = QPushButton("Merge detections") + self.mergeBtn.setEnabled(False) + self.mergeBtn.clicked.connect(self._merge_clicked) + self.tracksBtn = QPushButton("Join tracks") + self.tracksBtn.setEnabled(False) + self.tracksBtn.clicked.connect(self._tracks_clicked) + l.addWidget(self.mergeBtn) + l.addWidget(self.tracksBtn) + self.setLayout(l) + self._file_list = [] + + def _merge_clicked(self): + self.clicked.emit("Merge") + + def _tracks_clicked(self): + self.clicked.emit("Tracks") + + def _open_folder(self): + logging.debug("TasksWidget:select data folder") + folder = QFileDialog.getExistingDirectory() + if len(folder.strip()) == 0: + logging.debug("TasksWidget: is EMPTY") + return + p = pathlib.Path(folder) + logging.debug("TasksWidget: selected path is %s", p) + for d in p.iterdir(): + if d.is_file(): + self._file_list.append(d) + if len(self._file_list) > 0: + self.mergeBtn.setEnabled(True) + self.tracksBtn.setEnabled(True) + + @property + def fileList(self): + return self._file_list + + +class MergeDetections(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._files = [] + self.threadpool = QThreadPool() + self.right_imagereader = None + self.left_imagereader = None + + layout = QVBoxLayout() + layout.addWidget(QLabel("Merge Detections!")) + hbox = QHBoxLayout() + + leftvbox = QVBoxLayout() + self.leftdatacombo = QComboBox() + self.leftvideocombo = QComboBox() + self.leftvideocombo.addItems(self._files) + self.left_preview = QLabel() + self.left_preview.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + leftvbox.addWidget(self.left_preview) + leftvbox.addWidget(QLabel("Left data file:")) + leftvbox.addWidget(self.leftdatacombo) + leftvbox.addWidget(QLabel("Left video:")) + leftvbox.addWidget(self.leftvideocombo) + + rightvbox = QVBoxLayout() + self.rightdatacombo = QComboBox() + self.rightvideocombo = QComboBox() + self.rightvideocombo.addItems(self._files) + self.right_preview = QLabel() + self.right_preview.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + rightvbox.addWidget(QLabel("Right data file:")) + rightvbox.addWidget(self.rightdatacombo) + rightvbox.addWidget(QLabel("Right video:")) + rightvbox.addWidget(self.rightvideocombo) + rightvbox.addWidget(self.right_preview) + + hbox.addLayout(leftvbox) + hbox.addLayout(rightvbox) + layout.addLayout(hbox) + self.setLayout(layout) + + @property + def fileList(self): + return self._files + + @fileList.setter + def fileList(self, file_list): + logging.debug("MergeDetections.fileList: set new file list") + + logging.debug("MergeDetections.fileList: setting video combo boxes") + videoformats = [".mp4", ".avi"] + self._files = [str(f) for f in file_list if f.suffix in videoformats] + self.rightvideocombo.addItem("Please select") + self.leftvideocombo.addItem("Please select") + self.rightvideocombo.addItems(self.fileList) + self.leftvideocombo.addItems(self.fileList) + self.leftvideocombo.setCurrentIndex(0) + self.rightvideocombo.setCurrentIndex(0) + + logging.debug("MergeDetections.fileList: setting data combo boxes") + dataformats = [".csv"] + self._files = [str(f) for f in file_list if f.suffix in dataformats] + self.rightdatacombo.addItem("Please select") + self.leftdatacombo.addItem("Please select") + self.rightdatacombo.addItems(self.fileList) + self.leftdatacombo.addItems(self.fileList) + self.leftdatacombo.setCurrentIndex(0) + self.rightdatacombo.setCurrentIndex(0) + self.rightvideocombo.currentIndexChanged.connect(self.on_rightvideoSelection) + self.leftvideocombo.currentIndexChanged.connect(self.on_leftvideoSelection) + # self._files = file_list + + def on_rightvideoSelection(self): + logging.debug("Video selection of the %s side", "right") + self.right_imagereader = ImageReader(self.rightvideocombo.currentText(), 100) + self.right_imagereader.signals.finished.connect(self.right_imgreaderDone) + self.threadpool.start(self.right_imagereader) + + def right_imgreaderDone(self, state): + logging.debug("Right image reader done with state %s", str(state)) + frame = self.right_imagereader.frame + height, width, _ = frame.shape + bytesPerLine = 3 * width + img = QImage(frame.data, width, height, bytesPerLine, QImage.Format.Format_BGR888).rgbSwapped() + self.right_preview.setPixmap(QPixmap.fromImage(img).scaledToHeight(self.right_preview.height())) + + def on_leftvideoSelection(self): + logging.debug("Video selection of the %s side", "left") + self.left_imagereader = ImageReader(self.leftvideocombo.currentText(), 100) + self.left_imagereader.signals.finished.connect(self.left_imgreaderDone) + self.threadpool.start(self.left_imagereader) + + def left_imgreaderDone(self, state): + logging.debug("Left image reader done with state %s", str(state)) + frame = self.left_imagereader.frame + height, width, _ = frame.shape + bytesPerLine = 3 * width + img = QImage(frame.data, width, height, bytesPerLine, QImage.Format.Format_BGR888).rgbSwapped() + self.left_preview.setPixmap(QPixmap.fromImage(img).scaledToHeight(self.left_preview.height())) + + +class FixTracks(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._files = None + layout = QVBoxLayout() + layout.addWidget(QLabel("Fix Tracks!")) + self.setLayout(layout) + + @property + def fileList(self, file_list): + self._files = file_list \ No newline at end of file diff --git a/fixtracks/util.py b/fixtracks/util.py new file mode 100644 index 0000000..a52f7ff --- /dev/null +++ b/fixtracks/util.py @@ -0,0 +1,51 @@ +import logging +import cv2 as cv + +from PyQt6.QtCore import QRunnable, pyqtSlot, pyqtSignal, QObject + + +class ProducerSignals(QObject): + finished = pyqtSignal(bool) + # error = pyqtSignal(str) + # start = pyqtSignal(float) + # running = pyqtSignal() + + +class ImageReader(QRunnable): + finished = pyqtSignal(bool) + + def __init__(self, filename, frame=1000) -> None: + super().__init__() + self._filename = filename + self._framenumber = frame + self._signals = ProducerSignals() + self._frame = None + + @pyqtSlot() + def run(self): + ''' + Your code goes in this function + ''' + logging.debug("ImageReader: trying to open file %s", self._filename) + cap = cv.VideoCapture(self._filename) + if not cap.isOpened(): + logging.debug("ImageReader: failed to open file %s", self._filename) + self._signals.finished.emit(False) + fn = 0 + while cap.isOpened() and fn < self._framenumber: + ret, frame = cap.read() + if not ret: + logging.warning("ImageReader: failed to read frame %i", fn) + self._signals.finished.emit(False) + break + fn += 1 + self._frame = frame # cv.cvtColor(frame, cv.COLOR_BGR2RGB) + self._signals.finished.emit(True) + + @property + def signals(self): + return self._signals + + @property + def frame(self): + return self._frame