Compare commits
16 Commits
frontend
...
08431ee6ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08431ee6ca | ||
|
|
7dcf03970e | ||
|
|
44f2797a5c | ||
|
|
98ad6242fa | ||
|
|
b3b3d77394 | ||
|
|
e50ab9d03c | ||
|
|
c9c60f7237 | ||
|
|
de156a5c33 | ||
|
|
e4ec7fe244 | ||
|
|
1546528550 | ||
|
|
e7b4cc0f92 | ||
|
|
199d81891f | ||
|
|
ea1f0bcd85 | ||
|
|
9ae940585a | ||
|
|
78403395c1 | ||
|
|
b313fc7629 |
42
.gitea/workflows/run-mucapy.yml
Normal file
42
.gitea/workflows/run-mucapy.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Build MuCaPy Executable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-and-package:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Install PyInstaller
|
||||
run: pip install pyinstaller
|
||||
|
||||
- name: Build executable with PyInstaller
|
||||
run: |
|
||||
pyinstaller --onefile --windowed mucapy/main.py \
|
||||
--add-data "mucapy/styling:styling" \
|
||||
--add-data "mucapy/models:models"
|
||||
|
||||
|
||||
- name: Upload executable artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mucapy-executable
|
||||
path: dist/
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1 +1,10 @@
|
||||
|
||||
mucapy/seperate/__pycache__/__init__.cpython-313.pyc
|
||||
mucapy/seperate/__pycache__/AboutWindow.cpython-313.pyc
|
||||
mucapy/seperate/__pycache__/CameraDisplay.cpython-313.pyc
|
||||
mucapy/seperate/__pycache__/CollapsibleDock.cpython-313.pyc
|
||||
mucapy/seperate/__pycache__/Config.cpython-313.pyc
|
||||
mucapy/seperate/__pycache__/main.cpython-313.pyc
|
||||
mucapy/seperate/__pycache__/MainWindow.cpython-313.pyc
|
||||
mucapy/seperate/__pycache__/MultiCamYOLODetector.cpython-313.pyc
|
||||
mucapy/seperate/__pycache__/NetworkCameraDialog.cpython-313.pyc
|
||||
|
||||
41
LICENSE
Normal file
41
LICENSE
Normal file
@@ -0,0 +1,41 @@
|
||||
MuCaPy Proprietary Software License
|
||||
Version 1.0 — May 30, 2025
|
||||
|
||||
Copyright © 2025 Rattatwinko
|
||||
All Rights Reserved.
|
||||
|
||||
This software, including all source code, binaries, documentation, and associated files (collectively, the "Software"), is the exclusive property of the copyright holder.
|
||||
|
||||
The Software is proprietary and confidential. It is licensed, not sold, solely for internal, non-commercial use by the copyright holder or explicitly authorized individuals.
|
||||
1. License Grant
|
||||
|
||||
The copyright holder does not grant any rights to use, copy, modify, distribute, sublicense, or create derivative works from the Software except as explicitly authorized in writing.
|
||||
2. Restrictions
|
||||
|
||||
You shall not, under any circumstances:
|
||||
|
||||
Redistribute the Software in any form (source or binary).
|
||||
|
||||
Publish, transmit, upload, or make the Software publicly available.
|
||||
|
||||
Reverse engineer, decompile, or disassemble the Software.
|
||||
|
||||
Use the Software for any commercial or revenue-generating purpose.
|
||||
|
||||
Share the Software with third parties, contractors, or external collaborators.
|
||||
|
||||
Access to the Software must remain confined to private, secure environments under the control of the copyright holder.
|
||||
3. Ownership
|
||||
|
||||
All title, ownership rights, and intellectual property rights in and to the Software remain solely with the copyright holder.
|
||||
4. Termination
|
||||
|
||||
Any unauthorized use of the Software automatically terminates any implied rights and may result in legal action.
|
||||
5. Disclaimer of Warranty
|
||||
|
||||
The Software is provided "AS IS" without warranty of any kind, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, or non-infringement.
|
||||
6. Limitation of Liability
|
||||
|
||||
In no event shall the copyright holder be liable for any damages, including direct, indirect, incidental, special, or consequential damages arising out of or related to the use or inability to use the Software.
|
||||
|
||||
This license is not OSI-approved and must not be interpreted as open-source. For internal use only.
|
||||
169
README.md
Normal file
169
README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# MuCaPy: Multi-Camera Python🎥🧠
|
||||
|
||||
[](https://www.python.org/)
|
||||
[]()
|
||||
[](https://opencv.org/)
|
||||
[](https://riverbankcomputing.com/software/pyqt/)
|
||||
[]()
|
||||
|
||||
---
|
||||
|
||||
## 📌 Overview
|
||||
|
||||
**MuCaPy** (Multi-Camera Python) is a modern, robust, and user-friendly real-time multi-camera object detection platform powered by **YOLOv4** and **OpenCV**, designed with a professional PyQt5 GUI. It supports both **local** and **network IP cameras**, allows dynamic camera selection, advanced configuration, and comes with a beautiful dark-themed UI. CUDA support ensures high performance where available.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🔁 Connect multiple **local USB or IP cameras**
|
||||
- 💡 Real-time **YOLOv4 object detection**
|
||||
- 🎛️ Intelligent UI with **PyQt5**, collapsible dock widgets, and tabbed views
|
||||
- 📸 Take **screenshots** per camera feed
|
||||
- ⚙️ **Model loader** for dynamic YOLO weight/config/class sets
|
||||
- 🔌 **Network camera support** with authentication
|
||||
- 🖥️ **Hardware monitor** (CPU and per-core utilization via `psutil`)
|
||||
- 🖼️ Fullscreen camera views & dynamic layout switcher
|
||||
- 💾 Persistent **configuration management**
|
||||
- 🧪 **Camera connectivity test tools**
|
||||
|
||||
---
|
||||
|
||||
## 📦 Requirements
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><strong>Dependencies:</strong></summary>
|
||||
|
||||
- Python 3.8+
|
||||
- OpenCV (cv2)
|
||||
- PyQt5
|
||||
- NumPy
|
||||
- Requests
|
||||
- psutil
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
> ✅ Make sure your YOLOv4 model directory contains `.weights`, `.cfg`, and `.names` files.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Model Directory Structure
|
||||
This is important! If you dont have the correct directory structure, the model loader won't work.
|
||||
|
||||
Models are Included in the Git Repository!
|
||||
|
||||
```bash
|
||||
model/
|
||||
├── yolov4.cfg
|
||||
├── yolov4.weights
|
||||
└── coco.names
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 UI Highlights
|
||||
|
||||
- **Model Loader**: Easily select model directory
|
||||
- **Camera Selection**: Mix and match local/network sources
|
||||
- **Layouts**: Switch between 1, 2, 3, 4, or grid layouts
|
||||
- **FPS Control**: Adjustable frame rate
|
||||
- **Dark Theme**: Developer-friendly aesthetic
|
||||
- **Screenshot Button**: Save current camera frame
|
||||
- **Hardware Stats**: Monitor CPU load in real-time
|
||||
|
||||
---
|
||||
|
||||
## Gitea Worflow Download
|
||||
You can actually download Gitea Workflow compiled code that will run on your local machine. But for this to work you need to enter one of the following commands in your terminal:
|
||||
```sh
|
||||
chmod a+x main # or the name of your compiled file
|
||||
```
|
||||
|
||||
>Note: _This will only work on Linux! **( This is cause of the Gitea Runner (Which runs on Ubuntu!))** If you are on Windows you can compile it yourself with PyInstaller_
|
||||
|
||||
[](run.mp4)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Recommended YOLOv4 Model Links
|
||||
|
||||
- [YOLOv4.cfg](https://github.com/AlexeyAB/darknet/blob/master/cfg/yolov4.cfg)
|
||||
- [YOLOv4.weights](https://github.com/AlexeyAB/darknet/releases/download/yolov4/yolov4.weights)
|
||||
- [COCO.names](https://github.com/pjreddie/darknet/blob/master/data/coco.names)
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Network Camera Example
|
||||
|
||||
Supports URLs like:
|
||||
- `http://192.168.1.101:8080/video`
|
||||
- `http://username:password@ip:port/stream`
|
||||
|
||||
Authentication is optional and can be configured per-camera.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Camera Test Tools
|
||||
|
||||
- Test connectivity to any selected camera
|
||||
- Auto-handle reconnection on failures
|
||||
- Preview all selected feeds with drag-and-drop reordering
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration & Persistence
|
||||
|
||||
This is also Very Important if you have multiple cameras or want to save / delete your settings. You can save your settings to a file and load them later.
|
||||
|
||||
Settings (last model, selected cameras, layout, FPS, etc.) are saved to:
|
||||
|
||||
- **Linux/macOS**: `~/.config/mucapy/config.json`
|
||||
- **Windows**: `%APPDATA%\MuCaPy\config.json`
|
||||
|
||||
---
|
||||
|
||||
## 📸 Screenshot Storage
|
||||
|
||||
Default directory: `~/Pictures/MuCaPy` (can be changed from the UI)
|
||||
|
||||
---
|
||||
|
||||
## 📖 About
|
||||
|
||||
> _I built MuCaPy to learn about OpenCV, Python, and to have a simple, easy-to-use camera viewer for my Camera Server, since ContaCam doesn't work on my system (not well atleast!)._
|
||||
|
||||
---
|
||||
|
||||
## 🔮 To Be Added
|
||||
|
||||
- **YOLOv5 / YOLOv8 / YOLO-NAS support**
|
||||
- **RTSP stream handling** _( Currently works but is fuzzy / crashes easily)_
|
||||
- **Real-time performance dashboards**
|
||||
- **WebSocket remote monitoring**
|
||||
- **Not-so-laggy UI improvements**
|
||||
- **Better CUDA implementation**
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This Project is currently under a Proprietary Licsence and shouldnt be distributed!
|
||||
|
||||
---
|
||||
|
||||
## 🧑💻 Maintainers
|
||||
|
||||
- 👤 Rattatwinko
|
||||
0
__init__.py
Normal file
0
__init__.py
Normal file
419
mucapy/main.py
419
mucapy/main.py
@@ -4,20 +4,28 @@ import cv2
|
||||
import json
|
||||
import urllib.parse
|
||||
import numpy as np
|
||||
import psutil # Add psutil import
|
||||
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,
|
||||
QCheckBox, QTabWidget, QListWidgetItem, QSplitter)
|
||||
from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime, QRect, QThread, pyqtSignal, QMutex
|
||||
QCheckBox, QTabWidget, QListWidgetItem, QSplitter,
|
||||
QProgressBar) # Add QProgressBar
|
||||
from PyQt5.QtCore import Qt, QTimer, QDir, QSize, QSettings, QDateTime, QRect, QThread, pyqtSignal, QMutex, QObject
|
||||
from PyQt5.QtGui import (QImage, QPixmap, QIcon, QColor, QKeySequence, QPainter,
|
||||
QPen, QBrush)
|
||||
import platform
|
||||
import time
|
||||
import requests
|
||||
import subprocess
|
||||
|
||||
class getpath:
|
||||
def resource_path(relative_path):
|
||||
base_path = getattr(sys, '_MEIPASS', os.path.abspath("."))
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
# Use platform-specific user directory for config
|
||||
@@ -73,7 +81,6 @@ class Config:
|
||||
def load_setting(self, key, default=None):
|
||||
"""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)
|
||||
@@ -298,9 +305,9 @@ class CameraThread(QThread):
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.running = False
|
||||
|
||||
class MultiCamYOLODetector:
|
||||
def __init__(self):
|
||||
class MultiCamYOLODetector(QObject):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.cameras = []
|
||||
self.camera_threads = {} # Dictionary to store camera threads
|
||||
self.net = None
|
||||
@@ -487,7 +494,10 @@ class MultiCamYOLODetector:
|
||||
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)
|
||||
# Only perform detection if net is loaded and detection is requested
|
||||
parent_window = self.parent()
|
||||
if parent_window and self.net is not None and parent_window.detection_enabled:
|
||||
frame = self.get_detections(frame)
|
||||
frames.append(frame)
|
||||
except:
|
||||
frame = np.zeros((720, 1280, 3), dtype=np.uint8)
|
||||
@@ -547,7 +557,6 @@ class MultiCamYOLODetector:
|
||||
print(f"Detection error: {e}")
|
||||
|
||||
return frame
|
||||
|
||||
class CameraDisplay(QLabel):
|
||||
"""Custom QLabel for displaying camera feed with fullscreen support"""
|
||||
def __init__(self, parent=None):
|
||||
@@ -701,7 +710,6 @@ class CameraDisplay(QLabel):
|
||||
# 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):
|
||||
@@ -756,91 +764,186 @@ class CollapsibleDock(QDockWidget):
|
||||
self.resize(self.original_size)
|
||||
self.toggle_button.setIcon(QIcon.fromTheme("arrow-left"))
|
||||
self.collapsed = False
|
||||
|
||||
class AboutWindow(QDialog):
|
||||
def __init__(self, parent=None): # Add parent parameter with default None
|
||||
super().__init__(parent) # Pass parent to QDialog
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("About Multi-Camera YOLO Detection")
|
||||
self.setWindowIcon(QIcon.fromTheme("help-about"))
|
||||
self.resize(400, 300)
|
||||
self.resize(450, 420)
|
||||
|
||||
# Make it modal and stay on top
|
||||
self.setWindowModality(Qt.ApplicationModal)
|
||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setAlignment(Qt.AlignCenter)
|
||||
layout.setAlignment(Qt.AlignTop)
|
||||
layout.setSpacing(20)
|
||||
|
||||
# Application icon/logo (placeholder)
|
||||
# App icon
|
||||
icon_label = QLabel()
|
||||
icon_label.setPixmap(QIcon.fromTheme("camera-web").pixmap(64, 64))
|
||||
icon_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(icon_label)
|
||||
|
||||
# Application title
|
||||
# Title
|
||||
title_label = QLabel("MuCaPy - 1")
|
||||
title_label.setStyleSheet("font-size: 18px; font-weight: bold;")
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Version information
|
||||
# Version label
|
||||
version_label = QLabel("Version 1.0")
|
||||
version_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(version_label)
|
||||
|
||||
# Description
|
||||
desc_label = QLabel(
|
||||
"MuCaPy\n"
|
||||
"Multiple Camera Python\n"
|
||||
"Using CV2"
|
||||
)
|
||||
desc_label.setAlignment(Qt.AlignCenter)
|
||||
desc_label.setWordWrap(True)
|
||||
layout.addWidget(desc_label)
|
||||
# Get system info
|
||||
info = self.get_system_info()
|
||||
self.important_keys = ["Python", "OpenCV", "Memory", "CUDA"]
|
||||
self.full_labels = {}
|
||||
|
||||
# Close Button
|
||||
# === System Info Group ===
|
||||
self.sysinfo_box = QGroupBox()
|
||||
sysinfo_main_layout = QVBoxLayout()
|
||||
sysinfo_main_layout.setContentsMargins(8, 8, 8, 8)
|
||||
|
||||
# Header layout: title + triangle button
|
||||
header_layout = QHBoxLayout()
|
||||
header_label = QLabel("System Information")
|
||||
header_label.setStyleSheet("font-weight: bold;")
|
||||
header_layout.addWidget(header_label)
|
||||
|
||||
header_layout.addStretch()
|
||||
|
||||
self.toggle_btn = QToolButton()
|
||||
self.toggle_btn.setText("▶")
|
||||
self.toggle_btn.setCheckable(True)
|
||||
self.toggle_btn.setChecked(False)
|
||||
self.toggle_btn.setStyleSheet("""
|
||||
QToolButton {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
color: #DDD;
|
||||
}
|
||||
""")
|
||||
self.toggle_btn.toggled.connect(self.toggle_expand)
|
||||
header_layout.addWidget(self.toggle_btn)
|
||||
|
||||
sysinfo_main_layout.addLayout(header_layout)
|
||||
|
||||
# Details layout
|
||||
self.sysinfo_layout = QVBoxLayout()
|
||||
self.sysinfo_layout.setSpacing(5)
|
||||
|
||||
for key, value in info.items():
|
||||
if key == "MemoryGB":
|
||||
continue
|
||||
|
||||
label = QLabel(f"{key}: {value}")
|
||||
self.style_label(label, key, value)
|
||||
self.sysinfo_layout.addWidget(label)
|
||||
self.full_labels[key] = label
|
||||
|
||||
if key not in self.important_keys:
|
||||
label.setVisible(False)
|
||||
|
||||
sysinfo_main_layout.addLayout(self.sysinfo_layout)
|
||||
self.sysinfo_box.setLayout(sysinfo_main_layout)
|
||||
layout.addWidget(self.sysinfo_box)
|
||||
|
||||
# Close button
|
||||
close_btn = QPushButton("Close")
|
||||
close_btn.clicked.connect(self.accept)
|
||||
close_btn.setFixedWidth(100)
|
||||
layout.addWidget(close_btn, alignment=Qt.AlignCenter)
|
||||
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #2D2D2D;
|
||||
color: #DDD;
|
||||
}
|
||||
QLabel {
|
||||
color: #DDD;
|
||||
}
|
||||
QGroupBox {
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
padding-top: 15px;
|
||||
background-color: #252525;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 3px;
|
||||
color: #DDD;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #3A3A3A;
|
||||
color: #DDD;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
min-width: 80px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #4A4A4A;
|
||||
}
|
||||
""")
|
||||
# Set Styling for About Section
|
||||
style_file = getpath.resource_path("styling/about.qss")
|
||||
with open(style_file,"r") as aboutstyle:
|
||||
self.setStyleSheet(aboutstyle.read())
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def toggle_expand(self, checked):
|
||||
for key, label in self.full_labels.items():
|
||||
if key not in self.important_keys:
|
||||
label.setVisible(checked)
|
||||
self.toggle_btn.setText("▼" if checked else "▶")
|
||||
|
||||
def style_label(self, label, key, value):
|
||||
if key == "Python":
|
||||
label.setStyleSheet("color: #7FDBFF;")
|
||||
elif key == "OpenCV":
|
||||
label.setStyleSheet("color: #FF851B;")
|
||||
elif key == "CUDA":
|
||||
label.setStyleSheet("color: green;" if value == "Yes" else "color: red;")
|
||||
elif key == "NumPy":
|
||||
label.setStyleSheet("color: #B10DC9;")
|
||||
elif key == "Requests":
|
||||
label.setStyleSheet("color: #0074D9;")
|
||||
elif key == "Memory":
|
||||
try:
|
||||
ram = int(value.split()[0])
|
||||
if ram < 8:
|
||||
label.setStyleSheet("color: red;")
|
||||
elif ram < 16:
|
||||
label.setStyleSheet("color: yellow;")
|
||||
elif ram < 32:
|
||||
label.setStyleSheet("color: lightgreen;")
|
||||
else:
|
||||
label.setStyleSheet("color: #90EE90;")
|
||||
except:
|
||||
label.setStyleSheet("color: gray;")
|
||||
elif key == "CPU Usage":
|
||||
try:
|
||||
usage = float(value.strip('%'))
|
||||
if usage > 80:
|
||||
label.setStyleSheet("color: red;")
|
||||
elif usage > 50:
|
||||
label.setStyleSheet("color: yellow;")
|
||||
else:
|
||||
label.setStyleSheet("color: lightgreen;")
|
||||
except:
|
||||
label.setStyleSheet("color: gray;")
|
||||
elif key in ("CPU Cores", "Logical CPUs"):
|
||||
label.setStyleSheet("color: lightgreen;")
|
||||
elif key in ("CPU", "Architecture", "OS"):
|
||||
label.setStyleSheet("color: lightgray;")
|
||||
else:
|
||||
label.setStyleSheet("color: #DDD;")
|
||||
|
||||
def get_system_info(self):
|
||||
import platform
|
||||
|
||||
info = {}
|
||||
info['Python'] = sys.version.split()[0]
|
||||
info['OS'] = f"{platform.system()} {platform.release()}"
|
||||
info['Architecture'] = platform.machine()
|
||||
info['OpenCV'] = cv2.__version__
|
||||
info['CUDA'] = "Yes" if cv2.cuda.getCudaEnabledDeviceCount() > 0 else "No"
|
||||
info['NumPy'] = np.__version__
|
||||
info['Requests'] = requests.__version__
|
||||
|
||||
mem = psutil.virtual_memory()
|
||||
info['MemoryGB'] = mem.total // (1024**3)
|
||||
info['Memory'] = f"{info['MemoryGB']} GB RAM"
|
||||
|
||||
info['CPU Cores'] = psutil.cpu_count(logical=False)
|
||||
info['Logical CPUs'] = psutil.cpu_count(logical=True)
|
||||
info['CPU Usage'] = f"{psutil.cpu_percent()}%"
|
||||
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
info['CPU'] = platform.processor()
|
||||
elif sys.platform == "linux":
|
||||
info['CPU'] = subprocess.check_output("lscpu", shell=True).decode().split("\n")[0]
|
||||
elif sys.platform == "darwin":
|
||||
info['CPU'] = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"]).decode().strip()
|
||||
except Exception:
|
||||
info['CPU'] = "Unknown"
|
||||
|
||||
return info
|
||||
|
||||
|
||||
class NetworkCameraDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -976,7 +1079,6 @@ class NetworkCameraDialog(QDialog):
|
||||
if self.detector:
|
||||
self.detector.remove_network_camera(name)
|
||||
self.load_cameras()
|
||||
|
||||
class CameraSelectorDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -1305,11 +1407,10 @@ class CameraSelectorDialog(QDialog):
|
||||
dialog = NetworkCameraDialog(self)
|
||||
if dialog.exec_() == QDialog.Accepted:
|
||||
self.refresh_cameras()
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Multi-Camera YOLO Detection")
|
||||
self.setWindowTitle("MuCaPy - V1")
|
||||
self.setGeometry(100, 100, 1200, 800)
|
||||
|
||||
# Initialize configuration
|
||||
@@ -1317,93 +1418,24 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# Initialize default values
|
||||
self.current_layout = 0 # Default to single camera layout
|
||||
self.detector = MultiCamYOLODetector()
|
||||
self.detector = MultiCamYOLODetector(self) # Pass self as parent
|
||||
self.camera_settings = {}
|
||||
self.detection_enabled = True # Add detection toggle flag
|
||||
|
||||
# Load saved settings first
|
||||
self.load_saved_settings()
|
||||
|
||||
self.setWindowIcon(QIcon(getpath.resource_path("styling/logo.png"))) # Convert from SVG beforehand
|
||||
|
||||
# Initialize hardware monitor timer
|
||||
self.hw_timer = QTimer()
|
||||
self.hw_timer.timeout.connect(self.update_hardware_stats)
|
||||
self.hw_timer.start(1000) # Update every second
|
||||
|
||||
# Set dark theme style
|
||||
self.setStyleSheet("""
|
||||
QMainWindow, QWidget {
|
||||
background-color: #2D2D2D;
|
||||
color: #DDD;
|
||||
}
|
||||
QLabel {
|
||||
color: #DDD;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #3A3A3A;
|
||||
color: #DDD;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #4A4A4A;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #2A2A2A;
|
||||
color: #777;
|
||||
}
|
||||
QComboBox, QSpinBox {
|
||||
background-color: #3A3A3A;
|
||||
color: #DDD;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 3px;
|
||||
}
|
||||
QGroupBox {
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
padding-top: 15px;
|
||||
background-color: #252525;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 3px;
|
||||
color: #DDD;
|
||||
}
|
||||
QMenuBar {
|
||||
background-color: #252525;
|
||||
color: #DDD;
|
||||
}
|
||||
QMenuBar::item {
|
||||
background-color: transparent;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
background-color: #3A3A3A;
|
||||
}
|
||||
QMenu {
|
||||
background-color: #252525;
|
||||
border: 1px solid #444;
|
||||
color: #DDD;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: #3A3A3A;
|
||||
}
|
||||
QScrollArea {
|
||||
border: none;
|
||||
}
|
||||
QDockWidget {
|
||||
titlebar-close-icon: url(none);
|
||||
titlebar-normal-icon: url(none);
|
||||
}
|
||||
QDockWidget::title {
|
||||
background: #252525;
|
||||
padding-left: 5px;
|
||||
}
|
||||
QToolButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
style_file = getpath.resource_path("styling/mainwindow.qss")
|
||||
with open(style_file, "r") as mainstyle:
|
||||
self.setStyleSheet(mainstyle.read())
|
||||
|
||||
# Set palette for better dark mode support
|
||||
palette = self.palette()
|
||||
@@ -1518,6 +1550,14 @@ class MainWindow(QMainWindow):
|
||||
self.toggle_sidebar_action.triggered.connect(self.toggle_sidebar_visibility)
|
||||
view_menu.addAction(self.toggle_sidebar_action)
|
||||
|
||||
# Add toggle detection action
|
||||
self.toggle_detection_action = QAction('Enable Detection', self)
|
||||
self.toggle_detection_action.setCheckable(True)
|
||||
self.toggle_detection_action.setChecked(True)
|
||||
self.toggle_detection_action.setShortcut('Ctrl+D')
|
||||
self.toggle_detection_action.triggered.connect(self.toggle_detection)
|
||||
view_menu.addAction(self.toggle_detection_action)
|
||||
|
||||
# Camera menu
|
||||
self.camera_menu = menubar.addMenu('Cameras')
|
||||
|
||||
@@ -1897,6 +1937,49 @@ class MainWindow(QMainWindow):
|
||||
self.display_area.setLayout(self.display_layout)
|
||||
main_layout.addWidget(self.display_area)
|
||||
|
||||
# Hardware Monitor section
|
||||
hw_monitor_group = QGroupBox("Hardware Monitor")
|
||||
hw_monitor_layout = QVBoxLayout()
|
||||
|
||||
# CPU Usage
|
||||
cpu_layout = QHBoxLayout()
|
||||
cpu_layout.addWidget(QLabel("CPU Usage:"))
|
||||
self.cpu_progress = QProgressBar()
|
||||
self.cpu_progress.setRange(0, 100)
|
||||
self.cpu_progress.setTextVisible(True)
|
||||
self.cpu_progress.setFormat("%p%")
|
||||
|
||||
# Set Styling from cpu progress QSS file
|
||||
style_file = getpath.resource_path("styling/cpu_progress.qss")
|
||||
with open(style_file,"r") as cpu_progress_style:
|
||||
self.cpu_progress.setStyleSheet(cpu_progress_style.read())
|
||||
|
||||
cpu_layout.addWidget(self.cpu_progress)
|
||||
hw_monitor_layout.addLayout(cpu_layout)
|
||||
|
||||
# Per-core CPU Usage
|
||||
cores_layout = QGridLayout()
|
||||
self.core_bars = []
|
||||
num_cores = psutil.cpu_count()
|
||||
for i in range(num_cores):
|
||||
core_label = QLabel(f"Core {i}:")
|
||||
core_bar = QProgressBar()
|
||||
core_bar.setRange(0, 100)
|
||||
core_bar.setTextVisible(True)
|
||||
core_bar.setFormat("%p%")
|
||||
|
||||
core_file = getpath.resource_path("styling/core_bar.qss")
|
||||
with open(core_file,"r") as core_bar_styling:
|
||||
core_bar.setStyleSheet(core_bar_styling.read())
|
||||
|
||||
cores_layout.addWidget(core_label, i, 0)
|
||||
cores_layout.addWidget(core_bar, i, 1)
|
||||
self.core_bars.append(core_bar)
|
||||
hw_monitor_layout.addLayout(cores_layout)
|
||||
|
||||
hw_monitor_group.setLayout(hw_monitor_layout)
|
||||
sidebar_layout.addWidget(hw_monitor_group)
|
||||
|
||||
main_widget.setLayout(main_layout)
|
||||
self.setCentralWidget(main_widget)
|
||||
|
||||
@@ -2102,12 +2185,58 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
self.sidebar.collapse()
|
||||
|
||||
def update_hardware_stats(self):
|
||||
"""Update hardware statistics"""
|
||||
# Update overall CPU usage
|
||||
cpu_percent = psutil.cpu_percent()
|
||||
self.cpu_progress.setValue(int(cpu_percent))
|
||||
|
||||
# Update per-core CPU usage
|
||||
per_core = psutil.cpu_percent(percpu=True)
|
||||
for i, usage in enumerate(per_core):
|
||||
if i < len(self.core_bars):
|
||||
self.core_bars[i].setValue(int(usage))
|
||||
|
||||
# Set color based on usage
|
||||
for bar in [self.cpu_progress] + self.core_bars:
|
||||
value = bar.value()
|
||||
if value < 60:
|
||||
u60 = getpath.resource_path("styling/bar/u60.qss")
|
||||
with open(u60,"r") as u60_style:
|
||||
bar.setStyleSheet(u60_style.read())
|
||||
|
||||
elif value < 85:
|
||||
u85 = getpath.resource_path("styling/bar/a85.qss")
|
||||
with open(u85,"r") as a85_styling:
|
||||
bar.setStyleSheet(a85_styling.read())
|
||||
|
||||
else:
|
||||
else_file = getpath.resource_path("styling/bar/else.qss")
|
||||
with open(else_file, "r") as else_style:
|
||||
bar.setStyleSheet(else_style.read())
|
||||
|
||||
|
||||
def toggle_detection(self):
|
||||
"""Toggle detection enabled/disabled"""
|
||||
self.detection_enabled = self.toggle_detection_action.isChecked()
|
||||
if self.detection_enabled:
|
||||
self.start_btn.setEnabled(True)
|
||||
self.stop_btn.setEnabled(True)
|
||||
else:
|
||||
self.start_btn.setEnabled(False)
|
||||
self.stop_btn.setEnabled(False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Set the env
|
||||
os.environ["XDG_SESSION_TYPE"] = "xcb"
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Set application style to Fusion for better dark mode support
|
||||
app.setStyle("Fusion")
|
||||
|
||||
|
||||
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
36
mucapy/styling/about.qss
Normal file
36
mucapy/styling/about.qss
Normal file
@@ -0,0 +1,36 @@
|
||||
QDialog {
|
||||
background-color: #2D2D2D;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
QGroupBox {
|
||||
border: 1px solid #555;
|
||||
border-radius: 6px;
|
||||
margin-top: 10px;
|
||||
padding: 4px;
|
||||
background-color: #252525;
|
||||
}
|
||||
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 3px;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
QPushButton {
|
||||
background-color: #3A3A3A;
|
||||
color: #DDD;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #4A4A4A;
|
||||
}
|
||||
11
mucapy/styling/bar/a85.qss
Normal file
11
mucapy/styling/bar/a85.qss
Normal file
@@ -0,0 +1,11 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #E5A823;
|
||||
width: 1px;
|
||||
}
|
||||
11
mucapy/styling/bar/else.qss
Normal file
11
mucapy/styling/bar/else.qss
Normal file
@@ -0,0 +1,11 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #A23535;
|
||||
width: 1px;
|
||||
}
|
||||
11
mucapy/styling/bar/u60.qss
Normal file
11
mucapy/styling/bar/u60.qss
Normal file
@@ -0,0 +1,11 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #3A6EA5;
|
||||
width: 1px;
|
||||
}
|
||||
12
mucapy/styling/core_bar.qss
Normal file
12
mucapy/styling/core_bar.qss
Normal file
@@ -0,0 +1,12 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
max-height: 12px;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #3A6EA5;
|
||||
width: 1px;
|
||||
}
|
||||
11
mucapy/styling/cpu_progress.qss
Normal file
11
mucapy/styling/cpu_progress.qss
Normal file
@@ -0,0 +1,11 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #3A6EA5;
|
||||
width: 1px;
|
||||
}
|
||||
BIN
mucapy/styling/logo.png
Normal file
BIN
mucapy/styling/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
95
mucapy/styling/mainwindow.qss
Normal file
95
mucapy/styling/mainwindow.qss
Normal file
@@ -0,0 +1,95 @@
|
||||
QMainWindow, QWidget {
|
||||
background-color: #2D2D2D;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
QPushButton {
|
||||
background-color: #3A3A3A;
|
||||
color: #DDD;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #4A4A4A;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color: #2A2A2A;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
QComboBox, QSpinBox {
|
||||
background-color: #3A3A3A;
|
||||
color: #DDD;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
QGroupBox {
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
padding-top: 15px;
|
||||
background-color: #252525;
|
||||
}
|
||||
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 3px;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
QMenuBar {
|
||||
background-color: #252525;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
QMenuBar::item {
|
||||
background-color: transparent;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
QMenuBar::item:selected {
|
||||
background-color: #3A3A3A;
|
||||
}
|
||||
|
||||
QMenu {
|
||||
background-color: #252525;
|
||||
border: 1px solid #444;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: #3A3A3A;
|
||||
}
|
||||
|
||||
QScrollArea {
|
||||
border: none;
|
||||
}
|
||||
|
||||
QDockWidget {
|
||||
titlebar-close-icon: url(none);
|
||||
titlebar-normal-icon: url(none);
|
||||
}
|
||||
|
||||
QDockWidget::title {
|
||||
background: #252525;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
QToolButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
opencv-python>=4.5.0
|
||||
opencv-contrib-python>=4.5.0
|
||||
numpy>=1.19.0
|
||||
PyQt5>=5.15.0
|
||||
opencv-python==4.11.0.86
|
||||
numpy==2.2.6
|
||||
PyQt5==5.15.11
|
||||
requests==2.32.3
|
||||
psutil==7.0.0
|
||||
Reference in New Issue
Block a user