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:
Binary file not shown.
Binary file not shown.
@@ -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"""
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user