use crate::{Result, VisionError};
use serde::{Deserialize, Serialize};
use torsh_tensor::Tensor;
pub struct InteractiveViewer {
port: u16,
current_image: Option<Tensor<f32>>,
annotations: Vec<Annotation>,
transforms: Vec<Box<dyn TransformOp>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Annotation {
pub annotation_type: AnnotationType,
pub coordinates: Vec<f32>,
pub label: String,
pub color: (u8, u8, u8),
pub confidence: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AnnotationType {
BoundingBox,
Point,
Polygon,
Mask,
Text,
Arrow,
}
pub trait TransformOp: Send + Sync {
fn apply(&self, image: &Tensor<f32>) -> Result<Tensor<f32>>;
fn name(&self) -> &str;
fn parameters(&self) -> Vec<Parameter>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Parameter {
pub name: String,
pub value: f32,
pub min: f32,
pub max: f32,
pub step: f32,
}
impl InteractiveViewer {
pub fn new(port: u16) -> Self {
Self {
port,
current_image: None,
annotations: Vec::new(),
transforms: Vec::new(),
}
}
pub fn set_image(&mut self, image: Tensor<f32>) -> Result<()> {
let shape = image.shape();
if shape.dims().len() != 3 {
return Err(VisionError::InvalidShape(format!(
"Expected 3D tensor (C, H, W), got {}D",
shape.dims().len()
)));
}
self.current_image = Some(image);
Ok(())
}
pub fn add_annotation(&mut self, annotation: Annotation) {
self.annotations.push(annotation);
}
pub fn clear_annotations(&mut self) {
self.annotations.clear();
}
pub fn add_transform(&mut self, transform: Box<dyn TransformOp>) {
self.transforms.push(transform);
}
pub fn apply_transforms(&self) -> Result<Option<Tensor<f32>>> {
if let Some(ref image) = self.current_image {
let mut result = image.clone();
for transform in &self.transforms {
result = transform.apply(&result)?;
}
Ok(Some(result))
} else {
Ok(None)
}
}
pub fn generate_html_interface(&self) -> String {
let html = String::from(
r#"
<!DOCTYPE html>
<html>
<head>
<title>ToRSh Vision Interactive Viewer</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.container { display: flex; gap: 20px; }
.image-panel { flex: 2; }
.control-panel { flex: 1; background: #f5f5f5; padding: 20px; border-radius: 8px; }
.annotation { margin: 10px 0; padding: 10px; background: white; border-radius: 4px; }
.transform { margin: 10px 0; padding: 10px; background: white; border-radius: 4px; }
#canvas { border: 1px solid #ccc; max-width: 100%; }
button { padding: 8px 16px; margin: 4px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
input[type="range"] { width: 100%; }
</style>
</head>
<body>
<h1>ToRSh Vision Interactive Viewer</h1>
<div class="container">
<div class="image-panel">
<canvas id="canvas" width="800" height="600"></canvas>
<div>
<button onclick="clearAnnotations()">Clear Annotations</button>
<button onclick="saveImage()">Save Image</button>
<button onclick="exportData()">Export Data</button>
</div>
</div>
<div class="control-panel">
<h3>Annotations</h3>
<div id="annotations"></div>
<h3>Transforms</h3>
<div id="transforms"></div>
<h3>Add Annotation</h3>
<select id="annotationType">
<option value="BoundingBox">Bounding Box</option>
<option value="Point">Point</option>
<option value="Polygon">Polygon</option>
<option value="Text">Text</option>
</select>
<input type="text" id="annotationLabel" placeholder="Label" />
<button onclick="addAnnotation()">Add</button>
</div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let currentMode = 'view';
let annotations = [];
// Canvas event listeners for interactive annotation
canvas.addEventListener('click', handleCanvasClick);
canvas.addEventListener('mousemove', handleMouseMove);
function handleCanvasClick(event) {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
if (currentMode === 'annotate') {
createAnnotation(x, y);
}
}
function handleMouseMove(event) {
// Update cursor or preview based on current mode
}
function createAnnotation(x, y) {
const type = document.getElementById('annotationType').value;
const label = document.getElementById('annotationLabel').value || 'Annotation';
const annotation = {
type: type,
x: x,
y: y,
label: label,
color: [255, 0, 0]
};
annotations.push(annotation);
updateAnnotationsList();
redrawCanvas();
}
function updateAnnotationsList() {
const container = document.getElementById('annotations');
container.innerHTML = '';
annotations.forEach((ann, index) => {
const div = document.createElement('div');
div.className = 'annotation';
div.innerHTML = `
<strong>${ann.label}</strong> (${ann.type})<br>
Position: (${ann.x.toFixed(0)}, ${ann.y.toFixed(0)})<br>
<button onclick="removeAnnotation(${index})">Remove</button>
`;
container.appendChild(div);
});
}
function removeAnnotation(index) {
annotations.splice(index, 1);
updateAnnotationsList();
redrawCanvas();
}
function clearAnnotations() {
annotations = [];
updateAnnotationsList();
redrawCanvas();
}
function redrawCanvas() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw image (would be loaded from server)
// Draw annotations
annotations.forEach(ann => {
ctx.strokeStyle = `rgb(${ann.color[0]}, ${ann.color[1]}, ${ann.color[2]})`;
ctx.lineWidth = 2;
if (ann.type === 'Point') {
ctx.beginPath();
ctx.arc(ann.x, ann.y, 5, 0, 2 * Math.PI);
ctx.stroke();
} else if (ann.type === 'BoundingBox') {
ctx.strokeRect(ann.x - 25, ann.y - 25, 50, 50);
}
// Draw label
ctx.fillStyle = 'white';
ctx.fillRect(ann.x, ann.y - 20, ctx.measureText(ann.label).width + 4, 16);
ctx.fillStyle = 'black';
ctx.fillText(ann.label, ann.x + 2, ann.y - 6);
});
}
function addAnnotation() {
currentMode = 'annotate';
canvas.style.cursor = 'crosshair';
}
function saveImage() {
// Implementation for saving the current view
alert('Save functionality would be implemented here');
}
function exportData() {
const data = {
annotations: annotations,
timestamp: new Date().toISOString()
};
const dataStr = JSON.stringify(data, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'annotations.json';
link.click();
}
// Initialize
redrawCanvas();
</script>
</body>
</html>
"#,
);
html
}
pub fn start_server(&self) -> Result<()> {
println!("Interactive viewer would start on port {}", self.port);
println!(
"HTML interface generated with {} annotations",
self.annotations.len()
);
Ok(())
}
}
pub fn create_interactive_viewer(port: u16) -> InteractiveViewer {
InteractiveViewer::new(port)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interactive_viewer_creation() {
let viewer = InteractiveViewer::new(8080);
assert_eq!(viewer.port, 8080);
assert!(viewer.current_image.is_none());
assert!(viewer.annotations.is_empty());
assert!(viewer.transforms.is_empty());
}
#[test]
fn test_annotation_management() {
let mut viewer = InteractiveViewer::new(8080);
let annotation = Annotation {
annotation_type: AnnotationType::BoundingBox,
coordinates: vec![10.0, 10.0, 50.0, 50.0],
label: "Test".to_string(),
color: (255, 0, 0),
confidence: Some(0.95),
};
viewer.add_annotation(annotation);
assert_eq!(viewer.annotations.len(), 1);
viewer.clear_annotations();
assert!(viewer.annotations.is_empty());
}
#[test]
fn test_html_generation() {
let viewer = InteractiveViewer::new(8080);
let html = viewer.generate_html_interface();
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("ToRSh Vision Interactive Viewer"));
assert!(html.contains("<canvas"));
}
#[test]
fn test_parameter_serialization() {
let param = Parameter {
name: "brightness".to_string(),
value: 0.5,
min: 0.0,
max: 1.0,
step: 0.01,
};
let json = serde_json::to_string(¶m).expect("serde json should succeed");
let deserialized: Parameter =
serde_json::from_str(&json).expect("serde json should succeed");
assert_eq!(param.name, deserialized.name);
assert_eq!(param.value, deserialized.value);
}
#[test]
fn test_annotation_types() {
let annotation_types = vec![
AnnotationType::BoundingBox,
AnnotationType::Point,
AnnotationType::Polygon,
AnnotationType::Mask,
AnnotationType::Text,
AnnotationType::Arrow,
];
for annotation_type in annotation_types {
let annotation = Annotation {
annotation_type: annotation_type.clone(),
coordinates: vec![0.0, 0.0],
label: "Test".to_string(),
color: (255, 255, 255),
confidence: None,
};
let json = serde_json::to_string(&annotation).expect("serde json should succeed");
let _deserialized: Annotation =
serde_json::from_str(&json).expect("serde json should succeed");
}
}
}