From 2ebaf16006b3b35c37d3ffdcc57eee95a580e97f Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 26 May 2025 15:59:10 +0200 Subject: [PATCH 1/6] working really nicely --- .gitignore | 1 + mucapy/main.py | 330 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 298 insertions(+), 33 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0698ebe --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +mucapy_config.json diff --git a/mucapy/main.py b/mucapy/main.py index 943bf37..b943622 100644 --- a/mucapy/main.py +++ b/mucapy/main.py @@ -1,14 +1,50 @@ import os import sys import cv2 +import json import numpy as np from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QPushButton, QComboBox, QSpinBox, QFileDialog, QMessageBox, QMenu, QAction, QMenuBar, QActionGroup, QSizePolicy, QGridLayout, QGroupBox, - QDockWidget, QScrollArea, QToolButton, QDialog) -from PyQt5.QtCore import Qt, QTimer, QDir, QSize -from PyQt5.QtGui import QImage, QPixmap, QIcon, QColor + QDockWidget, QScrollArea, QToolButton, QDialog, + QShortcut, QListWidget, QFormLayout, QLineEdit) +from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime +from PyQt5.QtGui import QImage, QPixmap, QIcon, QColor, QKeySequence + +class Config: + def __init__(self): + # Use JSON file in current working directory instead of QSettings + self.config_file = os.path.join(os.getcwd(), 'mucapy_config.json') + self.settings = {} + self.load_config() + + def load_config(self): + """Load configuration from JSON file""" + try: + if os.path.exists(self.config_file): + with open(self.config_file, 'r') as f: + self.settings = json.load(f) + except Exception as e: + print(f"Error loading config: {e}") + self.settings = {} + + def save_config(self): + """Save configuration to JSON file""" + try: + with open(self.config_file, 'w') as f: + json.dump(self.settings, f, indent=4) + except Exception as e: + print(f"Error saving config: {e}") + + def save_setting(self, key, value): + """Save a setting to configuration""" + self.settings[key] = value + self.save_config() + + def load_setting(self, key, default=None): + """Load a setting from configuration""" + return self.settings.get(key, default) class MultiCamYOLODetector: def __init__(self): @@ -22,6 +58,11 @@ class MultiCamYOLODetector: self.available_cameras = [] self.model_dir = "" self.cuda_available = self.check_cuda() + self.confidence_threshold = 0.35 + self.config = Config() + + # Load saved network cameras + self.network_cameras = self.config.load_setting('network_cameras', {}) def check_cuda(self): """Check if CUDA is available""" @@ -31,8 +72,19 @@ class MultiCamYOLODetector: except: return False + def add_network_camera(self, name, url): + """Add a network camera to the saved list""" + self.network_cameras[name] = url + self.config.save_setting('network_cameras', self.network_cameras) + + def remove_network_camera(self, name): + """Remove a network camera from the saved list""" + if name in self.network_cameras: + del self.network_cameras[name] + self.config.save_setting('network_cameras', self.network_cameras) + def scan_for_cameras(self, max_to_check=10): - """Check for available cameras on Linux with better error handling""" + """Check for available cameras including network cameras""" self.available_cameras = [] # Check standard video devices @@ -45,7 +97,7 @@ class MultiCamYOLODetector: except: continue - # Also check direct device paths + # Check direct device paths v4l_paths = [f"/dev/video{i}" for i in range(max_to_check)] v4l_paths += [f"/dev/v4l/video{i}" for i in range(max_to_check)] @@ -60,6 +112,10 @@ class MultiCamYOLODetector: except: continue + # Add saved network cameras + for name, url in self.network_cameras.items(): + self.available_cameras.append(f"net:{url}") + return self.available_cameras def load_yolo_model(self, model_dir): @@ -105,13 +161,22 @@ class MultiCamYOLODetector: return False def connect_cameras(self, camera_paths): - """Connect to multiple cameras with better error handling""" + """Connect to multiple cameras including network cameras""" self.disconnect_cameras() for cam_path in camera_paths: try: - if isinstance(cam_path, str) and cam_path.startswith('/dev/'): - cap = cv2.VideoCapture(cam_path, cv2.CAP_V4L2) + if isinstance(cam_path, str): + if cam_path.startswith('net:'): + # Handle network camera + url = cam_path[4:] # Remove 'net:' prefix + cap = cv2.VideoCapture(url) + elif cam_path.startswith('/dev/'): + # Handle device path + cap = cv2.VideoCapture(cam_path, cv2.CAP_V4L2) + else: + # Handle numeric index + cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2) else: cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2) @@ -124,7 +189,6 @@ class MultiCamYOLODetector: cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) cap.set(cv2.CAP_PROP_FPS, self.target_fps) - cap.set(cv2.CAP_PROP_AUTOFOCUS, 0) cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) except: pass @@ -172,7 +236,7 @@ class MultiCamYOLODetector: class_id = np.argmax(scores) confidence = scores[class_id] - if confidence > 0.5: + if confidence > self.confidence_threshold: # Use configurable threshold box = detection[0:4] * np.array([frame.shape[1], frame.shape[0], frame.shape[1], frame.shape[0]]) (centerX, centerY, width, height) = box.astype('int') @@ -183,7 +247,7 @@ class MultiCamYOLODetector: confidences.append(float(confidence)) class_ids.append(class_id) - indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4) + indices = cv2.dnn.NMSBoxes(boxes, confidences, self.confidence_threshold, 0.4) if len(indices) > 0: for i in indices.flatten(): @@ -233,11 +297,49 @@ class CameraDisplay(QLabel): self.fullscreen_window = None self.cam_id = None self.fullscreen_timer = None + self.config = Config() + self.screenshot_dir = self.config.load_setting('screenshot_dir', os.path.expanduser('~/Pictures/MuCaPy')) + + # Create screenshot directory if it doesn't exist + if not os.path.exists(self.screenshot_dir): + os.makedirs(self.screenshot_dir, exist_ok=True) def set_cam_id(self, cam_id): """Set camera identifier for this display""" self.cam_id = cam_id + def take_screenshot(self): + """Take a screenshot of the current frame""" + if not self.pixmap(): + return + + # Ask for screenshot directory if not set + if not self.screenshot_dir: + dir_path = QFileDialog.getExistingDirectory( + self, + "Select Screenshot Directory", + os.path.expanduser('~/Pictures'), + QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks + ) + if dir_path: + self.screenshot_dir = dir_path + self.config.save_setting('screenshot_dir', dir_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path, exist_ok=True) + else: + return + + # Generate filename with timestamp + timestamp = QDateTime.currentDateTime().toString('yyyy-MM-dd_hh-mm-ss') + filename = f"camera_{self.cam_id}_{timestamp}.png" + filepath = os.path.join(self.screenshot_dir, filename) + + # Save the image + if self.pixmap().save(filepath): + QMessageBox.information(self, "Success", f"Screenshot saved to:\n{filepath}") + else: + QMessageBox.critical(self, "Error", "Failed to save screenshot") + def mouseDoubleClickEvent(self, event): """Handle double click to toggle fullscreen""" if self.pixmap() and not self.fullscreen_window: @@ -247,23 +349,35 @@ class CameraDisplay(QLabel): def show_fullscreen(self): """Show this camera in fullscreen mode""" - self.fullscreen_window = QMainWindow() + if not self.pixmap(): + return + + self.fullscreen_window = QMainWindow(self.window()) self.fullscreen_window.setWindowTitle(f"Camera {self.cam_id} - Fullscreen") - screen = QApplication.primaryScreen().availableGeometry() - self.fullscreen_window.resize(int(screen.width() * 0.9), int(screen.height() * 0.9)) + # Create central widget + central_widget = QWidget() + layout = QVBoxLayout(central_widget) + # Create fullscreen label label = QLabel() label.setAlignment(Qt.AlignCenter) - if self.pixmap(): - label.setPixmap(self.pixmap().scaled( - self.fullscreen_window.size(), - Qt.KeepAspectRatio, - Qt.SmoothTransformation - )) + label.setPixmap(self.pixmap().scaled( + QApplication.primaryScreen().size(), + Qt.KeepAspectRatio, + Qt.SmoothTransformation + )) + layout.addWidget(label) - self.fullscreen_window.setCentralWidget(label) - self.fullscreen_window.showFullScreen() + self.fullscreen_window.setCentralWidget(central_widget) + + # Add ESC shortcut + shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self.fullscreen_window) + shortcut.activated.connect(self.close_fullscreen) + + # Add screenshot shortcut (Ctrl+S) + screenshot_shortcut = QShortcut(QKeySequence("Ctrl+S"), self.fullscreen_window) + screenshot_shortcut.activated.connect(self.take_screenshot) # Update fullscreen image when main window updates self.fullscreen_timer = QTimer() @@ -271,6 +385,9 @@ class CameraDisplay(QLabel): lambda: self.update_fullscreen(label) ) self.fullscreen_timer.start(30) + + # Show fullscreen + self.fullscreen_window.showFullScreen() def update_fullscreen(self, label): """Update the fullscreen display""" @@ -416,12 +533,99 @@ class AboutWindow(QDialog): """) self.setLayout(layout) + +class NetworkCameraDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Network Camera Settings") + self.setModal(True) + self.resize(400, 300) + + layout = QVBoxLayout(self) + + # Instructions label + instructions = QLabel( + "Enter network camera details:\n" + "- For DroidCam: Use the IP and port shown in the app\n" + " Example: http://192.168.1.100:4747/video\n" + "- For other IP cameras: Enter the full stream URL" + ) + instructions.setWordWrap(True) + layout.addWidget(instructions) + + # Camera list + self.camera_list = QListWidget() + layout.addWidget(self.camera_list) + + # Input fields + form_layout = QFormLayout() + self.name_edit = QLineEdit() + self.url_edit = QLineEdit() + form_layout.addRow("Name:", self.name_edit) + form_layout.addRow("URL:", self.url_edit) + layout.addLayout(form_layout) + + # Buttons + btn_layout = QHBoxLayout() + add_btn = QPushButton("Add Camera") + add_btn.clicked.connect(self.add_camera) + remove_btn = QPushButton("Remove Camera") + remove_btn.clicked.connect(self.remove_camera) + close_btn = QPushButton("Close") + close_btn.clicked.connect(self.accept) + + btn_layout.addWidget(add_btn) + btn_layout.addWidget(remove_btn) + btn_layout.addWidget(close_btn) + layout.addLayout(btn_layout) + + self.detector = parent.detector if parent else None + self.load_cameras() + + def load_cameras(self): + """Load saved network cameras into the list""" + if not self.detector: + return + + self.camera_list.clear() + for name, url in self.detector.network_cameras.items(): + self.camera_list.addItem(f"{name} ({url})") + + def add_camera(self): + """Add a new network camera""" + name = self.name_edit.text().strip() + url = self.url_edit.text().strip() + + if not name or not url: + QMessageBox.warning(self, "Error", "Please enter both name and URL") + return + + if self.detector: + self.detector.add_network_camera(name, url) + self.load_cameras() + self.name_edit.clear() + self.url_edit.clear() + + def remove_camera(self): + """Remove selected network camera""" + current = self.camera_list.currentItem() + if not current: + return + + name = current.text().split(" (")[0] + if self.detector: + self.detector.remove_network_camera(name) + self.load_cameras() + class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Multi-Camera YOLO Detection") self.setGeometry(100, 100, 1200, 800) + # Initialize configuration + self.config = Config() + # Set dark theme style self.setStyleSheet(""" QMainWindow, QWidget { @@ -523,26 +727,46 @@ class MainWindow(QMainWindow): self.detector = MultiCamYOLODetector() self.camera_settings = {} + + # Load saved settings + self.load_saved_settings() + self.create_menus() self.init_ui() self.init_timer() - def show_menu(self): - about = AboutWindow(self) # Pass self as parent - about.exec_() # Use exec_() for modal dialog + def load_saved_settings(self): + """Load saved settings from configuration""" + # Load model directory + model_dir = self.config.load_setting('model_dir') + if model_dir and os.path.exists(model_dir): + self.detector.load_yolo_model(model_dir) + + # Load FPS setting + fps = self.config.load_setting('fps', 10) + self.detector.target_fps = int(fps) + self.detector.frame_interval = 1.0 / self.detector.target_fps + + # Load layout setting + self.current_layout = int(self.config.load_setting('layout', 0)) + + def save_settings(self): + """Save current settings to configuration""" + self.config.save_setting('model_dir', self.detector.model_dir) + self.config.save_setting('fps', self.fps_spin.value()) + self.config.save_setting('layout', self.layout_combo.currentIndex()) def create_menus(self): menubar = self.menuBar() - + # File Menu - file_menu = menubar.addMenu('File') + file_menu = menubar.addMenu('File') about_action = QAction("About", self) about_action.triggered.connect(self.show_menu) file_menu.addAction(about_action) - + # Model menu model_menu = menubar.addMenu('Model') - load_model_action = QAction('Load Model Directory...', self) load_model_action.triggered.connect(self.load_model_directory) model_menu.addAction(load_model_action) @@ -552,13 +776,18 @@ class MainWindow(QMainWindow): self.refresh_cameras_action = QAction('Refresh Camera List', self) self.refresh_cameras_action.triggered.connect(self.populate_camera_menu) self.camera_menu.addAction(self.refresh_cameras_action) + + # Add Network Camera action + network_camera_action = QAction('Network Camera Settings...', self) + network_camera_action.triggered.connect(self.show_network_camera_dialog) + self.camera_menu.addAction(network_camera_action) + self.camera_menu.addSeparator() self.camera_action_group = QActionGroup(self) self.camera_action_group.setExclusive(False) self.populate_camera_menu() - - + def populate_camera_menu(self): """Populate the camera menu with available cameras""" # Clear existing camera actions (except refresh) @@ -582,19 +811,21 @@ class MainWindow(QMainWindow): no_cam_action = QAction('No cameras found', self) no_cam_action.setEnabled(False) self.camera_menu.addAction(no_cam_action) - + def load_model_directory(self): """Open file dialog to select model directory""" + last_dir = self.config.load_setting('model_dir', QDir.homePath()) model_dir = QFileDialog.getExistingDirectory( self, "Select Model Directory", - QDir.homePath(), + last_dir, QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks ) if model_dir: if self.detector.load_yolo_model(model_dir): self.model_label.setText(f"Model: {os.path.basename(model_dir)}") + self.config.save_setting('model_dir', model_dir) QMessageBox.information(self, "Success", "Model loaded successfully!") else: QMessageBox.critical(self, "Error", "Failed to load model from selected directory") @@ -662,6 +893,11 @@ class MainWindow(QMainWindow): layout_layout.addWidget(self.layout_combo) settings_layout.addLayout(layout_layout) + # Add screenshot button to settings + screenshot_btn = QPushButton("Take Screenshot") + screenshot_btn.clicked.connect(self.take_screenshot) + settings_layout.addWidget(screenshot_btn) + settings_group.setLayout(settings_layout) sidebar_layout.addWidget(settings_group) @@ -712,6 +948,12 @@ class MainWindow(QMainWindow): # Start with sidebar expanded self.sidebar.expand() + + # Set saved FPS value + self.fps_spin.setValue(self.detector.target_fps) + + # Set saved layout + self.layout_combo.setCurrentIndex(self.current_layout) def change_camera_layout(self, index): """Change the camera display layout""" @@ -834,9 +1076,31 @@ class MainWindow(QMainWindow): display.setPixmap(pixmap.scaled(display.width(), display.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) + def take_screenshot(self): + """Take screenshot of active camera displays""" + active_displays = [d for d in self.camera_displays if d.isVisible() and d.pixmap()] + if not active_displays: + QMessageBox.warning(self, "Warning", "No active camera displays to capture!") + return + + for display in active_displays: + display.take_screenshot() + + def show_menu(self): + about = AboutWindow(self) # Pass self as parent + about.exec_() # Use exec_() for modal dialog + + def show_network_camera_dialog(self): + """Show the network camera management dialog""" + dialog = NetworkCameraDialog(self) + dialog.exec_() + # Refresh camera list after dialog closes + self.populate_camera_menu() + def closeEvent(self, event): """Handle window close event""" self.stop_detection() + self.save_settings() super().closeEvent(event) if __name__ == "__main__": -- 2.49.1 From b24426c1d313ef1760a5fe5ee9c858f17b4ba20b Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 26 May 2025 16:18:25 +0200 Subject: [PATCH 2/6] better bullshit --- mucapy/main.py | 419 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 376 insertions(+), 43 deletions(-) diff --git a/mucapy/main.py b/mucapy/main.py index b943622..cac7436 100644 --- a/mucapy/main.py +++ b/mucapy/main.py @@ -2,15 +2,18 @@ import os import sys import cv2 import json +import urllib.parse import numpy as np from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QPushButton, QComboBox, QSpinBox, QFileDialog, QMessageBox, QMenu, QAction, QMenuBar, QActionGroup, QSizePolicy, QGridLayout, QGroupBox, QDockWidget, QScrollArea, QToolButton, QDialog, - QShortcut, QListWidget, QFormLayout, QLineEdit) -from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime -from PyQt5.QtGui import QImage, QPixmap, QIcon, QColor, QKeySequence + QShortcut, QListWidget, QFormLayout, QLineEdit, + QCheckBox, QTabWidget, QListWidgetItem) +from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime, QRect +from PyQt5.QtGui import (QImage, QPixmap, QIcon, QColor, QKeySequence, QPainter, + QPen, QBrush) class Config: def __init__(self): @@ -72,9 +75,9 @@ class MultiCamYOLODetector: except: return False - def add_network_camera(self, name, url): + def add_network_camera(self, name, camera_info): """Add a network camera to the saved list""" - self.network_cameras[name] = url + self.network_cameras[name] = camera_info self.config.save_setting('network_cameras', self.network_cameras) def remove_network_camera(self, name): @@ -87,34 +90,37 @@ class MultiCamYOLODetector: """Check for available cameras including network cameras""" self.available_cameras = [] - # Check standard video devices + # Try numeric indices first (this works more reliably) for i in range(max_to_check): try: - cap = cv2.VideoCapture(i, cv2.CAP_V4L2) + cap = cv2.VideoCapture(i) if cap.isOpened(): self.available_cameras.append(str(i)) cap.release() except: continue - # Check direct device paths - v4l_paths = [f"/dev/video{i}" for i in range(max_to_check)] - v4l_paths += [f"/dev/v4l/video{i}" for i in range(max_to_check)] - - for path in v4l_paths: - if os.path.exists(path): - try: - cap = cv2.VideoCapture(path, cv2.CAP_V4L2) - if cap.isOpened(): - if path not in self.available_cameras: - self.available_cameras.append(path) + # Also check device paths as fallback + if os.path.exists('/dev'): + for i in range(max_to_check): + device_path = f"/dev/video{i}" + if os.path.exists(device_path) and device_path not in self.available_cameras: + try: + cap = cv2.VideoCapture(device_path) + if cap.isOpened(): + self.available_cameras.append(device_path) cap.release() - except: - continue + except: + continue # Add saved network cameras - for name, url in self.network_cameras.items(): - self.available_cameras.append(f"net:{url}") + for name, camera_info in self.network_cameras.items(): + if isinstance(camera_info, dict): + url = camera_info.get('url', '') + self.available_cameras.append(f"net:{name}") # Use name instead of URL for better identification + else: + # Handle old format where camera_info was just the URL + self.available_cameras.append(f"net:{name}") return self.available_cameras @@ -161,7 +167,7 @@ class MultiCamYOLODetector: return False def connect_cameras(self, camera_paths): - """Connect to multiple cameras including network cameras""" + """Connect to multiple cameras including network cameras with authentication""" self.disconnect_cameras() for cam_path in camera_paths: @@ -169,16 +175,30 @@ class MultiCamYOLODetector: if isinstance(cam_path, str): if cam_path.startswith('net:'): # Handle network camera - url = cam_path[4:] # Remove 'net:' prefix + camera_name = cam_path[4:] # Remove 'net:' prefix to get camera name + camera_info = self.network_cameras.get(camera_name) + + if isinstance(camera_info, dict): + url = camera_info['url'] + # Add authentication to URL if provided + if 'username' in camera_info and 'password' in camera_info: + # Parse URL and add authentication + parsed = urllib.parse.urlparse(url) + netloc = f"{camera_info['username']}:{camera_info['password']}@{parsed.netloc}" + url = parsed._replace(netloc=netloc).geturl() + else: + # Handle old format where camera_info was just the URL + url = camera_info + cap = cv2.VideoCapture(url) elif cam_path.startswith('/dev/'): # Handle device path - cap = cv2.VideoCapture(cam_path, cv2.CAP_V4L2) + cap = cv2.VideoCapture(cam_path) else: # Handle numeric index - cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2) + cap = cv2.VideoCapture(int(cam_path)) else: - cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2) + cap = cv2.VideoCapture(int(cam_path)) if not cap.isOpened(): print(f"Warning: Could not open camera {cam_path}") @@ -299,6 +319,7 @@ class CameraDisplay(QLabel): self.fullscreen_timer = None self.config = Config() self.screenshot_dir = self.config.load_setting('screenshot_dir', os.path.expanduser('~/Pictures/MuCaPy')) + self.camera_name = None # Create screenshot directory if it doesn't exist if not os.path.exists(self.screenshot_dir): @@ -308,6 +329,11 @@ class CameraDisplay(QLabel): """Set camera identifier for this display""" self.cam_id = cam_id + def set_camera_name(self, name): + """Set the camera name for display""" + self.camera_name = name + self.update() + def take_screenshot(self): """Take a screenshot of the current frame""" if not self.pixmap(): @@ -406,6 +432,23 @@ class CameraDisplay(QLabel): self.fullscreen_window.close() self.fullscreen_window = None self.fullscreen_timer = None + + def paintEvent(self, event): + """Override paint event to draw camera name overlay""" + super().paintEvent(event) + if self.camera_name and self.pixmap(): + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + # Draw semi-transparent background + painter.setPen(Qt.NoPen) + painter.setBrush(QBrush(QColor(0, 0, 0, 180))) + rect = QRect(10, 10, painter.fontMetrics().width(self.camera_name) + 20, 30) + painter.drawRoundedRect(rect, 5, 5) + + # Draw text + painter.setPen(QPen(QColor(255, 255, 255))) + painter.drawText(rect, Qt.AlignCenter, self.camera_name) class CollapsibleDock(QDockWidget): """Custom dock widget with collapse/expand functionality""" @@ -539,7 +582,7 @@ class NetworkCameraDialog(QDialog): super().__init__(parent) self.setWindowTitle("Network Camera Settings") self.setModal(True) - self.resize(400, 300) + self.resize(500, 400) layout = QVBoxLayout(self) @@ -548,7 +591,8 @@ class NetworkCameraDialog(QDialog): "Enter network camera details:\n" "- For DroidCam: Use the IP and port shown in the app\n" " Example: http://192.168.1.100:4747/video\n" - "- For other IP cameras: Enter the full stream URL" + "- For other IP cameras: Enter the full stream URL\n" + "- Enable authentication if the camera requires username/password" ) instructions.setWordWrap(True) layout.addWidget(instructions) @@ -559,12 +603,38 @@ class NetworkCameraDialog(QDialog): # Input fields form_layout = QFormLayout() + + # Name and URL self.name_edit = QLineEdit() self.url_edit = QLineEdit() form_layout.addRow("Name:", self.name_edit) form_layout.addRow("URL:", self.url_edit) + + # Authentication group + auth_group = QGroupBox("Authentication") + auth_layout = QVBoxLayout() + + self.auth_checkbox = QCheckBox("Enable Authentication") + self.auth_checkbox.stateChanged.connect(self.toggle_auth_fields) + auth_layout.addWidget(self.auth_checkbox) + + auth_form = QFormLayout() + self.username_edit = QLineEdit() + self.password_edit = QLineEdit() + self.password_edit.setEchoMode(QLineEdit.Password) + auth_form.addRow("Username:", self.username_edit) + auth_form.addRow("Password:", self.password_edit) + auth_layout.addLayout(auth_form) + + auth_group.setLayout(auth_layout) + form_layout.addRow(auth_group) + layout.addLayout(form_layout) + # Initially disable auth fields + self.username_edit.setEnabled(False) + self.password_edit.setEnabled(False) + # Buttons btn_layout = QHBoxLayout() add_btn = QPushButton("Add Camera") @@ -582,14 +652,32 @@ class NetworkCameraDialog(QDialog): self.detector = parent.detector if parent else None self.load_cameras() + def toggle_auth_fields(self, state): + """Enable/disable authentication fields based on checkbox state""" + enabled = state == Qt.Checked + self.username_edit.setEnabled(enabled) + self.password_edit.setEnabled(enabled) + if not enabled: + self.username_edit.clear() + self.password_edit.clear() + def load_cameras(self): """Load saved network cameras into the list""" if not self.detector: return self.camera_list.clear() - for name, url in self.detector.network_cameras.items(): - self.camera_list.addItem(f"{name} ({url})") + for name, camera_info in self.detector.network_cameras.items(): + if isinstance(camera_info, dict): + url = camera_info.get('url', '') + has_auth = camera_info.get('username') is not None + display_text = f"{name} ({url})" + if has_auth: + display_text += " [Auth]" + else: + # Handle old format where camera_info was just the URL + display_text = f"{name} ({camera_info})" + self.camera_list.addItem(display_text) def add_camera(self): """Add a new network camera""" @@ -601,10 +689,25 @@ class NetworkCameraDialog(QDialog): return if self.detector: - self.detector.add_network_camera(name, url) + camera_info = {'url': url} + + # Add authentication if enabled + if self.auth_checkbox.isChecked(): + username = self.username_edit.text().strip() + password = self.password_edit.text().strip() + if username and password: + camera_info['username'] = username + camera_info['password'] = password + + self.detector.add_network_camera(name, camera_info) self.load_cameras() + + # Clear fields self.name_edit.clear() self.url_edit.clear() + self.username_edit.clear() + self.password_edit.clear() + self.auth_checkbox.setChecked(False) def remove_camera(self): """Remove selected network camera""" @@ -617,6 +720,197 @@ class NetworkCameraDialog(QDialog): self.detector.remove_network_camera(name) self.load_cameras() +class CameraSelectorDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Camera Selector") + self.setModal(True) + self.resize(600, 500) + + self.detector = parent.detector if parent else None + self.selected_cameras = [] + + # Main layout + layout = QVBoxLayout(self) + + # Create tab widget + tabs = QTabWidget() + + # Local Cameras Tab + local_tab = QWidget() + local_layout = QVBoxLayout(local_tab) + + # Local camera list + local_group = QGroupBox("Local Cameras") + local_list_layout = QVBoxLayout() + self.local_list = QListWidget() + self.local_list.setSelectionMode(QListWidget.MultiSelection) + local_list_layout.addWidget(self.local_list) + + # Refresh button for local cameras + refresh_btn = QPushButton("Refresh Local Cameras") + refresh_btn.clicked.connect(self.refresh_local_cameras) + local_list_layout.addWidget(refresh_btn) + + local_group.setLayout(local_list_layout) + local_layout.addWidget(local_group) + tabs.addTab(local_tab, "Local Cameras") + + # Network Cameras Tab + network_tab = QWidget() + network_layout = QVBoxLayout(network_tab) + + # Network camera list + network_group = QGroupBox("Network Cameras") + network_list_layout = QVBoxLayout() + self.network_list = QListWidget() + self.network_list.setSelectionMode(QListWidget.MultiSelection) + network_list_layout.addWidget(self.network_list) + + # Network camera controls + net_btn_layout = QHBoxLayout() + add_net_btn = QPushButton("Add Network Camera") + add_net_btn.clicked.connect(self.show_network_dialog) + remove_net_btn = QPushButton("Remove Selected") + remove_net_btn.clicked.connect(self.remove_network_camera) + net_btn_layout.addWidget(add_net_btn) + net_btn_layout.addWidget(remove_net_btn) + network_list_layout.addLayout(net_btn_layout) + + network_group.setLayout(network_list_layout) + network_layout.addWidget(network_group) + tabs.addTab(network_tab, "Network Cameras") + + layout.addWidget(tabs) + + # Selected cameras preview + preview_group = QGroupBox("Selected Cameras") + preview_layout = QVBoxLayout() + self.preview_list = QListWidget() + preview_layout.addWidget(self.preview_list) + preview_group.setLayout(preview_layout) + layout.addWidget(preview_group) + + # Buttons + btn_layout = QHBoxLayout() + select_all_btn = QPushButton("Select All") + select_all_btn.clicked.connect(self.select_all) + clear_btn = QPushButton("Clear Selection") + clear_btn.clicked.connect(self.clear_selection) + ok_btn = QPushButton("OK") + ok_btn.clicked.connect(self.accept) + cancel_btn = QPushButton("Cancel") + cancel_btn.clicked.connect(self.reject) + + btn_layout.addWidget(select_all_btn) + btn_layout.addWidget(clear_btn) + btn_layout.addStretch() + btn_layout.addWidget(ok_btn) + btn_layout.addWidget(cancel_btn) + layout.addLayout(btn_layout) + + # Connect selection change signals + self.local_list.itemSelectionChanged.connect(self.update_preview) + self.network_list.itemSelectionChanged.connect(self.update_preview) + + self.refresh_cameras() + + def refresh_cameras(self): + """Refresh both local and network camera lists""" + self.refresh_local_cameras() + self.refresh_network_cameras() + + def refresh_local_cameras(self): + """Refresh the local camera list""" + self.local_list.clear() + if self.detector: + # Get local cameras + for i in range(10): # Check first 10 indices + try: + cap = cv2.VideoCapture(i) + if cap.isOpened(): + self.local_list.addItem(QListWidgetItem(f"Camera {i}")) + cap.release() + except: + continue + + # Check device paths + if os.path.exists('/dev'): + for i in range(10): + device_path = f"/dev/video{i}" + if os.path.exists(device_path): + try: + cap = cv2.VideoCapture(device_path) + if cap.isOpened(): + item = QListWidgetItem(os.path.basename(device_path)) + item.setData(Qt.UserRole, device_path) + self.local_list.addItem(item) + cap.release() + except: + continue + + def refresh_network_cameras(self): + """Refresh the network camera list""" + self.network_list.clear() + if self.detector: + for name, camera_info in self.detector.network_cameras.items(): + if isinstance(camera_info, dict): + url = camera_info.get('url', '') + has_auth = camera_info.get('username') is not None + display_text = f"{name} ({url})" + if has_auth: + display_text += " [Auth]" + else: + display_text = f"{name} ({camera_info})" + + item = QListWidgetItem(display_text) + item.setData(Qt.UserRole, f"net:{name}") + self.network_list.addItem(item) + + def show_network_dialog(self): + """Show the network camera configuration dialog""" + dialog = NetworkCameraDialog(self.parent()) + if dialog.exec_() == QDialog.Accepted: + self.refresh_network_cameras() + + def remove_network_camera(self): + """Remove selected network cameras""" + for item in self.network_list.selectedItems(): + cam_path = item.data(Qt.UserRole) + if cam_path.startswith('net:'): + name = cam_path[4:] + if self.detector: + self.detector.remove_network_camera(name) + self.refresh_network_cameras() + self.update_preview() + + def update_preview(self): + """Update the preview list with currently selected cameras""" + self.preview_list.clear() + self.selected_cameras = [] + + # Add selected local cameras + for item in self.local_list.selectedItems(): + cam_path = item.data(Qt.UserRole) if item.data(Qt.UserRole) else item.text().split()[-1] + self.selected_cameras.append(cam_path) + self.preview_list.addItem(f"✓ {item.text()}") + + # Add selected network cameras + for item in self.network_list.selectedItems(): + cam_path = item.data(Qt.UserRole) + self.selected_cameras.append(cam_path) + self.preview_list.addItem(f"✓ {item.text()}") + + def select_all(self): + """Select all cameras in both lists""" + self.local_list.selectAll() + self.network_list.selectAll() + + def clear_selection(self): + """Clear selection in both lists""" + self.local_list.clearSelection() + self.network_list.clearSelection() + class MainWindow(QMainWindow): def __init__(self): super().__init__() @@ -773,11 +1067,15 @@ class MainWindow(QMainWindow): # Camera menu self.camera_menu = menubar.addMenu('Cameras') - self.refresh_cameras_action = QAction('Refresh Camera List', self) - self.refresh_cameras_action.triggered.connect(self.populate_camera_menu) - self.camera_menu.addAction(self.refresh_cameras_action) - # Add Network Camera action + # Add Camera Selector action + select_cameras_action = QAction('Select Cameras...', self) + select_cameras_action.triggered.connect(self.show_camera_selector) + self.camera_menu.addAction(select_cameras_action) + + self.camera_menu.addSeparator() + + # Add Network Camera Settings action network_camera_action = QAction('Network Camera Settings...', self) network_camera_action.triggered.connect(self.show_network_camera_dialog) self.camera_menu.addAction(network_camera_action) @@ -790,19 +1088,22 @@ class MainWindow(QMainWindow): def populate_camera_menu(self): """Populate the camera menu with available cameras""" - # Clear existing camera actions (except refresh) - for action in self.camera_menu.actions()[2:]: + # Clear existing camera actions (except refresh and network camera settings) + for action in self.camera_menu.actions()[3:]: self.camera_menu.removeAction(action) available_cams = self.detector.scan_for_cameras() for cam_path in available_cams: # Display friendly name - if cam_path.startswith('/dev/'): - cam_name = os.path.basename(cam_path) + if cam_path.startswith('net:'): + name = cam_path[4:] # Use the camera name directly + display_name = f"{name}" + elif cam_path.startswith('/dev/'): + display_name = os.path.basename(cam_path) else: - cam_name = f"Camera {cam_path}" + display_name = f"Camera {cam_path}" - action = QAction(cam_name, self, checkable=True) + action = QAction(display_name, self, checkable=True) action.setData(cam_path) self.camera_action_group.addAction(action) self.camera_menu.addAction(action) @@ -1060,7 +1361,7 @@ class MainWindow(QMainWindow): """Update the camera feeds in the display""" frames = self.detector.get_frames() - for i, frame in enumerate(frames): + for i, (cam_path, frame) in enumerate(zip(self.detector.cameras, frames)): if i >= len(self.camera_displays): break @@ -1075,6 +1376,20 @@ class MainWindow(QMainWindow): display = self.camera_displays[i] display.setPixmap(pixmap.scaled(display.width(), display.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) + + # Update camera name + cam_path = cam_path[0] if isinstance(cam_path, tuple) else cam_path + if isinstance(cam_path, str): + if cam_path.startswith('net:'): + # For network cameras, show the saved name + camera_name = cam_path[4:] # Get the name directly + display.set_camera_name(camera_name) + elif cam_path.startswith('/dev/'): + # For device paths, show the device name + display.set_camera_name(os.path.basename(cam_path)) + else: + # For numeric indices, show Camera N + display.set_camera_name(f"Camera {cam_path}") def take_screenshot(self): """Take screenshot of active camera displays""" @@ -1097,6 +1412,24 @@ class MainWindow(QMainWindow): # Refresh camera list after dialog closes self.populate_camera_menu() + def show_camera_selector(self): + """Show the camera selector dialog""" + dialog = CameraSelectorDialog(self) + if dialog.exec_() == QDialog.Accepted and dialog.selected_cameras: + # Stop current detection if running + was_running = False + if self.stop_btn.isEnabled(): + was_running = True + self.stop_detection() + + # Update selected cameras + for action in self.camera_action_group.actions(): + action.setChecked(action.data() in dialog.selected_cameras) + + # Restart detection if it was running + if was_running: + self.start_detection() + def closeEvent(self, event): """Handle window close event""" self.stop_detection() -- 2.49.1 From c264acac29522a854eefe2d1c372f122170a60b0 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 26 May 2025 16:39:58 +0200 Subject: [PATCH 3/6] i hate python --- mucapy/main.py | 449 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 305 insertions(+), 144 deletions(-) diff --git a/mucapy/main.py b/mucapy/main.py index cac7436..1eb852f 100644 --- a/mucapy/main.py +++ b/mucapy/main.py @@ -10,16 +10,25 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout QActionGroup, QSizePolicy, QGridLayout, QGroupBox, QDockWidget, QScrollArea, QToolButton, QDialog, QShortcut, QListWidget, QFormLayout, QLineEdit, - QCheckBox, QTabWidget, QListWidgetItem) + QCheckBox, QTabWidget, QListWidgetItem, QSplitter) from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime, QRect from PyQt5.QtGui import (QImage, QPixmap, QIcon, QColor, QKeySequence, QPainter, QPen, QBrush) class Config: def __init__(self): - # Use JSON file in current working directory instead of QSettings + # Use JSON file in current working directory self.config_file = os.path.join(os.getcwd(), 'mucapy_config.json') - self.settings = {} + self.settings = { + 'network_cameras': {}, # Store network cameras configuration + 'last_model_dir': '', + 'last_screenshot_dir': os.path.expanduser('~/Pictures/MuCaPy'), + 'last_layout': 0, + 'last_fps': 10, + 'last_selected_cameras': [], + 'window_geometry': None, + 'confidence_threshold': 0.35, + } self.load_config() def load_config(self): @@ -27,14 +36,17 @@ class Config: try: if os.path.exists(self.config_file): with open(self.config_file, 'r') as f: - self.settings = json.load(f) + loaded_settings = json.load(f) + # Update settings while preserving default values for new keys + self.settings.update(loaded_settings) except Exception as e: print(f"Error loading config: {e}") - self.settings = {} def save_config(self): """Save configuration to JSON file""" try: + # Ensure the file's directory exists + os.makedirs(os.path.dirname(self.config_file), exist_ok=True) with open(self.config_file, 'w') as f: json.dump(self.settings, f, indent=4) except Exception as e: @@ -61,12 +73,19 @@ class MultiCamYOLODetector: self.available_cameras = [] self.model_dir = "" self.cuda_available = self.check_cuda() - self.confidence_threshold = 0.35 self.config = Config() - # Load saved network cameras + # Load settings + self.confidence_threshold = self.config.load_setting('confidence_threshold', 0.35) self.network_cameras = self.config.load_setting('network_cameras', {}) + self.target_fps = self.config.load_setting('last_fps', 10) + self.frame_interval = 1.0 / self.target_fps + # Load last used model if available + last_model = self.config.load_setting('last_model_dir') + if last_model and os.path.exists(last_model): + self.load_yolo_model(last_model) + def check_cuda(self): """Check if CUDA is available""" try: @@ -78,13 +97,17 @@ class MultiCamYOLODetector: def add_network_camera(self, name, camera_info): """Add a network camera to the saved list""" self.network_cameras[name] = camera_info - self.config.save_setting('network_cameras', self.network_cameras) + # Save to configuration immediately + self.config.settings['network_cameras'] = self.network_cameras + self.config.save_config() def remove_network_camera(self, name): """Remove a network camera from the saved list""" if name in self.network_cameras: del self.network_cameras[name] - self.config.save_setting('network_cameras', self.network_cameras) + # Save to configuration immediately + self.config.settings['network_cameras'] = self.network_cameras + self.config.save_config() def scan_for_cameras(self, max_to_check=10): """Check for available cameras including network cameras""" @@ -725,7 +748,7 @@ class CameraSelectorDialog(QDialog): super().__init__(parent) self.setWindowTitle("Camera Selector") self.setModal(True) - self.resize(600, 500) + self.resize(800, 600) # Increased size for better visibility self.detector = parent.detector if parent else None self.selected_cameras = [] @@ -733,183 +756,321 @@ class CameraSelectorDialog(QDialog): # Main layout layout = QVBoxLayout(self) - # Create tab widget - tabs = QTabWidget() + # Instructions with better formatting + instructions = QLabel( + "Camera Selection Guide:\n\n" + "• Local Cameras: Built-in and USB cameras\n" + "• Network Cameras: IP cameras, DroidCam, etc.\n" + "• Use checkboxes to select/deselect cameras\n" + "• Double-click a camera to test the connection\n" + "• Selected cameras will appear in the preview below" + ) + instructions.setStyleSheet("QLabel { background-color: #2A2A2A; padding: 10px; border-radius: 4px; }") + instructions.setWordWrap(True) + layout.addWidget(instructions) - # Local Cameras Tab - local_tab = QWidget() - local_layout = QVBoxLayout(local_tab) + # Split view for cameras + splitter = QSplitter(Qt.Horizontal) - # Local camera list + # Left side - Available Cameras + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + # Local Cameras Group local_group = QGroupBox("Local Cameras") - local_list_layout = QVBoxLayout() + local_layout = QVBoxLayout() self.local_list = QListWidget() - self.local_list.setSelectionMode(QListWidget.MultiSelection) - local_list_layout.addWidget(self.local_list) + self.local_list.setSelectionMode(QListWidget.ExtendedSelection) + local_layout.addWidget(self.local_list) + local_group.setLayout(local_layout) + left_layout.addWidget(local_group) - # Refresh button for local cameras - refresh_btn = QPushButton("Refresh Local Cameras") - refresh_btn.clicked.connect(self.refresh_local_cameras) - local_list_layout.addWidget(refresh_btn) - - local_group.setLayout(local_list_layout) - local_layout.addWidget(local_group) - tabs.addTab(local_tab, "Local Cameras") - - # Network Cameras Tab - network_tab = QWidget() - network_layout = QVBoxLayout(network_tab) - - # Network camera list + # Network Cameras Group network_group = QGroupBox("Network Cameras") - network_list_layout = QVBoxLayout() + network_layout = QVBoxLayout() self.network_list = QListWidget() - self.network_list.setSelectionMode(QListWidget.MultiSelection) - network_list_layout.addWidget(self.network_list) + self.network_list.setSelectionMode(QListWidget.ExtendedSelection) + network_layout.addWidget(self.network_list) + network_group.setLayout(network_layout) + left_layout.addWidget(network_group) - # Network camera controls - net_btn_layout = QHBoxLayout() + # Camera management buttons + btn_layout = QHBoxLayout() + refresh_btn = QPushButton("Refresh") + refresh_btn.clicked.connect(self.refresh_cameras) add_net_btn = QPushButton("Add Network Camera") add_net_btn.clicked.connect(self.show_network_dialog) - remove_net_btn = QPushButton("Remove Selected") - remove_net_btn.clicked.connect(self.remove_network_camera) - net_btn_layout.addWidget(add_net_btn) - net_btn_layout.addWidget(remove_net_btn) - network_list_layout.addLayout(net_btn_layout) - network_group.setLayout(network_list_layout) - network_layout.addWidget(network_group) - tabs.addTab(network_tab, "Network Cameras") + btn_layout.addWidget(refresh_btn) + btn_layout.addWidget(add_net_btn) + left_layout.addLayout(btn_layout) - layout.addWidget(tabs) + splitter.addWidget(left_widget) + + # Right side - Selected Cameras Preview + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + preview_label = QLabel("Selected Cameras Preview") + preview_label.setStyleSheet("font-weight: bold;") + right_layout.addWidget(preview_label) - # Selected cameras preview - preview_group = QGroupBox("Selected Cameras") - preview_layout = QVBoxLayout() self.preview_list = QListWidget() - preview_layout.addWidget(self.preview_list) - preview_group.setLayout(preview_layout) - layout.addWidget(preview_group) + self.preview_list.setDragDropMode(QListWidget.InternalMove) + self.preview_list.setSelectionMode(QListWidget.ExtendedSelection) + right_layout.addWidget(self.preview_list) - # Buttons - btn_layout = QHBoxLayout() + # Preview controls + preview_btn_layout = QHBoxLayout() + remove_btn = QPushButton("Remove Selected") + remove_btn.clicked.connect(self.remove_selected) + clear_btn = QPushButton("Clear All") + clear_btn.clicked.connect(self.clear_selection) + + preview_btn_layout.addWidget(remove_btn) + preview_btn_layout.addWidget(clear_btn) + right_layout.addLayout(preview_btn_layout) + + splitter.addWidget(right_widget) + layout.addWidget(splitter) + + # Bottom buttons + bottom_layout = QHBoxLayout() select_all_btn = QPushButton("Select All") select_all_btn.clicked.connect(self.select_all) - clear_btn = QPushButton("Clear Selection") - clear_btn.clicked.connect(self.clear_selection) + test_selected_btn = QPushButton("Test Selected") + test_selected_btn.clicked.connect(self.test_selected_cameras) ok_btn = QPushButton("OK") ok_btn.clicked.connect(self.accept) cancel_btn = QPushButton("Cancel") cancel_btn.clicked.connect(self.reject) - btn_layout.addWidget(select_all_btn) - btn_layout.addWidget(clear_btn) - btn_layout.addStretch() - btn_layout.addWidget(ok_btn) - btn_layout.addWidget(cancel_btn) - layout.addLayout(btn_layout) + bottom_layout.addWidget(select_all_btn) + bottom_layout.addWidget(test_selected_btn) + bottom_layout.addStretch() + bottom_layout.addWidget(ok_btn) + bottom_layout.addWidget(cancel_btn) + layout.addLayout(bottom_layout) - # Connect selection change signals - self.local_list.itemSelectionChanged.connect(self.update_preview) - self.network_list.itemSelectionChanged.connect(self.update_preview) + # Connect signals + self.local_list.itemChanged.connect(self.update_preview) + self.network_list.itemChanged.connect(self.update_preview) + self.preview_list.model().rowsMoved.connect(self.update_camera_order) + # Set splitter sizes + splitter.setSizes([400, 400]) + + # Initial camera refresh self.refresh_cameras() + + # Restore last selection if available + if self.detector: + last_selected = self.detector.config.load_setting('last_selected_cameras', []) + if last_selected: + self.restore_selection(last_selected) def refresh_cameras(self): """Refresh both local and network camera lists""" - self.refresh_local_cameras() - self.refresh_network_cameras() - - def refresh_local_cameras(self): - """Refresh the local camera list""" self.local_list.clear() - if self.detector: - # Get local cameras - for i in range(10): # Check first 10 indices - try: - cap = cv2.VideoCapture(i) - if cap.isOpened(): - self.local_list.addItem(QListWidgetItem(f"Camera {i}")) - cap.release() - except: - continue - - # Check device paths - if os.path.exists('/dev'): - for i in range(10): - device_path = f"/dev/video{i}" - if os.path.exists(device_path): - try: - cap = cv2.VideoCapture(device_path) - if cap.isOpened(): - item = QListWidgetItem(os.path.basename(device_path)) - item.setData(Qt.UserRole, device_path) - self.local_list.addItem(item) - cap.release() - except: - continue - - def refresh_network_cameras(self): - """Refresh the network camera list""" self.network_list.clear() - if self.detector: - for name, camera_info in self.detector.network_cameras.items(): - if isinstance(camera_info, dict): - url = camera_info.get('url', '') - has_auth = camera_info.get('username') is not None - display_text = f"{name} ({url})" - if has_auth: - display_text += " [Auth]" - else: - display_text = f"{name} ({camera_info})" - - item = QListWidgetItem(display_text) - item.setData(Qt.UserRole, f"net:{name}") - self.network_list.addItem(item) + + if not self.detector: + return + + # Add local cameras + for i in range(10): # Check first 10 indices + try: + cap = cv2.VideoCapture(i) + if cap.isOpened(): + item = QListWidgetItem(f"Camera {i}") + item.setData(Qt.UserRole, str(i)) + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Unchecked) + self.local_list.addItem(item) + cap.release() + except Exception as e: + print(f"Error checking camera {i}: {e}") + + # Check device paths + if os.path.exists('/dev'): + for i in range(10): + device_path = f"/dev/video{i}" + if os.path.exists(device_path): + try: + cap = cv2.VideoCapture(device_path) + if cap.isOpened(): + item = QListWidgetItem(f"{os.path.basename(device_path)}") + item.setData(Qt.UserRole, device_path) + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Unchecked) + self.local_list.addItem(item) + cap.release() + except Exception as e: + print(f"Error checking device {device_path}: {e}") + + # Add network cameras + for name, camera_info in self.detector.network_cameras.items(): + if isinstance(camera_info, dict): + url = camera_info.get('url', '') + has_auth = camera_info.get('username') is not None + display_text = f"{name} ({url})" + if has_auth: + display_text += " 🔒" + else: + display_text = f"{name} ({camera_info})" + + item = QListWidgetItem(display_text) + item.setData(Qt.UserRole, f"net:{name}") + item.setFlags(item.flags() | Qt.ItemIsUserCheckable) + item.setCheckState(Qt.Unchecked) + self.network_list.addItem(item) - def show_network_dialog(self): - """Show the network camera configuration dialog""" - dialog = NetworkCameraDialog(self.parent()) - if dialog.exec_() == QDialog.Accepted: - self.refresh_network_cameras() - - def remove_network_camera(self): - """Remove selected network cameras""" - for item in self.network_list.selectedItems(): - cam_path = item.data(Qt.UserRole) - if cam_path.startswith('net:'): - name = cam_path[4:] - if self.detector: - self.detector.remove_network_camera(name) - self.refresh_network_cameras() - self.update_preview() + def restore_selection(self, last_selected): + """Restore previous camera selection""" + for cam_id in last_selected: + # Check local cameras + for i in range(self.local_list.count()): + item = self.local_list.item(i) + if item.data(Qt.UserRole) == cam_id: + item.setCheckState(Qt.Checked) + + # Check network cameras + for i in range(self.network_list.count()): + item = self.network_list.item(i) + if item.data(Qt.UserRole) == cam_id: + item.setCheckState(Qt.Checked) def update_preview(self): """Update the preview list with currently selected cameras""" self.preview_list.clear() self.selected_cameras = [] - # Add selected local cameras - for item in self.local_list.selectedItems(): - cam_path = item.data(Qt.UserRole) if item.data(Qt.UserRole) else item.text().split()[-1] - self.selected_cameras.append(cam_path) - self.preview_list.addItem(f"✓ {item.text()}") + # Get selected local cameras + for i in range(self.local_list.count()): + item = self.local_list.item(i) + if item.checkState() == Qt.Checked: + cam_id = item.data(Qt.UserRole) + preview_item = QListWidgetItem(f"📷 {item.text()}") + preview_item.setData(Qt.UserRole, cam_id) + self.preview_list.addItem(preview_item) + self.selected_cameras.append(cam_id) - # Add selected network cameras - for item in self.network_list.selectedItems(): - cam_path = item.data(Qt.UserRole) - self.selected_cameras.append(cam_path) - self.preview_list.addItem(f"✓ {item.text()}") + # Get selected network cameras + for i in range(self.network_list.count()): + item = self.network_list.item(i) + if item.checkState() == Qt.Checked: + cam_id = item.data(Qt.UserRole) + preview_item = QListWidgetItem(f"🌐 {item.text()}") + preview_item.setData(Qt.UserRole, cam_id) + self.preview_list.addItem(preview_item) + self.selected_cameras.append(cam_id) + + # Save the current selection to config + if self.detector: + self.detector.config.save_setting('last_selected_cameras', self.selected_cameras) + + def update_camera_order(self): + """Update the camera order based on preview list order""" + self.selected_cameras = [] + for i in range(self.preview_list.count()): + item = self.preview_list.item(i) + self.selected_cameras.append(item.data(Qt.UserRole)) + + # Save the new order + if self.detector: + self.detector.config.save_setting('last_selected_cameras', self.selected_cameras) def select_all(self): """Select all cameras in both lists""" - self.local_list.selectAll() - self.network_list.selectAll() + for i in range(self.local_list.count()): + self.local_list.item(i).setCheckState(Qt.Checked) + for i in range(self.network_list.count()): + self.network_list.item(i).setCheckState(Qt.Checked) def clear_selection(self): - """Clear selection in both lists""" - self.local_list.clearSelection() - self.network_list.clearSelection() + """Clear all selections""" + for i in range(self.local_list.count()): + self.local_list.item(i).setCheckState(Qt.Unchecked) + for i in range(self.network_list.count()): + self.network_list.item(i).setCheckState(Qt.Unchecked) + + def remove_selected(self): + """Remove selected items from the preview list""" + selected_items = self.preview_list.selectedItems() + for item in selected_items: + cam_id = item.data(Qt.UserRole) + # Uncheck corresponding items in source lists + for i in range(self.local_list.count()): + if self.local_list.item(i).data(Qt.UserRole) == cam_id: + self.local_list.item(i).setCheckState(Qt.Unchecked) + for i in range(self.network_list.count()): + if self.network_list.item(i).data(Qt.UserRole) == cam_id: + self.network_list.item(i).setCheckState(Qt.Unchecked) + + def test_selected_cameras(self): + """Test connection to selected cameras""" + selected_cameras = [] + for i in range(self.preview_list.count()): + selected_cameras.append(self.preview_list.item(i).data(Qt.UserRole)) + + if not selected_cameras: + QMessageBox.warning(self, "Warning", "No cameras selected to test!") + return + + # Create progress dialog + progress = QMessageBox(self) + progress.setIcon(QMessageBox.Information) + progress.setWindowTitle("Testing Cameras") + progress.setText("Testing camera connections...\nThis may take a few seconds.") + progress.setStandardButtons(QMessageBox.NoButton) + progress.show() + QApplication.processEvents() + + # Test each camera + results = [] + for cam_id in selected_cameras: + try: + if cam_id.startswith('net:'): + name = cam_id[4:] + camera_info = self.detector.network_cameras.get(name) + if isinstance(camera_info, dict): + url = camera_info['url'] + if 'username' in camera_info and 'password' in camera_info: + parsed = urllib.parse.urlparse(url) + netloc = f"{camera_info['username']}:{camera_info['password']}@{parsed.netloc}" + url = parsed._replace(netloc=netloc).geturl() + else: + url = camera_info + cap = cv2.VideoCapture(url) + else: + cap = cv2.VideoCapture(int(cam_id) if cam_id.isdigit() else cam_id) + + if cap.isOpened(): + ret, frame = cap.read() + if ret: + results.append(f"✅ {cam_id}: Connection successful") + else: + results.append(f"⚠️ {cam_id}: Connected but no frame received") + else: + results.append(f"❌ {cam_id}: Failed to connect") + cap.release() + except Exception as e: + results.append(f"❌ {cam_id}: Error - {str(e)}") + + progress.close() + + # Show results + result_dialog = QMessageBox(self) + result_dialog.setWindowTitle("Camera Test Results") + result_dialog.setText("\n".join(results)) + result_dialog.setIcon(QMessageBox.Information) + result_dialog.exec_() + + def show_network_dialog(self): + """Show the network camera configuration dialog""" + dialog = NetworkCameraDialog(self) + if dialog.exec_() == QDialog.Accepted: + self.refresh_cameras() class MainWindow(QMainWindow): def __init__(self): -- 2.49.1 From b80dd3f7d788ce39265cec39b3cda41734fdfd15 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 26 May 2025 16:41:58 +0200 Subject: [PATCH 4/6] dirty shitt --- mucapy/main.py | 217 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 145 insertions(+), 72 deletions(-) diff --git a/mucapy/main.py b/mucapy/main.py index 1eb852f..5e67bac 100644 --- a/mucapy/main.py +++ b/mucapy/main.py @@ -11,9 +11,10 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout QDockWidget, QScrollArea, QToolButton, QDialog, QShortcut, QListWidget, QFormLayout, QLineEdit, QCheckBox, QTabWidget, QListWidgetItem, QSplitter) -from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime, QRect +from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime, QRect, QThread, pyqtSignal, QMutex from PyQt5.QtGui import (QImage, QPixmap, QIcon, QColor, QKeySequence, QPainter, QPen, QBrush) +import time class Config: def __init__(self): @@ -61,9 +62,99 @@ class Config: """Load a setting from configuration""" return self.settings.get(key, default) +class CameraThread(QThread): + """Thread class for handling camera connections and frame grabbing""" + frame_ready = pyqtSignal(int, np.ndarray) # Signal to emit when new frame is ready (camera_index, frame) + error_occurred = pyqtSignal(int, str) # Signal to emit when error occurs (camera_index, error_message) + + def __init__(self, camera_id, camera_info, parent=None): + super().__init__(parent) + self.camera_id = camera_id + self.camera_info = camera_info + self.running = False + self.cap = None + self.mutex = QMutex() + self.frame_interval = 1.0 / 30 # Default to 30 FPS + + def set_fps(self, fps): + """Set the target FPS for frame capture""" + self.frame_interval = 1.0 / fps + + def run(self): + """Main thread loop""" + try: + # Connect to camera + if isinstance(self.camera_info, str) and self.camera_info.startswith('net:'): + name = self.camera_info[4:] + detector = self.parent().detector if self.parent() else None + if detector and name in detector.network_cameras: + camera_info = detector.network_cameras[name] + if isinstance(camera_info, dict): + url = camera_info['url'] + if 'username' in camera_info and 'password' in camera_info: + parsed = urllib.parse.urlparse(url) + netloc = f"{camera_info['username']}:{camera_info['password']}@{parsed.netloc}" + url = parsed._replace(netloc=netloc).geturl() + else: + url = camera_info + self.cap = cv2.VideoCapture(url) + else: + # Local camera + self.cap = cv2.VideoCapture(int(self.camera_info) if str(self.camera_info).isdigit() else self.camera_info) + + if not self.cap.isOpened(): + self.error_occurred.emit(self.camera_id, "Failed to open camera") + return + + self.running = True + last_frame_time = time.time() + + while self.running: + self.mutex.lock() + if not self.running: + self.mutex.unlock() + break + + # Check if enough time has passed since last frame + current_time = time.time() + if current_time - last_frame_time < self.frame_interval: + self.mutex.unlock() + time.sleep(0.001) # Small sleep to prevent CPU hogging + continue + + ret, frame = self.cap.read() + self.mutex.unlock() + + if ret: + self.frame_ready.emit(self.camera_id, frame) + last_frame_time = current_time + else: + self.error_occurred.emit(self.camera_id, "Failed to read frame") + break + + except Exception as e: + self.error_occurred.emit(self.camera_id, str(e)) + + finally: + self.cleanup() + + def stop(self): + """Stop the thread safely""" + self.mutex.lock() + self.running = False + self.mutex.unlock() + self.wait() + + def cleanup(self): + """Clean up camera resources""" + if self.cap: + self.cap.release() + self.running = False + class MultiCamYOLODetector: def __init__(self): self.cameras = [] + self.camera_threads = {} # Dictionary to store camera threads self.net = None self.classes = [] self.colors = [] @@ -74,6 +165,8 @@ class MultiCamYOLODetector: self.model_dir = "" self.cuda_available = self.check_cuda() self.config = Config() + self.latest_frames = {} # Store latest frames from each camera + self.frame_lock = QMutex() # Mutex for thread-safe frame access # Load settings self.confidence_threshold = self.config.load_setting('confidence_threshold', 0.35) @@ -190,66 +283,64 @@ class MultiCamYOLODetector: return False def connect_cameras(self, camera_paths): - """Connect to multiple cameras including network cameras with authentication""" + """Connect to multiple cameras using threads""" self.disconnect_cameras() + success = True - for cam_path in camera_paths: + for i, cam_path in enumerate(camera_paths): try: - if isinstance(cam_path, str): - if cam_path.startswith('net:'): - # Handle network camera - camera_name = cam_path[4:] # Remove 'net:' prefix to get camera name - camera_info = self.network_cameras.get(camera_name) - - if isinstance(camera_info, dict): - url = camera_info['url'] - # Add authentication to URL if provided - if 'username' in camera_info and 'password' in camera_info: - # Parse URL and add authentication - parsed = urllib.parse.urlparse(url) - netloc = f"{camera_info['username']}:{camera_info['password']}@{parsed.netloc}" - url = parsed._replace(netloc=netloc).geturl() - else: - # Handle old format where camera_info was just the URL - url = camera_info - - cap = cv2.VideoCapture(url) - elif cam_path.startswith('/dev/'): - # Handle device path - cap = cv2.VideoCapture(cam_path) - else: - # Handle numeric index - cap = cv2.VideoCapture(int(cam_path)) - else: - cap = cv2.VideoCapture(int(cam_path)) - - if not cap.isOpened(): - print(f"Warning: Could not open camera {cam_path}") - continue - - # Try to set properties but continue if they fail - try: - cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) - cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) - cap.set(cv2.CAP_PROP_FPS, self.target_fps) - cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) - except: - pass - - self.cameras.append((cam_path, cap)) + thread = CameraThread(i, cam_path) + thread.frame_ready.connect(self.handle_new_frame) + thread.error_occurred.connect(self.handle_camera_error) + thread.set_fps(self.target_fps) + self.camera_threads[i] = thread + self.cameras.append((cam_path, None)) # Store camera path for reference + thread.start() except Exception as e: - print(f"Error opening camera {cam_path}: {e}") + print(f"Error connecting to camera {cam_path}: {e}") + success = False - return len(self.cameras) > 0 + return success def disconnect_cameras(self): - """Disconnect all cameras""" - for _, cam in self.cameras: - try: - cam.release() - except: - pass - self.cameras = [] + """Disconnect all cameras and stop threads""" + for thread in self.camera_threads.values(): + thread.stop() + self.camera_threads.clear() + self.cameras.clear() + self.latest_frames.clear() + + def handle_new_frame(self, camera_index, frame): + """Handle new frame from camera thread""" + self.frame_lock.lock() + try: + # Apply YOLO detection + processed_frame = self.get_detections(frame) + self.latest_frames[camera_index] = processed_frame + finally: + self.frame_lock.unlock() + + def handle_camera_error(self, camera_index, error_message): + """Handle camera errors""" + print(f"Camera {camera_index} error: {error_message}") + # You might want to implement more sophisticated error handling here + + def get_frames(self): + """Get the latest frames from all cameras""" + self.frame_lock.lock() + try: + frames = [] + for i in range(len(self.cameras)): + frame = self.latest_frames.get(i) + if frame is None: + # Create blank frame if no frame is available + frame = np.zeros((720, 1280, 3), dtype=np.uint8) + cv2.putText(frame, "No Signal", (480, 360), + cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2) + frames.append(frame) + return frames + finally: + self.frame_lock.unlock() def get_detections(self, frame): """Perform YOLO object detection on a frame with error handling""" @@ -303,24 +394,6 @@ class MultiCamYOLODetector: print(f"Detection error: {e}") return frame - - def get_frames(self): - """Get frames from all cameras with error handling""" - frames = [] - for i, (cam_path, cam) in enumerate(self.cameras): - try: - ret, frame = cam.read() - if not ret: - print(f"Warning: Could not read frame from camera {cam_path}") - frame = np.zeros((720, 1280, 3), dtype=np.uint8) - else: - frame = self.get_detections(frame) - frames.append(frame) - except: - frame = np.zeros((720, 1280, 3), dtype=np.uint8) - frames.append(frame) - - return frames class CameraDisplay(QLabel): """Custom QLabel for displaying camera feed with fullscreen support""" -- 2.49.1 From 5399cb87393c9cf6651c1180070766b4418d5c3a Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 26 May 2025 16:47:00 +0200 Subject: [PATCH 5/6] crossplatform? --- .gitignore | 2 +- mucapy/main.py | 109 ++++++++++++++++++++++++++++++++++++--------- mucapy_config.json | 13 ++++++ 3 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 mucapy_config.json diff --git a/.gitignore b/.gitignore index 0698ebe..8b13789 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -mucapy_config.json + diff --git a/mucapy/main.py b/mucapy/main.py index 5e67bac..c2e2282 100644 --- a/mucapy/main.py +++ b/mucapy/main.py @@ -18,12 +18,22 @@ import time class Config: def __init__(self): - # Use JSON file in current working directory - self.config_file = os.path.join(os.getcwd(), 'mucapy_config.json') + # Use platform-specific user directory for config + if sys.platform.startswith('win'): + config_dir = os.path.join(os.environ.get('APPDATA', os.path.expanduser('~')), 'MuCaPy') + pictures_dir = os.path.join(os.environ.get('USERPROFILE', os.path.expanduser('~')), 'Pictures', 'MuCaPy') + else: + config_dir = os.path.join(os.path.expanduser('~'), '.config', 'mucapy') + pictures_dir = os.path.join(os.path.expanduser('~'), 'Pictures', 'MuCaPy') + + # Create config directory if it doesn't exist + os.makedirs(config_dir, exist_ok=True) + + self.config_file = os.path.join(config_dir, 'config.json') self.settings = { 'network_cameras': {}, # Store network cameras configuration 'last_model_dir': '', - 'last_screenshot_dir': os.path.expanduser('~/Pictures/MuCaPy'), + 'last_screenshot_dir': pictures_dir, 'last_layout': 0, 'last_fps': 10, 'last_selected_cameras': [], @@ -206,7 +216,7 @@ class MultiCamYOLODetector: """Check for available cameras including network cameras""" self.available_cameras = [] - # Try numeric indices first (this works more reliably) + # Try numeric indices first (this works on all platforms) for i in range(max_to_check): try: cap = cv2.VideoCapture(i) @@ -216,18 +226,19 @@ class MultiCamYOLODetector: except: continue - # Also check device paths as fallback - if os.path.exists('/dev'): - for i in range(max_to_check): - device_path = f"/dev/video{i}" - if os.path.exists(device_path) and device_path not in self.available_cameras: - try: - cap = cv2.VideoCapture(device_path) - if cap.isOpened(): - self.available_cameras.append(device_path) - cap.release() - except: - continue + # Platform-specific device path checks + if sys.platform.startswith('linux'): + if os.path.exists('/dev'): + for i in range(max_to_check): + device_path = f"/dev/video{i}" + if os.path.exists(device_path) and device_path not in self.available_cameras: + try: + cap = cv2.VideoCapture(device_path) + if cap.isOpened(): + self.available_cameras.append(device_path) + cap.release() + except: + continue # Add saved network cameras for name, camera_info in self.network_cameras.items(): @@ -476,20 +487,58 @@ class CameraDisplay(QLabel): self.fullscreen_window = QMainWindow(self.window()) self.fullscreen_window.setWindowTitle(f"Camera {self.cam_id} - Fullscreen") + self.fullscreen_window.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) # Create central widget central_widget = QWidget() layout = QVBoxLayout(central_widget) + layout.setContentsMargins(0, 0, 0, 0) + + # Create title bar + title_bar = QWidget() + title_bar.setStyleSheet(""" + QWidget { + background-color: #1E1E1E; + color: #DDD; + } + """) + title_bar.setFixedHeight(30) + title_layout = QHBoxLayout(title_bar) + title_layout.setContentsMargins(10, 0, 10, 0) + + # Add title label + title_label = QLabel(f"Camera {self.cam_id} - Fullscreen") + title_layout.addWidget(title_label) + + # Add close button + close_btn = QPushButton("×") + close_btn.setFixedSize(20, 20) + close_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + color: #DDD; + border: none; + font-size: 16px; + } + QPushButton:hover { + background-color: #E81123; + color: white; + } + """) + close_btn.clicked.connect(self.close_fullscreen) + title_layout.addWidget(close_btn) + + layout.addWidget(title_bar) # Create fullscreen label - label = QLabel() - label.setAlignment(Qt.AlignCenter) - label.setPixmap(self.pixmap().scaled( + self.fullscreen_label = QLabel() + self.fullscreen_label.setAlignment(Qt.AlignCenter) + self.fullscreen_label.setPixmap(self.pixmap().scaled( QApplication.primaryScreen().size(), Qt.KeepAspectRatio, Qt.SmoothTransformation )) - layout.addWidget(label) + layout.addWidget(self.fullscreen_label) self.fullscreen_window.setCentralWidget(central_widget) @@ -501,15 +550,31 @@ class CameraDisplay(QLabel): screenshot_shortcut = QShortcut(QKeySequence("Ctrl+S"), self.fullscreen_window) screenshot_shortcut.activated.connect(self.take_screenshot) + # Make window draggable + title_bar.mousePressEvent = self.fullscreen_mousePressEvent + title_bar.mouseMoveEvent = self.fullscreen_mouseMoveEvent + # Update fullscreen image when main window updates self.fullscreen_timer = QTimer() self.fullscreen_timer.timeout.connect( - lambda: self.update_fullscreen(label) + lambda: self.update_fullscreen(self.fullscreen_label) ) self.fullscreen_timer.start(30) # Show fullscreen - self.fullscreen_window.showFullScreen() + self.fullscreen_window.showMaximized() + + def fullscreen_mousePressEvent(self, event): + """Handle mouse press events for dragging""" + if event.button() == Qt.LeftButton: + self.fullscreen_window.drag_position = event.globalPos() - self.fullscreen_window.frameGeometry().topLeft() + event.accept() + + def fullscreen_mouseMoveEvent(self, event): + """Handle mouse move events for dragging""" + if hasattr(self.fullscreen_window, 'drag_position'): + self.fullscreen_window.move(event.globalPos() - self.fullscreen_window.drag_position) + event.accept() def update_fullscreen(self, label): """Update the fullscreen display""" diff --git a/mucapy_config.json b/mucapy_config.json new file mode 100644 index 0000000..6317a28 --- /dev/null +++ b/mucapy_config.json @@ -0,0 +1,13 @@ +{ + "network_cameras": {}, + "last_model_dir": "", + "last_screenshot_dir": "/home/rattatwinko/Pictures/MuCaPy", + "last_layout": 0, + "last_fps": 10, + "last_selected_cameras": [], + "window_geometry": null, + "confidence_threshold": 0.35, + "model_dir": "/home/rattatwinko/Documents/mucapy/mucapy/mucapy/models", + "fps": 10, + "layout": 0 +} \ No newline at end of file -- 2.49.1 From 65be01870083b1b0b9791b78417447425ba916a5 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 26 May 2025 16:47:53 +0200 Subject: [PATCH 6/6] fixed the stupid ass fswindow --- mucapy/main.py | 69 +++++++------------------------------------------- 1 file changed, 9 insertions(+), 60 deletions(-) diff --git a/mucapy/main.py b/mucapy/main.py index c2e2282..fe9da44 100644 --- a/mucapy/main.py +++ b/mucapy/main.py @@ -481,60 +481,23 @@ class CameraDisplay(QLabel): self.close_fullscreen() def show_fullscreen(self): - """Show this camera in fullscreen mode""" + """Show this camera in a new window""" if not self.pixmap(): return self.fullscreen_window = QMainWindow(self.window()) - self.fullscreen_window.setWindowTitle(f"Camera {self.cam_id} - Fullscreen") - self.fullscreen_window.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) + self.fullscreen_window.setWindowTitle(f"Camera {self.cam_id}") # Create central widget central_widget = QWidget() layout = QVBoxLayout(central_widget) - layout.setContentsMargins(0, 0, 0, 0) - - # Create title bar - title_bar = QWidget() - title_bar.setStyleSheet(""" - QWidget { - background-color: #1E1E1E; - color: #DDD; - } - """) - title_bar.setFixedHeight(30) - title_layout = QHBoxLayout(title_bar) - title_layout.setContentsMargins(10, 0, 10, 0) - - # Add title label - title_label = QLabel(f"Camera {self.cam_id} - Fullscreen") - title_layout.addWidget(title_label) - - # Add close button - close_btn = QPushButton("×") - close_btn.setFixedSize(20, 20) - close_btn.setStyleSheet(""" - QPushButton { - background-color: transparent; - color: #DDD; - border: none; - font-size: 16px; - } - QPushButton:hover { - background-color: #E81123; - color: white; - } - """) - close_btn.clicked.connect(self.close_fullscreen) - title_layout.addWidget(close_btn) - - layout.addWidget(title_bar) # Create fullscreen label self.fullscreen_label = QLabel() self.fullscreen_label.setAlignment(Qt.AlignCenter) + self.fullscreen_label.setMinimumSize(640, 480) # Set minimum size self.fullscreen_label.setPixmap(self.pixmap().scaled( - QApplication.primaryScreen().size(), + QSize(1280, 720), # Default HD size Qt.KeepAspectRatio, Qt.SmoothTransformation )) @@ -542,7 +505,7 @@ class CameraDisplay(QLabel): self.fullscreen_window.setCentralWidget(central_widget) - # Add ESC shortcut + # Add ESC shortcut to close shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self.fullscreen_window) shortcut.activated.connect(self.close_fullscreen) @@ -550,10 +513,6 @@ class CameraDisplay(QLabel): screenshot_shortcut = QShortcut(QKeySequence("Ctrl+S"), self.fullscreen_window) screenshot_shortcut.activated.connect(self.take_screenshot) - # Make window draggable - title_bar.mousePressEvent = self.fullscreen_mousePressEvent - title_bar.mouseMoveEvent = self.fullscreen_mouseMoveEvent - # Update fullscreen image when main window updates self.fullscreen_timer = QTimer() self.fullscreen_timer.timeout.connect( @@ -561,20 +520,10 @@ class CameraDisplay(QLabel): ) self.fullscreen_timer.start(30) - # Show fullscreen - self.fullscreen_window.showMaximized() - - def fullscreen_mousePressEvent(self, event): - """Handle mouse press events for dragging""" - if event.button() == Qt.LeftButton: - self.fullscreen_window.drag_position = event.globalPos() - self.fullscreen_window.frameGeometry().topLeft() - event.accept() - - def fullscreen_mouseMoveEvent(self, event): - """Handle mouse move events for dragging""" - if hasattr(self.fullscreen_window, 'drag_position'): - self.fullscreen_window.move(event.globalPos() - self.fullscreen_window.drag_position) - event.accept() + # Set window size and show + screen = QApplication.primaryScreen().availableGeometry() + self.fullscreen_window.resize(min(1280, screen.width() * 0.8), min(720, screen.height() * 0.8)) + self.fullscreen_window.show() def update_fullscreen(self, label): """Update the fullscreen display""" -- 2.49.1