From c264acac29522a854eefe2d1c372f122170a60b0 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 26 May 2025 16:39:58 +0200 Subject: [PATCH] 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):