Compare commits
	
		
			42 commits
		
	
	
		
			dev
			...
			feat/track
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
							 | 
						9631a71073 | ||
| 
							 | 
						34ecff244c | ||
| 21700dce52 | |||
| 
							 | 
						356da144fc | ||
| 
							 | 
						98d4fdc410 | ||
| 4251a9b06d | |||
| 
							 | 
						ac85caca39 | ||
| c715b26a2a | |||
| 
							 | 
						2f52374ff4 | ||
| 
							 | 
						c01f449d0d | ||
| 0290dec27d | |||
| 7d29598b0f | |||
| 0ae26d3a6d | |||
| 5d7cacc4fc | |||
| 06270f04fc | |||
| 715546cd90 | |||
| 
							 | 
						0c6fc7a394 | ||
| 864be5cb47 | |||
| 
							 | 
						338bbb410e | ||
| 
							 | 
						cee856f59a | ||
| 
							 | 
						72eb7d55ea | ||
| 
							 | 
						5875b76d74 | ||
| 
							 | 
						39394caa8e | ||
| 
							 | 
						85b49ddf0f | ||
| 
							 | 
						80d9c925de | ||
| 
							 | 
						5bf2d49e6b | ||
| 
							 | 
						07eddd3f0d | ||
| 
							 | 
						5873945115 | ||
| 
							 | 
						3a4a27ca68 | ||
| 
							 | 
						a54da904f7 | ||
| 
							 | 
						007a3d48b9 | ||
| 
							 | 
						4342eb219b | ||
| 
							 | 
						c281ca6c6d | ||
| 0bcf572242 | |||
| 
							 | 
						c4ab4d6cde | ||
| 838028fcb0 | |||
| 
							 | 
						0f8b575c90 | ||
| 
							 | 
						9a1496f224 | ||
| 5f9050e04e | |||
| 
							 | 
						aaa90faef9 | ||
| 
							 | 
						e0a786a46c | ||
| 
							 | 
						975e6d03dc | 
					 19 changed files with 7578 additions and 437 deletions
				
			
		
							
								
								
									
										14
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -10,3 +10,17 @@ mptas
 | 
				
			||||||
detector_worker.log
 | 
					detector_worker.log
 | 
				
			||||||
.gitignore
 | 
					.gitignore
 | 
				
			||||||
no_frame_debug.log
 | 
					no_frame_debug.log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					feeder/
 | 
				
			||||||
 | 
					.venv/
 | 
				
			||||||
 | 
					.vscode/
 | 
				
			||||||
 | 
					dist/
 | 
				
			||||||
 | 
					websocket_comm.log
 | 
				
			||||||
 | 
					temp_debug/
 | 
				
			||||||
 | 
					.claude
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Video Test
 | 
				
			||||||
 | 
					video_rtsp/
 | 
				
			||||||
 | 
					multi_stream_viewer.py
 | 
				
			||||||
 | 
					multi_camera_simulator.py
 | 
				
			||||||
 | 
					start_4_cameras.bat
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,17 @@
 | 
				
			||||||
# Base image with all ML dependencies
 | 
					# Base image with all ML dependencies
 | 
				
			||||||
FROM python:3.13-bookworm
 | 
					FROM pytorch/pytorch:2.8.0-cuda12.6-cudnn9-runtime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Install system dependencies
 | 
					# Install system dependencies
 | 
				
			||||||
