From e18157648cab0338eb83c4d08399fdb841a828d5 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Thu, 20 Mar 2025 13:37:07 +0200 Subject: [PATCH] annotation save fixed --- src/DataHandler.js | 8 - src/components/AnnotationControls.js | 88 +++++++++- src/components/AnnotationMain.js | 231 ++++++++++++++++++++------- src/components/CanvasEditor.js | 77 +++++++-- src/components/Detection.js | 34 ++-- src/components/DetectionClassList.js | 80 +++++++--- src/components/VideoPlayer.js | 208 ++++++++++++++++++++---- src/services/AnnotationService.js | 74 ++++++--- src/services/DataHandler.js | 58 +++++++ 9 files changed, 686 insertions(+), 172 deletions(-) delete mode 100644 src/DataHandler.js create mode 100644 src/services/DataHandler.js diff --git a/src/DataHandler.js b/src/DataHandler.js deleted file mode 100644 index e689bff..0000000 --- a/src/DataHandler.js +++ /dev/null @@ -1,8 +0,0 @@ -const saveAnnotation = async (time, annotations, imageData) => { - // Implement save logic here, including YOLO format conversion - console.log("Saving annotation", time, annotations, imageData); - //Save the image data to a folder. - //Save the annotations to a .txt file in YOLO format. -}; - -export default saveAnnotation; \ No newline at end of file diff --git a/src/components/AnnotationControls.js b/src/components/AnnotationControls.js index a720bde..65abab8 100644 --- a/src/components/AnnotationControls.js +++ b/src/components/AnnotationControls.js @@ -2,12 +2,88 @@ import React from 'react'; function AnnotationControls({ onFrameBackward, onPlayPause, isPlaying, onFrameForward, onSaveAnnotation, onDelete }) { return ( -
- - - - - +
+ + + + +
); } diff --git a/src/components/AnnotationMain.js b/src/components/AnnotationMain.js index e1b7e03..38d1d40 100644 --- a/src/components/AnnotationMain.js +++ b/src/components/AnnotationMain.js @@ -1,11 +1,12 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import VideoPlayer from './VideoPlayer'; import AnnotationList from './AnnotationList'; import MediaList from './MediaList'; import DetectionClassList from './DetectionClassList'; import CanvasEditor from './CanvasEditor'; import * as AnnotationService from '../services/AnnotationService'; -import AnnotationControls from './AnnotationControls'; // Import the new component +import AnnotationControls from './AnnotationControls'; +import saveAnnotation from '../services/DataHandler'; function AnnotationMain() { const [files, setFiles] = useState([]); @@ -15,8 +16,13 @@ function AnnotationMain() { const [selectedClass, setSelectedClass] = useState(null); const [detections, setDetections] = useState([]); const [selectedDetectionIndices, setSelectedDetectionIndices] = useState([]); - const [isPlaying, setIsPlaying] = useState(false); // Add isPlaying state here - const videoRef = React.createRef(); + const [isPlaying, setIsPlaying] = useState(false); + const [videoWidth, setVideoWidth] = useState(640); + const [videoHeight, setVideoHeight] = useState(480); + const [errorMessage, setErrorMessage] = useState(""); + + const videoRef = useRef(null); + const containerRef = useRef(null); useEffect(() => { const initialFiles = []; @@ -24,49 +30,87 @@ function AnnotationMain() { }, []); const handleFileSelect = (file) => { - console.log("handleFileSelect called with:", file); + if (!file) return; + setSelectedFile(file); setAnnotations({}); setDetections([]); setSelectedDetectionIndices([]); setCurrentTime(0); - setIsPlaying(false); // Reset playing state + setIsPlaying(false); + setErrorMessage(""); }; const handleDropNewFiles = (newFiles) => { - setFiles(prevFiles => [...prevFiles, ...newFiles]); - if (!selectedFile) { - setSelectedFile(newFiles[0]); + if (!newFiles || newFiles.length === 0) return; + + const validFiles = [...newFiles]; + setFiles(prevFiles => [...prevFiles, ...validFiles]); + + if (!selectedFile && validFiles.length > 0) { + setSelectedFile(validFiles[0]); } }; const handleAnnotationSave = () => { - const containerRef = { current: { offsetWidth: videoRef.current.videoWidth, offsetHeight: videoRef.current.videoHeight } }; - const imageData = AnnotationService.createAnnotationImage(videoRef, detections, null, selectedClass, containerRef); + if (!videoRef.current) return; + + if (!detections || detections.length === 0) { + setErrorMessage("Please create at least one detection before saving"); + return; + } + + const safeContainerRef = { + current: { + offsetWidth: videoWidth, + offsetHeight: videoHeight + } + }; + + const imageData = AnnotationService.createAnnotationImage( + videoRef, + detections, + safeContainerRef + ); + if (imageData) { - setAnnotations(prevAnnotations => ({ - ...prevAnnotations, - [currentTime]: { time: currentTime, detections: detections, imageData }, - })); + setAnnotations(prevAnnotations => { + const newAnnotations = { + ...prevAnnotations, + [currentTime]: { time: currentTime, annotations: detections, imageData } + }; + + saveAnnotation(currentTime, detections, imageData); + setErrorMessage(""); + + return newAnnotations; + }); } }; const handleDelete = () => { + if (selectedDetectionIndices.length === 0) { + setErrorMessage("Please select a detection to delete"); + return; + } + const newDetections = detections.filter((_, index) => !selectedDetectionIndices.includes(index)); setDetections(newDetections); + setSelectedDetectionIndices([]); + setErrorMessage(""); }; const handleAnnotationClick = (time) => { setCurrentTime(time); const annotation = annotations[time]; if (annotation) { - setDetections(annotation.detections); + setDetections(annotation.annotations || []); setSelectedDetectionIndices([]); } if (videoRef.current) { videoRef.current.currentTime = time; } - setIsPlaying(false); // Pause when clicking an annotation + setIsPlaying(false); }; const handleClassSelect = (cls) => { @@ -81,71 +125,144 @@ function AnnotationMain() { setSelectedDetectionIndices(newSelection); }; - const handlePlayPause = () => { // Moved from VideoPlayer + const handlePlayPause = () => { setIsPlaying(prev => !prev); - if (videoRef.current) { - if (isPlaying) { - videoRef.current.pause(); - } else { - videoRef.current.play(); - } - } + }; - const handleFrameForward = () => { // Moved from VideoPlayer + const handleFrameForward = () => { if (videoRef.current) { videoRef.current.currentTime += 1 / 30; - setCurrentTime(videoRef.current.currentTime) + setCurrentTime(videoRef.current.currentTime); } }; - const handleFrameBackward = () => { // Moved from VideoPlayer + const handleFrameBackward = () => { if (videoRef.current) { videoRef.current.currentTime -= 1 / 30; - setCurrentTime(videoRef.current.currentTime) + setCurrentTime(videoRef.current.currentTime); } }; + const handleSizeChanged = (width, height) => { + setVideoWidth(width); + setVideoHeight(height); + }; + + const handleSetCurrentTime = (time) => { + setCurrentTime(time); + }; + + // Toggle debug mode with Ctrl+D + useEffect(() => { + const handleKeyDown = (e) => { + switch (e.key) { + case 'Space': + + } + if (e.ctrlKey && e.key === 'd') { + e.preventDefault(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + }, []); return ( -
-
+
+
-
+
- -
-
- - + {errorMessage && ( +
+ {errorMessage} +
+ )} + +
+
+ + + +
+ + - - +
+ +
+
diff --git a/src/components/CanvasEditor.js b/src/components/CanvasEditor.js index 23dbf17..e1dc603 100644 --- a/src/components/CanvasEditor.js +++ b/src/components/CanvasEditor.js @@ -20,6 +20,16 @@ function CanvasEditor({ const [resizeData, setResizeData] = useState(null); const [localDetections, setLocalDetections] = useState(detections || []); const [localSelectedIndices, setLocalSelectedIndices] = useState(selectedDetectionIndices || []); + const [dimensions, setDimensions] = useState({ width: width || 640, height: height || 480 }); + + // Track if we're in a dragging operation + const [isDragging, setIsDragging] = useState(false); + + useEffect(() => { + if (width && height) { + setDimensions({ width, height }); + } + }, [width, height]); useEffect(() => { setLocalDetections(detections || []); @@ -58,8 +68,9 @@ function CanvasEditor({ x: mouseX - localDetections[i].x1, y: mouseY - localDetections[i].y1, }); + setIsDragging(true); detectionFound = true; - break; // Stop the loop once a detection is found + break; } } @@ -82,10 +93,10 @@ function CanvasEditor({ if (localSelectedIndices.length > 0 && mouseDownPos && !resizeData) { // Dragging logic + setIsDragging(true); const newDetections = [...localDetections]; const firstSelectedIndex = localSelectedIndices[0]; - // Check for valid index before accessing. if (firstSelectedIndex === undefined || !newDetections[firstSelectedIndex]) return; const firstSelectedDetection = newDetections[firstSelectedIndex]; @@ -94,7 +105,6 @@ function CanvasEditor({ const deltaY = newY1 - firstSelectedDetection.y1; localSelectedIndices.forEach(index => { - // Check for valid index before accessing. if (newDetections[index] === undefined) return; const detection = newDetections[index]; @@ -112,17 +122,17 @@ function CanvasEditor({ setLocalDetections(newDetections); if (onDetectionsChange) { - onDetectionsChange(newDetections); // Notify about changes + onDetectionsChange(newDetections); } } else if (currentDetection && !resizeData) { - // Drawing a new detection. setCurrentDetection(prev => ({ ...prev, x2: mouseX, y2: mouseY })); } else if (resizeData) { + setIsDragging(true); const { index, position } = resizeData; if (localDetections[index] === undefined) return; const newDetections = [...localDetections]; - const detection = newDetections[index]; + const detection = { ...newDetections[index] }; const updatedDetection = AnnotationService.calculateResizedPosition(mouseX, mouseY, position, detection, containerRef); newDetections[index] = updatedDetection; setLocalDetections(newDetections); @@ -132,13 +142,27 @@ function CanvasEditor({ } }; - const handleMouseUp = () => { + const handleMouseUp = (e) => { + // If we're dragging (or resizing), stop propagation to prevent other elements from reacting + if (isDragging || resizeData) { + e.stopPropagation(); + } + if (currentDetection && mouseDownPos) { const dx = Math.abs(currentDetection.x2 - currentDetection.x1); const dy = Math.abs(currentDetection.y2 - currentDetection.y1); if (dx > 5 && dy > 5) { - const newDetections = [...localDetections, currentDetection]; + // Normalize coordinates so x1,y1 is always top-left and x2,y2 is bottom-right + const normalizedDetection = { + ...currentDetection, + x1: Math.min(currentDetection.x1, currentDetection.x2), + y1: Math.min(currentDetection.y1, currentDetection.y2), + x2: Math.max(currentDetection.x1, currentDetection.x2), + y2: Math.max(currentDetection.y1, currentDetection.y2), + }; + + const newDetections = [...localDetections, normalizedDetection]; setLocalDetections(newDetections); if (onDetectionsChange) { onDetectionsChange(newDetections); @@ -150,6 +174,7 @@ function CanvasEditor({ setMouseDownPos(null); setDragOffset({ x: 0, y: 0 }); setResizeData(null); + setIsDragging(false); }; const handleDetectionMouseDown = (e, index) => { @@ -172,8 +197,8 @@ function CanvasEditor({ x: mouseX - localDetections[index].x1, y: mouseY - localDetections[index].y1, }); - setMouseDownPos({x: mouseX, y: mouseY}) - + setMouseDownPos({x: mouseX, y: mouseY}); + setIsDragging(true); }; const handleResize = (e, index, position) => { @@ -190,20 +215,44 @@ function CanvasEditor({ onSelectionChange && onSelectionChange(newSelectedIndices); } } + setIsDragging(true); }; + // Add a document-level mouse move and up handler for dragging outside container + useEffect(() => { + if (isDragging || resizeData) { + const handleDocumentMouseMove = (e) => { + handleMouseMove(e); + }; + + const handleDocumentMouseUp = (e) => { + handleMouseUp(e); + }; + + document.addEventListener('mousemove', handleDocumentMouseMove); + document.addEventListener('mouseup', handleDocumentMouseUp); + + return () => { + document.removeEventListener('mousemove', handleDocumentMouseMove); + document.removeEventListener('mouseup', handleDocumentMouseUp); + }; + } + }, [isDragging, resizeData, mouseDownPos]); + return (
{ e.stopPropagation(); - onDetectionMouseDown(e); // Corrected prop name + onDetectionMouseDown(e); }; const handleResizeMouseDown = (e, position) => { @@ -73,11 +78,22 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) { backgroundColor: 'black', cursor: handle.cursor, pointerEvents: 'auto', + zIndex: 3, }} onMouseDown={(e) => handleResizeMouseDown(e, handle.position)} /> ))} - {detection.class.Name} + + {detection.class.Name} +
); } diff --git a/src/components/DetectionClassList.js b/src/components/DetectionClassList.js index 3c765d7..0b3c628 100644 --- a/src/components/DetectionClassList.js +++ b/src/components/DetectionClassList.js @@ -1,4 +1,3 @@ -// --- START OF FILE DetectionClassList.js --- (No changes needed) import React, { useEffect, useState } from 'react'; import DetectionClass from '../models/DetectionClass'; @@ -6,7 +5,7 @@ function DetectionClassList({ onClassSelect }) { const [detectionClasses, setDetectionClasses] = useState([]); const [selectedClass, setSelectedClass] = useState(null); - const colors = [ // Define colors *inside* the component + const colors = [ "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#000000", "#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#808080", "#C00000", "#00C000", "#0000C0", "#C0C000", "#C000C0", "#00C0C0", "#C0C0C0", @@ -17,7 +16,6 @@ function DetectionClassList({ onClassSelect }) { "#E00000", "#00E000", "#0000E0", "#E0E000", "#E000E0", "#00E0E0", "#E0E0E0" ]; - // Calculate color with opacity const calculateColor = (id, opacity = '0.2') => { const hexColor = colors[id % colors.length]; const r = parseInt(hexColor.slice(1, 3), 16); @@ -27,24 +25,60 @@ function DetectionClassList({ onClassSelect }) { }; useEffect(() => { - fetch('config.json') // Make sure this path is correct - .then(response => response.json()) - .then(data => { - const detectionClasses = data.classes.map(cls => { - const color = calculateColor(cls.Id, '1'); // Full opacity for border - return new DetectionClass(cls.Id, cls.Name, color); - }); - setDetectionClasses(detectionClasses); + const defaultClasses = [ + { Id: 0, Name: "Car" }, + { Id: 1, Name: "Person" }, + { Id: 2, Name: "Truck" }, + { Id: 3, Name: "Bicycle" }, + { Id: 4, Name: "Motorcycle" }, + { Id: 5, Name: "Bus" } + ]; - if (detectionClasses.length > 0 && onClassSelect) { - onClassSelect(detectionClasses[0]); // Select the first class by default - } - }) - .catch(error => console.error("Error loading detection classes:", error)); - }); + try { + fetch('config.json') + .then(response => response.json()) + .then(data => { + const classes = data.classes.map(cls => { + const color = calculateColor(cls.Id, '1'); + return new DetectionClass(cls.Id, cls.Name, color); + }); + setDetectionClasses(classes); + + if (classes.length > 0 && onClassSelect && !selectedClass) { + setSelectedClass(classes[0]); + onClassSelect(classes[0]); + } + }) + .catch(error => { + console.warn("Using default classes"); + const classes = defaultClasses.map(cls => { + const color = calculateColor(cls.Id, '1'); + return new DetectionClass(cls.Id, cls.Name, color); + }); + setDetectionClasses(classes); + + if (classes.length > 0 && onClassSelect && !selectedClass) { + setSelectedClass(classes[0]); + onClassSelect(classes[0]); + } + }); + } catch (error) { + console.warn("Using default classes"); + const classes = defaultClasses.map(cls => { + const color = calculateColor(cls.Id, '1'); + return new DetectionClass(cls.Id, cls.Name, color); + }); + setDetectionClasses(classes); + + if (classes.length > 0 && onClassSelect && !selectedClass) { + setSelectedClass(classes[0]); + onClassSelect(classes[0]); + } + } + }, []); const handleClassClick = (cls) => { - setSelectedClass(cls); // Update the selected ID + setSelectedClass(cls); onClassSelect && onClassSelect(cls); }; @@ -53,9 +87,9 @@ function DetectionClassList({ onClassSelect }) {

Classes

    {detectionClasses.map((cls) => { - const backgroundColor = calculateColor(cls.Id); // Calculate background color (0.2 opacity) - const darkBg = calculateColor(cls.Id, '0.8'); // Calculate selected background color (0.4 opacity) - const isSelected = selectedClass.Id === cls.Id; + const backgroundColor = calculateColor(cls.Id); + const darkBg = calculateColor(cls.Id, '0.8'); + const isSelected = selectedClass && selectedClass.Id === cls.Id; return (
  • { - console.log("useEffect Videoplayer"); - if (!(videoFile && videoRef.current)) - return; + if (!videoFile || !videoRef.current) return; - console.log("Setting video source:", videoFile); - videoRef.current.src = URL.createObjectURL(videoFile); - videoRef.current.onloadedmetadata = () => { + try { + // Clean up previous object URL + if (objectUrlRef.current) { + URL.revokeObjectURL(objectUrlRef.current); + } - }; + // Create new object URL and set it directly + const objectUrl = URL.createObjectURL(videoFile); + objectUrlRef.current = objectUrl; + + // Reset video and set new source + videoRef.current.pause(); + videoRef.current.src = objectUrl; + videoRef.current.load(); + setPlaybackError(null); + + return () => { + URL.revokeObjectURL(objectUrl); + objectUrlRef.current = null; + }; + } catch (err) { + console.error("Error loading video:", err); + setPlaybackError(`Error loading video: ${err.message}`); + } }, [videoFile]); - // useEffect(() => { - // if(videoRef.current){ - // videoRef.current.currentTime = currentTime; - // } - // }, [currentTime, videoRef]) - // - // useEffect(() => { - // if (videoRef.current) { - // if(isPlaying){ - // videoRef.current.play() - // } - // else{ - // videoRef.current.pause() - // } - // } - // }, [isPlaying, videoRef]) + // Handle metadata loading and size changes + useEffect(() => { + if (!videoRef.current) return; + + const handleMetadata = () => { + if (!videoRef.current) return; + + const width = videoRef.current.videoWidth || 640; + const height = videoRef.current.videoHeight || 480; + + if (onSizeChanged) { + onSizeChanged(width, height); + } + }; + + videoRef.current.addEventListener('loadedmetadata', handleMetadata); + + return () => { + if (videoRef.current) { + videoRef.current.removeEventListener('loadedmetadata', handleMetadata); + } + }; + }, [onSizeChanged]); + + // Handle play/pause state + useEffect(() => { + if (!videoRef.current) return; + + const attemptPlay = async () => { + try { + if (isPlaying) { + isPlaybackUpdateRef.current = true; + await videoRef.current.play(); + setPlaybackError(null); + } else { + videoRef.current.pause(); + } + } catch (err) { + console.error("Playback error:", err); + setPlaybackError(`Playback error: ${err.message}`); + } + }; + + attemptPlay(); + }, [isPlaying]); + + // Handle current time changes + useEffect(() => { + if (!videoRef.current) return; + + // Only update the video's time if it's not coming from natural playback + if (!isPlaybackUpdateRef.current) { + try { + if (videoRef.current.readyState > 0) { + videoRef.current.currentTime = currentTime; + } + } catch (err) { + console.warn("Error setting time:", err); + } + } else { + // Reset the flag after receiving the update + isPlaybackUpdateRef.current = false; + } + }, [currentTime]); + + // Set up time update events + useEffect(() => { + if (!videoRef.current) return; + + const handleTimeUpdate = () => { + if (videoRef.current && onSetCurrentTime && isPlaying) { + isPlaybackUpdateRef.current = true; + onSetCurrentTime(videoRef.current.currentTime); + } + }; + + const handleSeeked = () => { + if (videoRef.current && onSetCurrentTime) { + onSetCurrentTime(videoRef.current.currentTime); + } + }; + + videoRef.current.addEventListener('timeupdate', handleTimeUpdate); + videoRef.current.addEventListener('seeked', handleSeeked); + + return () => { + if (videoRef.current) { + videoRef.current.removeEventListener('timeupdate', handleTimeUpdate); + videoRef.current.removeEventListener('seeked', handleSeeked); + } + }; + }, [onSetCurrentTime, isPlaying]); return ( -
    - {/* Video Element */} -
    +
    +
    ); diff --git a/src/services/AnnotationService.js b/src/services/AnnotationService.js index 450f8a4..dce6ed5 100644 --- a/src/services/AnnotationService.js +++ b/src/services/AnnotationService.js @@ -13,48 +13,76 @@ export const isMouseOverDetection = (x, y, detection, containerRef) => { return relativeX >= detection.x1 && relativeX <= detection.x2 && relativeY >= detection.y1 && relativeY <= detection.y2; }; -// Function to create an annotation image export const createAnnotationImage = (videoRef, detections, containerRef) => { + if (!videoRef?.current || !containerRef?.current) { + console.warn("Missing video or container reference"); + return null; + } + const canvas = document.createElement('canvas'); - if (!containerRef.current) return null; const container = containerRef.current; - canvas.width = container.offsetWidth; - canvas.height = container.offsetHeight; + canvas.width = container.offsetWidth || 640; + canvas.height = container.offsetHeight || 480; + const ctx = canvas.getContext('2d'); + if (!ctx) return null; - ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height); + try { + ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height); + } catch (e) { + console.error("Error drawing video to canvas:", e); + return null; + } - detections.forEach(detection => { + if (detections && detections.length > 0) { + detections.forEach(detection => { + if (!detection?.class) return; - ctx.fillRect(detection.x1, detection.y1, detection.x2 - detection.x1, detection.y2 - detection.y1); - ctx.lineWidth = 2; - ctx.strokeRect(detection.x1, detection.y1, detection.x2 - detection.x1, detection.y2 - detection.y1); + // Ensure proper opacity for background - consistently using 0.4 opacity + const bgColor = detection.class.Color?.startsWith('rgba') + ? detection.class.Color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 0.4)') + : detection.class.Color?.replace(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/, 'rgba($1, $2, $3, 0.4)') || 'rgba(255, 0, 0, 0.4)'; - ctx.fillStyle = 'white'; - ctx.font = '12px Arial'; - ctx.fillText(detection.class.Name, detection.x1, detection.y1 - 5); - }); + // Ensure full opacity for border + const borderColor = detection.class.Color?.startsWith('rgba') + ? detection.class.Color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 1)') + : detection.class.Color || 'rgba(255, 0, 0, 1)'; + + ctx.fillStyle = bgColor; + ctx.strokeStyle = borderColor; + + const x = Math.max(0, detection.x1 || 0); + const y = Math.max(0, detection.y1 || 0); + const width = Math.max(1, (detection.x2 || 0) - (detection.x1 || 0)); + const height = Math.max(1, (detection.y2 || 0) - (detection.y1 || 0)); + + ctx.fillRect(x, y, width, height); + ctx.lineWidth = 2; + ctx.strokeRect(x, y, width, height); + + ctx.fillStyle = 'white'; + ctx.font = '12px Arial'; + ctx.fillText(detection.class.Name || 'Unknown', x, y - 5); + }); + } return canvas.toDataURL('image/png'); }; - -export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, containerRef) => { - +export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, containerRef) => { let newX1 = mouseX - dragOffset.x; let newY1 = mouseY - dragOffset.y; let newX2 = newX1 + (detection.x2 - detection.x1); let newY2 = newY1 + (detection.y2 - detection.y1); if (!containerRef.current) { - return { newX1, newY1, newX2, newY2 }; // Return early with unchanged values + return { newX1, newY1, newX2, newY2 }; } let containerWidth = containerRef.current.offsetWidth; let containerHeight = containerRef.current.offsetHeight; - if (newX1 < 0) { newX1 = 0; newX2 = detection.x2 - detection.x1; @@ -72,13 +100,13 @@ export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, con newY1 = newY2 - (detection.y2 - detection.y1); } return { newX1, newY1, newX2, newY2 }; - }; export const calculateResizedPosition = (mouseX, mouseY, position, detection, containerRef) => { let { x1, y1, x2, y2 } = detection; - const containerWidth = containerRef.current.offsetWidth; - const containerHeight = containerRef.current.offsetHeight; + const containerWidth = containerRef.current?.offsetWidth || 640; + const containerHeight = containerRef.current?.offsetHeight || 480; + switch (position) { case 'top-left': x1 = Math.min(mouseX, detection.x2 - 5); @@ -111,11 +139,11 @@ export const calculateResizedPosition = (mouseX, mouseY, position, detection, co default: break; } - // Boundary checks + x1 = Math.max(0, x1); y1 = Math.max(0, y1); x2 = Math.min(containerWidth, x2); y2 = Math.min(containerHeight, y2); - return { x1, y1, x2, y2 }; + return { ...detection, x1, y1, x2, y2 }; }; \ No newline at end of file diff --git a/src/services/DataHandler.js b/src/services/DataHandler.js new file mode 100644 index 0000000..e283b92 --- /dev/null +++ b/src/services/DataHandler.js @@ -0,0 +1,58 @@ +const convertToYoloFormat = (detection, imageWidth, imageHeight) => { + const width = (detection.x2 - detection.x1) / imageWidth; + const height = (detection.y2 - detection.y1) / imageHeight; + const centerX = (detection.x1 / imageWidth) + (width / 2); + const centerY = (detection.y1 / imageHeight) + (height / 2); + + return { + classId: detection.class.Id, + centerX, + centerY, + width, + height + }; +}; + +const saveAnnotation = (time, detections, imageData) => { + if (!detections || !detections.length || !imageData) { + console.warn("Nothing to save: missing detections or image data"); + return null; + } + + try { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const imageFilename = `annotation_${timestamp}.png`; + const annotationFilename = `annotation_${timestamp}.txt`; + + const imageWidth = detections[0]?.x2 > 0 ? + Math.max(...detections.map(d => d.x2)) : 640; + const imageHeight = detections[0]?.y2 > 0 ? + Math.max(...detections.map(d => d.y2)) : 480; + + const yoloAnnotations = detections.map(detection => { + const yolo = convertToYoloFormat(detection, imageWidth, imageHeight); + return `${yolo.classId} ${yolo.centerX.toFixed(6)} ${yolo.centerY.toFixed(6)} ${yolo.width.toFixed(6)} ${yolo.height.toFixed(6)}`; + }).join('\n'); + + console.log(`Saving image to: ${imageFilename}`); + console.log(`Saving annotations to: ${annotationFilename}`); + + const imageLink = document.createElement('a'); + imageLink.href = imageData; + imageLink.download = imageFilename; + imageLink.click(); + + const annotationBlob = new Blob([yoloAnnotations], { type: 'text/plain' }); + const annotationLink = document.createElement('a'); + annotationLink.href = URL.createObjectURL(annotationBlob); + annotationLink.download = annotationFilename; + annotationLink.click(); + + return { imageFilename, annotationFilename }; + } catch (error) { + console.error("Error saving annotation:", error); + return null; + } +}; + +export default saveAnnotation; \ No newline at end of file