Add USDA Vision Camera Streaming API and related functionality

- Implemented streaming API endpoints for starting, stopping, and retrieving live streams from cameras.
- Added support for concurrent streaming and recording operations.
- Created test scripts for frame conversion and streaming functionality.
- Developed a CameraStreamer class to manage live preview streaming without blocking recording.
- Included error handling and logging for camera operations.
- Added configuration endpoints for camera settings and real-time updates.
- Enhanced testing scenarios for various camera configurations and error handling.
This commit is contained in:
Alireza Vaezi
2025-07-28 18:09:48 -04:00
parent 7bc8138f24
commit ef0f9f85c5
20 changed files with 3594 additions and 4 deletions

View File

@@ -81,6 +81,74 @@ class StartRecordingRequest(BaseModel):
fps: Optional[float] = Field(default=None, description="Target frames per second")
class CameraConfigRequest(BaseModel):
"""Camera configuration update request model"""
# Basic settings
exposure_ms: Optional[float] = Field(default=None, ge=0.1, le=1000.0, description="Exposure time in milliseconds")
gain: Optional[float] = Field(default=None, ge=0.0, le=20.0, description="Camera gain value")
target_fps: Optional[float] = Field(default=None, ge=0.0, le=120.0, description="Target frames per second")
# Image Quality Settings
sharpness: Optional[int] = Field(default=None, ge=0, le=200, description="Sharpness (0-200, default 100)")
contrast: Optional[int] = Field(default=None, ge=0, le=200, description="Contrast (0-200, default 100)")
saturation: Optional[int] = Field(default=None, ge=0, le=200, description="Saturation (0-200, default 100)")
gamma: Optional[int] = Field(default=None, ge=0, le=300, description="Gamma (0-300, default 100)")
# Noise Reduction
noise_filter_enabled: Optional[bool] = Field(default=None, description="Enable basic noise filtering")
denoise_3d_enabled: Optional[bool] = Field(default=None, description="Enable advanced 3D denoising")
# Color Settings (for color cameras)
auto_white_balance: Optional[bool] = Field(default=None, description="Enable automatic white balance")
color_temperature_preset: Optional[int] = Field(default=None, ge=0, le=10, description="Color temperature preset")
# Advanced Settings
anti_flicker_enabled: Optional[bool] = Field(default=None, description="Reduce artificial lighting flicker")
light_frequency: Optional[int] = Field(default=None, ge=0, le=1, description="Light frequency (0=50Hz, 1=60Hz)")
# HDR Settings
hdr_enabled: Optional[bool] = Field(default=None, description="Enable High Dynamic Range")
hdr_gain_mode: Optional[int] = Field(default=None, ge=0, le=3, description="HDR processing mode")
class CameraConfigResponse(BaseModel):
"""Camera configuration response model"""
name: str
machine_topic: str
storage_path: str
enabled: bool
# Basic settings
exposure_ms: float
gain: float
target_fps: float
# Image Quality Settings
sharpness: int
contrast: int
saturation: int
gamma: int
# Noise Reduction
noise_filter_enabled: bool
denoise_3d_enabled: bool
# Color Settings
auto_white_balance: bool
color_temperature_preset: int
# Advanced Settings
anti_flicker_enabled: bool
light_frequency: int
bit_depth: int
# HDR Settings
hdr_enabled: bool
hdr_gain_mode: int
class StartRecordingResponse(BaseModel):
"""Start recording response model"""

View File

