Merge pull request '[Pongsatorn K. 2025/08/30] worker ver 1.0.0' (#4) from feat/tracker into dev
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Build Worker Base and Application Images / check-base-changes (push) Successful in 8s
				
			
		
			
				
	
				Build Worker Base and Application Images / build-base (push) Failing after 59s
				
			
		
			
				
	
				Build Worker Base and Application Images / build-docker (push) Has been skipped
				
			
		
			
				
	
				Build Worker Base and Application Images / deploy-stack (push) Has been skipped
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Build Worker Base and Application Images / check-base-changes (push) Successful in 8s
				
			Build Worker Base and Application Images / build-base (push) Failing after 59s
				
			Build Worker Base and Application Images / build-docker (push) Has been skipped
				
			Build Worker Base and Application Images / deploy-stack (push) Has been skipped
				
			Reviewed-on: #4 Reviewed-by: Siwat Sirichai <siwat@siwatinc.com>
This commit is contained in:
		
						commit
						864be5cb47
					
				
					 13 changed files with 3533 additions and 252 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -13,3 +13,5 @@ no_frame_debug.log
 | 
			
		|||
 | 
			
		||||
feeder/
 | 
			
		||||
.venv/
 | 
			
		||||
.vscode/
 | 
			
		||||
dist/
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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()
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,13 @@
 | 
			
		|||
torch
 | 
			
		||||
torchvision
 | 
			
		||||
ultralytics
 | 
			
		||||
opencv-python
 | 
			
		||||
scipy
 | 
			
		||||
filterpy
 | 
			
		||||
psycopg2-binary
 | 
			
		||||
torch>=1.12.0,<2.1.0
 | 
			
		||||
torchvision>=0.13.0,<0.16.0
 | 
			
		||||
ultralytics>=8.3.0
 | 
			
		||||
opencv-python>=4.6.0,<4.9.0
 | 
			
		||||
scipy>=1.9.0,<1.12.0
 | 
			
		||||
filterpy>=1.4.0,<1.5.0
 | 
			
		||||
psycopg2-binary>=2.9.0,<2.10.0
 | 
			
		||||
easydict
 | 
			
		||||
loguru
 | 
			
		||||
pyzmq
 | 
			
		||||
gitpython
 | 
			
		||||
gdown
 | 
			
		||||
lap
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
fastapi
 | 
			
		||||
fastapi[standard]
 | 
			
		||||
uvicorn
 | 
			
		||||
websockets
 | 
			
		||||
fastapi[standard]
 | 
			
		||||
redis
 | 
			
		||||
urllib3<2.0.0
 | 
			
		||||
| 
						 | 
				
			
			@ -80,37 +80,50 @@ class DatabaseManager:
 | 
			
		|||
        try:
 | 
			
		||||
            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 = []
 | 
			
		||||
            values = []
 | 
			
		||||
            update_values = []
 | 
			
		||||
            
 | 
			
		||||
            for field, value in fields.items():
 | 
			
		||||
                if value == "NOW()":
 | 
			
		||||
                    # Special handling for NOW()
 | 
			
		||||
                    insert_placeholders.append("NOW()")
 | 
			
		||||
                    set_clauses.append(f"{field} = NOW()")
 | 
			
		||||
                else:
 | 
			
		||||
                    insert_placeholders.append("%s")
 | 
			
		||||
                    insert_values.append(value)
 | 
			
		||||
                    set_clauses.append(f"{field} = %s")
 | 
			
		||||
                    values.append(value)
 | 
			
		||||
                    update_values.append(value)
 | 
			
		||||
            
 | 
			
		||||
            # Add schema prefix if table doesn't already have it
 | 
			
		||||
            full_table_name = table if '.' in table else f"gas_station_1.{table}"
 | 
			
		||||
            
 | 
			
		||||
            # Build the complete query
 | 
			
		||||
            query = f"""
 | 
			
		||||
            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})
 | 
			
		||||
            DO UPDATE SET {', '.join(set_clauses)}
 | 
			
		||||
            """
 | 
			
		||||
            
 | 
			
		||||
            # Add key_value to the beginning of values list
 | 
			
		||||
            all_values = [key_value] + list(fields.values()) + values
 | 
			
		||||
            # Combine values for the query: insert_values + update_values
 | 
			
		||||
            all_values = insert_values + update_values
 | 
			
		||||
            
 | 
			
		||||
            logger.debug(f"SQL Query: {query}")
 | 
			
		||||
            logger.debug(f"Values: {all_values}")
 | 
			
		||||
            
 | 
			
		||||
            cur.execute(query, all_values)
 | 
			
		||||
            self.connection.commit()
 | 
			
		||||
            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
 | 
			
		||||
        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:
 | 
			
		||||
                self.connection.rollback()
 | 
			
		||||
            return False
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												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