Compare commits
2 Commits
7752eaaf9d
...
frontend
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82f435b8c3 | ||
|
|
64b472ad8e |
@@ -1,43 +0,0 @@
|
||||
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 Python 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" \
|
||||
--add-data "mucapy/todopackage:todopackage"
|
||||
|
||||
- name: Upload executable artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mucapy-executable
|
||||
path: dist/
|
||||
|
||||
|
||||
103
.gitignore
vendored
103
.gitignore
vendored
@@ -1,104 +1 @@
|
||||
|
||||
# ============================
|
||||
# IDEs and Editors
|
||||
# ============================
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# ============================
|
||||
# Python
|
||||
# ============================
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
pip-wheel-metadata/
|
||||
pip-log.txt
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# ============================
|
||||
# Logs / runtime / temp files
|
||||
# ============================
|
||||
*.log
|
||||
*.pid
|
||||
*.seed
|
||||
*.out
|
||||
*.bak
|
||||
*.tmp
|
||||
*.temp
|
||||
*.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# ============================
|
||||
# System and OS junk
|
||||
# ============================
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
.DS_Store
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
.idea_modules/
|
||||
|
||||
# ============================
|
||||
# Custom project folders
|
||||
# ============================
|
||||
# Ignore project-specific compiled caches or outputs
|
||||
mucapy/**/__pycache__/
|
||||
|
||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
13
.idea/inspectionProfiles/Project_Default.xml
generated
13
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,13 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N812" />
|
||||
<option value="N802" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
12
.idea/material_theme_project_new.xml
generated
12
.idea/material_theme_project_new.xml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="migrated" value="true" />
|
||||
<option name="pristineConfig" value="false" />
|
||||
<option name="userId" value="13ba7435:19917931603:-7ffa" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/misc.xml
generated
7
.idea/misc.xml
generated
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.13 (mucapy)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (mucapy)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/mucapy.iml" filepath="$PROJECT_DIR$/.idea/mucapy.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
14
.idea/mucapy.iml
generated
14
.idea/mucapy.iml
generated
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.13 (mucapy)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
41
LICENSE
41
LICENSE
@@ -1,41 +0,0 @@
|
||||
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.
|
||||
166
README.md
166
README.md
@@ -1,166 +0,0 @@
|
||||
# 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/popout camera views with zoom, pan, grid & timestamp overlays, snapshots, and shortcuts
|
||||
- 💾 Persistent **configuration management**
|
||||
|
||||
---
|
||||
|
||||
## 📦 Requirements
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Troubleshooting (Windows): If you see "ImportError: DLL load failed while importing QtCore",
|
||||
- Uninstall conflicting Qt packages: `pip uninstall -y python-qt5 PySide2 PySide6`
|
||||
- Reinstall PyQt5: `pip install --upgrade --force-reinstall PyQt5==5.15.11`
|
||||
- Install Microsoft VC++ x64 runtime: https://aka.ms/vs/17/release/vc_redist.x64.exe
|
||||
|
||||
<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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## ⚙️ 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
|
||||
BIN
__pycache__/web_server.cpython-313.pyc
Normal file
BIN
__pycache__/web_server.cpython-313.pyc
Normal file
Binary file not shown.
2403
logs/access.log
Normal file
2403
logs/access.log
Normal file
File diff suppressed because it is too large
Load Diff
2003
logs/error.log
Normal file
2003
logs/error.log
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
import os
|
||||
from PIL import Image
|
||||
import PyInstaller.__main__
|
||||
import PyQt5
|
||||
|
||||
# Paths
|
||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
MAIN_SCRIPT = os.path.join(ROOT_DIR, "main.py")
|
||||
STYLING_DIR = os.path.join(ROOT_DIR, "styling")
|
||||
|
||||
# Icon paths
|
||||
PNG_ICON = os.path.join(STYLING_DIR, "logo.png")
|
||||
ICO_ICON = os.path.join(STYLING_DIR, "logo.ico")
|
||||
|
||||
# Convert PNG to ICO
|
||||
img = Image.open(PNG_ICON)
|
||||
img.save(ICO_ICON, format="ICO", sizes=[(256,256), (128,128), (64,64), (32,32), (16,16)])
|
||||
print(f"Converted {PNG_ICON} to {ICO_ICON}")
|
||||
|
||||
# Detect PyQt5 platforms folder automatically
|
||||
pyqt_dir = os.path.dirname(PyQt5.__file__)
|
||||
platforms_path = None
|
||||
|
||||
# Walk recursively to find the 'platforms' folder
|
||||
for root, dirs, files in os.walk(pyqt_dir):
|
||||
if 'platforms' in dirs:
|
||||
platforms_path = os.path.join(root, 'platforms')
|
||||
break
|
||||
|
||||
if platforms_path is None or not os.path.exists(platforms_path):
|
||||
raise FileNotFoundError(f"Could not locate PyQt5 'platforms' folder under {pyqt_dir}")
|
||||
|
||||
print(f"Using PyQt5 platforms folder: {platforms_path}")
|
||||
|
||||
# Build EXE with PyInstaller
|
||||
PyInstaller.__main__.run([
|
||||
MAIN_SCRIPT,
|
||||
'--noconfirm',
|
||||
'--onefile',
|
||||
'--windowed',
|
||||
f'--icon={ICO_ICON}',
|
||||
# Only include the platforms folder (minimal requirement for PyQt5)
|
||||
'--add-data', f'{platforms_path};PyQt5/Qt/plugins/platforms',
|
||||
])
|
||||
|
||||
print("Build complete! Check the 'dist' folder for the executable.")
|
||||
2157
mucapy/main.py
2157
mucapy/main.py
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #E5A823;
|
||||
width: 1px;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #A23535;
|
||||
width: 1px;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
QProgressBar {
|
||||
border: 2px solid #550000;
|
||||
border-radius: 50px;
|
||||
background-color: qradialgradient(
|
||||
cx:0.5, cy:0.5,
|
||||
fx:0.5, fy:0.5,
|
||||
radius:1.0,
|
||||
stop:0 #0d0d0d,
|
||||
stop:1 #1a1a1a
|
||||
);
|
||||
text-align: center;
|
||||
color: #cccccc;
|
||||
font-family: "Fira Code", "OCR A Std", monospace;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background: qlineargradient(
|
||||
x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #ff0033,
|
||||
stop:1 #ff6666
|
||||
);
|
||||
border-radius: 50px;
|
||||
margin: 1px;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
QProgressBar {
|
||||
border: 2px solid #550000;
|
||||
border-radius: 50px;
|
||||
background-color: qradialgradient(
|
||||
cx:0.5, cy:0.5,
|
||||
fx:0.5, fy:0.5,
|
||||
radius:1.0,
|
||||
stop:0 #0d0d0d,
|
||||
stop:1 #1a1a1a
|
||||
);
|
||||
text-align: center;
|
||||
color: #cccccc;
|
||||
font-family: "Fira Code", "OCR A Std", monospace;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background: qlineargradient(
|
||||
x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #ff0033,
|
||||
stop:1 #ff6666
|
||||
);
|
||||
border-radius: 50px;
|
||||
margin: 1px;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
QProgressBar {
|
||||
border: 2px solid #550000;
|
||||
border-radius: 50px;
|
||||
background-color: qradialgradient(
|
||||
cx:0.5, cy:0.5,
|
||||
fx:0.5, fy:0.5,
|
||||
radius:1.0,
|
||||
stop:0 #0d0d0d,
|
||||
stop:1 #1a1a1a
|
||||
);
|
||||
text-align: center;
|
||||
color: #cccccc;
|
||||
font-family: "Fira Code", "OCR A Std", monospace;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background: qlineargradient(
|
||||
x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #ff0033,
|
||||
stop:1 #ff6666
|
||||
);
|
||||
border-radius: 50px;
|
||||
margin: 1px;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #3A6EA5;
|
||||
width: 1px;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
QLabel {
|
||||
background-color: #1E1E1E;
|
||||
color: #DDD;
|
||||
border: 2px solid #444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
QLabel {
|
||||
background-color: #1E1E1E;
|
||||
color: #DDD;
|
||||
border: 2px solid #444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
max-height: 12px;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #3A6EA5;
|
||||
width: 1px;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
QProgressBar {
|
||||
border: 1px solid #555;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
background-color: #2A2A2A;
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
background-color: #3A6EA5;
|
||||
width: 1px;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,158 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
QScrollBar:vertical {
|
||||
background: #1E1E1E; /* Scrollbar background */
|
||||
width: 10px;
|
||||
margin: 0px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background: #3A3A3A; /* Scroll handle */
|
||||
min-height: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: #555555; /* Hover color */
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical:pressed {
|
||||
background: #6A6A6A; /* Active color */
|
||||
}
|
||||
|
||||
QScrollBar::add-line:vertical,
|
||||
QScrollBar::sub-line:vertical {
|
||||
height: 0px; /* Hide arrows */
|
||||
}
|
||||
|
||||
QScrollBar::add-page:vertical,
|
||||
QScrollBar::sub-page:vertical {
|
||||
background: none; /* No gap color */
|
||||
}
|
||||
|
||||
/* ===== Horizontal Scrollbar ===== */
|
||||
QScrollBar:horizontal {
|
||||
background: #1E1E1E;
|
||||
height: 10px;
|
||||
margin: 0px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background: #3A3A3A;
|
||||
min-width: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: #555555;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal:pressed {
|
||||
background: #6A6A6A;
|
||||
}
|
||||
|
||||
QScrollBar::add-line:horizontal,
|
||||
QScrollBar::sub-line:horizontal {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
QScrollBar::add-page:horizontal,
|
||||
QScrollBar::sub-page:horizontal {
|
||||
background: none;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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;
|
||||
}
|
||||
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
QLabel#todoLabel {
|
||||
color:rgb(21, 255, 0);
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
QToolButton {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
color: #DDD;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
"""
|
||||
Here we have a simple fucking class to get the fuckass Todo thing
|
||||
Probably will do none of them
|
||||
anyways, if you have suggestions pr this shit
|
||||
|
||||
idk if you are smarter than me then actually do them.
|
||||
|
||||
<-----------------------------------------------------------------
|
||||
I feel more like a Frontend Dev doing this shit.
|
||||
If you are a Frontend Dev then fuck you. Fuckass React Devs.
|
||||
----------------------------------------------------------------->
|
||||
|
||||
fuck you , if you can't read this then kys. This is designed to be very readable.
|
||||
-Rattatwinko 8.jun.25 (20:25)
|
||||
"""
|
||||
|
||||
class todo:
|
||||
def __init__(self):
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------#
|
||||
self.information : str = """
|
||||
|
||||
MuCaPy TODO List:
|
||||
|
||||
NOTE: If you want to run this in Visual Studio Codes built in Terminal you will get fucked in your tiny tight ass.
|
||||
NOTE: If you ran this from the Workflow Package, you cant do shit. fuck you.
|
||||
NOTE: If you compiled this yourself ; you are one Fucking Genious
|
||||
|
||||
Changes:
|
||||
Todo in About ; Seperate File (@todo.py)
|
||||
|
||||
|
||||
""" # This will center!
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------#
|
||||
"""
|
||||
!## This was the Quessadilla Bug at earlier 75. With next commit delete that out of the todo Variable ##!
|
||||
|
||||
This has nothing to do with the About Window
|
||||
It just happens to fit in here.
|
||||
I really dont like my Strings in my logic. It throws me off.
|
||||
I know it overcomplicates shit but Python is readable anyways.
|
||||
|
||||
"""
|
||||
self.instructions_CaSeDi_QLabel : str = """
|
||||
|
||||
Camera Selection Guide:\n
|
||||
• Local Cameras: Built-in and USB cameras
|
||||
• Network Cameras: IP cameras, DroidCam, etc.
|
||||
• Use checkboxes to select/deselect cameras
|
||||
• Double-click a camera to test the connection
|
||||
• Selected cameras will appear in the preview bellow
|
||||
"""
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------#
|
||||
self.todo : str="""
|
||||
TODO:
|
||||
[] - Fix Network Cameras from Crashing the Programm (This sometimes happens when only network cams are selected and it times out)
|
||||
[/] - Make Seperate Styling (unlikely that this will happen)
|
||||
- CPU seperate styling is available , set variable in QMainWindow
|
||||
[] - RTSP Camera Streaming
|
||||
|
||||
""" # This will display lefty (look in about window class)
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------#
|
||||
self.cameraURL : str = """
|
||||
Cameras:
|
||||
- http://pendelcam.kip.uni-heidelberg.de/mjpg/video.mjpg
|
||||
""" # This will also display centered
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------#
|
||||
self.beaver : str="""
|
||||
___
|
||||
.=" "=._.---.
|
||||
."" c ' Y'`p
|
||||
/ , `. w_/
|
||||
jgs | '-. / /
|
||||
_,..._| )_-\ \_=.\
|
||||
`-....-'`--------)))`=-'"`'"
|
||||
""" # The beaver isnt really angry anymore. Idk why, now hes chill
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------#
|
||||
self.archived_todo : str = """
|
||||
Archived Todo List:
|
||||
[X] - Fix Quesadilla Bug. @todo.py:75 , This has nothing to do with the About Window, but the Network Camera Dialog in @main.py:1038/1049
|
||||
- Fixed this. @todo.py now has a Singleton Initializer. Idk. "Brotha ew - Tsoding in May". Fuck you
|
||||
[] - Make MJPEG more stable and efficient. (maybe use c++ for this or rust)
|
||||
- Wont happen ; remove next commit, this is as efficient as it gets
|
||||
"""
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------#
|
||||
"""
|
||||
|
||||
For a change we actually declare the Types.
|
||||
I usually leave this to the Interpreter, it does it anyway
|
||||
|
||||
"""
|
||||
|
||||
# Return the Todo String
|
||||
def gettodo(self) -> str:
|
||||
return self.todo
|
||||
# Return the Information about the Programm
|
||||
def getinfo(self) -> str:
|
||||
return self.information
|
||||
|
||||
# Return the Camera URL Thing
|
||||
def getcams(self) -> str:
|
||||
return self.cameraURL
|
||||
# Get Network Camera Instructions (in the setup dialog)
|
||||
def get_instructions_CaSeDi_QLabel(self) -> str:
|
||||
return self.instructions_CaSeDi_QLabel
|
||||
# Get the fuckass beaver. very angwy :3
|
||||
def get_beaver(self) -> str:
|
||||
return self.beaver
|
||||
# Get the Archive.
|
||||
def getarchive(self) -> str:
|
||||
return self.archived_todo
|
||||
|
||||
todo = todo()
|
||||
@@ -1,6 +1,14 @@
|
||||
opencv-python==4.11.0.86
|
||||
numpy==2.2.6
|
||||
PyQt5==5.15.11
|
||||
requests==2.32.3
|
||||
psutil==7.0.0
|
||||
pytest==8.4.0
|
||||
# Web framework and extensions
|
||||
Flask>=3.0.0
|
||||
Flask-Cors>=4.0.0
|
||||
Werkzeug>=3.0.0
|
||||
gunicorn>=21.2.0
|
||||
|
||||
# Core dependencies
|
||||
opencv-python-headless>=4.8.0
|
||||
|
||||
# Flask dependencies
|
||||
click>=8.1.7
|
||||
itsdangerous>=2.1.2
|
||||
Jinja2>=3.1.2
|
||||
MarkupSafe>=2.1.3
|
||||
235
run_server.sh
Executable file
235
run_server.sh
Executable file
@@ -0,0 +1,235 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# Function to print error messages
|
||||
error() {
|
||||
echo -e "\e[31mERROR:\e[0m $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Function to print success messages
|
||||
success() {
|
||||
echo -e "\e[32mSUCCESS:\e[0m $1"
|
||||
}
|
||||
|
||||
# Function to print info messages
|
||||
info() {
|
||||
echo -e "\e[34mINFO:\e[0m $1"
|
||||
}
|
||||
|
||||
# Function to check if a command exists
|
||||
check_command() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
error "Required command '$1' not found. Please install it first."
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to compare version numbers
|
||||
version_compare() {
|
||||
if [[ "$1" == "$2" ]]; then
|
||||
echo 0
|
||||
return
|
||||
fi
|
||||
local IFS=.
|
||||
local i ver1=($1) ver2=($2)
|
||||
# Fill empty positions in ver1 with zeros
|
||||
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do
|
||||
ver1[i]=0
|
||||
done
|
||||
for ((i=0; i<${#ver1[@]}; i++)); do
|
||||
# Fill empty positions in ver2 with zeros
|
||||
if [[ -z ${ver2[i]} ]]; then
|
||||
ver2[i]=0
|
||||
fi
|
||||
if ((10#${ver1[i]} > 10#${ver2[i]})); then
|
||||
echo 1
|
||||
return
|
||||
fi
|
||||
if ((10#${ver1[i]} < 10#${ver2[i]})); then
|
||||
echo -1
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo 0
|
||||
}
|
||||
|
||||
# Check for required system commands
|
||||
check_command python3
|
||||
check_command pip3
|
||||
|
||||
# Check Python version
|
||||
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
|
||||
MIN_VERSION="3.8"
|
||||
if [ $(version_compare "$PYTHON_VERSION" "$MIN_VERSION") -lt 0 ]; then
|
||||
error "Python version must be $MIN_VERSION or higher (found $PYTHON_VERSION)"
|
||||
fi
|
||||
success "Python version check passed (found $PYTHON_VERSION)"
|
||||
|
||||
# Function to install packages on Fedora
|
||||
install_fedora_deps() {
|
||||
info "Installing Fedora dependencies..."
|
||||
# Check which packages need to be installed
|
||||
local packages=()
|
||||
local check_packages=(
|
||||
"python3-devel"
|
||||
"gcc"
|
||||
"python3-pip"
|
||||
"python3-setuptools"
|
||||
"python3-numpy"
|
||||
"python3-opencv"
|
||||
"python3-flask"
|
||||
"python3-gunicorn"
|
||||
"bc"
|
||||
"lsof"
|
||||
)
|
||||
|
||||
for pkg in "${check_packages[@]}"; do
|
||||
if ! rpm -q "$pkg" &>/dev/null; then
|
||||
packages+=("$pkg")
|
||||
fi
|
||||
done
|
||||
|
||||
# Only run dnf if there are packages to install
|
||||
if [ ${#packages[@]} -gt 0 ]; then
|
||||
info "Installing missing packages: ${packages[*]}"
|
||||
sudo dnf install -y "${packages[@]}" || error "Failed to install required packages"
|
||||
else
|
||||
info "All required system packages are already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to install packages on Ubuntu/Debian
|
||||
install_ubuntu_deps() {
|
||||
info "Installing Ubuntu/Debian dependencies..."
|
||||
sudo apt-get update || error "Failed to update package lists"
|
||||
sudo apt-get install -y \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-flask \
|
||||
python3-gunicorn \
|
||||
bc \
|
||||
lsof \
|
||||
|| error "Failed to install required packages"
|
||||
}
|
||||
|
||||
# Check and install system dependencies based on distribution
|
||||
install_system_deps() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
case $ID in
|
||||
fedora)
|
||||
install_fedora_deps
|
||||
;;
|
||||
ubuntu|debian)
|
||||
install_ubuntu_deps
|
||||
;;
|
||||
*)
|
||||
info "Unknown distribution. Please ensure you have the following packages installed:"
|
||||
echo "- Python development package (python3-devel/python3-dev)"
|
||||
echo "- GCC compiler"
|
||||
echo "- Python pip"
|
||||
echo "- Python venv"
|
||||
echo "- Python numpy"
|
||||
echo "- Python OpenCV"
|
||||
echo "- Python Flask"
|
||||
echo "- Python Gunicorn"
|
||||
echo "- bc"
|
||||
echo "- lsof"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
# Install system dependencies
|
||||
install_system_deps
|
||||
|
||||
# Create and activate virtual environment
|
||||
if [ ! -d "venv" ]; then
|
||||
info "Creating virtual environment..."
|
||||
# Create venv with system packages to use system numpy and opencv
|
||||
python3 -m venv venv --system-site-packages || error "Failed to create virtual environment"
|
||||
success "Virtual environment created successfully"
|
||||
fi
|
||||
|
||||
# Ensure virtual environment is activated
|
||||
if [ -z "$VIRTUAL_ENV" ]; then
|
||||
info "Activating virtual environment..."
|
||||
source venv/bin/activate || error "Failed to activate virtual environment"
|
||||
fi
|
||||
|
||||
# Upgrade pip to latest version
|
||||
info "Upgrading pip..."
|
||||
python3 -m pip install --upgrade pip setuptools wheel || error "Failed to upgrade pip and setuptools"
|
||||
|
||||
# Create or update requirements.txt with compatible package versions
|
||||
if [ ! -f "requirements.txt" ]; then
|
||||
info "Creating requirements.txt..."
|
||||
cat > requirements.txt << EOF
|
||||
# Web framework and extensions
|
||||
Flask>=3.0.0
|
||||
Flask-Cors>=4.0.0
|
||||
Werkzeug>=3.0.0
|
||||
gunicorn>=21.2.0
|
||||
|
||||
# Core dependencies
|
||||
opencv-python-headless>=4.8.0
|
||||
|
||||
# Flask dependencies
|
||||
click>=8.1.7
|
||||
itsdangerous>=2.1.2
|
||||
Jinja2>=3.1.2
|
||||
MarkupSafe>=2.1.3
|
||||
EOF
|
||||
success "Created requirements.txt"
|
||||
fi
|
||||
|
||||
# Install requirements with better error handling
|
||||
info "Installing/updating requirements..."
|
||||
# First ensure pip is up to date
|
||||
pip install --upgrade pip
|
||||
|
||||
# Install packages with specific options for better compatibility
|
||||
PYTHONWARNINGS="ignore" pip install \
|
||||
--no-cache-dir \
|
||||
--prefer-binary \
|
||||
--only-binary :all: \
|
||||
-r requirements.txt || error "Failed to install requirements"
|
||||
|
||||
success "Requirements installed successfully"
|
||||
|
||||
# Create necessary directories
|
||||
info "Creating required directories..."
|
||||
mkdir -p logs || error "Failed to create logs directory"
|
||||
success "Created required directories"
|
||||
|
||||
# Function to check if port is available
|
||||
check_port() {
|
||||
if lsof -Pi :5000 -sTCP:LISTEN -t >/dev/null ; then
|
||||
error "Port 5000 is already in use. Please stop the other process first."
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if port is available
|
||||
check_port
|
||||
|
||||
# Start Gunicorn with modern settings
|
||||
info "Starting server with Gunicorn..."
|
||||
exec gunicorn web_server:app \
|
||||
--bind 0.0.0.0:5000 \
|
||||
--workers $(nproc) \
|
||||
--worker-class gthread \
|
||||
--threads 2 \
|
||||
--timeout 120 \
|
||||
--access-logfile logs/access.log \
|
||||
--error-logfile logs/error.log \
|
||||
--capture-output \
|
||||
--log-level info \
|
||||
--reload \
|
||||
--max-requests 1000 \
|
||||
--max-requests-jitter 50 \
|
||||
|| error "Failed to start Gunicorn server"
|
||||
1132
templates/index.html
Normal file
1132
templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
788
web_server.py
Normal file
788
web_server.py
Normal file
@@ -0,0 +1,788 @@
|
||||
import os
|
||||
import cv2
|
||||
import json
|
||||
import numpy as np
|
||||
from flask import Flask, Response, render_template, jsonify, request
|
||||
from flask_cors import CORS
|
||||
import threading
|
||||
import time
|
||||
import queue
|
||||
import urllib.parse
|
||||
import glob
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # Enable CORS for all routes
|
||||
|
||||
def find_yolo_model():
|
||||
"""Scan current directory and subdirectories for YOLO model files"""
|
||||
# Look for common YOLO file patterns
|
||||
weights_files = glob.glob('**/*.weights', recursive=True) + glob.glob('**/*.onnx', recursive=True)
|
||||
cfg_files = glob.glob('**/*.cfg', recursive=True)
|
||||
names_files = glob.glob('**/*.names', recursive=True)
|
||||
|
||||
# Find directories containing all required files
|
||||
model_dirs = set()
|
||||
for weights in weights_files:
|
||||
directory = os.path.dirname(weights)
|
||||
if not directory:
|
||||
directory = '.'
|
||||
|
||||
# Check if this directory has all required files
|
||||
has_cfg = any(cfg for cfg in cfg_files if os.path.dirname(cfg) == directory)
|
||||
has_names = any(names for names in names_files if os.path.dirname(names) == directory)
|
||||
|
||||
if has_cfg and has_names:
|
||||
model_dirs.add(directory)
|
||||
|
||||
# Return the first valid directory found, or None
|
||||
return next(iter(model_dirs), None)
|
||||
|
||||
class YOLODetector:
|
||||
def __init__(self):
|
||||
self.net = None
|
||||
self.classes = []
|
||||
self.colors = []
|
||||
self.confidence_threshold = 0.35
|
||||
self.cuda_available = self.check_cuda()
|
||||
self.model_loaded = False
|
||||
self.current_model = None
|
||||
|
||||
def check_cuda(self):
|
||||
"""Check if CUDA is available"""
|
||||
try:
|
||||
count = cv2.cuda.getCudaEnabledDeviceCount()
|
||||
return count > 0
|
||||
except:
|
||||
return False
|
||||
|
||||
def scan_for_model(self):
|
||||
"""Auto-scan for YOLO model files in current directory"""
|
||||
try:
|
||||
# Look for model files in current directory
|
||||
weights = [f for f in os.listdir('.') if f.endswith(('.weights', '.onnx'))]
|
||||
configs = [f for f in os.listdir('.') if f.endswith('.cfg')]
|
||||
classes = [f for f in os.listdir('.') if f.endswith('.names')]
|
||||
|
||||
if weights and configs and classes:
|
||||
self.load_yolo_model('.', weights[0], configs[0], classes[0])
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error scanning for model: {e}")
|
||||
return False
|
||||
|
||||
def load_yolo_model(self, model_dir, weights_file=None, config_file=None, classes_file=None):
|
||||
"""Load YOLO model with specified files or auto-detect"""
|
||||
try:
|
||||
if not weights_file:
|
||||
weights = [f for f in os.listdir(model_dir) if f.endswith(('.weights', '.onnx'))]
|
||||
configs = [f for f in os.listdir(model_dir) if f.endswith('.cfg')]
|
||||
classes = [f for f in os.listdir(model_dir) if f.endswith('.names')]
|
||||
|
||||
if not (weights and configs and classes):
|
||||
return False
|
||||
|
||||
weights_file = weights[0]
|
||||
config_file = configs[0]
|
||||
classes_file = classes[0]
|
||||
|
||||
weights_path = os.path.join(model_dir, weights_file)
|
||||
config_path = os.path.join(model_dir, config_file)
|
||||
classes_path = os.path.join(model_dir, classes_file)
|
||||
|
||||
self.net = cv2.dnn.readNet(weights_path, config_path)
|
||||
self.current_model = weights_file
|
||||
|
||||
if self.cuda_available:
|
||||
try:
|
||||
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
|
||||
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
|
||||
except:
|
||||
self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
|
||||
self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
|
||||
|
||||
with open(classes_path, 'r') as f:
|
||||
self.classes = f.read().strip().split('\n')
|
||||
|
||||
np.random.seed(42)
|
||||
self.colors = np.random.randint(0, 255, size=(len(self.classes), 3), dtype='uint8')
|
||||
self.model_loaded = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error loading model: {e}")
|
||||
self.model_loaded = False
|
||||
return False
|
||||
|
||||
def get_camera_resolution(self, cap):
|
||||
"""Get the optimal resolution for a camera"""
|
||||
try:
|
||||
# Common resolutions to try
|
||||
resolutions = [
|
||||
(1920, 1080), # Full HD
|
||||
(1280, 720), # HD
|
||||
(800, 600), # SVGA
|
||||
(640, 480) # VGA
|
||||
]
|
||||
|
||||
best_width = 640
|
||||
best_height = 480
|
||||
|
||||
for width, height in resolutions:
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
||||
actual_width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
|
||||
actual_height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
|
||||
|
||||
if actual_width > 0 and actual_height > 0:
|
||||
best_width = actual_width
|
||||
best_height = actual_height
|
||||
break
|
||||
|
||||
return int(best_width), int(best_height)
|
||||
except:
|
||||
return 640, 480
|
||||
|
||||
def scan_cameras(self):
|
||||
"""Scan for available cameras, skipping video0"""
|
||||
cameras = []
|
||||
|
||||
# Start from video1 since video0 is often empty or system camera
|
||||
for i in range(1, 10):
|
||||
try:
|
||||
cap = cv2.VideoCapture(i)
|
||||
if cap.isOpened():
|
||||
# Get optimal resolution
|
||||
width, height = self.get_camera_resolution(cap)
|
||||
cameras.append({
|
||||
'id': i,
|
||||
'width': width,
|
||||
'height': height
|
||||
})
|
||||
cap.release()
|
||||
except:
|
||||
continue
|
||||
|
||||
# Check device paths
|
||||
for i in range(1, 10):
|
||||
path = f"/dev/video{i}"
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
cap = cv2.VideoCapture(path)
|
||||
if cap.isOpened():
|
||||
width, height = self.get_camera_resolution(cap)
|
||||
cameras.append({
|
||||
'id': path,
|
||||
'width': width,
|
||||
'height': height
|
||||
})
|
||||
cap.release()
|
||||
except:
|
||||
continue
|
||||
|
||||
return cameras
|
||||
|
||||
def detect(self, frame):
|
||||
"""Perform object detection on frame"""
|
||||
if self.net is None or not self.model_loaded:
|
||||
return frame
|
||||
|
||||
try:
|
||||
height, width = frame.shape[:2]
|
||||
blob = cv2.dnn.blobFromImage(frame, 1/255.0, (416, 416), swapRB=True, crop=False)
|
||||
self.net.setInput(blob)
|
||||
|
||||
try:
|
||||
layer_names = self.net.getLayerNames()
|
||||
output_layers = [layer_names[i - 1] for i in self.net.getUnconnectedOutLayers()]
|
||||
except:
|
||||
output_layers = self.net.getUnconnectedOutLayersNames()
|
||||
|
||||
outputs = self.net.forward(output_layers)
|
||||
|
||||
# Process detections
|
||||
boxes = []
|
||||
confidences = []
|
||||
class_ids = []
|
||||
|
||||
for output in outputs:
|
||||
for detection in output:
|
||||
scores = detection[5:]
|
||||
class_id = np.argmax(scores)
|
||||
confidence = scores[class_id]
|
||||
|
||||
if confidence > self.confidence_threshold:
|
||||
# Convert YOLO coords to screen coords
|
||||
center_x = int(detection[0] * width)
|
||||
center_y = int(detection[1] * height)
|
||||
w = int(detection[2] * width)
|
||||
h = int(detection[3] * height)
|
||||
|
||||
# Get top-left corner
|
||||
x = max(0, int(center_x - w/2))
|
||||
y = max(0, int(center_y - h/2))
|
||||
|
||||
boxes.append([x, y, w, h])
|
||||
confidences.append(float(confidence))
|
||||
class_ids.append(class_id)
|
||||
|
||||
# Apply non-maximum suppression
|
||||
indices = cv2.dnn.NMSBoxes(boxes, confidences, self.confidence_threshold, 0.4)
|
||||
|
||||
if len(indices) > 0:
|
||||
for i in indices.flatten():
|
||||
try:
|
||||
(x, y, w, h) = boxes[i]
|
||||
# Ensure coordinates are within frame bounds
|
||||
x = max(0, min(x, width - 1))
|
||||
y = max(0, min(y, height - 1))
|
||||
w = min(w, width - x)
|
||||
h = min(h, height - y)
|
||||
|
||||
color = [int(c) for c in self.colors[class_ids[i]]]
|
||||
cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
|
||||
|
||||
# Draw label with background
|
||||
text = f"{self.classes[class_ids[i]]}: {confidences[i]:.2f}"
|
||||
font_scale = 0.5
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
thickness = 1
|
||||
(text_w, text_h), baseline = cv2.getTextSize(text, font, font_scale, thickness)
|
||||
|
||||
# Draw background rectangle for text
|
||||
cv2.rectangle(frame, (x, y - text_h - baseline - 5), (x + text_w, y), color, -1)
|
||||
# Draw text
|
||||
cv2.putText(frame, text, (x, y - 5), font, font_scale, (255, 255, 255), thickness)
|
||||
except Exception as e:
|
||||
print(f"Error drawing detection {i}: {e}")
|
||||
continue
|
||||
|
||||
return frame
|
||||
|
||||
except Exception as e:
|
||||
print(f"Detection error: {e}")
|
||||
return frame
|
||||
|
||||
class CameraStream:
|
||||
def __init__(self, camera_id, detector):
|
||||
self.camera_id = camera_id
|
||||
self.cap = None
|
||||
self.frame_queue = queue.Queue(maxsize=10)
|
||||
self.running = False
|
||||
self.thread = None
|
||||
self.lock = threading.Lock()
|
||||
self.detector = detector
|
||||
self.is_network_camera = isinstance(camera_id, str) and camera_id.startswith('net:')
|
||||
self.last_frame_time = time.time()
|
||||
self.frame_timeout = 5.0 # Timeout after 5 seconds without frames
|
||||
self.reconnect_interval = 5.0 # Try to reconnect every 5 seconds
|
||||
self.last_reconnect_attempt = 0
|
||||
self.connection_retries = 0
|
||||
self.max_retries = 3
|
||||
|
||||
def start(self):
|
||||
"""Start the camera stream with improved error handling"""
|
||||
if self.running:
|
||||
return False
|
||||
|
||||
try:
|
||||
if self.is_network_camera:
|
||||
# Handle network camera
|
||||
name = self.camera_id[4:] # Remove 'net:' prefix
|
||||
camera_info = camera_manager.network_cameras.get(name)
|
||||
if not camera_info:
|
||||
raise Exception(f"Network camera {name} not found")
|
||||
|
||||
if isinstance(camera_info, dict):
|
||||
url = camera_info['url']
|
||||
# Handle DroidCam URL formatting
|
||||
if ':4747' in url and not url.endswith('/video'):
|
||||
url = url.rstrip('/') + '/video'
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
url = 'http://' + url
|
||||
|
||||
if 'username' in camera_info and 'password' in camera_info:
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
netloc = f"{camera_info['username']}:{camera_info['password']}@{parsed.netloc}"
|
||||
url = parsed._replace(netloc=netloc).geturl()
|
||||
else:
|
||||
url = camera_info
|
||||
|
||||
logger.info(f"Connecting to network camera: {url}")
|
||||
self.cap = cv2.VideoCapture(url)
|
||||
|
||||
# Wait for connection
|
||||
retry_count = 0
|
||||
while not self.cap.isOpened() and retry_count < 3:
|
||||
time.sleep(1)
|
||||
retry_count += 1
|
||||
self.cap.open(url)
|
||||
|
||||
if not self.cap.isOpened():
|
||||
raise Exception(f"Failed to connect to network camera at {url}")
|
||||
else:
|
||||
# Handle local camera
|
||||
self.cap = cv2.VideoCapture(self.camera_id)
|
||||
|
||||
if not self.cap.isOpened():
|
||||
raise Exception(f"Failed to open camera {self.camera_id}")
|
||||
|
||||
# Set camera properties
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||||
self.cap.set(cv2.CAP_PROP_FPS, 30)
|
||||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
|
||||
self.running = True
|
||||
self.connection_retries = 0
|
||||
self.thread = threading.Thread(target=self._capture_loop)
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting camera {self.camera_id}: {e}")
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
self.connection_retries += 1
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
"""Stop the camera stream"""
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join()
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
|
||||
while not self.frame_queue.empty():
|
||||
try:
|
||||
self.frame_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
|
||||
def _capture_loop(self):
|
||||
"""Main capture loop with improved error handling and reconnection"""
|
||||
while self.running:
|
||||
try:
|
||||
if not self.cap or not self.cap.isOpened():
|
||||
current_time = time.time()
|
||||
if current_time - self.last_reconnect_attempt >= self.reconnect_interval:
|
||||
if self.connection_retries < self.max_retries:
|
||||
logger.info(f"Attempting to reconnect camera {self.camera_id}")
|
||||
self.last_reconnect_attempt = current_time
|
||||
if self.start():
|
||||
logger.info(f"Successfully reconnected camera {self.camera_id}")
|
||||
else:
|
||||
logger.warning(f"Failed to reconnect camera {self.camera_id}")
|
||||
else:
|
||||
logger.error(f"Max reconnection attempts reached for camera {self.camera_id}")
|
||||
self.running = False
|
||||
break
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
current_time = time.time()
|
||||
if current_time - self.last_frame_time > self.frame_timeout:
|
||||
logger.warning(f"No frames received from camera {self.camera_id} for {self.frame_timeout} seconds")
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
self.last_frame_time = time.time()
|
||||
|
||||
# Apply object detection
|
||||
if self.detector and self.detector.net is not None:
|
||||
frame = self.detector.detect(frame)
|
||||
|
||||
# Convert to JPEG
|
||||
_, jpeg = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
||||
|
||||
# Update queue
|
||||
try:
|
||||
self.frame_queue.put_nowait(jpeg.tobytes())
|
||||
except queue.Full:
|
||||
try:
|
||||
self.frame_queue.get_nowait()
|
||||
self.frame_queue.put_nowait(jpeg.tobytes())
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in capture loop for camera {self.camera_id}: {e}")
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
time.sleep(1)
|
||||
|
||||
def get_frame(self):
|
||||
"""Get the latest frame"""
|
||||
try:
|
||||
return self.frame_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
return None
|
||||
|
||||
class CameraManager:
|
||||
def __init__(self):
|
||||
self.cameras = {}
|
||||
self.network_cameras = {}
|
||||
self.lock = threading.Lock()
|
||||
self.detector = YOLODetector()
|
||||
|
||||
# Auto-scan for model directory
|
||||
model_dir = os.getenv('YOLO_MODEL_DIR')
|
||||
if not model_dir or not os.path.exists(model_dir):
|
||||
model_dir = find_yolo_model()
|
||||
|
||||
if model_dir:
|
||||
print(f"Found YOLO model in directory: {model_dir}")
|
||||
self.detector.load_yolo_model(model_dir)
|
||||
else:
|
||||
print("No YOLO model found in current directory")
|
||||
|
||||
def add_camera(self, camera_id):
|
||||
"""Add a camera to the manager"""
|
||||
with self.lock:
|
||||
if camera_id not in self.cameras:
|
||||
camera = CameraStream(camera_id, self.detector)
|
||||
if camera.start():
|
||||
self.cameras[camera_id] = camera
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_camera(self, camera_id):
|
||||
"""Remove a camera from the manager"""
|
||||
with self.lock:
|
||||
if camera_id in self.cameras:
|
||||
self.cameras[camera_id].stop()
|
||||
del self.cameras[camera_id]
|
||||
|
||||
def get_camera(self, camera_id):
|
||||
"""Get a camera by ID"""
|
||||
return self.cameras.get(camera_id)
|
||||
|
||||
def get_all_cameras(self):
|
||||
"""Get list of all camera IDs"""
|
||||
return list(self.cameras.keys())
|
||||
|
||||
def add_network_camera(self, name, url, username=None, password=None):
|
||||
"""Add a network camera"""
|
||||
camera_info = {
|
||||
'url': url,
|
||||
'username': username,
|
||||
'password': password
|
||||
} if username and password else url
|
||||
|
||||
self.network_cameras[name] = camera_info
|
||||
return True
|
||||
|
||||
def remove_network_camera(self, name):
|
||||
"""Remove a network camera"""
|
||||
if name in self.network_cameras:
|
||||
camera_id = f"net:{name}"
|
||||
if camera_id in self.cameras:
|
||||
self.remove_camera(camera_id)
|
||||
del self.network_cameras[name]
|
||||
return True
|
||||
return False
|
||||
|
||||
camera_manager = CameraManager()
|
||||
|
||||
def gen_frames(camera_id):
|
||||
"""Generator function for camera frames"""
|
||||
camera = camera_manager.get_camera(camera_id)
|
||||
if not camera:
|
||||
return
|
||||
|
||||
while True:
|
||||
frame = camera.get_frame()
|
||||
if frame is not None:
|
||||
yield (b'--frame\r\n'
|
||||
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Serve the main page"""
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/video_feed/<path:camera_id>')
|
||||
def video_feed(camera_id):
|
||||
"""Video streaming route"""
|
||||
# Handle both local and network cameras
|
||||
if camera_id.startswith('net:'):
|
||||
camera_id = camera_id # Keep as string for network cameras
|
||||
else:
|
||||
try:
|
||||
camera_id = int(camera_id) # Convert to int for local cameras
|
||||
except ValueError:
|
||||
camera_id = camera_id # Keep as string if not convertible
|
||||
|
||||
return Response(gen_frames(camera_id),
|
||||
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
|
||||
@app.route('/scan_cameras')
|
||||
def scan_cameras_route():
|
||||
"""Scan and return available cameras"""
|
||||
cameras = scan_cameras_silently()
|
||||
return jsonify({'cameras': cameras})
|
||||
|
||||
@app.route('/cameras')
|
||||
def get_cameras():
|
||||
"""Get list of available cameras"""
|
||||
# Get local cameras
|
||||
cameras = scan_cameras_silently()
|
||||
|
||||
# Add network cameras
|
||||
for name, info in camera_manager.network_cameras.items():
|
||||
url = info['url'] if isinstance(info, dict) else info
|
||||
cameras.append({
|
||||
'id': f'net:{name}',
|
||||
'type': 'network',
|
||||
'name': f'{name} ({url})',
|
||||
'width': 1280, # Default width for network cameras
|
||||
'height': 720 # Default height for network cameras
|
||||
})
|
||||
|
||||
# Add status information for active cameras
|
||||
for camera in cameras:
|
||||
camera_stream = camera_manager.get_camera(camera['id'])
|
||||
if camera_stream:
|
||||
camera['active'] = True
|
||||
camera['status'] = 'connected' if camera_stream.cap and camera_stream.cap.isOpened() else 'reconnecting'
|
||||
else:
|
||||
camera['active'] = False
|
||||
camera['status'] = 'disconnected'
|
||||
|
||||
return jsonify({'cameras': cameras})
|
||||
|
||||
@app.route('/add_camera/<path:camera_id>')
|
||||
def add_camera(camera_id):
|
||||
"""Add a camera to the stream"""
|
||||
if camera_id.startswith('net:'):
|
||||
camera_id = camera_id # Keep as string for network cameras
|
||||
else:
|
||||
try:
|
||||
camera_id = int(camera_id) # Convert to int for local cameras
|
||||
except ValueError:
|
||||
camera_id = camera_id # Keep as string if not convertible
|
||||
|
||||
success = camera_manager.add_camera(camera_id)
|
||||
return jsonify({'success': success})
|
||||
|
||||
@app.route('/remove_camera/<path:camera_id>')
|
||||
def remove_camera(camera_id):
|
||||
"""Remove a camera from the stream"""
|
||||
camera_manager.remove_camera(camera_id)
|
||||
return jsonify({'success': True})
|
||||
|
||||
@app.route('/network_cameras', methods=['POST'])
|
||||
def add_network_camera():
|
||||
"""Add a network camera"""
|
||||
data = request.json
|
||||
name = data.get('name')
|
||||
url = data.get('url')
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
if not name or not url:
|
||||
return jsonify({'success': False, 'error': 'Name and URL required'})
|
||||
|
||||
try:
|
||||
success = camera_manager.add_network_camera(name, url, username, password)
|
||||
if success:
|
||||
# Try to connect to the camera to verify it works
|
||||
camera_id = f"net:{name}"
|
||||
test_success = camera_manager.add_camera(camera_id)
|
||||
if test_success:
|
||||
camera_manager.remove_camera(camera_id) # Remove test connection
|
||||
else:
|
||||
camera_manager.remove_network_camera(name)
|
||||
return jsonify({'success': False, 'error': 'Failed to connect to camera'})
|
||||
return jsonify({'success': success})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
@app.route('/load_model', methods=['POST'])
|
||||
def load_model():
|
||||
"""Load YOLO model"""
|
||||
data = request.json
|
||||
model_dir = data.get('model_dir')
|
||||
|
||||
if not model_dir or not os.path.exists(model_dir):
|
||||
return jsonify({'success': False, 'error': 'Invalid model directory'})
|
||||
|
||||
success = camera_manager.detector.load_yolo_model(model_dir)
|
||||
return jsonify({'success': success})
|
||||
|
||||
@app.route('/check_model')
|
||||
def check_model():
|
||||
"""Check YOLO model status"""
|
||||
return jsonify({
|
||||
'model_loaded': camera_manager.detector.model_loaded,
|
||||
'model_name': camera_manager.detector.current_model,
|
||||
'cuda_available': camera_manager.detector.cuda_available
|
||||
})
|
||||
|
||||
@app.route('/model_info')
|
||||
def model_info():
|
||||
"""Get detailed model information"""
|
||||
detector = camera_manager.detector
|
||||
return jsonify({
|
||||
'model_loaded': detector.model_loaded,
|
||||
'model_name': detector.current_model,
|
||||
'cuda_available': detector.cuda_available,
|
||||
'classes': detector.classes,
|
||||
'confidence_threshold': detector.confidence_threshold
|
||||
})
|
||||
|
||||
@app.route('/update_model_settings', methods=['POST'])
|
||||
def update_model_settings():
|
||||
"""Update model settings"""
|
||||
data = request.json
|
||||
if 'confidence_threshold' in data:
|
||||
try:
|
||||
threshold = float(data['confidence_threshold'])
|
||||
if 0 <= threshold <= 1:
|
||||
camera_manager.detector.confidence_threshold = threshold
|
||||
return jsonify({'success': True})
|
||||
except ValueError:
|
||||
pass
|
||||
return jsonify({'success': False, 'error': 'Invalid settings'})
|
||||
|
||||
@app.route('/network_cameras/list')
|
||||
def list_network_cameras():
|
||||
"""List all configured network cameras"""
|
||||
cameras = []
|
||||
for name, info in camera_manager.network_cameras.items():
|
||||
if isinstance(info, dict):
|
||||
cameras.append({
|
||||
'name': name,
|
||||
'url': info['url'],
|
||||
'username': info.get('username'),
|
||||
'has_auth': bool(info.get('username') and info.get('password'))
|
||||
})
|
||||
else:
|
||||
cameras.append({
|
||||
'name': name,
|
||||
'url': info,
|
||||
'has_auth': False
|
||||
})
|
||||
return jsonify({'cameras': cameras})
|
||||
|
||||
@app.route('/network_cameras/remove/<name>', methods=['POST'])
|
||||
def remove_network_camera(name):
|
||||
"""Remove a network camera"""
|
||||
success = camera_manager.remove_network_camera(name)
|
||||
return jsonify({'success': success})
|
||||
|
||||
@app.route('/camera_settings/<path:camera_id>', methods=['GET', 'POST'])
|
||||
def camera_settings(camera_id):
|
||||
"""Get or update camera settings"""
|
||||
camera = camera_manager.get_camera(camera_id)
|
||||
if not camera:
|
||||
return jsonify({'success': False, 'error': 'Camera not found'})
|
||||
|
||||
if request.method == 'POST':
|
||||
data = request.json
|
||||
try:
|
||||
if 'width' in data and 'height' in data:
|
||||
width = int(data['width'])
|
||||
height = int(data['height'])
|
||||
if camera.cap:
|
||||
camera.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
|
||||
camera.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
||||
if 'fps' in data:
|
||||
fps = int(data['fps'])
|
||||
if camera.cap:
|
||||
camera.cap.set(cv2.CAP_PROP_FPS, fps)
|
||||
if 'reconnect' in data and data['reconnect']:
|
||||
# Force camera reconnection
|
||||
camera.stop()
|
||||
time.sleep(1) # Wait for cleanup
|
||||
camera.start()
|
||||
return jsonify({'success': True})
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating camera settings: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
else:
|
||||
if not camera.cap:
|
||||
return jsonify({'success': False, 'error': 'Camera not connected'})
|
||||
|
||||
settings = {
|
||||
'width': int(camera.cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
|
||||
'height': int(camera.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
|
||||
'fps': int(camera.cap.get(cv2.CAP_PROP_FPS)),
|
||||
'is_network': camera.is_network_camera,
|
||||
'status': 'connected' if camera.cap and camera.cap.isOpened() else 'disconnected'
|
||||
}
|
||||
return jsonify({'success': True, 'settings': settings})
|
||||
|
||||
# Reduce camera scanning noise
|
||||
def scan_cameras_silently():
|
||||
"""Scan for cameras while suppressing OpenCV warnings"""
|
||||
import contextlib
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
with contextlib.redirect_stderr(devnull):
|
||||
cameras = []
|
||||
# Check device paths first
|
||||
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():
|
||||
cameras.append({
|
||||
'id': device_path,
|
||||
'type': 'local',
|
||||
'name': f'Camera {i} ({device_path})'
|
||||
})
|
||||
cap.release()
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking device {device_path}: {e}")
|
||||
|
||||
# Check numeric indices
|
||||
for i in range(2): # Only check first two indices to reduce noise
|
||||
try:
|
||||
cap = cv2.VideoCapture(i)
|
||||
if cap.isOpened():
|
||||
cameras.append({
|
||||
'id': str(i),
|
||||
'type': 'local',
|
||||
'name': f'Camera {i}'
|
||||
})
|
||||
cap.release()
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking camera {i}: {e}")
|
||||
|
||||
return cameras
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Create templates directory if it doesn't exist
|
||||
os.makedirs('templates', exist_ok=True)
|
||||
|
||||
# Load model from environment variable if available
|
||||
model_dir = os.getenv('YOLO_MODEL_DIR')
|
||||
if model_dir and os.path.exists(model_dir):
|
||||
camera_manager.detector.load_yolo_model(model_dir)
|
||||
|
||||
# Check if running with Gunicorn
|
||||
if os.environ.get('GUNICORN_CMD_ARGS') is not None:
|
||||
# Running with Gunicorn, let it handle the server
|
||||
pass
|
||||
else:
|
||||
# Development server warning
|
||||
logger.warning("Running in development mode. Use Gunicorn for production!")
|
||||
app.run(host='0.0.0.0', port=5000, threaded=True)
|
||||
Reference in New Issue
Block a user