@@ -13,7 +13,7 @@ import threading
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, StreamingResponse
import uvicorn
from ..core.config import Config
@@ -243,6 +243,149 @@ class APIServer:
self.logger.error(f"Error testing camera connection: {e}")
raise HTTPException(status_code=500, detail=str(e))
@self.app.get("/cameras/{camera_name}/stream")
async def camera_stream(camera_name: str):
"""Get live MJPEG stream from camera"""
try:
if not self.camera_manager:
raise HTTPException(status_code=503, detail="Camera manager not available")
# Get camera streamer
streamer = self.camera_manager.get_camera_streamer(camera_name)
if not streamer:
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
# Start streaming if not already active
if not streamer.is_streaming():
success = streamer.start_streaming()
if not success:
raise HTTPException(status_code=500, detail=f"Failed to start streaming for camera {camera_name}")
# Return MJPEG stream
return StreamingResponse(streamer.get_frame_generator(), media_type="multipart/x-mixed-replace; boundary=frame")
except HTTPException:
raise
except Exception as e:
self.logger.error(f"Error starting camera stream: {e}")
raise HTTPException(status_code=500, detail=str(e))
@self.app.post("/cameras/{camera_name}/start-stream")
async def start_camera_stream(camera_name: str):
"""Start streaming for a camera"""
try:
if not self.camera_manager:
raise HTTPException(status_code=503, detail="Camera manager not available")
success = self.camera_manager.start_camera_streaming(camera_name)
if success:
return {"success": True, "message": f"Started streaming for camera {camera_name}"}
else:
return {"success": False, "message": f"Failed to start streaming for camera {camera_name}"}
except Exception as e:
self.logger.error(f"Error starting camera stream: {e}")
raise HTTPException(status_code=500, detail=str(e))
@self.app.post("/cameras/{camera_name}/stop-stream")
async def stop_camera_stream(camera_name: str):
"""Stop streaming for a camera"""
try:
if not self.camera_manager:
raise HTTPException(status_code=503, detail="Camera manager not available")
success = self.camera_manager.stop_camera_streaming(camera_name)
if success:
return {"success": True, "message": f"Stopped streaming for camera {camera_name}"}
else:
return {"success": False, "message": f"Failed to stop streaming for camera {camera_name}"}
except Exception as e:
self.logger.error(f"Error stopping camera stream: {e}")
raise HTTPException(status_code=500, detail=str(e))
@self.app.get("/cameras/{camera_name}/config", response_model=CameraConfigResponse)
async def get_camera_config(camera_name: str):
"""Get camera configuration"""
try:
if not self.camera_manager:
raise HTTPException(status_code=503, detail="Camera manager not available")
config = self.camera_manager.get_camera_config(camera_name)
if not config:
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found")
return CameraConfigResponse(
name=config.name,
machine_topic=config.machine_topic,
storage_path=config.storage_path,
enabled=config.enabled,
exposure_ms=config.exposure_ms,
gain=config.gain,
target_fps=config.target_fps,
sharpness=config.sharpness,
contrast=config.contrast,
saturation=config.saturation,
gamma=config.gamma,
noise_filter_enabled=config.noise_filter_enabled,
denoise_3d_enabled=config.denoise_3d_enabled,
auto_white_balance=config.auto_white_balance,
color_temperature_preset=config.color_temperature_preset,
anti_flicker_enabled=config.anti_flicker_enabled,
light_frequency=config.light_frequency,
bit_depth=config.bit_depth,
hdr_enabled=config.hdr_enabled,
hdr_gain_mode=config.hdr_gain_mode,
)
except HTTPException:
raise
except Exception as e:
self.logger.error(f"Error getting camera config: {e}")
raise HTTPException(status_code=500, detail=str(e))
@self.app.put("/cameras/{camera_name}/config")
async def update_camera_config(camera_name: str, request: CameraConfigRequest):
"""Update camera configuration"""
try:
if not self.camera_manager:
raise HTTPException(status_code=503, detail="Camera manager not available")
# Convert request to dict, excluding None values
config_updates = {k: v for k, v in request.dict().items() if v is not None}
if not config_updates:
raise HTTPException(status_code=400, detail="No configuration updates provided")
success = self.camera_manager.update_camera_config(camera_name, **config_updates)
if success:
return {"success": True, "message": f"Camera {camera_name} configuration updated", "updated_settings": list(config_updates.keys())}
else:
raise HTTPException(status_code=404, detail=f"Camera {camera_name} not found or update failed")
except HTTPException:
raise
except Exception as e:
self.logger.error(f"Error updating camera config: {e}")
raise HTTPException(status_code=500, detail=str(e))
@self.app.post("/cameras/{camera_name}/apply-config")
async def apply_camera_config(camera_name: str):
"""Apply current configuration to active camera (requires camera restart)"""
try:
if not self.camera_manager:
raise HTTPException(status_code=503, detail="Camera manager not available")
success = self.camera_manager.apply_camera_config(camera_name)
if success:
return {"success": True, "message": f"Configuration applied to camera {camera_name}"}
else:
return {"success": False, "message": f"Failed to apply configuration to camera {camera_name}"}
except Exception as e:
self.logger.error(f"Error applying camera config: {e}")
raise HTTPException(status_code=500, detail=str(e))
@self.app.post("/cameras/{camera_name}/reconnect", response_model=CameraRecoveryResponse)
async def reconnect_camera(camera_name: str):
"""Reconnect to a camera"""