Ali Abdullah commited on
Commit
98a79a7
Β·
0 Parent(s):

Fix requirements.txt encoding for HF

Browse files
.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
+ ![Python](https://img.shields.io/badge/python-3.10+-blue.svg)
14
+ ![CUDA](https://img.shields.io/badge/CUDA-12.0+-green.svg)
15
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
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>