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/
|
||||
.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
|
||||
torchvision>=0.13.0,<0.16.0
|
||||
ultralytics>=8.3.0
|
||||
opencv-python>=4.6.0,<4.9.0
|
||||
scipy>=1.9.0,<1.12.0
|
||||
filterpy>=1.4.0,<1.5.0
|
||||
psycopg2-binary>=2.9.0,<2.10.0
|
||||
easydict
|
||||
loguru
|
||||
pyzmq
|
||||
gitpython
|
||||
gdown
|
||||
lap
|
||||
torch
|
||||
torchvision
|
||||
ultralytics
|
||||
opencv-python
|
||||
scipy
|
||||
filterpy
|
||||
psycopg2-binary
|
|
@ -1,5 +1,6 @@
|
|||
fastapi[standard]
|
||||
fastapi
|
||||
uvicorn
|
||||
websockets
|
||||
fastapi[standard]
|
||||
redis
|
||||
urllib3<2.0.0
|
|
@ -80,50 +80,37 @@ class DatabaseManager:
|
|||
try:
|
||||
cur = self.connection.cursor()
|
||||
|
||||
# Build the INSERT and UPDATE query dynamically
|
||||
insert_placeholders = []
|
||||
insert_values = [key_value] # Start with key_value
|
||||
|
||||
# Build the UPDATE query dynamically
|
||||
set_clauses = []
|
||||
update_values = []
|
||||
values = []
|
||||
|
||||
for field, value in fields.items():
|
||||
if value == "NOW()":
|
||||
# Special handling for NOW()
|
||||
insert_placeholders.append("NOW()")
|
||||
set_clauses.append(f"{field} = NOW()")
|
||||
else:
|
||||
insert_placeholders.append("%s")
|
||||
insert_values.append(value)
|
||||
set_clauses.append(f"{field} = %s")
|
||||
update_values.append(value)
|
||||
values.append(value)
|
||||
|
||||
# Add schema prefix if table doesn't already have it
|
||||
full_table_name = table if '.' in table else f"gas_station_1.{table}"
|
||||
|
||||
# Build the complete query
|
||||
query = f"""
|
||||
INSERT INTO {full_table_name} ({key_field}, {', '.join(fields.keys())})
|
||||
VALUES (%s, {', '.join(insert_placeholders)})
|
||||
VALUES (%s, {', '.join(['%s'] * len(fields))})
|
||||
ON CONFLICT ({key_field})
|
||||
DO UPDATE SET {', '.join(set_clauses)}
|
||||
"""
|
||||
|
||||
# Combine values for the query: insert_values + update_values
|
||||
all_values = insert_values + update_values
|
||||
|
||||
logger.debug(f"SQL Query: {query}")
|
||||
logger.debug(f"Values: {all_values}")
|
||||
# Add key_value to the beginning of values list
|
||||
all_values = [key_value] + list(fields.values()) + values
|
||||
|
||||
cur.execute(query, all_values)
|
||||
self.connection.commit()
|
||||
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
|
||||
except Exception as 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'}")
|
||||
logger.error(f"Failed to execute update on {table}: {e}")
|
||||
if self.connection:
|
||||
self.connection.rollback()
|
||||
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