diff --git a/annolid/gui/app.py b/annolid/gui/app.py index cac3119..d95579b 100644 --- a/annolid/gui/app.py +++ b/annolid/gui/app.py @@ -1,75 +1,75 @@ +from annolid.gui.widgets.video_manager import VideoManagerWidget +from annolid.gui.workers import FlexibleWorker, LoadFrameThread +from annolid.gui.shape import Shape +from labelme.app import MainWindow +from labelme.utils import newAction +from labelme.widgets import BrightnessContrastDialog +from labelme.widgets import LabelListWidgetItem +from labelme import utils +from annolid.utils.logger import logger +from annolid.utils.files import count_json_files +from labelme.widgets import ToolBar +from annolid.gui.label_file import LabelFileError +from annolid.gui.label_file import LabelFile +from annolid.configs import get_config +from annolid.gui.widgets.canvas import Canvas +from annolid.gui.widgets.text_prompt import AiRectangleWidget +from annolid.annotation import labelme2coco +from annolid.data import videos +from annolid.gui.widgets import ExtractFrameDialog +from annolid.gui.widgets import ConvertCOODialog +from annolid.gui.widgets import TrainModelDialog +from annolid.gui.widgets import Glitter2Dialog +from annolid.gui.widgets import QualityControlDialog +from annolid.gui.widgets import TrackDialog +from annolid.gui.widgets import SystemInfoDialog +from annolid.postprocessing.glitter import tracks2nix +from annolid.postprocessing.quality_control import TracksResults +from annolid.gui.widgets import ProgressingWindow +import webbrowser +import atexit +from annolid.gui.widgets.video_slider import VideoSlider, VideoSliderMark +from annolid.gui.widgets.step_size_widget import StepSizeWidget +from annolid.gui.widgets.downsample_videos_dialog import VideoRescaleWidget +from annolid.gui.widgets.convert_sleap_dialog import ConvertSleapDialog +from annolid.gui.widgets.extract_keypoints_dialog import ExtractShapeKeyPointsDialog +from annolid.gui.widgets.convert_labelme2csv_dialog import LabelmeJsonToCsvDialog +from annolid.postprocessing.quality_control import pred_dict_to_labelme +from annolid.annotation.timestamps import convert_frame_number_to_time +from annolid.segmentation.SAM.edge_sam_bg import VideoProcessor +from annolid.annotation import labelme2csv +from annolid.gui.widgets.advanced_parameters_dialog import AdvancedParametersDialog +from annolid.gui.widgets.place_preference_dialog import TrackingAnalyzerDialog +from annolid.data.videos import get_video_files +from annolid.gui.widgets.caption import CaptionWidget +from labelme.ai import MODELS +from qtpy import QtCore +from qtpy.QtCore import Qt +from qtpy import QtWidgets +from qtpy import QtGui +from labelme import PY2 +from labelme import QT5 +from PIL import ImageQt +import pandas as pd +import numpy as np +import torch +import codecs +import imgviz +import argparse +from pathlib import Path +import functools +import requests +import subprocess +import re +import csv +import os.path as osp +import time +import html +import shutil import sys import os # Enable CPU fallback for unsupported MPS ops os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1" -import shutil -import html -import time -import os.path as osp -import csv -import re -import subprocess -import requests -import functools -from pathlib import Path -import argparse -import imgviz -import codecs -import torch -import numpy as np -import pandas as pd -from PIL import ImageQt -from labelme import QT5 -from labelme import PY2 -from qtpy import QtGui -from qtpy import QtWidgets -from qtpy.QtCore import Qt -from qtpy import QtCore -from labelme.ai import MODELS -from annolid.gui.widgets.caption import CaptionWidget -from annolid.data.videos import get_video_files -from annolid.gui.widgets.place_preference_dialog import TrackingAnalyzerDialog -from annolid.gui.widgets.advanced_parameters_dialog import AdvancedParametersDialog -from annolid.annotation import labelme2csv -from annolid.segmentation.SAM.edge_sam_bg import VideoProcessor -from annolid.annotation.timestamps import convert_frame_number_to_time -from annolid.postprocessing.quality_control import pred_dict_to_labelme -from annolid.gui.widgets.convert_labelme2csv_dialog import LabelmeJsonToCsvDialog -from annolid.gui.widgets.extract_keypoints_dialog import ExtractShapeKeyPointsDialog -from annolid.gui.widgets.convert_sleap_dialog import ConvertSleapDialog -from annolid.gui.widgets.downsample_videos_dialog import VideoRescaleWidget -from annolid.gui.widgets.step_size_widget import StepSizeWidget -from annolid.gui.widgets.video_slider import VideoSlider, VideoSliderMark -import atexit -import webbrowser -from annolid.gui.widgets import ProgressingWindow -from annolid.postprocessing.quality_control import TracksResults -from annolid.postprocessing.glitter import tracks2nix -from annolid.gui.widgets import SystemInfoDialog -from annolid.gui.widgets import TrackDialog -from annolid.gui.widgets import QualityControlDialog -from annolid.gui.widgets import Glitter2Dialog -from annolid.gui.widgets import TrainModelDialog -from annolid.gui.widgets import ConvertCOODialog -from annolid.gui.widgets import ExtractFrameDialog -from annolid.data import videos -from annolid.annotation import labelme2coco -from annolid.gui.widgets.text_prompt import AiRectangleWidget -from annolid.gui.widgets.canvas import Canvas -from annolid.configs import get_config -from annolid.gui.label_file import LabelFile -from annolid.gui.label_file import LabelFileError -from labelme.widgets import ToolBar -from annolid.utils.files import count_json_files -from annolid.utils.logger import logger -from labelme import utils -from labelme.widgets import LabelListWidgetItem -from labelme.widgets import BrightnessContrastDialog -from labelme.utils import newAction -from labelme.app import MainWindow -from annolid.gui.shape import Shape -from annolid.gui.workers import FlexibleWorker, LoadFrameThread - __appname__ = 'Annolid' __version__ = "1.2.1" @@ -129,6 +129,21 @@ def __init__(self, self.label_dock.setVisible(True) self.shape_dock.setVisible(True) self.file_dock.setVisible(True) + + # Create the Video Manager Widget + self.video_manager_widget = VideoManagerWidget() + self.video_manager_widget.video_selected.connect(self._load_video) + + # Create the Dock Widget + self.video_dock = QtWidgets.QDockWidget("Video List", self) + self.video_dock.setWidget(self.video_manager_widget) + self.video_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable | + QtWidgets.QDockWidget.DockWidgetClosable | + QtWidgets.QDockWidget.DockWidgetFloatable) + + # Add the Dock Widget to the Main Window + self.addDockWidget(Qt.RightDockWidgetArea, self.video_dock) + self.here = Path(__file__).resolve().parent action = functools.partial(newAction, self) self._df = None @@ -2075,7 +2090,16 @@ def _load_labels(self, labels_csv_file): self._df = pd.read_csv(labels_csv_file) self._df.rename(columns={'Unnamed: 0': 'frame_number'}, inplace=True) - def openVideo(self, _value=False): + def _load_video(self, video_path): + """Open a video for annotation frame by frame.""" + if not video_path: + return + self.openVideo(from_video_list=True, video_path=video_path) + + def openVideo(self, _value=False, + from_video_list=False, + video_path=None, + ): """open a video for annotaiton frame by frame Args: @@ -2097,17 +2121,20 @@ def openVideo(self, _value=False): elif choice == QtWidgets.QMessageBox.Cancel: return # Cancel operation - video_path = Path(self.filename).parent if self.filename else "." - formats = ["*.*"] - filters = self.tr(f"Video files {formats[0]}") - video_filename = QtWidgets.QFileDialog.getOpenFileName( - self, - self.tr(f"{__appname__} - Choose Video"), - str(video_path), - filters, - ) - if QT5: - video_filename, _ = video_filename + if not from_video_list: + video_path = Path(self.filename).parent if self.filename else "." + formats = ["*.*"] + filters = self.tr(f"Video files {formats[0]}") + video_filename = QtWidgets.QFileDialog.getOpenFileName( + self, + self.tr(f"{__appname__} - Choose Video"), + str(video_path), + filters, + ) + if QT5: + video_filename, _ = video_filename + else: + video_filename = video_path video_filename = str(video_filename) self.stepSizeWidget.setEnabled(True) diff --git a/annolid/gui/widgets/video_manager.py b/annolid/gui/widgets/video_manager.py new file mode 100644 index 0000000..820bd6e --- /dev/null +++ b/annolid/gui/widgets/video_manager.py @@ -0,0 +1,92 @@ +import os +from qtpy.QtCore import Signal +from qtpy.QtWidgets import ( + QWidget, QVBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, + QFileDialog, QMessageBox, QAbstractItemView +) + + +class VideoManagerWidget(QWidget): + video_selected = Signal(str) # Signal to send the selected video path + + def __init__(self, parent=None): + super().__init__(parent) + + # Layouts + self.layout = QVBoxLayout(self) + + # Set to track imported videos + self.imported_videos = set() + + # Import Button + self.import_button = QPushButton("Import Videos") + self.import_button.clicked.connect(self.import_videos) + self.layout.addWidget(self.import_button) + + # Video Table + # Rows: 0, Columns: 4 (Name, Path, Load, Delete) + self.video_table = QTableWidget(0, 4) + self.video_table.setHorizontalHeaderLabels( + ["Name", "Path", "Load", "Delete"]) + self.video_table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.layout.addWidget(self.video_table) + + def import_videos(self): + # Open folder dialog + folder_path = QFileDialog.getExistingDirectory( + self, "Select Video Folder") + if not folder_path: + return + + # Get video files (recursively) + video_extensions = {'.mp4', '.avi', '.mov', '.mkv', + '.mpg'} # Add more extensions if needed + video_files = [] + for root, _, files in os.walk(folder_path): + for file in files: + if os.path.splitext(file)[1].lower() in video_extensions: + video_files.append(os.path.join(root, file)) + + # Add videos to the table + for video in video_files: + # Check if video is already added + if video not in self.imported_videos: + self.add_video_to_table(video) + + def add_video_to_table(self, video_path): + # Add the video to the imported videos set + self.imported_videos.add(video_path) + # Get video name + video_name = os.path.basename(video_path) + + # Create a new row + row_position = self.video_table.rowCount() + self.video_table.insertRow(row_position) + + # Add Name and Path + self.video_table.setItem(row_position, 0, QTableWidgetItem(video_name)) + self.video_table.setItem(row_position, 1, QTableWidgetItem(video_path)) + + # Add Load Button + load_button = QPushButton("Load") + load_button.clicked.connect( + lambda: self.video_selected.emit(video_path)) + self.video_table.setCellWidget(row_position, 2, load_button) + + # Add Delete Button + delete_button = QPushButton("Delete") + delete_button.clicked.connect( + lambda: self.delete_video(row_position, video_path)) + self.video_table.setCellWidget(row_position, 3, delete_button) + + def delete_video(self, row, video_path): + # Confirm deletion + confirmation = QMessageBox.question( + self, "Delete Video", "Are you sure you want to delete this video from the list?", + QMessageBox.Yes | QMessageBox.No + ) + if confirmation == QMessageBox.Yes: + # Remove from the imported videos set + self.imported_videos.discard(video_path) + + self.video_table.removeRow(row)