Spaces:
Sleeping
Sleeping
Ali Abdullah commited on
Commit Β·
98a79a7
0
Parent(s):
Fix requirements.txt encoding for HF
Browse files- .gitignore +0 -0
- Dockerfile +35 -0
- LICENSE +21 -0
- Procfile +1 -0
- README.md +124 -0
- app.py +707 -0
- config.yaml +141 -0
- requirements.txt +30 -0
- runtime.txt +1 -0
- src/__init__.py +14 -0
- src/detection/__init__.py +7 -0
- src/detection/detector.py +369 -0
- src/heatmap/__init__.py +7 -0
- src/heatmap/generator.py +316 -0
- src/utils/__init__.py +8 -0
- src/utils/config.py +65 -0
- src/utils/logger.py +67 -0
- src/video/__init__.py +7 -0
- src/video/handler.py +328 -0
- static/422671.jpg +0 -0
- static/script.js +411 -0
- static/style.css +922 -0
- templates/index.html +189 -0
.gitignore
ADDED
|
Binary file (865 Bytes). View file
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Create user with UID 1000 - Hugging Face requires this specific setup
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
|
| 6 |
+
# Install system dependencies needed for OpenCV
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
libgl1 \
|
| 9 |
+
libglib2.0-0 \
|
| 10 |
+
libsm6 \
|
| 11 |
+
libxext6 \
|
| 12 |
+
libxrender-dev \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# Switch to the non-root user
|
| 16 |
+
USER user
|
| 17 |
+
ENV HOME=/home/user \
|
| 18 |
+
PATH=/home/user/.local/bin:$PATH
|
| 19 |
+
|
| 20 |
+
# Set the working directory
|
| 21 |
+
WORKDIR $HOME/app
|
| 22 |
+
|
| 23 |
+
# Copy dependencies first (for Docker caching)
|
| 24 |
+
COPY --chown=user requirements.txt .
|
| 25 |
+
RUN pip install --no-cache-dir torch==2.5.1 torchvision==0.20.1 --index-url https://download.pytorch.org/whl/cpu && pip install --no-cache-dir -r requirements.txt
|
| 26 |
+
|
| 27 |
+
# Copy the rest of the application
|
| 28 |
+
COPY --chown=user . $HOME/app
|
| 29 |
+
|
| 30 |
+
# Hugging Face exposes exactly port 7860
|
| 31 |
+
ENV PORT=7860
|
| 32 |
+
EXPOSE 7860
|
| 33 |
+
|
| 34 |
+
# Run the Flask App
|
| 35 |
+
CMD ["python", "app.py"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Ali Abdullah
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: python app.py
|
README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ο»Ώ---
|
| 2 |
+
title: Smart Crowd Detector
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Smart Crowd Detector
|
| 12 |
+
|
| 13 |
+

|
| 14 |
+

|
| 15 |
+

|
| 16 |
+
|
| 17 |
+
An AI-powered real-time crowd detection and monitoring system using YOLOv8 with an adaptive heatmap visualization. Built for safety, efficiency, and smart crowd management.
|
| 18 |
+
|
| 19 |
+
## π― Features
|
| 20 |
+
|
| 21 |
+
- **Real-time People Detection** - GPU-accelerated YOLOv8 model with TensorRT, OpenCV, and PyTorch.
|
| 22 |
+
- **Dynamic Detection Modes** - Switch dynamically depending on your environment:
|
| 23 |
+
- π’ **Stadium Mode**: High density optimization (500 max detections, low confidence/IOU thresholds) perfectly tailored for massive crowds and distant individuals.
|
| 24 |
+
- π₯ **Normal Mode**: Balanced accuracy and framing for standard environments (offices, retail).
|
| 25 |
+
- β‘ **Fast Mode**: Maximum framerate via skipped frames and lower resolutions for basic needs on lower-end hardware.
|
| 26 |
+
- **Adaptive Heatmaps** - Smart kernel sizing based on object distance to represent crowd density accurate to visual depth.
|
| 27 |
+
- **Dual Source Input** - Works seamlessly with an active webcam or video files with continuous looping playback support.
|
| 28 |
+
- **Alert System** - Highly configurable warning/critical crowd density thresholds.
|
| 29 |
+
- **High Performance** - Thread-safe state caching, frame deferring, GPU pipeline offloading, and optimized resolutions delivering 30-35 FPS on fast mode or extreme accuracy insights for dense stadium applications.
|
| 30 |
+
- **Modern Dashboard** - Clean F1-themed interface built on vanilla JS and standard web sockets.
|
| 31 |
+
|
| 32 |
+
## π Requirements
|
| 33 |
+
|
| 34 |
+
- **GPU**: CUDA-capable NVIDIA GPU (RTX 3050+) is heavily recommended for Stadium mode.
|
| 35 |
+
- **RAM**: 8GB minimum, 16GB recommended.
|
| 36 |
+
- **Software**: Python 3.10+, CUDA 12.0+
|
| 37 |
+
|
| 38 |
+
## π Installation & Local Setup
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
# 1. Clone the Repository
|
| 42 |
+
git clone https://github.com/nowayitsme-eng/Smart_Crowd_Detector.git
|
| 43 |
+
cd Smart_Crowd_Detector
|
| 44 |
+
|
| 45 |
+
# 2. Create and Activate Virtual Environment
|
| 46 |
+
python -m venv venv
|
| 47 |
+
# On Windows:
|
| 48 |
+
venv\Scripts\activate
|
| 49 |
+
# On Linux/macOS:
|
| 50 |
+
source venv/bin/activate
|
| 51 |
+
|
| 52 |
+
# 3. Install Dependencies
|
| 53 |
+
pip install -r requirements.txt
|
| 54 |
+
|
| 55 |
+
# 4. Run the application
|
| 56 |
+
python app.py
|
| 57 |
+
```
|
| 58 |
+
*Access the dashboard at `http://localhost:5000`*
|
| 59 |
+
|
| 60 |
+
## π³ Docker & Cloud Deployment
|
| 61 |
+
|
| 62 |
+
β οΈ **Note on Serverless (e.g. Vercel)**: Standard serverless does not support this application. Real-time video processing requires persistent connections, background sockets, and large ML sizes which bypass standard serverless constraints. Use Docker or standard scalable containers.
|
| 63 |
+
|
| 64 |
+
### Docker (Recommended for AWS, GCP, Azure, DigitalOcean)
|
| 65 |
+
|
| 66 |
+
Create a `Dockerfile` with the following:
|
| 67 |
+
```dockerfile
|
| 68 |
+
FROM python:3.11-slim
|
| 69 |
+
RUN apt-get update && apt-get install -y libgl1-mesa-glx libglib2.0-0 libsm6 libxext6 libxrender-dev && rm -rf /var/lib/apt/lists/*
|
| 70 |
+
WORKDIR /app
|
| 71 |
+
COPY requirements.txt .
|
| 72 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 73 |
+
COPY . .
|
| 74 |
+
EXPOSE 5000
|
| 75 |
+
CMD ["python", "app.py"]
|
| 76 |
+
```
|
| 77 |
+
Build and run:
|
| 78 |
+
```bash
|
| 79 |
+
docker build -t app-monitor .
|
| 80 |
+
docker run -p 5000:5000 --device=/dev/video0 app-monitor
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Render.com / Railway.app
|
| 84 |
+
- Connect your GitHub Repository natively.
|
| 85 |
+
- **Railway**: Instantly supported utilizing persistent Docker builds.
|
| 86 |
+
- **Render**: Use a standard Web Service provider. Build command: `pip install -r requirements.txt`. Start command: `python app.py`.
|
| 87 |
+
|
| 88 |
+
### Heroku
|
| 89 |
+
Use Heroku buildpacks if not building via container setup:
|
| 90 |
+
```bash
|
| 91 |
+
heroku create smart-crowd-monitor
|
| 92 |
+
heroku buildpacks:add --index 1 heroku-community/apt
|
| 93 |
+
heroku buildpacks:add --index 2 heroku/python
|
| 94 |
+
git push heroku main
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## βοΈ Configuration
|
| 98 |
+
|
| 99 |
+
Tweak main properties centrally in `config.yaml`:
|
| 100 |
+
```yaml
|
| 101 |
+
video:
|
| 102 |
+
source: 0 # Camera index or path/to/video.mp4
|
| 103 |
+
fps: 30
|
| 104 |
+
resolution:
|
| 105 |
+
width: 640
|
| 106 |
+
height: 480
|
| 107 |
+
|
| 108 |
+
model:
|
| 109 |
+
confidence_threshold: 0.35
|
| 110 |
+
device: "cuda" # switch to "cpu" if no GPU available
|
| 111 |
+
|
| 112 |
+
crowd:
|
| 113 |
+
density_threshold: 20
|
| 114 |
+
warning_threshold: 35
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
## π License & Credits
|
| 118 |
+
|
| 119 |
+
MIT License
|
| 120 |
+
|
| 121 |
+
**Author:** Ali Abdullah - [GitHub: nowayitsme-eng](https://github.com/nowayitsme-eng)
|
| 122 |
+
**Acknowledgments:** YOLOv8 by Ultralytics, OpenCV, Flask Framework
|
| 123 |
+
|
| 124 |
+
|
app.py
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Zaytrics Smart Crowd Monitoring System - Web Server
|
| 3 |
+
Optimized for small object detection and better performance
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
print("[*] Starting Zaytrics...")
|
| 7 |
+
|
| 8 |
+
# GPU Verification - Check CUDA availability
|
| 9 |
+
print("[*] Checking GPU...")
|
| 10 |
+
import torch
|
| 11 |
+
if torch.cuda.is_available():
|
| 12 |
+
gpu_name = torch.cuda.get_device_name(0)
|
| 13 |
+
gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1024**3
|
| 14 |
+
print(f"[OK] GPU Detected: {gpu_name} ({gpu_memory:.1f} GB VRAM)")
|
| 15 |
+
print(f" CUDA Version: {torch.version.cuda}")
|
| 16 |
+
# Set CUDA optimizations
|
| 17 |
+
torch.backends.cudnn.benchmark = True # Auto-tune for best performance
|
| 18 |
+
torch.backends.cuda.matmul.allow_tf32 = True # Allow TF32 for faster matmul
|
| 19 |
+
else:
|
| 20 |
+
print("[WARN] WARNING: CUDA not available, using CPU (slower)")
|
| 21 |
+
|
| 22 |
+
print("[*] Loading Flask...")
|
| 23 |
+
from flask import Flask, render_template, Response, jsonify, request, send_from_directory
|
| 24 |
+
from flask_cors import CORS
|
| 25 |
+
from werkzeug.utils import secure_filename
|
| 26 |
+
print("[OK] Flask loaded")
|
| 27 |
+
|
| 28 |
+
print("[*] Loading OpenCV...")
|
| 29 |
+
import cv2
|
| 30 |
+
import numpy as np
|
| 31 |
+
print("[OK] OpenCV loaded")
|
| 32 |
+
|
| 33 |
+
import os
|
| 34 |
+
import json
|
| 35 |
+
import time
|
| 36 |
+
import logging
|
| 37 |
+
from datetime import datetime
|
| 38 |
+
from threading import Thread, Lock
|
| 39 |
+
from queue import Queue
|
| 40 |
+
from collections import deque
|
| 41 |
+
|
| 42 |
+
print("[*] Loading detection modules...")
|
| 43 |
+
from src.detection.detector import CrowdDetector
|
| 44 |
+
print("[OK] Detector loaded")
|
| 45 |
+
|
| 46 |
+
from src.heatmap.generator import HeatmapGenerator
|
| 47 |
+
print("[OK] Heatmap loaded")
|
| 48 |
+
|
| 49 |
+
from src.video.handler import VideoHandler
|
| 50 |
+
print("[OK] Video handler loaded")
|
| 51 |
+
|
| 52 |
+
from src.utils.config import load_config
|
| 53 |
+
from src.utils.logger import setup_logger
|
| 54 |
+
print("[OK] All modules loaded")
|
| 55 |
+
|
| 56 |
+
# Initialize Flask app
|
| 57 |
+
app = Flask(__name__, static_folder='static', template_folder='templates')
|
| 58 |
+
CORS(app)
|
| 59 |
+
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching for development
|
| 60 |
+
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB max file size
|
| 61 |
+
app.config['UPLOAD_FOLDER'] = 'videos'
|
| 62 |
+
ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov', 'mkv', 'webm'}
|
| 63 |
+
|
| 64 |
+
# Add CORS and security headers
|
| 65 |
+
@app.after_request
|
| 66 |
+
def add_security_headers(response):
|
| 67 |
+
"""Add security headers to all responses"""
|
| 68 |
+
response.headers['X-Content-Type-Options'] = 'nosniff'
|
| 69 |
+
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
| 70 |
+
response.headers['X-XSS-Protection'] = '1; mode=block'
|
| 71 |
+
# Allow same-origin requests only
|
| 72 |
+
if 'Origin' in request.headers:
|
| 73 |
+
origin = request.headers['Origin']
|
| 74 |
+
# Only allow localhost origins for security
|
| 75 |
+
if 'localhost' in origin or '127.0.0.1' in origin or origin.startswith('http://10.'):
|
| 76 |
+
response.headers['Access-Control-Allow-Origin'] = origin
|
| 77 |
+
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
|
| 78 |
+
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
| 79 |
+
return response
|
| 80 |
+
|
| 81 |
+
# Ensure upload directory exists
|
| 82 |
+
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
| 83 |
+
|
| 84 |
+
# Load configuration
|
| 85 |
+
config = load_config('config.yaml')
|
| 86 |
+
logger = setup_logger(config)
|
| 87 |
+
|
| 88 |
+
# Initialize components with optimized parameters for small objects
|
| 89 |
+
detector = CrowdDetector(config)
|
| 90 |
+
heatmap_generator = HeatmapGenerator(config)
|
| 91 |
+
video_handler = VideoHandler(config)
|
| 92 |
+
|
| 93 |
+
# Thread-safe state management
|
| 94 |
+
state_lock = Lock()
|
| 95 |
+
|
| 96 |
+
def allowed_file(filename):
|
| 97 |
+
"""Check if file extension is allowed"""
|
| 98 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 99 |
+
|
| 100 |
+
# Check for existing video files and set default source
|
| 101 |
+
def get_latest_video():
|
| 102 |
+
"""Get the most recent uploaded video file"""
|
| 103 |
+
try:
|
| 104 |
+
videos_dir = 'videos'
|
| 105 |
+
if os.path.exists(videos_dir):
|
| 106 |
+
videos = [f for f in os.listdir(videos_dir) if allowed_file(f)]
|
| 107 |
+
if videos:
|
| 108 |
+
videos.sort(reverse=True) # Sort by timestamp (filename starts with timestamp)
|
| 109 |
+
return videos[0]
|
| 110 |
+
except Exception as e:
|
| 111 |
+
print(f"Error getting latest video: {e}")
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
latest_video = get_latest_video()
|
| 115 |
+
default_source = 'video' if latest_video else 'camera'
|
| 116 |
+
|
| 117 |
+
state = {
|
| 118 |
+
'running': False,
|
| 119 |
+
'heatmap_enabled': False,
|
| 120 |
+
'total_detections': 0,
|
| 121 |
+
'count_history': [],
|
| 122 |
+
'time_history': [],
|
| 123 |
+
'current_count': 0,
|
| 124 |
+
'fps': 0,
|
| 125 |
+
'alert_level': 'normal',
|
| 126 |
+
'statistics': {},
|
| 127 |
+
'last_detection_time': 0,
|
| 128 |
+
'detection_cache': [],
|
| 129 |
+
'frame_cache': None,
|
| 130 |
+
'source_type': default_source, # 'camera' or 'video'
|
| 131 |
+
'video_file': latest_video,
|
| 132 |
+
'video_loop': True # Loop videos by default
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
print(f"Default source: {default_source}, Video file: {latest_video}")
|
| 136 |
+
|
| 137 |
+
# Use deque for frame times
|
| 138 |
+
frame_times = deque(maxlen=100) # Keep last 100 frames
|
| 139 |
+
|
| 140 |
+
# Detection Mode System - Toggle between Normal and Dense Crowd modes
|
| 141 |
+
DETECTION_MODES = {
|
| 142 |
+
'normal': { # Current working baseline - DO NOT MODIFY
|
| 143 |
+
'interval': 3,
|
| 144 |
+
'confidence': 0.35,
|
| 145 |
+
'iou': 0.45,
|
| 146 |
+
'resize': 1.0,
|
| 147 |
+
'min_size': 20,
|
| 148 |
+
'multi_scale': False,
|
| 149 |
+
'max_det': 300,
|
| 150 |
+
'imgsz': 416,
|
| 151 |
+
'second_pass_conf': 0.05,
|
| 152 |
+
'duplicate_threshold': 30,
|
| 153 |
+
'min_box_size': 5
|
| 154 |
+
},
|
| 155 |
+
'dense': { # Aggressive mode for dense crowds (stadiums, concerts)
|
| 156 |
+
'interval': 2, # Process every 2nd frame (faster than normal)
|
| 157 |
+
'confidence': 0.25, # Lower confidence to catch more people
|
| 158 |
+
'iou': 0.35, # Lower IOU to allow more overlap
|
| 159 |
+
'resize': 1.0, # Full resolution
|
| 160 |
+
'min_size': 15, # Smaller minimum size
|
| 161 |
+
'multi_scale': False, # Keep same as normal for compatibility
|
| 162 |
+
'max_det': 500, # Allow more detections
|
| 163 |
+
'imgsz': 416, # MUST match TensorRT engine size
|
| 164 |
+
'second_pass_conf': 0.02, # Much lower for second pass
|
| 165 |
+
'duplicate_threshold': 25, # Slightly tighter duplicate threshold
|
| 166 |
+
'min_box_size': 3 # Accept smaller boxes
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
# Start in normal mode (current working baseline)
|
| 171 |
+
CURRENT_MODE = 'normal'
|
| 172 |
+
active_mode = DETECTION_MODES[CURRENT_MODE]
|
| 173 |
+
|
| 174 |
+
# Detection parameters from config
|
| 175 |
+
DETECTION_INTERVAL = active_mode['interval']
|
| 176 |
+
MIN_CONFIDENCE = active_mode['confidence']
|
| 177 |
+
RESIZE_FACTOR = active_mode['resize']
|
| 178 |
+
MIN_OBJECT_SIZE = active_mode['min_size']
|
| 179 |
+
ENABLE_MULTI_SCALE = active_mode['multi_scale']
|
| 180 |
+
|
| 181 |
+
# Alert thresholds from config
|
| 182 |
+
WARNING_THRESHOLD = config.get('crowd', {}).get('density_threshold', 15)
|
| 183 |
+
CRITICAL_THRESHOLD = config.get('crowd', {}).get('warning_threshold', 25)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def update_state(key, value):
|
| 187 |
+
"""Thread-safe state update"""
|
| 188 |
+
with state_lock:
|
| 189 |
+
state[key] = value
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def get_alert_level(count):
|
| 193 |
+
"""Determine alert level based on count (REQ-7)"""
|
| 194 |
+
if count >= config['crowd']['warning_threshold']:
|
| 195 |
+
return 'critical'
|
| 196 |
+
elif count >= config['crowd']['density_threshold']:
|
| 197 |
+
return 'warning'
|
| 198 |
+
else:
|
| 199 |
+
return 'normal'
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def generate_frames():
|
| 203 |
+
"""Generate video frames with detections - supports both camera and video file"""
|
| 204 |
+
global state
|
| 205 |
+
|
| 206 |
+
logger.info("generate_frames() called")
|
| 207 |
+
|
| 208 |
+
# Wait for running state to be true
|
| 209 |
+
max_wait = 50 # 5 seconds max
|
| 210 |
+
wait_count = 0
|
| 211 |
+
while not state.get('running', False) and wait_count < max_wait:
|
| 212 |
+
time.sleep(0.1)
|
| 213 |
+
wait_count += 1
|
| 214 |
+
|
| 215 |
+
if not state.get('running', False):
|
| 216 |
+
logger.error("Monitoring not started, exiting generate_frames")
|
| 217 |
+
return
|
| 218 |
+
|
| 219 |
+
# Determine video source based on state
|
| 220 |
+
with state_lock:
|
| 221 |
+
source_type = state['source_type']
|
| 222 |
+
video_file = state['video_file']
|
| 223 |
+
|
| 224 |
+
logger.info(f"Source type: {source_type}, Video file: {video_file}")
|
| 225 |
+
logger.info(f"Will use: {'VIDEO FILE' if (source_type == 'video' and video_file) else 'CAMERA'}")
|
| 226 |
+
|
| 227 |
+
if source_type == 'video' and video_file:
|
| 228 |
+
logger.info(f"Opening video file: {video_file}")
|
| 229 |
+
video_path = os.path.join(app.config['UPLOAD_FOLDER'], video_file)
|
| 230 |
+
if not os.path.exists(video_path):
|
| 231 |
+
logger.error(f"Video file not found: {video_path}")
|
| 232 |
+
# Generate error frame
|
| 233 |
+
error_frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 234 |
+
cv2.putText(error_frame, "Video File Not Found", (150, 240),
|
| 235 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
|
| 236 |
+
ret, buffer = cv2.imencode('.jpg', error_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 60])
|
| 237 |
+
if ret:
|
| 238 |
+
yield (b'--frame\r\n'
|
| 239 |
+
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
| 240 |
+
return
|
| 241 |
+
# Set video source properly
|
| 242 |
+
video_handler.set_source(video_path, is_camera=False)
|
| 243 |
+
logger.info(f"Set video source to: {video_path}")
|
| 244 |
+
else:
|
| 245 |
+
logger.info("Opening camera source")
|
| 246 |
+
# Set camera source properly - read from config
|
| 247 |
+
camera_index = config.get('video', {}).get('source', 0)
|
| 248 |
+
video_handler.set_source(camera_index, is_camera=True)
|
| 249 |
+
logger.info(f"Set camera source to: {camera_index}")
|
| 250 |
+
|
| 251 |
+
# Try to open video source with retry logic
|
| 252 |
+
max_retries = 3
|
| 253 |
+
retry_count = 0
|
| 254 |
+
while retry_count < max_retries:
|
| 255 |
+
if video_handler.open():
|
| 256 |
+
break
|
| 257 |
+
retry_count += 1
|
| 258 |
+
logger.warning(f"Failed to open video source, retry {retry_count}/{max_retries}")
|
| 259 |
+
time.sleep(1)
|
| 260 |
+
|
| 261 |
+
if retry_count >= max_retries:
|
| 262 |
+
logger.error("Failed to open video source after retries")
|
| 263 |
+
# Generate error frame
|
| 264 |
+
error_frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 265 |
+
cv2.putText(error_frame, "Camera Not Available", (150, 240),
|
| 266 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
|
| 267 |
+
ret, buffer = cv2.imencode('.jpg', error_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 60])
|
| 268 |
+
if ret:
|
| 269 |
+
yield (b'--frame\r\n'
|
| 270 |
+
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
| 271 |
+
return
|
| 272 |
+
|
| 273 |
+
logger.info("Video source opened successfully")
|
| 274 |
+
frame_count = 0
|
| 275 |
+
start_time = time.time()
|
| 276 |
+
|
| 277 |
+
# Caching for frame skipping
|
| 278 |
+
last_detections = []
|
| 279 |
+
last_count = 0
|
| 280 |
+
last_annotated_frame = None # Initialize to prevent NameError
|
| 281 |
+
consecutive_failures = 0
|
| 282 |
+
max_consecutive_failures = 10
|
| 283 |
+
|
| 284 |
+
try:
|
| 285 |
+
while state['running']:
|
| 286 |
+
ret, frame = video_handler.read_frame()
|
| 287 |
+
|
| 288 |
+
if not ret:
|
| 289 |
+
# Handle video loop on read failure
|
| 290 |
+
if state['source_type'] == 'video' and state['video_loop']:
|
| 291 |
+
logger.info("Video ended, restarting loop...")
|
| 292 |
+
if video_handler.restart():
|
| 293 |
+
frame_count = 0
|
| 294 |
+
start_time = time.time()
|
| 295 |
+
consecutive_failures = 0
|
| 296 |
+
logger.info("Video loop restarted successfully")
|
| 297 |
+
continue
|
| 298 |
+
|
| 299 |
+
# For non-looping videos or cameras, count failures
|
| 300 |
+
consecutive_failures += 1
|
| 301 |
+
logger.warning(f"Failed to read frame (attempt {consecutive_failures}/{max_consecutive_failures})")
|
| 302 |
+
|
| 303 |
+
if consecutive_failures >= max_consecutive_failures:
|
| 304 |
+
logger.error("Too many consecutive frame read failures")
|
| 305 |
+
break
|
| 306 |
+
|
| 307 |
+
time.sleep(0.1)
|
| 308 |
+
continue
|
| 309 |
+
|
| 310 |
+
consecutive_failures = 0 # Reset on successful read
|
| 311 |
+
|
| 312 |
+
# Apply resize factor if configured (performance optimization)
|
| 313 |
+
if RESIZE_FACTOR < 1.0:
|
| 314 |
+
new_width = int(frame.shape[1] * RESIZE_FACTOR)
|
| 315 |
+
new_height = int(frame.shape[0] * RESIZE_FACTOR)
|
| 316 |
+
frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
|
| 317 |
+
|
| 318 |
+
frame_count += 1
|
| 319 |
+
|
| 320 |
+
# Run detection based on configured interval (GPU-optimized)
|
| 321 |
+
# Run on frames 0, DETECTION_INTERVAL, DETECTION_INTERVAL*2, etc.
|
| 322 |
+
should_detect = (frame_count - 1) % DETECTION_INTERVAL == 0
|
| 323 |
+
if should_detect:
|
| 324 |
+
detections, count, detection_time = detector.detect(frame)
|
| 325 |
+
last_detections = detections
|
| 326 |
+
last_count = count
|
| 327 |
+
|
| 328 |
+
# Choose display mode: heatmap-only OR bounding boxes
|
| 329 |
+
if state['heatmap_enabled']:
|
| 330 |
+
# Heatmap mode: Skip bounding boxes for cleaner visualization
|
| 331 |
+
frame_display, heatmap_time = heatmap_generator.generate_heatmap(
|
| 332 |
+
frame, detections # Generator copies internally
|
| 333 |
+
)
|
| 334 |
+
else:
|
| 335 |
+
# Normal mode: Draw bounding boxes (copies frame internally)
|
| 336 |
+
frame_display = detector.draw_detections(frame, detections)
|
| 337 |
+
|
| 338 |
+
# Cache the annotated frame for reuse (no copy needed, frame_display is already a copy)
|
| 339 |
+
last_annotated_frame = frame_display
|
| 340 |
+
else:
|
| 341 |
+
# Reuse cached annotated frame instead of re-drawing (MAJOR OPTIMIZATION)
|
| 342 |
+
detections = last_detections
|
| 343 |
+
count = last_count
|
| 344 |
+
if last_annotated_frame is not None:
|
| 345 |
+
frame_display = last_annotated_frame
|
| 346 |
+
else:
|
| 347 |
+
frame_display = detector.draw_detections(frame, detections)
|
| 348 |
+
|
| 349 |
+
# Update state with proper locking to prevent race conditions
|
| 350 |
+
with state_lock:
|
| 351 |
+
state['current_count'] = count
|
| 352 |
+
# Only track current frame count, not accumulating total (prevents infinite growth)
|
| 353 |
+
state['last_detection_time'] = time.time()
|
| 354 |
+
|
| 355 |
+
# Update alert level based on configurable thresholds
|
| 356 |
+
if count >= CRITICAL_THRESHOLD:
|
| 357 |
+
state['alert_level'] = 'critical'
|
| 358 |
+
elif count >= WARNING_THRESHOLD:
|
| 359 |
+
state['alert_level'] = 'warning'
|
| 360 |
+
else:
|
| 361 |
+
state['alert_level'] = 'normal'
|
| 362 |
+
|
| 363 |
+
# Debug log for detection count (reduced logging frequency)
|
| 364 |
+
if count > 0 and frame_count % 30 == 0: # Log every 30 frames instead of every frame
|
| 365 |
+
logger.debug(f"Detected {count} people in frame {frame_count}")
|
| 366 |
+
|
| 367 |
+
# Calculate FPS using deque for memory efficiency
|
| 368 |
+
current_time = time.time()
|
| 369 |
+
frame_times.append(current_time)
|
| 370 |
+
if len(frame_times) >= 2:
|
| 371 |
+
elapsed = frame_times[-1] - frame_times[0]
|
| 372 |
+
# Update FPS with state lock
|
| 373 |
+
with state_lock:
|
| 374 |
+
state['fps'] = len(frame_times) / elapsed if elapsed > 0 else 0
|
| 375 |
+
|
| 376 |
+
# Encode frame to JPEG with good quality (80% - improved quality)
|
| 377 |
+
ret, buffer = cv2.imencode('.jpg', frame_display, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
|
| 378 |
+
|
| 379 |
+
if ret:
|
| 380 |
+
yield (b'--frame\r\n'
|
| 381 |
+
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
| 382 |
+
|
| 383 |
+
except Exception as e:
|
| 384 |
+
logger.error(f"Error in generate_frames: {e}", exc_info=True)
|
| 385 |
+
# Generate error frame
|
| 386 |
+
error_frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 387 |
+
cv2.putText(error_frame, "Processing Error", (180, 220),
|
| 388 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
|
| 389 |
+
cv2.putText(error_frame, "Check logs for details", (150, 260),
|
| 390 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
|
| 391 |
+
ret, buffer = cv2.imencode('.jpg', error_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 60])
|
| 392 |
+
if ret:
|
| 393 |
+
yield (b'--frame\r\n'
|
| 394 |
+
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
| 395 |
+
finally:
|
| 396 |
+
video_handler.release()
|
| 397 |
+
logger.info("Video handler released")
|
| 398 |
+
# Clear frame times on exit
|
| 399 |
+
frame_times.clear()
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
@app.route('/')
|
| 403 |
+
def index():
|
| 404 |
+
"""Render main page"""
|
| 405 |
+
return render_template('index.html')
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
@app.route('/video_feed')
|
| 409 |
+
def video_feed():
|
| 410 |
+
"""Video streaming route with optimized buffering"""
|
| 411 |
+
return Response(generate_frames(),
|
| 412 |
+
mimetype='multipart/x-mixed-replace; boundary=frame',
|
| 413 |
+
headers={
|
| 414 |
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
| 415 |
+
'Pragma': 'no-cache',
|
| 416 |
+
'Expires': '0'
|
| 417 |
+
})
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
@app.route('/api/start', methods=['POST'])
|
| 421 |
+
def start_monitoring():
|
| 422 |
+
"""Start monitoring (REQ-6)"""
|
| 423 |
+
update_state('running', True)
|
| 424 |
+
logger.info("Monitoring started")
|
| 425 |
+
return jsonify({'status': 'started'})
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
@app.route('/api/stop', methods=['POST'])
|
| 429 |
+
def stop_monitoring():
|
| 430 |
+
"""Stop monitoring"""
|
| 431 |
+
update_state('running', False)
|
| 432 |
+
logger.info("Monitoring stopped")
|
| 433 |
+
return jsonify({'status': 'stopped'})
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
@app.route('/api/upload_video', methods=['POST'])
|
| 437 |
+
def upload_video():
|
| 438 |
+
"""Upload a video file for processing with enhanced validation"""
|
| 439 |
+
try:
|
| 440 |
+
if 'file' not in request.files:
|
| 441 |
+
return jsonify({'error': 'No file provided'}), 400
|
| 442 |
+
|
| 443 |
+
file = request.files['file']
|
| 444 |
+
if file.filename == '':
|
| 445 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 446 |
+
|
| 447 |
+
# Validate file extension
|
| 448 |
+
if not allowed_file(file.filename):
|
| 449 |
+
return jsonify({'error': 'Invalid file type. Allowed: mp4, avi, mov, mkv, webm'}), 400
|
| 450 |
+
|
| 451 |
+
# Additional security: Check file size before saving
|
| 452 |
+
file.seek(0, 2) # Seek to end
|
| 453 |
+
file_size = file.tell()
|
| 454 |
+
file.seek(0) # Reset to beginning
|
| 455 |
+
|
| 456 |
+
if file_size > app.config['MAX_CONTENT_LENGTH']:
|
| 457 |
+
return jsonify({'error': f'File too large. Maximum size is 100MB'}), 400
|
| 458 |
+
|
| 459 |
+
if file_size == 0:
|
| 460 |
+
return jsonify({'error': 'File is empty'}), 400
|
| 461 |
+
|
| 462 |
+
# Save the file with secure filename
|
| 463 |
+
filename = secure_filename(file.filename)
|
| 464 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 465 |
+
filename = f"{timestamp}_{filename}"
|
| 466 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 467 |
+
|
| 468 |
+
file.save(filepath)
|
| 469 |
+
|
| 470 |
+
# Validate video file can be opened and has valid frames
|
| 471 |
+
test_cap = None
|
| 472 |
+
try:
|
| 473 |
+
test_cap = cv2.VideoCapture(filepath)
|
| 474 |
+
if not test_cap.isOpened():
|
| 475 |
+
os.remove(filepath) # Delete invalid file
|
| 476 |
+
return jsonify({'error': 'Invalid video file. Cannot be opened by OpenCV.'}), 400
|
| 477 |
+
|
| 478 |
+
# Verify it has frames
|
| 479 |
+
ret, test_frame = test_cap.read()
|
| 480 |
+
if not ret or test_frame is None:
|
| 481 |
+
os.remove(filepath)
|
| 482 |
+
return jsonify({'error': 'Invalid video file. No readable frames.'}), 400
|
| 483 |
+
finally:
|
| 484 |
+
if test_cap is not None:
|
| 485 |
+
test_cap.release()
|
| 486 |
+
|
| 487 |
+
# Update state to use video file
|
| 488 |
+
with state_lock:
|
| 489 |
+
state['source_type'] = 'video'
|
| 490 |
+
state['video_file'] = filename
|
| 491 |
+
state['video_loop'] = request.form.get('loop', 'false').lower() == 'true'
|
| 492 |
+
|
| 493 |
+
logger.info(f"Video uploaded successfully: {filename}")
|
| 494 |
+
return jsonify({
|
| 495 |
+
'status': 'success',
|
| 496 |
+
'filename': filename,
|
| 497 |
+
'source_type': 'video'
|
| 498 |
+
})
|
| 499 |
+
except Exception as e:
|
| 500 |
+
logger.error(f"Error uploading video: {e}", exc_info=True)
|
| 501 |
+
return jsonify({'error': f'Upload failed: {str(e)}'}), 500
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
@app.route('/api/switch_source', methods=['POST'])
|
| 505 |
+
def switch_source():
|
| 506 |
+
"""Switch between camera and video file"""
|
| 507 |
+
data = request.get_json()
|
| 508 |
+
source_type = data.get('source_type', 'camera')
|
| 509 |
+
|
| 510 |
+
# Stop current monitoring if running
|
| 511 |
+
with state_lock:
|
| 512 |
+
was_running = state['running']
|
| 513 |
+
state['running'] = False
|
| 514 |
+
|
| 515 |
+
time.sleep(0.5) # Allow current stream to stop
|
| 516 |
+
|
| 517 |
+
# Update source - ENSURE camera mode clears video file
|
| 518 |
+
with state_lock:
|
| 519 |
+
state['source_type'] = source_type
|
| 520 |
+
if source_type == 'camera':
|
| 521 |
+
state['video_file'] = None
|
| 522 |
+
logger.info("Camera mode activated - cleared video file from state")
|
| 523 |
+
else:
|
| 524 |
+
logger.info(f"Video mode - current video: {state.get('video_file', 'None')}")
|
| 525 |
+
|
| 526 |
+
logger.info(f"Switched to {source_type} source")
|
| 527 |
+
|
| 528 |
+
return jsonify({
|
| 529 |
+
'status': 'success',
|
| 530 |
+
'source_type': source_type,
|
| 531 |
+
'was_running': was_running
|
| 532 |
+
})
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
@app.route('/api/list_videos', methods=['GET'])
|
| 536 |
+
def list_videos():
|
| 537 |
+
"""List available uploaded videos"""
|
| 538 |
+
try:
|
| 539 |
+
videos = []
|
| 540 |
+
for filename in os.listdir(app.config['UPLOAD_FOLDER']):
|
| 541 |
+
if allowed_file(filename):
|
| 542 |
+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 543 |
+
videos.append({
|
| 544 |
+
'filename': filename,
|
| 545 |
+
'size': os.path.getsize(filepath),
|
| 546 |
+
'modified': datetime.fromtimestamp(os.path.getmtime(filepath)).isoformat()
|
| 547 |
+
})
|
| 548 |
+
return jsonify({'videos': videos})
|
| 549 |
+
except Exception as e:
|
| 550 |
+
logger.error(f"Error listing videos: {e}")
|
| 551 |
+
return jsonify({'error': str(e)}), 500
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
@app.route('/api/toggle_heatmap', methods=['POST'])
|
| 555 |
+
def toggle_heatmap():
|
| 556 |
+
"""Toggle heatmap (REQ-8, REQ-9)"""
|
| 557 |
+
with state_lock:
|
| 558 |
+
state['heatmap_enabled'] = not state['heatmap_enabled']
|
| 559 |
+
logger.info(f"Heatmap {'enabled' if state['heatmap_enabled'] else 'disabled'}")
|
| 560 |
+
return jsonify({'heatmap_enabled': state['heatmap_enabled']})
|
| 561 |
+
|
| 562 |
+
@app.route('/api/set_detection_mode', methods=['POST'])
|
| 563 |
+
def set_detection_mode():
|
| 564 |
+
"""Switch between normal and dense crowd detection modes"""
|
| 565 |
+
global CURRENT_MODE, DETECTION_INTERVAL, MIN_CONFIDENCE, RESIZE_FACTOR
|
| 566 |
+
global MIN_OBJECT_SIZE, ENABLE_MULTI_SCALE
|
| 567 |
+
|
| 568 |
+
data = request.get_json()
|
| 569 |
+
mode = data.get('mode', 'normal')
|
| 570 |
+
|
| 571 |
+
if mode not in DETECTION_MODES:
|
| 572 |
+
return jsonify({'error': f'Invalid mode. Choose: normal or dense'}), 400
|
| 573 |
+
|
| 574 |
+
# Update mode
|
| 575 |
+
CURRENT_MODE = mode
|
| 576 |
+
active_mode = DETECTION_MODES[mode]
|
| 577 |
+
|
| 578 |
+
# Update global parameters
|
| 579 |
+
DETECTION_INTERVAL = active_mode['interval']
|
| 580 |
+
MIN_CONFIDENCE = active_mode['confidence']
|
| 581 |
+
RESIZE_FACTOR = active_mode['resize']
|
| 582 |
+
MIN_OBJECT_SIZE = active_mode['min_size']
|
| 583 |
+
ENABLE_MULTI_SCALE = active_mode['multi_scale']
|
| 584 |
+
|
| 585 |
+
# Update detector instance dynamically
|
| 586 |
+
detector.confidence_threshold = MIN_CONFIDENCE
|
| 587 |
+
detector.iou_threshold = active_mode['iou']
|
| 588 |
+
detector.min_size = MIN_OBJECT_SIZE
|
| 589 |
+
detector.imgsz = active_mode['imgsz']
|
| 590 |
+
detector.max_det = active_mode['max_det']
|
| 591 |
+
detector.second_pass_conf = active_mode['second_pass_conf']
|
| 592 |
+
detector.duplicate_threshold = active_mode['duplicate_threshold']
|
| 593 |
+
detector.min_box_size = active_mode['min_box_size']
|
| 594 |
+
|
| 595 |
+
logger.info(f"Detection mode switched to: {mode}")
|
| 596 |
+
logger.info(f"Settings: interval={DETECTION_INTERVAL}, conf={MIN_CONFIDENCE}, iou={active_mode['iou']}, max_det={active_mode['max_det']}")
|
| 597 |
+
|
| 598 |
+
return jsonify({
|
| 599 |
+
'status': 'success',
|
| 600 |
+
'mode': mode,
|
| 601 |
+
'settings': active_mode
|
| 602 |
+
})
|
| 603 |
+
|
| 604 |
+
@app.route('/api/reset', methods=['POST'])
|
| 605 |
+
def reset_statistics():
|
| 606 |
+
"""Reset statistics"""
|
| 607 |
+
with state_lock:
|
| 608 |
+
state['total_detections'] = 0
|
| 609 |
+
state['count_history'] = []
|
| 610 |
+
state['time_history'] = []
|
| 611 |
+
logger.info("Statistics reset")
|
| 612 |
+
return jsonify({'status': 'reset'})
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
@app.route('/api/optimize', methods=['POST'])
|
| 616 |
+
def optimize_detection():
|
| 617 |
+
"""Manual optimization endpoint for small objects"""
|
| 618 |
+
global MIN_CONFIDENCE, DETECTION_INTERVAL, RESIZE_FACTOR, ENABLE_MULTI_SCALE
|
| 619 |
+
|
| 620 |
+
data = request.get_json()
|
| 621 |
+
if data:
|
| 622 |
+
MIN_CONFIDENCE = data.get('confidence', MIN_CONFIDENCE)
|
| 623 |
+
DETECTION_INTERVAL = max(1, data.get('interval', DETECTION_INTERVAL))
|
| 624 |
+
RESIZE_FACTOR = min(1.0, max(0.3, data.get('resize_factor', RESIZE_FACTOR)))
|
| 625 |
+
ENABLE_MULTI_SCALE = data.get('multi_scale', ENABLE_MULTI_SCALE)
|
| 626 |
+
|
| 627 |
+
logger.info(f"Small object optimization applied: confidence={MIN_CONFIDENCE}, interval={DETECTION_INTERVAL}")
|
| 628 |
+
return jsonify({
|
| 629 |
+
'confidence': MIN_CONFIDENCE,
|
| 630 |
+
'interval': DETECTION_INTERVAL,
|
| 631 |
+
'resize_factor': RESIZE_FACTOR,
|
| 632 |
+
'multi_scale': ENABLE_MULTI_SCALE,
|
| 633 |
+
'min_object_size': MIN_OBJECT_SIZE
|
| 634 |
+
})
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
@app.route('/api/stats')
|
| 638 |
+
def get_statistics():
|
| 639 |
+
"""Get current statistics (REQ-6, REQ-7)"""
|
| 640 |
+
with state_lock:
|
| 641 |
+
return jsonify({
|
| 642 |
+
'count': state['current_count'],
|
| 643 |
+
'fps': round(state['fps'], 1),
|
| 644 |
+
'alert_level': state['alert_level'],
|
| 645 |
+
'total_detections': state['total_detections'],
|
| 646 |
+
'running': state['running'],
|
| 647 |
+
'heatmap_enabled': state['heatmap_enabled'],
|
| 648 |
+
'count_history': state['count_history'][-50:],
|
| 649 |
+
'time_history': state['time_history'][-50:],
|
| 650 |
+
'thresholds': {
|
| 651 |
+
'warning': config['crowd']['density_threshold'],
|
| 652 |
+
'critical': config['crowd']['warning_threshold']
|
| 653 |
+
},
|
| 654 |
+
'optimization': {
|
| 655 |
+
'confidence': MIN_CONFIDENCE,
|
| 656 |
+
'detection_interval': DETECTION_INTERVAL,
|
| 657 |
+
'resize_factor': RESIZE_FACTOR,
|
| 658 |
+
'multi_scale': ENABLE_MULTI_SCALE,
|
| 659 |
+
'min_object_size': MIN_OBJECT_SIZE
|
| 660 |
+
}
|
| 661 |
+
})
|
| 662 |
+
|
| 663 |
+
|
| 664 |
+
@app.route('/api/config', methods=['GET'])
|
| 665 |
+
def get_config():
|
| 666 |
+
"""Get system configuration"""
|
| 667 |
+
return jsonify({
|
| 668 |
+
'video_source': config['video']['source'],
|
| 669 |
+
'confidence_threshold': config['model']['confidence_threshold'],
|
| 670 |
+
'density_threshold': config['crowd']['density_threshold'],
|
| 671 |
+
'warning_threshold': config['crowd']['warning_threshold'],
|
| 672 |
+
'small_object_optimization': {
|
| 673 |
+
'min_confidence': MIN_CONFIDENCE,
|
| 674 |
+
'detection_interval': DETECTION_INTERVAL,
|
| 675 |
+
'resize_factor': RESIZE_FACTOR,
|
| 676 |
+
'multi_scale': ENABLE_MULTI_SCALE,
|
| 677 |
+
'min_object_size': MIN_OBJECT_SIZE
|
| 678 |
+
}
|
| 679 |
+
})
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
@app.route('/api/health')
|
| 683 |
+
def health_check():
|
| 684 |
+
"""System health check"""
|
| 685 |
+
with state_lock:
|
| 686 |
+
return jsonify({
|
| 687 |
+
'status': 'healthy',
|
| 688 |
+
'running': state['running'],
|
| 689 |
+
'fps': state['fps'],
|
| 690 |
+
'current_count': state['current_count'],
|
| 691 |
+
'timestamp': datetime.now().isoformat()
|
| 692 |
+
})
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
if __name__ == '__main__':
|
| 696 |
+
import os
|
| 697 |
+
port = int(os.environ.get('PORT', 5000))
|
| 698 |
+
logger.info("Starting Enhanced Zaytrics Web Server (Small Object Optimized)")
|
| 699 |
+
logger.info(f"Access the dashboard at: http://localhost:{port}")
|
| 700 |
+
logger.info("Small Object Detection Optimizations:")
|
| 701 |
+
logger.info(f" - Detection interval: {DETECTION_INTERVAL} frames")
|
| 702 |
+
logger.info(f" - Minimum confidence: {MIN_CONFIDENCE}")
|
| 703 |
+
logger.info(f" - Resize factor: {RESIZE_FACTOR}")
|
| 704 |
+
logger.info(f" - Multi-scale detection: {ENABLE_MULTI_SCALE}")
|
| 705 |
+
logger.info(f" - Minimum object size: {MIN_OBJECT_SIZE} pixels")
|
| 706 |
+
|
| 707 |
+
app.run(host='0.0.0.0', port=port, debug=False, threaded=True)
|
config.yaml
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Zaytrics Smart Crowd Monitoring System - Configuration File
|
| 2 |
+
# Version 2.0 - Enhanced for Small Object Detection
|
| 3 |
+
|
| 4 |
+
# Model Configuration
|
| 5 |
+
model:
|
| 6 |
+
name: "yolov8m.pt" # MEDIUM model - best accuracy
|
| 7 |
+
confidence_threshold: 0.35
|
| 8 |
+
iou_threshold: 0.45
|
| 9 |
+
device: "cpu" # Using RTX 3050 GPU for acceleration (auto-detects cuda:0)
|
| 10 |
+
class_filter: [0] # Class IDs to detect (0 = person in COCO dataset)
|
| 11 |
+
|
| 12 |
+
# Small Object Detection Parameters
|
| 13 |
+
small_object_mode: true # Enable specialized small object detection
|
| 14 |
+
min_confidence_small: 0.05 # Low confidence for second pass
|
| 15 |
+
multi_scale: false
|
| 16 |
+
small_object_threshold: 50 # Pixel size below which object is considered "small"
|
| 17 |
+
|
| 18 |
+
# Performance Optimization
|
| 19 |
+
detection_interval: 3 # Run detection every 3rd frame for better performance
|
| 20 |
+
resize_factor: 1.0 # Full resolution for best accuracy
|
| 21 |
+
|
| 22 |
+
# Video Input Configuration
|
| 23 |
+
video:
|
| 24 |
+
source: 0 # 0 for webcam, or path to video file e.g., "videos/crowd.mp4"
|
| 25 |
+
fps: 30 # Target frames per second
|
| 26 |
+
resolution:
|
| 27 |
+
width: 640 # Balanced resolution for YOLOv8s
|
| 28 |
+
height: 480
|
| 29 |
+
buffer_size: 1 # Reduced buffer for lower latency
|
| 30 |
+
skip_frames: 1 # Process every frame by default
|
| 31 |
+
|
| 32 |
+
# Crowd Detection Settings
|
| 33 |
+
crowd:
|
| 34 |
+
density_threshold: 15 # Alert threshold for crowd count
|
| 35 |
+
warning_threshold: 25 # Critical warning threshold
|
| 36 |
+
min_detection_size: 20 # Minimum bounding box size in pixels
|
| 37 |
+
max_detection_size: 400 # Maximum bounding box size to filter outliers
|
| 38 |
+
|
| 39 |
+
# Small Object Settings
|
| 40 |
+
enable_size_filtering: true
|
| 41 |
+
min_small_object_size: 10 # Absolute minimum size for valid detection
|
| 42 |
+
small_object_boost: true # Apply special processing for small objects
|
| 43 |
+
|
| 44 |
+
# Heatmap Configuration
|
| 45 |
+
heatmap:
|
| 46 |
+
enabled: false # Enable/disable heatmap generation
|
| 47 |
+
kernel_size: 30 # Base kernel size (used when adaptive is false)
|
| 48 |
+
alpha: 0.6 # Heatmap overlay transparency (0.0 - 1.0)
|
| 49 |
+
colormap: "jet" # Options: jet, hot, viridis, plasma
|
| 50 |
+
|
| 51 |
+
# Adaptive Heatmap Settings (better for mixed close/far videos)
|
| 52 |
+
adaptive: true # Enable adaptive kernel sizing based on detection size
|
| 53 |
+
min_kernel_size: 30 # Minimum kernel size for far videos (small people)
|
| 54 |
+
max_kernel_size: 150 # Maximum kernel size for close videos (large people)
|
| 55 |
+
blur_strength: 0.6 # Gaussian sigma multiplier for smoother blending
|
| 56 |
+
intensity_boost: 1.2 # Boost intensity for small object visibility
|
| 57 |
+
|
| 58 |
+
# Preprocessing Configuration
|
| 59 |
+
preprocessing:
|
| 60 |
+
enable_enhancement: true # Enable frame enhancement for small objects
|
| 61 |
+
clahe_clip_limit: 2.0 # Contrast enhancement strength
|
| 62 |
+
clahe_grid_size: [8, 8] # CLAHE grid size
|
| 63 |
+
sharpening_strength: 1.2 # Sharpening filter strength
|
| 64 |
+
noise_reduction: true # Enable noise reduction
|
| 65 |
+
|
| 66 |
+
# Small Object Enhancement
|
| 67 |
+
small_object_enhancement: true
|
| 68 |
+
contrast_boost: 1.1
|
| 69 |
+
detail_enhancement: true
|
| 70 |
+
|
| 71 |
+
# Dashboard Settings
|
| 72 |
+
dashboard:
|
| 73 |
+
title: "Zaytrics Smart Crowd Monitoring System - Enhanced"
|
| 74 |
+
theme: "dark" # Options: dark, light
|
| 75 |
+
refresh_rate: 1 # Dashboard refresh rate in seconds
|
| 76 |
+
show_fps: true # Display FPS counter
|
| 77 |
+
show_confidence: true # Show detection confidence scores
|
| 78 |
+
show_small_objects: true # Highlight small object detections
|
| 79 |
+
alert_sound: false # Enable audio alerts
|
| 80 |
+
|
| 81 |
+
# Visualization Enhancements
|
| 82 |
+
small_object_color: [255, 255, 0] # Yellow for small objects
|
| 83 |
+
normal_object_color: [0, 255, 0] # Green for normal objects
|
| 84 |
+
show_centers: true # Show center points for small objects
|
| 85 |
+
|
| 86 |
+
# Performance Settings
|
| 87 |
+
performance:
|
| 88 |
+
max_processing_time: 0.5 # Reduced for real-time performance
|
| 89 |
+
max_queue_size: 5 # Smaller queue for lower latency
|
| 90 |
+
enable_profiling: false # Enable performance profiling
|
| 91 |
+
adaptive_processing: true # Adjust processing based on FPS
|
| 92 |
+
|
| 93 |
+
# Small Object Performance
|
| 94 |
+
small_object_interval: 3 # Run small object detection every Nth frame
|
| 95 |
+
cache_detections: true # Cache detections between frames
|
| 96 |
+
max_cache_age: 0.3 # Maximum age for cached detections (seconds)
|
| 97 |
+
|
| 98 |
+
# Logging Configuration
|
| 99 |
+
logging:
|
| 100 |
+
level: "INFO" # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
| 101 |
+
save_logs: true # Save logs to file
|
| 102 |
+
log_file: "logs/system.log"
|
| 103 |
+
log_detections: false # Log individual detections (verbose)
|
| 104 |
+
|
| 105 |
+
# Data Storage
|
| 106 |
+
storage:
|
| 107 |
+
save_detections: false # Save detection results to CSV
|
| 108 |
+
save_frames: false # Save processed frames
|
| 109 |
+
output_directory: "output"
|
| 110 |
+
save_small_object_stats: true # Track small object detection statistics
|
| 111 |
+
|
| 112 |
+
# System Constraints (from SRS)
|
| 113 |
+
constraints:
|
| 114 |
+
min_accuracy: 0.75 # Slightly reduced for small object focus
|
| 115 |
+
max_frame_delay: 0.5 # Reduced delay requirement for real-time
|
| 116 |
+
max_heatmap_delay: 1.0 # Reduced heatmap delay
|
| 117 |
+
|
| 118 |
+
# Small Object Specific Constraints
|
| 119 |
+
min_small_object_accuracy: 0.65 # Minimum accuracy for small objects
|
| 120 |
+
max_small_object_size: 50 # Maximum size considered "small"
|
| 121 |
+
|
| 122 |
+
# Alert System
|
| 123 |
+
alerts:
|
| 124 |
+
enable_crowd_alerts: true
|
| 125 |
+
enable_small_object_alerts: false # Special alerts for small object detection
|
| 126 |
+
alert_cooldown: 5 # Seconds between repeated alerts
|
| 127 |
+
|
| 128 |
+
# Small Object Alert Settings
|
| 129 |
+
small_object_threshold: 5 # Minimum small objects to trigger alert
|
| 130 |
+
small_object_density_alert: 10 # Alert if small object density is high
|
| 131 |
+
|
| 132 |
+
# Advanced Settings
|
| 133 |
+
advanced:
|
| 134 |
+
enable_debug_overlay: false # Show debug information on video
|
| 135 |
+
save_detection_samples: false # Save samples of detections for analysis
|
| 136 |
+
detection_sample_interval: 30 # Save sample every N seconds
|
| 137 |
+
|
| 138 |
+
# Small Object Analysis
|
| 139 |
+
track_small_objects: true
|
| 140 |
+
small_object_analysis: false # Advanced analysis of small object patterns
|
| 141 |
+
enable_iou_tracking: true # Use IoU for tracking small objects between frames
|
requirements.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Zaytrics Smart Crowd Monitoring System - Dependencies
|
| 2 |
+
# Python 3.8+ required
|
| 3 |
+
|
| 4 |
+
# Core ML/CV Libraries
|
| 5 |
+
opencv-python==4.8.1.78
|
| 6 |
+
numpy==1.24.3
|
| 7 |
+
pillow==10.0.1
|
| 8 |
+
|
| 9 |
+
# YOLOv8 and Detection
|
| 10 |
+
ultralytics==8.0.196
|
| 11 |
+
#torch==2.1.0
|
| 12 |
+
#torchvision==0.16.0
|
| 13 |
+
|
| 14 |
+
# Web Framework
|
| 15 |
+
flask==3.0.0
|
| 16 |
+
|
| 17 |
+
# Visualization
|
| 18 |
+
matplotlib==3.8.0
|
| 19 |
+
seaborn==0.13.0
|
| 20 |
+
plotly==5.17.0
|
| 21 |
+
|
| 22 |
+
# Utilities
|
| 23 |
+
pandas==2.1.1
|
| 24 |
+
pyyaml==6.0.1
|
| 25 |
+
tqdm==4.66.1
|
| 26 |
+
|
| 27 |
+
# Optional GPU support (comment out if CPU only)
|
| 28 |
+
# Install manually if needed:
|
| 29 |
+
# pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
|
| 30 |
+
flask-cors==4.0.0
|
runtime.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
python-3.11.0
|
src/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Zaytrics Smart Crowd Monitoring System
|
| 3 |
+
Version 1.0
|
| 4 |
+
|
| 5 |
+
A computer vision-based application for real-time crowd detection,
|
| 6 |
+
density estimation, and monitoring dashboard.
|
| 7 |
+
|
| 8 |
+
Organization: SEECS, NUST
|
| 9 |
+
Prepared by: AMMO
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
__version__ = "1.0.0"
|
| 13 |
+
__author__ = "AMMO"
|
| 14 |
+
__organization__ = "SEECS, NUST"
|
src/detection/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Detection module for YOLOv8-based crowd detection
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .detector import CrowdDetector
|
| 6 |
+
|
| 7 |
+
__all__ = ['CrowdDetector']
|
src/detection/detector.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
YOLOv8 Crowd Detection Module - Enhanced for Small Objects
|
| 3 |
+
Implements REQ-1, REQ-2, REQ-3: Person detection with bounding boxes and counting
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import cv2
|
| 7 |
+
import numpy as np
|
| 8 |
+
from ultralytics import YOLO
|
| 9 |
+
import time
|
| 10 |
+
from typing import List, Tuple, Dict
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class CrowdDetector:
|
| 17 |
+
"""
|
| 18 |
+
Main crowd detection class using YOLOv8 - Enhanced for small objects
|
| 19 |
+
|
| 20 |
+
Satisfies SRS Requirements:
|
| 21 |
+
- REQ-1: Detect individuals in video frames using pre-trained model
|
| 22 |
+
- REQ-2: Display bounding boxes around detected individuals
|
| 23 |
+
- REQ-3: Update count continuously as frames are processed
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, config: Dict):
|
| 27 |
+
"""
|
| 28 |
+
Initialize the YOLOv8 detector
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
config: Configuration dictionary from config.yaml
|
| 32 |
+
"""
|
| 33 |
+
self.config = config
|
| 34 |
+
self.model_name = config['model']['name']
|
| 35 |
+
self.confidence_threshold = config['model']['confidence_threshold']
|
| 36 |
+
self.iou_threshold = config['model']['iou_threshold']
|
| 37 |
+
self.device = config['model']['device']
|
| 38 |
+
self.class_filter = config['model']['class_filter']
|
| 39 |
+
self.min_size = config['crowd']['min_detection_size']
|
| 40 |
+
|
| 41 |
+
# Optimization parameters
|
| 42 |
+
self.small_object_mode = config['model'].get('small_object_mode', True)
|
| 43 |
+
self.imgsz = 416 # Lower resolution for faster TensorRT inference
|
| 44 |
+
|
| 45 |
+
# Dynamic mode parameters (can be updated via API)
|
| 46 |
+
self.max_det = 300 # Default max detections
|
| 47 |
+
self.second_pass_conf = 0.05 # Default second pass confidence
|
| 48 |
+
self.duplicate_threshold = 30 # Default duplicate detection threshold
|
| 49 |
+
self.min_box_size = 5 # Default minimum box size
|
| 50 |
+
|
| 51 |
+
# Performance tracking
|
| 52 |
+
from collections import deque
|
| 53 |
+
self.frame_times = deque(maxlen=30) # Keep last 30 frame times
|
| 54 |
+
self.detection_count = 0
|
| 55 |
+
self.frame_count = 0 # Track frames for logging throttle
|
| 56 |
+
|
| 57 |
+
logger.info(f"Initializing YOLOv8 Detector with model: {self.model_name}")
|
| 58 |
+
logger.info(f"Device: {self.device}, Confidence: {self.confidence_threshold}")
|
| 59 |
+
logger.info(f"Small object mode: {self.small_object_mode}")
|
| 60 |
+
|
| 61 |
+
# Check for TensorRT optimized model first
|
| 62 |
+
tensorrt_model = self.model_name.replace('.pt', '.engine')
|
| 63 |
+
use_tensorrt = False
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
import os
|
| 67 |
+
if os.path.exists(tensorrt_model):
|
| 68 |
+
logger.info(f"Loading TensorRT optimized model: {tensorrt_model}")
|
| 69 |
+
self.model = YOLO(tensorrt_model)
|
| 70 |
+
use_tensorrt = True
|
| 71 |
+
else:
|
| 72 |
+
logger.info(f"Loading PyTorch model: {self.model_name}")
|
| 73 |
+
logger.info(f"TIP: Export to TensorRT for 2-3x speedup: yolo export model={self.model_name} format=engine half=True device=0")
|
| 74 |
+
self.model = YOLO(self.model_name)
|
| 75 |
+
self.model.to(self.device)
|
| 76 |
+
use_tensorrt = False
|
| 77 |
+
|
| 78 |
+
logger.info("YOLOv8 model loaded successfully")
|
| 79 |
+
logger.info(f"*** USING {'TensorRT ENGINE' if use_tensorrt else 'PyTorch FP16'} for inference ***")
|
| 80 |
+
|
| 81 |
+
# GPU Warmup - run dummy inference to compile CUDA kernels
|
| 82 |
+
logger.info("Warming up GPU (this may take a few seconds)...")
|
| 83 |
+
dummy_frame = np.zeros((416, 416, 3), dtype=np.uint8)
|
| 84 |
+
for _ in range(5): # Run 5 warmup passes for better optimization
|
| 85 |
+
self.model(dummy_frame, conf=0.5, verbose=False, device=self.device, half=(self.device != "cpu"), imgsz=self.imgsz)
|
| 86 |
+
logger.info("GPU warmup complete - ready for fast inference")
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Failed to load YOLOv8 model: {e}")
|
| 90 |
+
raise
|
| 91 |
+
|
| 92 |
+
def preprocess_frame(self, frame: np.ndarray) -> np.ndarray:
|
| 93 |
+
"""
|
| 94 |
+
Light preprocessing - only applied if needed
|
| 95 |
+
Returns frame as-is for speed (YOLO handles normalization)
|
| 96 |
+
"""
|
| 97 |
+
return frame # Skip preprocessing for speed
|
| 98 |
+
|
| 99 |
+
def detect_additional_pass(self, frame: np.ndarray, existing_detections: List[Dict]) -> List[Dict]:
|
| 100 |
+
"""
|
| 101 |
+
Additional detection pass with very low confidence for missed small objects
|
| 102 |
+
"""
|
| 103 |
+
try:
|
| 104 |
+
# Second pass with lower confidence threshold for small/distant objects
|
| 105 |
+
results = self.model(
|
| 106 |
+
frame,
|
| 107 |
+
conf=self.second_pass_conf, # Use dynamic second pass confidence
|
| 108 |
+
iou=self.iou_threshold,
|
| 109 |
+
classes=self.class_filter,
|
| 110 |
+
verbose=False,
|
| 111 |
+
imgsz=self.imgsz,
|
| 112 |
+
device=self.device,
|
| 113 |
+
half=(self.device != "cpu")
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
additional_detections = []
|
| 117 |
+
existing_centers = [(d['center'][0], d['center'][1]) for d in existing_detections]
|
| 118 |
+
|
| 119 |
+
for result in results:
|
| 120 |
+
boxes = result.boxes
|
| 121 |
+
|
| 122 |
+
for box in boxes:
|
| 123 |
+
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
| 124 |
+
confidence = float(box.conf[0].cpu().numpy())
|
| 125 |
+
class_id = int(box.cls[0].cpu().numpy())
|
| 126 |
+
|
| 127 |
+
width = x2 - x1
|
| 128 |
+
height = y2 - y1
|
| 129 |
+
center_x = int((x1 + x2) / 2)
|
| 130 |
+
center_y = int((y1 + y2) / 2)
|
| 131 |
+
|
| 132 |
+
# Filter out very small noise using dynamic min_box_size
|
| 133 |
+
if width < self.min_box_size or height < self.min_box_size:
|
| 134 |
+
continue
|
| 135 |
+
|
| 136 |
+
# Check if this is a duplicate (near existing detection)
|
| 137 |
+
is_duplicate = False
|
| 138 |
+
for ex, ey in existing_centers:
|
| 139 |
+
distance = ((center_x - ex)**2 + (center_y - ey)**2)**0.5
|
| 140 |
+
if distance < self.duplicate_threshold: # Use dynamic threshold
|
| 141 |
+
is_duplicate = True
|
| 142 |
+
break
|
| 143 |
+
|
| 144 |
+
if not is_duplicate:
|
| 145 |
+
detection = {
|
| 146 |
+
'bbox': [int(x1), int(y1), int(x2), int(y2)],
|
| 147 |
+
'confidence': confidence,
|
| 148 |
+
'class_id': class_id,
|
| 149 |
+
'class_name': 'person',
|
| 150 |
+
'center': [center_x, center_y],
|
| 151 |
+
'size': 'tiny' if (width < 10 or height < 10) else ('small' if (width < 50 or height < 50) else 'normal')
|
| 152 |
+
}
|
| 153 |
+
additional_detections.append(detection)
|
| 154 |
+
|
| 155 |
+
return additional_detections
|
| 156 |
+
|
| 157 |
+
except Exception as e:
|
| 158 |
+
logger.error(f"Additional detection pass error: {e}")
|
| 159 |
+
return []
|
| 160 |
+
|
| 161 |
+
def detect(self, frame: np.ndarray, resize_factor: float = 1.0,
|
| 162 |
+
confidence_threshold: float = None) -> Tuple[List[Dict], int, float]:
|
| 163 |
+
"""
|
| 164 |
+
Detect people in the frame using YOLOv8 with TensorRT
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
frame: Input frame (BGR format from OpenCV)
|
| 168 |
+
resize_factor: Ignored - always uses full resolution for best accuracy
|
| 169 |
+
confidence_threshold: Optional override for detection threshold
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
detections: List of all detected people (primary + second pass)
|
| 173 |
+
count: Total number of people detected
|
| 174 |
+
processing_time: Time taken for detection
|
| 175 |
+
"""
|
| 176 |
+
start_time = time.time()
|
| 177 |
+
detections = []
|
| 178 |
+
|
| 179 |
+
if confidence_threshold is None:
|
| 180 |
+
confidence_threshold = self.confidence_threshold
|
| 181 |
+
|
| 182 |
+
try:
|
| 183 |
+
# Primary detection with CUDA using configured device
|
| 184 |
+
results = self.model(
|
| 185 |
+
frame,
|
| 186 |
+
conf=confidence_threshold,
|
| 187 |
+
iou=self.iou_threshold,
|
| 188 |
+
classes=self.class_filter,
|
| 189 |
+
verbose=False,
|
| 190 |
+
imgsz=self.imgsz,
|
| 191 |
+
device=self.device,
|
| 192 |
+
half=(self.device != "cpu"), # FP16 inference for 2x speedup on RTX 3050
|
| 193 |
+
max_det=self.max_det, # Use dynamic max detections
|
| 194 |
+
agnostic_nms=False,
|
| 195 |
+
retina_masks=False # Disable for speed
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# Extract primary detections
|
| 199 |
+
for result in results:
|
| 200 |
+
boxes = result.boxes
|
| 201 |
+
|
| 202 |
+
for box in boxes:
|
| 203 |
+
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
|
| 204 |
+
confidence = float(box.conf[0].cpu().numpy())
|
| 205 |
+
class_id = int(box.cls[0].cpu().numpy())
|
| 206 |
+
|
| 207 |
+
width = x2 - x1
|
| 208 |
+
height = y2 - y1
|
| 209 |
+
|
| 210 |
+
# Filter by minimum size to remove noise
|
| 211 |
+
if width >= self.min_size and height >= self.min_size:
|
| 212 |
+
detection = {
|
| 213 |
+
'bbox': [int(x1), int(y1), int(x2), int(y2)],
|
| 214 |
+
'confidence': confidence,
|
| 215 |
+
'class_id': class_id,
|
| 216 |
+
'class_name': 'person',
|
| 217 |
+
'center': [int((x1 + x2) / 2), int((y1 + y2) / 2)],
|
| 218 |
+
'size': 'tiny' if (width < 10 or height < 10) else ('small' if (width < 50 or height < 50) else 'normal')
|
| 219 |
+
}
|
| 220 |
+
detections.append(detection)
|
| 221 |
+
|
| 222 |
+
# Second pass for better small object detection
|
| 223 |
+
if self.small_object_mode:
|
| 224 |
+
additional = self.detect_additional_pass(frame, detections)
|
| 225 |
+
detections.extend(additional)
|
| 226 |
+
|
| 227 |
+
processing_time = time.time() - start_time
|
| 228 |
+
self.frame_times.append(processing_time)
|
| 229 |
+
self.detection_count += len(detections)
|
| 230 |
+
self.frame_count += 1
|
| 231 |
+
|
| 232 |
+
count = len(detections)
|
| 233 |
+
|
| 234 |
+
# Log detection count occasionally (every 30 frames) to avoid log spam
|
| 235 |
+
if self.frame_count % 30 == 0 or count > 0:
|
| 236 |
+
logger.debug(f"Detected {count} people in {processing_time:.3f}s (frame {self.frame_count})")
|
| 237 |
+
|
| 238 |
+
return detections, count, processing_time
|
| 239 |
+
|
| 240 |
+
except Exception as e:
|
| 241 |
+
logger.error(f"Detection error: {e}")
|
| 242 |
+
import traceback
|
| 243 |
+
logger.error(traceback.format_exc())
|
| 244 |
+
return [], 0, 0.0
|
| 245 |
+
|
| 246 |
+
def _calculate_iou(self, box1: List[int], box2: List[int]) -> float:
|
| 247 |
+
"""
|
| 248 |
+
Calculate Intersection over Union for two bounding boxes with edge case handling
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
box1: [x1, y1, x2, y2]
|
| 252 |
+
box2: [x1, y1, x2, y2]
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
IoU value between 0.0 and 1.0
|
| 256 |
+
"""
|
| 257 |
+
# Calculate intersection area
|
| 258 |
+
x1_inter = max(box1[0], box2[0])
|
| 259 |
+
y1_inter = max(box1[1], box2[1])
|
| 260 |
+
x2_inter = min(box1[2], box2[2])
|
| 261 |
+
y2_inter = min(box1[3], box2[3])
|
| 262 |
+
|
| 263 |
+
inter_area = max(0, x2_inter - x1_inter) * max(0, y2_inter - y1_inter)
|
| 264 |
+
|
| 265 |
+
# Calculate union area
|
| 266 |
+
box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
|
| 267 |
+
box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
|
| 268 |
+
|
| 269 |
+
# Handle edge cases
|
| 270 |
+
if box1_area <= 0 or box2_area <= 0:
|
| 271 |
+
return 0.0
|
| 272 |
+
|
| 273 |
+
union_area = box1_area + box2_area - inter_area
|
| 274 |
+
|
| 275 |
+
# Avoid division by zero
|
| 276 |
+
if union_area <= 0:
|
| 277 |
+
return 0.0
|
| 278 |
+
|
| 279 |
+
return inter_area / union_area
|
| 280 |
+
|
| 281 |
+
def draw_detections(self, frame: np.ndarray, detections: List[Dict],
|
| 282 |
+
show_confidence: bool = True) -> np.ndarray:
|
| 283 |
+
"""
|
| 284 |
+
Draw bounding boxes with high visibility for crowd detection
|
| 285 |
+
Color-coded by confidence level
|
| 286 |
+
|
| 287 |
+
Args:
|
| 288 |
+
frame: Input frame
|
| 289 |
+
detections: List of detections
|
| 290 |
+
show_confidence: Whether to display confidence scores
|
| 291 |
+
|
| 292 |
+
Returns:
|
| 293 |
+
frame: Frame with drawn bounding boxes
|
| 294 |
+
"""
|
| 295 |
+
frame_copy = frame.copy()
|
| 296 |
+
|
| 297 |
+
for i, det in enumerate(detections):
|
| 298 |
+
x1, y1, x2, y2 = det['bbox']
|
| 299 |
+
confidence = det['confidence']
|
| 300 |
+
is_small = det.get('size') == 'small'
|
| 301 |
+
|
| 302 |
+
# Color by confidence: Green (high) -> Yellow (medium) -> Orange (low)
|
| 303 |
+
if confidence >= 0.5:
|
| 304 |
+
color = (0, 255, 0) # Green - high confidence
|
| 305 |
+
elif confidence >= 0.25:
|
| 306 |
+
color = (0, 255, 255) # Yellow - medium
|
| 307 |
+
elif confidence >= 0.15:
|
| 308 |
+
color = (0, 165, 255) # Orange - lower
|
| 309 |
+
else:
|
| 310 |
+
color = (0, 128, 255) # Light orange - very low
|
| 311 |
+
|
| 312 |
+
thickness = 1 if is_small else 2
|
| 313 |
+
|
| 314 |
+
# Draw bounding box
|
| 315 |
+
cv2.rectangle(frame_copy, (x1, y1), (x2, y2), color, thickness)
|
| 316 |
+
|
| 317 |
+
# Draw center dot for all detections
|
| 318 |
+
center_x, center_y = det['center']
|
| 319 |
+
cv2.circle(frame_copy, (center_x, center_y), 2, color, -1)
|
| 320 |
+
|
| 321 |
+
# Draw prominent count display in top-left corner
|
| 322 |
+
count = len(detections)
|
| 323 |
+
count_text = f"PEOPLE: {count}"
|
| 324 |
+
|
| 325 |
+
# Background box for count
|
| 326 |
+
(text_w, text_h), _ = cv2.getTextSize(count_text, cv2.FONT_HERSHEY_SIMPLEX, 1.0, 2)
|
| 327 |
+
cv2.rectangle(frame_copy, (5, 5), (text_w + 15, text_h + 15), (0, 0, 0), -1)
|
| 328 |
+
cv2.rectangle(frame_copy, (5, 5), (text_w + 15, text_h + 15), (0, 255, 0), 2)
|
| 329 |
+
|
| 330 |
+
# Count text
|
| 331 |
+
cv2.putText(frame_copy, count_text, (10, text_h + 8),
|
| 332 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2)
|
| 333 |
+
|
| 334 |
+
return frame_copy
|
| 335 |
+
|
| 336 |
+
def get_statistics(self) -> Dict:
|
| 337 |
+
"""
|
| 338 |
+
Get detection statistics
|
| 339 |
+
|
| 340 |
+
Returns:
|
| 341 |
+
stats: Dictionary with performance metrics
|
| 342 |
+
"""
|
| 343 |
+
if not self.frame_times:
|
| 344 |
+
return {
|
| 345 |
+
'avg_processing_time': 0.0,
|
| 346 |
+
'fps': 0.0,
|
| 347 |
+
'total_detections': 0,
|
| 348 |
+
'frames_processed': 0
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
avg_time = np.mean(self.frame_times[-100:]) # Last 100 frames
|
| 352 |
+
fps = 1.0 / avg_time if avg_time > 0 else 0.0
|
| 353 |
+
|
| 354 |
+
return {
|
| 355 |
+
'avg_processing_time': avg_time,
|
| 356 |
+
'fps': fps,
|
| 357 |
+
'total_detections': self.detection_count,
|
| 358 |
+
'frames_processed': len(self.frame_times),
|
| 359 |
+
'max_processing_time': max(self.frame_times) if self.frame_times else 0.0,
|
| 360 |
+
'min_processing_time': min(self.frame_times) if self.frame_times else 0.0
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
def reset_statistics(self):
|
| 364 |
+
"""Reset detection statistics"""
|
| 365 |
+
from collections import deque
|
| 366 |
+
self.frame_times = deque(maxlen=30)
|
| 367 |
+
self.detection_count = 0
|
| 368 |
+
self.frame_count = 0
|
| 369 |
+
logger.info("Detection statistics reset")
|
src/heatmap/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Heatmap generation module for crowd density visualization
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .generator import HeatmapGenerator
|
| 6 |
+
|
| 7 |
+
__all__ = ['HeatmapGenerator']
|
src/heatmap/generator.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Crowd Density Heatmap Generator
|
| 3 |
+
Implements REQ-4, REQ-5: Generate and visualize crowd density zones
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import cv2
|
| 7 |
+
import numpy as np
|
| 8 |
+
from typing import List, Dict, Tuple
|
| 9 |
+
import time
|
| 10 |
+
import logging
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class HeatmapGenerator:
|
| 16 |
+
"""
|
| 17 |
+
Generate crowd density heatmaps
|
| 18 |
+
|
| 19 |
+
Satisfies SRS Requirements:
|
| 20 |
+
- REQ-4: Generate localized density zones
|
| 21 |
+
- REQ-5: Apply color map representing crowd concentration
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, config: Dict):
|
| 25 |
+
"""
|
| 26 |
+
Initialize heatmap generator
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
config: Configuration dictionary
|
| 30 |
+
"""
|
| 31 |
+
self.config = config
|
| 32 |
+
self.enabled = config['heatmap']['enabled']
|
| 33 |
+
self.kernel_size = config['heatmap']['kernel_size']
|
| 34 |
+
self.alpha = config['heatmap']['alpha']
|
| 35 |
+
self.colormap_name = config['heatmap']['colormap']
|
| 36 |
+
|
| 37 |
+
# Adaptive heatmap settings
|
| 38 |
+
self.adaptive = config['heatmap'].get('adaptive', True)
|
| 39 |
+
self.min_kernel_size = config['heatmap'].get('min_kernel_size', 30)
|
| 40 |
+
self.max_kernel_size = config['heatmap'].get('max_kernel_size', 150)
|
| 41 |
+
self.blur_strength = config['heatmap'].get('blur_strength', 0.6)
|
| 42 |
+
|
| 43 |
+
# Map colormap name to OpenCV constant
|
| 44 |
+
colormap_dict = {
|
| 45 |
+
'jet': cv2.COLORMAP_JET,
|
| 46 |
+
'hot': cv2.COLORMAP_HOT,
|
| 47 |
+
'viridis': cv2.COLORMAP_VIRIDIS,
|
| 48 |
+
'plasma': cv2.COLORMAP_PLASMA,
|
| 49 |
+
'rainbow': cv2.COLORMAP_RAINBOW,
|
| 50 |
+
'cool': cv2.COLORMAP_COOL
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
self.colormap = colormap_dict.get(self.colormap_name, cv2.COLORMAP_JET)
|
| 54 |
+
|
| 55 |
+
# Performance tracking
|
| 56 |
+
self.generation_times = []
|
| 57 |
+
|
| 58 |
+
logger.info(f"Heatmap Generator initialized: Enabled={self.enabled}")
|
| 59 |
+
logger.info(f"Kernel size: {self.kernel_size}, Alpha: {self.alpha}, Colormap: {self.colormap_name}")
|
| 60 |
+
logger.info(f"Adaptive mode: {self.adaptive}, Range: {self.min_kernel_size}-{self.max_kernel_size}")
|
| 61 |
+
|
| 62 |
+
def generate_heatmap(self, frame: np.ndarray, detections: List[Dict]) -> Tuple[np.ndarray, float]:
|
| 63 |
+
"""
|
| 64 |
+
Generate crowd density heatmap with ADAPTIVE kernel sizing
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
frame: Input frame
|
| 68 |
+
detections: List of detections with center points and bbox
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
heatmap_overlay: Frame with heatmap overlay
|
| 72 |
+
generation_time: Time taken to generate heatmap
|
| 73 |
+
|
| 74 |
+
Implements:
|
| 75 |
+
- REQ-4: Generate localized density zones
|
| 76 |
+
- REQ-5: Apply color map for crowd concentration
|
| 77 |
+
- ADAPTIVE: Auto-adjusts kernel size based on detection box dimensions
|
| 78 |
+
"""
|
| 79 |
+
start_time = time.time()
|
| 80 |
+
|
| 81 |
+
# Validate inputs
|
| 82 |
+
if frame is None or frame.size == 0:
|
| 83 |
+
logger.error("Invalid frame provided to heatmap generator")
|
| 84 |
+
return np.zeros((480, 640, 3), dtype=np.uint8), 0.0
|
| 85 |
+
|
| 86 |
+
# Only check if there are detections
|
| 87 |
+
if not detections or len(detections) == 0:
|
| 88 |
+
generation_time = time.time() - start_time
|
| 89 |
+
return frame.copy(), generation_time
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
h, w = frame.shape[:2]
|
| 93 |
+
|
| 94 |
+
# Validate frame dimensions
|
| 95 |
+
if h <= 0 or w <= 0:
|
| 96 |
+
logger.error(f"Invalid frame dimensions: {h}x{w}")
|
| 97 |
+
return frame.copy(), 0.0
|
| 98 |
+
|
| 99 |
+
# Create empty density map (REQ-4: localized density zones)
|
| 100 |
+
density_map = np.zeros((h, w), dtype=np.float32)
|
| 101 |
+
|
| 102 |
+
# Calculate adaptive kernel size with validation
|
| 103 |
+
if self.adaptive and len(detections) > 0:
|
| 104 |
+
total_size = 0
|
| 105 |
+
valid_detections = 0
|
| 106 |
+
|
| 107 |
+
for det in detections:
|
| 108 |
+
try:
|
| 109 |
+
bbox = det.get('bbox', [])
|
| 110 |
+
if len(bbox) != 4:
|
| 111 |
+
continue
|
| 112 |
+
|
| 113 |
+
x1, y1, x2, y2 = bbox
|
| 114 |
+
box_width = max(0, x2 - x1)
|
| 115 |
+
box_height = max(0, y2 - y1)
|
| 116 |
+
|
| 117 |
+
if box_width > 0 and box_height > 0:
|
| 118 |
+
total_size += (box_width + box_height) / 2
|
| 119 |
+
valid_detections += 1
|
| 120 |
+
except (KeyError, TypeError, ValueError) as e:
|
| 121 |
+
logger.debug(f"Skipping invalid detection: {e}")
|
| 122 |
+
continue
|
| 123 |
+
|
| 124 |
+
if valid_detections > 0:
|
| 125 |
+
avg_box_size = total_size / valid_detections
|
| 126 |
+
|
| 127 |
+
# Scale kernel size based on average object size
|
| 128 |
+
kernel_radius = int(np.clip(avg_box_size * 0.8,
|
| 129 |
+
self.min_kernel_size,
|
| 130 |
+
self.max_kernel_size))
|
| 131 |
+
kernel_radius = max(15, kernel_radius)
|
| 132 |
+
logger.debug(f"Adaptive kernel: avg={avg_box_size:.1f}, radius={kernel_radius}")
|
| 133 |
+
else:
|
| 134 |
+
kernel_radius = self.kernel_size
|
| 135 |
+
else:
|
| 136 |
+
kernel_radius = self.kernel_size
|
| 137 |
+
|
| 138 |
+
# Add Gaussian blobs at each detection center with validation
|
| 139 |
+
for det in detections:
|
| 140 |
+
try:
|
| 141 |
+
center = det.get('center', [])
|
| 142 |
+
bbox = det.get('bbox', [])
|
| 143 |
+
|
| 144 |
+
if len(center) != 2 or len(bbox) != 4:
|
| 145 |
+
continue
|
| 146 |
+
|
| 147 |
+
cx, cy = center
|
| 148 |
+
|
| 149 |
+
# Validate center coordinates
|
| 150 |
+
if not (0 <= cx < w and 0 <= cy < h):
|
| 151 |
+
logger.debug(f"Skipping out-of-bounds detection at ({cx}, {cy})")
|
| 152 |
+
continue
|
| 153 |
+
|
| 154 |
+
# Get detection-specific size for better adaptation
|
| 155 |
+
if self.adaptive:
|
| 156 |
+
x1, y1, x2, y2 = bbox
|
| 157 |
+
det_width = max(0, x2 - x1)
|
| 158 |
+
det_height = max(0, y2 - y1)
|
| 159 |
+
det_size = (det_width + det_height) / 2
|
| 160 |
+
|
| 161 |
+
if det_size <= 0:
|
| 162 |
+
det_kernel = kernel_radius
|
| 163 |
+
else:
|
| 164 |
+
det_kernel = int(np.clip(det_size * 0.8,
|
| 165 |
+
self.min_kernel_size,
|
| 166 |
+
self.max_kernel_size))
|
| 167 |
+
det_kernel = max(15, det_kernel)
|
| 168 |
+
else:
|
| 169 |
+
det_kernel = kernel_radius
|
| 170 |
+
|
| 171 |
+
# Calculate ROI bounds with proper clamping
|
| 172 |
+
y_min = max(0, cy - det_kernel)
|
| 173 |
+
y_max = min(h, cy + det_kernel)
|
| 174 |
+
x_min = max(0, cx - det_kernel)
|
| 175 |
+
x_max = min(w, cx + det_kernel)
|
| 176 |
+
|
| 177 |
+
# Validate ROI dimensions
|
| 178 |
+
kernel_height = y_max - y_min
|
| 179 |
+
kernel_width = x_max - x_min
|
| 180 |
+
|
| 181 |
+
if kernel_height <= 0 or kernel_width <= 0:
|
| 182 |
+
continue
|
| 183 |
+
|
| 184 |
+
# Create 2D Gaussian with bounds checking
|
| 185 |
+
y_range = np.arange(y_min, y_max) - cy
|
| 186 |
+
x_range = np.arange(x_min, x_max) - cx
|
| 187 |
+
|
| 188 |
+
if len(y_range) == 0 or len(x_range) == 0:
|
| 189 |
+
continue
|
| 190 |
+
|
| 191 |
+
x_grid, y_grid = np.meshgrid(x_range, y_range)
|
| 192 |
+
|
| 193 |
+
# Gaussian formula with adaptive sigma
|
| 194 |
+
det_sigma = det_kernel * self.blur_strength
|
| 195 |
+
gaussian = np.exp(-(x_grid**2 + y_grid**2) / (2 * det_sigma**2))
|
| 196 |
+
|
| 197 |
+
# Use confidence as intensity multiplier for better visualization
|
| 198 |
+
intensity = det.get('confidence', 1.0)
|
| 199 |
+
|
| 200 |
+
# Add to density map with bounds safety
|
| 201 |
+
try:
|
| 202 |
+
density_map[y_min:y_max, x_min:x_max] += gaussian.astype(np.float32) * intensity
|
| 203 |
+
except (ValueError, IndexError) as e:
|
| 204 |
+
logger.debug(f"Skipping gaussian placement: {e}")
|
| 205 |
+
continue
|
| 206 |
+
|
| 207 |
+
except (KeyError, TypeError, ValueError, IndexError) as e:
|
| 208 |
+
logger.debug(f"Error processing detection for heatmap: {e}")
|
| 209 |
+
continue
|
| 210 |
+
|
| 211 |
+
# Normalize density map to 0-255
|
| 212 |
+
if density_map.max() > 0:
|
| 213 |
+
density_map = (density_map / density_map.max() * 255).astype(np.uint8)
|
| 214 |
+
else:
|
| 215 |
+
density_map = density_map.astype(np.uint8)
|
| 216 |
+
|
| 217 |
+
# Apply single Gaussian blur for smooth appearance (removed double blur)
|
| 218 |
+
blur_size = max(11, min(21, kernel_radius // 4)) # Adaptive blur size
|
| 219 |
+
if blur_size % 2 == 0:
|
| 220 |
+
blur_size += 1 # Must be odd
|
| 221 |
+
density_map = cv2.GaussianBlur(density_map, (blur_size, blur_size), 0)
|
| 222 |
+
|
| 223 |
+
# Apply colormap (REQ-5: color map representing concentration)
|
| 224 |
+
heatmap_colored = cv2.applyColorMap(density_map, self.colormap)
|
| 225 |
+
|
| 226 |
+
# Overlay heatmap on original frame
|
| 227 |
+
heatmap_overlay = cv2.addWeighted(
|
| 228 |
+
frame,
|
| 229 |
+
1 - self.alpha,
|
| 230 |
+
heatmap_colored,
|
| 231 |
+
self.alpha,
|
| 232 |
+
0
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
generation_time = time.time() - start_time
|
| 236 |
+
self.generation_times.append(generation_time)
|
| 237 |
+
|
| 238 |
+
# Check performance constraint (SRS: β€ 1.5s per frame)
|
| 239 |
+
if generation_time > self.config['constraints']['max_heatmap_delay']:
|
| 240 |
+
logger.warning(
|
| 241 |
+
f"Heatmap generation exceeded constraint: "
|
| 242 |
+
f"{generation_time:.3f}s > {self.config['constraints']['max_heatmap_delay']}s"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
return heatmap_overlay, generation_time
|
| 246 |
+
|
| 247 |
+
except Exception as e:
|
| 248 |
+
logger.error(f"Heatmap generation error: {e}")
|
| 249 |
+
import traceback
|
| 250 |
+
logger.error(traceback.format_exc())
|
| 251 |
+
return frame.copy(), time.time() - start_time
|
| 252 |
+
|
| 253 |
+
def generate_density_grid(self, frame: np.ndarray, detections: List[Dict],
|
| 254 |
+
grid_size: int = 50) -> np.ndarray:
|
| 255 |
+
"""
|
| 256 |
+
Generate grid-based density visualization (alternative method)
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
frame: Input frame
|
| 260 |
+
detections: List of detections
|
| 261 |
+
grid_size: Size of each grid cell
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
grid_overlay: Frame with grid density overlay
|
| 265 |
+
"""
|
| 266 |
+
h, w = frame.shape[:2]
|
| 267 |
+
overlay = frame.copy()
|
| 268 |
+
|
| 269 |
+
# Create grid
|
| 270 |
+
grid_h = h // grid_size + 1
|
| 271 |
+
grid_w = w // grid_size + 1
|
| 272 |
+
density_grid = np.zeros((grid_h, grid_w), dtype=int)
|
| 273 |
+
|
| 274 |
+
# Count detections in each grid cell
|
| 275 |
+
for det in detections:
|
| 276 |
+
cx, cy = det['center']
|
| 277 |
+
grid_x = min(cx // grid_size, grid_w - 1)
|
| 278 |
+
grid_y = min(cy // grid_size, grid_h - 1)
|
| 279 |
+
density_grid[grid_y, grid_x] += 1
|
| 280 |
+
|
| 281 |
+
# Draw grid with color intensity based on density
|
| 282 |
+
max_density = density_grid.max() if density_grid.max() > 0 else 1
|
| 283 |
+
|
| 284 |
+
for gy in range(grid_h):
|
| 285 |
+
for gx in range(grid_w):
|
| 286 |
+
if density_grid[gy, gx] > 0:
|
| 287 |
+
x1 = gx * grid_size
|
| 288 |
+
y1 = gy * grid_size
|
| 289 |
+
x2 = min(x1 + grid_size, w)
|
| 290 |
+
y2 = min(y1 + grid_size, h)
|
| 291 |
+
|
| 292 |
+
# Color intensity based on density
|
| 293 |
+
intensity = int(255 * (density_grid[gy, gx] / max_density))
|
| 294 |
+
color = (0, intensity, 255 - intensity) # Blue to red
|
| 295 |
+
|
| 296 |
+
# Draw semi-transparent rectangle
|
| 297 |
+
sub_img = overlay[y1:y2, x1:x2]
|
| 298 |
+
rect = np.full_like(sub_img, color, dtype=np.uint8)
|
| 299 |
+
overlay[y1:y2, x1:x2] = cv2.addWeighted(sub_img, 0.7, rect, 0.3, 0)
|
| 300 |
+
|
| 301 |
+
return overlay
|
| 302 |
+
|
| 303 |
+
def get_statistics(self) -> Dict:
|
| 304 |
+
"""Get heatmap generation statistics"""
|
| 305 |
+
if not self.generation_times:
|
| 306 |
+
return {
|
| 307 |
+
'avg_generation_time': 0.0,
|
| 308 |
+
'total_heatmaps': 0
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
return {
|
| 312 |
+
'avg_generation_time': np.mean(self.generation_times[-100:]),
|
| 313 |
+
'total_heatmaps': len(self.generation_times),
|
| 314 |
+
'max_generation_time': max(self.generation_times),
|
| 315 |
+
'min_generation_time': min(self.generation_times)
|
| 316 |
+
}
|
src/utils/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility functions and helpers
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .logger import setup_logger
|
| 6 |
+
from .config import load_config
|
| 7 |
+
|
| 8 |
+
__all__ = ['setup_logger', 'load_config']
|
src/utils/config.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration loader utility
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import yaml
|
| 6 |
+
import os
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def load_config(config_path: str = 'config.yaml') -> dict:
|
| 13 |
+
"""
|
| 14 |
+
Load configuration from YAML file
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
config_path: Path to configuration file
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
config: Configuration dictionary
|
| 21 |
+
"""
|
| 22 |
+
try:
|
| 23 |
+
if not os.path.exists(config_path):
|
| 24 |
+
logger.error(f"Configuration file not found: {config_path}")
|
| 25 |
+
raise FileNotFoundError(f"Config file not found: {config_path}")
|
| 26 |
+
|
| 27 |
+
with open(config_path, 'r') as f:
|
| 28 |
+
config = yaml.safe_load(f)
|
| 29 |
+
|
| 30 |
+
logger.info(f"β Configuration loaded from {config_path}")
|
| 31 |
+
return config
|
| 32 |
+
|
| 33 |
+
except Exception as e:
|
| 34 |
+
logger.error(f"Failed to load configuration: {e}")
|
| 35 |
+
raise
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def validate_config(config: dict) -> bool:
|
| 39 |
+
"""
|
| 40 |
+
Validate configuration values
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
config: Configuration dictionary
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
valid: True if configuration is valid
|
| 47 |
+
"""
|
| 48 |
+
required_keys = ['model', 'video', 'crowd', 'heatmap', 'dashboard']
|
| 49 |
+
|
| 50 |
+
for key in required_keys:
|
| 51 |
+
if key not in config:
|
| 52 |
+
logger.error(f"Missing required configuration section: {key}")
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
# Validate threshold values
|
| 56 |
+
if not 0 <= config['model']['confidence_threshold'] <= 1:
|
| 57 |
+
logger.error("Invalid confidence threshold (must be 0-1)")
|
| 58 |
+
return False
|
| 59 |
+
|
| 60 |
+
if not 0 <= config['heatmap']['alpha'] <= 1:
|
| 61 |
+
logger.error("Invalid heatmap alpha (must be 0-1)")
|
| 62 |
+
return False
|
| 63 |
+
|
| 64 |
+
logger.info("β Configuration validation passed")
|
| 65 |
+
return True
|
src/utils/logger.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Logging utility setup
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import os
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def setup_logger(config: dict) -> logging.Logger:
|
| 11 |
+
"""
|
| 12 |
+
Setup logging configuration
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
config: Configuration dictionary
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
logger: Configured logger instance
|
| 19 |
+
"""
|
| 20 |
+
log_config = config.get('logging', {})
|
| 21 |
+
log_level = log_config.get('level', 'INFO')
|
| 22 |
+
save_logs = log_config.get('save_logs', True)
|
| 23 |
+
log_file = log_config.get('log_file', 'logs/system.log')
|
| 24 |
+
|
| 25 |
+
# Create logs directory if it doesn't exist
|
| 26 |
+
if save_logs:
|
| 27 |
+
log_dir = os.path.dirname(log_file)
|
| 28 |
+
if log_dir and not os.path.exists(log_dir):
|
| 29 |
+
os.makedirs(log_dir)
|
| 30 |
+
|
| 31 |
+
# Configure logging format
|
| 32 |
+
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 33 |
+
date_format = '%Y-%m-%d %H:%M:%S'
|
| 34 |
+
|
| 35 |
+
# Setup handlers
|
| 36 |
+
handlers = []
|
| 37 |
+
|
| 38 |
+
# Console handler
|
| 39 |
+
console_handler = logging.StreamHandler()
|
| 40 |
+
console_handler.setLevel(getattr(logging, log_level))
|
| 41 |
+
console_handler.setFormatter(logging.Formatter(log_format, date_format))
|
| 42 |
+
handlers.append(console_handler)
|
| 43 |
+
|
| 44 |
+
# File handler
|
| 45 |
+
if save_logs:
|
| 46 |
+
# Add timestamp to log file
|
| 47 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 48 |
+
log_file_with_timestamp = log_file.replace('.log', f'_{timestamp}.log')
|
| 49 |
+
|
| 50 |
+
file_handler = logging.FileHandler(log_file_with_timestamp)
|
| 51 |
+
file_handler.setLevel(getattr(logging, log_level))
|
| 52 |
+
file_handler.setFormatter(logging.Formatter(log_format, date_format))
|
| 53 |
+
handlers.append(file_handler)
|
| 54 |
+
|
| 55 |
+
# Configure root logger
|
| 56 |
+
logging.basicConfig(
|
| 57 |
+
level=getattr(logging, log_level),
|
| 58 |
+
handlers=handlers
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
logger = logging.getLogger('Zaytrics')
|
| 62 |
+
logger.info("=" * 80)
|
| 63 |
+
logger.info("Zaytrics Smart Crowd Monitoring System - Version 1.0")
|
| 64 |
+
logger.info("Organization: SEECS, NUST")
|
| 65 |
+
logger.info("=" * 80)
|
| 66 |
+
|
| 67 |
+
return logger
|
src/video/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Video input handler module
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from .handler import VideoHandler
|
| 6 |
+
|
| 7 |
+
__all__ = ['VideoHandler']
|
src/video/handler.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Video Input Handler
|
| 3 |
+
Manages video input from camera or file
|
| 4 |
+
Optimized with threaded capture for GPU inference
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import cv2
|
| 8 |
+
import numpy as np
|
| 9 |
+
from typing import Optional, Tuple
|
| 10 |
+
import logging
|
| 11 |
+
import os
|
| 12 |
+
import threading
|
| 13 |
+
from collections import deque
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class VideoHandler:
|
| 19 |
+
"""
|
| 20 |
+
Handle video input from webcam or video file
|
| 21 |
+
|
| 22 |
+
Supports:
|
| 23 |
+
- Webcam input (source = 0, 1, 2, ...)
|
| 24 |
+
- Video file input (source = path to file)
|
| 25 |
+
- Frame skipping for performance
|
| 26 |
+
- Resolution configuration
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self, config: dict):
|
| 30 |
+
"""
|
| 31 |
+
Initialize video handler
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
config: Configuration dictionary
|
| 35 |
+
"""
|
| 36 |
+
self.config = config
|
| 37 |
+
self.source = config['video']['source']
|
| 38 |
+
self.target_fps = config['video']['fps']
|
| 39 |
+
self.skip_frames = config['video']['skip_frames']
|
| 40 |
+
self.target_width = config['video']['resolution']['width']
|
| 41 |
+
self.target_height = config['video']['resolution']['height']
|
| 42 |
+
|
| 43 |
+
self.cap = None
|
| 44 |
+
self.frame_count = 0
|
| 45 |
+
self.is_camera = False
|
| 46 |
+
|
| 47 |
+
# Threaded capture for async frame reading (cameras only)
|
| 48 |
+
self._thread = None
|
| 49 |
+
self._stopped = False
|
| 50 |
+
self._frame_buffer = deque(maxlen=2) # Small buffer for low latency
|
| 51 |
+
self._lock = threading.Lock()
|
| 52 |
+
self._use_threading = False # Disabled - causes issues with video files
|
| 53 |
+
|
| 54 |
+
logger.info(f"Video Handler initialized with source: {self.source}")
|
| 55 |
+
|
| 56 |
+
def set_source(self, source, is_camera: bool = None):
|
| 57 |
+
"""
|
| 58 |
+
Set video source and optionally specify if it's a camera
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
source: Video source (int for camera, str for file path)
|
| 62 |
+
is_camera: Explicitly specify if source is camera (optional)
|
| 63 |
+
"""
|
| 64 |
+
self.source = source
|
| 65 |
+
|
| 66 |
+
if is_camera is not None:
|
| 67 |
+
self.is_camera = is_camera
|
| 68 |
+
self._is_camera_explicit = True # Mark as explicitly set
|
| 69 |
+
else:
|
| 70 |
+
# Auto-detect
|
| 71 |
+
self.is_camera = isinstance(source, int)
|
| 72 |
+
if hasattr(self, '_is_camera_explicit'):
|
| 73 |
+
delattr(self, '_is_camera_explicit')
|
| 74 |
+
|
| 75 |
+
logger.info(f"Source set to: {source}, is_camera: {self.is_camera}")
|
| 76 |
+
|
| 77 |
+
def open(self) -> bool:
|
| 78 |
+
"""
|
| 79 |
+
Open video source
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
success: True if video source opened successfully
|
| 83 |
+
"""
|
| 84 |
+
try:
|
| 85 |
+
# Release any existing capture
|
| 86 |
+
if self.cap is not None:
|
| 87 |
+
self.cap.release()
|
| 88 |
+
self.cap = None
|
| 89 |
+
|
| 90 |
+
# Validate source type
|
| 91 |
+
if isinstance(self.source, int):
|
| 92 |
+
# Camera source - only update is_camera if not already explicitly set
|
| 93 |
+
if not hasattr(self, '_is_camera_explicit'):
|
| 94 |
+
self.is_camera = True
|
| 95 |
+
logger.info(f"Opening webcam: Camera {self.source}")
|
| 96 |
+
elif isinstance(self.source, str):
|
| 97 |
+
# File source - only update is_camera if not already explicitly set
|
| 98 |
+
if not hasattr(self, '_is_camera_explicit'):
|
| 99 |
+
self.is_camera = False
|
| 100 |
+
if not os.path.exists(self.source):
|
| 101 |
+
logger.error(f"Video file not found: {self.source}")
|
| 102 |
+
return False
|
| 103 |
+
logger.info(f"Opening video file: {self.source}")
|
| 104 |
+
else:
|
| 105 |
+
logger.error(f"Invalid source type: {type(self.source)}")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
# Open video source with DirectShow backend for Windows cameras
|
| 109 |
+
if self.is_camera:
|
| 110 |
+
# Try with DirectShow backend first (Windows)
|
| 111 |
+
self.cap = cv2.VideoCapture(self.source, cv2.CAP_DSHOW)
|
| 112 |
+
if not self.cap.isOpened():
|
| 113 |
+
logger.warning("DirectShow backend failed, trying default backend")
|
| 114 |
+
self.cap = cv2.VideoCapture(self.source)
|
| 115 |
+
else:
|
| 116 |
+
# For video files, use default backend
|
| 117 |
+
logger.info(f"Creating VideoCapture for file: {self.source}")
|
| 118 |
+
self.cap = cv2.VideoCapture(self.source)
|
| 119 |
+
|
| 120 |
+
if not self.cap.isOpened():
|
| 121 |
+
logger.error(f"Failed to open video source: {self.source}")
|
| 122 |
+
return False
|
| 123 |
+
|
| 124 |
+
# Set camera properties if using webcam
|
| 125 |
+
if self.is_camera:
|
| 126 |
+
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.target_width)
|
| 127 |
+
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.target_height)
|
| 128 |
+
self.cap.set(cv2.CAP_PROP_FPS, self.target_fps)
|
| 129 |
+
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Minimize buffer for low latency
|
| 130 |
+
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G')) # MJPEG for speed
|
| 131 |
+
|
| 132 |
+
# Get actual video properties
|
| 133 |
+
actual_fps = self.cap.get(cv2.CAP_PROP_FPS)
|
| 134 |
+
actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 135 |
+
actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 136 |
+
total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 137 |
+
|
| 138 |
+
logger.info(f"Video source opened successfully")
|
| 139 |
+
logger.info(f" Resolution: {actual_width}x{actual_height}")
|
| 140 |
+
logger.info(f" FPS: {actual_fps}")
|
| 141 |
+
if not self.is_camera:
|
| 142 |
+
logger.info(f" Total frames: {total_frames}")
|
| 143 |
+
|
| 144 |
+
# Start threaded capture for async frame reading
|
| 145 |
+
if self._use_threading:
|
| 146 |
+
self._start_capture_thread()
|
| 147 |
+
|
| 148 |
+
return True
|
| 149 |
+
|
| 150 |
+
except Exception as e:
|
| 151 |
+
logger.error(f"Error opening video source: {e}")
|
| 152 |
+
return False
|
| 153 |
+
|
| 154 |
+
def _start_capture_thread(self):
|
| 155 |
+
"""Start background thread for frame capture"""
|
| 156 |
+
self._stopped = False
|
| 157 |
+
self._thread = threading.Thread(target=self._capture_loop, daemon=True)
|
| 158 |
+
self._thread.start()
|
| 159 |
+
logger.info("Threaded frame capture started")
|
| 160 |
+
|
| 161 |
+
def _capture_loop(self):
|
| 162 |
+
"""Background thread that continuously captures frames"""
|
| 163 |
+
while not self._stopped and self.cap is not None and self.cap.isOpened():
|
| 164 |
+
# Skip frames if configured
|
| 165 |
+
for _ in range(self.skip_frames - 1):
|
| 166 |
+
self.cap.grab()
|
| 167 |
+
|
| 168 |
+
ret, frame = self.cap.read()
|
| 169 |
+
|
| 170 |
+
if ret:
|
| 171 |
+
# Resize if needed
|
| 172 |
+
if frame.shape[1] != self.target_width or frame.shape[0] != self.target_height:
|
| 173 |
+
frame = cv2.resize(frame, (self.target_width, self.target_height),
|
| 174 |
+
interpolation=cv2.INTER_NEAREST)
|
| 175 |
+
|
| 176 |
+
with self._lock:
|
| 177 |
+
self._frame_buffer.append((True, frame))
|
| 178 |
+
else:
|
| 179 |
+
with self._lock:
|
| 180 |
+
self._frame_buffer.append((False, None))
|
| 181 |
+
|
| 182 |
+
def read_frame(self) -> Tuple[bool, Optional[np.ndarray]]:
|
| 183 |
+
"""
|
| 184 |
+
Read a frame from video source
|
| 185 |
+
Uses threaded capture if enabled for non-blocking reads
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
success: True if frame read successfully
|
| 189 |
+
frame: Frame as numpy array (BGR format)
|
| 190 |
+
"""
|
| 191 |
+
if self.cap is None or not self.cap.isOpened():
|
| 192 |
+
return False, None
|
| 193 |
+
|
| 194 |
+
try:
|
| 195 |
+
# Use threaded capture if enabled
|
| 196 |
+
if self._use_threading and self._thread is not None:
|
| 197 |
+
with self._lock:
|
| 198 |
+
if len(self._frame_buffer) > 0:
|
| 199 |
+
ret, frame = self._frame_buffer.pop()
|
| 200 |
+
if ret:
|
| 201 |
+
self.frame_count += 1
|
| 202 |
+
return ret, frame
|
| 203 |
+
else:
|
| 204 |
+
return False, None
|
| 205 |
+
|
| 206 |
+
# Fallback to synchronous capture
|
| 207 |
+
for _ in range(self.skip_frames - 1):
|
| 208 |
+
self.cap.grab()
|
| 209 |
+
|
| 210 |
+
ret, frame = self.cap.read()
|
| 211 |
+
|
| 212 |
+
if not ret:
|
| 213 |
+
return False, None
|
| 214 |
+
|
| 215 |
+
# Only resize if dimensions don't match (optimization)
|
| 216 |
+
if frame.shape[1] != self.target_width or frame.shape[0] != self.target_height:
|
| 217 |
+
frame = cv2.resize(frame, (self.target_width, self.target_height),
|
| 218 |
+
interpolation=cv2.INTER_NEAREST) # Fastest interpolation
|
| 219 |
+
|
| 220 |
+
self.frame_count += 1
|
| 221 |
+
return True, frame
|
| 222 |
+
|
| 223 |
+
except Exception as e:
|
| 224 |
+
logger.error(f"Error reading frame: {e}")
|
| 225 |
+
return False, None
|
| 226 |
+
|
| 227 |
+
def release(self):
|
| 228 |
+
"""Release video source and stop capture thread with proper cleanup"""
|
| 229 |
+
try:
|
| 230 |
+
self._stopped = True
|
| 231 |
+
|
| 232 |
+
# Wait for thread to finish with extended timeout
|
| 233 |
+
if self._thread is not None and self._thread.is_alive():
|
| 234 |
+
self._thread.join(timeout=3.0) # Increased from 1.0 to 3.0 seconds
|
| 235 |
+
|
| 236 |
+
# Force cleanup if thread is still alive
|
| 237 |
+
if self._thread.is_alive():
|
| 238 |
+
logger.warning("Capture thread did not stop within timeout, forcing cleanup")
|
| 239 |
+
|
| 240 |
+
self._thread = None
|
| 241 |
+
|
| 242 |
+
# Release OpenCV capture
|
| 243 |
+
if self.cap is not None:
|
| 244 |
+
if self.cap.isOpened():
|
| 245 |
+
self.cap.release()
|
| 246 |
+
self.cap = None
|
| 247 |
+
logger.info("Video source released")
|
| 248 |
+
|
| 249 |
+
# Clear buffer
|
| 250 |
+
with self._lock:
|
| 251 |
+
self._frame_buffer.clear()
|
| 252 |
+
|
| 253 |
+
except Exception as e:
|
| 254 |
+
logger.error(f"Error releasing video capture: {e}")
|
| 255 |
+
# Force cleanup even on error
|
| 256 |
+
self.cap = None
|
| 257 |
+
self._thread = None
|
| 258 |
+
|
| 259 |
+
def is_opened(self) -> bool:
|
| 260 |
+
"""Check if video source is opened"""
|
| 261 |
+
return self.cap is not None and self.cap.isOpened()
|
| 262 |
+
|
| 263 |
+
def get_properties(self) -> dict:
|
| 264 |
+
"""
|
| 265 |
+
Get video source properties
|
| 266 |
+
|
| 267 |
+
Returns:
|
| 268 |
+
properties: Dictionary with video properties
|
| 269 |
+
"""
|
| 270 |
+
if self.cap is None or not self.cap.isOpened():
|
| 271 |
+
return {}
|
| 272 |
+
|
| 273 |
+
return {
|
| 274 |
+
'width': int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
|
| 275 |
+
'height': int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
|
| 276 |
+
'fps': self.cap.get(cv2.CAP_PROP_FPS),
|
| 277 |
+
'total_frames': int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)),
|
| 278 |
+
'current_frame': self.frame_count,
|
| 279 |
+
'is_camera': self.is_camera
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
def is_video_end(self) -> bool:
|
| 283 |
+
"""
|
| 284 |
+
Check if video file has reached the end
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
True if video has ended, False otherwise (or if camera)
|
| 288 |
+
"""
|
| 289 |
+
if self.is_camera or self.cap is None:
|
| 290 |
+
return False
|
| 291 |
+
|
| 292 |
+
try:
|
| 293 |
+
# Get current position and total frames
|
| 294 |
+
current_pos = self.cap.get(cv2.CAP_PROP_POS_FRAMES)
|
| 295 |
+
total_frames = self.cap.get(cv2.CAP_PROP_FRAME_COUNT)
|
| 296 |
+
|
| 297 |
+
# Check if we're at or past the end
|
| 298 |
+
# Use -2 threshold to catch end before actual failure
|
| 299 |
+
if total_frames > 0 and current_pos >= total_frames - 2:
|
| 300 |
+
return True
|
| 301 |
+
|
| 302 |
+
return False
|
| 303 |
+
except Exception as e:
|
| 304 |
+
logger.error(f"Error checking video end: {e}")
|
| 305 |
+
return False
|
| 306 |
+
|
| 307 |
+
def restart(self) -> bool:
|
| 308 |
+
"""
|
| 309 |
+
Restart video from beginning (for video files only)
|
| 310 |
+
|
| 311 |
+
Returns:
|
| 312 |
+
success: True if restart successful
|
| 313 |
+
"""
|
| 314 |
+
if self.is_camera:
|
| 315 |
+
logger.warning("Cannot restart camera feed")
|
| 316 |
+
return False
|
| 317 |
+
|
| 318 |
+
if self.cap is not None:
|
| 319 |
+
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
| 320 |
+
self.frame_count = 0
|
| 321 |
+
logger.info("Video restarted from beginning")
|
| 322 |
+
return True
|
| 323 |
+
|
| 324 |
+
return False
|
| 325 |
+
|
| 326 |
+
def __del__(self):
|
| 327 |
+
"""Destructor to ensure video source is released"""
|
| 328 |
+
self.release()
|
static/422671.jpg
ADDED
|
static/script.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Zaytrics - Crowd Monitoring Dashboard
|
| 2 |
+
|
| 3 |
+
class ZaytricsDashboard {
|
| 4 |
+
constructor() {
|
| 5 |
+
this.isRunning = false;
|
| 6 |
+
this.currentSource = 'camera';
|
| 7 |
+
this.peopleCount = 0;
|
| 8 |
+
this.fps = 0;
|
| 9 |
+
this.init();
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
init() {
|
| 13 |
+
console.log('Zaytrics Dashboard Initialized');
|
| 14 |
+
this.setupEventListeners();
|
| 15 |
+
this.setupFileUpload();
|
| 16 |
+
this.startStatsPolling();
|
| 17 |
+
this.animateStats();
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
setupEventListeners() {
|
| 21 |
+
// Control button interactions
|
| 22 |
+
document.querySelectorAll('.control-btn').forEach(btn => {
|
| 23 |
+
btn.addEventListener('click', (e) => {
|
| 24 |
+
if (!e.currentTarget.id.includes('heatmap')) {
|
| 25 |
+
document.querySelectorAll('.control-btn').forEach(b => {
|
| 26 |
+
if (!b.id.includes('heatmap')) {
|
| 27 |
+
b.classList.remove('active');
|
| 28 |
+
}
|
| 29 |
+
});
|
| 30 |
+
e.currentTarget.classList.add('active');
|
| 31 |
+
}
|
| 32 |
+
});
|
| 33 |
+
});
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
setupFileUpload() {
|
| 37 |
+
const uploadZone = document.getElementById('uploadZone');
|
| 38 |
+
const fileInput = document.getElementById('fileInput');
|
| 39 |
+
const uploadStatus = document.getElementById('uploadStatus');
|
| 40 |
+
|
| 41 |
+
// Click to upload
|
| 42 |
+
uploadZone.addEventListener('click', () => fileInput.click());
|
| 43 |
+
|
| 44 |
+
// File selection
|
| 45 |
+
fileInput.addEventListener('change', (e) => {
|
| 46 |
+
const file = e.target.files[0];
|
| 47 |
+
if (file) {
|
| 48 |
+
this.uploadVideo(file);
|
| 49 |
+
}
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
// Drag and drop
|
| 53 |
+
uploadZone.addEventListener('dragover', (e) => {
|
| 54 |
+
e.preventDefault();
|
| 55 |
+
uploadZone.style.borderColor = 'var(--primary)';
|
| 56 |
+
uploadZone.style.backgroundColor = 'rgba(99, 102, 241, 0.1)';
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
uploadZone.addEventListener('dragleave', (e) => {
|
| 60 |
+
e.preventDefault();
|
| 61 |
+
uploadZone.style.borderColor = '';
|
| 62 |
+
uploadZone.style.backgroundColor = '';
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
uploadZone.addEventListener('drop', (e) => {
|
| 66 |
+
e.preventDefault();
|
| 67 |
+
uploadZone.style.borderColor = '';
|
| 68 |
+
uploadZone.style.backgroundColor = '';
|
| 69 |
+
|
| 70 |
+
const file = e.dataTransfer.files[0];
|
| 71 |
+
if (file) {
|
| 72 |
+
this.uploadVideo(file);
|
| 73 |
+
}
|
| 74 |
+
});
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
async uploadVideo(file) {
|
| 78 |
+
const uploadStatus = document.getElementById('uploadStatus');
|
| 79 |
+
const loopVideo = document.getElementById('loopVideo').checked;
|
| 80 |
+
|
| 81 |
+
// Validate file type
|
| 82 |
+
const allowedTypes = ['video/mp4', 'video/avi', 'video/quicktime', 'video/x-matroska', 'video/webm'];
|
| 83 |
+
if (!allowedTypes.includes(file.type)) {
|
| 84 |
+
uploadStatus.innerHTML = '<div class="error">β Invalid file type. Please upload MP4, AVI, MOV, MKV, or WEBM.</div>';
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Validate file size (100MB)
|
| 89 |
+
if (file.size > 100 * 1024 * 1024) {
|
| 90 |
+
uploadStatus.innerHTML = '<div class="error">β File too large. Maximum size is 100MB.</div>';
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
uploadStatus.innerHTML = '<div class="info">β³ Uploading video...</div>';
|
| 95 |
+
|
| 96 |
+
const formData = new FormData();
|
| 97 |
+
formData.append('file', file);
|
| 98 |
+
formData.append('loop', loopVideo);
|
| 99 |
+
|
| 100 |
+
try {
|
| 101 |
+
const response = await fetch('/api/upload_video', {
|
| 102 |
+
method: 'POST',
|
| 103 |
+
body: formData
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
const data = await response.json();
|
| 107 |
+
|
| 108 |
+
if (response.ok) {
|
| 109 |
+
uploadStatus.innerHTML = '<div class="success">β
Video uploaded successfully!</div>';
|
| 110 |
+
this.currentSource = 'video';
|
| 111 |
+
|
| 112 |
+
// Auto-switch to uploaded video
|
| 113 |
+
setTimeout(() => {
|
| 114 |
+
uploadStatus.innerHTML = '';
|
| 115 |
+
}, 3000);
|
| 116 |
+
} else {
|
| 117 |
+
uploadStatus.innerHTML = `<div class="error">β ${data.error}</div>`;
|
| 118 |
+
}
|
| 119 |
+
} catch (error) {
|
| 120 |
+
console.error('Upload error:', error);
|
| 121 |
+
uploadStatus.innerHTML = '<div class="error">β Upload failed. Please try again.</div>';
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
startStatsPolling() {
|
| 126 |
+
// Clear any existing interval
|
| 127 |
+
if (this.statsInterval) {
|
| 128 |
+
clearInterval(this.statsInterval);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Poll stats from Flask API
|
| 132 |
+
this.statsInterval = setInterval(async () => {
|
| 133 |
+
if (this.isRunning) {
|
| 134 |
+
await this.updateStatsFromAPI();
|
| 135 |
+
}
|
| 136 |
+
}, 1000);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
stopStatsPolling() {
|
| 140 |
+
if (this.statsInterval) {
|
| 141 |
+
clearInterval(this.statsInterval);
|
| 142 |
+
this.statsInterval = null;
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
async updateStatsFromAPI() {
|
| 147 |
+
try {
|
| 148 |
+
const response = await fetch('/api/stats');
|
| 149 |
+
const data = await response.json();
|
| 150 |
+
|
| 151 |
+
this.peopleCount = data.count || 0;
|
| 152 |
+
this.fps = data.fps || 0;
|
| 153 |
+
|
| 154 |
+
document.getElementById('peopleCount').textContent = this.peopleCount;
|
| 155 |
+
document.getElementById('fpsCount').textContent = this.fps.toFixed(1);
|
| 156 |
+
|
| 157 |
+
// Update status indicator based on alert level
|
| 158 |
+
const statusDot = document.querySelector('.status-dot');
|
| 159 |
+
if (statusDot) {
|
| 160 |
+
const alertLevel = data.alert_level || 'normal';
|
| 161 |
+
statusDot.className = 'status-dot';
|
| 162 |
+
if (alertLevel === 'warning') {
|
| 163 |
+
statusDot.style.backgroundColor = '#f59e0b';
|
| 164 |
+
} else if (alertLevel === 'critical') {
|
| 165 |
+
statusDot.style.backgroundColor = '#ef4444';
|
| 166 |
+
} else {
|
| 167 |
+
statusDot.style.backgroundColor = '#10b981';
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
} catch (error) {
|
| 171 |
+
console.error('Error fetching stats:', error);
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
animateStats() {
|
| 176 |
+
// Animate stat numbers when they change
|
| 177 |
+
const observer = new MutationObserver((mutations) => {
|
| 178 |
+
mutations.forEach((mutation) => {
|
| 179 |
+
if (mutation.type === 'characterData' || mutation.type === 'childList') {
|
| 180 |
+
const element = mutation.target.parentElement;
|
| 181 |
+
if (element && element.classList.contains('stat-value')) {
|
| 182 |
+
element.style.transform = 'scale(1.1)';
|
| 183 |
+
setTimeout(() => {
|
| 184 |
+
element.style.transform = 'scale(1)';
|
| 185 |
+
}, 300);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
});
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
const statElements = document.querySelectorAll('.stat-value');
|
| 192 |
+
statElements.forEach(element => {
|
| 193 |
+
observer.observe(element, {
|
| 194 |
+
characterData: true,
|
| 195 |
+
childList: true,
|
| 196 |
+
subtree: true
|
| 197 |
+
});
|
| 198 |
+
});
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// Global functions for button handlers
|
| 203 |
+
let dashboard;
|
| 204 |
+
|
| 205 |
+
async function switchSource(source) {
|
| 206 |
+
const uploadSection = document.getElementById('uploadSection');
|
| 207 |
+
const liveCameraBtn = document.getElementById('liveCameraBtn');
|
| 208 |
+
const uploadVideoBtn = document.getElementById('uploadVideoBtn');
|
| 209 |
+
|
| 210 |
+
if (source === 'upload') {
|
| 211 |
+
uploadSection.style.display = 'block';
|
| 212 |
+
dashboard.currentSource = 'upload';
|
| 213 |
+
} else {
|
| 214 |
+
uploadSection.style.display = 'none';
|
| 215 |
+
dashboard.currentSource = 'camera';
|
| 216 |
+
|
| 217 |
+
// Switch backend to camera
|
| 218 |
+
console.log('Switching to camera source...');
|
| 219 |
+
try {
|
| 220 |
+
const response = await fetch('/api/switch_source', {
|
| 221 |
+
method: 'POST',
|
| 222 |
+
headers: { 'Content-Type': 'application/json' },
|
| 223 |
+
body: JSON.stringify({ source_type: 'camera' })
|
| 224 |
+
});
|
| 225 |
+
const data = await response.json();
|
| 226 |
+
console.log('Switched to camera:', data);
|
| 227 |
+
} catch (err) {
|
| 228 |
+
console.error('Error switching source:', err);
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
async function startMonitoring() {
|
| 234 |
+
console.log('Starting monitoring...');
|
| 235 |
+
dashboard.isRunning = true;
|
| 236 |
+
|
| 237 |
+
// Restart stats polling
|
| 238 |
+
if (dashboard) {
|
| 239 |
+
dashboard.startStatsPolling();
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
const startBtn = document.getElementById('startBtn');
|
| 243 |
+
const stopBtn = document.getElementById('stopBtn');
|
| 244 |
+
const placeholder = document.getElementById('videoPlaceholder');
|
| 245 |
+
const videoFeed = document.getElementById('videoFeed');
|
| 246 |
+
|
| 247 |
+
startBtn.style.display = 'none';
|
| 248 |
+
stopBtn.style.display = 'block';
|
| 249 |
+
|
| 250 |
+
try {
|
| 251 |
+
// First call the start API to set state
|
| 252 |
+
const response = await fetch('/api/start', { method: 'POST' });
|
| 253 |
+
const data = await response.json();
|
| 254 |
+
console.log('Start API response:', data);
|
| 255 |
+
|
| 256 |
+
if (data.status === 'started') {
|
| 257 |
+
// Small delay to let backend initialize
|
| 258 |
+
await new Promise(resolve => setTimeout(resolve, 300));
|
| 259 |
+
|
| 260 |
+
// Now set video source and display
|
| 261 |
+
if (videoFeed) {
|
| 262 |
+
console.log('Setting video feed source...');
|
| 263 |
+
videoFeed.src = '/video_feed?t=' + new Date().getTime();
|
| 264 |
+
|
| 265 |
+
// Add load event listener for debugging
|
| 266 |
+
videoFeed.onload = function() {
|
| 267 |
+
console.log('Video feed loaded successfully');
|
| 268 |
+
};
|
| 269 |
+
|
| 270 |
+
videoFeed.onerror = function(e) {
|
| 271 |
+
console.error('Video feed error:', e);
|
| 272 |
+
alert('Failed to load video stream. Check console for details.');
|
| 273 |
+
};
|
| 274 |
+
|
| 275 |
+
videoFeed.style.display = 'block';
|
| 276 |
+
console.log('Video feed displayed');
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// Hide placeholder
|
| 280 |
+
if (placeholder) {
|
| 281 |
+
placeholder.style.display = 'none';
|
| 282 |
+
console.log('Placeholder hidden');
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
} catch (error) {
|
| 286 |
+
console.error('Error starting monitoring:', error);
|
| 287 |
+
alert('Failed to start monitoring. Error: ' + error.message);
|
| 288 |
+
dashboard.isRunning = false;
|
| 289 |
+
startBtn.style.display = 'block';
|
| 290 |
+
stopBtn.style.display = 'none';
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
async function stopMonitoring() {
|
| 295 |
+
console.log('Stopping monitoring...');
|
| 296 |
+
dashboard.isRunning = false;
|
| 297 |
+
|
| 298 |
+
// Stop stats polling to prevent unnecessary API calls
|
| 299 |
+
if (dashboard) {
|
| 300 |
+
dashboard.stopStatsPolling();
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
const startBtn = document.getElementById('startBtn');
|
| 304 |
+
const stopBtn = document.getElementById('stopBtn');
|
| 305 |
+
|
| 306 |
+
startBtn.style.display = 'block';
|
| 307 |
+
stopBtn.style.display = 'none';
|
| 308 |
+
|
| 309 |
+
try {
|
| 310 |
+
const response = await fetch('/api/stop', { method: 'POST' });
|
| 311 |
+
const data = await response.json();
|
| 312 |
+
|
| 313 |
+
if (data.status === 'stopped') {
|
| 314 |
+
// Hide video feed
|
| 315 |
+
const placeholder = document.getElementById('videoPlaceholder');
|
| 316 |
+
const videoFeed = document.getElementById('videoFeed');
|
| 317 |
+
|
| 318 |
+
if (videoFeed) {
|
| 319 |
+
videoFeed.style.display = 'none';
|
| 320 |
+
videoFeed.removeAttribute('src');
|
| 321 |
+
}
|
| 322 |
+
if (placeholder) placeholder.style.display = 'flex';
|
| 323 |
+
|
| 324 |
+
// Reset counts
|
| 325 |
+
document.getElementById('peopleCount').textContent = '0';
|
| 326 |
+
document.getElementById('fpsCount').textContent = '0';
|
| 327 |
+
}
|
| 328 |
+
} catch (error) {
|
| 329 |
+
console.error('Error stopping monitoring:', error);
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
async function toggleHeatmap() {
|
| 334 |
+
const heatmapBtn = document.getElementById('heatmapBtn');
|
| 335 |
+
|
| 336 |
+
try {
|
| 337 |
+
const response = await fetch('/api/toggle_heatmap', { method: 'POST' });
|
| 338 |
+
const data = await response.json();
|
| 339 |
+
|
| 340 |
+
if (data.heatmap_enabled) {
|
| 341 |
+
heatmapBtn.classList.add('active');
|
| 342 |
+
} else {
|
| 343 |
+
heatmapBtn.classList.remove('active');
|
| 344 |
+
}
|
| 345 |
+
} catch (error) {
|
| 346 |
+
console.error('Error toggling heatmap:', error);
|
| 347 |
+
}
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
async function switchMode(mode) {
|
| 351 |
+
try {
|
| 352 |
+
const response = await fetch('/api/set_detection_mode', {
|
| 353 |
+
method: 'POST',
|
| 354 |
+
headers: { 'Content-Type': 'application/json' },
|
| 355 |
+
body: JSON.stringify({ mode: mode })
|
| 356 |
+
});
|
| 357 |
+
|
| 358 |
+
if (response.ok) {
|
| 359 |
+
const data = await response.json();
|
| 360 |
+
console.log(`Switched to ${mode} mode:`, data.settings);
|
| 361 |
+
|
| 362 |
+
// Update button states
|
| 363 |
+
document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
|
| 364 |
+
document.getElementById(`${mode}ModeBtn`).classList.add('active');
|
| 365 |
+
|
| 366 |
+
// Show notification
|
| 367 |
+
console.log(`β ${mode === 'normal' ? 'Normal' : 'Dense Crowd'} mode activated`);
|
| 368 |
+
} else {
|
| 369 |
+
const error = await response.json();
|
| 370 |
+
console.error('Mode switch error:', error.error);
|
| 371 |
+
}
|
| 372 |
+
} catch (error) {
|
| 373 |
+
console.error('Error switching mode:', error);
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// Cleanup on page unload
|
| 378 |
+
window.addEventListener('beforeunload', async () => {
|
| 379 |
+
if (dashboard && dashboard.isRunning) {
|
| 380 |
+
// Stop monitoring before page closes
|
| 381 |
+
await fetch('/api/stop', { method: 'POST' });
|
| 382 |
+
}
|
| 383 |
+
});
|
| 384 |
+
|
| 385 |
+
// Initialize when DOM is loaded
|
| 386 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 387 |
+
dashboard = new ZaytricsDashboard();
|
| 388 |
+
|
| 389 |
+
// Add scroll animations
|
| 390 |
+
const observerOptions = {
|
| 391 |
+
threshold: 0.1,
|
| 392 |
+
rootMargin: '0px 0px -50px 0px'
|
| 393 |
+
};
|
| 394 |
+
|
| 395 |
+
const observer = new IntersectionObserver((entries) => {
|
| 396 |
+
entries.forEach(entry => {
|
| 397 |
+
if (entry.isIntersecting) {
|
| 398 |
+
entry.target.style.opacity = '1';
|
| 399 |
+
entry.target.style.transform = 'translateY(0)';
|
| 400 |
+
}
|
| 401 |
+
});
|
| 402 |
+
}, observerOptions);
|
| 403 |
+
|
| 404 |
+
// Observe elements for scroll animations
|
| 405 |
+
document.querySelectorAll('.feature-card, .dashboard-card').forEach(el => {
|
| 406 |
+
el.style.opacity = '0';
|
| 407 |
+
el.style.transform = 'translateY(30px)';
|
| 408 |
+
el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
|
| 409 |
+
observer.observe(el);
|
| 410 |
+
});
|
| 411 |
+
});
|
static/style.css
ADDED
|
@@ -0,0 +1,922 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
margin: 0;
|
| 3 |
+
padding: 0;
|
| 4 |
+
box-sizing: border-box;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
:root {
|
| 8 |
+
--bg-primary: #0a0a0a;
|
| 9 |
+
--bg-secondary: #111111;
|
| 10 |
+
--bg-card: #1a1a1a;
|
| 11 |
+
--bg-overlay: rgba(255, 255, 255, 0.02);
|
| 12 |
+
--primary: #0066ff;
|
| 13 |
+
--primary-glow: rgba(0, 102, 255, 0.15);
|
| 14 |
+
--text-primary: #ffffff;
|
| 15 |
+
--text-secondary: #a0a0a0;
|
| 16 |
+
--text-tertiary: #666666;
|
| 17 |
+
--border: rgba(255, 255, 255, 0.08);
|
| 18 |
+
--border-light: rgba(255, 255, 255, 0.04);
|
| 19 |
+
--success: #00d26a;
|
| 20 |
+
--warning: #ffb224;
|
| 21 |
+
--error: #ff375f;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
body {
|
| 25 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 26 |
+
background: var(--bg-primary);
|
| 27 |
+
color: var(--text-primary);
|
| 28 |
+
line-height: 1.6;
|
| 29 |
+
overflow-x: hidden;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/* Background Elements */
|
| 33 |
+
.bg-grid {
|
| 34 |
+
position: fixed;
|
| 35 |
+
top: 0;
|
| 36 |
+
left: 0;
|
| 37 |
+
width: 100%;
|
| 38 |
+
height: 100%;
|
| 39 |
+
background-image:
|
| 40 |
+
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
| 41 |
+
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
| 42 |
+
background-size: 50px 50px;
|
| 43 |
+
pointer-events: none;
|
| 44 |
+
z-index: -2;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.bg-glow {
|
| 48 |
+
position: fixed;
|
| 49 |
+
top: 50%;
|
| 50 |
+
left: 50%;
|
| 51 |
+
width: 600px;
|
| 52 |
+
height: 600px;
|
| 53 |
+
background: radial-gradient(circle, var(--primary-glow) 0%, transparent 70%);
|
| 54 |
+
transform: translate(-50%, -50%);
|
| 55 |
+
pointer-events: none;
|
| 56 |
+
z-index: -1;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.container {
|
| 60 |
+
max-width: 1600px;
|
| 61 |
+
margin: 0 auto;
|
| 62 |
+
padding: 0 24px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* Header & Navigation */
|
| 66 |
+
.header {
|
| 67 |
+
padding: 24px 0;
|
| 68 |
+
border-bottom: 1px solid var(--border-light);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.nav {
|
| 72 |
+
display: flex;
|
| 73 |
+
align-items: center;
|
| 74 |
+
justify-content: space-between;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.logo {
|
| 78 |
+
display: flex;
|
| 79 |
+
align-items: center;
|
| 80 |
+
gap: 12px;
|
| 81 |
+
font-weight: 700;
|
| 82 |
+
font-size: 20px;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.logo-icon {
|
| 86 |
+
width: 32px;
|
| 87 |
+
height: 32px;
|
| 88 |
+
background: linear-gradient(135deg, #001a33, #000d1a);
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
display: flex;
|
| 91 |
+
align-items: center;
|
| 92 |
+
justify-content: center;
|
| 93 |
+
font-weight: 800;
|
| 94 |
+
font-size: 16px;
|
| 95 |
+
box-shadow: 0 4px 12px rgba(0, 102, 255, 0.3);
|
| 96 |
+
border: 1px solid rgba(0, 102, 255, 0.3);
|
| 97 |
+
transition: all 0.3s ease;
|
| 98 |
+
position: relative;
|
| 99 |
+
overflow: hidden;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.logo-icon::before {
|
| 103 |
+
content: '';
|
| 104 |
+
position: absolute;
|
| 105 |
+
top: -50%;
|
| 106 |
+
left: -50%;
|
| 107 |
+
width: 200%;
|
| 108 |
+
height: 200%;
|
| 109 |
+
background: linear-gradient(
|
| 110 |
+
45deg,
|
| 111 |
+
transparent,
|
| 112 |
+
rgba(0, 102, 255, 0.1),
|
| 113 |
+
transparent
|
| 114 |
+
);
|
| 115 |
+
animation: shine 3s infinite;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.logo-icon:hover {
|
| 119 |
+
box-shadow: 0 6px 20px rgba(0, 102, 255, 0.5);
|
| 120 |
+
transform: translateY(-2px);
|
| 121 |
+
background: linear-gradient(135deg, #002147, #00142e);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
@keyframes shine {
|
| 125 |
+
0% {
|
| 126 |
+
transform: translateX(-100%) translateY(-100%) rotate(45deg);
|
| 127 |
+
}
|
| 128 |
+
100% {
|
| 129 |
+
transform: translateX(100%) translateY(100%) rotate(45deg);
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.nav-links {
|
| 134 |
+
display: flex;
|
| 135 |
+
gap: 32px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.nav-link {
|
| 139 |
+
color: var(--text-secondary);
|
| 140 |
+
text-decoration: none;
|
| 141 |
+
font-weight: 500;
|
| 142 |
+
font-size: 14px;
|
| 143 |
+
transition: color 0.2s ease;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.nav-link:hover {
|
| 147 |
+
color: var(--text-primary);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.nav-actions {
|
| 151 |
+
display: flex;
|
| 152 |
+
gap: 12px;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* Buttons */
|
| 156 |
+
.btn {
|
| 157 |
+
padding: 12px 20px;
|
| 158 |
+
border: none;
|
| 159 |
+
border-radius: 8px;
|
| 160 |
+
font-weight: 600;
|
| 161 |
+
font-size: 14px;
|
| 162 |
+
cursor: pointer;
|
| 163 |
+
transition: all 0.2s ease;
|
| 164 |
+
text-decoration: none;
|
| 165 |
+
display: inline-flex;
|
| 166 |
+
align-items: center;
|
| 167 |
+
gap: 8px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.btn-primary {
|
| 171 |
+
background: var(--primary);
|
| 172 |
+
color: white;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.btn-primary:hover {
|
| 176 |
+
background: #0052cc;
|
| 177 |
+
transform: translateY(-1px);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.btn-secondary {
|
| 181 |
+
background: var(--bg-overlay);
|
| 182 |
+
color: var(--text-primary);
|
| 183 |
+
border: 1px solid var(--border);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.btn-secondary:hover {
|
| 187 |
+
background: rgba(255, 255, 255, 0.05);
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.btn-large {
|
| 191 |
+
padding: 16px 32px;
|
| 192 |
+
font-size: 16px;
|
| 193 |
+
display: inline-flex;
|
| 194 |
+
align-items: center;
|
| 195 |
+
justify-content: center;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* Hero Section */
|
| 199 |
+
.hero {
|
| 200 |
+
display: flex;
|
| 201 |
+
flex-direction: column;
|
| 202 |
+
align-items: center;
|
| 203 |
+
gap: 48px;
|
| 204 |
+
padding: 60px 0;
|
| 205 |
+
min-height: 85vh;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.hero-intro {
|
| 209 |
+
text-align: center;
|
| 210 |
+
max-width: 800px;
|
| 211 |
+
display: flex;
|
| 212 |
+
flex-direction: column;
|
| 213 |
+
gap: 24px;
|
| 214 |
+
align-items: center;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.badge {
|
| 218 |
+
display: inline-flex;
|
| 219 |
+
background: var(--bg-overlay);
|
| 220 |
+
border: 1px solid var(--border);
|
| 221 |
+
border-radius: 20px;
|
| 222 |
+
padding: 8px 16px;
|
| 223 |
+
font-size: 14px;
|
| 224 |
+
font-weight: 500;
|
| 225 |
+
color: var(--text-secondary);
|
| 226 |
+
width: fit-content;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.hero-title {
|
| 230 |
+
font-size: 48px;
|
| 231 |
+
font-weight: 800;
|
| 232 |
+
line-height: 1.1;
|
| 233 |
+
letter-spacing: -0.02em;
|
| 234 |
+
margin: 0;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.gradient-text {
|
| 238 |
+
background: linear-gradient(135deg, var(--primary), #00d4ff);
|
| 239 |
+
-webkit-background-clip: text;
|
| 240 |
+
-webkit-text-fill-color: transparent;
|
| 241 |
+
background-clip: text;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.hero-description {
|
| 245 |
+
font-size: 16px;
|
| 246 |
+
color: var(--text-secondary);
|
| 247 |
+
line-height: 1.6;
|
| 248 |
+
margin: 0;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.hero-actions {
|
| 252 |
+
display: flex;
|
| 253 |
+
gap: 16px;
|
| 254 |
+
flex-wrap: wrap;
|
| 255 |
+
align-items: center;
|
| 256 |
+
justify-content: center;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
/* Dashboard Card */
|
| 260 |
+
.dashboard-card {
|
| 261 |
+
background: var(--bg-card);
|
| 262 |
+
border: 1px solid var(--border);
|
| 263 |
+
border-radius: 16px;
|
| 264 |
+
overflow: hidden;
|
| 265 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
| 266 |
+
max-width: 1100px;
|
| 267 |
+
width: 100%;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.card-header {
|
| 271 |
+
padding: 24px;
|
| 272 |
+
border-bottom: 1px solid var(--border);
|
| 273 |
+
display: flex;
|
| 274 |
+
justify-content: space-between;
|
| 275 |
+
align-items: center;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.card-title {
|
| 279 |
+
font-size: 18px;
|
| 280 |
+
font-weight: 600;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.status-indicator {
|
| 284 |
+
display: flex;
|
| 285 |
+
align-items: center;
|
| 286 |
+
gap: 8px;
|
| 287 |
+
font-size: 14px;
|
| 288 |
+
color: var(--success);
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.status-dot {
|
| 292 |
+
width: 8px;
|
| 293 |
+
height: 8px;
|
| 294 |
+
background: var(--success);
|
| 295 |
+
border-radius: 50%;
|
| 296 |
+
animation: pulse 2s infinite;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
@keyframes pulse {
|
| 300 |
+
0%, 100% { opacity: 1; }
|
| 301 |
+
50% { opacity: 0.5; }
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.video-container {
|
| 305 |
+
position: relative;
|
| 306 |
+
background: #000;
|
| 307 |
+
aspect-ratio: 16/9;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.video-placeholder {
|
| 311 |
+
position: absolute;
|
| 312 |
+
top: 0;
|
| 313 |
+
left: 0;
|
| 314 |
+
width: 100%;
|
| 315 |
+
height: 100%;
|
| 316 |
+
background: linear-gradient(135deg, #1a1a1a, #2a2a2a);
|
| 317 |
+
display: flex;
|
| 318 |
+
align-items: center;
|
| 319 |
+
justify-content: center;
|
| 320 |
+
z-index: 1;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
#videoFeed {
|
| 324 |
+
position: absolute;
|
| 325 |
+
top: 0;
|
| 326 |
+
left: 0;
|
| 327 |
+
width: 100%;
|
| 328 |
+
height: 100%;
|
| 329 |
+
object-fit: contain;
|
| 330 |
+
z-index: 2;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.video-overlay {
|
| 334 |
+
text-align: center;
|
| 335 |
+
color: var(--text-secondary);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.camera-icon {
|
| 339 |
+
font-size: 48px;
|
| 340 |
+
margin-bottom: 16px;
|
| 341 |
+
opacity: 0.7;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.video-text {
|
| 345 |
+
font-size: 14px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.video-stats {
|
| 349 |
+
position: absolute;
|
| 350 |
+
bottom: 20px;
|
| 351 |
+
left: 20px;
|
| 352 |
+
display: flex;
|
| 353 |
+
gap: 16px;
|
| 354 |
+
z-index: 10;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.video-stat {
|
| 358 |
+
display: flex;
|
| 359 |
+
align-items: center;
|
| 360 |
+
gap: 12px;
|
| 361 |
+
background: rgba(0, 0, 0, 0.8);
|
| 362 |
+
padding: 12px 16px;
|
| 363 |
+
border-radius: 8px;
|
| 364 |
+
backdrop-filter: blur(10px);
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.stat-icon {
|
| 368 |
+
font-size: 20px;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.stat-info {
|
| 372 |
+
display: flex;
|
| 373 |
+
flex-direction: column;
|
| 374 |
+
gap: 2px;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.stat-value {
|
| 378 |
+
font-size: 18px;
|
| 379 |
+
font-weight: 700;
|
| 380 |
+
color: var(--text-primary);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.stat-label {
|
| 384 |
+
font-size: 12px;
|
| 385 |
+
color: var(--text-secondary);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.card-controls {
|
| 389 |
+
display: flex;
|
| 390 |
+
padding: 20px;
|
| 391 |
+
gap: 8px;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.control-btn {
|
| 395 |
+
flex: 1;
|
| 396 |
+
padding: 12px 16px;
|
| 397 |
+
background: var(--bg-overlay);
|
| 398 |
+
border: 1px solid var(--border);
|
| 399 |
+
border-radius: 8px;
|
| 400 |
+
color: var(--text-secondary);
|
| 401 |
+
font-size: 14px;
|
| 402 |
+
font-weight: 500;
|
| 403 |
+
cursor: pointer;
|
| 404 |
+
transition: all 0.2s ease;
|
| 405 |
+
display: flex;
|
| 406 |
+
align-items: center;
|
| 407 |
+
justify-content: center;
|
| 408 |
+
gap: 8px;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.control-btn:hover,
|
| 412 |
+
.control-btn.active {
|
| 413 |
+
background: var(--primary);
|
| 414 |
+
color: white;
|
| 415 |
+
border-color: var(--primary);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
/* Mode Selector Styles */
|
| 419 |
+
.mode-selector {
|
| 420 |
+
border-top: 1px solid var(--border);
|
| 421 |
+
background: var(--bg-overlay);
|
| 422 |
+
padding: 16px 20px;
|
| 423 |
+
display: flex;
|
| 424 |
+
align-items: center;
|
| 425 |
+
gap: 12px;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.mode-label {
|
| 429 |
+
font-size: 13px;
|
| 430 |
+
font-weight: 600;
|
| 431 |
+
color: var(--text-secondary);
|
| 432 |
+
margin-right: 8px;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.mode-btn {
|
| 436 |
+
flex: 1;
|
| 437 |
+
padding: 10px 14px;
|
| 438 |
+
font-size: 13px;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
/* Upload Section */
|
| 442 |
+
.upload-section {
|
| 443 |
+
padding: 20px;
|
| 444 |
+
border-top: 1px solid var(--border);
|
| 445 |
+
background: var(--bg-overlay);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.upload-zone {
|
| 449 |
+
border: 2px dashed var(--border);
|
| 450 |
+
border-radius: 12px;
|
| 451 |
+
padding: 40px;
|
| 452 |
+
text-align: center;
|
| 453 |
+
cursor: pointer;
|
| 454 |
+
transition: all 0.3s ease;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
.upload-zone:hover {
|
| 458 |
+
border-color: var(--primary);
|
| 459 |
+
background: rgba(0, 102, 255, 0.05);
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
.upload-content {
|
| 463 |
+
display: flex;
|
| 464 |
+
flex-direction: column;
|
| 465 |
+
align-items: center;
|
| 466 |
+
gap: 12px;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.upload-icon {
|
| 470 |
+
font-size: 48px;
|
| 471 |
+
opacity: 0.5;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.upload-text {
|
| 475 |
+
font-size: 16px;
|
| 476 |
+
font-weight: 500;
|
| 477 |
+
color: var(--text-primary);
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.upload-subtext {
|
| 481 |
+
font-size: 13px;
|
| 482 |
+
color: var(--text-secondary);
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.upload-options {
|
| 486 |
+
margin-top: 16px;
|
| 487 |
+
display: flex;
|
| 488 |
+
justify-content: center;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.checkbox-label {
|
| 492 |
+
display: flex;
|
| 493 |
+
align-items: center;
|
| 494 |
+
gap: 8px;
|
| 495 |
+
font-size: 14px;
|
| 496 |
+
color: var(--text-secondary);
|
| 497 |
+
cursor: pointer;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.checkbox-label input[type="checkbox"] {
|
| 501 |
+
width: 16px;
|
| 502 |
+
height: 16px;
|
| 503 |
+
cursor: pointer;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.upload-status {
|
| 507 |
+
margin-top: 12px;
|
| 508 |
+
font-size: 14px;
|
| 509 |
+
text-align: center;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.upload-status .error {
|
| 513 |
+
color: var(--error);
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.upload-status .success {
|
| 517 |
+
color: var(--success);
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.upload-status .info {
|
| 521 |
+
color: var(--primary);
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
/* Monitoring Controls */
|
| 525 |
+
.monitoring-controls {
|
| 526 |
+
padding: 20px;
|
| 527 |
+
display: flex;
|
| 528 |
+
gap: 12px;
|
| 529 |
+
justify-content: center;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.monitoring-controls .btn {
|
| 533 |
+
min-width: 180px;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
/* Features Section */
|
| 537 |
+
.features {
|
| 538 |
+
padding: 120px 0;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.section-header {
|
| 542 |
+
text-align: center;
|
| 543 |
+
margin-bottom: 64px;
|
| 544 |
+
max-width: 600px;
|
| 545 |
+
margin-left: auto;
|
| 546 |
+
margin-right: auto;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.section-badge {
|
| 550 |
+
display: inline-flex;
|
| 551 |
+
background: var(--bg-overlay);
|
| 552 |
+
border: 1px solid var(--border);
|
| 553 |
+
border-radius: 20px;
|
| 554 |
+
padding: 8px 16px;
|
| 555 |
+
font-size: 14px;
|
| 556 |
+
font-weight: 500;
|
| 557 |
+
color: var(--text-secondary);
|
| 558 |
+
margin-bottom: 20px;
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.section-title {
|
| 562 |
+
font-size: 48px;
|
| 563 |
+
font-weight: 700;
|
| 564 |
+
line-height: 1.1;
|
| 565 |
+
letter-spacing: -0.02em;
|
| 566 |
+
margin-bottom: 20px;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.section-description {
|
| 570 |
+
font-size: 18px;
|
| 571 |
+
color: var(--text-secondary);
|
| 572 |
+
line-height: 1.6;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.features-grid {
|
| 576 |
+
display: grid;
|
| 577 |
+
grid-template-columns: repeat(2, 1fr);
|
| 578 |
+
gap: 24px;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.feature-card {
|
| 582 |
+
background: var(--bg-card);
|
| 583 |
+
border: 1px solid var(--border);
|
| 584 |
+
border-radius: 16px;
|
| 585 |
+
padding: 32px;
|
| 586 |
+
transition: all 0.3s ease;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.feature-card:hover {
|
| 590 |
+
transform: translateY(-4px);
|
| 591 |
+
border-color: var(--primary);
|
| 592 |
+
box-shadow: 0 8px 32px rgba(0, 102, 255, 0.1);
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.feature-icon {
|
| 596 |
+
font-size: 32px;
|
| 597 |
+
margin-bottom: 20px;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.feature-title {
|
| 601 |
+
font-size: 20px;
|
| 602 |
+
font-weight: 600;
|
| 603 |
+
margin-bottom: 12px;
|
| 604 |
+
color: var(--text-primary);
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
.feature-description {
|
| 608 |
+
color: var(--text-secondary);
|
| 609 |
+
line-height: 1.6;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
/* Press Section */
|
| 613 |
+
.press {
|
| 614 |
+
padding: 80px 0;
|
| 615 |
+
border-top: 1px solid var(--border-light);
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.press-logos {
|
| 619 |
+
display: flex;
|
| 620 |
+
justify-content: center;
|
| 621 |
+
align-items: center;
|
| 622 |
+
gap: 60px;
|
| 623 |
+
flex-wrap: wrap;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.press-logo {
|
| 627 |
+
font-size: 18px;
|
| 628 |
+
font-weight: 600;
|
| 629 |
+
color: var(--text-tertiary);
|
| 630 |
+
transition: color 0.2s ease;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
.press-logo:hover {
|
| 634 |
+
color: var(--text-secondary);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
/* Responsive Design */
|
| 638 |
+
@media (max-width: 968px) {
|
| 639 |
+
.hero {
|
| 640 |
+
gap: 40px;
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
.hero-title {
|
| 644 |
+
font-size: 40px;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.features-grid {
|
| 648 |
+
grid-template-columns: 1fr;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.nav-links {
|
| 652 |
+
display: none;
|
| 653 |
+
}
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
@media (max-width: 768px) {
|
| 657 |
+
.hero-title {
|
| 658 |
+
font-size: 36px;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.section-title {
|
| 662 |
+
font-size: 36px;
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
.hero-actions {
|
| 666 |
+
justify-content: center;
|
| 667 |
+
}
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
@media (max-width: 480px) {
|
| 671 |
+
.container {
|
| 672 |
+
padding: 0 16px;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.hero-title {
|
| 676 |
+
font-size: 28px;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.hero-description {
|
| 680 |
+
font-size: 16px;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.btn-large {
|
| 684 |
+
padding: 14px 24px;
|
| 685 |
+
font-size: 14px;
|
| 686 |
+
}
|
| 687 |
+
}
|
| 688 |
+
/* Background Image with Overlay */
|
| 689 |
+
.bg-image {
|
| 690 |
+
position: fixed;
|
| 691 |
+
top: 0;
|
| 692 |
+
left: 0;
|
| 693 |
+
width: 100%;
|
| 694 |
+
height: 100%;
|
| 695 |
+
background-image: url('/static/422671.jpg');
|
| 696 |
+
background-size: cover;
|
| 697 |
+
background-position: center;
|
| 698 |
+
background-repeat: no-repeat;
|
| 699 |
+
z-index: -3;
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
.bg-image::before {
|
| 703 |
+
content: '';
|
| 704 |
+
position: absolute;
|
| 705 |
+
top: 0;
|
| 706 |
+
left: 0;
|
| 707 |
+
width: 100%;
|
| 708 |
+
height: 100%;
|
| 709 |
+
background: linear-gradient(
|
| 710 |
+
135deg,
|
| 711 |
+
rgba(10, 10, 10, 0.95) 0%,
|
| 712 |
+
rgba(10, 10, 10, 0.85) 50%,
|
| 713 |
+
rgba(10, 10, 10, 0.95) 100%
|
| 714 |
+
);
|
| 715 |
+
backdrop-filter: blur(1px);
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
/* Enhanced Background Elements */
|
| 719 |
+
.bg-grid {
|
| 720 |
+
position: fixed;
|
| 721 |
+
top: 0;
|
| 722 |
+
left: 0;
|
| 723 |
+
width: 100%;
|
| 724 |
+
height: 100%;
|
| 725 |
+
background-image:
|
| 726 |
+
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
| 727 |
+
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
| 728 |
+
background-size: 50px 50px;
|
| 729 |
+
pointer-events: none;
|
| 730 |
+
z-index: -2;
|
| 731 |
+
mix-blend-mode: overlay;
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
.bg-glow {
|
| 735 |
+
position: fixed;
|
| 736 |
+
top: 50%;
|
| 737 |
+
left: 50%;
|
| 738 |
+
width: 600px;
|
| 739 |
+
height: 600px;
|
| 740 |
+
background: radial-gradient(circle, var(--primary-glow) 0%, transparent 70%);
|
| 741 |
+
transform: translate(-50%, -50%);
|
| 742 |
+
pointer-events: none;
|
| 743 |
+
z-index: -1;
|
| 744 |
+
mix-blend-mode: screen;
|
| 745 |
+
opacity: 0.3;
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
/* Update existing container for better contrast */
|
| 749 |
+
.container {
|
| 750 |
+
position: relative;
|
| 751 |
+
z-index: 1;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
/* Enhanced cards with more transparency */
|
| 755 |
+
.dashboard-card,
|
| 756 |
+
.feature-card {
|
| 757 |
+
background: rgba(26, 26, 26, 0.85);
|
| 758 |
+
backdrop-filter: blur(20px);
|
| 759 |
+
border: 1px solid var(--border);
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
/* Update header for better readability */
|
| 763 |
+
.header {
|
| 764 |
+
background: rgba(17, 17, 17, 0.8);
|
| 765 |
+
backdrop-filter: blur(20px);
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
/* Add motion blur effect to simulate speed */
|
| 769 |
+
@keyframes subtleMove {
|
| 770 |
+
0%, 100% {
|
| 771 |
+
transform: translateX(0) scale(1);
|
| 772 |
+
}
|
| 773 |
+
50% {
|
| 774 |
+
transform: translateX(10px) scale(1.02);
|
| 775 |
+
}
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
.bg-image {
|
| 779 |
+
animation: subtleMove 20s ease-in-out infinite;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
+
/* Enhanced video container to match F1 theme */
|
| 783 |
+
.video-container {
|
| 784 |
+
position: relative;
|
| 785 |
+
background: #000;
|
| 786 |
+
aspect-ratio: 16/9;
|
| 787 |
+
border-radius: 12px;
|
| 788 |
+
overflow: hidden;
|
| 789 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
.video-placeholder {
|
| 793 |
+
width: 100%;
|
| 794 |
+
height: 100%;
|
| 795 |
+
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 50%, #1a1a1a 100%);
|
| 796 |
+
display: flex;
|
| 797 |
+
align-items: center;
|
| 798 |
+
justify-content: center;
|
| 799 |
+
position: relative;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
/* Add racing circuit pattern overlay */
|
| 803 |
+
.video-placeholder::before {
|
| 804 |
+
content: '';
|
| 805 |
+
position: absolute;
|
| 806 |
+
top: 0;
|
| 807 |
+
left: 0;
|
| 808 |
+
width: 100%;
|
| 809 |
+
height: 100%;
|
| 810 |
+
background-image:
|
| 811 |
+
radial-gradient(circle at 20% 50%, rgba(0, 102, 255, 0.1) 0%, transparent 50%),
|
| 812 |
+
radial-gradient(circle at 80% 20%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
|
| 813 |
+
radial-gradient(circle at 40% 80%, rgba(0, 102, 255, 0.1) 0%, transparent 50%);
|
| 814 |
+
animation: circuitMove 15s linear infinite;
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
@keyframes circuitMove {
|
| 818 |
+
0% {
|
| 819 |
+
background-position: 0% 0%, 100% 100%, 50% 50%;
|
| 820 |
+
}
|
| 821 |
+
100% {
|
| 822 |
+
background-position: 100% 100%, 0% 0%, 150% 150%;
|
| 823 |
+
}
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
/* Update hero section for better contrast with background */
|
| 827 |
+
.hero-content {
|
| 828 |
+
background: rgba(10, 10, 10, 0.6);
|
| 829 |
+
backdrop-filter: blur(10px);
|
| 830 |
+
border-radius: 20px;
|
| 831 |
+
padding: 40px;
|
| 832 |
+
margin: -40px;
|
| 833 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
/* Enhanced badge styles */
|
| 837 |
+
.badge {
|
| 838 |
+
display: flex;
|
| 839 |
+
align-items: center;
|
| 840 |
+
justify-content: center;
|
| 841 |
+
width: fit-content;
|
| 842 |
+
margin: 0 auto;
|
| 843 |
+
background: rgba(0, 102, 255, 0.15);
|
| 844 |
+
border: 1px solid rgba(0, 102, 255, 0.3);
|
| 845 |
+
color: #0066ff;
|
| 846 |
+
backdrop-filter: blur(10px);
|
| 847 |
+
text-align: center;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
/* Update feature cards for better readability */
|
| 851 |
+
.feature-card {
|
| 852 |
+
background: rgba(26, 26, 26, 0.8);
|
| 853 |
+
backdrop-filter: blur(20px);
|
| 854 |
+
transition: all 0.3s ease;
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
.feature-card:hover {
|
| 858 |
+
background: rgba(26, 26, 26, 0.9);
|
| 859 |
+
transform: translateY(-5px);
|
| 860 |
+
border-color: var(--primary);
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
/* Enhanced press section */
|
| 864 |
+
.press {
|
| 865 |
+
background: rgba(10, 10, 10, 0.8);
|
| 866 |
+
backdrop-filter: blur(20px);
|
| 867 |
+
margin: 0 -24px;
|
| 868 |
+
padding: 80px 24px;
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
/* Racing-inspired status indicators */
|
| 872 |
+
.status-indicator {
|
| 873 |
+
background: rgba(0, 210, 106, 0.1);
|
| 874 |
+
border: 1px solid rgba(0, 210, 106, 0.3);
|
| 875 |
+
padding: 8px 16px;
|
| 876 |
+
border-radius: 20px;
|
| 877 |
+
backdrop-filter: blur(10px);
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
/* F1-inspired speed metrics */
|
| 881 |
+
.video-stat {
|
| 882 |
+
background: rgba(0, 0, 0, 0.9);
|
| 883 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 884 |
+
backdrop-filter: blur(20px);
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
/* Update button styles for better contrast */
|
| 888 |
+
.btn-primary {
|
| 889 |
+
background: linear-gradient(135deg, var(--primary), #0099ff);
|
| 890 |
+
box-shadow: 0 4px 20px rgba(0, 102, 255, 0.3);
|
| 891 |
+
display: inline-flex;
|
| 892 |
+
align-items: center;
|
| 893 |
+
justify-content: center;
|
| 894 |
+
text-align: center;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
.btn-primary:hover {
|
| 898 |
+
background: linear-gradient(135deg, #0052cc, #0077cc);
|
| 899 |
+
box-shadow: 0 6px 25px rgba(0, 102, 255, 0.4);
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
/* Mobile optimizations for background */
|
| 903 |
+
@media (max-width: 768px) {
|
| 904 |
+
.bg-image {
|
| 905 |
+
background-attachment: scroll;
|
| 906 |
+
animation: none; /* Remove animation on mobile for performance */
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
.hero-content {
|
| 910 |
+
background: rgba(10, 10, 10, 0.8);
|
| 911 |
+
margin: -20px;
|
| 912 |
+
padding: 30px 20px;
|
| 913 |
+
}
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
/* Performance optimizations */
|
| 917 |
+
@media (prefers-reduced-motion: reduce) {
|
| 918 |
+
.bg-image,
|
| 919 |
+
.video-placeholder::before {
|
| 920 |
+
animation: none;
|
| 921 |
+
}
|
| 922 |
+
}
|
templates/index.html
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Zaytrics - Crowd Monitoring</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<!-- Background Image -->
|
| 12 |
+
<div class="bg-image"></div>
|
| 13 |
+
<!-- Background Elements -->
|
| 14 |
+
<div class="bg-grid"></div>
|
| 15 |
+
<div class="bg-glow"></div>
|
| 16 |
+
<div class="container">
|
| 17 |
+
<!-- Header -->
|
| 18 |
+
<header class="header">
|
| 19 |
+
<nav class="nav">
|
| 20 |
+
<div class="logo">
|
| 21 |
+
<div class="logo-icon">Z</div>
|
| 22 |
+
<span class="logo-text">Zaytrics</span>
|
| 23 |
+
</div>
|
| 24 |
+
</nav>
|
| 25 |
+
</header>
|
| 26 |
+
|
| 27 |
+
<!-- Hero Section -->
|
| 28 |
+
<section class="hero">
|
| 29 |
+
<!-- Introduction Section -->
|
| 30 |
+
<div class="hero-intro">
|
| 31 |
+
<div class="badge">
|
| 32 |
+
<span class="badge-text">AI-Powered Analytics</span>
|
| 33 |
+
</div>
|
| 34 |
+
<h1 class="hero-title">
|
| 35 |
+
Real-time Crowd
|
| 36 |
+
<span class="gradient-text">Intelligence</span>
|
| 37 |
+
Platform
|
| 38 |
+
</h1>
|
| 39 |
+
<p class="hero-description">
|
| 40 |
+
Advanced computer vision and AI to monitor, analyze, and optimize crowd movement
|
| 41 |
+
in real-time. Make data-driven decisions for safety and efficiency.
|
| 42 |
+
</p>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<!-- Main Dashboard Card -->
|
| 46 |
+
<div class="dashboard-card">
|
| 47 |
+
<div class="card-header">
|
| 48 |
+
<div class="card-title">Live Monitoring Dashboard</div>
|
| 49 |
+
<div class="card-actions">
|
| 50 |
+
<div class="status-indicator">
|
| 51 |
+
<div class="status-dot"></div>
|
| 52 |
+
<span>Live</span>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<div class="video-container">
|
| 58 |
+
<div class="video-placeholder" id="videoPlaceholder">
|
| 59 |
+
<div class="video-overlay">
|
| 60 |
+
<div class="camera-icon">πΉ</div>
|
| 61 |
+
<div class="video-text">Click Start Monitoring to begin</div>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
<img id="videoFeed" src="" alt="Live feed" style="display:none; position:absolute; top:0; left:0; width:100%; height:100%; object-fit:contain;">
|
| 65 |
+
|
| 66 |
+
<div class="video-stats">
|
| 67 |
+
<div class="video-stat">
|
| 68 |
+
<div class="stat-icon">π₯</div>
|
| 69 |
+
<div class="stat-info">
|
| 70 |
+
<div class="stat-value" id="peopleCount">0</div>
|
| 71 |
+
<div class="stat-label">People Detected</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
<div class="video-stat">
|
| 75 |
+
<div class="stat-icon">β‘</div>
|
| 76 |
+
<div class="stat-info">
|
| 77 |
+
<div class="stat-value" id="fpsCount">0</div>
|
| 78 |
+
<div class="stat-label">FPS</div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<div class="card-controls">
|
| 85 |
+
<button class="control-btn active" id="liveCameraBtn" onclick="switchSource('camera')">
|
| 86 |
+
<span class="control-icon">πΉ</span>
|
| 87 |
+
Live Camera
|
| 88 |
+
</button>
|
| 89 |
+
<button class="control-btn" id="uploadVideoBtn" onclick="switchSource('upload')">
|
| 90 |
+
<span class="control-icon">π</span>
|
| 91 |
+
Upload Video
|
| 92 |
+
</button>
|
| 93 |
+
<button class="control-btn" id="heatmapBtn" onclick="toggleHeatmap()">
|
| 94 |
+
<span class="control-icon">π₯</span>
|
| 95 |
+
Heatmap
|
| 96 |
+
</button>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<!-- Detection Mode Toggle -->
|
| 100 |
+
<div class="card-controls mode-selector">
|
| 101 |
+
<div class="mode-label">Detection Mode:</div>
|
| 102 |
+
<button class="control-btn mode-btn active" id="normalModeBtn" onclick="switchMode('normal')">
|
| 103 |
+
<span class="control-icon">π₯</span>
|
| 104 |
+
Normal
|
| 105 |
+
</button>
|
| 106 |
+
<button class="control-btn mode-btn" id="denseModeBtn" onclick="switchMode('dense')">
|
| 107 |
+
<span class="control-icon">ποΈ</span>
|
| 108 |
+
Dense Crowd
|
| 109 |
+
</button>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<!-- Upload Form (Hidden by default) -->
|
| 113 |
+
<div class="upload-section" id="uploadSection" style="display: none;">
|
| 114 |
+
<div class="upload-zone" id="uploadZone">
|
| 115 |
+
<input type="file" id="fileInput" accept=".mp4,.avi,.mov,.mkv,.webm" style="display: none;">
|
| 116 |
+
<div class="upload-content">
|
| 117 |
+
<div class="upload-icon">π</div>
|
| 118 |
+
<div class="upload-text">Drag & drop video file here or click to browse</div>
|
| 119 |
+
<div class="upload-subtext">Supported formats: MP4, AVI, MOV, MKV, WEBM (Max 100MB)</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="upload-options">
|
| 123 |
+
<label class="checkbox-label">
|
| 124 |
+
<input type="checkbox" id="loopVideo" checked>
|
| 125 |
+
<span>Loop video playback</span>
|
| 126 |
+
</label>
|
| 127 |
+
</div>
|
| 128 |
+
<div class="upload-status" id="uploadStatus"></div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
<!-- Start/Stop Controls -->
|
| 132 |
+
<div class="monitoring-controls">
|
| 133 |
+
<button class="btn btn-primary btn-large" id="startBtn" onclick="startMonitoring()">
|
| 134 |
+
<span class="btn-icon">βΆ</span>
|
| 135 |
+
Start Monitoring
|
| 136 |
+
</button>
|
| 137 |
+
<button class="btn btn-secondary btn-large" id="stopBtn" onclick="stopMonitoring()" style="display: none;">
|
| 138 |
+
<span class="btn-icon">β </span>
|
| 139 |
+
Stop Monitoring
|
| 140 |
+
</button>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</section>
|
| 144 |
+
|
| 145 |
+
<!-- Features Grid -->
|
| 146 |
+
<section class="features">
|
| 147 |
+
<div class="section-header">
|
| 148 |
+
<h2 class="section-title">Advanced Crowd Intelligence</h2>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<div class="features-grid">
|
| 152 |
+
<div class="feature-card">
|
| 153 |
+
<div class="feature-icon">π―</div>
|
| 154 |
+
<h3 class="feature-title">Real-time Detection</h3>
|
| 155 |
+
<p class="feature-description">
|
| 156 |
+
Instant people counting and movement tracking with sub-second latency
|
| 157 |
+
</p>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div class="feature-card">
|
| 161 |
+
<div class="feature-icon">π₯</div>
|
| 162 |
+
<h3 class="feature-title">Heatmap Visualization</h3>
|
| 163 |
+
<p class="feature-description">
|
| 164 |
+
Visual density maps showing crowd concentration and movement patterns
|
| 165 |
+
</p>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<div class="feature-card">
|
| 169 |
+
<div class="feature-icon">πΉ</div>
|
| 170 |
+
<h3 class="feature-title">Dual Source Support</h3>
|
| 171 |
+
<p class="feature-description">
|
| 172 |
+
Monitor live camera feeds or analyze uploaded video files
|
| 173 |
+
</p>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<div class="feature-card">
|
| 177 |
+
<div class="feature-icon">β‘</div>
|
| 178 |
+
<h3 class="feature-title">Optimized Performance</h3>
|
| 179 |
+
<p class="feature-description">
|
| 180 |
+
Efficient processing for real-time analysis on standard hardware
|
| 181 |
+
</p>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</section>
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 188 |
+
</body>
|
| 189 |
+
</html>
|