ты мать эта говно и пидарас
This commit is contained in:
BIN
__pycache__/web_server.cpython-313.pyc
Normal file
BIN
__pycache__/web_server.cpython-313.pyc
Normal file
Binary file not shown.
17
logs/access.log
Normal file
17
logs/access.log
Normal file
@@ -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"
|
||||
88
logs/error.log
Normal file
88
logs/error.log
Normal file
@@ -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
|
||||
@@ -1,4 +1,14 @@
|
||||
opencv-python>=4.5.0
|
||||
opencv-contrib-python>=4.5.0
|
||||
numpy>=1.19.0
|
||||
PyQt5>=5.15.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"
|
||||
321
templates/index.html
Normal file
321
templates/index.html
Normal file
@@ -0,0 +1,321 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Camera Viewer</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #000080;
|
||||
--bg-color: #c0c0c0;
|
||||
--window-bg: #ffffff;
|
||||
--border-light: #ffffff;
|
||||
--border-dark: #808080;
|
||||
--border-darker: #404040;
|
||||
--text-color: #000000;
|
||||
--button-face: #c0c0c0;
|
||||
--title-bar: #000080;
|
||||
--title-text: #ffffff;
|
||||
--button-highlight: #ffffff;
|
||||
--button-shadow: #808080;
|
||||
--button-shadow-dark: #404040;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.window {
|
||||
background-color: var(--window-bg);
|
||||
border: 2px solid var(--border-darker);
|
||||
border-top: 2px solid var(--border-light);
|
||||
border-left: 2px solid var(--border-light);
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2);
|
||||
margin: 0 auto;
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
background-color: var(--title-bar);
|
||||
color: var(--title-text);
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.title-bar-controls {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title-bar-button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: var(--button-face);
|
||||
border: 1px solid var(--border-darker);
|
||||
border-top: 1px solid var(--button-highlight);
|
||||
border-left: 1px solid var(--button-highlight);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.camera-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
position: relative;
|
||||
background-color: var(--bg-color);
|
||||
border: 2px solid var(--border-darker);
|
||||
border-top: 2px solid var(--border-light);
|
||||
border-left: 2px solid var(--border-light);
|
||||
padding: 8px;
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.camera-title {
|
||||
background-color: var(--title-bar);
|
||||
color: var(--title-text);
|
||||
padding: 4px 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.camera-feed {
|
||||
flex: 1;
|
||||
background-color: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.camera-feed img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background-color: var(--button-face);
|
||||
border-top: 2px solid var(--border-darker);
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
background-color: var(--button-face);
|
||||
border: 2px solid var(--border-darker);
|
||||
border-top: 2px solid var(--border-light);
|
||||
border-left: 2px solid var(--border-light);
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: var(--button-face);
|
||||
border: 2px solid var(--border-darker);
|
||||
border-top: 2px solid var(--button-highlight);
|
||||
border-left: 2px solid var(--button-highlight);
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
border: 2px solid var(--border-darker);
|
||||
border-bottom: 2px solid var(--button-highlight);
|
||||
border-right: 2px solid var(--button-highlight);
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--title-text);
|
||||
}
|
||||
|
||||
.status-message {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.camera-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="window">
|
||||
<div class="title-bar">
|
||||
<span>Multi-Camera YOLO Detection</span>
|
||||
<div class="title-bar-controls">
|
||||
<div class="title-bar-button">_</div>
|
||||
<div class="title-bar-button">□</div>
|
||||
<div class="title-bar-button">×</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="control-panel">
|
||||
<button class="button primary" onclick="startSystem()">Start System</button>
|
||||
<div class="status-message" id="modelStatus">Status: Initializing...</div>
|
||||
</div>
|
||||
|
||||
<div class="camera-grid" id="cameraGrid">
|
||||
<!-- Camera feeds will be added here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<span id="systemStatus">Ready</span>
|
||||
<span id="modelName"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let activeCameras = new Set();
|
||||
let isSystemRunning = false;
|
||||
|
||||
async function startSystem() {
|
||||
if (isSystemRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startButton = document.querySelector('.button.primary');
|
||||
startButton.textContent = 'Starting...';
|
||||
startButton.disabled = true;
|
||||
|
||||
try {
|
||||
// Auto-scan for cameras
|
||||
const cameras = await scanCameras();
|
||||
|
||||
// Initialize camera grid
|
||||
initializeCameraGrid(cameras);
|
||||
|
||||
// Start camera streams
|
||||
for (const camera of cameras) {
|
||||
startCameraStream(camera);
|
||||
}
|
||||
|
||||
isSystemRunning = true;
|
||||
updateStatus('System running');
|
||||
startButton.textContent = 'System Running';
|
||||
} catch (error) {
|
||||
console.error('Error starting system:', error);
|
||||
updateStatus('Error starting system');
|
||||
startButton.textContent = 'Start System';
|
||||
startButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function scanCameras() {
|
||||
const response = await fetch('/scan_cameras');
|
||||
const data = await response.json();
|
||||
return data.cameras;
|
||||
}
|
||||
|
||||
function initializeCameraGrid(cameras) {
|
||||
const grid = document.getElementById('cameraGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
cameras.forEach((camera, index) => {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'camera-container';
|
||||
container.innerHTML = `
|
||||
<div class="camera-title">Camera ${index + 1}</div>
|
||||
<div class="camera-feed">
|
||||
<img id="camera${camera.id}" src="" alt="Camera ${index + 1}"
|
||||
style="width: ${camera.width}px; height: ${camera.height}px;">
|
||||
</div>
|
||||
<div class="status-bar">
|
||||
<span>Resolution: ${camera.width}x${camera.height}</span>
|
||||
<span id="status${camera.id}">Connecting...</span>
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(container);
|
||||
});
|
||||
}
|
||||
|
||||
function startCameraStream(camera) {
|
||||
const img = document.getElementById(`camera${camera.id}`);
|
||||
const statusElement = document.getElementById(`status${camera.id}`);
|
||||
|
||||
function updateStream() {
|
||||
if (!isSystemRunning) return;
|
||||
|
||||
img.src = `/video_feed/${camera.id}?t=${Date.now()}`;
|
||||
statusElement.textContent = 'Connected';
|
||||
activeCameras.add(camera.id);
|
||||
|
||||
img.onload = () => {
|
||||
if (isSystemRunning) {
|
||||
requestAnimationFrame(updateStream);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
statusElement.textContent = 'Error';
|
||||
activeCameras.delete(camera.id);
|
||||
setTimeout(() => {
|
||||
if (isSystemRunning) {
|
||||
updateStream();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
updateStream();
|
||||
}
|
||||
|
||||
function updateStatus(message) {
|
||||
document.getElementById('systemStatus').textContent = message;
|
||||
}
|
||||
|
||||
// Check for model on load
|
||||
fetch('/check_model')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const modelStatus = document.getElementById('modelStatus');
|
||||
const modelName = document.getElementById('modelName');
|
||||
if (data.model_loaded) {
|
||||
modelStatus.textContent = 'Status: Model loaded';
|
||||
modelName.textContent = `Model: ${data.model_name || 'Unknown'}`;
|
||||
} else {
|
||||
modelStatus.textContent = 'Status: No model loaded';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
655
web_server.py
Normal file
655
web_server.py
Normal file
@@ -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/<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('/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/<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})
|
||||
|
||||
# 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