From 64b472ad8e1ed9873c85ec2b42d92bc6bd4cdc6d Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 26 May 2025 20:29:02 +0200 Subject: [PATCH] =?UTF-8?q?=D1=82=D1=8B=20=D0=BC=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=8D=D1=82=D0=B0=20=D0=B3=D0=BE=D0=B2=D0=BD=D0=BE=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B8=D0=B4=D0=B0=D1=80=D0=B0=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/web_server.cpython-313.pyc | Bin 0 -> 33089 bytes logs/access.log | 17 + logs/error.log | 88 ++++ requirements.txt | 18 +- run_server.sh | 235 +++++++++ templates/index.html | 321 ++++++++++++ web_server.py | 655 +++++++++++++++++++++++++ 7 files changed, 1330 insertions(+), 4 deletions(-) create mode 100644 __pycache__/web_server.cpython-313.pyc create mode 100644 logs/access.log create mode 100644 logs/error.log create mode 100755 run_server.sh create mode 100644 templates/index.html create mode 100644 web_server.py diff --git a/__pycache__/web_server.cpython-313.pyc b/__pycache__/web_server.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5a1b67db4316f52e6c77a39ae3027b26575539a GIT binary patch literal 33089 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 literal 0 HcmV?d00001 diff --git a/logs/access.log b/logs/access.log new file mode 100644 index 0000000..f611b17 --- /dev/null +++ b/logs/access.log @@ -0,0 +1,17 @@ +127.0.0.1 - - [26/May/2025:20:25:21 +0200] "GET / HTTP/1.1" 200 12917 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:22 +0200] "GET /cameras HTTP/1.1" 200 192 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:22 +0200] "GET /favicon.ico HTTP/1.1" 404 207 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:27 +0200] "GET /cameras HTTP/1.1" 200 192 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:32 +0200] "GET /cameras HTTP/1.1" 200 192 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:33 +0200] "GET /add_camera/0 HTTP/1.1" 200 17 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:37 +0200] "GET /cameras HTTP/1.1" 200 3 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:40 +0200] "GET /cameras HTTP/1.1" 200 3 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:42 +0200] "GET /cameras HTTP/1.1" 200 3 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:25:44 +0200] "GET /video_feed/0 HTTP/1.1" 200 8815858 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:28:42 +0200] "GET / HTTP/1.1" 200 10364 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:28:42 +0200] "GET /check_model HTTP/1.1" 404 207 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:28:44 +0200] "GET /scan_cameras HTTP/1.1" 404 207 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:28:46 +0200] "GET /scan_cameras HTTP/1.1" 404 207 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:28:46 +0200] "GET /scan_cameras HTTP/1.1" 404 207 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:28:46 +0200] "GET /scan_cameras HTTP/1.1" 404 207 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" +127.0.0.1 - - [26/May/2025:20:28:50 +0200] "GET /scan_cameras HTTP/1.1" 404 207 "http://localhost:5000/" "Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0" diff --git a/logs/error.log b/logs/error.log new file mode 100644 index 0000000..9c000d1 --- /dev/null +++ b/logs/error.log @@ -0,0 +1,88 @@ +[2025-05-26 20:24:22 +0200] [824587] [INFO] Starting gunicorn 23.0.0 +[2025-05-26 20:24:22 +0200] [824587] [INFO] Listening at: http://0.0.0.0:5000 (824587) +[2025-05-26 20:24:22 +0200] [824587] [INFO] Using worker: gthread +[2025-05-26 20:24:22 +0200] [825529] [INFO] Booting worker with pid: 825529 +[2025-05-26 20:24:22 +0200] [825544] [INFO] Booting worker with pid: 825544 +[2025-05-26 20:24:22 +0200] [825556] [INFO] Booting worker with pid: 825556 +[2025-05-26 20:24:22 +0200] [825571] [INFO] Booting worker with pid: 825571 +[2025-05-26 20:24:23 +0200] [825584] [INFO] Booting worker with pid: 825584 +[2025-05-26 20:24:23 +0200] [825600] [INFO] Booting worker with pid: 825600 +[2025-05-26 20:24:23 +0200] [825601] [INFO] Booting worker with pid: 825601 +[2025-05-26 20:24:23 +0200] [825602] [INFO] Booting worker with pid: 825602 +[2025-05-26 20:24:23 +0200] [825639] [INFO] Booting worker with pid: 825639 +[2025-05-26 20:24:23 +0200] [825651] [INFO] Booting worker with pid: 825651 +[2025-05-26 20:24:23 +0200] [825666] [INFO] Booting worker with pid: 825666 +Found YOLO model in directory: mucapy/models +[2025-05-26 20:24:23 +0200] [825682] [INFO] Booting worker with pid: 825682 +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +[ WARN:0@58.649] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video1): can't open camera by index +[ERROR:0@58.649] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[ WARN:0@64.731] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video1): can't open camera by index +[ERROR:0@64.732] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[ WARN:0@69.536] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video1): can't open camera by index +[ERROR:0@69.537] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[ WARN:0@74.431] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video0): can't open camera by index +[ERROR:0@74.431] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[ WARN:0@74.431] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video1): can't open camera by index +[ERROR:0@74.431] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[ WARN:0@76.868] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video0): can't open camera by index +[ERROR:0@76.868] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[ WARN:0@76.868] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video1): can't open camera by index +[ERROR:0@76.868] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[ WARN:0@79.438] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video0): can't open camera by index +[ERROR:0@79.438] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[ WARN:0@79.438] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video1): can't open camera by index +[ERROR:0@79.438] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range +[2025-05-26 20:25:47 +0200] [824587] [INFO] Handling signal: int +[2025-05-26 20:25:47 +0200] [825556] [INFO] Worker exiting (pid: 825556) +[2025-05-26 20:25:47 +0200] [825600] [INFO] Worker exiting (pid: 825600) +[2025-05-26 20:25:47 +0200] [825602] [INFO] Worker exiting (pid: 825602) +[2025-05-26 20:25:47 +0200] [825584] [INFO] Worker exiting (pid: 825584) +[2025-05-26 20:25:47 +0200] [825639] [INFO] Worker exiting (pid: 825639) +[2025-05-26 20:25:47 +0200] [825544] [INFO] Worker exiting (pid: 825544) +[2025-05-26 20:25:47 +0200] [825666] [INFO] Worker exiting (pid: 825666) +[2025-05-26 20:25:47 +0200] [825571] [INFO] Worker exiting (pid: 825571) +[2025-05-26 20:25:47 +0200] [825651] [INFO] Worker exiting (pid: 825651) +[2025-05-26 20:25:47 +0200] [825682] [INFO] Worker exiting (pid: 825682) +[2025-05-26 20:25:47 +0200] [825529] [INFO] Worker exiting (pid: 825529) +[2025-05-26 20:25:47 +0200] [825601] [INFO] Worker exiting (pid: 825601) +terminate called without an active exception +[2025-05-26 20:25:49 +0200] [824587] [ERROR] Worker (pid:825651) was sent code 134! +[2025-05-26 20:25:49 +0200] [824587] [INFO] Shutting down: Master +[2025-05-26 20:28:26 +0200] [833260] [INFO] Starting gunicorn 23.0.0 +[2025-05-26 20:28:26 +0200] [833260] [INFO] Listening at: http://0.0.0.0:5000 (833260) +[2025-05-26 20:28:26 +0200] [833260] [INFO] Using worker: gthread +[2025-05-26 20:28:26 +0200] [833415] [INFO] Booting worker with pid: 833415 +[2025-05-26 20:28:26 +0200] [833428] [INFO] Booting worker with pid: 833428 +[2025-05-26 20:28:26 +0200] [833448] [INFO] Booting worker with pid: 833448 +[2025-05-26 20:28:26 +0200] [833464] [INFO] Booting worker with pid: 833464 +[2025-05-26 20:28:26 +0200] [833481] [INFO] Booting worker with pid: 833481 +[2025-05-26 20:28:26 +0200] [833493] [INFO] Booting worker with pid: 833493 +[2025-05-26 20:28:26 +0200] [833508] [INFO] Booting worker with pid: 833508 +[2025-05-26 20:28:26 +0200] [833509] [INFO] Booting worker with pid: 833509 +Found YOLO model in directory: mucapy/models +[2025-05-26 20:28:26 +0200] [833537] [INFO] Booting worker with pid: 833537 +[2025-05-26 20:28:27 +0200] [833556] [INFO] Booting worker with pid: 833556 +Found YOLO model in directory: mucapy/models +[2025-05-26 20:28:27 +0200] [833570] [INFO] Booting worker with pid: 833570 +[2025-05-26 20:28:27 +0200] [833585] [INFO] Booting worker with pid: 833585 +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models +Found YOLO model in directory: mucapy/models diff --git a/requirements.txt b/requirements.txt index 24c3e2a..b2ac9ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,14 @@ -opencv-python>=4.5.0 -opencv-contrib-python>=4.5.0 -numpy>=1.19.0 -PyQt5>=5.15.0 \ No newline at end of file +# 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 \ No newline at end of file diff --git a/run_server.sh b/run_server.sh new file mode 100755 index 0000000..aed450e --- /dev/null +++ b/run_server.sh @@ -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" \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..447938f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,321 @@ + + + + + + Camera Viewer + + + + +
+
+ Multi-Camera YOLO Detection +
+
_
+
+
×
+
+
+ +
+
+ +
Status: Initializing...
+
+ +
+ +
+
+ +
+ Ready + +
+
+ + + + + \ No newline at end of file diff --git a/web_server.py b/web_server.py new file mode 100644 index 0000000..4994584 --- /dev/null +++ b/web_server.py @@ -0,0 +1,655 @@ +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 + + def start(self): + """Start the camera stream""" + if self.running: + return + + 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) + 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.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 + 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 automatic 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: + logger.info(f"Attempting to reconnect camera {self.camera_id}") + self.last_reconnect_attempt = current_time + self.start() + 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/') +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('/cameras') +def get_cameras(): + """Get list of available cameras""" + # Scan for local cameras silently + 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})' + }) + + # 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) + +@app.route('/add_camera/') +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/') +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}) + +# 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) \ No newline at end of file