working really nicely
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
mucapy_config.json
|
||||||
310
mucapy/main.py
310
mucapy/main.py
@@ -1,14 +1,50 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import cv2
|
import cv2
|
||||||
|
import json
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
|
||||||
QWidget, QLabel, QPushButton, QComboBox, QSpinBox,
|
QWidget, QLabel, QPushButton, QComboBox, QSpinBox,
|
||||||
QFileDialog, QMessageBox, QMenu, QAction, QMenuBar,
|
QFileDialog, QMessageBox, QMenu, QAction, QMenuBar,
|
||||||
QActionGroup, QSizePolicy, QGridLayout, QGroupBox,
|
QActionGroup, QSizePolicy, QGridLayout, QGroupBox,
|
||||||
QDockWidget, QScrollArea, QToolButton, QDialog)
|
QDockWidget, QScrollArea, QToolButton, QDialog,
|
||||||
from PyQt5.QtCore import Qt, QTimer, QDir, QSize
|
QShortcut, QListWidget, QFormLayout, QLineEdit)
|
||||||
from PyQt5.QtGui import QImage, QPixmap, QIcon, QColor
|
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:
|
class MultiCamYOLODetector:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -22,6 +58,11 @@ class MultiCamYOLODetector:
|
|||||||
self.available_cameras = []
|
self.available_cameras = []
|
||||||
self.model_dir = ""
|
self.model_dir = ""
|
||||||
self.cuda_available = self.check_cuda()
|
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):
|
def check_cuda(self):
|
||||||
"""Check if CUDA is available"""
|
"""Check if CUDA is available"""
|
||||||
@@ -31,8 +72,19 @@ class MultiCamYOLODetector:
|
|||||||
except:
|
except:
|
||||||
return False
|
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):
|
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 = []
|
self.available_cameras = []
|
||||||
|
|
||||||
# Check standard video devices
|
# Check standard video devices
|
||||||
@@ -45,7 +97,7 @@ class MultiCamYOLODetector:
|
|||||||
except:
|
except:
|
||||||
continue
|
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/video{i}" for i in range(max_to_check)]
|
||||||
v4l_paths += [f"/dev/v4l/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:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Add saved network cameras
|
||||||
|
for name, url in self.network_cameras.items():
|
||||||
|
self.available_cameras.append(f"net:{url}")
|
||||||
|
|
||||||
return self.available_cameras
|
return self.available_cameras
|
||||||
|
|
||||||
def load_yolo_model(self, model_dir):
|
def load_yolo_model(self, model_dir):
|
||||||
@@ -105,13 +161,22 @@ class MultiCamYOLODetector:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def connect_cameras(self, camera_paths):
|
def connect_cameras(self, camera_paths):
|
||||||
"""Connect to multiple cameras with better error handling"""
|
"""Connect to multiple cameras including network cameras"""
|
||||||
self.disconnect_cameras()
|
self.disconnect_cameras()
|
||||||
|
|
||||||
for cam_path in camera_paths:
|
for cam_path in camera_paths:
|
||||||
try:
|
try:
|
||||||
if isinstance(cam_path, str) and cam_path.startswith('/dev/'):
|
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)
|
cap = cv2.VideoCapture(cam_path, cv2.CAP_V4L2)
|
||||||
|
else:
|
||||||
|
# Handle numeric index
|
||||||
|
cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2)
|
||||||
else:
|
else:
|
||||||
cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2)
|
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_WIDTH, 1280)
|
||||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||||||
cap.set(cv2.CAP_PROP_FPS, self.target_fps)
|
cap.set(cv2.CAP_PROP_FPS, self.target_fps)
|
||||||
cap.set(cv2.CAP_PROP_AUTOFOCUS, 0)
|
|
||||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
|
cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -172,7 +236,7 @@ class MultiCamYOLODetector:
|
|||||||
class_id = np.argmax(scores)
|
class_id = np.argmax(scores)
|
||||||
confidence = scores[class_id]
|
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],
|
box = detection[0:4] * np.array([frame.shape[1], frame.shape[0],
|
||||||
frame.shape[1], frame.shape[0]])
|
frame.shape[1], frame.shape[0]])
|
||||||
(centerX, centerY, width, height) = box.astype('int')
|
(centerX, centerY, width, height) = box.astype('int')
|
||||||
@@ -183,7 +247,7 @@ class MultiCamYOLODetector:
|
|||||||
confidences.append(float(confidence))
|
confidences.append(float(confidence))
|
||||||
class_ids.append(class_id)
|
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:
|
if len(indices) > 0:
|
||||||
for i in indices.flatten():
|
for i in indices.flatten():
|
||||||
@@ -233,11 +297,49 @@ class CameraDisplay(QLabel):
|
|||||||
self.fullscreen_window = None
|
self.fullscreen_window = None
|
||||||
self.cam_id = None
|
self.cam_id = None
|
||||||
self.fullscreen_timer = 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):
|
def set_cam_id(self, cam_id):
|
||||||
"""Set camera identifier for this display"""
|
"""Set camera identifier for this display"""
|
||||||
self.cam_id = cam_id
|
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):
|
def mouseDoubleClickEvent(self, event):
|
||||||
"""Handle double click to toggle fullscreen"""
|
"""Handle double click to toggle fullscreen"""
|
||||||
if self.pixmap() and not self.fullscreen_window:
|
if self.pixmap() and not self.fullscreen_window:
|
||||||
@@ -247,23 +349,35 @@ class CameraDisplay(QLabel):
|
|||||||
|
|
||||||
def show_fullscreen(self):
|
def show_fullscreen(self):
|
||||||
"""Show this camera in fullscreen mode"""
|
"""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")
|
self.fullscreen_window.setWindowTitle(f"Camera {self.cam_id} - Fullscreen")
|
||||||
|
|
||||||
screen = QApplication.primaryScreen().availableGeometry()
|
# Create central widget
|
||||||
self.fullscreen_window.resize(int(screen.width() * 0.9), int(screen.height() * 0.9))
|
central_widget = QWidget()
|
||||||
|
layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# Create fullscreen label
|
||||||
label = QLabel()
|
label = QLabel()
|
||||||
label.setAlignment(Qt.AlignCenter)
|
label.setAlignment(Qt.AlignCenter)
|
||||||
if self.pixmap():
|
|
||||||
label.setPixmap(self.pixmap().scaled(
|
label.setPixmap(self.pixmap().scaled(
|
||||||
self.fullscreen_window.size(),
|
QApplication.primaryScreen().size(),
|
||||||
Qt.KeepAspectRatio,
|
Qt.KeepAspectRatio,
|
||||||
Qt.SmoothTransformation
|
Qt.SmoothTransformation
|
||||||
))
|
))
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
self.fullscreen_window.setCentralWidget(label)
|
self.fullscreen_window.setCentralWidget(central_widget)
|
||||||
self.fullscreen_window.showFullScreen()
|
|
||||||
|
# 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
|
# Update fullscreen image when main window updates
|
||||||
self.fullscreen_timer = QTimer()
|
self.fullscreen_timer = QTimer()
|
||||||
@@ -272,6 +386,9 @@ class CameraDisplay(QLabel):
|
|||||||
)
|
)
|
||||||
self.fullscreen_timer.start(30)
|
self.fullscreen_timer.start(30)
|
||||||
|
|
||||||
|
# Show fullscreen
|
||||||
|
self.fullscreen_window.showFullScreen()
|
||||||
|
|
||||||
def update_fullscreen(self, label):
|
def update_fullscreen(self, label):
|
||||||
"""Update the fullscreen display"""
|
"""Update the fullscreen display"""
|
||||||
if self.pixmap():
|
if self.pixmap():
|
||||||
@@ -416,12 +533,99 @@ class AboutWindow(QDialog):
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
self.setLayout(layout)
|
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):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("Multi-Camera YOLO Detection")
|
self.setWindowTitle("Multi-Camera YOLO Detection")
|
||||||
self.setGeometry(100, 100, 1200, 800)
|
self.setGeometry(100, 100, 1200, 800)
|
||||||
|
|
||||||
|
# Initialize configuration
|
||||||
|
self.config = Config()
|
||||||
|
|
||||||
# Set dark theme style
|
# Set dark theme style
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QMainWindow, QWidget {
|
QMainWindow, QWidget {
|
||||||
@@ -523,13 +727,34 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.detector = MultiCamYOLODetector()
|
self.detector = MultiCamYOLODetector()
|
||||||
self.camera_settings = {}
|
self.camera_settings = {}
|
||||||
|
|
||||||
|
# Load saved settings
|
||||||
|
self.load_saved_settings()
|
||||||
|
|
||||||
self.create_menus()
|
self.create_menus()
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
self.init_timer()
|
self.init_timer()
|
||||||
|
|
||||||
def show_menu(self):
|
def load_saved_settings(self):
|
||||||
about = AboutWindow(self) # Pass self as parent
|
"""Load saved settings from configuration"""
|
||||||
about.exec_() # Use exec_() for modal dialog
|
# 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):
|
def create_menus(self):
|
||||||
menubar = self.menuBar()
|
menubar = self.menuBar()
|
||||||
@@ -542,7 +767,6 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Model menu
|
# Model menu
|
||||||
model_menu = menubar.addMenu('Model')
|
model_menu = menubar.addMenu('Model')
|
||||||
|
|
||||||
load_model_action = QAction('Load Model Directory...', self)
|
load_model_action = QAction('Load Model Directory...', self)
|
||||||
load_model_action.triggered.connect(self.load_model_directory)
|
load_model_action.triggered.connect(self.load_model_directory)
|
||||||
model_menu.addAction(load_model_action)
|
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 = QAction('Refresh Camera List', self)
|
||||||
self.refresh_cameras_action.triggered.connect(self.populate_camera_menu)
|
self.refresh_cameras_action.triggered.connect(self.populate_camera_menu)
|
||||||
self.camera_menu.addAction(self.refresh_cameras_action)
|
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_menu.addSeparator()
|
||||||
|
|
||||||
self.camera_action_group = QActionGroup(self)
|
self.camera_action_group = QActionGroup(self)
|
||||||
self.camera_action_group.setExclusive(False)
|
self.camera_action_group.setExclusive(False)
|
||||||
self.populate_camera_menu()
|
self.populate_camera_menu()
|
||||||
|
|
||||||
|
|
||||||
def populate_camera_menu(self):
|
def populate_camera_menu(self):
|
||||||
"""Populate the camera menu with available cameras"""
|
"""Populate the camera menu with available cameras"""
|
||||||
# Clear existing camera actions (except refresh)
|
# Clear existing camera actions (except refresh)
|
||||||
@@ -585,16 +814,18 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def load_model_directory(self):
|
def load_model_directory(self):
|
||||||
"""Open file dialog to select model directory"""
|
"""Open file dialog to select model directory"""
|
||||||
|
last_dir = self.config.load_setting('model_dir', QDir.homePath())
|
||||||
model_dir = QFileDialog.getExistingDirectory(
|
model_dir = QFileDialog.getExistingDirectory(
|
||||||
self,
|
self,
|
||||||
"Select Model Directory",
|
"Select Model Directory",
|
||||||
QDir.homePath(),
|
last_dir,
|
||||||
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
|
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
|
||||||
)
|
)
|
||||||
|
|
||||||
if model_dir:
|
if model_dir:
|
||||||
if self.detector.load_yolo_model(model_dir):
|
if self.detector.load_yolo_model(model_dir):
|
||||||
self.model_label.setText(f"Model: {os.path.basename(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!")
|
QMessageBox.information(self, "Success", "Model loaded successfully!")
|
||||||
else:
|
else:
|
||||||
QMessageBox.critical(self, "Error", "Failed to load model from selected directory")
|
QMessageBox.critical(self, "Error", "Failed to load model from selected directory")
|
||||||
@@ -662,6 +893,11 @@ class MainWindow(QMainWindow):
|
|||||||
layout_layout.addWidget(self.layout_combo)
|
layout_layout.addWidget(self.layout_combo)
|
||||||
settings_layout.addLayout(layout_layout)
|
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)
|
settings_group.setLayout(settings_layout)
|
||||||
sidebar_layout.addWidget(settings_group)
|
sidebar_layout.addWidget(settings_group)
|
||||||
|
|
||||||
@@ -713,6 +949,12 @@ class MainWindow(QMainWindow):
|
|||||||
# Start with sidebar expanded
|
# Start with sidebar expanded
|
||||||
self.sidebar.expand()
|
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):
|
def change_camera_layout(self, index):
|
||||||
"""Change the camera display layout"""
|
"""Change the camera display layout"""
|
||||||
# Clear the layout
|
# Clear the layout
|
||||||
@@ -834,9 +1076,31 @@ class MainWindow(QMainWindow):
|
|||||||
display.setPixmap(pixmap.scaled(display.width(), display.height(),
|
display.setPixmap(pixmap.scaled(display.width(), display.height(),
|
||||||
Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
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):
|
def closeEvent(self, event):
|
||||||
"""Handle window close event"""
|
"""Handle window close event"""
|
||||||
self.stop_detection()
|
self.stop_detection()
|
||||||
|
self.save_settings()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user