update tracker
This commit is contained in:
parent
a54da904f7
commit
3a4a27ca68
7 changed files with 1405 additions and 84 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -13,3 +13,5 @@ no_frame_debug.log
|
|||
|
||||
feeder/
|
||||
.venv/
|
||||
.vscode/
|
||||
dist/
|
||||
|
|
7
app.py
7
app.py
|
@ -27,7 +27,7 @@ from websockets.exceptions import ConnectionClosedError
|
|||
from ultralytics import YOLO
|
||||
|
||||
# Import shared pipeline functions
|
||||
from siwatsystem.pympta import load_pipeline_from_zip, run_pipeline
|
||||
from siwatsystem.pympta import load_pipeline_from_zip, run_pipeline, cleanup_camera_stability
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
@ -325,6 +325,7 @@ async def detect(websocket: WebSocket):
|
|||
"type": "imageDetection",
|
||||
"subscriptionIdentifier": stream["subscriptionIdentifier"],
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S.%fZ", time.gmtime()),
|
||||
# "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) + f".{int(time.time() * 1000) % 1000:03d}Z",
|
||||
"data": {
|
||||
"detection": detection_dict,
|
||||
"modelId": stream["modelId"],
|
||||
|
@ -704,6 +705,7 @@ async def detect(websocket: WebSocket):
|
|||
del camera_streams[camera_url]
|
||||
|
||||
latest_frames.pop(subscription_id, None)
|
||||
cleanup_camera_stability(subscription_id)
|
||||
logger.info(f"Unsubscribed from camera {subscription_id}")
|
||||
|
||||
async def process_streams():
|
||||
|
@ -1018,8 +1020,9 @@ async def detect(websocket: WebSocket):
|
|||
else:
|
||||
logger.info(f"Shared stream for {camera_url} still has {shared_stream['ref_count']} references")
|
||||
|
||||
# Clean up cached frame
|
||||
# Clean up cached frame and stability tracking
|
||||
latest_frames.pop(camera_id, None)
|
||||
cleanup_camera_stability(camera_id)
|
||||
logger.info(f"Unsubscribed from camera {camera_id}")
|
||||
# Note: Keep models in memory for potential reuse
|
||||
elif msg_type == "requestState":
|
||||
|
|
142
debug/test_camera_indices.py
Normal file
142
debug/test_camera_indices.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to check available camera indices
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import logging
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("camera_index_test")
|
||||
|
||||
def test_camera_index(index):
|
||||
"""Test if a camera index is available"""
|
||||
try:
|
||||
cap = cv2.VideoCapture(index)
|
||||
if cap.isOpened():
|
||||
ret, frame = cap.read()
|
||||
if ret and frame is not None:
|
||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
|
||||
cap.release()
|
||||
return True, f"{width}x{height} @ {fps}fps"
|
||||
else:
|
||||
cap.release()
|
||||
return False, "Can open but cannot read frames"
|
||||
else:
|
||||
cap.release()
|
||||
return False, "Cannot open camera"
|
||||
except Exception as e:
|
||||
return False, f"Error: {str(e)}"
|
||||
|
||||
def get_windows_cameras_ffmpeg():
|
||||
"""Get available cameras on Windows using FFmpeg"""
|
||||
try:
|
||||
result = subprocess.run(['ffmpeg', '-f', 'dshow', '-list_devices', 'true', '-i', 'dummy'],
|
||||
capture_output=True, text=True, timeout=10, encoding='utf-8', errors='ignore')
|
||||
output = result.stderr
|
||||
|
||||
lines = output.split('\n')
|
||||
video_devices = []
|
||||
|
||||
# Parse the output - look for lines with (video) that contain device names in quotes
|
||||
for line in lines:
|
||||
if '[dshow @' in line and '(video)' in line and '"' in line:
|
||||
# Extract device name between first pair of quotes
|
||||
start = line.find('"') + 1
|
||||
end = line.find('"', start)
|
||||
if start > 0 and end > start:
|
||||
device_name = line[start:end]
|
||||
video_devices.append(device_name)
|
||||
|
||||
logger.info(f"FFmpeg detected video devices: {video_devices}")
|
||||
return video_devices
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get Windows camera names: {e}")
|
||||
return []
|
||||
|
||||
def main():
|
||||
logger.info("=== Camera Index Test ===")
|
||||
|
||||
# Check FFmpeg availability for Windows device detection
|
||||
ffmpeg_available = False
|
||||
try:
|
||||
result = subprocess.run(['ffmpeg', '-version'], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
ffmpeg_available = True
|
||||
logger.info("FFmpeg is available")
|
||||
except:
|
||||
logger.info("FFmpeg not available")
|
||||
|
||||
# Get Windows camera names if possible
|
||||
if sys.platform.startswith('win') and ffmpeg_available:
|
||||
logger.info("\n=== Windows Camera Devices (FFmpeg) ===")
|
||||
cameras = get_windows_cameras_ffmpeg()
|
||||
if cameras:
|
||||
for i, camera in enumerate(cameras):
|
||||
logger.info(f"Device {i}: {camera}")
|
||||
else:
|
||||
logger.info("No cameras detected via FFmpeg")
|
||||
|
||||
# Test camera indices 0-9
|
||||
logger.info("\n=== Testing Camera Indices ===")
|
||||
available_cameras = []
|
||||
|
||||
for index in range(10):
|
||||
logger.info(f"Testing camera index {index}...")
|
||||
is_available, info = test_camera_index(index)
|
||||
|
||||
if is_available:
|
||||
logger.info(f"✓ Camera {index}: AVAILABLE - {info}")
|
||||
available_cameras.append(index)
|
||||
else:
|
||||
logger.info(f"✗ Camera {index}: NOT AVAILABLE - {info}")
|
||||
|
||||
# Summary
|
||||
logger.info("\n=== Summary ===")
|
||||
if available_cameras:
|
||||
logger.info(f"Available camera indices: {available_cameras}")
|
||||
logger.info(f"Default camera index to use: {available_cameras[0]}")
|
||||
|
||||
# Test the first available camera more thoroughly
|
||||
logger.info(f"\n=== Detailed Test for Camera {available_cameras[0]} ===")
|
||||
cap = cv2.VideoCapture(available_cameras[0])
|
||||
if cap.isOpened():
|
||||
# Get properties
|
||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
backend = cap.getBackendName()
|
||||
|
||||
logger.info(f"Resolution: {width}x{height}")
|
||||
logger.info(f"FPS: {fps}")
|
||||
logger.info(f"Backend: {backend}")
|
||||
|
||||
# Test frame capture
|
||||
ret, frame = cap.read()
|
||||
if ret and frame is not None:
|
||||
logger.info(f"Frame capture: SUCCESS")
|
||||
logger.info(f"Frame shape: {frame.shape}")
|
||||
logger.info(f"Frame dtype: {frame.dtype}")
|
||||
else:
|
||||
logger.info(f"Frame capture: FAILED")
|
||||
|
||||
cap.release()
|
||||
else:
|
||||
logger.error("No cameras available!")
|
||||
logger.info("Possible solutions:")
|
||||
logger.info("1. Check if camera is connected and not used by another application")
|
||||
logger.info("2. Check camera permissions")
|
||||
logger.info("3. Try different camera indices")
|
||||
logger.info("4. Install camera drivers")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -17,6 +17,13 @@ from .database import DatabaseManager
|
|||
# Create a logger specifically for this module
|
||||
logger = logging.getLogger("detector_worker.pympta")
|
||||
|
||||
# Global camera-aware stability tracking
|
||||
# Structure: {camera_id: {model_id: {"track_stability_counters": {track_id: count}, "stable_tracks": set(), "session_state": {...}}}}
|
||||
_camera_stability_tracking = {}
|
||||
|
||||
# Timer-based cooldown configuration (for testing)
|
||||
_cooldown_duration_seconds = 30
|
||||
|
||||
def validate_redis_config(redis_config: dict) -> bool:
|
||||
"""Validate Redis configuration parameters."""
|
||||
required_fields = ["host", "port"]
|
||||
|
@ -78,7 +85,7 @@ def load_pipeline_node(node_config: dict, mpta_dir: str, redis_client, db_manage
|
|||
logger.info(f"Loading model for node {node_config['modelId']} from {model_path}")
|
||||
model = YOLO(model_path)
|
||||
if torch.cuda.is_available():
|
||||
logger.info(f"CUDA available. Moving model {node_config['modelId']} to GPU")
|
||||
logger.info(f"CUDA available. Moving model {node_config['modelId']} to GPU VRAM")
|
||||
model.to("cuda")
|
||||
else:
|
||||
logger.info(f"CUDA not available. Using CPU for model {node_config['modelId']}")
|
||||
|
@ -92,6 +99,10 @@ def load_pipeline_node(node_config: dict, mpta_dir: str, redis_client, db_manage
|
|||
if name in trigger_classes]
|
||||
logger.debug(f"Converted trigger classes to indices: {trigger_class_indices}")
|
||||
|
||||
# Extract stability threshold from tracking config
|
||||
tracking_config = node_config.get("tracking", {"enabled": True, "reidConfigPath": "botsort.yaml"})
|
||||
stability_threshold = tracking_config.get("stabilityThreshold", 1)
|
||||
|
||||
node = {
|
||||
"modelId": node_config["modelId"],
|
||||
"modelFile": node_config["modelFile"],
|
||||
|
@ -105,6 +116,8 @@ def load_pipeline_node(node_config: dict, mpta_dir: str, redis_client, db_manage
|
|||
"parallel": node_config.get("parallel", False),
|
||||
"actions": node_config.get("actions", []),
|
||||
"parallelActions": node_config.get("parallelActions", []),
|
||||
"tracking": tracking_config,
|
||||
"stabilityThreshold": stability_threshold,
|
||||
"model": model,
|
||||
"branches": [],
|
||||
"redis_client": redis_client,
|
||||
|
@ -514,6 +527,342 @@ def resolve_field_mapping(value_template, branch_results, action_context):
|
|||
logger.error(f"Error resolving field mapping '{value_template}': {e}")
|
||||
return None
|
||||
|
||||
def run_detection_with_tracking(frame, node, context=None):
|
||||
"""
|
||||
Structured function for running YOLO detection with BoT-SORT tracking.
|
||||
|
||||
Args:
|
||||
frame: Input frame/image
|
||||
node: Pipeline node configuration with model and settings
|
||||
context: Optional context information (camera info, session data, etc.)
|
||||
|
||||
Returns:
|
||||
tuple: (all_detections, regions_dict) where:
|
||||
- all_detections: List of all detection objects
|
||||
- regions_dict: Dict mapping class names to highest confidence detections
|
||||
|
||||
Configuration options in node:
|
||||
- model: YOLO model instance
|
||||
- triggerClassIndices: List of class indices to detect (None for all classes)
|
||||
- minConfidence: Minimum confidence threshold
|
||||
- multiClass: Whether to enable multi-class detection mode
|
||||
- expectedClasses: List of expected class names for multi-class validation
|
||||
- tracking: Dict with tracking configuration
|
||||
- enabled: Boolean to enable/disable tracking
|
||||
- reidConfigPath: Path to ReID config file (default: "botsort.yaml")
|
||||
"""
|
||||
try:
|
||||
# Extract tracking configuration
|
||||
tracking_config = node.get("tracking", {})
|
||||
tracking_enabled = tracking_config.get("enabled", True)
|
||||
reid_config_path = tracking_config.get("reidConfigPath", "botsort.yaml")
|
||||
|
||||
# Check if we need to reset tracker after cooldown
|
||||
camera_id = context.get("camera_id", "unknown") if context else "unknown"
|
||||
model_id = node.get("modelId", "unknown")
|
||||
stability_data = get_camera_stability_data(camera_id, model_id)
|
||||
session_state = stability_data["session_state"]
|
||||
|
||||
if session_state.get("reset_tracker_on_resume", False):
|
||||
# Reset YOLO tracker to get fresh track IDs
|
||||
if hasattr(node["model"], 'trackers') and node["model"].trackers:
|
||||
node["model"].trackers.clear() # Clear tracker state
|
||||
logger.info(f"Camera {camera_id}: 🔄 Reset YOLO tracker - new cars will get fresh track IDs")
|
||||
session_state["reset_tracker_on_resume"] = False # Clear the flag
|
||||
|
||||
# Get tracking zone from runtime context (camera-specific)
|
||||
tracking_zone = context.get("trackingZone", []) if context else []
|
||||
|
||||
# Prepare class filtering
|
||||
trigger_class_indices = node.get("triggerClassIndices")
|
||||
class_filter = {"classes": trigger_class_indices} if trigger_class_indices else {}
|
||||
|
||||
logger.debug(f"Running detection for {node['modelId']} - tracking: {tracking_enabled}, classes: {node.get('triggerClasses', 'all')}")
|
||||
|
||||
if tracking_enabled and tracking_zone:
|
||||
# Use tracking with zone validation
|
||||
logger.debug(f"Using tracking with ReID config: {reid_config_path}")
|
||||
res = node["model"].track(
|
||||
frame,
|
||||
stream=False,
|
||||
persist=True,
|
||||
tracker=reid_config_path,
|
||||
**class_filter
|
||||
)[0]
|
||||
elif tracking_enabled:
|
||||
# Use tracking without zone restriction
|
||||
logger.debug("Using tracking without zone restriction")
|
||||
res = node["model"].track(
|
||||
frame,
|
||||
stream=False,
|
||||
persist=True,
|
||||
**class_filter
|
||||
)[0]
|
||||
else:
|
||||
# Use detection only (no tracking)
|
||||
logger.debug("Using detection only (tracking disabled)")
|
||||
res = node["model"].predict(
|
||||
frame,
|
||||
stream=False,
|
||||
**class_filter
|
||||
)[0]
|
||||
|
||||
# Process detection results
|
||||
all_detections = []
|
||||
regions_dict = {}
|
||||
min_confidence = node.get("minConfidence", 0.0)
|
||||
|
||||
if res.boxes is None or len(res.boxes) == 0:
|
||||
logger.debug("No detections found")
|
||||
return [], {}
|
||||
|
||||
logger.debug(f"Processing {len(res.boxes)} raw detections")
|
||||
|
||||
for i, box in enumerate(res.boxes):
|
||||
# Extract detection data
|
||||
conf = float(box.cpu().conf[0])
|
||||
cls_id = int(box.cpu().cls[0])
|
||||
class_name = node["model"].names[cls_id]
|
||||
|
||||
# Apply confidence filtering
|
||||
if conf < min_confidence:
|
||||
logger.debug(f"Detection {i} '{class_name}' rejected: {conf:.3f} < {min_confidence}")
|
||||
continue
|
||||
|
||||
# Extract bounding box
|
||||
xy = box.cpu().xyxy[0]
|
||||
x1, y1, x2, y2 = map(int, xy)
|
||||
bbox = (x1, y1, x2, y2)
|
||||
|
||||
# Extract tracking ID if available
|
||||
track_id = None
|
||||
if hasattr(box, "id") and box.id is not None:
|
||||
track_id = int(box.id.item())
|
||||
|
||||
# Apply tracking zone validation if enabled
|
||||
if tracking_enabled and tracking_zone:
|
||||
bbox_center_x = (x1 + x2) // 2
|
||||
bbox_center_y = (y1 + y2) // 2
|
||||
|
||||
# Check if detection center is within tracking zone
|
||||
if not _point_in_polygon((bbox_center_x, bbox_center_y), tracking_zone):
|
||||
logger.debug(f"Detection {i} '{class_name}' outside tracking zone")
|
||||
continue
|
||||
|
||||
# Create detection object
|
||||
detection = {
|
||||
"class": class_name,
|
||||
"confidence": conf,
|
||||
"id": track_id,
|
||||
"bbox": bbox,
|
||||
"class_id": cls_id
|
||||
}
|
||||
|
||||
all_detections.append(detection)
|
||||
logger.debug(f"Detection {i} accepted: {class_name} (conf={conf:.3f}, id={track_id}, bbox={bbox})")
|
||||
|
||||
# Update regions_dict with highest confidence detection per class
|
||||
if class_name not in regions_dict or conf > regions_dict[class_name]["confidence"]:
|
||||
regions_dict[class_name] = {
|
||||
"bbox": bbox,
|
||||
"confidence": conf,
|
||||
"detection": detection,
|
||||
"track_id": track_id
|
||||
}
|
||||
|
||||
# Multi-class validation
|
||||
if node.get("multiClass", False) and node.get("expectedClasses"):
|
||||
expected_classes = node["expectedClasses"]
|
||||
detected_classes = list(regions_dict.keys())
|
||||
|
||||
logger.debug(f"Multi-class validation: expected={expected_classes}, detected={detected_classes}")
|
||||
|
||||
# Check for required classes (flexible - at least one must match)
|
||||
matching_classes = [cls for cls in expected_classes if cls in detected_classes]
|
||||
if not matching_classes:
|
||||
logger.warning(f"Multi-class validation failed: no expected classes detected")
|
||||
return [], {}
|
||||
|
||||
logger.info(f"Multi-class validation passed: {matching_classes} detected")
|
||||
|
||||
logger.info(f"Detection completed: {len(all_detections)} detections, {len(regions_dict)} unique classes")
|
||||
|
||||
# Update stability tracking for detections with track IDs (requires camera_id from context)
|
||||
camera_id = context.get("camera_id", "unknown") if context else "unknown"
|
||||
update_track_stability(node, all_detections, camera_id)
|
||||
|
||||
return all_detections, regions_dict
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in detection_with_tracking for {node.get('modelId', 'unknown')}: {e}")
|
||||
logger.debug(f"Detection error traceback: {traceback.format_exc()}")
|
||||
return [], {}
|
||||
|
||||
def _point_in_polygon(point, polygon):
|
||||
"""Check if a point is inside a polygon using ray casting algorithm."""
|
||||
if not polygon or len(polygon) < 3:
|
||||
return True # No zone restriction if invalid polygon
|
||||
|
||||
x, y = point
|
||||
n = len(polygon)
|
||||
inside = False
|
||||
|
||||
p1x, p1y = polygon[0]
|
||||
for i in range(1, n + 1):
|
||||
p2x, p2y = polygon[i % n]
|
||||
if y > min(p1y, p2y):
|
||||
if y <= max(p1y, p2y):
|
||||
if x <= max(p1x, p2x):
|
||||
if p1y != p2y:
|
||||
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
||||
if p1x == p2x or x <= xinters:
|
||||
inside = not inside
|
||||
p1x, p1y = p2x, p2y
|
||||
|
||||
return inside
|
||||
|
||||
def get_camera_stability_data(camera_id, model_id):
|
||||
"""Get or create stability tracking data for a specific camera and model."""
|
||||
global _camera_stability_tracking
|
||||
|
||||
if camera_id not in _camera_stability_tracking:
|
||||
_camera_stability_tracking[camera_id] = {}
|
||||
|
||||
if model_id not in _camera_stability_tracking[camera_id]:
|
||||
logger.warning(f"🔄 Camera {camera_id}: Creating NEW stability data for {model_id} - this will reset any cooldown!")
|
||||
_camera_stability_tracking[camera_id][model_id] = {
|
||||
"track_stability_counters": {},
|
||||
"stable_tracks": set(),
|
||||
"session_state": {
|
||||
"active": True,
|
||||
"cooldown_until": 0.0,
|
||||
"reset_tracker_on_resume": False
|
||||
}
|
||||
}
|
||||
|
||||
return _camera_stability_tracking[camera_id][model_id]
|
||||
|
||||
def update_track_stability(node, detections, camera_id):
|
||||
"""Update stability counters for tracked objects per camera."""
|
||||
stability_threshold = node.get("stabilityThreshold", 1)
|
||||
model_id = node.get("modelId", "unknown")
|
||||
|
||||
# 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"]
|
||||
|
||||
# Get current track IDs from detections
|
||||
current_track_ids = set()
|
||||
for detection in detections:
|
||||
track_id = detection.get("id")
|
||||
if track_id is not None:
|
||||
current_track_ids.add(track_id)
|
||||
|
||||
# Increment counter for this track
|
||||
track_counters[track_id] = track_counters.get(track_id, 0) + 1
|
||||
|
||||
# Check if track becomes stable
|
||||
if track_counters[track_id] >= stability_threshold and track_id not in stable_tracks:
|
||||
stable_tracks.add(track_id)
|
||||
logger.info(f"Camera {camera_id}: Track ID {track_id} became stable after {track_counters[track_id]} detections (threshold: {stability_threshold})")
|
||||
|
||||
# Clean up counters for tracks that disappeared
|
||||
disappeared_tracks = set(track_counters.keys()) - current_track_ids
|
||||
for track_id in disappeared_tracks:
|
||||
logger.debug(f"Camera {camera_id}: Track ID {track_id} disappeared, removing from counters")
|
||||
track_counters.pop(track_id, None)
|
||||
stable_tracks.discard(track_id)
|
||||
|
||||
logger.debug(f"Camera {camera_id}: Track stability: active={list(current_track_ids)}, stable={list(stable_tracks)}, counters={track_counters}")
|
||||
|
||||
def check_stable_tracks(camera_id, model_id, regions_dict):
|
||||
"""Check if any stable tracks match the detected classes for a specific camera."""
|
||||
# Get camera-specific stability data
|
||||
stability_data = get_camera_stability_data(camera_id, model_id)
|
||||
stable_tracks = stability_data["stable_tracks"]
|
||||
|
||||
if not stable_tracks:
|
||||
return False, []
|
||||
|
||||
# Check if any detection in regions_dict has a stable track ID
|
||||
stable_detections = []
|
||||
for class_name, region_data in regions_dict.items():
|
||||
detection = region_data.get("detection", {})
|
||||
track_id = detection.get("id")
|
||||
|
||||
if track_id is not None and track_id in stable_tracks:
|
||||
stable_detections.append((class_name, track_id))
|
||||
logger.debug(f"Camera {camera_id}: Found stable detection: {class_name} with stable track ID {track_id}")
|
||||
|
||||
has_stable_tracks = len(stable_detections) > 0
|
||||
return has_stable_tracks, stable_detections
|
||||
|
||||
def start_cooldown_timer(camera_id, model_id):
|
||||
"""Start 30-second cooldown timer after successful pipeline completion."""
|
||||
stability_data = get_camera_stability_data(camera_id, model_id)
|
||||
session_state = stability_data["session_state"]
|
||||
|
||||
# Start timer-based cooldown
|
||||
cooldown_until = time.time() + _cooldown_duration_seconds
|
||||
session_state["cooldown_until"] = cooldown_until
|
||||
session_state["active"] = False
|
||||
session_state["reset_tracker_on_resume"] = True # Flag to reset YOLO tracker
|
||||
|
||||
logger.info(f"Camera {camera_id}: 🛑 Starting {_cooldown_duration_seconds}s cooldown timer (until: {cooldown_until:.2f})")
|
||||
|
||||
# DO NOT clear tracking state here - preserve it during cooldown
|
||||
# Tracking state will be cleared when cooldown expires and new session starts
|
||||
|
||||
def is_camera_active(camera_id, model_id):
|
||||
"""Check if camera should be processing detections (timer-based cooldown)."""
|
||||
stability_data = get_camera_stability_data(camera_id, model_id)
|
||||
session_state = stability_data["session_state"]
|
||||
|
||||
# Check if cooldown timer has expired
|
||||
if not session_state["active"]:
|
||||
current_time = time.time()
|
||||
cooldown_until = session_state["cooldown_until"]
|
||||
remaining_time = cooldown_until - current_time
|
||||
|
||||
if current_time >= cooldown_until:
|
||||
session_state["active"] = True
|
||||
session_state["reset_tracker_on_resume"] = True # Ensure tracker reset flag is set
|
||||
|
||||
# Clear tracking state NOW - before new detection session starts
|
||||
stability_data["track_stability_counters"].clear()
|
||||
stability_data["stable_tracks"].clear()
|
||||
|
||||
logger.info(f"Camera {camera_id}: 📢 Cooldown timer ended, resuming detection with fresh track IDs")
|
||||
logger.info(f"Camera {camera_id}: 🧹 Cleared stability counters and stable tracks for fresh session")
|
||||
else:
|
||||
logger.debug(f"Camera {camera_id}: Still in cooldown - {remaining_time:.1f}s remaining")
|
||||
|
||||
return session_state["active"]
|
||||
|
||||
def cleanup_camera_stability(camera_id):
|
||||
"""Clean up stability tracking data when a camera is disconnected, preserving cooldown timers."""
|
||||
global _camera_stability_tracking
|
||||
if camera_id in _camera_stability_tracking:
|
||||
# Check if any models are still in cooldown before cleanup
|
||||
models_in_cooldown = []
|
||||
for model_id, model_data in _camera_stability_tracking[camera_id].items():
|
||||
session_state = model_data.get("session_state", {})
|
||||
if not session_state.get("active", True) and time.time() < session_state.get("cooldown_until", 0):
|
||||
cooldown_remaining = session_state["cooldown_until"] - time.time()
|
||||
models_in_cooldown.append((model_id, cooldown_remaining))
|
||||
logger.warning(f"⚠️ Camera {camera_id}: Model {model_id} is in cooldown ({cooldown_remaining:.1f}s remaining) - preserving timer!")
|
||||
|
||||
if models_in_cooldown:
|
||||
# DO NOT clear any tracking data during cooldown - preserve everything
|
||||
logger.warning(f"⚠️ Camera {camera_id}: PRESERVING ALL data during cooldown - no cleanup performed!")
|
||||
logger.warning(f" - Track IDs will reset only AFTER cooldown expires")
|
||||
logger.warning(f" - Stability counters preserved until cooldown ends")
|
||||
else:
|
||||
# Safe to delete everything - no active cooldowns
|
||||
del _camera_stability_tracking[camera_id]
|
||||
logger.info(f"Cleaned up stability tracking data for camera {camera_id} (no active cooldowns)")
|
||||
|
||||
def validate_pipeline_execution(node, regions_dict):
|
||||
"""
|
||||
Pre-validate that all required branches will execute successfully before
|
||||
|
@ -618,92 +967,42 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
|
|||
execute_actions(node, frame, det)
|
||||
return (det, None) if return_bbox else det
|
||||
|
||||
# ─── Detection stage - Multi-class support ──────────────────
|
||||
tk = node["triggerClassIndices"]
|
||||
logger.debug(f"Running detection for node {node['modelId']} with trigger classes: {node.get('triggerClasses', [])} (indices: {tk})")
|
||||
logger.debug(f"Node configuration: minConfidence={node['minConfidence']}, multiClass={node.get('multiClass', False)}")
|
||||
# ─── Session management check ───────────────────────────────────────
|
||||
camera_id = context.get("camera_id", "unknown") if context else "unknown"
|
||||
model_id = node.get("modelId", "unknown")
|
||||
|
||||
res = node["model"].track(
|
||||
frame,
|
||||
stream=False,
|
||||
persist=True,
|
||||
**({"classes": tk} if tk else {})
|
||||
)[0]
|
||||
if not is_camera_active(camera_id, model_id):
|
||||
logger.info(f"⏰ Camera {camera_id}: Tracker stopped - in cooldown period, skipping all detection")
|
||||
return (None, None) if return_bbox else None
|
||||
|
||||
# Collect all detections above confidence threshold
|
||||
all_detections = []
|
||||
all_boxes = []
|
||||
regions_dict = {}
|
||||
|
||||
logger.debug(f"Raw detection results from model: {len(res.boxes) if res.boxes is not None else 0} detections")
|
||||
|
||||
for i, box in enumerate(res.boxes):
|
||||
conf = float(box.cpu().conf[0])
|
||||
cid = int(box.cpu().cls[0])
|
||||
name = node["model"].names[cid]
|
||||
|
||||
logger.debug(f"Detection {i}: class='{name}' (id={cid}), confidence={conf:.3f}, threshold={node['minConfidence']}")
|
||||
|
||||
if conf < node["minConfidence"]:
|
||||
logger.debug(f" -> REJECTED: confidence {conf:.3f} < threshold {node['minConfidence']}")
|
||||
continue
|
||||
|
||||
xy = box.cpu().xyxy[0]
|
||||
x1, y1, x2, y2 = map(int, xy)
|
||||
bbox = (x1, y1, x2, y2)
|
||||
|
||||
detection = {
|
||||
"class": name,
|
||||
"confidence": conf,
|
||||
"id": box.id.item() if hasattr(box, "id") else None,
|
||||
"bbox": bbox
|
||||
}
|
||||
|
||||
all_detections.append(detection)
|
||||
all_boxes.append(bbox)
|
||||
|
||||
logger.debug(f" -> ACCEPTED: {name} with confidence {conf:.3f}, bbox={bbox}")
|
||||
|
||||
# Store highest confidence detection for each class
|
||||
if name not in regions_dict or conf > regions_dict[name]["confidence"]:
|
||||
regions_dict[name] = {
|
||||
"bbox": bbox,
|
||||
"confidence": conf,
|
||||
"detection": detection
|
||||
}
|
||||
logger.debug(f" -> Updated regions_dict['{name}'] with confidence {conf:.3f}")
|
||||
|
||||
logger.info(f"Detection summary: {len(all_detections)} accepted detections from {len(res.boxes) if res.boxes is not None else 0} total")
|
||||
logger.info(f"Detected classes: {list(regions_dict.keys())}")
|
||||
# ─── Detection stage - Using structured detection function ──────────────────
|
||||
all_detections, regions_dict = run_detection_with_tracking(frame, node, context)
|
||||
|
||||
if not all_detections:
|
||||
logger.warning("No detections above confidence threshold - returning null")
|
||||
logger.warning("No detections from structured detection function - returning null")
|
||||
return (None, None) if return_bbox else None
|
||||
|
||||
# ─── Multi-class validation ─────────────────────────────────
|
||||
if node.get("multiClass", False) and node.get("expectedClasses"):
|
||||
expected_classes = node["expectedClasses"]
|
||||
detected_classes = list(regions_dict.keys())
|
||||
# Extract bounding boxes for compatibility
|
||||
all_boxes = [det["bbox"] for det in all_detections]
|
||||
|
||||
logger.info(f"Multi-class validation: expected={expected_classes}, detected={detected_classes}")
|
||||
# ─── Stability validation (only for root pipeline node) ────────────────────────
|
||||
stability_threshold = node.get("stabilityThreshold", 1)
|
||||
if stability_threshold > 1:
|
||||
# Extract camera_id for stability check
|
||||
camera_id = context.get("camera_id", "unknown") if context else "unknown"
|
||||
model_id = node.get("modelId", "unknown")
|
||||
|
||||
# Check if at least one expected class is detected (flexible mode)
|
||||
matching_classes = [cls for cls in expected_classes if cls in detected_classes]
|
||||
missing_classes = [cls for cls in expected_classes if cls not in detected_classes]
|
||||
# Check if we have stable tracks for this specific camera
|
||||
has_stable_tracks, stable_detections = check_stable_tracks(camera_id, model_id, regions_dict)
|
||||
|
||||
logger.debug(f"Matching classes: {matching_classes}, Missing classes: {missing_classes}")
|
||||
|
||||
if not matching_classes:
|
||||
# No expected classes found at all
|
||||
logger.warning(f"PIPELINE REJECTED: No expected classes detected. Expected: {expected_classes}, Detected: {detected_classes}")
|
||||
return (None, None) if return_bbox else None
|
||||
|
||||
if missing_classes:
|
||||
logger.info(f"Partial multi-class detection: {matching_classes} found, {missing_classes} missing")
|
||||
if not has_stable_tracks:
|
||||
logger.info(f"Camera {camera_id}: Track not stable yet (threshold: {stability_threshold}) - validation only, skipping branches")
|
||||
# Return early with just the detection result, no branch processing
|
||||
primary_detection = max(all_detections, key=lambda x: x["confidence"]) if all_detections else {"class": "none", "confidence": 0.0, "bbox": [0, 0, 0, 0]}
|
||||
primary_bbox = primary_detection.get("bbox", [0, 0, 0, 0])
|
||||
return (primary_detection, primary_bbox) if return_bbox else primary_detection
|
||||
else:
|
||||
logger.info(f"Complete multi-class detection success: {detected_classes}")
|
||||
else:
|
||||
logger.debug("No multi-class validation - proceeding with all detections")
|
||||
logger.info(f"Camera {camera_id}: Stable tracks {[det[1] for det in stable_detections]} detected - proceeding with full pipeline")
|
||||
|
||||
# ─── Pre-validate pipeline execution ────────────────────────
|
||||
pipeline_valid, missing_branches = validate_pipeline_execution(node, regions_dict)
|
||||
|
@ -752,10 +1051,14 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
|
|||
|
||||
execute_actions(node, frame, detection_result, regions_dict)
|
||||
|
||||
# ─── Parallel branch processing ─────────────────────────────
|
||||
# ─── Branch processing (no stability check here) ─────────────────────────────
|
||||
if node["branches"]:
|
||||
branch_results = {}
|
||||
|
||||
# Extract camera_id for logging
|
||||
camera_id = detection_result.get("camera_id", context.get("camera_id", "unknown") if context else "unknown")
|
||||
|
||||
|
||||
# Filter branches that should be triggered
|
||||
active_branches = []
|
||||
for br in node["branches"]:
|
||||
|
@ -849,6 +1152,10 @@ def run_pipeline(frame, node: dict, return_bbox: bool=False, context=None):
|
|||
if node.get("parallelActions") and "branch_results" in detection_result:
|
||||
execute_parallel_actions(node, frame, detection_result, regions_dict)
|
||||
|
||||
# ─── Start 30s cooldown timer after successful pipeline completion ─────────────────
|
||||
start_cooldown_timer(camera_id, model_id)
|
||||
logger.info(f"Camera {camera_id}: Pipeline completed successfully, starting 30s cooldown")
|
||||
|
||||
# ─── Return detection result ────────────────────────────────
|
||||
primary_detection = max(all_detections, key=lambda x: x["confidence"])
|
||||
primary_bbox = primary_detection["bbox"]
|
||||
|
|
352
test_botsort_zone_track.py
Normal file
352
test_botsort_zone_track.py
Normal file
|
@ -0,0 +1,352 @@
|
|||
import cv2
|
||||
import torch
|
||||
import numpy as np
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from ultralytics import YOLO
|
||||
|
||||
def point_in_polygon(point, polygon):
|
||||
"""Check if a point is inside a polygon using ray casting algorithm"""
|
||||
x, y = point
|
||||
n = len(polygon)
|
||||
inside = False
|
||||
|
||||
p1x, p1y = polygon[0]
|
||||
for i in range(1, n + 1):
|
||||
p2x, p2y = polygon[i % n]
|
||||
if y > min(p1y, p2y):
|
||||
if y <= max(p1y, p2y):
|
||||
if x <= max(p1x, p2x):
|
||||
if p1y != p2y:
|
||||
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
||||
if p1x == p2x or x <= xinters:
|
||||
inside = not inside
|
||||
p1x, p1y = p2x, p2y
|
||||
|
||||
return inside
|
||||
|
||||
def draw_zone(frame, zone_polygon, color=(255, 0, 0), thickness=3):
|
||||
"""Draw tracking zone on frame"""
|
||||
pts = np.array(zone_polygon, np.int32)
|
||||
pts = pts.reshape((-1, 1, 2))
|
||||
cv2.polylines(frame, [pts], True, color, thickness)
|
||||
|
||||
# Add semi-transparent fill
|
||||
overlay = frame.copy()
|
||||
cv2.fillPoly(overlay, [pts], color)
|
||||
cv2.addWeighted(overlay, 0.2, frame, 0.8, 0, frame)
|
||||
|
||||
def setup_video_writer(output_path, fps, width, height):
|
||||
"""Setup video writer for output"""
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||
return cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
||||
|
||||
def write_frame_to_video(video_writer, frame, repeat_count):
|
||||
"""Write frame to video with specified repeat count"""
|
||||
for _ in range(repeat_count):
|
||||
video_writer.write(frame)
|
||||
|
||||
def finalize_video(video_writer):
|
||||
"""Release video writer"""
|
||||
video_writer.release()
|
||||
|
||||
def main():
|
||||
video_path = "sample2.mp4"
|
||||
yolo_model = "bangchakv2/yolov8n.pt"
|
||||
|
||||
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
||||
print(f"Using device: {device}")
|
||||
|
||||
print("Loading YOLO model...")
|
||||
model = YOLO(yolo_model)
|
||||
|
||||
print("Opening video...")
|
||||
cap = cv2.VideoCapture(video_path)
|
||||
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
|
||||
print(f"Video info: {width}x{height}, {fps} FPS, {total_frames} frames")
|
||||
|
||||
# Define tracking zone - Gas station floor area (trapezoidal shape)
|
||||
# Based on the perspective of the gas station floor from your image
|
||||
# width 2560, height 1440
|
||||
|
||||
tracking_zone = [
|
||||
(423, 974), # Point 1
|
||||
(1540, 1407), # Point 2
|
||||
(1976, 806), # Point 3
|
||||
(1364, 749) # Point 4
|
||||
]
|
||||
|
||||
print(f"🎯 Tracking zone defined: {tracking_zone}")
|
||||
|
||||
# CONTINUOUS TRACKING: Process every 118 frames (~2.0s intervals)
|
||||
frame_skip = 118
|
||||
|
||||
print(f"🎯 CONTINUOUS MODE: Processing every {frame_skip} frames ({frame_skip/fps:.2f}s intervals)")
|
||||
print(f"🎬 Output video will have same duration as input (each processed frame shown for 2 seconds)")
|
||||
print("🔥 ZONE-FIRST TRACKING: Only cars entering the zone will be tracked!")
|
||||
print("Requires 5 consecutive detections IN ZONE for verification")
|
||||
print("🕐 24/7 MODE: Memory reset every hour to prevent overflow")
|
||||
print("Press 'q' to quit")
|
||||
|
||||
# Setup video writer for output (same fps as input for normal playback speed)
|
||||
output_path = "tracking_output_botsort_zone_track.mp4"
|
||||
output_fps = fps # Use same fps as input video
|
||||
out = setup_video_writer(output_path, output_fps, width, height)
|
||||
|
||||
# Track car IDs and their consecutive detections
|
||||
car_id_counts = defaultdict(int)
|
||||
successful_cars = set()
|
||||
last_positions = {}
|
||||
processed_count = 0
|
||||
|
||||
# ID remapping for clean sequential zone IDs
|
||||
tracker_to_zone_id = {} # Maps tracker IDs to clean zone IDs
|
||||
next_zone_id = 1 # Next clean zone ID to assign
|
||||
|
||||
# Store previous frame detections to filter tracking inputs
|
||||
previous_zone_cars = set()
|
||||
|
||||
# 24/7 operation: Reset every hour (1800 snapshots at 2-sec intervals = 1 hour)
|
||||
RESET_INTERVAL = 1800 # Reset every 1800 processed frames (1 hour)
|
||||
|
||||
frame_idx = 0
|
||||
|
||||
while True:
|
||||
# Skip frames to maintain interval
|
||||
for _ in range(frame_skip):
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print("\nNo more frames to read")
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
return
|
||||
frame_idx += 1
|
||||
|
||||
processed_count += 1
|
||||
current_time = frame_idx / fps
|
||||
|
||||
print(f"\n🎬 Frame {frame_idx} at {current_time:.2f}s (processed #{processed_count})")
|
||||
|
||||
# 24/7 Memory Management: Reset every hour
|
||||
if processed_count % RESET_INTERVAL == 0:
|
||||
print(f"🕐 HOURLY RESET: Clearing all tracking data (processed {processed_count} frames)")
|
||||
print(f" 📊 Before reset: {len(tracker_to_zone_id)} tracked cars, next Zone ID was {next_zone_id}")
|
||||
|
||||
# Clear all tracking data
|
||||
tracker_to_zone_id.clear()
|
||||
car_id_counts.clear()
|
||||
successful_cars.clear()
|
||||
last_positions.clear()
|
||||
next_zone_id = 1 # Reset to 1
|
||||
|
||||
# Reset BoT-SORT tracker state
|
||||
try:
|
||||
model.reset()
|
||||
print(f" ✅ BoT-SORT tracker reset successfully")
|
||||
except:
|
||||
print(f" ⚠️ BoT-SORT reset not available (continuing without reset)")
|
||||
|
||||
print(f" 🆕 Zone IDs will start from 1 again")
|
||||
|
||||
# Draw tracking zone on frame
|
||||
draw_zone(frame, tracking_zone, color=(0, 255, 255), thickness=3) # Yellow zone
|
||||
|
||||
# First run YOLO detection (without tracking) to find cars in zone
|
||||
detection_results = model(frame, verbose=False, conf=0.7, classes=[2])
|
||||
|
||||
# Find cars currently in the tracking zone
|
||||
current_zone_cars = []
|
||||
total_detections = 0
|
||||
|
||||
if detection_results[0].boxes is not None:
|
||||
boxes = detection_results[0].boxes.xyxy.cpu()
|
||||
scores = detection_results[0].boxes.conf.cpu()
|
||||
|
||||
total_detections = len(boxes)
|
||||
print(f" 🔍 Total car detections: {total_detections}")
|
||||
|
||||
for i in range(len(boxes)):
|
||||
x1, y1, x2, y2 = boxes[i]
|
||||
conf = float(scores[i])
|
||||
|
||||
# Check if detection is in zone (using bottom center)
|
||||
box_bottom = ((x1 + x2) / 2, y2)
|
||||
if point_in_polygon(box_bottom, tracking_zone):
|
||||
current_zone_cars.append({
|
||||
'bbox': [float(x1), float(y1), float(x2), float(y2)],
|
||||
'conf': conf,
|
||||
'center': ((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
'bottom': box_bottom
|
||||
})
|
||||
|
||||
print(f" 🎯 Cars in zone: {len(current_zone_cars)}")
|
||||
|
||||
# Only run tracking if there are cars in the zone
|
||||
detected_car_ids = set()
|
||||
|
||||
if current_zone_cars:
|
||||
# Run tracking on the full frame (let tracker handle associations)
|
||||
# But we'll filter results to only zone cars afterward
|
||||
results = model.track(
|
||||
frame,
|
||||
persist=True,
|
||||
verbose=False,
|
||||
conf=0.7,
|
||||
classes=[2],
|
||||
tracker="botsort_reid.yaml"
|
||||
)
|
||||
|
||||
if results[0].boxes is not None and results[0].boxes.id is not None:
|
||||
boxes = results[0].boxes.xyxy.cpu()
|
||||
scores = results[0].boxes.conf.cpu()
|
||||
track_ids = results[0].boxes.id.cpu().int()
|
||||
|
||||
print(f" 📊 Total tracked objects: {len(track_ids)}")
|
||||
|
||||
# Filter tracked objects to only those in zone
|
||||
zone_tracks = []
|
||||
for i, track_id in enumerate(track_ids):
|
||||
x1, y1, x2, y2 = boxes[i]
|
||||
conf = float(scores[i])
|
||||
|
||||
# Check if this tracked object is in our zone
|
||||
box_bottom = ((x1 + x2) / 2, y2)
|
||||
if point_in_polygon(box_bottom, tracking_zone):
|
||||
zone_tracks.append({
|
||||
'id': int(track_id),
|
||||
'bbox': [int(x1), int(y1), int(x2), int(y2)],
|
||||
'conf': conf,
|
||||
'center': ((x1 + x2) / 2, (y1 + y2) / 2),
|
||||
'bottom': box_bottom
|
||||
})
|
||||
|
||||
print(f" ✅ Zone tracks: {len(zone_tracks)}")
|
||||
|
||||
# Process each zone track
|
||||
for track in zone_tracks:
|
||||
tracker_id = track['id'] # Original tracker ID
|
||||
x1, y1, x2, y2 = track['bbox']
|
||||
conf = track['conf']
|
||||
box_center = track['center']
|
||||
|
||||
# Map tracker ID to clean zone ID
|
||||
if tracker_id not in tracker_to_zone_id:
|
||||
tracker_to_zone_id[tracker_id] = next_zone_id
|
||||
print(f" 🆕 New car: Tracker ID {tracker_id} → Zone ID {next_zone_id}")
|
||||
next_zone_id += 1
|
||||
|
||||
zone_id = tracker_to_zone_id[tracker_id] # Clean sequential ID
|
||||
|
||||
# Validate track continuity (use tracker_id for internal logic)
|
||||
is_valid = True
|
||||
|
||||
# Check for suspicious jumps
|
||||
if tracker_id in last_positions:
|
||||
last_center = last_positions[tracker_id]
|
||||
distance = np.sqrt((box_center[0] - last_center[0])**2 +
|
||||
(box_center[1] - last_center[1])**2)
|
||||
|
||||
if distance > 400: # pixels in ~2.0s
|
||||
is_valid = False
|
||||
print(f" ⚠️ Zone ID {zone_id} (Tracker {tracker_id}): suspicious jump {distance:.0f}px")
|
||||
|
||||
# Skip already successful cars (use zone_id for user logic)
|
||||
if zone_id in successful_cars:
|
||||
is_valid = False
|
||||
print(f" ✅ Zone ID {zone_id}: already successful, skipping")
|
||||
|
||||
# Only process valid, high-confidence zone tracks
|
||||
if is_valid and conf > 0.7:
|
||||
detected_car_ids.add(zone_id) # Use zone_id for display
|
||||
car_id_counts[zone_id] += 1
|
||||
last_positions[tracker_id] = box_center # Track by tracker_id internally
|
||||
|
||||
# Draw tracking results with clean zone ID
|
||||
zone_color = (0, 255, 0) # Green for zone cars
|
||||
cv2.rectangle(frame, (x1, y1), (x2, y2), zone_color, 2)
|
||||
cv2.putText(frame, f'ZONE ID:{zone_id}',
|
||||
(x1, y1-30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, zone_color, 2)
|
||||
cv2.putText(frame, f'#{car_id_counts[zone_id]} {conf:.2f}',
|
||||
(x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, zone_color, 2)
|
||||
|
||||
# Draw center point
|
||||
cv2.circle(frame, (int(track['bottom'][0]), int(track['bottom'][1])), 5, zone_color, -1)
|
||||
|
||||
print(f" ✅ Zone ID {zone_id} (Tracker {tracker_id}): ZONE detection #{car_id_counts[zone_id]} (conf: {conf:.2f})")
|
||||
|
||||
# Check for success (5 consecutive detections IN ZONE)
|
||||
if car_id_counts[zone_id] == 5:
|
||||
print(f"🏆 SUCCESS: Zone ID {zone_id} achieved 5 continuous ZONE detections - TRIGGER NEXT MODEL!")
|
||||
successful_cars.add(zone_id)
|
||||
|
||||
# Add success indicator to frame
|
||||
cv2.putText(frame, f"SUCCESS: Zone Car {zone_id}!",
|
||||
(50, height-50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 3)
|
||||
else:
|
||||
print(" 📋 No cars in zone - no tracking performed")
|
||||
|
||||
# Draw any cars outside the zone in red (for reference)
|
||||
if detection_results[0].boxes is not None:
|
||||
boxes = detection_results[0].boxes.xyxy.cpu()
|
||||
scores = detection_results[0].boxes.conf.cpu()
|
||||
|
||||
for i in range(len(boxes)):
|
||||
x1, y1, x2, y2 = boxes[i]
|
||||
conf = float(scores[i])
|
||||
|
||||
box_bottom = ((x1 + x2) / 2, y2)
|
||||
if not point_in_polygon(box_bottom, tracking_zone):
|
||||
# Draw cars outside zone in red (not tracked)
|
||||
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
|
||||
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 1)
|
||||
cv2.putText(frame, f'OUT {conf:.2f}',
|
||||
(x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
|
||||
|
||||
# Display results
|
||||
if detected_car_ids:
|
||||
print(f" 📋 Active Zone IDs: {sorted(detected_car_ids)} (Clean sequential IDs)")
|
||||
|
||||
# Show ID mapping for debugging
|
||||
if tracker_to_zone_id:
|
||||
mapping_str = ", ".join([f"Tracker{k}→Zone{v}" for k, v in tracker_to_zone_id.items()])
|
||||
print(f" 🔄 ID Mapping: {mapping_str}")
|
||||
|
||||
# Add annotations to frame
|
||||
cv2.putText(frame, f"BoT-SORT Zone-First Tracking | Frame: {frame_idx} | {current_time:.2f}s",
|
||||
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
|
||||
cv2.putText(frame, f"Zone Cars: {len(current_zone_cars)} | Active Tracks: {len(detected_car_ids)}",
|
||||
(10, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
|
||||
cv2.putText(frame, f"Successful Cars: {len(successful_cars)}",
|
||||
(10, 100), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
|
||||
cv2.putText(frame, "TRACKING ZONE",
|
||||
(tracking_zone[0][0], tracking_zone[0][1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)
|
||||
|
||||
# Write annotated frame to output video (repeat for 2 seconds duration)
|
||||
write_frame_to_video(out, frame, frame_skip)
|
||||
|
||||
# Show video with zone tracking info
|
||||
display_frame = cv2.resize(frame, (960, 540))
|
||||
cv2.imshow('BoT-SORT Zone-First Tracking', display_frame)
|
||||
|
||||
# Quick check for quit
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord('q'):
|
||||
break
|
||||
|
||||
# Small delay to see results
|
||||
time.sleep(0.1)
|
||||
|
||||
cap.release()
|
||||
finalize_video(out)
|
||||
cv2.destroyAllWindows()
|
||||
print(f"\n🎯 BoT-SORT zone-first tracking completed!")
|
||||
print(f"📊 Processed {processed_count} frames with {frame_skip/fps:.2f}s intervals")
|
||||
print(f"🏆 Successfully tracked {len(successful_cars)} unique cars IN ZONE")
|
||||
print(f"💾 Annotated video saved to: {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
190
test_detection_tracking.py
Normal file
190
test_detection_tracking.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the refactored detection and tracking functionality.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import cv2
|
||||
import numpy as np
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project root to Python path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from siwatsystem.pympta import run_detection_with_tracking, load_pipeline_from_zip
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def create_test_frame():
|
||||
"""Create a simple test frame for detection testing."""
|
||||
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
# Add some simple shapes to simulate objects
|
||||
cv2.rectangle(frame, (50, 50), (200, 150), (255, 0, 0), -1) # Blue rectangle
|
||||
cv2.rectangle(frame, (300, 200), (450, 350), (0, 255, 0), -1) # Green rectangle
|
||||
cv2.putText(frame, "Test Frame", (250, 400), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
||||
return frame
|
||||
|
||||
def test_detection_function():
|
||||
"""Test the structured detection function with mock data."""
|
||||
logger.info("Testing run_detection_with_tracking function...")
|
||||
|
||||
# Create test frame
|
||||
test_frame = create_test_frame()
|
||||
|
||||
# Mock node configuration (simulating what would come from an MPTA file)
|
||||
mock_node = {
|
||||
"modelId": "test_detection_v1",
|
||||
"triggerClasses": ["car", "person"],
|
||||
"triggerClassIndices": [0, 1],
|
||||
"minConfidence": 0.5,
|
||||
"multiClass": False,
|
||||
"expectedClasses": [],
|
||||
"tracking": {
|
||||
"enabled": True,
|
||||
"reidConfigPath": "botsort.yaml"
|
||||
}
|
||||
}
|
||||
|
||||
# Mock context
|
||||
test_context = {
|
||||
"display_id": "test-display-001",
|
||||
"camera_id": "test-cam-001"
|
||||
}
|
||||
|
||||
logger.info("Mock node configuration:")
|
||||
for key, value in mock_node.items():
|
||||
logger.info(f" {key}: {value}")
|
||||
|
||||
# Note: This test will fail without a real YOLO model, but demonstrates the structure
|
||||
try:
|
||||
detections, regions = run_detection_with_tracking(test_frame, mock_node, test_context)
|
||||
logger.info(f"Function executed successfully!")
|
||||
logger.info(f"Returned detections: {len(detections)}")
|
||||
logger.info(f"Returned regions: {list(regions.keys())}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Function failed (expected without real model): {e}")
|
||||
return False
|
||||
|
||||
def test_mpta_loading():
|
||||
"""Test loading an MPTA file with tracking configuration."""
|
||||
logger.info("Testing MPTA loading with tracking configuration...")
|
||||
|
||||
# Check if models directory exists
|
||||
models_dir = Path("models")
|
||||
if not models_dir.exists():
|
||||
logger.warning("No models directory found - skipping MPTA test")
|
||||
return False
|
||||
|
||||
# Look for any .mpta files
|
||||
mpta_files = list(models_dir.glob("**/*.mpta"))
|
||||
if not mpta_files:
|
||||
logger.warning("No .mpta files found in models directory - skipping MPTA test")
|
||||
return False
|
||||
|
||||
mpta_file = mpta_files[0]
|
||||
logger.info(f"Testing with MPTA file: {mpta_file}")
|
||||
|
||||
try:
|
||||
# Attempt to load pipeline
|
||||
target_dir = f"temp_test_{os.getpid()}"
|
||||
pipeline = load_pipeline_from_zip(str(mpta_file), target_dir)
|
||||
|
||||
if pipeline:
|
||||
logger.info("MPTA loaded successfully!")
|
||||
logger.info(f"Pipeline model ID: {pipeline.get('modelId')}")
|
||||
logger.info(f"Tracking config: {pipeline.get('tracking')}")
|
||||
|
||||
# Clean up
|
||||
import shutil
|
||||
if os.path.exists(target_dir):
|
||||
shutil.rmtree(target_dir)
|
||||
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to load MPTA pipeline")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MPTA loading failed: {e}")
|
||||
return False
|
||||
|
||||
def print_usage_example():
|
||||
"""Print example usage of the new structured functions."""
|
||||
logger.info("\n" + "="*60)
|
||||
logger.info("USAGE EXAMPLE - Structured Detection & Tracking")
|
||||
logger.info("="*60)
|
||||
|
||||
example_config = '''
|
||||
Example pipeline.json configuration:
|
||||
|
||||
{
|
||||
"pipeline": {
|
||||
"modelId": "car_frontal_detection_v1",
|
||||
"modelFile": "yolo11n.pt",
|
||||
"triggerClasses": ["Car", "Frontal"],
|
||||
"minConfidence": 0.7,
|
||||
"multiClass": true,
|
||||
"expectedClasses": ["Car", "Frontal"],
|
||||
"tracking": {
|
||||
"enabled": true,
|
||||
"reidConfigPath": "botsort_reid.yaml"
|
||||
},
|
||||
"actions": [...],
|
||||
"branches": [...]
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
logger.info(example_config)
|
||||
|
||||
code_example = '''
|
||||
Usage in code:
|
||||
|
||||
# Load pipeline from MPTA file
|
||||
pipeline = load_pipeline_from_zip("model.mpta", "temp_dir")
|
||||
|
||||
# Run detection with tracking
|
||||
detections, regions = run_detection_with_tracking(frame, pipeline, context)
|
||||
|
||||
# Process results
|
||||
for detection in detections:
|
||||
class_name = detection["class"]
|
||||
confidence = detection["confidence"]
|
||||
track_id = detection["id"] # Available when tracking enabled
|
||||
bbox = detection["bbox"]
|
||||
print(f"Detected: {class_name} (ID: {track_id}, conf: {confidence:.2f})")
|
||||
'''
|
||||
|
||||
logger.info(code_example)
|
||||
|
||||
def main():
|
||||
"""Main test function."""
|
||||
logger.info("Starting detection & tracking refactoring tests...")
|
||||
|
||||
# Test 1: Function structure
|
||||
test1_passed = test_detection_function()
|
||||
|
||||
# Test 2: MPTA loading
|
||||
test2_passed = test_mpta_loading()
|
||||
|
||||
# Print usage examples
|
||||
print_usage_example()
|
||||
|
||||
# Summary
|
||||
logger.info("\n" + "="*60)
|
||||
logger.info("TEST SUMMARY")
|
||||
logger.info("="*60)
|
||||
logger.info(f"Function structure test: {'PASS' if test1_passed else 'EXPECTED FAIL (no model)'}")
|
||||
logger.info(f"MPTA loading test: {'PASS' if test2_passed else 'SKIP (no files)'}")
|
||||
logger.info("\nRefactoring completed successfully!")
|
||||
logger.info("The detection and tracking code is now structured and easy to configure.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
325
webcam_rtsp_server.py
Normal file
325
webcam_rtsp_server.py
Normal file
|
@ -0,0 +1,325 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enhanced webcam server that provides both RTSP streaming and HTTP snapshot endpoints
|
||||
Compatible with CMS UI requirements for camera configuration
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import socket
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("webcam_rtsp_server")
|
||||
|
||||
# Global webcam capture object
|
||||
webcam_cap = None
|
||||
rtsp_process = None
|
||||
|
||||
class WebcamHTTPHandler(BaseHTTPRequestHandler):
|
||||
"""HTTP handler for snapshot requests"""
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/snapshot' or self.path == '/snapshot.jpg':
|
||||
try:
|
||||
# Capture fresh frame from webcam for each request
|
||||
ret, frame = webcam_cap.read()
|
||||
if ret and frame is not None:
|
||||
# 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())
|
||||
logger.debug(f"Served webcam snapshot, size: {len(buffer)} bytes")
|
||||
return
|
||||
else:
|
||||
logger.error("Failed to encode frame as JPEG")
|
||||
else:
|
||||
logger.error("Failed to capture frame from webcam")
|
||||
|
||||
# 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:
|
||||
logger.error(f"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(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)
|
||||
|
||||
status = f'{{"status": "online", "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 log_message(self, format, *args):
|
||||
# Suppress default HTTP server logging to avoid spam
|
||||
pass
|
||||
|
||||
def check_ffmpeg():
|
||||
"""Check if FFmpeg is available for RTSP streaming"""
|
||||
try:
|
||||
result = subprocess.run(['ffmpeg', '-version'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
logger.info("FFmpeg found and working")
|
||||
return True
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
||||
pass
|
||||
|
||||
logger.warning("FFmpeg not found. RTSP streaming will not be available.")
|
||||
logger.info("To enable RTSP streaming, install FFmpeg:")
|
||||
logger.info(" Windows: Download from https://ffmpeg.org/download.html")
|
||||
logger.info(" Linux: sudo apt install ffmpeg")
|
||||
logger.info(" macOS: brew install ffmpeg")
|
||||
return False
|
||||
|
||||
def get_windows_camera_name():
|
||||
"""Get the actual camera device name on Windows"""
|
||||
try:
|
||||
# List video devices using FFmpeg with proper encoding handling
|
||||
result = subprocess.run(['ffmpeg', '-f', 'dshow', '-list_devices', 'true', '-i', 'dummy'],
|
||||
capture_output=True, text=True, timeout=10, encoding='utf-8', errors='ignore')
|
||||
output = result.stderr # FFmpeg outputs device list to stderr
|
||||
|
||||
# Look for video devices in the output
|
||||
lines = output.split('\n')
|
||||
video_devices = []
|
||||
|
||||
# Parse the output - look for lines with (video) that contain device names in quotes
|
||||
for line in lines:
|
||||
if '[dshow @' in line and '(video)' in line and '"' in line:
|
||||
# Extract device name between first pair of quotes
|
||||
start = line.find('"') + 1
|
||||
end = line.find('"', start)
|
||||
if start > 0 and end > start:
|
||||
device_name = line[start:end]
|
||||
video_devices.append(device_name)
|
||||
|
||||
logger.info(f"Found Windows video devices: {video_devices}")
|
||||
if video_devices:
|
||||
# Force use the first device (index 0) which is the Logitech HD webcam
|
||||
return video_devices[0] # This will be "罗技高清网络摄像机 C930c"
|
||||
else:
|
||||
logger.info("No devices found via FFmpeg detection, using fallback")
|
||||
# Fall through to fallback names
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to get Windows camera name: {e}")
|
||||
|
||||
# Try common camera device names as fallback
|
||||
# Prioritize Integrated Camera since that's what's working now
|
||||
common_names = [
|
||||
"Integrated Camera", # This is working for the current setup
|
||||
"USB Video Device", # Common name for USB cameras
|
||||
"USB2.0 Camera",
|
||||
"C930c", # Direct model name
|
||||
"HD Pro Webcam C930c", # Full Logitech name
|
||||
"Logitech", # Brand name
|
||||
"USB Camera",
|
||||
"Webcam"
|
||||
]
|
||||
logger.info(f"Using fallback camera names: {common_names}")
|
||||
return common_names[0] # Return "Integrated Camera" first
|
||||
|
||||
def start_rtsp_stream(webcam_index=0, rtsp_port=8554):
|
||||
"""Start RTSP streaming using FFmpeg"""
|
||||
global rtsp_process
|
||||
|
||||
if not check_ffmpeg():
|
||||
return None
|
||||
|
||||
try:
|
||||
# Get the actual camera device name for Windows
|
||||
if sys.platform.startswith('win'):
|
||||
camera_name = get_windows_camera_name()
|
||||
logger.info(f"Using Windows camera device: {camera_name}")
|
||||
|
||||
# FFmpeg command to stream webcam via RTSP
|
||||
if sys.platform.startswith('win'):
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
'-f', 'dshow',
|
||||
'-i', f'video={camera_name}', # Use detected camera name
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'veryfast',
|
||||
'-tune', 'zerolatency',
|
||||
'-r', '30',
|
||||
'-s', '1280x720',
|
||||
'-f', 'rtsp',
|
||||
f'rtsp://localhost:{rtsp_port}/stream'
|
||||
]
|
||||
elif sys.platform.startswith('linux'):
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
'-f', 'v4l2',
|
||||
'-i', f'/dev/video{webcam_index}',
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'veryfast',
|
||||
'-tune', 'zerolatency',
|
||||
'-r', '30',
|
||||
'-s', '1280x720',
|
||||
'-f', 'rtsp',
|
||||
f'rtsp://localhost:{rtsp_port}/stream'
|
||||
]
|
||||
else: # macOS
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
'-f', 'avfoundation',
|
||||
'-i', f'{webcam_index}:',
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'veryfast',
|
||||
'-tune', 'zerolatency',
|
||||
'-r', '30',
|
||||
'-s', '1280x720',
|
||||
'-f', 'rtsp',
|
||||
f'rtsp://localhost:{rtsp_port}/stream'
|
||||
]
|
||||
|
||||
logger.info(f"Starting RTSP stream on rtsp://localhost:{rtsp_port}/stream")
|
||||
logger.info(f"FFmpeg command: {' '.join(cmd)}")
|
||||
|
||||
rtsp_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Give FFmpeg a moment to start
|
||||
time.sleep(2)
|
||||
|
||||
# Check if process is still running
|
||||
if rtsp_process.poll() is None:
|
||||
logger.info("RTSP streaming started successfully")
|
||||
return rtsp_process
|
||||
else:
|
||||
# Get error output if process failed
|
||||
stdout, stderr = rtsp_process.communicate(timeout=2)
|
||||
logger.error("RTSP streaming failed to start")
|
||||
logger.error(f"FFmpeg stdout: {stdout}")
|
||||
logger.error(f"FFmpeg stderr: {stderr}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start RTSP stream: {e}")
|
||||
return None
|
||||
|
||||
def get_local_ip():
|
||||
"""Get the Wireguard IP address for external access"""
|
||||
# Use Wireguard IP for external access
|
||||
return "10.101.1.4"
|
||||
|
||||
def main():
|
||||
global webcam_cap, rtsp_process
|
||||
|
||||
# Configuration - Force use index 0 for Logitech HD webcam
|
||||
webcam_index = 0 # Logitech HD webcam C930c (1920x1080@30fps)
|
||||
http_port = 8080
|
||||
rtsp_port = 8554
|
||||
|
||||
logger.info("=== Webcam RTSP & HTTP Server ===")
|
||||
|
||||
# Initialize webcam
|
||||
logger.info("Initializing webcam...")
|
||||
webcam_cap = cv2.VideoCapture(webcam_index)
|
||||
|
||||
if not webcam_cap.isOpened():
|
||||
logger.error(f"Failed to open webcam at index {webcam_index}")
|
||||
logger.info("Try different webcam indices (0, 1, 2, etc.)")
|
||||
return
|
||||
|
||||
# Set webcam properties - Use high resolution for Logitech HD webcam
|
||||
webcam_cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
|
||||
webcam_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
|
||||
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)
|
||||
|
||||
logger.info(f"Webcam initialized: {width}x{height} @ {fps}fps")
|
||||
|
||||
# Get local IP for CMS configuration
|
||||
local_ip = get_local_ip()
|
||||
|
||||
# Start RTSP streaming (optional, requires FFmpeg)
|
||||
rtsp_process = start_rtsp_stream(webcam_index, rtsp_port)
|
||||
|
||||
# Start HTTP server for snapshots
|
||||
server_address = ('0.0.0.0', http_port) # Bind to all interfaces
|
||||
http_server = HTTPServer(server_address, WebcamHTTPHandler)
|
||||
|
||||
logger.info("\n=== Server URLs for CMS Configuration ===")
|
||||
logger.info(f"HTTP Snapshot URL: http://{local_ip}:{http_port}/snapshot")
|
||||
|
||||
if rtsp_process:
|
||||
logger.info(f"RTSP Stream URL: rtsp://{local_ip}:{rtsp_port}/stream")
|
||||
else:
|
||||
logger.info("RTSP Stream: Not available (FFmpeg not found)")
|
||||
logger.info("HTTP-only mode: Use Snapshot URL for camera input")
|
||||
|
||||
logger.info(f"Status URL: http://{local_ip}:{http_port}/status")
|
||||
logger.info("\n=== CMS Configuration Suggestions ===")
|
||||
logger.info(f"Camera Identifier: webcam-local-01")
|
||||
logger.info(f"RTSP Stream URL: rtsp://{local_ip}:{rtsp_port}/stream")
|
||||
logger.info(f"Snapshot URL: http://{local_ip}:{http_port}/snapshot")
|
||||
logger.info(f"Snapshot Interval: 2000 (ms)")
|
||||
logger.info("\nPress Ctrl+C to stop all servers")
|
||||
|
||||
try:
|
||||
# Start HTTP server
|
||||
http_server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Shutting down servers...")
|
||||
finally:
|
||||
# Clean up
|
||||
if webcam_cap:
|
||||
webcam_cap.release()
|
||||
|
||||
if rtsp_process:
|
||||
logger.info("Stopping RTSP stream...")
|
||||
rtsp_process.terminate()
|
||||
try:
|
||||
rtsp_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
rtsp_process.kill()
|
||||
|
||||
http_server.server_close()
|
||||
logger.info("All servers stopped")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue