dev #7

Merged
chawanwit.p merged 25 commits from dev into main 2025-09-09 12:33:37 +00:00
7 changed files with 4184 additions and 357 deletions
Showing only changes of commit 72eb7d55ea - Show all commits

172
MULTI_CAMERA_GUIDE.md Normal file
View file

@ -0,0 +1,172 @@
# Multi-Camera Simulation Guide
This guide explains how to simulate 4 cameras using a single webcam for testing the LPR integration in a realistic multi-camera environment.
## 🎯 Purpose
Simulate 4 real-world cameras to test:
- Multiple camera streams with car detection
- LPR integration across different cameras
- Session management for multiple simultaneous detections
- Database updates from different camera sources
## 🚀 Quick Start
### Windows
```bash
# Option 1: Use batch file (opens 4 terminal windows)
start_4_cameras.bat
# Option 2: Manual start (run each in separate terminal)
python multi_camera_simulator.py 1
python multi_camera_simulator.py 2
python multi_camera_simulator.py 3
python multi_camera_simulator.py 4
```
### Linux/macOS
```bash
# Option 1: Use shell script (opens 4 terminal windows)
./start_4_cameras.sh
# Option 2: Manual start (run each in separate terminal)
python3 multi_camera_simulator.py 1
python3 multi_camera_simulator.py 2
python3 multi_camera_simulator.py 3
python3 multi_camera_simulator.py 4
```
## 📡 Camera URLs
Each simulated camera will have unique URLs:
| Camera | HTTP Snapshot | RTSP Stream | Visual ID |
|--------|--------------|-------------|-----------|
| 1 | `http://10.101.1.4:8080/snapshot` | `rtsp://10.101.1.4:8550/stream` | Yellow border |
| 2 | `http://10.101.1.4:8081/snapshot` | `rtsp://10.101.1.4:8551/stream` | Magenta border |
| 3 | `http://10.101.1.4:8082/snapshot` | `rtsp://10.101.1.4:8552/stream` | Green border |
| 4 | `http://10.101.1.4:8083/snapshot` | `rtsp://10.101.1.4:8553/stream` | Blue border |
## 🎨 Visual Differentiation
Each camera adds visual branding to help distinguish streams:
- **Camera 1**: Yellow border + "CAMERA 1" text
- **Camera 2**: Magenta border + "CAMERA 2" text
- **Camera 3**: Green border + "CAMERA 3" text
- **Camera 4**: Blue border + "CAMERA 4" text
- **All**: Timestamp with camera ID
## 🔧 Configuration Options
### Basic Usage
```bash
python multi_camera_simulator.py <camera_id>
```
### Advanced Options
```bash
python multi_camera_simulator.py 1 --webcam-index 0 --base-port 8080 --rtsp-base-port 8550
python multi_camera_simulator.py 2 --no-rtsp # HTTP only
```
### Parameters
- `camera_id`: Required (1-4)
- `--webcam-index`: Webcam device index (default: 0)
- `--base-port`: Base HTTP port (default: 8080)
- `--rtsp-base-port`: Base RTSP port (default: 8550)
- `--no-rtsp`: Disable RTSP streaming
## 🏗️ CMS Configuration
Configure each camera in your CMS with these settings:
### Camera 1
```
Camera Identifier: webcam-camera-01
Snapshot URL: http://10.101.1.4:8080/snapshot
RTSP URL: rtsp://10.101.1.4:8550/stream
Snapshot Interval: 2000
```
### Camera 2
```
Camera Identifier: webcam-camera-02
Snapshot URL: http://10.101.1.4:8081/snapshot
RTSP URL: rtsp://10.101.1.4:8551/stream
Snapshot Interval: 2000
```
### Camera 3
```
Camera Identifier: webcam-camera-03
Snapshot URL: http://10.101.1.4:8082/snapshot
RTSP URL: rtsp://10.101.1.4:8552/stream
Snapshot Interval: 2000
```
### Camera 4
```
Camera Identifier: webcam-camera-04
Snapshot URL: http://10.101.1.4:8083/snapshot
RTSP URL: rtsp://10.101.1.4:8553/stream
Snapshot Interval: 2000
```
## 🧪 Testing LPR Integration
With 4 cameras running, you can test:
1. **Multiple simultaneous detections** - Cars detected by different cameras
2. **Session isolation** - Each camera gets separate session IDs
3. **LPR processing** - License plate results for different cameras
4. **Database updates** - Multiple car records with different session IDs
### Test Scenario
1. Start all 4 cameras
2. Connect all 4 to detector worker
3. Show car to webcam (all cameras see it)
4. Each camera should get separate session ID
5. Send LPR results for each session ID
6. Verify database updates for each camera
## 🔧 Troubleshooting
### Port Already in Use
If you see port conflicts:
```bash
# Check what's using the port
netstat -an | findstr :8080
netstat -an | findstr :8550
# Use different base ports
python multi_camera_simulator.py 1 --base-port 9080 --rtsp-base-port 9550
```
### Webcam Access Issues
```bash
# Try different webcam indices
python multi_camera_simulator.py 1 --webcam-index 1
# Check available cameras
python -c "import cv2; print([i for i in range(10) if cv2.VideoCapture(i).isOpened()])"
```
### FFmpeg Issues
```bash
# Disable RTSP if FFmpeg unavailable
python multi_camera_simulator.py 1 --no-rtsp
```
## 💡 Pro Tips
1. **Start cameras in order** (1, 2, 3, 4) for easier tracking
2. **Use different terminals** for each camera to see individual logs
3. **Check status endpoints** for health monitoring:
- `http://10.101.1.4:8080/status`
- `http://10.101.1.4:8081/status`
- etc.
4. **Monitor logs** to see which camera is processing which detection
5. **Test LPR with unique session IDs** from each camera
This setup provides a realistic multi-camera environment for comprehensive LPR integration testing! 🎉

201
app.py
View file

