Add 'api/' from commit '14ac229098e65aa643f84e8e17e0c5f1aaf8d639'

git-subtree-dir: api
git-subtree-mainline: 4743f19aef
git-subtree-split: 14ac229098
This commit is contained in:
Alireza Vaezi
2025-08-07 20:57:34 -04:00
146 changed files with 31249 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
"""
Camera module for the USDA Vision Camera System.
This module handles GigE camera discovery, management, monitoring, and recording
using the camera SDK library (mvsdk).
"""
from .manager import CameraManager
from .recorder import CameraRecorder
from .monitor import CameraMonitor
from .streamer import CameraStreamer
__all__ = ["CameraManager", "CameraRecorder", "CameraMonitor", "CameraStreamer"]

View File

@@ -0,0 +1,546 @@
"""
Camera Manager for the USDA Vision Camera System.
This module manages GigE camera discovery, initialization, and coordination
with the recording system based on machine state changes.
"""
import sys
import os
import threading
import logging
from typing import Dict, List, Optional, Tuple, Any
from datetime import datetime
# Add camera SDK to path
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "camera_sdk"))
import mvsdk
from ..core.config import Config, CameraConfig
from ..core.state_manager import StateManager, CameraStatus
from ..core.events import EventSystem, EventType, Event, publish_camera_status_changed
from ..core.timezone_utils import format_filename_timestamp
from .recorder import CameraRecorder
from .monitor import CameraMonitor
from .streamer import CameraStreamer
from .sdk_config import initialize_sdk_with_suppression
class CameraManager:
"""Manages all cameras in the system"""
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem):
self.config = config
self.state_manager = state_manager
self.event_system = event_system
self.logger = logging.getLogger(__name__)
# Initialize SDK early to suppress error messages
initialize_sdk_with_suppression()
# Camera management
self.available_cameras: List[Any] = [] # mvsdk camera device info
self.camera_recorders: Dict[str, CameraRecorder] = {} # camera_name -> recorder
self.camera_streamers: Dict[str, CameraStreamer] = {} # camera_name -> streamer
self.camera_monitor: Optional[CameraMonitor] = None
# Threading
self._lock = threading.RLock()
self.running = False
# Subscribe to machine state changes
self.event_system.subscribe(EventType.MACHINE_STATE_CHANGED, self._on_machine_state_changed)
# Initialize camera discovery
self._discover_cameras()
# Create camera monitor
self.camera_monitor = CameraMonitor(config=config, state_manager=state_manager, event_system=event_system, camera_manager=self)
def start(self) -> bool:
"""Start the camera manager"""
if self.running:
self.logger.warning("Camera manager is already running")
return True
self.logger.info("Starting camera manager...")
self.running = True
# Start camera monitor
if self.camera_monitor:
self.camera_monitor.start()
# Initialize camera recorders
self._initialize_recorders()
# Initialize camera streamers
self._initialize_streamers()
self.logger.info("Camera manager started successfully")
return True
def stop(self) -> None:
"""Stop the camera manager"""
if not self.running:
return
self.logger.info("Stopping camera manager...")
self.running = False
# Stop camera monitor
if self.camera_monitor:
self.camera_monitor.stop()
# Stop all active recordings
with self._lock:
for recorder in self.camera_recorders.values():
if recorder.is_recording():
recorder.stop_recording()
recorder.cleanup()
# Stop all active streaming
with self._lock:
for streamer in self.camera_streamers.values():
if streamer.is_streaming():
streamer.stop_streaming()
self.logger.info("Camera manager stopped")
def _discover_cameras(self) -> None:
"""Discover available GigE cameras"""
try:
self.logger.info("Discovering GigE cameras...")
# Enumerate cameras using mvsdk
device_list = mvsdk.CameraEnumerateDevice()
self.available_cameras = device_list
self.logger.info(f"Found {len(device_list)} camera(s)")
for i, dev_info in enumerate(device_list):
try:
name = dev_info.GetFriendlyName()
port_type = dev_info.GetPortType()
serial = getattr(dev_info, "acSn", "Unknown")
self.logger.info(f" Camera {i}: {name} ({port_type}) - Serial: {serial}")
# Update state manager with discovered camera
camera_name = f"camera{i+1}" # Default naming
self.state_manager.update_camera_status(name=camera_name, status="available", device_info={"friendly_name": name, "port_type": port_type, "serial_number": serial, "device_index": i})
except Exception as e:
self.logger.error(f"Error processing camera {i}: {e}")
except Exception as e:
self.logger.error(f"Error discovering cameras: {e}")
self.available_cameras = []
def _initialize_recorders(self) -> None:
"""Initialize camera recorders for configured cameras"""
with self._lock:
for camera_config in self.config.cameras:
if not camera_config.enabled:
continue
try:
# Find matching physical camera
device_info = self._find_camera_device(camera_config.name)
if device_info is None:
self.logger.warning(f"No physical camera found for configured camera: {camera_config.name}")
# Update state to indicate camera is not available
self.state_manager.update_camera_status(name=camera_config.name, status="not_found", device_info=None)
continue
# Create recorder (uses lazy initialization - camera will be initialized when recording starts)
recorder = CameraRecorder(camera_config=camera_config, device_info=device_info, state_manager=self.state_manager, event_system=self.event_system)
# Add recorder to the list (camera will be initialized lazily when needed)
self.camera_recorders[camera_config.name] = recorder
self.logger.info(f"Successfully created recorder for camera: {camera_config.name} (lazy initialization)")
except Exception as e:
self.logger.error(f"Error initializing recorder for {camera_config.name}: {e}")
# Update state to indicate error
self.state_manager.update_camera_status(name=camera_config.name, status="error", device_info={"error": str(e)})
def _find_camera_device(self, camera_name: str) -> Optional[Any]:
"""Find physical camera device for a configured camera"""
# For now, use simple mapping: camera1 -> device 0, camera2 -> device 1, etc.
# This could be enhanced to use serial numbers or other identifiers
camera_index_map = {"camera1": 0, "camera2": 1, "camera3": 2, "camera4": 3}
device_index = camera_index_map.get(camera_name)
if device_index is not None and device_index < len(self.available_cameras):
return self.available_cameras[device_index]
return None
def _on_machine_state_changed(self, event: Event) -> None:
"""Handle machine state change events"""
try:
machine_name = event.data.get("machine_name")
new_state = event.data.get("state")
if not machine_name or not new_state:
return
self.logger.info(f"Handling machine state change: {machine_name} -> {new_state}")
# Find camera associated with this machine
camera_config = None
for config in self.config.cameras:
if config.machine_topic == machine_name:
camera_config = config
break
if not camera_config:
self.logger.warning(f"No camera configured for machine: {machine_name}")
return
# Get the recorder for this camera
recorder = self.camera_recorders.get(camera_config.name)
if not recorder:
self.logger.warning(f"No recorder found for camera: {camera_config.name}")
return
# Handle state change
if new_state == "on":
self._start_recording(camera_config.name, recorder)
elif new_state in ["off", "error"]:
self._stop_recording(camera_config.name, recorder)
except Exception as e:
self.logger.error(f"Error handling machine state change: {e}")
def _start_recording(self, camera_name: str, recorder: CameraRecorder) -> None:
"""Start recording for a camera"""
try:
if recorder.is_recording():
self.logger.info(f"Camera {camera_name} is already recording")
return
# Generate filename with Atlanta timezone timestamp
timestamp = format_filename_timestamp()
camera_config = self.config.get_camera_by_name(camera_name)
video_format = camera_config.video_format if camera_config else "mp4"
filename = f"{camera_name}_recording_{timestamp}.{video_format}"
# Start recording
success = recorder.start_recording(filename)
if success:
self.logger.info(f"Started recording for camera {camera_name}: {filename}")
else:
self.logger.error(f"Failed to start recording for camera {camera_name}")
except Exception as e:
self.logger.error(f"Error starting recording for {camera_name}: {e}")
def _stop_recording(self, camera_name: str, recorder: CameraRecorder) -> None:
"""Stop recording for a camera"""
try:
if not recorder.is_recording():
self.logger.info(f"Camera {camera_name} is not recording")
return
# Stop recording
success = recorder.stop_recording()
if success:
self.logger.info(f"Stopped recording for camera {camera_name}")
else:
self.logger.error(f"Failed to stop recording for camera {camera_name}")
except Exception as e:
self.logger.error(f"Error stopping recording for {camera_name}: {e}")
def get_camera_status(self, camera_name: str) -> Optional[Dict[str, Any]]:
"""Get status of a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
return None
return recorder.get_status()
def get_all_camera_status(self) -> Dict[str, Dict[str, Any]]:
"""Get status of all cameras"""
status = {}
with self._lock:
for camera_name, recorder in self.camera_recorders.items():
status[camera_name] = recorder.get_status()
return status
def manual_start_recording(self, camera_name: str, filename: Optional[str] = None, exposure_ms: Optional[float] = None, gain: Optional[float] = None, fps: Optional[float] = None) -> bool:
"""Manually start recording for a camera with optional camera settings"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
# Update camera settings if provided
if exposure_ms is not None or gain is not None or fps is not None:
settings_updated = recorder.update_camera_settings(exposure_ms=exposure_ms, gain=gain, target_fps=fps)
if not settings_updated:
self.logger.warning(f"Failed to update camera settings for {camera_name}")
# Generate filename with datetime prefix
timestamp = format_filename_timestamp()
camera_config = self.config.get_camera_by_name(camera_name)
video_format = camera_config.video_format if camera_config else "mp4"
if filename:
# Always prepend datetime to the provided filename
filename = f"{timestamp}_{filename}"
else:
filename = f"{camera_name}_manual_{timestamp}.{video_format}"
return recorder.start_recording(filename)
def manual_stop_recording(self, camera_name: str) -> bool:
"""Manually stop recording for a camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
return recorder.stop_recording()
def get_available_cameras(self) -> List[Dict[str, Any]]:
"""Get list of available physical cameras"""
cameras = []
for i, dev_info in enumerate(self.available_cameras):
try:
cameras.append({"index": i, "name": dev_info.GetFriendlyName(), "port_type": dev_info.GetPortType(), "serial_number": getattr(dev_info, "acSn", "Unknown")})
except Exception as e:
self.logger.error(f"Error getting info for camera {i}: {e}")
return cameras
def refresh_camera_discovery(self) -> int:
"""Refresh camera discovery and return number of cameras found"""
self._discover_cameras()
return len(self.available_cameras)
def is_running(self) -> bool:
"""Check if camera manager is running"""
return self.running
def test_camera_connection(self, camera_name: str) -> bool:
"""Test connection for a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
return recorder.test_connection()
def reconnect_camera(self, camera_name: str) -> bool:
"""Attempt to reconnect a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
success = recorder.reconnect()
# Update camera status based on result
if success:
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
else:
self.state_manager.update_camera_status(name=camera_name, status="connection_failed", error="Reconnection failed")
return success
def restart_camera_grab(self, camera_name: str) -> bool:
"""Restart grab process for a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
success = recorder.restart_grab()
# Update camera status based on result
if success:
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
else:
self.state_manager.update_camera_status(name=camera_name, status="grab_failed", error="Grab restart failed")
return success
def reset_camera_timestamp(self, camera_name: str) -> bool:
"""Reset timestamp for a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
return recorder.reset_timestamp()
def full_reset_camera(self, camera_name: str) -> bool:
"""Perform full reset for a specific camera"""
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera not found: {camera_name}")
return False
success = recorder.full_reset()
# Update camera status based on result
if success:
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
else:
self.state_manager.update_camera_status(name=camera_name, status="reset_failed", error="Full reset failed")
return success
def reinitialize_failed_camera(self, camera_name: str) -> bool:
"""Attempt to reinitialize a camera that failed to initialize"""
with self._lock:
# Find the camera config
camera_config = None
for config in self.config.cameras:
if config.name == camera_name:
camera_config = config
break
if not camera_config:
self.logger.error(f"No configuration found for camera: {camera_name}")
return False
if not camera_config.enabled:
self.logger.error(f"Camera {camera_name} is disabled in configuration")
return False
try:
# Remove existing recorder if any
if camera_name in self.camera_recorders:
old_recorder = self.camera_recorders[camera_name]
try:
old_recorder._cleanup_camera()
except:
pass # Ignore cleanup errors
del self.camera_recorders[camera_name]
# Find matching physical camera
device_info = self._find_camera_device(camera_name)
if device_info is None:
self.logger.warning(f"No physical camera found for configured camera: {camera_name}")
self.state_manager.update_camera_status(name=camera_name, status="not_found", device_info=None)
return False
# Create new recorder (uses lazy initialization)
recorder = CameraRecorder(camera_config=camera_config, device_info=device_info, state_manager=self.state_manager, event_system=self.event_system)
# Success - add to recorders (camera will be initialized lazily when needed)
self.camera_recorders[camera_name] = recorder
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
self.logger.info(f"Successfully reinitialized camera recorder: {camera_name} (lazy initialization)")
return True
except Exception as e:
self.logger.error(f"Error reinitializing camera {camera_name}: {e}")
self.state_manager.update_camera_status(name=camera_name, status="error", device_info={"error": str(e)})
return False
def _initialize_streamers(self) -> None:
"""Initialize camera streamers for configured cameras"""
with self._lock:
for camera_config in self.config.cameras:
if not camera_config.enabled:
continue
try:
# Find matching physical camera
device_info = self._find_camera_device(camera_config.name)
if device_info is None:
self.logger.warning(f"No physical camera found for streaming: {camera_config.name}")
continue
# Create streamer
streamer = CameraStreamer(camera_config=camera_config, device_info=device_info, state_manager=self.state_manager, event_system=self.event_system)
# Add streamer to the list
self.camera_streamers[camera_config.name] = streamer
self.logger.info(f"Successfully created streamer for camera: {camera_config.name}")
except Exception as e:
self.logger.error(f"Error initializing streamer for {camera_config.name}: {e}")
def get_camera_streamer(self, camera_name: str) -> Optional[CameraStreamer]:
"""Get camera streamer for a specific camera"""
return self.camera_streamers.get(camera_name)
def start_camera_streaming(self, camera_name: str) -> bool:
"""Start streaming for a specific camera"""
streamer = self.camera_streamers.get(camera_name)
if not streamer:
self.logger.error(f"Camera streamer not found: {camera_name}")
return False
return streamer.start_streaming()
def stop_camera_streaming(self, camera_name: str) -> bool:
"""Stop streaming for a specific camera"""
streamer = self.camera_streamers.get(camera_name)
if not streamer:
self.logger.error(f"Camera streamer not found: {camera_name}")
return False
return streamer.stop_streaming()
def is_camera_streaming(self, camera_name: str) -> bool:
"""Check if a camera is currently streaming"""
streamer = self.camera_streamers.get(camera_name)
if not streamer:
return False
return streamer.is_streaming()
def get_camera_config(self, camera_name: str) -> Optional[CameraConfig]:
"""Get camera configuration"""
return self.config.get_camera_by_name(camera_name)
def update_camera_config(self, camera_name: str, **kwargs) -> bool:
"""Update camera configuration and save to config file"""
try:
# Update the configuration
success = self.config.update_camera_config(camera_name, **kwargs)
if success:
self.logger.info(f"Updated configuration for camera {camera_name}: {kwargs}")
return True
else:
self.logger.error(f"Failed to update configuration for camera {camera_name}")
return False
except Exception as e:
self.logger.error(f"Error updating camera configuration: {e}")
return False
def apply_camera_config(self, camera_name: str) -> bool:
"""Apply current configuration to active camera (requires camera restart)"""
try:
# Get the recorder for this camera
recorder = self.camera_recorders.get(camera_name)
if not recorder:
self.logger.error(f"Camera recorder not found: {camera_name}")
return False
# Stop recording if active
was_recording = recorder.is_recording()
if was_recording:
recorder.stop_recording()
# Reinitialize the camera with new settings
success = self.reinitialize_failed_camera(camera_name)
if success:
self.logger.info(f"Successfully applied configuration to camera {camera_name}")
return True
else:
self.logger.error(f"Failed to apply configuration to camera {camera_name}")
return False
except Exception as e:
self.logger.error(f"Error applying camera configuration: {e}")
return False

View File

@@ -0,0 +1,256 @@
"""
Camera Monitor for the USDA Vision Camera System.
This module monitors camera status and availability at regular intervals.
"""
import sys
import os
import threading
import time
import logging
import contextlib
from typing import Dict, List, Optional, Any
# Add camera SDK to path
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "camera_sdk"))
import mvsdk
from ..core.config import Config
from ..core.state_manager import StateManager, CameraStatus
from ..core.events import EventSystem, publish_camera_status_changed
from .sdk_config import ensure_sdk_initialized
@contextlib.contextmanager
def suppress_camera_errors():
"""Context manager to temporarily suppress camera SDK error output"""
# Save original file descriptors
original_stderr = os.dup(2)
original_stdout = os.dup(1)
try:
# Redirect stderr and stdout to devnull
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, 2) # stderr
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
os.close(devnull)
yield
finally:
# Restore original file descriptors
os.dup2(original_stderr, 2)
os.dup2(original_stdout, 1)
os.close(original_stderr)
os.close(original_stdout)
class CameraMonitor:
"""Monitors camera status and availability"""
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager=None):
self.config = config
self.state_manager = state_manager
self.event_system = event_system
self.camera_manager = camera_manager # Reference to camera manager
self.logger = logging.getLogger(__name__)
# Monitoring settings
self.check_interval = config.system.camera_check_interval_seconds
# Threading
self.running = False
self._thread: Optional[threading.Thread] = None
self._stop_event = threading.Event()
# Status tracking
self.last_check_time: Optional[float] = None
self.check_count = 0
self.error_count = 0
def start(self) -> bool:
"""Start camera monitoring"""
if self.running:
self.logger.warning("Camera monitor is already running")
return True
self.logger.info(f"Starting camera monitor (check interval: {self.check_interval}s)")
self.running = True
self._stop_event.clear()
# Start monitoring thread
self._thread = threading.Thread(target=self._monitoring_loop, daemon=True)
self._thread.start()
return True
def stop(self) -> None:
"""Stop camera monitoring"""
if not self.running:
return
self.logger.info("Stopping camera monitor...")
self.running = False
self._stop_event.set()
if self._thread and self._thread.is_alive():
self._thread.join(timeout=5)
self.logger.info("Camera monitor stopped")
def _monitoring_loop(self) -> None:
"""Main monitoring loop"""
self.logger.info("Camera monitoring loop started")
while self.running and not self._stop_event.is_set():
try:
self.last_check_time = time.time()
self.check_count += 1
# Check all configured cameras
self._check_all_cameras()
# Wait for next check
if self._stop_event.wait(self.check_interval):
break
except Exception as e:
self.error_count += 1
self.logger.error(f"Error in camera monitoring loop: {e}")
# Wait a bit before retrying
if self._stop_event.wait(min(self.check_interval, 10)):
break
self.logger.info("Camera monitoring loop ended")
def _check_all_cameras(self) -> None:
"""Check status of all configured cameras"""
for camera_config in self.config.cameras:
if not camera_config.enabled:
continue
try:
self._check_camera_status(camera_config.name)
except Exception as e:
self.logger.error(f"Error checking camera {camera_config.name}: {e}")
def _check_camera_status(self, camera_name: str) -> None:
"""Check status of a specific camera"""
try:
# Get current status from state manager
current_info = self.state_manager.get_camera_status(camera_name)
# Perform actual camera check
status, details, device_info = self._perform_camera_check(camera_name)
# Update state if changed
old_status = current_info.status.value if current_info else "unknown"
if old_status != status:
self.state_manager.update_camera_status(name=camera_name, status=status, error=details if status == "error" else None, device_info=device_info)
# Publish status change event
publish_camera_status_changed(camera_name=camera_name, status=status, details=details)
self.logger.info(f"Camera {camera_name} status changed: {old_status} -> {status}")
except Exception as e:
self.logger.error(f"Error checking camera {camera_name}: {e}")
# Update to error state
self.state_manager.update_camera_status(name=camera_name, status="error", error=str(e))
def _perform_camera_check(self, camera_name: str) -> tuple[str, str, Optional[Dict[str, Any]]]:
"""Perform actual camera availability check"""
try:
# Get camera device info from camera manager
if not self.camera_manager:
return "error", "Camera manager not available", None
device_info = self.camera_manager._find_camera_device(camera_name)
if not device_info:
return "disconnected", "Camera device not found", None
# Check if camera is already opened by another process
if mvsdk.CameraIsOpened(device_info):
# Camera is opened - check if it's our recorder that's currently recording
recorder = self.camera_manager.camera_recorders.get(camera_name)
if recorder and recorder.hCamera and recorder.recording:
return "available", "Camera recording (in use by system)", self._get_device_info_dict(device_info)
else:
return "busy", "Camera opened by another process", self._get_device_info_dict(device_info)
# Try to initialize camera briefly to test availability
try:
# Ensure SDK is initialized
ensure_sdk_initialized()
# Suppress output to avoid MVCAMAPI error messages during camera testing
with suppress_camera_errors():
hCamera = mvsdk.CameraInit(device_info, -1, -1)
# Quick test - try to get one frame
try:
mvsdk.CameraSetTriggerMode(hCamera, 0)
mvsdk.CameraPlay(hCamera)
# Try to capture with short timeout
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(hCamera, 500)
mvsdk.CameraReleaseImageBuffer(hCamera, pRawData)
# Success - camera is available
mvsdk.CameraUnInit(hCamera)
return "available", "Camera test successful", self._get_device_info_dict(device_info)
except mvsdk.CameraException as e:
mvsdk.CameraUnInit(hCamera)
if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT:
return "available", "Camera available but slow response", self._get_device_info_dict(device_info)
else:
return "error", f"Camera test failed: {e.message}", self._get_device_info_dict(device_info)
except mvsdk.CameraException as e:
return "error", f"Camera initialization failed: {e.message}", self._get_device_info_dict(device_info)
except Exception as e:
return "error", f"Camera check failed: {str(e)}", None
def _get_device_info_dict(self, device_info) -> Dict[str, Any]:
"""Convert device info to dictionary"""
try:
return {"friendly_name": device_info.GetFriendlyName(), "port_type": device_info.GetPortType(), "serial_number": getattr(device_info, "acSn", "Unknown"), "last_checked": time.time()}
except Exception as e:
self.logger.error(f"Error getting device info: {e}")
return {"error": str(e)}
def check_camera_now(self, camera_name: str) -> Dict[str, Any]:
"""Manually check a specific camera status"""
try:
status, details, device_info = self._perform_camera_check(camera_name)
# Update state
self.state_manager.update_camera_status(name=camera_name, status=status, error=details if status == "error" else None, device_info=device_info)
return {"camera_name": camera_name, "status": status, "details": details, "device_info": device_info, "check_time": time.time()}
except Exception as e:
error_msg = f"Manual camera check failed: {e}"
self.logger.error(error_msg)
return {"camera_name": camera_name, "status": "error", "details": error_msg, "device_info": None, "check_time": time.time()}
def check_all_cameras_now(self) -> Dict[str, Dict[str, Any]]:
"""Manually check all cameras"""
results = {}
for camera_config in self.config.cameras:
if camera_config.enabled:
results[camera_config.name] = self.check_camera_now(camera_config.name)
return results
def get_monitoring_stats(self) -> Dict[str, Any]:
"""Get monitoring statistics"""
return {"running": self.running, "check_interval_seconds": self.check_interval, "total_checks": self.check_count, "error_count": self.error_count, "last_check_time": self.last_check_time, "success_rate": (self.check_count - self.error_count) / max(self.check_count, 1) * 100}
def is_running(self) -> bool:
"""Check if monitor is running"""
return self.running

View File

@@ -0,0 +1,915 @@
"""
Camera Recorder for the USDA Vision Camera System.
This module handles video recording from GigE cameras using the camera SDK library (mvsdk).
"""
import sys
import os
import threading
import time
import logging
import cv2
import numpy as np
import contextlib
from typing import Optional, Dict, Any
from datetime import datetime
from pathlib import Path
# Add camera SDK to path
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "camera_sdk"))
import mvsdk
from ..core.config import CameraConfig
from ..core.state_manager import StateManager
from ..core.events import EventSystem, publish_recording_started, publish_recording_stopped, publish_recording_error
from ..core.timezone_utils import now_atlanta, format_filename_timestamp
from .sdk_config import ensure_sdk_initialized
@contextlib.contextmanager
def suppress_camera_errors():
"""Context manager to temporarily suppress camera SDK error output"""
# Save original file descriptors
original_stderr = os.dup(2)
original_stdout = os.dup(1)
try:
# Redirect stderr and stdout to devnull
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, 2) # stderr
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
os.close(devnull)
yield
finally:
# Restore original file descriptors
os.dup2(original_stderr, 2)
os.dup2(original_stdout, 1)
os.close(original_stderr)
os.close(original_stdout)
class CameraRecorder:
"""Handles video recording for a single camera"""
def __init__(self, camera_config: CameraConfig, device_info: Any, state_manager: StateManager, event_system: EventSystem, storage_manager=None):
self.camera_config = camera_config
self.device_info = device_info
self.state_manager = state_manager
self.event_system = event_system
self.storage_manager = storage_manager
self.logger = logging.getLogger(f"{__name__}.{camera_config.name}")
# Camera handle and properties
self.hCamera: Optional[int] = None
self.cap = None
self.monoCamera = False
self.frame_buffer = None
self.frame_buffer_size = 0
# Recording state
self.recording = False
self.video_writer: Optional[cv2.VideoWriter] = None
self.output_filename: Optional[str] = None
self.frame_count = 0
self.start_time: Optional[datetime] = None
# Threading
self._recording_thread: Optional[threading.Thread] = None
self._stop_recording_event = threading.Event()
self._lock = threading.RLock()
# Don't initialize camera immediately - use lazy initialization
# Camera will be initialized when recording starts
self.logger.info(f"Camera recorder created for: {self.camera_config.name} (lazy initialization)")
def _initialize_camera(self) -> bool:
"""Initialize the camera with configured settings"""
try:
self.logger.info(f"Initializing camera: {self.camera_config.name}")
# Ensure SDK is initialized
ensure_sdk_initialized()
# Check if device_info is valid
if self.device_info is None:
self.logger.error("No device info provided for camera initialization")
return False
# Initialize camera (suppress output to avoid MVCAMAPI error messages)
with suppress_camera_errors():
self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1)
self.logger.info("Camera initialized successfully")
# Get camera capabilities
self.cap = mvsdk.CameraGetCapability(self.hCamera)
self.monoCamera = self.cap.sIspCapacity.bMonoSensor != 0
self.logger.info(f"Camera type: {'Monochrome' if self.monoCamera else 'Color'}")
# Set output format based on bit depth configuration
if self.monoCamera:
if self.camera_config.bit_depth == 16:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO16)
elif self.camera_config.bit_depth == 12:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO12)
elif self.camera_config.bit_depth == 10:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO10)
else: # Default to 8-bit
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO8)
else:
if self.camera_config.bit_depth == 16:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_RGB16)
elif self.camera_config.bit_depth == 12:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR12)
elif self.camera_config.bit_depth == 10:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR10)
else: # Default to 8-bit
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR8)
self.logger.info(f"Output format set to {self.camera_config.bit_depth}-bit {'mono' if self.monoCamera else 'color'}")
# Configure camera settings
self._configure_camera_settings()
# Allocate frame buffer based on bit depth
bytes_per_pixel = self._get_bytes_per_pixel()
self.frame_buffer_size = self.cap.sResolutionRange.iWidthMax * self.cap.sResolutionRange.iHeightMax * bytes_per_pixel
self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16)
# Start camera
mvsdk.CameraPlay(self.hCamera)
self.logger.info("Camera started successfully")
return True
except mvsdk.CameraException as e:
error_msg = f"Camera initialization failed({e.error_code}): {e.message}"
if e.error_code == 32774:
error_msg += " - This may indicate the camera is already in use by another process or there's a resource conflict"
self.logger.error(error_msg)
return False
except Exception as e:
self.logger.error(f"Unexpected error during camera initialization: {e}")
return False
def _get_bytes_per_pixel(self) -> int:
"""Calculate bytes per pixel based on camera type and bit depth"""
if self.monoCamera:
# Monochrome camera
if self.camera_config.bit_depth >= 16:
return 2 # 16-bit mono
elif self.camera_config.bit_depth >= 12:
return 2 # 12-bit mono (stored in 16-bit)
elif self.camera_config.bit_depth >= 10:
return 2 # 10-bit mono (stored in 16-bit)
else:
return 1 # 8-bit mono
else:
# Color camera
if self.camera_config.bit_depth >= 16:
return 6 # 16-bit RGB (2 bytes × 3 channels)
elif self.camera_config.bit_depth >= 12:
return 6 # 12-bit RGB (stored as 16-bit)
elif self.camera_config.bit_depth >= 10:
return 6 # 10-bit RGB (stored as 16-bit)
else:
return 3 # 8-bit RGB
def _configure_camera_settings(self) -> None:
"""Configure camera settings from config"""
try:
# Set trigger mode (continuous acquisition)
mvsdk.CameraSetTriggerMode(self.hCamera, 0)
# Set manual exposure
mvsdk.CameraSetAeState(self.hCamera, 0) # Disable auto exposure
exposure_us = int(self.camera_config.exposure_ms * 1000) # Convert ms to microseconds
mvsdk.CameraSetExposureTime(self.hCamera, exposure_us)
# Set analog gain
gain_value = int(self.camera_config.gain * 100) # Convert to camera units
mvsdk.CameraSetAnalogGain(self.hCamera, gain_value)
# Configure image quality settings
self._configure_image_quality()
# Configure noise reduction
self._configure_noise_reduction()
# Configure color settings (for color cameras)
if not self.monoCamera:
self._configure_color_settings()
# Configure advanced settings
self._configure_advanced_settings()
self.logger.info(f"Camera settings configured - Exposure: {exposure_us}μs, Gain: {gain_value}")
except Exception as e:
self.logger.warning(f"Error configuring camera settings: {e}")
def _configure_image_quality(self) -> None:
"""Configure image quality settings"""
try:
# Set sharpness (0-200, default 100)
mvsdk.CameraSetSharpness(self.hCamera, self.camera_config.sharpness)
# Set contrast (0-200, default 100)
mvsdk.CameraSetContrast(self.hCamera, self.camera_config.contrast)
# Set gamma (0-300, default 100)
mvsdk.CameraSetGamma(self.hCamera, self.camera_config.gamma)
# Set saturation for color cameras (0-200, default 100)
if not self.monoCamera:
mvsdk.CameraSetSaturation(self.hCamera, self.camera_config.saturation)
self.logger.info(f"Image quality configured - Sharpness: {self.camera_config.sharpness}, " f"Contrast: {self.camera_config.contrast}, Gamma: {self.camera_config.gamma}")
except Exception as e:
self.logger.warning(f"Error configuring image quality: {e}")
def _configure_noise_reduction(self) -> None:
"""Configure noise reduction settings"""
try:
# Enable/disable basic noise filter
mvsdk.CameraSetNoiseFilter(self.hCamera, self.camera_config.noise_filter_enabled)
# Configure 3D denoising if enabled
if self.camera_config.denoise_3d_enabled:
# Enable 3D denoising with default parameters (3 frames, equal weights)
mvsdk.CameraSetDenoise3DParams(self.hCamera, True, 3, None)
self.logger.info("3D denoising enabled")
else:
mvsdk.CameraSetDenoise3DParams(self.hCamera, False, 2, None)
self.logger.info(f"Noise reduction configured - Filter: {self.camera_config.noise_filter_enabled}, " f"3D Denoise: {self.camera_config.denoise_3d_enabled}")
except Exception as e:
self.logger.warning(f"Error configuring noise reduction: {e}")
def _configure_color_settings(self) -> None:
"""Configure color settings for color cameras"""
try:
# Set white balance mode
mvsdk.CameraSetWbMode(self.hCamera, self.camera_config.auto_white_balance)
# Set color temperature preset if not using auto white balance
if not self.camera_config.auto_white_balance:
mvsdk.CameraSetPresetClrTemp(self.hCamera, self.camera_config.color_temperature_preset)
# Set manual RGB gains for manual white balance
red_gain = int(self.camera_config.wb_red_gain * 100) # Convert to camera units
green_gain = int(self.camera_config.wb_green_gain * 100)
blue_gain = int(self.camera_config.wb_blue_gain * 100)
mvsdk.CameraSetUserClrTempGain(self.hCamera, red_gain, green_gain, blue_gain)
self.logger.info(f"Color settings configured - Auto WB: {self.camera_config.auto_white_balance}, " f"Color Temp Preset: {self.camera_config.color_temperature_preset}, " f"RGB Gains: R={self.camera_config.wb_red_gain}, G={self.camera_config.wb_green_gain}, B={self.camera_config.wb_blue_gain}")
except Exception as e:
self.logger.warning(f"Error configuring color settings: {e}")
def _configure_advanced_settings(self) -> None:
"""Configure advanced camera settings"""
try:
# Set anti-flicker
mvsdk.CameraSetAntiFlick(self.hCamera, self.camera_config.anti_flicker_enabled)
# Set light frequency (0=50Hz, 1=60Hz)
mvsdk.CameraSetLightFrequency(self.hCamera, self.camera_config.light_frequency)
# Configure HDR if enabled (check if HDR functions are available)
try:
if self.camera_config.hdr_enabled:
mvsdk.CameraSetHDR(self.hCamera, 1) # Enable HDR
mvsdk.CameraSetHDRGainMode(self.hCamera, self.camera_config.hdr_gain_mode)
self.logger.info(f"HDR enabled with gain mode: {self.camera_config.hdr_gain_mode}")
else:
mvsdk.CameraSetHDR(self.hCamera, 0) # Disable HDR
except AttributeError:
self.logger.info("HDR functions not available in this SDK version, skipping HDR configuration")
self.logger.info(f"Advanced settings configured - Anti-flicker: {self.camera_config.anti_flicker_enabled}, " f"Light Freq: {self.camera_config.light_frequency}Hz, HDR: {self.camera_config.hdr_enabled}")
except Exception as e:
self.logger.warning(f"Error configuring advanced settings: {e}")
def update_camera_settings(self, exposure_ms: Optional[float] = None, gain: Optional[float] = None, target_fps: Optional[float] = None) -> bool:
"""Update camera settings dynamically"""
if not self.hCamera:
self.logger.error("Camera not initialized")
return False
try:
settings_updated = False
# Update exposure if provided
if exposure_ms is not None:
mvsdk.CameraSetAeState(self.hCamera, 0) # Disable auto exposure
exposure_us = int(exposure_ms * 1000) # Convert ms to microseconds
mvsdk.CameraSetExposureTime(self.hCamera, exposure_us)
self.camera_config.exposure_ms = exposure_ms
self.logger.info(f"Updated exposure time: {exposure_ms}ms")
settings_updated = True
# Update gain if provided
if gain is not None:
gain_value = int(gain * 100) # Convert to camera units
mvsdk.CameraSetAnalogGain(self.hCamera, gain_value)
self.camera_config.gain = gain
self.logger.info(f"Updated gain: {gain}x")
settings_updated = True
# Update target FPS if provided
if target_fps is not None:
self.camera_config.target_fps = target_fps
self.logger.info(f"Updated target FPS: {target_fps}")
settings_updated = True
return settings_updated
except Exception as e:
self.logger.error(f"Error updating camera settings: {e}")
return False
def update_advanced_camera_settings(self, **kwargs) -> bool:
"""Update advanced camera settings dynamically"""
if not self.hCamera:
self.logger.error("Camera not initialized")
return False
try:
settings_updated = False
# Update basic settings
if "exposure_ms" in kwargs and kwargs["exposure_ms"] is not None:
mvsdk.CameraSetAeState(self.hCamera, 0)
exposure_us = int(kwargs["exposure_ms"] * 1000)
mvsdk.CameraSetExposureTime(self.hCamera, exposure_us)
self.camera_config.exposure_ms = kwargs["exposure_ms"]
settings_updated = True
if "gain" in kwargs and kwargs["gain"] is not None:
gain_value = int(kwargs["gain"] * 100)
mvsdk.CameraSetAnalogGain(self.hCamera, gain_value)
self.camera_config.gain = kwargs["gain"]
settings_updated = True
if "target_fps" in kwargs and kwargs["target_fps"] is not None:
self.camera_config.target_fps = kwargs["target_fps"]
settings_updated = True
# Update image quality settings
if "sharpness" in kwargs and kwargs["sharpness"] is not None:
mvsdk.CameraSetSharpness(self.hCamera, kwargs["sharpness"])
self.camera_config.sharpness = kwargs["sharpness"]
settings_updated = True
if "contrast" in kwargs and kwargs["contrast"] is not None:
mvsdk.CameraSetContrast(self.hCamera, kwargs["contrast"])
self.camera_config.contrast = kwargs["contrast"]
settings_updated = True
if "gamma" in kwargs and kwargs["gamma"] is not None:
mvsdk.CameraSetGamma(self.hCamera, kwargs["gamma"])
self.camera_config.gamma = kwargs["gamma"]
settings_updated = True
if "saturation" in kwargs and kwargs["saturation"] is not None and not self.monoCamera:
mvsdk.CameraSetSaturation(self.hCamera, kwargs["saturation"])
self.camera_config.saturation = kwargs["saturation"]
settings_updated = True
# Update noise reduction settings
if "noise_filter_enabled" in kwargs and kwargs["noise_filter_enabled"] is not None:
# Note: Noise filter settings may require camera restart to take effect
self.camera_config.noise_filter_enabled = kwargs["noise_filter_enabled"]
settings_updated = True
if "denoise_3d_enabled" in kwargs and kwargs["denoise_3d_enabled"] is not None:
# Note: 3D denoise settings may require camera restart to take effect
self.camera_config.denoise_3d_enabled = kwargs["denoise_3d_enabled"]
settings_updated = True
# Update color settings (for color cameras)
if not self.monoCamera:
if "auto_white_balance" in kwargs and kwargs["auto_white_balance"] is not None:
mvsdk.CameraSetWbMode(self.hCamera, kwargs["auto_white_balance"])
self.camera_config.auto_white_balance = kwargs["auto_white_balance"]
settings_updated = True
if "color_temperature_preset" in kwargs and kwargs["color_temperature_preset"] is not None:
if not self.camera_config.auto_white_balance:
mvsdk.CameraSetPresetClrTemp(self.hCamera, kwargs["color_temperature_preset"])
self.camera_config.color_temperature_preset = kwargs["color_temperature_preset"]
settings_updated = True
# Update RGB gains for manual white balance
rgb_gains_updated = False
if "wb_red_gain" in kwargs and kwargs["wb_red_gain"] is not None:
self.camera_config.wb_red_gain = kwargs["wb_red_gain"]
rgb_gains_updated = True
settings_updated = True
if "wb_green_gain" in kwargs and kwargs["wb_green_gain"] is not None:
self.camera_config.wb_green_gain = kwargs["wb_green_gain"]
rgb_gains_updated = True
settings_updated = True
if "wb_blue_gain" in kwargs and kwargs["wb_blue_gain"] is not None:
self.camera_config.wb_blue_gain = kwargs["wb_blue_gain"]
rgb_gains_updated = True
settings_updated = True
# Apply RGB gains if any were updated and we're in manual white balance mode
if rgb_gains_updated and not self.camera_config.auto_white_balance:
red_gain = int(self.camera_config.wb_red_gain * 100)
green_gain = int(self.camera_config.wb_green_gain * 100)
blue_gain = int(self.camera_config.wb_blue_gain * 100)
mvsdk.CameraSetUserClrTempGain(self.hCamera, red_gain, green_gain, blue_gain)
# Update advanced settings
if "anti_flicker_enabled" in kwargs and kwargs["anti_flicker_enabled"] is not None:
mvsdk.CameraSetAntiFlick(self.hCamera, kwargs["anti_flicker_enabled"])
self.camera_config.anti_flicker_enabled = kwargs["anti_flicker_enabled"]
settings_updated = True
if "light_frequency" in kwargs and kwargs["light_frequency"] is not None:
mvsdk.CameraSetLightFrequency(self.hCamera, kwargs["light_frequency"])
self.camera_config.light_frequency = kwargs["light_frequency"]
settings_updated = True
# Update HDR settings (if supported)
if "hdr_enabled" in kwargs and kwargs["hdr_enabled"] is not None:
try:
mvsdk.CameraSetHDR(self.hCamera, 1 if kwargs["hdr_enabled"] else 0)
self.camera_config.hdr_enabled = kwargs["hdr_enabled"]
settings_updated = True
except AttributeError:
self.logger.warning("HDR functions not available in this SDK version")
if "hdr_gain_mode" in kwargs and kwargs["hdr_gain_mode"] is not None:
try:
if self.camera_config.hdr_enabled:
mvsdk.CameraSetHDRGainMode(self.hCamera, kwargs["hdr_gain_mode"])
self.camera_config.hdr_gain_mode = kwargs["hdr_gain_mode"]
settings_updated = True
except AttributeError:
self.logger.warning("HDR gain mode functions not available in this SDK version")
if settings_updated:
updated_settings = [k for k, v in kwargs.items() if v is not None]
self.logger.info(f"Updated camera settings: {updated_settings}")
return settings_updated
except Exception as e:
self.logger.error(f"Error updating advanced camera settings: {e}")
return False
def start_recording(self, filename: str) -> bool:
"""Start video recording"""
with self._lock:
if self.recording:
self.logger.warning("Already recording!")
return False
# Initialize camera if not already initialized (lazy initialization)
if not self.hCamera:
self.logger.info("Camera not initialized, initializing now...")
if not self._initialize_camera():
self.logger.error("Failed to initialize camera for recording")
return False
try:
# Prepare output path
output_path = os.path.join(self.camera_config.storage_path, filename)
Path(self.camera_config.storage_path).mkdir(parents=True, exist_ok=True)
# Test camera capture before starting recording
if not self._test_camera_capture():
self.logger.error("Camera capture test failed")
return False
# Initialize recording state
self.output_filename = output_path
self.frame_count = 0
self.start_time = now_atlanta() # Use Atlanta timezone
self._stop_recording_event.clear()
# Start recording thread
self._recording_thread = threading.Thread(target=self._recording_loop, daemon=True)
self._recording_thread.start()
# Update state
self.recording = True
recording_id = self.state_manager.start_recording(self.camera_config.name, output_path)
# Publish event
publish_recording_started(self.camera_config.name, output_path)
self.logger.info(f"Started recording to: {output_path}")
return True
except Exception as e:
self.logger.error(f"Error starting recording: {e}")
publish_recording_error(self.camera_config.name, str(e))
return False
def _test_camera_capture(self) -> bool:
"""Test if camera can capture frames"""
try:
# Try to capture one frame
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 1000) # 1 second timeout
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
return True
except Exception as e:
self.logger.error(f"Camera capture test failed: {e}")
return False
def stop_recording(self) -> bool:
"""Stop video recording"""
with self._lock:
if not self.recording:
self.logger.warning("Not currently recording")
return False
try:
# Signal recording thread to stop
self._stop_recording_event.set()
# Wait for recording thread to finish
if self._recording_thread and self._recording_thread.is_alive():
self._recording_thread.join(timeout=5)
# Update state
self.recording = False
# Calculate duration and file size
duration = 0
file_size = 0
if self.start_time:
duration = (now_atlanta() - self.start_time).total_seconds()
if self.output_filename and os.path.exists(self.output_filename):
file_size = os.path.getsize(self.output_filename)
# Update state manager
if self.output_filename:
self.state_manager.stop_recording(self.output_filename, file_size, self.frame_count)
# Publish event
publish_recording_stopped(self.camera_config.name, self.output_filename or "unknown", duration)
# Clean up camera resources after recording (lazy cleanup)
self._cleanup_camera()
self.logger.info("Camera resources cleaned up after recording")
self.logger.info(f"Stopped recording - Duration: {duration:.1f}s, Frames: {self.frame_count}")
return True
except Exception as e:
self.logger.error(f"Error stopping recording: {e}")
return False
def _recording_loop(self) -> None:
"""Main recording loop running in separate thread"""
try:
# Initialize video writer
if not self._initialize_video_writer():
self.logger.error("Failed to initialize video writer")
return
self.logger.info("Recording loop started")
while not self._stop_recording_event.is_set():
try:
# Capture frame
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 200) # 200ms timeout
# Process frame
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
# Convert to OpenCV format
frame = self._convert_frame_to_opencv(FrameHead)
# Write frame to video
if frame is not None and self.video_writer:
self.video_writer.write(frame)
self.frame_count += 1
# Release buffer
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
# Control frame rate (skip sleep if target_fps is 0 for maximum speed)
if self.camera_config.target_fps > 0:
time.sleep(1.0 / self.camera_config.target_fps)
except mvsdk.CameraException as e:
if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT:
continue # Timeout is normal, continue
else:
self.logger.error(f"Camera error during recording: {e.message}")
break
except Exception as e:
self.logger.error(f"Error in recording loop: {e}")
break
self.logger.info("Recording loop ended")
except Exception as e:
self.logger.error(f"Fatal error in recording loop: {e}")
publish_recording_error(self.camera_config.name, str(e))
finally:
self._cleanup_recording()
def _initialize_video_writer(self) -> bool:
"""Initialize OpenCV video writer"""
try:
# Get frame dimensions by capturing a test frame
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 1000)
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
# Set up video writer with configured codec
fourcc = cv2.VideoWriter_fourcc(*self.camera_config.video_codec)
frame_size = (FrameHead.iWidth, FrameHead.iHeight)
# Use 30 FPS for video writer if target_fps is 0 (unlimited)
video_fps = self.camera_config.target_fps if self.camera_config.target_fps > 0 else 30.0
# Create video writer with quality settings
self.video_writer = cv2.VideoWriter(self.output_filename, fourcc, video_fps, frame_size)
# Set quality if supported (for some codecs)
if hasattr(self.video_writer, "set") and self.camera_config.video_quality:
try:
self.video_writer.set(cv2.VIDEOWRITER_PROP_QUALITY, self.camera_config.video_quality)
except:
pass # Quality setting not supported for this codec
if not self.video_writer.isOpened():
self.logger.error(f"Failed to open video writer for {self.output_filename}")
return False
self.logger.info(f"Video writer initialized - Size: {frame_size}, FPS: {self.camera_config.target_fps}")
return True
except Exception as e:
self.logger.error(f"Error initializing video writer: {e}")
return False
def _convert_frame_to_opencv(self, frame_head) -> Optional[np.ndarray]:
"""Convert camera frame to OpenCV format"""
try:
# Convert the frame buffer memory address to a proper buffer
# that numpy can work with using mvsdk.c_ubyte
frame_data_buffer = (mvsdk.c_ubyte * frame_head.uBytes).from_address(self.frame_buffer)
# Handle different bit depths
if self.camera_config.bit_depth > 8:
# For >8-bit, data is stored as 16-bit values
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint16)
if self.monoCamera:
# Monochrome camera - convert to 8-bit BGR for video
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
# Scale down to 8-bit (simple right shift)
frame_8bit = (frame >> (self.camera_config.bit_depth - 8)).astype(np.uint8)
frame_bgr = cv2.cvtColor(frame_8bit, cv2.COLOR_GRAY2BGR)
else:
# Color camera - convert to 8-bit BGR
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
# Scale down to 8-bit
frame_bgr = (frame >> (self.camera_config.bit_depth - 8)).astype(np.uint8)
else:
# 8-bit data
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint8)
if self.monoCamera:
# Monochrome camera - convert to BGR
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
else:
# Color camera - already in BGR format
frame_bgr = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
return frame_bgr
except Exception as e:
self.logger.error(f"Error converting frame: {e}")
return None
def _cleanup_recording(self) -> None:
"""Clean up recording resources"""
try:
if self.video_writer:
self.video_writer.release()
self.video_writer = None
self.recording = False
except Exception as e:
self.logger.error(f"Error during recording cleanup: {e}")
def test_connection(self) -> bool:
"""Test camera connection"""
try:
if self.hCamera is None:
self.logger.error("Camera not initialized")
return False
# Test connection using SDK function
result = mvsdk.CameraConnectTest(self.hCamera)
if result == 0: # CAMERA_STATUS_SUCCESS
self.logger.info("Camera connection test passed")
return True
else:
self.logger.error(f"Camera connection test failed with code: {result}")
return False
except Exception as e:
self.logger.error(f"Error testing camera connection: {e}")
return False
def reconnect(self) -> bool:
"""Attempt to reconnect to the camera"""
try:
if self.hCamera is None:
self.logger.error("Camera not initialized, cannot reconnect")
return False
self.logger.info("Attempting to reconnect camera...")
# Stop any ongoing operations
if self.recording:
self.logger.info("Stopping recording before reconnect")
self.stop_recording()
# Attempt reconnection using SDK function
result = mvsdk.CameraReConnect(self.hCamera)
if result == 0: # CAMERA_STATUS_SUCCESS
self.logger.info("Camera reconnected successfully")
# Restart camera if it was playing
try:
mvsdk.CameraPlay(self.hCamera)
self.logger.info("Camera restarted after reconnection")
except Exception as e:
self.logger.warning(f"Failed to restart camera after reconnection: {e}")
return True
else:
self.logger.error(f"Camera reconnection failed with code: {result}")
return False
except Exception as e:
self.logger.error(f"Error during camera reconnection: {e}")
return False
def restart_grab(self) -> bool:
"""Restart the camera grab process"""
try:
if self.hCamera is None:
self.logger.error("Camera not initialized")
return False
self.logger.info("Restarting camera grab process...")
# Stop any ongoing recording
if self.recording:
self.logger.info("Stopping recording before restart")
self.stop_recording()
# Restart grab using SDK function
result = mvsdk.CameraRestartGrab(self.hCamera)
if result == 0: # CAMERA_STATUS_SUCCESS
self.logger.info("Camera grab restarted successfully")
return True
else:
self.logger.error(f"Camera grab restart failed with code: {result}")
return False
except Exception as e:
self.logger.error(f"Error restarting camera grab: {e}")
return False
def reset_timestamp(self) -> bool:
"""Reset camera timestamp"""
try:
if self.hCamera is None:
self.logger.error("Camera not initialized")
return False
self.logger.info("Resetting camera timestamp...")
result = mvsdk.CameraRstTimeStamp(self.hCamera)
if result == 0: # CAMERA_STATUS_SUCCESS
self.logger.info("Camera timestamp reset successfully")
return True
else:
self.logger.error(f"Camera timestamp reset failed with code: {result}")
return False
except Exception as e:
self.logger.error(f"Error resetting camera timestamp: {e}")
return False
def full_reset(self) -> bool:
"""Perform a full camera reset (uninitialize and reinitialize)"""
try:
self.logger.info("Performing full camera reset...")
# Stop any ongoing recording
if self.recording:
self.logger.info("Stopping recording before reset")
self.stop_recording()
# Store device info for reinitialization
device_info = self.device_info
# Cleanup current camera
self._cleanup_camera()
# Wait a moment
time.sleep(1)
# Reinitialize camera
self.device_info = device_info
success = self._initialize_camera()
if success:
self.logger.info("Full camera reset completed successfully")
return True
else:
self.logger.error("Full camera reset failed during reinitialization")
return False
except Exception as e:
self.logger.error(f"Error during full camera reset: {e}")
return False
def _cleanup_camera(self) -> None:
"""Clean up camera resources"""
try:
# Stop camera if running
if self.hCamera is not None:
try:
mvsdk.CameraStop(self.hCamera)
except:
pass # Ignore errors during stop
# Uninitialize camera
try:
mvsdk.CameraUnInit(self.hCamera)
except:
pass # Ignore errors during uninit
self.hCamera = None
# Free frame buffer
if self.frame_buffer is not None:
try:
mvsdk.CameraAlignFree(self.frame_buffer)
except:
pass # Ignore errors during free
self.frame_buffer = None
self.logger.info("Camera resources cleaned up")
except Exception as e:
self.logger.error(f"Error during camera cleanup: {e}")
def cleanup(self) -> None:
"""Clean up camera resources"""
try:
# Stop recording if active
if self.recording:
self.stop_recording()
# Clean up camera
if self.hCamera:
mvsdk.CameraUnInit(self.hCamera)
self.hCamera = None
# Free frame buffer
if self.frame_buffer:
mvsdk.CameraAlignFree(self.frame_buffer)
self.frame_buffer = None
self.logger.info("Camera resources cleaned up")
except Exception as e:
self.logger.error(f"Error during cleanup: {e}")
def is_recording(self) -> bool:
"""Check if currently recording"""
return self.recording
def get_status(self) -> Dict[str, Any]:
"""Get recorder status"""
return {"camera_name": self.camera_config.name, "is_recording": self.recording, "current_file": self.output_filename, "frame_count": self.frame_count, "start_time": self.start_time.isoformat() if self.start_time else None, "camera_initialized": self.hCamera is not None, "storage_path": self.camera_config.storage_path}

View File

@@ -0,0 +1,89 @@
"""
SDK Configuration for the USDA Vision Camera System.
This module handles SDK initialization and configuration to suppress error messages.
"""
import sys
import os
import logging
# Add camera SDK to path
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "camera_sdk"))
import mvsdk
logger = logging.getLogger(__name__)
# Global flag to track SDK initialization
_sdk_initialized = False
def initialize_sdk_with_suppression():
"""Initialize the camera SDK with error suppression"""
global _sdk_initialized
if _sdk_initialized:
return True
try:
# Initialize SDK with English language
result = mvsdk.CameraSdkInit(1)
if result == 0:
logger.info("Camera SDK initialized successfully")
# Try to set system options to suppress logging
try:
# These are common options that might control logging
# We'll try them and ignore failures since they might not be supported
# Try to disable debug output
try:
mvsdk.CameraSetSysOption("DebugLevel", "0")
except:
pass
# Try to disable console output
try:
mvsdk.CameraSetSysOption("ConsoleOutput", "0")
except:
pass
# Try to disable error logging
try:
mvsdk.CameraSetSysOption("ErrorLog", "0")
except:
pass
# Try to set log level to none
try:
mvsdk.CameraSetSysOption("LogLevel", "0")
except:
pass
# Try to disable verbose mode
try:
mvsdk.CameraSetSysOption("Verbose", "0")
except:
pass
logger.debug("Attempted to configure SDK logging options")
except Exception as e:
logger.debug(f"Could not configure SDK logging options: {e}")
_sdk_initialized = True
return True
else:
logger.error(f"SDK initialization failed with code: {result}")
return False
except Exception as e:
logger.error(f"SDK initialization failed: {e}")
return False
def ensure_sdk_initialized():
"""Ensure the SDK is initialized before camera operations"""
if not _sdk_initialized:
return initialize_sdk_with_suppression()
return True

View File

@@ -0,0 +1,412 @@
"""
Camera Streamer for the USDA Vision Camera System.
This module provides live preview streaming from GigE cameras without blocking recording.
It creates a separate camera connection for streaming that doesn't interfere with recording.
"""
import sys
import os
import threading
import time
import logging
import cv2
import numpy as np
import contextlib
from typing import Optional, Dict, Any, Generator
from datetime import datetime
import queue
# Add camera SDK to path
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "camera_sdk"))
import mvsdk
from ..core.config import CameraConfig
from ..core.state_manager import StateManager
from ..core.events import EventSystem
from .sdk_config import ensure_sdk_initialized
@contextlib.contextmanager
def suppress_camera_errors():
"""Context manager to temporarily suppress camera SDK error output"""
# Save original file descriptors
original_stderr = os.dup(2)
original_stdout = os.dup(1)
try:
# Redirect stderr and stdout to devnull
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, 2) # stderr
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
os.close(devnull)
yield
finally:
# Restore original file descriptors
os.dup2(original_stderr, 2)
os.dup2(original_stdout, 1)
os.close(original_stderr)
os.close(original_stdout)
class CameraStreamer:
"""Provides live preview streaming from cameras without blocking recording"""
def __init__(self, camera_config: CameraConfig, device_info: Any, state_manager: StateManager, event_system: EventSystem):
self.camera_config = camera_config
self.device_info = device_info
self.state_manager = state_manager
self.event_system = event_system
self.logger = logging.getLogger(f"{__name__}.{camera_config.name}")
# Camera handle and properties (separate from recorder)
self.hCamera: Optional[int] = None
self.cap = None
self.monoCamera = False
self.frame_buffer = None
self.frame_buffer_size = 0
# Streaming state
self.streaming = False
self._streaming_thread: Optional[threading.Thread] = None
self._stop_streaming_event = threading.Event()
self._frame_queue = queue.Queue(maxsize=5) # Buffer for latest frames
self._lock = threading.RLock()
# Stream settings (optimized for preview)
self.preview_fps = 10.0 # Lower FPS for preview to reduce load
self.preview_quality = 70 # JPEG quality for streaming
def start_streaming(self) -> bool:
"""Start streaming preview frames"""
with self._lock:
if self.streaming:
self.logger.warning("Streaming already active")
return True
try:
# Initialize camera for streaming
if not self._initialize_camera():
return False
# Start streaming thread
self._stop_streaming_event.clear()
self._streaming_thread = threading.Thread(target=self._streaming_loop, daemon=True)
self._streaming_thread.start()
self.streaming = True
self.logger.info(f"Started streaming for camera: {self.camera_config.name}")
return True
except Exception as e:
self.logger.error(f"Error starting streaming: {e}")
self._cleanup_camera()
return False
def stop_streaming(self) -> bool:
"""Stop streaming preview frames"""
with self._lock:
if not self.streaming:
return True
try:
# Signal streaming thread to stop
self._stop_streaming_event.set()
# Wait for thread to finish
if self._streaming_thread and self._streaming_thread.is_alive():
self._streaming_thread.join(timeout=5.0)
# Cleanup camera resources
self._cleanup_camera()
self.streaming = False
self.logger.info(f"Stopped streaming for camera: {self.camera_config.name}")
return True
except Exception as e:
self.logger.error(f"Error stopping streaming: {e}")
return False
def get_latest_frame(self) -> Optional[bytes]:
"""Get the latest frame as JPEG bytes for streaming"""
try:
# Get latest frame from queue (non-blocking)
frame = self._frame_queue.get_nowait()
# Encode as JPEG
_, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, self.preview_quality])
return buffer.tobytes()
except queue.Empty:
return None
except Exception as e:
self.logger.error(f"Error getting latest frame: {e}")
return None
def get_frame_generator(self) -> Generator[bytes, None, None]:
"""Generator for MJPEG streaming"""
while self.streaming:
frame_bytes = self.get_latest_frame()
if frame_bytes:
yield (b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + frame_bytes + b"\r\n")
else:
time.sleep(0.1) # Wait a bit if no frame available
def _initialize_camera(self) -> bool:
"""Initialize camera for streaming (separate from recording)"""
try:
self.logger.info(f"Initializing camera for streaming: {self.camera_config.name}")
# Ensure SDK is initialized
ensure_sdk_initialized()
# Check if device_info is valid
if self.device_info is None:
self.logger.error("No device info provided for camera initialization")
return False
# Initialize camera (suppress output to avoid MVCAMAPI error messages)
with suppress_camera_errors():
self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1)
self.logger.info("Camera initialized successfully for streaming")
# Get camera capabilities
self.cap = mvsdk.CameraGetCapability(self.hCamera)
# Determine if camera is monochrome
self.monoCamera = self.cap.sIspCapacity.bMonoSensor != 0
# Set output format based on camera type and bit depth
if self.monoCamera:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_MONO8)
else:
mvsdk.CameraSetIspOutFormat(self.hCamera, mvsdk.CAMERA_MEDIA_TYPE_BGR8)
# Configure camera settings for streaming (optimized for preview)
self._configure_streaming_settings()
# Allocate frame buffer
bytes_per_pixel = 1 if self.monoCamera else 3
self.frame_buffer_size = self.cap.sResolutionRange.iWidthMax * self.cap.sResolutionRange.iHeightMax * bytes_per_pixel
self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16)
# Start camera
mvsdk.CameraPlay(self.hCamera)
self.logger.info("Camera started successfully for streaming")
return True
except Exception as e:
self.logger.error(f"Error initializing camera for streaming: {e}")
self._cleanup_camera()
return False
def _configure_streaming_settings(self):
"""Configure camera settings from config.json for streaming"""
try:
# Set trigger mode to free run for continuous streaming
mvsdk.CameraSetTriggerMode(self.hCamera, 0)
# Set manual exposure
mvsdk.CameraSetAeState(self.hCamera, 0) # Disable auto exposure
exposure_us = int(self.camera_config.exposure_ms * 1000) # Convert ms to microseconds
mvsdk.CameraSetExposureTime(self.hCamera, exposure_us)
# Set analog gain
gain_value = int(self.camera_config.gain * 100) # Convert to camera units
mvsdk.CameraSetAnalogGain(self.hCamera, gain_value)
# Set frame rate for streaming (lower than recording)
if hasattr(mvsdk, "CameraSetFrameSpeed"):
mvsdk.CameraSetFrameSpeed(self.hCamera, int(self.preview_fps))
# Configure image quality settings
self._configure_image_quality()
# Configure noise reduction
self._configure_noise_reduction()
# Configure color settings (for color cameras)
if not self.monoCamera:
self._configure_color_settings()
# Configure advanced settings
self._configure_advanced_settings()
self.logger.info(f"Streaming settings configured: exposure={self.camera_config.exposure_ms}ms, gain={self.camera_config.gain}, fps={self.preview_fps}")
except Exception as e:
self.logger.warning(f"Could not configure some streaming settings: {e}")
def _streaming_loop(self):
"""Main streaming loop that captures frames continuously"""
self.logger.info("Starting streaming loop")
try:
while not self._stop_streaming_event.is_set():
try:
# Capture frame with timeout
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(self.hCamera, 200) # 200ms timeout
# Process frame
mvsdk.CameraImageProcess(self.hCamera, pRawData, self.frame_buffer, FrameHead)
# Convert to OpenCV format
frame = self._convert_frame_to_opencv(FrameHead)
if frame is not None:
# Add frame to queue (replace oldest if queue is full)
try:
self._frame_queue.put_nowait(frame)
except queue.Full:
# Remove oldest frame and add new one
try:
self._frame_queue.get_nowait()
self._frame_queue.put_nowait(frame)
except queue.Empty:
pass
# Release buffer
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
# Control frame rate
time.sleep(1.0 / self.preview_fps)
except Exception as e:
if not self._stop_streaming_event.is_set():
self.logger.error(f"Error in streaming loop: {e}")
time.sleep(0.1) # Brief pause before retrying
except Exception as e:
self.logger.error(f"Fatal error in streaming loop: {e}")
finally:
self.logger.info("Streaming loop ended")
def _convert_frame_to_opencv(self, FrameHead) -> Optional[np.ndarray]:
"""Convert camera frame to OpenCV format"""
try:
# Convert the frame buffer memory address to a proper buffer
# that numpy can work with using mvsdk.c_ubyte
frame_data_buffer = (mvsdk.c_ubyte * FrameHead.uBytes).from_address(self.frame_buffer)
if self.monoCamera:
# Monochrome camera
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint8)
frame = frame_data.reshape((FrameHead.iHeight, FrameHead.iWidth))
# Convert to 3-channel for consistency
frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
else:
# Color camera (BGR format)
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint8)
frame = frame_data.reshape((FrameHead.iHeight, FrameHead.iWidth, 3))
return frame
except Exception as e:
self.logger.error(f"Error converting frame: {e}")
return None
def _cleanup_camera(self):
"""Clean up camera resources"""
try:
if self.frame_buffer:
mvsdk.CameraAlignFree(self.frame_buffer)
self.frame_buffer = None
if self.hCamera is not None:
mvsdk.CameraUnInit(self.hCamera)
self.hCamera = None
self.logger.info("Camera resources cleaned up for streaming")
except Exception as e:
self.logger.error(f"Error cleaning up camera resources: {e}")
def is_streaming(self) -> bool:
"""Check if streaming is active"""
return self.streaming
def _configure_image_quality(self) -> None:
"""Configure image quality settings"""
try:
# Set sharpness (0-200, default 100)
mvsdk.CameraSetSharpness(self.hCamera, self.camera_config.sharpness)
# Set contrast (0-200, default 100)
mvsdk.CameraSetContrast(self.hCamera, self.camera_config.contrast)
# Set gamma (0-300, default 100)
mvsdk.CameraSetGamma(self.hCamera, self.camera_config.gamma)
# Set saturation for color cameras (0-200, default 100)
if not self.monoCamera:
mvsdk.CameraSetSaturation(self.hCamera, self.camera_config.saturation)
self.logger.info(f"Image quality configured - Sharpness: {self.camera_config.sharpness}, " f"Contrast: {self.camera_config.contrast}, Gamma: {self.camera_config.gamma}")
except Exception as e:
self.logger.warning(f"Error configuring image quality: {e}")
def _configure_noise_reduction(self) -> None:
"""Configure noise reduction settings"""
try:
# Note: Some noise reduction settings may require specific SDK functions
# that might not be available in all SDK versions
self.logger.info(f"Noise reduction configured - Filter: {self.camera_config.noise_filter_enabled}, " f"3D Denoise: {self.camera_config.denoise_3d_enabled}")
except Exception as e:
self.logger.warning(f"Error configuring noise reduction: {e}")
def _configure_color_settings(self) -> None:
"""Configure color settings for color cameras"""
try:
# Set white balance mode
mvsdk.CameraSetWbMode(self.hCamera, self.camera_config.auto_white_balance)
# Set color temperature preset if not using auto white balance
if not self.camera_config.auto_white_balance:
mvsdk.CameraSetPresetClrTemp(self.hCamera, self.camera_config.color_temperature_preset)
# Set manual RGB gains for manual white balance
red_gain = int(self.camera_config.wb_red_gain * 100) # Convert to camera units
green_gain = int(self.camera_config.wb_green_gain * 100)
blue_gain = int(self.camera_config.wb_blue_gain * 100)
mvsdk.CameraSetUserClrTempGain(self.hCamera, red_gain, green_gain, blue_gain)
self.logger.info(f"Color settings configured - Auto WB: {self.camera_config.auto_white_balance}, " f"Color Temp Preset: {self.camera_config.color_temperature_preset}, " f"RGB Gains: R={self.camera_config.wb_red_gain}, G={self.camera_config.wb_green_gain}, B={self.camera_config.wb_blue_gain}")
except Exception as e:
self.logger.warning(f"Error configuring color settings: {e}")
def _configure_advanced_settings(self) -> None:
"""Configure advanced camera settings"""
try:
# Set anti-flicker
mvsdk.CameraSetAntiFlick(self.hCamera, self.camera_config.anti_flicker_enabled)
# Set light frequency (0=50Hz, 1=60Hz)
mvsdk.CameraSetLightFrequency(self.hCamera, self.camera_config.light_frequency)
# Configure HDR if enabled (check if HDR functions are available)
try:
if self.camera_config.hdr_enabled:
mvsdk.CameraSetHDR(self.hCamera, 1) # Enable HDR
mvsdk.CameraSetHDRGainMode(self.hCamera, self.camera_config.hdr_gain_mode)
self.logger.info(f"HDR enabled with gain mode: {self.camera_config.hdr_gain_mode}")
else:
mvsdk.CameraSetHDR(self.hCamera, 0) # Disable HDR
except AttributeError:
self.logger.info("HDR functions not available in this SDK version, skipping HDR configuration")
self.logger.info(f"Advanced settings configured - Anti-flicker: {self.camera_config.anti_flicker_enabled}, " f"Light Freq: {self.camera_config.light_frequency}Hz, HDR: {self.camera_config.hdr_enabled}")
except Exception as e:
self.logger.warning(f"Error configuring advanced settings: {e}")
def __del__(self):
"""Destructor to ensure cleanup"""
if self.streaming:
self.stop_streaming()