better bullshit
This commit is contained in:
419
mucapy/main.py
419
mucapy/main.py
@@ -2,15 +2,18 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import cv2
|
import cv2
|
||||||
import json
|
import json
|
||||||
|
import urllib.parse
|
||||||
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,
|
||||||
QShortcut, QListWidget, QFormLayout, QLineEdit)
|
QShortcut, QListWidget, QFormLayout, QLineEdit,
|
||||||
from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime
|
QCheckBox, QTabWidget, QListWidgetItem)
|
||||||
from PyQt5.QtGui import QImage, QPixmap, QIcon, QColor, QKeySequence
|
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:
|
class Config:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -72,9 +75,9 @@ class MultiCamYOLODetector:
|
|||||||
except:
|
except:
|
||||||
return False
|
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"""
|
"""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)
|
self.config.save_setting('network_cameras', self.network_cameras)
|
||||||
|
|
||||||
def remove_network_camera(self, name):
|
def remove_network_camera(self, name):
|
||||||
@@ -87,34 +90,37 @@ class MultiCamYOLODetector:
|
|||||||
"""Check for available cameras including network cameras"""
|
"""Check for available cameras including network cameras"""
|
||||||
self.available_cameras = []
|
self.available_cameras = []
|
||||||
|
|
||||||
# Check standard video devices
|
# Try numeric indices first (this works more reliably)
|
||||||
for i in range(max_to_check):
|
for i in range(max_to_check):
|
||||||
try:
|
try:
|
||||||
cap = cv2.VideoCapture(i, cv2.CAP_V4L2)
|
cap = cv2.VideoCapture(i)
|
||||||
if cap.isOpened():
|
if cap.isOpened():
|
||||||
self.available_cameras.append(str(i))
|
self.available_cameras.append(str(i))
|
||||||
cap.release()
|
cap.release()
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check direct device paths
|
# Also check device paths as fallback
|
||||||
v4l_paths = [f"/dev/video{i}" for i in range(max_to_check)]
|
if os.path.exists('/dev'):
|
||||||
v4l_paths += [f"/dev/v4l/video{i}" for i in range(max_to_check)]
|
for i in range(max_to_check):
|
||||||
|
device_path = f"/dev/video{i}"
|
||||||
for path in v4l_paths:
|
if os.path.exists(device_path) and device_path not in self.available_cameras:
|
||||||
if os.path.exists(path):
|
try:
|
||||||
try:
|
cap = cv2.VideoCapture(device_path)
|
||||||
cap = cv2.VideoCapture(path, cv2.CAP_V4L2)
|
if cap.isOpened():
|
||||||
if cap.isOpened():
|
self.available_cameras.append(device_path)
|
||||||
if path not in self.available_cameras:
|
|
||||||
self.available_cameras.append(path)
|
|
||||||
cap.release()
|
cap.release()
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Add saved network cameras
|
# Add saved network cameras
|
||||||
for name, url in self.network_cameras.items():
|
for name, camera_info in self.network_cameras.items():
|
||||||
self.available_cameras.append(f"net:{url}")
|
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
|
return self.available_cameras
|
||||||
|
|
||||||
@@ -161,7 +167,7 @@ class MultiCamYOLODetector:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def connect_cameras(self, camera_paths):
|
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()
|
self.disconnect_cameras()
|
||||||
|
|
||||||
for cam_path in camera_paths:
|
for cam_path in camera_paths:
|
||||||
@@ -169,16 +175,30 @@ class MultiCamYOLODetector:
|
|||||||
if isinstance(cam_path, str):
|
if isinstance(cam_path, str):
|
||||||
if cam_path.startswith('net:'):
|
if cam_path.startswith('net:'):
|
||||||
# Handle network camera
|
# 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)
|
cap = cv2.VideoCapture(url)
|
||||||
elif cam_path.startswith('/dev/'):
|
elif cam_path.startswith('/dev/'):
|
||||||
# Handle device path
|
# Handle device path
|
||||||
cap = cv2.VideoCapture(cam_path, cv2.CAP_V4L2)
|
cap = cv2.VideoCapture(cam_path)
|
||||||
else:
|
else:
|
||||||
# Handle numeric index
|
# Handle numeric index
|
||||||
cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2)
|
cap = cv2.VideoCapture(int(cam_path))
|
||||||
else:
|
else:
|
||||||
cap = cv2.VideoCapture(int(cam_path), cv2.CAP_V4L2)
|
cap = cv2.VideoCapture(int(cam_path))
|
||||||
|
|
||||||
if not cap.isOpened():
|
if not cap.isOpened():
|
||||||
print(f"Warning: Could not open camera {cam_path}")
|
print(f"Warning: Could not open camera {cam_path}")
|
||||||
@@ -299,6 +319,7 @@ class CameraDisplay(QLabel):
|
|||||||
self.fullscreen_timer = None
|
self.fullscreen_timer = None
|
||||||
self.config = Config()
|
self.config = Config()
|
||||||
self.screenshot_dir = self.config.load_setting('screenshot_dir', os.path.expanduser('~/Pictures/MuCaPy'))
|
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
|
# Create screenshot directory if it doesn't exist
|
||||||
if not os.path.exists(self.screenshot_dir):
|
if not os.path.exists(self.screenshot_dir):
|
||||||
@@ -308,6 +329,11 @@ class CameraDisplay(QLabel):
|
|||||||
"""Set camera identifier for this display"""
|
"""Set camera identifier for this display"""
|
||||||
self.cam_id = cam_id
|
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):
|
def take_screenshot(self):
|
||||||
"""Take a screenshot of the current frame"""
|
"""Take a screenshot of the current frame"""
|
||||||
if not self.pixmap():
|
if not self.pixmap():
|
||||||
@@ -406,6 +432,23 @@ class CameraDisplay(QLabel):
|
|||||||
self.fullscreen_window.close()
|
self.fullscreen_window.close()
|
||||||
self.fullscreen_window = None
|
self.fullscreen_window = None
|
||||||
self.fullscreen_timer = 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):
|
class CollapsibleDock(QDockWidget):
|
||||||
"""Custom dock widget with collapse/expand functionality"""
|
"""Custom dock widget with collapse/expand functionality"""
|
||||||
@@ -539,7 +582,7 @@ class NetworkCameraDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Network Camera Settings")
|
self.setWindowTitle("Network Camera Settings")
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
self.resize(400, 300)
|
self.resize(500, 400)
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
@@ -548,7 +591,8 @@ class NetworkCameraDialog(QDialog):
|
|||||||
"Enter network camera details:\n"
|
"Enter network camera details:\n"
|
||||||
"- For DroidCam: Use the IP and port shown in the app\n"
|
"- For DroidCam: Use the IP and port shown in the app\n"
|
||||||
" Example: http://192.168.1.100:4747/video\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)
|
instructions.setWordWrap(True)
|
||||||
layout.addWidget(instructions)
|
layout.addWidget(instructions)
|
||||||
@@ -559,12 +603,38 @@ class NetworkCameraDialog(QDialog):
|
|||||||
|
|
||||||
# Input fields
|
# Input fields
|
||||||
form_layout = QFormLayout()
|
form_layout = QFormLayout()
|
||||||
|
|
||||||
|
# Name and URL
|
||||||
self.name_edit = QLineEdit()
|
self.name_edit = QLineEdit()
|
||||||
self.url_edit = QLineEdit()
|
self.url_edit = QLineEdit()
|
||||||
form_layout.addRow("Name:", self.name_edit)
|
form_layout.addRow("Name:", self.name_edit)
|
||||||
form_layout.addRow("URL:", self.url_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)
|
layout.addLayout(form_layout)
|
||||||
|
|
||||||
|
# Initially disable auth fields
|
||||||
|
self.username_edit.setEnabled(False)
|
||||||
|
self.password_edit.setEnabled(False)
|
||||||
|
|
||||||
# Buttons
|
# Buttons
|
||||||
btn_layout = QHBoxLayout()
|
btn_layout = QHBoxLayout()
|
||||||
add_btn = QPushButton("Add Camera")
|
add_btn = QPushButton("Add Camera")
|
||||||
@@ -582,14 +652,32 @@ class NetworkCameraDialog(QDialog):
|
|||||||
self.detector = parent.detector if parent else None
|
self.detector = parent.detector if parent else None
|
||||||
self.load_cameras()
|
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):
|
def load_cameras(self):
|
||||||
"""Load saved network cameras into the list"""
|
"""Load saved network cameras into the list"""
|
||||||
if not self.detector:
|
if not self.detector:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.camera_list.clear()
|
self.camera_list.clear()
|
||||||
for name, url in self.detector.network_cameras.items():
|
for name, camera_info in self.detector.network_cameras.items():
|
||||||
self.camera_list.addItem(f"{name} ({url})")
|
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):
|
def add_camera(self):
|
||||||
"""Add a new network camera"""
|
"""Add a new network camera"""
|
||||||
@@ -601,10 +689,25 @@ class NetworkCameraDialog(QDialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.detector:
|
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()
|
self.load_cameras()
|
||||||
|
|
||||||
|
# Clear fields
|
||||||
self.name_edit.clear()
|
self.name_edit.clear()
|
||||||
self.url_edit.clear()
|
self.url_edit.clear()
|
||||||
|
self.username_edit.clear()
|
||||||
|
self.password_edit.clear()
|
||||||
|
self.auth_checkbox.setChecked(False)
|
||||||
|
|
||||||
def remove_camera(self):
|
def remove_camera(self):
|
||||||
"""Remove selected network camera"""
|
"""Remove selected network camera"""
|
||||||
@@ -617,6 +720,197 @@ class NetworkCameraDialog(QDialog):
|
|||||||
self.detector.remove_network_camera(name)
|
self.detector.remove_network_camera(name)
|
||||||
self.load_cameras()
|
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):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -773,11 +1067,15 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Camera menu
|
# Camera menu
|
||||||
self.camera_menu = menubar.addMenu('Cameras')
|
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 = QAction('Network Camera Settings...', self)
|
||||||
network_camera_action.triggered.connect(self.show_network_camera_dialog)
|
network_camera_action.triggered.connect(self.show_network_camera_dialog)
|
||||||
self.camera_menu.addAction(network_camera_action)
|
self.camera_menu.addAction(network_camera_action)
|
||||||
@@ -790,19 +1088,22 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
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 and network camera settings)
|
||||||
for action in self.camera_menu.actions()[2:]:
|
for action in self.camera_menu.actions()[3:]:
|
||||||
self.camera_menu.removeAction(action)
|
self.camera_menu.removeAction(action)
|
||||||
|
|
||||||
available_cams = self.detector.scan_for_cameras()
|
available_cams = self.detector.scan_for_cameras()
|
||||||
for cam_path in available_cams:
|
for cam_path in available_cams:
|
||||||
# Display friendly name
|
# Display friendly name
|
||||||
if cam_path.startswith('/dev/'):
|
if cam_path.startswith('net:'):
|
||||||
cam_name = os.path.basename(cam_path)
|
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:
|
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)
|
action.setData(cam_path)
|
||||||
self.camera_action_group.addAction(action)
|
self.camera_action_group.addAction(action)
|
||||||
self.camera_menu.addAction(action)
|
self.camera_menu.addAction(action)
|
||||||
@@ -1060,7 +1361,7 @@ class MainWindow(QMainWindow):
|
|||||||
"""Update the camera feeds in the display"""
|
"""Update the camera feeds in the display"""
|
||||||
frames = self.detector.get_frames()
|
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):
|
if i >= len(self.camera_displays):
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -1075,6 +1376,20 @@ class MainWindow(QMainWindow):
|
|||||||
display = self.camera_displays[i]
|
display = self.camera_displays[i]
|
||||||
display.setPixmap(pixmap.scaled(display.width(), display.height(),
|
display.setPixmap(pixmap.scaled(display.width(), display.height(),
|
||||||
Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
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):
|
def take_screenshot(self):
|
||||||
"""Take screenshot of active camera displays"""
|
"""Take screenshot of active camera displays"""
|
||||||
@@ -1097,6 +1412,24 @@ class MainWindow(QMainWindow):
|
|||||||
# Refresh camera list after dialog closes
|
# Refresh camera list after dialog closes
|
||||||
self.populate_camera_menu()
|
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):
|
def closeEvent(self, event):
|
||||||
"""Handle window close event"""
|
"""Handle window close event"""
|
||||||
self.stop_detection()
|
self.stop_detection()
|
||||||
|
|||||||
Reference in New Issue
Block a user