better bullshit

This commit is contained in:
rattatwinko
2025-05-26 16:18:25 +02:00
parent 2ebaf16006
commit b24426c1d3

View File

@@ -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():
@@ -407,6 +433,23 @@ class CameraDisplay(QLabel):
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"""
def __init__(self, title, parent=None):
@@ -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
@@ -1076,6 +1377,20 @@ class MainWindow(QMainWindow):
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"""
active_displays = [d for d in self.camera_displays if d.isVisible() and d.pixmap()]
@@ -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()