@ -646,10 +646,13 @@ def get_or_init_session_pipeline_state(camera_id):
"session_id_received": False,
"full_pipeline_completed": False,
"absence_counter": 0,
"validation_counter": 0, # Counter for validation phase
"validation_threshold": 4, # Default validation threshold
"max_absence_frames": 3,
"yolo_inference_enabled": True, # Controls whether to run YOLO inference
"cached_detection_dict": None, # Cached detection dict for lightweight mode
"stable_track_id": None # The stable track ID we're monitoring
"stable_track_id": None, # The stable track ID we're monitoring
"validated_detection": None # Stored detection result from validation phase for full_pipeline reuse
}
return session_pipeline_states[camera_id]
@ -659,6 +662,16 @@ def update_session_pipeline_mode(camera_id, new_mode, session_id=None):
old_mode = state["mode"]
state["mode"] = new_mode
# Reset counters based on mode transition
if new_mode == "validation_detecting":
# Transitioning to validation mode - reset both counters for fresh start
old_validation_counter = state.get("validation_counter", 0)
old_absence_counter = state.get("absence_counter", 0)
state["validation_counter"] = 0
state["absence_counter"] = 0
if old_validation_counter > 0 or old_absence_counter > 0:
logger.info(f"🧹 Camera {camera_id}: VALIDATION MODE RESET - validation_counter: {old_validation_counter}→0, absence_counter: {old_absence_counter}→0")
if session_id:
state["session_id_received"] = True
state["absence_counter"] = 0 # Reset absence counter when session starts
@ -812,6 +825,7 @@ async def detect(websocket: WebSocket):
logger.debug(f"🔍 SESSIONID LOOKUP: display='{display_identifier}', session_id={repr(backend_session_id)}, mode='{current_mode}'")
logger.debug(f"🔍 Available session_ids: {session_ids}")
logger.debug(f"🔍 VALIDATED_DETECTION TRACE: {pipeline_state.get('validated_detection')}")
# ═══ SESSION ID-BASED PROCESSING MODE ═══
if not backend_session_id:
@ -832,7 +846,8 @@ async def detect(websocket: WebSocket):
pipeline_context = {
"camera_id": camera_id,
"display_id": display_identifier,
"backend_session_id": backend_session_id
"backend_session_id": backend_session_id,
"current_mode": current_mode # Pass current mode to pipeline
}
start_time = time.time()
@ -880,7 +895,13 @@ async def detect(websocket: WebSocket):
"bbox": stable_detection.get("bbox", [0, 0, 0, 0]),
"track_id": stable_detection.get("id")
}
# Store validated detection for full_pipeline mode to reuse
pipeline_state["validated_detection"] = detection_result.copy()
logger.debug(f"🔍 Camera {camera_id}: VALIDATION DEBUG - storing detection_result = {detection_result}")
logger.debug(f"🔍 Camera {camera_id}: VALIDATION DEBUG - pipeline_state after storing = {pipeline_state.get('validated_detection')}")
logger.info(f"🚗 Camera {camera_id}: SENDING STABLE DETECTION - track ID {detection_result['track_id']}")
logger.info(f"💾 Camera {camera_id}: STORED VALIDATED DETECTION for full_pipeline reuse")
else:
logger.warning(f"⚠️ Camera {camera_id}: Stable tracks found but no matching detection")
else:
@ -917,6 +938,11 @@ async def detect(websocket: WebSocket):
"confidence": best_detection.get("confidence", 0.0),
"bbox": best_detection.get("bbox", [0, 0, 0, 0])
}
# Store validated detection for full_pipeline mode to reuse
pipeline_state["validated_detection"] = detection_result.copy()
logger.debug(f"🔍 Camera {camera_id}: BASIC VALIDATION DEBUG - storing detection_result = {detection_result}")
logger.info(f"💾 Camera {camera_id}: STORED BASIC VALIDATED DETECTION for full_pipeline reuse")
logger.info(f"🎯 Camera {camera_id}: BASIC VALIDATION COMPLETED after {current_count} frames")
else:
logger.info(f"📊 Camera {camera_id}: Basic validation progress {current_count}/{threshold}")
@ -955,8 +981,20 @@ async def detect(websocket: WebSocket):
elif current_mode == "full_pipeline":
# ═══ FULL PIPELINE MODE ═══
logger.info(f"🔥 Camera {camera_id}: Running FULL PIPELINE (detection + branches + Redis + PostgreSQL)")
detection_result = run_pipeline(cropped_frame, model_tree, context=pipeline_context)
logger.info(f"🔥 Camera {camera_id}: Running FULL PIPELINE (classification branches + Redis + PostgreSQL)")
# Use validated detection from validation phase instead of detecting again
validated_detection = pipeline_state.get("validated_detection")
logger.debug(f"🔍 Camera {camera_id}: FULL_PIPELINE DEBUG - validated_detection = {validated_detection}")
logger.debug(f"🔍 Camera {camera_id}: FULL_PIPELINE DEBUG - pipeline_state keys = {list(pipeline_state.keys())}")
if validated_detection:
logger.info(f"🔄 Camera {camera_id}: Using validated detection for full pipeline: track_id={validated_detection.get('track_id')}")
detection_result = run_pipeline(cropped_frame, model_tree, context=pipeline_context, validated_detection=validated_detection)
# Clear the validated detection after using it
pipeline_state["validated_detection"] = None
else:
logger.warning(f"⚠️ Camera {camera_id}: No validated detection found for full pipeline - this shouldn't happen")
detection_result = run_pipeline(cropped_frame, model_tree, context=pipeline_context)
if detection_result and isinstance(detection_result, dict):
# Cache the full pipeline result
@ -975,89 +1013,98 @@ async def detect(websocket: WebSocket):
else:
logger.warning(f"⚠️ Camera {camera_id}: No track_id found in detection_result: {detection_result.keys()}")
# Ensure we have a cached detection dict for lightweight mode
if not pipeline_state.get("cached_detection_dict"):
# Create fallback cached detection dict if branch processing didn't populate it
fallback_detection = {
"carModel": None,
"carBrand": None,
"carYear": None,
"bodyType": None,
"licensePlateText": None,
"licensePlateConfidence": None
}
pipeline_state["cached_detection_dict"] = fallback_detection
logger.warning(f"⚠️ Camera {camera_id}: Created fallback cached detection dict (branch processing may have failed)")
# Switch to lightweight mode
update_session_pipeline_mode(camera_id, "lightweight")
logger.info(f"✅ Camera {camera_id}: Full pipeline completed - switching to LIGHTWEIGHT mode")
elif current_mode == "lightweight":
# ═══ ENHANCED LIGHTWEIGHT MODE ═══
# Only run YOLO11n.pt to check stable track presence, use cached detection dict
# ═══ SIMPLIFIED LIGHTWEIGHT MODE ═══
# Send cached detection dict + check for 2 consecutive empty frames to reset
stable_track_id = pipeline_state.get("stable_track_id")
cached_detection_dict = pipeline_state.get("cached_detection_dict")
logger.debug(f"🪶 Camera {camera_id}: LIGHTWEIGHT MODE - monitoring stable track_id={stable_track_id}")
logger.debug(f"🪶 Camera {camera_id}: LIGHTWEIGHT MODE - stable_track_id={stable_track_id}")
if not pipeline_state.get("yolo_inference_enabled", True):
# YOLO inference disabled - car considered gone, wait for reset
# YOLO inference disabled - waiting for reset
logger.debug(f"🛑 Camera {camera_id}: YOLO inference disabled - waiting for reset")
detection_result = None # Don't send anything
else:
# Run lightweight YOLO inference to check track presence only (no full pipeline)
# Run YOLO inference to check car presence for reset logic
from siwatsystem.pympta import run_detection_with_tracking
all_detections, regions_dict, track_validation_result = run_detection_with_tracking(cropped_frame, model_tree, pipeline_context)
# OPTION A: Car presence only (track ID kept for internal use)
any_car_detected = len(all_detections) > 0
current_tracks = track_validation_result.get("current_tracks", [])
logger.debug(f"🪶 Camera {camera_id}: LIGHTWEIGHT - any_cars={any_car_detected} (main decision), current_tracks={current_tracks} (internal only)")
if not any_car_detected:
# NO cars detected at all - increment absence counter
if any_car_detected:
# Car detected - reset absence counter, continue sending cached detection dict
pipeline_state["absence_counter"] = 0 # Reset absence since cars are present
if cached_detection_dict:
detection_result = cached_detection_dict # Always send cached data
logger.info(f"💾 Camera {camera_id}: LIGHTWEIGHT - car detected, sending cached detection dict")
else:
logger.warning(f"⚠️ Camera {camera_id}: LIGHTWEIGHT - car detected but no cached detection dict available")
detection_result = None
else:
# No car detected - increment absence counter
pipeline_state["absence_counter"] += 1
absence_count = pipeline_state["absence_counter"]
max_absence = 2 # Changed from 3 to 2 consecutive frames
max_absence = 3 # Need 3 consecutive empty frames
logger.info(f"👻 Camera {camera_id}: NO CARS detected - absence {absence_count}/{max_absence}")
logger.info(f"👻 Camera {camera_id}: LIGHTWEIGHT - no car detected (absence {absence_count}/{max_absence})")
# Check robust AND condition: backend confirmed AND detection confirmed
backend_confirmed_gone = (backend_session_id is None)
detection_confirmed_gone = (absence_count >= max_absence)
logger.debug(f"🔍 Camera {camera_id}: Reset conditions - backend_null={backend_confirmed_gone}, absence_2frames={detection_confirmed_gone}")
if backend_confirmed_gone and detection_confirmed_gone:
# BOTH conditions met - RESET TO VALIDATION PHASE
logger.info(f"🔄 Camera {camera_id}: ROBUST RESET - both conditions met (backend=null AND absence>=2)")
if absence_count >= max_absence:
# SIMPLE RESET CONDITION: 2 consecutive empty frames
logger.info(f"🔄 Camera {camera_id}: RESET CONDITION MET - {max_absence} consecutive empty frames")
# Clear all state and prepare for next car
cached_full_pipeline_results.pop(camera_id, None)
pipeline_state["cached_detection_dict"] = None
pipeline_state["stable_track_id"] = None
pipeline_state["validated_detection"] = None
old_absence_counter = pipeline_state["absence_counter"]
old_validation_counter = pipeline_state.get("validation_counter", 0)
pipeline_state["absence_counter"] = 0
pipeline_state["yolo_inference_enabled"] = True # Re-enable for next car
pipeline_state["validation_counter"] = 0 # Clear validation counter
pipeline_state["yolo_inference_enabled"] = True
logger.info(f"🧹 Camera {camera_id}: CLEARING ALL COUNTERS - absence_counter: {old_absence_counter}→0, validation_counter: {old_validation_counter}→0")
# Clear stability tracking data for this camera
from siwatsystem.pympta import reset_camera_stability_tracking
reset_camera_stability_tracking(camera_id, model_tree.get("modelId", "unknown"))
# Switch back to validation phase - ready for next car
update_session_pipeline_mode(camera_id, "detection_dict")
logger.info(f"🔄 Camera {camera_id}: RESET TO VALIDATION - model ready for next car")
# Switch back to validation phase
update_session_pipeline_mode(camera_id, "validation_detecting")
logger.info(f"✅ Camera {camera_id}: RESET TO VALIDATION COMPLETE - ready for new car")
detection_result = None # Stop sending data during reset
# Now in validation mode - send what YOLO detection finds (will be null since no car)
detection_result = {"class": "none", "confidence": 1.0, "bbox": [0, 0, 0, 0]}
else:
# One or both conditions not met - keep sending cached detection dict
# Still within absence threshold - continue sending cached detection dict
if cached_detection_dict:
detection_result = cached_detection_dict # Always send cached data
logger.info(f"⏳ Camera {camera_id}: NO CARS absence {absence_count}/2, backend_null={backend_confirmed_gone} - sending cached detection dict")
detection_result = cached_detection_dict # Send cached data
logger.info(f"⏳ Camera {camera_id}: LIGHTWEIGHT - no car but absence<{max_absence}, still sending cached detection dict")
else:
logger.warning(f"⚠️ Camera {camera_id}: NO CARS but no cached detection dict available")
logger.warning(f"⚠️ Camera {camera_id}: LIGHTWEIGHT - no cached detection dict available")
detection_result = None
else:
# Cars detected - reset absence counter, send cached detection dict
pipeline_state["absence_counter"] = 0 # Reset absence since cars are present
if cached_detection_dict:
detection_result = cached_detection_dict # Always send cached data
logger.info(f"🪶 Camera {camera_id}: CARS DETECTED - sending cached detection dict:")
logger.info(f"🪶 Camera {camera_id}: - Cached dict: {cached_detection_dict}")
logger.info(f"🪶 Camera {camera_id}: - Track info (internal): {current_tracks}")
else:
logger.warning(f"⚠️ Camera {camera_id}: Cars detected but no cached detection dict available")
detection_result = None
elif current_mode == "car_gone_waiting":
# ═══ CAR GONE WAITING STATE ═══
@ -1072,6 +1119,7 @@ async def detect(websocket: WebSocket):
pipeline_state["absence_counter"] = 0
pipeline_state["stable_track_id"] = None
pipeline_state["cached_detection_dict"] = None
pipeline_state["validated_detection"] = None
# Clear stability tracking data for this camera
from siwatsystem.pympta import reset_camera_stability_tracking
@ -1126,35 +1174,26 @@ async def detect(websocket: WebSocket):
if backend_session_id:
logger.debug(f"🔄 Camera {camera_id}: Note - sessionId {backend_session_id} exists but still in send_detections mode (transition pending)")
elif detection_result.get("class") == "none":
# "None" detection - skip override if lightweight mode already made the decision
if current_mode == "lightweight":
# Lightweight mode already set detection_result correctly, don't override
logger.debug(f"🪶 Camera {camera_id}: Lightweight mode - respecting detection_result decision")
if detection_result is None:
detection_dict = None
logger.info(f"📤 LIGHTWEIGHT SENDING 'NONE' - Reset conditions met for camera {camera_id}")
else:
# detection_result should be the cached_detection_dict
detection_dict = detection_result
logger.info(f"💾 LIGHTWEIGHT SENDING CACHED - Maintaining session for camera {camera_id}")
elif current_mode == "lightweight":
# ═══ SIMPLIFIED LIGHTWEIGHT MODE DETECTION PROCESSING ═══
if detection_result.get("class") == "none":
# No car detected - this happens when resetting to validation
detection_dict = None # Send detection: null
logger.info(f"🚫 LIGHTWEIGHT - no car detected, sending detection=null")
elif isinstance(detection_result, dict) and ("carBrand" in detection_result or "carModel" in detection_result):
# This is a cached detection dict - send it
detection_dict = detection_result
logger.info(f"💾 LIGHTWEIGHT - sending cached detection dict")
else:
# Other modes - send null to clear session
logger.warning(f"⚠️ LIGHTWEIGHT - unexpected detection_result type: {type(detection_result)}")
detection_dict = None
logger.info(f"📤 SENDING 'NONE' (detection: null) - Car absent, expecting backend to clear session for camera {camera_id}")
elif detection_result.get("cached_mode", False):
# Cached mode in lightweight - use cached detection dict directly
cached_dict = detection_result.get("branch_results", {})
detection_dict = cached_dict if cached_dict else {
"carModel": None,
"carBrand": None,
"carYear": None,
"bodyType": None,
"licensePlateText": None,
"licensePlateConfidence": None
}
elif detection_result.get("class") == "none":
# Other modes - send null to clear session
detection_dict = None
logger.info(f"📤 SENDING 'NONE' (detection: null) - Car absent, expecting backend to clear session for camera {camera_id}")
elif detection_result and "carBrand" in detection_result:
# Lightweight mode - detection_result IS the cached detection dict
# Handle cached detection dict format (fallback for compatibility)
detection_dict = detection_result
logger.info(f"💾 Camera {camera_id}: LIGHTWEIGHT MODE - using detection_result as detection_dict:")
logger.info(f"💾 Camera {camera_id}: - detection_dict: {detection_dict}")
@ -1235,13 +1274,17 @@ async def detect(websocket: WebSocket):
# Backend manages sessionIds independently based on detection content
logger.debug(f"TX message prepared (no sessionId) - detection_dict type: {type(detection_dict)}")
# Log detection details
if detection_result and "class" in detection_result and detection_result.get("class") != "none":
# Log detection details for different modes
if current_mode == "lightweight":
if detection_result and detection_result.get("class") == "none":
logger.info(f"🚫 Camera {camera_id}: LIGHTWEIGHT - No car detected (resetting to validation)")
elif isinstance(detection_result, dict) and ("carBrand" in detection_result or "carModel" in detection_result):
logger.info(f"💾 Camera {camera_id}: LIGHTWEIGHT - Sending cached detection data")
else:
logger.info(f"🪶 Camera {camera_id}: LIGHTWEIGHT - Processing detection")
elif detection_result and "class" in detection_result and detection_result.get("class") != "none":
confidence = detection_result.get("confidence", 0.0)
logger.info(f"Camera {camera_id}: Detected {detection_result['class']} with confidence {confidence:.2f} using model {stream['modelName']}")
elif detection_result and "carBrand" in detection_result:
# Lightweight mode cached detection dict - different format
logger.info(f"Camera {camera_id}: Using cached detection dict (lightweight mode) - {detection_result.get('carBrand', 'Unknown')} {detection_result.get('bodyType', '')}")
logger.info(f"🚗 Camera {camera_id}: Detected {detection_result['class']} with confidence {confidence:.2f} using model {stream['modelName']}")
# Send detection data to backend (session gating handled above in processing logic)
logger.debug(f"📤 SENDING TO BACKEND for camera {camera_id}: {json.dumps(detection_data, indent=2)}")

