annotation save fixed

This commit is contained in:
Alex Bezdieniezhnykh
2025-03-20 13:37:07 +02:00
parent 0fa2c0ddd8
commit e18157648c
9 changed files with 686 additions and 172 deletions
-8
View File
@@ -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;
+82 -6
View File
@@ -2,12 +2,88 @@ import React from 'react';
function AnnotationControls({ onFrameBackward, onPlayPause, isPlaying, onFrameForward, onSaveAnnotation, onDelete }) { function AnnotationControls({ onFrameBackward, onPlayPause, isPlaying, onFrameForward, onSaveAnnotation, onDelete }) {
return ( return (
<div style={{ display: 'flex', justifyContent: 'center', position: 'relative', zIndex: 1 }}> <div style={{
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onFrameBackward}></button> display: 'flex',
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onPlayPause}>{isPlaying ? 'Pause' : 'Play'}</button> justifyContent: 'center',
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onFrameForward}></button> position: 'relative',
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onSaveAnnotation}>Save Annotation</button> zIndex: 1,
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onDelete}>Delete</button> padding: '10px',
background: '#f5f5f5',
borderRadius: '4px',
marginTop: '10px'
}}>
<button
style={{
padding: '10px 20px',
fontSize: '16px',
margin: '0 5px',
background: '#e0e0e0',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer'
}}
onClick={onFrameBackward}
title="Previous Frame"
>
</button>
<button
style={{
padding: '10px 20px',
fontSize: '16px',
margin: '0 5px',
background: isPlaying ? '#ff8a8a' : '#8aff8a',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer'
}}
onClick={onPlayPause}
>
{isPlaying ? '⏸️ Pause' : '▶️ Play'}
</button>
<button
style={{
padding: '10px 20px',
fontSize: '16px',
margin: '0 5px',
background: '#e0e0e0',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer'
}}
onClick={onFrameForward}
title="Next Frame"
>
</button>
<button
style={{
padding: '10px 20px',
fontSize: '16px',
margin: '0 5px',
background: '#8ad4ff',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer'
}}
onClick={onSaveAnnotation}
>
💾 Save
</button>
<button
style={{
padding: '10px 20px',
fontSize: '16px',
margin: '0 5px',
background: '#ffb38a',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer'
}}
onClick={onDelete}
>
🗑 Delete
</button>
</div> </div>
); );
} }
+153 -36
View File
@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import VideoPlayer from './VideoPlayer'; import VideoPlayer from './VideoPlayer';
import AnnotationList from './AnnotationList'; import AnnotationList from './AnnotationList';
import MediaList from './MediaList'; import MediaList from './MediaList';
import DetectionClassList from './DetectionClassList'; import DetectionClassList from './DetectionClassList';
import CanvasEditor from './CanvasEditor'; import CanvasEditor from './CanvasEditor';
import * as AnnotationService from '../services/AnnotationService'; 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() { function AnnotationMain() {
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
@@ -15,8 +16,13 @@ function AnnotationMain() {
const [selectedClass, setSelectedClass] = useState(null); const [selectedClass, setSelectedClass] = useState(null);
const [detections, setDetections] = useState([]); const [detections, setDetections] = useState([]);
const [selectedDetectionIndices, setSelectedDetectionIndices] = useState([]); const [selectedDetectionIndices, setSelectedDetectionIndices] = useState([]);
const [isPlaying, setIsPlaying] = useState(false); // Add isPlaying state here const [isPlaying, setIsPlaying] = useState(false);
const videoRef = React.createRef(); const [videoWidth, setVideoWidth] = useState(640);
const [videoHeight, setVideoHeight] = useState(480);
const [errorMessage, setErrorMessage] = useState("");
const videoRef = useRef(null);
const containerRef = useRef(null);
useEffect(() => { useEffect(() => {
const initialFiles = []; const initialFiles = [];
@@ -24,49 +30,87 @@ function AnnotationMain() {
}, []); }, []);
const handleFileSelect = (file) => { const handleFileSelect = (file) => {
console.log("handleFileSelect called with:", file); if (!file) return;
setSelectedFile(file); setSelectedFile(file);
setAnnotations({}); setAnnotations({});
setDetections([]); setDetections([]);
setSelectedDetectionIndices([]); setSelectedDetectionIndices([]);
setCurrentTime(0); setCurrentTime(0);
setIsPlaying(false); // Reset playing state setIsPlaying(false);
setErrorMessage("");
}; };
const handleDropNewFiles = (newFiles) => { const handleDropNewFiles = (newFiles) => {
setFiles(prevFiles => [...prevFiles, ...newFiles]); if (!newFiles || newFiles.length === 0) return;
if (!selectedFile) {
setSelectedFile(newFiles[0]); const validFiles = [...newFiles];
setFiles(prevFiles => [...prevFiles, ...validFiles]);
if (!selectedFile && validFiles.length > 0) {
setSelectedFile(validFiles[0]);
} }
}; };
const handleAnnotationSave = () => { const handleAnnotationSave = () => {
const containerRef = { current: { offsetWidth: videoRef.current.videoWidth, offsetHeight: videoRef.current.videoHeight } }; if (!videoRef.current) return;
const imageData = AnnotationService.createAnnotationImage(videoRef, detections, null, selectedClass, containerRef);
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) { if (imageData) {
setAnnotations(prevAnnotations => ({ setAnnotations(prevAnnotations => {
const newAnnotations = {
...prevAnnotations, ...prevAnnotations,
[currentTime]: { time: currentTime, detections: detections, imageData }, [currentTime]: { time: currentTime, annotations: detections, imageData }
})); };
saveAnnotation(currentTime, detections, imageData);
setErrorMessage("");
return newAnnotations;
});
} }
}; };
const handleDelete = () => { const handleDelete = () => {
if (selectedDetectionIndices.length === 0) {
setErrorMessage("Please select a detection to delete");
return;
}
const newDetections = detections.filter((_, index) => !selectedDetectionIndices.includes(index)); const newDetections = detections.filter((_, index) => !selectedDetectionIndices.includes(index));
setDetections(newDetections); setDetections(newDetections);
setSelectedDetectionIndices([]);
setErrorMessage("");
}; };
const handleAnnotationClick = (time) => { const handleAnnotationClick = (time) => {
setCurrentTime(time); setCurrentTime(time);
const annotation = annotations[time]; const annotation = annotations[time];
if (annotation) { if (annotation) {
setDetections(annotation.detections); setDetections(annotation.annotations || []);
setSelectedDetectionIndices([]); setSelectedDetectionIndices([]);
} }
if (videoRef.current) { if (videoRef.current) {
videoRef.current.currentTime = time; videoRef.current.currentTime = time;
} }
setIsPlaying(false); // Pause when clicking an annotation setIsPlaying(false);
}; };
const handleClassSelect = (cls) => { const handleClassSelect = (cls) => {
@@ -81,57 +125,115 @@ function AnnotationMain() {
setSelectedDetectionIndices(newSelection); setSelectedDetectionIndices(newSelection);
}; };
const handlePlayPause = () => { // Moved from VideoPlayer const handlePlayPause = () => {
setIsPlaying(prev => !prev); 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) { if (videoRef.current) {
videoRef.current.currentTime += 1 / 30; videoRef.current.currentTime += 1 / 30;
setCurrentTime(videoRef.current.currentTime) setCurrentTime(videoRef.current.currentTime);
} }
}; };
const handleFrameBackward = () => { // Moved from VideoPlayer const handleFrameBackward = () => {
if (videoRef.current) { if (videoRef.current) {
videoRef.current.currentTime -= 1 / 30; 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 ( return (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
<div style={{ width: '15%', paddingRight: '10px', display: 'flex', flexDirection: 'column' }}> <div style={{
width: '15%',
height: '100%',
overflowY: 'auto',
borderRight: '1px solid #ccc',
display: 'flex',
flexDirection: 'column'
}}>
<MediaList <MediaList
files={files} files={files}
selectedFile={selectedFile} selectedFile={selectedFile}
onFileSelect={handleFileSelect} onFileSelect={handleFileSelect}
onDropNewFiles={handleDropNewFiles} onDropNewFiles={handleDropNewFiles}
/> />
<div style={{ marginTop: 'auto' }}> <div style={{ flexGrow: 1 }}>
<DetectionClassList onClassSelect={handleClassSelect} /> <DetectionClassList onClassSelect={handleClassSelect} />
</div> </div>
<AnnotationList annotations={Object.values(annotations)} onAnnotationClick={handleAnnotationClick} />
</div> </div>
<div style={{ width: '85%' }}>
<div style={{
width: '70%',
height: '100%',
display: 'flex',
flexDirection: 'column'
}}>
{errorMessage && (
<div style={{
backgroundColor: '#ffdddd',
color: '#d8000c',
padding: '6px',
margin: '6px',
borderRadius: '4px'
}}>
{errorMessage}
</div>
)}
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
position: 'relative',
minHeight: 0
}} ref={containerRef}>
<div style={{
flex: 1,
position: 'relative',
backgroundColor: '#000',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden'
}}>
<VideoPlayer <VideoPlayer
videoFile={selectedFile} videoFile={selectedFile}
currentTime={currentTime} currentTime={currentTime}
videoRef={videoRef} videoRef={videoRef}
isPlaying={isPlaying} isPlaying={isPlaying}
onSizeChanged={handleSizeChanged}
onSetCurrentTime={handleSetCurrentTime}
> >
<CanvasEditor <CanvasEditor
width={videoRef.current ? videoRef.current.videoWidth : 0} width={videoWidth}
height={videoRef.current ? videoRef.current.videoHeight : 0} height={videoHeight}
detections={detections} detections={detections}
selectedDetectionIndices={selectedDetectionIndices} selectedDetectionIndices={selectedDetectionIndices}
onDetectionsChange={handleDetectionsChange} onDetectionsChange={handleDetectionsChange}
@@ -139,6 +241,8 @@ function AnnotationMain() {
detectionClass={selectedClass} detectionClass={selectedClass}
/> />
</VideoPlayer> </VideoPlayer>
</div>
<AnnotationControls <AnnotationControls
onFrameBackward={handleFrameBackward} onFrameBackward={handleFrameBackward}
onPlayPause={handlePlayPause} onPlayPause={handlePlayPause}
@@ -149,6 +253,19 @@ function AnnotationMain() {
/> />
</div> </div>
</div> </div>
<div style={{
width: '15%',
height: '100%',
overflowY: 'auto',
borderLeft: '1px solid #ccc'
}}>
<AnnotationList
annotations={Object.values(annotations)}
onAnnotationClick={handleAnnotationClick}
/>
</div>
</div>
); );
} }
+63 -14
View File
@@ -20,6 +20,16 @@ function CanvasEditor({
const [resizeData, setResizeData] = useState(null); const [resizeData, setResizeData] = useState(null);
const [localDetections, setLocalDetections] = useState(detections || []); const [localDetections, setLocalDetections] = useState(detections || []);
const [localSelectedIndices, setLocalSelectedIndices] = useState(selectedDetectionIndices || []); 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(() => { useEffect(() => {
setLocalDetections(detections || []); setLocalDetections(detections || []);
@@ -58,8 +68,9 @@ function CanvasEditor({
x: mouseX - localDetections[i].x1, x: mouseX - localDetections[i].x1,
y: mouseY - localDetections[i].y1, y: mouseY - localDetections[i].y1,
}); });
setIsDragging(true);
detectionFound = 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) { if (localSelectedIndices.length > 0 && mouseDownPos && !resizeData) {
// Dragging logic // Dragging logic
setIsDragging(true);
const newDetections = [...localDetections]; const newDetections = [...localDetections];
const firstSelectedIndex = localSelectedIndices[0]; const firstSelectedIndex = localSelectedIndices[0];
// Check for valid index before accessing.
if (firstSelectedIndex === undefined || !newDetections[firstSelectedIndex]) return; if (firstSelectedIndex === undefined || !newDetections[firstSelectedIndex]) return;
const firstSelectedDetection = newDetections[firstSelectedIndex]; const firstSelectedDetection = newDetections[firstSelectedIndex];
@@ -94,7 +105,6 @@ function CanvasEditor({
const deltaY = newY1 - firstSelectedDetection.y1; const deltaY = newY1 - firstSelectedDetection.y1;
localSelectedIndices.forEach(index => { localSelectedIndices.forEach(index => {
// Check for valid index before accessing.
if (newDetections[index] === undefined) return; if (newDetections[index] === undefined) return;
const detection = newDetections[index]; const detection = newDetections[index];
@@ -112,17 +122,17 @@ function CanvasEditor({
setLocalDetections(newDetections); setLocalDetections(newDetections);
if (onDetectionsChange) { if (onDetectionsChange) {
onDetectionsChange(newDetections); // Notify about changes onDetectionsChange(newDetections);
} }
} else if (currentDetection && !resizeData) { } else if (currentDetection && !resizeData) {
// Drawing a new detection.
setCurrentDetection(prev => ({ ...prev, x2: mouseX, y2: mouseY })); setCurrentDetection(prev => ({ ...prev, x2: mouseX, y2: mouseY }));
} else if (resizeData) { } else if (resizeData) {
setIsDragging(true);
const { index, position } = resizeData; const { index, position } = resizeData;
if (localDetections[index] === undefined) return; if (localDetections[index] === undefined) return;
const newDetections = [...localDetections]; const newDetections = [...localDetections];
const detection = newDetections[index]; const detection = { ...newDetections[index] };
const updatedDetection = AnnotationService.calculateResizedPosition(mouseX, mouseY, position, detection, containerRef); const updatedDetection = AnnotationService.calculateResizedPosition(mouseX, mouseY, position, detection, containerRef);
newDetections[index] = updatedDetection; newDetections[index] = updatedDetection;
setLocalDetections(newDetections); 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) { if (currentDetection && mouseDownPos) {
const dx = Math.abs(currentDetection.x2 - currentDetection.x1); const dx = Math.abs(currentDetection.x2 - currentDetection.x1);
const dy = Math.abs(currentDetection.y2 - currentDetection.y1); const dy = Math.abs(currentDetection.y2 - currentDetection.y1);
if (dx > 5 && dy > 5) { 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); setLocalDetections(newDetections);
if (onDetectionsChange) { if (onDetectionsChange) {
onDetectionsChange(newDetections); onDetectionsChange(newDetections);
@@ -150,6 +174,7 @@ function CanvasEditor({
setMouseDownPos(null); setMouseDownPos(null);
setDragOffset({ x: 0, y: 0 }); setDragOffset({ x: 0, y: 0 });
setResizeData(null); setResizeData(null);
setIsDragging(false);
}; };
const handleDetectionMouseDown = (e, index) => { const handleDetectionMouseDown = (e, index) => {
@@ -172,8 +197,8 @@ function CanvasEditor({
x: mouseX - localDetections[index].x1, x: mouseX - localDetections[index].x1,
y: mouseY - localDetections[index].y1, y: mouseY - localDetections[index].y1,
}); });
setMouseDownPos({x: mouseX, y: mouseY}) setMouseDownPos({x: mouseX, y: mouseY});
setIsDragging(true);
}; };
const handleResize = (e, index, position) => { const handleResize = (e, index, position) => {
@@ -190,20 +215,44 @@ function CanvasEditor({
onSelectionChange && onSelectionChange(newSelectedIndices); 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 ( return (
<div <div
style={{ style={{
position: 'relative', position: 'absolute',
width: `${width}px`, width: '100%',
height: `${height}px`, height: '100%',
top: 0,
left: 0,
pointerEvents: 'auto', pointerEvents: 'auto',
}} }}
> >
<div ref={containerRef} <div ref={containerRef}
style={{ style={{
position: 'relative', position: 'absolute',
width: '100%', width: '100%',
height: '100%', height: '100%',
pointerEvents: 'auto', pointerEvents: 'auto',
+25 -9
View File
@@ -1,7 +1,6 @@
// src/components/Detection.js
import React from 'react'; import React from 'react';
function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) { // Corrected prop name function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
if (!detection || !detection.class) { if (!detection || !detection.class) {
return null; return null;
} }
@@ -14,8 +13,13 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
} }
// Use startsWith to correctly handle RGBA and hex colors // Use startsWith to correctly handle RGBA and hex colors
const backgroundColor = color.startsWith('rgba') ? color : color.replace('1', '0.4'); // Ensure opacity for background const backgroundColor = color.startsWith('rgba')
const borderColor = color.startsWith('rgba') ? color.replace('0.4', '1') : color; // Ensure full opacity for border ? color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 0.4)')
: color.replace(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/, 'rgba($1, $2, $3, 0.4)');
const borderColor = color.startsWith('rgba')
? color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 1)')
: color;
const resizeHandleSize = 8; const resizeHandleSize = 8;
@@ -36,21 +40,22 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
top: `${detection.y1}px`, top: `${detection.y1}px`,
width: `${detection.x2 - detection.x1}px`, width: `${detection.x2 - detection.x1}px`,
height: `${detection.y2 - detection.y1}px`, height: `${detection.y2 - detection.y1}px`,
backgroundColor: backgroundColor, // Use the calculated backgroundColor backgroundColor: backgroundColor,
border: `2px solid ${borderColor}`, // Use the calculated borderColor border: `2px solid ${borderColor}`,
boxSizing: 'border-box', boxSizing: 'border-box',
cursor: isSelected ? 'move' : 'default', cursor: isSelected ? 'move' : 'default',
pointerEvents: 'auto', pointerEvents: 'auto',
zIndex: isSelected ? 2 : 1,
}; };
if (isSelected) { if (isSelected) {
style.border = `3px solid black`; style.border = `3px solid black`;
style.boxShadow = `0 0 4px 2px ${borderColor}`; // Use calculated border color style.boxShadow = `0 0 4px 2px ${borderColor}`;
} }
const handleMouseDown = (e) => { const handleMouseDown = (e) => {
e.stopPropagation(); e.stopPropagation();
onDetectionMouseDown(e); // Corrected prop name onDetectionMouseDown(e);
}; };
const handleResizeMouseDown = (e, position) => { const handleResizeMouseDown = (e, position) => {
@@ -73,11 +78,22 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
backgroundColor: 'black', backgroundColor: 'black',
cursor: handle.cursor, cursor: handle.cursor,
pointerEvents: 'auto', pointerEvents: 'auto',
zIndex: 3,
}} }}
onMouseDown={(e) => handleResizeMouseDown(e, handle.position)} onMouseDown={(e) => handleResizeMouseDown(e, handle.position)}
/> />
))} ))}
<span style={{ color: 'white', fontSize: '12px', position: "absolute", top: "-18px", left: "0px" }}>{detection.class.Name}</span> <span style={{
color: 'white',
fontSize: '12px',
position: "absolute",
top: "-18px",
left: "0px",
textShadow: '1px 1px 2px black',
pointerEvents: 'none'
}}>
{detection.class.Name}
</span>
</div> </div>
); );
} }
+50 -16
View File
@@ -1,4 +1,3 @@
// --- START OF FILE DetectionClassList.js --- (No changes needed)
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import DetectionClass from '../models/DetectionClass'; import DetectionClass from '../models/DetectionClass';
@@ -6,7 +5,7 @@ function DetectionClassList({ onClassSelect }) {
const [detectionClasses, setDetectionClasses] = useState([]); const [detectionClasses, setDetectionClasses] = useState([]);
const [selectedClass, setSelectedClass] = useState(null); const [selectedClass, setSelectedClass] = useState(null);
const colors = [ // Define colors *inside* the component const colors = [
"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#000000", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#000000",
"#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#808080", "#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#808080",
"#C00000", "#00C000", "#0000C0", "#C0C000", "#C000C0", "#00C0C0", "#C0C0C0", "#C00000", "#00C000", "#0000C0", "#C0C000", "#C000C0", "#00C0C0", "#C0C0C0",
@@ -17,7 +16,6 @@ function DetectionClassList({ onClassSelect }) {
"#E00000", "#00E000", "#0000E0", "#E0E000", "#E000E0", "#00E0E0", "#E0E0E0" "#E00000", "#00E000", "#0000E0", "#E0E000", "#E000E0", "#00E0E0", "#E0E0E0"
]; ];
// Calculate color with opacity
const calculateColor = (id, opacity = '0.2') => { const calculateColor = (id, opacity = '0.2') => {
const hexColor = colors[id % colors.length]; const hexColor = colors[id % colors.length];
const r = parseInt(hexColor.slice(1, 3), 16); const r = parseInt(hexColor.slice(1, 3), 16);
@@ -27,24 +25,60 @@ function DetectionClassList({ onClassSelect }) {
}; };
useEffect(() => { useEffect(() => {
fetch('config.json') // Make sure this path is correct 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" }
];
try {
fetch('config.json')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const detectionClasses = data.classes.map(cls => { const classes = data.classes.map(cls => {
const color = calculateColor(cls.Id, '1'); // Full opacity for border const color = calculateColor(cls.Id, '1');
return new DetectionClass(cls.Id, cls.Name, color); return new DetectionClass(cls.Id, cls.Name, color);
}); });
setDetectionClasses(detectionClasses); setDetectionClasses(classes);
if (detectionClasses.length > 0 && onClassSelect) { if (classes.length > 0 && onClassSelect && !selectedClass) {
onClassSelect(detectionClasses[0]); // Select the first class by default setSelectedClass(classes[0]);
onClassSelect(classes[0]);
} }
}) })
.catch(error => console.error("Error loading detection classes:", error)); .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) => { const handleClassClick = (cls) => {
setSelectedClass(cls); // Update the selected ID setSelectedClass(cls);
onClassSelect && onClassSelect(cls); onClassSelect && onClassSelect(cls);
}; };
@@ -53,9 +87,9 @@ function DetectionClassList({ onClassSelect }) {
<h3 style={{ marginTop: '15px', fontSize: '14px' }}>Classes</h3> <h3 style={{ marginTop: '15px', fontSize: '14px' }}>Classes</h3>
<ul style={{ listStyleType: 'none', padding: 0, margin: 0 }}> <ul style={{ listStyleType: 'none', padding: 0, margin: 0 }}>
{detectionClasses.map((cls) => { {detectionClasses.map((cls) => {
const backgroundColor = calculateColor(cls.Id); // Calculate background color (0.2 opacity) const backgroundColor = calculateColor(cls.Id);
const darkBg = calculateColor(cls.Id, '0.8'); // Calculate selected background color (0.4 opacity) const darkBg = calculateColor(cls.Id, '0.8');
const isSelected = selectedClass.Id === cls.Id; const isSelected = selectedClass && selectedClass.Id === cls.Id;
return ( return (
<li <li
@@ -63,8 +97,8 @@ function DetectionClassList({ onClassSelect }) {
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
padding: '8px', padding: '8px',
border: `1px solid ${isSelected ? '#000' : '#eee'}`, // Use cls.Color for the selected border border: `1px solid ${isSelected ? '#000' : '#eee'}`,
backgroundColor: isSelected ? darkBg : backgroundColor, // Conditional background backgroundColor: isSelected ? darkBg : backgroundColor,
fontSize: '14px', fontSize: '14px',
marginBottom: '2px', marginBottom: '2px',
display: 'flex', display: 'flex',
+173 -29
View File
@@ -1,45 +1,189 @@
import React, {useEffect } from 'react'; import React, { useEffect, useRef, useState } from 'react';
function VideoPlayer({ children, videoFile, currentTime, videoRef, isPlaying }) { function VideoPlayer({ children, videoFile, currentTime, videoRef, isPlaying, onSizeChanged, onSetCurrentTime }) {
const containerRef = useRef(null);
const [playbackError, setPlaybackError] = useState(null);
const objectUrlRef = useRef(null);
// Flag to track if time update is coming from natural playback
const isPlaybackUpdateRef = useRef(false);
// Set up the video file when it changes
useEffect(() => { useEffect(() => {
console.log("useEffect Videoplayer"); if (!videoFile || !videoRef.current) return;
if (!(videoFile && videoRef.current))
return;
console.log("Setting video source:", videoFile); try {
videoRef.current.src = URL.createObjectURL(videoFile); // Clean up previous object URL
videoRef.current.onloadedmetadata = () => { 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]); }, [videoFile]);
// useEffect(() => { // Handle metadata loading and size changes
// if(videoRef.current){ useEffect(() => {
// videoRef.current.currentTime = currentTime; if (!videoRef.current) return;
// }
// }, [currentTime, videoRef]) const handleMetadata = () => {
// if (!videoRef.current) return;
// useEffect(() => {
// if (videoRef.current) { const width = videoRef.current.videoWidth || 640;
// if(isPlaying){ const height = videoRef.current.videoHeight || 480;
// videoRef.current.play()
// } if (onSizeChanged) {
// else{ onSizeChanged(width, height);
// videoRef.current.pause() }
// } };
// }
// }, [isPlaying, videoRef]) 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 ( return (
<div style={{ position: 'relative'}}> <div
{/* Video Element */} ref={containerRef}
<div style={{ width: '100%', height: '100%'}}> style={{
{children} position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#000',
overflow: 'hidden'
}}
>
<video <video
ref={videoRef} ref={videoRef}
style={{ width: '100%', height: '100%', pointerEvents: isPlaying ? 'none' : 'auto' }} style={{
width: '100%',
height: 'auto',
maxHeight: '100%',
display: 'block',
objectFit: 'contain',
pointerEvents: 'none'
}}
preload="auto"
playsInline
muted
/> />
{playbackError && (
<div style={{
position: 'absolute',
top: '10px',
left: '10px',
background: 'rgba(255,0,0,0.7)',
color: 'white',
padding: '5px',
borderRadius: '3px',
fontSize: '12px',
zIndex: 10
}}>
{playbackError}
</div>
)}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'auto'
}}
>
{children}
</div> </div>
</div> </div>
); );
+44 -16
View File
@@ -13,48 +13,76 @@ export const isMouseOverDetection = (x, y, detection, containerRef) => {
return relativeX >= detection.x1 && relativeX <= detection.x2 && relativeY >= detection.y1 && relativeY <= detection.y2; 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) => { 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'); const canvas = document.createElement('canvas');
if (!containerRef.current) return null;
const container = containerRef.current; const container = containerRef.current;
canvas.width = container.offsetWidth; canvas.width = container.offsetWidth || 640;
canvas.height = container.offsetHeight; canvas.height = container.offsetHeight || 480;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return null;
try {
ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height); ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Error drawing video to canvas:", e);
return null;
}
if (detections && detections.length > 0) {
detections.forEach(detection => { detections.forEach(detection => {
if (!detection?.class) return;
ctx.fillRect(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)';
// 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.lineWidth = 2;
ctx.strokeRect(detection.x1, detection.y1, detection.x2 - detection.x1, detection.y2 - detection.y1); ctx.strokeRect(x, y, width, height);
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.font = '12px Arial'; ctx.font = '12px Arial';
ctx.fillText(detection.class.Name, detection.x1, detection.y1 - 5); ctx.fillText(detection.class.Name || 'Unknown', x, y - 5);
}); });
}
return canvas.toDataURL('image/png'); 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 newX1 = mouseX - dragOffset.x;
let newY1 = mouseY - dragOffset.y; let newY1 = mouseY - dragOffset.y;
let newX2 = newX1 + (detection.x2 - detection.x1); let newX2 = newX1 + (detection.x2 - detection.x1);
let newY2 = newY1 + (detection.y2 - detection.y1); let newY2 = newY1 + (detection.y2 - detection.y1);
if (!containerRef.current) { if (!containerRef.current) {
return { newX1, newY1, newX2, newY2 }; // Return early with unchanged values return { newX1, newY1, newX2, newY2 };
} }
let containerWidth = containerRef.current.offsetWidth; let containerWidth = containerRef.current.offsetWidth;
let containerHeight = containerRef.current.offsetHeight; let containerHeight = containerRef.current.offsetHeight;
if (newX1 < 0) { if (newX1 < 0) {
newX1 = 0; newX1 = 0;
newX2 = detection.x2 - detection.x1; newX2 = detection.x2 - detection.x1;
@@ -72,13 +100,13 @@ export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, con
newY1 = newY2 - (detection.y2 - detection.y1); newY1 = newY2 - (detection.y2 - detection.y1);
} }
return { newX1, newY1, newX2, newY2 }; return { newX1, newY1, newX2, newY2 };
}; };
export const calculateResizedPosition = (mouseX, mouseY, position, detection, containerRef) => { export const calculateResizedPosition = (mouseX, mouseY, position, detection, containerRef) => {
let { x1, y1, x2, y2 } = detection; let { x1, y1, x2, y2 } = detection;
const containerWidth = containerRef.current.offsetWidth; const containerWidth = containerRef.current?.offsetWidth || 640;
const containerHeight = containerRef.current.offsetHeight; const containerHeight = containerRef.current?.offsetHeight || 480;
switch (position) { switch (position) {
case 'top-left': case 'top-left':
x1 = Math.min(mouseX, detection.x2 - 5); x1 = Math.min(mouseX, detection.x2 - 5);
@@ -111,11 +139,11 @@ export const calculateResizedPosition = (mouseX, mouseY, position, detection, co
default: default:
break; break;
} }
// Boundary checks
x1 = Math.max(0, x1); x1 = Math.max(0, x1);
y1 = Math.max(0, y1); y1 = Math.max(0, y1);
x2 = Math.min(containerWidth, x2); x2 = Math.min(containerWidth, x2);
y2 = Math.min(containerHeight, y2); y2 = Math.min(containerHeight, y2);
return { x1, y1, x2, y2 }; return { ...detection, x1, y1, x2, y2 };
}; };
+58
View File
@@ -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;