RUN apt update && apt install -y libgl1 && rm -rf /var/lib/apt/lists/*
 | 
					RUN apt update && apt install -y \
 | 
				
			||||||
 | 
					    libgl1 \
 | 
				
			||||||
 | 
					    libglib2.0-0 \
 | 
				
			||||||
 | 
					    libgstreamer1.0-0 \
 | 
				
			||||||
 | 
					    libgtk-3-0 \
 | 
				
			||||||
 | 
					    libavcodec58 \
 | 
				
			||||||
 | 
					    libavformat58 \
 | 
				
			||||||
 | 
					    libswscale5 \
 | 
				
			||||||
 | 
					    libgomp1 \
 | 
				
			||||||
 | 
					    && rm -rf /var/lib/apt/lists/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Copy and install base requirements (ML dependencies that rarely change)
 | 
					# Copy and install base requirements (ML dependencies that rarely change)
 | 
				
			||||||
COPY requirements.base.txt .
 | 
					COPY requirements.base.txt .
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "poll_interval_ms": 100,
 | 
					  "poll_interval_ms": 100,
 | 
				
			||||||
  "max_streams": 5,
 | 
					  "max_streams": 999,
 | 
				
			||||||
  "target_fps": 2,
 | 
					  "target_fps": 2,
 | 
				
			||||||
  "reconnect_interval_sec": 5,
 | 
					  "reconnect_interval_sec": 5,
 | 
				
			||||||
  "max_retries": -1
 | 
					  "max_retries": -1
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										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()
 | 
				
			||||||
							
								
								
									
										1449
									
								
								docs/MasterElection.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1449
									
								
								docs/MasterElection.md
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1498
									
								
								docs/WorkerConnection.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1498
									
								
								docs/WorkerConnection.md
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
					@ -1,7 +1,12 @@
 | 
				
			||||||
torch
 | 
					ultralytics>=8.3.0
 | 
				
			||||||
torchvision
 | 
					opencv-python>=4.6.0
 | 
				
			||||||
ultralytics
 | 
					scipy>=1.9.0
 | 
				
			||||||
opencv-python
 | 
					filterpy>=1.4.0
 | 
				
			||||||
scipy
 | 
					psycopg2-binary>=2.9.0
 | 
				
			||||||
filterpy
 | 
					easydict
 | 
				
			||||||
psycopg2-binary
 | 
					loguru
 | 
				
			||||||
 | 
					pyzmq
 | 
				
			||||||
 | 
					gitpython
 | 
				
			||||||
 | 
					gdown
 | 
				
			||||||
 | 
					lap
 | 
				
			||||||
 | 
					pynvml
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
fastapi
 | 
					fastapi[standard]
 | 
				
			||||||
uvicorn
 | 
					uvicorn
 | 
				
			||||||
websockets
 | 
					websockets
 | 
				
			||||||
fastapi[standard]
 | 
					 | 
				
			||||||
redis
 | 
					redis
 | 
				
			||||||
urllib3<2.0.0
 | 
					urllib3<2.0.0
 | 
				
			||||||
| 
						 | 
					@ -80,37 +80,50 @@ class DatabaseManager:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            cur = self.connection.cursor()
 | 
					            cur = self.connection.cursor()
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            # Build the UPDATE query dynamically
 | 
					            # Build the INSERT and UPDATE query dynamically
 | 
				
			||||||
 | 
					            insert_placeholders = []
 | 
				
			||||||
 | 
					            insert_values = [key_value]  # Start with key_value
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            set_clauses = []
 | 
					            set_clauses = []
 | 
				
			||||||
            values = []
 | 
					            update_values = []
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            for field, value in fields.items():
 | 
					            for field, value in fields.items():
 | 
				
			||||||
                if value == "NOW()":
 | 
					                if value == "NOW()":
 | 
				
			||||||
 | 
					                    # Special handling for NOW()
 | 
				
			||||||
 | 
					                    insert_placeholders.append("NOW()")
 | 
				
			||||||
                    set_clauses.append(f"{field} = NOW()")
 | 
					                    set_clauses.append(f"{field} = NOW()")
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
 | 
					                    insert_placeholders.append("%s")
 | 
				
			||||||
 | 
					                    insert_values.append(value)
 | 
				
			||||||
                    set_clauses.append(f"{field} = %s")
 | 
					                    set_clauses.append(f"{field} = %s")
 | 
				
			||||||
                    values.append(value)
 | 
					                    update_values.append(value)
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            # Add schema prefix if table doesn't already have it
 | 
					            # Add schema prefix if table doesn't already have it
 | 
				
			||||||
            full_table_name = table if '.' in table else f"gas_station_1.{table}"
 | 
					            full_table_name = table if '.' in table else f"gas_station_1.{table}"
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
 | 
					            # Build the complete query
 | 
				
			||||||
            query = f"""
 | 
					            query = f"""
 | 
				
			||||||
            INSERT INTO {full_table_name} ({key_field}, {', '.join(fields.keys())})
 | 
					            INSERT INTO {full_table_name} ({key_field}, {', '.join(fields.keys())})
 | 
				
			||||||
            VALUES (%s, {', '.join(['%s'] * len(fields))})
 | 
					            VALUES (%s, {', '.join(insert_placeholders)})
 | 
				
			||||||
            ON CONFLICT ({key_field})
 | 
					            ON CONFLICT ({key_field})
 | 
				
			||||||
            DO UPDATE SET {', '.join(set_clauses)}
 | 
					            DO UPDATE SET {', '.join(set_clauses)}
 | 
				
			||||||
            """
 | 
					            """
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            # Add key_value to the beginning of values list
 | 
					            # Combine values for the query: insert_values + update_values
 | 
				
			||||||
            all_values = [key_value] + list(fields.values()) + values
 | 
					            all_values = insert_values + update_values
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.debug(f"SQL Query: {query}")
 | 
				
			||||||
 | 
					            logger.debug(f"Values: {all_values}")
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            cur.execute(query, all_values)
 | 
					            cur.execute(query, all_values)
 | 
				
			||||||
            self.connection.commit()
 | 
					            self.connection.commit()
 | 
				
			||||||
            cur.close()
 | 
					            cur.close()
 | 
				
			||||||
            logger.info(f"Updated {table} for {key_field}={key_value}")
 | 
					            logger.info(f"✅ Updated {table} for {key_field}={key_value} with fields: {fields}")
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            logger.error(f"Failed to execute update on {table}: {e}")
 | 
					            logger.error(f"❌ Failed to execute update on {table}: {e}")
 | 
				
			||||||
 | 
					            logger.debug(f"Query: {query if 'query' in locals() else 'Query not built'}")
 | 
				
			||||||
 | 
					            logger.debug(f"Values: {all_values if 'all_values' in locals() else 'Values not prepared'}")
 | 
				
			||||||
            if self.connection:
 | 
					            if self.connection:
 | 
				
			||||||
                self.connection.rollback()
 | 
					                self.connection.rollback()
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										242
									
								
								siwatsystem/model_registry.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								siwatsystem/model_registry.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,242 @@
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Shared Model Registry for Memory Optimization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This module implements a global shared model registry to prevent duplicate model loading
 | 
				
			||||||
 | 
					in memory when multiple cameras use the same model. This significantly reduces RAM and
 | 
				
			||||||
 | 
					GPU VRAM usage by ensuring only one instance of each unique model is loaded.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Key Features:
 | 
				
			||||||
 | 
					- Thread-safe model loading and access
 | 
				
			||||||
 | 
					- Reference counting for proper cleanup
 | 
				
			||||||
 | 
					- Automatic model lifecycle management
 | 
				
			||||||
 | 
					- Maintains compatibility with existing pipeline system
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import threading
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					from typing import Dict, Any, Optional, Set
 | 
				
			||||||
 | 
					import torch
 | 
				
			||||||
 | 
					from ultralytics import YOLO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create a logger for this module
 | 
				
			||||||
 | 
					logger = logging.getLogger("detector_worker.model_registry")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ModelRegistry:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Singleton class for managing shared YOLO models across multiple cameras.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    This registry ensures that each unique model is loaded only once in memory,
 | 
				
			||||||
 | 
					    dramatically reducing RAM and GPU VRAM usage when multiple cameras use the
 | 
				
			||||||
 | 
					    same model.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    _instance = None
 | 
				
			||||||
 | 
					    _lock = threading.Lock()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def __new__(cls):
 | 
				
			||||||
 | 
					        if cls._instance is None:
 | 
				
			||||||
 | 
					            with cls._lock:
 | 
				
			||||||
 | 
					                if cls._instance is None:
 | 
				
			||||||
 | 
					                    cls._instance = super(ModelRegistry, cls).__new__(cls)
 | 
				
			||||||
 | 
					                    cls._instance._initialized = False
 | 
				
			||||||
 | 
					        return cls._instance
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        if self._initialized:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        self._initialized = True
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Thread-safe storage for loaded models
 | 
				
			||||||
 | 
					        self._models: Dict[str, YOLO] = {}  # modelId -> YOLO model instance
 | 
				
			||||||
 | 
					        self._model_files: Dict[str, str] = {}  # modelId -> file path
 | 
				
			||||||
 | 
					        self._reference_counts: Dict[str, int] = {}  # modelId -> reference count
 | 
				
			||||||
 | 
					        self._model_lock = threading.RLock()  # Reentrant lock for nested calls
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.info("🏭 Shared Model Registry initialized - ready for memory-optimized model loading")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def get_model(self, model_id: str, model_file_path: str) -> YOLO:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Get or load a YOLO model. Returns shared instance if already loaded.
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            model_id: Unique identifier for the model
 | 
				
			||||||
 | 
					            model_file_path: Path to the model file
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            YOLO model instance (shared across all callers)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._model_lock:
 | 
				
			||||||
 | 
					            if model_id in self._models:
 | 
				
			||||||
 | 
					                # Model already loaded - increment reference count and return
 | 
				
			||||||
 | 
					                self._reference_counts[model_id] += 1
 | 
				
			||||||
 | 
					                logger.info(f"📖 Model '{model_id}' reused (ref_count: {self._reference_counts[model_id]}) - SAVED MEMORY!")
 | 
				
			||||||
 | 
					                return self._models[model_id]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Model not loaded yet - load it
 | 
				
			||||||
 | 
					            logger.info(f"🔄 Loading NEW model '{model_id}' from {model_file_path}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if not os.path.exists(model_file_path):
 | 
				
			||||||
 | 
					                raise FileNotFoundError(f"Model file {model_file_path} not found")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                # Load the YOLO model
 | 
				
			||||||
 | 
					                model = YOLO(model_file_path)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Move to GPU if available
 | 
				
			||||||
 | 
					                if torch.cuda.is_available():
 | 
				
			||||||
 | 
					                    logger.info(f"🚀 CUDA available. Moving model '{model_id}' to GPU VRAM")
 | 
				
			||||||
 | 
					                    model.to("cuda")
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    logger.info(f"💻 CUDA not available. Using CPU for model '{model_id}'")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Store in registry
 | 
				
			||||||
 | 
					                self._models[model_id] = model
 | 
				
			||||||
 | 
					                self._model_files[model_id] = model_file_path
 | 
				
			||||||
 | 
					                self._reference_counts[model_id] = 1
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                logger.info(f"✅ Model '{model_id}' loaded and registered (ref_count: 1)")
 | 
				
			||||||
 | 
					                self._log_registry_status()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                return model
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            except Exception as e:
 | 
				
			||||||
 | 
					                logger.error(f"❌ Failed to load model '{model_id}' from {model_file_path}: {e}")
 | 
				
			||||||
 | 
					                raise
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def release_model(self, model_id: str) -> None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Release a reference to a model. If reference count reaches zero,
 | 
				
			||||||
 | 
					        the model may be unloaded to free memory.
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            model_id: Unique identifier for the model to release
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._model_lock:
 | 
				
			||||||
 | 
					            if model_id not in self._reference_counts:
 | 
				
			||||||
 | 
					                logger.warning(f"⚠️ Attempted to release unknown model '{model_id}'")
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            self._reference_counts[model_id] -= 1
 | 
				
			||||||
 | 
					            logger.info(f"📉 Model '{model_id}' reference count decreased to {self._reference_counts[model_id]}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # For now, keep models in memory even when ref count reaches 0
 | 
				
			||||||
 | 
					            # This prevents reload overhead if the same model is needed again soon
 | 
				
			||||||
 | 
					            # In the future, we could implement LRU eviction policy
 | 
				
			||||||
 | 
					            # if self._reference_counts[model_id] <= 0:
 | 
				
			||||||
 | 
					            #     logger.info(f"💤 Model '{model_id}' has 0 references but keeping in memory for reuse")
 | 
				
			||||||
 | 
					                # Optionally: self._unload_model(model_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _unload_model(self, model_id: str) -> None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Internal method to unload a model from memory.
 | 
				
			||||||
 | 
					        Currently not used to prevent reload overhead.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._model_lock:
 | 
				
			||||||
 | 
					            if model_id in self._models:
 | 
				
			||||||
 | 
					                logger.info(f"🗑️ Unloading model '{model_id}' from memory")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Clear GPU memory if model was on GPU
 | 
				
			||||||
 | 
					                model = self._models[model_id]
 | 
				
			||||||
 | 
					                if hasattr(model, 'model') and hasattr(model.model, 'cuda'):
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        # Move model to CPU before deletion to free GPU memory
 | 
				
			||||||
 | 
					                        model.to('cpu')
 | 
				
			||||||
 | 
					                    except Exception as e:
 | 
				
			||||||
 | 
					                        logger.warning(f"⚠️ Failed to move model '{model_id}' to CPU: {e}")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Remove from registry
 | 
				
			||||||
 | 
					                del self._models[model_id]
 | 
				
			||||||
 | 
					                del self._model_files[model_id]
 | 
				
			||||||
 | 
					                del self._reference_counts[model_id]
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Force garbage collection
 | 
				
			||||||
 | 
					                import gc
 | 
				
			||||||
 | 
					                gc.collect()
 | 
				
			||||||
 | 
					                if torch.cuda.is_available():
 | 
				
			||||||
 | 
					                    torch.cuda.empty_cache()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                logger.info(f"✅ Model '{model_id}' unloaded and memory freed")
 | 
				
			||||||
 | 
					                self._log_registry_status()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def get_registry_status(self) -> Dict[str, Any]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Get current status of the model registry.
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Dictionary with registry statistics
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._model_lock:
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                "total_models": len(self._models),
 | 
				
			||||||
 | 
					                "models": {
 | 
				
			||||||
 | 
					                    model_id: {
 | 
				
			||||||
 | 
					                        "file_path": self._model_files[model_id],
 | 
				
			||||||
 | 
					                        "reference_count": self._reference_counts[model_id]
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    for model_id in self._models
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                "total_references": sum(self._reference_counts.values())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _log_registry_status(self) -> None:
 | 
				
			||||||
 | 
					        """Log current registry status for debugging."""
 | 
				
			||||||
 | 
					        status = self.get_registry_status()
 | 
				
			||||||
 | 
					        logger.info(f"📊 Model Registry Status: {status['total_models']} unique models, {status['total_references']} total references")
 | 
				
			||||||
 | 
					        for model_id, info in status['models'].items():
 | 
				
			||||||
 | 
					            logger.debug(f"   📋 '{model_id}': refs={info['reference_count']}, file={os.path.basename(info['file_path'])}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def cleanup_all(self) -> None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Clean up all models from the registry. Used during shutdown.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._model_lock:
 | 
				
			||||||
 | 
					            model_ids = list(self._models.keys())
 | 
				
			||||||
 | 
					            logger.info(f"🧹 Cleaning up {len(model_ids)} models from registry")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for model_id in model_ids:
 | 
				
			||||||
 | 
					                self._unload_model(model_id)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.info("✅ Model registry cleanup complete")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Global singleton instance
 | 
				
			||||||
 | 
					_registry = ModelRegistry()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_shared_model(model_id: str, model_file_path: str) -> YOLO:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function to get a shared model instance.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        model_id: Unique identifier for the model
 | 
				
			||||||
 | 
					        model_file_path: Path to the model file
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        YOLO model instance (shared across all callers)
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return _registry.get_model(model_id, model_file_path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def release_shared_model(model_id: str) -> None:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function to release a shared model reference.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        model_id: Unique identifier for the model to release
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    _registry.release_model(model_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_registry_status() -> Dict[str, Any]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function to get registry status.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        Dictionary with registry statistics
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return _registry.get_registry_status()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def cleanup_registry() -> None:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function to cleanup the entire registry.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    _registry.cleanup_all()
 | 
				
			||||||
							
								
								
									
										375
									
								
								siwatsystem/mpta_manager.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										375
									
								
								siwatsystem/mpta_manager.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,375 @@
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Shared MPTA Manager for Disk Space Optimization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This module implements shared MPTA file management to prevent duplicate downloads
 | 
				
			||||||
 | 
					and extractions when multiple cameras use the same model. MPTA files are stored
 | 
				
			||||||
 | 
					in modelId-based directories and shared across all cameras using that model.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Key Features:
 | 
				
			||||||
 | 
					- Thread-safe MPTA downloading and extraction
 | 
				
			||||||
 | 
					- ModelId-based directory structure: models/{modelId}/
 | 
				
			||||||
 | 
					- Reference counting for proper cleanup
 | 
				
			||||||
 | 
					- Eliminates duplicate MPTA downloads
 | 
				
			||||||
 | 
					- Maintains compatibility with existing pipeline system
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import threading
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					from typing import Dict, Set, Optional
 | 
				
			||||||
 | 
					from urllib.parse import urlparse
 | 
				
			||||||
 | 
					from .pympta import load_pipeline_from_zip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create a logger for this module
 | 
				
			||||||
 | 
					logger = logging.getLogger("detector_worker.mpta_manager")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MPTAManager:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Singleton class for managing shared MPTA files across multiple cameras.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    This manager ensures that each unique modelId is downloaded and extracted
 | 
				
			||||||
 | 
					    only once, dramatically reducing disk usage and download time when multiple
 | 
				
			||||||
 | 
					    cameras use the same model.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    _instance = None
 | 
				
			||||||
 | 
					    _lock = threading.Lock()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def __new__(cls):
 | 
				
			||||||
 | 
					        if cls._instance is None:
 | 
				
			||||||
 | 
					            with cls._lock:
 | 
				
			||||||
 | 
					                if cls._instance is None:
 | 
				
			||||||
 | 
					                    cls._instance = super(MPTAManager, cls).__new__(cls)
 | 
				
			||||||
 | 
					                    cls._instance._initialized = False
 | 
				
			||||||
 | 
					        return cls._instance
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        if self._initialized:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        self._initialized = True
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Thread-safe storage for MPTA management
 | 
				
			||||||
 | 
					        self._model_paths: Dict[int, str] = {}  # modelId -> shared_extraction_path
 | 
				
			||||||
 | 
					        self._mpta_file_paths: Dict[int, str] = {}  # modelId -> local_mpta_file_path
 | 
				
			||||||
 | 
					        self._reference_counts: Dict[int, int] = {}  # modelId -> reference count  
 | 
				
			||||||
 | 
					        self._download_locks: Dict[int, threading.Lock] = {}  # modelId -> download lock
 | 
				
			||||||
 | 
					        self._cameras_using_model: Dict[int, Set[str]] = {}  # modelId -> set of camera_ids
 | 
				
			||||||
 | 
					        self._manager_lock = threading.RLock()  # Reentrant lock for nested calls
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        logger.info("🏭 Shared MPTA Manager initialized - ready for disk-optimized MPTA management")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def get_or_download_mpta(self, model_id: int, model_url: str, camera_id: str) -> Optional[tuple[str, str]]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Get or download an MPTA file. Returns (extraction_path, mpta_file_path) if successful.
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            model_id: Unique identifier for the model
 | 
				
			||||||
 | 
					            model_url: URL to download the MPTA file from
 | 
				
			||||||
 | 
					            camera_id: Identifier for the requesting camera
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Tuple of (extraction_path, mpta_file_path), or None if failed
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._manager_lock:
 | 
				
			||||||
 | 
					            # Track camera usage
 | 
				
			||||||
 | 
					            if model_id not in self._cameras_using_model:
 | 
				
			||||||
 | 
					                self._cameras_using_model[model_id] = set()
 | 
				
			||||||
 | 
					            self._cameras_using_model[model_id].add(camera_id)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Check if model directory already exists on disk (from previous sessions)
 | 
				
			||||||
 | 
					            if model_id not in self._model_paths:
 | 
				
			||||||
 | 
					                potential_path = f"models/{model_id}"
 | 
				
			||||||
 | 
					                if os.path.exists(potential_path) and os.path.isdir(potential_path):
 | 
				
			||||||
 | 
					                    # Directory exists from previous session, find the MPTA file
 | 
				
			||||||
 | 
					                    mpta_files = [f for f in os.listdir(potential_path) if f.endswith('.mpta')]
 | 
				
			||||||
 | 
					                    if mpta_files:
 | 
				
			||||||
 | 
					                        # Use the first .mpta file found
 | 
				
			||||||
 | 
					                        mpta_file_path = os.path.join(potential_path, mpta_files[0])
 | 
				
			||||||
 | 
					                        self._model_paths[model_id] = potential_path
 | 
				
			||||||
 | 
					                        self._mpta_file_paths[model_id] = mpta_file_path
 | 
				
			||||||
 | 
					                        self._reference_counts[model_id] = 0  # Will be incremented below
 | 
				
			||||||
 | 
					                        logger.info(f"📂 Found existing MPTA modelId {model_id} from previous session")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Check if already available
 | 
				
			||||||
 | 
					            if model_id in self._model_paths:
 | 
				
			||||||
 | 
					                shared_path = self._model_paths[model_id]
 | 
				
			||||||
 | 
					                mpta_file_path = self._mpta_file_paths.get(model_id)
 | 
				
			||||||
 | 
					                if os.path.exists(shared_path) and mpta_file_path and os.path.exists(mpta_file_path):
 | 
				
			||||||
 | 
					                    self._reference_counts[model_id] += 1
 | 
				
			||||||
 | 
					                    logger.info(f"📂 MPTA modelId {model_id} reused for camera {camera_id} (ref_count: {self._reference_counts[model_id]}) - SAVED DOWNLOAD!")
 | 
				
			||||||
 | 
					                    return (shared_path, mpta_file_path)
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    # Path was deleted externally, clean up our records
 | 
				
			||||||
 | 
					                    logger.warning(f"⚠️ MPTA path for modelId {model_id} was deleted externally, will re-download")
 | 
				
			||||||
 | 
					                    del self._model_paths[model_id]
 | 
				
			||||||
 | 
					                    self._mpta_file_paths.pop(model_id, None)
 | 
				
			||||||
 | 
					                    self._reference_counts.pop(model_id, 0)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Need to download - get or create download lock for this modelId
 | 
				
			||||||
 | 
					            if model_id not in self._download_locks:
 | 
				
			||||||
 | 
					                self._download_locks[model_id] = threading.Lock()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        # Download with model-specific lock (released _manager_lock to allow other models)
 | 
				
			||||||
 | 
					        download_lock = self._download_locks[model_id]
 | 
				
			||||||
 | 
					        with download_lock:
 | 
				
			||||||
 | 
					            # Double-check after acquiring download lock
 | 
				
			||||||
 | 
					            with self._manager_lock:
 | 
				
			||||||
 | 
					                if model_id in self._model_paths and os.path.exists(self._model_paths[model_id]):
 | 
				
			||||||
 | 
					                    mpta_file_path = self._mpta_file_paths.get(model_id)
 | 
				
			||||||
 | 
					                    if mpta_file_path and os.path.exists(mpta_file_path):
 | 
				
			||||||
 | 
					                        self._reference_counts[model_id] += 1
 | 
				
			||||||
 | 
					                        logger.info(f"📂 MPTA modelId {model_id} became available during wait (ref_count: {self._reference_counts[model_id]})")
 | 
				
			||||||
 | 
					                        return (self._model_paths[model_id], mpta_file_path)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Actually download and extract
 | 
				
			||||||
 | 
					            shared_path = f"models/{model_id}"
 | 
				
			||||||
 | 
					            logger.info(f"🔄 Downloading NEW MPTA for modelId {model_id} from {model_url}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                # Ensure directory exists
 | 
				
			||||||
 | 
					                os.makedirs(shared_path, exist_ok=True)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Download MPTA file
 | 
				
			||||||
 | 
					                mpta_filename = self._extract_filename_from_url(model_url) or f"model_{model_id}.mpta"
 | 
				
			||||||
 | 
					                local_mpta_path = os.path.join(shared_path, mpta_filename)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if not self._download_file(model_url, local_mpta_path):
 | 
				
			||||||
 | 
					                    logger.error(f"❌ Failed to download MPTA for modelId {model_id}")
 | 
				
			||||||
 | 
					                    return None
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Extract MPTA
 | 
				
			||||||
 | 
					                pipeline_tree = load_pipeline_from_zip(local_mpta_path, shared_path)
 | 
				
			||||||
 | 
					                if pipeline_tree is None:
 | 
				
			||||||
 | 
					                    logger.error(f"❌ Failed to extract MPTA for modelId {model_id}")
 | 
				
			||||||
 | 
					                    return None
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Success - register in manager
 | 
				
			||||||
 | 
					                with self._manager_lock:
 | 
				
			||||||
 | 
					                    self._model_paths[model_id] = shared_path
 | 
				
			||||||
 | 
					                    self._mpta_file_paths[model_id] = local_mpta_path
 | 
				
			||||||
 | 
					                    self._reference_counts[model_id] = 1
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    logger.info(f"✅ MPTA modelId {model_id} downloaded and registered (ref_count: 1)")
 | 
				
			||||||
 | 
					                    self._log_manager_status()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                return (shared_path, local_mpta_path)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            except Exception as e:
 | 
				
			||||||
 | 
					                logger.error(f"❌ Error downloading/extracting MPTA for modelId {model_id}: {e}")
 | 
				
			||||||
 | 
					                # Clean up partial download
 | 
				
			||||||
 | 
					                if os.path.exists(shared_path):
 | 
				
			||||||
 | 
					                    shutil.rmtree(shared_path, ignore_errors=True)
 | 
				
			||||||
 | 
					                return None
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def release_mpta(self, model_id: int, camera_id: str) -> None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Release a reference to an MPTA. If reference count reaches zero,
 | 
				
			||||||
 | 
					        the MPTA directory may be cleaned up to free disk space.
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            model_id: Unique identifier for the model to release
 | 
				
			||||||
 | 
					            camera_id: Identifier for the camera releasing the reference
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._manager_lock:
 | 
				
			||||||
 | 
					            if model_id not in self._reference_counts:
 | 
				
			||||||
 | 
					                logger.warning(f"⚠️ Attempted to release unknown MPTA modelId {model_id} for camera {camera_id}")
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Remove camera from usage tracking
 | 
				
			||||||
 | 
					            if model_id in self._cameras_using_model:
 | 
				
			||||||
 | 
					                self._cameras_using_model[model_id].discard(camera_id)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            self._reference_counts[model_id] -= 1
 | 
				
			||||||
 | 
					            logger.info(f"📉 MPTA modelId {model_id} reference count decreased to {self._reference_counts[model_id]} (released by {camera_id})")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Clean up if no more references
 | 
				
			||||||
 | 
					            # if self._reference_counts[model_id] <= 0:
 | 
				
			||||||
 | 
					            #     self._cleanup_mpta(model_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _cleanup_mpta(self, model_id: int) -> None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Internal method to clean up an MPTA directory and free disk space.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if model_id in self._model_paths:
 | 
				
			||||||
 | 
					            shared_path = self._model_paths[model_id]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                if os.path.exists(shared_path):
 | 
				
			||||||
 | 
					                    shutil.rmtree(shared_path)
 | 
				
			||||||
 | 
					                    logger.info(f"🗑️ Cleaned up MPTA directory: {shared_path}")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Remove from tracking
 | 
				
			||||||
 | 
					                del self._model_paths[model_id]
 | 
				
			||||||
 | 
					                self._mpta_file_paths.pop(model_id, None)
 | 
				
			||||||
 | 
					                del self._reference_counts[model_id]
 | 
				
			||||||
 | 
					                self._cameras_using_model.pop(model_id, None)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Clean up download lock (optional, could keep for future use)
 | 
				
			||||||
 | 
					                self._download_locks.pop(model_id, None)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                logger.info(f"✅ MPTA modelId {model_id} fully cleaned up and disk space freed")
 | 
				
			||||||
 | 
					                self._log_manager_status()
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            except Exception as e:
 | 
				
			||||||
 | 
					                logger.error(f"❌ Error cleaning up MPTA modelId {model_id}: {e}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def get_shared_path(self, model_id: int) -> Optional[str]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Get the shared extraction path for a modelId without downloading.
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            model_id: Model identifier to look up
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Shared path if available, None otherwise
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._manager_lock:
 | 
				
			||||||
 | 
					            return self._model_paths.get(model_id)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def get_manager_status(self) -> Dict:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Get current status of the MPTA manager.
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            Dictionary with manager statistics
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._manager_lock:
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                "total_mpta_models": len(self._model_paths),
 | 
				
			||||||
 | 
					                "models": {
 | 
				
			||||||
 | 
					                    str(model_id): {
 | 
				
			||||||
 | 
					                        "shared_path": path,
 | 
				
			||||||
 | 
					                        "reference_count": self._reference_counts.get(model_id, 0),
 | 
				
			||||||
 | 
					                        "cameras_using": list(self._cameras_using_model.get(model_id, set()))
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    for model_id, path in self._model_paths.items()
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                "total_references": sum(self._reference_counts.values()),
 | 
				
			||||||
 | 
					                "active_downloads": len(self._download_locks)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _log_manager_status(self) -> None:
 | 
				
			||||||
 | 
					        """Log current manager status for debugging."""
 | 
				
			||||||
 | 
					        status = self.get_manager_status()
 | 
				
			||||||
 | 
					        logger.info(f"📊 MPTA Manager Status: {status['total_mpta_models']} unique models, {status['total_references']} total references")
 | 
				
			||||||
 | 
					        for model_id, info in status['models'].items():
 | 
				
			||||||
 | 
					            cameras_str = ','.join(info['cameras_using'][:3])  # Show first 3 cameras
 | 
				
			||||||
 | 
					            if len(info['cameras_using']) > 3:
 | 
				
			||||||
 | 
					                cameras_str += f"+{len(info['cameras_using'])-3} more"
 | 
				
			||||||
 | 
					            logger.debug(f"   📋 ModelId {model_id}: refs={info['reference_count']}, cameras=[{cameras_str}]")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def cleanup_all(self) -> None:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Clean up all MPTA directories. Used during shutdown.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        with self._manager_lock:
 | 
				
			||||||
 | 
					            model_ids = list(self._model_paths.keys())
 | 
				
			||||||
 | 
					            logger.info(f"🧹 Cleaning up {len(model_ids)} MPTA directories")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for model_id in model_ids:
 | 
				
			||||||
 | 
					                self._cleanup_mpta(model_id)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # Clear all tracking data
 | 
				
			||||||
 | 
					            self._download_locks.clear()
 | 
				
			||||||
 | 
					            logger.info("✅ MPTA manager cleanup complete")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _download_file(self, url: str, local_path: str) -> bool:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Download a file from URL to local path with progress logging.
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            url: URL to download from
 | 
				
			||||||
 | 
					            local_path: Local path to save to
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        Returns:
 | 
				
			||||||
 | 
					            True if successful, False otherwise
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            logger.info(f"⬇️ Starting download from {url}")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            response = requests.get(url, stream=True)
 | 
				
			||||||
 | 
					            response.raise_for_status()
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            total_size = int(response.headers.get('content-length', 0))
 | 
				
			||||||
 | 
					            if total_size > 0:
 | 
				
			||||||
 | 
					                logger.info(f"📦 File size: {total_size / 1024 / 1024:.2f} MB")
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            downloaded = 0
 | 
				
			||||||
 | 
					            last_logged_progress = 0
 | 
				
			||||||
 | 
					            with open(local_path, 'wb') as f:
 | 
				
			||||||
 | 
					                for chunk in response.iter_content(chunk_size=8192):
 | 
				
			||||||
 | 
					                    if chunk:
 | 
				
			||||||
 | 
					                        f.write(chunk)
 | 
				
			||||||
 | 
					                        downloaded += len(chunk)
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        if total_size > 0:
 | 
				
			||||||
 | 
					                            progress = int((downloaded / total_size) * 100)
 | 
				
			||||||
 | 
					                            # Log at 10% intervals (10%, 20%, 30%, etc.)
 | 
				
			||||||
 | 
					                            if progress >= last_logged_progress + 10 and progress <= 100:
 | 
				
			||||||
 | 
					                                logger.debug(f"Download progress: {progress}%")
 | 
				
			||||||
 | 
					                                last_logged_progress = progress
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            logger.info(f"✅ Successfully downloaded to {local_path}")
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            logger.error(f"❌ Download failed: {e}")
 | 
				
			||||||
 | 
					            # Clean up partial file
 | 
				
			||||||
 | 
					            if os.path.exists(local_path):
 | 
				
			||||||
 | 
					                os.remove(local_path)
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _extract_filename_from_url(self, url: str) -> Optional[str]:
 | 
				
			||||||
 | 
					        """Extract filename from URL."""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            parsed = urlparse(url)
 | 
				
			||||||
 | 
					            filename = os.path.basename(parsed.path)
 | 
				
			||||||
 | 
					            return filename if filename else None
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Global singleton instance
 | 
				
			||||||
 | 
					_mpta_manager = MPTAManager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_or_download_mpta(model_id: int, model_url: str, camera_id: str) -> Optional[tuple[str, str]]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function to get or download a shared MPTA.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        model_id: Unique identifier for the model
 | 
				
			||||||
 | 
					        model_url: URL to download the MPTA file from
 | 
				
			||||||
 | 
					        camera_id: Identifier for the requesting camera
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        Tuple of (extraction_path, mpta_file_path), or None if failed
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return _mpta_manager.get_or_download_mpta(model_id, model_url, camera_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def release_mpta(model_id: int, camera_id: str) -> None:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function to release a shared MPTA reference.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Args:
 | 
				
			||||||
 | 
					        model_id: Unique identifier for the model to release
 | 
				
			||||||
 | 
					        camera_id: Identifier for the camera releasing the reference
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    _mpta_manager.release_mpta(model_id, camera_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_mpta_manager_status() -> Dict:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function to get MPTA manager status.
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    Returns:
 | 
				
			||||||
 | 
					        Dictionary with manager statistics
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return _mpta_manager.get_manager_status()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def cleanup_mpta_manager() -> None:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Convenience function to cleanup the entire MPTA manager.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    _mpta_manager.cleanup_all()
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								test/sample.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								test/sample.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.8 MiB  | 
							
								
								
									
										
											BIN
										
									
								
								test/sample2.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								test/sample2.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.1 MiB  | 
							
								
								
									
										60
									
								
								test/test.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								test/test.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,60 @@
 | 
				
			||||||
 | 
					from ultralytics import YOLO
 | 
				
			||||||
 | 
					import cv2
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Load the model
 | 
				
			||||||
 | 
					# model = YOLO('../models/webcam-local-01/4/bangchak_poc/yolo11n.pt')
 | 
				
			||||||
 | 
					model = YOLO('yolo11m.pt')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_image(image_path):
 | 
				
			||||||
 | 
					    """Test a single image with YOLO model"""
 | 
				
			||||||
 | 
					    if not os.path.exists(image_path):
 | 
				
			||||||
 | 
					        print(f"Image not found: {image_path}")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Run inference - filter for car class only (class 2 in COCO)
 | 
				
			||||||
 | 
					    results = model(image_path, classes=[2, 5, 7])  # 2, 5, 7 = car, bus, truck in COCO dataset
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Display results
 | 
				
			||||||
 | 
					    for r in results:
 | 
				
			||||||
 | 
					        im_array = r.plot()  # plot a BGR numpy array of predictions
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Resize image for display (max width/height 800px)
 | 
				
			||||||
 | 
					        height, width = im_array.shape[:2]
 | 
				
			||||||
 | 
					        max_dimension = 800
 | 
				
			||||||
 | 
					        if width > max_dimension or height > max_dimension:
 | 
				
			||||||
 | 
					            if width > height:
 | 
				
			||||||
 | 
					                new_width = max_dimension
 | 
				
			||||||
 | 
					                new_height = int(height * (max_dimension / width))
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                new_height = max_dimension
 | 
				
			||||||
 | 
					                new_width = int(width * (max_dimension / height))
 | 
				
			||||||
 | 
					            im_array = cv2.resize(im_array, (new_width, new_height))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Show image with predictions
 | 
				
			||||||
 | 
					        cv2.imshow('YOLO Test - Car Detection Only', im_array)
 | 
				
			||||||
 | 
					        cv2.waitKey(0)
 | 
				
			||||||
 | 
					        cv2.destroyAllWindows()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Print detection info
 | 
				
			||||||
 | 
					        print(f"\nDetections for {image_path}:")
 | 
				
			||||||
 | 
					        if r.boxes is not None and len(r.boxes) > 0:
 | 
				
			||||||
 | 
					            for i, box in enumerate(r.boxes):
 | 
				
			||||||
 | 
					                cls = int(box.cls[0])
 | 
				
			||||||
 | 
					                conf = float(box.conf[0])
 | 
				
			||||||
 | 
					                original_class = model.names[cls]  # Original class name (car/bus/truck)
 | 
				
			||||||
 | 
					                # Get bounding box coordinates
 | 
				
			||||||
 | 
					                x1, y1, x2, y2 = box.xyxy[0].tolist()
 | 
				
			||||||
 | 
					                # Rename all vehicle types to "car"
 | 
				
			||||||
 | 
					                print(f"Detection {i+1}: car (was: {original_class}) - Confidence: {conf:.3f} - BBox: ({x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f})")
 | 
				
			||||||
 | 
					            print(f"Total cars detected: {len(r.boxes)}")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            print("No cars detected in the image")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    # Test with an image file
 | 
				
			||||||
 | 
					    image_path = input("Enter image path (or press Enter for default test): ")
 | 
				
			||||||
 | 
					    if not image_path:
 | 
				
			||||||
 | 
					        image_path = "sample.png"  # Default test image
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    test_image(image_path)
 | 
				
			||||||
							
								
								
									
										352
									
								
								test/test_botsort_zone_track.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										352
									
								
								test/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()
 | 
				
			||||||
							
								
								
									
										162
									
								
								view_redis_images.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								view_redis_images.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,162 @@
 | 
				
			||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Script to view frontal images saved in Redis
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import redis
 | 
				
			||||||
 | 
					import cv2
 | 
				
			||||||
 | 
					import numpy as np
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Redis connection config (from pipeline.json)
 | 
				
			||||||
 | 
					REDIS_CONFIG = {
 | 
				
			||||||
 | 
					    "host": "10.100.1.3",
 | 
				
			||||||
 | 
					    "port": 6379,
 | 
				
			||||||
 | 
					    "password": "FBQgi0i5RevAAMO5Hh66",
 | 
				
			||||||
 | 
					    "db": 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def connect_redis():
 | 
				
			||||||
 | 
					    """Connect to Redis server."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        client = redis.Redis(
 | 
				
			||||||
 | 
					            host=REDIS_CONFIG["host"],
 | 
				
			||||||
 | 
					            port=REDIS_CONFIG["port"],
 | 
				
			||||||
 | 
					            password=REDIS_CONFIG["password"],
 | 
				
			||||||
 | 
					            db=REDIS_CONFIG["db"],
 | 
				
			||||||
 | 
					            decode_responses=False  # Keep bytes for images
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        client.ping()
 | 
				
			||||||
 | 
					        print(f"✅ Connected to Redis at {REDIS_CONFIG['host']}:{REDIS_CONFIG['port']}")
 | 
				
			||||||
 | 
					        return client
 | 
				
			||||||
 | 
					    except redis.exceptions.ConnectionError as e:
 | 
				
			||||||
 | 
					        print(f"❌ Failed to connect to Redis: {e}")
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def list_image_keys(client):
 | 
				
			||||||
 | 
					    """List all image keys in Redis."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Look for keys matching the inference pattern
 | 
				
			||||||
 | 
					        keys = client.keys("inference:*")
 | 
				
			||||||
 | 
					        print(f"\n📋 Found {len(keys)} image keys:")
 | 
				
			||||||
 | 
					        for i, key in enumerate(keys):
 | 
				
			||||||
 | 
					            key_str = key.decode() if isinstance(key, bytes) else key
 | 
				
			||||||
 | 
					            print(f"{i+1}. {key_str}")
 | 
				
			||||||
 | 
					        return keys
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print(f"❌ Error listing keys: {e}")
 | 
				
			||||||
 | 
					        return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def view_image(client, key):
 | 
				
			||||||
 | 
					    """View a specific image from Redis."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Get image data from Redis
 | 
				
			||||||
 | 
					        image_data = client.get(key)
 | 
				
			||||||
 | 
					        if image_data is None:
 | 
				
			||||||
 | 
					            print(f"❌ No data found for key: {key}")
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        print(f"📸 Image size: {len(image_data)} bytes")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Convert bytes to numpy array
 | 
				
			||||||
 | 
					        nparr = np.frombuffer(image_data, np.uint8)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Decode image
 | 
				
			||||||
 | 
					        img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
 | 
				
			||||||
 | 
					        if img is None:
 | 
				
			||||||
 | 
					            print("❌ Failed to decode image data")
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        print(f"🖼️  Image dimensions: {img.shape[1]}x{img.shape[0]} pixels")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Display image
 | 
				
			||||||
 | 
					        key_str = key.decode() if isinstance(key, bytes) else key
 | 
				
			||||||
 | 
					        cv2.imshow(f'Redis Image: {key_str}', img)
 | 
				
			||||||
 | 
					        print("👁️  Image displayed. Press any key to close...")
 | 
				
			||||||
 | 
					        cv2.waitKey(0)
 | 
				
			||||||
 | 
					        cv2.destroyAllWindows()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Ask if user wants to save the image
 | 
				
			||||||
 | 
					        save = input("💾 Save image to file? (y/n): ").lower().strip()
 | 
				
			||||||
 | 
					        if save == 'y':
 | 
				
			||||||
 | 
					            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
 | 
				
			||||||
 | 
					            filename = f"redis_image_{timestamp}.jpg"
 | 
				
			||||||
 | 
					            cv2.imwrite(filename, img)
 | 
				
			||||||
 | 
					            print(f"💾 Image saved as: {filename}")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print(f"❌ Error viewing image: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def monitor_new_images(client):
 | 
				
			||||||
 | 
					    """Monitor for new images being added to Redis."""
 | 
				
			||||||
 | 
					    print("👀 Monitoring for new images... (Press Ctrl+C to stop)")
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        # Subscribe to Redis pub/sub for car detections
 | 
				
			||||||
 | 
					        pubsub = client.pubsub()
 | 
				
			||||||
 | 
					        pubsub.subscribe('car_detections')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        for message in pubsub.listen():
 | 
				
			||||||
 | 
					            if message['type'] == 'message':
 | 
				
			||||||
 | 
					                data = message['data'].decode()
 | 
				
			||||||
 | 
					                print(f"🚨 New detection: {data}")
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                # Try to extract image key from message
 | 
				
			||||||
 | 
					                import json
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    detection_data = json.loads(data)
 | 
				
			||||||
 | 
					                    image_key = detection_data.get('image_key')
 | 
				
			||||||
 | 
					                    if image_key:
 | 
				
			||||||
 | 
					                        print(f"🖼️  New image available: {image_key}")
 | 
				
			||||||
 | 
					                        view_choice = input("View this image now? (y/n): ").lower().strip()
 | 
				
			||||||
 | 
					                        if view_choice == 'y':
 | 
				
			||||||
 | 
					                            view_image(client, image_key)
 | 
				
			||||||
 | 
					                except json.JSONDecodeError:
 | 
				
			||||||
 | 
					                    pass
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					    except KeyboardInterrupt:
 | 
				
			||||||
 | 
					        print("\n👋 Stopping monitor...")
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print(f"❌ Monitor error: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    """Main function."""
 | 
				
			||||||
 | 
					    print("🔍 Redis Image Viewer")
 | 
				
			||||||
 | 
					    print("=" * 50)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Connect to Redis
 | 
				
			||||||
 | 
					    client = connect_redis()
 | 
				
			||||||
 | 
					    if not client:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        print("\n📋 Options:")
 | 
				
			||||||
 | 
					        print("1. List all image keys")
 | 
				
			||||||
 | 
					        print("2. View specific image")
 | 
				
			||||||
 | 
					        print("3. Monitor for new images")
 | 
				
			||||||
 | 
					        print("4. Exit")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        choice = input("\nEnter choice (1-4): ").strip()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if choice == '1':
 | 
				
			||||||
 | 
					            keys = list_image_keys(client)
 | 
				
			||||||
 | 
					        elif choice == '2':
 | 
				
			||||||
 | 
					            keys = list_image_keys(client)
 | 
				
			||||||
 | 
					            if keys:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    idx = int(input(f"\nEnter image number (1-{len(keys)}): ")) - 1
 | 
				
			||||||
 | 
					                    if 0 <= idx < len(keys):
 | 
				
			||||||
 | 
					                        view_image(client, keys[idx])
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        print("❌ Invalid selection")
 | 
				
			||||||
 | 
					                except ValueError:
 | 
				
			||||||
 | 
					                    print("❌ Please enter a valid number")
 | 
				
			||||||
 | 
					        elif choice == '3':
 | 
				
			||||||
 | 
					            monitor_new_images(client)
 | 
				
			||||||
 | 
					        elif choice == '4':
 | 
				
			||||||
 | 
					            print("👋 Goodbye!")
 | 
				
			||||||
 | 
					            break
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            print("❌ Invalid choice")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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