328
multi_camera_simulator.py Normal file
View file

@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""
Multi-Camera Simulator
Creates 4 virtual cameras using the same webcam with different ports
Run this in 4 separate terminals to simulate real-world scenario with 4 cameras
"""
import cv2
import threading
import time
import logging
import socket
import sys
import os
import argparse
import subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] Camera%(camera_id)s: %(message)s"
)
class MultiCameraHandler(BaseHTTPRequestHandler):
"""HTTP handler for camera snapshot requests with camera-specific branding"""
def __init__(self, camera_id, webcam_cap, *args, **kwargs):
self.camera_id = camera_id
self.webcam_cap = webcam_cap
super().__init__(*args, **kwargs)
def do_GET(self):
if self.path == '/snapshot' or self.path == '/snapshot.jpg':
try:
# Capture fresh frame from webcam
ret, frame = self.webcam_cap.read()
if ret and frame is not None:
# Add camera branding overlay
frame = self.add_camera_branding(frame)
# Encode as JPEG
success, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
if success:
self.send_response(200)
self.send_header('Content-Type', 'image/jpeg')
self.send_header('Content-Length', str(len(buffer)))
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
self.end_headers()
self.wfile.write(buffer.tobytes())
return
# Send error response
self.send_response(500)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'Failed to capture webcam frame')
except Exception as e:
logging.error(f"Camera{self.camera_id}: Error serving snapshot: {e}")
self.send_response(500)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(f'Error: {str(e)}'.encode())
elif self.path == '/status':
# Status endpoint for health checking
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
width = int(self.webcam_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(self.webcam_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = self.webcam_cap.get(cv2.CAP_PROP_FPS)
status = f'{{"status": "online", "camera_id": "{self.camera_id}", "width": {width}, "height": {height}, "fps": {fps}}}'
self.wfile.write(status.encode())
else:
# 404 for other paths
self.send_response(404)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'Not Found - Available endpoints: /snapshot, /snapshot.jpg, /status')
def add_camera_branding(self, frame):
"""Add visual branding to differentiate cameras"""
# Clone frame to avoid modifying original
branded_frame = frame.copy()
# Camera-specific colors and positions
colors = {
1: (0, 255, 255), # Yellow
2: (255, 0, 255), # Magenta
3: (0, 255, 0), # Green
4: (255, 0, 0) # Blue
}
# Add colored border
color = colors.get(self.camera_id, (255, 255, 255))
cv2.rectangle(branded_frame, (0, 0), (branded_frame.shape[1]-1, branded_frame.shape[0]-1), color, 10)
# Add camera ID text
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 2
thickness = 3
# Camera ID text
text = f"CAMERA {self.camera_id}"
text_size = cv2.getTextSize(text, font, font_scale, thickness)[0]
text_x = 30
text_y = 60
# Add background rectangle for text
cv2.rectangle(branded_frame,
(text_x - 10, text_y - text_size[1] - 10),
(text_x + text_size[0] + 10, text_y + 10),
(0, 0, 0), -1)
# Add text
cv2.putText(branded_frame, text, (text_x, text_y), font, font_scale, color, thickness)
# Add timestamp
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
ts_text = f"CAM{self.camera_id}: {timestamp}"
cv2.putText(branded_frame, ts_text, (30, branded_frame.shape[0] - 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)
return branded_frame
def log_message(self, format, *args):
# Suppress default HTTP server logging
pass
def create_handler_class(camera_id, webcam_cap):
"""Create a handler class with bound camera_id and webcam_cap"""
def handler(*args, **kwargs):
return MultiCameraHandler(camera_id, webcam_cap, *args, **kwargs)
return handler
def check_port_available(port):
"""Check if a port is available"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', port))
return True
except OSError:
return False
def start_rtsp_stream(camera_id, webcam_index, rtsp_port):
"""Start RTSP streaming for a specific camera"""
try:
# Check if FFmpeg is available
result = subprocess.run(['ffmpeg', '-version'],
capture_output=True, text=True, timeout=5)
if result.returncode != 0:
logging.warning(f"Camera{camera_id}: FFmpeg not available, RTSP disabled")
return None
except Exception:
logging.warning(f"Camera{camera_id}: FFmpeg not available, RTSP disabled")
return None
try:
# Get camera device name for Windows
if sys.platform.startswith('win'):
# Use the integrated camera
camera_name = "Integrated Camera"
# FFmpeg command to stream webcam via RTSP
if sys.platform.startswith('win'):
cmd = [
'ffmpeg',
'-f', 'dshow',
'-i', f'video={camera_name}',
'-c:v', 'libx264',
'-preset', 'veryfast',
'-tune', 'zerolatency',
'-r', '15', # Lower FPS for multiple streams
'-s', '1280x720',
'-f', 'rtsp',
f'rtsp://localhost:{rtsp_port}/stream'
]
else:
cmd = [
'ffmpeg',
'-f', 'v4l2',
'-i', f'/dev/video{webcam_index}',
'-c:v', 'libx264',
'-preset', 'veryfast',
'-tune', 'zerolatency',
'-r', '15',
'-s', '1280x720',
'-f', 'rtsp',
f'rtsp://localhost:{rtsp_port}/stream'
]
logging.info(f"Camera{camera_id}: Starting RTSP stream on port {rtsp_port}")
rtsp_process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Give FFmpeg a moment to start
time.sleep(2)
if rtsp_process.poll() is None:
logging.info(f"Camera{camera_id}: RTSP streaming started successfully")
return rtsp_process
else:
logging.error(f"Camera{camera_id}: RTSP streaming failed to start")
return None
except Exception as e:
logging.error(f"Camera{camera_id}: Failed to start RTSP stream: {e}")
return None
def main():
parser = argparse.ArgumentParser(description='Multi-Camera Simulator')
parser.add_argument('camera_id', type=int, choices=[1, 2, 3, 4],
help='Camera ID (1-4)')
parser.add_argument('--webcam-index', type=int, default=0,
help='Webcam device index (default: 0)')
parser.add_argument('--base-port', type=int, default=8080,
help='Base port for HTTP servers (default: 8080)')
parser.add_argument('--rtsp-base-port', type=int, default=8550,
help='Base port for RTSP servers (default: 8550)')
parser.add_argument('--no-rtsp', action='store_true',
help='Disable RTSP streaming (HTTP only)')
args = parser.parse_args()
camera_id = args.camera_id
webcam_index = args.webcam_index
# Calculate ports based on camera ID
http_port = args.base_port + camera_id - 1 # 8080, 8081, 8082, 8083
rtsp_port = args.rtsp_base_port + camera_id - 1 # 8550, 8551, 8552, 8553
# Check if ports are available
if not check_port_available(http_port):
logging.error(f"Camera{camera_id}: HTTP port {http_port} is already in use")
sys.exit(1)
if not args.no_rtsp and not check_port_available(rtsp_port):
logging.error(f"Camera{camera_id}: RTSP port {rtsp_port} is already in use")
sys.exit(1)
logging.info(f"=== Starting Camera {camera_id} Simulator ===")
# Initialize webcam
logging.info(f"Camera{camera_id}: Initializing webcam at index {webcam_index}...")
webcam_cap = cv2.VideoCapture(webcam_index)
if not webcam_cap.isOpened():
logging.error(f"Camera{camera_id}: Failed to open webcam at index {webcam_index}")
sys.exit(1)
# Set webcam properties
webcam_cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
webcam_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
webcam_cap.set(cv2.CAP_PROP_FPS, 30)
width = int(webcam_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(webcam_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = webcam_cap.get(cv2.CAP_PROP_FPS)
logging.info(f"Camera{camera_id}: Webcam initialized: {width}x{height} @ {fps}fps")
# Start RTSP streaming (if enabled)
rtsp_process = None
if not args.no_rtsp:
rtsp_process = start_rtsp_stream(camera_id, webcam_index, rtsp_port)
# Start HTTP server
server_address = ('0.0.0.0', http_port)
handler_class = create_handler_class(camera_id, webcam_cap)
http_server = HTTPServer(server_address, handler_class)
# Get local IP
local_ip = "10.101.1.4" # Wireguard IP
logging.info(f"\n=== Camera {camera_id} URLs ===")
logging.info(f"HTTP Snapshot: http://{local_ip}:{http_port}/snapshot")
logging.info(f"Status: http://{local_ip}:{http_port}/status")
if rtsp_process:
logging.info(f"RTSP Stream: rtsp://{local_ip}:{rtsp_port}/stream")
else:
logging.info("RTSP Stream: Disabled")
logging.info(f"\n=== CMS Configuration for Camera {camera_id} ===")
logging.info(f"Camera Identifier: webcam-camera-0{camera_id}")
logging.info(f"Snapshot URL: http://{local_ip}:{http_port}/snapshot")
logging.info(f"Snapshot Interval: 2000")
if rtsp_process:
logging.info(f"RTSP URL: rtsp://{local_ip}:{rtsp_port}/stream")
logging.info(f"\nCamera {camera_id} is ready! Press Ctrl+C to stop")
try:
# Start HTTP server
http_server.serve_forever()
except KeyboardInterrupt:
logging.info(f"Camera{camera_id}: Shutting down...")
finally:
# Clean up
if webcam_cap:
webcam_cap.release()
if rtsp_process:
logging.info(f"Camera{camera_id}: Stopping RTSP stream...")
rtsp_process.terminate()
try:
rtsp_process.wait(timeout=5)
except subprocess.TimeoutExpired:
rtsp_process.kill()
http_server.server_close()
logging.info(f"Camera{camera_id}: Stopped")
if __name__ == "__main__":
main()

View file

@ -681,7 +681,7 @@ def run_detection_with_tracking(frame, node, context=None):
# Update stability tracking even when no detection (to reset counters)
camera_id = context.get("camera_id", "unknown") if context else "unknown"
model_id = node.get("modelId", "unknown")
track_validation_result = update_single_track_stability(node, None, camera_id, frame.shape, stability_threshold)
track_validation_result = update_single_track_stability(node, None, camera_id, frame.shape, stability_threshold, context)
# Store validation state in context for pipeline decisions
if context is not None:
@ -745,7 +745,7 @@ def run_detection_with_tracking(frame, node, context=None):
# Update stability tracking even when no detection (to reset counters)
camera_id = context.get("camera_id", "unknown") if context else "unknown"
model_id = node.get("modelId", "unknown")
track_validation_result = update_single_track_stability(node, None, camera_id, frame.shape, stability_threshold)
track_validation_result = update_single_track_stability(node, None, camera_id, frame.shape, stability_threshold, context)
# Store validation state in context for pipeline decisions
if context is not None:
@ -813,7 +813,7 @@ def run_detection_with_tracking(frame, node, context=None):
model_id = node.get("modelId", "unknown")
# Update stability tracking for the single best detection
track_validation_result = update_single_track_stability(node, best_detection, camera_id, frame.shape, stability_threshold)
track_validation_result = update_single_track_stability(node, best_detection, camera_id, frame.shape, stability_threshold, context)
# Store validation state in context for pipeline decisions
if context is not None:
@ -866,14 +866,8 @@ def get_camera_stability_data(camera_id, model_id):
"waiting_for_backend_session": False,
"wait_start_time": 0.0,
"reset_tracker_on_resume": False
},
"occupancy_state": {
"phase": "validation", # "validation", "waiting_for_session", or "occupancy"
"absence_counter": 0, # Count consecutive frames without stable tracks
"max_absence_frames": 3, # Trigger "none" after this many absent frames
"pipeline_completed": False # Track if full pipeline has run
}
# Removed detection_counter - using only track-based validation now
# Removed obsolete occupancy_state - app.py handles all mode transitions now
}
return _camera_stability_tracking[camera_id][model_id]
@ -886,6 +880,7 @@ def reset_camera_stability_tracking(camera_id, model_id):
# Clear all tracking data
track_counters = stability_data["track_stability_counters"]
stable_tracks = stability_data["stable_tracks"]
session_state = stability_data["session_state"]
old_counters = dict(track_counters)
old_stable = list(stable_tracks)
@ -893,17 +888,16 @@ def reset_camera_stability_tracking(camera_id, model_id):
track_counters.clear()
stable_tracks.clear()
# Reset occupancy state to validation
stability_data["occupancy_state"]["phase"] = "validation"
stability_data["occupancy_state"]["absence_counter"] = 0
stability_data["occupancy_state"]["pipeline_completed"] = False
# IMPORTANT: Set flag to reset YOLO tracker on next detection run
# This will ensure track IDs start fresh (1, 2, 3...) instead of continuing from old IDs
session_state["reset_tracker_on_resume"] = True
logger.info(f"🧹 Camera {camera_id}: CLEARED stability tracking - old_counters={old_counters}, old_stable={old_stable}")
# Occupancy state reset logging removed - not used in enhanced lightweight mode
logger.info(f"🔄 Camera {camera_id}: YOLO tracker will be reset on next detection - fresh track IDs will start from 1")
else:
logger.debug(f"🧹 Camera {camera_id}: No stability tracking data to clear for model {model_id}")
def update_single_track_stability(node, detection, camera_id, frame_shape=None, stability_threshold=4):
def update_single_track_stability(node, detection, camera_id, frame_shape=None, stability_threshold=4, context=None):
"""Update track stability validation for a single highest confidence car."""
model_id = node.get("modelId", "unknown")
@ -913,113 +907,102 @@ def update_single_track_stability(node, detection, camera_id, frame_shape=None,
logger.debug(f"⏭️ Camera {camera_id}: Skipping validation for branch node {model_id} - validation only done at main pipeline level")
return {"validation_complete": False, "branch_node": True, "stable_tracks": [], "current_tracks": []}
# Check current mode - VALIDATION COUNTERS should increment in both validation_detecting and full_pipeline modes
current_mode = context.get("current_mode", "unknown") if context else "unknown"
is_validation_mode = (current_mode in ["validation_detecting", "full_pipeline"])
# Get camera-specific stability data
stability_data = get_camera_stability_data(camera_id, model_id)
track_counters = stability_data["track_stability_counters"]
stable_tracks = stability_data["stable_tracks"]
occupancy_state = stability_data["occupancy_state"]
current_phase = occupancy_state["phase"]
current_track_id = detection.get("id") if detection else None
if current_phase == "validation":
# ═══ VALIDATION PHASE: Count consecutive frames for single track ═══
logger.debug(f"📋 Camera {camera_id}: === TRACK VALIDATION ANALYSIS ===")
logger.debug(f"📋 Camera {camera_id}: Current track_id: {current_track_id}")
logger.debug(f"📋 Camera {camera_id}: Existing counters: {dict(track_counters)}")
logger.debug(f"📋 Camera {camera_id}: Stable tracks: {list(stable_tracks)}")
# ═══ MODE-AWARE TRACK VALIDATION ═══
logger.debug(f"📋 Camera {camera_id}: === TRACK VALIDATION ANALYSIS ===")
logger.debug(f"📋 Camera {camera_id}: Current mode: {current_mode} (validation_mode={is_validation_mode})")
logger.debug(f"📋 Camera {camera_id}: Current track_id: {current_track_id} (assigned by YOLO tracking - not sequential)")
logger.debug(f"📋 Camera {camera_id}: Existing counters: {dict(track_counters)}")
logger.debug(f"📋 Camera {camera_id}: Stable tracks: {list(stable_tracks)}")
# IMPORTANT: Only modify validation counters during validation_detecting mode
if not is_validation_mode:
logger.debug(f"🚫 Camera {camera_id}: NOT in validation mode - skipping counter modifications")
return {
"validation_complete": False,
"stable_tracks": list(stable_tracks),
"current_tracks": [current_track_id] if current_track_id is not None else []
}
if current_track_id is not None:
# Check if this is a different track than we were tracking
previous_track_ids = list(track_counters.keys())
if current_track_id is not None:
# Check if this is a different track than we were tracking
previous_track_ids = list(track_counters.keys())
# ALWAYS reset counter if:
# 1. This is a different track ID than before
# 2. OR if we had no previous tracking (fresh start)
should_reset = (
len(previous_track_ids) == 0 or # No previous tracking
current_track_id not in previous_track_ids # Different track ID
)
logger.debug(f"📋 Camera {camera_id}: Previous track_ids: {previous_track_ids}")
logger.debug(f"📋 Camera {camera_id}: Should reset counters: {should_reset} (no_previous={len(previous_track_ids) == 0}, different_id={current_track_id not in previous_track_ids})")
if should_reset and previous_track_ids:
# Clear all previous tracking - different car detected
# VALIDATION MODE: Reset counter if different track OR if track was previously stable
should_reset = (
len(previous_track_ids) == 0 or # No previous tracking
current_track_id not in previous_track_ids or # Different track ID
current_track_id in stable_tracks # Track was stable - start fresh validation
)
logger.debug(f"📋 Camera {camera_id}: Previous track_ids: {previous_track_ids}")
logger.debug(f"📋 Camera {camera_id}: Track {current_track_id} was stable: {current_track_id in stable_tracks}")
logger.debug(f"📋 Camera {camera_id}: Should reset counters: {should_reset}")
if should_reset:
# Clear all previous tracking - fresh validation needed
if previous_track_ids:
for old_track_id in previous_track_ids:
old_count = track_counters.pop(old_track_id, 0)
stable_tracks.discard(old_track_id)
logger.info(f"🔄 Camera {camera_id}: Different car detected (track {current_track_id}) - RESET previous track {old_track_id} counter from {old_count} to 0")
logger.debug(f"🔄 Camera {camera_id}: Cleared track {old_track_id} from counters and stable_tracks")
logger.info(f"🔄 Camera {camera_id}: VALIDATION RESET - track {old_track_id} counter from {old_count} to 0 (reason: {'stable_track_restart' if current_track_id == old_track_id else 'different_track'})")
# Set counter to 1 for current track (fresh start each frame)
# Start fresh validation for this track
old_count = track_counters.get(current_track_id, 0) # Store old count for logging
track_counters[current_track_id] = 1
current_count = 1
logger.info(f"🆕 Camera {camera_id}: FRESH VALIDATION - Track {current_track_id} starting at 1/{stability_threshold}")
else:
# Continue validation for same track
old_count = track_counters.get(current_track_id, 0)
track_counters[current_track_id] = track_counters.get(current_track_id, 0) + 1
track_counters[current_track_id] = old_count + 1
current_count = track_counters[current_track_id]
logger.debug(f"🔢 Camera {camera_id}: Track {current_track_id} counter: {old_count}{current_count}")
logger.info(f"🔍 Camera {camera_id}: Track ID {current_track_id} validation {current_count}/{stability_threshold}")
# Check if track has reached stability threshold
logger.debug(f"📊 Camera {camera_id}: Checking stability: {current_count} >= {stability_threshold}? {current_count >= stability_threshold}")
logger.debug(f"📊 Camera {camera_id}: Already stable: {current_track_id in stable_tracks}")
if current_count >= stability_threshold and current_track_id not in stable_tracks:
stable_tracks.add(current_track_id)
occupancy_state["phase"] = "waiting_for_session"
occupancy_state["pipeline_completed"] = False
logger.info(f"✅ Camera {camera_id}: Track ID {current_track_id} STABLE after {current_count} consecutive frames")
logger.info(f"🎯 Camera {camera_id}: TRACK VALIDATION COMPLETE")
logger.debug(f"🎯 Camera {camera_id}: Phase changed to: waiting_for_session")
logger.debug(f"🎯 Camera {camera_id}: Stable tracks now: {list(stable_tracks)}")
return {
"validation_complete": True,
"send_none_detection": True,
"stable_tracks": [current_track_id],
"newly_stable_tracks": [current_track_id],
"current_tracks": [current_track_id]
}
elif current_count >= stability_threshold:
logger.debug(f"📊 Camera {camera_id}: Track {current_track_id} already stable - not re-adding")
else:
# No car detected - ALWAYS clear all tracking and reset counters
logger.debug(f"🚫 Camera {camera_id}: NO CAR DETECTED - clearing all tracking")
if track_counters:
logger.debug(f"🚫 Camera {camera_id}: Existing counters before reset: {dict(track_counters)}")
for track_id in list(track_counters.keys()):
old_count = track_counters.pop(track_id, 0)
stable_tracks.discard(track_id)
logger.info(f"🔄 Camera {camera_id}: No car detected - RESET track {track_id} counter from {old_count} to 0")
logger.debug(f"🚫 Camera {camera_id}: Cleared track {track_id} (was at {old_count}/{stability_threshold})")
track_counters.clear() # Ensure complete reset
stable_tracks.clear() # Clear all stable tracks
logger.debug(f"🚫 Camera {camera_id}: All counters and stable tracks cleared")
else:
logger.debug(f"🚫 Camera {camera_id}: No existing counters to clear")
logger.debug(f"Camera {camera_id}: VALIDATION - no car detected (all counters reset)")
elif current_phase == "waiting_for_session":
# ═══ WAITING PHASE: Maintain track stability ═══
logger.debug(f"⏳ Camera {camera_id}: WAITING FOR SESSION - monitoring stable track")
logger.debug(f"⏳ Camera {camera_id}: Current track_id: {current_track_id}, Stable tracks: {list(stable_tracks)}")
if current_track_id is None or current_track_id not in stable_tracks:
# Lost the stable track
logger.debug(f"⏳ Camera {camera_id}: Stable track lost - clearing all tracking")
stable_tracks.clear()
track_counters.clear()
logger.info(f"🔄 Camera {camera_id}: Lost stable track during waiting phase")
else:
logger.debug(f"⏳ Camera {camera_id}: Stable track {current_track_id} still present")
logger.debug(f"🔢 Camera {camera_id}: Track {current_track_id} counter: {old_count}{current_count}")
logger.info(f"🔍 Camera {camera_id}: Track ID {current_track_id} validation {current_count}/{stability_threshold}")
elif current_phase == "occupancy":
# ═══ OCCUPANCY PHASE: UNUSED in enhanced lightweight mode ═══
# This phase is bypassed by the new lightweight mode system
# Keeping minimal logic for backward compatibility but no CLI logging
if current_track_id is not None and current_track_id in stable_tracks:
occupancy_state["absence_counter"] = 0
# Check if track has reached stability threshold
logger.debug(f"📊 Camera {camera_id}: Checking stability: {current_count} >= {stability_threshold}? {current_count >= stability_threshold}")
logger.debug(f"📊 Camera {camera_id}: Already stable: {current_track_id in stable_tracks}")
if current_count >= stability_threshold and current_track_id not in stable_tracks:
stable_tracks.add(current_track_id)
logger.info(f"✅ Camera {camera_id}: Track ID {current_track_id} STABLE after {current_count} consecutive frames")
logger.info(f"🎯 Camera {camera_id}: TRACK VALIDATION COMPLETE")
logger.debug(f"🎯 Camera {camera_id}: Stable tracks now: {list(stable_tracks)}")
return {
"validation_complete": True,
"send_none_detection": True,
"stable_tracks": [current_track_id],
"newly_stable_tracks": [current_track_id],
"current_tracks": [current_track_id]
}
elif current_count >= stability_threshold:
logger.debug(f"📊 Camera {camera_id}: Track {current_track_id} already stable - not re-adding")
else:
# No car detected - ALWAYS clear all tracking and reset counters
logger.debug(f"🚫 Camera {camera_id}: NO CAR DETECTED - clearing all tracking")
if track_counters or stable_tracks:
logger.debug(f"🚫 Camera {camera_id}: Existing state before reset: counters={dict(track_counters)}, stable={list(stable_tracks)}")
for track_id in list(track_counters.keys()):
old_count = track_counters.pop(track_id, 0)
logger.info(f"🔄 Camera {camera_id}: No car detected - RESET track {track_id} counter from {old_count} to 0")
track_counters.clear() # Ensure complete reset
stable_tracks.clear() # Clear all stable tracks
logger.info(f"✅ Camera {camera_id}: RESET TO VALIDATION PHASE - All counters and stable tracks cleared")
else:
occupancy_state["absence_counter"] += 1
logger.debug(f"🚫 Camera {camera_id}: No existing counters to clear")
logger.debug(f"Camera {camera_id}: VALIDATION - no car detected (all counters reset)")
# Final return - validation not complete
result = {
@ -1040,9 +1023,9 @@ def update_track_stability_validation(node, detections, camera_id, frame_shape=N
logger.warning(f"update_track_stability_validation called for camera {camera_id} - this function is deprecated, use update_single_track_stability instead")
if detections:
best_detection = max(detections, key=lambda x: x.get("confidence", 0))
return update_single_track_stability(node, best_detection, camera_id, frame_shape, stability_threshold)
return update_single_track_stability(node, best_detection, camera_id, frame_shape, stability_threshold, None)
else:
return update_single_track_stability(node, None, camera_id, frame_shape, stability_threshold)
return update_single_track_stability(node, None, camera_id, frame_shape, stability_threshold, None)
def update_detection_stability(node, detections, camera_id, frame_shape=None):
"""Legacy detection-based stability counter - DEPRECATED."""
@ -1051,95 +1034,9 @@ def update_detection_stability(node, detections, camera_id, frame_shape=None):
return {"validation_complete": False, "valid_detections": 0, "deprecated": True}
def update_track_stability(node, detections, camera_id, frame_shape=None):
"""Update stability counters with two-phase detection system: validation → occupancy."""
stability_threshold = node.get("stabilityThreshold", 1)
model_id = node.get("modelId", "unknown")
min_bbox_area_ratio = node.get("minBboxAreaRatio", 0.0)
# Note: This function is deprecated - using detection-based stability now
# Get camera-specific stability data
stability_data = get_camera_stability_data(camera_id, model_id)
track_counters = stability_data["track_stability_counters"]
stable_tracks = stability_data["stable_tracks"]
occupancy_state = stability_data["occupancy_state"]
# Validate detections against confidence + area requirements
valid_detections = []
if frame_shape is not None:
frame_height, frame_width = frame_shape[:2]
frame_area = frame_width * frame_height
for detection in detections:
bbox = detection.get("bbox", [])
if len(bbox) >= 4:
x1, y1, x2, y2 = bbox
bbox_width = abs(x2 - x1)
bbox_height = abs(y2 - y1)
bbox_area = bbox_width * bbox_height
area_ratio = bbox_area / frame_area if frame_area > 0 else 0.0
if area_ratio >= min_bbox_area_ratio:
valid_detections.append(detection)
pass # Valid detection - no debug spam
else:
pass # Small detection - no debug spam
else:
valid_detections = detections
current_phase = occupancy_state["phase"]
if current_phase == "validation":
# ═══ VALIDATION PHASE: Count detections until stable ═══
detection_key = f"camera_{camera_id}_detections"
if valid_detections:
# Valid detection found - increment counter
track_counters[detection_key] = track_counters.get(detection_key, 0) + 1
current_count = track_counters[detection_key]
pass # Validation count - shown in main logs
# Check if we've reached the stability threshold
if current_count >= stability_threshold and detection_key not in stable_tracks:
stable_tracks.add(detection_key)
# Switch to waiting for backend session phase
occupancy_state["phase"] = "waiting_for_session"
occupancy_state["absence_counter"] = 0
occupancy_state["pipeline_completed"] = False
logger.info(f"✅ Camera {camera_id}: VALIDATION COMPLETE after {current_count} detections - READY FOR FULL PIPELINE")
else:
# No valid detections - reset validation counter for consecutive requirement
if detection_key in track_counters:
old_count = track_counters[detection_key]
track_counters.pop(detection_key, None)
stable_tracks.discard(detection_key)
logger.info(f"🔄 Camera {camera_id}: VALIDATION RESET - no valid detection, counter reset from {old_count} to 0 (requires consecutive detections)")
else:
logger.debug(f"Camera {camera_id}: VALIDATION - no valid detection, counter remains 0")
elif current_phase == "waiting_for_session":
# ═══ WAITING FOR BACKEND SESSION PHASE ═══
# Don't do occupancy monitoring yet, just maintain validation of current detections
# The main pipeline will handle sessionId detection and phase transition
pass # Waiting phase - no occupancy logic yet
elif current_phase == "occupancy":
# ═══ OCCUPANCY PHASE: Monitor car presence ═══
if valid_detections:
# Car still present - reset absence counter
if occupancy_state["absence_counter"] > 0:
pass # Car detected - counter reset (no debug spam)
occupancy_state["absence_counter"] = 0
else:
# No car detected - increment absence counter
occupancy_state["absence_counter"] += 1
pass # Absence count - will show in timeout log
pass # Phase summary - excessive debug
# Return occupancy state for pipeline decisions
return occupancy_state
"""DEPRECATED: This function is obsolete and should not be used."""
logger.warning(f"update_track_stability called for camera {camera_id} - this function is deprecated and obsolete")
return {"phase": "validation", "absence_counter": 0, "deprecated": True}
def check_stable_tracks(camera_id, model_id, regions_dict):
"""Check if any stable tracks match the detected classes for a specific camera."""
@ -1434,7 +1331,7 @@ def run_lightweight_detection(frame, node: dict):
logger.error(f"Error in lightweight detection: {str(e)}", exc_info=True)
return {"car_detected": False, "best_detection": None}
def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None, validated_detection=None):
"""
Enhanced pipeline that supports:
- Multi-class detection (detecting multiple classes simultaneously)
@ -1501,8 +1398,31 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
}
return (none_detection, [0, 0, 0, 0]) if return_bbox else none_detection
# ─── Detection stage - Using structured detection function ──────────────────
all_detections, regions_dict, track_validation_result = run_detection_with_tracking(frame, node, context)
# ─── Detection stage - Use validated detection if provided (full_pipeline mode) ───
if validated_detection:
track_id = validated_detection.get('track_id')
logger.info(f"🔄 PIPELINE: Using validated detection from validation phase - track_id={track_id}")
# Convert validated detection back to all_detections format for branch processing
all_detections = [validated_detection]
# Create regions_dict based on validated detection class with proper structure
class_name = validated_detection.get("class", "car")
regions_dict = {
class_name: {
"confidence": validated_detection.get("confidence"),
"bbox": validated_detection.get("bbox", [0, 0, 0, 0]),
"detection": validated_detection
}
}
# Bypass track validation completely - force pipeline execution
track_validation_result = {
"validation_complete": True,
"stable_tracks": ["cached"], # Use dummy stable track to force pipeline execution
"current_tracks": ["cached"],
"bypass_validation": True
}
else:
# Normal detection stage - Using structured detection function
all_detections, regions_dict, track_validation_result = run_detection_with_tracking(frame, node, context)
if not all_detections:
logger.debug("No detections from structured detection function - sending 'none' detection")
@ -1524,14 +1444,14 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
camera_id = context.get("camera_id", "unknown") if context else "unknown"
if stability_threshold > 1 and tracking_config.get("enabled", True):
# Extract occupancy state from stability data (updated by track validation function)
# Note: Old occupancy state system removed - app.py handles all mode transitions now
# Track validation is handled by update_single_track_stability function
model_id = node.get("modelId", "unknown")
stability_data = get_camera_stability_data(camera_id, model_id)
occupancy_state = stability_data["occupancy_state"]
current_phase = occupancy_state.get("phase", "validation")
absence_counter = occupancy_state.get("absence_counter", 0)
max_absence_frames = occupancy_state.get("max_absence_frames", 3)
# Simplified: just check if we have stable tracks from track validation
current_phase = "validation" # Always validation phase in simplified system
absence_counter = 0
max_absence_frames = 3
if current_phase == "validation":
# ═══ TRACK VALIDATION PHASE ═══
@ -1562,78 +1482,8 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
# We have stable tracks - validation is complete, proceed with pipeline
logger.info(f"🎯 Camera {camera_id}: STABLE TRACKS DETECTED - proceeding with full pipeline (tracks: {stable_tracks})")
elif current_phase == "waiting_for_session":
# ═══ WAITING FOR BACKEND SESSION PHASE ═══
if backend_session_id:
# Backend has responded with sessionId - NOW run the full pipeline for the first time
logger.info(f"🎯 Camera {camera_id}: BACKEND SESSION RECEIVED - RUNNING FULL PIPELINE (sessionId: {backend_session_id})")
occupancy_state["phase"] = "occupancy"
occupancy_state["absence_counter"] = 0
# Continue with normal pipeline processing now that we have sessionId
else:
# Still waiting for backend sessionId - send None detection dict to trigger sessionId generation
if not occupancy_state["pipeline_completed"]:
# First time in waiting phase - send empty detection to trigger sessionId
logger.info(f"⚙️ Camera {camera_id}: WAITING PHASE - sending empty detection {{}} for sessionId generation")
occupancy_state["pipeline_completed"] = True
# Return a special detection that signals app.py to send empty detection: {}
none_detection = {
"class": "validation_complete",
"confidence": 1.0,
"bbox": [0, 0, 0, 0],
"send_empty_detection": True
}
return (none_detection, [0, 0, 0, 0]) if return_bbox else none_detection
else:
# Already sent None detection - continue waiting for sessionId
logger.debug(f"⏳ Camera {camera_id}: WAITING FOR BACKEND SESSION - None detection already sent, waiting for sessionId")
waiting_detection = {
"class": "waiting_session_id",
"confidence": 1.0,
"bbox": [0, 0, 0, 0],
"waiting_for_session": True
}
return (waiting_detection, [0, 0, 0, 0]) if return_bbox else waiting_detection
elif current_phase == "occupancy":
# ═══ OCCUPANCY PHASE ═══
stable_tracks = track_validation_result.get("stable_tracks", [])
current_tracks = track_validation_result.get("current_tracks", [])
# Check if any stable tracks are still present
stable_tracks_present = bool(set(stable_tracks) & set(current_tracks))
if absence_counter >= max_absence_frames:
# Stable tracks have been absent for too long - trigger "none" detection and reset
# Occupancy timeout logging removed - not used in enhanced lightweight mode
# Reset occupancy state to validation phase
stability_data = get_camera_stability_data(camera_id, model_id)
stability_data["occupancy_state"]["phase"] = "validation"
stability_data["occupancy_state"]["absence_counter"] = 0
stability_data["track_stability_counters"].clear()
stability_data["stable_tracks"].clear()
logger.info(f"🔄 Camera {camera_id}: RESET TO VALIDATION PHASE - cleared track stability tracking (sessionId should become null)")
# Return "none" detection to trigger cache clearing in app.py
none_detection = {"class": "none", "confidence": 1.0, "bbox": [0, 0, 0, 0], "occupancy_triggered": True}
return (none_detection, [0, 0, 0, 0]) if return_bbox else none_detection
else:
# Still in occupancy phase - check if stable tracks are present
if stable_tracks_present:
# Stable tracks detected - continue with cached result or light processing
# Occupancy phase logging removed - not used in enhanced lightweight mode
pass
else:
# No stable tracks - absence counter was already incremented in track validation
# Occupancy phase logging removed - not used in enhanced lightweight mode
pass
# Continue with normal pipeline processing
pass
# Note: Old waiting_for_session and occupancy phases removed
# app.py lightweight mode handles all state transitions now
# ─── Pre-validate pipeline execution (only proceed if we have stable tracks for main pipeline) ────────────────────────
is_branch_node = node.get("cropClass") is not None or node.get("parallel") is True

58
start_4_cameras.bat Normal file
View file

@ -0,0 +1,58 @@
@echo off
echo ========================================
echo Starting 4-Camera Simulation
echo ========================================
echo.
echo This will open 4 terminal windows for 4 virtual cameras
echo Each camera will use the same webcam but with different ports
echo.
echo Camera URLs will be:
echo Camera 1: http://10.101.1.4:8080/snapshot
echo Camera 2: http://10.101.1.4:8081/snapshot
echo Camera 3: http://10.101.1.4:8082/snapshot
echo Camera 4: http://10.101.1.4:8083/snapshot
echo.
echo Press any key to start all cameras...
pause
echo Starting Camera 1...
start "Camera 1" cmd /k "python multi_camera_simulator.py 1 && pause"
echo Starting Camera 2...
start "Camera 2" cmd /k "python multi_camera_simulator.py 2 && pause"
echo Starting Camera 3...
start "Camera 3" cmd /k "python multi_camera_simulator.py 3 && pause"
echo Starting Camera 4...
start "Camera 4" cmd /k "python multi_camera_simulator.py 4 && pause"
echo.
echo ========================================
echo All 4 cameras started!
echo ========================================
echo.
echo Use these URLs in your CMS:
echo.
echo Camera 1 (Yellow border):
echo ID: webcam-camera-01
echo Snapshot: http://10.101.1.4:8080/snapshot
echo RTSP: rtsp://10.101.1.4:8550/stream
echo.
echo Camera 2 (Magenta border):
echo ID: webcam-camera-02
echo Snapshot: http://10.101.1.4:8081/snapshot
echo RTSP: rtsp://10.101.1.4:8551/stream
echo.
echo Camera 3 (Green border):
echo ID: webcam-camera-03
echo Snapshot: http://10.101.1.4:8082/snapshot
echo RTSP: rtsp://10.101.1.4:8552/stream
echo.
echo Camera 4 (Blue border):
echo ID: webcam-camera-04
echo Snapshot: http://10.101.1.4:8083/snapshot
echo RTSP: rtsp://10.101.1.4:8553/stream
echo.
echo Press any key to exit...
pause

78
start_4_cameras.sh Normal file
View file

@ -0,0 +1,78 @@
#!/bin/bash
echo "========================================"
echo "Starting 4-Camera Simulation"
echo "========================================"
echo ""
echo "This will start 4 virtual cameras in background"
echo "Each camera will use the same webcam but with different ports"
echo ""
echo "Camera URLs will be:"
echo "Camera 1: http://10.101.1.4:8080/snapshot"
echo "Camera 2: http://10.101.1.4:8081/snapshot"
echo "Camera 3: http://10.101.1.4:8082/snapshot"
echo "Camera 4: http://10.101.1.4:8083/snapshot"
echo ""
# Function to start camera in new terminal
start_camera() {
local camera_id=$1
echo "Starting Camera $camera_id..."
# Try different terminal emulators
if command -v gnome-terminal &> /dev/null; then
gnome-terminal --title="Camera $camera_id" -- bash -c "python3 multi_camera_simulator.py $camera_id; read -p 'Press Enter to close...'"
elif command -v xterm &> /dev/null; then
xterm -title "Camera $camera_id" -e "python3 multi_camera_simulator.py $camera_id; read -p 'Press Enter to close...'" &
elif command -v konsole &> /dev/null; then
konsole --title "Camera $camera_id" -e bash -c "python3 multi_camera_simulator.py $camera_id; read -p 'Press Enter to close...'" &
elif [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
osascript -e "tell application \"Terminal\" to do script \"cd $(pwd) && python3 multi_camera_simulator.py $camera_id\""
else
echo "No suitable terminal emulator found. Starting in background..."
python3 multi_camera_simulator.py $camera_id &
echo "Camera $camera_id PID: $!"
fi
sleep 1
}
# Start all 4 cameras
start_camera 1
start_camera 2
start_camera 3
start_camera 4
echo ""
echo "========================================"
echo "All 4 cameras started!"
echo "========================================"
echo ""
echo "Use these URLs in your CMS:"
echo ""
echo "Camera 1 (Yellow border):"
echo " ID: webcam-camera-01"
echo " Snapshot: http://10.101.1.4:8080/snapshot"
echo " RTSP: rtsp://10.101.1.4:8550/stream"
echo ""
echo "Camera 2 (Magenta border):"
echo " ID: webcam-camera-02"
echo " Snapshot: http://10.101.1.4:8081/snapshot"
echo " RTSP: rtsp://10.101.1.4:8551/stream"
echo ""
echo "Camera 3 (Green border):"
echo " ID: webcam-camera-03"
echo " Snapshot: http://10.101.1.4:8082/snapshot"
echo " RTSP: rtsp://10.101.1.4:8552/stream"
echo ""
echo "Camera 4 (Blue border):"
echo " ID: webcam-camera-04"
echo " Snapshot: http://10.101.1.4:8083/snapshot"
echo " RTSP: rtsp://10.101.1.4:8553/stream"
echo ""
echo "To stop all cameras, close the terminal windows or press Ctrl+C in each"
echo ""
# Keep script running
read -p "Press Enter to exit..."

File diff suppressed because it is too large Load diff