Ali Abdullah
Fix requirements.txt encoding for HF
98a79a7
"""
Zaytrics Smart Crowd Monitoring System - Web Server
Optimized for small object detection and better performance
"""
print("[*] Starting Zaytrics...")
# GPU Verification - Check CUDA availability
print("[*] Checking GPU...")
import torch
if torch.cuda.is_available():
gpu_name = torch.cuda.get_device_name(0)
gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1024**3
print(f"[OK] GPU Detected: {gpu_name} ({gpu_memory:.1f} GB VRAM)")
print(f" CUDA Version: {torch.version.cuda}")
# Set CUDA optimizations
torch.backends.cudnn.benchmark = True # Auto-tune for best performance
torch.backends.cuda.matmul.allow_tf32 = True # Allow TF32 for faster matmul
else:
print("[WARN] WARNING: CUDA not available, using CPU (slower)")
print("[*] Loading Flask...")
from flask import Flask, render_template, Response, jsonify, request, send_from_directory
from flask_cors import CORS
from werkzeug.utils import secure_filename
print("[OK] Flask loaded")
print("[*] Loading OpenCV...")
import cv2
import numpy as np
print("[OK] OpenCV loaded")
import os
import json
import time
import logging
from datetime import datetime
from threading import Thread, Lock
from queue import Queue
from collections import deque
print("[*] Loading detection modules...")
from src.detection.detector import CrowdDetector
print("[OK] Detector loaded")
from src.heatmap.generator import HeatmapGenerator
print("[OK] Heatmap loaded")
from src.video.handler import VideoHandler
print("[OK] Video handler loaded")
from src.utils.config import load_config
from src.utils.logger import setup_logger
print("[OK] All modules loaded")
# Initialize Flask app
app = Flask(__name__, static_folder='static', template_folder='templates')
CORS(app)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 # Disable caching for development
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB max file size
app.config['UPLOAD_FOLDER'] = 'videos'
ALLOWED_EXTENSIONS = {'mp4', 'avi', 'mov', 'mkv', 'webm'}
# Add CORS and security headers
@app.after_request
def add_security_headers(response):
"""Add security headers to all responses"""
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-XSS-Protection'] = '1; mode=block'
# Allow same-origin requests only
if 'Origin' in request.headers:
origin = request.headers['Origin']
# Only allow localhost origins for security
if 'localhost' in origin or '127.0.0.1' in origin or origin.startswith('http://10.'):
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
return response
# Ensure upload directory exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
# Load configuration
config = load_config('config.yaml')
logger = setup_logger(config)
# Initialize components with optimized parameters for small objects
detector = CrowdDetector(config)
heatmap_generator = HeatmapGenerator(config)
video_handler = VideoHandler(config)
# Thread-safe state management
state_lock = Lock()
def allowed_file(filename):
"""Check if file extension is allowed"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
# Check for existing video files and set default source
def get_latest_video():
"""Get the most recent uploaded video file"""
try:
videos_dir = 'videos'
if os.path.exists(videos_dir):
videos = [f for f in os.listdir(videos_dir) if allowed_file(f)]
if videos:
videos.sort(reverse=True) # Sort by timestamp (filename starts with timestamp)
return videos[0]
except Exception as e:
print(f"Error getting latest video: {e}")
return None
latest_video = get_latest_video()
default_source = 'video' if latest_video else 'camera'
state = {
'running': False,
'heatmap_enabled': False,
'total_detections': 0,
'count_history': [],
'time_history': [],
'current_count': 0,
'fps': 0,
'alert_level': 'normal',
'statistics': {},
'last_detection_time': 0,
'detection_cache': [],
'frame_cache': None,
'source_type': default_source, # 'camera' or 'video'
'video_file': latest_video,
'video_loop': True # Loop videos by default
}
print(f"Default source: {default_source}, Video file: {latest_video}")
# Use deque for frame times
frame_times = deque(maxlen=100) # Keep last 100 frames
# Detection Mode System - Toggle between Normal and Dense Crowd modes
DETECTION_MODES = {
'normal': { # Current working baseline - DO NOT MODIFY
'interval': 3,
'confidence': 0.35,
'iou': 0.45,
'resize': 1.0,
'min_size': 20,
'multi_scale': False,
'max_det': 300,
'imgsz': 416,
'second_pass_conf': 0.05,
'duplicate_threshold': 30,
'min_box_size': 5
},
'dense': { # Aggressive mode for dense crowds (stadiums, concerts)
'interval': 2, # Process every 2nd frame (faster than normal)
'confidence': 0.25, # Lower confidence to catch more people
'iou': 0.35, # Lower IOU to allow more overlap
'resize': 1.0, # Full resolution
'min_size': 15, # Smaller minimum size
'multi_scale': False, # Keep same as normal for compatibility
'max_det': 500, # Allow more detections
'imgsz': 416, # MUST match TensorRT engine size
'second_pass_conf': 0.02, # Much lower for second pass
'duplicate_threshold': 25, # Slightly tighter duplicate threshold
'min_box_size': 3 # Accept smaller boxes
}
}
# Start in normal mode (current working baseline)
CURRENT_MODE = 'normal'
active_mode = DETECTION_MODES[CURRENT_MODE]
# Detection parameters from config
DETECTION_INTERVAL = active_mode['interval']
MIN_CONFIDENCE = active_mode['confidence']
RESIZE_FACTOR = active_mode['resize']
MIN_OBJECT_SIZE = active_mode['min_size']
ENABLE_MULTI_SCALE = active_mode['multi_scale']
# Alert thresholds from config
WARNING_THRESHOLD = config.get('crowd', {}).get('density_threshold', 15)
CRITICAL_THRESHOLD = config.get('crowd', {}).get('warning_threshold', 25)
def update_state(key, value):
"""Thread-safe state update"""
with state_lock:
state[key] = value
def get_alert_level(count):
"""Determine alert level based on count (REQ-7)"""
if count >= config['crowd']['warning_threshold']:
return 'critical'
elif count >= config['crowd']['density_threshold']:
return 'warning'
else:
return 'normal'
def generate_frames():
"""Generate video frames with detections - supports both camera and video file"""
global state
logger.info("generate_frames() called")
# Wait for running state to be true
max_wait = 50 # 5 seconds max
wait_count = 0
while not state.get('running', False) and wait_count < max_wait:
time.sleep(0.1)
wait_count += 1
if not state.get('running', False):
logger.error("Monitoring not started, exiting generate_frames")
return
# Determine video source based on state
with state_lock:
source_type = state['source_type']
video_file = state['video_file']
logger.info(f"Source type: {source_type}, Video file: {video_file}")
logger.info(f"Will use: {'VIDEO FILE' if (source_type == 'video' and video_file) else 'CAMERA'}")
if source_type == 'video' and video_file:
logger.info(f"Opening video file: {video_file}")
video_path = os.path.join(app.config['UPLOAD_FOLDER'], video_file)
if not os.path.exists(video_path):
logger.error(f"Video file not found: {video_path}")
# Generate error frame
error_frame = np.zeros((480, 640, 3), dtype=np.uint8)
cv2.putText(error_frame, "Video File Not Found", (150, 240),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
ret, buffer = cv2.imencode('.jpg', error_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 60])
if ret:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
return
# Set video source properly
video_handler.set_source(video_path, is_camera=False)
logger.info(f"Set video source to: {video_path}")
else:
logger.info("Opening camera source")
# Set camera source properly - read from config
camera_index = config.get('video', {}).get('source', 0)
video_handler.set_source(camera_index, is_camera=True)
logger.info(f"Set camera source to: {camera_index}")
# Try to open video source with retry logic
max_retries = 3
retry_count = 0
while retry_count < max_retries:
if video_handler.open():
break
retry_count += 1
logger.warning(f"Failed to open video source, retry {retry_count}/{max_retries}")
time.sleep(1)
if retry_count >= max_retries:
logger.error("Failed to open video source after retries")
# Generate error frame
error_frame = np.zeros((480, 640, 3), dtype=np.uint8)
cv2.putText(error_frame, "Camera Not Available", (150, 240),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
ret, buffer = cv2.imencode('.jpg', error_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 60])
if ret:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
return
logger.info("Video source opened successfully")
frame_count = 0
start_time = time.time()
# Caching for frame skipping
last_detections = []
last_count = 0
last_annotated_frame = None # Initialize to prevent NameError
consecutive_failures = 0
max_consecutive_failures = 10
try:
while state['running']:
ret, frame = video_handler.read_frame()
if not ret:
# Handle video loop on read failure
if state['source_type'] == 'video' and state['video_loop']:
logger.info("Video ended, restarting loop...")
if video_handler.restart():
frame_count = 0
start_time = time.time()
consecutive_failures = 0
logger.info("Video loop restarted successfully")
continue
# For non-looping videos or cameras, count failures
consecutive_failures += 1
logger.warning(f"Failed to read frame (attempt {consecutive_failures}/{max_consecutive_failures})")
if consecutive_failures >= max_consecutive_failures:
logger.error("Too many consecutive frame read failures")
break
time.sleep(0.1)
continue
consecutive_failures = 0 # Reset on successful read
# Apply resize factor if configured (performance optimization)
if RESIZE_FACTOR < 1.0:
new_width = int(frame.shape[1] * RESIZE_FACTOR)
new_height = int(frame.shape[0] * RESIZE_FACTOR)
frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
frame_count += 1
# Run detection based on configured interval (GPU-optimized)
# Run on frames 0, DETECTION_INTERVAL, DETECTION_INTERVAL*2, etc.
should_detect = (frame_count - 1) % DETECTION_INTERVAL == 0
if should_detect:
detections, count, detection_time = detector.detect(frame)
last_detections = detections
last_count = count
# Choose display mode: heatmap-only OR bounding boxes
if state['heatmap_enabled']:
# Heatmap mode: Skip bounding boxes for cleaner visualization
frame_display, heatmap_time = heatmap_generator.generate_heatmap(
frame, detections # Generator copies internally
)
else:
# Normal mode: Draw bounding boxes (copies frame internally)
frame_display = detector.draw_detections(frame, detections)
# Cache the annotated frame for reuse (no copy needed, frame_display is already a copy)
last_annotated_frame = frame_display
else:
# Reuse cached annotated frame instead of re-drawing (MAJOR OPTIMIZATION)
detections = last_detections
count = last_count
if last_annotated_frame is not None:
frame_display = last_annotated_frame
else:
frame_display = detector.draw_detections(frame, detections)
# Update state with proper locking to prevent race conditions
with state_lock:
state['current_count'] = count
# Only track current frame count, not accumulating total (prevents infinite growth)
state['last_detection_time'] = time.time()
# Update alert level based on configurable thresholds
if count >= CRITICAL_THRESHOLD:
state['alert_level'] = 'critical'
elif count >= WARNING_THRESHOLD:
state['alert_level'] = 'warning'
else:
state['alert_level'] = 'normal'
# Debug log for detection count (reduced logging frequency)
if count > 0 and frame_count % 30 == 0: # Log every 30 frames instead of every frame
logger.debug(f"Detected {count} people in frame {frame_count}")
# Calculate FPS using deque for memory efficiency
current_time = time.time()
frame_times.append(current_time)
if len(frame_times) >= 2:
elapsed = frame_times[-1] - frame_times[0]
# Update FPS with state lock
with state_lock:
state['fps'] = len(frame_times) / elapsed if elapsed > 0 else 0
# Encode frame to JPEG with good quality (80% - improved quality)
ret, buffer = cv2.imencode('.jpg', frame_display, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
if ret:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
except Exception as e:
logger.error(f"Error in generate_frames: {e}", exc_info=True)
# Generate error frame
error_frame = np.zeros((480, 640, 3), dtype=np.uint8)
cv2.putText(error_frame, "Processing Error", (180, 220),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
cv2.putText(error_frame, "Check logs for details", (150, 260),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
ret, buffer = cv2.imencode('.jpg', error_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 60])
if ret:
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
finally:
video_handler.release()
logger.info("Video handler released")
# Clear frame times on exit
frame_times.clear()
@app.route('/')
def index():
"""Render main page"""
return render_template('index.html')
@app.route('/video_feed')
def video_feed():
"""Video streaming route with optimized buffering"""
return Response(generate_frames(),
mimetype='multipart/x-mixed-replace; boundary=frame',
headers={
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
})
@app.route('/api/start', methods=['POST'])
def start_monitoring():
"""Start monitoring (REQ-6)"""
update_state('running', True)
logger.info("Monitoring started")
return jsonify({'status': 'started'})
@app.route('/api/stop', methods=['POST'])
def stop_monitoring():
"""Stop monitoring"""
update_state('running', False)
logger.info("Monitoring stopped")
return jsonify({'status': 'stopped'})
@app.route('/api/upload_video', methods=['POST'])
def upload_video():
"""Upload a video file for processing with enhanced validation"""
try:
if 'file' not in request.files:
return jsonify({'error': 'No file provided'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
# Validate file extension
if not allowed_file(file.filename):
return jsonify({'error': 'Invalid file type. Allowed: mp4, avi, mov, mkv, webm'}), 400
# Additional security: Check file size before saving
file.seek(0, 2) # Seek to end
file_size = file.tell()
file.seek(0) # Reset to beginning
if file_size > app.config['MAX_CONTENT_LENGTH']:
return jsonify({'error': f'File too large. Maximum size is 100MB'}), 400
if file_size == 0:
return jsonify({'error': 'File is empty'}), 400
# Save the file with secure filename
filename = secure_filename(file.filename)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"{timestamp}_{filename}"
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
# Validate video file can be opened and has valid frames
test_cap = None
try:
test_cap = cv2.VideoCapture(filepath)
if not test_cap.isOpened():
os.remove(filepath) # Delete invalid file
return jsonify({'error': 'Invalid video file. Cannot be opened by OpenCV.'}), 400
# Verify it has frames
ret, test_frame = test_cap.read()
if not ret or test_frame is None:
os.remove(filepath)
return jsonify({'error': 'Invalid video file. No readable frames.'}), 400
finally:
if test_cap is not None:
test_cap.release()
# Update state to use video file
with state_lock:
state['source_type'] = 'video'
state['video_file'] = filename
state['video_loop'] = request.form.get('loop', 'false').lower() == 'true'
logger.info(f"Video uploaded successfully: {filename}")
return jsonify({
'status': 'success',
'filename': filename,
'source_type': 'video'
})
except Exception as e:
logger.error(f"Error uploading video: {e}", exc_info=True)
return jsonify({'error': f'Upload failed: {str(e)}'}), 500
@app.route('/api/switch_source', methods=['POST'])
def switch_source():
"""Switch between camera and video file"""
data = request.get_json()
source_type = data.get('source_type', 'camera')
# Stop current monitoring if running
with state_lock:
was_running = state['running']
state['running'] = False
time.sleep(0.5) # Allow current stream to stop
# Update source - ENSURE camera mode clears video file
with state_lock:
state['source_type'] = source_type
if source_type == 'camera':
state['video_file'] = None
logger.info("Camera mode activated - cleared video file from state")
else:
logger.info(f"Video mode - current video: {state.get('video_file', 'None')}")
logger.info(f"Switched to {source_type} source")
return jsonify({
'status': 'success',
'source_type': source_type,
'was_running': was_running
})
@app.route('/api/list_videos', methods=['GET'])
def list_videos():
"""List available uploaded videos"""
try:
videos = []
for filename in os.listdir(app.config['UPLOAD_FOLDER']):
if allowed_file(filename):
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
videos.append({
'filename': filename,
'size': os.path.getsize(filepath),
'modified': datetime.fromtimestamp(os.path.getmtime(filepath)).isoformat()
})
return jsonify({'videos': videos})
except Exception as e:
logger.error(f"Error listing videos: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/toggle_heatmap', methods=['POST'])
def toggle_heatmap():
"""Toggle heatmap (REQ-8, REQ-9)"""
with state_lock:
state['heatmap_enabled'] = not state['heatmap_enabled']
logger.info(f"Heatmap {'enabled' if state['heatmap_enabled'] else 'disabled'}")
return jsonify({'heatmap_enabled': state['heatmap_enabled']})
@app.route('/api/set_detection_mode', methods=['POST'])
def set_detection_mode():
"""Switch between normal and dense crowd detection modes"""
global CURRENT_MODE, DETECTION_INTERVAL, MIN_CONFIDENCE, RESIZE_FACTOR
global MIN_OBJECT_SIZE, ENABLE_MULTI_SCALE
data = request.get_json()
mode = data.get('mode', 'normal')
if mode not in DETECTION_MODES:
return jsonify({'error': f'Invalid mode. Choose: normal or dense'}), 400
# Update mode
CURRENT_MODE = mode
active_mode = DETECTION_MODES[mode]
# Update global parameters
DETECTION_INTERVAL = active_mode['interval']
MIN_CONFIDENCE = active_mode['confidence']
RESIZE_FACTOR = active_mode['resize']
MIN_OBJECT_SIZE = active_mode['min_size']
ENABLE_MULTI_SCALE = active_mode['multi_scale']
# Update detector instance dynamically
detector.confidence_threshold = MIN_CONFIDENCE
detector.iou_threshold = active_mode['iou']
detector.min_size = MIN_OBJECT_SIZE
detector.imgsz = active_mode['imgsz']
detector.max_det = active_mode['max_det']
detector.second_pass_conf = active_mode['second_pass_conf']
detector.duplicate_threshold = active_mode['duplicate_threshold']
detector.min_box_size = active_mode['min_box_size']
logger.info(f"Detection mode switched to: {mode}")
logger.info(f"Settings: interval={DETECTION_INTERVAL}, conf={MIN_CONFIDENCE}, iou={active_mode['iou']}, max_det={active_mode['max_det']}")
return jsonify({
'status': 'success',
'mode': mode,
'settings': active_mode
})
@app.route('/api/reset', methods=['POST'])
def reset_statistics():
"""Reset statistics"""
with state_lock:
state['total_detections'] = 0
state['count_history'] = []
state['time_history'] = []
logger.info("Statistics reset")
return jsonify({'status': 'reset'})
@app.route('/api/optimize', methods=['POST'])
def optimize_detection():
"""Manual optimization endpoint for small objects"""
global MIN_CONFIDENCE, DETECTION_INTERVAL, RESIZE_FACTOR, ENABLE_MULTI_SCALE
data = request.get_json()
if data:
MIN_CONFIDENCE = data.get('confidence', MIN_CONFIDENCE)
DETECTION_INTERVAL = max(1, data.get('interval', DETECTION_INTERVAL))
RESIZE_FACTOR = min(1.0, max(0.3, data.get('resize_factor', RESIZE_FACTOR)))
ENABLE_MULTI_SCALE = data.get('multi_scale', ENABLE_MULTI_SCALE)
logger.info(f"Small object optimization applied: confidence={MIN_CONFIDENCE}, interval={DETECTION_INTERVAL}")
return jsonify({
'confidence': MIN_CONFIDENCE,
'interval': DETECTION_INTERVAL,
'resize_factor': RESIZE_FACTOR,
'multi_scale': ENABLE_MULTI_SCALE,
'min_object_size': MIN_OBJECT_SIZE
})
@app.route('/api/stats')
def get_statistics():
"""Get current statistics (REQ-6, REQ-7)"""
with state_lock:
return jsonify({
'count': state['current_count'],
'fps': round(state['fps'], 1),
'alert_level': state['alert_level'],
'total_detections': state['total_detections'],
'running': state['running'],
'heatmap_enabled': state['heatmap_enabled'],
'count_history': state['count_history'][-50:],
'time_history': state['time_history'][-50:],
'thresholds': {
'warning': config['crowd']['density_threshold'],
'critical': config['crowd']['warning_threshold']
},
'optimization': {
'confidence': MIN_CONFIDENCE,
'detection_interval': DETECTION_INTERVAL,
'resize_factor': RESIZE_FACTOR,
'multi_scale': ENABLE_MULTI_SCALE,
'min_object_size': MIN_OBJECT_SIZE
}
})
@app.route('/api/config', methods=['GET'])
def get_config():
"""Get system configuration"""
return jsonify({
'video_source': config['video']['source'],
'confidence_threshold': config['model']['confidence_threshold'],
'density_threshold': config['crowd']['density_threshold'],
'warning_threshold': config['crowd']['warning_threshold'],
'small_object_optimization': {
'min_confidence': MIN_CONFIDENCE,
'detection_interval': DETECTION_INTERVAL,
'resize_factor': RESIZE_FACTOR,
'multi_scale': ENABLE_MULTI_SCALE,
'min_object_size': MIN_OBJECT_SIZE
}
})
@app.route('/api/health')
def health_check():
"""System health check"""
with state_lock:
return jsonify({
'status': 'healthy',
'running': state['running'],
'fps': state['fps'],
'current_count': state['current_count'],
'timestamp': datetime.now().isoformat()
})
if __name__ == '__main__':
import os
port = int(os.environ.get('PORT', 5000))
logger.info("Starting Enhanced Zaytrics Web Server (Small Object Optimized)")
logger.info(f"Access the dashboard at: http://localhost:{port}")
logger.info("Small Object Detection Optimizations:")
logger.info(f" - Detection interval: {DETECTION_INTERVAL} frames")
logger.info(f" - Minimum confidence: {MIN_CONFIDENCE}")
logger.info(f" - Resize factor: {RESIZE_FACTOR}")
logger.info(f" - Multi-scale detection: {ENABLE_MULTI_SCALE}")
logger.info(f" - Minimum object size: {MIN_OBJECT_SIZE} pixels")
app.run(host='0.0.0.0', port=port, debug=False, threaded=True)