//! Shared IPC types for the Object Detector demo //! //! This crate defines the data structures shared between the Rust backend //! and the TypeScript frontend for the YOLO-style object detection demo. use serde::{Deserialize, Serialize}; /// Supported YOLO model variants #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[allow(non_camel_case_types)] pub enum DetectorModel { /// YOLOv8 Nano (smallest, fastest) #[serde(rename = "yolov8_n")] YOLOv8_N, /// YOLOv8 Small #[serde(rename = "yolov8_s")] YOLOv8_S, /// YOLOv8 Medium #[serde(rename = "yolov8_m")] YOLOv8_M, /// YOLOv8 Large #[serde(rename = "yolov8_l")] YOLOv8_L, /// YOLOv8 XLarge (largest, most accurate) #[serde(rename = "yolov8_x")] YOLOv8_X, } impl DetectorModel { /// Get human-readable name pub fn display_name(&self) -> &'static str { match self { Self::YOLOv8_N => "YOLOv8-Nano", Self::YOLOv8_S => "YOLOv8-Small", Self::YOLOv8_M => "YOLOv8-Medium", Self::YOLOv8_L => "YOLOv8-Large", Self::YOLOv8_X => "YOLOv8-XLarge", } } /// Get approximate parameter count pub fn param_count(&self) -> usize { match self { Self::YOLOv8_N => 3_200_000, // 3.2M Self::YOLOv8_S => 11_200_000, // 11.2M Self::YOLOv8_M => 25_900_000, // 25.9M Self::YOLOv8_L => 43_700_000, // 43.7M Self::YOLOv8_X => 68_200_000, // 68.2M } } } /// Bounding box in normalized coordinates (0.0-1.0) #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct BoundingBox { /// X coordinate of top-left corner (normalized 0-1) pub x: f32, /// Y coordinate of top-left corner (normalized 0-1) pub y: f32, /// Width (normalized 0-1) pub width: f32, /// Height (normalized 0-1) pub height: f32, } impl BoundingBox { /// Calculate area of bounding box pub fn area(&self) -> f32 { self.width * self.height } /// Calculate center point (cx, cy) pub fn center(&self) -> (f32, f32) { (self.x + self.width / 2.0, self.y + self.height / 2.0) } /// Calculate intersection area with another box pub fn intersection(&self, other: &Self) -> f32 { let x1 = self.x.max(other.x); let y1 = self.y.max(other.y); let x2 = (self.x + self.width).min(other.x + other.width); let y2 = (self.y + self.height).min(other.y + other.height); let width = (x2 - x1).max(0.0); let height = (y2 - y1).max(0.0); width * height } /// Calculate Intersection over Union (IoU) with another box pub fn iou(&self, other: &Self) -> f32 { let intersection = self.intersection(other); let union = self.area() + other.area() - intersection; if union > 0.0 { intersection / union } else { 0.0 } } } /// A single detection result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Detection { /// Bounding box pub bbox: BoundingBox, /// Class ID (0-79 for COCO) pub class_id: usize, /// Human-readable class name pub class_name: String, /// Confidence score (0.0-1.0) pub confidence: f32, /// Color for visualization (hex string) pub color: String, } /// Result of object detection #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DetectionResult { /// List of detections pub detections: Vec, /// Inference time in milliseconds pub inference_time_ms: f64, /// Input image width pub image_width: usize, /// Input image height pub image_height: usize, } /// Configuration for the object detector #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DetectorConfig { /// Model variant to use pub model: DetectorModel, /// Confidence threshold (0.0-1.0) pub confidence_threshold: f32, /// Non-maximum suppression threshold (0.0-1.0) pub nms_threshold: f32, /// Maximum number of detections to return pub max_detections: usize, /// Whether to use GPU if available pub use_gpu: bool, } impl Default for DetectorConfig { fn default() -> Self { Self { model: DetectorModel::YOLOv8_N, confidence_threshold: 0.25, nms_threshold: 0.45, max_detections: 100, use_gpu: true, } } } /// Status of the detector service #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DetectorStatus { /// Whether the model is loaded pub initialized: bool, /// Current model (if loaded) pub model: Option, /// Compute device being used pub device: String, /// Total number of detections performed pub detection_count: u64, /// Average inference time in milliseconds pub avg_inference_time_ms: f64, } /// Request types for detector IPC #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum DetectorRequest { /// Initialize the detector with config Initialize { config: DetectorConfig }, /// Detect objects in an image Detect { image_data: String }, /// Get list of supported classes GetClasses, /// Get detector status GetStatus, } /// Response types for detector IPC #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum DetectorResponse { /// Initialization result Initialized { success: bool }, /// Detection result Detected { result: DetectionResult }, /// List of classes Classes { classes: Vec }, /// Detector status Status { status: DetectorStatus }, /// Error response Error { message: String }, } /// COCO dataset class names (80 classes) pub const COCO_CLASSES: &[&str] = &[ "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush", ]; #[cfg(test)] mod tests { use super::*; #[test] fn test_coco_classes_count() { assert_eq!(COCO_CLASSES.len(), 80); } #[test] fn test_coco_classes_first_is_person() { assert_eq!(COCO_CLASSES[0], "person"); } #[test] fn test_coco_classes_last_is_toothbrush() { assert_eq!(COCO_CLASSES[79], "toothbrush"); } #[test] fn test_coco_classes_contains_common_objects() { assert!(COCO_CLASSES.contains(&"car")); assert!(COCO_CLASSES.contains(&"dog")); assert!(COCO_CLASSES.contains(&"cat")); } #[test] fn test_detector_model_serialization() { let model = DetectorModel::YOLOv8_N; let json = serde_json::to_string(&model).expect("failed to serialize"); assert_eq!(json, "\"yolov8_n\""); } #[test] fn test_detector_model_deserialization() { let json = "\"yolov8_s\""; let model: DetectorModel = serde_json::from_str(json).expect("failed to deserialize"); assert_eq!(model, DetectorModel::YOLOv8_S); } #[test] fn test_detector_model_display_name() { assert_eq!(DetectorModel::YOLOv8_N.display_name(), "YOLOv8-Nano"); assert_eq!(DetectorModel::YOLOv8_S.display_name(), "YOLOv8-Small"); assert_eq!(DetectorModel::YOLOv8_M.display_name(), "YOLOv8-Medium"); assert_eq!(DetectorModel::YOLOv8_L.display_name(), "YOLOv8-Large"); assert_eq!(DetectorModel::YOLOv8_X.display_name(), "YOLOv8-XLarge"); } #[test] fn test_detector_model_param_count() { assert!(DetectorModel::YOLOv8_N.param_count() < DetectorModel::YOLOv8_S.param_count()); assert!(DetectorModel::YOLOv8_S.param_count() < DetectorModel::YOLOv8_M.param_count()); assert!(DetectorModel::YOLOv8_M.param_count() < DetectorModel::YOLOv8_L.param_count()); assert!(DetectorModel::YOLOv8_L.param_count() < DetectorModel::YOLOv8_X.param_count()); } #[test] fn test_bounding_box_serialization() { let bbox = BoundingBox { x: 0.25, y: 0.30, width: 0.50, height: 0.40, }; let json = serde_json::to_string(&bbox).expect("failed to serialize"); assert!(json.contains("0.25")); assert!(json.contains("0.3")); } #[test] fn test_bounding_box_normalized_values() { let bbox = BoundingBox { x: 0.0, y: 0.0, width: 1.0, height: 1.0, }; assert!(bbox.x >= 0.0 && bbox.x <= 1.0); assert!(bbox.y >= 0.0 && bbox.y <= 1.0); assert!(bbox.width >= 0.0 && bbox.width <= 1.0); assert!(bbox.height >= 0.0 && bbox.height <= 1.0); } #[test] fn test_bounding_box_area() { let bbox = BoundingBox { x: 0.2, y: 0.2, width: 0.4, height: 0.3, }; let area = bbox.area(); assert!((area - 0.12).abs() < 1e-6); } #[test] fn test_bounding_box_center() { let bbox = BoundingBox { x: 0.2, y: 0.3, width: 0.4, height: 0.6, }; let (cx, cy) = bbox.center(); assert_eq!(cx, 0.4); assert_eq!(cy, 0.6); } #[test] fn test_bounding_box_intersection() { let bbox1 = BoundingBox { x: 0.1, y: 0.1, width: 0.5, height: 0.5, }; let bbox2 = BoundingBox { x: 0.3, y: 0.3, width: 0.5, height: 0.5, }; let intersection = bbox1.intersection(&bbox2); assert_eq!(intersection, 0.09); } #[test] fn test_bounding_box_iou() { let bbox1 = BoundingBox { x: 0.0, y: 0.0, width: 0.5, height: 0.5, }; let bbox2 = BoundingBox { x: 0.0, y: 0.0, width: 0.5, height: 0.5, }; let iou = bbox1.iou(&bbox2); assert_eq!(iou, 1.0); } #[test] fn test_bounding_box_no_overlap_iou() { let bbox1 = BoundingBox { x: 0.0, y: 0.0, width: 0.2, height: 0.2, }; let bbox2 = BoundingBox { x: 0.5, y: 0.5, width: 0.2, height: 0.2, }; let iou = bbox1.iou(&bbox2); assert_eq!(iou, 0.0); } #[test] fn test_detection_serialization() { let detection = Detection { bbox: BoundingBox { x: 0.1, y: 0.2, width: 0.3, height: 0.4, }, class_id: 0, class_name: "person".to_string(), confidence: 0.95, color: "#FF5733".to_string(), }; let json = serde_json::to_string(&detection).expect("failed to serialize"); assert!(json.contains("person")); assert!(json.contains("0.95")); } #[test] fn test_detection_result_serialization() { let result = DetectionResult { detections: vec![], inference_time_ms: 42.5, image_width: 640, image_height: 480, }; let json = serde_json::to_string(&result).expect("failed to serialize"); assert!(json.contains("42.5")); assert!(json.contains("640")); } #[test] fn test_detector_config_default() { let config = DetectorConfig::default(); assert_eq!(config.model, DetectorModel::YOLOv8_N); assert_eq!(config.confidence_threshold, 0.25); assert_eq!(config.nms_threshold, 0.45); assert_eq!(config.max_detections, 100); assert!(config.use_gpu); } #[test] fn test_detector_config_custom() { let config = DetectorConfig { model: DetectorModel::YOLOv8_L, confidence_threshold: 0.5, nms_threshold: 0.5, max_detections: 50, use_gpu: false, }; assert_eq!(config.model, DetectorModel::YOLOv8_L); assert_eq!(config.confidence_threshold, 0.5); } #[test] fn test_detector_config_serialization() { let config = DetectorConfig::default(); let json = serde_json::to_string(&config).expect("failed to serialize"); let deserialized: DetectorConfig = serde_json::from_str(&json).expect("failed to deserialize"); assert_eq!(config.model, deserialized.model); assert_eq!( config.confidence_threshold, deserialized.confidence_threshold ); } #[test] fn test_detector_request_initialize() { let request = DetectorRequest::Initialize { config: DetectorConfig::default(), }; let json = serde_json::to_string(&request).expect("failed to serialize"); assert!(json.contains("initialize")); } #[test] fn test_detector_request_detect() { let request = DetectorRequest::Detect { image_data: "base64data".to_string(), }; let json = serde_json::to_string(&request).expect("failed to serialize"); assert!(json.contains("detect")); assert!(json.contains("base64data")); } #[test] fn test_detector_request_get_classes() { let request = DetectorRequest::GetClasses; let json = serde_json::to_string(&request).expect("failed to serialize"); assert!(json.contains("get_classes")); } #[test] fn test_detector_request_get_status() { let request = DetectorRequest::GetStatus; let json = serde_json::to_string(&request).expect("failed to serialize"); assert!(json.contains("get_status")); } #[test] fn test_detector_response_initialized() { let response = DetectorResponse::Initialized { success: true }; let json = serde_json::to_string(&response).expect("failed to serialize"); assert!(json.contains("initialized")); assert!(json.contains("true")); } #[test] fn test_detector_response_detected() { let result = DetectionResult { detections: vec![], inference_time_ms: 50.0, image_width: 800, image_height: 600, }; let response = DetectorResponse::Detected { result }; let json = serde_json::to_string(&response).expect("failed to serialize"); assert!(json.contains("detected")); } #[test] fn test_detector_response_classes() { let classes = vec!["person".to_string(), "car".to_string()]; let response = DetectorResponse::Classes { classes }; let json = serde_json::to_string(&response).expect("failed to serialize"); assert!(json.contains("classes")); assert!(json.contains("person")); } #[test] fn test_detector_response_status() { let status = DetectorStatus { initialized: true, model: Some("YOLOv8-Nano".to_string()), device: "CPU".to_string(), detection_count: 42, avg_inference_time_ms: 35.5, }; let response = DetectorResponse::Status { status }; let json = serde_json::to_string(&response).expect("failed to serialize"); assert!(json.contains("status")); assert!(json.contains("42")); } #[test] fn test_detector_response_error() { let response = DetectorResponse::Error { message: "Test error".to_string(), }; let json = serde_json::to_string(&response).expect("failed to serialize"); assert!(json.contains("error")); assert!(json.contains("Test error")); } #[test] fn test_detector_status_uninitialized() { let status = DetectorStatus { initialized: false, model: None, device: "CPU".to_string(), detection_count: 0, avg_inference_time_ms: 0.0, }; assert!(!status.initialized); assert!(status.model.is_none()); } }