[Pongsatorn K. 2025/08/30] worker ver 1.0.0 #4
13 changed files with 3533 additions and 252 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -13,3 +13,5 @@ no_frame_debug.log
|
||||||
|
|
||||||
feeder/
|
feeder/
|
||||||
.venv/
|
.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
|
torch>=1.12.0,<2.1.0
|
||||||
torchvision
|
torchvision>=0.13.0,<0.16.0
|
||||||
ultralytics
|
ultralytics>=8.3.0
|
||||||
opencv-python
|
opencv-python>=4.6.0,<4.9.0
|
||||||
scipy
|
scipy>=1.9.0,<1.12.0
|
||||||
filterpy
|
filterpy>=1.4.0,<1.5.0
|
||||||
psycopg2-binary
|
psycopg2-binary>=2.9.0,<2.10.0
|
||||||
|
easydict
|
||||||
|
loguru
|
||||||
|
pyzmq
|
||||||
|
gitpython
|
||||||
|
gdown
|
||||||
|
lap
|
|
@ -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
|
||||||
|
|
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