ты мать эта говно и пидарас
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
|
# Web framework and extensions
|
||||||
opencv-contrib-python>=4.5.0
|
Flask>=3.0.0
|
||||||
numpy>=1.19.0
|
Flask-Cors>=4.0.0
|
||||||
PyQt5>=5.15.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