feat: custom bot-sort based tracker
This commit is contained in:
parent
bd201acac1
commit
791f611f7d
8 changed files with 649 additions and 282 deletions
|
@ -21,6 +21,34 @@ os.environ["OPENCV_FFMPEG_LOGLEVEL"] = "-8" # Suppress FFMPEG warnings
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Color codes for pretty logging
|
||||
class Colors:
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
BLUE = '\033[94m'
|
||||
PURPLE = '\033[95m'
|
||||
CYAN = '\033[96m'
|
||||
WHITE = '\033[97m'
|
||||
BOLD = '\033[1m'
|
||||
END = '\033[0m'
|
||||
|
||||
def log_success(camera_id: str, message: str):
|
||||
"""Log success messages in green"""
|
||||
logger.info(f"{Colors.GREEN}[{camera_id}] {message}{Colors.END}")
|
||||
|
||||
def log_warning(camera_id: str, message: str):
|
||||
"""Log warnings in yellow"""
|
||||
logger.warning(f"{Colors.YELLOW}[{camera_id}] {message}{Colors.END}")
|
||||
|
||||
def log_error(camera_id: str, message: str):
|
||||
"""Log errors in red"""
|
||||
logger.error(f"{Colors.RED}[{camera_id}] {message}{Colors.END}")
|
||||
|
||||
def log_info(camera_id: str, message: str):
|
||||
"""Log info in cyan"""
|
||||
logger.info(f"{Colors.CYAN}[{camera_id}] {message}{Colors.END}")
|
||||
|
||||
# Removed watchdog logging configuration - no longer using file watching
|
||||
|
||||
|
||||
|
@ -56,7 +84,7 @@ class FFmpegRTSPReader:
|
|||
self.stop_event.clear()
|
||||
self.thread = threading.Thread(target=self._read_frames, daemon=True)
|
||||
self.thread.start()
|
||||
logger.info(f"Started FFmpeg reader for camera {self.camera_id}")
|
||||
log_success(self.camera_id, "Stream started")
|
||||
|
||||
def stop(self):
|
||||
"""Stop the FFmpeg subprocess reader."""
|
||||
|
@ -69,61 +97,12 @@ class FFmpegRTSPReader:
|
|||
self.process.kill()
|
||||
if self.thread:
|
||||
self.thread.join(timeout=5.0)
|
||||
logger.info(f"Stopped FFmpeg reader for camera {self.camera_id}")
|
||||
log_info(self.camera_id, "Stream stopped")
|
||||
|
||||
def _probe_stream_info(self):
|
||||
"""Probe stream to get resolution and other info."""
|
||||
try:
|
||||
cmd = [
|
||||
'ffprobe',
|
||||
'-v', 'quiet',
|
||||
'-print_format', 'json',
|
||||
'-show_streams',
|
||||
'-select_streams', 'v:0', # First video stream
|
||||
'-rtsp_transport', 'tcp',
|
||||
self.rtsp_url
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Camera {self.camera_id}: ffprobe failed (code {result.returncode})")
|
||||
if result.stderr:
|
||||
logger.error(f"Camera {self.camera_id}: ffprobe stderr: {result.stderr}")
|
||||
if result.stdout:
|
||||
logger.debug(f"Camera {self.camera_id}: ffprobe stdout: {result.stdout}")
|
||||
return None
|
||||
|
||||
import json
|
||||
data = json.loads(result.stdout)
|
||||
if not data.get('streams'):
|
||||
logger.error(f"Camera {self.camera_id}: No video streams found")
|
||||
return None
|
||||
|
||||
stream = data['streams'][0]
|
||||
width = stream.get('width')
|
||||
height = stream.get('height')
|
||||
|
||||
if not width or not height:
|
||||
logger.error(f"Camera {self.camera_id}: Could not determine resolution")
|
||||
return None
|
||||
|
||||
logger.info(f"Camera {self.camera_id}: Detected resolution {width}x{height}")
|
||||
return width, height
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Camera {self.camera_id}: Error probing stream: {e}")
|
||||
return None
|
||||
# Removed _probe_stream_info - BMP headers contain dimensions
|
||||
|
||||
def _start_ffmpeg_process(self):
|
||||
"""Start FFmpeg subprocess outputting raw RGB frames to stdout pipe."""
|
||||
# First probe the stream to get resolution
|
||||
probe_result = self._probe_stream_info()
|
||||
if not probe_result:
|
||||
logger.error(f"Camera {self.camera_id}: Failed to probe stream info")
|
||||
return False
|
||||
|
||||
self.actual_width, self.actual_height = probe_result
|
||||
|
||||
"""Start FFmpeg subprocess outputting BMP frames to stdout pipe."""
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
# DO NOT REMOVE
|
||||
|
@ -131,17 +110,14 @@ class FFmpegRTSPReader:
|
|||
# '-hwaccel_device', '0',
|
||||
'-rtsp_transport', 'tcp',
|
||||
'-i', self.rtsp_url,
|
||||
'-f', 'rawvideo', # Raw video output instead of PPM
|
||||
'-pix_fmt', 'rgb24', # Raw RGB24 format
|
||||
'-f', 'image2pipe', # Output images to pipe
|
||||
'-vcodec', 'bmp', # BMP format with header containing dimensions
|
||||
# Use native stream resolution and framerate
|
||||
'-an', # No audio
|
||||
'-' # Output to stdout
|
||||
]
|
||||
|
||||
try:
|
||||
# Log the FFmpeg command for debugging
|
||||
logger.info(f"Starting FFmpeg for camera {self.camera_id} with command: {' '.join(cmd)}")
|
||||
|
||||
# Start FFmpeg with stdout pipe to read frames directly
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
|
@ -149,46 +125,60 @@ class FFmpegRTSPReader:
|
|||
stderr=subprocess.DEVNULL,
|
||||
bufsize=0 # Unbuffered for real-time processing
|
||||
)
|
||||
logger.info(f"Started FFmpeg process PID {self.process.pid} for camera {self.camera_id} -> stdout pipe (resolution: {self.actual_width}x{self.actual_height})")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start FFmpeg for camera {self.camera_id}: {e}")
|
||||
log_error(self.camera_id, f"FFmpeg startup failed: {e}")
|
||||
return False
|
||||
|
||||
def _read_raw_frame(self, pipe):
|
||||
"""Read raw RGB frame data from pipe with proper buffering."""
|
||||
def _read_bmp_frame(self, pipe):
|
||||
"""Read BMP frame from pipe - BMP header contains dimensions."""
|
||||
try:
|
||||
# Calculate frame size using actual detected dimensions
|
||||
frame_size = self.actual_width * self.actual_height * 3
|
||||
# Read BMP header (14 bytes file header + 40 bytes info header = 54 bytes minimum)
|
||||
header_data = b''
|
||||
bytes_to_read = 54
|
||||
|
||||
# Read frame data in chunks until we have the complete frame
|
||||
frame_data = b''
|
||||
bytes_remaining = frame_size
|
||||
while len(header_data) < bytes_to_read:
|
||||
chunk = pipe.read(bytes_to_read - len(header_data))
|
||||
if not chunk:
|
||||
return None # Silent end of stream
|
||||
header_data += chunk
|
||||
|
||||
while bytes_remaining > 0:
|
||||
chunk = pipe.read(bytes_remaining)
|
||||
if not chunk: # EOF
|
||||
if len(frame_data) == 0:
|
||||
logger.debug(f"Camera {self.camera_id}: No more data (stream ended)")
|
||||
else:
|
||||
logger.warning(f"Camera {self.camera_id}: Stream ended mid-frame: {len(frame_data)}/{frame_size} bytes")
|
||||
return None
|
||||
# Parse BMP header
|
||||
if header_data[:2] != b'BM':
|
||||
return None # Invalid format, skip frame silently
|
||||
|
||||
frame_data += chunk
|
||||
bytes_remaining -= len(chunk)
|
||||
# Extract file size from header (bytes 2-5)
|
||||
import struct
|
||||
file_size = struct.unpack('<L', header_data[2:6])[0]
|
||||
|
||||
# Convert raw RGB data to numpy array using actual dimensions
|
||||
frame_array = np.frombuffer(frame_data, dtype=np.uint8)
|
||||
frame_rgb = frame_array.reshape((self.actual_height, self.actual_width, 3))
|
||||
# Extract width and height from info header (bytes 18-21 and 22-25)
|
||||
width = struct.unpack('<L', header_data[18:22])[0]
|
||||
height = struct.unpack('<L', header_data[22:26])[0]
|
||||
|
||||
# Convert RGB to BGR for OpenCV compatibility
|
||||
frame_bgr = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)
|
||||
# Read remaining file data
|
||||
remaining_size = file_size - 54
|
||||
remaining_data = b''
|
||||
|
||||
return frame_bgr
|
||||
while len(remaining_data) < remaining_size:
|
||||
chunk = pipe.read(remaining_size - len(remaining_data))
|
||||
if not chunk:
|
||||
return None # Stream ended silently
|
||||
remaining_data += chunk
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Camera {self.camera_id}: Error reading raw frame: {e}")
|
||||
return None
|
||||
# Complete BMP data
|
||||
bmp_data = header_data + remaining_data
|
||||
|
||||
# Use OpenCV to decode BMP directly from memory
|
||||
frame_array = np.frombuffer(bmp_data, dtype=np.uint8)
|
||||
frame = cv2.imdecode(frame_array, cv2.IMREAD_COLOR)
|
||||
|
||||
if frame is None:
|
||||
return None # Decode failed silently
|
||||
|
||||
return frame
|
||||
|
||||
except Exception:
|
||||
return None # Error reading frame silently
|
||||
|
||||
def _read_frames(self):
|
||||
"""Read frames directly from FFmpeg stdout pipe."""
|
||||
|
@ -200,51 +190,45 @@ class FFmpegRTSPReader:
|
|||
# Start FFmpeg if not running
|
||||
if not self.process or self.process.poll() is not None:
|
||||
if self.process and self.process.poll() is not None:
|
||||
logger.warning(f"FFmpeg process died for camera {self.camera_id}, restarting...")
|
||||
log_warning(self.camera_id, "Stream disconnected, reconnecting...")
|
||||
|
||||
if not self._start_ffmpeg_process():
|
||||
time.sleep(5.0)
|
||||
continue
|
||||
|
||||
logger.info(f"FFmpeg started for camera {self.camera_id}, reading frames from pipe...")
|
||||
|
||||
# Read frames directly from FFmpeg stdout
|
||||
try:
|
||||
if self.process and self.process.stdout:
|
||||
# Read raw frame data
|
||||
frame = self._read_raw_frame(self.process.stdout)
|
||||
# Read BMP frame data
|
||||
frame = self._read_bmp_frame(self.process.stdout)
|
||||
if frame is None:
|
||||
continue
|
||||
|
||||
# Call frame callback
|
||||
if self.frame_callback:
|
||||
self.frame_callback(self.camera_id, frame)
|
||||
logger.debug(f"Camera {self.camera_id}: Called frame callback with shape {frame.shape}")
|
||||
|
||||
frame_count += 1
|
||||
|
||||
# Log progress
|
||||
# Log progress every 60 seconds (quieter)
|
||||
current_time = time.time()
|
||||
if current_time - last_log_time >= 30:
|
||||
logger.info(f"Camera {self.camera_id}: {frame_count} frames processed via pipe")
|
||||
if current_time - last_log_time >= 60:
|
||||
log_success(self.camera_id, f"{frame_count} frames captured ({frame.shape[1]}x{frame.shape[0]})")
|
||||
last_log_time = current_time
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Camera {self.camera_id}: Error reading from pipe: {e}")
|
||||
except Exception:
|
||||
# Process might have died, let it restart on next iteration
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process = None
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Camera {self.camera_id}: Error in pipe frame reading: {e}")
|
||||
except Exception:
|
||||
time.sleep(1.0)
|
||||
|
||||
# Cleanup
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
logger.info(f"FFmpeg pipe reader ended for camera {self.camera_id}")
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue