Compare commits
No commits in common. "864be5cb4751778845cb424ea9f5dab71fd6f210" and "007a3d48b92d590a0baae979f8eb7194cc926c49" have entirely different histories.
864be5cb47
...
007a3d48b9
13 changed files with 248 additions and 3529 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -13,5 +13,3 @@ no_frame_debug.log
|
||||||
|
|
||||||
feeder/
|
feeder/
|
||||||
.venv/
|
.venv/
|
||||||
.vscode/
|
|
||||||
dist/
|
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
#!/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,13 +1,7 @@
|
||||||
torch>=1.12.0,<2.1.0
|
torch
|
||||||
torchvision>=0.13.0,<0.16.0
|
torchvision
|
||||||
ultralytics>=8.3.0
|
ultralytics
|
||||||
opencv-python>=4.6.0,<4.9.0
|
opencv-python
|
||||||
scipy>=1.9.0,<1.12.0
|
scipy
|
||||||
filterpy>=1.4.0,<1.5.0
|
filterpy
|
||||||
psycopg2-binary>=2.9.0,<2.10.0
|
psycopg2-binary
|
||||||
easydict
|
|
||||||
loguru
|
|
||||||
pyzmq
|
|
||||||
gitpython
|
|
||||||
gdown
|
|
||||||
lap
|
|
|
@ -1,5 +1,6 @@
|
||||||
fastapi[standard]
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
websockets
|
websockets
|
||||||
|
fastapi[standard]
|
||||||
redis
|
redis
|
||||||
urllib3<2.0.0
|
urllib3<2.0.0
|
|
@ -80,50 +80,37 @@ class DatabaseManager:
|
||||||
try:
|
try:
|
||||||
cur = self.connection.cursor()
|
cur = self.connection.cursor()
|
||||||
|
|
||||||
# Build the INSERT and UPDATE query dynamically
|
# Build the UPDATE query dynamically
|
||||||
insert_placeholders = []
|
|
||||||
insert_values = [key_value] # Start with key_value
|
|
||||||
|
|
||||||
set_clauses = []
|
set_clauses = []
|
||||||
update_values = []
|
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")
|
||||||
update_values.append(value)
|
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(insert_placeholders)})
|
VALUES (%s, {', '.join(['%s'] * len(fields))})
|
||||||
ON CONFLICT ({key_field})
|
ON CONFLICT ({key_field})
|
||||||
DO UPDATE SET {', '.join(set_clauses)}
|
DO UPDATE SET {', '.join(set_clauses)}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Combine values for the query: insert_values + update_values
|
# Add key_value to the beginning of values list
|
||||||
all_values = insert_values + update_values
|
all_values = [key_value] + list(fields.values()) + 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} with fields: {fields}")
|
logger.info(f"Updated {table} for {key_field}={key_value}")
|
||||||
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
BIN
test/sample.png
Binary file not shown.
Before Width: | Height: | Size: 2.8 MiB |
BIN
test/sample2.png
BIN
test/sample2.png
Binary file not shown.
Before Width: | Height: | Size: 3.1 MiB |
60
test/test.py
60
test/test.py
|
@ -1,60 +0,0 @@
|
||||||
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)
|
|
|
@ -1,352 +0,0 @@
|
||||||
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()
|
|
|
@ -1,162 +0,0 @@
|
||||||
#!/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()
|
|
|
@ -1,325 +0,0 @@
|
||||||
#!/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